Skip to main content

แคมเปญติดตามการเพิ่มเพื่อน

ภาพรวม

ระบบวัดผลว่าเพื่อนใหม่ของ OA มาจากช่องทางใด แอดมินสร้างแคมเปญพร้อม token แล้วนำลิงก์ไปติดตาม ช่องทางต่างๆ เช่น โฆษณา, QR ที่หน้าร้าน หรือบทความ เมื่อผู้ใช้กดเข้ามา หน้า LIFF จะบันทึก "visit" ก่อนพาไปเพิ่มเพื่อน และเมื่อ LINE ยิง webhook follow กลับมา ระบบก็สามารถผูก follow นั้นเข้ากับ visit เดิมได้

ฟีเจอร์นี้มี 3 endpoint ได้แก่ การหาแคมเปญที่ active จาก OA hash, การดึงรายละเอียดแคมเปญจาก token และการบันทึก visit

Business Flow

หาแคมเปญจาก OA hash — GET /api/friend-track/by-hash/:hash

  1. ค้น line_oa จาก hash โดยไม่มีเงื่อนไข status หรือ soft-delete (เพื่อรักษา parity กับระบบเดิม) หากไม่พบตอบ 404 "LINE OA not found"
  2. หาแคมเปญที่ active และไม่ถูกลบของ OA นั้น โดยเลือกตัวล่าสุด หากไม่พบตอบ 404 "No active campaign found for this LINE OA"
  3. คืน {token, name, description, botBasicId} โดย description เป็น pointer เพื่อให้คอลัมน์ที่เป็น NULL ออกมาเป็น JSON null แทนที่จะเป็น key ที่หายไป

ดึงรายละเอียดแคมเปญจาก token — GET /api/friend-track/:token

  1. ค้นแคมเปญที่ active ด้วย token พร้อม relation line_oa หากไม่พบตอบ 404 "Campaign not found"
  2. หากไม่มี relation ของ OA ตอบ 404 "LINE OA not found for this campaign"
  3. คืน {name, description, botBasicId, lineLiffId, lineOaHash} โดย lineLiffId มาจาก line_login_info.lineLiffId และ fallback เป็นสตริงว่าง

บันทึก visit — POST /api/friend-track/:token/visit (rate limit 10/60s)

body {displayName?, pictureUrl?, refId?} ทุก field เป็น optional และ bind error ถูก ignore เพราะ ตัวตนของผู้ใช้มาจาก header ไม่ใช่จาก body

  1. resolve แคมเปญและ OA โดยใช้เงื่อนไข 404 ชุดเดียวกับด้านบน
  2. verify token — หากมี header x-liff-token จะต้องมี channel ของ OA ด้วย (ไม่มีตอบ 401 "LINE Login not configured for this channel") แล้วจึงเรียก VerifyIDToken หากไม่มี header ดังกล่าวจะใช้ x-liff-access-token ผ่าน VerifyAccessToken และหากไม่มีทั้งคู่ตอบ 401 "No authentication token provided" โดยเส้นทางนี้เรียกเมธอด verify โดยตรงไม่ผ่าน liff.Service เพราะตัวหลังจะ auto-provision guest ซึ่งไม่ใช่พฤติกรรมที่ต้องการ ณ จุดนี้
  3. กฎ dedup ที่ยอมให้นับใหม่ได้ ซึ่งเป็นจุดที่ละเอียดที่สุดของฟีเจอร์
    • หา event liff_visit ครั้งล่าสุดของคู่ (campaign, user)
    • หากพบ ให้หา event unfollow ครั้งล่าสุดของคู่ (user, OA) แล้วเทียบเวลา
      • ไม่มี unfollow หลัง visit ถือว่าเป็น funnel เดิม จึงคืน {success:true, isFriend, alreadyTracked:true} โดยไม่เขียนอะไรเพิ่ม
      • มี unfollow หลัง visit ถือว่าเป็น funnel ใหม่ จึงทำงานต่อ เพราะคนที่เลิกติดตามแล้วกลับมาอีกครั้ง ควรถูกนับใหม่
  4. insert แถว friend_track_event ชนิด liff_visit โดย displayName และ pictureUrl ใช้ค่าจาก body ก่อน แล้ว fallback เป็นค่าจาก token ส่วน refId ที่ว่างจะถูกเก็บเป็น null
  5. publish trigger {campaignId, eventType, lineUserId, lineOaId, organizationId} เข้าคิว friend_track_event_trigger แบบ best-effort โดย log แล้วกลืน error
  6. merge custom attribute เมื่อแคมเปญมี attribute_config ที่เป็น object และมีอย่างน้อย 1 key หรือมี ref_attribute_key พร้อมกับได้รับ refId
    • ค่าที่ merge คือ custom attribute เดิมรวมกับ attribute config และหากมี refAttributeKey ระบบจะตั้งค่าคีย์นั้นเป็น refId
    • การ update เกิดขึ้นเฉพาะเมื่อมีแถว line_user อยู่แล้ว (guard ด้วย findOne) จะไม่สร้างแถวใหม่
  7. ตรวจว่าเป็นเพื่อนอยู่แล้วหรือไม่ด้วย IsFriend
  8. หากเป็นเพื่อนอยู่แล้ว ระบบจะบันทึก event follow ให้ด้วยหากยังไม่มี เนื่องจาก LINE จะไม่ยิง webhook FOLLOW สำหรับผู้ที่เป็นเพื่อนอยู่ก่อนแล้ว ถ้าไม่ทำขั้นตอนนี้ funnel จะค้างอยู่ที่ visit ตลอด
  9. คืน {success:true, isFriend, alreadyTracked:false}

ไฟล์และฟังก์ชันหลัก

RouteRate limitHandler
GET /api/friend-track/by-hash/:hashinternal/friendtrack/handler.go(*Handler).GetByHash
GET /api/friend-track/:token(*Handler).GetCampaign
POST /api/friend-track/:token/visit10/60s(*Handler).RecordVisit
  • internal/friendtrack/register.goRegister(r, deps) ซึ่งผูก RouteRateLimit(rdb, 10, 60) เฉพาะ route visit
  • internal/friendtrack/service.goGetCampaignByLineOaHash, GetCampaignByToken, RecordVisit, mergeAttributes, publishTrigger, coalesce, nilIfEmpty, hasNonEmptyObject
  • internal/friendtrack/repository.goFindLineOaByHash, FindActiveCampaignByOaLatest, FindActiveCampaignByToken, FindLatestEvent, FindEventByType, InsertEvent, IsFriend, FindLineUserCustomAttribute, UpdateLineUserCustomAttribute
  • internal/friendtrack/entity.goCampaign, EventInput, EventTypeLiffVisit, EventTypeFollow, EventTypeUnfollow
  • Response types — ByHashResponse, ByTokenResponse, VisitResponse

จุดเชื่อมต่อกับ Service อื่น

  • ตารางฐานข้อมูล friend_track_campaign (token, jsonb attribute_config, ref_attribute_key), friend_track_event (event_type เป็น liff_visit, follow หรือ unfollow), line_oa และ line_user (jsonb custom_attribute)
  • RabbitMQ queue friend_track_event_trigger ซึ่ง worker นำไปประเมิน trigger rule
  • event follow และ unfollow ตัวจริงมาจาก line-management-webhook-go ฟีเจอร์นี้เขียนเฉพาะ liff_visit และเขียน follow เสริมในกรณีที่ผู้ใช้เป็นเพื่อนอยู่ก่อนแล้ว
  • content-page-viewer ส่งบล็อก friendTrack (campaignToken และ buttonText) ให้เว็บใช้เรียก endpoint เหล่านี้
  • liff-authentication โดยฟีเจอร์นี้ใช้ verifier โดยตรง ไม่ผ่าน liff.Service
  • ตรงกับ client-web feature: friend-track