All friends
ภาพรวม
All friends คือหน้าฐานข้อมูลผู้ใช้ LINE ทั้งหมดของ OA — ใช้ค้นหา/กรองรายชื่อ ดูและแก้ไขข้อมูลรายบุคคล สั่งซิงก์ผู้ติดตามจาก LINE ส่งออกฐานเพื่อนทั้งหมดเป็น CSV และนำเข้ารายชื่อ LINE User ID จากไฟล์ CSV
จุดที่ต้องเข้าใจก่อนคือ หน้านี้ไม่ได้อยู่ในโมดูลเดียวกันทั้งหมด ตารางรายชื่อ/ส่งออก/ประวัติการนำเข้า มาจากโมดูล report ฝั่ง backend ส่วนหน้าต่างรายละเอียด การแก้ไขโปรไฟล์ การซิงก์ และการนำเข้า CSV มาจากโมดูล line-user ซึ่งมีระดับการบังคับสิทธิ์ต่างกันจริง ๆ (ดูหัวข้อ สิทธิ์การเข้าถึง)
บทบาทที่ใช้งาน — ทีมที่ต้องการภาพรวมฐานผู้ติดตาม ตรวจข้อมูลรายคนก่อนทำแคมเปญ หรือแก้ไขข้อมูลลูกค้าให้ตรงกับระบบต้นทาง
เส้นทางผู้ใช้งาน
1. ค้นหาและกรองรายชื่อ
- เปิดเมนู All friends หน้ารายการเริ่มที่แท็บ อยู่ในระบบ เสมอ (แท็บที่เลือกถูกเก็บไว้ใน URL เป็น
?tabs=system/?tabs=historyจึงแชร์ลิงก์หรือกดปุ่มย้อนกลับของเบราว์เซอร์ได้) - กรอกเงื่อนไขในแถบเครื่องมือ — ค้นหา (ชื่อหรือ UID), ประเภท (ผู้เยี่ยมชม/สมาชิก), สถานะ (ใช้งาน/ไม่ใช้งาน) และ ใช้งานล่าสุด (7 วัน / 30 วัน / 90 วัน / 6 เดือน / 1 ปี)
- กดปุ่ม ค้นหา — ค่าที่พิมพ์/เลือกไว้ถูกเก็บเป็นสถานะชั่วคราวในหน้าเว็บก่อน ยังไม่ยิง API จนกดค้นหา (ไม่มีการยิงคำขอทุกครั้งที่พิมพ์) เมื่อมีตัวกรองทำงานอยู่จะมีปุ่ม ล้าง (n) บอกจำนวนตัวกรองที่ใช้อยู่
- คอลัมน์คุณสมบัติเพิ่มเติมจะโผล่เป็นคอลัมน์ต่อท้ายตารางอัตโนมัติ — เฉพาะแอตทริบิวต์ที่ถูกติ๊กให้แสดงในรายงาน ที่หน้า Settings > User Attribute Setup เท่านั้น (ดึงจาก endpoint
GET /attribute-master/reportableแยกจากรายการแอตทริบิวต์ทั้งหมด)
2. ดูและแก้ไขข้อมูลรายบุคคล
- คลิกปุ่มรูปตาท้ายแถวเพื่อเปิดหน้าต่างรายละเอียด
- ในหน้าต่างมีสองส่วนที่แก้ไขได้ แยกกันอย่างอิสระ (ปุ่มแก้ไข/บันทึกของใครของมัน)
- ข้อมูลโปรไฟล์ — ชื่อ, นามสกุล, อีเมล, เบอร์มือถือ (บันทึกด้วย
PUT /line-users/{id}/profile) - คุณสมบัติเพิ่มเติม — ช่องกรอกเปลี่ยนชนิดตามที่ประกาศไว้ใน Attribute Setup (ข้อความ/ตัวเลข/วันที่/สวิตช์) บันทึกด้วย
PUT /line-users/{id}/custom-attribute
- ข้อมูลโปรไฟล์ — ชื่อ, นามสกุล, อีเมล, เบอร์มือถือ (บันทึกด้วย
- LINE User ID และชื่อที่แสดง (display name) แก้ไขไม่ได้โดยเจตนา — UID คือตัวเชื่อมตัวตนกับ LINE และชื่อที่แสดงเป็นค่าที่ระบบซิงก์มาจากโปรไฟล์ LINE
- หน้าต่างนี้ยังแสดงกลุ่มเป้าหมายที่เพื่อนคนนี้อยู่ โดยนำ
audienceIdsไปเทียบกับรายการจากเมนู Segment audience เพื่อแปลงเป็นชื่อกลุ่มให้อ่านได้ - บันทึกแล้วตารางหลักจะรีเฟรชให้ทันที
3. ดูเส้นทางการใช้งานเว็บไซต์ (GA journey)
- เปิดหน้ารายละเอียดแบบเต็ม (
/report/all-friends/detail?id=...) — ลิงก์เข้าหน้านี้ส่วนใหญ่มาจากเมนู Segment audience - เลือกช่วงเวลา 7 / 14 / 30 / 90 วันล่าสุด ระบบแสดงไทม์ไลน์เหตุการณ์แยกตามวัน พร้อมไอคอนตามชนิดเหตุการณ์ (
page_view,session_start,scroll,click,purchase) - ถ้า OA นี้ยังไม่ได้ตั้งค่าการซิงก์ BigQuery ระบบจะแจ้งตรง ๆ ว่า "ยังไม่ได้ตั้งค่าการซิงก์ BigQuery สำหรับ LINE OA นี้" โดยไม่ถือเป็นข้อผิดพลาด
4. ซิงก์ผู้ติดตามและส่งออกข้อมูล
- กดปุ่ม ซิงก์ ทางขวาของแถบเครื่องมือเพื่อสั่งดึงรายชื่อผู้ติดตามล่าสุดจาก LINE — ข้อความ
ซิงค์ล่าสุด: {วันเวลา}ข้างปุ่มบอกรอบล่าสุดที่สำเร็จ - กดปุ่ม ส่งออก เพื่อดาวน์โหลดฐานเพื่อนเป็นไฟล์ CSV (ชื่อไฟล์รูปแบบ
report-all-friend DD-MM-YYYY HH-mm-ss.csv)
5. นำเข้ารายชื่อจากไฟล์ CSV
- สลับไปแท็บ ประวัติการนำเข้า กดปุ่ม นำเข้าไฟล์
- กรอกชื่องานนำเข้า (title) และคำอธิบาย แล้วลากไฟล์ CSV ที่มีคอลัมน์
Line UserIDมาวาง - ระบบตรวจไฟล์ ทั้งหมดในเบราว์เซอร์ก่อนส่ง — ถ้าพบแถวผิดพลาดจะเปิดตารางแจ้งเป็นรายแถวให้แก้ก่อน (แถวที่ว่าง, UID ซ้ำ, UID ผิดรูปแบบ)
- ยืนยันนำเข้า ระบบพากลับไปแท็บประวัติเพื่อติดตามสถานะ (ใหม่ / กำลังประมวลผล / ล้มเหลว / เสร็จสมบูรณ์) และดาวน์โหลดไฟล์ผลลัพธ์ได้เมื่อสถานะเป็น "เสร็จสมบูรณ์" แล้วเท่านั้น
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก internal/modules/report/{controller,service}.go และ internal/modules/lineuser/{controller,service,repository,lineoa,gajourney}.go ของ line-management-cms-api-go:
- ปุ่มส่งออกไม่ได้ส่งออก "ผลที่กรองอยู่" แต่ส่งออกเพื่อนทั้งหมดของ OA — หน้าเว็บเรียก
GET /report/all-friend/exportโดยไม่ส่ง query parameter ใดเลย ค่าlimitจึงตกเป็นค่าเริ่มต้น 0 ซึ่งฝั่ง backend แปลว่า "ไม่ใส่ LIMIT" ผลคือได้ทุกแถวของ OA ไม่ว่าจะตั้งตัวกรองอะไรค้างอยู่บนหน้าจอก็ตาม - ไฟล์ CSV ที่ส่งออกเดินทางมาเป็นสตริงก้อนเดียวใน JSON — endpoint ตอบเป็นออบเจ็กต์
{fileName, csvData}(ไม่ใช่ไฟล์แนบ) แล้วหน้าเว็บเป็นฝ่ายประกอบเป็นไฟล์ให้ดาวน์โหลดเอง — ยิ่งฐานเพื่อนใหญ่ ก้อน JSON นี้ยิ่งใหญ่ตามและถูกโหลดเข้าหน่วยความจำเบราว์เซอร์ทั้งก้อน - คอลัมน์ในไฟล์ส่งออกออกแบบไม่ให้ข้อมูลหลุดหาย — เริ่มจาก
Line UserID, Display Name, Follow, Status, Created Dateแล้วต่อด้วยหนึ่งคอลัมน์ต่อหนึ่งแอตทริบิวต์ที่ประกาศไว้ใน attribute master ที่สถานะ active (ไล่ลูกหลานในโครงสร้างด้วย) และปิดท้ายด้วยคีย์ที่พบในcustom_attributeของเพื่อนจริงแต่ไม่มีในนิยามใด ๆ (เรียงตามตัวอักษร) — ค่าที่เคยเขียนไว้จากแหล่งอื่นจึงไม่หายไปจากไฟล์แม้จะถูกลบนิยามไปแล้ว - ตัวกรอง "สถานะ" กับคอลัมน์ "สถานะ" ในตารางเป็นข้อมูลสองคนละคอลัมน์กัน — ตัวกรองส่งค่า
statusซึ่ง backend ใช้กรองคอลัมน์statusของตารางline_user(สถานะเรกคอร์ด) แต่ป้ายสถานะที่ตารางแสดงคือlastActivityStatus(คำนวณจากการใช้งานล่าสุด) — กรอง "ใช้งาน" แล้วเห็นแถวที่ป้ายเป็น "ไม่ใช้งาน" จึงเป็นพฤติกรรมตามที่โค้ดเขียนไว้ ไม่ใช่ข้อมูลเพี้ยน - ตัวกรอง "ใช้งานล่าสุด" ทำงานกับ
last_activity_dateและต้องมีทั้งวันเริ่มและวันจบ — หน้าเว็บแปลงตัวเลือก 7/30/90/180/365 วันเป็นคู่startDate+endDateให้อัตโนมัติ ฝั่ง backend ใช้เงื่อนไขBETWEENโดยขยายเป็นต้นวัน–ท้ายวัน ถ้าส่งมาแค่ข้างเดียวเงื่อนไขจะถูกข้ามทั้งอัน - การเรียงลำดับตารางถูกตรึงไว้ที่
created_date DESCเสมอ — ค่าorderBy/orderที่หน้าเว็บเก็บไว้ในสถานะไม่เคยถูกส่งไปกับคำขอ และ backend ไม่มีทางเลือกการเรียงอื่นให้เลย - การค้นหาไล่สามเงื่อนไข —
display_name ILIKE,user_id ILIKEและชื่อ+นามสกุลต่อกัน (CONCAT(firstname, ' ', lastname)) โดยเงื่อนไขที่สามถูกประกอบด้วยการแทรกข้อความค้นหาลงใน SQL ตรง ๆ (คงไว้ตามต้นฉบับเพื่อความเข้ากันได้ของพฤติกรรม) — อักขระที่มีความหมายพิเศษใน SQL/LIKE เช่น%_'จึงให้ผลไม่ตรงสัญชาตญาณเมื่อค้นหาด้วยชื่อ-นามสกุล - การซิงก์ผู้ติดตามเป็นคำขอแบบ "รอผลได้ประมาณ 30 วินาที" — backend ตรวจล็อกก่อน (Redis + คอลัมน์สถานะใน DB) ถ้ามีรอบที่กำลังทำงานอยู่ตอบ 409 ทันที; ถ้าผ่าน จะลองเรียก API ผู้ติดตามของ LINE ด้วย
limit=1เพื่อทดสอบสิทธิ์ — ล้มเหลวด้วยเหตุใดก็ตามในขั้นนี้จะถูกตีความเป็น 403 "ต้องเป็น OA ที่ยืนยันตัวตนหรือระดับพรีเมียม"; หลังจากส่งงานเข้าคิวแล้ว handler จะวนรอผลใน Redis ทุก 3 วินาที ไม่เกิน 10 รอบ แล้วตอบ 408 พร้อมข้อความว่าเริ่มซิงก์แล้วแต่ยังไม่เสร็จ — หน้าเว็บแสดง 403/408/409 เป็นข้อความเตือน (warning) พร้อมข้อความจากเซิร์ฟเวอร์ ไม่ใช่ข้อความ error - สถานะ "ซิงก์ล่าสุด" บนหน้าจอมาจากฐานข้อมูล ไม่ใช่จากล็อกสด — endpoint ที่หน้าเว็บถามสถานะอ่าน Redis hash ด้วยคีย์ที่ขึ้นต้นด้วย
LINE_USER:แต่ขั้นตอนซิงก์เขียนล็อกไว้ใต้LINE_OA:(คงไว้ตามต้นฉบับ) ผลคือการอ่านนั้นแทบไม่เคยเจอค่า และคำตอบมาจากคอลัมน์sync_follower_statusของตารางline_oaเสมอ - การนำเข้า CSV ถูกตรวจสองชั้นด้วยกฎที่ "ไม่เหมือนกัน" — ฝั่งหน้าเว็บบังคับให้หัวคอลัมน์สะกดตรงเป๊ะว่า
Line UserID(เทียบแบบ case-sensitive) และบังคับให้ทุก UID ตรงรูปแบบ^U[a-f0-9]{32}$ส่วนฝั่ง backend รับหัวคอลัมน์แบบไม่สนตัวพิมพ์ (line useridหรือline_userid) และไม่ตรวจรูปแบบ UID ที่ขั้นนี้เลย — เพดาน 10,000 แถวถูกตรวจทั้งสองฝั่ง - backend ปฏิเสธตัวคั่นที่ไม่ใช่จุลภาคด้วยการดูแค่บรรทัดแรก — ถ้าบรรทัดแรกมีแท็บ, อัฒภาค (
;) หรือ pipe (|) จะตอบ 400 "Only comma (,) is allowed as a delimiter" ทันที (ตรวจตามลำดับนี้) — หมายความว่าหัวคอลัมน์ที่มีอัฒภาคอยู่ในชื่อก็ทำให้ไฟล์ถูกปฏิเสธได้ - มีล็อก "นำเข้าได้ทีละงาน" ต่อ LINE OA อายุ 1 ชั่วโมง — เมื่อสร้างงานนำเข้าสำเร็จ ระบบตั้งคีย์ Redis กันงานซ้อน ถ้ามีอีกคนสั่งนำเข้าระหว่างนั้นจะได้ 409 รหัส
LUS_004("มีการนำเข้าอื่นกำลังทำงานอยู่") — ล็อกนี้หมดอายุเองใน 1 ชั่วโมงเพื่อไม่ให้ค้างตลอดไปหากงานเบื้องหลังล้ม - ชื่องานนำเข้าต้องไม่ซ้ำ แต่ตรวจโดยไม่ดูองค์กร — เงื่อนไขตรวจซ้ำใช้
title+line_oa_idเท่านั้น (ไม่มีorganization_id) ตามต้นฉบับ; เมื่อซ้ำจะตอบ 400 รหัสAPP_CUSTOMพร้อมรายละเอียดชี้ไปที่ช่องtitleให้หน้าเว็บขึ้น error ใต้ช่องนั้นได้ตรงจุด - แถวงานนำเข้าถูกสร้างด้วย
total_records = 0และสถานะnew— จำนวนแถวจริงและไฟล์ผลลัพธ์เป็นหน้าที่ของ worker ที่รับงานจากคิวไปเติมภายหลัง จึงเป็นเรื่องปกติที่งานที่เพิ่งสั่งจะยังไม่มีตัวเลขให้ดู - ลิงก์ไฟล์ในประวัติการนำเข้าเป็น presigned URL ที่สร้างใหม่ทุกครั้งที่โหลดรายการ — ทั้งไฟล์ต้นฉบับและไฟล์ผลลัพธ์ ลิงก์ที่คัดลอกเก็บไว้จึงหมดอายุ ต้องกดจากหน้ารายการที่เพิ่งโหลด
- ช่อง
page/limit/searchของประวัติการนำเข้าไม่ผ่านการตรวจสอบชนิดข้อมูลเลย — DTO ของ endpoint นี้เป็นแค่ interface ไม่ใช่คลาสที่มี validator จึงรับค่าดิบเป็นสตริงแล้วไปแปลงเป็นตัวเลขตอนคำนวณ offset (ต่างจาก endpoint รายชื่อเพื่อนที่ตรวจชนิดข้อมูลจริง) - การบันทึกคุณสมบัติเพิ่มเติมเป็นการ "รวมค่า" ไม่ใช่ "แทนที่" — หน้าเว็บส่งออบเจ็กต์เดิมทั้งก้อนแล้วเขียนทับเฉพาะคีย์ที่แก้ ทำให้คีย์เดิมไม่ถูกลบทิ้ง; คุณสมบัติชนิด object และ array แสดงเป็นข้อมูลอ่านเท่านั้น (ไม่มีช่องกรอก) และคีย์ที่มีอยู่ในข้อมูลจริงแต่ไม่มีนิยามใน Attribute Setup จะถูกแสดงต่อท้ายให้เห็นแต่แก้ไม่ได้
- endpoint บันทึกคุณสมบัติเพิ่มเติมตอบ 200 พร้อม body ว่าง — ต้นฉบับเรียกฟังก์ชันจัดรูปคำตอบโดยไม่ใส่อาร์กิวเมนต์ ผลลัพธ์จึงเป็น undefined และถูกส่งออกไปเป็น body เปล่า (คงไว้ตามต้นฉบับ) — หน้าเว็บจึงต้องดึงข้อมูลใหม่เองหลังบันทึก ไม่ได้อ่านค่าที่อัปเดตจากคำตอบ
- การแก้ไขโปรไฟล์มีกฎตรวจที่ backend ครบชุด — ชื่อ/นามสกุลไม่เกิน 100 ตัวอักษร, อีเมลไม่เกิน 254 ตัวอักษรและต้องตรงรูปแบบ, เบอร์มือถือต้องเป็นตัวเลข 9–10 หลักหลังตัดช่องว่างและขีดออก และถูกบันทึกเป็นตัวเลขล้วนเสมอ เพื่อให้การค้นหา/ส่งออกอยู่ในรูปแบบเดียวกัน; ส่งช่องว่างมาหมายถึง "ล้างค่า" (เขียน NULL) และถ้าไม่ส่งช่องใดมาเลยจะได้ 400 "Nothing to update"
- เส้นทาง GA ถูกแคชไว้ 5 นาทีด้วยคีย์ที่ไม่มีช่วงวันอยู่ในนั้น — คีย์แคชคือ
GA_JOURNEY:{id}เท่านั้น ดังนั้นถ้าเปลี่ยนตัวเลือกช่วงเวลาจาก 30 วันเป็น 90 วันภายใน 5 นาที จะยังได้ผลของช่วงเดิมกลับมา — ต้องรอให้แคชหมดอายุก่อนจึงจะเห็นช่วงใหม่จริง - เส้นทาง GA รวมพฤติกรรมก่อนรู้ตัวตนเข้ามาด้วยโดยเจตนา — คำสั่งค้นหาใน BigQuery หา
user_pseudo_idทุกตัวที่เคยผูกกับ LINE UID นี้ก่อน แล้วจึงดึงทุกเหตุการณ์ของ pseudo id เหล่านั้น จำกัด 1,000 เหตุการณ์ล่าสุดตามลำดับเวลา — ค่าidentityMatchDateคือเวลาของเหตุการณ์แรกที่มีuser_idติดมา ซึ่งเป็นจุดแบ่งระหว่างช่วง "ยังไม่รู้ว่าเป็นใคร" กับ "รู้แล้ว" - BigQuery ที่ตั้งค่าไม่ครบไม่ทำให้หน้าเสีย — ถ้าไม่มีคอนฟิกที่เปิดใช้งาน หรือ credentials อ่านไม่ออก หรือคำสั่งค้นหาล้มเหลว ระบบตอบกลับเป็นโครงสร้างว่าง (
totalEvents: 0, ไม่มี session) พร้อมข้อความอธิบายสาเหตุ แทนที่จะโยน error ออกมา
เมนูที่เกี่ยวข้องกันในเส้นทางนี้
- Segment audience — ใช้ฐานเพื่อนจากเมนูนี้เป็นวัตถุดิบสร้างกลุ่มเป้าหมาย และเป็นต้นทางลิงก์เข้าหน้ารายละเอียดแบบเต็ม (หน้าที่มีไทม์ไลน์ GA) ในทางกลับกัน หน้าต่างรายละเอียดของ All friends ก็เรียกรายการกลุ่มเป้าหมายจากเมนูนั้นมาแปลง
audienceIdsให้เป็นชื่อกลุ่มที่อ่านได้ - Settings > User Attribute Setup — เป็นที่ประกาศคุณสมบัติเพิ่มเติมทั้งหมด และเป็นตัวกำหนดสองอย่างพร้อมกัน: (1) คอลัมน์พิเศษที่จะโผล่ในตาราง All friends (เฉพาะแอตทริบิวต์ที่ติ๊กให้แสดงในรายงาน) และ (2) ช่องกรอกที่แก้ไขได้ในหน้าต่างรายละเอียด (ยกเว้นชนิด object/array ที่อ่านได้เท่านั้น)
- Import Data — คนละกลไกกับการนำเข้า CSV ในแท็บนี้ การนำเข้าที่ All friends คือการส่งรายชื่อ LINE User ID ทั้งไฟล์เข้าคิวให้ worker ไปสร้าง/อ้างอิงเป็นเรกคอร์ดเพื่อน โดยบังคับหัวคอลัมน์ชื่อ
Line UserIDเพียงคอลัมน์เดียวและไม่มีการจับคู่ฟิลด์ใด ๆ ส่วน Import Data เป็นการ อัปเดตค่าลงฟิลด์ของเพื่อนที่มีอยู่แล้ว ผ่านการเลือก match key และจับคู่คอลัมน์เอง - BigQuery Sync (ตั้งค่าระดับ OA) — ต้องมีคอนฟิกที่เปิดใช้งานอยู่ก่อน ไทม์ไลน์ GA journey ในหน้ารายละเอียดจึงจะมีข้อมูล
- Dashboard — ตัวเลขภาพรวมผู้ติดตามใช้ฐานข้อมูลชุดเดียวกันนี้
จุดที่ควรระวัง
- ปุ่มส่งออก = ส่งออกทั้ง OA ไม่ใช่ผลที่กรองไว้ ถ้าต้องการเฉพาะกลุ่ม ให้ใช้เมนู Segment audience สร้างกลุ่มแล้วส่งออกจากที่นั่นแทน
- การกดซิงก์สำเร็จหมายถึง "ส่งงานเข้าคิวแล้ว" ไม่ใช่ "ข้อมูลอัปเดตครบแล้ว" — คำขอนี้รออยู่แค่ประมาณ 30 วินาที ถ้าเกินกว่านั้นจะได้ข้อความเตือนว่ากำลังทำงานอยู่ ให้รอแล้วรีเฟรชหน้าใหม่ ไม่ต้องกดซ้ำ (กดซ้ำระหว่างนั้นจะได้ 409)
- ถ้า OA ไม่ใช่บัญชีที่ยืนยันตัวตน/พรีเมียม การซิงก์จะไม่ทำงานเลยและได้ 403 ทุกครั้ง — เป็นข้อจำกัดของ LINE เอง ไม่ใช่การตั้งค่าสิทธิ์ใน CMS
- หลังสั่งนำเข้าไฟล์สำเร็จ จะนำเข้าไฟล์ถัดไปได้เมื่องานเดิมเสร็จ (หรือรอให้ล็อกหมดอายุ 1 ชั่วโมง) — งานที่ค้างสถานะ "กำลังประมวลผล" นานผิดปกติควรตรวจฝั่ง worker
- หัวคอลัมน์ในไฟล์นำเข้าต้องสะกดว่า
Line UserIDตรงตัวพิมพ์ให้ตรง เพราะการตรวจฝั่งหน้าเว็บเข้มกว่า backend — ไฟล์ที่ backend ยอมรับอาจถูกหน้าเว็บปฏิเสธก่อนถึงเซิร์ฟเวอร์ - ปุ่มดาวน์โหลดไฟล์ผลลัพธ์ในประวัติการนำเข้าจะกดได้เมื่อสถานะเป็น "เสร็จสมบูรณ์" เท่านั้น และลิงก์เป็น presigned URL ที่หมดอายุ — เก็บลิงก์ไว้ใช้ภายหลังไม่ได้
- เปลี่ยนช่วงวันของไทม์ไลน์ GA แล้วตัวเลขไม่ขยับ ให้รอ 5 นาทีให้แคชหมดอายุก่อน (แคชไม่ได้แยกตามช่วงวัน)
- ป้าย "สถานะ" ในตารางกับตัวกรอง "สถานะ" ไม่ใช่ข้อมูลชุดเดียวกัน (ดูหัวข้อรายละเอียดข้างต้น) — อย่าใช้ตัวกรองนี้เพื่อยืนยันจำนวนคนที่ป้ายขึ้น "ใช้งาน"
สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)
- ฝั่งหน้าเว็บ — หน้ารายการ (
/report/all-friends) และหน้ารายละเอียดแบบเต็ม (/report/all-friends/detail) ถูกครอบด้วยPermissionGuardตรวจ actionVIEWของ subjectreport/all-friends - หน้าฟอร์มนำเข้า CSV (
/report/all-friends/form) ไม่ได้ถูกครอบด้วยPermissionGuardเลย — ต่างจากอีกสองหน้าในกลุ่มเดียวกัน ผู้ที่ล็อกอินอยู่แล้วจึงเปิดหน้านี้ได้ตรงจาก URL แม้ไม่มีสิทธิ์ดูรายการเพื่อน - ฝั่งเซิร์ฟเวอร์ — โมดูล
reportถูกกั้นจริง ทั้งสามเส้นทาง (GET /report/all-friend,GET /report/all-friend/export,GET /report/import-history) ถูกครอบด้วยModuleGate(d, "report-all-friends")ที่ระดับ route group ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลนี้ให้องค์กรใด ผู้ใช้ประเภท customer ในองค์กรนั้นจะได้ 403 ทันที (ผู้ใช้ประเภท onemoby ข้ามการกั้นนี้เสมอ) - ฝั่งเซิร์ฟเวอร์ — โมดูล
line-userตั้งใจไม่กั้น เส้นทางที่หน้าต่างรายละเอียด/แก้ไขโปรไฟล์/แก้ไขคุณสมบัติ/ซิงก์/นำเข้า CSV เรียกใช้ (/line-users/*) อยู่ในโมดูลline-userซึ่งถูกขึ้นทะเบียนไว้ในexemptModulesของ modulegate อย่างชัดเจนว่า "ต้องไม่ถูกกั้นเด็ดขาด" ด้วยเหตุผลว่าเรกคอร์ดผู้ใช้ LINE เป็นรากฐานของ audience/แชท/รายงานทั้งหมด — ผลในทางปฏิบัติคือ ปิดโมดูล report-all-friends แล้วตารางรายชื่อจะ 403 แต่ endpoint ดูรายละเอียด แก้ไขโปรไฟล์ ซิงก์ และนำเข้า CSV ยังเรียกได้อยู่ การควบคุมส่วนนั้นอยู่ที่บทบาท/สิทธิ์เท่านั้น - ModuleGate ออกแบบให้ fail open — ถ้าระบุตัวผู้เรียกไม่ได้ ไม่พบองค์กร หรือการค้นค่าคอนฟิกล้มเหลว จะปล่อยให้ผ่าน (พร้อมบันทึก log) เพื่อไม่ให้ปัญหาชั่วคราวกลายเป็นการล็อกทั้งโมดูล
- policy annotation ระดับการกระทำ (
readAll/read/create/update/export) มีประกาศครบทุกเส้นทางแต่วิ่งผ่าน middleware ที่เป็น no-op เหมือนโมดูลอื่นในระบบ — การบังคับจริงจึงอยู่ที่ ModuleGate ระดับโมดูล ไม่ใช่ที่ policy ระดับการกระทำ - มีชุดทดสอบเชิงกำกับดูแล (
internal/modules/modulegate/coverage_test.go) ที่สแกนซอร์สทั้งรีโปเพื่อยืนยันว่าโมดูลใดถูกกั้นจริงและโมดูลใดอยู่ในรายการยกเว้น — การเพิ่ม/ถอด ModuleGate โดยไม่อัปเดตรายการนี้จะทำให้ build ไม่ผ่าน
ไฟล์อ้างอิงจากซอร์สโค้ด
Frontend (cms-web-2026)
src/app/report/all-friends/page.tsx,detail/page.tsx,form/page.tsx— จุดเข้าหน้ารายการ / รายละเอียดเต็ม / ฟอร์มนำเข้า (สังเกตว่าไฟล์ที่สามไม่มีPermissionGuard)src/components/report/report-all-friends-table.container.tsx— แท็บ, ตัวกรอง, คอลัมน์ไดนามิกจากแอตทริบิวต์, ปุ่มซิงก์/ส่งออกsrc/components/report/report-all-friends-table.tsx— แถบเครื่องมือและตัวเลือกตัวกรองทั้งหมดsrc/components/report/all-friends-detail-modal.tsx— สองโหมดแก้ไขที่แยกกัน, การแปลงaudienceIds, คุณสมบัติที่แก้ไม่ได้src/components/report/user-ga-journey.tsx— ไทม์ไลน์และตัวเลือกช่วงวันsrc/components/report/csv-form.container.tsx— การตรวจไฟล์ CSV ทั้งหมดฝั่งเบราว์เซอร์และการจัดการรหัสข้อผิดพลาดจาก backendsrc/services/report.service.ts— endpoint ที่หน้าเว็บเรียก และการประกอบไฟล์ CSV จากcsvDatasrc/utils/line.util.ts— กฎรูปแบบ LINE User ID ที่ใช้ตรวจฝั่งหน้าเว็บ
Backend (line-management-cms-api-go)
internal/modules/report/controller.go— 3 เส้นทาง +ModuleGate("report-all-friends")internal/modules/report/service.go— การประกอบไฟล์ CSV, คอลัมน์แอตทริบิวต์, ประวัติการนำเข้าและ presigned URLinternal/modules/lineuser/controller.go— 8 เส้นทางของ/line-users(ไม่มี ModuleGate)internal/modules/lineuser/repository.go— เงื่อนไขค้นหา/กรอง, การเรียงลำดับ, กฎ limit = 0 คือไม่ใส่ LIMITinternal/modules/lineuser/service.go— กฎตรวจโปรไฟล์, ขั้นตอนนำเข้า CSV, ล็อกกันนำเข้าซ้อนinternal/modules/lineuser/lineoa.go— ขั้นตอนซิงก์ผู้ติดตาม, การตรวจ OA พรีเมียม, การวนรอผลinternal/modules/lineuser/gajourney.go— คำสั่ง BigQuery, การแคช 5 นาที, การจับคู่ตัวตนinternal/modules/modulegate/modulegate.go,coverage_test.go— กลไกกั้นโมดูลและรายการโมดูลที่ยกเว้น
ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ รายงานเพื่อนทั้งหมด