feat(spec58): 审批人解析组件 ReviewerResolver 与发起接线
- 新增 ReviewerResolver:USER:<userId> / COLLEGE_ROLE:<roleCode>,; 分隔多段取并集升序 - reviewer_rule 为空回退 review_user_id;按学院解析空集合抛 BusinessException,消息含学院名/层级/缺什么 - ReviewFlowStarterService 发起与驳回重提改由解析取人,解析为空不写任何节点 - ReviewFlow 增加 reviewerRule 字段;DeclareRecordMapper 增加 selectReviewContext(仅 4 张在跑表)
This commit is contained in:
@@ -51,6 +51,9 @@ public class ReviewFlow implements Serializable {
|
||||
|
||||
private Integer reviewUserId;
|
||||
|
||||
@ApiModelProperty(value = "审批人规则,;分隔:USER:<userId> / COLLEGE_ROLE:<roleCode>;为空回退 review_user_id")
|
||||
private String reviewerRule;
|
||||
|
||||
@TableField(exist = false)
|
||||
private List<ReviewFlow> flows;
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ public interface DeclareRecordMapper {
|
||||
+ "FROM ${table} WHERE id = #{id} AND deleted = 0")
|
||||
Map<String, Object> selectRecord(@Param("table") String table, @Param("id") Integer id);
|
||||
|
||||
/** 审批人解析用:申报记录的申报人与学院标识(仅 4 张有 college_id 的在跑表可用) */
|
||||
@Select("SELECT id AS id, user_id AS userId, college_id AS collegeId "
|
||||
+ "FROM ${table} WHERE id = #{id} AND deleted = 0")
|
||||
Map<String, Object> selectReviewContext(@Param("table") String table, @Param("id") Integer id);
|
||||
|
||||
/** 同项目下其他有效申报的负责人手机号与团队成员,用于团队重复校验 */
|
||||
@Select("SELECT id AS id, phone AS phone, team_members AS teamMembers FROM gxmu_tzbcy_form "
|
||||
+ "WHERE declare_id = #{declareId} AND declare_status IN (1, 2, 3) AND deleted = 0 "
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gxwebsoft.gxmu.service;
|
||||
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 审批人解析(见 docs/adr/0006-approval-chain-convergence.md)。
|
||||
*
|
||||
* <p>输入一条审批链配置(reviewerRule / reviewUserId / organizationId)与申报记录的学院标识、申报人,
|
||||
* 输出该级审批人的 userId 集合(升序、去重)。规则语法:{@code ;} 分隔可拼装多段,
|
||||
* {@code USER:<userId>} 固定人,{@code COLLEGE_ROLE:<roleCode>} 在申报人学院的组织子树内找持有该角色的用户;
|
||||
* reviewerRule 为空时回退 reviewUserId(存量配置零迁移)。</p>
|
||||
*
|
||||
* <p>解析不出来时抛 {@link com.gxwebsoft.common.core.exception.BusinessException},
|
||||
* 消息里带学院名、第几级与缺什么——宁可发不出单,也不要发出无人认领的单。</p>
|
||||
*/
|
||||
public interface ReviewerResolver {
|
||||
|
||||
/**
|
||||
* 解析某一级审批链的审批人。
|
||||
*
|
||||
* @param flow 审批链配置(含 reviewerRule / reviewUserId / level)
|
||||
* @param collegeId 申报记录的学院标识(sys_organization.organization_id),可为空
|
||||
* @param declareUserId 申报人 userId,仅用于报错时定位,可为空
|
||||
* @return 审批人 userId 集合,升序去重且非空
|
||||
*/
|
||||
List<Integer> resolve(ReviewFlow flow, Integer collegeId, Integer declareUserId);
|
||||
}
|
||||
@@ -1,33 +1,52 @@
|
||||
package com.gxwebsoft.gxmu.service.impl;
|
||||
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlowConfig;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewList;
|
||||
import com.gxwebsoft.gxmu.mapper.DeclareRecordMapper;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowConfigService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewFlowStarterService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewListService;
|
||||
import com.gxwebsoft.gxmu.service.ReviewerResolver;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 审核流程发起服务实现
|
||||
*
|
||||
* <p>审批人不再写死:由 {@link ReviewerResolver} 按审批链配置的 reviewer_rule 解析,
|
||||
* 一期串行单审取 userId 最小的那个;解析不出来直接抛业务异常,不生成待办。</p>
|
||||
*
|
||||
* @author LX
|
||||
* @since 2026-03-18
|
||||
*/
|
||||
@Service
|
||||
public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
||||
/** 有 college_id 的 4 张在跑业务表(#59);其余模块的申报记录只取申报人 */
|
||||
private static final Set<String> COLLEGE_MODULES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
||||
"gxmu_sjqn", "gxmu_tzbcy_form", "gxmu_qmgc_form", "gxmu_wxxzx_project")));
|
||||
|
||||
@Resource
|
||||
private ReviewFlowConfigService reviewFlowConfigService;
|
||||
@Resource
|
||||
private ReviewFlowService reviewFlowService;
|
||||
@Resource
|
||||
private ReviewListService reviewListService;
|
||||
@Resource
|
||||
private ReviewerResolver reviewerResolver;
|
||||
@Resource
|
||||
private DeclareRecordMapper declareRecordMapper;
|
||||
|
||||
@Override
|
||||
public void startReview(String module, Integer pk) {
|
||||
@@ -91,11 +110,46 @@ public class ReviewFlowStarterServiceImpl implements ReviewFlowStarterService {
|
||||
}
|
||||
|
||||
private void saveFirstReviewNode(String module, Integer pk, ReviewFlow firstOne) {
|
||||
Map<String, Object> record = selectDeclareRecord(module, pk);
|
||||
Integer collegeId = intValue(record, "collegeId");
|
||||
Integer declareUserId = intValue(record, "userId");
|
||||
List<Integer> reviewers = reviewerResolver.resolve(firstOne, collegeId, declareUserId);
|
||||
if (reviewers == null || reviewers.isEmpty()) {
|
||||
throw new BusinessException("模块 " + module + " 主键 " + pk + " 第 "
|
||||
+ (firstOne.getLevel() == null ? "?" : firstOne.getLevel())
|
||||
+ " 级审批人解析结果为空,未生成待办");
|
||||
}
|
||||
ReviewList reviewList = new ReviewList();
|
||||
reviewList.setPk(pk);
|
||||
reviewList.setModule(module);
|
||||
reviewList.setUserId(firstOne.getReviewUserId());
|
||||
reviewList.setUserId(reviewers.get(0));
|
||||
reviewList.setSortNumber(0);
|
||||
reviewListService.save(reviewList);
|
||||
}
|
||||
|
||||
/** 读申报记录的申报人与学院标识;只有 4 张在跑表有 college_id */
|
||||
private Map<String, Object> selectDeclareRecord(String module, Integer pk) {
|
||||
if (COLLEGE_MODULES.contains(module)) {
|
||||
return declareRecordMapper.selectReviewContext(module, pk);
|
||||
}
|
||||
return declareRecordMapper.selectRecord(module, pk);
|
||||
}
|
||||
|
||||
private Integer intValue(Map<String, Object> record, String key) {
|
||||
if (record == null) {
|
||||
return null;
|
||||
}
|
||||
Object value = record.get(key);
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(value.toString().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.gxwebsoft.gxmu.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.gxwebsoft.common.core.exception.BusinessException;
|
||||
import com.gxwebsoft.common.system.entity.Organization;
|
||||
import com.gxwebsoft.common.system.entity.Role;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.entity.UserRole;
|
||||
import com.gxwebsoft.common.system.service.OrganizationService;
|
||||
import com.gxwebsoft.common.system.service.RoleService;
|
||||
import com.gxwebsoft.common.system.service.UserRoleService;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import com.gxwebsoft.gxmu.entity.ReviewFlow;
|
||||
import com.gxwebsoft.gxmu.service.ReviewerResolver;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审批人解析实现(ADR 0006)。
|
||||
*
|
||||
* <p>规则语法:{@code ;} 分隔可拼装多段,{@code USER:<userId>} 固定人,
|
||||
* {@code COLLEGE_ROLE:<roleCode>} 在申报人学院的组织子树内找持有该角色的用户。
|
||||
* reviewerRule 为空回退 reviewUserId,存量配置零迁移。多段取并集、按 userId 升序。</p>
|
||||
*
|
||||
* <p>按学院解析走不通时抛 {@link BusinessException},消息带学院名、第几级与缺什么:
|
||||
* 组织树与角色没维护时是「发不出单」,而不是「发出无人认领的单」。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-11-10
|
||||
*/
|
||||
@Service
|
||||
public class ReviewerResolverImpl implements ReviewerResolver {
|
||||
private static final String RULE_SEPARATOR = ";";
|
||||
private static final String PREFIX_USER = "USER:";
|
||||
private static final String PREFIX_COLLEGE_ROLE = "COLLEGE_ROLE:";
|
||||
|
||||
@Resource
|
||||
private OrganizationService organizationService;
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Resource
|
||||
private UserRoleService userRoleService;
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Override
|
||||
public List<Integer> resolve(ReviewFlow flow, Integer collegeId, Integer declareUserId) {
|
||||
if (flow == null) {
|
||||
throw new BusinessException("审批链配置不存在,无法解析审批人");
|
||||
}
|
||||
String rule = flow.getReviewerRule();
|
||||
if (StrUtil.isBlank(rule)) {
|
||||
if (flow.getReviewUserId() == null) {
|
||||
throw new BusinessException("第 " + levelOf(flow) + " 级未配置审批人规则,也未指定审批人,无法发起审批");
|
||||
}
|
||||
return Collections.singletonList(flow.getReviewUserId());
|
||||
}
|
||||
Set<Integer> reviewers = new TreeSet<>();
|
||||
for (String rawSegment : rule.split(RULE_SEPARATOR)) {
|
||||
String segment = rawSegment.trim();
|
||||
if (segment.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (segment.startsWith(PREFIX_USER)) {
|
||||
reviewers.add(parseFixedUser(flow, segment));
|
||||
} else if (segment.startsWith(PREFIX_COLLEGE_ROLE)) {
|
||||
String roleCode = segment.substring(PREFIX_COLLEGE_ROLE.length()).trim();
|
||||
if (StrUtil.isBlank(roleCode)) {
|
||||
throw unrecognizedRule(flow, segment);
|
||||
}
|
||||
reviewers.addAll(resolveByCollegeRole(flow, collegeId, declareUserId, roleCode));
|
||||
} else {
|
||||
throw unrecognizedRule(flow, segment);
|
||||
}
|
||||
}
|
||||
if (reviewers.isEmpty()) {
|
||||
throw new BusinessException("第 " + levelOf(flow) + " 级审批人解析结果为空(规则:" + rule + ")");
|
||||
}
|
||||
return new ArrayList<>(reviewers);
|
||||
}
|
||||
|
||||
/** USER:<userId> */
|
||||
private Integer parseFixedUser(ReviewFlow flow, String segment) {
|
||||
String value = segment.substring(PREFIX_USER.length()).trim();
|
||||
try {
|
||||
return Integer.valueOf(value);
|
||||
} catch (NumberFormatException e) {
|
||||
throw unrecognizedRule(flow, segment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* COLLEGE_ROLE:<roleCode>:在学院组织子树内找持该角色的在岗用户。
|
||||
*/
|
||||
private Set<Integer> resolveByCollegeRole(ReviewFlow flow, Integer collegeId, Integer declareUserId,
|
||||
String roleCode) {
|
||||
Integer level = flow.getLevel();
|
||||
if (collegeId == null) {
|
||||
throw new BusinessException("第 " + levelOf(flow) + " 级审批人规则按学院解析,但申报记录缺少学院标识"
|
||||
+ (declareUserId == null ? "" : "(申报人 " + declareUserId + ")")
|
||||
+ ",无法定位学院;请先按学生档案回填申报记录的学院标识");
|
||||
}
|
||||
String collegeName = collegeName(collegeId);
|
||||
Set<Integer> organizationIds = subtreeOf(collegeId);
|
||||
Set<Integer> roleIds = roleIdsOf(roleCode);
|
||||
if (roleIds.isEmpty()) {
|
||||
throw new BusinessException("学院「" + collegeName + "」第 " + level + " 级审批人无法解析:" + roleCode
|
||||
+ " 角色在系统中不存在,请先维护角色");
|
||||
}
|
||||
List<UserRole> userRoles = userRoleService.list(new LambdaQueryWrapper<UserRole>()
|
||||
.select(UserRole::getUserId, UserRole::getRoleId)
|
||||
.in(UserRole::getRoleId, roleIds));
|
||||
List<Integer> roleUserIds = userRoles == null ? Collections.emptyList()
|
||||
: userRoles.stream().map(UserRole::getUserId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
if (roleUserIds.isEmpty()) {
|
||||
throw new BusinessException("学院「" + collegeName + "」第 " + level + " 级审批人无法解析:没有用户持有角色 "
|
||||
+ roleCode + ",请先维护该角色的审核人");
|
||||
}
|
||||
List<User> users = userService.list(new LambdaQueryWrapper<User>()
|
||||
.select(User::getUserId)
|
||||
.in(User::getUserId, roleUserIds)
|
||||
.in(User::getOrganizationId, organizationIds)
|
||||
.eq(User::getStatus, 0));
|
||||
if (users == null || users.isEmpty()) {
|
||||
throw new BusinessException("学院「" + collegeName + "」第 " + level + " 级审批人无法解析:该学院组织子树内"
|
||||
+ "缺少持有角色 " + roleCode + " 的在岗用户,请先在组织树上维护审核人");
|
||||
}
|
||||
return users.stream().map(User::getUserId).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
/** 学院及其全部下级组织 id(参照 BaseController#resolveCurrentAndChildOrganizationIds) */
|
||||
private Set<Integer> subtreeOf(Integer organizationId) {
|
||||
Set<Integer> result = new LinkedHashSet<>();
|
||||
result.add(organizationId);
|
||||
List<Organization> organizations = organizationService.list(new LambdaQueryWrapper<Organization>()
|
||||
.select(Organization::getOrganizationId, Organization::getParentId));
|
||||
if (organizations == null || organizations.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
Map<Integer, List<Integer>> childrenMap = new HashMap<>();
|
||||
for (Organization organization : organizations) {
|
||||
if (organization.getOrganizationId() == null) {
|
||||
continue;
|
||||
}
|
||||
childrenMap.computeIfAbsent(organization.getParentId(), key -> new ArrayList<>())
|
||||
.add(organization.getOrganizationId());
|
||||
}
|
||||
Deque<Integer> queue = new ArrayDeque<>();
|
||||
queue.add(organizationId);
|
||||
while (!queue.isEmpty()) {
|
||||
Integer current = queue.poll();
|
||||
for (Integer child : childrenMap.getOrDefault(current, Collections.emptyList())) {
|
||||
if (result.add(child)) {
|
||||
queue.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<Integer> roleIdsOf(String roleCode) {
|
||||
List<Role> roles = roleService.list(new LambdaQueryWrapper<Role>()
|
||||
.select(Role::getRoleId, Role::getRoleCode)
|
||||
.eq(Role::getRoleCode, roleCode));
|
||||
if (roles == null) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return roles.stream().map(Role::getRoleId).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private String collegeName(Integer collegeId) {
|
||||
Organization organization = organizationService.getById(collegeId);
|
||||
if (organization == null || StrUtil.isBlank(organization.getOrganizationName())) {
|
||||
return "未知学院(" + collegeId + ")";
|
||||
}
|
||||
return organization.getOrganizationName();
|
||||
}
|
||||
|
||||
private BusinessException unrecognizedRule(ReviewFlow flow, String segment) {
|
||||
return new BusinessException("第 " + levelOf(flow) + " 级审批人规则无法识别:" + segment
|
||||
+ "(仅支持 USER:<userId> 与 COLLEGE_ROLE:<roleCode>)");
|
||||
}
|
||||
|
||||
private String levelOf(ReviewFlow flow) {
|
||||
return flow.getLevel() == null ? "?" : String.valueOf(flow.getLevel());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user