33 KiB
权限系统全面修复实施计划
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: 补全 SQL 权限模型(sys_menu, sys_role, sys_role_menu, sys_role_data_scope_detail, sys_user_role)定义的全部权限执行逻辑,使菜单权限和数据权限在后端 API 和前端 UI 层真正生效。
Architecture: 后端添加 data_scope.py 服务模块实现数据权限过滤,在现有路由上叠加 require_menu_code() 菜单权限守卫;前端用户管理添加角色选择器,业务页面按钮添加 hasAuth 权限控制。
Tech Stack: Python/FastAPI/SQLAlchemy (backend), Vue 3/TypeScript/Element Plus (frontend)
File Structure
后端新增/修改文件
| 操作 | 文件 | 职责 |
|---|---|---|
| 新增 | backend/app/core/data_scope.py |
数据权限过滤服务:读取用户角色 data_scope,构建 SQLAlchemy 过滤条件 |
| 修改 | backend/app/api/basic_info.py |
客户/样本类型/采集地区/协议号/样本编码 路由添加 require_menu_code + apply_data_scope |
| 修改 | backend/app/api/operations.py |
洁净室/仓储/展厅预约 路由添加 require_menu_code + apply_data_scope |
| 修改 | backend/app/api/collection_protocol.py |
采集协议路由添加 require_menu_code + apply_data_scope |
| 修改 | backend/app/api/sample_reception.py |
样本接收路由添加 require_menu_code + apply_data_scope |
| 修改 | backend/app/api/admin.py |
用户/角色管理路由添加 require_menu_code(仅业务部分) |
| 修改 | backend/app/api/public.py |
修复 admin 无权限码返回 all:true 的安全漏洞 |
| 新增 | docs/sql/sys_menu_button_perms.sql |
为每个 C 类型菜单补充 F 类型按钮权限记录 |
前端修改文件
| 操作 | 文件 | 职责 |
|---|---|---|
| 修改 | web-admin/src/views/system/user/index.vue |
用户弹窗添加角色多选下拉框 |
| 修改 | web-admin/src/views/basic-info/customer/index.vue |
按钮添加 hasAuth 权限控制 |
| 修改 | web-admin/src/views/basic-info/cell-sample-type/index.vue |
按钮添加 hasAuth 权限控制 |
| 修改 | web-admin/src/views/basic-info/collection-region/index.vue |
按钮添加 hasAuth 权限控制 |
| 修改 | web-admin/src/views/basic-info/collection-protocol-no/index.vue |
按钮添加 hasAuth 权限控制 |
| 修改 | web-admin/src/views/basic-info/sample-code/index.vue |
按钮添加 hasAuth 权限控制 |
Task 1: 创建 F 类型按钮权限 SQL 脚本
Files:
- Create:
docs/sql/sys_menu_button_perms.sql
说明: 当前 sys_menu 中 C 类型菜单的 perms 字段(如 biz:customer:list)用于列表页面权限。需为每个 C 类型菜单新增 F 类型子记录,用于增删改操作权限控制。
- Step 1: 编写 SQL 脚本
-- sys_menu 按钮(F)权限初始化
-- 为每个 C 类型菜单新增 list/add/edit/delete 四个 F 类型按钮权限
-- 执行前请确认 sys_menu 中已有的 C 类型菜单记录
-- ============================================================
-- 客户管理 (parent: 121, perms prefix: biz:customer)
-- ============================================================
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
(UUID(), '客户查询', '121', 'F', '', NULL, 'biz:customer:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '客户新增', '121', 'F', '', NULL, 'biz:customer:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '客户修改', '121', 'F', '', NULL, 'biz:customer:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '客户删除', '121', 'F', '', NULL, 'biz:customer:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 细胞样本类型管理 (parent: 122, perms prefix: biz:cellSampleType)
-- ============================================================
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
(UUID(), '样本类型查询', '122', 'F', '', NULL, 'biz:cellSampleType:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本类型新增', '122', 'F', '', NULL, 'biz:cellSampleType:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本类型修改', '122', 'F', '', NULL, 'biz:cellSampleType:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本类型删除', '122', 'F', '', NULL, 'biz:cellSampleType:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 采集地区及医院管理 (parent: 123, perms prefix: biz:collectionRegion)
-- ============================================================
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
(UUID(), '采集地区查询', '123', 'F', '', NULL, 'biz:collectionRegion:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '采集地区新增', '123', 'F', '', NULL, 'biz:collectionRegion:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '采集地区修改', '123', 'F', '', NULL, 'biz:collectionRegion:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '采集地区删除', '123', 'F', '', NULL, 'biz:collectionRegion:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 采集协议号管理 (parent: 124, perms prefix: biz:collectionProtocolNo)
-- ============================================================
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
(UUID(), '协议号查询', '124', 'F', '', NULL, 'biz:collectionProtocolNo:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '协议号新增', '124', 'F', '', NULL, 'biz:collectionProtocolNo:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '协议号修改', '124', 'F', '', NULL, 'biz:collectionProtocolNo:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '协议号删除', '124', 'F', '', NULL, 'biz:collectionProtocolNo:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 样本编码管理 (parent: 125, perms prefix: biz:sampleCode)
-- ============================================================
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
(UUID(), '样本编码查询', '125', 'F', '', NULL, 'biz:sampleCode:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本编码新增', '125', 'F', '', NULL, 'biz:sampleCode:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本编码修改', '125', 'F', '', NULL, 'biz:sampleCode:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本编码删除', '125', 'F', '', NULL, 'biz:sampleCode:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 租户管理 (parent: 101, perms prefix: system:tenant) — super_admin only
-- ============================================================
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
(UUID(), '租户查询', '101', 'F', '', NULL, 'system:tenant:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '租户新增', '101', 'F', '', NULL, 'system:tenant:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '租户修改', '101', 'F', '', NULL, 'system:tenant:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '租户删除', '101', 'F', '', NULL, 'system:tenant:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 字典管理 (parent: 102, perms prefix: system:dict)
-- ============================================================
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
(UUID(), '字典查询', '102', 'F', '', NULL, 'system:dict:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '字典新增', '102', 'F', '', NULL, 'system:dict:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '字典修改', '102', 'F', '', NULL, 'system:dict:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '字典删除', '102', 'F', '', NULL, 'system:dict:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 组织管理 (parent: 103, perms prefix: system:dept)
-- ============================================================
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
(UUID(), '组织查询', '103', 'F', '', NULL, 'system:dept:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '组织新增', '103', 'F', '', NULL, 'system:dept:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '组织修改', '103', 'F', '', NULL, 'system:dept:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '组织删除', '103', 'F', '', NULL, 'system:dept:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 用户管理 (parent: 104, perms prefix: system:user)
-- ============================================================
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
(UUID(), '用户查询', '104', 'F', '', NULL, 'system:user:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '用户新增', '104', 'F', '', NULL, 'system:user:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '用户修改', '104', 'F', '', NULL, 'system:user:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '用户删除', '104', 'F', '', NULL, 'system:user:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 角色管理 (parent: 105, perms prefix: system:role)
-- ============================================================
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
(UUID(), '角色查询', '105', 'F', '', NULL, 'system:role:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '角色新增', '105', 'F', '', NULL, 'system:role:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '角色修改', '105', 'F', '', NULL, 'system:role:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '角色删除', '105', 'F', '', NULL, 'system:role:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 菜单管理 (parent: 106, perms prefix: system:menu)
-- ============================================================
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
(UUID(), '菜单查询', '106', 'F', '', NULL, 'system:menu:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '菜单新增', '106', 'F', '', NULL, 'system:menu:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '菜单修改', '106', 'F', '', NULL, 'system:menu:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '菜单删除', '106', 'F', '', NULL, 'system:menu:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 站点管理 (parent: 107, perms prefix: system:site)
-- ============================================================
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
(UUID(), '站点查询', '107', 'F', '', NULL, 'system:site:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '站点新增', '107', 'F', '', NULL, 'system:site:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '站点修改', '107', 'F', '', NULL, 'system:site:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '站点删除', '107', 'F', '', NULL, 'system:site:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 职位管理 (parent: 108, perms prefix: system:post)
-- ============================================================
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
(UUID(), '职位查询', '108', 'F', '', NULL, 'system:post:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '职位新增', '108', 'F', '', NULL, 'system:post:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '职位修改', '108', 'F', '', NULL, 'system:post:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '职位删除', '108', 'F', '', NULL, 'system:post:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 物品单位 (parent: 109, perms prefix: system:unit)
-- ============================================================
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
(UUID(), '单位查询', '109', 'F', '', NULL, 'system:unit:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '单位新增', '109', 'F', '', NULL, 'system:unit:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '单位修改', '109', 'F', '', NULL, 'system:unit:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '单位删除', '109', 'F', '', NULL, 'system:unit:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 快递公司 (parent: 110, perms prefix: system:express)
-- ============================================================
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
(UUID(), '快递公司查询', '110', 'F', '', NULL, 'system:express:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '快递公司新增', '110', 'F', '', NULL, 'system:express:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '快递公司修改', '110', 'F', '', NULL, 'system:express:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '快递公司删除', '110', 'F', '', NULL, 'system:express:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- ============================================================
-- 质量检测-样本接收 (parent: 70000000000000000000000000000002, perms prefix: biz:sampleReception)
-- ============================================================
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
(UUID(), '样本接收查询', '70000000000000000000000000000002', 'F', '', NULL, 'biz:sampleReception:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本接收新增', '70000000000000000000000000000002', 'F', '', NULL, 'biz:sampleReception:add', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本接收修改', '70000000000000000000000000000002', 'F', '', NULL, 'biz:sampleReception:edit', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0),
(UUID(), '样本接收删除', '70000000000000000000000000000002', 'F', '', NULL, 'biz:sampleReception:delete', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
- Step 2: Commit
git add docs/sql/sys_menu_button_perms.sql
git commit -m "feat: 添加 sys_menu 按钮权限(F 类型)初始化 SQL 脚本"
Task 2: 创建后端数据权限服务 data_scope.py
Files:
-
Create:
backend/app/core/data_scope.py -
Step 1: 编写数据权限服务模块
"""数据权限过滤服务
根据用户角色的 data_scope 设置,在 SQLAlchemy query 上叠加过滤条件。
支持 5 种数据范围:全部、本部门、本部门及以下、仅本人、自定义。
多角色场景取并集。
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Set, Tuple
from sqlalchemy import and_, or_
from sqlalchemy.orm import Session
from app.models.models import (
SysRole,
SysRoleDataScopeDetail,
SysUserRole,
SysDept,
)
def get_user_dept_id(db: Session, user_id: str) -> Optional[str]:
"""获取用户的部门ID"""
from app.models.models import SysUser
user = db.query(SysUser).filter(SysUser.id == user_id).first()
return user.dept_id if user else None
def _get_child_dept_ids(db: Session, dept_id: str, tenant_no: str) -> List[str]:
"""递归获取部门及所有子部门ID列表"""
result = [dept_id]
children = (
db.query(SysDept.id)
.filter(
SysDept.parent_id == dept_id,
SysDept.del_flag == 0,
SysDept.status == "1",
SysDept.tenant_no == tenant_no,
)
.all()
)
for child in children:
result.extend(_get_child_dept_ids(db, child.id, tenant_no))
return result
def _resolve_user_role_scopes(
db: Session, user_id: str, tenant_no: str
) -> List[Dict[str, Any]]:
"""获取用户所有角色的 data_scope 和自定义范围明细。
Returns:
[{"data_scope": "1", "scope_details": [...]}]
"""
roles = (
db.query(SysRole)
.join(SysUserRole, SysUserRole.role_id == SysRole.id)
.filter(
SysUserRole.user_id == user_id,
SysUserRole.del_flag == 0,
SysUserRole.tenant_no == tenant_no,
SysRole.del_flag == 0,
SysRole.status == "1",
SysRole.tenant_no == tenant_no,
)
.all()
)
result = []
for role in roles:
scope_details = (
db.query(SysRoleDataScopeDetail)
.filter(
SysRoleDataScopeDetail.role_id == role.id,
SysRoleDataScopeDetail.del_flag == 0,
)
.all()
)
result.append({
"data_scope": role.data_scope or "1",
"scope_details": [
{"scope_type": d.scope_type, "scope_value": d.scope_value}
for d in scope_details
],
})
return result
def apply_data_scope(
query: Any,
model: Any,
user_info: dict,
db: Session,
tenant_no: Optional[str] = None,
) -> Any:
"""在 SQLAlchemy query 上叠加数据权限过滤条件。
Args:
query: 原始 SQLAlchemy query 对象
model: ORM 模型类(需要有 create_dept_id, create_by 字段)
user_info: get_current_user 返回的 dict(含 user_id, dept_id 或 region_id)
db: 数据库 session
tenant_no: 租户编号(可选,用于部门树查询)
Returns:
添加了数据权限过滤条件的 query。如果用户是 super_admin 或无角色,返回原 query。
"""
# super_admin 不过滤
role = user_info.get("role")
if role == "super_admin":
return query
user_id = user_info.get("user_id")
if not user_id:
return query
tn = tenant_no or "zkxw"
# 确保模型有必要字段
if not hasattr(model, "create_dept_id") and not hasattr(model, "create_by"):
return query
# 获取用户所有角色的数据范围
role_scopes = _resolve_user_role_scopes(db, user_id, tn)
if not role_scopes:
# 无角色 → 不过滤(由菜单守卫控制是否可访问)
return query
# 任一角色 scope=1(全部数据)→ 不过滤
if any(rs["data_scope"] == "1" for rs in role_scopes):
return query
# 用户的部门ID
user_dept_id = user_info.get("dept_id") or user_info.get("region_id") or get_user_dept_id(db, user_id)
# 合并所有角色的过滤范围(并集)
dept_ids: Set[str] = set()
user_ids: Set[str] = set()
for rs in role_scopes:
ds = rs["data_scope"]
if ds == "2":
# 本部门
if user_dept_id:
dept_ids.add(user_dept_id)
elif ds == "3":
# 本部门及以下
if user_dept_id:
dept_ids.update(_get_child_dept_ids(db, user_dept_id, tn))
elif ds == "4":
# 仅本人
user_ids.add(user_id)
elif ds == "5":
# 自定义
for detail in rs["scope_details"]:
val = detail["scope_value"]
if detail["scope_type"] == "dept":
dept_ids.add(val)
elif detail["scope_type"] == "user":
user_ids.add(val)
# 构建过滤条件
conditions = []
has_dept_field = hasattr(model, "create_dept_id")
has_create_by_field = hasattr(model, "create_by")
if dept_ids and has_dept_field:
conditions.append(model.create_dept_id.in_(dept_ids))
if user_ids and has_create_by_field:
conditions.append(model.create_by.in_(user_ids))
if not conditions:
# 没有任何有效范围 → 退回不过滤
return query
# 多条件取 OR(并集语义)
if len(conditions) == 1:
return query.filter(conditions[0])
return query.filter(or_(*conditions))
- Step 2: Commit
git add backend/app/core/data_scope.py
git commit -m "feat: 添加数据权限过滤服务 data_scope.py"
Task 3: 修复 admin 无权限码安全漏洞
Files:
-
Modify:
backend/app/api/public.py(lines 272-279) -
Step 1: 修改 public.py 中 menu-codes 端点
将 backend/app/api/public.py 第 272-279 行的代码:
if role == "admin" and not menu_codes:
return success_response(
{
"isSuperAdmin": False,
"all": True,
"menuCodes": [],
}
)
改为:
# admin 无权限码时返回空列表(不再赋予全部权限)
# 安全修复:空权限码 = 无菜单访问权限
即删除整个 if role == "admin" and not menu_codes: 分支。后续的 return success_response(...) 会自动处理,返回 all: False, menuCodes: []。
- Step 2: Commit
git add backend/app/api/public.py
git commit -m "fix: 修复 admin 无权限码时返回 all:true 的安全漏洞"
Task 4: 后端业务路由添加菜单权限守卫 + 数据权限过滤
Files:
- Modify:
backend/app/api/basic_info.py - Modify:
backend/app/api/operations.py - Modify:
backend/app/api/collection_protocol.py - Modify:
backend/app/api/sample_reception.py - Modify:
backend/app/api/admin.py(用户/角色管理部分)
原则:
-
每个路由文件的 import 中添加
from app.core.menu_guard import require_menu_code和from app.core.data_scope import apply_data_scope -
将
Depends(require_admin_or_super_admin)替换为Depends(require_menu_code("xxx:list"))(list 端点)、Depends(require_menu_code("xxx:add"))(create 端点)等 -
list 端点在查询链中调用
apply_data_scope(query, Model, user, db, tenant_no) -
Step 1: 修改
basic_info.py
- 替换 import:
from app.core.menu_guard import require_admin_or_super_admin→from app.core.menu_guard import require_menu_code - 添加 import:
from app.core.data_scope import apply_data_scope - 客户管理路由:
list_customers:Depends(require_menu_code("biz:customer:list"))+ 查询添加apply_data_scopecreate_customer:Depends(require_menu_code("biz:customer:add"))update_customer:Depends(require_menu_code("biz:customer:edit"))delete_customer:Depends(require_menu_code("biz:customer:delete"))
- 细胞样本类型路由:同理,perms 为
biz:cellSampleType:* - 采集地区路由:同理,perms 为
biz:collectionRegion:* - 采集协议号路由:同理,perms 为
biz:collectionProtocolNo:* - 样本编码路由:同理,perms 为
biz:sampleCode:*
- Step 2: 修改
operations.py
洁净室路由添加 require_menu_code("biz:cleanRoom:*") + apply_data_scope。仓储设施、展厅预约同理。
注意:operations.py 中的业务目前没有对应的 sys_menu.perms,需要根据 SQL 中是否有对应菜单决定。如果 SQL 中尚无菜单记录,则保持 require_admin_or_super_admin 不变,待后续添加菜单后再改。
- Step 3: 修改
collection_protocol.py
采集协议路由添加 require_menu_code("biz:collectionProtocol:*") + apply_data_scope。
注意:SQL 中当前没有 biz:collectionProtocol:list 的菜单记录。如果此模块菜单尚未配置,保持 require_admin_or_super_admin。
- Step 4: 修改
sample_reception.py
样本接收路由添加 require_menu_code("biz:sampleReception:*") + apply_data_scope。
- Step 5: 修改
admin.py
用户管理、角色管理、字典管理等系统管理接口添加 require_menu_code:
- 用户 CRUD:
system:user:list/add/edit/delete - 角色 CRUD:
system:role:list/add/edit/delete - 租户 CRUD:保持
require_admin_or_super_admin(仅 super_admin) - 字典管理:
system:dict:list/add/edit/delete - 组织管理:
system:dept:list/add/edit/delete - 菜单管理:
system:menu:list/add/edit/delete
用户列表添加 apply_data_scope 过滤。
- Step 6: Commit
git add backend/app/api/basic_info.py backend/app/api/operations.py backend/app/api/collection_protocol.py backend/app/api/sample_reception.py backend/app/api/admin.py
git commit -m "feat: 所有业务路由添加菜单权限守卫和数据权限过滤"
Task 5: 前端用户管理添加角色选择器
Files:
-
Modify:
web-admin/src/views/system/user/index.vue -
Step 1: 在 formData 中添加 roleIds 字段
const formData = reactive({
userNo: '',
nickName: '',
password: '',
deptId: '',
email: '',
phoneNo: '',
sex: '0',
avatar: '',
status: '1',
orderNum: 0,
remark: '',
roleIds: [] as string[], // 新增
})
- Step 2: 添加角色选项加载逻辑
import { fetchRoleOptions } from '@/api/admin'
const roleOptions = ref<Array<{ label: string; value: string }>>([])
async function loadRoleOptions() {
try {
roleOptions.value = await fetchRoleOptions()
} catch (e) {
console.error('加载角色选项失败:', e)
}
}
- Step 3: 在弹窗表单中添加角色多选下拉框
在 <div class="form-grid-2"> 中,"状态" 选择器后面、"备注" 前面添加:
<el-form-item label="角色" style="grid-column: 1 / -1">
<el-select
v-model="formData.roleIds"
multiple
placeholder="请选择角色"
style="width: 100%"
>
<el-option
v-for="item in roleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
- Step 4: 在 handleSubmit 中传递 roleIds
// create 分支
await fetchCreateUser({
...其他字段,
roleIds: formData.roleIds.length ? formData.roleIds : undefined,
})
// update 分支
await fetchUpdateUser(editingId.value, {
...其他字段,
roleIds: formData.roleIds.length ? formData.roleIds : undefined,
})
- Step 5: 在 handleEdit 中回填 roleIds
编辑时从 row.roleIds 回填:
function handleEdit(row: Api.User.UserItem) {
// ... 现有字段回填 ...
formData.roleIds = (row as any).roleIds || [] // 后端 get_user_detail 返回 roleIds
}
- Step 6: 在 resetForm 和 onMounted 中处理
function resetForm() {
// ... 现有重置 ...
formData.roleIds = []
}
onMounted(() => {
loadDeptTree()
loadData()
loadRoleOptions()
})
- Step 7: Commit
git add web-admin/src/views/system/user/index.vue
git commit -m "feat: 用户管理弹窗添加角色多选下拉框"
Task 6: 前端业务页面按钮添加权限控制
Files:
- Modify:
web-admin/src/views/basic-info/customer/index.vue - Modify:
web-admin/src/views/basic-info/cell-sample-type/index.vue - Modify:
web-admin/src/views/basic-info/collection-region/index.vue - Modify:
web-admin/src/views/basic-info/collection-protocol-no/index.vue - Modify:
web-admin/src/views/basic-info/sample-code/index.vue
每个页面的修改模式相同:
- Step 1: 在
<script setup>中导入 useAuth
import { useAuth } from '@/hooks/core/useAuth'
const { hasAuth } = useAuth()
- Step 2: 在模板按钮上添加 v-if 权限控制
工具栏"新增"按钮:
<el-button v-if="hasAuth('add')" type="primary" @click="handleAdd">新增</el-button>
表格操作列按钮:
<el-button v-if="hasAuth('edit')" link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" link type="danger" @click="handleDelete(row)">删除</el-button>
- Step 3: 对所有 5 个页面重复以上修改
每个页面的按钮权限标识通过 hasAuth('add') / hasAuth('edit') / hasAuth('delete') 控制,对应 sys_menu 中 F 类型按钮记录的 perms 尾段。
- Step 4: Commit
git add web-admin/src/views/basic-info/
git commit -m "feat: 基础信息管理页面按钮添加 hasAuth 权限控制"
自检清单
- Spec 覆盖:Task 1 覆盖 F 类型菜单数据;Task 2 覆盖数据权限服务;Task 3 覆盖安全漏洞修复;Task 4 覆盖后端路由守卫;Task 5 覆盖前端角色选择器;Task 6 覆盖前端按钮权限
- Placeholder 扫描:无 TBD、TODO、"implement later"
- 类型一致性:
apply_data_scope函数签名在 Task 2 定义,Task 4 使用时参数匹配 - 文件路径准确:所有路径基于实际代码库结构