ประตูรับ Webhook จาก LINE
ภาพรวม
POST /api/line/:id คือ endpoint เดียวที่ LINE Platform ยิงเข้ามาโดยตรงเมื่อมี event เกิดขึ้นกับ LINE OA
ไม่ว่าจะเป็นข้อความ การกดปุ่ม การเพิ่มเพื่อน การบล็อก การเข้า/ออกกลุ่ม หรือ beacon
พารามิเตอร์ :id คือ webhook id ที่ผูกกับ LINE OA หนึ่งตัว (คอลัมน์ line_oa.webhook_id)
สิ่งที่ต้องเข้าใจก่อนอ่านรายละเอียด: service นี้ ไม่ได้แยก handler ตาม event type แต่ทำหน้าที่เป็น router หรือตัวคัดกรองบาง ๆ ที่ถามคำถามเพียง 3 ข้อ แล้วส่งงานต่อ
- OA ตัวนี้ต้อง forward webhook ดิบไปยัง endpoint ของลูกค้าหรือไม่
- event นี้เข้าเงื่อนไข mbox (การส่งต่อให้เจ้าหน้าที่) หรือ postback พิเศษหรือไม่
- ถ้าไม่เข้าเงื่อนไขใดเลย จะ publish ทั้งก้อนลงคิว
line_webhookให้ worker-go ไปแยกแยะต่อ
ด้วยเหตุนี้ event ประเภท follow, unfollow, join, leave, beacon และ videoPlayComplete
จึงไม่มีโค้ดจัดการอยู่ในโปรเจกต์นี้เลย ทั้งหมดตกลงสู่เส้นทาง default ไปยังคิว line_webhook
ส่วน logic จริงเรื่อง auto-response, friend-track และตาราง line_user_friend อยู่ที่ฝั่ง worker-go ทั้งหมด
endpoint นี้ทำงานแบบ fire-and-forget โดยตอบ 200 พร้อม body
{"code":"RES_SUCCESS_001","message":"OK"} กลับให้ LINE ทันที แล้วจึงประมวลผลใน goroutine เบื้องหลัง
เนื่องจาก LINE มี timeout สั้นและจะ retry หากตอบช้า จึงจำเป็นต้องตอบกลับก่อนเสมอ
Business Flow
ขั้นรับ request (handler)
- อ่าน
:idเป็นwebhookId - เก็บ header ทั้งหมดแล้วแปลง key เป็นตัวพิมพ์เล็ก เนื่องจาก LINE และ Express ส่งมาเป็นตัวพิมพ์เล็ก
แต่ Go จะแปลงเป็นรูปแบบ Canonical จึงต้องแปลงกลับ มิฉะนั้นการ lookup
x-line-signatureจะพลาด - อ่าน body พร้อมเก็บ bytes ดิบไว้ใน
BodyRawนอกเหนือจาก map ที่ parse แล้ว (ดูรายละเอียดที่ การส่งต่อ Raw Body และลายเซ็น) - spawn goroutine เรียก
Service.ProcessLineบนcontext.Background()โดยมีrecover()ป้องกัน panic ทำให้ process ตาย แล้วตอบ200กลับทันที
ขั้นประมวลผล (ProcessLine)
- อ่าน config ด้วย
HGETALL webhook_config:{webhookId}จาก Redis (ดู Redis Cache ของ Webhook Config)- cache miss (hash ว่าง) จะ publish payload ทั้งก้อนลงคิว
line_webhookแล้วจบ โดย worker-go จะไปค้น config จากฐานข้อมูลเอง - อ่าน error จะ log แล้วหยุดทันที ไม่ publish ทำให้ข้อความหายไป ซึ่งเป็นพฤติกรรมเดียวกับระบบต้นฉบับ
- cache miss (hash ว่าง) จะ publish payload ทั้งก้อนลงคิว
- ถ้า config มี
forwardWebhookUrlจะยิงต่อแบบไม่รอผล (ดู ส่งต่อ Webhook ไปยังระบบลูกค้า) - คัด event ที่
type == "message"ออกมา ถ้าmboxEnabled == "1"และมี message event จะเข้าเส้นทาง mbox (ดู ส่งต่อให้เจ้าหน้าที่) เส้นทางนี้หากเข้าเงื่อนไขจะ return ทันทีโดยไม่ publish ลงline_webhookซึ่งมีผลเป็นการปิดบอทชั่วคราว - คัด event ที่
type == "postback"เมื่อmboxEnabled == "1"postback.dataขึ้นต้นด้วยmbox_team:จะ handoff เข้าทีมที่ผู้ใช้เลือกpostback.dataขึ้นต้นด้วยappt_cancel:จะเข้าสู่การยกเลิกนัดหมาย (ดู ยกเลิกนัดหมายผ่าน Postback)
- เส้นทาง default จะ publish payload ที่ประกอบด้วย
webhookId,headersและbodyลงคิวline_webhookในรูปแบบ bare JSON ให้ worker-go ประมวลผลบอทต่อ
:::note ข้อสังเกต
ขั้นตอนที่ 7 และ 8 ตรวจสอบเฉพาะ event ตัวแรก ของแต่ละประเภทเท่านั้น
(messageEvents[0] และ postbackEvents[0]) แม้ LINE จะส่งมาหลาย event ต่อหนึ่ง request ได้
ทั้งนี้เพื่อรักษา parity กับระบบ NestJS เดิม และหาก mbox ตัดสินใจ handoff แล้ว
event อื่นใน batch เดียวกันจะถูกทิ้งทั้งหมด
:::
ไฟล์และฟังก์ชันหลัก
| Method | Route | Auth | Handler |
|---|---|---|---|
| POST | /api/line/:id | ไม่มี | Handler.processLine |
| POST | /api/Line/:id | ไม่มี | Handler.processLine (ตัวเดียวกัน) |
ระบบลงทะเบียนทั้ง 2 รูปแบบ เนื่องจาก NestJS และ Express จับคู่ path แบบไม่สนใจตัวพิมพ์
(ต้นฉบับประกาศเป็น @Controller('Line')) ในขณะที่ gin เป็น case-sensitive
และ traffic จริงจาก LINE ยิงเข้ามาเป็นตัวพิมพ์เล็ก
โค้ดทั้งหมดอยู่ในแพ็กเกจ internal/line/
| ไฟล์ | ฟังก์ชันสำคัญ |
|---|---|
internal/line/handler.go | Register, Handler.processLine, toLowerHeader, decodeJSONObject |
internal/line/service.go | Service.ProcessLine (dispatcher หลัก), eventList, eventType, sourceUserID, messageText, postbackData, Service.publish, parseLineOaID, splitSecond |
Type สำคัญคือ LineWebhookPayload ซึ่งประกอบด้วย webhookId, headers และ body
โดยมี MarshalJSON แบบ custom ที่ปล่อย bytes ดิบออกไปเป็นฟิลด์ body
จุดเชื่อมต่อกับ Service อื่น
- Redis — อ่าน
webhook_config:{webhookId}แบบอ่านอย่างเดียว และอ่าน/เขียนagent_mode:{lineOaId}:{userId}ผ่านinternal/cache.Cacheที่เติม prefixLINE_MANAGEMENT:ให้อัตโนมัติ - RabbitMQ — exchange
line_exchangeพร้อมคิวline_webhook,mbox_handoffและbooking_notificationโดยใช้ชื่อคิวเป็น routing key และส่งเป็น bare JSON - ไม่มีการเชื่อมต่อฐานข้อมูล — โมดูล
lineไม่แตะ PostgreSQL เลย ทุกอย่างมาจาก Redis cache - worker-go — เป็นผู้ consume คิว
line_webhookแล้วไปทำ auto-response, friend-track และบันทึกข้อมูลลงตารางline_userกับline_user_friend - cms-api — โมดูล line-oa-management เป็นผู้เขียน
webhook_config:*ลง Redis เมื่อมีการแก้ไข config ของ OA