Segment audience — Multi-source filter
ภาพรวม
Multi-source filter คือโหมดสร้างกลุ่มเป้าหมายจาก หลายเงื่อนไขรวมกัน โครงสร้างข้อมูลคือ audience หนึ่งชุดมีหลาย กลุ่ม (group) และแต่ละกลุ่มมีหลาย แหล่งข้อมูล (source) กำหนดตัวเชื่อม AND/OR ได้สองระดับ — ระหว่าง source ภายในกลุ่มเดียวกัน และระหว่างกลุ่มทั้งหมด เหมาะกับการตัดกลุ่มลูกค้าที่ต้องใช้เงื่อนไขซับซ้อนกว่าโหมด Filter audience แบบเดียว
จุดที่ต้องเข้าใจก่อนใช้งานคือ การรวมเซตทำในหน่วยความจำของ Go ไม่ได้ทำใน SQL — ระบบไปหารายชื่อของแต่ละ source แยกกันจนได้อาเรย์ของ LINE user id แล้วเอาอาเรย์เหล่านั้นมา intersect/union กันในโค้ด พฤติกรรมสำคัญหลายอย่าง (รวมถึงกับดักที่ต้องรู้) เกิดจากวิธีนี้ทั้งหมด (ดูหัวข้อ รายละเอียดสำคัญ)
บทบาทที่ใช้งาน — ทีมการตลาดที่ต้องตัดกลุ่มลูกค้าแบบละเอียด เช่น "คลิกแคมเปญ A และ (เป็นสมาชิกระดับ Gold หรือมีแต้มคงเหลือเกิน 500)"
เส้นทางผู้ใช้งาน
1. เข้าฟอร์มและตั้งชื่อ
เปิดเมนู Segment audience > Multi-source filter หรือกดปุ่ม Create segment จากหน้ารายการที่เมนู Filter audience ซึ่งพามาที่ฟอร์มนี้เป็นค่าเริ่มต้น กรอกชื่อ audience (ต้องไม่ซ้ำ และห้ามเป็นช่องว่างล้วน)
ฟอร์มรับพารามิเตอร์ ?preset= เป็น JSON ที่มีชื่อและชุด source มาตั้งต้นให้ได้ — ถ้าเข้ามาทางนั้นจะเห็นแถบเขียวบอกว่า "ใช้ preset ที่บันทึกไว้: {ชื่อ}" และ preset จะถูกเติมให้ครั้งเดียวเท่านั้น ไม่ทับค่าที่แก้ต่อ
2. เพิ่มและตั้งค่า source
กดปุ่ม Add Source ในกลุ่มใดกลุ่มหนึ่งเพื่อเลือกชนิด — หน้าเว็บเสนอ 7 ชนิด: แคมเปญ, Rich Menu, activity tag, Auto Response, ฟอร์ม, custom attribute, และ Loyalty แต่ละ source แสดงเป็นการ์ดที่มีป้ายสีตามชนิด ปุ่มพับเก็บ ปุ่มลบ และ สวิตช์เปิด/ปิดใช้งานชั่วคราว (การ์ดที่ปิดจะจางลงและถูกตัดออกจากการคำนวณทั้งหมด) หัวกลุ่มบอกจำนวน "{ที่เปิด}/{ทั้งหมด} sources"
การตั้งค่าตามชนิด
- แคมเปญ — เลือกแคมเปญจากเมนู Campaign ติ๊ก "Reach (View)" และ/หรือ spot ที่ติดตามคลิกได้ พร้อมตัวเชื่อม AND/OR ของ source นั้นเอง
- Rich Menu — เลือกชนิดเมนู (guest/member/custom/switch) จากเมนู Rich menu แล้วเลือกเมนูและติ๊ก spot ถ้าเมนูนั้นไม่มี action ที่บันทึกคลิกได้ จะขึ้นข้อความว่า "ไม่มี spot ที่ติดตามได้ มีเพียง action ชนิด Link, Text และ Switch ที่บันทึกการคลิก"
- Activity tag — ติ๊กช่วงความเคลื่อนไหว 4 แบบเหมือนโหมดแหล่งเดียว พร้อมตัวเชื่อม AND/OR
- Auto Response — เลือกกฎจากเมนู Auto-response message ได้สูงสุด 5 รายการต่อ source
- ฟอร์ม — เลือกฟอร์มจากเมนู Form builder แล้วสร้างเงื่อนไขจากคำถามได้ (กลุ่มเดียว แต่ไม่จำกัดจำนวนเงื่อนไข) เลือกคำถาม ตัวดำเนินการ และค่า โดยช่องกรอกค่าจะเปลี่ยนตามชนิดคำถาม (วันที่/ตัวเลข/ตัวเลือก/ข้อความ)
- Custom attribute — เลือก attribute จากสองหมวด ("User Profile" กับ "Custom Attributes") แล้วเลือกตัวดำเนินการจาก 13 ตัวที่กรองตามชนิดข้อมูลของ attribute นั้น
- Loyalty — เลือกตัวชี้วัด 1 ตัวต่อ source (แต้มคงเหลือ, ได้รับ, แลกไป, ระดับ, บัตรที่สะสมครบ, จำนวนวันตั้งแต่ได้แต้มล่าสุด) พร้อมตัวดำเนินการ
>=/<=/=/ ระหว่าง และตัวเลือกจำกัดสาขาและช่วงวันที่
3. กำหนดตัวเชื่อมและดูไดอะแกรม
- ระหว่าง source ภายในกลุ่ม — ปุ่มวงกลม AND/OR ที่หัวการ์ดกลุ่ม (ค่าเริ่มต้น AND)
- ระหว่างกลุ่ม — ปุ่ม "AND (Intersection)" / "OR (Union)" ที่การ์ดบนสุด (ค่าเริ่มต้น OR) กลุ่มจะลบได้เมื่อมีมากกว่า 1 กลุ่มเท่านั้น
แผงด้านขวาวาดไดอะแกรมแบบ Venn สดตามที่แก้ทันทีโดยไม่ต้องกดดูตัวอย่าง กลุ่มที่มี 1–2 source วาดเป็นวงกลมสองวงคลาสสิก 3 source เป็นสามเหลี่ยม และ 4 source ขึ้นไปจัดเป็นตาราง 2 แถว (ไม่ได้ตัดที่ 3 วงแบบไดอะแกรมทั่วไป) แบบ AND จะวาดพื้นที่ทับซ้อนจริงทุกวงและติดป้าย "AND" กลางภาพ แบบ OR จะระบายเต็มวง แต่ละกลุ่มเรียงลงล่างคั่นด้วยสัญลักษณ์ ∩ หรือ ∪ ปิดท้ายด้วยป้าย "= Audience"
4. ดูตัวอย่างผลลัพธ์และสร้าง
- กดปุ่ม Preview Results — ยิง
POST /audiences-filter/preview-multi-source-filterระบบคืน จำนวนรวมทั้งหมด, รายชื่อ 10 รายการแรก และ จำนวนที่แต่ละ source เข้าเงื่อนไข (แถบ "Source breakdown") - ตารางตัวอย่างแสดงลำดับ, ผู้ใช้ (รูป + ชื่อ), User ID (คัดลอกได้), คอลัมน์ Sources ที่บอกว่าคนนั้นตรงกับ source ใดบ้าง, และประเภทผู้ใช้ ถ้ามี source ประเภทฟอร์มที่เปิดใช้งานอยู่ จะมีคอลัมน์ Form data เพิ่มให้กดเปิดดูคำตอบล่าสุดของผู้ใช้คนนั้น
- กดสร้าง — ปุ่มสร้างเปิดใช้งานได้ต่อเมื่อกดดูตัวอย่างสำเร็จมาก่อนแล้วเท่านั้น และตอนกดยังตรวจอีกสองข้อคือต้องมีชื่อที่ไม่ใช่ช่องว่าง และต้องมี source ที่เปิดใช้งานอย่างน้อย 1 รายการ
- ระบบยิง
POST /audiences-filter/create-multi-source-filterแล้วพากลับไปหน้ารายการที่เมนู Filter audience ซึ่งใช้หน้าเดียวกันกับกลุ่มแบบแหล่งเดียว
รายละเอียดสำคัญที่ตรวจสอบจากซอร์สโค้ดจริง
ยืนยันจาก internal/modules/audiencefilter/{controller,service,dto,multi_source_query_builder}.go ของ line-management-cms-api-go:
- การรวมเซตทำในหน่วยความจำ ไม่ใช่ใน SQL —
buildGroupQueryไปหารายชื่อของแต่ละ source ที่เปิดใช้งานแยกกันจนได้อาเรย์ของ user id แล้วcombineUserIdSetsเอามารวมกัน แบบ AND ทำ intersection โดยเริ่มจากเซตที่เล็กที่สุดก่อน (เรียงตามขนาดแบบ stable แล้วไล่ตัดจากเซตนั้น) เพื่อลดงาน และรักษาลำดับตามเซตเล็กสุด ส่วนแบบ OR ทำ union โดยรักษาลำดับที่พบครั้งแรก — จากนั้นbuildCombinedQueryเอาผลของแต่ละกลุ่มมารวมด้วยตัวเชื่อมระดับบนอีกชั้นด้วยฟังก์ชันเดียวกัน - ⚠️ เซตว่างถูกทิ้งก่อนรวม ทำให้ AND ไม่ให้ผลว่างอย่างที่คาด — นี่คือกับดักที่สำคัญที่สุดของหน้านี้
combineUserIdSetsกรองเซตที่ว่างออกทิ้งก่อนเสมอ (nonEmpty) และbuildCombinedQueryก็กรองกลุ่มที่นับได้ศูนย์ออกก่อนรวมระดับบน (nonEmptyGroupResults) ผลคือ "คลิกแคมเปญ A AND ระดับสมาชิก Gold" ในกรณีที่ไม่มีใครเป็น Gold เลย จะคืนคนที่คลิกแคมเปญ A ทั้งหมด ไม่ใช่ศูนย์คน — เงื่อนไขที่หาใครไม่ได้เลยจะถูกเมินทิ้งแทนที่จะตัดผลลัพธ์ให้เป็นเซตว่าง ต้องดูแถบ "Source breakdown" ในผลตัวอย่างเสมอ ถ้าเห็น source ใดขึ้น0ให้ถือว่า source นั้นไม่ได้มีผลต่อผลลัพธ์เลย - backend รองรับ source 8 ชนิด แต่หน้าเว็บเสนอแค่ 7 — enum ของ backend มี
campaign,rich_menu,activity_tag,auto_response,form/form_builder(สองชื่อชี้ตัวจัดการเดียวกัน),custom_attribute,loyaltyและtrigger_ruleซึ่งมีตัวค้นรายชื่อทำงานได้ครบในgetTriggerRuleUserIdsแต่ไม่มี UI ให้เลือก — เรียกใช้ได้ทาง API ตรงเท่านั้น - source ประเภทฟอร์มถูกกรองเหลือแต่คนที่ยังเป็นเพื่อนอยู่ ต่างจากโหมดแหล่งเดียว —
getFormBuilderUserIdsเรียกkeepFollowersตัดคนที่follow != 'yes'ออก โดยคอมเมนต์ระบุเหตุผลว่า "การกรอกฟอร์มพิสูจน์แค่ว่าเคยกรอก ไม่ได้พิสูจน์ว่าวันนี้ยังติดต่อได้" และอธิบายว่าที่ต้องกรองทีหลังแทนที่จะใส่ในคิวรีเลย เพราะbuildQueryใช้ร่วมกับฟีเจอร์ form-filter แบบแหล่งเดียว การใส่เงื่อนไข follow ลงไปจะเปลี่ยนพฤติกรรมของฟีเจอร์นั้นไปด้วยเงียบ ๆ — ผลคือเงื่อนไขฟอร์มชุดเดียวกันจะได้คนมากกว่าใน Filter audience แบบแหล่งเดียว และน้อยกว่าเมื่อใช้เป็น source ในหน้านี้ - source ประเภท Rich Menu ไม่รับตัวเชื่อม AND/OR เลย —
richMenuSourceConfigไม่มีฟิลด์conditionและgetRichMenuUserIdsตั้งค่า OR ตายตัวพร้อมคอมเมนต์ "Rich menu typically uses OR" แปลว่าถ้าติ๊กหลาย spot ในเมนูเดียว จะได้ผลเป็น "กดอันใดอันหนึ่ง" เสมอ ไม่มีทางทำให้เป็น "กดครบทุกอัน" ได้ (ต่างจาก source แคมเปญ, activity tag, auto response และ trigger rule ที่ส่งตัวเชื่อมเข้าไปได้จริง) - เงื่อนไข custom attribute มีด่านกัน SQL injection ที่ระดับชื่อฟิลด์ — attribute ที่ขึ้นต้นด้วย
__fixed.จะถูกแปลงเป็นชื่อคอลัมน์จริงจาก whitelist 10 ตัว (display_name,firstname,lastname,email,mobile_no,language,follow,user_type,registered_date,last_activity_date) ชื่อที่ไม่อยู่ใน whitelist จะถูกปฏิเสธ ส่วน attribute อื่นถูกประกอบเป็นcustom_attribute->>'key'โดยชื่อคีย์ต้องผ่าน regex^[a-zA-Z0-9_.-]+$ก่อนเท่านั้น - หน้านี้เป็นเส้นทางเดียวที่ยอมสร้างกลุ่มที่ไม่มีสมาชิกเลย —
CreateMultiSourceFilterถ้าคำนวณได้ศูนย์รายชื่อ จะยังสร้างแถวและปิดสถานะเป็นcompletedพร้อมสถิติศูนย์ทั้งหมดและlineUserIdsPathFilenameเป็นค่าว่าง (ไม่มีไฟล์ CSV) ขณะที่create-filterและcreate-form-filterตอบ 400 ปฏิเสธไปเลย — กลุ่มว่างที่สถานะ "เสร็จแล้ว" ในหน้ารายการจึงมาจากเส้นทางนี้เท่านั้น - การสร้างเป็นงานซิงโครนัสเหมือนโหมดแหล่งเดียว ไม่มีคิว — คำนวณ → สร้างแถว → ผูก
audience_idsเป็นก้อนละ 5,000 รายชื่อ → เขียนไฟล์ CSV ขึ้น object storage → ปิดสถานะcompletedทั้งหมดในคำขอเดียว filterInfoเก็บทั้งเงื่อนไขและ "ผลการคำนวณครั้งนั้น" ไว้ด้วยกัน — นอกจากisMultiSource,sourceJoinและgroupsแล้ว ยังบันทึกgroupResults(จำนวนที่แต่ละกลุ่มได้) และsourceResults(จำนวนที่แต่ละ source ได้) ติดไปกับกลุ่มด้วย เป็นภาพนิ่งของตอนสร้าง ไม่ได้อัปเดตตามการคำนวณรอบใหม่ — และ โครงสร้างนี้เองที่งาน auto refresh อ่านย้อนกลับมาใช้ประเมินเงื่อนไขใหม่ทุกครั้ง ผ่านเส้นทางisMultiSourceในrefreshAudience- ตัวอย่างผลลัพธ์คืนได้ไม่เกิน 10 รายการเสมอ แต่ยอดรวมเป็นตัวเลขจริง — DTO ตรวจ
limitด้วย@Min(1) @Max(10)ส่วนช่อง total มาจากจำนวนรายชื่อทั้งชุดที่คำนวณได้จริง แถบ Source breakdown ก็เป็นตัวเลขเต็มของแต่ละ source ไม่ได้ถูกตัดตาม limit - คอลัมน์ Sources ในตารางตัวอย่างคำนวณจากการไล่เทียบทุก source — ระบบวนดูผลของทุก source แล้วจดว่าผู้ใช้แต่ละคนไปโผล่ที่ source ใด โดยรักษาลำดับที่พบก่อนหลังไว้ (เลียนลำดับของ Set ใน JavaScript) จึงบอกได้ว่าคนคนนั้นเข้ามาด้วยเงื่อนไขข้อไหน
- source ที่ปิดสวิตช์ถูกตัดออกสองชั้น —
getSourceUserIdsคืนอาเรย์ว่างทันทีถ้าenabled = falseและbuildGroupQueryก็กรอง source ที่ปิดออกก่อนคำนวณอีกที กลุ่มที่ไม่มี source เปิดอยู่เลยจะคืนศูนย์รายชื่อและถูกทิ้งจากการรวมระดับบน (ตามกฎเซตว่างข้างต้น) - ไดอะแกรม Venn ยังไม่ครบสองจุด — (1) ชนิด
loyaltyไม่อยู่ในตารางสีของ visualizer จึงถูกวาดเป็นสีเทาและติดป้ายด้วยชื่อ enum ดิบว่าloyaltyและ (2) ตัวคอมโพเนนต์รับ prop จำนวนรวมได้ แต่ฟอร์มไม่เคยส่งค่านั้นเข้าไป ป้าย "= Audience" จึงไม่เคยแสดงตัวเลขจำนวนคนเลย - ข้อความของ source ประเภท Loyalty ยังไม่ได้แปลเป็นอังกฤษ — คีย์ทั้งชุด
sourceEntryCard.loyaltyCfg.*ในไฟล์messages/en/audience.jsonยังเป็นข้อความไทย ผู้ใช้ที่สลับภาษาเป็นอังกฤษจะเห็นการ์ด Loyalty เป็นภาษาไทย
เมนูที่เกี่ยวข้องกันในเส้นทางนี้
- Filter audience — ใช้หน้ารายการและหน้ารายละเอียดร่วมกันทั้งหมด ต่างกันแค่ตอนสร้าง และเป็นหน้าที่ระบบพากลับไปหลังสร้างเสร็จ กลุ่มจากทั้งสองโหมดถูกบันทึกด้วย
dataSource = 'filter'เหมือนกัน จึงเข้าร่วมรอบ auto refresh ได้เหมือนกัน (รายละเอียดรอบคำนวณอยู่ที่หน้านั้น) - Upload audience — โหมดที่ใช้ไฟล์แทนเงื่อนไข ไม่รองรับการรวมเซตแบบนี้และไม่รองรับ auto refresh
- Campaign — แหล่งข้อมูลของ source ประเภทแคมเปญ ดึง tracking spot ทั้งหมดมาให้ติ๊ก
- Rich menu — แหล่งข้อมูลของ source ประเภท Rich Menu (และเป็นชนิดเดียวที่ตัวเชื่อม AND/OR ไม่มีผล)
- Auto-response message — แหล่งข้อมูลของ source ประเภท Auto Response เลือกได้สูงสุด 5 กฎต่อ source
- Form builder — แหล่งข้อมูลของ source ประเภทฟอร์ม ทั้งรายการฟอร์ม รายการคำถาม และคำตอบล่าสุดที่แสดงในกล่อง Form data ของตารางตัวอย่าง
จุดที่ควรระวัง
- ต้องกดดูตัวอย่างผลลัพธ์สำเร็จก่อนเสมอ ปุ่มสร้างจะไม่เปิดใช้งานถ้าข้ามขั้นตอนนี้ และการกดดูตัวอย่างไม่มีการตรวจความครบถ้วนฝั่งหน้าเว็บก่อนยิง — ถ้า backend คืน error จะเห็นแค่ข้อความ "ดูตัวอย่างผลลัพธ์ไม่สำเร็จ" แบบกว้าง ๆ แล้วสถานะถูกรีเซ็ต
- ระวังกฎ "เซตว่างถูกเมิน" ให้มากที่สุด — เงื่อนไข AND ที่ไม่มีใครเข้าเลยจะไม่ทำให้ผลลัพธ์เป็นศูนย์ แต่จะหายไปจากสมการเฉย ๆ ทำให้ได้กลุ่มที่กว้างกว่าที่ตั้งใจ ตรวจแถบ Source breakdown ทุกครั้งว่าไม่มี source ใดขึ้นเลข
0 - ผลลัพธ์เป็นภาพนิ่งของช่วงเวลาที่กดสร้าง — ถ้าไม่เปิด auto refresh ที่หน้ารายการ กลุ่มจะไม่คำนวณตัวเองอีกเลย
- เงื่อนไขฟอร์มให้ผลไม่เท่ากันระหว่างสองหน้า — ที่นี่ตัดคนที่เลิกเป็นเพื่อนออก ส่วนโหมดแหล่งเดียวไม่ตัด ถ้าตัวเลขไม่ตรงกันนี่คือสาเหตุ
- ผู้ใช้ภาษาอังกฤษจะเห็นการ์ด Loyalty เป็นภาษาไทย และไดอะแกรมจะวาด source ชนิด Loyalty เป็นวงสีเทาที่ติดป้ายว่า
loyalty - ปุ่ม Copy ที่หน้ารายการพามาที่ฟอร์มนี้เสมอ แต่จะดึงค่าเดิมมาให้เฉพาะกลุ่มที่เป็นแบบหลายแหล่ง — กด Copy ที่กลุ่มแบบแหล่งเดียวจะได้ฟอร์มเปล่า
- ตัวเชื่อมค่าเริ่มต้นของสองระดับไม่เหมือนกัน — ระหว่าง source ในกลุ่มเป็น AND แต่ระหว่างกลุ่มเป็น OR ถ้าเพิ่มกลุ่มที่สองโดยไม่แตะตัวเชื่อม จะได้ผลเป็นการรวม (union) ไม่ใช่การตัด (intersection)
สิทธิ์การเข้าถึง (ตรวจสอบจากซอร์สโค้ดจริง)
หน้านี้ทำงานผ่านโมดูล backend audiencefilter เกือบทั้งหมด ซึ่ง มีการบังคับสิทธิ์น้อยกว่าโมดูล audience อย่างชัดเจน — ตรวจแยกทีละไฟล์แล้ว
- โมดูล
audiencefilter(เส้นทาง/api/audiences-filter/*) ไม่มีmodulegate.ModuleGateเลยแม้แต่บรรทัดเดียว —controller.goลงทะเบียนทั้ง 11 เส้นทางที่ต้องยืนยันตัวตนตรงบนauthedgroup โดยมีแต่auth.CheckPolicies(auth.Can(...))ที่เป็น no-op ดังนั้นPOST /audiences-filter/create-multi-source-filterและPOST /audiences-filter/preview-multi-source-filterที่หน้านี้ใช้ ยังเรียกได้ปกติแม้ผู้ดูแลระดับแพลตฟอร์มจะปิดโมดูล audiences ให้องค์กรนั้นแล้ว - โมดูล
audience(เส้นทาง/api/audiences/*) ตรงกันข้าม คือมีmodulegate.ModuleGate(d, "audiences")ครอบทั้งกลุ่ม — ผลที่ตามมาคือถ้าโมดูลถูกปิด ผู้ใช้จะ สร้างกลุ่มใหม่จากหน้านี้ได้ แต่ดูรายการกลุ่มที่สร้างไปแล้วไม่ได้ (GET /audiencesคืน 403) และเปิด/ปิด auto refresh กับกดคำนวณใหม่ก็ไม่ได้ เป็นความไม่สอดคล้องที่เกิดจากด่านที่ขาดไปในโมดูลหนึ่ง ไม่ใช่การออกแบบ - เส้นทางเดียวในโมดูลนี้ที่มีการบังคับจริงคือ
POST /audiences-filter/internal/refresh— อยู่บน public group ไม่ผ่าน JWT แต่ครอบด้วย guard เทียบ headerx-internal-keyกับ envINTERNAL_API_KEYถ้า env ว่างจะปฏิเสธทุกคำขอด้วย 401Invalid internal API key GET /audiences-filter/form-response(กล่อง Form data ในตารางตัวอย่าง) เป็นเส้นทางที่บางกว่าที่อื่น — ตรวจแค่ว่ามีlineUserIdและformIdsส่งมา ไม่ได้ตรวจว่าผู้เรียกมีสิทธิ์ดูคำตอบของผู้ใช้คนนั้นเป็นรายบุคคล ความปลอดภัยพึ่งการจำกัดขอบเขตline_oa_idจาก CLS ใน query ที่ดึงฟอร์มเท่านั้น และคืน error เป็น 500 ล้วนเมื่อ query ล้มเหลว (ไม่มีการห่อเป็น AppError)- ฝั่งหน้าเว็บไม่มี
PermissionGuardบนหน้า audience ใด ๆ รวมถึงหน้านี้ ควบคุมได้แค่การซ่อนเมนูข้างด้วยability.can('view', 'audience')(คีย์เมนูย่อยของหน้านี้คือaudience-multi-source) พิมพ์ URL เข้าตรงไม่ถูกกัน - ทุก query ยังถูกจำกัดขอบเขตด้วย
line_oa_idจาก CLS context ทั้งการหารายชื่อของทุก source, การตรวจชื่อซ้ำ และการดึงคำตอบฟอร์ม — ข้อมูลข้าม OA จึงไม่รั่วถึงกัน สิ่งที่ขาดไปคือด่านระดับโมดูล ไม่ใช่ด่าน tenant
ไฟล์อ้างอิงจากซอร์สโค้ด
Frontend (cms-web-2026)
src/app/audience/filter/multi-source/form/page.tsx— จุดเข้าฟอร์ม (รับ?id=,?action=copy,?preset=)src/components/audience/filter/multi-source-filter-form.container.tsx— สถานะhasPreviewed, การตรวจก่อนส่ง, การเติมค่าจาก preset และโหมดคัดลอกsrc/components/audience/filter/multi-source-filter-form.tsx— ชนิด source 7 แบบ, ตัวเชื่อมสองระดับ, ปุ่ม Preview Resultssrc/components/audience/filter/components/source-entry-card.tsx— การ์ด source ทุกชนิดและช่องตั้งค่าทั้งหมดsrc/components/audience/filter/components/multi-source-visualizer.tsx— ไดอะแกรม Venn (เรขาคณิตตามจำนวน source, ตารางสีต่อชนิด)src/components/audience/filter/multi-source-filter-table.tsx— ตารางตัวอย่างผลลัพธ์, แถบ Source breakdown, กล่อง Form datasrc/components/audience/interfaces/multi-source-filter.interface.ts— โครงสร้างMultiSourceFilterFormValues/SourceGroup/SourceFilterEntrysrc/services/audience-filter.service.ts
Backend (line-management-cms-api-go)
internal/modules/audiencefilter/controller.go— เส้นทาง create/preview multi-source, ไม่มี ModuleGate,formResponsePreview,internalApiKeyGuardinternal/modules/audiencefilter/multi_source_query_builder.go—getSourceUserIds(8 ชนิด),combineUserIdSets(กฎ AND/OR และการทิ้งเซตว่าง),keepFollowers,getCustomAttributeUserIds,getLoyaltyUserIdsinternal/modules/audiencefilter/service.go—CreateMultiSourceFilter,PreviewMultiSourceFilter,GetFormResponsePreview, การอ่านfilterInfoกลับมาใช้ตอน refreshinternal/modules/audiencefilter/dto.go— กฎ validation ของgroups/sourcesแบบซ้อนชั้น และเพดานlimitที่ 10internal/modules/audiencefilter/enums.go— enum ชนิด source ทั้ง 9 ค่า (รวมtrigger_ruleที่ไม่มี UI)internal/modules/audiencefilter/form_filter_query_builder.go— ตัวประกอบคิวรีเงื่อนไขฟอร์มที่ใช้ร่วมกับโหมดแหล่งเดียว
ดูรายละเอียดเชิงเทคนิคเพิ่มเติมที่ จัดการกลุ่มเป้าหมาย (Audience Management)