From 5dbc6c5e79238cdf1d14ccaece72133f6882c250 Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 23 Apr 2026 15:48:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(system):=20=E8=B6=85=E7=AE=A1=E7=BB=95?= =?UTF-8?q?=E8=BF=87=20user=5Fproject=20+=20=E9=A1=B9=E7=9B=AE=E6=88=90?= =?UTF-8?q?=E5=91=98=E5=88=86=E9=A1=B5/=E5=A2=9E=E9=87=8F=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端为配合前端"项目管理成员"从 Modal 改 Drawer 重构接口: - ProjectServiceImpl.getAuthorizedProjectIds 新增超管分支: 若 hasAnySuperAdmin(userRoleIds) 成立,直接返回本租户全部项目 ID 连带影响 getAuthorizedEnabledProjects / getDefaultProjectId / ProjectSecurityWebFilter.authorizedProjectIds.contains 全部自动生效 - 新增 UserProjectService 三个方法: * getProjectUserPage(reqVO) 分页返回成员 AdminUserDO,过滤超管 * addProjectUsers(projectId, userIds) 增量添加,已在的用户跳过 * removeProjectUser(projectId, userId) 单删,带超管/自踢守卫 - 新增 Controller 三个端点: * GET /system/user-project/project-user-page * POST /system/user-project/add-project-users * DELETE /system/user-project/remove-project-user - 新增 VO:UserProjectPageReqVO / UserProjectAddProjectUsersReqVO - 权限点沿用 system:project:assign-user Co-Authored-By: Claude Opus 4.7 (1M context) --- .../admin/project/UserProjectController.java | 35 ++++++ .../vo/UserProjectAddProjectUsersReqVO.java | 27 +++++ .../project/vo/UserProjectPageReqVO.java | 28 +++++ .../service/project/ProjectServiceImpl.java | 31 ++++++ .../service/project/UserProjectService.java | 29 +++++ .../project/UserProjectServiceImpl.java | 100 ++++++++++++++++++ 6 files changed, 250 insertions(+) create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAddProjectUsersReqVO.java create mode 100644 viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectPageReqVO.java 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 index 061082ce..420219fc 100644 --- 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 @@ -1,8 +1,14 @@ package com.viewsh.module.system.controller.admin.project; 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.module.system.controller.admin.project.vo.UserProjectAddProjectUsersReqVO; 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.controller.admin.project.vo.UserProjectPageReqVO; +import com.viewsh.module.system.controller.admin.user.vo.user.UserRespVO; +import com.viewsh.module.system.dal.dataobject.user.AdminUserDO; import com.viewsh.module.system.service.project.UserProjectService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -10,6 +16,7 @@ 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.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -70,4 +77,32 @@ public class UserProjectController { return success(userProjectService.getUserIdsByProjectId(projectId)); } + @GetMapping("/project-user-page") + @Operation(summary = "分页查询项目成员", description = "自动过滤超级管理员;支持按 username/nickname/mobile 模糊搜索") + @PreAuthorize("@ss.hasPermission('system:project:assign-user')") + public CommonResult> getProjectUserPage(@Valid UserProjectPageReqVO reqVO) { + PageResult page = userProjectService.getProjectUserPage(reqVO); + return success(BeanUtils.toBean(page, UserRespVO.class)); + } + + @PostMapping("/add-project-users") + @Operation(summary = "增量给项目添加成员", + description = "已经是成员的用户跳过,只插入新成员;不影响已有绑定") + @PreAuthorize("@ss.hasPermission('system:project:assign-user')") + public CommonResult addProjectUsers(@Valid @RequestBody UserProjectAddProjectUsersReqVO reqVO) { + userProjectService.addProjectUsers(reqVO.getProjectId(), reqVO.getUserIds()); + return success(true); + } + + @DeleteMapping("/remove-project-user") + @Operation(summary = "从项目中移除单个成员") + @Parameter(name = "projectId", description = "项目编号", required = true, example = "101") + @Parameter(name = "userId", description = "用户编号", required = true, example = "11") + @PreAuthorize("@ss.hasPermission('system:project:assign-user')") + public CommonResult removeProjectUser(@RequestParam("projectId") Long projectId, + @RequestParam("userId") Long userId) { + userProjectService.removeProjectUser(projectId, userId); + return success(true); + } + } diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAddProjectUsersReqVO.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAddProjectUsersReqVO.java new file mode 100644 index 00000000..f04838cd --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectAddProjectUsersReqVO.java @@ -0,0 +1,27 @@ +package com.viewsh.module.system.controller.admin.project.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.Set; + +/** + * 管理后台 - 增量给项目添加成员 Req VO + * + * @author lzh + */ +@Schema(description = "管理后台 - 增量给项目添加成员 Req VO") +@Data +public class UserProjectAddProjectUsersReqVO { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "要新增的用户编号集合", requiredMode = Schema.RequiredMode.REQUIRED, example = "11,12,13") + @NotEmpty(message = "用户编号集合不能为空") + private Set userIds; + +} diff --git a/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectPageReqVO.java b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectPageReqVO.java new file mode 100644 index 00000000..bec8c4c1 --- /dev/null +++ b/viewsh-module-system/viewsh-module-system-server/src/main/java/com/viewsh/module/system/controller/admin/project/vo/UserProjectPageReqVO.java @@ -0,0 +1,28 @@ +package com.viewsh.module.system.controller.admin.project.vo; + +import com.viewsh.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 管理后台 - 项目成员分页查询 Req VO + * + * @author lzh + */ +@Schema(description = "管理后台 - 项目成员分页查询 Req VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class UserProjectPageReqVO extends PageParam { + + @Schema(description = "项目编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "101") + @NotNull(message = "项目编号不能为空") + private Long projectId; + + @Schema(description = "关键字(按 username / nickname / mobile 模糊)", example = "张三") + private String keyword; + +} 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 733c6303..2da0a696 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 @@ -12,6 +12,8 @@ import com.viewsh.module.system.dal.dataobject.project.UserProjectDO; import com.viewsh.module.system.dal.dataobject.user.AdminUserDO; import com.viewsh.module.system.dal.mysql.project.ProjectMapper; 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 com.viewsh.module.system.service.user.AdminUserService; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; @@ -21,6 +23,7 @@ import org.springframework.validation.annotation.Validated; 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.module.system.enums.ErrorCodeConstants.*; @@ -44,6 +47,14 @@ public class ProjectServiceImpl implements ProjectService { @Lazy // 延迟,避免循环依赖报错 private AdminUserService userService; + @Resource + @Lazy + private PermissionService permissionService; + + @Resource + @Lazy + private RoleService roleService; + @Override public Long createProject(ProjectSaveReqVO createReqVO) { // 校验项目名称是否重复 @@ -148,10 +159,30 @@ public class ProjectServiceImpl implements ProjectService { @Override public List getAuthorizedProjectIds(Long userId) { + // 超管绕过 user_project 绑定校验:直接返回当前租户下所有项目 ID + // 与 TenantSecurityWebFilter 对超管的处理语义一致 + if (isSuperAdmin(userId)) { + return CollectionUtils.convertList(projectMapper.selectList(), ProjectDO::getId); + } List userProjects = userProjectMapper.selectListByUserId(userId); return CollectionUtils.convertList(userProjects, UserProjectDO::getProjectId); } + /** + * 判断用户是否为超级管理员 + * 查角色列表 → hasAnySuperAdmin 判别 + */ + private boolean isSuperAdmin(Long userId) { + if (userId == null) { + return false; + } + Set roleIds = permissionService.getUserRoleIdListByUserId(userId); + if (CollUtil.isEmpty(roleIds)) { + return false; + } + return roleService.hasAnySuperAdmin(roleIds); + } + @Override public List getAuthorizedEnabledProjects(Long userId) { List authorizedIds = 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 index bc40ee59..805ee66a 100644 --- 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 @@ -1,5 +1,9 @@ package com.viewsh.module.system.service.project; +import com.viewsh.framework.common.pojo.PageResult; +import com.viewsh.module.system.controller.admin.project.vo.UserProjectPageReqVO; +import com.viewsh.module.system.dal.dataobject.user.AdminUserDO; + import java.util.Set; /** @@ -59,4 +63,29 @@ public interface UserProjectService { */ void deleteByUserId(Long userId); + /** + * 分页查询项目的成员列表,自动过滤超级管理员 + * 超管通过角色天然拥有所有项目,不应该出现在成员管理界面 + * + * @param reqVO 分页查询参数 + * @return 用户分页结果(返回 AdminUserDO 便于前端渲染用户名/昵称/部门) + */ + PageResult getProjectUserPage(UserProjectPageReqVO reqVO); + + /** + * 增量把一组用户加入到项目(已在的用户跳过,不影响现有绑定) + * + * @param projectId 项目编号 + * @param userIds 要新增的用户编号集合 + */ + void addProjectUsers(Long projectId, Set userIds); + + /** + * 从项目中移除单个成员(带超管/自踢守卫) + * + * @param projectId 项目编号 + * @param userId 用户编号 + */ + void removeProjectUser(Long projectId, 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 index c6b22d4a..e6b5017f 100644 --- 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 @@ -1,12 +1,18 @@ package com.viewsh.module.system.service.project; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.viewsh.framework.common.pojo.PageResult; import com.viewsh.framework.common.util.collection.CollectionUtils; +import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX; import com.viewsh.framework.security.core.util.SecurityFrameworkUtils; import com.viewsh.framework.tenant.core.context.ProjectContextHolder; +import com.viewsh.module.system.controller.admin.project.vo.UserProjectPageReqVO; import com.viewsh.module.system.dal.dataobject.project.UserProjectDO; +import com.viewsh.module.system.dal.dataobject.user.AdminUserDO; import com.viewsh.module.system.dal.mysql.project.UserProjectMapper; +import com.viewsh.module.system.dal.mysql.user.AdminUserMapper; import com.viewsh.module.system.service.permission.PermissionService; import com.viewsh.module.system.service.permission.RoleService; import jakarta.annotation.Resource; @@ -15,6 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -45,6 +52,9 @@ public class UserProjectServiceImpl implements UserProjectService { @Lazy private RoleService roleService; + @Resource + private AdminUserMapper userMapper; + @Override @Transactional(rollbackFor = Exception.class) public void assignUserProjects(Long userId, Set projectIds) { @@ -170,4 +180,94 @@ public class UserProjectServiceImpl implements UserProjectService { .eq(UserProjectDO::getUserId, userId)); } + @Override + public PageResult getProjectUserPage(UserProjectPageReqVO reqVO) { + // 1. 查项目下所有 userIds + Set memberIds = convertSet( + userProjectMapper.selectListByProjectId(reqVO.getProjectId()), + UserProjectDO::getUserId); + if (CollUtil.isEmpty(memberIds)) { + return PageResult.empty(); + } + + // 2. 过滤超管(超管靠角色天然拥有所有项目,不应出现在成员列表) + List filteredIds = new ArrayList<>(memberIds.size()); + for (Long uid : memberIds) { + if (!isSuperAdmin(uid)) { + filteredIds.add(uid); + } + } + if (CollUtil.isEmpty(filteredIds)) { + return PageResult.empty(); + } + + // 3. 按 ids + keyword 分页查 AdminUserDO + LambdaQueryWrapperX wrapper = new LambdaQueryWrapperX() + .in(AdminUserDO::getId, filteredIds); + if (StrUtil.isNotBlank(reqVO.getKeyword())) { + String kw = reqVO.getKeyword(); + wrapper.and(q -> q.like(AdminUserDO::getUsername, kw) + .or().like(AdminUserDO::getNickname, kw) + .or().like(AdminUserDO::getMobile, kw)); + } + wrapper.orderByDesc(AdminUserDO::getId); + return userMapper.selectPage(reqVO, wrapper); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void addProjectUsers(Long projectId, Set userIds) { + Set target = CollUtil.emptyIfNull(userIds); + if (target.isEmpty()) { + return; + } + // 查当前已绑定,避免重复插入 + Set existing = convertSet( + userProjectMapper.selectListByProjectId(projectId), + UserProjectDO::getUserId); + Collection toInsert = CollUtil.subtract(target, existing); + if (CollUtil.isEmpty(toInsert)) { + return; + } + userProjectMapper.insertBatch(CollectionUtils.convertList(toInsert, uid -> { + UserProjectDO entity = new UserProjectDO(); + entity.setUserId(uid); + entity.setProjectId(projectId); + return entity; + })); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void removeProjectUser(Long projectId, Long userId) { + // 超管守卫:不让把超管从项目里踢掉(本身超管就不该在 user_project 里;即使被历史数据污染也保护) + if (isSuperAdmin(userId)) { + throw exception(USER_PROJECT_CANNOT_REMOVE_SUPER_ADMIN); + } + // 自踢守卫:当前登录人 && 当前项目 == 目标项目 → 拒绝 + Long currentLoginUserId = SecurityFrameworkUtils.getLoginUserId(); + Long currentProjectId = ProjectContextHolder.getProjectId(); + if (currentLoginUserId != null && currentLoginUserId.equals(userId) + && currentProjectId != null && currentProjectId.equals(projectId)) { + throw exception(USER_PROJECT_CANNOT_REMOVE_SELF_CURRENT); + } + userProjectMapper.delete(Wrappers.lambdaQuery() + .eq(UserProjectDO::getProjectId, projectId) + .eq(UserProjectDO::getUserId, userId)); + } + + /** + * 判断用户是否超管 + */ + private boolean isSuperAdmin(Long userId) { + if (userId == null) { + return false; + } + Set roleIds = permissionService.getUserRoleIdListByUserId(userId); + if (CollUtil.isEmpty(roleIds)) { + return false; + } + return roleService.hasAnySuperAdmin(roleIds); + } + }