7.2 KiB
sub-implementer-backend-5 — WAVE 5 BE: vá F1 + F2 (2 FLAG HIGH mất dữ liệu)
Ghi TRONG LÚC LÀM, flush từng phần, tự
wc -ckiểm byte thật.
- Ngày: 2026-07-27 · run
2026-07-27-S155-pe-delete-approver - Vật sửa:
src/Backend/SolutionErp.Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs - Nền đo trước wave:
dotnet build0W/0E ·dotnet test549 PASS / 0 FAIL
§0 — ĐỌC NGUYÊN VĂN F1 + F2 (reviewer-diff-dot1.md:33-80)
F1: PurgeOrphanOpinionsAsync (:886-918) dùng ExecuteDeleteAsync = auto-commit NGAY khi
không có transaction ambient. Gọi tại :681-683, SaveChangesAsync mãi :822. 4 nguồn ném
nằm GIỮA (ResolveUserNamesAsync:686 round-trip · Remove():771 severed-association ·
db.PurchaseEvaluations…ToListAsync:794-800 round-trip · ghi changelog N phiếu trong SaveChanges).
Tiêu chí nghiệm thu: test giả lập SaveChanges ném SAU purge ⇒ số row opinion KHÔNG đổi.
F2: điều kiện purge :889-891 (+6 khối lặp tới :915) rộng hơn nhãn "mồ côi" ở CẢ HAI vế —
(a) !db.PurchaseEvaluations.Any(p => p.Id == … && !p.IsDeleted) coi phiếu XOÁ MỀM ngang phiếu
không tồn tại; (b) o.IsDeleted — opinion xoá mềm trên phiếu CÒN SỐNG = bản ghi kiểm toán.
Phương án (a) của reviewer: chỉ purge khi phiếu cha thật sự KHÔNG CÒN ROW
(IgnoreQueryFilters().Any(...)), phiếu xoá mềm thì CHẶN admin như phiếu sống.
§1 — ĐO NỀN TRƯỚC KHI SỬA (không tin doc, chạm đĩa)
| Sự thật | Bằng chứng |
|---|---|
IApplicationDbContext KHÔNG expose Database |
grep -n "Database|BeginTransaction" src/.../Common/Interfaces/IApplicationDbContext.cs → 0 hit |
Handler đã có tiền lệ ép kiểu ((DbContext)db) |
ApprovalWorkflowV2AdminFeatures.cs:941 (trong DetachTrackedOpinionsOn) |
| Tiền lệ transaction repo | ContractCodeGenerator.cs:35-59 — var context = (DbContext)db; await using var tx = await context.Database.BeginTransactionAsync(IsolationLevel.Serializable, ct); + try/Commit/catch/Rollback |
Microsoft.EntityFrameworkCore.Relational CÓ trong Application |
SolutionErp.Application.csproj — <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.6" /> |
7 bảng *LevelOpinion KHÔNG có global query filter |
grep -rn "HasQueryFilter" Configurations/ → chỉ 1 hit PurchaseEvaluationConfiguration.cs:84 (bảng CHA). ⇒ vế o.IsDeleted trong purge là THẬT (không phải dead code) |
| FK opinion→cha = Cascade, opinion→Level = Restrict | PurchaseEvaluationLevelOpinionConfiguration.cs:20-28 |
| SQLite fixture BẬT FK enforcement thật | test cũ UpdateAwDefinitionTests.cs:562-565 assert DbUpdateException khi Remove(level) có opinion ⇒ FK Restrict fire thật trong SQLite |
| Fixture SQLite hỗ trợ transaction | SqliteDbFixture.cs:36-40 comment + UseSqlite(shared connection) |
Hệ quả SUY RA TỪ ĐO (quan trọng cho thiết kế)
FK cha→opinion là Cascade ⇒ xoá cứng phiếu cha thì opinion cũng đi theo ⇒ "mồ côi thật"
(cha KHÔNG CÒN ROW) gần như không sinh ra được qua đường ứng dụng. Nghĩa là sau khi thu hẹp
theo F2, PurgeOrphanOpinionsAsync gần như là lớp phòng-vệ-chiều-sâu cho dữ liệu lệch sinh
ngoài luồng (sửa DB tay / import / FK tắt), KHÔNG còn là đường chạy thường ngày. Đúng ý F2:
guard chặn HẾT những gì còn cha, purge chỉ dọn thứ đã không thể khôi phục.
§2 — VÁ F1 (rào giao dịch) — ĐÃ LAND
Kỹ thuật chọn: await using var tx + CommitAsync cuối, KHÔNG try/catch — đúng khuôn nhà
(LeaveOtApprovalFeatures.cs:34-44, ProposalFeatures.cs:386, TravelVehicleApprovalFeatures.cs:822).
Dispose của IDbContextTransaction rollback nếu chưa Commit ⇒ diff là CHÈN-THÊM, 0 dòng thân
method bị thụt lề lại (nếu bọc try/catch thì ~150 dòng phải re-indent = nhiễu review, khó soi).
- Mở rào: ngay TRƯỚC
PurgeOrphanOpinionsAsync(thao tác không-hoàn-tác đầu tiên). Guard phía trên chỉ ĐỌCAsNoTrackingrồi ném, không cần nằm trong rào; mở sớm hơn chỉ kéo dài giữ khoá. - KHÔNG dùng
IsolationLevel.Serializablenhư 4 code-generator: ở đó Serializable chống ĐUA cấp-số; ở đây yêu cầu là NGUYÊN TỬ, siết isolation chỉ làm màn quản trị dễ deadlock. ExecuteDeleteAsynctự enlist vào transaction đang mở của context ⇒ 7 câu DELETE nay cùng sống/chết vớiSaveChangesAsync.
§3 — VÁ F2 (thu hẹp purge + chặn hồ sơ xoá mềm) — ĐÃ LAND
Phân hoạch KÍN, không còn kẽ (đây là điểm thiết kế chính):
| Trạng thái chữ ký | Xử lý sau vá | Ai lo |
|---|---|---|
| cha SỐNG + ý kiến SỐNG | chặn 409 (thông điệp CŨ, giữ nguyên) | CountLiveOpinionsAsync (tầng 1) |
| cha XOÁ MỀM (row còn) | chặn 409 + nêu TÊN hồ sơ ← MỚI | ListRetainedOpinionsAsync (tầng 2) |
| ý kiến XOÁ MỀM, cha sống | chặn 409 + nêu TÊN hồ sơ ← MỚI | ListRetainedOpinionsAsync (tầng 2) |
| cha KHÔNG CÒN ROW | purge (xoá cứng) — đường duy nhất còn lại | PurgeOrphanOpinionsAsync |
- Áp CẢ 7 bảng chữ ký (PE · HĐ · Đề xuất · Nghỉ phép · Tăng ca · Công tác · Đơn xe), tầng 2
JOIN sang cha để lấy mã hồ sơ:
MaPhieu·MaHopDong·MaDeXuat·MaDonTu×4. - Guard tầng 2 áp cho cả
toRemovelẫnapproverChanged— reviewer viết "chặn admin như phiếu sống", mà phiếu sống thì cả bỏ-người lẫn đổi-người đều bị chặn. Đổi người trên dòng còn giữ chữ ký của hồ sơ xoá mềm = khi khôi phục, ý kiến người A hoá thành của người B. IgnoreQueryFilters()BẮT BUỘC ở tầng 2 và ở purge: PE/Contract có global filter!IsDeleted(PurchaseEvaluationConfiguration.cs:84) — không tắt thì hồ sơ xoá mềm biến mất khỏi JOIN và ta lại kết luận "mồ côi" = đúng cái sai đang chữa.- Sửa luôn dòng changelog
"Dọn N chữ ký mồ côi (thuộc phiếu đã xoá)"→"(hồ sơ mang chữ ký đã KHÔNG CÒN trong hệ thống)"— nhãn cũ nay MÔ TẢ SAI việc đang làm.
Đo ngay sau khi vá (trước khi đụng test)
dotnet build SolutionErp.slnx → 0 Warning / 0 Error.
dotnet test --filter UpdateAwDefinitionTests → 14 PASS / 1 FAIL. Đúng 1 fail, đúng cái phải
fail: F14_F5a_OpinionBelongsToDeletedPe_RemovalAllowed_AndOrphanPurged — test này khẳng định
CHÍNH hành vi mà F2 gọi là sai ("phiếu xoá mềm ⇒ cho gỡ + purge chữ ký"). Thông điệp thật nhận
được: … còn 1 chữ ký/ý kiến đang LƯU, thuộc: Phiếu duyệt NCC PE-F14b-001 — đã xoá …
⇒ chứng minh 3 việc cùng lúc: guard tầng 2 CHẠY · IgnoreQueryFilters() DỊCH ĐƯỢC trên SQLite ·
thông điệp NÊU ĐÚNG TÊN hồ sơ. 0 test nào khác đỏ ⇒ vá không lan.