# 智链云枢托盘业务系统 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 下完成的运行态验收,不把未运行测试表述为通过。