diff --git a/backend/app/api/lab_sample_process.py b/backend/app/api/lab_sample_process.py index 434ead37..050a091c 100644 --- a/backend/app/api/lab_sample_process.py +++ b/backend/app/api/lab_sample_process.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.orm import Session from app.core.database import get_db -from app.core.menu_guard import require_menu_code +from app.core.menu_guard import require_any_menu_code from app.core.response import error_response, success_response from app.crud import lab_sample_process as svc from app.models.models import SysUser @@ -14,8 +14,17 @@ from app.services import lab_sample_process as ledger router = APIRouter(prefix="/web/admin", tags=["质检管理"]) -LIST_MENU = "biz:labSampleProcess:list" -SUPPLEMENT_MENU = "biz:labSampleProcess:supplement" +# 市场/生产/质检三处菜单权限码不同,接口按并集鉴权 +LIST_MENU_CODES = ( + "biz:labSampleProcess:list", + "production:labSampleProcess:list", + "cellStorage:labSampleProcess:list", +) +SUPPLEMENT_MENU_CODES = ( + "biz:labSampleProcess:supplement", + "production:labSampleProcess:supplement", + "cellStorage:labSampleProcess:supplement", +) def _tenant_no(db: Session, user: dict) -> str: @@ -59,7 +68,7 @@ def list_lab_sample_process( createTimeStart: Optional[str] = Query(None), createTimeEnd: Optional[str] = Query(None), db: Session = Depends(get_db), - user=Depends(require_menu_code(LIST_MENU)), + user=Depends(require_any_menu_code(*LIST_MENU_CODES)), ): try: tenant_no = _tenant_no(db, user) @@ -98,7 +107,7 @@ def list_lab_sample_process( def get_supplement( labStorageCode: str = Query(...), db: Session = Depends(get_db), - user=Depends(require_menu_code(LIST_MENU)), + user=Depends(require_any_menu_code(*LIST_MENU_CODES)), ): try: tenant_no = _tenant_no(db, user) @@ -114,7 +123,7 @@ def get_supplement( def save_supplement( body: SupplementBody, db: Session = Depends(get_db), - user=Depends(require_menu_code(SUPPLEMENT_MENU)), + user=Depends(require_any_menu_code(*SUPPLEMENT_MENU_CODES)), ): try: tenant_no = _tenant_no(db, user) diff --git a/docs/sql/migrations/2026-11-12-lab-sample-process-multi-menu.sql b/docs/sql/migrations/2026-11-12-lab-sample-process-multi-menu.sql new file mode 100644 index 00000000..d7b2f401 --- /dev/null +++ b/docs/sql/migrations/2026-11-12-lab-sample-process-multi-menu.sql @@ -0,0 +1,175 @@ +-- Issue #102:实验室样本处理数据挂到市场/生产/质检,权限码分端 +-- 1) 质检管理:调到「入库放行审核」下方 +-- 2) 存储服务:挂到「缴费财务确认」下方(cellStorage:labSampleProcess:*) +-- 3) 生产管理:挂到「废弃物管理」下方(production:labSampleProcess:*;若废弃物菜单缺失则先补齐) +-- 4) 拥有 menu:market / menu:production / menu:quality 的角色,授予对应大菜单下入口 +-- MySQL 5.7,可重复执行 + +-- ── 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 +) +SELECT + INSERT(INSERT(MD5(CONCAT('prod-waste-output:', p.tenant_no, ':', p.id)), 13, 1, '3'), 17, 1, '8'), + '废弃物管理', p.id, 'C', 'waste-output', + '/production/waste-output/index', 'production:waste-output:list', + 1, '1', '1', p.tenant_no, + COALESCE((SELECT MAX(c.order_num) FROM sys_menu c WHERE c.parent_id = p.id AND c.del_flag = 0), 0) + 1, + 'issue#102 补齐生产废弃物管理以便台账排序', + 'migration', '', NOW(), 'migration', NOW(), 0 +FROM sys_menu p +WHERE p.del_flag = 0 AND p.status = '1' AND p.menu_name = '生产管理' + AND NOT EXISTS ( + SELECT 1 FROM sys_menu e + WHERE e.tenant_no = p.tenant_no AND e.parent_id = p.id + AND e.perms = 'production:waste-output:list' AND e.del_flag = 0 + ); + +-- ── 1) 质检:调到入库放行审核下方(可重复;不连带改动其他菜单 order)──────── +UPDATE sys_menu lab +JOIN sys_menu release_m + ON release_m.parent_id = lab.parent_id + AND release_m.del_flag = 0 + AND release_m.menu_name = '入库放行审核' + AND release_m.perms = 'quality:storageReleaseReview:list' +SET lab.order_num = release_m.order_num + 1, + lab.update_by = 'migration', + lab.update_time = NOW() +WHERE lab.del_flag = 0 + AND lab.perms = 'biz:labSampleProcess:list' + AND lab.path = 'lab-sample-process' + AND (lab.order_num IS NULL OR lab.order_num <> release_m.order_num + 1); + +-- ── 2) 存储服务:缴费财务确认下方 ─────────────────────────────────────────── +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 + INSERT(INSERT(MD5(CONCAT('cell-lab-sample-process:', anchor.tenant_no, ':', anchor.id)), 13, 1, '3'), 17, 1, '8'), + '实验室样本处理数据', anchor.parent_id, 'C', 'lab-sample-process', + 'quality-inspection/lab-sample-process/index', 'cellStorage:labSampleProcess:list', + 1, '1', '1', anchor.tenant_no, anchor.order_num + 1, + '市场-存储服务-实验室样本处理数据', + 'migration', '', NOW(), 'migration', NOW(), 0 +FROM sys_menu anchor +WHERE anchor.del_flag = 0 AND anchor.status = '1' + AND anchor.perms = 'biz:cellStoragePaymentConfirm:list' + AND anchor.menu_name = '缴费财务确认' + AND NOT EXISTS ( + SELECT 1 FROM sys_menu e + WHERE e.tenant_no = anchor.tenant_no + AND e.parent_id = anchor.parent_id + AND e.perms = 'cellStorage:labSampleProcess:list' + AND e.del_flag = 0 + ); + +-- ── 3) 生产管理:废弃物管理下方 ───────────────────────────────────────────── +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 + INSERT(INSERT(MD5(CONCAT('prod-lab-sample-process:', anchor.tenant_no, ':', anchor.id)), 13, 1, '3'), 17, 1, '8'), + '实验室样本处理数据', anchor.parent_id, 'C', 'lab-sample-process', + 'quality-inspection/lab-sample-process/index', 'production:labSampleProcess:list', + 1, '1', '1', anchor.tenant_no, anchor.order_num + 1, + '生产-生产管理-实验室样本处理数据', + 'migration', '', NOW(), 'migration', NOW(), 0 +FROM sys_menu anchor +WHERE anchor.del_flag = 0 AND anchor.status = '1' + AND anchor.perms = 'production:waste-output:list' + AND anchor.menu_name = '废弃物管理' + AND NOT EXISTS ( + SELECT 1 FROM sys_menu e + WHERE e.tenant_no = anchor.tenant_no + AND e.parent_id = anchor.parent_id + AND e.perms = 'production:labSampleProcess:list' + AND e.del_flag = 0 + ); + +-- ── 4) 补录 / 导出按钮(三处列表菜单各挂一套)────────────────────────────── +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 + INSERT(INSERT(MD5(CONCAT('lab-sp-btn:', m.tenant_no, ':', m.id, ':', btn.perms)), 13, 1, '3'), 17, 1, '8'), + btn.menu_name, m.id, 'F', '', NULL, btn.perms, + 1, '1', '1', m.tenant_no, btn.order_num, btn.remark, + 'migration', '', NOW(), 'migration', NOW(), 0 +FROM sys_menu m +JOIN ( + SELECT '补录' AS menu_name, 'cellStorage:labSampleProcess:supplement' AS perms, 10 AS order_num, + '存储服务-实验室样本处理数据-补录' AS remark, 'cellStorage:labSampleProcess:list' AS parent_perms + UNION ALL + SELECT '导出', 'cellStorage:labSampleProcess:export', 20, + '存储服务-实验室样本处理数据-导出', 'cellStorage:labSampleProcess:list' + UNION ALL + SELECT '补录', 'production:labSampleProcess:supplement', 10, + '生产-实验室样本处理数据-补录', 'production:labSampleProcess:list' + UNION ALL + SELECT '导出', 'production:labSampleProcess:export', 20, + '生产-实验室样本处理数据-导出', 'production:labSampleProcess:list' +) btn ON btn.parent_perms = m.perms +WHERE m.del_flag = 0 AND m.status = '1' + AND NOT EXISTS ( + SELECT 1 FROM sys_menu e + WHERE e.tenant_no = m.tenant_no AND e.parent_id = m.id + AND e.perms = btn.perms AND e.del_flag = 0 + ); + +-- ── 5) 按顶级大菜单授权对应入口(含列表/补录/导出)───────────────────────── +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 + INSERT(INSERT(MD5(CONCAT('lab-sp-rm:', r.tenant_no, ':', r.id, ':', m.id)), 13, 1, '3'), 17, 1, '8'), + r.id, m.id, r.tenant_no, NULL, + CONCAT('issue#102 grant from ', map.top_perms), + 'migration', '', NOW(), 'migration', NOW(), 0 +FROM ( + SELECT 'menu:market' AS top_perms, 'cellStorage:labSampleProcess:list' AS list_perms + UNION ALL SELECT 'menu:market', 'cellStorage:labSampleProcess:supplement' + UNION ALL SELECT 'menu:market', 'cellStorage:labSampleProcess:export' + UNION ALL SELECT 'menu:production', 'production:labSampleProcess:list' + UNION ALL SELECT 'menu:production', 'production:labSampleProcess:supplement' + UNION ALL SELECT 'menu:production', 'production:labSampleProcess:export' + UNION ALL SELECT 'menu:quality', 'biz:labSampleProcess:list' + UNION ALL SELECT 'menu:quality', 'biz:labSampleProcess:supplement' + UNION ALL SELECT 'menu:quality', 'biz:labSampleProcess:export' +) map +JOIN sys_role r ON r.del_flag = 0 +JOIN sys_role_menu src + ON src.role_id = r.id AND src.tenant_no = r.tenant_no AND src.del_flag = 0 +JOIN sys_menu top + ON top.id = src.menu_id AND top.tenant_no = src.tenant_no AND top.del_flag = 0 + AND top.perms = map.top_perms +JOIN sys_menu m + ON m.tenant_no = r.tenant_no AND m.del_flag = 0 AND m.status = '1' + AND m.perms = map.list_perms +WHERE NOT EXISTS ( + SELECT 1 FROM sys_role_menu existing + WHERE existing.role_id = r.id + AND existing.menu_id = m.id + AND existing.tenant_no = r.tenant_no + AND existing.del_flag = 0 +); + +-- 核对 +SELECT p.menu_name AS parent_name, c.menu_name, c.path, c.perms, c.order_num +FROM sys_menu c +JOIN sys_menu p ON p.id = c.parent_id +WHERE c.del_flag = 0 + AND ( + c.perms LIKE '%labSampleProcess%' + OR (c.menu_name IN ('缴费财务确认', '废弃物管理', '入库放行审核') + AND p.menu_name IN ('存储服务', '生产管理', '质检管理')) + ) +ORDER BY p.menu_name, c.order_num, c.menu_name; diff --git a/web-admin/src/router/modules/sqxp.ts b/web-admin/src/router/modules/sqxp.ts index 32331f8e..c2cabbaa 100644 --- a/web-admin/src/router/modules/sqxp.ts +++ b/web-admin/src/router/modules/sqxp.ts @@ -190,6 +190,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [ menuCode: 'biz:cellStoragePaymentConfirm:list' } }, + { + id: 'CEL0000000000000000000000000LSP1', + name: 'CellStorageLabSampleProcess', + path: 'lab-sample-process', + component: '/quality-inspection/lab-sample-process/index', + meta: { + title: '实验室样本处理数据', + keepAlive: true, + menuCode: 'cellStorage:labSampleProcess:list' + } + }, // 市场侧入库放行审核(总览)暂下线:改由生产/质检入口审核 // { // id: '70000000000000000000000000000011', @@ -922,6 +933,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [ keepAlive: true, menuCode: 'production:waste-output:list' } + }, + { + id: 'PRM0000000000000000000000000LSP1', + name: 'ProductionLabSampleProcess', + path: 'lab-sample-process', + component: '/quality-inspection/lab-sample-process/index', + meta: { + title: '实验室样本处理数据', + keepAlive: true, + menuCode: 'production:labSampleProcess:list' + } } ] }, @@ -1123,17 +1145,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [ menuCode: 'biz:sampleReception:list' } }, - { - id: '70000000000000000000000000000015', - name: 'QualityInspectionLabSampleProcess', - path: 'lab-sample-process', - component: '/quality-inspection/lab-sample-process/index', - meta: { - title: '实验室样本处理数据', - keepAlive: true, - menuCode: 'biz:labSampleProcess:list' - } - }, { id: '70000000000000000000000000000007', name: 'QualityInspectionQcWorkOrder', @@ -1201,6 +1212,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [ menuCode: 'quality:storageReleaseReview:list' }, props: { reviewRole: 'qc' } + }, + { + id: '70000000000000000000000000000015', + name: 'QualityInspectionLabSampleProcess', + path: 'lab-sample-process', + component: '/quality-inspection/lab-sample-process/index', + meta: { + title: '实验室样本处理数据', + keepAlive: true, + menuCode: 'biz:labSampleProcess:list' + } } ] },