Phân tích luồng sau chuyển — warehouse/create — vòng 2
Kết quả nguyên văn của agent
logic-reviewer(phần sau dòng=== PHÂN TÍCH LUỒNG ===).
F-01 Tạo kho mới (id không gửi)
- Điểm vào: cũ
easyposbackend1:src/main/java/vn/softdreams/easypos/web/rest/WarehouseResource.java:52-57→ mớisrc/main/java/com/softdreams/easypos/warehouse/presentation/web/warehouse/WarehouseController.java:50-58 - Quy tắc: R-01, R-02, R-03, R-04, R-05, R-06, R-07, R-08, R-09, R-10, R-11, R-12, R-15, R-16
Sơ đồ luồng mới (theo code thực tế)
sequenceDiagram
participant C as Client
participant WC as WarehouseController.createWarehouse
participant WS as WarehouseManagementService.saveWarehouse (@Transactional)
participant CC as CurrentCompanyService.requireCompanyOfCurrentUser
participant WR as WarehouseRelationalRepository
participant SQ as SequenceService.nextCode
participant SS as SequenceSeedJdbcRepository.findSeed
participant LR as LegacyRedisSequenceReader
participant RC as RedisSequenceCounter
participant W as Warehouse / WarehouseDuplicatePolicy
C->>WC: POST /create
WC->>WC: Guard.validateInput(validator, request)
WC->>WS: saveWarehouse(toSaveCommand(request))
WS->>CC: requireCompanyOfCurrentUser(comId)
WS->>WR: lockCompanyWarehouses (SELECT TOP 1 + sp_getapplock, 10s)
WS->>WS: WarehouseAudit(findUserId().orElse(0), now(clock))
loop generateCode ≤ 10
WS->>SQ: nextCode(comId,"WH")
SQ->>RC: current (HGET {comId}_SEQ_NEW WH)
alt chưa có
SQ->>SS: findSeed → warehouseSeed
SS->>LR: findWarehouseSequence (GET {comId}_SEQ)
SS->>SS: không có → SELECT COUNT(*) warehouse +1
SQ->>RC: initialize (HSET)
else đã có
SQ->>RC: increase (HINCRBY 1)
end
WS->>WR: existsBySku(comId, code)
end
WS->>W: Warehouse.create(command, code, audit) (copyFrom, status=true, cleanText, audit)
WS->>WR: findNameSkus(comId)
WS->>W: WarehouseDuplicatePolicy.validate(rows, command)
WS->>W: validateDefaultWarehouse()
WS->>WR: save → JpaRepository.save(toEntity) (INSERT identity)
WS-->>WC: WarehouseResult
WC-->>C: BaseResponseDTO.ok(WarehouseResponse, SUCCESS_CREATE)
Đối chiếu từng bước
| # | Luồng cũ (repo:path:dòng) |
Luồng mới (path:dòng) |
Kết luận |
|---|---|---|---|
| 1 | Bean Validation baseCommon.validateInput — easyposbackend1:.../web/rest/WarehouseResource.java:54, easyposbase:.../dto/warehouse/WarehouseSaveRequest.java:12-15 |
WarehouseController.java:52, dto/request/WarehouseSaveRequest.java (@NotNull comId, @NotEmpty name) |
KHỚP về mã lỗi; định dạng body khác có chủ đích (D-02) |
| 2 | Không kiểm comId theo token | WarehouseManagementService.java:76 → CurrentCompanyService.java:30-39 |
KHÁC có chủ đích (D-01) |
| 3 | Không khoá | WarehouseManagementService.java:77 → WarehouseRelationalRepository.java:37-49,113-121 |
CHỈ CÓ Ở MỚI (D-05) |
| 4 | genCode: retry 10 lần, genCodeByRedis, existsByComIdAndSKU — easyposbase:.../services/BaseSequenceService.java:51-72,253-289,436-443 |
WarehouseManagementService.java:101-109, SequenceService.java:23-36, RedisSequenceCounter.java:31-44 |
KHỚP (D-06, D-14 GIỮ) |
| 5 | Mốc: {comId}_SEQ.warehouse + 1 (int), không có thì count+1; lỗi count bị nuốt thành "1" — BaseSequenceService.java:266-285,331-335, BaseUserRepositoryImpl.java:35-50 |
SequenceSeedJdbcRepository.java:52-54,62-69, LegacyRedisSequenceReader.java:48-73 |
KHỚP ở nhánh thường; lỗi DB không nuốt (D-03); dữ liệu cũ hỏng → lỗi (D-07); tràn int khác (F-logic-1) |
| 6 | BeanUtils.copyProperties, SKU rỗng → code, status=true, normalizedName(name, request.SKU) — BaseWarehouseServiceImpl.java:182-189 |
Warehouse.java:76-86,154-164, WarehouseText.java |
KHỚP |
| 7 | Làm sạch chuỗi ở @PrePersist (sau kiểm trùng) — CleanTextListener.java:14-34 |
Warehouse.create → cleanText() (Warehouse.java:80,170-178), chạy trước kiểm trùng |
KHỚP về kết quả: kiểm trùng dùng giá trị thô của command, luật WH1 dùng code/status/sellable (không bị làm sạch đổi) |
| 8 | checkDuplicateByComId (không ORDER BY), F-01 trùng đầu tiên → lỗi, SKU trước tên — BaseWarehouseServiceImpl.java:190-205,229-245, BaseWarehouseRepository.java:37-38 |
WarehouseManagementService.java:92,112-120, WarehouseDuplicatePolicy.java:32-59, WarehouseJpaRepository native SELECT id, name, SKU … WHERE com_id |
KHỚP (D-11 GIỮ) |
| 9 | checkDataWarehouseDefault — BaseWarehouseServiceImpl.java:207,219-227 |
Warehouse.java:118-134 |
KHỚP; NPE (sellable null) → SystemException(EXCEPTION_ERROR) 500 (D-03, D-13) |
| 10 | warehouseRepository.save INSERT, audit @CreatedBy/@LastModifiedBy/@LastModifiedDate — BaseWarehouseServiceImpl.java:215, BaseAbstractAuditingEntity.java:31-52 |
WarehouseRelationalRepository.java:125-127, WarehouseJpaMapper.toEntity |
KHỚP (một thời điểm Clock cho cả hai cột thời gian — spec §7) |
| 11 | BaseResultDTO(SUCCESS, SUCCESS_CREATE, true, warehouse) |
WarehouseController.java:53-57, WarehouseResponse |
KHỚP về dữ liệu; reason khác có chủ đích (D-02) |
| 12 | Interceptor kiểm gói dịch vụ — HandleInterceptorCustom.java:86-105 |
(chưa có) | CHỈ CÓ Ở CŨ (D-16) |
Dữ liệu và side effect
| Bảng / topic / cache | Cũ (đọc/ghi, điều kiện) | Mới (đọc/ghi, điều kiện) | Ghi chú |
|---|---|---|---|
warehouse |
đọc: count (khi Redis chưa có WH), exists SKU ≤10 lần, select name,SKU; ghi: INSERT 1 dòng |
đọc thêm SELECT TOP (1) trong lệnh khoá; count qua JdbcTemplate trên DataSource @Primary (cùng transaction); exists, select id,name,SKU; ghi: INSERT 1 dòng, mọi cột tường minh |
Lọc com_id ở mọi câu |
Redis {comId}_SEQ_NEW field WH |
HGET; HSET mốc / HINCRBY 1 mỗi lần thử | như cũ (RedisSequenceCounter, vùng lõi không đổi) |
Không hoàn số khi rollback (D-08). Hash key serializer mặc định (JDK) giống nhau ở cả hai bên: backend:config/RedisConfig.java:52-67 và starter-config:RedisAutoConfiguration.java:89-92 |
Redis {comId}_SEQ |
GET (Jackson) khi chưa có WH |
GET byte thô + readTree |
D-07; F-logic-1 |
MSSQL applock warehouse:save:{comId} |
— | giữ tới hết transaction | D-05 |
| Kafka / outbox / cache | không | không | — |
Transaction, khoá, idempotency, tenant
- Ranh giới transaction: cũ: class
@TransactionalBaseWarehouseServiceImpl.java:66bao sinh mã, kiểm trùng và INSERT → mới:@TransactionalởWarehouseManagementService.java:74, bao khoá, đọc và INSERT. Controller gọi qua proxy, không có self-invocation. Redis nằm ngoài transaction ở cả hai bên. - Khoá / cập nhật nguyên tử: cũ không khoá → mới dùng
sp_getapplockExclusive, owner Transaction, timeout 10s (kết quả < 0 → 500). Khoá chỉ bảo vệ giữa các request của service; monolith không lấy khoá này (D-05). - Idempotency, thời điểm phát event: không có event; REST đồng bộ.
- Lọc
comId: có ở mọi truy vấn (count, exists, findNameSkus, lệnh khoá) và ở Redis key.
Lệch so với luồng mới đã duyệt (spec mục 6)
Không có lệch thực chất. cleanText() của F-01 chạy trong Warehouse.create (Warehouse.java:80) chứ không chạy ngay trước save như sơ đồ. Kết quả lưu không đổi.
Dev cần chú ý khi review
| Vị trí | Vì sao |
|---|---|
WarehouseRelationalRepository.java:37-49,113-121 |
Khoá chạy được hay không phụ thuộc implicit transaction của mssql-jdbc (auto-commit: false, provider_disables_autocommit: true). Lệnh là batch nhiều câu chạy qua getSingleResult của Hibernate 6. Mới có test mock, cần chạy một lần trên MSSQL thật. |
WarehouseManagementService.java:77-85 |
Thứ tự: khoá → Redis → INSERT. Khoá được giữ suốt thời gian gọi Redis. |
SequenceSeedJdbcRepository.java:62-69, LegacyRedisSequenceReader.java:48-73 |
Nhánh Redis cũ (D-07), lỗi count không nuốt (D-03), tràn int (F-logic-1). |
Warehouse.java:118-134 |
WH1 + sellable null → 500 EXCEPTION_ERROR. Lưu ý: ví dụ trong decisions.md#D-13 vẫn ghi NULL_EXCEPTION, còn R-10/D-03 và code dùng EXCEPTION_ERROR. Người cần sửa câu chữ của D-13. |
F-02 /create gửi kèm id → sửa kho
- Điểm vào: cũ
easyposbackend1:src/main/java/vn/softdreams/easypos/web/rest/WarehouseResource.java:52-57→ mớisrc/main/java/com/softdreams/easypos/warehouse/presentation/web/warehouse/WarehouseController.java:50-58 - Quy tắc: R-01, R-02, R-05, R-06, R-08, R-09, R-10, R-11, R-12, R-13, R-14, R-15, R-16
Sơ đồ luồng mới (theo code thực tế)
sequenceDiagram
participant C as Client
participant WC as WarehouseController.createWarehouse
participant WS as WarehouseManagementService.saveWarehouse (@Transactional)
participant CC as CurrentCompanyService
participant WR as WarehouseRelationalRepository
participant W as Warehouse / WarehouseDuplicatePolicy
participant BP as BillRelationalAdapter
C->>WC: POST /create {id,...}
WC->>WC: Guard.validateInput
WC->>WS: saveWarehouse(command)
WS->>CC: requireCompanyOfCurrentUser(comId)
WS->>WR: lockCompanyWarehouses(comId)
WS->>WR: findById(comId,id) → JPA findByComIdAndId → toDomain (không có → SystemException WAREHOUSE_NOT_FOUND)
WS->>W: warehouse.update(command, audit): copyFrom, status=activeStatus, so businessValues; đổi → cleanText + updater/updateTime
WS->>WR: findNameSkus(comId)
WS->>WS: duplicateRows: thay dòng id của kho bằng warehouse.nameSku()
WS->>W: WarehouseDuplicatePolicy.validate (đếm, chạm 2 → lỗi)
WS->>W: validateDefaultWarehouse()
opt !isActive
WS->>BP: countProcessingBills(comId, id) > 0 → BadRequest(WAREHOUSE_STATUS_FALSE_CHECK_BILL, tên hiện tại)
end
WS->>WR: save → findByComIdAndId (instance đã managed) → mapper.apply → toResult
Note over WR: UPDATE (@DynamicUpdate) lúc commit, chỉ cột thay đổi
WC-->>C: BaseResponseDTO.ok(..., SUCCESS_UPDATE)
Đối chiếu từng bước
| # | Luồng cũ (repo:path:dòng) |
Luồng mới (path:dòng) |
Kết luận |
|---|---|---|---|
| 1 | Validate — WarehouseResource.java:54 |
WarehouseController.java:52 |
KHỚP (D-02) |
| 2 | Không kiểm comId theo token | WarehouseManagementService.java:76 |
KHÁC có chủ đích (D-01) |
| 3 | Không khoá | WarehouseManagementService.java:77 |
CHỈ CÓ Ở MỚI (D-05) |
| 4 | findByComIdAndId → BaseInternalServerException 500 — BaseWarehouseServiceImpl.java:174-178 |
WarehouseManagementService.java:87-88, WarehouseRelationalRepository.java:96-98 |
KHỚP (D-04 GIỮ) |
| 5 | copyProperties toàn bộ (kể cả null), getStatus() null→false, SKU rỗng → code hiện có, normalizedName từ request — BaseWarehouseServiceImpl.java:182-189, WarehouseSaveRequest.java:75-77 |
Warehouse.java:97-101,154-164 |
KHỚP (D-09, D-10 GIỮ) |
| 6 | Hibernate auto-flush trước native checkDuplicateByComId: entity dirty → @PreUpdate làm sạch + audit + UPDATE; dòng của chính kho trả giá trị mới — BaseWarehouseRepository.java:37-38, CleanTextListener.java:14-34 |
Warehouse.java:101-107 (so businessValues trước khi làm sạch), WarehouseManagementService.java:112-120 (thay dòng của chính kho) |
KHỚP theo mô phỏng (D-12 GIỮ). Hành vi Hibernate 5 chưa được kiểm trên DB thật |
| 7 | Đếm trùng SKU/tên, chạm 2 → lỗi — BaseWarehouseServiceImpl.java:195-205,235-245 |
WarehouseDuplicatePolicy.java:32-59 |
KHỚP |
| 8 | checkDataWarehouseDefault (code null hoặc sellable null → NPE) — BaseWarehouseServiceImpl.java:219-227 |
Warehouse.java:118-134 |
KHỚP; NPE → 500 EXCEPTION_ERROR (D-03, D-13) |
| 9 | status=false → checkExistsBillProcessing(comId, request.id) > 0 → lỗi kèm tên đã làm sạch — BaseWarehouseServiceImpl.java:208-213, BaseWarehouseRepository.java:84-85 |
WarehouseManagementService.java:94-96,122-129, BillRelationalAdapter.java:27-35,53-59 |
KHỚP (SQL giống hệt; tham số tên = tên của model, đã làm sạch nếu có thay đổi) |
| 10 | UPDATE xảy ra ở lần flush trước câu kiểm trùng; không thay đổi thì không UPDATE và audit giữ nguyên | WarehouseRelationalRepository.java:125-134: chép vào entity managed, UPDATE lúc commit, creator/createTime không cập nhật |
KHỚP về dữ liệu cuối. Thời điểm ghi khác (lúc commit thay vì giữa transaction) nhưng không có câu nào sau đó đọc lại dòng này |
| 11 | SUCCESS_UPDATE, data = entity sau flush |
WarehouseController.java:56, mapper.toResult(entity) |
KHỚP (D-02 cho reason) |
| 12 | Interceptor gói dịch vụ | (chưa có) | CHỈ CÓ Ở CŨ (D-16) |
Dữ liệu và side effect
| Bảng / topic / cache | Cũ (đọc/ghi, điều kiện) | Mới (đọc/ghi, điều kiện) | Ghi chú |
|---|---|---|---|
warehouse |
đọc findByComIdAndId, select name,SKU; ghi UPDATE cột thay đổi khi entity dirty |
đọc SELECT TOP (1) (khoá), findByComIdAndId 2 lần (lần 2 trả về instance đã managed), select id,name,SKU; ghi UPDATE cột thay đổi lúc commit khi có thay đổi |
Lọc com_id ở mọi câu |
bill, bill_product |
COUNT khi status=false |
như cũ, qua BillPort |
Đọc chéo context bán hàng qua port |
| Redis | không | không | F-02 không sinh mã |
| MSSQL applock | — | giữ tới hết transaction | D-05 |
Transaction, khoá, idempotency, tenant
- Ranh giới transaction: một
@Transactionalbao mọi thao tác, ở cả cũ và mới. Lỗi kiểm trùng, WH1 hoặc bill → rollback. Bên cũ rollback cả UPDATE đã flush; bên mới chưa ghi gì. - Khoá / cập nhật nguyên tử: cũ không khoá, ghi đè toàn bộ bằng dữ liệu request → mới khoá theo công ty trước khi đọc kho (D-05). Không có đọc-sửa-ghi số lượng.
- Idempotency, event: không có.
- Lọc
comId:findByComIdAndId,findNameSkus, COUNT bill đều cócom_id.
Lệch so với luồng mới đã duyệt (spec mục 6)
Không.
Dev cần chú ý khi review
| Vị trí | Vì sao |
|---|---|
Warehouse.java:97-108,166-168 |
Mô phỏng dirty check của Hibernate 5: so 9 trường trước khi làm sạch, chỉ làm sạch và gán audit khi có thay đổi. Sai ở đây làm F-02 báo trùng sai (D-12) hoặc cập nhật sai updater/updateTime. |
WarehouseManagementService.java:112-120 |
Dòng của chính kho trong danh sách kiểm trùng phải mang giá trị hiện tại của model. Đây là cơ sở của luật "chạm 2" ở F-02. |
WarehouseRelationalRepository.java:125-134 |
UPDATE dựa vào dirty check của Hibernate 6 trên entity đã nạp ở findById, và mapper.apply chép đè cả cột null (mặc định SET_TO_NULL của MapStruct). Đây là chỗ ghi duy nhất của F-02. |
WarehouseJpaMapper.java (toStatus/toStatusColumn) |
Cột status kiểu bit được map sang Integer: null giữ null, 1 thành true, còn lại thành false. Bước so thay đổi ở F-02 dựa vào chuyển đổi này. |
BillRelationalAdapter.java:27-35,53-59 |
Nhánh lỗi ngừng kho khi còn đơn chưa xử lý; tham số tên kho lấy từ model sau khi làm sạch. |