Skip to main content

Switch rich menu

ภาพรวม​

Switch rich menu คือเมนูย่อยที่ผูกกับเมนูหลัก (Rich menu default หรือ Rich menu custom) ใช้เป็นปลายทางเมื่อผู้ใช้กดช่องชนิด "สลับริชเมนู" จากเมนูหลัก เหมาะกับการทำเมนูหลายชั้นหรือแบบแท็บ จุดสำคัญที่ต้องรู้ก่อนเริ่มคือ การเชื่อมโยงระหว่างเมนูใช้ชื่อ alias ที่คำนวณจากสูตรแฮชคงที่ ไม่ได้ค้นหาจากฐานข้อมูล ฉะนั้นถ้าเมนูปลายทางยังไม่เคยถูกส่งขึ้น LINE จริง ปุ่มสลับจะกดแล้ว "ไม่มีอะไรเกิดขึ้น" โดยไม่มีข้อความแจ้งเตือนใด ๆ

บทบาทที่ใช้งาน — ผู้ดูแลที่กำลังออกแบบเมนูหลายชั้นต่อจากเมนู Rich menu default/custom ที่มีอยู่แล้ว (โมดูลนี้มี ModuleGate บังคับจริงที่ backend — ดูหัวข้อ สิทธิ์การเข้าถึง)

เส้นทางผู้ใช้งาน​

1. ดูรายการเมนูย่อย​

เปิดเมนู Rich menu > Switch rich menu หน้านี้มีตัวกรอง 3 ช่อง — ค้นหา (จากชื่อ), ริชเมนูหลัก (ดรอปดาวน์แบ่งกลุ่มเป็น "ริชเมนูเริ่มต้น" และ "ริชเมนูกำหนดเอง" พร้อมรูปตัวอย่างเล็ก ๆ ข้างชื่อ) และ อัปเดตเมื่อ (ช่วงวันที่ กำกับ "(เวลากรุงเทพฯ)")

ตารางแสดงคอลัมน์ ลำดับ / รูปภาพ / ชื่อ / ริชเมนูหลัก / อัปเดตเมื่อ / จัดการ — สังเกตว่าหน้านี้ไม่มีคอลัมน์สถานะ (ต่างจากอีกสองหน้า) เพราะเมนูย่อยไม่มีสถานะให้ผู้ใช้กำหนดเอง

2. สร้างเมนูย่อย​

  1. ต้องมีเมนูหลักอยู่ก่อนเสมอ เพราะฟอร์มบังคับให้ระบุว่าเป็นเมนูย่อยของเมนูใด
  2. กดสร้างใหม่ เข้าฟอร์ม /rich-menu/form?type=switch — หรือเข้าทางลัดจากฟอร์มของเมนูหลักโดยตรง: ในช่องการกระทำชนิด "สลับริชเมนู" มีปุ่ม "สร้างใหม่" ที่เปิดแท็บใหม่ไปยังฟอร์มนี้พร้อมส่ง parentId มาให้เลย (ปุ่มนี้ถูกปิดเมื่อเมนูหลักไม่อยู่ในสถานะแบบร่างหรือปิดใช้งาน)
  3. เลือกริชเมนูหลัก — ดรอปดาวน์แบ่งกลุ่มเป็นเมนูเริ่มต้นและเมนูกำหนดเอง ค้นหาด้วยชื่อได้ การเปลี่ยนเมนูหลักจะล้างพื้นที่กดและการกระทำที่ตั้งไว้ทั้งหมด และในหน้าแก้ไข ช่องนี้ถูกปิดถาวร — ย้ายเมนูย่อยไปอยู่ใต้เมนูหลักตัวอื่นไม่ได้
  4. เลือกเลย์เอาต์ / อัปโหลดรูป / ตั้งค่าการกระทำต่อช่อง — เหมือนอีกสองชนิดทุกอย่าง (15 เลย์เอาต์ 3 แท็บ, รูป JPG/PNG ไม่เกิน 1 MB ตามความละเอียดที่กำหนด, การกระทำ 5 แบบ)
  5. ฟอร์มนี้ไม่มีช่องเลือกสถานะเลย ทั้งตอนสร้างและตอนแก้ไข — ระบบซ่อนไว้และตั้งค่าเป็น "เปิดใช้งาน" (active) ให้อัตโนมัติ ผลคือเมนูย่อยถูกส่งขึ้น LINE ทันทีที่บันทึก และเพราะสถานะเป็น active ตั้งแต่แรก ช่องในส่วนตั้งค่าการกระทำจึงบังคับกรอกทุกช่อง (ไม่มีโหมดแบบร่างให้เก็บงานค้างไว้)
  6. หนึ่งในตัวเลือกการกระทำคือ "สลับริชเมนู" ชี้กลับไปยังเมนูหลักหรือไปยังเมนูย่อยพี่น้องกันได้ ดรอปดาวน์แบ่งเป็นสองกลุ่ม — "ริชเมนูหลัก" และ "ริชเมนูย่อย" — พร้อมรูปตัวอย่างของเมนูปลายทางที่เลือก และมีข้อความกำกับว่า "คุณสามารถเลือกได้เฉพาะริชเมนูที่มีริชเมนูหลักเดียวกันเท่านั้น"
  7. กดบันทึก ยืนยัน แล้วระบบเรียก POST /api/rich-menu

3. ดูสถิติและจัดการเมนูย่อยที่มีอยู่​

  • กดปุ่มแว่นตาเพื่อดูโหมดอ่านอย่างเดียว พร้อมตัวอย่างเมนูและตารางสถิติการคลิกรายช่อง และปุ่ม ซิงค์ใหม่ สำหรับสั่งคำนวณสถิติใหม่
  • แก้ไขได้เฉพาะเมื่อเมนูหลักอยู่ในสถานะแบบร่างหรือปิดใช้งานเท่านั้น (สำหรับเมนูหลักชนิด custom) และแก้ไม่ได้เมื่อเมนูย่อยเองอยู่ในสถานะเสร็จสิ้น หมดอายุ หรือถูกลบ — tooltip บนปุ่มจะบอกเหตุผลตรง ๆ (เช่น "ไม่สามารถแก้ไขได้: ริชเมนูหลักต้องมีสถานะแบบร่างหรือปิดใช้งาน")
  • ลบไม่ได้เมื่อเมนูย่อยเองหรือเมนูหลักอยู่ในสถานะ "เสร็จสิ้น" — สถานะอื่นลบได้ (ต้องยืนยันก่อน)

รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง​

ยืนยันจาก internal/modules/richmenu/{service,update,create_update}.go, internal/modules/linemessageapi/richmenu.go, internal/core/httpx/codes.go ของ line-management-cms-api-go:

  • ชนิด switch เป็นชนิดเดียวที่ backend ตรวจกฎสถานะให้จริง — ฟังก์ชัน ValidateStatusTransition / ValidateStatusForAction / ValidateStatusForDelete ทั้งสามตัวเปิดด้วย if type == switch แล้วผ่านไปเลยสำหรับชนิดอื่น กฎที่บังคับจริงคือ: แบบร่างเปลี่ยนไปได้เฉพาะ "เปิดใช้งาน", เปิดใช้งานเปลี่ยนไปได้เฉพาะ "เปิดใช้งาน/ปิดใช้งาน/ลบ", ปิดใช้งานเปลี่ยนไปได้เฉพาะ "ลบ", และ ห้ามย้อนกลับจาก "ปิดใช้งาน" หรือ "ลบ" นอกจากนี้ยังห้ามแก้ไขเมนูย่อยที่อยู่ในสถานะ ปิดใช้งาน / ลบ / เสร็จสิ้น / หมดอายุ (ตอบ 400 "Cannot modify rich menu in current status") และห้ามลบเมนูย่อยที่สถานะ "เสร็จสิ้น"
  • ผลข้างเคียงที่ตามมาจากสองข้อรวมกัน: เมนูย่อยที่ถูกปิดใช้งานแล้วจะแก้ไขผ่านหน้าเว็บไม่ได้อีกเลย — เพราะฟอร์มซ่อนช่องสถานะและส่ง status = active ขึ้นมาตลอด แต่แถวจริงเป็น inactive (โดยมากถูกไล่เปลี่ยนมาจากเมนูหลัก) การบันทึกจึงถูกปฏิเสธด้วยกฎ "ห้ามย้อนกลับจากปิดใช้งาน" — และถูกปฏิเสธซ้ำอีกชั้นด้วยกฎ "ห้ามแก้ไขเมนูในสถานะปัจจุบัน" ทางเดียวคือสร้างเมนูย่อยใหม่
  • การเชื่อมโยงปุ่มสลับใช้แฮชคงที่ ไม่มีการค้นหาในฐานข้อมูล — ตอนสร้างพื้นที่กดชนิดสลับ ระบบคำนวณ richMenuAliasId เป็น richmenu-alias- + md5("{organizationId}_{lineOaId}_{รหัสเมนูปลายทาง}") 16 ตัวแรก และตั้ง data เป็น richmenu-changed-to- + แฮชเดียวกัน ขณะที่เมนูแต่ละตัวลงทะเบียน alias ของตัวเองบน LINE ด้วยสูตรเดียวกันเป๊ะ — สองฝั่งจึงมาบรรจบกันโดยไม่ต้องเก็บความสัมพันธ์ไว้ที่ใดเลย ข้อดีคือ line_rich_menu_id เปลี่ยนได้ทุกครั้งที่บันทึกทับโดยไม่ต้องแก้ปุ่มสลับ ข้อเสียคือถ้าเมนูปลายทางยังไม่เคยถูกส่งขึ้น LINE alias นั้นก็ไม่มีอยู่จริงบน LINE และการกดปุ่มจะไม่เกิดอะไรขึ้น
  • ระบบไม่ได้ตรวจว่าเมนูปลายทางพร้อมใช้จริงหรือไม่ก่อนบันทึก — โค้ดข้อความแสดงข้อผิดพลาด RMN_003 ("Rich menu alias is not active") มีประกาศอยู่ในตารางรหัสข้อผิดพลาด และหน้าเว็บมีการรองรับทั้ง RMN_003 และ RMN_004 ไว้พร้อมข้อความอธิบาย แต่ โมดูลริชเมนูฝั่ง Go ไม่เคยส่งรหัสทั้งสองออกมาเลย (RMN_004 ไม่ถูกประกาศในตารางรหัสด้วยซ้ำ) ผลคือการชี้ปุ่มสลับไปยังเมนูที่ยังไม่พร้อมจะ "บันทึกผ่าน" แล้วล้มเงียบตอนผู้ใช้กดบนแอป LINE
  • ดรอปดาวน์เลือกปลายทางในฟอร์ม Switch กรองสถานะไม่เหมือนฟอร์มอื่น — เมื่ออยู่ในฟอร์มของเมนูหลัก (default/custom) รายการเมนูย่อยที่ให้เลือกถูกกรองด้วย status = active แต่เมื่ออยู่ในฟอร์ม Switch เอง กลับไม่ส่งเงื่อนไขสถานะขึ้นไปเลย จึงเห็นเมนูย่อยพี่น้องทุกสถานะรวมทั้งที่ยังไม่เปิดใช้งาน — นี่คือช่องทางหลักที่ทำให้เกิดปุ่มสลับที่ชี้ไปยังเมนูที่ไม่พร้อมตามข้อก่อนหน้า ทั้งสองกรณีกรองด้วย parentId เดียวกัน และตัดเมนูตัวเองออกจากรายการเสมอในหน้าแก้ไข
  • ทุกครั้งที่บันทึกทับ เมนูย่อยถูกสร้างใหม่บน LINE แล้วลบตัวเก่าทิ้ง — เหมือนอีกสองชนิด และ alias จะถูก update ก่อน ถ้าไม่สำเร็จจึงถอยไป create ใหม่ ฉะนั้นปุ่มสลับจากเมนูหลักไม่ต้องแก้ตามเลย
  • การลบเมนูย่อยไม่ได้แก้เมนูหลักให้เลย — โค้ดในเส้นทางลบไปเก็บ "รายชื่อเมนูย่อยที่ต้องลบตาม" เฉพาะเมื่อเมนูที่กำลังลบเป็นชนิด custom เท่านั้น การลบเมนูย่อยจึงเป็นแค่ soft delete ตัวเอง (status = delete + ประทับ deleted_date) แล้วพยายามลบออกจาก LINE แบบ best-effort — พื้นที่กดชนิดสลับบนเมนูหลักยังชี้ไปที่ alias ที่ตายแล้ว จนกว่าจะเข้าไปแก้และบันทึกเมนูหลักใหม่
  • สถิติของเมนูย่อยไม่ได้ใช้กลไก "เทียบ archive สองรอบล่าสุด" แบบเมนูตั้งต้น — กลไกคำนวณสดนั้นทำงานกับชนิด guest/member เท่านั้น (และแถว rich_menu_archive ก็ถูกสร้างเฉพาะสองชนิดนั้น) เมนูย่อยจึงอ่านค่าจาก template_tracking ที่ worker เขียนไว้ตรง ๆ
  • การติดตามคลิกของปุ่มสลับเก็บผ่าน webhook ไม่ใช่ลิงก์ redirect — พื้นที่กดชนิดสลับถูกแปลงเป็น LINE action ชนิด richmenuswitch พร้อมแนบ metadata (contentType: rich_menu, type: richmenu_switch, organizationId, lineOaId, richMenuId, richMenuActionIndex, richMenuArchiveId) ลงในฟิลด์ data — และเพราะเหตุการณ์นี้ไม่ผ่านบริการ redirect มันจึงไม่ปักธง RICH_MENU_STAT_DIRTY ทำให้ cron รายชั่วโมงไม่หยิบเมนูนั้นไปคำนวณสถิติใหม่ ต้องกดปุ่ม ซิงค์ใหม่ เอง (รายละเอียดในหน้า Rich menu default)

เมนูที่เกี่ยวข้องกันในเส้นทางนี้​

  • Rich menu default — เป็นเมนูหลักได้ (ชนิด guest หรือ member) และเป็นที่ที่ผู้ใช้จะกดปุ่มสลับมาถึงเมนูย่อยนี้ ในฟอร์มของเมนูตั้งต้น รายการเมนูย่อยที่ให้เลือกถูกกรองเฉพาะที่สถานะ active
  • Rich menu custom — เป็นเมนูหลักได้เช่นกัน และเป็นชนิดที่มีผลกับเมนูย่อยมากที่สุด: สถานะของเมนูย่อยจะถูกไล่เปลี่ยนตามเมื่อเมนู custom หมดอายุหรือถูกลบ (พร้อมเขียนเหตุผลลงคอลัมน์ reason ว่ามาจากเมนูหลักตัวใด) และสิทธิ์ในการแก้ไข/ลบเมนูย่อยก็ขึ้นกับสถานะของเมนู custom นั้น
  • Segment audience — ไม่เกี่ยวกับเมนูย่อยโดยตรง แต่เป็นตัวกำหนดว่าใครจะเห็นเมนูหลักชนิด custom (และจึงจะกดมาถึงเมนูย่อยนี้ได้)
  • Workflow / Trigger rule — สั่งสลับริชเมนูให้ผู้ใช้รายคนได้โดยตรงผ่านงานคิว line_change_richmenu ชนิด setRichMenuByTriggerRule โดยไม่ต้องรอให้ผู้ใช้กดปุ่มเอง

จุดที่ควรระวัง​

  • ตรวจให้แน่ใจว่าเมนูปลายทางถูกส่งขึ้น LINE แล้วจริง ก่อนชี้ปุ่มสลับไปหา — ระบบไม่ตรวจให้ ไม่มีข้อความเตือน และผู้ใช้ปลายทางจะเจอปุ่มที่กดแล้วเงียบ
  • เมนูย่อยที่ถูกปิดใช้งานไปแล้วแก้ไขไม่ได้อีก ต้องสร้างใหม่แทน (ดูรายละเอียดข้างต้น)
  • เลือกเมนูหลักผิดตอนสร้างต้องสร้างใหม่ — ช่อง "ริชเมนูหลัก" ถูกปิดถาวรในหน้าแก้ไข ย้ายไปอยู่ใต้เมนูหลักตัวอื่นไม่ได้
  • การเปลี่ยนเมนูหลักในหน้าสร้างจะล้างพื้นที่กดและการกระทำที่ตั้งไว้ทั้งหมด — ควรเลือกเมนูหลักให้เสร็จก่อนเริ่มตั้งค่าการกระทำ
  • ลบเมนูย่อยแล้วต้องเข้าไปแก้เมนูหลักด้วย — ไม่เช่นนั้นปุ่มสลับบนเมนูหลักจะชี้ไปที่ปลายทางที่ไม่มีอยู่แล้ว
  • ฟอร์มนี้ไม่มีโหมดแบบร่าง — เพราะสถานะถูกบังคับเป็น "เปิดใช้งาน" ทุกช่องจึงบังคับกรอกครบ ไม่สามารถเก็บงานที่ทำค้างไว้ได้
  • หลีกเลี่ยงเลย์เอาต์แบบที่ 15 เช่นเดียวกับอีกสองชนิด — พื้นที่กดที่วาดไว้จะไม่ทำงานบน LINE จริง

สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)​

  • ฝั่งหน้าเว็บ — หน้านี้ถูกครอบด้วย PermissionGuard ตรวจ action VIEW ของ subject rich-menu-switch ซึ่งเป็น subject แยกจาก rich-menu-default และ rich-menu-custom
  • ฝั่งเซิร์ฟเวอร์ — ทุกเส้นทางถูกครอบด้วย ModuleGate("rich-menu") ที่บังคับใช้จริง และเป็น gate ตัวเดียวที่คุมทั้งสามชนิดพร้อมกัน (Default / Custom / Switch อยู่บนตาราง rich_menu เดียวกัน แยกด้วยคอลัมน์ type) — ปิดโมดูล rich-menu ให้องค์กรใด ทั้งสามหน้าจะได้ 403 พร้อมกัน ไม่มีทางเปิดให้ใช้เฉพาะเมนูย่อยหรือปิดเฉพาะเมนูย่อยที่ระดับ backend
  • ชนิดนี้เป็นชนิดเดียวที่มีการบังคับกฎ "การกระทำใดทำได้ในสถานะใด" ที่ฝั่งเซิร์ฟเวอร์จริง — ต่างจาก Default/Custom ที่กฎสถานะทั้งหมดอยู่ฝั่งหน้าเว็บ การเรียก API ตรงเพื่อแก้ไขเมนูย่อยที่ปิดใช้งาน/เสร็จสิ้น/หมดอายุ จะถูกปฏิเสธด้วย 400 จริง
  • การกันข้ามองค์กร (IDOR) — findById (ซึ่ง PUT และ DELETE เรียกใช้เป็นจังหวะแรก) ผูก line_oa_id ของผู้เรียกเสมอ (ไม่พบได้ 400 APP_007), recalculate-stat เช็กความเป็นเจ้าของก่อนส่งงานเข้าคิว (ไม่พบได้ 404) และคำสั่ง soft delete ทุกคำสั่งผูก line_oa_id
  • policy annotation ระดับการกระทำมีประกาศไว้ครบแต่วิ่งผ่าน middleware ที่เป็น no-op — การบังคับระดับโมดูลอยู่ที่ ModuleGate ส่วนการบังคับระดับสถานะอยู่ในตรรกะของ service
  • DELETE /api/rich-menu/:id เป็น soft delete ส่วน DELETE /api/rich-menu/:id/hard ต้องเป็น Super Admin และส่ง confirm=true มาด้วยเท่านั้น

ไฟล์อ้างอิงจากซอร์สโค้ด​

Frontend (cms-web-2026)

  • src/app/rich-menu/switch/page.tsx — จุดเข้าหน้ารายการ (PermissionGuard subject rich-menu-switch)
  • src/components/rich-menu/list/rich-menu-switch-filter.tsx — ตัวกรอง 3 ช่องเฉพาะของชนิด switch (ค้นหา / ริชเมนูหลัก / อัปเดตเมื่อ)
  • src/components/rich-menu/list/rich-menu.container.tsx — คอลัมน์ริชเมนูหลักและอัปเดตเมื่อ, การซ่อนคอลัมน์สถานะ, กฎ disable ปุ่มแก้ไข-ลบตามสถานะเมนูหลัก
  • src/components/rich-menu/form/action-setting/action-setting-render-type.tsx — RenderTypeAlias: ดรอปดาวน์แบ่งกลุ่ม, ปุ่ม "สร้างใหม่" ที่ส่ง parentId, รูปตัวอย่างปลายทาง
  • src/components/rich-menu/form/default/rich-menu-default-form.container.tsx — การกรองรายการปลายทางด้วย parentId (และเงื่อนไข status ที่ต่างกันตามชนิดฟอร์ม), การรับ parentId จาก URL
  • src/components/rich-menu/form/default/components/RichMenuStatusRadioGroup.tsx — การซ่อนช่องสถานะทั้งหมดสำหรับชนิด switch
  • src/services/rich-menu.service.ts — endpoint ทั้งหมดที่หน้าเว็บเรียกใช้

Backend (line-management-cms-api-go)

  • internal/modules/richmenu/service.go — ValidateStatusTransition / ValidateStatusForAction / ValidateStatusForDelete / GetAvailableStatuses (ทั้งหมดมีผลกับชนิด switch เท่านั้น) และเส้นทางลบ
  • internal/modules/richmenu/update.go — การตรวจกฎสถานะก่อนแก้ไข, การไล่เปลี่ยนสถานะจากเมนูหลัก, การจัดการ alias แบบ update-แล้วถอยไป-create
  • internal/modules/richmenu/create_update.go — generateRichMenuAliasId และลำดับการเรียก LINE ตอนสร้าง
  • internal/modules/richmenu/util.go — สูตรแฮชของ alias และกฎตรวจความถูกต้องของชื่อ alias (ไม่ว่าง, ไม่เกิน 255 ตัว, เฉพาะ a-z A-Z 0-9 - _)
  • internal/modules/linemessageapi/richmenu.go — การแปลงพื้นที่กดชนิดสลับเป็น LINE action richmenuswitch และ metadata ที่แนบไป
  • internal/core/httpx/codes.go — รหัส RMN_003 ที่ประกาศไว้แต่ไม่มีโมดูลใดส่งออกมา

ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ ริชเมนู (Default / Custom / Switch)