diff --git a/docs/design/2026-04-23-user-project-binding.md b/docs/design/2026-04-23-user-project-binding.md index 40da7afb9..e64ccb45c 100644 --- a/docs/design/2026-04-23-user-project-binding.md +++ b/docs/design/2026-04-23-user-project-binding.md @@ -2,7 +2,7 @@ - Generated by /office-hours on 2026-04-23 - Frontend branch: `feat/multi-tenant-project`(yudao-ui-admin-vben) -- Backend branch: `feat/multi-tenant`(aiot-platform-cloud) +- Backend branch: `feat/multi-tenant`(aiot-platform-cloud) - Status: APPROVED - Mode: 内部项目(双端联动) @@ -23,7 +23,7 @@ ### 后端(aiot-platform-cloud · `viewsh-module-system`)已就位 | 组件 | 路径 | 说明 | -|---|---|---| +| --- | --- | --- | | `system_project` 表 | `sql/mysql/project/01-create-tables.sql` | 含租户隔离、`CODE=DEFAULT` 约定 | | `system_user_project` 表 | 同上 | 中间表 `(user_id, project_id)` 唯一约束 + `tenant_id` | | `ProjectDO` / `UserProjectDO` | `dal/dataobject/project/` | UserProjectDO 只有二元组字段 | @@ -38,7 +38,7 @@ ### 前端(yudao-ui-admin-vben · `apps/web-antd`)已就位 | 组件 | 路径 | -|---|---| +| --- | --- | | 项目 CRUD 页面 | `views/system/project/{index.vue, modules/form.vue, data.ts}` | | 项目 API | `api/system/project/index.ts` | | 顶栏项目切换器 | `packages/effects/layouts/src/widgets/project-dropdown/project-dropdown.vue` | @@ -68,6 +68,7 @@ ## Approaches Considered ### Approach A: 一次到位(已选) + 一个 PR 同时交付:UserProject 管理 API + 双入口前端弹窗 + `simple-list` bug 修复。 - Completeness: 9/10 @@ -77,6 +78,7 @@ - Reuses: `UserProjectMapper`、`PermissionController.assign-user-role` 模式、`assign-role-form.vue` 模板 ### Approach B: 分两步交付(备选) + PR1 只做"用户视角"(后端 2 个接口 + 修 bug + 前端用户页入口);PR2 做"项目视角"。 - Completeness: 8/10 @@ -85,6 +87,7 @@ PR1 只做"用户视角"(后端 2 个接口 + 修 bug + 前端用户页入口 - Cons: 项目经理视角的体验要等下个迭代 ### Approach C: 极简版(备选) + 在用户编辑表单里直接加"所属项目"多选下拉框(类似 `deptId`/`postIds`),不做独立弹窗,不改项目页。 - Completeness: 5/10 @@ -104,15 +107,14 @@ PR1 只做"用户视角"(后端 2 个接口 + 修 bug + 前端用户页入口 #### 1. 新建 `UserProjectController` -路径:`controller/admin/project/UserProjectController.java` -基础路径:`/system/user-project` +路径:`controller/admin/project/UserProjectController.java` 基础路径:`/system/user-project` | 方法 | 路径 | 权限 | Body / Param | 返回 | -|---|---|---|---|---| +| --- | --- | --- | --- | --- | | POST | `/assign-user-projects` | `system:user:assign-project` | `{userId, projectIds: Set}` | `Boolean` | | POST | `/assign-project-users` | `system:project:assign-user` | `{projectId, userIds: Set}` | `Boolean` | -| GET | `/list-project-ids-by-user` | `system:user:assign-project` | `?userId=` | `Set` | -| GET | `/list-user-ids-by-project` | `system:project:assign-user` | `?projectId=` | `Set` | +| GET | `/list-project-ids-by-user` | `system:user:assign-project` | `?userId=` | `Set` | +| GET | `/list-user-ids-by-project` | `system:project:assign-user` | `?projectId=` | `Set` | 两个 `assign-*` 都是**幂等覆盖写入**(传全集,Service 内 diff)。 @@ -183,10 +185,10 @@ public CommonResult> getAllProjectSimpleList() { #### 4. 边界守卫(Service 层) - **`assignUserProjects`**:若 `projectIds` 不包含用户当前 `ProjectContextHolder.getProjectId()`,**且调用者 `SecurityFrameworkUtils.getLoginUserId()` 就是 `userId` 本人**(即自己改自己的分配),抛 `USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT` 业务异常。 - - *注释要写清*:本守卫只阻止自己把自己踢出当前项目;管理员给别人改分配不受此影响(即使被改的用户当前正在访问某项目)。 + - _注释要写清_:本守卫只阻止自己把自己踢出当前项目;管理员给别人改分配不受此影响(即使被改的用户当前正在访问某项目)。 - **`assignProjectUsers`**: - - *superadmin 守卫*:若 `userIds` 计算出的"要移除的子集"中任何一个持有超管角色,拒绝。**不能用 `userId == 1` 判别**(不同租户的管理员 id 不一定是 1)。正确做法:注入 `PermissionService.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode())` 或类似的现有工具;yudao 里可查 `AdminUserService.isSuperAdmin()` 的实现并复用。 - - *自踢守卫*:若当前登录人 id 在被移除列表 **且** 当前 `ProjectContextHolder.getProjectId()` 等于目标 `projectId`,拒绝。 + - _superadmin 守卫_:若 `userIds` 计算出的"要移除的子集"中任何一个持有超管角色,拒绝。**不能用 `userId == 1` 判别**(不同租户的管理员 id 不一定是 1)。正确做法:注入 `PermissionService.hasAnyRoles(userId, RoleCodeEnum.SUPER_ADMIN.getCode())` 或类似的现有工具;yudao 里可查 `AdminUserService.isSuperAdmin()` 的实现并复用。 + - _自踢守卫_:若当前登录人 id 在被移除列表 **且** 当前 `ProjectContextHolder.getProjectId()` 等于目标 `projectId`,拒绝。 - **项目删除级联**: - `ProjectService.deleteProject` 需在**同一个 `@Transactional` 方法内**:先 `userProjectMapper.delete(lambdaQuery.eq(projectId, id))`(走 MyBatis Plus 自动软删标记 `deleted=1`),再 `projectMapper.deleteById(id)`。 - 项目 `disable` 不触发级联(用户关系保留,恢复启用后仍生效)。 @@ -247,11 +249,15 @@ export namespace SystemUserProjectApi { } } -export function assignUserProjects(data: SystemUserProjectApi.AssignUserProjectsReq) { +export function assignUserProjects( + data: SystemUserProjectApi.AssignUserProjectsReq, +) { return requestClient.post('/system/user-project/assign-user-projects', data); } -export function assignProjectUsers(data: SystemUserProjectApi.AssignProjectUsersReq) { +export function assignProjectUsers( + data: SystemUserProjectApi.AssignProjectUsersReq, +) { return requestClient.post('/system/user-project/assign-project-users', data); }