แคมเปญติดตามการเพิ่มเพื่อน
ภาพรวม
ระบบวัดผลว่าเพื่อนใหม่ของ 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
- ค้น
line_oaจาก hash โดยไม่มีเงื่อนไข status หรือ soft-delete (เพื่อรักษา parity กับระบบเดิม) หากไม่พบตอบ 404"LINE OA not found" - หาแคมเปญที่ active และไม่ถูกลบของ OA นั้น โดยเลือกตัวล่าสุด หากไม่พบตอบ 404
"No active campaign found for this LINE OA" - คืน
{token, name, description, botBasicId}โดยdescriptionเป็น pointer เพื่อให้คอลัมน์ที่เป็น NULL ออกมาเป็น JSONnullแทนที่จะเป็น key ที่หายไป
ดึงรายละเอียดแคมเปญจาก token — GET /api/friend-track/:token
- ค้นแคมเปญที่ active ด้วย token พร้อม relation
line_oaหากไม่พบตอบ 404"Campaign not found" - หากไม่มี relation ของ OA ตอบ 404
"LINE OA not found for this campaign" - คืน
{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
- resolve แคมเปญและ OA โดยใช้เงื่อนไข 404 ชุดเดียวกับด้านบน
- 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 ซึ่งไม่ใช่พฤติกรรมที่ต้องการ ณ จุดนี้ - กฎ dedup ที่ยอมให้นับใหม่ได้ ซึ่งเป็นจุดที่ละเอียดที่สุดของฟีเจอร์
- หา event
liff_visitครั้งล่าสุดของคู่ (campaign, user) - หากพบ ให้หา event
unfollowครั้งล่าสุดของคู่ (user, OA) แล้วเทียบเวลา- ไม่มี unfollow หลัง visit ถือว่าเป็น funnel เดิม จึงคืน
{success:true, isFriend, alreadyTracked:true}โดยไม่เขียนอะไรเพิ่ม - มี unfollow หลัง visit ถือว่าเป็น funnel ใหม่ จึงทำงานต่อ เพราะคนที่เลิกติดตามแล้วกลับมาอีกครั้ง ควรถูกนับใหม่
- ไม่มี unfollow หลัง visit ถือว่าเป็น funnel เดิม จึงคืน
- หา event
- insert แถว
friend_track_eventชนิดliff_visitโดยdisplayNameและpictureUrlใช้ค่าจาก body ก่อน แล้ว fallback เป็นค่าจาก token ส่วนrefIdที่ว่างจะถูกเก็บเป็น null - publish trigger
{campaignId, eventType, lineUserId, lineOaId, organizationId}เข้าคิวfriend_track_event_triggerแบบ best-effort โดย log แล้วกลืน error - merge custom attribute เมื่อแคมเปญมี
attribute_configที่เป็น object และมีอย่างน้อย 1 key หรือมีref_attribute_keyพร้อมกับได้รับrefId- ค่าที่ merge คือ custom attribute เดิมรวมกับ attribute config และหากมี refAttributeKey
ระบบจะตั้งค่าคีย์นั้นเป็น
refId - การ update เกิดขึ้นเฉพาะเมื่อมีแถว
line_userอยู่แล้ว (guard ด้วยfindOne) จะไม่สร้างแถวใหม่
- ค่าที่ merge คือ custom attribute เดิมรวมกับ attribute config และหากมี refAttributeKey
ระบบจะตั้งค่าคีย์นั้นเป็น
- ตรวจว่าเป็นเพื่อนอยู่แล้วหรือไม่ด้วย
IsFriend - หากเป็นเพื่อนอยู่แล้ว ระบบจะบันทึก event
followให้ด้วยหากยังไม่มี เนื่องจาก LINE จะไม่ยิง webhook FOLLOW สำหรับผู้ที่เป็นเพื่อนอยู่ก่อนแล้ว ถ้าไม่ทำขั้นตอนนี้ funnel จะค้างอยู่ที่ visit ตลอด - คืน
{success:true, isFriend, alreadyTracked:false}
ไฟล์และฟังก์ชันหลัก
| Route | Rate limit | Handler |
|---|---|---|
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/visit | 10/60s | (*Handler).RecordVisit |
internal/friendtrack/register.go—Register(r, deps)ซึ่งผูกRouteRateLimit(rdb, 10, 60)เฉพาะ route visitinternal/friendtrack/service.go—GetCampaignByLineOaHash,GetCampaignByToken,RecordVisit,mergeAttributes,publishTrigger,coalesce,nilIfEmpty,hasNonEmptyObjectinternal/friendtrack/repository.go—FindLineOaByHash,FindActiveCampaignByOaLatest,FindActiveCampaignByToken,FindLatestEvent,FindEventByType,InsertEvent,IsFriend,FindLineUserCustomAttribute,UpdateLineUserCustomAttributeinternal/friendtrack/entity.go—Campaign,EventInput,EventTypeLiffVisit,EventTypeFollow,EventTypeUnfollow- Response types —
ByHashResponse,ByTokenResponse,VisitResponse
จุดเชื่อมต่อกับ Service อื่น
- ตารางฐานข้อมูล
friend_track_campaign(token, jsonbattribute_config,ref_attribute_key),friend_track_event(event_typeเป็นliff_visit,followหรือunfollow),line_oaและline_user(jsonbcustom_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