From 88cab42a9cceefcabcaecae369f2146d9cd26490 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 23 Apr 2026 14:48:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(system):=20=E7=94=A8=E6=88=B7-=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=91=E5=AE=9A=E7=AE=A1=E7=90=86=20API=20+=20?= =?UTF-8?q?=E9=A1=B6=E6=A0=8F=E9=A1=B9=E7=9B=AE=E4=B8=8B=E6=8B=89=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 UserProjectService/ServiceImpl/Controller:给用户分配项目、给项目分配成员 幂等覆盖写入(diff 出增删),参考 PermissionServiceImpl.assignUserRole 模式 - 自踢守卫:禁止用户把自己从当前正在访问的项目中移除 - 超管守卫:assignProjectUsers 拒绝移除持有超管角色的用户(用 RoleService.hasAnySuperAdmin 判别,非 userId==1) - ProjectController.simple-list 改为只返回"当前用户授权且启用"的项目(修 bug:原返回整租户启用项目,会让顶栏下拉看到无权访问的项目) - 新增 /system/project/all-simple-list:管理员分配场景的全量项目下拉,权限复用 system:project:query - ProjectService.deleteProject 加 @Transactional,同事务内级联软删 system_user_project - 新增两条菜单权限种子 SQL,parent_id 子查询动态定位: * system:user:assign-project * system:project:assign-user - 新增错误码 USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT / USER_PROJECT_CANNOT_REMOVE_SUPER_ADMIN 设计文档:docs/design/2026-04-23-user-project-binding.md(在前端仓库) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-04-23_user_project_permissions.sql | 59 ++++++ .../system/enums/ErrorCodeConstants.java | 2 + .../admin/project/ProjectController.java | 15 +- .../admin/project/UserProjectController.java | 73 ++++++++ .../UserProjectAssignProjectUsersReqVO.java | 26 +++ .../UserProjectAssignUserProjectsReqVO.java | 26 +++ .../service/project/ProjectService.java | 8 + .../service/project/ProjectServiceImpl.java | 21 ++- .../service/project/UserProjectService.java | 62 +++++++ .../project/UserProjectServiceImpl.java | 173 ++++++++++++++++++ 10 files changed, 463 insertions(+), 2 deletions(-) create mode 100644 sql/mysql/migrations/2026-04-23_user_project_permissions.sql create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/UserProjectController.java create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignProjectUsersReqVO.java create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignUserProjectsReqVO.java create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectService.java create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectServiceImpl.java diff --git a/sql/mysql/migrations/2026-04-23_user_project_permissions.sql b/sql/mysql/migrations/2026-04-23_user_project_permissions.sql new file mode 100644 index 00000000..a30f776b --- /dev/null +++ b/sql/mysql/migrations/2026-04-23_user_project_permissions.sql @@ -0,0 +1,59 @@ +-- ============================================================== +-- 用户-项目绑定 菜单权限种子 +-- feat/user-project-api | 2026-04-23 +-- -------------------------------------------------------------- +-- 新增两条按钮权限: +-- 1) system:user:assign-project —— 用户管理页"分配项目"按钮 +-- 2) system:project:assign-user —— 项目管理页"管理成员"按钮 +-- +-- parent_id 用子查询动态定位,避免不同环境 menu id 不同 +-- ============================================================== + +-- 1) 用户分配项目(挂在 用户管理 菜单下) +INSERT INTO system_menu + (name, permission, type, sort, parent_id, + path, icon, component, status, + creator, create_time, updater, update_time, deleted) +SELECT + '用户分配项目', + 'system:user:assign-project', + 3, -- 3 = 按钮 + 10, -- 排序值,靠后 + m.id, -- 父菜单 = 用户管理 + '', '', '', 0, -- 按钮不需要 path/icon/component;status=0 启用 + '1', NOW(), '1', NOW(), 0 +FROM system_menu m +WHERE m.permission = 'system:user:list' AND m.deleted = 0 + AND NOT EXISTS ( + SELECT 1 FROM system_menu x + WHERE x.permission = 'system:user:assign-project' AND x.deleted = 0 + ) +LIMIT 1; + +-- 2) 项目管理成员(挂在 项目管理 菜单下) +INSERT INTO system_menu + (name, permission, type, sort, parent_id, + path, icon, component, status, + creator, create_time, updater, update_time, deleted) +SELECT + '项目管理成员', + 'system:project:assign-user', + 3, + 10, + m.id, + '', '', '', 0, + '1', NOW(), '1', NOW(), 0 +FROM system_menu m +WHERE m.permission = 'system:project:query' AND m.deleted = 0 + AND NOT EXISTS ( + SELECT 1 FROM system_menu x + WHERE x.permission = 'system:project:assign-user' AND x.deleted = 0 + ) +LIMIT 1; + +-- -------------------------------------------------------------- +-- 部署后核对: +-- SELECT id, name, permission, parent_id FROM system_menu +-- WHERE permission IN ('system:user:assign-project','system:project:assign-user'); +-- 期望:2 行,parent_id 非 NULL 且分别指向"用户管理"和"项目管理"菜单 +-- -------------------------------------------------------------- diff --git a/viewsh-module-system/viewsh-module-system-api/src/main/java/com/viewsh/module/system/enums/ErrorCodeConstants.java b/viewsh-module-system/viewsh-module-system-api/src/main/java/com/viewsh/module/system/enums/ErrorCodeConstants.java index 3ba60773..cd91a3b3 100644 --- a/viewsh-module-system/viewsh-module-system-api/src/main/java/com/viewsh/module/system/enums/ErrorCodeConstants.java +++ b/viewsh-module-system/viewsh-module-system-api/src/main/java/com/viewsh/module/system/enums/ErrorCodeConstants.java @@ -177,5 +177,7 @@ public interface ErrorCodeConstants { ErrorCode PROJECT_NAME_DUPLICATE = new ErrorCode(1_002_030_002, "名字为【{}】的项目已存在"); ErrorCode PROJECT_CODE_DUPLICATE = new ErrorCode(1_002_030_003, "编码为【{}】的项目已存在"); ErrorCode PROJECT_NOT_AUTHORIZED = new ErrorCode(1_002_030_004, "您未被授权访问该项目"); + ErrorCode USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT = new ErrorCode(1_002_030_010, "不能将自己从当前正在访问的项目中移除"); + ErrorCode USER_PROJECT_CANNOT_REMOVE_SUPER_ADMIN = new ErrorCode(1_002_030_011, "不能从项目中移除超级管理员"); } diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/ProjectController.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/ProjectController.java index 2f0989a4..6b84cd20 100644 --- a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/ProjectController.java +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/ProjectController.java @@ -4,6 +4,7 @@ import com.viewsh.framework.common.enums.CommonStatusEnum; import com.viewsh.framework.common.pojo.CommonResult; import com.viewsh.framework.common.pojo.PageResult; import com.viewsh.framework.common.util.object.BeanUtils; +import com.viewsh.framework.security.core.util.SecurityFrameworkUtils; import com.viewsh.module.system.controller.admin.project.vo.ProjectPageReqVO; import com.viewsh.module.system.controller.admin.project.vo.ProjectRespVO; import com.viewsh.module.system.controller.admin.project.vo.ProjectSaveReqVO; @@ -72,9 +73,21 @@ public class ProjectController { } @GetMapping("/simple-list") - @Operation(summary = "获取项目精简信息列表", description = "只包含被开启的项目,用于下拉选择") + @Operation(summary = "获取当前用户授权的项目精简列表", + description = "只包含当前登录用户授权且启用的项目,用于顶栏项目切换器") @PreAuthorize("@ss.hasPermission('system:project:query')") public CommonResult> getProjectSimpleList() { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + List list = projectService.getAuthorizedEnabledProjects(userId); + return success(convertList(list, project -> + new ProjectRespVO().setId(project.getId()).setName(project.getName()).setCode(project.getCode()))); + } + + @GetMapping("/all-simple-list") + @Operation(summary = "获取本租户全部启用项目(管理员分配场景用)", + description = "不做用户授权过滤,供管理员在为别人分配项目的下拉里使用。权限点复用 system:project:query。") + @PreAuthorize("@ss.hasPermission('system:project:query')") + public CommonResult> getAllProjectSimpleList() { List list = projectService.getProjectListByStatus(CommonStatusEnum.ENABLE.getStatus()); return success(convertList(list, project -> new ProjectRespVO().setId(project.getId()).setName(project.getName()).setCode(project.getCode()))); diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/UserProjectController.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/UserProjectController.java new file mode 100644 index 00000000..061082ce --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/UserProjectController.java @@ -0,0 +1,73 @@ +package com.viewsh.module.system.controller.admin.project; + +import com.viewsh.framework.common.pojo.CommonResult; +import com.viewsh.module.system.controller.admin.project.vo.UserProjectAssignProjectUsersReqVO; +import com.viewsh.module.system.controller.admin.project.vo.UserProjectAssignUserProjectsReqVO; +import com.viewsh.module.system.service.project.UserProjectService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Set; + +import static com.viewsh.framework.common.pojo.CommonResult.success; + +/** + * 管理后台 - 用户与项目绑定管理 + * + * 提供两个视角的覆盖式分配: + * - 用户视角:/assign-user-projects 给某用户分配项目集合 + * - 项目视角:/assign-project-users 给某项目分配成员集合 + * + * @author lzh + */ +@Tag(name = "管理后台 - 用户与项目绑定") +@RestController +@RequestMapping("/system/user-project") +public class UserProjectController { + + @Resource + private UserProjectService userProjectService; + + @PostMapping("/assign-user-projects") + @Operation(summary = "给用户分配项目(覆盖写入)") + @PreAuthorize("@ss.hasPermission('system:user:assign-project')") + public CommonResult assignUserProjects(@Valid @RequestBody UserProjectAssignUserProjectsReqVO reqVO) { + userProjectService.assignUserProjects(reqVO.getUserId(), reqVO.getProjectIds()); + return success(true); + } + + @PostMapping("/assign-project-users") + @Operation(summary = "给项目分配成员(覆盖写入)") + @PreAuthorize("@ss.hasPermission('system:project:assign-user')") + public CommonResult assignProjectUsers(@Valid @RequestBody UserProjectAssignProjectUsersReqVO reqVO) { + userProjectService.assignProjectUsers(reqVO.getProjectId(), reqVO.getUserIds()); + return success(true); + } + + @GetMapping("/list-project-ids-by-user") + @Operation(summary = "查询某用户已绑定的项目编号集合") + @Parameter(name = "userId", description = "用户编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:user:assign-project')") + public CommonResult> getProjectIdsByUserId(@RequestParam("userId") Long userId) { + return success(userProjectService.getProjectIdsByUserId(userId)); + } + + @GetMapping("/list-user-ids-by-project") + @Operation(summary = "查询某项目下的用户编号集合") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "101") + @PreAuthorize("@ss.hasPermission('system:project:assign-user')") + public CommonResult> getUserIdsByProjectId(@RequestParam("projectId") Long projectId) { + return success(userProjectService.getUserIdsByProjectId(projectId)); + } + +} diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignProjectUsersReqVO.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignProjectUsersReqVO.java new file mode 100644 index 00000000..9a9ca3dc --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignProjectUsersReqVO.java @@ -0,0 +1,26 @@ +package com.viewsh.module.system.controller.admin.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Collections; +import java.util.Set; + +/** + * 管理后台 - 给项目分配成员(覆盖写入) Req VO + * + * @author lzh + */ +@Schema(description = "管理后台 - 给项目分配成员(覆盖写入) Req VO") +@Data +public class UserProjectAssignProjectUsersReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "用户编号集合(传空集即清空所有成员)", example = "11,12,13") + private Set userIds = Collections.emptySet(); + +} diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignUserProjectsReqVO.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignUserProjectsReqVO.java new file mode 100644 index 00000000..c5e9154f --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAssignUserProjectsReqVO.java @@ -0,0 +1,26 @@ +package com.viewsh.module.system.controller.admin.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Collections; +import java.util.Set; + +/** + * 管理后台 - 给用户分配项目(覆盖写入) Req VO + * + * @author lzh + */ +@Schema(description = "管理后台 - 给用户分配项目(覆盖写入) Req VO") +@Data +public class UserProjectAssignUserProjectsReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "项目编号集合(传空集即清空所有分配)", example = "1,2,3") + private Set projectIds = Collections.emptySet(); + +} diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectService.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectService.java index 9139a473..41aa030d 100644 --- a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectService.java +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectService.java @@ -110,4 +110,12 @@ public interface ProjectService { */ Long getDefaultProjectId(Long userId); + /** + * 获得指定用户"授权且启用"的项目列表(用于顶栏项目切换器的下拉数据源) + * + * @param userId 用户编号 + * @return 授权启用的项目列表 + */ + List getAuthorizedEnabledProjects(Long userId); + } diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectServiceImpl.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectServiceImpl.java index 3da910f3..733c6303 100644 --- a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectServiceImpl.java +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/ProjectServiceImpl.java @@ -16,8 +16,10 @@ import com.viewsh.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.util.Collections; import java.util.List; import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception; @@ -72,10 +74,15 @@ public class ProjectServiceImpl implements ProjectService { } @Override + @Transactional(rollbackFor = Exception.class) public void deleteProject(Long id) { // 校验存在 validateProjectExists(id); - // 删除 + // 级联软删用户-项目关联(同一事务内,任一失败全部回滚) + userProjectMapper.delete( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(UserProjectDO::getProjectId, id)); + // 删除项目 projectMapper.deleteById(id); } @@ -145,6 +152,18 @@ public class ProjectServiceImpl implements ProjectService { return CollectionUtils.convertList(userProjects, UserProjectDO::getProjectId); } + @Override + public List getAuthorizedEnabledProjects(Long userId) { + List authorizedIds = getAuthorizedProjectIds(userId); + if (CollUtil.isEmpty(authorizedIds)) { + return Collections.emptyList(); + } + List projects = projectMapper.selectByIds(authorizedIds); + return projects.stream() + .filter(p -> CommonStatusEnum.ENABLE.getStatus().equals(p.getStatus())) + .toList(); + } + @Override public Long getDefaultProjectId(Long userId) { List authorizedProjectIds = getAuthorizedProjectIds(userId); diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectService.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectService.java new file mode 100644 index 00000000..bc40ee59 --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectService.java @@ -0,0 +1,62 @@ +package com.viewsh.module.system.service.project; + +import java.util.Set; + +/** + * 用户-项目关联 Service 接口 + * + * 管理 {@link com.viewsh.module.system.dal.dataobject.project.UserProjectDO} 的增删查。 + * + * @author lzh + */ +public interface UserProjectService { + + /** + * 覆盖式分配某用户的项目集合 + * 传入全集,内部 diff 出新增和移除。 + * + * @param userId 用户编号 + * @param projectIds 期望的项目编号集合(传空集即清空该用户所有项目分配) + */ + void assignUserProjects(Long userId, Set projectIds); + + /** + * 覆盖式分配某项目的成员集合 + * 传入全集,内部 diff 出新增和移除。 + * + * @param projectId 项目编号 + * @param userIds 期望的用户编号集合(传空集即清空该项目所有成员) + */ + void assignProjectUsers(Long projectId, Set userIds); + + /** + * 查询某用户已绑定的项目编号集合 + * + * @param userId 用户编号 + * @return 项目编号集合 + */ + Set getProjectIdsByUserId(Long userId); + + /** + * 查询某项目下绑定的用户编号集合 + * + * @param projectId 项目编号 + * @return 用户编号集合 + */ + Set getUserIdsByProjectId(Long projectId); + + /** + * 批量清理项目下所有绑定(供项目删除时级联调用) + * + * @param projectId 项目编号 + */ + void deleteByProjectId(Long projectId); + + /** + * 批量清理用户的所有绑定(供用户删除时级联调用) + * + * @param userId 用户编号 + */ + void deleteByUserId(Long userId); + +} diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectServiceImpl.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectServiceImpl.java new file mode 100644 index 00000000..c6b22d4a --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/service/project/UserProjectServiceImpl.java @@ -0,0 +1,173 @@ +package com.viewsh.module.system.service.project; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.viewsh.framework.common.util.collection.CollectionUtils; +import com.viewsh.framework.security.core.util.SecurityFrameworkUtils; +import com.viewsh.framework.tenant.core.context.ProjectContextHolder; +import com.viewsh.module.system.dal.dataobject.project.UserProjectDO; +import com.viewsh.module.system.dal.mysql.project.UserProjectMapper; +import com.viewsh.module.system.service.permission.PermissionService; +import com.viewsh.module.system.service.permission.RoleService; +import jakarta.annotation.Resource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.viewsh.framework.common.util.collection.CollectionUtils.convertSet; +import static com.viewsh.module.system.enums.ErrorCodeConstants.USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT; +import static com.viewsh.module.system.enums.ErrorCodeConstants.USER_PROJECT_CANNOT_REMOVE_SUPER_ADMIN; + +/** + * 用户-项目关联 Service 实现 + * + * @author lzh + */ +@Service +@Validated +public class UserProjectServiceImpl implements UserProjectService { + + @Resource + private UserProjectMapper userProjectMapper; + + @Resource + @Lazy // 避免与 PermissionService 循环依赖 + private PermissionService permissionService; + + @Resource + @Lazy + private RoleService roleService; + + @Override + @Transactional(rollbackFor = Exception.class) + public void assignUserProjects(Long userId, Set projectIds) { + Set targetIds = CollUtil.emptyIfNull(projectIds); + + // 1. 查当前绑定 + Set dbIds = convertSet(userProjectMapper.selectListByUserId(userId), + UserProjectDO::getProjectId); + + // 2. 自踢守卫:如果调用者就是 userId 本人,且期望集合不包含当前正在访问的项目,拒绝 + Long currentLoginUserId = SecurityFrameworkUtils.getLoginUserId(); + Long currentProjectId = ProjectContextHolder.getProjectId(); + if (currentLoginUserId != null && currentLoginUserId.equals(userId) + && currentProjectId != null + && dbIds.contains(currentProjectId) + && !targetIds.contains(currentProjectId)) { + throw exception(USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT); + } + + // 3. 计算增删 + Collection toInsert = CollUtil.subtract(targetIds, dbIds); + Collection toDelete = CollUtil.subtract(dbIds, targetIds); + + // 4. 执行 + if (CollUtil.isNotEmpty(toInsert)) { + userProjectMapper.insertBatch(CollectionUtils.convertList(toInsert, projectId -> { + UserProjectDO entity = new UserProjectDO(); + entity.setUserId(userId); + entity.setProjectId(projectId); + return entity; + })); + } + if (CollUtil.isNotEmpty(toDelete)) { + userProjectMapper.delete(Wrappers.lambdaQuery() + .eq(UserProjectDO::getUserId, userId) + .in(UserProjectDO::getProjectId, toDelete)); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void assignProjectUsers(Long projectId, Set userIds) { + Set targetIds = CollUtil.emptyIfNull(userIds); + + // 1. 查当前成员 + Set dbIds = convertSet(userProjectMapper.selectListByProjectId(projectId), + UserProjectDO::getUserId); + + // 2. 算要移除的用户 + Collection toDelete = CollUtil.subtract(dbIds, targetIds); + + // 3. 超管守卫:被移除的用户里若有超管角色,拒绝 + if (CollUtil.isNotEmpty(toDelete)) { + for (Long uid : toDelete) { + Set userRoleIds = permissionService.getUserRoleIdListByUserId(uid); + if (roleService.hasAnySuperAdmin(userRoleIds)) { + throw exception(USER_PROJECT_CANNOT_REMOVE_SUPER_ADMIN); + } + } + } + + // 4. 自踢守卫:若当前登录人在被移除列表中,且当前项目就是目标项目,拒绝 + Long currentLoginUserId = SecurityFrameworkUtils.getLoginUserId(); + Long currentProjectId = ProjectContextHolder.getProjectId(); + if (currentLoginUserId != null && currentProjectId != null + && projectId.equals(currentProjectId) + && toDelete.contains(currentLoginUserId)) { + throw exception(USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT); + } + + // 5. 算要新增的用户 + Collection toInsert = CollUtil.subtract(targetIds, dbIds); + + // 6. 执行 + if (CollUtil.isNotEmpty(toInsert)) { + userProjectMapper.insertBatch(CollectionUtils.convertList(toInsert, userId -> { + UserProjectDO entity = new UserProjectDO(); + entity.setUserId(userId); + entity.setProjectId(projectId); + return entity; + })); + } + if (CollUtil.isNotEmpty(toDelete)) { + userProjectMapper.delete(Wrappers.lambdaQuery() + .eq(UserProjectDO::getProjectId, projectId) + .in(UserProjectDO::getUserId, toDelete)); + } + } + + @Override + public Set getProjectIdsByUserId(Long userId) { + if (userId == null) { + return Collections.emptySet(); + } + List list = userProjectMapper.selectListByUserId(userId); + return convertSet(list, UserProjectDO::getProjectId); + } + + @Override + public Set getUserIdsByProjectId(Long projectId) { + if (projectId == null) { + return Collections.emptySet(); + } + List list = userProjectMapper.selectListByProjectId(projectId); + return convertSet(list, UserProjectDO::getUserId); + } + + @Override + public void deleteByProjectId(Long projectId) { + if (projectId == null) { + return; + } + userProjectMapper.delete(Wrappers.lambdaQuery() + .eq(UserProjectDO::getProjectId, projectId)); + } + + @Override + public void deleteByUserId(Long userId) { + if (userId == null) { + return; + } + userProjectMapper.delete(Wrappers.lambdaQuery() + .eq(UserProjectDO::getUserId, userId)); + } + +}