feat(system): 用户-项目绑定管理 API + 顶栏项目下拉修正
- 新增 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<List<ProjectRespVO>> getProjectSimpleList() {
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
List<ProjectDO> 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<List<ProjectRespVO>> getAllProjectSimpleList() {
|
||||
List<ProjectDO> list = projectService.getProjectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
return success(convertList(list, project ->
|
||||
new ProjectRespVO().setId(project.getId()).setName(project.getName()).setCode(project.getCode())));
|
||||
|
||||
@@ -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<Boolean> 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<Boolean> 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<Set<Long>> 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<Set<Long>> getUserIdsByProjectId(@RequestParam("projectId") Long projectId) {
|
||||
return success(userProjectService.getUserIdsByProjectId(projectId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<Long> userIds = Collections.emptySet();
|
||||
|
||||
}
|
||||
@@ -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<Long> projectIds = Collections.emptySet();
|
||||
|
||||
}
|
||||
@@ -110,4 +110,12 @@ public interface ProjectService {
|
||||
*/
|
||||
Long getDefaultProjectId(Long userId);
|
||||
|
||||
/**
|
||||
* 获得指定用户"授权且启用"的项目列表(用于顶栏项目切换器的下拉数据源)
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @return 授权启用的项目列表
|
||||
*/
|
||||
List<ProjectDO> getAuthorizedEnabledProjects(Long userId);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<UserProjectDO>()
|
||||
.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<ProjectDO> getAuthorizedEnabledProjects(Long userId) {
|
||||
List<Long> authorizedIds = getAuthorizedProjectIds(userId);
|
||||
if (CollUtil.isEmpty(authorizedIds)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<ProjectDO> projects = projectMapper.selectByIds(authorizedIds);
|
||||
return projects.stream()
|
||||
.filter(p -> CommonStatusEnum.ENABLE.getStatus().equals(p.getStatus()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getDefaultProjectId(Long userId) {
|
||||
List<Long> authorizedProjectIds = getAuthorizedProjectIds(userId);
|
||||
|
||||
@@ -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<Long> projectIds);
|
||||
|
||||
/**
|
||||
* 覆盖式分配某项目的成员集合
|
||||
* 传入全集,内部 diff 出新增和移除。
|
||||
*
|
||||
* @param projectId 项目编号
|
||||
* @param userIds 期望的用户编号集合(传空集即清空该项目所有成员)
|
||||
*/
|
||||
void assignProjectUsers(Long projectId, Set<Long> userIds);
|
||||
|
||||
/**
|
||||
* 查询某用户已绑定的项目编号集合
|
||||
*
|
||||
* @param userId 用户编号
|
||||
* @return 项目编号集合
|
||||
*/
|
||||
Set<Long> getProjectIdsByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 查询某项目下绑定的用户编号集合
|
||||
*
|
||||
* @param projectId 项目编号
|
||||
* @return 用户编号集合
|
||||
*/
|
||||
Set<Long> getUserIdsByProjectId(Long projectId);
|
||||
|
||||
/**
|
||||
* 批量清理项目下所有绑定(供项目删除时级联调用)
|
||||
*
|
||||
* @param projectId 项目编号
|
||||
*/
|
||||
void deleteByProjectId(Long projectId);
|
||||
|
||||
/**
|
||||
* 批量清理用户的所有绑定(供用户删除时级联调用)
|
||||
*
|
||||
* @param userId 用户编号
|
||||
*/
|
||||
void deleteByUserId(Long userId);
|
||||
|
||||
}
|
||||
@@ -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<Long> projectIds) {
|
||||
Set<Long> targetIds = CollUtil.emptyIfNull(projectIds);
|
||||
|
||||
// 1. 查当前绑定
|
||||
Set<Long> 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<Long> toInsert = CollUtil.subtract(targetIds, dbIds);
|
||||
Collection<Long> 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.<UserProjectDO>lambdaQuery()
|
||||
.eq(UserProjectDO::getUserId, userId)
|
||||
.in(UserProjectDO::getProjectId, toDelete));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void assignProjectUsers(Long projectId, Set<Long> userIds) {
|
||||
Set<Long> targetIds = CollUtil.emptyIfNull(userIds);
|
||||
|
||||
// 1. 查当前成员
|
||||
Set<Long> dbIds = convertSet(userProjectMapper.selectListByProjectId(projectId),
|
||||
UserProjectDO::getUserId);
|
||||
|
||||
// 2. 算要移除的用户
|
||||
Collection<Long> toDelete = CollUtil.subtract(dbIds, targetIds);
|
||||
|
||||
// 3. 超管守卫:被移除的用户里若有超管角色,拒绝
|
||||
if (CollUtil.isNotEmpty(toDelete)) {
|
||||
for (Long uid : toDelete) {
|
||||
Set<Long> 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<Long> 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.<UserProjectDO>lambdaQuery()
|
||||
.eq(UserProjectDO::getProjectId, projectId)
|
||||
.in(UserProjectDO::getUserId, toDelete));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getProjectIdsByUserId(Long userId) {
|
||||
if (userId == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
List<UserProjectDO> list = userProjectMapper.selectListByUserId(userId);
|
||||
return convertSet(list, UserProjectDO::getProjectId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Long> getUserIdsByProjectId(Long projectId) {
|
||||
if (projectId == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
List<UserProjectDO> list = userProjectMapper.selectListByProjectId(projectId);
|
||||
return convertSet(list, UserProjectDO::getUserId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByProjectId(Long projectId) {
|
||||
if (projectId == null) {
|
||||
return;
|
||||
}
|
||||
userProjectMapper.delete(Wrappers.<UserProjectDO>lambdaQuery()
|
||||
.eq(UserProjectDO::getProjectId, projectId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByUserId(Long userId) {
|
||||
if (userId == null) {
|
||||
return;
|
||||
}
|
||||
userProjectMapper.delete(Wrappers.<UserProjectDO>lambdaQuery()
|
||||
.eq(UserProjectDO::getUserId, userId));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user