feat(guidelines): 更新工作指引并添加批量成员管理功能
- 简化 AGENTS.md 内容,统一引用 CLAUDE.md 作为主要指引 - 更新 CLAUDE.md 中的工作方式和验证流程说明 - 添加产品和项目成员批量新增/移出的错误码定义 - 扩展系统角色 API 响应 DTO,增加可见性字段 - 实现产品团队成员批量新增和批量移出控制器接口 - 添加产品成员批量操作的服务层实现和业务校验逻辑 - 实现项目团队成员批量操作的相关控制器接口 - 优化产品成员列表查询,过滤不可见角色行 - 添加批量操作的审计日志记录功能
This commit is contained in:
@@ -39,6 +39,12 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PRODUCT_INITIAL_TEAM_ROLE_INVALID = new ErrorCode(1_008_001_026, "初始团队中存在非法角色");
|
||||
ErrorCode PRODUCT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_001_027, "原产品经理在该产品已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
||||
ErrorCode PRODUCT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_001_028, "内置产品角色【{}】未在 system_role 找到,请联系管理员");
|
||||
ErrorCode PRODUCT_MEMBER_USER_INVALID = new ErrorCode(1_008_001_029, "产品成员不是有效系统用户");
|
||||
// 批量新增(POST /project/product/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_001_030, "请勿在批量列表中重复添加同一成员");
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_001_031, "批量新增不允许指定为经理,请通过编辑成员调整");
|
||||
// 批量移出(POST /project/product/{id}/members/batch/inactive)专用:同一请求内 memberId 重复
|
||||
ErrorCode PRODUCT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_001_032, "请勿在批量移出列表中重复指定同一成员");
|
||||
|
||||
// ========== 产品需求 1-008-002-000 ==========
|
||||
ErrorCode REQUIREMENT_NOT_EXISTS = new ErrorCode(1_008_002_000, "产品需求不存在");
|
||||
@@ -103,6 +109,11 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROJECT_DIRECTION_NOT_MATCH_PRODUCT = new ErrorCode(1_008_002_032, "项目方向与所属产品方向不一致");
|
||||
ErrorCode PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE = new ErrorCode(1_008_002_033, "原项目经理在该项目已持有目标角色【{}】(含历史失效行),不能直接转交,请先清理后重试");
|
||||
ErrorCode PROJECT_INTERNAL_ROLE_NOT_CONFIGURED = new ErrorCode(1_008_002_034, "内置项目角色【{}】未在 system_role 找到,请联系管理员");
|
||||
// 批量新增(POST /project/project/{id}/members/batch)专用:同一请求内 userId 重复 / 经理拦截
|
||||
ErrorCode PROJECT_MEMBER_BATCH_USER_DUPLICATE = new ErrorCode(1_008_002_035, "请勿在批量列表中重复添加同一成员");
|
||||
ErrorCode PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED = new ErrorCode(1_008_002_036, "批量新增不允许指定为经理,请通过编辑成员调整");
|
||||
// 批量移出(POST /project/project/{id}/members/batch/inactive)专用:同一请求内 memberId 重复
|
||||
ErrorCode PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE = new ErrorCode(1_008_002_037, "请勿在批量移出列表中重复指定同一成员");
|
||||
|
||||
// ========== 执行管理 1-008-003-000 ==========
|
||||
ErrorCode PROJECT_EXECUTION_NOT_EXISTS = new ErrorCode(1_008_003_000, "执行不存在");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
|
||||
@@ -42,6 +44,14 @@ public class ProductMemberController {
|
||||
return success(productMemberService.createProductMember(productId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch")
|
||||
@Operation(summary = "批量新增产品团队成员")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<List<Long>> batchCreateProductMembers(@PathVariable("id") Long productId,
|
||||
@Valid @RequestBody ProductMemberBatchCreateReqVO reqVO) {
|
||||
return success(productMemberService.batchCreateProductMembers(productId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/members/{memberId}")
|
||||
@Operation(summary = "调整产品团队成员角色")
|
||||
public CommonResult<Boolean> updateProductMember(@PathVariable("id") Long productId,
|
||||
@@ -60,4 +70,13 @@ public class ProductMemberController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch/inactive")
|
||||
@Operation(summary = "批量移出产品团队成员")
|
||||
@Parameter(name = "id", description = "产品编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> batchInactiveProductMembers(@PathVariable("id") Long productId,
|
||||
@Valid @RequestBody ProductMemberBatchInactiveReqVO reqVO) {
|
||||
productMemberService.batchInactiveProductMembers(productId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员批量新增 Request VO")
|
||||
@Data
|
||||
public class ProductMemberBatchCreateReqVO {
|
||||
|
||||
/**
|
||||
* 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。
|
||||
* 经理角色(product_manager)由 Service 兜底拦截,不在此体现。
|
||||
*/
|
||||
@Schema(description = "待新增的成员列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "请至少选择一名成员")
|
||||
@Size(max = 200, message = "单次批量加入成员不能超过 200 人")
|
||||
@Valid
|
||||
private List<Item> members;
|
||||
|
||||
@Schema(description = "批量新增成员项")
|
||||
@Data
|
||||
public static class Item {
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001")
|
||||
@NotNull(message = "角色编号不能为空")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "备注", example = "本次批量加入")
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.product.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 产品团队成员批量移出 Request VO")
|
||||
@Data
|
||||
public class ProductMemberBatchInactiveReqVO {
|
||||
|
||||
/**
|
||||
* 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。
|
||||
* 经理、已失效、重复 memberId 等业务规则由 Service 兜底,不在此体现。
|
||||
*/
|
||||
@Schema(description = "待移出的成员关系 ID 列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "请至少选择一名成员")
|
||||
@Size(max = 200, message = "单次批量移出成员不能超过 200 人")
|
||||
private List<Long> memberIds;
|
||||
|
||||
@Schema(description = "移出原因,单一字符串应用于本批所有成员", example = "组织架构调整,本批成员退出当前产品团队")
|
||||
@Size(max = 500, message = "移出原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project;
|
||||
|
||||
import com.njcn.rdms.framework.common.pojo.CommonResult;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
|
||||
@@ -42,6 +44,14 @@ public class ProjectMemberController {
|
||||
return success(projectMemberService.createProjectMember(projectId, reqVO));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch")
|
||||
@Operation(summary = "批量新增项目成员")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<List<Long>> batchCreateProjectMembers(@PathVariable("id") Long projectId,
|
||||
@Valid @RequestBody ProjectMemberBatchCreateReqVO reqVO) {
|
||||
return success(projectMemberService.batchCreateProjectMembers(projectId, reqVO));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/members/{memberId}")
|
||||
@Operation(summary = "调整项目成员角色")
|
||||
public CommonResult<Boolean> updateProjectMember(@PathVariable("id") Long projectId,
|
||||
@@ -60,4 +70,13 @@ public class ProjectMemberController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/members/batch/inactive")
|
||||
@Operation(summary = "批量移出项目成员")
|
||||
@Parameter(name = "id", description = "项目编号", required = true, example = "1024")
|
||||
public CommonResult<Boolean> batchInactiveProjectMembers(@PathVariable("id") Long projectId,
|
||||
@Valid @RequestBody ProjectMemberBatchInactiveReqVO reqVO) {
|
||||
projectMemberService.batchInactiveProjectMembers(projectId, reqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 项目成员批量新增 Request VO")
|
||||
@Data
|
||||
public class ProjectMemberBatchCreateReqVO {
|
||||
|
||||
/**
|
||||
* 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。
|
||||
* 经理角色(project_manager)由 Service 兜底拦截,不在此体现。
|
||||
*/
|
||||
@Schema(description = "待新增的成员列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "请至少选择一名成员")
|
||||
@Size(max = 200, message = "单次批量加入成员不能超过 200 人")
|
||||
@Valid
|
||||
private List<Item> members;
|
||||
|
||||
@Schema(description = "批量新增成员项")
|
||||
@Data
|
||||
public static class Item {
|
||||
|
||||
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "用户编号不能为空")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3100000002001")
|
||||
@NotNull(message = "角色编号不能为空")
|
||||
private Long roleId;
|
||||
|
||||
@Schema(description = "备注", example = "本次批量加入")
|
||||
@Size(max = 500, message = "备注长度不能超过500个字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.njcn.rdms.module.project.controller.admin.project.vo.member;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - 项目成员批量移出 Request VO")
|
||||
@Data
|
||||
public class ProjectMemberBatchInactiveReqVO {
|
||||
|
||||
/**
|
||||
* 批量上限沿用需求约定的 200,超过走 Bean Validation 直接 400 拦截。
|
||||
* 经理、已失效、重复 memberId、仍担任未关闭执行负责人等业务规则由 Service 兜底,不在此体现。
|
||||
*/
|
||||
@Schema(description = "待移出的成员关系 ID 列表,长度 [1, 200]", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "请至少选择一名成员")
|
||||
@Size(max = 200, message = "单次批量移出成员不能超过 200 人")
|
||||
private List<Long> memberIds;
|
||||
|
||||
@Schema(description = "移出原因,单一字符串应用于本批所有成员", example = "组织架构调整,本批成员退出当前项目团队")
|
||||
@Size(max = 500, message = "移出原因长度不能超过500个字符")
|
||||
private String reason;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.njcn.rdms.module.project.service.product;
|
||||
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
|
||||
@@ -29,6 +31,15 @@ public interface ProductMemberService {
|
||||
*/
|
||||
Long createProductMember(Long productId, ProductMemberSaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 批量新增产品团队成员(不承担经理交接语义;事务性入库,全部成功或整体回滚)。
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @param reqVO 请求参数
|
||||
* @return 新建的成员关系 ID 列表,顺序与入参 members 一致
|
||||
*/
|
||||
List<Long> batchCreateProductMembers(Long productId, ProductMemberBatchCreateReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 调整产品团队成员角色
|
||||
*
|
||||
@@ -47,4 +58,15 @@ public interface ProductMemberService {
|
||||
*/
|
||||
void inactiveProductMember(Long productId, Long memberId, ProductMemberInactiveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 批量移出产品团队成员(事务性更新,全部成功或整体回滚)。
|
||||
* <p>
|
||||
* 业务校验:同请求 memberId 重复、不存在/不属于当前产品、已失效、产品经理均拒绝;
|
||||
* 经理判定与单条 inactive 一致,按 product.managerUserId 比对。
|
||||
*
|
||||
* @param productId 产品编号
|
||||
* @param reqVO 请求参数
|
||||
*/
|
||||
void batchInactiveProductMembers(Long productId, ProductMemberBatchInactiveReqVO reqVO);
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProductObjectConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.product.vo.member.ProductMemberSaveReqVO;
|
||||
@@ -32,6 +34,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -66,6 +69,14 @@ public class ProductMemberServiceImpl implements ProductMemberService {
|
||||
ProductDO product = validateProductExists(productId);
|
||||
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProductObjectConstants.OBJECT_TYPE, productId);
|
||||
Map<Long, ObjectRoleRespDTO> roleMap = getRoleMap(members.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()));
|
||||
// 过滤掉 visible=0 的角色行(业务自动赋予的角色,如创建者 / 隐式观察者;不在团队列表显示);
|
||||
// visible=null 视同显示,兼容旧数据未回填的场景。ACTIVE / INACTIVE 行一并过滤。
|
||||
members = members.stream()
|
||||
.filter(m -> {
|
||||
ObjectRoleRespDTO role = roleMap.get(m.getRoleId());
|
||||
return role == null || !Integer.valueOf(0).equals(role.getVisible());
|
||||
})
|
||||
.toList();
|
||||
Map<Long, AdminUserRespDTO> userMap = getUserMap(members.stream().map(UserObjectRoleDO::getUserId).collect(Collectors.toSet()));
|
||||
|
||||
// 拆分 ACTIVE / INACTIVE:
|
||||
@@ -192,6 +203,85 @@ public class ProductMemberServiceImpl implements ProductMemberService {
|
||||
return member.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
|
||||
permission = ProductObjectConstants.PERMISSION_UPDATE)
|
||||
public List<Long> batchCreateProductMembers(Long productId, ProductMemberBatchCreateReqVO reqVO) {
|
||||
validateProductEditable(productId);
|
||||
|
||||
List<ProductMemberBatchCreateReqVO.Item> items = reqVO.getMembers();
|
||||
// 同请求内 userId 重复一律拒绝(user-level),即使是不同角色也算重复——批量语义就是一人一行
|
||||
Set<Long> seenUserIds = new HashSet<>(items.size());
|
||||
for (ProductMemberBatchCreateReqVO.Item item : items) {
|
||||
if (!seenUserIds.add(item.getUserId())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_BATCH_USER_DUPLICATE);
|
||||
}
|
||||
}
|
||||
// 批量校验用户存在/启用——产品域单条接口未做此校验(历史负债),批量接口按需求 §4 兜底
|
||||
validateMemberUsers(seenUserIds);
|
||||
|
||||
List<Long> memberIds = new ArrayList<>(items.size());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (ProductMemberBatchCreateReqVO.Item item : items) {
|
||||
ObjectRoleRespDTO targetRole = validateProductRole(item.getRoleId());
|
||||
if (isManagerRole(targetRole)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_BATCH_MANAGER_NOT_ALLOWED);
|
||||
}
|
||||
// user-level 拒绝:该用户在本产品下有任意 ACTIVE 行(含其他角色),不允许再批量加入
|
||||
List<UserObjectRoleDO> activeRows = userObjectRoleMapper
|
||||
.selectActiveListByObjectAndUserId(ProductObjectConstants.OBJECT_TYPE, productId, item.getUserId());
|
||||
if (!activeRows.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
// 多角色支持:按 (user, object, role) 三元组判存在;INACTIVE 行复活,避免唯一索引 INSERT 冲突
|
||||
UserObjectRoleDO existingMember = userObjectRoleMapper
|
||||
.selectByObjectUserAndRole(ProductObjectConstants.OBJECT_TYPE, productId,
|
||||
item.getUserId(), targetRole.getId());
|
||||
UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
|
||||
UserObjectRoleDO member;
|
||||
String actionType;
|
||||
if (existingMember == null) {
|
||||
member = new UserObjectRoleDO();
|
||||
member.setUserId(item.getUserId());
|
||||
member.setObjectType(ProductObjectConstants.OBJECT_TYPE);
|
||||
member.setObjectId(productId);
|
||||
member.setRoleId(targetRole.getId());
|
||||
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
|
||||
member.setJoinedTime(now);
|
||||
member.setLeftTime(null);
|
||||
member.setRemark(normalizeNullableText(item.getRemark()));
|
||||
userObjectRoleMapper.insert(member);
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
|
||||
} else {
|
||||
member = existingMember;
|
||||
member.setRoleId(targetRole.getId());
|
||||
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
|
||||
member.setJoinedTime(now);
|
||||
member.setLeftTime(null);
|
||||
member.setRemark(normalizeNullableText(item.getRemark()));
|
||||
userObjectRoleMapper.updateById(member);
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_REACTIVATE;
|
||||
}
|
||||
writeMemberAuditLog(member, actionType, before, member, null);
|
||||
memberIds.add(member.getId());
|
||||
}
|
||||
return memberIds;
|
||||
}
|
||||
|
||||
private void validateMemberUsers(Set<Long> userIds) {
|
||||
try {
|
||||
Boolean valid = adminUserApi.validateUserList(new ArrayList<>(userIds)).getCheckedData();
|
||||
if (Boolean.TRUE.equals(valid)) {
|
||||
return;
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_USER_INVALID);
|
||||
}
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_USER_INVALID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
|
||||
@@ -257,6 +347,80 @@ public class ProductMemberServiceImpl implements ProductMemberService {
|
||||
normalizeNullableText(reqVO.getReason()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProductObjectConstants.OBJECT_TYPE, objectId = "#productId",
|
||||
permission = ProductObjectConstants.PERMISSION_UPDATE)
|
||||
public void batchInactiveProductMembers(Long productId, ProductMemberBatchInactiveReqVO reqVO) {
|
||||
ProductDO product = validateProductEditable(productId);
|
||||
|
||||
List<Long> memberIds = reqVO.getMemberIds();
|
||||
// 同请求 memberId 重复一律拒绝,避免对同一行重复 update 造成 audit 重复落库
|
||||
Set<Long> seen = new HashSet<>(memberIds.size());
|
||||
for (Long memberId : memberIds) {
|
||||
if (!seen.add(memberId)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE);
|
||||
}
|
||||
}
|
||||
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// 收集批次摘要,循环外聚合写一条审计日志(需求 §6 口径)
|
||||
List<UserObjectRoleDO> removed = new ArrayList<>(memberIds.size());
|
||||
for (Long memberId : memberIds) {
|
||||
UserObjectRoleDO member = validateMemberExists(productId, memberId);
|
||||
if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MEMBER_NOT_ACTIVE);
|
||||
}
|
||||
// 经理判定沿用单条 inactive 口径:按 product.managerUserId 比对,不引入 role.code 判定避免分叉
|
||||
if (Objects.equals(member.getUserId(), product.getManagerUserId())) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_MANAGER_MEMBER_NOT_ALLOW_REMOVE);
|
||||
}
|
||||
|
||||
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE);
|
||||
member.setLeftTime(now);
|
||||
userObjectRoleMapper.updateById(member);
|
||||
removed.add(member);
|
||||
}
|
||||
|
||||
writeBatchInactiveAuditLog(productId, removed, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移出场景的聚合审计:写一条对象维度日志(bizType=product, bizId=productId),
|
||||
* fieldChanges 体现批量摘要 batchCount + members 数组,与 writeManagerChangeAuditLog 风格对齐。
|
||||
* 单条 before/after 字段差异在聚合视图下故意丢弃,由读侧按需展开。
|
||||
*/
|
||||
private void writeBatchInactiveAuditLog(Long productId, List<UserObjectRoleDO> members, String reason) {
|
||||
if (members.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(ProductObjectConstants.OBJECT_TYPE);
|
||||
auditLog.setBizId(productId);
|
||||
auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_REMOVE);
|
||||
auditLog.setFieldChanges(buildBatchInactiveFieldChanges(members));
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
private String buildBatchInactiveFieldChanges(List<UserObjectRoleDO> members) {
|
||||
List<Map<String, Object>> memberSummaries = new ArrayList<>(members.size());
|
||||
for (UserObjectRoleDO member : members) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("memberId", member.getId());
|
||||
summary.put("userId", member.getUserId());
|
||||
summary.put("roleId", member.getRoleId());
|
||||
memberSummaries.add(summary);
|
||||
}
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("batchCount", members.size());
|
||||
root.put("members", memberSummaries);
|
||||
return JsonUtils.toJsonString(root);
|
||||
}
|
||||
|
||||
private ProductDO validateProductExists(Long productId) {
|
||||
if (productId == null) {
|
||||
throw exception(ErrorCodeConstants.PRODUCT_NOT_EXISTS);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.njcn.rdms.module.project.service.project;
|
||||
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
|
||||
@@ -16,8 +18,28 @@ public interface ProjectMemberService {
|
||||
|
||||
Long createProjectMember(Long projectId, ProjectMemberSaveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 批量新增项目成员(不承担经理交接语义;事务性入库,全部成功或整体回滚)。
|
||||
*
|
||||
* @param projectId 项目编号
|
||||
* @param reqVO 请求参数
|
||||
* @return 新建的成员关系 ID 列表,顺序与入参 members 一致
|
||||
*/
|
||||
List<Long> batchCreateProjectMembers(Long projectId, ProjectMemberBatchCreateReqVO reqVO);
|
||||
|
||||
void updateProjectMember(Long projectId, Long memberId, ProjectMemberUpdateReqVO reqVO);
|
||||
|
||||
void inactiveProjectMember(Long projectId, Long memberId, ProjectMemberInactiveReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 批量移出项目成员(事务性更新,全部成功或整体回滚)。
|
||||
* <p>
|
||||
* 业务校验:同请求 memberId 重复、不存在/不属于当前项目、已失效、项目经理、仍担任未关闭执行负责人均拒绝;
|
||||
* 经理判定与单条 inactive 一致,按 project.managerUserId 比对。
|
||||
*
|
||||
* @param projectId 项目编号
|
||||
* @param reqVO 请求参数
|
||||
*/
|
||||
void batchInactiveProjectMembers(Long projectId, ProjectMemberBatchInactiveReqVO reqVO);
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import com.njcn.rdms.module.project.constant.ObjectActivityConstants;
|
||||
import com.njcn.rdms.module.project.constant.ObjectRoleConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectExecutionConstants;
|
||||
import com.njcn.rdms.module.project.constant.ProjectObjectConstants;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchCreateReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberBatchInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO;
|
||||
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
|
||||
@@ -34,6 +36,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -70,6 +73,14 @@ public class ProjectMemberServiceImpl implements ProjectMemberService {
|
||||
ProjectDO project = validateProjectExists(projectId);
|
||||
List<UserObjectRoleDO> members = userObjectRoleMapper.selectListByObject(ProjectObjectConstants.OBJECT_TYPE, projectId);
|
||||
Map<Long, ObjectRoleRespDTO> roleMap = getRoleMap(members.stream().map(UserObjectRoleDO::getRoleId).collect(Collectors.toSet()));
|
||||
// 过滤掉 visible=0 的角色行(业务自动赋予的角色,如创建者 / 隐式观察者;不在团队列表显示);
|
||||
// visible=null 视同显示,兼容旧数据未回填的场景。ACTIVE / INACTIVE 行一并过滤。
|
||||
members = members.stream()
|
||||
.filter(m -> {
|
||||
ObjectRoleRespDTO role = roleMap.get(m.getRoleId());
|
||||
return role == null || !Integer.valueOf(0).equals(role.getVisible());
|
||||
})
|
||||
.toList();
|
||||
Map<Long, AdminUserRespDTO> userMap = getUserMap(members.stream().map(UserObjectRoleDO::getUserId).collect(Collectors.toSet()));
|
||||
|
||||
// 拆分 ACTIVE / INACTIVE:
|
||||
@@ -188,6 +199,78 @@ public class ProjectMemberServiceImpl implements ProjectMemberService {
|
||||
return member.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectObjectConstants.PERMISSION_MEMBER)
|
||||
public List<Long> batchCreateProjectMembers(Long projectId, ProjectMemberBatchCreateReqVO reqVO) {
|
||||
validateProjectEditable(projectId);
|
||||
|
||||
List<ProjectMemberBatchCreateReqVO.Item> items = reqVO.getMembers();
|
||||
// 同请求内 userId 重复一律拒绝(user-level),即使是不同角色也算重复——批量语义就是一人一行
|
||||
Set<Long> seenUserIds = new HashSet<>(items.size());
|
||||
for (ProjectMemberBatchCreateReqVO.Item item : items) {
|
||||
if (!seenUserIds.add(item.getUserId())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_USER_DUPLICATE);
|
||||
}
|
||||
}
|
||||
// 批量校验用户存在/启用——按需求 §4 兜底
|
||||
validateMemberUsers(seenUserIds);
|
||||
|
||||
List<Long> memberIds = new ArrayList<>(items.size());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (ProjectMemberBatchCreateReqVO.Item item : items) {
|
||||
ObjectRoleRespDTO targetRole = validateProjectRole(item.getRoleId());
|
||||
if (isManagerRole(targetRole)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_MANAGER_NOT_ALLOWED);
|
||||
}
|
||||
// user-level 拒绝:该用户在本项目下有任意 ACTIVE 行(含其他角色),不允许再批量加入
|
||||
List<UserObjectRoleDO> activeRows = userObjectRoleMapper
|
||||
.selectActiveListByObjectAndUserId(ProjectObjectConstants.OBJECT_TYPE, projectId, item.getUserId());
|
||||
if (!activeRows.isEmpty()) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS);
|
||||
}
|
||||
|
||||
// 多角色支持:按 (user, object, role) 三元组判存在;INACTIVE 行复活,避免唯一索引 INSERT 冲突
|
||||
UserObjectRoleDO existingMember = userObjectRoleMapper
|
||||
.selectByObjectUserAndRole(ProjectObjectConstants.OBJECT_TYPE, projectId,
|
||||
item.getUserId(), targetRole.getId());
|
||||
UserObjectRoleDO before = existingMember == null ? null : cloneMember(existingMember);
|
||||
UserObjectRoleDO member = existingMember == null ? new UserObjectRoleDO() : existingMember;
|
||||
member.setUserId(item.getUserId());
|
||||
member.setObjectType(ProjectObjectConstants.OBJECT_TYPE);
|
||||
member.setObjectId(projectId);
|
||||
member.setRoleId(targetRole.getId());
|
||||
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_ACTIVE);
|
||||
member.setJoinedTime(now);
|
||||
member.setLeftTime(null);
|
||||
member.setRemark(normalizeNullableText(item.getRemark()));
|
||||
String actionType;
|
||||
if (existingMember == null) {
|
||||
userObjectRoleMapper.insert(member);
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_ADD;
|
||||
} else {
|
||||
userObjectRoleMapper.updateById(member);
|
||||
actionType = ObjectActivityConstants.MEMBER_ACTION_REACTIVATE;
|
||||
}
|
||||
writeMemberAuditLog(member, actionType, before, member, null);
|
||||
memberIds.add(member.getId());
|
||||
}
|
||||
return memberIds;
|
||||
}
|
||||
|
||||
private void validateMemberUsers(Set<Long> userIds) {
|
||||
try {
|
||||
Boolean valid = adminUserApi.validateUserList(new ArrayList<>(userIds)).getCheckedData();
|
||||
if (Boolean.TRUE.equals(valid)) {
|
||||
return;
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID);
|
||||
}
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_USER_INVALID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
@@ -253,6 +336,47 @@ public class ProjectMemberServiceImpl implements ProjectMemberService {
|
||||
normalizeNullableText(reqVO.getReason()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CheckObjectPermission(objectType = ProjectObjectConstants.OBJECT_TYPE, objectId = "#projectId",
|
||||
permission = ProjectObjectConstants.PERMISSION_MEMBER)
|
||||
public void batchInactiveProjectMembers(Long projectId, ProjectMemberBatchInactiveReqVO reqVO) {
|
||||
ProjectDO project = validateProjectEditable(projectId);
|
||||
|
||||
List<Long> memberIds = reqVO.getMemberIds();
|
||||
// 同请求 memberId 重复一律拒绝,避免对同一行重复 update 造成 audit 重复落库
|
||||
Set<Long> seen = new HashSet<>(memberIds.size());
|
||||
for (Long memberId : memberIds) {
|
||||
if (!seen.add(memberId)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_BATCH_INACTIVE_MEMBER_DUPLICATE);
|
||||
}
|
||||
}
|
||||
|
||||
String reason = normalizeNullableText(reqVO.getReason());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// 收集批次摘要,循环外聚合写一条审计日志(需求 §6 口径)
|
||||
List<UserObjectRoleDO> removed = new ArrayList<>(memberIds.size());
|
||||
for (Long memberId : memberIds) {
|
||||
UserObjectRoleDO member = validateMemberExists(projectId, memberId);
|
||||
if (!Objects.equals(member.getStatus(), ObjectRoleConstants.MEMBER_STATUS_ACTIVE)) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MEMBER_NOT_ACTIVE);
|
||||
}
|
||||
// 经理判定沿用单条 inactive 口径:按 project.managerUserId 比对,不引入 role.code 判定避免分叉
|
||||
if (Objects.equals(member.getUserId(), project.getManagerUserId())) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_MANAGER_MEMBER_NOT_ALLOW_REMOVE);
|
||||
}
|
||||
// 与单条 inactive 一致:仍担任未关闭执行负责人的成员不能直接移出,需先完成执行负责人交接
|
||||
validateNoOpenOwnedExecutions(projectId, member.getUserId());
|
||||
|
||||
member.setStatus(ObjectRoleConstants.MEMBER_STATUS_INACTIVE);
|
||||
member.setLeftTime(now);
|
||||
userObjectRoleMapper.updateById(member);
|
||||
removed.add(member);
|
||||
}
|
||||
|
||||
writeBatchInactiveAuditLog(projectId, removed, reason);
|
||||
}
|
||||
|
||||
private ProjectDO validateProjectExists(Long projectId) {
|
||||
if (projectId == null) {
|
||||
throw exception(ErrorCodeConstants.PROJECT_NOT_EXISTS);
|
||||
@@ -485,6 +609,41 @@ public class ProjectMemberServiceImpl implements ProjectMemberService {
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移出场景的聚合审计:写一条对象维度日志(bizType=project, bizId=projectId),
|
||||
* fieldChanges 体现批量摘要 batchCount + members 数组,与 writeManagerChangeAuditLog 风格对齐。
|
||||
* 单条 before/after 字段差异在聚合视图下故意丢弃,由读侧按需展开。
|
||||
*/
|
||||
private void writeBatchInactiveAuditLog(Long projectId, List<UserObjectRoleDO> members, String reason) {
|
||||
if (members.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
BizAuditLogDO auditLog = new BizAuditLogDO();
|
||||
auditLog.setBizType(ProjectObjectConstants.OBJECT_TYPE);
|
||||
auditLog.setBizId(projectId);
|
||||
auditLog.setActionType(ObjectActivityConstants.MEMBER_ACTION_REMOVE);
|
||||
auditLog.setFieldChanges(buildBatchInactiveFieldChanges(members));
|
||||
auditLog.setReason(reason);
|
||||
auditLog.setOperatorUserId(SecurityFrameworkUtils.getLoginUserId());
|
||||
auditLog.setOperatorName(defaultText(SecurityFrameworkUtils.getLoginUserNickname()));
|
||||
bizAuditLogMapper.insert(auditLog);
|
||||
}
|
||||
|
||||
private String buildBatchInactiveFieldChanges(List<UserObjectRoleDO> members) {
|
||||
List<Map<String, Object>> memberSummaries = new ArrayList<>(members.size());
|
||||
for (UserObjectRoleDO member : members) {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("memberId", member.getId());
|
||||
summary.put("userId", member.getUserId());
|
||||
summary.put("roleId", member.getRoleId());
|
||||
memberSummaries.add(summary);
|
||||
}
|
||||
Map<String, Object> root = new LinkedHashMap<>();
|
||||
root.put("batchCount", members.size());
|
||||
root.put("members", memberSummaries);
|
||||
return JsonUtils.toJsonString(root);
|
||||
}
|
||||
|
||||
private String buildMemberFieldChanges(UserObjectRoleDO before, UserObjectRoleDO after) {
|
||||
Map<String, Object> fieldChanges = new LinkedHashMap<>();
|
||||
appendFieldChange(fieldChanges, "userId", valueOf(before, UserObjectRoleDO::getUserId),
|
||||
|
||||
Reference in New Issue
Block a user