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/:hash—internal/friendtrack/handler.go → (*Handler).GetByHash
GET /api/friend-track/:token—(*Handler).GetCampaign
POST /api/friend-track/:token/visit10/60s(*Handler).RecordVisit
  • internal/friendtrack/register.go — Register(r, deps) ซึ่งผูก RouteRateLimit(rdb, 10, 60) เฉพาะ route visit
  • internal/friendtrack/service.go — GetCampaignByLineOaHash, GetCampaignByToken, RecordVisit, mergeAttributes, publishTrigger, coalesce, nilIfEmpty, hasNonEmptyObject
  • internal/friendtrack/repository.go — FindLineOaByHash, FindActiveCampaignByOaLatest, FindActiveCampaignByToken, FindLatestEvent, FindEventByType, InsertEvent, IsFriend, FindLineUserCustomAttribute, UpdateLineUserCustomAttribute
  • internal/friendtrack/entity.go — Campaign, 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