Campaign
ภาพรวม
Campaign คือเมนูที่ทีมการตลาดใช้ส่งข้อความออกไปหาเพื่อนใน LINE OA ทั้งแบบส่งหาทุกคน (Broadcast) และส่งเฉพาะกลุ่มเป้าหมาย (Multicast) จุดสำคัญที่ต้องรู้ก่อนเริ่มคือ แคมเปญไม่ได้สร้างเนื้อหาข้อความเอง ต้องมี Rich message ที่เปิดใช้งานอยู่แล้วจากเมนู Rich message ก่อนเสมอ
บทบาทที่ใช้งาน — ทีมการตลาด/แอดมินที่ได้รับสิทธิ์โมดูล Campaign (โมดูลนี้มี ModuleGate บังคับจริงที่ backend — ดูหัวข้อ สิทธิ์การเข้าถึง)
เส้นทางผู้ใช้งาน
1. ดูรายการแคมเปญ
เปิดเมนู Campaign หน้ารายการเริ่มที่แท็บ ส่งแล้ว เสมอ เรียงจากใหม่ไปเก่าตามวันที่ส่ง สลับไปดูแท็บอื่นได้ — ตั้งเวลา (เรียงจากใกล้ที่สุดขึ้นก่อน), ยกเลิก และ ฉบับร่าง (ทั้งสองแท็บนี้เรียงตามวันที่แก้ไขล่าสุด) แต่ละแท็บมีคอลัมน์วันที่ที่ความหมายเปลี่ยนไปตามแท็บ (วันที่ส่ง / ยกเลิกเมื่อ / แก้ไขล่าสุดเมื่อ) กำกับด้วย "(เวลากรุงเทพฯ)" เสมอ
ตัวกรองมี 3 ช่อง — ค้นหาจากชื่อ, เลือกผู้รับ (ดึงจาก endpoint แยกที่คืนเฉพาะกลุ่มเป้าหมายที่ มีแคมเปญอยู่จริง เท่านั้น ไม่เสนอตัวเลือกที่ไม่มีข้อมูลรองรับ — ถ้ามีแคมเปญ Broadcast อยู่จริงจะมีตัวเลือก "เพื่อนทั้งหมดใน OA" ขึ้นก่อนเสมอ), และช่วงวันที่ ผลการกรองจะถูกจดจำไว้ใน sessionStorage ระหว่างที่อยู่ในหน้านี้
2. สร้างและส่งแคมเปญ
- กดปุ่มสร้างแคมเปญใหม่ เข้าสู่ฟอร์ม
- กรอกชื่อแคมเปญ — ต้องไม่ซ้ำกับแคมเปญอื่นในระบบเดียวกัน ไม่เกิน 100 ตัวอักษร (ข้อจำกัดความยาวนี้บังคับแค่ฝั่งหน้าเว็บเท่านั้น — API ฝั่ง backend ไม่มีการตรวจความยาวชื่อเลย ตรวจแค่ว่าไม่ใช่ค่าว่าง)
- เลือกผู้รับ — ส่งหาเพื่อนทั้งหมดบน OA (Broadcast) หรือเลือกกลุ่มเป้าหมายที่เตรียมไว้แล้วจากเมนู Segment audience (Multicast)
- เลือกเนื้อหา — เลือก Rich message ที่เปิดใช้งานอยู่จากรายการ ระบบจะโชว์ตัวอย่างข้อความในกรอบแชททันที พร้อม Quick Reply ที่ผูกไว้ (ถ้ามี)
- หากเลือกส่งแบบ Broadcast ตัวเลือก Rich message ที่มี merge tag (เช่น
{{name}}) จะยังปรากฏในรายการแต่ถูก disabled พร้อมคำอธิบายว่าใช้ไม่ได้กับ "เพื่อนทั้งหมดใน OA" — และถ้าสลับจาก Multicast มา Broadcast ทั้งที่เพิ่งเลือก Rich message แบบมี merge tag ไว้ ระบบจะล้างค่าที่เลือกให้อัตโนมัติพร้อมข้อความอธิบายเหตุผล ไม่ปล่อยให้ปุ่มบันทึกดูเหมือนใช้งานไม่ได้แบบไม่มีสาเหตุ - หากเลือกส่งแบบ Multicast และ Rich message ที่เลือกมี merge tag จะมีเช็กบ็อกซ์เพิ่มขึ้นมาให้ติ๊ก "ข้ามการส่งให้ผู้ใช้ที่มีข้อมูล merge tag ไม่ครบถ้วน"
- หากเลือกส่งแบบ Broadcast ตัวเลือก Rich message ที่มี merge tag (เช่น
- เลือกวิธีส่ง — ส่งทันที, ตั้งเวลา, หรือเก็บเป็นฉบับร่าง
- ถ้าเลือก ฉบับร่าง ระบบจะผ่อนคลายทุกช่องอื่นให้ไม่บังคับ ต้องมีแค่ชื่อแคมเปญเท่านั้นจึงจะบันทึกได้
- ถ้าเลือก ตั้งเวลา ระบบเติมวันเวลาให้อัตโนมัติเป็นปัจจุบัน + 35 นาที (เผื่อเวลา 5 นาทีให้กรอกฟอร์มทัน) แต่กฎจริงคือต้องห่างจากเวลาปัจจุบันอย่างน้อย 30 นาที ตรวจสอบสองชั้น (ขณะพิมพ์ + ตอนกดบันทึก) หากเลือกวันเป็นวันนี้ ช่องเลือกชั่วโมง/นาทีที่ผ่านมาแล้วจะถูกปิดไว้ให้
- กดบันทึก ระบบเปิดกล่องยืนยันที่ข้อความและปุ่มเปลี่ยนไปตามวิธีส่งที่เลือก (ส่งทันที / บันทึกแคมเปญ / บันทึกเป็นฉบับร่าง) เมื่อยืนยัน ระบบเรียก
POST /campaign(สร้าง/คัดลอก) หรือPUT /campaign/{id}(แก้ไข)
3. ดูผลลัพธ์แคมเปญที่ส่งแล้ว
- จากแท็บ "ส่งแล้ว" คลิกชื่อแคมเปญ (หรือคลิกตัวเลข "กิจกรรมทั้งหมด" ในตาราง) เพื่อเปิดหน้าดูผลลัพธ์ — ทุกช่องในฟอร์มถูกปิดการแก้ไข
- ดูการ์ดสรุป 4 ใบ — ส่งแล้ว, การเข้าถึง, คลิกที่ไม่ซ้ำ, คลิกทั้งหมด สามการ์ดหลัง (ยกเว้น "ส่งแล้ว") กดได้เพื่อเปิดแผงด้านข้างแสดงรายชื่อผู้ใช้ LINE จริงที่ทำกิจกรรมนั้น พร้อมรูปโปรไฟล์ ชื่อ จำนวนครั้ง เวลาที่ทำครั้งแรก/ครั้งล่าสุด และแบ่งหน้าได้
- เจาะลึกลงตารางแยกรายข้อความ (Message A, B, C…) เพื่อดูว่าข้อความ/ปุ่มใดถูกคลิกมากที่สุด — ข้อความชนิดรูปภาพ/วิดีโอแสดงคอลัมน์จำนวนการดู ส่วนชนิดรูปภาพกดได้/Flex แสดงคอลัมน์ตำแหน่ง ข้อมูล ประเภท คลิกไม่ซ้ำ คลิกทั้งหมด (คลิกตัวเลขเพื่อเปิดแผงรายชื่อผู้ใช้เจาะจงปุ่ม/ลิงก์นั้นได้)
- ข้อความชนิดตัวอักษรล้วนจะระบุตรงว่า "ไม่สามารถติดตามการคลิกได้"
4. จัดการแคมเปญที่มีอยู่
- ฉบับร่าง — แก้ไขได้ (ปุ่มดินสอ) หรือลบได้ (ต้องยืนยันก่อน)
- ตั้งเวลาไว้ — ยกเลิกได้ (ต้องยืนยันก่อน แคมเปญที่ยกเลิกจะย้ายไปแท็บ "ยกเลิก" ทันที)
- ทุกสถานะ — คัดลอกได้เสมอ (พาไปฟอร์มสร้างใหม่โดยดึงข้อมูลจากแคมเปญเดิมมาเป็นต้นแบบ)
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก internal/modules/campaign/{controller,service,dto,tracking}.go ของ line-management-cms-api-go:
- การส่งทันทีเป็นแบบ "จองสิทธิ์ก่อนส่ง" (claim-based) — เมื่อกด "ส่งทันที" backend ไม่ได้เซ็ตสถานะเป็น "sent" ตรง ๆ แต่ใช้คำสั่ง SQL แบบมีเงื่อนไขเพื่อเปลี่ยนสถานะเป็น
sendingก่อน (UPDATE ... WHERE status NOT IN ('sending','sent')) ถ้าไม่มีแถวถูกแก้ไข (เช่น มีอีกคำขอกดส่งซ้ำพร้อมกัน หรืองานเดิมกำลังส่งอยู่แล้ว) จะตอบกลับ 409 Conflict ทันทีโดยไม่ publish งานซ้ำ เป็นกลไกป้องกันการกดส่งซ้ำสองครั้ง (double-click) ไม่ให้ส่งข้อความซ้ำ - สถานะ
sendingไม่มีอยู่ในหน้าจอฝั่งหน้าเว็บเลย — enum สถานะที่ frontend รู้จักมีแค่sent/scheduled/cancel/draftเท่านั้น หมายความว่าในช่วงเวลาสั้น ๆ ระหว่างกดส่งทันทีจนกว่า worker จะประมวลผลเสร็จและเปลี่ยนสถานะเป็นsentแคมเปญนั้นจะไม่ปรากฏในแท็บใดเลย ของหน้ารายการ (ไม่อยู่ในตั้งเวลา ไม่อยู่ในฉบับร่าง ไม่อยู่ในส่งแล้ว) เป็นพฤติกรรมชั่วคราวที่ควรรู้ไว้เผื่อผู้ใช้สงสัยว่าแคมเปญที่กดส่งไปหายไปไหน - การตรวจ merge tag ใช้ regex ที่เข้มงวด — ต้องเป็นรูปแบบ
{{...}}ที่ปิดด้วยวงเล็บปีกกาคู่จริง ๆ ({{name}}ตรวจพบ แต่ข้อความที่พิมพ์วงเล็บไม่ครบจะไม่ถูกตรวจพบและถูกส่งออกไปตามตัวอักษร) และ backend ตรวจซ้ำอีกชั้นตอนบันทึกจริง ไม่ได้เชื่อผลตรวจจากฝั่งหน้าเว็บอย่างเดียว — ถ้าเนื้อหามี merge tag แต่ตั้งcastType = broadcastจะถูกปฏิเสธด้วย 400 "Merge tags are only supported with multicast campaigns" แม้เรียก API ตรงข้ามหน้าเว็บก็ตาม - ตัวเลข "ส่งแล้ว" ในหน้าผลลัพธ์ไม่ได้เก็บเป็นค่านิ่งเสมอ — ถ้ายังไม่มีค่าที่บันทึกไว้ (
totalRecipient = 0) backend จะคำนวณสด: สำหรับ Multicast จะไปอ่านค่าtotalSuccessจากสรุปผลของกลุ่มเป้าหมายในเมนู Segment audience แล้วเขียนกลับเข้าแคมเปญไว้ใช้ครั้งต่อไป ส่วน Broadcast จะนับจำนวนผู้ใช้ไม่ซ้ำจาก log การติดตามผลโดยตรง — เป็นเหตุผลที่ตัวเลข "ส่งแล้ว" อาจยังเป็น 0 อยู่พักหนึ่งหลังส่งไป ก่อนที่ระบบติดตามผลจะมีข้อมูลเข้ามาให้คำนวณ - ข้อมูลการติดตามผลใช้โครงสร้างแบบผสมระหว่างค่าที่คำนวณล่วงหน้ากับข้อมูลสด — ตัวเลขคลิก/การดูของแต่ละข้อความถูกเก็บสรุปไว้เป็นระยะ (มีเคอร์เซอร์วันที่ "คำนวณล่าสุดถึงเมื่อไหร่") แล้วนำมารวมกับ log เหตุการณ์ที่เกิดขึ้น "หลัง" เคอร์เซอร์นั้นแบบสดทุกครั้งที่เปิดหน้าดูผลลัพธ์ ทำให้อ่านได้เร็วแม้แคมเปญมีผู้รับและการคลิกจำนวนมาก
- การอ้างอิงชื่อซ้ำมีจุดที่ backend เขียนต่างจากพฤติกรรมเดิมโดยเจตนา — ถ้าไม่ได้ส่งชื่อมาเลยตอนแก้ไข (เช่น กดยกเลิกแคมเปญที่ส่งแค่
{status: "cancel"}) ระบบจะไม่ตรวจชื่อซ้ำ เพื่อไม่ให้การแก้ไขที่ไม่ได้แตะชื่อเลยถูกปฏิเสธด้วยข้อความ "Title is already exist" อย่างผิดพลาด
เมนูที่เกี่ยวข้องกันในเส้นทางนี้
- Rich message — ต้องสร้างและเปิดใช้งานเนื้อหาไว้ก่อนจึงจะสร้างแคมเปญได้
- Segment audience — จำเป็นเมื่อส่งแบบ Multicast และเป็นแหล่งของ "totalSuccess" ที่ใช้คำนวณยอด "ส่งแล้ว" ของแคมเปญ Multicast ด้วย ส่วนตัวเลือก "campaign" ที่ใช้เป็นเงื่อนไขสร้างกลุ่มเป้าหมายในเมนูนั้น จะดึงเฉพาะแคมเปญที่ ส่งแล้วและส่งภายใน 3 เดือนล่าสุดเท่านั้น มาให้เลือก (กรองด้วย query parameter พิเศษที่ backend เตรียมไว้ให้)
- Quick reply — ปุ่มตอบกลับที่ผูกกับ Rich Message จะติดไปกับแคมเปญโดยอัตโนมัติ
- Campaign Planner — มุมมองปฏิทินของแคมเปญชุดเดียวกันนี้
จุดที่ควรระวัง
- การกดส่งสำเร็จหมายถึง "รับงานเข้าคิวแล้ว" ไม่ใช่ "ส่งถึงผู้รับแล้ว" — cms-api ไม่ได้ยิงข้อความไปที่ LINE เอง เพียง publish งานเข้า RabbitMQ (คิว
line_broadcast_rich_messageหรือline_multicast_rich_messageตามประเภท) ให้ worker เป็นผู้ส่งจริง - ระบบป้องกันการกดส่งซ้ำด้วยกลไก claim ที่กล่าวถึงข้างต้นแล้ว แต่ ไม่มีปุ่ม/สถานะใดในหน้าเว็บที่บอกผู้ใช้ตรง ๆ ว่า "กำลังส่งอยู่" — ถ้าแคมเปญหายไปจากทุกแท็บชั่วครู่หลังกดส่งทันที ให้รอสักครู่แล้วรีเฟรชหน้าอีกครั้ง
- ความยาวชื่อแคมเปญ (ไม่เกิน 100 ตัวอักษร) เป็นข้อจำกัดฝั่งหน้าเว็บเท่านั้น ไม่มีการตรวจซ้ำที่ backend
สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)
- ฝั่งหน้าเว็บ — ถูกครอบด้วย
PermissionGuardตรวจ actionVIEWของ subjectcampaign-management - ฝั่งเซิร์ฟเวอร์ — ทุกเส้นทางของโมดูลนี้ถูกครอบด้วย
ModuleGate("campaign")ที่บังคับใช้จริง (ต่างจากโมดูล Dashboard ที่ policy เป็น no-op) — ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลแคมเปญให้องค์กรใด ผู้ใช้ในองค์กรนั้นจะได้ 403 ทันทีแม้เรียก API ตรง ไม่ว่าจะผ่านหน้าเว็บหรือไม่ - policy annotation ระดับการกระทำ (
create/update/delete/read/readAll) มีประกาศไว้แต่วิ่งผ่าน middleware ที่เป็น no-op เหมือนโมดูลอื่นส่วนใหญ่ในระบบ — การบังคับจริงจึงอยู่ที่ ModuleGate ระดับโมดูล ไม่ใช่ที่ policy ระดับการกระทำ DELETE /api/campaign/:idเป็น soft delete (เปลี่ยนสถานะเป็นcancelแล้วประทับเวลาลบ) ส่วนการลบจริง (DELETE /api/campaign/:id/hard) ต้องเป็น Super Admin เท่านั้น และต้องส่ง query parameterconfirm=trueมาด้วย ไม่เช่นนั้นจะถูกปฏิเสธ
ไฟล์อ้างอิงจากซอร์สโค้ด
Frontend (cms-web-2026)
src/app/campaign-management/page.tsx,src/app/campaign-management/form/page.tsx— จุดเข้าหน้ารายการและฟอร์มsrc/components/campaign-management/list/—campaign-management.container.tsx(ตาราง/แท็บ/ตัวกรอง),campaign-management-filter.tsxsrc/components/campaign-management/form/—campaign-management-form.container.tsx,campaign-management-form.tsx(กฎ merge tag, กฎเวลา, การ์ดผลลัพธ์),tracking-users-drawer.tsxsrc/services/campaign-management.service.ts— endpoint ทั้งหมดที่หน้าเว็บเรียกใช้
Backend (line-management-cms-api-go)
internal/modules/campaign/controller.go— 8 เส้นทาง,ModuleGate,SuperAdminGuardของ hard deleteinternal/modules/campaign/service.go— ตรรกะสร้าง/แก้ไข/ลบ, กลไก claim ก่อนส่ง, cache Redisinternal/modules/campaign/dto.go— กฎตรวจสอบข้อมูลขาเข้าinternal/modules/campaign/tracking.go— การรวมข้อมูลติดตามผลแบบผสม pre-aggregate + สด
ดูรายละเอียดเชิงเทคนิคเพิ่มเติม (เวอร์ชันก่อนหน้าของทีมเอกสาร) ที่ จัดการแคมเปญ Broadcast