Files
ShiErFeng/docs/superpowers/plans/2026-07-24-pallet-workflow-system.md
T

1844 lines
106 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 智链云枢托盘业务系统 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, list[int] | None = None) -> 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 下完成的运行态验收,不把未运行测试表述为通过。