ลิงก์ติดตามการคลิก
ภาพรวม
endpoint ที่แปลง tracking token เป็น URL ปลายทางพร้อมกับบันทึกสถิติการคลิกไปในคราวเดียว ใช้กับปุ่ม บน rich menu เป็นหลัก กล่าวคือผู้ใช้กดปุ่ม, LIFF เปิดขึ้น, เว็บเรียก endpoint นี้เพื่อขอ URL แล้วพา ผู้ใช้ไปต่อ ในขณะที่ระบบบันทึกว่าใครกดปุ่มไหน
หลักการสำคัญคือ endpoint นี้ต้องคืน destinationUrl เสมอ การบันทึกคลิกเป็นงานแบบ fire-through
ที่กลืน error ทั้งหมด ไม่มีสิ่งใดทำให้ redirect พังได้ ยกเว้นกรณีเดียวคือหา token ไม่พบ ซึ่งตอบ 404
Business Flow
GET /api/tracking/redirect/:token (header x-liff-token เป็น optional)
token อาจถูก resolve ได้จาก 2 เส้นทาง
เส้นทาง built-in (ไม่แตะฐานข้อมูลเลย)
- ถอดรหัส token ด้วย keyring AES-256-GCM ซึ่งมีโครงสร้าง
keyId(1) ‖ nonce(12) ‖ ciphertext ‖ tag(16)encode เป็น base64url แบบไม่มี padding โดย key มาจาก configCAMPAIGN_LINK_KEYS - AAD ของเส้นทางนี้คือ
"tracking-link-v1"ซึ่งต่างจากของ campaign redirect โดยเจตนา token ของ campaign จึงถอดออกมาเป็น tracking token ไม่ได้ แม้จะใช้ key ชุดเดียวกัน - เมื่อถอดสำเร็จ หาก token ยังไม่หมดอายุ (
p.E) และctมีค่าเป็นrich_menuระบบจะเขียนคลิก แบบ best-effort (ค่าlead_genจะไม่ถูกนับคลิกตาม parity ของระบบเดิม) แล้วคืนp.Dเป็น destination - หาก keyring ว่างหรือถอดรหัสไม่ได้ ระบบจะ fail closed แบบเงียบแล้วไปใช้เส้นทาง legacy
เส้นทาง legacy (อ่านจากตาราง)
SELECT ... FROM tracking_token WHERE token = $1 LIMIT 1หากไม่มีแถวตอบ 404"Tracking token not found"- บันทึกคลิกเฉพาะเมื่อ token ยังไม่หมดอายุ และ
statusเป็นactiveและcontent_typeเป็นrich_menuนอกเหนือจากนั้นระบบยังคงคืน destination ตามปกติ - คำนวณ field ของคลิกจากแถวข้อมูลและ jsonb
metadataตามลำดับ fallback ของระบบเดิมอย่างเคร่งครัด ได้แก่richMenuId(มาจาก metadata เท่านั้น),richMenuActionIndex(จากrow.action_indexก่อน แล้วจึงmetadata.richMenuActionIndex),richMenuArchiveId(จากmetadata.richMenuArchiveId) และtrackingLabel(จากrow.tracking_labelแล้วจึงmetadata.trackingLabelแล้วจึงสตริงว่าง)
การเขียนคลิก (writeClick) — ทำงานเหมือนกันทั้งสองเส้นทาง
- ระบุตัวผู้ใช้ ผ่าน
liff.VerifyAndGetLineUserซึ่ง auto-provision guest จึงทำให้ FK ถูกต้องเสมอ กรณีที่ไม่มี token, verifier ไม่ได้ตั้งค่า, verify ล้มเหลว หรือไม่ได้ user ระบบจะบันทึก เป็น anonymous โดยline_uidเป็น NULL พร้อม log ที่แยกแยะสาเหตุไว้ (ระหว่าง "ไม่มี token ส่งมา" กับ "token มาแต่ verify ไม่ผ่าน") เพื่อให้ diagnose แถว anonymous ได้ภายหลัง - insert ลงตาราง
tracking_logด้วยservice='redirect',action_type='click',type='uri'และcontent_type='rich_menu'โดยคอลัมน์titleทำหน้าที่เก็บ trackingLabel ไปด้วยเพราะเป็น NOT NULL- คอลัมน์
raw_dataเป็น jsonb ที่ต้องส่งค่าเป็น string ไม่ใช่[]byteเนื่องจาก pool ใช้ pgx simple query protocol ซึ่ง text-encode[]byteเป็น bytea แล้วคอลัมน์ jsonb จะปฏิเสธ จุดนี้เคยเป็นบั๊กที่ทำให้tracking_logไม่ถูกเขียนแบบเงียบๆ ในทั้งสองเส้นทาง
- คอลัมน์
- upsert ตาราง
tracking_line_usersเพื่อ dedup ต่อคู่ผู้ใช้และจุดคลิก เฉพาะเมื่อรู้ตัวผู้ใช้และ มีrichMenuIdโดยtracking_keyมีรูปแบบ{richMenuId}:{archiveId}:{actionIndex}(archiveId ที่ไม่มีค่าใช้ 0) รูปแบบนี้ต้องตรงกับที่ worker ใช้ และ upsert ใช้เงื่อนไขON CONFLICT (tracking_key, line_user_id) DO NOTHING - ตั้ง flag
RICH_MENU_STAT_DIRTY:{id}ใน Redis ด้วย TTL 7200 วินาที เพื่อให้ cron คำนวณสถิติใหม่ โดย guard ของระบบเดิมเป็น JS truthy check ดังนั้นrichMenuIdที่เป็น 0 จะไม่ตั้ง flag ซึ่งต่าง จาก upsert ในข้อก่อนหน้าที่ใช้เงื่อนไข not-null
ไฟล์และฟังก์ชันหลัก
| รายการ | ค่า |
|---|---|
| Route | GET /api/tracking/redirect/:token |
| Register | internal/trackingredirect/register.go → Register(r, deps) |
| Handler | internal/trackingredirect/handler.go → (*Handler).Resolve |
| Service | internal/trackingredirect/service.go → New(...), ResolveAndTrack, recordClick, writeClick, fieldsFromBuiltin, fieldsFromRow และ type Destination, clickFields |
| Token ring | internal/trackingtoken/token.go → NewRing(env), (Ring).Decrypt, Payload และ aad = "tracking-link-v1" |
| Helper | internal/trackingredirect/util.go → parseMetadata, jsonNumberToInt64, jsInt64, jsNullInt64, rawMeta, nowFn |
| Cache | internal/cache → Set ซึ่งถูกใช้เป็น DirtyMarker |
หมายเหตุ: package นี้ใช้ raw SQL โดยไม่มี entity หรือ repository แยกออกมาโดยเจตนา เพื่อ mirror
dataSource.query ของระบบเดิมแบบตัวต่อตัว ทั้ง placeholder $N, ชุดคอลัมน์ และรูปแบบ tracking_key
จุดเชื่อมต่อกับ Service อื่น
- ตารางฐานข้อมูล
tracking_token,tracking_log,tracking_line_users - Redis ผ่าน
internal/cacheสำหรับ flagRICH_MENU_STAT_DIRTY:* - Config
CAMPAIGN_LINK_KEYSซึ่งใช้ร่วมกับ campaign-redirect แต่แยก namespace ออกจากกันด้วย AAD - liff-authentication ผ่าน
VerifyAndGetLineUserซึ่ง auto-provision guest - token ถูกสร้างโดย cms-api-go (
internal/core/trackinglink) ส่วนสถิติถูกอ่านโดย CMS และ cron ฝั่ง worker ซึ่งต้องใช้รูปแบบtracking_keyเดียวกัน - ตรงกับ client-web feature:
tracking-redirect(เส้นทาง/{hash}/r/{id}และการหลบการเปิด LIFF ซ้อนกัน)