Private Docs

AMLO — 5 เรื่องที่ BU ตีกลับ (รอบ 1) พร้อมต้นเหตุและทางแก้

BU ตีกลับ 5 เรื่องจากหน้าประวัติ หน้าดูไฟล์หลักฐาน และเพดานการแนบไฟล์ — เอกสารนี้บอกว่าแต่ละเรื่องมีต้นเหตุอยู่บรรทัดไหน แก้ที่ฝั่งไหน และมีอะไรที่ต้องยอมรับ

อัปเดต: 2026-09-10

ฉบับที่ 5 (10/09/2026) — จัดโครงใหม่ให้อ่านง่าย เนื้อหาเท่าฉบับ 4 ที่ผ่าน review 4 รอบ เขียนจากโค้ดบน origin/development และข้อมูลจริงบน dev · spec ลงมือทำอยู่ที่ 10 — Implementation Spec

สรุปใน 1 นาที

ข้อBU เห็นอะไรแก้ที่ไหนทางที่เลือก
1ปลดทั้งบริษัทแล้วประวัติขึ้น 2 แถวbackendผูกแถวลูกกับแถวแม่ด้วย CorrelationId แล้วซ่อนแถวลูก — ไม่กลับมติ Owner 04/09
2ประวัติแสดง email แทนชื่อ-นามสกุลbackendresolve ชื่อตอนอ่านจาก UsersEmployees → ตกกลับ email
3ไฟล์หลักฐานชื่อ “ไฟล์ 1 / ไฟล์ 2”ทั้งสองฝั่งหน้าจอส่งชื่อไฟล์มาตอนบันทึก backend เก็บลง JSON เดิม
4เปิด PDF แล้วเหมือนค้างหน้าจอยืดสถานะ loading ไปจน iframe วาดเสร็จ
5ขอเพดาน 5 MB ต่อไฟล์ไม่ต้องแก้โค้ดทุกชั้นรับได้แล้ว เหลือตรวจ Kong บน uat ก่อนส่งทดสอบ

ทั้ง 5 ข้อ ไม่มี migration และ ไม่ต้องแก้ IaC (ข้อ 5 อาจต้องแก้ Kong บน uat ซึ่งอยู่นอก Backend_Iac)


ข้อ 1 — ปลดทั้งบริษัทแล้วได้ 2 แถว

ต้นเหตุ

การกดปลดล็อกทั้งบริษัทหนึ่งครั้ง เขียนลงตาราง AmloAuditLogs สองชนิดพร้อมกัน:

  • AdminUnblockCompanyHandler.cs:177 เขียนแถว CompanyUnblocked หนึ่งแถว — แถวที่ BU ต้องการ
  • loop ที่ AdminUnblockCompanyHandler.cs:193 เขียนแถว UserUnblockGranted เพิ่มอีก หนึ่งแถวต่อสมาชิกที่ Active

จำนวนแถวส่วนเกินจึงเท่ากับจำนวนสมาชิก ไม่ใช่ 1 เสมอ — บริษัทที่มีสมาชิก 10 คนจะได้ 11 แถวต่อการกดหนึ่งครั้ง

ตัวอย่างจาก dev (แถวเวลาเดียวกันเป๊ะจับคู่กันเสมอ):

เวลา (UTC)บริษัทแถวที่เกิดขึ้น
08/09 07:35:135807711CompanyUnblocked + UserUnblockGranted × 1
07/09 03:49:052678283CompanyUnblocked + UserUnblockGranted × 1
06/09 04:31:352534759CompanyUnblocked + UserUnblockGranted × 2

ทำไมตัด loop ทิ้งไม่ได้

comment ที่ AdminUnblockCompanyHandler.cs:173 ระบุว่าเป็น มติของ Owner เมื่อ 04/09 ให้เขียนทั้งสองแถว — แถวบริษัทถือหลักฐานและเหตุผล ส่วนแถวรายคนมีไว้ให้ trail ยังบอกได้ว่า “ใครบ้างที่ถูกปลด” การตัด loop คือการกลับมติ ไม่ใช่การแก้บั๊ก

ทางแก้ที่เลือก — ผูกแถวลูกกับแถวแม่ด้วย CorrelationId

  1. สร้างแถว CompanyUnblocked ก่อน แล้วเก็บ Id ไว้ (Id ถูกกำหนดด้วย Guid.NewGuid() ตั้งแต่ตอนสร้าง object ที่ AmloAuditLog.cs:70 จึงไม่ต้อง SaveChanges คั่นกลาง)
  2. ส่ง Id.ToString() เป็น correlationId ให้ทุกแถวใน loop (AmloAuditLog.Create รับ parameter นี้อยู่แล้ว แต่ยังไม่มี caller ไหนส่ง — นั่นคือเหตุที่คอลัมน์นี้เป็น NULL ทุกแถว · ชนิด string? ยาวไม่เกิน 100 ตาม AmloAuditLogConfiguration.cs:66-67)
  3. ฝั่งอ่านแสดงเฉพาะแถวแม่ ส่วนรายชื่อสมาชิกดึงมาแสดงประกอบแถวแม่

ได้ทั้งสองอย่าง: BU เห็นแถวเดียว และมติ 04/09 ยังอยู่ครบ

กับดักที่ต้องเลี่ยงตอนทำ

  • ต้องกรองที่ระดับ query ไม่ใช่หลังดึงมาGetUnblockHistoryPagedAsync ทำ CountAsync แล้ว Skip/Take บนแถวดิบ (AmloAuditLogRepository.cs:56-61) ถ้ายุบทีหลังในหน่วยความจำ Total จะเกินจริง และหน้าที่ขอ 10 รายการ อาจเหลือ 1 → ต้องใส่เงื่อนไข CorrelationId IS NULL ใน baseQuery ก่อนทั้ง Count และ Skip/Take
  • ต้องยุบทั้งสองหน้า — หน้าประวัติรวม (GetAmloUnblockHistoryAll) และหน้าประวัติรายบริษัท (GetAmloUnblockHistory, AdminAmloController.cs:105) อ่านแถวชุดเดียวกัน ยุบไม่เท่ากัน BU จะงงกว่าเดิม
  • ยุบเฉพาะแถวที่มี CorrelationId ตรงกับแถวแม่จริง ห้ามยุบตามเวลาที่ตรงกัน
  • การเปิดดูรายชื่อสมาชิกจากแถวแม่ต้องเพิ่ม field ใน DTO (เป็นการเพิ่ม ไม่ลบของเดิม client เก่าไม่พัง)
  • search ค้นจาก CompanyCustCode และ ActorEmail ซึ่งแถวลูกถือค่าเดียวกับแถวแม่ การซ่อนแถวลูกจึงไม่เสียความสามารถค้นหา

สิ่งที่ต้องยอมรับ

  • แถวเก่าบน dev แก้ไม่ได้ — เหตุการณ์ปลดทั้งบริษัทที่เกิดแล้ว (query เมื่อ 10/09 ได้ 5 เหตุการณ์ ยังไม่ verify ซ้ำ) เขียนไปโดยไม่มี CorrelationId ตารางเป็น append-only ไม่ backfill จะยังแสดงแยกแถวต่อไป
  • ยังมีแถว UserUnblockGranted ที่ไม่ได้มาจากการปลดทั้งบริษัทAmloExistingGrantJoinAuditor.cs:49 เขียนแถวนี้ ทุกครั้งที่สมาชิกใหม่เข้าบริษัทที่มีสิทธิ์ปลดล็อกค้างอยู่ (actor system:amlo-existing-grant) แถวพวกนี้ไม่มีแถวแม่ และต้องแสดงต่อไปตามปกติ
  • บน origin/development ไม่มี command ปลดรายคนแล้ว (AdminUnblockUser เหลือแค่บน origin/uat) ⇒ ข้อกังวลว่า “แยกไม่ออกว่าแถวไหนมาจากการปลดรายคน” ใช้กับแถวที่เขียนก่อน 04/09 เท่านั้น
  • UAT ถูก promote เมื่อ 31/08 ก่อนมีฟีเจอร์ปลดทั้งบริษัท จึงไม่มีแถวซ้ำให้จัดการ

ข้อ 2 — เปลี่ยน email เป็นชื่อ-นามสกุล

ต้นเหตุ

backend ส่งเฉพาะ email — GetAmloUnblockHistoryAllDtos.cs มี ActorEmail ไม่มี field ชื่อคน หน้าจอจึงไม่มีอะไรให้แสดง

สิ่งที่รู้แน่และสิ่งที่ยังไม่รู้

  • ผู้กดมีแถวใน Users แน่นอน — ตัวตนมาจาก Redis ที่ warm ด้วย GetUserByAzureObjectIdWithCacheDetailsAsync ซึ่งอ่านตาราง Users (UserCacheRefreshService.cs:98) และ handler ปฏิเสธถ้า resolve ไม่ได้ด้วย AMLO_ACTOR_UNRESOLVED (AdminUnblockCompanyHandler.cs:56-58) การปลดเมื่อ 08/09 ที่สำเร็จเป็นหลักฐานในตัว
  • ยังไม่ verify ว่าชื่อไทยถูกกรอกไว้User.cs:161 ยอมให้ FirstNameTH / LastNameTH ว่างได้ (เส้นทาง OTP สร้าง ผู้ใช้โดยไม่มีชื่อไทย) ต่อฐานข้อมูล dev จากเครื่องไม่ได้ในรอบนี้

ทางแก้ที่เลือก

เพิ่ม field ชื่อผู้ดำเนินการใน DTO แล้ว resolve ตอนอ่านด้วย batch เดียวกับที่ handler ใช้กับ “ผู้ถูกปลด” อยู่แล้ว (GetUsersByIdsAsync) ตามลำดับ Users.FirstNameTH/LastNameTHEmployees.EmpFirstNameTH/EmpLastNameTH (Employee.cs:11-16 join EmpEmail = ActorEmail) → ตกกลับแสดง email ไม่พังไม่ว่าผลจะออกทางไหน

  • คง ActorEmail ใน DTO (ไม่ถอด — การถอดคือเปลี่ยนสัญญา API) หน้าจอเลิกแสดงคอลัมน์ email แทน
  • แถวจากงานเบื้องหลัง (system:amlo-grant-expiry-job, system:amlo-existing-grant) ไม่มีผู้ใช้ให้ resolve หน้าจอต้องมีข้อความสำรอง ไม่ปล่อยช่องว่าง
  • ไม่ต้องแก้โครงตาราง ไม่มี migration

ข้อ 3 — ชื่อไฟล์จริงแทน “ไฟล์ 1 / ไฟล์ 2”

ต้นเหตุ

ชื่อไฟล์จริง มีอยู่ในระบบแล้ว — บริการไฟล์คืนได้สองทาง: header Content-Disposition ตอนดาวน์โหลด (FileManageController.cs:157) และ GET {fileId}/metadata ที่คืน OriginalFileName (:227)

ที่ขาดคือ AmloAuditLogs.EvidenceFileRefs เก็บ JSON ของ id ล้วน (AdminUnblockCompanyHandler.cs:115) หน้าประวัติได้แต่ id จึงตั้งชื่อเอง:

  • amlo-history.ts:110 สร้าง label ไฟล์ {ลำดับ} — คือ “ไฟล์ 1 / ไฟล์ 2” ที่ BU เห็น
  • evidence-viewer.ts:209 ตอนดาวน์โหลดตั้งชื่อจาก id + นามสกุลที่เดาจาก content type

ทางที่พิจารณาแล้วไม่เลือก

  • หน้าจออ่าน Content-Disposition — ใช้กับปุ่มดาวน์โหลดได้ แต่ป้ายในรายการต้องรู้ชื่อทุกไฟล์ก่อนกด (ต้องดาวน์โหลดทุกไฟล์ล่วงหน้า) และยังไม่ verify 2 เรื่อง: ไม่พบ Access-Control-Expose-Headers ใน src/apim/ และหน้าจอเรียกด้วย responseType: 'blob' ไม่มี observe: 'response' (amlo.service.ts:203) จึงไม่เห็น header
  • handler เรียก /metadata — client มีอยู่แล้ว (section FileService ที่ appsettings.json:129-133, AddHttpClient<IGetProfilePhotoClient> / IUploadProfilePhotoClient ที่ DependencyInjection.cs:355-372) แต่เป็นการยิง network กลาง transaction ของการปลดล็อก — บริการไฟล์ช้าหรือล่ม การปลดล็อกจะพังตาม แลกกับป้ายชื่อไฟล์ ยังไม่คุ้ม · ถ้าวันหน้าต้องการชื่อระดับหลักฐาน ให้ถามชื่อให้เสร็จก่อนเปิด transaction แล้วเขียนทีเดียว ห้ามเติมทีหลัง — ตารางนี้ append-only จริง: IAmloAuditLogRepository มีแต่ AddAsync กับ method อ่าน และ property ข้อมูล audit ทุกตัวเป็น private set การเปิดทางแก้แถวย้อนหลังคือเปลี่ยนนโยบาย ต้องขออนุมัติแยก
  • หมายเหตุเส้นทาง: บน cluster IaC overlay ตั้ง FileService.BaseUrl เป็น internal DNS (Backend_Iac/config/user-service/{env}/appsettings.json) แต่ base appsettings.json:130 และ appsettings.Uat.json ใน repo ชี้ APIM ⇒ รันจากเครื่องจะวิ่งผ่าน APIM · src/apim/ ไม่มีไฟล์ filemanagement ถ้าจะให้หน้าจอเรียก /metadata เองต้องเปิด operation เพิ่ม

ทางแก้ที่เลือก — หน้าจอส่งชื่อไฟล์มาตอนบันทึก

หน้าจอมี originalFileName จาก response ของการอัปโหลดอยู่แล้ว (amlo.model.ts:142) จึงส่งมาพร้อม id ตอนกดปลดล็อก แล้ว backend เก็บ EvidenceFileRefs เป็น [{ id, name }] แทน [id]

สิ่งที่ต้องยอมรับและต้องเขียนใน PR ให้ชัด:

  • เป็นการเปลี่ยนสัญญา API — request เดิมรับแค่ EvidenceFileIds ต้องเพิ่ม field ใหม่
  • ชื่อไฟล์เป็นค่าที่ client ส่ง ถือเป็นป้ายกำกับ ไม่ใช่หลักฐาน — ความน่าเชื่อถือของ audit ยังอยู่ที่ fileId กับ fingerprint และตอนดาวน์โหลดจริงชื่อยังมาจากบริการไฟล์
  • ต้องมี validation ฝั่ง backend — validator ปัจจุบันตรวจแค่ EvidenceFileIds (AdminUnblockCompanyValidator.cs:27) อย่างน้อยจำกัดความยาว (หน้าจอกัน 255 ที่ evidence-file-validation.ts:21 แต่หน้าจอไม่ใช่ด่าน) และตัดอักขระ path ก่อนเก็บ
  • คอลัมน์เป็น jsonb อยู่แล้ว ไม่ต้องเพิ่มคอลัมน์ ไม่มี migration
  • ฝั่งอ่านมี 2 handler ต้องรองรับทั้งสองรูปแบบ: GetAmloUnblockHistoryAllHandler.cs:76 และ GetAmloUnblockHistoryHandler.cs:61 — แถวเก่าเป็น id ล้วน ไม่ backfill จะยังแสดง “ไฟล์ 1 / ไฟล์ 2” ต่อไป ซึ่งถูกต้องแล้ว (AmloExistingGrantJoinAuditor.cs:65 เขียน evidenceFileRefs: null ไม่กระทบ)
  • หน้าจอต้องแก้สองจุดคู่กันเสมอ: ชื่อในรายการ และชื่อตอนกดดาวน์โหลด

ข้อ 4 — ตัวบอกสถานะระหว่างเปิด PDF

ต้นเหตุ

หน้าต่างดูไฟล์มีข้อความ “กำลังเปิดไฟล์…” อยู่แล้ว (evidence-viewer.html:39) แต่ปิดสถานะที่ evidence-viewer.ts:198 (busy.set(false)) ทันทีที่ดาวน์โหลดเสร็จ ไม่ได้รอให้ iframe วาด PDF เสร็จ และไม่มี handler ดัก load ของ iframe ช่วงที่ลูกค้าเข้าใจว่าค้างคือช่วงหลังจากนั้น — ไฟล์มาถึงแล้ว แต่จอยังว่างเปล่า

ทางแก้ที่เลือก

ยืดสถานะ loading ไปจน iframe แจ้งว่าวาดเสร็จ จึงค่อยซ่อน — งานฝั่งหน้าจอล้วน ไม่แตะ backend การตรวจรับต้องเห็นด้วยตาบนหน้าจอจริง ว่าตัวหมุนขึ้นระหว่างรอ — เทสที่ยืนยันแค่ตัวแปรเปลี่ยนค่าไม่ใช่หลักฐานว่าผู้ใช้เห็น


ข้อ 5 — เพดานแนบไฟล์ 5 MB ต่อไฟล์

ไม่ต้องแก้โค้ด — ทุกชั้นของแอปรับได้แล้ว

  • หน้าจอ: 5,000,000 ไบต์ต่อไฟล์ (evidence-file-validation.ts:19-20) สูงสุด 10 ไฟล์ (amlo-cases.ts:54) และอัปโหลดทีละไฟล์ต่อหนึ่ง request (amlo.service.ts:167-169) จำนวนไฟล์ไม่ทบกัน
  • บริการไฟล์: หมวด amlo-evidence เพดานไฟล์ 10 MiB · เพดาน request 12 MiB (ObservabilityExtensions.cs:49)
  • dev: ingress-nginx ค่าเริ่มต้น client_max_body_size 1m (1,048,576 ไบต์) ตอนนี้ตั้ง "12m" แล้วเมื่อ 09/09 (236fc85f) ยืนยันบน cluster ว่าอ่านค่าได้จริง

จุดที่ต้องระวัง — uat ใช้ Kong ไม่ใช่ nginx

envingress classวิธีตั้งเพดาน body
devnginx-internal (ingress-nginx)annotation nginx.ingress.kubernetes.io/proxy-body-size
uatkong-uat (Kong)annotation ของ nginx ไม่มีผล — ต้องใช้ทางของ Kong
  • uat: src/yamls/uat/superapp/ingresses/filemanagement-service-ingress.yaml ใช้ ingressClassName: kong-uat มี annotation แค่ konghq.com/strip-path: "false"ห้ามเอา annotation ของ nginx ไปแปะ Kong ไม่อ่าน
  • Kong เป็น Helm release kong-uat ที่ namespace kong-system (deployment kong-uat-kong, 2 replica) ไม่ได้ถูกจัดการโดย Backend_Iac · ทั้ง cluster ไม่มี KongPlugin · deployment ไม่มี env เรื่อง body size · src/yamls/uat/ ไม่มีคำว่า client_max_body_size / request-size-limiting / allowed_payload_size
  • ยังไม่ verify: ค่า client_max_body_size ที่ Kong ใช้จริง (การเชื่อมต่อ cluster หลุด) ⇒ ยังสรุปไม่ได้ว่า 5 MB ผ่าน uat หรือไม่ Kong หลายรุ่น default ไม่จำกัด ถ้าใช่ก็ไม่ต้องแก้

สิ่งที่ต้องทำก่อนส่ง UAT ทดสอบ (สลับลำดับไม่ได้)

  1. อ่านค่าจริงจาก Kong: kubectl -n kong-system exec deploy/kong-uat-kong -c proxy -- sh -c "grep -r client_max_body_size /usr/local/kong/"
  2. ถ้าไม่จำกัด → ไม่ต้องแก้ บันทึกไว้ว่าตรวจแล้ว
  3. ถ้าจำกัดต่ำกว่าที่ต้องใช้ → ผูก KongPlugin ชนิด request-size-limiting กับ ingress ตัวนี้ (annotation konghq.com/plugins) หรือแก้ Helm values ของ release kong-uat ซึ่งอยู่นอก Backend_Iac — ต้องประสานคนดูแล Kong

ห้ามสรุปว่า uat ปลอดภัยเพราะ dev ผ่านแล้ว สอง env ไม่ได้ใช้ตัวเดียวกัน


สถานะ branch และ PR (10/09/2026)

ส่วนbranchสภาพ
หน้าจอ Adminfix/amlo-v2 (Frontend_AdminSuperApp)มีงานของ dev อีกท่าน 1 commit (f752571) ตามหลัง development 18 commit
backendfix/amlo-v2 (Backend_UserService)ไม่มี commit ของตัวเอง ตามหลัง development 16 commit
  • f752571 จัดระเบียบข้อความและโครงโค้ดหน้า AMLO ทั้งชุด แตะไฟล์เดียวกันกับที่งานรอบนี้ต้องแก้ (หน้าประวัติ ตัวแสดงไฟล์ หน้า cases) → ต้อง merge development เข้า fix/amlo-v2 ก่อนเริ่ม ไม่งั้นชนแน่นอน
  • branch ที่มีคำว่า amlo ใน Frontend_AdminSuperApp 19 ตัว — 17 ตัวเนื้อหาอยู่ใน development ครบแล้ว (ยังไม่ถูกลบ ยังอยู่บน origin ทั้งหมด) ที่มีของค้างจริง 2 ตัว: fix/amlo-v2, fix/uat/amlo-eslint-selector-directive
  • Frontend_AdminSuperApp มี PR Active 5 ใบ: 3060, 3028, 2888, 2878, 2374
    • #2878 (feature/amlo-matched-roles-casesdevelopment) และ #2888 (feature/amlo-name-view-modalfeature/amlo-matched-roles-cases) เนื้อหาเข้า development แล้วทั้งคู่ (commit ส่วนต่าง 0) ปิดได้ branch ต้นทางยังอยู่บน origin
    • #2374 ไม่ใช่งาน AMLO — Owner จอดไว้ส่งต่อเจ้าของ repo
    • #3060 / #3028 ยังไม่ได้ตรวจ
  • ถอนข้อความฉบับก่อน ที่ว่า #2724 มี +18 commit ห้ามปิด — หาไม่เจอใน Frontend_AdminSuperApp, Backend_UserService, Backend_Centralized, Frontend_HostAppSuperApp, Backend_Iac

สิ่งที่ยังไม่ได้ยืนยัน

  • ค่า client_max_body_size ที่ Kong ใช้จริงบน uat (ข้อ 5)
  • FirstNameTH / LastNameTH ของบัญชีพนักงานถูกกรอกไว้จริงหรือไม่ (ข้อ 2)
  • ข้ออ้างจากฐานข้อมูล (CorrelationId เป็น NULL ทุกแถว · 5 เหตุการณ์เก่าบน dev · ชื่อผู้ดำเนินการ) ยังไม่ได้ query ซ้ำ เพราะต่อ PG ของ dev จากเครื่องไม่ได้ — เรื่อง CorrelationId สอดคล้องกับโค้ดอยู่แล้วเพราะไม่มี caller ส่งค่า
  • APIM เปิด operation /metadata ของบริการไฟล์ให้ Admin หรือไม่ และมี Access-Control-Expose-Headers หรือไม่ (ข้อ 3 — ไม่กระทบทางที่เลือก)
  • ยังไม่มีใครกดดูผลของทั้งห้าข้อบนหน้าจอจริง ตัวเลขและบรรทัดในเอกสารนี้มาจากโค้ดกับฐานข้อมูล