Rich menu default
ภาพรวม
Rich menu default คือเมนูตั้งต้นของ LINE OA — เมนูรูปภาพที่ปักอยู่ด้านล่างหน้าแชท แบ่งเป็นเมนูสำหรับผู้เยี่ยมชม (guest) และสำหรับสมาชิก (member) สร้างได้ประเภทละ 1 ตัวเท่านั้น จุดสำคัญที่ต้องรู้ก่อนเริ่มคือ การกดบันทึกที่นี่ไม่ใช่แค่เขียนข้อมูลลงฐานข้อมูล แต่เป็นการยิงคำสั่งไปสร้างริชเมนูขึ้นบน LINE จริงหลายจังหวะติดกัน ถ้าจังหวะใดพลาด เมนูจะถูกประทับสถานะ failed พร้อมเหตุผลที่ผิดพลาดเก็บไว้
บทบาทที่ใช้งาน — ผู้ดูแลที่ต้องตั้งเมนูหลักให้ทุกคนที่แชทกับ OA เห็นเป็นเมนูเดียวกัน (โมดูลนี้มี ModuleGate บังคับจริงที่ backend — ดูหัวข้อ สิทธิ์การเข้าถึง)
เส้นทางผู้ใช้งาน
1. ดูรายการเมนูตั้งต้น
เปิดเมนู Rich menu > Rich menu default หน้านี้ไม่มีแถบตัวกรองเลย (ต่างจาก Rich menu custom และ Switch rich menu) เพราะมีได้อย่างมาก 2 รายการ ตารางแสดงคอลัมน์ ลำดับ / รูปภาพ / ชื่อ / ประเภท / ช่วงเวลา / สถานะ / จัดการ — คอลัมน์ "ช่วงเวลา" จะขึ้น - เสมอสำหรับเมนูตั้งต้น เพราะเมนูชนิด guest/member ไม่มีวันเริ่ม-สิ้นสุด (ช่องนั้นมีไว้ให้ชนิด custom ใช้)
ปุ่ม "สร้างริชเมนู" จะหายไปเองเมื่อมีทั้ง guest และ member ครบแล้ว และหน้านี้ไม่มีปุ่มลบ — มีแค่ปุ่มดู (แว่นตา) และแก้ไข (ดินสอ) เท่านั้น
2. สร้างเมนูตั้งต้น
- กดสร้างใหม่ เข้าฟอร์ม
/rich-menu/form?type=default - กรอกชื่อ — ต้องไม่ซ้ำกับริชเมนูอื่นใน OA เดียวกัน (ตรวจที่ backend)
- เลือกประเภท — ผู้เยี่ยมชม (guest) หรือ สมาชิก (member) ระบบดึงรายการที่มีอยู่แล้วมาตรวจ ถ้ามี member อยู่แล้วตัวเลือก member จะถูก disable และเลือก guest ให้อัตโนมัติ (และกลับกัน) ฟอร์มนี้ไม่มีช่องเลือกสถานะเลย — ระบบบังคับเป็น "ใช้งาน" (active) ให้ตอนกดบันทึก
- เลือกเลย์เอาต์ จาก 15 แบบ แบ่งเป็น 3 แท็บ — ใหญ่ (แบบที่ 1–8), กะทัดรัด (9–13), กำหนดเอง (14–15) ค่าเริ่มต้นที่เลือกไว้ให้คือแบบที่ 8 (พื้นที่กดเดียวเต็มภาพ) ไม่ใช่แบบที่ 1 การเปลี่ยนแท็บหรือเปลี่ยนเลย์เอาต์จะล้างรูปภาพและพื้นที่กดเดิมทิ้งทั้งหมด
- อัปโหลดรูปภาพพื้นหลัง — JPG/JPEG/PNG ไม่เกิน 1 MB และขนาดภาพต้องตรงกับรายการที่กำหนดเท่านั้น: แบบใหญ่/กำหนดเองแบบเต็ม (เลย์เอาต์ 1–8, 14) รับ
2500×1686,1200×810,800×540— แบบกะทัดรัด (9–13, 15) รับ2500×843,1200×405,800×270ขนาดอื่นถูกปฏิเสธทันทีก่อนอัปโหลด - ตั้งป้ายชื่อแถบเมนู — "เมนู (ค่าเริ่มต้น)" หรือป้ายกำหนดเองไม่เกิน 14 ตัวอักษร
- ตั้งค่าการกระทำต่อช่อง (ป้าย A, B, C…) — เลือกได้ 5 แบบ: ลิงก์ (URL ต้องขึ้นต้น
https://ไม่เกิน 500 ตัวอักษร มีเช็กบ็อกซ์ "การติดตาม"), ข้อความ (ไม่เกิน 300 ตัวอักษร), โทรศัพท์ (ไม่เกิน 10 หลัก ตรวจรูปแบบเบอร์ไทย), สลับริชเมนู (เลือกเมนูย่อยจาก Switch rich menu ที่ผูกกับเมนูนี้), หรือ ไม่มีการทำงาน ทุกแบบมีช่อง "ป้ายชื่อการทำงาน" บังคับ ไม่เกิน 20 ตัวอักษร (เท่ากับเพดานที่ LINE บังคับเอง) - กดบันทึก ระบบเปิดกล่องยืนยันก่อน แล้วเรียก
POST /api/rich-menuแบบ multipart/form-data
3. ดูสถิติการคลิกย้อนหลัง
- จากหน้ารายการ กดปุ่มแว่นตาเพื่อเข้าโหมดดู — ทุกช่องถูกปิดการแก้ไข และมีตัวอย่างเมนูจริงแสดงขึ้นมา
- ด้านล่างมีตารางสถิติแยกรายช่อง — ตำแหน่ง (แปลงเป็นตัวอักษร A, B, C…), ข้อมูล (ป้ายชื่อ; ถ้าเป็นชนิดลิงก์จะกดเปิด URL ปลายทางเดิมได้), ประเภทการทำงาน, จำนวนคลิกทั้งหมด, จำนวนคลิกที่ไม่ซ้ำกัน (สองคอลัมน์หลังเรียงลำดับได้)
- มีข้อความกำกับว่า "สถิติการคลิกจะอัปเดตอัตโนมัติทุกชั่วโมง" พร้อมปุ่ม ซิงค์ใหม่ — ปุ่มนี้เรียก
POST /api/rich-menu/{id}/recalculate-statซึ่งเป็นการส่งงานเข้าคิว ไม่ใช่คำนวณสดทันที หน้าเว็บรอ 2.5 วินาทีแล้วดึงข้อมูลใหม่ให้เอง
4. แก้ไขเมนูที่มีอยู่
กดปุ่มดินสอเพื่อแก้ไข — ช่องสถานะถูกซ่อนทั้งหมดในหน้าแก้ไขของเมนูตั้งต้น การบันทึกทับจะสร้างริชเมนูใหม่บน LINE แล้วลบตัวเก่าทิ้ง (ดูหัวข้อถัดไป)
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก internal/modules/richmenu/{controller,service,dto,create_update,update,archive_stats}.go และ internal/modules/linemessageapi/{richmenu,layout}.go ของ line-management-cms-api-go:
- ลำดับการเรียก LINE ตอนบันทึกมี 6 จังหวะเรียงกัน และทำเฉพาะเมื่อสถานะที่บันทึกจริงเป็น
active— (1) อ่านความกว้าง/สูงจากไฟล์ภาพที่อัปโหลด (2)POSTสร้างริชเมนูบน LINE ด้วย payload{size, selected, name, chatBarText, areas}(3) เขียนline_rich_menu_idที่ LINE ตอบกลับมาลงฐานข้อมูล พร้อมสำเนาลงrich_menu_archive(4) อัปโหลดรูปภาพเข้าริชเมนูตัวนั้น (5) สร้าง rich menu alias (6) ถ้าเป็นชนิด guest จึงสั่ง "ตั้งเป็นเมนูตั้งต้นของ OA" แล้วจึงล้างแคช Redis และส่งงานคำนวณสถิติเข้าคิว หากจังหวะใดล้มเหลว ระบบเขียนstatus = failedพร้อมเหตุผลลงคอลัมน์reasonแล้วตอบ 400"createRichMenuOnLineOA : Fail" - เมนูชนิด member ตอน "สร้าง" ไม่ได้ถูกผูกให้ผู้ใช้คนไหนเลย — จังหวะที่ 6 ข้างต้นเช็กเฉพาะ guest เท่านั้น การผูกเมนู member เข้ากับสมาชิกทุกคนเกิดขึ้นตอน แก้ไข เท่านั้น (ตอนนั้น backend จะ publish งาน
setRichMenuMemberเข้าคิวline_change_richmenuให้ worker ไปดึงline_userที่user_type = memberทั้งหมด แล้วเรียก LINE ผูกทีละชุด 500 คน) — ในทางปฏิบัติ เมนู member ที่สร้างใหม่จึงมักต้องกดบันทึกอีกครั้งจึงจะมีผลกับสมาชิกจริง - ทุกครั้งที่กดบันทึกทับ ระบบสร้างริชเมนู "ตัวใหม่" บน LINE แล้วลบตัวเก่าทิ้ง —
line_rich_menu_idเปลี่ยนทุกครั้ง ส่วน alias ใช้สูตรคงที่คือrichmenu-alias-+md5("{organizationId}_{lineOaId}_{richMenuId}")16 ตัวแรก จึงชี้ไปยังเมนูตัวใหม่ได้เสมอโดยไม่ต้องแก้ที่อื่น (ตอนแก้ไขจะพยายาม update alias ก่อน ถ้าไม่สำเร็จจึงถอยไป create ใหม่) และเพราะสร้างแถวrich_menu_archiveใหม่ทุกครั้งที่บันทึก จึงมีประวัติทุกเวอร์ชันไว้ผูกกับสถิติ - เลย์เอาต์แบบที่ 15 (กำหนดเอง–กะทัดรัด) ส่งพื้นที่กดขึ้น LINE เป็น "ศูนย์ช่อง" — ตารางเลย์เอาต์ฝั่ง backend ประกาศแบบที่ 14 และ 15 ไว้ว่า
TotalActions: 0และไม่มีกรอบพื้นที่ตายตัว แต่โค้ดที่คำนวณพื้นที่จากภาพที่ผู้ใช้ลากเอง (imagemap) เช็กเงื่อนไขlayoutNo == 14เท่านั้น เลย์เอาต์ 15 จึงตกไปใช้ค่าTotalActions: 0ของตาราง ทำให้areasที่ส่งขึ้น LINE ว่างเปล่า — พื้นที่กดที่วาดไว้ยังถูกบันทึกลงฐานข้อมูลและยังโชว์ในตารางสถิติ แต่กดบนแอป LINE จริงจะไม่มีอะไรเกิดขึ้น ถ้าต้องการเมนูแบบวาดพื้นที่เอง ให้ใช้แบบที่ 14 (กำหนดเอง–เต็ม) แทน - ช่องแบบ "ไม่มีการทำงาน" ถูกตัดออกจาก payload ที่ส่งขึ้น LINE ทั้งช่อง — ไม่ได้ส่งขึ้นไปแบบไม่มี action แต่หายไปจากรายการ
areasเลย ทำให้ตำแหน่งนั้นเป็นพื้นที่ตายบนภาพจริง ๆ - การติดตามคลิกทำงานต่างกันโดยสิ้นเชิงระหว่างชนิดของการกระทำ — ช่องชนิด ลิงก์ที่ติ๊ก "การติดตาม" จะถูกแปลง URL ปลายทางเป็นลิงก์ redirect สั้นที่ระบบสร้างขึ้นใหม่ พร้อมแนบ metadata (
contentType: rich_menu,organizationId,lineOaId,richMenuId,richMenuActionIndex,richMenuArchiveId) ก่อนส่งขึ้น LINE — ถ้าไม่ติ๊ก ระบบส่ง URL เดิมขึ้น LINE ตรง ๆ และจะไม่มีข้อมูลคลิกใด ๆ เลย ส่วนช่องชนิด ข้อความ ถูกแปลงเป็น LINE action ชนิดpostback(ไม่ใช่message) และช่องชนิด สลับริชเมนู เป็นrichmenuswitchทั้งสองแบบเก็บข้อมูลผ่าน webhook ไม่ผ่านลิงก์ redirect ส่วนช่องชนิด โทรศัพท์ ส่งเป็นtel:ตรงและไม่มีการติดตามเลย - สถิติจะอัปเดตอัตโนมัติเฉพาะเมนูที่มีการคลิกลิงก์ที่เปิดติดตามไว้เท่านั้น — งาน cron รายชั่วโมงจะหยิบเฉพาะเมนูที่ถูกปักธง
RICH_MENU_STAT_DIRTY:{id}ไว้ใน Redis และผู้ที่ปักธงนั้นมีเพียงบริการ redirect ที่รับคลิกลิงก์ติดตามเท่านั้น การคลิกชนิด postback/สลับริชเมนู (ซึ่งวิ่งผ่าน webhook) ถูกบันทึกลงtracking_logจริงแต่ไม่ปักธง — ฉะนั้นเมนูที่มีแต่ช่องข้อความ/โทรศัพท์/สลับริชเมนู จะไม่เคยถูกคำนวณสถิติใหม่โดยอัตโนมัติ ต้องกดปุ่ม ซิงค์ใหม่ เอง - ตัวเลขสถิติของเมนูตั้งต้นเป็นยอดสะสมข้ามทุกครั้งที่บันทึกทับ และมีกลไกกันตัวเลขเพี้ยนเมื่อเปลี่ยนเลย์เอาต์ —
template_trackingผูกกับrich_menu.idที่คงที่ ไม่ได้ผูกกับ archive แต่ละรอบ ถ้าเปลี่ยนความหมายของช่องแล้วตัวเลขเก่าจะปนกับตัวเลขใหม่ ฝั่ง read จึงมีการเทียบ archive สองรอบล่าสุด (เทียบด้วย index + ประเภท + ลิงก์) ถ้าพบว่าเลย์เอาต์ต่างกัน จะคำนวณสถิติสดเฉพาะ archive รอบล่าสุดมาแสดงแทน โดยไม่เขียนกลับลงฐานข้อมูล - การตรวจกฎการเปลี่ยนสถานะที่ backend มีผลกับชนิด
switchเท่านั้น — ฟังก์ชันValidateStatusTransition/ValidateStatusForAction/ValidateStatusForDeleteเปิดด้วยเงื่อนไขif type == switchทั้งหมด กฎสถานะของเมนู guest/member (และ custom) จึงบังคับอยู่ที่ฝั่งหน้าเว็บเท่านั้น - เพดานขนาดไฟล์รูปที่ backend ยอมรับคือ 10 MB ไม่ใช่ 1 MB — DTO ตรวจ mime
image/jpeg/image/pngและขนาดไม่เกิน10,000,000ไบต์ ส่วนกฎ 1 MB และการตรวจความละเอียดภาพเป็นข้อจำกัดฝั่งหน้าเว็บล้วน ๆ backend ไม่ตรวจความละเอียดเลย
เมนูที่เกี่ยวข้องกันในเส้นทางนี้
- Switch rich menu — ต้องมีเมนูตั้งต้นนี้อยู่ก่อน จึงจะสร้างเมนูย่อยที่ผูกกับมันได้ และเมนูย่อยที่สร้างไว้แล้วจะกลายเป็นตัวเลือกของการกระทำ "สลับริชเมนู" ในฟอร์มนี้ (ดรอปดาวน์กรองด้วย
parentIdเท่ากับเมนูตั้งต้นตัวนี้ และเอาเฉพาะที่สถานะactive) ในฟอร์มมีปุ่ม "สร้างใหม่" ที่เปิดแท็บใหม่ไปยังฟอร์ม Switch rich menu พร้อมส่งparentIdไปให้เลย - Rich menu custom — ใช้ระบบเดียวกันทั้งหมด (ตารางเดียว โมดูลเดียว ฟอร์มเดียว) ต่างกันที่ชนิดข้อมูลและช่วงเวลา/กลุ่มเป้าหมาย
- Segment audience — ไม่เกี่ยวกับเมนูตั้งต้นโดยตรง แต่จำเป็นสำหรับ Rich menu custom
- Workflow / Trigger rule — สั่งสลับริชเมนูให้ผู้ใช้รายคนได้ผ่านงานคิว
line_change_richmenuชนิดsetRichMenuByTriggerRule
จุดที่ควรระวัง
- สร้างเมนู member แล้วยังไม่มีผลกับสมาชิก จนกว่าจะกดบันทึกแก้ไขอีกครั้ง (ดูรายละเอียดข้างต้น) — ถ้าสงสัยว่าทำไมสมาชิกยังเห็นเมนูเดิม ให้เข้าไปกดบันทึกซ้ำ
- หลีกเลี่ยงเลย์เอาต์แบบที่ 15 จนกว่าจะมีการแก้ — พื้นที่กดที่วาดไว้จะไม่ทำงานบน LINE จริง
- ลิงก์ที่ไม่ติ๊ก "การติดตาม" จะไม่มีสถิติเลย ไม่ใช่มีสถิติแต่เป็นศูนย์ — ตารางสถิติจะแสดงแถวนั้นด้วยตัวเลข 0 ค้างอยู่ตลอด
- สถิติของเมนูที่ไม่มีลิงก์ติดตามจะไม่อัปเดตเอง แม้จะมีคนกดจริง — ต้องกด "ซิงค์ใหม่" (ข้อความบนหน้าจอที่บอกว่า "อัปเดตอัตโนมัติทุกชั่วโมง" จริงเฉพาะเมนูที่ถูกปักธงจากคลิกลิงก์ติดตาม)
- การบันทึกทับใช้เวลาหลายวินาที เพราะต้องคุยกับ LINE 3–4 รอบ (สร้าง / อัปโหลดรูป / จัดการ alias / ตั้งเป็นค่าเริ่มต้น) และถ้าหลุดกลางทางจะเห็นสถานะ
failed— การกดบันทึกอีกครั้งจะถูกตีความเป็นการ "ลองใหม่" และดันสถานะกลับเป็นactiveให้เอง - ขนาดไฟล์ 1 MB และการตรวจความละเอียดภาพเป็นกฎฝั่งหน้าเว็บเท่านั้น การเรียก API ตรงจะผ่านได้ถึง 10 MB และภาพขนาดใดก็ได้ (แต่ LINE อาจปฏิเสธเองภายหลัง)
สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)
- ฝั่งหน้าเว็บ — หน้านี้ถูกครอบด้วย
PermissionGuardตรวจ actionVIEWของ subjectrich-menu-defaultซึ่งเป็น subject แยกจากrich-menu-customและrich-menu-switch - ฝั่งเซิร์ฟเวอร์ — ทุกเส้นทางของโมดูลถูกครอบด้วย
ModuleGate("rich-menu")ที่บังคับใช้จริง และเป็น gate ตัวเดียวที่คุมทั้งสามชนิดพร้อมกัน (Default / Custom / Switch อยู่บนตารางrich_menuเดียวกัน แยกกันด้วยคอลัมน์typeเท่านั้น) — ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลrich-menuให้องค์กรใด ทั้งสามหน้าจะถูกปฏิเสธ 403 พร้อมกัน ไม่สามารถปิดเฉพาะบางชนิดที่ระดับ backend ได้ (ผู้เรียกฝั่ง onemoby ข้าม gate นี้ได้) - ความต่างที่ควรรู้ — สิทธิ์ฝั่งหน้าเว็บแยกได้ 3 ระดับ (default/custom/switch) แต่ฝั่ง backend มีชั้นเดียว การซ่อนเมนูฝั่งหน้าเว็บจึงไม่ได้ปิดการเรียก API ของชนิดนั้นโดยตรง
- policy annotation ระดับการกระทำ (
create/update/delete/read/readAll) มีประกาศไว้ครบแต่วิ่งผ่าน middleware ที่เป็น no-op เหมือนโมดูลอื่นส่วนใหญ่ — การบังคับจริงอยู่ที่ ModuleGate ระดับโมดูล - การกันข้ามองค์กร (IDOR) ทำที่ระดับ query —
findByIdเพิ่มเงื่อนไขline_oa_idของผู้เรียกเสมอ (ไม่พบจะได้ 400APP_007),recalculate-statเช็กความเป็นเจ้าของก่อนส่งงานเข้าคิว (ไม่พบได้ 404) และการลบก็ผูกline_oa_idไว้ทุกคำสั่ง DELETE /api/rich-menu/:idเป็น soft delete (เปลี่ยนสถานะเป็นdeleteแล้วประทับdeleted_date) ส่วนการลบจริงDELETE /api/rich-menu/:id/hardต้องเป็น Super Admin และต้องส่งconfirm=trueมาด้วย ไม่เช่นนั้นถูกปฏิเสธ
ไฟล์อ้างอิงจากซอร์สโค้ด
Frontend (cms-web-2026)
src/app/rich-menu/default/page.tsx— จุดเข้าหน้ารายการ (PermissionGuardsubjectrich-menu-default)src/app/rich-menu/form/page.tsx— จุดเข้าฟอร์มร่วมของทั้งสามชนิด (อ่านtypeจาก query string)src/components/rich-menu/list/rich-menu.container.tsx— ตาราง/คอลัมน์ตามชนิด, กฎ disable ปุ่มแก้ไข-ลบ, การซ่อนปุ่มสร้างเมื่อครบ guest+membersrc/components/rich-menu/form/default/rich-menu-default-form.container.tsx— การประกอบ FormData, กฎ 1 MB, การซิงค์สถิติ,getCreateStatussrc/components/rich-menu/form/template/—template-layout.tsx(15 แบบ / 3 แท็บ),template-content.tsx,template-custom-area.tsx(เพดาน 20 พื้นที่)src/components/rich-menu/form/action-setting/—action-setting.tsx(5 ชนิดการกระทำ),action-setting-render-type.tsx(กฎแต่ละชนิด)src/components/rich-menu/form/utils/upload-file.ts— กฎขนาดไฟล์และความละเอียดภาพต่อเลย์เอาต์src/services/rich-menu.service.ts— endpoint ทั้งหมดที่หน้าเว็บเรียกใช้messages/{th,en}/rich-menu.json— ข้อความและป้ายชื่อทั้งหมดของทั้งสามหน้า
Backend (line-management-cms-api-go)
internal/modules/richmenu/controller.go— 8 เส้นทาง,ModuleGate("rich-menu"),SuperAdminGuardของ hard deleteinternal/modules/richmenu/create_update.go— ลำดับการเรียก LINE ตอนสร้าง,effectiveCreateStatus, การเขียน archiveinternal/modules/richmenu/update.go— ลำดับตอนแก้ไข, การลบเมนูเก่าบน LINE, กลไกลองใหม่ของเมนูที่failedinternal/modules/richmenu/service.go—findById(ผูกline_oa_id),RecalculateStat, กฎสถานะเฉพาะชนิด switch, การลบinternal/modules/richmenu/archive_stats.go— การคำนวณสถิติสดเมื่อเลย์เอาต์เปลี่ยนระหว่าง archiveinternal/modules/linemessageapi/richmenu.go— การแปลง action เป็น payload ของ LINE และการสร้างลิงก์ติดตามinternal/modules/linemessageapi/layout.go— ตารางเลย์เอาต์ 15 แบบและขนาดภาพ
Worker (line-management-worker-go)
internal/cronscheduler/rich_menu_stat_scanner.go— cron รายชั่วโมงที่หยิบเฉพาะเมนูที่ถูกปักธงinternal/richmenu/service_methods.go—setRichMenuMemberByLineOaId(ผูกเมนู member ทีละ 500 คน)
ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ ริชเมนู (Default / Custom / Switch)