feat(system): 超管绕过 user_project + 项目成员分页/增量 API

后端为配合前端"项目管理成员"从 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) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-04-23 15:48:18 +08:00
parent db31462774
commit 5dbc6c5e79
6 changed files with 250 additions and 0 deletions

View File

@@ -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<PageResult<UserRespVO>> getProjectUserPage(@Valid UserProjectPageReqVO reqVO) {
PageResult<AdminUserDO> 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<Boolean> 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<Boolean> removeProjectUser(@RequestParam("projectId") Long projectId,
@RequestParam("userId") Long userId) {
userProjectService.removeProjectUser(projectId, userId);
return success(true);
}
}

View File

@@ -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<Long> userIds;
}

View File

@@ -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;
}

View File

@@ -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<Long> getAuthorizedProjectIds(Long userId) {
// 超管绕过 user_project 绑定校验:直接返回当前租户下所有项目 ID
// 与 TenantSecurityWebFilter 对超管的处理语义一致
if (isSuperAdmin(userId)) {
return CollectionUtils.convertList(projectMapper.selectList(), ProjectDO::getId);
}
List<UserProjectDO> userProjects = userProjectMapper.selectListByUserId(userId);
return CollectionUtils.convertList(userProjects, UserProjectDO::getProjectId);
}
/**
* 判断用户是否为超级管理员
* 查角色列表 → hasAnySuperAdmin 判别
*/
private boolean isSuperAdmin(Long userId) {
if (userId == null) {
return false;
}
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
if (CollUtil.isEmpty(roleIds)) {
return false;
}
return roleService.hasAnySuperAdmin(roleIds);
}
@Override
public List<ProjectDO> getAuthorizedEnabledProjects(Long userId) {
List<Long> authorizedIds = getAuthorizedProjectIds(userId);

View File

@@ -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<AdminUserDO> getProjectUserPage(UserProjectPageReqVO reqVO);
/**
* 增量把一组用户加入到项目(已在的用户跳过,不影响现有绑定)
*
* @param projectId 项目编号
* @param userIds 要新增的用户编号集合
*/
void addProjectUsers(Long projectId, Set<Long> userIds);
/**
* 从项目中移除单个成员(带超管/自踢守卫)
*
* @param projectId 项目编号
* @param userId 用户编号
*/
void removeProjectUser(Long projectId, Long userId);
}

View File

@@ -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<Long> projectIds) {
@@ -170,4 +180,94 @@ public class UserProjectServiceImpl implements UserProjectService {
.eq(UserProjectDO::getUserId, userId));
}
@Override
public PageResult<AdminUserDO> getProjectUserPage(UserProjectPageReqVO reqVO) {
// 1. 查项目下所有 userIds
Set<Long> memberIds = convertSet(
userProjectMapper.selectListByProjectId(reqVO.getProjectId()),
UserProjectDO::getUserId);
if (CollUtil.isEmpty(memberIds)) {
return PageResult.empty();
}
// 2. 过滤超管(超管靠角色天然拥有所有项目,不应出现在成员列表)
List<Long> 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<AdminUserDO> wrapper = new LambdaQueryWrapperX<AdminUserDO>()
.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<Long> userIds) {
Set<Long> target = CollUtil.emptyIfNull(userIds);
if (target.isEmpty()) {
return;
}
// 查当前已绑定,避免重复插入
Set<Long> existing = convertSet(
userProjectMapper.selectListByProjectId(projectId),
UserProjectDO::getUserId);
Collection<Long> 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.<UserProjectDO>lambdaQuery()
.eq(UserProjectDO::getProjectId, projectId)
.eq(UserProjectDO::getUserId, userId));
}
/**
* 判断用户是否超管
*/
private boolean isSuperAdmin(Long userId) {
if (userId == null) {
return false;
}
Set<Long> roleIds = permissionService.getUserRoleIdListByUserId(userId);
if (CollUtil.isEmpty(roleIds)) {
return false;
}
return roleService.hasAnySuperAdmin(roleIds);
}
}