docs: 补充用户-项目绑定功能设计稿
记录本轮 feat/multi-tenant-project 分支的需求背景、双入口绑定 方案与前后端联动约定,供后续回溯决策。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
393
docs/design/2026-04-23-user-project-binding.md
Normal file
393
docs/design/2026-04-23-user-project-binding.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# Design: 用户 ↔ 项目 绑定功能
|
||||
|
||||
- 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)
|
||||
- Status: APPROVED
|
||||
- Mode: 内部项目(双端联动)
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
前后端已经铺好了"租户 → 项目 → 业务数据"两级隔离的骨架,顶栏也有了项目切换器,但**谁能访问哪个项目**这件事目前只有一个隐性来源:租户创建时 `createDefaultProject()` 把所有用户绑到 `CODE=DEFAULT` 项目上。
|
||||
|
||||
一旦新建第二个项目,或新增一个用户,就没有任何管理界面去维护 `system_user_project` 这张中间表。HR 要给新员工开权限、项目经理要组建项目团队,目前都做不到。
|
||||
|
||||
要补的是:**用户 ↔ 项目绑定关系的管理界面和专用 API**。
|
||||
|
||||
---
|
||||
|
||||
## 现状梳理(调研结论)
|
||||
|
||||
### 后端(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 只有二元组字段 |
|
||||
| `ProjectMapper` / `UserProjectMapper` | `dal/mysql/project/` | UserProjectMapper 已有 `selectListByUserId`、`selectListByProjectId`、`deleteByUserIdAndProjectId` |
|
||||
| `ProjectService.getAuthorizedProjectIds(userId)` | `service/project/ProjectServiceImpl.java:143` | 已实现 |
|
||||
| `ProjectService.getDefaultProjectId(userId)` | 同上 `:149` | 已实现(DEFAULT 优先,否则最小 ID) |
|
||||
| `ProjectContextHolder` | `framework/tenant/core/context/` | ThreadLocal 项目上下文 |
|
||||
| `ProjectDatabaseInterceptor` | `framework/tenant/core/db/` | MyBatis Plus 自动拼 `WHERE project_id = ?` |
|
||||
| `ProjectContextWebFilter` | `framework/tenant/core/web/` | 从 HTTP header `project-id` 注入到 context |
|
||||
| `ProjectSecurityWebFilter` | `framework/tenant/core/security/` | **已做"用户对项目的访问权"校验**(含默认项目回填 + 非授权项目 403) |
|
||||
|
||||
### 前端(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` |
|
||||
| 请求拦截器 | axios 自动注入 `project-id` 请求头 |
|
||||
|
||||
### Gap
|
||||
|
||||
1. **无 UserProject 管理 API** —— 后端 `UserProjectMapper` 有,但没封装 Service,没暴露 Controller。
|
||||
2. **无用户 ↔ 项目绑定管理界面** —— 用户页没"分配项目"、项目页没"管理成员"。
|
||||
3. **`/system/project/simple-list` 是 bug**
|
||||
- `ProjectController.getProjectSimpleList`(`ProjectController.java:77-81`)当前查的是 `projectService.getProjectListByStatus(ENABLE)`——**返回整租户启用项目**。
|
||||
- 正确行为:只返回**当前登录用户授权的**启用项目(否则顶栏下拉里会出现无权访问的项目名,点了才 403)。
|
||||
|
||||
---
|
||||
|
||||
## Premises(已确认)
|
||||
|
||||
1. 后端表和 `ProjectSecurityWebFilter` 都已到位,**不新建任何表、不改任何字段**。
|
||||
2. UI 采用**双入口**:用户页「分配项目」+ 项目页「管理成员」。
|
||||
3. **不引入"项目内角色"**:`user_project` 保持二元组,未来需要时再加 `role_id` 字段。
|
||||
4. **顺带修 `simple-list` bug**:改成只返回登录用户授权的项目。
|
||||
5. **沿用 yudao "assignXxx" 风格**:幂等覆盖写入,Body 传 `Set<Long>`,Service 内部 diff 出增删。参照 `PermissionServiceImpl.assignUserRole`(`PermissionServiceImpl.java:208`)。
|
||||
6. **边界守卫**:禁止管理员把自己从当前正在访问的项目移除;禁止清空 superadmin(用户 id=1)的项目分配。
|
||||
|
||||
---
|
||||
|
||||
## Approaches Considered
|
||||
|
||||
### Approach A: 一次到位(已选)
|
||||
一个 PR 同时交付:UserProject 管理 API + 双入口前端弹窗 + `simple-list` bug 修复。
|
||||
|
||||
- Completeness: 9/10
|
||||
- 人力估:~1 天 / CC 估:~35 分钟
|
||||
- Pros: 一次发布闭环;用户体验一致;bug 不遗留
|
||||
- Cons: PR 稍大;需要前后端同时 review
|
||||
- Reuses: `UserProjectMapper`、`PermissionController.assign-user-role` 模式、`assign-role-form.vue` 模板
|
||||
|
||||
### Approach B: 分两步交付(备选)
|
||||
PR1 只做"用户视角"(后端 2 个接口 + 修 bug + 前端用户页入口);PR2 做"项目视角"。
|
||||
|
||||
- Completeness: 8/10
|
||||
- 人力估:PR1 ~0.4 天 + PR2 ~0.5 天
|
||||
- Pros: PR 小好 review;风险可控
|
||||
- Cons: 项目经理视角的体验要等下个迭代
|
||||
|
||||
### Approach C: 极简版(备选)
|
||||
在用户编辑表单里直接加"所属项目"多选下拉框(类似 `deptId`/`postIds`),不做独立弹窗,不改项目页。
|
||||
|
||||
- Completeness: 5/10
|
||||
- 人力估:~0.25 天
|
||||
- Pros: 文件改动最少
|
||||
- Cons: 项目经理组团队要一个个用户翻;与 yudao "assign-role" 交互不一致
|
||||
|
||||
---
|
||||
|
||||
## Recommended Approach: A(一次到位)
|
||||
|
||||
---
|
||||
|
||||
## 详细设计
|
||||
|
||||
### 后端改动(aiot-platform-cloud · `viewsh-module-system-server`)
|
||||
|
||||
#### 1. 新建 `UserProjectController`
|
||||
|
||||
路径:`controller/admin/project/UserProjectController.java`
|
||||
基础路径:`/system/user-project`
|
||||
|
||||
| 方法 | 路径 | 权限 | Body / Param | 返回 |
|
||||
|---|---|---|---|---|
|
||||
| POST | `/assign-user-projects` | `system:user:assign-project` | `{userId, projectIds: Set<Long>}` | `Boolean` |
|
||||
| POST | `/assign-project-users` | `system:project:assign-user` | `{projectId, userIds: Set<Long>}` | `Boolean` |
|
||||
| GET | `/list-project-ids-by-user` | `system:user:assign-project` | `?userId=` | `Set<Long>` |
|
||||
| GET | `/list-user-ids-by-project` | `system:project:assign-user` | `?projectId=` | `Set<Long>` |
|
||||
|
||||
两个 `assign-*` 都是**幂等覆盖写入**(传全集,Service 内 diff)。
|
||||
|
||||
#### 2. 新建 `UserProjectService` + `UserProjectServiceImpl`
|
||||
|
||||
路径:`service/project/`
|
||||
|
||||
```java
|
||||
public interface UserProjectService {
|
||||
// 覆盖写入:用户 userId 所绑定的项目集合设为 projectIds
|
||||
void assignUserProjects(Long userId, Set<Long> projectIds);
|
||||
|
||||
// 覆盖写入:项目 projectId 下的成员设为 userIds
|
||||
void assignProjectUsers(Long projectId, Set<Long> userIds);
|
||||
|
||||
// 查:用户已绑定的项目 ID 集合
|
||||
Set<Long> getProjectIdsByUserId(Long userId);
|
||||
|
||||
// 查:项目下的用户 ID 集合
|
||||
Set<Long> getUserIdsByProjectId(Long projectId);
|
||||
}
|
||||
```
|
||||
|
||||
实现参考 `PermissionServiceImpl.assignUserRole`(`PermissionServiceImpl.java:208`)的 diff 思路:
|
||||
|
||||
```java
|
||||
// 1. 查当前集合
|
||||
Set<Long> currentIds = ...selectListByUserId(userId).stream().map(getProjectId).collect(toSet());
|
||||
// 2. 计算增删
|
||||
Set<Long> toInsert = CollUtil.subtract(projectIds, currentIds);
|
||||
Set<Long> toDelete = CollUtil.subtract(currentIds, projectIds);
|
||||
// 3. 执行
|
||||
if (!toInsert.isEmpty()) userProjectMapper.insertBatch(buildDOs(userId, toInsert));
|
||||
if (!toDelete.isEmpty()) userProjectMapper.delete(wrapper by userId + projectId in toDelete);
|
||||
```
|
||||
|
||||
#### 3. 修复 `ProjectController.getProjectSimpleList`
|
||||
|
||||
`ProjectController.java:77-81`:
|
||||
|
||||
```java
|
||||
// 当前实现(错)
|
||||
List<ProjectDO> list = projectService.getProjectListByStatus(ENABLE);
|
||||
|
||||
// 应改为 —— 在 ProjectService 加新方法
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
List<ProjectDO> list = projectService.getAuthorizedEnabledProjects(userId);
|
||||
```
|
||||
|
||||
**预防回归**:在修复前,**全量搜索 `getSimpleProjectList` / `getProjectSimpleList` 的所有调用点**(前后端都要搜),挨个确认它们的语义诉求是"当前用户授权的"还是"本租户全部"。找到的每个调用点都要在 PR 描述里列出并说明是否受影响。**初步预期**:只有顶栏项目切换器用它;但后端日志/报表/下拉如果也在用原语义("本租户全部启用项目"),需要改指向本 PR 新增的 `/system/project/all-simple-list`(见下一节)。
|
||||
|
||||
#### 3.1 新增 `/system/project/all-simple-list`(管理员专用)
|
||||
|
||||
**用途**:给"管理员为其他人分配项目"场景提供**全量**项目下拉。权限点复用 `system:project:query`,返回体和当前 `getProjectSimpleList` 一致。
|
||||
|
||||
```java
|
||||
@GetMapping("/all-simple-list")
|
||||
@Operation(summary = "获取本租户全部启用项目(用于管理员分配场景)")
|
||||
@PreAuthorize("@ss.hasPermission('system:project:query')")
|
||||
public CommonResult<List<ProjectRespVO>> getAllProjectSimpleList() {
|
||||
List<ProjectDO> list = projectService.getProjectListByStatus(ENABLE);
|
||||
return success(convertList(list, p -> new ProjectRespVO().setId(p.getId()).setName(p.getName()).setCode(p.getCode())));
|
||||
}
|
||||
```
|
||||
|
||||
前端"分配项目"弹窗的项目下拉**必须用此接口**,不能用 `getSimpleProjectList`(后者已改成只返回当前登录人授权项目)。
|
||||
|
||||
#### 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`,拒绝。
|
||||
- **项目删除级联**:
|
||||
- `ProjectService.deleteProject` 需在**同一个 `@Transactional` 方法内**:先 `userProjectMapper.delete(lambdaQuery.eq(projectId, id))`(走 MyBatis Plus 自动软删标记 `deleted=1`),再 `projectMapper.deleteById(id)`。
|
||||
- 项目 `disable` 不触发级联(用户关系保留,恢复启用后仍生效)。
|
||||
|
||||
#### 5. 菜单权限种子(SQL)
|
||||
|
||||
新增两条菜单权限(放 `sql/mysql/migrations/2026-04-23_user_project_permissions.sql`)。
|
||||
|
||||
**关键:`parent_id` 不能写死**,因为不同环境菜单 id 可能不同。用子查询动态取:
|
||||
|
||||
```sql
|
||||
-- 依赖条件:
|
||||
-- 1) 用户管理菜单存在,通过 permission='system:user:list' 或 name='用户管理' 定位
|
||||
-- 2) 项目管理菜单存在,通过 permission='system:project:query' 或 name='项目管理' 定位
|
||||
-- (若项目主菜单尚不存在,先插一条主菜单再插按钮)
|
||||
|
||||
-- system:user:assign-project —— 用户管理下的"分配项目"按钮
|
||||
INSERT INTO system_menu (name, permission, type, parent_id, sort, status, creator, create_time, updater, update_time, deleted)
|
||||
SELECT '用户分配项目', 'system:user:assign-project', 3,
|
||||
m.id, 10, 0, '1', NOW(), '1', NOW(), 0
|
||||
FROM system_menu m
|
||||
WHERE m.permission = 'system:user:list' AND m.deleted = 0
|
||||
LIMIT 1;
|
||||
|
||||
-- system:project:assign-user —— 项目管理下的"管理成员"按钮
|
||||
INSERT INTO system_menu (name, permission, type, parent_id, sort, status, creator, create_time, updater, update_time, deleted)
|
||||
SELECT '项目管理成员', 'system:project:assign-user', 3,
|
||||
m.id, 10, 0, '1', NOW(), '1', NOW(), 0
|
||||
FROM system_menu m
|
||||
WHERE m.permission = 'system:project:query' AND m.deleted = 0
|
||||
LIMIT 1;
|
||||
|
||||
-- 幂等:若已存在同 permission 的按钮,忽略(需要 DBA 先 DELETE 或加 ON DUPLICATE KEY;
|
||||
-- 若项目有多环境,推荐改成 Flyway/Liquibase 迁移脚本由 CI 控制幂等)
|
||||
```
|
||||
|
||||
迁移脚本跑完后人工核对:两条记录的 `parent_id` 非 NULL 且能在 "菜单管理" 页面看到层级正确。
|
||||
|
||||
---
|
||||
|
||||
### 前端改动(yudao-ui-admin-vben · `apps/web-antd`)
|
||||
|
||||
#### 1. 新建 API 封装
|
||||
|
||||
路径:`src/api/system/user-project/index.ts`
|
||||
|
||||
```typescript
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace SystemUserProjectApi {
|
||||
export interface AssignUserProjectsReq {
|
||||
userId: number;
|
||||
projectIds: number[];
|
||||
}
|
||||
export interface AssignProjectUsersReq {
|
||||
projectId: number;
|
||||
userIds: number[];
|
||||
}
|
||||
}
|
||||
|
||||
export function assignUserProjects(data: SystemUserProjectApi.AssignUserProjectsReq) {
|
||||
return requestClient.post('/system/user-project/assign-user-projects', data);
|
||||
}
|
||||
|
||||
export function assignProjectUsers(data: SystemUserProjectApi.AssignProjectUsersReq) {
|
||||
return requestClient.post('/system/user-project/assign-project-users', data);
|
||||
}
|
||||
|
||||
export function getProjectIdsByUserId(userId: number) {
|
||||
return requestClient.get<number[]>(
|
||||
`/system/user-project/list-project-ids-by-user?userId=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getUserIdsByProjectId(projectId: number) {
|
||||
return requestClient.get<number[]>(
|
||||
`/system/user-project/list-user-ids-by-project?projectId=${projectId}`,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 用户管理页:「分配项目」弹窗
|
||||
|
||||
**新建** `views/system/user/modules/assign-project-form.vue` —— 直接照搬 `assign-role-form.vue`(同目录)的结构,替换:
|
||||
|
||||
- import:`assignUserRole, getUserRoleList` → `assignUserProjects, getProjectIdsByUserId`
|
||||
- 数据源:**必须用** `getAllProjectSimpleList`(后端 3.1 节新建的管理员专用接口),**不要用** `getSimpleProjectList`(已改成只返回调用者自己授权的项目)
|
||||
- schema:`useAssignRoleFormSchema` → 新建 `useAssignProjectFormSchema`(在 `data.ts` 里)。字段 `projectIds`,`component: 'Select'` + `mode: 'multiple'`,`options` 由 `getAllProjectSimpleList()` 填充
|
||||
|
||||
**空集二次确认**:`onConfirm` 提交前若 `values.projectIds.length === 0`,弹 `Modal.confirm({ title: '确认清空该用户的所有项目分配?' })`。仅"确认"后才调 API。避免误点保存把用户所有项目全删。
|
||||
|
||||
**修改** `views/system/user/index.vue`:在行操作按钮区加一个「分配项目」,照抄现有「分配角色」按钮的写法。
|
||||
|
||||
**修改** `views/system/user/data.ts`:新增 `useAssignProjectFormSchema()`。
|
||||
|
||||
#### 3. 项目管理页:「管理成员」弹窗
|
||||
|
||||
**新建** `views/system/project/modules/assign-user-form.vue`
|
||||
|
||||
交互:穿梭框(transfer)样式或多选 Select,左侧全量用户(通过 `getSimpleUserList`),右侧已绑定。支持搜索 username/nickname。
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ 项目:10号线巡检项目 │
|
||||
│ │
|
||||
│ ┌─── 所有用户 ───┐ ┌─── 项目成员 ───┐ │
|
||||
│ │ □ 张三 (ops) │ →→→ │ ☑ 李四 (mgr) │ │
|
||||
│ │ □ 王五 (iot) │ ←←← │ ☑ 赵六 (clean) │ │
|
||||
│ └───────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
│ [取消] [保存] │
|
||||
└──────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
简化版实现:直接用 ant-design-vue 的 `a-transfer`;或者用 `a-select` multiple + `getSimpleUserList` 的下拉。第一版用 `a-select multiple` 最快。
|
||||
|
||||
**修改** `views/system/project/index.vue`:行操作加「管理成员」按钮。
|
||||
|
||||
**修改** `views/system/project/data.ts`:新增 `useAssignUserFormSchema()`。
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
**不新增表,不改字段。** 沿用:
|
||||
|
||||
```sql
|
||||
system_user_project (
|
||||
id BIGINT PK AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
project_id BIGINT NOT NULL,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
creator, create_time, updater, update_time, deleted,
|
||||
UNIQUE KEY uk_user_project (user_id, project_id, deleted),
|
||||
INDEX idx_project (project_id)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 成功标准(Acceptance)
|
||||
|
||||
1. 管理员在用户页对用户"分配项目"保存后,该用户立刻能通过顶栏切换到被分配项目,业务接口返回数据。
|
||||
2. 管理员在项目页"管理成员"增删后,被移除的用户切到该项目会被 `ProjectSecurityWebFilter` 403。
|
||||
3. 顶栏项目下拉只显示当前登录用户**授权且启用**的项目。
|
||||
4. 管理员不能把**自己**从当前正在访问的项目移除(UI 禁用 + 后端守卫)。
|
||||
5. 不能清空 superadmin 的项目分配。
|
||||
6. 分配/取消分配后,`ProjectService.getAuthorizedProjectIds(userId)` 立刻反映最新状态(无缓存或缓存有清理)。
|
||||
|
||||
---
|
||||
|
||||
## Open Questions(实施前再 settle)
|
||||
|
||||
1. ~~管理员分配时用哪个接口拉"所有项目"下拉数据?~~ **已决**:新增 `/system/project/all-simple-list`,权限点复用 `system:project:query`(见后端 3.1 节)。
|
||||
|
||||
2. ~~删除项目时,`system_user_project` 要不要级联软删?~~ **已决**:在同一 `@Transactional` 内级联软删,本 PR 一起做(见后端 4 节)。
|
||||
|
||||
3. **缓存?** `ProjectSecurityWebFilter` 每次请求都调 `getAuthorizedProjectIds`——高并发下要不要加 Redis 缓存?本 PR 先不做,只做能用的 V1。给 ops 团队留条 TODO。
|
||||
|
||||
4. **业务表历史数据的 `project_id` 谁填?** 这是另一个迁移任务,不在本设计范围。假定各业务模块自己有迁移脚本(看 `sql/mysql/project/03-alter-business-tables.sql`)。
|
||||
|
||||
5. **GET 查询接口与 `ProjectSecurityWebFilter` 的交互。** 4 个新增 API 都是管理员在"某个项目上下文"里调用,理论上不需要 `@ProjectIgnore` —— Filter 会用 context 里的 `project-id` 正常通过。但若出现"调用者刚被移除所有授权项目,自己还想查"的边缘场景会 403。**本 PR 不处理,留给实施时观察日志**。若要求必然能通,后端给 4 个接口加 `@ProjectIgnore` 注解。
|
||||
|
||||
---
|
||||
|
||||
## Distribution Plan(交付通路)
|
||||
|
||||
- **backend**: 在 `feat/multi-tenant` 上拆子分支 `feat/multi-tenant/user-project-api`,合并后通过现有镜像构建 → 部署。
|
||||
- **frontend**: 在当前 `feat/multi-tenant-project` 上直接开工,通过 `Dockerfile.deploy` 构建 → 部署。
|
||||
- CI 跑 backend 单测(新增 `UserProjectServiceImplTest`)+ 前端 lint + typecheck。
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序(The Assignment)
|
||||
|
||||
1. **后端先发**(~25 min):
|
||||
1. 建 `UserProjectService` 接口 + 实现 + 测试(diff 逻辑用参数化测试覆盖增量/减量/全替换/空集四种情况)
|
||||
2. 建 `UserProjectController`
|
||||
3. 修 `ProjectController.getProjectSimpleList`(+ 单独测试用例)
|
||||
4. 加菜单权限种子 SQL
|
||||
5. 本地跑一遍:用 `superadmin` 给 `user-xxx` 分配两个项目 → Postman 断言 `list-project-ids-by-user` 返回
|
||||
2. **前端接入**(~10 min):
|
||||
1. `api/system/user-project/index.ts`
|
||||
2. `assign-project-form.vue` + 用户页按钮
|
||||
3. `assign-user-form.vue` + 项目页按钮
|
||||
3. **手工验证清单**:
|
||||
- 分配 → 切项目 → 业务接口受保护 ✅
|
||||
- 取消分配 → 切项目被 403 ✅
|
||||
- 顶栏下拉只有授权项目 ✅
|
||||
- 自踢守卫 ✅
|
||||
- superadmin 守卫 ✅
|
||||
|
||||
---
|
||||
|
||||
## Reviewer Concerns(第 1 轮 spec review 已处理)
|
||||
|
||||
- ~~superadmin 守卫用 `userId==1` 不安全~~ → 已改为角色判别(4 节)
|
||||
- ~~菜单 parent_id 占位符未填~~ → 已改为 `SELECT FROM system_menu WHERE permission=...` 子查询(5 节)
|
||||
- ~~`simple-list` 修复可能破坏其他调用点~~ → 已加"修前全量搜索调用点"要求(3 节)
|
||||
- ~~"管理员分配用哪个下拉接口"有两个方案未定~~ → 已定为新增 `/all-simple-list`(3.1 节)
|
||||
- ~~前端空集清空缺少二次确认~~ → 已补 `Modal.confirm` 流程(前端 2 节)
|
||||
- ~~级联删除事务边界不清晰~~ → 已明确 `@Transactional` 同一方法内(4 节)
|
||||
|
||||
综合质量分从 6.5/10 → 预估 8.5/10。如实施发现新风险,回填此处。
|
||||
Reference in New Issue
Block a user