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 แทนชื่อ-นามสกุล | backend | resolve ชื่อตอนอ่านจาก Users → Employees → ตกกลับ 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:13 | 5807711 | CompanyUnblocked + UserUnblockGranted × 1 |
| 07/09 03:49:05 | 2678283 | CompanyUnblocked + UserUnblockGranted × 1 |
| 06/09 04:31:35 | 2534759 | CompanyUnblocked + UserUnblockGranted × 2 |
ทำไมตัด loop ทิ้งไม่ได้
comment ที่ AdminUnblockCompanyHandler.cs:173 ระบุว่าเป็น มติของ Owner เมื่อ 04/09 ให้เขียนทั้งสองแถว —
แถวบริษัทถือหลักฐานและเหตุผล ส่วนแถวรายคนมีไว้ให้ trail ยังบอกได้ว่า “ใครบ้างที่ถูกปลด”
การตัด loop คือการกลับมติ ไม่ใช่การแก้บั๊ก
ทางแก้ที่เลือก — ผูกแถวลูกกับแถวแม่ด้วย CorrelationId
- สร้างแถว
CompanyUnblockedก่อน แล้วเก็บIdไว้ (Idถูกกำหนดด้วยGuid.NewGuid()ตั้งแต่ตอนสร้าง object ที่AmloAuditLog.cs:70จึงไม่ต้องSaveChangesคั่นกลาง) - ส่ง
Id.ToString()เป็นcorrelationIdให้ทุกแถวใน loop (AmloAuditLog.Createรับ parameter นี้อยู่แล้ว แต่ยังไม่มี caller ไหนส่ง — นั่นคือเหตุที่คอลัมน์นี้เป็นNULLทุกแถว · ชนิดstring?ยาวไม่เกิน 100 ตามAmloAuditLogConfiguration.cs:66-67) - ฝั่งอ่านแสดงเฉพาะแถวแม่ ส่วนรายชื่อสมาชิกดึงมาแสดงประกอบแถวแม่
ได้ทั้งสองอย่าง: 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เขียนแถวนี้ ทุกครั้งที่สมาชิกใหม่เข้าบริษัทที่มีสิทธิ์ปลดล็อกค้างอยู่ (actorsystem: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/LastNameTH → Employees.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 มีอยู่แล้ว (sectionFileServiceที่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) แต่ baseappsettings.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
| env | ingress class | วิธีตั้งเพดาน body |
|---|---|---|
| dev | nginx-internal (ingress-nginx) | annotation nginx.ingress.kubernetes.io/proxy-body-size |
| uat | kong-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ที่ namespacekong-system(deploymentkong-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 ทดสอบ (สลับลำดับไม่ได้)
- อ่านค่าจริงจาก Kong:
kubectl -n kong-system exec deploy/kong-uat-kong -c proxy -- sh -c "grep -r client_max_body_size /usr/local/kong/" - ถ้าไม่จำกัด → ไม่ต้องแก้ บันทึกไว้ว่าตรวจแล้ว
- ถ้าจำกัดต่ำกว่าที่ต้องใช้ → ผูก
KongPluginชนิดrequest-size-limitingกับ ingress ตัวนี้ (annotationkonghq.com/plugins) หรือแก้ Helm values ของ releasekong-uatซึ่งอยู่นอกBackend_Iac— ต้องประสานคนดูแล Kong
ห้ามสรุปว่า uat ปลอดภัยเพราะ dev ผ่านแล้ว สอง env ไม่ได้ใช้ตัวเดียวกัน
สถานะ branch และ PR (10/09/2026)
| ส่วน | branch | สภาพ |
|---|---|---|
| หน้าจอ Admin | fix/amlo-v2 (Frontend_AdminSuperApp) | มีงานของ dev อีกท่าน 1 commit (f752571) ตามหลัง development 18 commit |
| backend | fix/amlo-v2 (Backend_UserService) | ไม่มี commit ของตัวเอง ตามหลัง development 16 commit |
f752571จัดระเบียบข้อความและโครงโค้ดหน้า AMLO ทั้งชุด แตะไฟล์เดียวกันกับที่งานรอบนี้ต้องแก้ (หน้าประวัติ ตัวแสดงไฟล์ หน้า cases) → ต้อง mergedevelopmentเข้า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-cases→development) และ #2888 (feature/amlo-name-view-modal→feature/amlo-matched-roles-cases) เนื้อหาเข้าdevelopmentแล้วทั้งคู่ (commit ส่วนต่าง 0) ปิดได้ branch ต้นทางยังอยู่บน origin - #2374 ไม่ใช่งาน AMLO — Owner จอดไว้ส่งต่อเจ้าของ repo
- #3060 / #3028 ยังไม่ได้ตรวจ
- #2878 (
- ถอนข้อความฉบับก่อน ที่ว่า #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 — ไม่กระทบทางที่เลือก) - ยังไม่มีใครกดดูผลของทั้งห้าข้อบนหน้าจอจริง ตัวเลขและบรรทัดในเอกสารนี้มาจากโค้ดกับฐานข้อมูล