拆分顺序:数据模型 → 接口协议 → 底层实现 → 上层编排 → 入口层 → 旁路回归 每个任务控制在可独立提交的原子变更内 Commit message 格式:
[org-scoped-role-permission] <中文简述>archive 状态:Task 1-11 已实施;Task 12 已完成静态检查、后端编译和前端构建;补充完成create_dept当前组织写入修正。Review 后已修复服务层权限兜底、旧角色加人接口绕过、角色加人直接组织筛选、业务消息组织上下文和超级管理员显式组织绑定边界。接口/数据库实测待可用运行环境补充。
SysUserServiceImpl 增加用户/组织/角色管理权限兜底;SysOrgServiceImpl 增加组织读写权限兜底;SysRoleServiceImpl 增加角色管理权限兜底。@ApiPermissionIgnore 继续遵循既有策略。SysRoleServiceImpl#addUsersToRole 不再写 sys_user_role,统一返回旧接口废弃错误,要求走组织角色授权。UserSelectPanel 增加 directOrgOnly,角色页锁定授权组织时只查询直接加入该组织的用户,避免选中子组织成员后保存失败。activeOrgId/orgId/deptId/createDept,并向查询传入业务 tenantId + orgId,Session 仅作为兜底。sys_user_org 判断真实绑定关系;有显式绑定时,/system/org/current/options 只返回绑定组织,/system/org/switch 只允许切换绑定组织;仅无任何绑定的历史超级管理员账号保留全量组织兜底。git diff --check 已通过;服务级接口/数据库实测仍待可控本地 MySQL/Redis 环境。spec.md 作为本轮 /apply 执行依据。git status --short,确认不在 master 分支直接编码,并识别已有用户改动。V1.0.0,本变更脚本名固定为 V1.0.1__add_org_scoped_role_permission.sql。code-copilot/rules/automated-testing-standard.md、当前 test-spec.md 和 execution-log.md。状态:completed
forge-server/db/migration/V1.0.1__add_org_scoped_role_permission.sqlforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/entity/SysRoleOrg.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/entity/SysUserOrgRole.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/mapper/SysRoleOrgMapper.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/mapper/SysUserOrgRoleMapper.java@TableName("sys_user_org_role") public class SysUserOrgRole extends TenantEntity { }
public interface SysRoleOrgMapper extends BaseMapper { }
public interface SysUserOrgRoleMapper extends BaseMapper { }
- **执行步骤**:
- [ ] 创建两个新表,字段包含 `id`, `tenant_id`, `role_id`, `org_id`, `user_id`(仅 `sys_user_org_role`), `create_by`, `create_time`, `create_dept`, `update_by`, `update_time`。
- [ ] 为 `sys_role_org` 增加 `uk_role_org(tenant_id, role_id, org_id)` 和 `idx_org_role(tenant_id, org_id, role_id)`。
- [ ] 为 `sys_user_org_role` 增加 `uk_user_org_role(tenant_id, user_id, org_id, role_id)`, `idx_user_org(tenant_id, user_id, org_id)`, `idx_org_role(tenant_id, org_id, role_id)`。
- [ ] 迁移角色适用组织:把每个数据中心内已有角色绑定到该数据中心下已有组织,使用 `WHERE NOT EXISTS` 防重复。
- [ ] 迁移用户组织角色:把 `sys_user_role` 复制到该用户已绑定的所有组织,使用 `WHERE NOT EXISTS` 防重复。
- [ ] 执行静态检查:`rg -n '\$\{[^}]+\}' forge-server/db/migration` 应无输出。
## Task 2: 登录态增加当前组织上下文
> 状态:completed
- **目标**:让 `LoginUser` 能表达当前组织,并支持在用户信息刷新时保留有效当前组织。
- **涉及文件**:
- 修改:`forge-server/forge-framework/forge-starter-parent/forge-starter-core/src/main/java/com/mdframe/forge/starter/core/session/LoginUser.java`
- 修改:`forge-server/forge-framework/forge-starter-parent/forge-starter-core/src/main/java/com/mdframe/forge/starter/core/session/SessionHelper.java`
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/IUserLoadService.java`
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/impl/UserLoadServiceImpl.java`
- 修改:`forge-server/forge-framework/forge-starter-parent/forge-starter-auth/src/main/java/com/mdframe/forge/starter/auth/controller/AuthController.java`
- **关键签名**:
```java
private Long activeOrgId;
private String activeOrgName;
public static Long getActiveOrgId();
LoginUser loadUserByUserId(Long userId, Long tenantId, Long preferredActiveOrgId);
LoginUser loadUserByUsername(String username, Long tenantId, Long preferredActiveOrgId);
LoginUser 新增 activeOrgId、activeOrgName。SessionHelper 新增 getActiveOrgId() 和 getActiveOrgName()。IUserLoadService 增加带 preferredActiveOrgId 的重载,旧方法委托到新方法。UserLoadServiceImpl#loadUserOrgs 加载当前数据中心组织后,按“preferredActiveOrgId 有效 → 主组织 → 第一个组织”确定当前组织。UserLoadServiceImpl#loadUserRegion 改为优先使用 activeOrgId 对应组织行政区划,再回退用户自身行政区划。AuthController#getUserInfo 重建用户信息时传入旧 Session 的 activeOrgId,避免刷新覆盖组织切换结果。状态:completed
roleIds/roleKeys/permissions/apiPermissions 按当前组织授权计算。UserLoadServiceImpl.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/resources/mapper/SysUserOrgRoleMapper.xmlforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/resources/mapper/SysResourceMapper.xmlList selectActiveRoleIdsByUserOrg(@Param("tenantId") Long tenantId,
@Param("userId") Long userId,
@Param("orgId") Long orgId);
- **执行步骤**:
- [ ] 普通用户角色查询改为 `tenant_id + user_id + active_org_id` 查询 `sys_user_org_role`。
- [ ] 查询角色时过滤 `role_status = 1`。
- [ ] 查询角色时校验角色仍适用于当前组织:存在 `sys_role_org(tenant_id, role_id, activeOrgId)`。
- [ ] 当前组织无角色时设置空 `roleIds/roleKeys/permissions/apiPermissions`,不得回退其他组织角色。
- [ ] 系统管理员和数据中心管理员保持现有特权边界,但仍返回当前组织展示字段。
- [ ] 确认 `loadUserPermissions` 和 `loadApiPermissions` 不再需要直接感知组织,只消费已经收窄后的 `roleIds`。
## Task 4: 当前组织切换接口
> 状态:completed
- **目标**:提供当前数据中心内组织切换能力,返回重算后的登录态。
- **涉及文件**:
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/controller/SysOrgController.java`
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/ISysOrgService.java`
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/impl/SysOrgServiceImpl.java`
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/impl/SysTenantServiceImpl.java`
- **关键签名**:
```java
List<SysOrgTreeVO> selectCurrentUserOrgOptions();
LoginUser switchCurrentOrg(Long orgId);
GET /system/org/current/options,只返回当前数据中心下当前用户已加入组织。POST /system/org/switch?orgId=,校验组织属于当前数据中心且用户已加入。IUserLoadService.loadUserByUserId(userId, tenantId, orgId) 重建权限并写回 Session。tenantId 和 TenantContextHolder。SysTenantServiceImpl#switchTenant,切换数据中心后不复用旧数据中心的 activeOrgId,按新数据中心默认组织重建。状态:completed
SysUserServiceImpl.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/ISysUserService.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/vo/UserOrgBindingVO.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/vo/UserOrgRoleSummaryVO.javaboolean bindUserOrgs(Long userId, List orgIds, Long mainOrgId, Long requestedTenantId);
- **执行步骤**:
- [ ] `bindUserOrgs` 删除组织关系前,先删除 `sys_user_org_role` 中该用户、该数据中心、被移除组织的授权。
- [ ] `unbindUserOrg` 删除单个组织时同步删除对应组织角色授权。
- [ ] `removeTenantUser` 和 `bindUserTenants` 移除数据中心成员时同步删除 `sys_user_org_role`。
- [ ] 新增 `GET /system/user/{userId}/org-bindings` 返回组织名称、是否主组织、角色数量和角色名称摘要。
- [ ] 当前登录用户组织关系变化时,同步更新 Session 中 `orgIds/mainOrgId/activeOrgId/activeOrgName`。
## Task 6: 用户组织角色授权接口
> 状态:completed
- **目标**:提供按组织授权用户角色的主接口,替代旧全局用户角色授权入口。
- **涉及文件**:
- 修改:`SysUserController.java`
- 修改:`ISysUserService.java`
- 修改:`SysUserServiceImpl.java`
- 新增:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/dto/UserOrgRoleBindDTO.java`
- **关键签名**:
```java
List<Long> selectUserOrgRoleIds(Long userId, Long orgId, Long tenantId);
boolean bindUserOrgRoles(Long userId, Long orgId, List<Long> roleIds, Long tenantId);
GET /system/user/{userId}/org-roles?orgId=&tenantId=。POST /system/user/{userId}/org-roles,请求体包含 orgId、roleIds。/system/user/{userId}/roles 保留兼容,不作为新前端主入口。状态:completed
SysRoleController.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/ISysRoleService.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/service/impl/SysRoleServiceImpl.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/java/com/mdframe/forge/plugin/system/dto/SysRoleDTO.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-system/src/main/resources/mapper/SysRoleMapper.xmlboolean bindRoleOrgs(Long roleId, List orgIds);
- **执行步骤**:
- [ ] `SysRoleDTO` 增加 `List<Long> orgIds`,用于角色新增/编辑时可选保存适用组织。
- [ ] 新增 `GET /system/role/{roleId}/orgs` 和 `POST /system/role/{roleId}/orgs`。
- [ ] 保存适用组织时校验组织属于角色数据中心。
- [ ] 角色适用组织被移除时,删除该角色在被移除组织下的 `sys_user_org_role` 授权。
- [ ] `selectRoleUsers` 支持可选 `orgId`,按 `sys_user_org_role` 查询用户。
- [ ] `countUsersByRole` 改为统计 `sys_user_org_role`,防止删除仍被组织授权使用的角色。
## Task 8: 数据权限主链路收窄
> 状态:completed
- **目标**:MyBatis 数据权限和低代码动态数据权限都按当前组织执行。
- **涉及文件**:
- 修改:`forge-server/forge-framework/forge-starter-parent/forge-starter-datascope/src/main/java/com/mdframe/forge/starter/datascope/context/DataScopeContext.java`
- 修改:`forge-server/forge-framework/forge-starter-parent/forge-starter-datascope/src/main/java/com/mdframe/forge/starter/datascope/service/impl/DataScopeServiceImpl.java`
- 修改:`forge-server/forge-framework/forge-starter-parent/forge-starter-datascope/src/main/java/com/mdframe/forge/starter/datascope/handler/DataScopeInterceptor.java`
- 修改:`forge-server/forge-framework/forge-plugin-parent/forge-plugin-generator/src/main/java/com/mdframe/forge/plugin/generator/service/DynamicDataScopeService.java`
- **关键签名**:
```java
private Long activeOrgId;
public DataScopeContext getCurrentUserDataScope();
DataScopeContext 增加 activeOrgId,保留 orgIds 兼容现有 SQL 占位符。DataScopeServiceImpl 普通用户上下文设置 activeOrgId,并把 orgIds 设置为只包含当前组织的单元素列表。ORG 使用当前组织;当前组织为空时强制无数据。ORG_AND_CHILD 只展开当前组织及子组织。CUSTOM 只基于当前组织有效角色的自定义组织范围。DynamicDataScopeService 复用收窄后的 DataScopeContext,不再自行扩大到全部组织。状态:completed
sys_user_role 或全部组织导致的旁路串权。forge-server/forge-framework/forge-plugin-parent/forge-plugin-data/src/main/java/com/mdframe/forge/plugin/data/service/impl/DataDatasetAccessServiceImpl.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-data/src/main/java/com/mdframe/forge/plugin/data/service/impl/DataDatasetRowScopeServiceImpl.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-generator/src/main/resources/mapper/BusinessMessageChannelMapper.xmlforge-server/forge-framework/forge-plugin-parent/forge-plugin-generator/src/main/java/com/mdframe/forge/plugin/generator/service/businessapp/BusinessMessageChannelService.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-flow/src/main/java/com/mdframe/forge/starter/flow/service/impl/FlowInstanceServiceImpl.javaforge-server/forge-framework/forge-plugin-parent/forge-plugin-flow/src/main/java/com/mdframe/forge/starter/flow/service/impl/FlowOrgIntegrationServiceImpl.javaList selectUserOrgRoleIds(Long userId, Long orgId, Long tenantId);
- **执行步骤**:
- [ ] 数据集 ACL 使用当前组织和当前组织角色匹配 `ROLE/ORG` 主体。
- [ ] 数据集行级权限的 `ORG_AND_CHILD` 只展开当前组织。
- [ ] 消息按角色接收人查询改为 `sys_user_org_role`,并传入当前组织或业务明确组织。
- [ ] 流程启动变量 `startUserRoleIds` 改为当前组织角色。
- [ ] 流程启动变量保留 `startUserOrgIds` 兼容旧表达式,同时新增 `startUserActiveOrgId`。
- [ ] 流程角色判断必须带 `tenantId + activeOrgId`,不能只按 `userId` 查询所有角色。
## Task 10: 前端状态和当前组织切换器
> 状态:completed
- **目标**:前端展示当前组织并在切换后刷新权限、菜单和路由缓存。
- **涉及文件**:
- 修改:`forge-admin-ui/src/store/helper.js`
- 修改:`forge-admin-ui/src/store/modules/user.js`
- 修改:`forge-admin-ui/src/api/index.js`
- 新增:`forge-admin-ui/src/layouts/components/OrgSwitcher.vue`
- 修改:`forge-admin-ui/src/layouts/components/index.js`
- 修改:`forge-admin-ui/src/layouts/normal/header/index.vue`
- 修改:`forge-admin-ui/src/layouts/immersive/header/index.vue`
- 修改:`forge-admin-ui/src/layouts/top-menu/index.vue`
- 修改:`forge-admin-ui/src/layouts/top-side-menu/index.vue`
- 修改:`forge-admin-ui/src/layouts/full/header/index.vue`
- 修改:`forge-admin-ui/src/layouts/nexus/header/index.vue`
- **关键签名**:
```js
getCurrentOrgOptions: () => request.get('/system/org/current/options')
switchOrg: orgId => request.post('/system/org/switch', null, { params: { orgId } })
helper.js 映射 activeOrgId/activeOrgName。user.js 增加 activeOrgId、activeOrgName getters。OrgSwitcher.vue,单组织用户隐藏或禁用,多组织用户可切换。authStore.resetLoginState({ resetAuth: false }),保留 token。状态:completed
forge-admin-ui/src/views/system/user.vueforge-admin-ui/src/views/system/role.vueforge-admin-ui/src/views/system/components/UserOrgRolePanel.vueforge-admin-ui/src/views/system/components/RoleOrgScopePanel.vuetext
GET /system/user/{userId}/org-bindings
GET /system/user/{userId}/org-roles?orgId=
POST /system/user/{userId}/org-roles
GET /system/role/{roleId}/orgs
POST /system/role/{roleId}/orgs
orgIds + mainOrgId,重新打开能回显多个组织。roleIds 不再作为新主入口,避免误导。状态:partially completed(静态检查、编译、构建已通过;服务级接口/数据库实测待补)
code-copilot/changes/org-scoped-role-permission/test-spec.mdcode-copilot/changes/org-scoped-role-permission/execution-log.mdcode-copilot/changes/org-scoped-role-permission/spec.mdcode-copilot/changes/org-scoped-role-permission/tasks.mdgit diff --check。mvn -pl forge-admin-server -am compile -DskipTests。source ~/.nvm/nvm.sh && nvm use v20.19.0 && NODE_OPTIONS=--max-old-space-size=8192 pnpm --dir forge-admin-ui build。create_dept 优先使用 LoginUser.activeOrgId,避免多组织下新数据落到非当前组织。ORG 和 ORG_AND_CHILD 数据权限。(本轮未启动服务和数据库;本地 3407 MySQL 未运行,3306 root 无密码访问被拒,Redis 探测受沙箱限制且提权未获批)execution-log.md。tasks.md 任务状态和 spec.md 审查结论。