Skip to main content

All friends

ภาพรวม​

All friends คือหน้าฐานข้อมูลผู้ใช้ LINE ทั้งหมดของ OA — ใช้ค้นหา/กรองรายชื่อ ดูและแก้ไขข้อมูลรายบุคคล สั่งซิงก์ผู้ติดตามจาก LINE ส่งออกฐานเพื่อนทั้งหมดเป็น CSV และนำเข้ารายชื่อ LINE User ID จากไฟล์ CSV

จุดที่ต้องเข้าใจก่อนคือ หน้านี้ไม่ได้อยู่ในโมดูลเดียวกันทั้งหมด ตารางรายชื่อ/ส่งออก/ประวัติการนำเข้า มาจากโมดูล report ฝั่ง backend ส่วนหน้าต่างรายละเอียด การแก้ไขโปรไฟล์ การซิงก์ และการนำเข้า CSV มาจากโมดูล line-user ซึ่งมีระดับการบังคับสิทธิ์ต่างกันจริง ๆ (ดูหัวข้อ สิทธิ์การเข้าถึง)

บทบาทที่ใช้งาน — ทีมที่ต้องการภาพรวมฐานผู้ติดตาม ตรวจข้อมูลรายคนก่อนทำแคมเปญ หรือแก้ไขข้อมูลลูกค้าให้ตรงกับระบบต้นทาง

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

1. ค้นหาและกรองรายชื่อ​

  1. เปิดเมนู All friends หน้ารายการเริ่มที่แท็บ อยู่ในระบบ เสมอ (แท็บที่เลือกถูกเก็บไว้ใน URL เป็น ?tabs=system / ?tabs=history จึงแชร์ลิงก์หรือกดปุ่มย้อนกลับของเบราว์เซอร์ได้)
  2. กรอกเงื่อนไขในแถบเครื่องมือ — ค้นหา (ชื่อหรือ UID), ประเภท (ผู้เยี่ยมชม/สมาชิก), สถานะ (ใช้งาน/ไม่ใช้งาน) และ ใช้งานล่าสุด (7 วัน / 30 วัน / 90 วัน / 6 เดือน / 1 ปี)
  3. กดปุ่ม ค้นหา — ค่าที่พิมพ์/เลือกไว้ถูกเก็บเป็นสถานะชั่วคราวในหน้าเว็บก่อน ยังไม่ยิง API จนกดค้นหา (ไม่มีการยิงคำขอทุกครั้งที่พิมพ์) เมื่อมีตัวกรองทำงานอยู่จะมีปุ่ม ล้าง (n) บอกจำนวนตัวกรองที่ใช้อยู่
  4. คอลัมน์คุณสมบัติเพิ่มเติมจะโผล่เป็นคอลัมน์ต่อท้ายตารางอัตโนมัติ — เฉพาะแอตทริบิวต์ที่ถูกติ๊กให้แสดงในรายงาน ที่หน้า Settings > User Attribute Setup เท่านั้น (ดึงจาก endpoint GET /attribute-master/reportable แยกจากรายการแอตทริบิวต์ทั้งหมด)

2. ดูและแก้ไขข้อมูลรายบุคคล​

  1. คลิกปุ่มรูปตาท้ายแถวเพื่อเปิดหน้าต่างรายละเอียด
  2. ในหน้าต่างมีสองส่วนที่แก้ไขได้ แยกกันอย่างอิสระ (ปุ่มแก้ไข/บันทึกของใครของมัน)
    • ข้อมูลโปรไฟล์ — ชื่อ, นามสกุล, อีเมล, เบอร์มือถือ (บันทึกด้วย PUT /line-users/{id}/profile)
    • คุณสมบัติเพิ่มเติม — ช่องกรอกเปลี่ยนชนิดตามที่ประกาศไว้ใน Attribute Setup (ข้อความ/ตัวเลข/วันที่/สวิตช์) บันทึกด้วย PUT /line-users/{id}/custom-attribute
  3. LINE User ID และชื่อที่แสดง (display name) แก้ไขไม่ได้โดยเจตนา — UID คือตัวเชื่อมตัวตนกับ LINE และชื่อที่แสดงเป็นค่าที่ระบบซิงก์มาจากโปรไฟล์ LINE
  4. หน้าต่างนี้ยังแสดงกลุ่มเป้าหมายที่เพื่อนคนนี้อยู่ โดยนำ audienceIds ไปเทียบกับรายการจากเมนู Segment audience เพื่อแปลงเป็นชื่อกลุ่มให้อ่านได้
  5. บันทึกแล้วตารางหลักจะรีเฟรชให้ทันที

3. ดูเส้นทางการใช้งานเว็บไซต์ (GA journey)​

  1. เปิดหน้ารายละเอียดแบบเต็ม (/report/all-friends/detail?id=...) — ลิงก์เข้าหน้านี้ส่วนใหญ่มาจากเมนู Segment audience
  2. เลือกช่วงเวลา 7 / 14 / 30 / 90 วันล่าสุด ระบบแสดงไทม์ไลน์เหตุการณ์แยกตามวัน พร้อมไอคอนตามชนิดเหตุการณ์ (page_view, session_start, scroll, click, purchase)
  3. ถ้า OA นี้ยังไม่ได้ตั้งค่าการซิงก์ BigQuery ระบบจะแจ้งตรง ๆ ว่า "ยังไม่ได้ตั้งค่าการซิงก์ BigQuery สำหรับ LINE OA นี้" โดยไม่ถือเป็นข้อผิดพลาด

4. ซิงก์ผู้ติดตามและส่งออกข้อมูล​

  1. กดปุ่ม ซิงก์ ทางขวาของแถบเครื่องมือเพื่อสั่งดึงรายชื่อผู้ติดตามล่าสุดจาก LINE — ข้อความ ซิงค์ล่าสุด: {วันเวลา} ข้างปุ่มบอกรอบล่าสุดที่สำเร็จ
  2. กดปุ่ม ส่งออก เพื่อดาวน์โหลดฐานเพื่อนเป็นไฟล์ CSV (ชื่อไฟล์รูปแบบ report-all-friend DD-MM-YYYY HH-mm-ss.csv)

5. นำเข้ารายชื่อจากไฟล์ CSV​

  1. สลับไปแท็บ ประวัติการนำเข้า กดปุ่ม นำเข้าไฟล์
  2. กรอกชื่องานนำเข้า (title) และคำอธิบาย แล้วลากไฟล์ CSV ที่มีคอลัมน์ Line UserID มาวาง
  3. ระบบตรวจไฟล์ ทั้งหมดในเบราว์เซอร์ก่อนส่ง — ถ้าพบแถวผิดพลาดจะเปิดตารางแจ้งเป็นรายแถวให้แก้ก่อน (แถวที่ว่าง, UID ซ้ำ, UID ผิดรูปแบบ)
  4. ยืนยันนำเข้า ระบบพากลับไปแท็บประวัติเพื่อติดตามสถานะ (ใหม่ / กำลังประมวลผล / ล้มเหลว / เสร็จสมบูรณ์) และดาวน์โหลดไฟล์ผลลัพธ์ได้เมื่อสถานะเป็น "เสร็จสมบูรณ์" แล้วเท่านั้น

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

ยืนยันจาก 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 ตรวจ action VIEW ของ subject report/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 ทั้งหมดฝั่งเบราว์เซอร์และการจัดการรหัสข้อผิดพลาดจาก backend
  • src/services/report.service.ts — endpoint ที่หน้าเว็บเรียก และการประกอบไฟล์ CSV จาก csvData
  • src/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 URL
  • internal/modules/lineuser/controller.go — 8 เส้นทางของ /line-users (ไม่มี ModuleGate)
  • internal/modules/lineuser/repository.go — เงื่อนไขค้นหา/กรอง, การเรียงลำดับ, กฎ limit = 0 คือไม่ใส่ LIMIT
  • internal/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 — กลไกกั้นโมดูลและรายการโมดูลที่ยกเว้น

ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ รายงานเพื่อนทั้งหมด