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 — จับคู่ฟิลด์
- เลือก match key — ซ้ายคือฟิลด์ในโปรไฟล์เพื่อน (LINE User ID / เบอร์มือถือ / อีเมล / custom attribute ที่ประกาศไว้ในหน้า Attribute Setup) ขวาคือคอลัมน์ใน CSV
- จับคู่คอลัมน์ที่เหลือกับฟิลด์ปลายทาง — ตัวเลือกถูกแบ่งเป็นสองกลุ่ม (ฟิลด์โปรไฟล์ / แอตทริบิวต์ที่กำหนดเอง) แต่ละคอลัมน์เลือก "ไม่นำเข้า" ได้ถ้าไม่ต้องการ
- คอลัมน์ที่เลือกเป็น match key จะถูกถอดออกจากรายการจับคู่โดยอัตโนมัติ และแสดงแท็ก "คีย์ที่ใช้จับคู่ — ใช้เพื่อค้นหาเพื่อน" แทนช่องเลือก
- ฟิลด์ปลายทางหนึ่งฟิลด์ใช้ได้ครั้งเดียว — เมื่อคอลัมน์หนึ่งจับคู่กับฟิลด์ใดไปแล้ว ฟิลด์นั้นจะถูกปิดในรายการของคอลัมน์อื่น
- ปุ่มถัดไปเปิดใช้งานเมื่อครบทั้งสามอย่าง: เลือกฟิลด์ match key แล้ว, เลือกคอลัมน์ match key แล้ว และจับคู่คอลัมน์อื่นไว้อย่างน้อยหนึ่งคอลัมน์
ขั้นที่ 4 — ตรวจทานและสั่งนำเข้า
- กดถัดไป ระบบเรียกการตรวจแบบ dry-run กับไฟล์จริง แล้วแสดงการ์ดสรุป 4 ใบ — จำนวนแถวทั้งหมด, จะอัปเดต, ไม่พบคู่ที่ตรงกัน — ข้าม, ข้อมูลไม่ถูกต้อง — ข้าม
- ถ้ามีแถวที่จะถูกข้าม จะมีกล่องเตือนบอกจำนวน และตาราง สาเหตุที่ข้าม พร้อมจำนวนแถวต่อสาเหตุ
- กด เริ่มนำเข้า ระบบสร้างงานสถานะ
pendingส่งเข้าคิว แล้วพากลับหน้ารายการให้ติดตามสถานะ
3. ตรวจผลและแก้แถวที่ถูกข้าม
- รอให้สถานะเปลี่ยนเป็น เสร็จสิ้น หรือ เสร็จสิ้นโดยมีข้อผิดพลาด (หน้ารีเฟรชให้เองทุก 10 วินาที)
- ถ้ามีแถวถูกข้าม กด ดาวน์โหลดบันทึก เพื่อได้ไฟล์ CSV ที่มีหัวคอลัมน์เดิมทั้งชุด บวกคอลัมน์
Error Reasonต่อท้าย และมีเฉพาะแถวที่ถูกข้าม พร้อมเหตุผลทุกข้อของแถวนั้น - แก้ไฟล์ตามเหตุผล แล้วนำเข้าเฉพาะไฟล์ที่แก้แล้ว — การนำเข้าซ้ำด้วยไฟล์เดิมทั้งไฟล์จะเขียนค่าเดิมทับอีกครั้ง ไม่มีการตรวจว่าเคยนำเข้าแล้ว
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก 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ตรวจ actionVIEWของ subjectimport-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— จุดเข้าและPermissionGuardsrc/components/import-mapping/import-mapping-list.container.tsx— การดึงรายการและการรีเฟรชอัตโนมัติทุก 10 วินาทีเมื่อมีงานทำงานอยู่src/components/import-mapping/import-mapping-table.tsx— แท็กสถานะทั้ง 5 แบบ, คอลัมน์จำนวนข้อมูลที่เปลี่ยนความหมายตามสถานะ, ลิงก์ดาวน์โหลด logsrc/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ถูกกันไว้), รายการเหตุผล, โครงสร้าง statsinternal/importmapping/consumer.go— การผูก handler เข้ากับคิว
ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ นำเข้าข้อมูลและ Mapping ฟิลด์ (Import Mapping)