diff --git a/backend/README.md b/backend/README.md index 1eeeb84e..3f330b71 100644 --- a/backend/README.md +++ b/backend/README.md @@ -58,8 +58,12 @@ SQL 位于 [docs/sql](../docs/sql/)。当前初始化基线不完整,部分历 [2026-11-02-project_management.sql](../docs/sql/migrations/2026-11-02-project_management.sql), 再依次执行[菜单种子](../docs/sql/migrations/2026-11-02-project_management_menu_seed.sql)与 [项目一级菜单迁移](../docs/sql/migrations/2026-11-02-project_top_menu.sql), -最后执行[全部项目权限种子](../docs/sql/migrations/2026-11-02-project_all_permission.sql)。 -“项目”与“市场、生产、质检”平级,包含项目组管理、项目管理、业务统计、历史归属维护。 +再执行[全部项目权限种子](../docs/sql/migrations/2026-11-02-project_all_permission.sql), +最后执行[项目管理分组迁移](../docs/sql/migrations/2026-11-04-project_management_group.sql)。 +已执行前四个脚本的数据库只需追加分组迁移;完整重跑时也须将分组迁移放在最后。 +“项目”与“市场、生产、质检”平级,下设“项目管理”目录,其下包含项目组管理、项目管理、业务统计、历史归属维护。 +分组迁移保留四个页面的 ID 和权限,仅为已授权子页面/编辑按钮的角色补齐祖先; +旧 `/project/{页面}` 和 `/market/project-management/{页面}` 地址跳转至 `/project/management/{页面}`。 菜单种子不会给角色新增业务权限,应在现有角色管理分配项目管理权限及祖先菜单; 一级菜单迁移保留已有叶子 ID/授权,仅给已授权项目子菜单的角色补齐新祖先。 “所属项目”默认只列出本人担任负责人或成员的项目组内项目(多组取并集)。 diff --git a/backend/app/api/menu.py b/backend/app/api/menu.py index 6470f56d..b8034d13 100644 --- a/backend/app/api/menu.py +++ b/backend/app/api/menu.py @@ -19,14 +19,20 @@ STATIC_MENU_TREE = [ "isTopMenu": True, "menuCode": "menu:project", "roles": ["admin"], }, "children": [ - {"name": "ProjectManagementTeams", "path": "teams", "component": "/project-management/teams/index", - "meta": {"title": "项目组管理", "keepAlive": True, "menuCode": "project:team:list"}}, - {"name": "ProjectManagementProjects", "path": "projects", "component": "/project-management/projects/index", - "meta": {"title": "项目管理", "keepAlive": True, "menuCode": "project:project:list"}}, - {"name": "ProjectManagementStatistics", "path": "statistics", "component": "/project-management/statistics/index", - "meta": {"title": "业务统计", "keepAlive": True, "menuCode": "project:statistics:list"}}, - {"name": "ProjectManagementAssignments", "path": "assignments", "component": "/project-management/assignments/index", - "meta": {"title": "历史归属维护", "keepAlive": True, "menuCode": "project:assignment:edit"}}, + { + "name": "ProjectManagement", "path": "management", "component": "", + "meta": {"title": "项目管理", "keepAlive": False, "menuCode": "menu:project:management"}, + "children": [ + {"name": "ProjectManagementTeams", "path": "teams", "component": "/project-management/teams/index", + "meta": {"title": "项目组管理", "keepAlive": True, "menuCode": "project:team:list"}}, + {"name": "ProjectManagementProjects", "path": "projects", "component": "/project-management/projects/index", + "meta": {"title": "项目管理", "keepAlive": True, "menuCode": "project:project:list"}}, + {"name": "ProjectManagementStatistics", "path": "statistics", "component": "/project-management/statistics/index", + "meta": {"title": "业务统计", "keepAlive": True, "menuCode": "project:statistics:list"}}, + {"name": "ProjectManagementAssignments", "path": "assignments", "component": "/project-management/assignments/index", + "meta": {"title": "历史归属维护", "keepAlive": True, "menuCode": "project:assignment:edit"}}, + ], + }, ], }, # ──────────────────────────────────────────── diff --git a/docs/sql/migrations/2026-11-04-project_management_group.sql b/docs/sql/migrations/2026-11-04-project_management_group.sql new file mode 100644 index 00000000..3c44b0c1 --- /dev/null +++ b/docs/sql/migrations/2026-11-04-project_management_group.sql @@ -0,0 +1,46 @@ +-- Issue #13:在 project_all_permission 之后执行,可重复执行。 +-- 项目 → 项目管理 → 四个原页面。保留页面 ID/权限及移动菜单。 +-- 全部项目权限仍挂在项目一级节点,不以目录授权隐式授予业务权限。 +START TRANSACTION; + +INSERT INTO sys_menu (id, menu_name, parent_id, menu_type, path, component, perms, + is_frame, visible, status, tenant_no, order_num, create_dept_id, create_time, update_time, del_flag) +SELECT MD5(CONCAT(top_menu.tenant_no, ':menu:project:management')), '项目管理', top_menu.id, + 'M', 'management', '', 'menu:project:management', 1, '1', '1', top_menu.tenant_no, + 1, '', NOW(), NOW(), 0 +FROM sys_menu top_menu +WHERE top_menu.perms='menu:project' AND top_menu.del_flag=0 +AND NOT EXISTS (SELECT 1 FROM sys_menu existing + WHERE existing.tenant_no=top_menu.tenant_no AND existing.perms='menu:project:management'); + +UPDATE sys_menu leaf +JOIN sys_menu group_menu ON group_menu.tenant_no=leaf.tenant_no + AND group_menu.perms='menu:project:management' AND group_menu.del_flag=0 +SET leaf.parent_id=group_menu.id, leaf.update_time=NOW() +WHERE leaf.del_flag=0 AND leaf.perms IN ( + 'project:team:list', 'project:project:list', 'project:statistics:list', 'project:assignment:edit'); + +-- 仅已有页面/编辑按钮授权的角色补齐目录、项目一级和 Web 根。 +-- 只拥有全部项目选择权限的角色不增加四个管理页面的访问权限。 +INSERT INTO sys_role_menu (id, role_id, menu_id, tenant_no, create_dept_id, create_time, update_time, del_flag) +SELECT DISTINCT MD5(CONCAT(grant_row.tenant_no, ':project-management-ancestor:', grant_row.role_id, ':', ancestor.id)), + grant_row.role_id, ancestor.id, grant_row.tenant_no, '', NOW(), NOW(), 0 +FROM sys_role_menu grant_row +JOIN sys_role role_row ON role_row.id=grant_row.role_id AND role_row.tenant_no=grant_row.tenant_no + AND role_row.del_flag=0 +JOIN sys_menu leaf ON leaf.id=grant_row.menu_id AND leaf.tenant_no=grant_row.tenant_no + AND leaf.del_flag=0 AND leaf.perms IN ('project:team:list', 'project:project:list', + 'project:statistics:list', 'project:assignment:edit', 'project:team:edit', 'project:project:edit') +JOIN sys_menu group_menu ON group_menu.tenant_no=grant_row.tenant_no + AND group_menu.perms='menu:project:management' AND group_menu.del_flag=0 +JOIN sys_menu top_menu ON top_menu.id=group_menu.parent_id AND top_menu.tenant_no=grant_row.tenant_no + AND top_menu.perms='menu:project' AND top_menu.del_flag=0 +JOIN sys_menu ancestor ON ancestor.tenant_no=grant_row.tenant_no AND ancestor.del_flag=0 + AND ancestor.id IN (group_menu.id, top_menu.id, top_menu.parent_id) +WHERE grant_row.del_flag=0 +AND NOT EXISTS (SELECT 1 FROM sys_role_menu existing + WHERE existing.tenant_no=grant_row.tenant_no AND existing.role_id=grant_row.role_id + AND existing.menu_id=ancestor.id AND existing.del_flag=0) +ON DUPLICATE KEY UPDATE del_flag=0, update_time=NOW(); + +COMMIT; diff --git a/openspec/changes/project-business-statistics/specs/project-management/spec.md b/openspec/changes/project-business-statistics/specs/project-management/spec.md index a5d990dd..246fdd09 100644 --- a/openspec/changes/project-business-statistics/specs/project-management/spec.md +++ b/openspec/changes/project-business-statistics/specs/project-management/spec.md @@ -1,5 +1,14 @@ ## ADDED Requirements +### Requirement: 项目管理三级导航 +Web SHALL 使用“项目 → 项目管理 → 项目组管理 / 项目管理 / 业务统计 / 历史归属维护”层级,复用现有导航组件;原页面权限、名称和功能保持。 +#### Scenario: 现有角色升级 +- **WHEN** 执行项目管理分组迁移 +- **THEN** 保留子页面 ID 与原授权,仅为已授权子页面/编辑按钮的角色补齐目录祖先;全部项目选择权限仍独立挂在项目一级下,不授予管理页面权限。 +#### Scenario: 旧地址访问 +- **WHEN** 访问 `/project/{页面}` 或 `/market/project-management/{页面}` +- **THEN** 携带原查询参数及 hash 跳转至 `/project/management/{页面}`,目标仍按原页面权限检查。 + ### Requirement: 组织信息与业务地点独立 系统 SHALL 维护项目组负责人、成员及区域医院资料,区域医院仅为信息记录。 #### Scenario: 修改负责医院 diff --git a/openspec/changes/project-business-statistics/tasks.md b/openspec/changes/project-business-statistics/tasks.md index eda4cedc..90b95c5d 100644 --- a/openspec/changes/project-business-statistics/tasks.md +++ b/openspec/changes/project-business-statistics/tasks.md @@ -4,6 +4,17 @@ **架构:** 新管理模块统一校验与统计;现有业务显式接入;前端复用已有样式。 **技术栈:** FastAPI/SQLAlchemy/Pydantic、Vue3/Element Plus、Vue3/Vant。 +## 2026-11-04 菜单增加项目管理分组 + +用户明确指定层级:项目 → 项目管理 → 项目组管理 / 项目管理 / 业务统计 / 历史归属维护。仅重组现有菜单配置,复用市场三级导航,不新增业务页面或修改页面样式。沿用用户自测及先合入 V2.0 再测试的安排,工单保持开放。 +- [x] 同步 Web `sqxp.ts` 与后端 `menu.py`,新增 `menu:project:management` 目录,保留四个页面名称、组件和权限码。 +- [x] `staticRoutes.ts` 兼容两代旧地址,保留 query 与 hash。 +- [x] 新增 `2026-11-04-project_management_group.sql`,保留叶子 ID 与角色授权,仅补新祖先;全部项目权限仍独立挂在项目一级下,更新 README 顺序。 +- [x] 独立源码审查未发现 P1/P2,Git 差异核对通过;用户测试模式未运行测试或构建。 +- [ ] 提交续接 PR、合入 V2.0、同步主目录;备份并更新已确认测试库菜单,交付用户验收。 + +以下为此前阶段历史记录;其中待合并等状态已由 2026-11-04 用户明确先合并再测试的指令取代。 + 用户追加所属项目权限要求:将跨组选择从归属维护权限分离为 `project:project:all`(全部项目),默认仅项目组负责人/有效成员所在组内项目;Web与移动共用后端范围校验。 - [x] 在 `backend/tests/test_project_selection_permissions.py` 增加成员/多组/无组、独立全部项目权限、多角色及职位继承、撤权、跨租户和越权提交用例,先执行确认失败。 - [x] 在 `backend/app/services/project_management.py` 统一候选范围,赋值校验改用独立权限;筛选仍叠加原订单数据范围,历史回显与转换继承保持原有规则。 diff --git a/web-admin/src/router/modules/sqxp.ts b/web-admin/src/router/modules/sqxp.ts index cdb3c945..d9c3375b 100644 --- a/web-admin/src/router/modules/sqxp.ts +++ b/web-admin/src/router/modules/sqxp.ts @@ -20,28 +20,36 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [ }, children: [ { - name: 'ProjectManagementTeams', - path: 'teams', - component: '/project-management/teams/index', - meta: { title: '项目组管理', keepAlive: true, menuCode: 'project:team:list' } - }, - { - name: 'ProjectManagementProjects', - path: 'projects', - component: '/project-management/projects/index', - meta: { title: '项目管理', keepAlive: true, menuCode: 'project:project:list' } - }, - { - name: 'ProjectManagementStatistics', - path: 'statistics', - component: '/project-management/statistics/index', - meta: { title: '业务统计', keepAlive: true, menuCode: 'project:statistics:list' } - }, - { - name: 'ProjectManagementAssignments', - path: 'assignments', - component: '/project-management/assignments/index', - meta: { title: '历史归属维护', keepAlive: true, menuCode: 'project:assignment:edit' } + name: 'ProjectManagement', + path: 'management', + component: '', + meta: { title: '项目管理', keepAlive: false, menuCode: 'menu:project:management' }, + children: [ + { + name: 'ProjectManagementTeams', + path: 'teams', + component: '/project-management/teams/index', + meta: { title: '项目组管理', keepAlive: true, menuCode: 'project:team:list' } + }, + { + name: 'ProjectManagementProjects', + path: 'projects', + component: '/project-management/projects/index', + meta: { title: '项目管理', keepAlive: true, menuCode: 'project:project:list' } + }, + { + name: 'ProjectManagementStatistics', + path: 'statistics', + component: '/project-management/statistics/index', + meta: { title: '业务统计', keepAlive: true, menuCode: 'project:statistics:list' } + }, + { + name: 'ProjectManagementAssignments', + path: 'assignments', + component: '/project-management/assignments/index', + meta: { title: '历史归属维护', keepAlive: true, menuCode: 'project:assignment:edit' } + } + ] } ] }, diff --git a/web-admin/src/router/routes/staticRoutes.ts b/web-admin/src/router/routes/staticRoutes.ts index f70cd1f9..952eb27a 100644 --- a/web-admin/src/router/routes/staticRoutes.ts +++ b/web-admin/src/router/routes/staticRoutes.ts @@ -11,14 +11,16 @@ import { AppRouteRecordRaw } from '@/utils/router' * 2、静态路由不管是否登录都可以访问 */ export const staticRoutes: AppRouteRecordRaw[] = [ - ...['teams', 'projects', 'statistics', 'assignments'].map((section) => ({ - path: `/market/project-management/${section}`, - redirect: (to: { query: Record; hash: string }) => ({ - path: `/project/${section}`, - query: to.query, - hash: to.hash - }) - })), + ...['/market/project-management', '/project'].flatMap((prefix) => + ['teams', 'projects', 'statistics', 'assignments'].map((section) => ({ + path: `${prefix}/${section}`, + redirect: (to: { query: Record; hash: string }) => ({ + path: `/project/management/${section}`, + query: to.query, + hash: to.hash + }) + })) + ), { path: '/auth/login', name: 'Login',