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

106 KiB
Raw Blame History

智链云枢托盘业务系统 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。

文件结构

后端业务模块

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

后端测试

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

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

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 张:

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/报表如需展示名必须在本计划中显式声明映射,不能在实现时另起未记录的同义字段:

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 是同一页面内的顺序;六个报表没有按钮权限:

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 权限:

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 签名固定为:

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: 写删除契约测试

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:

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:

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: 安装已批准的二维码依赖
conda run -n ShiErFeng python -m pip install qrcode==8.2

Expected: Successfully installed qrcode-8.2;不得安装或升级其他项目依赖。

  • Step 6: 运行删除契约、OpenAPI 和静态检查
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: 核对并提交不重叠文件
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: 写状态迁移失败测试

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: 运行测试并确认导入失败
conda run -n ShiErFeng python -m unittest tests.module_business.test_state_machine -v

Expected: FAIL with ModuleNotFoundError: module_business.domain。

  • Step 3: 实现枚举和迁移表
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
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: 提交
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: 写元数据契约测试

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: 运行测试并确认失败
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。

关键列必须精确满足:

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。

关键唯一约束:

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
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: 提交
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: 写别名和数量边界测试

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: 运行测试并确认失败
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 固定模型:

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
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: 提交
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: 写管理员放行和普通用户拒绝测试

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: 运行测试并确认失败
conda run -n ShiErFeng python -m unittest tests.module_business.test_access_service -v

Expected: FAIL,AccessService 不存在。

  • Step 3: 实现 DAO 和 Service
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
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: 提交
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 固定以下断言:

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: 运行测试并确认失败
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 单独实现:

/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
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: 提交
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: 运行测试并确认失败
conda run -n ShiErFeng python -m unittest tests.module_business.test_pallet_service -v

Expected: FAIL,PalletService 不存在。

  • Step 3: 实现托盘与二维码服务
@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
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: 提交
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: 运行测试并确认失败
conda run -n ShiErFeng python -m unittest tests.module_business.test_inventory_dao -v

Expected: FAIL,InventoryDao 不存在。

  • Step 3: 实现 DAO 固定签名
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
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: 提交
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: 运行测试并确认失败
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
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: 提交
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: 运行测试并确认失败
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
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: 提交
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: 运行测试并确认失败
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
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: 提交
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: 运行测试并确认失败
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
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: 提交
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: 运行测试并确认失败
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
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: 提交
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: 运行测试并确认失败
conda run -n ShiErFeng python -m unittest tests.module_business.test_report_service -v

Expected: FAIL,ReportService 不存在。

  • Step 3: 实现 DAO、Service 和 Controller

固定只读接口:

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
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: 提交
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/ 路径均失败。

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: 运行测试并修正契约差异
conda run -n ShiErFeng python -m unittest tests.module_business.test_api_permission_contract -v

Expected: 首次运行可能因遗漏路由/权限失败;仅修正 Controller 与已确认契约,不扩大功能。

  • Step 3: 运行所有后端业务单元测试和 Ruff
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: 提交
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 失败测试

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: 运行测试并确认模块不存在
/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 增加:

"test:business": "node --test scripts/verify-business-contract.mjs"

保留 Task 1 已完成的 AI 依赖删除和用户品牌描述。

  • Step 4: 运行 Node 测试
/Users/jiaolongyan/.nvm/versions/node/v20.20.2/bin/node --test scripts/verify-business-contract.mjs

Expected: PASS。

  • Step 5: 提交
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: 运行测试并确认页面缺失
/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: 运行契约测试和生产构建
/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: 提交
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: 运行测试并确认页面缺失
/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: 运行契约测试和生产构建
/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: 提交
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: 运行测试并确认页面缺失
/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: 运行契约测试和生产构建
/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: 提交
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 静态契约测试

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 缺失
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 测试和差异检查
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: 提交
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: 运行契约测试并修复差异
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: 提交
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: 运行全部无数据库验证

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 源码、接口和依赖残留
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、环境和用户暂存改动保护
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: 提交

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 下完成的运行态验收,不把未运行测试表述为通过。