From a509dada7932a25a54bd9fa7af5e08f8fb029043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A6=E9=BE=99=E8=A8=80?= Date: Fri, 24 Jul 2026 18:00:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=89=98=E7=9B=98=E4=B8=9A=E5=8A=A1=E5=AE=9E=E6=96=BD=E8=AE=A1?= =?UTF-8?q?=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-07-24-pallet-workflow-system.md | 1843 +++++++++++++++++ 1 file changed, 1843 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-24-pallet-workflow-system.md diff --git a/docs/superpowers/plans/2026-07-24-pallet-workflow-system.md b/docs/superpowers/plans/2026-07-24-pallet-workflow-system.md new file mode 100644 index 0000000..d78f75f --- /dev/null +++ b/docs/superpowers/plans/2026-07-24-pallet-workflow-system.md @@ -0,0 +1,1843 @@ +# 智链云枢托盘业务系统 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking. + +**Goal:** 移除 AI 管理和“施尔丰仓库”外链菜单,并在现有 Web/FastAPI 架构内实现具备真实权限、库存事务和完整追溯的托盘业务系统。 + +**Architecture:** 新增独立 module_business,继续使用 controller → service → dao → entity/do + entity/vo 分层。托盘以业务单据、当前库存快照和只追加事件流水组成聚合,所有状态变更由 Service 在单一事务中完成;Web 继续使用后端动态菜单和现有权限体系。 + +**Tech Stack:** Python 3.10+、FastAPI、Pydantic v2、异步 SQLAlchemy 2、MySQL/PostgreSQL 初始化 SQL、Vue 3、Element Plus、Pinia、Vite 6、后端 qrcode 8.2、Python unittest、Node 20 内置测试。 + +**Design:** ../specs/2026-07-24-pallet-workflow-system-design.md + +--- + +## 执行安全边界 + +1. 当前工作区有用户未提交改动,尤其是 APP、环境文件、前端 package.json 和已暂存的 .idea/git_toolbox_prj.xml。每次提交都必须使用精确文件路径和 git commit --only,禁止提交或取消暂存用户文件;不得对任何可能包含 __pycache__ 的目录执行普通 git add。 +2. 不修改 shierfeng-fastapi-app,不修改 .env.dev、.env.prod 或任何前端 .env 文件。 +3. 不启动 app.py、server.py、uvicorn 或 shierfeng app run。启动生命周期会执行 Base.metadata.create_all(),可能写入本地 MySQL。 +4. 不执行两套初始化 SQL 或 MySQL 增量补丁,不运行会连接业务数据库的 TestClient 生命周期或集成测试。 +5. 如果实施开始时发现该项目后端进程正在运行,立即停止代码修改并请用户先关闭服务;不得主动终止未知进程。 +6. Conda 环境没有 pytest,但有 ruff 0.15.0。新增测试统一使用 unittest.IsolatedAsyncioTestCase 和 unittest.mock。 +7. 前端没有 lint、typecheck 或单元测试脚本。新增 Node 20 内置契约测试,不引入测试依赖;生产构建使用 Node v20.20.2 的绝对路径。 +8. qrcode 8.2 的安装已获得用户确认;不安装、升级或卸载其他 Conda/npm 依赖。 +9. package.json 已包含任务开始前的“智链云枢管理系统”修改。本任务必须保留该修改,但不得在没有额外确认时把它自动纳入提交;Task 1 的依赖删除和 Task 16 的 test:business 脚本允许留在工作区未提交。 +10. 受保护改动当前基线 SHA-256 为:APP diff 6b7871be6b8cdede4c508adeb345e70084b01583403cfd0aab67e5b3beeb7e1c,环境文件 diff 5f1189e56551fc16485244e46c8e78cd32ce9ac9f0eaac91cfd0cf42b1ded57a,已暂存 .idea diff f21bea19dc68716136d77a3f1567659fd1af2f10f7b1fc6d246851dcfaa645c1。实施结束必须重新计算并完全一致。 +11. 计划提交“文档:新增托盘业务实施计划”是后续实现历史的基线锚点;最终使用该提交到 HEAD 的完整路径历史检查 APP、.env、.idea、__pycache__ 和 .pyc,而不是只检查 HEAD~1。 + +## 文件结构 + +### 后端业务模块 + +~~~text +shierfeng-fastapi-backend/module_business/ +├── domain/ +│ ├── enums.py +│ └── pallet_state_machine.py +├── entity/ +│ ├── do/ +│ │ ├── master_do.py +│ │ └── operation_do.py +│ └── vo/ +│ ├── master_vo.py +│ ├── operation_vo.py +│ └── report_vo.py +├── dao/ +│ ├── access_dao.py +│ ├── master_dao.py +│ ├── inventory_dao.py +│ ├── operation_dao.py +│ └── report_dao.py +├── service/ +│ ├── access_service.py +│ ├── master_service.py +│ ├── pallet_service.py +│ ├── inbound_service.py +│ ├── filling_service.py +│ ├── shipment_service.py +│ ├── usage_service.py +│ ├── return_service.py +│ └── report_service.py +└── controller/ + ├── strain_controller.py + ├── customer_controller.py + ├── site_controller.py + ├── line_controller.py + ├── warehouse_controller.py + ├── pallet_controller.py + ├── inbound_controller.py + ├── filling_controller.py + ├── shipment_controller.py + ├── usage_controller.py + ├── return_controller.py + └── report_controller.py +~~~ + +### 后端测试 + +~~~text +shierfeng-fastapi-backend/tests/module_business/ +├── test_ai_removal_contract.py +├── test_state_machine.py +├── test_model_contract.py +├── test_vo_contract.py +├── test_access_service.py +├── test_master_service.py +├── test_pallet_service.py +├── test_inventory_dao.py +├── test_inbound_service.py +├── test_filling_service.py +├── test_shipment_service.py +├── test_usage_service.py +├── test_return_service.py +├── test_report_service.py +├── test_api_permission_contract.py +└── test_sql_contract.py +~~~ + +### Web + +~~~text +shierfeng-fastapi-frontend/src/api/master/{strain,customer,site,line,warehouse,pallet}.js +shierfeng-fastapi-frontend/src/api/business/{inbound,filling,shipment,usage,return}.js +shierfeng-fastapi-frontend/src/api/report/{palletInventory,filling,shipment,consumption,return,palletTrace}.js +shierfeng-fastapi-frontend/src/views/master/{strain,customer,site,line,warehouse,pallet}/index.vue +shierfeng-fastapi-frontend/src/views/business/{inbound,filling,shipment,usage,return}/index.vue +shierfeng-fastapi-frontend/src/views/report/{palletInventory,filling,shipment,consumption,return,palletTrace}/index.vue +shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue +shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue +shierfeng-fastapi-frontend/src/utils/palletScan.js +shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +~~~ + +### SQL + +~~~text +shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql +shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql +shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql +~~~ + +## 固定数据库与权限契约 + +业务表固定为 18 张: + +~~~text +biz_strain +biz_warehouse +biz_customer +biz_customer_site +biz_production_line +biz_pallet +biz_user_warehouse +biz_user_site +biz_inbound_order +biz_inbound_item +biz_filling_record +biz_shipment_order +biz_shipment_item +biz_usage_record +biz_return_order +biz_return_item +biz_pallet_inventory +biz_pallet_event +~~~ + +菜单 ID 固定使用当前空闲区间,不复用已删除 AI/外链 ID: + +- 121–123:基础资料维护、业务管理、报表查询。 +- 502–518:6 个基础页面、5 个业务页面、6 个报表页面。 +- 1065–1108:44 个按钮权限。 +- 保留 sys_menu 的 auto_increment/restart 2000。 + +API 与权限固定使用设计稿中的 master:*、business:*、report:*,不改为 pallet:*。 + +### 18 张表的字段与约束 + +所有主数据和单据沿用项目审计列:create_by、create_time、update_by、update_time、remark。主数据 status/asset_status 使用 CHAR(1) 并 CHECK IN ('0', '1'),0=正常、1=停用;普通单据状态、退回状态、flow_state、event_type 使用 String(32),分别按 DocumentStatus、ReturnStatus、PalletFlowState、PalletEventType 建 CHECK。以下字段名是 ORM、MySQL 和 PostgreSQL 的共同持久化契约;VO/报表如需展示名必须在本计划中显式声明映射,不能在实现时另起未记录的同义字段: + +~~~text +BizStrain: + strain_id PK, strain_code UQ, strain_name, status + 审计字段 +BizWarehouse: + warehouse_id PK, warehouse_code UQ, warehouse_name, address, status + 审计字段 +BizCustomer: + customer_id PK, customer_code UQ, customer_name, status + 审计字段 +BizCustomerSite: + site_id PK, customer_id FK, site_code, site_name, address, status, + UQ(customer_id, site_code) + 审计字段 +BizProductionLine: + line_id PK, site_id FK, line_code, line_name, status, + UQ(site_id, line_code) + 审计字段 +BizPallet: + pallet_id PK, pallet_code UQ, qr_token UQ, capacity_packages CHECK > 0, + asset_status + 审计字段 +BizUserWarehouse: + user_id FK, warehouse_id FK, create_by, create_time, + PK(user_id, warehouse_id) +BizUserSite: + user_id FK, site_id FK, create_by, create_time, + PK(user_id, site_id) + +BizInboundOrder: + inbound_id PK, inbound_no UQ, warehouse_id FK, status, + confirm_idempotency_key UQ NULL, confirm_request_hash NULL, + confirmed_by FK sys_user NULL, confirmed_at NULL + 审计字段 +BizInboundItem: + inbound_item_id PK, inbound_id FK, pallet_id FK, + UQ(inbound_id, pallet_id), create_by, create_time +BizFillingRecord: + filling_id PK, filling_no UQ, pallet_id FK, warehouse_id FK, strain_id FK, + strain_code_snapshot, strain_name_snapshot, batch_no, package_spec, + initial_packages CHECK > 0, active_slot NULL, closed_at NULL, + idempotency_key UQ, request_hash, operator_id FK, operated_at, + UQ(pallet_id, active_slot) + 审计字段 +BizShipmentOrder: + shipment_id PK, shipment_no UQ, source_warehouse_id FK, target_site_id FK, + status, confirm_idempotency_key UQ NULL, confirm_request_hash NULL, + confirmed_by FK sys_user NULL, confirmed_at NULL + 审计字段 +BizShipmentItem: + shipment_item_id PK, shipment_id FK, pallet_id FK, + active_filling_id FK NULL, package_qty NULL CHECK(package_qty IS NULL OR package_qty > 0), + UQ(shipment_id, pallet_id), create_by, create_time +BizUsageRecord: + usage_id PK, usage_no UQ, pallet_id FK, active_filling_id FK, + site_id FK, line_id FK, used_packages CHECK > 0, + remaining_packages_after CHECK >= 0, idempotency_key UQ, request_hash, + operator_id FK, occurred_at +BizReturnOrder: + return_id PK, return_no UQ, source_site_id FK, target_warehouse_id FK, + status, request_idempotency_key UQ, request_hash, + confirm_idempotency_key UQ NULL, confirm_request_hash NULL, + requested_by FK sys_user, requested_at, + confirmed_by FK sys_user NULL, confirmed_at NULL + 审计字段 +BizReturnItem: + return_item_id PK, return_id FK, pallet_id FK, + active_filling_id_snapshot FK NULL, + remaining_packages_snapshot CHECK >= 0, + UQ(return_id, pallet_id), create_by, create_time +BizPalletInventory: + pallet_id PK/FK, active_filling_id FK NULL, + warehouse_id FK NULL, site_id FK NULL, current_line_id FK NULL, + flow_state, remaining_packages CHECK >= 0, + last_event_id FK NULL, version CHECK >= 0, updated_at +BizPalletEvent: + event_id PK, pallet_id FK, event_seq, event_type, source_type, source_id, + before_state, after_state, + before_warehouse_id, before_site_id, before_line_id, + after_warehouse_id, after_site_id, after_line_id, + before_remaining_packages, after_remaining_packages, + operator_id FK, occurred_at, idempotency_key, request_hash, + UQ(pallet_id, event_seq), UQ(pallet_id, idempotency_key) +~~~ + +额外约束固定如下: + +- active_slot 仅在装填周期活动时写 ACTIVE,周期关闭时置 NULL;数据库同时建立 CHECK(active_slot IS NULL OR active_slot = 'ACTIVE') 和 UQ(pallet_id, active_slot),在 MySQL/PostgreSQL 下共同保证单托盘最多一条活动装填记录。 +- BizPalletInventory.current_line_id 是当前驻场周期的产线锁权威字段;首次使用设置,退回确认清空。历史产线由 BizUsageRecord.line_id 和 BizPalletEvent 保存,不在 BizPallet 主数据上保存。 +- 余量归零时同时关闭装填记录、清 active_slot、写 closed_at 并清 inventory.active_filling_id;带料退回保留活动装填记录和 active_filling_id,再次发货后可重新绑定产线。 +- RETURNING 期间仍保留 source site 和 current_line_id,直到仓库确认;UNSTOCKED 两个位置均为空,仓库状态只能有 warehouse_id,现场状态只能有 site_id。 +- remaining_packages 不得超过 BizPallet.capacity_packages;该跨表约束由 Service 在持锁后校验,数据库保留非负检查。 +- request_hash 是对规范化请求业务字段计算的 SHA-256:对象键排序,pallet_ids/scanned_pallet_ids 去重后按数值排序,不包含时间、操作人或随机字段;同一幂等键同一 hash 返回原结果,同键不同 hash 返回失败。 +- 幂等重试返回既有结果前仍必须重新执行当前用户的接口权限和 warehouse/site 数据范围校验,不能用幂等键绕过授权或探测其他用户单据。 +- 每次成功的托盘状态或数量变更都必须在持锁事务内把 inventory.version 精确加 1,并同步 last_event_id;幂等重试不得再次增加 version 或追加事件。 + +### 菜单页面固定映射 + +目录 121、122、123 的 parent_id 均为系统管理 1,menu_type=M,perms 为空。页面 menu_type=C,映射固定如下;全部新目录/页面使用 visible=0、status=0、is_frame=1、is_cache=0,按钮的 path/component/route_name 为空: + +| ID | parent | 菜单名 | path | component | API 前缀 | 页面权限 | +|---:|---:|---|---|---|---|---| +| 502 | 121 | 菌种资料 | strain | master/strain/index | /master/strain | master:strain:list | +| 503 | 121 | 客户资料 | customer | master/customer/index | /master/customer | master:customer:list | +| 504 | 121 | 客户现场 | site | master/site/index | /master/site | master:site:list | +| 505 | 121 | 产线资料 | line | master/line/index | /master/line | master:line:list | +| 506 | 121 | 仓库资料 | warehouse | master/warehouse/index | /master/warehouse | master:warehouse:list | +| 507 | 121 | 托盘资料 | pallet | master/pallet/index | /master/pallet | master:pallet:list | +| 508 | 122 | 仓库入库 | inbound | business/inbound/index | /business/inbound | business:inbound:list | +| 509 | 122 | 工厂装填 | filling | business/filling/index | /business/filling | business:filling:list | +| 510 | 122 | 发货出库 | shipment | business/shipment/index | /business/shipment | business:shipment:list | +| 511 | 122 | 客户现场使用 | usage | business/usage/index | /business/usage | business:usage:list | +| 512 | 122 | 退回仓库 | return | business/return/index | /business/return | business:return:list | +| 513 | 123 | 托盘库存 | palletInventory | report/palletInventory/index | /report/pallet-inventory | report:palletinventory:list | +| 514 | 123 | 装填记录 | reportFilling | report/filling/index | /report/filling | report:filling:list | +| 515 | 123 | 发货出库 | reportShipment | report/shipment/index | /report/shipment | report:shipment:list | +| 516 | 123 | 产线消耗 | consumption | report/consumption/index | /report/consumption | report:consumption:list | +| 517 | 123 | 退回仓库 | reportReturn | report/return/index | /report/return | report:return:list | +| 518 | 123 | 托盘追溯 | palletTrace | report/palletTrace/index | /report/pallet-trace | report:pallettrace:list | + +17 个页面的 route_name 固定为空字符串,运行时名称由唯一 path 生成;reportFilling、reportShipment、reportReturn 必须保持当前唯一 path,不能改成业务页面已使用的 filling、shipment、return。三个目录的 path 分别固定为 businessMaster、businessOperation、businessReport,order_num 分别为 10、11、12。页面 502–507、508–512、513–518 在各自目录内的 order_num 均从 1 连续递增。 + +### 按钮 ID 固定映射 + +以下 44 行 menu_type=F,order_num 是同一页面内的顺序;六个报表没有按钮权限: + +~~~text +1065 parent=502 order=1 菌种查询 master:strain:query +1066 parent=502 order=2 菌种新增 master:strain:add +1067 parent=502 order=3 菌种修改 master:strain:edit +1068 parent=502 order=4 菌种删除 master:strain:remove +1069 parent=503 order=1 客户查询 master:customer:query +1070 parent=503 order=2 客户新增 master:customer:add +1071 parent=503 order=3 客户修改 master:customer:edit +1072 parent=503 order=4 客户删除 master:customer:remove +1073 parent=504 order=1 现场查询 master:site:query +1074 parent=504 order=2 现场新增 master:site:add +1075 parent=504 order=3 现场修改 master:site:edit +1076 parent=504 order=4 现场删除 master:site:remove +1077 parent=504 order=5 分配用户 master:site:assign +1078 parent=505 order=1 产线查询 master:line:query +1079 parent=505 order=2 产线新增 master:line:add +1080 parent=505 order=3 产线修改 master:line:edit +1081 parent=505 order=4 产线删除 master:line:remove +1082 parent=506 order=1 仓库查询 master:warehouse:query +1083 parent=506 order=2 仓库新增 master:warehouse:add +1084 parent=506 order=3 仓库修改 master:warehouse:edit +1085 parent=506 order=4 仓库删除 master:warehouse:remove +1086 parent=506 order=5 分配用户 master:warehouse:assign +1087 parent=507 order=1 托盘查询 master:pallet:query +1088 parent=507 order=2 托盘新增 master:pallet:add +1089 parent=507 order=3 托盘修改 master:pallet:edit +1090 parent=507 order=4 托盘删除 master:pallet:remove +1091 parent=507 order=5 托盘打印 master:pallet:print +1092 parent=508 order=1 入库查询 business:inbound:query +1093 parent=508 order=2 入库新增 business:inbound:add +1094 parent=508 order=3 入库修改 business:inbound:edit +1095 parent=508 order=4 入库删除 business:inbound:remove +1096 parent=508 order=5 入库确认 business:inbound:confirm +1097 parent=509 order=1 装填查询 business:filling:query +1098 parent=509 order=2 装填新增 business:filling:add +1099 parent=510 order=1 发货查询 business:shipment:query +1100 parent=510 order=2 发货新增 business:shipment:add +1101 parent=510 order=3 发货修改 business:shipment:edit +1102 parent=510 order=4 发货删除 business:shipment:remove +1103 parent=510 order=5 发货确认 business:shipment:confirm +1104 parent=511 order=1 使用查询 business:usage:query +1105 parent=511 order=2 使用新增 business:usage:add +1106 parent=512 order=1 退回查询 business:return:query +1107 parent=512 order=2 退回申请 business:return:request +1108 parent=512 order=3 退回确认 business:return:confirm +~~~ + +### HTTP、扫码和 Service 固定契约 + +六类 master 资源都提供 GET /list、GET /{resource_id}、POST 根路径、PUT 根路径、DELETE /{resource_ids},依次使用 list/query/add/edit/remove 权限;参数名分别固定为 strain_id(s)、customer_id(s)、site_id(s)、line_id(s)、warehouse_id(s)、pallet_id(s)。额外接口为 GET/PUT /master/site/{site_id}/users、GET/PUT /master/warehouse/{warehouse_id}/users(assign)和 GET /master/pallet/{pallet_id}/qrcode(print)。 + +| 资源 | 固定接口 | 权限 | +|---|---|---| +| inbound | GET /business/inbound/list;GET /business/inbound/scan/{qr_token};GET /business/inbound/{inbound_id};POST /business/inbound;PUT /business/inbound;DELETE /business/inbound/{inbound_ids};PUT /business/inbound/{inbound_id}/confirm | list;query;query;add;edit;remove;confirm | +| filling | GET /business/filling/list;GET /business/filling/scan/{qr_token};GET /business/filling/{filling_id};POST /business/filling | list;query;query;add | +| shipment | GET /business/shipment/list;GET /business/shipment/scan/{qr_token};GET /business/shipment/{shipment_id};POST /business/shipment;PUT /business/shipment;DELETE /business/shipment/{shipment_ids};PUT /business/shipment/{shipment_id}/confirm | list;query;query;add;edit;remove;confirm | +| usage | GET /business/usage/list;GET /business/usage/scan/{qr_token};GET /business/usage/{usage_id};POST /business/usage | list;query;query;add | +| return | GET /business/return/list;GET /business/return/scan/{qr_token};GET /business/return/{return_id};POST /business/return/request;PUT /business/return/{return_id}/confirm | list;query;query;request;confirm | + +五个 /scan/{qr_token} 必须在对应资源的动态详情路由之前声明,且复用本资源 query 权限,不新增 scan 权限。扫码上下文查询参数固定为:inbound 使用 warehouseId;shipment 使用 sourceWarehouseId 和 targetSiteId;return 使用 mode=request 或 mode=confirm,confirm 模式同时传 returnId;filling、usage 不需要额外上下文。后端必须重新校验这些 ID,不信任前端选择。 + +业务人员不应为了填写单据而获得 master:*:list,因此各业务 Controller 还提供以下只读选项接口,全部在对应资源的动态详情路由之前声明并复用本资源 query 权限: + +~~~text +GET /business/inbound/warehouse-options +GET /business/filling/strain-options +GET /business/shipment/warehouse-options +GET /business/shipment/site-options +GET /business/usage/line-options?siteId={site_id} +GET /business/return/warehouse-options +~~~ + +inbound/shipment 的 warehouse 和 shipment site 选项仅返回当前用户关联且启用的数据;usage line 只返回用户已授权 site 下的启用产线;filling strain 返回启用 SKU。return warehouse 是客户发起退回所需的目标字典,只返回所有启用仓库的 id/code/name,不要求发起人拥有仓库数据权限;真正确认仍必须校验确认人对目标仓库的关联。六个报表只提供各自 GET /list;不存在导出或写接口。 + +核心 Service 签名固定为: + +~~~text +confirm_inbound_services(AsyncSession, int, ConfirmCommandModel, CurrentUserModel) -> CrudResponseModel +fill_pallet_services(AsyncSession, FillingCreateModel, CurrentUserModel) -> CrudResponseModel +confirm_shipment_services(AsyncSession, int, ConfirmCommandModel, CurrentUserModel) -> CrudResponseModel +record_usage_services(AsyncSession, UsageCreateModel, CurrentUserModel) -> CrudResponseModel +request_return_services(AsyncSession, ReturnRequestModel, CurrentUserModel) -> CrudResponseModel +confirm_return_services(AsyncSession, int, ReturnConfirmModel, CurrentUserModel) -> CrudResponseModel +~~~ + +对应 page_object 类型依次为 ConfirmCommandModel、FillingCreateModel、ConfirmCommandModel、UsageCreateModel、ReturnRequestModel、ReturnConfirmModel;返回类型统一 CrudResponseModel,current_user 统一 CurrentUserModel。 + +--- + +### Task 1: 删除 AI 专属能力和外链菜单种子 + +**Files:** +- Create: shierfeng-fastapi-backend/tests/module_business/test_ai_removal_contract.py +- Delete: shierfeng-fastapi-backend/module_ai/controller/ai_chat_controller.py +- Delete: shierfeng-fastapi-backend/module_ai/controller/ai_model_controller.py +- Delete: shierfeng-fastapi-backend/module_ai/dao/ai_chat_dao.py +- Delete: shierfeng-fastapi-backend/module_ai/dao/ai_model_dao.py +- Delete: shierfeng-fastapi-backend/module_ai/entity/do/ai_chat_do.py +- Delete: shierfeng-fastapi-backend/module_ai/entity/do/ai_model_do.py +- Delete: shierfeng-fastapi-backend/module_ai/entity/vo/ai_chat_vo.py +- Delete: shierfeng-fastapi-backend/module_ai/entity/vo/ai_model_vo.py +- Delete: shierfeng-fastapi-backend/module_ai/service/ai_chat_service.py +- Delete: shierfeng-fastapi-backend/module_ai/service/ai_model_service.py +- Delete: shierfeng-fastapi-backend/utils/ai_util.py +- Delete: shierfeng-fastapi-backend/utils/crypto_util.py +- Delete: shierfeng-fastapi-frontend/src/api/ai/chat.js +- Delete: shierfeng-fastapi-frontend/src/api/ai/model.js +- Delete: shierfeng-fastapi-frontend/src/views/ai/chat/components/AiMessage.vue +- Delete: shierfeng-fastapi-frontend/src/views/ai/chat/index.vue +- Delete: shierfeng-fastapi-frontend/src/views/ai/model/index.vue +- Delete: shierfeng-fastapi-frontend/src/assets/icons/svg/ai-chat.svg +- Delete: shierfeng-fastapi-frontend/src/assets/icons/svg/ai-manage.svg +- Delete: shierfeng-fastapi-frontend/src/assets/icons/svg/ai-model.svg +- Modify: shierfeng-fastapi-backend/common/constant.py +- Modify: shierfeng-fastapi-backend/common/annotation/rate_limit_annotation.py +- Modify: shierfeng-fastapi-backend/utils/log_util.py:593 +- Modify: shierfeng-fastapi-backend/requirements.txt +- Modify: shierfeng-fastapi-backend/requirements-pg.txt +- Modify: shierfeng-fastapi-frontend/package.json +- Modify: shierfeng-fastapi-frontend/vite/plugins/index.js +- Modify: shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql +- Modify: shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql +- Modify: README.md +- Modify: CHANGELOG.md + +- [ ] **Step 1: 写删除契约测试** + +~~~python +import json +from pathlib import Path +import re +import unittest + + +class AiRemovalContractTest(unittest.TestCase): + root = Path(__file__).resolve().parents[3] + + def test_ai_source_files_are_removed(self): + module_root = self.root / 'shierfeng-fastapi-backend/module_ai' + remaining = list(module_root.rglob('*.py')) if module_root.exists() else [] + explicit_files = ( + 'shierfeng-fastapi-backend/utils/ai_util.py', + 'shierfeng-fastapi-backend/utils/crypto_util.py', + 'shierfeng-fastapi-frontend/src/api/ai/chat.js', + 'shierfeng-fastapi-frontend/src/api/ai/model.js', + 'shierfeng-fastapi-frontend/src/views/ai/chat/components/AiMessage.vue', + 'shierfeng-fastapi-frontend/src/views/ai/chat/index.vue', + 'shierfeng-fastapi-frontend/src/views/ai/model/index.vue', + 'shierfeng-fastapi-frontend/src/assets/icons/svg/ai-chat.svg', + 'shierfeng-fastapi-frontend/src/assets/icons/svg/ai-manage.svg', + 'shierfeng-fastapi-frontend/src/assets/icons/svg/ai-model.svg', + ) + remaining.extend(self.root / path for path in explicit_files if (self.root / path).is_file()) + self.assertEqual([], [str(path.relative_to(self.root)) for path in remaining]) + + def test_ai_dependencies_are_removed(self): + backend_forbidden = { + 'agno', 'anthropic', 'cerebras-cloud-sdk', 'cohere', 'google-genai', + 'groq', 'litellm', 'llama-api-client', 'mistralai', 'ollama', + 'openai', 'portkey-ai', + } + for name in ('requirements.txt', 'requirements-pg.txt'): + content = (self.root / 'shierfeng-fastapi-backend' / name).read_text(encoding='utf-8') + installed = { + re.split(r'[<>=!~;\[]', line, maxsplit=1)[0].strip().lower() + for line in content.splitlines() + if line.strip() and not line.lstrip().startswith('#') + } + self.assertTrue(backend_forbidden.isdisjoint(installed), backend_forbidden & installed) + + package = json.loads( + (self.root / 'shierfeng-fastapi-frontend/package.json').read_text(encoding='utf-8') + ) + installed = set(package.get('dependencies', {})) | set(package.get('devDependencies', {})) + frontend_forbidden = { + 'katex', 'markstream-vue', 'mermaid', 'shiki', 'stream-markdown', + 'stream-monaco', 'uuid', 'vite-plugin-monaco-editor-esm', + } + self.assertTrue(frontend_forbidden.isdisjoint(installed), frontend_forbidden & installed) + + def test_initialization_sql_has_no_ai_or_external_menu(self): + for name in ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql'): + content = (self.root / 'shierfeng-fastapi-backend/sql' / name).read_text(encoding='utf-8') + self.assertNotIn('ai_models', content) + self.assertNotIn('ai_chat_config', content) + self.assertNotIn('ai_provider_type', content) + self.assertNotIn('施尔丰仓库', content) +~~~ + +- [ ] **Step 2: 运行测试并确认当前失败** + +Run from shierfeng-fastapi-backend: + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_ai_removal_contract -v +~~~ + +Expected: FAIL,列出仍存在的 AI 路径、依赖和初始化 SQL 内容。 + +- [ ] **Step 3: 删除专属文件并局部清理共享文件** + +仅删除文件清单中的 20 个跟踪源码/资源文件,不递归删除 module_ai 目录,不清理、不暂存现有 __pycache__ 和 .pyc;目录因未跟踪缓存继续存在不视为失败。common/constant.py 仅删除 6 个 AI namespace、DATA_SCOPE_MUTATION 中 3 个 AI 成员、AI_MODEL_MUTATION 和 AI_CHAT_CONFIG_MUTATION;rate_limit_annotation.py 仅删除 USER_INTERACTIVE_HIGH_FREQ;log_util.py 仅删除 LiteLLM 特殊日志级别;保留 transport_crypto_util.py 和通用脱敏测试。 + +requirements 两份文件删除以下 12 项,并增加 qrcode==8.2: + +~~~text +agno +anthropic +cerebras-cloud-sdk +cohere +google-genai +groq +litellm +llama-api-client +mistralai +ollama +openai +portkey-ai +~~~ + +package.json 仅删除 katex、markstream-vue、mermaid、shiki、stream-markdown、stream-monaco、uuid 和 vite-plugin-monaco-editor-esm,保留用户已有的“智链云枢管理系统”描述;vite/plugins/index.js 删除 Monaco 导入、worker 配置和插件注册。README 删除当前 AI 功能描述;CHANGELOG 在最新章节增加移除记录,但保留历史新增记录。 + +- [ ] **Step 4: 清理两套初始化 SQL** + +删除菜单 4、118、119、1061–1064、角色 2 到菜单 4 的映射、ai_provider_type 字典和 ai_models/ai_chat_config DDL;删除菜单 99。保留其他菜单 ID、字典 ID、序列和 auto_increment,不删除 CHANGELOG 中历史新增记录。 + +- [ ] **Step 5: 安装已批准的二维码依赖** + +~~~bash +conda run -n ShiErFeng python -m pip install qrcode==8.2 +~~~ + +Expected: Successfully installed qrcode-8.2;不得安装或升级其他项目依赖。 + +- [ ] **Step 6: 运行删除契约、OpenAPI 和静态检查** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_ai_removal_contract -v +conda run -n ShiErFeng python -c "from server import create_app; app=create_app(); assert not any(path.startswith('/ai/') for path in app.openapi()['paths'])" +conda run -n ShiErFeng python -m ruff check common/constant.py common/annotation/rate_limit_annotation.py utils/log_util.py tests/module_business/test_ai_removal_contract.py +~~~ + +Expected: 全部通过;OpenAPI 不包含 /ai/*。静态残留检查不得把 CHANGELOG、设计文档和未来补丁中的清理语句误判为代码残留。 + +- [ ] **Step 7: 核对并提交不重叠文件** + +~~~bash +git status --short --untracked-files=all +git diff --cached --name-status +git add -- shierfeng-fastapi-backend/tests/module_business/test_ai_removal_contract.py +git commit --only -m "重构:移除 AI 管理模块" -- README.md CHANGELOG.md shierfeng-fastapi-backend/module_ai shierfeng-fastapi-backend/utils/ai_util.py shierfeng-fastapi-backend/utils/crypto_util.py shierfeng-fastapi-backend/common/constant.py shierfeng-fastapi-backend/common/annotation/rate_limit_annotation.py shierfeng-fastapi-backend/utils/log_util.py shierfeng-fastapi-backend/requirements.txt shierfeng-fastapi-backend/requirements-pg.txt shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql shierfeng-fastapi-backend/tests/module_business/test_ai_removal_contract.py shierfeng-fastapi-frontend/src/api/ai shierfeng-fastapi-frontend/src/views/ai shierfeng-fastapi-frontend/src/assets/icons/svg/ai-chat.svg shierfeng-fastapi-frontend/src/assets/icons/svg/ai-manage.svg shierfeng-fastapi-frontend/src/assets/icons/svg/ai-model.svg shierfeng-fastapi-frontend/vite/plugins/index.js +~~~ + +不得执行 git add -A,不得对 module_ai 目录执行普通 git add。package.json 的依赖删除保留在工作区但不纳入该提交,因为同一文件含有任务开始前的用户品牌修改;现有 .idea 暂存状态必须原样保留。 + +### Task 2: 实现纯托盘状态机 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/domain/enums.py +- Create: shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_state_machine.py + +- [ ] **Step 1: 写状态迁移失败测试** + +~~~python +import unittest +from module_business.domain.enums import PalletFlowState +from module_business.domain.pallet_state_machine import InvalidPalletTransition, PalletStateMachine + + +class PalletStateMachineTest(unittest.TestCase): + def test_confirmed_flow_is_allowed(self): + allowed = ( + (PalletFlowState.UNSTOCKED, PalletFlowState.EMPTY_IN_WAREHOUSE), + (PalletFlowState.EMPTY_IN_WAREHOUSE, PalletFlowState.FILLED_IN_WAREHOUSE), + (PalletFlowState.FILLED_IN_WAREHOUSE, PalletFlowState.AT_SITE_UNUSED), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.AT_SITE_PARTIAL), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.AT_SITE_EMPTY), + (PalletFlowState.AT_SITE_UNUSED, PalletFlowState.RETURNING), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.AT_SITE_PARTIAL), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.AT_SITE_EMPTY), + (PalletFlowState.AT_SITE_PARTIAL, PalletFlowState.RETURNING), + (PalletFlowState.AT_SITE_EMPTY, PalletFlowState.RETURNING), + (PalletFlowState.RETURNING, PalletFlowState.FILLED_IN_WAREHOUSE), + (PalletFlowState.RETURNING, PalletFlowState.EMPTY_IN_WAREHOUSE), + ) + for current, target in allowed: + PalletStateMachine.ensure_transition(current, target) + + def test_refill_at_customer_site_is_rejected(self): + with self.assertRaises(InvalidPalletTransition): + PalletStateMachine.ensure_transition( + PalletFlowState.AT_SITE_EMPTY, + PalletFlowState.FILLED_IN_WAREHOUSE, + ) +~~~ + +- [ ] **Step 2: 运行测试并确认导入失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_state_machine -v +~~~ + +Expected: FAIL with ModuleNotFoundError: module_business.domain。 + +- [ ] **Step 3: 实现枚举和迁移表** + +~~~python +from enum import Enum + + +class PalletFlowState(str, Enum): + UNSTOCKED = 'UNSTOCKED' + EMPTY_IN_WAREHOUSE = 'EMPTY_IN_WAREHOUSE' + FILLED_IN_WAREHOUSE = 'FILLED_IN_WAREHOUSE' + AT_SITE_UNUSED = 'AT_SITE_UNUSED' + AT_SITE_PARTIAL = 'AT_SITE_PARTIAL' + AT_SITE_EMPTY = 'AT_SITE_EMPTY' + RETURNING = 'RETURNING' + + +class DocumentStatus(str, Enum): + DRAFT = 'DRAFT' + CONFIRMED = 'CONFIRMED' + + +class ReturnStatus(str, Enum): + REQUESTED = 'REQUESTED' + CONFIRMED = 'CONFIRMED' + + +class PalletEventType(str, Enum): + INBOUND_CONFIRMED = 'INBOUND_CONFIRMED' + FILLING_CONFIRMED = 'FILLING_CONFIRMED' + SHIPMENT_CONFIRMED = 'SHIPMENT_CONFIRMED' + USAGE_RECORDED = 'USAGE_RECORDED' + RETURN_REQUESTED = 'RETURN_REQUESTED' + RETURN_CONFIRMED = 'RETURN_CONFIRMED' +~~~ + +pallet_state_machine.py 定义与测试 allowed 元组完全相同的唯一迁移集合,允许未使用/部分使用/空托盘发起退回,并根据退回余量允许 RETURNING 回到 FILLED_IN_WAREHOUSE 或 EMPTY_IN_WAREHOUSE;不提供任意状态赋值入口。使用 str + Enum 兼容项目声明的 Python 3.10,不使用 Python 3.11 才提供的 StrEnum。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_state_machine -v +conda run -n ShiErFeng python -m ruff check module_business/domain tests/module_business/test_state_machine.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/domain/enums.py shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py shierfeng-fastapi-backend/tests/module_business/test_state_machine.py +git commit --only -m "功能:新增托盘状态机" -- shierfeng-fastapi-backend/module_business/domain/enums.py shierfeng-fastapi-backend/module_business/domain/pallet_state_machine.py shierfeng-fastapi-backend/tests/module_business/test_state_machine.py +~~~ + +### Task 3: 建立 18 张业务 ORM 模型 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/entity/do/master_do.py +- Create: shierfeng-fastapi-backend/module_business/entity/do/operation_do.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_model_contract.py + +- [ ] **Step 1: 写元数据契约测试** + +~~~python +import unittest +from config.database import Base +from module_business.entity.do import master_do, operation_do + + +class BusinessModelContractTest(unittest.TestCase): + def test_exact_business_tables_are_registered(self): + expected = { + 'biz_strain', 'biz_warehouse', 'biz_customer', 'biz_customer_site', + 'biz_production_line', 'biz_pallet', 'biz_user_warehouse', 'biz_user_site', + 'biz_inbound_order', 'biz_inbound_item', 'biz_filling_record', + 'biz_shipment_order', 'biz_shipment_item', 'biz_usage_record', + 'biz_return_order', 'biz_return_item', 'biz_pallet_inventory', + 'biz_pallet_event', + } + actual = {name for name in Base.metadata.tables if name.startswith('biz_')} + self.assertEqual(expected, actual) + + def test_pallet_identifiers_are_unique(self): + table = Base.metadata.tables['biz_pallet'] + unique_columns = {column.name for column in table.columns if column.unique} + self.assertEqual({'pallet_code', 'qr_token'}, unique_columns) +~~~ + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_model_contract -v +~~~ + +Expected: FAIL,module_business.entity.do 不存在。 + +- [ ] **Step 3: 创建主数据模型** + +master_do.py 创建 BizStrain、BizWarehouse、BizCustomer、BizCustomerSite、BizProductionLine、BizPallet、BizUserWarehouse、BizUserSite。字段和约束逐项实现“18 张表的字段与约束”章节,不增加第 19 张 biz_* 表。所有主数据使用 BigInteger 主键、UTF-8 字符串编码、CHAR(1) 状态和 0/1 CHECK、创建/更新时间;用户关联表使用 user_id + resource_id 联合主键和明确 ForeignKey。 + +关键列必须精确满足: + +~~~text +BizStrain: strain_id, strain_code UQ, strain_name, status, remark +BizWarehouse: warehouse_id, warehouse_code UQ, warehouse_name, address, status, remark +BizCustomer: customer_id, customer_code UQ, customer_name, status, remark +BizCustomerSite: site_id, customer_id FK, site_code, site_name, address, status, UQ(customer_id, site_code) +BizProductionLine: line_id, site_id FK, line_code, line_name, status, UQ(site_id, line_code) +BizPallet: pallet_id, pallet_code UQ, qr_token UQ, capacity_packages CHECK > 0, asset_status, remark +BizUserWarehouse: user_id FK sys_user, warehouse_id FK biz_warehouse +BizUserSite: user_id FK sys_user, site_id FK biz_customer_site +~~~ + +- [ ] **Step 4: 创建业务模型** + +operation_do.py 创建 10 张业务表,字段逐项实现“18 张表的字段与约束”章节。单据、退回、流转和事件状态统一使用 String(32) + 各自 CheckConstraint,不使用方言原生 Enum。包数字段使用 Integer,检查约束固定为初始量 > 0、使用量 > 0、余量 >= 0;BizReturnItem 必须包含 active_filling_id_snapshot 和 remaining_packages_snapshot,BizFillingRecord 必须保存 strain_code_snapshot、strain_name_snapshot、active_slot、closed_at 和 request_hash。 + +关键唯一约束: + +~~~text +biz_inbound_order.inbound_no +biz_inbound_order.confirm_idempotency_key +biz_filling_record.filling_no +biz_filling_record.idempotency_key +(biz_filling_record.pallet_id, active_slot) +biz_shipment_order.shipment_no +biz_shipment_order.confirm_idempotency_key +biz_usage_record.usage_no +biz_usage_record.idempotency_key +biz_return_order.return_no +biz_return_order.request_idempotency_key +biz_return_order.confirm_idempotency_key +(biz_inbound_item.inbound_id, pallet_id) +(biz_shipment_item.shipment_id, pallet_id) +(biz_return_item.return_id, pallet_id) +biz_pallet_inventory.pallet_id +(biz_pallet_event.pallet_id, event_seq) +(biz_pallet_event.pallet_id, idempotency_key) +~~~ + +biz_pallet_inventory 必须包含 active_filling_id、warehouse_id、site_id、current_line_id、flow_state、remaining_packages、last_event_id、version、updated_at;仓库和现场位置不能同时非空。元数据测试还要断言 BizReturnItem 和 BizFillingRecord 的上述快照/活动字段存在,并断言精确 18 张 biz_* 表而非子集。 + +- [ ] **Step 5: 运行元数据测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_model_contract -v +conda run -n ShiErFeng python -m ruff check module_business/entity/do tests/module_business/test_model_contract.py +~~~ + +Expected: PASS;不连接数据库。 + +- [ ] **Step 6: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/entity/do/master_do.py shierfeng-fastapi-backend/module_business/entity/do/operation_do.py shierfeng-fastapi-backend/tests/module_business/test_model_contract.py +git commit --only -m "功能:新增托盘业务数据模型" -- shierfeng-fastapi-backend/module_business/entity/do/master_do.py shierfeng-fastapi-backend/module_business/entity/do/operation_do.py shierfeng-fastapi-backend/tests/module_business/test_model_contract.py +~~~ + +### Task 4: 固化 Pydantic 输入输出契约 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/entity/vo/master_vo.py +- Create: shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py +- Create: shierfeng-fastapi-backend/module_business/entity/vo/report_vo.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py + +- [ ] **Step 1: 写别名和数量边界测试** + +~~~python +import unittest +from pydantic import ValidationError +from module_business.entity.vo.operation_vo import FillingCreateModel, UsageCreateModel + + +class BusinessVoContractTest(unittest.TestCase): + def test_filling_accepts_camel_case_and_positive_packages(self): + model = FillingCreateModel.model_validate({ + 'qrToken': 'qr-12345678', + 'warehouseId': 1, + 'strainId': 2, + 'batchNo': 'B20260724', + 'packageSpec': '20kg/包', + 'initialPackages': 10, + 'idempotencyKey': 'fill-key-1', + }) + self.assertEqual(10, model.initial_packages) + + def test_usage_rejects_zero_packages(self): + with self.assertRaises(ValidationError): + UsageCreateModel.model_validate({ + 'qrToken': 'qr-12345678', + 'lineId': 3, + 'usedPackages': 0, + 'idempotencyKey': 'usage-key-1', + }) +~~~ + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_vo_contract -v +~~~ + +Expected: FAIL,VO 文件不存在。 + +- [ ] **Step 3: 实现 VO** + +所有模型使用 ConfigDict(alias_generator=to_camel, from_attributes=True, populate_by_name=True);所有写模型额外设置 extra='forbid'。分页模型固定 page_num/page_size;包数字段使用 Field(strict=True, gt=0) 拒绝小数和字符串,ID 使用 Field(gt=0)。pallet_ids、scanned_pallet_ids 和 UserAssignModel.user_ids 至少一项并通过 field_validator 拒绝重复元素。test_vo_contract.py 额外断言 usedPackages=1.5 和 usedPackages='1' 均触发 ValidationError。 + +master_vo.py 固定定义 StrainModel/StrainPageQueryModel/DeleteStrainModel、CustomerModel/CustomerPageQueryModel/DeleteCustomerModel、SiteModel/SitePageQueryModel/DeleteSiteModel、LineModel/LinePageQueryModel/DeleteLineModel、WarehouseModel/WarehousePageQueryModel/DeleteWarehouseModel、PalletModel/PalletCreateModel/PalletEditModel/PalletPageQueryModel/DeletePalletModel 和 UserAssignModel。PalletEditModel 不声明 pallet_code 或 qr_token,避免通过 exclude 逻辑静默忽略非法修改。 + +operation_vo.py 固定模型: + +~~~text +InboundCreateModel(warehouse_id, pallet_ids) +InboundUpdateModel(inbound_id, warehouse_id, pallet_ids) +ConfirmCommandModel(idempotency_key) +FillingCreateModel(qr_token, warehouse_id, strain_id, batch_no, package_spec, initial_packages, idempotency_key) +ShipmentCreateModel(source_warehouse_id, target_site_id, pallet_ids) +ShipmentUpdateModel(shipment_id, source_warehouse_id, target_site_id, pallet_ids) +UsageCreateModel(qr_token, line_id, used_packages, idempotency_key) +ReturnRequestModel(source_site_id, target_warehouse_id, pallet_ids, idempotency_key) +ReturnConfirmModel(scanned_pallet_ids, idempotency_key) +~~~ + +operation_vo.py 还必须定义 InboundModel/InboundItemModel、FillingModel、ShipmentModel/ShipmentItemModel、UsageModel、ReturnModel/ReturnItemModel,以及 InboundPageQueryModel、FillingPageQueryModel、ShipmentPageQueryModel、UsagePageQueryModel、ReturnPageQueryModel、InboundScanQueryModel(warehouse_id)、ShipmentScanQueryModel(source_warehouse_id, target_site_id)、ReturnScanQueryModel(mode, return_id)、BusinessOptionModel 和 PalletScanResponseModel;mode 只允许 request/confirm,confirm 时 return_id 必填,request 时必须为空。扫码响应字段固定为 pallet_id、pallet_code、flow_state、warehouse_id、site_id、current_line_id、active_filling_id、strain_id、strain_code、strain_name、batch_no、package_spec、capacity_packages、remaining_packages、allowed_actions;后端按具体业务接口过滤 allowed_actions,不返回未授权动作。 + +report_vo.py 为六个报表定义独立查询和行模型:PalletInventoryReportQueryModel/PalletInventoryReportModel、FillingReportQueryModel/FillingReportModel、ShipmentReportQueryModel/ShipmentReportModel、ConsumptionReportQueryModel/ConsumptionReportModel、ReturnReportQueryModel/ReturnReportModel、PalletTraceReportQueryModel/PalletTraceReportModel。查询模型公共字段为 page_num、page_size、begin_time、end_time;页面所需过滤字段显式声明,不使用任意 dict。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_vo_contract -v +conda run -n ShiErFeng python -m ruff check module_business/entity/vo tests/module_business/test_vo_contract.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/entity/vo/master_vo.py shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py shierfeng-fastapi-backend/module_business/entity/vo/report_vo.py shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py +git commit --only -m "功能:新增托盘业务接口模型" -- shierfeng-fastapi-backend/module_business/entity/vo/master_vo.py shierfeng-fastapi-backend/module_business/entity/vo/operation_vo.py shierfeng-fastapi-backend/module_business/entity/vo/report_vo.py shierfeng-fastapi-backend/tests/module_business/test_vo_contract.py +~~~ + +### Task 5: 实现仓库和客户现场数据范围 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/dao/access_dao.py +- Create: shierfeng-fastapi-backend/module_business/service/access_service.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_access_service.py + +- [ ] **Step 1: 写管理员放行和普通用户拒绝测试** + +~~~python +from types import SimpleNamespace +import unittest +from unittest.mock import AsyncMock, patch +from exceptions.exception import PermissionException +from module_business.service.access_service import AccessService + + +class AccessServiceTest(unittest.IsolatedAsyncioTestCase): + async def test_admin_bypasses_mapping(self): + user = SimpleNamespace(user=SimpleNamespace(user_id=1, admin=True)) + await AccessService.assert_warehouse_access(AsyncMock(), user, 99) + + @patch('module_business.service.access_service.AccessDao.user_has_site', new_callable=AsyncMock) + async def test_unmapped_site_is_forbidden(self, user_has_site): + user_has_site.return_value = False + user = SimpleNamespace(user=SimpleNamespace(user_id=8, admin=False)) + with self.assertRaises(PermissionException): + await AccessService.assert_site_access(AsyncMock(), user, 7) +~~~ + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_access_service -v +~~~ + +Expected: FAIL,AccessService 不存在。 + +- [ ] **Step 3: 实现 DAO 和 Service** + +~~~python +class AccessService: + @classmethod + async def assert_warehouse_access(cls, db, current_user, warehouse_id): + if current_user.user.admin: + return + if not await AccessDao.user_has_warehouse(db, current_user.user.user_id, warehouse_id): + raise PermissionException(data='', message='无权访问该仓库') + + @classmethod + async def assert_site_access(cls, db, current_user, site_id): + if current_user.user.admin: + return + if not await AccessDao.user_has_site(db, current_user.user.user_id, site_id): + raise PermissionException(data='', message='无权访问该客户现场') +~~~ + +AccessDao 还必须提供 list_user_warehouse_ids、list_user_site_ids、replace_warehouse_users、replace_site_users;替换关联只 flush,不 commit,由上层 Service 统一事务。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_access_service -v +conda run -n ShiErFeng python -m ruff check module_business/dao/access_dao.py module_business/service/access_service.py tests/module_business/test_access_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/dao/access_dao.py shierfeng-fastapi-backend/module_business/service/access_service.py shierfeng-fastapi-backend/tests/module_business/test_access_service.py +git commit --only -m "功能:新增仓库与客户现场数据权限" -- shierfeng-fastapi-backend/module_business/dao/access_dao.py shierfeng-fastapi-backend/module_business/service/access_service.py shierfeng-fastapi-backend/tests/module_business/test_access_service.py +~~~ + +### Task 6: 实现五类非托盘基础资料 CRUD + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/dao/master_dao.py +- Create: shierfeng-fastapi-backend/module_business/service/master_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/strain_controller.py +- Create: shierfeng-fastapi-backend/module_business/controller/customer_controller.py +- Create: shierfeng-fastapi-backend/module_business/controller/site_controller.py +- Create: shierfeng-fastapi-backend/module_business/controller/line_controller.py +- Create: shierfeng-fastapi-backend/module_business/controller/warehouse_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_master_service.py + +- [ ] **Step 1: 写唯一性、层级和引用删除测试** + +使用 IsolatedAsyncioTestCase 和 patch 固定以下断言: + +~~~python +async def test_duplicate_strain_code_is_rejected(self): + with patch.object(MasterDao, 'find_strain_by_code', new=AsyncMock(return_value=object())): + with self.assertRaises(ServiceException): + await MasterService.add_strain(self.db, self.strain) + +async def test_line_must_belong_to_enabled_site(self): + with patch.object(MasterDao, 'get_site', new=AsyncMock(return_value=None)): + with self.assertRaises(ServiceException): + await MasterService.add_line(self.db, self.line) + +async def test_referenced_master_can_only_be_disabled(self): + with patch.object(MasterDao, 'count_references', new=AsyncMock(return_value=1)): + with self.assertRaises(ServiceWarning): + await MasterService.remove_strain(self.db, 1) +~~~ + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_master_service -v +~~~ + +Expected: FAIL,MasterDao/MasterService 不存在。 + +- [ ] **Step 3: 实现 DAO、Service 和 Controller** + +MasterDao 为每类主数据提供 list/get/find_by_code/add/update/delete/count_references。MasterService 负责唯一性、父级启用状态、引用删除保护、commit/rollback;唯一键并发冲突捕获 IntegrityError,rollback 后转换为明确 ServiceException,不返回原始 500。五个 Controller 的固定前缀和权限;托盘在 Task 7 单独实现: + +~~~text +/master/strain master:strain:list/query/add/edit/remove +/master/customer master:customer:list/query/add/edit/remove +/master/site master:site:list/query/add/edit/remove +/master/line master:line:list/query/add/edit/remove +/master/warehouse master:warehouse:list/query/add/edit/remove +~~~ + +每个 APIRouterPro 配置 PreAuthDependency;写接口配置 Log 和 CurrentUserDependency;分页返回 PageResponseModel;ServiceException/ServiceWarning 沿用现有全局处理。 + +- [ ] **Step 4: 运行单元测试、OpenAPI 路由检查和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_master_service -v +conda run -n ShiErFeng python -c "from server import create_app; paths=create_app().openapi()['paths']; assert '/master/strain/list' in paths and '/master/warehouse/list' in paths" +conda run -n ShiErFeng python -m ruff check module_business/dao/master_dao.py module_business/service/master_service.py module_business/controller tests/module_business/test_master_service.py +~~~ + +Expected: PASS;不启动 lifespan。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/dao/master_dao.py shierfeng-fastapi-backend/module_business/service/master_service.py shierfeng-fastapi-backend/module_business/controller/strain_controller.py shierfeng-fastapi-backend/module_business/controller/customer_controller.py shierfeng-fastapi-backend/module_business/controller/site_controller.py shierfeng-fastapi-backend/module_business/controller/line_controller.py shierfeng-fastapi-backend/module_business/controller/warehouse_controller.py shierfeng-fastapi-backend/tests/module_business/test_master_service.py +git commit --only -m "功能:新增业务基础资料接口" -- shierfeng-fastapi-backend/module_business/dao/master_dao.py shierfeng-fastapi-backend/module_business/service/master_service.py shierfeng-fastapi-backend/module_business/controller/strain_controller.py shierfeng-fastapi-backend/module_business/controller/customer_controller.py shierfeng-fastapi-backend/module_business/controller/site_controller.py shierfeng-fastapi-backend/module_business/controller/line_controller.py shierfeng-fastapi-backend/module_business/controller/warehouse_controller.py shierfeng-fastapi-backend/tests/module_business/test_master_service.py +~~~ + +### Task 7: 实现托盘、二维码和用户分配 + +**Files:** +- Modify: shierfeng-fastapi-backend/module_business/dao/master_dao.py +- Modify: shierfeng-fastapi-backend/module_business/entity/vo/master_vo.py +- Create: shierfeng-fastapi-backend/module_business/service/pallet_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/pallet_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_pallet_service.py +- Modify: shierfeng-fastapi-backend/module_business/controller/warehouse_controller.py +- Modify: shierfeng-fastapi-backend/module_business/controller/site_controller.py + +- [ ] **Step 1: 写二维码不可变和分配事务测试** + +测试必须断言:创建托盘使用 secrets.token_urlsafe(32) 生成 token 并同时创建 UNSTOCKED 快照;编辑请求不能改变 pallet_code/qr_token;PNG 结果以八字节 PNG 签名开头;替换用户关联失败时 rollback。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_pallet_service -v +~~~ + +Expected: FAIL,PalletService 不存在。 + +- [ ] **Step 3: 实现托盘与二维码服务** + +~~~python +@staticmethod +def render_qr_png(qr_token: str) -> bytes: + output = BytesIO() + image = qrcode.make(qr_token) + image.save(output, format='PNG') + return output.getvalue() +~~~ + +MasterDao 和 master_vo.py 补齐托盘 list/query/add/edit/remove 所需 DAO 与 VO。PalletService.add_pallet 在同一事务写 BizPallet 和 BizPalletInventory(UNSTOCKED, remaining_packages=0, version=0),并把 pallet_code/qr_token 唯一冲突转换为 ServiceException。edit_pallet 明确排除 pallet_code、qr_token;remove_pallet 仅允许 UNSTOCKED 且无事件。Controller 完整提供 master:pallet:list/query/add/edit/remove,并提供 GET /master/pallet/{pallet_id}/qrcode,权限为 master:pallet:print,返回 image/png。 + +仓库和客户现场 Controller 分别增加 GET/PUT /master/warehouse/{warehouse_id}/users 和 GET/PUT /master/site/{site_id}/users,权限分别为 master:warehouse:assign、master:site:assign;读取用户选项复用 SysUser,但不要求调用方额外拥有 system:user:list。 + +- [ ] **Step 4: 运行测试、OpenAPI 和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_pallet_service -v +conda run -n ShiErFeng python -c "from server import create_app; assert '/master/pallet/{pallet_id}/qrcode' in create_app().openapi()['paths']" +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_pallet_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/service/pallet_service.py shierfeng-fastapi-backend/module_business/controller/pallet_controller.py shierfeng-fastapi-backend/tests/module_business/test_pallet_service.py +git commit --only -m "功能:新增托盘二维码与用户分配" -- shierfeng-fastapi-backend/module_business/dao/master_dao.py shierfeng-fastapi-backend/module_business/entity/vo/master_vo.py shierfeng-fastapi-backend/module_business/service/pallet_service.py shierfeng-fastapi-backend/module_business/controller/pallet_controller.py shierfeng-fastapi-backend/module_business/controller/warehouse_controller.py shierfeng-fastapi-backend/module_business/controller/site_controller.py shierfeng-fastapi-backend/tests/module_business/test_pallet_service.py +~~~ + +### Task 8: 实现库存锁、幂等和事件基础设施 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/dao/inventory_dao.py +- Create: shierfeng-fastapi-backend/module_business/dao/operation_dao.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py + +- [ ] **Step 1: 写锁顺序和事件不可变测试** + +测试 patch AsyncSession.execute,断言 get_inventories_for_update([9, 2, 5]) 构造按 2、5、9 排序且带 FOR UPDATE 的 SELECT;InventoryDao 仅暴露 add_event,不存在 update_event/delete_event;同一 pallet_id + idempotency_key 返回既有事件。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_inventory_dao -v +~~~ + +Expected: FAIL,InventoryDao 不存在。 + +- [ ] **Step 3: 实现 DAO 固定签名** + +~~~text +InventoryDao.get_inventory_for_update(AsyncSession, int) -> BizPalletInventory | None +InventoryDao.get_inventories_for_update(AsyncSession, list[int]) -> list[BizPalletInventory] +InventoryDao.get_pallet_by_qr_token(AsyncSession, str) -> BizPallet | None +InventoryDao.get_pallets_by_ids(AsyncSession, list[int]) -> list[BizPallet] +InventoryDao.get_event_by_idempotency_key(AsyncSession, int, str) -> BizPalletEvent | None +InventoryDao.add_event(AsyncSession, BizPalletInventory, BizPalletEvent) -> BizPalletEvent + +OperationDao.get_inbound_for_update(AsyncSession, int) -> BizInboundOrder | None +OperationDao.get_shipment_for_update(AsyncSession, int) -> BizShipmentOrder | None +OperationDao.get_return_for_update(AsyncSession, int) -> BizReturnOrder | None +OperationDao.get_active_filling_for_update(AsyncSession, int) -> BizFillingRecord | None +OperationDao.get_inbound_by_confirm_idempotency_key(AsyncSession, str) -> BizInboundOrder | None +OperationDao.get_shipment_by_confirm_idempotency_key(AsyncSession, str) -> BizShipmentOrder | None +OperationDao.get_filling_by_idempotency_key(AsyncSession, str) -> BizFillingRecord | None +OperationDao.get_usage_by_idempotency_key(AsyncSession, str) -> BizUsageRecord | None +OperationDao.get_return_by_request_idempotency_key(AsyncSession, str) -> BizReturnOrder | None +OperationDao.get_return_by_confirm_idempotency_key(AsyncSession, str) -> BizReturnOrder | None + +OperationDao.get_inbound_list / get_inbound_by_id +OperationDao.add_inbound / update_inbound / delete_inbound / replace_inbound_items +OperationDao.get_filling_list / get_filling_by_id / add_filling_record / update_filling_record +OperationDao.get_shipment_list / get_shipment_by_id +OperationDao.add_shipment / update_shipment / delete_shipment / replace_shipment_items +OperationDao.get_usage_list / get_usage_by_id / add_usage_record +OperationDao.get_return_list / get_return_by_id / add_return_order / update_return_order / add_return_items +~~~ + +列表方法接收相应 PageQueryModel 和 is_page,返回 PageModel 或列表;by_id 返回对应 ORM 或 None;add/update/delete/replace 方法接收明确 ORM/VO,且只 add/execute/flush,不 commit,Service 不得绕过 DAO 直接执行单据持久化。get_inventories_for_update 对输入先去重、升序排序,再构造带 FOR UPDATE 的 SELECT;add_event 在同一锁内分配 event_seq,flush 后同步 inventory.last_event_id。 + +唯一键并发冲突捕获 IntegrityError 后必须先 rollback,再按幂等键重读并比较摘要:filling、usage、return request 比较 request_hash;inbound confirm、shipment confirm、return confirm 比较 confirm_request_hash。同一键同一摘要返回原结果,同键不同摘要抛 ServiceException,不得静默复用。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_inventory_dao -v +conda run -n ShiErFeng python -m ruff check module_business/dao tests/module_business/test_inventory_dao.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/dao/inventory_dao.py shierfeng-fastapi-backend/module_business/dao/operation_dao.py shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py +git commit --only -m "功能:新增托盘库存事务基础" -- shierfeng-fastapi-backend/module_business/dao/inventory_dao.py shierfeng-fastapi-backend/module_business/dao/operation_dao.py shierfeng-fastapi-backend/tests/module_business/test_inventory_dao.py +~~~ + +### Task 9: 实现首次入库 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/service/inbound_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/inbound_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py + +- [ ] **Step 1: 写整单确认与回滚测试** + +覆盖:草稿可增删明细;确认要求仓库权限;仅 UNSTOCKED 可入库;重复托盘拒绝;任一托盘非法时不写单据/事件并 rollback;同一确认幂等键重试只返回既有结果。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_inbound_service -v +~~~ + +Expected: FAIL,InboundService 不存在。 + +- [ ] **Step 3: 实现 Service 和 Controller** + +confirm_inbound_services(query_db, inbound_id, page_object, current_user) 的固定顺序:仓库权限 → 通过 get_inbound_by_confirm_idempotency_key 比较 confirm_request_hash → 按 pallet_id 加锁 → 全量状态校验 → 更新订单 CONFIRMED → 每托盘转 EMPTY_IN_WAREHOUSE → 写事件 → flush → 单次 commit。异常统一 rollback。 + +Controller 前缀 /business/inbound,权限为 list/query/add/edit/remove/confirm;增加 GET /warehouse-options 和 GET /scan/{qr_token}?warehouseId={warehouse_id},均使用 business:inbound:query,只返回授权仓库选项以及 UNSTOCKED 且可加入所选仓库的托盘信息;确认接口 PUT /{inbound_id}/confirm,请求体只包含 idempotencyKey。 + +- [ ] **Step 4: 运行测试、路由检查和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_inbound_service -v +conda run -n ShiErFeng python -c "from server import create_app; assert '/business/inbound/{inbound_id}/confirm' in create_app().openapi()['paths']" +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_inbound_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/service/inbound_service.py shierfeng-fastapi-backend/module_business/controller/inbound_controller.py shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py +git commit --only -m "功能:实现空托盘首次入库" -- shierfeng-fastapi-backend/module_business/service/inbound_service.py shierfeng-fastapi-backend/module_business/controller/inbound_controller.py shierfeng-fastapi-backend/tests/module_business/test_inbound_service.py +~~~ + +### Task 10: 实现单托盘装填 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/service/filling_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/filling_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_filling_service.py + +- [ ] **Step 1: 写状态、容量和幂等测试** + +覆盖:仅仓库空托盘允许装填;用户必须拥有仓库权限;包数为正整数且不超过托盘容量;同托盘不能存在 active filling;记录保存菌种编码/名称快照并写 active_slot=ACTIVE;同键同请求返回原结果,同键不同请求拒绝。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_filling_service -v +~~~ + +Expected: FAIL,FillingService 不存在。 + +- [ ] **Step 3: 实现装填事务** + +锁定 inventory 后创建 BizFillingRecord,写入菌种、菌种编码/名称快照、批次、规格、initial_packages 和 active_slot=ACTIVE;设置 active_filling_id、remaining_packages、FILLED_IN_WAREHOUSE、version+1;写 FILLING_CONFIRMED 事件;单次 commit。 + +Controller 前缀 /business/filling,提供 /list、/strain-options、/scan/{qr_token}、/{filling_id} 和 POST 根路径;选项、扫码、详情使用 business:filling:query,写入使用 business:filling:add。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_filling_service -v +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_filling_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/service/filling_service.py shierfeng-fastapi-backend/module_business/controller/filling_controller.py shierfeng-fastapi-backend/tests/module_business/test_filling_service.py +git commit --only -m "功能:实现单托盘装填" -- shierfeng-fastapi-backend/module_business/service/filling_service.py shierfeng-fastapi-backend/module_business/controller/filling_controller.py shierfeng-fastapi-backend/tests/module_business/test_filling_service.py +~~~ + +### Task 11: 实现多托盘整托发货 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/service/shipment_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/shipment_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py + +- [ ] **Step 1: 写整托、双数据范围和原子性测试** + +覆盖:来源仓库和目标现场权限缺一即拒绝;所有托盘必须在同一来源仓库并处于 FILLED_IN_WAREHOUSE;明细数量只能取当前全部余量;任一托盘不合法则整单 rollback;确认后直接 AT_SITE_UNUSED,无在途状态。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_shipment_service -v +~~~ + +Expected: FAIL,ShipmentService 不存在。 + +- [ ] **Step 3: 实现发货事务** + +草稿保存托盘 ID,不接受客户端发货包数;shipment_item.active_filling_id/package_qty 在草稿中为空。confirm_shipment_services(query_db, shipment_id, page_object, current_user) 先通过 get_shipment_by_confirm_idempotency_key 比较 confirm_request_hash,再按 pallet_id 锁定并生成 shipment_item.active_filling_id/package_qty 快照,清空 warehouse_id,设置 site_id、current_line_id=None、AT_SITE_UNUSED、version+1,并逐托盘写 SHIPMENT_CONFIRMED 事件。 + +Controller 前缀 /business/shipment,权限为 list/query/add/edit/remove/confirm;增加 GET /warehouse-options、GET /site-options 和 GET /scan/{qr_token}?sourceWarehouseId={source_warehouse_id}&targetSiteId={target_site_id},均使用 business:shipment:query;后端校验双数据范围且只返回可整托发货的 FILLED_IN_WAREHOUSE 托盘;确认接口 PUT /{shipment_id}/confirm。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_shipment_service -v +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_shipment_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/service/shipment_service.py shierfeng-fastapi-backend/module_business/controller/shipment_controller.py shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py +git commit --only -m "功能:实现多托盘发货出库" -- shierfeng-fastapi-backend/module_business/service/shipment_service.py shierfeng-fastapi-backend/module_business/controller/shipment_controller.py shierfeng-fastapi-backend/tests/module_business/test_shipment_service.py +~~~ + +### Task 12: 实现客户现场使用 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/service/usage_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/usage_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_usage_service.py + +- [ ] **Step 1: 写首次锁线、超用和重复提交测试** + +覆盖:用户必须拥有托盘所在现场权限;产线必须属于该现场;第一次使用写 current_line_id;后续异线拒绝;used_packages 不得超过余量;余量归零转 AT_SITE_EMPTY,并清 active_filling_id/active_slot、写 closed_at;同一幂等键不重复扣减。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_usage_service -v +~~~ + +Expected: FAIL,UsageService 不存在。 + +- [ ] **Step 3: 实现使用事务** + +record_usage_services 依次解析二维码、校验 site 权限、锁定 inventory、校验 active filling/current_line_id/数量、创建 usage_record、扣减余量、设置 AT_SITE_PARTIAL 或 AT_SITE_EMPTY、version+1、写 USAGE_RECORDED 事件并 commit。首次使用同时写 inventory.current_line_id;后续只能使用相同 line_id。余量归零时把活动装填记录 active_slot 置 NULL、写 closed_at 并清 inventory.active_filling_id,但 current_line_id 保留到退回确认以反映当前现场位置。 + +Controller 前缀 /business/usage,提供 /list、/line-options?siteId={site_id}、/scan/{qr_token}、/{usage_id}、POST;选项、扫码、详情使用 business:usage:query,写入使用 business:usage:add。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_usage_service -v +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_usage_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/service/usage_service.py shierfeng-fastapi-backend/module_business/controller/usage_controller.py shierfeng-fastapi-backend/tests/module_business/test_usage_service.py +git commit --only -m "功能:实现客户现场使用记录" -- shierfeng-fastapi-backend/module_business/service/usage_service.py shierfeng-fastapi-backend/module_business/controller/usage_controller.py shierfeng-fastapi-backend/tests/module_business/test_usage_service.py +~~~ + +### Task 13: 实现两阶段退回 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/service/return_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/return_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_return_service.py + +- [ ] **Step 1: 写发起冻结、收货确认和残料再发测试** + +覆盖:同一退回单托盘必须属于同一 site;发起人拥有 site 权限;发起后 RETURNING 且不能使用;确认人拥有目标 warehouse 权限;确认扫码集合与单据明细一致;remaining_packages_snapshot 或 active_filling_id_snapshot 任一被改变时拒绝;有余料回 FILLED_IN_WAREHOUSE 并保留活动装填,无余料回 EMPTY_IN_WAREHOUSE 且活动装填为空;两者均清 current_line_id。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_return_service -v +~~~ + +Expected: FAIL,ReturnService 不存在。 + +- [ ] **Step 3: 实现发起和确认事务** + +request_return_services 创建 REQUESTED 单据,为每个托盘保存 active_filling_id_snapshot 和 remaining_packages_snapshot,使用 request_hash 做申请幂等,状态转 RETURNING,写 RETURN_REQUESTED 事件。confirm_return_services(query_db, return_id, page_object, current_user) 通过 get_return_by_confirm_idempotency_key 比较 confirm_request_hash 后,重新按 pallet_id 加锁,核对状态、两个快照和扫码集合,将位置转入仓库,按余量选择目标状态,清 site_id/current_line_id,写 RETURN_CONFIRMED 事件并确认单据。有余料时必须保留 active_filling_id 和 active_slot 以支持再次整托发货;无余料时两者必须为空。 + +Controller 前缀 /business/return,权限为 list/query/request/confirm;增加 GET /warehouse-options、GET /scan/{qr_token}?mode=request 和 GET /scan/{qr_token}?mode=confirm&returnId={return_id},均使用 business:return:query,按当前操作人的 site/warehouse 数据范围返回可发起或可确认的托盘;写接口为 POST /request 和 PUT /{return_id}/confirm。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_return_service -v +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_return_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/service/return_service.py shierfeng-fastapi-backend/module_business/controller/return_controller.py shierfeng-fastapi-backend/tests/module_business/test_return_service.py +git commit --only -m "功能:实现托盘两阶段退回" -- shierfeng-fastapi-backend/module_business/service/return_service.py shierfeng-fastapi-backend/module_business/controller/return_controller.py shierfeng-fastapi-backend/tests/module_business/test_return_service.py +~~~ + +### Task 14: 实现六类只读报表 + +**Files:** +- Create: shierfeng-fastapi-backend/module_business/dao/report_dao.py +- Create: shierfeng-fastapi-backend/module_business/service/report_service.py +- Create: shierfeng-fastapi-backend/module_business/controller/report_controller.py +- Create: shierfeng-fastapi-backend/tests/module_business/test_report_service.py + +- [ ] **Step 1: 写数据范围和查询来源测试** + +测试六个方法均将管理员/普通用户可见 warehouse_ids/site_ids 传入 DAO;库存报表读取 inventory + active filling,装填报表读取 BizFillingRecord.strain_code_snapshot/strain_name_snapshot,追溯读取 pallet_event 的前后状态、位置和数量字段;不得从可变菌种主数据反推历史名称,不得从 sys_oper_log 构造业务报表;接口不包含导出方法。 + +- [ ] **Step 2: 运行测试并确认失败** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_report_service -v +~~~ + +Expected: FAIL,ReportService 不存在。 + +- [ ] **Step 3: 实现 DAO、Service 和 Controller** + +固定只读接口: + +~~~text +GET /report/pallet-inventory/list report:palletinventory:list +GET /report/filling/list report:filling:list +GET /report/shipment/list report:shipment:list +GET /report/consumption/list report:consumption:list +GET /report/return/list report:return:list +GET /report/pallet-trace/list report:pallettrace:list +~~~ + +全部使用 PageUtil.paginate;每个查询显式应用日期和对应业务过滤字段;普通用户查询条件与授权 warehouse/site 取交集;无可见范围返回空分页,不泄露总数。 + +ReportDao/ReportService 方法名逐项固定为 get_pallet_inventory_list/get_pallet_inventory_list_services、get_filling_report_list/get_filling_report_list_services、get_shipment_report_list/get_shipment_report_list_services、get_consumption_report_list/get_consumption_report_list_services、get_return_report_list/get_return_report_list_services、get_pallet_trace_list/get_pallet_trace_list_services;Controller 不通过一个 mode 参数复用单一路由。 + +- [ ] **Step 4: 运行测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_report_service -v +conda run -n ShiErFeng python -m ruff check module_business tests/module_business/test_report_service.py +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/module_business/dao/report_dao.py shierfeng-fastapi-backend/module_business/service/report_service.py shierfeng-fastapi-backend/module_business/controller/report_controller.py shierfeng-fastapi-backend/tests/module_business/test_report_service.py +git commit --only -m "功能:新增托盘业务报表接口" -- shierfeng-fastapi-backend/module_business/dao/report_dao.py shierfeng-fastapi-backend/module_business/service/report_service.py shierfeng-fastapi-backend/module_business/controller/report_controller.py shierfeng-fastapi-backend/tests/module_business/test_report_service.py +~~~ + +### Task 15: 固化后端 API 与权限契约 + +**Files:** +- Create: shierfeng-fastapi-backend/tests/module_business/test_api_permission_contract.py +- Modify: shierfeng-fastapi-backend/module_business/controller/*.py + +- [ ] **Step 1: 写完整 OpenAPI 路径和源码权限集合测试** + +测试创建 app 但不进入 lifespan。预期路径集合覆盖 6 个 master、5 个 business、6 个 report 前缀;源码提取 UserInterfaceAuthDependency 字符串,必须等于设计稿权限集合;任何 /ai/ 路径均失败。 + +~~~python +def test_no_ai_and_required_business_paths(self): + from server import create_app + paths = set(create_app().openapi()['paths']) + self.assertFalse(any(path.startswith('/ai/') for path in paths)) + for resource in ('inbound', 'filling', 'shipment', 'usage', 'return'): + self.assertIn(f'/business/{resource}/scan/{{qr_token}}', paths) + for path in ( + '/business/inbound/warehouse-options', + '/business/filling/strain-options', + '/business/shipment/warehouse-options', + '/business/shipment/site-options', + '/business/usage/line-options', + '/business/return/warehouse-options', + ): + self.assertIn(path, paths) + self.assertIn('/business/return/{return_id}/confirm', paths) + self.assertIn('/report/pallet-trace/list', paths) +~~~ + +- [ ] **Step 2: 运行测试并修正契约差异** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_api_permission_contract -v +~~~ + +Expected: 首次运行可能因遗漏路由/权限失败;仅修正 Controller 与已确认契约,不扩大功能。 + +- [ ] **Step 3: 运行所有后端业务单元测试和 Ruff** + +~~~bash +conda run -n ShiErFeng python -m unittest discover -s tests/module_business -p "test_*.py" -v +conda run -n ShiErFeng python -m ruff format --check module_business tests/module_business +conda run -n ShiErFeng python -m ruff check module_business tests/module_business +~~~ + +Expected: PASS;不连接数据库。 + +- [ ] **Step 4: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/tests/module_business/test_api_permission_contract.py +git commit --only -m "测试:固化业务接口权限契约" -- shierfeng-fastapi-backend/module_business/controller/strain_controller.py shierfeng-fastapi-backend/module_business/controller/customer_controller.py shierfeng-fastapi-backend/module_business/controller/site_controller.py shierfeng-fastapi-backend/module_business/controller/line_controller.py shierfeng-fastapi-backend/module_business/controller/warehouse_controller.py shierfeng-fastapi-backend/module_business/controller/pallet_controller.py 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/controller/report_controller.py shierfeng-fastapi-backend/tests/module_business/test_api_permission_contract.py +~~~ + +### Task 16: 新增 Web API、扫码工具和契约测试 + +**Files:** +- Create: shierfeng-fastapi-frontend/src/api/master/*.js +- Create: shierfeng-fastapi-frontend/src/api/business/*.js +- Create: shierfeng-fastapi-frontend/src/api/report/*.js +- Create: shierfeng-fastapi-frontend/src/utils/palletScan.js +- Create: shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue +- Create: shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +- Modify: shierfeng-fastapi-frontend/package.json + +- [ ] **Step 1: 写 Node 20 失败测试** + +~~~javascript +import test from 'node:test' +import assert from 'node:assert/strict' +import fs from 'node:fs' +import { normalizeScanValue, isDuplicateScan } from '../src/utils/palletScan.js' + +test('扫码值去除首尾空白并拒绝空值', () => { + assert.equal(normalizeScanValue(' qr-12345678\n'), 'qr-12345678') + assert.throws(() => normalizeScanValue(' '), /二维码不能为空/) +}) + +test('短时间重复扫码会被识别', () => { + assert.equal(isDuplicateScan('qr-a', 'qr-a', 1200, 1000, 500), true) + assert.equal(isDuplicateScan('qr-a', 'qr-b', 1200, 1000, 500), false) +}) + +test('业务 API 文件不出现未确认导出接口', () => { + const reportApi = fs.readFileSync(new URL('../src/api/report/palletInventory.js', import.meta.url), 'utf8') + assert.doesNotMatch(reportApi, /export/i) +}) +~~~ + +同一脚本还要以“HTTP、扫码和 Service 固定契约”为硬编码 expectedRequests,逐个读取 17 个 API 文件并核对 request 的 url 和 method;必须显式覆盖五个 /business/*/scan/{qr_token}、六个业务选项 GET、三个确认 PUT、退回 POST /request、托盘二维码 GET 和六个报表 GET /list,不能只比较前后端彼此相等。 + +- [ ] **Step 2: 运行测试并确认模块不存在** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +~~~ + +Expected: FAIL,palletScan.js 或 API 文件不存在。 + +- [ ] **Step 3: 实现工具、API 和扫码组件** + +palletScan.js 导出 normalizeScanValue、isDuplicateScan、createIdempotencyKey;createIdempotencyKey 使用 globalThis.crypto.randomUUID(),不可回退到时间戳。 + +17 个 API 文件严格映射“HTTP、扫码和 Service 固定契约”。PalletScanInput 接收 disabled、loading、minLength、duplicateWindowMs;发出 scan、invalid、duplicate;Enter 时 trim、校验、抑制短时间重复码;提交完成后 nextTick 重新 focus。样式使用最小 48px 控件高度和移动端全宽,不调用摄像头/BarcodeDetector。 + +package.json 增加: + +~~~json +"test:business": "node --test scripts/verify-business-contract.mjs" +~~~ + +保留 Task 1 已完成的 AI 依赖删除和用户品牌描述。 + +- [ ] **Step 4: 运行 Node 测试** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-frontend/src/api/master/strain.js shierfeng-fastapi-frontend/src/api/master/customer.js shierfeng-fastapi-frontend/src/api/master/site.js shierfeng-fastapi-frontend/src/api/master/line.js shierfeng-fastapi-frontend/src/api/master/warehouse.js shierfeng-fastapi-frontend/src/api/master/pallet.js shierfeng-fastapi-frontend/src/api/business/inbound.js shierfeng-fastapi-frontend/src/api/business/filling.js shierfeng-fastapi-frontend/src/api/business/shipment.js shierfeng-fastapi-frontend/src/api/business/usage.js shierfeng-fastapi-frontend/src/api/business/return.js shierfeng-fastapi-frontend/src/api/report/palletInventory.js shierfeng-fastapi-frontend/src/api/report/filling.js shierfeng-fastapi-frontend/src/api/report/shipment.js shierfeng-fastapi-frontend/src/api/report/consumption.js shierfeng-fastapi-frontend/src/api/report/return.js shierfeng-fastapi-frontend/src/api/report/palletTrace.js shierfeng-fastapi-frontend/src/utils/palletScan.js shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +git commit --only -m "功能:新增业务接口与扫码组件" -- shierfeng-fastapi-frontend/src/api/master/strain.js shierfeng-fastapi-frontend/src/api/master/customer.js shierfeng-fastapi-frontend/src/api/master/site.js shierfeng-fastapi-frontend/src/api/master/line.js shierfeng-fastapi-frontend/src/api/master/warehouse.js shierfeng-fastapi-frontend/src/api/master/pallet.js shierfeng-fastapi-frontend/src/api/business/inbound.js shierfeng-fastapi-frontend/src/api/business/filling.js shierfeng-fastapi-frontend/src/api/business/shipment.js shierfeng-fastapi-frontend/src/api/business/usage.js shierfeng-fastapi-frontend/src/api/business/return.js shierfeng-fastapi-frontend/src/api/report/palletInventory.js shierfeng-fastapi-frontend/src/api/report/filling.js shierfeng-fastapi-frontend/src/api/report/shipment.js shierfeng-fastapi-frontend/src/api/report/consumption.js shierfeng-fastapi-frontend/src/api/report/return.js shierfeng-fastapi-frontend/src/api/report/palletTrace.js shierfeng-fastapi-frontend/src/utils/palletScan.js shierfeng-fastapi-frontend/src/components/PalletScanInput/index.vue shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +~~~ + +package.json 的 test:business 脚本和 Task 1 依赖删除继续保留在工作区,不纳入该提交;只有用户另行确认允许同时提交任务开始前的品牌描述修改时,才提交 package.json 当前内容。 + +### Task 17: 实现六个基础资料页面 + +**Files:** +- Create: shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue +- Create: shierfeng-fastapi-frontend/src/views/master/strain/index.vue +- Create: shierfeng-fastapi-frontend/src/views/master/customer/index.vue +- Create: shierfeng-fastapi-frontend/src/views/master/site/index.vue +- Create: shierfeng-fastapi-frontend/src/views/master/line/index.vue +- Create: shierfeng-fastapi-frontend/src/views/master/warehouse/index.vue +- Create: shierfeng-fastapi-frontend/src/views/master/pallet/index.vue +- Modify: shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs + +- [ ] **Step 1: 扩展契约测试** + +读取六个 Vue 文件并断言每个页面包含对应 list 权限、按钮 v-hasPermi、Pagination;仓库/现场包含 assign;托盘包含 master:pallet:print 且不允许编辑 palletCode/qrToken。 + +- [ ] **Step 2: 运行测试并确认页面缺失** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +~~~ + +Expected: FAIL,页面文件不存在。 + +- [ ] **Step 3: 实现页面** + +页面复用 system/post/index.vue 的搜索、分页、表格和弹窗结构,不提供导入导出。字段固定为设计稿主数据字段;删除按钮只在后端返回 canRemove=true 时显示,停用操作走 edit。BusinessUserAssignDialog 复用角色用户选择弹窗交互,但调用 warehouse/site 自身 users API。 + +托盘页面创建时只输入 palletCode、capacityPackages、remark;详情显示只读 qrToken;打印按钮请求 PNG Blob 并打开独立打印窗口,不在浏览器生成二维码。 + +- [ ] **Step 4: 运行契约测试和生产构建** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node /Users/jiaolongyan/.nvm/versions/node/v20.20.2/lib/node_modules/npm/bin/npm-cli.js run build:prod +~~~ + +Expected: PASS;Vite build completed successfully。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue shierfeng-fastapi-frontend/src/views/master/strain/index.vue shierfeng-fastapi-frontend/src/views/master/customer/index.vue shierfeng-fastapi-frontend/src/views/master/site/index.vue shierfeng-fastapi-frontend/src/views/master/line/index.vue shierfeng-fastapi-frontend/src/views/master/warehouse/index.vue shierfeng-fastapi-frontend/src/views/master/pallet/index.vue +git commit --only -m "功能:新增业务基础资料页面" -- shierfeng-fastapi-frontend/src/components/BusinessUserAssignDialog/index.vue shierfeng-fastapi-frontend/src/views/master/strain/index.vue shierfeng-fastapi-frontend/src/views/master/customer/index.vue shierfeng-fastapi-frontend/src/views/master/site/index.vue shierfeng-fastapi-frontend/src/views/master/line/index.vue shierfeng-fastapi-frontend/src/views/master/warehouse/index.vue shierfeng-fastapi-frontend/src/views/master/pallet/index.vue shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +~~~ + +### Task 18: 实现五个业务操作页面 + +**Files:** +- Create: shierfeng-fastapi-frontend/src/views/business/inbound/index.vue +- Create: shierfeng-fastapi-frontend/src/views/business/filling/index.vue +- Create: shierfeng-fastapi-frontend/src/views/business/shipment/index.vue +- Create: shierfeng-fastapi-frontend/src/views/business/usage/index.vue +- Create: shierfeng-fastapi-frontend/src/views/business/return/index.vue +- Modify: shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs + +- [ ] **Step 1: 扩展业务页面契约测试** + +断言五页分别包含已确认权限;入库/发货只有草稿显示编辑删除;确认按钮使用 confirm;使用页使用 PalletScanInput 和 business:usage:add;退回页分别使用 request/confirm;所有提交函数在 Promise 未完成时禁用扫码和按钮。幂等写操作在首次构造请求时生成 key,网络失败后的原请求重试复用同一个 key,只在成功、取消或业务输入改变后清空并重新生成。 + +- [ ] **Step 2: 运行测试并确认页面缺失** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +~~~ + +Expected: FAIL。 + +- [ ] **Step 3: 实现入库、装填和发货页面** + +入库:通过本业务 warehouse-options 选择授权仓库、连续扫码待入库托盘、明细去重、确认前二次确认。 + +装填:扫码后显示托盘/仓库,通过本业务 strain-options 选择菌种,输入 batchNo、packageSpec、正整数 initialPackages,首次提交时生成并保存幂等键;同一表单重试不得重新生成。 + +发货:通过本业务 warehouse-options/site-options 选择来源仓库和目标现场,连续扫描有料托盘;数量只读显示后端 remainingPackages;确认前二次确认。 + +- [ ] **Step 4: 实现使用和退回 PDA 页面** + +使用:移动端大控件,扫码后展示菌种/批次/余量;首次通过本业务 line-options 选择现场产线,后续只读;输入正整数本次使用包数;成功后显示新余量和状态。入库确认、发货确认、使用、退回申请和退回确认采用与装填相同的幂等键生命周期。 + +退回:同页以权限区分“客户发起”和“仓库确认”操作区;发起时通过本业务 warehouse-options 选择目标仓库并扫描同现场托盘;确认时扫描退回单全部托盘,集合不一致时不提交。五个业务页面不得调用 master API 获取操作选项。 + +- [ ] **Step 5: 运行契约测试和生产构建** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node /Users/jiaolongyan/.nvm/versions/node/v20.20.2/lib/node_modules/npm/bin/npm-cli.js run build:prod +~~~ + +Expected: PASS。 + +- [ ] **Step 6: 提交** + +~~~bash +git add -- shierfeng-fastapi-frontend/src/views/business/inbound/index.vue shierfeng-fastapi-frontend/src/views/business/filling/index.vue shierfeng-fastapi-frontend/src/views/business/shipment/index.vue shierfeng-fastapi-frontend/src/views/business/usage/index.vue shierfeng-fastapi-frontend/src/views/business/return/index.vue +git commit --only -m "功能:新增托盘业务操作页面" -- shierfeng-fastapi-frontend/src/views/business/inbound/index.vue shierfeng-fastapi-frontend/src/views/business/filling/index.vue shierfeng-fastapi-frontend/src/views/business/shipment/index.vue shierfeng-fastapi-frontend/src/views/business/usage/index.vue shierfeng-fastapi-frontend/src/views/business/return/index.vue shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +~~~ + +### Task 19: 实现六个报表页面 + +**Files:** +- Create: shierfeng-fastapi-frontend/src/views/report/palletInventory/index.vue +- Create: shierfeng-fastapi-frontend/src/views/report/filling/index.vue +- Create: shierfeng-fastapi-frontend/src/views/report/shipment/index.vue +- Create: shierfeng-fastapi-frontend/src/views/report/consumption/index.vue +- Create: shierfeng-fastapi-frontend/src/views/report/return/index.vue +- Create: shierfeng-fastapi-frontend/src/views/report/palletTrace/index.vue +- Modify: shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs + +- [ ] **Step 1: 扩展报表契约测试** + +断言六页只有 list 权限,没有 add/edit/remove/export;每页包含分页和搜索重置;追溯页面按 occurredAt 展示 before/after 状态、位置和数量。 + +- [ ] **Step 2: 运行测试并确认页面缺失** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +~~~ + +Expected: FAIL。 + +- [ ] **Step 3: 实现报表页面** + +六页字段和过滤器严格按设计稿第 9.3 节实现。所有筛选提交时 pageNum 重置为 1;空数据展示 Element Plus empty;不添加导出按钮、不调用 proxy.download。 + +- [ ] **Step 4: 运行契约测试和生产构建** + +~~~bash +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node /Users/jiaolongyan/.nvm/versions/node/v20.20.2/lib/node_modules/npm/bin/npm-cli.js run build:prod +~~~ + +Expected: PASS。 + +- [ ] **Step 5: 提交** + +~~~bash +git add -- shierfeng-fastapi-frontend/src/views/report/palletInventory/index.vue shierfeng-fastapi-frontend/src/views/report/filling/index.vue shierfeng-fastapi-frontend/src/views/report/shipment/index.vue shierfeng-fastapi-frontend/src/views/report/consumption/index.vue shierfeng-fastapi-frontend/src/views/report/return/index.vue shierfeng-fastapi-frontend/src/views/report/palletTrace/index.vue +git commit --only -m "功能:新增托盘业务报表页面" -- shierfeng-fastapi-frontend/src/views/report/palletInventory/index.vue shierfeng-fastapi-frontend/src/views/report/filling/index.vue shierfeng-fastapi-frontend/src/views/report/shipment/index.vue shierfeng-fastapi-frontend/src/views/report/consumption/index.vue shierfeng-fastapi-frontend/src/views/report/return/index.vue shierfeng-fastapi-frontend/src/views/report/palletTrace/index.vue shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +~~~ + +### Task 20: 同步两套初始化 SQL 并生成 MySQL 补丁 + +**Files:** +- Create: shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql +- Create: shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py +- Modify: shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql +- Modify: shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql + +- [ ] **Step 1: 写 SQL 静态契约测试** + +~~~python +from pathlib import Path +import re +import unittest + + +class SqlContractTest(unittest.TestCase): + root = Path(__file__).resolve().parents[3] / 'shierfeng-fastapi-backend/sql' + + def test_initializers_have_exact_business_tables_and_permissions(self): + expected_tables = { + 'biz_strain', 'biz_warehouse', 'biz_customer', 'biz_customer_site', + 'biz_production_line', 'biz_pallet', 'biz_user_warehouse', 'biz_user_site', + 'biz_inbound_order', 'biz_inbound_item', 'biz_filling_record', + 'biz_shipment_order', 'biz_shipment_item', 'biz_usage_record', + 'biz_return_order', 'biz_return_item', 'biz_pallet_inventory', + 'biz_pallet_event', + } + for name in ('shierfeng-fastapi.sql', 'shierfeng-fastapi-pg.sql'): + sql = (self.root / name).read_text(encoding='utf-8').lower() + actual_tables = set(re.findall( + r'create\s+table\s+(?:if\s+not\s+exists\s+)?[`"]?(biz_[a-z0-9_]+)', + sql, + )) + self.assertEqual(expected_tables, actual_tables) + for column in ( + 'strain_code_snapshot', 'strain_name_snapshot', 'active_slot', + 'active_filling_id_snapshot', 'remaining_packages_snapshot', + 'confirm_idempotency_key', 'request_idempotency_key', 'request_hash', + ): + self.assertIn(column, sql) + for permission in ('master:pallet:print', 'business:return:confirm', 'report:pallettrace:list'): + self.assertIn(permission, sql) + + def test_patch_never_mutates_ai_tables(self): + raw = (self.root / 'patches/20260724_pallet_workflow_mysql.sql').read_text( + encoding='utf-8' + ).lower() + sql = re.sub(r'/\*.*?\*/', '', raw, flags=re.S) + sql = re.sub(r'(?m)^\s*(?:--|#).*$\n?', '', sql).replace('`', '') + ai_tables = ('ai_models', 'ai_chat_config', 'ai_sessions', 'ai_memories', 'ai_metrics', + 'ai_eval_runs', 'ai_knowledge', 'ai_culture', 'ai_traces', 'ai_spans', + 'ai_schema_versions') + for table in ai_tables: + qualified = rf'(?:[a-z0-9_]+\.)?{re.escape(table)}' + patterns = ( + rf'\bdrop\s+(?:temporary\s+)?table\s+(?:if\s+exists\s+)?{qualified}\b', + rf'\btruncate\s+(?:table\s+)?{qualified}\b', + rf'\bdelete\s+from\s+{qualified}\b', + ) + for pattern in patterns: + self.assertIsNone(re.search(pattern, sql), f'危险 AI 表操作: {table}') +~~~ + +- [ ] **Step 2: 运行测试并确认业务 SQL 缺失** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_sql_contract -v +~~~ + +Expected: FAIL,业务表或补丁不存在。 + +- [ ] **Step 3: 同步 18 张表 DDL** + +以“18 张表的字段与约束”及 ORM 元数据为唯一契约,在 MySQL/PostgreSQL 初始化脚本分别写出等价 DDL。SQL 测试必须精确提取 18 张 biz_* 表,并检查快照、活动装填、幂等摘要、唯一约束、外键、检查约束和索引。不得新增 Alembic revision,不改 create_all 行为,不执行 SQL。 + +- [ ] **Step 4: 写入固定菜单和权限** + +目录 ID:121 基础资料维护、122 业务管理、123 报表查询。 + +页面 502–518 必须逐项使用“菜单页面固定映射”表中的 menu_name、parent_id、path、component、API 语义和 perms;按钮 1065–1108 必须逐行使用“按钮 ID 固定映射”,不得在实现时重新排序。不写任何新 sys_role_menu。两套脚本保持 ID、parent_id、menu_name、path、route_name、order_num、perms 和 component 完全一致。 + +- [ ] **Step 5: 生成一份不执行的 MySQL 增量补丁** + +补丁包含 18 张表 CREATE TABLE、菜单/权限 INSERT、明确 ID 的 AI 菜单/字典和菜单 99 DELETE。DDL 和 DML 分节,注明 MySQL DDL 隐式提交;禁止 DROP/TRUNCATE/DELETE 任何 AI 业务表。补丁末尾记录应用后需要清理 /getRouters、用户信息和 sys_dict:ai_provider_type 缓存,但不自动执行缓存命令。 + +- [ ] **Step 6: 运行 SQL 测试和差异检查** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_sql_contract -v +rg -n "master:|business:|report:" shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql +~~~ + +Expected: PASS;两份权限集合相同;补丁安全测试无 AI 表删除命中。 + +- [ ] **Step 7: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py +git commit --only -m "功能:新增托盘业务数据库脚本" -- shierfeng-fastapi-backend/sql/shierfeng-fastapi.sql shierfeng-fastapi-backend/sql/shierfeng-fastapi-pg.sql shierfeng-fastapi-backend/sql/patches/20260724_pallet_workflow_mysql.sql shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py +~~~ + +### Task 21: 校验前后端权限、组件和 API 一致性 + +**Files:** +- Modify: shierfeng-fastapi-backend/tests/module_business/test_api_permission_contract.py +- Modify: shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py +- Modify: shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs + +- [ ] **Step 1: 增加四方权限集合比较** + +Python 测试提取 SQL sys_menu.perms、后端 UserInterfaceAuthDependency 和 Vue v-hasPermi;Node 测试检查 17 个 component 路径存在。预期集合硬编码为本计划的 17 个页面 list 权限和 44 个按钮权限,共 61 个;禁止只比较“彼此相等”而同时漏项。菜单测试还逐项比较 121–123、502–518、1065–1108 的 parent_id、menu_name、path、route_name、order_num、perms 和 component。 + +- [ ] **Step 2: 运行契约测试并修复差异** + +~~~bash +conda run -n ShiErFeng python -m unittest tests.module_business.test_api_permission_contract tests.module_business.test_sql_contract -v +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +~~~ + +Expected: PASS;SQL、后端、Web 和组件文件映射一致。 + +- [ ] **Step 3: 提交** + +~~~bash +git add -- shierfeng-fastapi-backend/tests/module_business/test_api_permission_contract.py shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +git commit --only -m "测试:校验菜单接口权限一致性" -- shierfeng-fastapi-backend/tests/module_business/test_api_permission_contract.py shierfeng-fastapi-backend/tests/module_business/test_sql_contract.py shierfeng-fastapi-frontend/scripts/verify-business-contract.mjs +~~~ + +### Task 22: 完成静态验证和交付文档 + +**Files:** +- Modify: README.md +- Modify: CHANGELOG.md +- Create: docs/pallet-workflow-deployment.md + +- [ ] **Step 1: 更新文档** + +README 说明新增基础资料、五类业务、六类报表和 PDA Web;CHANGELOG 增加本次功能与 AI 移除条目但保留历史记录;部署文档明确: + +- 先备份数据库并审查补丁。 +- 本轮未执行补丁。 +- 现有 AI/Agno 表可能残留及其精确表名。 +- 应用补丁后清理菜单/用户/字典缓存。 +- 只有应用补丁后才允许启动后端做运行态验收。 + +- [ ] **Step 2: 运行全部无数据库验证** + +~~~bash +cd /Users/jiaolongyan/PycharmProjects/牛牛小屋/ShiErFeng/shierfeng-fastapi-backend +conda run -n ShiErFeng python -m unittest discover -s tests/module_business -p "test_*.py" -v +conda run -n ShiErFeng python -m ruff format --check module_business tests/module_business +conda run -n ShiErFeng python -m ruff check module_business tests/module_business +conda run -n ShiErFeng python -c "from server import create_app; app=create_app(); paths=app.openapi()['paths']; assert not any(path.startswith('/ai/') for path in paths)" +conda run -n ShiErFeng python -m pip check +cd /Users/jiaolongyan/PycharmProjects/牛牛小屋/ShiErFeng/shierfeng-fastapi-frontend +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs +/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node /Users/jiaolongyan/.nvm/versions/node/v20.20.2/lib/node_modules/npm/bin/npm-cli.js run build:prod +~~~ + +Expected: 所有命令通过。构建产物如 dist 由现有 .gitignore 管理,不加入提交。 + +- [ ] **Step 3: 检查 AI 源码、接口和依赖残留** + +~~~bash +cd /Users/jiaolongyan/PycharmProjects/牛牛小屋/ShiErFeng/shierfeng-fastapi-backend +conda run -n ShiErFeng python -c "from pathlib import Path; p=Path('module_ai'); files=[] if not p.exists() else [str(x) for x in p.rglob('*.py')]; assert not files, files" +rg -n --glob '!**/__pycache__/**' --glob '!build/**' --glob '!logs/**' 'module_ai|utils\.ai_util|utils\.crypto_util|AiUtil|AI_MODEL_|AI_CHAT_|USER_INTERACTIVE_HIGH_FREQ|LiteLLM|litellm' common config middlewares module_admin module_business sub_applications utils server.py +cd /Users/jiaolongyan/PycharmProjects/牛牛小屋/ShiErFeng/shierfeng-fastapi-frontend +rg -n '@/api/ai|/ai/(chat|model)|ai:model|ai:chat|ai_provider_type|markstream-vue|stream-monaco|vite-plugin-monaco-editor-esm' src vite package.json +~~~ + +Expected: Python 断言通过;两个 rg 均无输出并以 1 退出。检查范围故意排除 tests、CHANGELOG、设计文档、计划文档和增量补丁中的预期清理文本;module_ai 下未跟踪 __pycache__ 可以继续存在。 + +- [ ] **Step 4: 检查 APP、环境和用户暂存改动保护** + +~~~bash +cd /Users/jiaolongyan/PycharmProjects/牛牛小屋/ShiErFeng +git status --short --untracked-files=all +PALLET_PLAN_COMMIT=$(git log -1 --format=%H --fixed-strings --grep='^文档:新增托盘业务实施计划$') +test -n "$PALLET_PLAN_COMMIT" +git diff --name-only "$PALLET_PLAN_COMMIT"..HEAD -- shierfeng-fastapi-app shierfeng-fastapi-backend/.env.dev shierfeng-fastapi-backend/.env.prod shierfeng-fastapi-frontend/.env.development shierfeng-fastapi-frontend/.env.docker shierfeng-fastapi-frontend/.env.production shierfeng-fastapi-frontend/.env.staging .idea/git_toolbox_prj.xml +git diff --name-only "$PALLET_PLAN_COMMIT"..HEAD | rg '(^|/)(__pycache__/|[^/]+\.pyc$)' +git diff --binary -- shierfeng-fastapi-app | shasum -a 256 +git diff --binary -- shierfeng-fastapi-backend/.env.dev shierfeng-fastapi-backend/.env.prod shierfeng-fastapi-frontend/.env.development shierfeng-fastapi-frontend/.env.docker shierfeng-fastapi-frontend/.env.production shierfeng-fastapi-frontend/.env.staging | shasum -a 256 +git diff --cached --binary -- .idea/git_toolbox_prj.xml | shasum -a 256 +rg -n '"description": "智链云枢管理系统"' shierfeng-fastapi-frontend/package.json +~~~ + +Expected: 受保护路径历史检查和缓存文件历史检查均无输出;三个 SHA-256 依次仍为 6b7871be6b8cdede4c508adeb345e70084b01583403cfd0aab67e5b3beeb7e1c、5f1189e56551fc16485244e46c8e78cd32ce9ac9f0eaac91cfd0cf42b1ded57a、f21bea19dc68716136d77a3f1567659fd1af2f10f7b1fc6d246851dcfaa645c1;品牌描述命中。工作区原有 .idea/git_toolbox_prj.xml 继续保持暂存状态,package.json 保留用户品牌和本任务依赖/脚本改动但未自动提交。 + +- [ ] **Step 5: 记录未执行验收项** + +最终交付必须明确标为未验证: + +- MySQL 真实建表和约束。 +- 运行态 /getRouters 菜单结果。 +- 不同真实角色、仓库和现场映射下的端到端流程。 +- MySQL 行锁、并发竞争和事务回滚。 +- Redis 缓存清理。 + +- [ ] **Step 6: 提交** + +~~~bash +git add -- docs/pallet-workflow-deployment.md +git commit --only -m "文档:补充托盘业务部署说明" -- README.md CHANGELOG.md docs/pallet-workflow-deployment.md +~~~ + +## 完成标准 + +- AI 专属代码、接口、页面、权限、依赖和新环境 DDL 已移除,共享能力未受影响。 +- “施尔丰仓库”菜单种子及增量清理语句已完成,导航栏独立源码/文档入口仍保留。 +- 18 张 ORM 表、状态机、数据范围、五类事务和六类报表均有无数据库单元测试。 +- 17 个动态页面、44 个按钮权限、后端依赖和 API URL 四方一致。 +- Web 生产构建与 Node 契约测试通过,APP 无本次改动。 +- 两套初始化脚本语义一致,MySQL 补丁只生成不执行且不删除任何 AI 业务表。 +- 最终说明准确列出无法在方案 A 下完成的运行态验收,不把未运行测试表述为通过。