diff --git a/docs/superpowers/plans/2026-08-17-production-pre-transit-sample.md b/docs/superpowers/plans/2026-08-17-production-pre-transit-sample.md new file mode 100644 index 00000000..360a7322 --- /dev/null +++ b/docs/superpowers/plans/2026-08-17-production-pre-transit-sample.md @@ -0,0 +1,472 @@ +# 生产管理 — 预产及在途样本独立副本 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:** 在【生产管理】下新增与质检侧代码独立的「预产及在途样本」页面与菜单,共用现有查询 API,并自动授权给已有生产管理子菜单的角色。 + +**Architecture:** 完整复制 `quality-inspection/pre-transit-sample` 到 `production/pre-transit-sample`;路由挂在 `production-group/production`、库位管理之前;权限码 `production:preTransitSample:list`;后端两列表接口改为 `require_any_menu_code`(质检码 OR 生产码);SQL 幂等插菜单并按 PRM 子菜单授权。 + +**Tech Stack:** Vue 3 + Element Plus(web-admin)、FastAPI menu_guard、MySQL `sys_menu` / `sys_role_menu` + +**Spec:** `docs/superpowers/specs/2026-08-17-production-pre-transit-sample-design.md` + +--- + +## File map + +| 文件 | 职责 | +|------|------| +| `web-admin/src/views/production/pre-transit-sample/index.vue` | 生产侧页面壳(自质检复制) | +| `web-admin/src/views/production/pre-transit-sample/PreTransitProtocolCardList.vue` | 生产侧卡片列表(自质检复制) | +| `web-admin/src/router/modules/sqxp.ts` | 注册生产侧路由 | +| `backend/app/api/pre_transit_sample.py` | 共用 API 鉴权 OR | +| `docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql` | 菜单 + 角色授权迁移 | +| `docs/sql/zkxw.sql` | 基线插入两条 `sys_menu` | +| `待同步到生产的sql脚本` | 追加待上线 SQL 片段 | + +不改:`web-admin/src/api/pre-transit-sample.ts`、质检侧 `views/quality-inspection/pre-transit-sample/**`。 + +**ID / 权限常量(全任务统一)** + +| 常量 | 值 | +|------|-----| +| C 菜单 id | `95200000000000000000000000000001` | +| F 按钮 id | `95200000000000000000000000000002` | +| parent_id | `PRM00000000000000000000000000001` | +| perms / menuCode | `production:preTransitSample:list` | +| order_num(C) | `0` | +| 组件 path | `/production/pre-transit-sample/index` | +| 路由 Hash | `/#/production-group/production/pre-transit-sample` | + +--- + +### Task 1: 复制前端页面到生产目录 + +**Files:** +- Create: `web-admin/src/views/production/pre-transit-sample/index.vue` +- Create: `web-admin/src/views/production/pre-transit-sample/PreTransitProtocolCardList.vue` + +- [ ] **Step 1: 复制目录** + +```bash +mkdir -p web-admin/src/views/production/pre-transit-sample +cp web-admin/src/views/quality-inspection/pre-transit-sample/index.vue \ + web-admin/src/views/production/pre-transit-sample/index.vue +cp web-admin/src/views/quality-inspection/pre-transit-sample/PreTransitProtocolCardList.vue \ + web-admin/src/views/production/pre-transit-sample/PreTransitProtocolCardList.vue +``` + +Expected: 两文件存在且行数与源文件接近(卡片约 1422 行)。 + +- [ ] **Step 2: 改 `index.vue` 注释与组件名** + +将文件头注释改为: + +```vue + +``` + +将: + +```ts +defineOptions({ name: 'QualityInspectionPreTransitSample' }) +``` + +改为: + +```ts +defineOptions({ name: 'ProductionPreTransitSample' }) +``` + +(keep-alive 需唯一组件名;勿与质检侧同名。) + +其余逻辑、`@/api/pre-transit-sample` import、弹窗路径保持不变。`PreTransitProtocolCardList.vue` 仅改文件头注释为「生产管理 — …」即可,相对 import `./` 无需改。 + +- [ ] **Step 3: 确认无交叉引用质检目录** + +```bash +rg -n "quality-inspection/pre-transit" web-admin/src/views/production/pre-transit-sample || true +``` + +Expected: 无匹配。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/views/production/pre-transit-sample +git commit -m "$(cat <<'EOF' +feat: copy pre-transit sample views under production. + +EOF +)" +``` + +--- + +### Task 2: 注册生产侧路由 + +**Files:** +- Modify: `web-admin/src/router/modules/sqxp.ts`(`ProductionManage.children`,库位管理节点之前) + +- [ ] **Step 1: 在库位管理前插入路由** + +定位 `name: 'ProductionStorageFacility'`(约 L682)。在其**之前**插入: + +```ts + { + id: '95200000000000000000000000000001', + name: 'ProductionPreTransitSample', + path: 'pre-transit-sample', + component: '/production/pre-transit-sample/index', + meta: { + title: '预产及在途样本', + keepAlive: true, + menuCode: 'production:preTransitSample:list' + } + }, +``` + +保持其后的 `ProductionStorageFacility` 块不变。 + +- [ ] **Step 2: 核对顺序** + +```bash +rg -n "ProductionPreTransitSample|ProductionStorageFacility" web-admin/src/router/modules/sqxp.ts +``` + +Expected: `ProductionPreTransitSample` 行号小于 `ProductionStorageFacility`(在同一 `children` 内)。 + +- [ ] **Step 3: Commit** + +```bash +git add web-admin/src/router/modules/sqxp.ts +git commit -m "$(cat <<'EOF' +feat: register production pre-transit sample route. + +EOF +)" +``` + +--- + +### Task 3: 后端共用 API 接受生产权限码 + +**Files:** +- Modify: `backend/app/api/pre_transit_sample.py` + +- [ ] **Step 1: 改 import 与两处 Depends** + +将: + +```python +from app.core.menu_guard import require_menu_code +``` + +改为: + +```python +from app.core.menu_guard import require_any_menu_code +``` + +将 `list_in_transit_samples` 与 `list_pre_delivery_samples` 中的: + +```python +user: dict = Depends(require_menu_code("biz:preTransitSample:list")), +``` + +均改为: + +```python +user: dict = Depends( + require_any_menu_code( + "biz:preTransitSample:list", + "production:preTransitSample:list", + ) + ), +``` + +- [ ] **Step 2: 静态核对** + +```bash +rg -n "require_menu_code|require_any_menu_code|production:preTransitSample" backend/app/api/pre_transit_sample.py +``` + +Expected: 无 `require_menu_code`;两处 `require_any_menu_code`;含 `production:preTransitSample:list`。 + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/api/pre_transit_sample.py +git commit -m "$(cat <<'EOF' +fix: allow production perm on shared pre-transit APIs. + +EOF +)" +``` + +--- + +### Task 4: 菜单与角色授权 SQL + +**Files:** +- Create: `docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql` +- Modify: `docs/sql/zkxw.sql`(插入两条 `sys_menu`) +- Modify: `待同步到生产的sql脚本`(追加可执行片段) + +- [ ] **Step 1: 写迁移脚本** + +创建 `docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql`,全文如下: + +```sql +-- 生产管理:预产及在途样本(独立页面 + 独立权限码) +-- 幂等:可重复执行 +-- 授权:已有【生产管理】PRM 下任一有效子菜单的角色 → 授予本 C/F,并补授 PRM 祖先 + +SET @tenant_no = CONVERT('zkxw' USING utf8mb4) COLLATE utf8mb4_unicode_ci; +SET @prm_id = CONVERT('PRM00000000000000000000000000001' USING utf8mb4) COLLATE utf8mb4_unicode_ci; +SET @menu_c = CONVERT('95200000000000000000000000000001' USING utf8mb4) COLLATE utf8mb4_unicode_ci; +SET @menu_f = CONVERT('95200000000000000000000000000002' USING utf8mb4) COLLATE utf8mb4_unicode_ci; + +-- 1) C 菜单 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + @menu_c, + '预产及在途样本', + @prm_id, + 'C', + 'pre-transit-sample', + '/production/pre-transit-sample/index', + 'production:preTransitSample:list', + 1, '1', '1', @tenant_no, 0, + '生产看板:在途样本与预产协议(协议单维度只读;与质检页代码独立)', + NULL, '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu m + WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_c +); + +UPDATE sys_menu +SET + menu_name = '预产及在途样本', + parent_id = @prm_id, + menu_type = 'C', + path = 'pre-transit-sample', + component = '/production/pre-transit-sample/index', + perms = 'production:preTransitSample:list', + visible = '1', + status = '1', + order_num = 0, + remark = '生产看板:在途样本与预产协议(协议单维度只读;与质检页代码独立)', + del_flag = 0, + update_time = NOW() +WHERE id COLLATE utf8mb4_unicode_ci = @menu_c; + +-- 2) F 按钮 +INSERT INTO sys_menu ( + id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + @menu_f, + '预产及在途查询', + @menu_c, + 'F', + '', + '', + 'production:preTransitSample:list', + 1, '0', '1', @tenant_no, 1, + '生产侧预产及在途样本列表查询', + NULL, '', NOW(), NULL, NOW(), 0 +FROM DUAL +WHERE NOT EXISTS ( + SELECT 1 FROM sys_menu m + WHERE m.id COLLATE utf8mb4_unicode_ci = @menu_f +); + +UPDATE sys_menu +SET + menu_name = '预产及在途查询', + parent_id = @menu_c, + menu_type = 'F', + path = '', + component = '', + perms = 'production:preTransitSample:list', + visible = '0', + status = '1', + order_num = 1, + remark = '生产侧预产及在途样本列表查询', + del_flag = 0, + update_time = NOW() +WHERE id COLLATE utf8mb4_unicode_ci = @menu_f; + +-- 3) 目标角色:已有 PRM 下任一有效子菜单授权 +-- 3a) 恢复软删的 PRM / C / F 授权 +UPDATE sys_role_menu rm +INNER JOIN sys_role r + ON r.id COLLATE utf8mb4_unicode_ci = rm.role_id COLLATE utf8mb4_unicode_ci + AND r.del_flag = 0 + AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no +SET + rm.del_flag = 0, + rm.update_time = NOW(), + rm.remark = 'grant production pre-transit to PRM roles' +WHERE rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no + AND rm.del_flag = 1 + AND rm.menu_id COLLATE utf8mb4_unicode_ci IN (@prm_id, @menu_c, @menu_f) + AND EXISTS ( + SELECT 1 + FROM sys_role_menu rm2 + INNER JOIN sys_menu m2 + ON m2.id COLLATE utf8mb4_unicode_ci = rm2.menu_id COLLATE utf8mb4_unicode_ci + AND m2.del_flag = 0 + AND m2.parent_id COLLATE utf8mb4_unicode_ci = @prm_id + WHERE rm2.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci + AND rm2.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no + AND rm2.del_flag = 0 + ); + +-- 3b) 插入缺失授权(PRM + C + F) +INSERT INTO sys_role_menu ( + id, role_id, menu_id, tenant_no, order_num, remark, + create_by, create_dept_id, create_time, update_by, update_time, del_flag +) +SELECT + UPPER(MD5(CONCAT(r.id, '|', x.menu_id))), + r.id, + x.menu_id, + @tenant_no, + 0, + 'grant production pre-transit to PRM roles', + 'admin', + '', + NOW(), + NULL, + NOW(), + 0 +FROM sys_role r +INNER JOIN ( + SELECT @prm_id AS menu_id + UNION ALL SELECT @menu_c + UNION ALL SELECT @menu_f +) x +WHERE r.del_flag = 0 + AND r.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no + AND EXISTS ( + SELECT 1 + FROM sys_role_menu rm2 + INNER JOIN sys_menu m2 + ON m2.id COLLATE utf8mb4_unicode_ci = rm2.menu_id COLLATE utf8mb4_unicode_ci + AND m2.del_flag = 0 + AND m2.parent_id COLLATE utf8mb4_unicode_ci = @prm_id + WHERE rm2.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci + AND rm2.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no + AND rm2.del_flag = 0 + ) + AND EXISTS ( + SELECT 1 FROM sys_menu m + WHERE m.id COLLATE utf8mb4_unicode_ci = x.menu_id + AND m.del_flag = 0 + ) + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm + WHERE rm.role_id COLLATE utf8mb4_unicode_ci = r.id COLLATE utf8mb4_unicode_ci + AND rm.menu_id COLLATE utf8mb4_unicode_ci = x.menu_id + AND rm.tenant_no COLLATE utf8mb4_unicode_ci = @tenant_no + AND rm.del_flag = 0 + ); +``` + +- [ ] **Step 2: 基线 `zkxw.sql` 插入两条菜单** + +在 `sys_menu` 插入段中、`95000000000000000000000000000003`(洁净服管理)之前或附近按 id 排序插入(建议紧挨 `951…` 段之前插入 `952…`): + +```sql +INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('95200000000000000000000000000001', '预产及在途样本', 'PRM00000000000000000000000000001', 'C', 'pre-transit-sample', '/production/pre-transit-sample/index', 'production:preTransitSample:list', 1, '1', '1', 'zkxw', 0, '生产看板:在途样本与预产协议(协议单维度只读;与质检页代码独立)', NULL, '', '2026-08-17 12:00:00', NULL, '2026-08-17 12:00:00', 0); +INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('95200000000000000000000000000002', '预产及在途查询', '95200000000000000000000000000001', 'F', '', '', 'production:preTransitSample:list', 1, '0', '1', 'zkxw', 1, '生产侧预产及在途样本列表查询', NULL, '', '2026-08-17 12:00:00', NULL, '2026-08-17 12:00:00', 0); +``` + +核对无重复 id: + +```bash +rg "9520000000000000000000000000000" docs/sql/zkxw.sql +``` + +Expected: 恰好两行(C + F)。 + +- [ ] **Step 3: 追加到 `待同步到生产的sql脚本`** + +在文件末尾追加注释 + 与 Step 1 相同的可执行 SQL(或 `SOURCE`/`-- 见 docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql` 并粘贴全文,与仓库现有「待同步」习惯一致:粘贴全文便于运维一次执行)。 + +- [ ] **Step 4: Commit** + +```bash +git add docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql \ + docs/sql/zkxw.sql \ + 待同步到生产的sql脚本 +git commit -m "$(cat <<'EOF' +docs(sql): add production pre-transit sample menu and grants. + +EOF +)" +``` + +--- + +### Task 5: 图谱更新与手工验收 + +**Files:** +- 运行:`./scripts/update-graph.sh backend` 与 `./scripts/update-graph.sh frontend`(或 `all`) + +- [ ] **Step 1: 更新知识图谱** + +```bash +./scripts/update-graph.sh all +``` + +若图谱产物有变更则提交: + +```bash +git add backend/graphify-out web-admin/graphify-out +git status --short +# 若有变更: +git commit -m "$(cat <<'EOF' +chore: update graphify after production pre-transit sample. + +EOF +)" +``` + +- [ ] **Step 2: 本地手工验收清单** + +1. 在本地/测试库执行 `docs/sql/alter_sys_menu_add_production_pre_transit_sample.sql`。 +2. 重新登录 web-admin(或刷新菜单权限缓存)。 +3. 打开 `/#/production-group/production/pre-transit-sample`:双分区列表可加载;与质检侧同筛选结果一致。 +4. 侧栏【生产管理】中「预产及在途样本」在【库位管理】上方。 +5. 质检路由 `/#/quality-group/quality-manage/pre-transit-sample` 仍正常。 +6. (可选)角色管理中确认已有 PRM 子菜单的角色已勾选新菜单;仅有生产基础信息、无 PRM 子菜单的角色未自动获得。 + +- [ ] **Step 3: 完成标记** + +将本 plan 中已完成 Task 的 `- [ ]` 改为 `- [x]`(若执行 agent 负责跟踪)。 + +--- + +## Spec coverage (self-review) + +| Spec 要求 | Task | +|-----------|------| +| 完整复制 Vue 到 production | Task 1 | +| 独立组件名 / 不引用质检目录 | Task 1 | +| 路由在库位管理前、独立 menuCode | Task 2 | +| 共用 API + OR 权限码 | Task 3 | +| 菜单 ID 952…、SQL 幂等、PRM 角色授权、zkxw/待同步 | Task 4 | +| 图谱 + 验收 | Task 5 | +| 不改质检页、不新建业务查询 API | 各 Task 未触及 | + +无 TBD/占位符;ID 与权限码全文一致。