Skip to main content

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. สร้างเมนูตั้งต้น​

  1. กดสร้างใหม่ เข้าฟอร์ม /rich-menu/form?type=default
  2. กรอกชื่อ — ต้องไม่ซ้ำกับริชเมนูอื่นใน OA เดียวกัน (ตรวจที่ backend)
  3. เลือกประเภท — ผู้เยี่ยมชม (guest) หรือ สมาชิก (member) ระบบดึงรายการที่มีอยู่แล้วมาตรวจ ถ้ามี member อยู่แล้วตัวเลือก member จะถูก disable และเลือก guest ให้อัตโนมัติ (และกลับกัน) ฟอร์มนี้ไม่มีช่องเลือกสถานะเลย — ระบบบังคับเป็น "ใช้งาน" (active) ให้ตอนกดบันทึก
  4. เลือกเลย์เอาต์ จาก 15 แบบ แบ่งเป็น 3 แท็บ — ใหญ่ (แบบที่ 1–8), กะทัดรัด (9–13), กำหนดเอง (14–15) ค่าเริ่มต้นที่เลือกไว้ให้คือแบบที่ 8 (พื้นที่กดเดียวเต็มภาพ) ไม่ใช่แบบที่ 1 การเปลี่ยนแท็บหรือเปลี่ยนเลย์เอาต์จะล้างรูปภาพและพื้นที่กดเดิมทิ้งทั้งหมด
  5. อัปโหลดรูปภาพพื้นหลัง — JPG/JPEG/PNG ไม่เกิน 1 MB และขนาดภาพต้องตรงกับรายการที่กำหนดเท่านั้น: แบบใหญ่/กำหนดเองแบบเต็ม (เลย์เอาต์ 1–8, 14) รับ 2500×1686, 1200×810, 800×540 — แบบกะทัดรัด (9–13, 15) รับ 2500×843, 1200×405, 800×270 ขนาดอื่นถูกปฏิเสธทันทีก่อนอัปโหลด
  6. ตั้งป้ายชื่อแถบเมนู — "เมนู (ค่าเริ่มต้น)" หรือป้ายกำหนดเองไม่เกิน 14 ตัวอักษร
  7. ตั้งค่าการกระทำต่อช่อง (ป้าย A, B, C…) — เลือกได้ 5 แบบ: ลิงก์ (URL ต้องขึ้นต้น https:// ไม่เกิน 500 ตัวอักษร มีเช็กบ็อกซ์ "การติดตาม"), ข้อความ (ไม่เกิน 300 ตัวอักษร), โทรศัพท์ (ไม่เกิน 10 หลัก ตรวจรูปแบบเบอร์ไทย), สลับริชเมนู (เลือกเมนูย่อยจาก Switch rich menu ที่ผูกกับเมนูนี้), หรือ ไม่มีการทำงาน ทุกแบบมีช่อง "ป้ายชื่อการทำงาน" บังคับ ไม่เกิน 20 ตัวอักษร (เท่ากับเพดานที่ LINE บังคับเอง)
  8. กดบันทึก ระบบเปิดกล่องยืนยันก่อน แล้วเรียก POST /api/rich-menu แบบ multipart/form-data

3. ดูสถิติการคลิกย้อนหลัง​

  1. จากหน้ารายการ กดปุ่มแว่นตาเพื่อเข้าโหมดดู — ทุกช่องถูกปิดการแก้ไข และมีตัวอย่างเมนูจริงแสดงขึ้นมา
  2. ด้านล่างมีตารางสถิติแยกรายช่อง — ตำแหน่ง (แปลงเป็นตัวอักษร A, B, C…), ข้อมูล (ป้ายชื่อ; ถ้าเป็นชนิดลิงก์จะกดเปิด URL ปลายทางเดิมได้), ประเภทการทำงาน, จำนวนคลิกทั้งหมด, จำนวนคลิกที่ไม่ซ้ำกัน (สองคอลัมน์หลังเรียงลำดับได้)
  3. มีข้อความกำกับว่า "สถิติการคลิกจะอัปเดตอัตโนมัติทุกชั่วโมง" พร้อมปุ่ม ซิงค์ใหม่ — ปุ่มนี้เรียก 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 ตรวจ action VIEW ของ subject rich-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 ของผู้เรียกเสมอ (ไม่พบจะได้ 400 APP_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 — จุดเข้าหน้ารายการ (PermissionGuard subject rich-menu-default)
  • src/app/rich-menu/form/page.tsx — จุดเข้าฟอร์มร่วมของทั้งสามชนิด (อ่าน type จาก query string)
  • src/components/rich-menu/list/rich-menu.container.tsx — ตาราง/คอลัมน์ตามชนิด, กฎ disable ปุ่มแก้ไข-ลบ, การซ่อนปุ่มสร้างเมื่อครบ guest+member
  • src/components/rich-menu/form/default/rich-menu-default-form.container.tsx — การประกอบ FormData, กฎ 1 MB, การซิงค์สถิติ, getCreateStatus
  • src/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 delete
  • internal/modules/richmenu/create_update.go — ลำดับการเรียก LINE ตอนสร้าง, effectiveCreateStatus, การเขียน archive
  • internal/modules/richmenu/update.go — ลำดับตอนแก้ไข, การลบเมนูเก่าบน LINE, กลไกลองใหม่ของเมนูที่ failed
  • internal/modules/richmenu/service.go — findById (ผูก line_oa_id), RecalculateStat, กฎสถานะเฉพาะชนิด switch, การลบ
  • internal/modules/richmenu/archive_stats.go — การคำนวณสถิติสดเมื่อเลย์เอาต์เปลี่ยนระหว่าง archive
  • internal/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)