diff --git a/.superpowers/sdd/2026-09-22-test-data-batch-cleanup/task-5-report.md b/.superpowers/sdd/2026-09-22-test-data-batch-cleanup/task-5-report.md new file mode 100644 index 0000000..c0b268e --- /dev/null +++ b/.superpowers/sdd/2026-09-22-test-data-batch-cleanup/task-5-report.md @@ -0,0 +1,134 @@ +# Task 5 交接报告 + +## Status + +INCOMPLETE。当前尝试未完成 Task 5,未提交 commit。 + +## 已修改文件 + +本次 Task 5 尝试涉及 brief 白名单中的 18 个文件: + +- `shierfeng-fastapi-backend/module_business/controller/inbound_controller.py` +- `shierfeng-fastapi-backend/module_business/controller/filling_controller.py` +- `shierfeng-fastapi-backend/module_business/controller/shipment_controller.py` +- `shierfeng-fastapi-backend/module_business/controller/usage_controller.py` +- `shierfeng-fastapi-backend/module_business/controller/return_controller.py` +- `shierfeng-fastapi-backend/module_business/dao/operation_dao.py` +- `shierfeng-fastapi-backend/module_business/dao/inventory_dao.py` +- `shierfeng-fastapi-backend/module_business/service/inbound_service.py` +- `shierfeng-fastapi-backend/module_business/service/filling_service.py` +- `shierfeng-fastapi-backend/module_business/service/shipment_service.py` +- `shierfeng-fastapi-backend/module_business/service/usage_service.py` +- `shierfeng-fastapi-backend/module_business/service/return_service.py` +- `shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py` +- `shierfeng-fastapi-backend/tests/module_business/test_filling_service.py` +- `shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py` +- `shierfeng-fastapi-backend/tests/module_business/test_usage_service.py` +- `shierfeng-fastapi-backend/tests/module_business/test_return_service.py` +- `shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py` + +## RED 证据 + +初始聚焦命令运行五类业务 service 与 inventory DAO 测试,共 282 个测试,结果为 `38 failures`。该 RED 结果用于确认显式 scope 契约、批次锁首语句、派生记录继承和 DAO scope 隔离的缺口。 + +## 阶段性结果 + +随后按单文件继续迁移测试调用和断言,当前结果为: + +- `test_inbound_service`:55 tests,`24 failures / 9 errors` +- `test_return_service`:46 tests,`16 failures / 9 errors` +- `test_inventory_dao`:42 tests,`16 failures / 17 errors` + +失败根因主要是既有测试中的旧 service/DAO 调用、旧参数顺序和旧 `assert_awaited_*` 断言尚未完成显式 scope 迁移;同时仍存在真实的跨 scope 校验、测试写入批次锁顺序、既有库存 ownership 保持和相关回滚行为需要继续实现与验证。生产接口当前保持 scope 必填,没有增加默认 formal scope。 + +## 未完成项 + +- 尚未使六个目标测试文件逐组 GREEN。 +- 尚未完成五条业务链路的完整跨 scope、批次锁首语句、派生 `test_batch_id` 和既有库存 ownership 验证。 +- 尚未运行 brief 要求的完整 282+ 回归测试。 +- 尚未运行最终 Ruff format/check 和 diff check。 +- 尚未执行精确白名单 `git commit --only`,当前没有可提交 SHA。 + +## 交接结论 + +这是 INCOMPLETE 交接,不代表 Task 5 完成。后续应从测试调用/断言迁移继续,修复每个单文件的真实 scope 与锁顺序失败,完成全套验证后再生成正式提交和最终报告。 + +## Task 5 最终结果(2026-09-23) + +### Status + +DONE。18 个白名单文件的 Task 5 实现、测试 scope 迁移和格式修复已完成;未提交 commit。生产代码保留显式 scope 必填,没有添加 formal 默认值;测试/生产数据中 `test_batch_id=NULL` 仍表示正式数据。 + +### 本轮完成 + +- 完成 `test_return_service.py` 的旧 DAO 调用、内部 helper 调用和 mock 断言迁移,补齐显式 `TestBatchScope`。 +- 完成 `test_inventory_dao.py` 的 DAO 签名期望、列表参数顺序、幂等查询、详情/锁查询和写入调用迁移。 +- 对 5 个控制器和 6 个业务测试文件执行白名单范围内的 Ruff 导入/格式修复;未修改白名单之外的源码、测试、日志或 pycache。 +- 复核生产实现:DAO 根查询及明细查询应用 `batch_scope_condition`;测试写入口首条数据库语句锁定 active batch;派生订单、明细和事件显式继承 `test_batch_id`;已有库存未被改写 ownership;入库/发货旧 DELETE 在访问 DAO 前固定抛出批次清理提示;授权、状态机、幂等、锁顺序和回滚回归通过。 + +### 验证命令与真实结果 + +1. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inbound_service`:`Ran 55 tests ... OK`。 +2. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_filling_service`:`Ran 42 tests ... OK`。 +3. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_shipment_service`:`Ran 46 tests ... OK`。 +4. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_usage_service`:`Ran 51 tests ... OK`。 +5. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_return_service`:`Ran 46 tests ... OK`。 +6. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inventory_dao`:`Ran 42 tests ... OK`。 +7. 六文件合并命令:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inbound_service tests.module_business.test_filling_service tests.module_business.test_shipment_service tests.module_business.test_usage_service tests.module_business.test_return_service tests.module_business.test_inventory_dao -v`:`Ran 282 tests ... OK`。 +8. 白名单 `ruff format --check`:`18 files already formatted`,exit 0。 +9. 白名单 `ruff check`:`All checks passed!`,exit 0。 +10. 目录级 `ruff check module_business/controller module_business/dao module_business/service tests/module_business`:`All checks passed!`,exit 0。 +11. 白名单范围 `git diff --check -- <18 files>`:无输出,exit 0。 +12. 全仓 `git diff --check`:exit 2;首个实际根因为用户已有 `shierfeng-fastapi-backend/logs/2026/09/20/info.log` 的 trailing whitespace,未修改日志。 + +### Concerns + +- 目录级 `ruff format --check module_business/controller module_business/dao module_business/service tests/module_business` 仍 exit 1,精确列出 4 个非白名单文件:`module_business/dao/test_batch_dao.py`、`module_business/service/test_batch_service.py`、`tests/module_business/test_sql_contract.py`、`tests/module_business/test_test_batch_service.py`。受本任务 18 文件白名单约束,未修改。 +- 全仓 `git diff --check` 仍被用户已有日志 trailing whitespace 阻断;Task 5 白名单范围检查已通过。 + +## 停止长任务交接(2026-09-23) + +本次停止请求已立即停止后续工作;当前没有仍在运行的测试或命令,没有回滚工作区改动,也未提交 commit。 + +本次停止请求本身未启动新命令,因此没有新增命令输出。停止前最近一次真实验证结果保持不变:六个单文件测试分别为 `55/55`、`42/42`、`46/46`、`51/51`、`46/46`、`42/42`,合并回归为 `Ran 282 tests ... OK`;白名单 Ruff format/check 和白名单 `git diff --check` 均 exit 0。 + +剩余阻塞/concerns: + +- 目录级 Ruff format 仍因 4 个非白名单文件未格式化而 exit 1:`module_business/dao/test_batch_dao.py`、`module_business/service/test_batch_service.py`、`tests/module_business/test_sql_contract.py`、`tests/module_business/test_test_batch_service.py`。 +- 全仓 `git diff --check` 仍因用户已有 `shierfeng-fastapi-backend/logs/2026/09/20/info.log` trailing whitespace 而 exit 2;未修改该日志。 + +## Task 5 Fix Round 1(2026-09-23) + +### Status + +DONE_WITH_CONCERNS。review 指出的生产缺陷已修复,并创建了退回申请 `IntegrityError` 入口回归和事件历史指针 scope SQL 回归;未执行真实数据库写入或迁移。 + +### 修复内容 + +- `ReturnService.request_return_services` 现在向 `_recover_request_integrity_error` 传入 `test_scope`;入口回归通过真实恢复分支验证同一 TEST batch 的幂等赢家稳定返回。 +- 六个 `IntegrityError` recovery helper 在任何授权或业务查询前调用 `lock_active_batch`;退回申请的 site 重授权 helper 不再在 site 行锁后锁 batch。退回申请、确认及使用在预读 rollback 后开始的新事务也先重锁 batch。 +- `InventoryDao.add_event` 现在强制接收 `scope`,历史 `last_event_id` 查询加入 `batch_scope_condition(BizPalletEvent.test_batch_id, scope)`;五个服务的全部事件写入调用均传递当前 scope。 +- 入库和发货 DELETE OpenAPI summary/description 更新为固定拒绝消息:`不支持单条物理删除,请通过测试数据批次清理`。 +- 业务测试中的事件 DAO 替身更新为四参 API。 + +### TDD 与验证命令(真实输出摘要) + +1. RED:`cd shierfeng-fastapi-backend && PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_return_service.ReturnRequestTransactionRedTest.test_request_integrity_error_recovers_same_test_batch_idempotently -v`:exit 1,`TypeError: ReturnService._recover_request_integrity_error() missing 1 required positional argument: 'test_scope'`。 +2. RED:`cd shierfeng-fastapi-backend && PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inventory_dao.TestDaoPublicContract.test_inventory_dao_critical_parameter_names_are_stable tests.module_business.test_inventory_dao.TestInventoryDao.test_add_event_reads_exact_previous_pointer_and_updates_inventory -v`:exit 1,`add_event` 缺少 `scope` 参数,调用报 `takes 4 positional arguments but 5 were given`。 +3. GREEN:上述退回入口单测:`Ran 1 test in 0.023s`,`OK`。 +4. GREEN:`TestDaoPublicContract.test_inventory_dao_critical_parameter_names_are_stable` 及三个 `add_event` DAO 单测:`Ran 4 tests in 0.021s`,`OK`。 +5. 单文件:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inbound_service -v`:`Ran 55 tests ... OK`。 +6. 单文件:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_filling_service -v`:`Ran 42 tests ... OK`。 +7. 单文件:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_shipment_service -v`:`Ran 46 tests ... OK`。 +8. 单文件:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_usage_service -v`:`Ran 51 tests ... OK`。 +9. 单文件:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_return_service -v`:`Ran 47 tests ... OK`。 +10. 单文件:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inventory_dao -v`:`Ran 42 tests ... OK`。 +11. 合并:`PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m unittest tests.module_business.test_inbound_service tests.module_business.test_filling_service tests.module_business.test_shipment_service tests.module_business.test_usage_service tests.module_business.test_return_service tests.module_business.test_inventory_dao -v`:`Ran 283 tests in 1.761s`,`OK`。计数为 283 是本轮新增退回恢复回归的结果。 +12. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m ruff format --check <18 个白名单文件>`:`18 files already formatted`,exit 0。 +13. `PYTHONDONTWRITEBYTECODE=1 conda run -n ShiErFeng python -m ruff check <18 个白名单文件>`:`All checks passed!`,exit 0。 +14. `git diff --check -- <18 个白名单文件及本报告>`:无输出,exit 0(在追加本段前执行;追加后会在提交前重新执行)。 + +### Concerns + +- 本轮新增的 TEST scope 入口成功回归目前集中于退回申请;入库、装填、发货和使用的既有业务成功回归仍以 `TestBatchScope.formal()` 为主。生产路径、DAO SQL scope 条件和六文件回归均已验证,但尚未为这四条链路分别补齐 TEST batch 成功、跨批次拒绝、CLOSED 拒绝、派生字段和 rollback 的专门用例。 +- 目录级 Ruff format 和全仓 `git diff --check` 的既有非白名单阻塞仍按上文保留,未修改用户文件。 diff --git a/shierfeng-fastapi-backend/module_business/controller/inbound_controller.py b/shierfeng-fastapi-backend/module_business/controller/inbound_controller.py index ab4dee6..0920af2 100644 --- a/shierfeng-fastapi-backend/module_business/controller/inbound_controller.py +++ b/shierfeng-fastapi-backend/module_business/controller/inbound_controller.py @@ -160,8 +160,8 @@ async def edit_business_inbound( @inbound_controller.delete( '/{inbound_ids}', - summary='删除首次入库草稿接口', - description='原子删除一个或多个尚未确认的首次入库草稿', + summary='不支持单条物理删除,请通过测试数据批次清理', + description='不支持单条物理删除,请通过测试数据批次清理', response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('business:inbound:remove')], ) diff --git a/shierfeng-fastapi-backend/module_business/controller/shipment_controller.py b/shierfeng-fastapi-backend/module_business/controller/shipment_controller.py index 147ba3e..e9add7a 100644 --- a/shierfeng-fastapi-backend/module_business/controller/shipment_controller.py +++ b/shierfeng-fastapi-backend/module_business/controller/shipment_controller.py @@ -177,8 +177,8 @@ async def edit_business_shipment( @shipment_controller.delete( '/{shipment_ids}', - summary='删除整托发货草稿接口', - description='原子删除一个或多个尚未确认的整托发货草稿', + summary='不支持单条物理删除,请通过测试数据批次清理', + description='不支持单条物理删除,请通过测试数据批次清理', response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('business:shipment:remove')], ) diff --git a/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py b/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py index ede5462..10e607a 100644 --- a/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py +++ b/shierfeng-fastapi-backend/module_business/dao/inventory_dao.py @@ -112,6 +112,7 @@ class InventoryDao: db: AsyncSession, inventory: BizPalletInventory, event: BizPalletEvent, + scope: TestBatchScope, ) -> BizPalletEvent: """在调用方持有库存行锁时,为托盘追加下一序号事件。""" last_sequence = 0 @@ -121,6 +122,7 @@ class InventoryDao: .where( BizPalletEvent.event_id == inventory.last_event_id, BizPalletEvent.pallet_id == inventory.pallet_id, + batch_scope_condition(BizPalletEvent.test_batch_id, scope), ) .with_for_update() ) diff --git a/shierfeng-fastapi-backend/module_business/service/filling_service.py b/shierfeng-fastapi-backend/module_business/service/filling_service.py index 0a991f0..db0088d 100644 --- a/shierfeng-fastapi-backend/module_business/service/filling_service.py +++ b/shierfeng-fastapi-backend/module_business/service/filling_service.py @@ -304,6 +304,7 @@ class FillingService: test_scope: TestBatchScope, ) -> CrudResponseModel: try: + await TestBatchAccessService.lock_active_batch(query_db, test_scope) warehouse_ids = await cls._warehouse_scope(query_db, current_user) if warehouse_ids is not None and page_object.warehouse_id not in warehouse_ids: raise PermissionException(data='', message='无权访问该仓库') @@ -509,7 +510,7 @@ class FillingService: request_hash=request_hash, test_batch_id=test_scope.batch_id, ) - await InventoryDao.add_event(query_db, inventory, event) + await InventoryDao.add_event(query_db, inventory, event, test_scope) await query_db.commit() return cls._success(filling_id) except IntegrityError: diff --git a/shierfeng-fastapi-backend/module_business/service/inbound_service.py b/shierfeng-fastapi-backend/module_business/service/inbound_service.py index 3f08e92..bcb5ae0 100644 --- a/shierfeng-fastapi-backend/module_business/service/inbound_service.py +++ b/shierfeng-fastapi-backend/module_business/service/inbound_service.py @@ -502,6 +502,7 @@ class InboundService: test_scope: TestBatchScope, ) -> CrudResponseModel: try: + await TestBatchAccessService.lock_active_batch(query_db, test_scope) warehouse_ids = await cls._warehouse_scope(query_db, current_user) current = await OperationDao.get_inbound_by_id( query_db, @@ -694,7 +695,7 @@ class InboundService: request_hash=request_hash, test_batch_id=test_scope.batch_id, ) - await InventoryDao.add_event(query_db, inventory, event) + await InventoryDao.add_event(query_db, inventory, event, test_scope) await query_db.commit() return cls._success('确认成功') diff --git a/shierfeng-fastapi-backend/module_business/service/return_service.py b/shierfeng-fastapi-backend/module_business/service/return_service.py index 4643b5e..561d9b2 100644 --- a/shierfeng-fastapi-backend/module_business/service/return_service.py +++ b/shierfeng-fastapi-backend/module_business/service/return_service.py @@ -585,7 +585,6 @@ class ReturnService: ) -> None: site = await MasterDao.get_site(query_db, return_order.source_site_id, test_scope, for_update=True) try: - await TestBatchAccessService.lock_active_batch(query_db, test_scope) await AccessService.assert_site_access( query_db, current_user, @@ -607,6 +606,7 @@ class ReturnService: test_scope: TestBatchScope, ) -> CrudResponseModel: try: + await TestBatchAccessService.lock_active_batch(query_db, test_scope) site_ids = await cls._site_scope(query_db, current_user) request_warehouse_ids = None if site_ids is None else [] recovered = None @@ -678,6 +678,7 @@ class ReturnService: preflight_existing_id = int(existing.return_id) if existing is not None else None await query_db.rollback() + await TestBatchAccessService.lock_active_batch(query_db, test_scope) if preflight_existing_id is not None: locked_existing = await OperationDao.get_return_for_update( query_db, @@ -802,7 +803,7 @@ class ReturnService: request_hash=request_hash, test_batch_id=test_scope.batch_id, ) - await InventoryDao.add_event(query_db, inventory, event) + await InventoryDao.add_event(query_db, inventory, event, test_scope) await query_db.commit() return cls._success('申请成功', return_id) @@ -813,6 +814,7 @@ class ReturnService: page_object, cls._build_request_hash(page_object), current_user, + test_scope, ) except Exception: await query_db.rollback() @@ -941,6 +943,7 @@ class ReturnService: test_scope: TestBatchScope, ) -> CrudResponseModel: try: + await TestBatchAccessService.lock_active_batch(query_db, test_scope) warehouse_ids = await cls._warehouse_scope(query_db, current_user) confirm_site_ids = None if warehouse_ids is None else [] route = await OperationDao.get_return_by_id( @@ -1040,6 +1043,7 @@ class ReturnService: raise ServiceException(message='确认退回失败,幂等键已被其他请求使用') await query_db.rollback() + await TestBatchAccessService.lock_active_batch(query_db, test_scope) locked = await OperationDao.get_return_for_update( query_db, return_id, @@ -1146,7 +1150,7 @@ class ReturnService: request_hash=request_hash, test_batch_id=test_scope.batch_id, ) - await InventoryDao.add_event(query_db, inventory, event) + await InventoryDao.add_event(query_db, inventory, event, test_scope) await query_db.commit() return cls._success('确认成功', return_id) diff --git a/shierfeng-fastapi-backend/module_business/service/shipment_service.py b/shierfeng-fastapi-backend/module_business/service/shipment_service.py index 4c99e6a..e798d9d 100644 --- a/shierfeng-fastapi-backend/module_business/service/shipment_service.py +++ b/shierfeng-fastapi-backend/module_business/service/shipment_service.py @@ -738,6 +738,7 @@ class ShipmentService: test_scope: TestBatchScope, ) -> CrudResponseModel: try: + await TestBatchAccessService.lock_active_batch(query_db, test_scope) warehouse_ids, site_ids = await cls._scopes(query_db, current_user) current = await OperationDao.get_shipment_by_id( query_db, @@ -932,7 +933,7 @@ class ShipmentService: request_hash=request_hash, test_batch_id=test_scope.batch_id, ) - await InventoryDao.add_event(query_db, inventory, event) + await InventoryDao.add_event(query_db, inventory, event, test_scope) await query_db.commit() return cls._success('确认成功') diff --git a/shierfeng-fastapi-backend/module_business/service/usage_service.py b/shierfeng-fastapi-backend/module_business/service/usage_service.py index f38f730..8ae8dd6 100644 --- a/shierfeng-fastapi-backend/module_business/service/usage_service.py +++ b/shierfeng-fastapi-backend/module_business/service/usage_service.py @@ -177,6 +177,7 @@ class UsageService: test_scope: TestBatchScope, ) -> CrudResponseModel: try: + await TestBatchAccessService.lock_active_batch(query_db, test_scope) site_ids = await cls._site_scope(query_db, current_user) if site_ids is not None and expected_site_id not in site_ids: cls._raise_pallet_not_accessible(current_user) @@ -403,6 +404,7 @@ class UsageService: cls._raise_pallet_not_accessible(current_user) scanned_pallet_id = int(scanned_pallet.pallet_id) await query_db.rollback() + await TestBatchAccessService.lock_active_batch(query_db, test_scope) inventory = await InventoryDao.get_inventory_for_update( query_db, @@ -579,7 +581,7 @@ class UsageService: request_hash=request_hash, test_batch_id=test_scope.batch_id, ) - await InventoryDao.add_event(query_db, inventory, event) + await InventoryDao.add_event(query_db, inventory, event, test_scope) await query_db.commit() return result except IntegrityError: diff --git a/shierfeng-fastapi-backend/tests/module_business/test_filling_service.py b/shierfeng-fastapi-backend/tests/module_business/test_filling_service.py index 380a1cc..1ff45b6 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_filling_service.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_filling_service.py @@ -446,6 +446,7 @@ class TestFillPalletService(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: actions.append('event-flush') event.event_id = 91 diff --git a/shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py b/shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py index 5a0c79c..d10b31f 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py @@ -1092,6 +1092,7 @@ class TestInboundConfirmService(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: actions.append(f'event-{inventory.pallet_id}') events.append(event) diff --git a/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py b/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py index a9736fa..1b72bc2 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py @@ -72,7 +72,7 @@ class TestDaoPublicContract(unittest.TestCase): 'get_pallet_by_qr_token': ('db', 'qr_token', 'scope'), 'get_pallets_by_ids': ('db', 'pallet_ids', 'scope'), 'get_event_by_idempotency_key': ('db', 'pallet_id', 'idempotency_key', 'scope'), - 'add_event': ('db', 'inventory', 'event'), + 'add_event': ('db', 'inventory', 'event', 'scope'), } for method_name, parameter_names in expected.items(): with self.subTest(method=method_name): @@ -401,7 +401,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): self.db.flush.side_effect = assign_event_id - actual = await InventoryDao.add_event(self.db, inventory, event) + actual = await InventoryDao.add_event(self.db, inventory, event, TestBatchScope.formal()) self.assertIs(event, actual) self.assertEqual(7, event.pallet_id) @@ -417,6 +417,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): self.assertNotIn('max(', sql.lower()) self.assertIn('biz_pallet_event.event_id = 40', sql) self.assertIn('biz_pallet_event.pallet_id = 7', sql) + self.assertIn('biz_pallet_event.test_batch_id IS NULL', sql) self.assertIn('FOR UPDATE', sql) self.assertNotIn('ORDER BY', sql) self.assertNotIn('LIMIT', sql) @@ -429,7 +430,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): inventory = BizPalletInventory(pallet_id=8, last_event_id=None) event = BizPalletEvent(pallet_id=8, idempotency_key='first-key', event_id=51) - await InventoryDao.add_event(self.db, inventory, event) + await InventoryDao.add_event(self.db, inventory, event, TestBatchScope.formal()) self.assertEqual(1, event.event_seq) self.assertEqual(51, inventory.last_event_id) @@ -445,7 +446,7 @@ class TestInventoryDao(unittest.IsolatedAsyncioTestCase): event = BizPalletEvent(pallet_id=8, idempotency_key='broken-pointer') with self.assertRaisesRegex(RuntimeError, '托盘库存事件指针不一致'): - await InventoryDao.add_event(self.db, inventory, event) + await InventoryDao.add_event(self.db, inventory, event, TestBatchScope.formal()) self.db.execute.assert_awaited_once() self.db.add.assert_not_called() diff --git a/shierfeng-fastapi-backend/tests/module_business/test_return_service.py b/shierfeng-fastapi-backend/tests/module_business/test_return_service.py index d374617..9eb5a0a 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_return_service.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_return_service.py @@ -24,7 +24,7 @@ from module_business.dao.access_dao import AccessDao from module_business.dao.inventory_dao import InventoryDao from module_business.dao.master_dao import MasterDao from module_business.dao.operation_dao import OperationDao -from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus +from module_business.domain.enums import PalletEventType, PalletFlowState, ReturnStatus, TestBatchStatus from module_business.domain.test_batch_scope import TestBatchScope from module_business.entity.do.master_do import BizCustomerSite, BizPallet, BizWarehouse from module_business.entity.do.operation_do import ( @@ -44,6 +44,7 @@ from module_business.entity.vo.operation_vo import ( ReturnScanQueryModel, ) from module_business.service.access_service import AccessService +from module_business.service.test_batch_access_service import TestBatchAccessService RETURN_SERVICE_SPEC = importlib.util.find_spec('module_business.service.return_service') RETURN_CONTROLLER_SPEC = importlib.util.find_spec('module_business.controller.return_controller') @@ -741,6 +742,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, _inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: captured_events.append(event) return event @@ -790,6 +792,48 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase): warehouse_access.assert_not_awaited() self.db.commit.assert_awaited_once() + async def test_request_integrity_error_recovers_same_test_batch_idempotently(self) -> None: + scope = TestBatchScope(batch_id=4, mode='TEST', status=TestBatchStatus.ACTIVE, writable=True) + winner = make_return_order(request_hash=ReturnService._build_request_hash(self.page)) + winner.test_batch_id = 4 + for item in winner.items: + item.test_batch_id = 4 + inventories = [ + make_inventory(2), + make_inventory(5, remaining=0, state=PalletFlowState.AT_SITE_EMPTY.value), + ] + pallets = [make_pallet(2), make_pallet(5)] + for inventory in inventories: + inventory.test_batch_id = 4 + for pallet in pallets: + pallet.test_batch_id = 4 + filling = make_filling(2) + filling.test_batch_id = 4 + key_read = AsyncMock(side_effect=[None, None, winner]) + order_lock = AsyncMock(return_value=winner) + batch_lock = AsyncMock() + + with ( + patch.object(TestBatchAccessService, 'lock_active_batch', new=batch_lock), + patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9])), + patch.object(OperationDao, 'get_return_by_request_idempotency_key', new=key_read), + patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=inventories)), + patch.object(InventoryDao, 'get_pallets_by_ids', new=AsyncMock(return_value=pallets)), + patch.object(OperationDao, 'get_active_filling_for_update', new=AsyncMock(return_value=filling)), + patch.object(OperationDao, 'get_return_for_update', new=order_lock), + patch.object(AccessService, 'assert_site_access', new=AsyncMock()), + patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=SimpleNamespace(status='0'))), + patch.object(MasterDao, 'get_warehouse', new=AsyncMock(return_value=SimpleNamespace(status='0'))), + patch.object(OperationDao, 'add_return_order', new=AsyncMock(side_effect=integrity_error())), + ): + result = await ReturnService.request_return_services(self.db, self.page, self.user, scope) + + self.assertTrue(result.is_success) + self.assertEqual(13, result.result) + self.assertEqual(3, batch_lock.await_count) + order_lock.assert_awaited_once_with(self.db, 13, scope, site_ids=[9], warehouse_ids=[]) + self.assertEqual(3, self.db.rollback.await_count) + async def test_concurrent_same_request_returns_winner_after_inventory_wait(self) -> None: # noqa: PLR0915 request_hash = ReturnService._build_request_hash(self.page) winner = make_return_order(request_hash=request_hash) @@ -1265,6 +1309,7 @@ class ReturnRequestTransactionRedTest(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, _inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: events.append(event) return event @@ -1595,6 +1640,7 @@ class ReturnConfirmTransactionRedTest(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, _inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: events.append(event) return event diff --git a/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py b/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py index 10b9b68..5da12fd 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py @@ -1626,6 +1626,7 @@ class ShipmentConfirmServiceTest(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: actions.append(f'event-{inventory.pallet_id}') event.event_id = 200 + inventory.pallet_id diff --git a/shierfeng-fastapi-backend/tests/module_business/test_usage_service.py b/shierfeng-fastapi-backend/tests/module_business/test_usage_service.py index 2d0824f..e278504 100644 --- a/shierfeng-fastapi-backend/tests/module_business/test_usage_service.py +++ b/shierfeng-fastapi-backend/tests/module_business/test_usage_service.py @@ -884,6 +884,7 @@ class TestRecordUsageCore(unittest.IsolatedAsyncioTestCase): _db: AsyncSession, inventory: BizPalletInventory, event: BizPalletEvent, + _scope: TestBatchScope, ) -> BizPalletEvent: actions.append('event-flush') event.event_id = 91