Skip to main content

Import Data

ภาพรวม​

Import Data (ฝั่งระบบเรียก Import Mapping) คือเมนูนำเข้าไฟล์ CSV เพื่อ อัปเดตข้อมูลโปรไฟล์ของเพื่อน (LINE friend) ที่มีอยู่แล้ว เป็นแบบ update-only เท่านั้น — ไม่สร้างผู้ใช้ใหม่จากไฟล์ และไม่ลบใครออก

หัวใจของเมนูนี้คือ match key — ต้องบอกระบบว่าจะใช้ฟิลด์ใดในโปรไฟล์เพื่อน (LINE UID / เบอร์มือถือ / อีเมล / custom attribute) จับคู่กับคอลัมน์ใดใน CSV เพื่อระบุว่าแต่ละแถวหมายถึงเพื่อนคนไหน แถวที่จับคู่ไม่ได้จะถูกข้ามไป ไม่ถูกสร้างเป็นเพื่อนใหม่

งานนำเข้าไม่ได้ทำในคำขอเดียว — cms-api แค่บันทึกงานเป็นสถานะ pending แล้วส่งเข้าคิว RabbitMQ ให้ line-management-worker-go เป็นผู้อัปเดตฐานข้อมูลจริง

บทบาทที่ใช้งาน — ผู้ดูแล CMS ที่ถือข้อมูลลูกค้าจากระบบภายนอกแล้วต้อง sync ค่าลงมาที่โปรไฟล์เพื่อนหรือ custom attribute จำนวนมากพร้อมกัน

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

1. ดูรายการงานนำเข้า​

เปิดเมนู Import Data ตารางแสดง ไฟล์, สถานะ, จำนวนข้อมูล, สร้างเมื่อ, บันทึกการนำเข้า เรียงจากใหม่ไปเก่า

  • สถานะมี 5 แบบ — รอดำเนินการ / กำลังดำเนินการ / เสร็จสิ้น / เสร็จสิ้นโดยมีข้อผิดพลาด / ล้มเหลว
  • คอลัมน์จำนวนข้อมูลเปลี่ยนความหมายตามสถานะ: ระหว่างรอ/กำลังทำจะแสดง {n} แถว จากผลตรวจที่บันทึกไว้ตอนสั่งงาน แต่เมื่อเสร็จแล้วจะเปลี่ยนเป็น อัปเดต {x} · ข้าม {y} จากผลจริงที่ worker เขียนกลับมา
  • ถ้ามีงานที่สถานะยังเป็นรอ/กำลังทำอยู่ หน้าเว็บจะรีเฟรชรายการให้เองทุก 10 วินาที และหยุดรีเฟรชเมื่อทุกงานจบแล้ว
  • คอลัมน์บันทึกการนำเข้าจะมีลิงก์ดาวน์โหลดเฉพาะงานที่มีแถวถูกข้ามจริง

2. เข้าสู่ wizard นำเข้า 4 ขั้นตอน​

กดปุ่ม นำเข้าไฟล์ CSV

ขั้นที่ 1 — อัปโหลดไฟล์​

ลากไฟล์ CSV มาวาง (ไม่เกิน 20 MB) หน้าเว็บตรวจขนาดก่อนส่ง แล้วอัปโหลดขึ้น storage ระบบอ่านคอลัมน์และตัวอย่าง 10 แถวแรกกลับมาให้ พร้อมจำนวนแถวทั้งหมด

ขั้นที่ 2 — ดูตัวอย่างข้อมูล​

ตารางแสดง 10 แถวแรกพร้อมข้อความ แสดง {n} แถวแรกจากทั้งหมด {m} แถว ให้ตรวจว่าไฟล์ถูกอ่านตรงตามที่คาด (โดยเฉพาะคอลัมน์ที่อาจเลื่อนเพราะเครื่องหมายคำพูดหรือจุลภาคในข้อมูล) ก่อนไปตั้งค่าการจับคู่

ขั้นที่ 3 — จับคู่ฟิลด์​

  1. เลือก match key — ซ้ายคือฟิลด์ในโปรไฟล์เพื่อน (LINE User ID / เบอร์มือถือ / อีเมล / custom attribute ที่ประกาศไว้ในหน้า Attribute Setup) ขวาคือคอลัมน์ใน CSV
  2. จับคู่คอลัมน์ที่เหลือกับฟิลด์ปลายทาง — ตัวเลือกถูกแบ่งเป็นสองกลุ่ม (ฟิลด์โปรไฟล์ / แอตทริบิวต์ที่กำหนดเอง) แต่ละคอลัมน์เลือก "ไม่นำเข้า" ได้ถ้าไม่ต้องการ
  3. คอลัมน์ที่เลือกเป็น match key จะถูกถอดออกจากรายการจับคู่โดยอัตโนมัติ และแสดงแท็ก "คีย์ที่ใช้จับคู่ — ใช้เพื่อค้นหาเพื่อน" แทนช่องเลือก
  4. ฟิลด์ปลายทางหนึ่งฟิลด์ใช้ได้ครั้งเดียว — เมื่อคอลัมน์หนึ่งจับคู่กับฟิลด์ใดไปแล้ว ฟิลด์นั้นจะถูกปิดในรายการของคอลัมน์อื่น
  5. ปุ่มถัดไปเปิดใช้งานเมื่อครบทั้งสามอย่าง: เลือกฟิลด์ match key แล้ว, เลือกคอลัมน์ match key แล้ว และจับคู่คอลัมน์อื่นไว้อย่างน้อยหนึ่งคอลัมน์

ขั้นที่ 4 — ตรวจทานและสั่งนำเข้า​

  1. กดถัดไป ระบบเรียกการตรวจแบบ dry-run กับไฟล์จริง แล้วแสดงการ์ดสรุป 4 ใบ — จำนวนแถวทั้งหมด, จะอัปเดต, ไม่พบคู่ที่ตรงกัน — ข้าม, ข้อมูลไม่ถูกต้อง — ข้าม
  2. ถ้ามีแถวที่จะถูกข้าม จะมีกล่องเตือนบอกจำนวน และตาราง สาเหตุที่ข้าม พร้อมจำนวนแถวต่อสาเหตุ
  3. กด เริ่มนำเข้า ระบบสร้างงานสถานะ pending ส่งเข้าคิว แล้วพากลับหน้ารายการให้ติดตามสถานะ

3. ตรวจผลและแก้แถวที่ถูกข้าม​

  1. รอให้สถานะเปลี่ยนเป็น เสร็จสิ้น หรือ เสร็จสิ้นโดยมีข้อผิดพลาด (หน้ารีเฟรชให้เองทุก 10 วินาที)
  2. ถ้ามีแถวถูกข้าม กด ดาวน์โหลดบันทึก เพื่อได้ไฟล์ CSV ที่มีหัวคอลัมน์เดิมทั้งชุด บวกคอลัมน์ Error Reason ต่อท้าย และมีเฉพาะแถวที่ถูกข้าม พร้อมเหตุผลทุกข้อของแถวนั้น
  3. แก้ไฟล์ตามเหตุผล แล้วนำเข้าเฉพาะไฟล์ที่แก้แล้ว — การนำเข้าซ้ำด้วยไฟล์เดิมทั้งไฟล์จะเขียนค่าเดิมทับอีกครั้ง ไม่มีการตรวจว่าเคยนำเข้าแล้ว

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

ยืนยันจาก internal/modules/importmapping/{controller,service,dto}.go ของ line-management-cms-api-go และ internal/importmapping/{service,types}.go ของ line-management-worker-go:

  • LINE User ID ใช้เป็น match key ได้ แต่เขียนทับไม่ได้เด็ดขาด — ฝั่ง worker แยกรายการคอลัมน์ออกเป็นสองชุด: ชุดที่อนุญาตให้ใช้เป็นคีย์ (มี user_id อยู่ด้วย) และชุดที่อนุญาตให้เขียนค่าลงไป (ไม่มี user_id โดยเจตนา พร้อมคอมเมนต์ระบุว่า UID คือตัวเชื่อมตัวตนกับ LINE จึงเป็นคีย์อ่านอย่างเดียว) ในทางกลับกัน ชื่อที่แสดง (display name) เป็นฟิลด์ปลายทางได้แต่ใช้เป็น match key ไม่ได้ — รายการตัวเลือกทั้งสองฝั่งจึงไม่เหมือนกัน
  • ตัวเลขในขั้นที่ 4 กับผลจริงอาจไม่ตรงกัน เพราะ "การตรวจ" และ "การทำงานจริง" ใช้เกณฑ์ต่างกันในสองจุด นี่คือรายละเอียดที่กระทบผู้ใช้มากที่สุดของเมนูนี้:
    • การจับคู่เบอร์มือถือ — ฝั่ง worker ตัดช่องว่างและขีดออกจากทั้งค่าใน CSV และค่าในฐานข้อมูล ก่อนเทียบ ("081-234-5678" จึงจับคู่กับ "0812345678" ได้) แต่ขั้นตรวจของ cms-api เทียบสตริงกันตรง ๆ หลัง trim อย่างเดียว — แถวที่ใช้เบอร์เป็น match key อาจถูกรายงานว่า "ไม่พบคู่ที่ตรงกัน" ในขั้นที่ 4 แต่ถูกอัปเดตสำเร็จจริงตอน worker ทำงาน
    • การตรวจรูปแบบอีเมล — cms-api ใช้ regex ที่เข้มกว่า ส่วน worker ใช้เกณฑ์หลวมกว่า (แค่มี @ และมีจุดในโดเมน) — บางแถวจึงถูกนับเป็น "ข้อมูลไม่ถูกต้อง" ในขั้นตรวจแต่ผ่านและถูกอัปเดตจริง
    • ผลตรวจจึงควรอ่านเป็น การประมาณแบบระวังไว้ก่อน (conservative) ไม่ใช่คำทำนายที่แม่นยำ
  • ตัวเลขในตาราง "สาเหตุที่ข้าม" รวมกันได้มากกว่าจำนวนแถวที่ถูกข้าม — โดยออกแบบมาอย่างนั้น: แถวหนึ่งที่ทั้งอีเมลผิดรูปแบบและหาเพื่อนไม่เจอ จะถูกนับในทั้งสองสาเหตุ ขณะที่การ์ด "ไม่พบคู่ที่ตรงกัน" กับ "ข้อมูลไม่ถูกต้อง" ถูกทำให้ไม่ทับกันเพื่อให้การ์ดสี่ใบบวกกันได้เท่ากับจำนวนแถวทั้งหมดพอดี
  • ลำดับความสำคัญของการจัดกลุ่มถูกกำหนดไว้ชัด — แถวที่หาเพื่อนไม่เจอถูกนับเป็น "ไม่พบคู่ที่ตรงกัน" เสมอ แม้ข้อมูลในแถวจะผิดรูปแบบด้วย (เพราะแก้ข้อมูลอย่างไรก็ยังนำเข้าไม่ได้ถ้าไม่มีเพื่อนคนนั้น) ส่วน "ข้อมูลไม่ถูกต้อง" สงวนไว้ให้แถวที่จับคู่ได้ (หรือจับคู่ไม่ได้เลยเพราะคีย์ว่าง) แต่ข้อมูลเสีย
  • การตรวจใช้การอ่านฐานข้อมูลครั้งเดียวสำหรับทั้งไฟล์ — cms-api ดึงค่าคีย์ที่ไม่ว่างของเพื่อนทั้ง OA มาทำเป็นชุดค้นหาในหน่วยความจำก่อน แล้วจึงไล่ตรวจทุกแถว ไม่มีการยิง query ต่อแถว — ไฟล์หมื่นแถวจึงตรวจเสร็จเร็ว แต่ก็หมายความว่าผลตรวจเป็นภาพนิ่งของฐานเพื่อน ณ วินาทีนั้น
  • เซลล์ว่างไม่เคยลบค่าเดิม — worker ข้ามเซลล์ที่ว่าง (หลัง trim) ไม่นำไปเขียนทับ จึงไม่มีทางที่การนำเข้าไฟล์ที่มีช่องว่างจะไปล้างข้อมูลเดิมของเพื่อนออก — ถ้าต้องการล้างค่าใดจริง ๆ ต้องไปแก้ทีละคนที่หน้า All friends
  • custom attribute ถูก "รวม" ไม่ใช่ "แทนที่" — worker เขียนด้วยตัวดำเนินการรวม jsonb (custom_attribute = COALESCE(custom_attribute,'{}') || {ค่าใหม่}) คีย์ที่ไม่ได้อยู่ในไฟล์จึงคงอยู่เหมือนเดิม
  • คีย์ที่ซ้ำกันในฐานข้อมูล "ตัวแรกชนะ" แบบไม่กำหนดผู้ชนะแน่นอน — ตอนสร้างชุดค้นหา worker เก็บเฉพาะแถวแรกที่พบของแต่ละค่าคีย์ และ query ไม่มี ORDER BY — ถ้ามีเพื่อนสองคนใช้อีเมลเดียวกัน ไม่มีการรับประกันว่าคนไหนจะถูกอัปเดต ควรเลือก match key ที่ไม่ซ้ำ (LINE UID ปลอดภัยที่สุด)
  • คีย์ที่ซ้ำกันในไฟล์ CSV "แถวหลังชนะ" — worker ไล่ทำทีละแถวตามลำดับในไฟล์ ทุกแถวที่จับคู่กับเพื่อนคนเดียวกันจะเขียนทับกันไปเรื่อย ๆ ค่าสุดท้ายที่เหลืออยู่คือค่าจากแถวล่างสุด
  • สถานะ "ล้มเหลว" ไม่ได้แปลว่าไฟล์เสียเสมอไป — worker สรุปสถานะจากผลจริง: อัปเดต > 0 และไม่มีแถวถูกข้าม → success; อัปเดต > 0 และมีแถวถูกข้าม → partial; นอกนั้นทั้งหมด (รวมกรณีอ่านไฟล์ได้ปกติแต่ไม่มีแถวใดจับคู่ได้เลย) → failed — งานที่ขึ้น "ล้มเหลว" ควรเปิดไฟล์ log ดูก่อนสรุปว่าระบบพัง
  • มีกลไกกันงานซ้ำที่ระดับฐานข้อมูล — worker "จอง" งานด้วยคำสั่งมีเงื่อนไข UPDATE ... WHERE id = ? AND status = 'pending' ถ้าไม่มีแถวถูกแก้ (งานหาย, ถูกจองไปแล้ว, หรือจบแล้ว) จะ ack ทิ้งข้อความนั้นไปเลย — ข้อความซ้ำจากคิวจึงไม่ทำให้ข้อมูลถูกอัปเดตสองรอบ
  • ทุกความล้มเหลวหลังจองงานสำเร็จจะปิดงานเป็น failed แล้ว ack ไม่ปล่อยให้ค้างที่ processing และไม่ requeue วนซ้ำ
  • ถ้าคอลัมน์ที่จับคู่ไว้หายไปจากหัวไฟล์ การจับคู่นั้นจะถูกข้ามอย่างเงียบ ๆ — worker บันทึกเป็น warning ใน log ฝั่งเซิร์ฟเวอร์แล้วทำงานต่อกับคู่ที่เหลือ ไม่ได้ทำให้งานล้ม (ต่างจากคอลัมน์ match key ที่หายไป ซึ่งทำให้งานล้มทั้งงาน)
  • ไฟล์ต้นทางถูกอ่านสามครั้งจากคนละที่ — ตอนอัปโหลด (cms-api, เพื่อทำพรีวิว), ตอนตรวจขั้นที่ 4 (cms-api, อ่านจาก storage อีกครั้ง) และตอน worker ทำงานจริง (อ่านจาก storage) — ทั้งสามครั้งตัด BOM ออกก่อนแยกวิเคราะห์ (คอมเมนต์ใน worker ระบุว่าไฟล์ที่ระบบเราส่งออกเองมี BOM ติดมาเพื่อให้ Excel ภาษาไทยอ่านได้ ถ้าไม่ตัดออกหัวคอลัมน์แรกจะไม่เคยตรงกับชื่อที่ตั้งค่าไว้)
  • ไฟล์ถูกตรวจว่าเป็นของ OA นี้จริงด้วยการดู prefix ของ path — ทั้งขั้นตรวจและขั้นสร้างงานบังคับว่า filePath ต้องขึ้นต้นด้วย private/<lineOaHash>/imports/mapping/ ไม่เช่นนั้นตอบ 400 — ป้องกันการส่ง path ของ OA อื่นเข้ามาแม้จะเดาชื่อไฟล์ได้
  • ชื่อไฟล์ที่เก็บถูกล้างอักขระและเติมเวลาต่อหน้า — อักขระที่ไม่ใช่ตัวอักษร/ตัวเลข/./-/_ ถูกแทนด้วย _ และเติม epoch millisecond ไว้ข้างหน้า ชื่อไฟล์ภาษาไทยจึงกลายเป็นขีดล่างทั้งชุดใน storage (ชื่อเดิมยังถูกเก็บไว้ในฟิลด์ file_name และเป็นชื่อที่แสดงในตาราง)
  • เพดาน 20 MB ถูกตรวจสามชั้น — หน้าเว็บตรวจก่อนส่ง, controller ตรวจจากขนาดที่ประกาศมาใน multipart, และตรวจอีกครั้งจากจำนวนไบต์ที่อ่านได้จริง (กันกรณีขนาดที่ประกาศไม่ตรงกับของจริง)
  • ชนิดไฟล์รับกว้างกว่าที่หน้าเว็บให้เลือก — controller ยอมรับไฟล์ที่นามสกุลเป็น .csv หรือ ที่ content type มีคำว่า csv หรือ เป็น text/plain — ไฟล์ข้อความที่ตั้งชื่อไม่มีนามสกุลจึงยังผ่านได้ถ้าเรียก API ตรง
  • ผลตรวจที่บันทึกไว้บนงานคือ "ภาพที่ผู้ใช้เห็นและกดยืนยัน" ไม่ใช่ค่าที่คำนวณใหม่ — หน้าเว็บส่งผลตรวจจากขั้นที่ 4 แนบไปกับคำขอสร้างงาน และ backend เก็บลงคอลัมน์ validation ตามที่ส่งมา (เก็บเฉพาะเมื่อไม่ใช่ null) — ตัวเลขจำนวนแถวที่เห็นในตารางระหว่างงานยังทำงานอยู่มาจากค่านี้
  • ถ้าส่งงานเข้าคิวไม่สำเร็จ แถวงานจะถูกสร้างค้างไว้แล้ว — backend สร้างแถว import_mapping_job ก่อน แล้วจึง publish เข้าคิว ถ้า publish ล้มเหลวจะตอบ 500 แต่ไม่มีการย้อนการสร้างแถวนั้น — ผลคืองานค้างสถานะ pending ตลอดไปโดยไม่มีใครหยิบไปทำ (และไม่มีปุ่มลบในหน้าเว็บ) ถ้าเห็นงานค้าง pending นานผิดปกติทั้งที่ worker ทำงานปกติ ให้สงสัยกรณีนี้
  • ลำดับการเรียงสาเหตุใน log ผลจริงไม่เหมือนในหน้าตรวจ — ขั้นตรวจของ cms-api เรียงตามลำดับคงที่ (คีย์ว่าง → อีเมลผิด → เบอร์ผิด → ไม่พบคู่) ส่วน stats ที่ worker เขียนกลับเรียงตามจำนวนมากไปน้อย แล้วจึงตามตัวอักษร — จึงไม่ควรเทียบสองรายการนี้แบบบรรทัดต่อบรรทัด
  • ลิงก์ดาวน์โหลดไฟล์ log เป็น presigned URL ที่สร้างใหม่ทุกครั้งที่โหลดรายการ และช่องนี้จะว่าง (—) เมื่อไม่มีแถวใดถูกข้าม
  • ถ้าอัปโหลดไฟล์ log ไม่สำเร็จ งานยังนับว่าสำเร็จ — ข้อมูลถูกอัปเดตไปแล้วจริง worker จึงบันทึก error ไว้ใน log ฝั่งเซิร์ฟเวอร์แล้วปิดงานโดยไม่มีลิงก์ไฟล์ให้ดาวน์โหลด

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

  • Member Database — คนละกลไกกันโดยสิ้นเชิง ไม่ใช่แค่คนละเมนู ดูตารางเปรียบเทียบท้ายหัวข้อนี้
  • Settings > User Attribute Setup — แอตทริบิวต์ที่ประกาศไว้ที่นั่นคือที่มาของตัวเลือก custom.<key> ทั้งใน match key และในฟิลด์ปลายทาง (ไล่ลูกหลานในโครงสร้างด้วย และเอาเฉพาะที่สถานะ active) ถ้ายังไม่ประกาศแอตทริบิวต์ไว้ รายการจะมีแต่ฟิลด์โปรไฟล์มาตรฐาน
  • All friends — เป็นที่ตรวจผลลัพธ์รายคนหลังนำเข้าเสร็จ และเป็นที่เดียวที่ ล้างค่าฟิลด์ทีละคน ได้ (การนำเข้าด้วยเซลล์ว่างไม่ล้างค่า) นอกจากนี้การนำเข้า CSV ในแท็บ "ประวัติการนำเข้า" ของเมนูนั้นเป็นกลไกต่างจากเมนูนี้ — ที่นั่นรับเฉพาะคอลัมน์ Line UserID เพื่อสร้าง/อ้างอิงรายชื่อ ไม่มีการจับคู่ฟิลด์
  • Segment audience — ค่าที่นำเข้ามาด้วยเมนูนี้ใช้เป็นเงื่อนไขสร้างกลุ่มเป้าหมายได้ทันทีหลังงานเสร็จ
  • Rich message / Campaign — ฟิลด์ที่นำเข้ามาคือค่าที่ merge tag ในเนื้อหาข้อความจะดึงไปใช้ การนำเข้าให้ครบก่อนส่งจึงลดจำนวนผู้รับที่ข้อมูล merge tag ไม่ครบ

Import Data ต่างจาก Member Database อย่างไร​

ความต่างทั้งหมดนี้ตรวจสอบจากซอร์สโค้ดของทั้งสองโมดูลได้จริง:

Import Data (เมนูนี้)Member Database
ตารางปลายทางline_user — ฟิลด์โปรไฟล์และ custom_attribute ของเพื่อนที่มีอยู่customer_database / customer_database_row เป็นตารางของตัวเอง
วิธีระบุตัวตนต้องเลือก match key เทียบกับคอลัมน์ใน CSVไม่มี — เก็บทุกแถวตามที่อยู่ในไฟล์
ผลต่อโปรไฟล์เพื่อนเขียนทับค่าเดิมของเพื่อนที่จับคู่ได้ไม่แตะโปรไฟล์เพื่อนเลย
การจับคู่ฟิลด์จับคู่คอลัมน์→ฟิลด์เองทีละคอลัมน์ไม่มี — ชื่อคอลัมน์เป็นชื่อฟิลด์ตรงตัว
ผู้ใช้ปลายทางของข้อมูลทุกเมนูที่อ่านโปรไฟล์เพื่อน (All friends, Segment audience, merge tag ในแคมเปญ)เมนู Form builder เพื่อตรวจว่าผู้กรอกเป็นสมาชิกจริง
การประมวลผลเข้าคิว RabbitMQ ให้ worker ทำเบื้องหลังทำทันทีในคำขอเดียว (ทรานแซกชัน)
แถวที่ใช้ไม่ได้ถูกข้ามเป็นรายแถวและสรุปในไฟล์ logทำให้ทั้งไฟล์ถูกปฏิเสธ
ประวัติ/การลบเก็บเป็นประวัติงานนำเข้า ไม่มีการลบลบได้ (soft delete) เว้นแต่มีฟอร์มผูกอยู่

พูดสั้นที่สุด: เมนูนี้เปลี่ยนตัวข้อมูลเพื่อน ส่วน Member Database เพิ่มตารางอ้างอิงไว้ข้าง ๆ โดยไม่แตะข้อมูลเพื่อน

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

  • อย่าอ่านตัวเลขในขั้นที่ 4 เป็นคำตอบสุดท้าย — ผลตรวจกับผลจริงใช้เกณฑ์ต่างกันในการจับคู่เบอร์มือถือและการตรวจรูปแบบอีเมล (ดูรายละเอียดข้างต้น) ตัวเลข "จะอัปเดต" มักจะต่ำกว่าผลจริงเมื่อใช้เบอร์เป็น match key
  • ผลตรวจในขั้นที่ 4 เป็นภาพ ณ เวลาที่ตรวจ ไม่ใช่การจองล็อกข้อมูล — ถ้ามีเพื่อนถูกเพิ่มหรือลบระหว่างรอคิว ตัวเลขจริงจะต่างจากที่แสดง
  • ไม่มีปุ่มยกเลิกหลังกดเริ่มนำเข้า ไม่มีปุ่มลบงาน และไม่มีหน้าดูรายละเอียดงานรายตัว ข้อมูลทั้งหมดที่ดูได้มีแค่ตารางประวัติและไฟล์ log
  • ไม่มีทางย้อนค่าที่ถูกเขียนทับ — ระบบไม่เก็บค่าก่อนหน้าไว้ที่ใดเลย ถ้าจับคู่ฟิลด์ผิดแล้วสั่งนำเข้า วิธีแก้เดียวคือนำเข้าไฟล์ที่ถูกต้องทับลงไปอีกครั้ง
  • เลือก match key ที่ไม่ซ้ำเสมอ — ถ้าเพื่อนหลายคนมีเบอร์หรืออีเมลเดียวกัน ไม่มีการรับประกันว่าใครจะถูกอัปเดต
  • ถ้าไฟล์ CSV มีหลายแถวที่ชี้ไปเพื่อนคนเดียวกัน ค่าที่เหลืออยู่จะเป็นค่าจากแถวล่างสุด — ควรกำจัดแถวซ้ำในไฟล์ก่อน
  • สถานะ ล้มเหลว อาจหมายถึง "ไม่มีแถวใดจับคู่ได้เลย" ซึ่งมักเป็นสัญญาณว่าเลือก match key หรือคอลัมน์ผิด — ให้เปิดไฟล์ log ดูก่อน
  • งานที่ค้างสถานะ รอดำเนินการ นานผิดปกติทั้งที่ worker ทำงานปกติ อาจเป็นงานที่สร้างแถวสำเร็จแต่ส่งเข้าคิวไม่สำเร็จ — ต้องแก้ที่ฝั่งระบบ ไม่มีทางแก้จากหน้าเว็บ
  • เซลล์ว่างในไฟล์ไม่ล้างค่าเดิม จึงใช้เมนูนี้ "ล้างข้อมูล" ไม่ได้
  • ลิงก์ดาวน์โหลดไฟล์ log หมดอายุ (presigned URL) ต้องกดจากหน้ารายการที่เพิ่งโหลด

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

  • ฝั่งหน้าเว็บ — ถูกครอบด้วย PermissionGuard ตรวจ action VIEW ของ subject import-mapping
  • ฝั่งเซิร์ฟเวอร์ — ทั้ง 5 เส้นทางถูกครอบด้วย ModuleGate(d, "import-mapping") ที่บังคับใช้จริง โดย middleware ตัวเดียวกันถูกผูกไว้กับทุกเส้นทางแยกกัน (POST /import-mapping/upload, GET /import-mapping/fields, POST /import-mapping/validate, POST /import-mapping, GET /import-mapping) — ถ้าผู้ดูแลระดับแพลตฟอร์มปิดโมดูลนี้ให้องค์กรใด ผู้ใช้ประเภท customer ในองค์กรนั้นจะได้ 403 ทันทีแม้เรียก API ตรง (ผู้ใช้ประเภท onemoby ข้ามการกั้นนี้เสมอ) — ชื่อโมดูลตรงกันทั้งฝั่งหน้าเว็บและฝั่งเซิร์ฟเวอร์
  • ทุกเส้นทางบังคับให้มีบริบท OA ครบสามอย่าง — lineOaId, organizationId และ lineOaHash (ค่าที่ได้มาตอนเลือก LINE OA หลังล็อกอิน) ถ้าขาดข้อใดจะตอบ 400 "Line OA context is required" ก่อนแตะฐานข้อมูลหรือ storage
  • มีการกั้นข้ามผู้เช่าอีกชั้นที่ระดับ path ของไฟล์ — filePath ที่ส่งมาต้องขึ้นต้นด้วย prefix ของ OA ตัวเอง (private/<lineOaHash>/imports/mapping/) ไม่เช่นนั้นตอบ 400 — สิทธิ์ระดับโมดูลเพียงอย่างเดียวจึงไม่พอที่จะสั่งประมวลผลไฟล์ของ OA อื่น
  • การอ่านรายการงานและการค้นคีย์ทั้งหมดถูกจำกัดด้วย line_oa_id + organization_id จาก context เสมอ และตัดแถวที่มี deleted_date ออก
  • ModuleGate ออกแบบให้ fail open — ถ้าระบุตัวผู้เรียกไม่ได้ ไม่พบองค์กร หรือการค้นค่าคอนฟิกล้มเหลว จะปล่อยให้ผ่าน (พร้อมบันทึก log)
  • policy annotation ระดับการกระทำ (create / read / readAll) มีประกาศครบทุกเส้นทางแต่วิ่งผ่าน middleware ที่เป็น no-op เหมือนโมดูลอื่น — การบังคับจริงอยู่ที่ ModuleGate ระดับโมดูล
  • ไม่มีเส้นทางลบหรือแก้ไขงานนำเข้าเลย (มีเพียงอัปโหลด/ตรวจ/สร้าง/อ่าน) — ประวัติงานนำเข้าจึงเป็นข้อมูลที่เพิ่มได้เท่านั้น

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

Frontend (cms-web-2026)

  • src/app/import-mapping/page.tsx — จุดเข้าและ PermissionGuard
  • src/components/import-mapping/import-mapping-list.container.tsx — การดึงรายการและการรีเฟรชอัตโนมัติทุก 10 วินาทีเมื่อมีงานทำงานอยู่
  • src/components/import-mapping/import-mapping-table.tsx — แท็กสถานะทั้ง 5 แบบ, คอลัมน์จำนวนข้อมูลที่เปลี่ยนความหมายตามสถานะ, ลิงก์ดาวน์โหลด log
  • src/components/import-mapping/import-wizard.container.tsx — 4 ขั้นตอน, เงื่อนไขเปิดปุ่มถัดไป, การล้างค่าขั้นถัดไปเมื่อเปลี่ยนไฟล์
  • src/components/import-mapping/steps/upload-step.tsx — การตรวจ 20 MB ฝั่งเบราว์เซอร์
  • src/components/import-mapping/steps/mapping-step.tsx — การเลือก match key, การจัดกลุ่มตัวเลือก, กฎ "ฟิลด์ปลายทางใช้ได้ครั้งเดียว"
  • src/components/import-mapping/steps/review-step.tsx — การ์ดสรุป 4 ใบและตารางสาเหตุที่ข้าม

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

  • internal/modules/importmapping/controller.go — 5 เส้นทาง, ModuleGate("import-mapping"), การตรวจขนาด/ชนิดไฟล์
  • internal/modules/importmapping/service.go — การแยกวิเคราะห์ CSV, การเก็บไฟล์ลง storage, ตรรกะการตรวจแบบ dry-run และการจัดกลุ่มเหตุผล, การสร้างงานและส่งเข้าคิว
  • internal/modules/importmapping/dto.go — รายการฟิลด์ที่ใช้เป็นคีย์/ปลายทางได้ และ regex ตรวจอีเมล/เบอร์ของฝั่ง cms-api

Worker (line-management-worker-go)

  • internal/importmapping/service.go — การจองงานแบบ atomic, การสร้างชุดค้นหาคีย์, การอัปเดตทีละแถว, การรวม jsonb, การสร้างไฟล์ log และการสรุปสถานะ
  • internal/importmapping/types.go — รายการคอลัมน์ที่ใช้เป็นคีย์ได้ vs. ที่เขียนได้ (จุดที่ user_id ถูกกันไว้), รายการเหตุผล, โครงสร้าง stats
  • internal/importmapping/consumer.go — การผูก handler เข้ากับคิว

ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ นำเข้าข้อมูลและ Mapping ฟิลด์ (Import Mapping)