From ab708eb1bbc1d861ee2fb67b00a11f3317c246dc Mon Sep 17 00:00:00 2001 From: song-cc-rock Date: Fri, 15 May 2026 16:00:27 +0800 Subject: [PATCH] feat: Approval detail --- .../cn/cordys/common/constants/FormKey.java | 15 ++ .../approval/aspect/HitApprovalAspect.java | 2 +- .../crm/approval/domain/ApprovalRecord.java | 3 + .../crm/approval/domain/ApprovalTask.java | 3 + .../approval/dto/ApprovalInstanceDetail.java | 8 +- .../crm/approval/dto/ApprovalRecordNode.java | 3 + .../crm/approval/dto/ApprovalTaskNode.java | 2 +- .../dto/ResourceSnapshotApprovalParam.java | 12 + .../mapper/ExtApprovalInstanceMapper.java | 10 + .../mapper/ExtApprovalInstanceMapper.xml | 7 + .../service/ApprovalActionService.java | 35 ++- .../approval/service/ApprovalFlowService.java | 7 +- .../service/ApprovalInstanceService.java | 215 ++++++++++-------- .../service/ApprovalResourceService.java | 67 ++++-- .../service/ContractInvoiceService.java | 23 +- .../crm/contract/service/ContractService.java | 19 ++ .../service/OpportunityQuotationService.java | 17 ++ .../cn/cordys/crm/system/dto/UserSimple.java | 17 ++ .../crm/system/service/UserExtendService.java | 16 ++ .../migration/1.7.0/ddl/V1.7.0_2__ga_ddl.sql | 2 + .../init_approval_todo_cc_initiated_test.sql | 8 +- .../dml/init_approval_todo_list_test.sql | 8 +- .../dml/init_approval_todo_processed_test.sql | 6 +- .../dml/init_resource_approval_test.sql | 12 +- 24 files changed, 370 insertions(+), 147 deletions(-) create mode 100644 backend/crm/src/main/java/cn/cordys/crm/approval/dto/ResourceSnapshotApprovalParam.java create mode 100644 backend/crm/src/main/java/cn/cordys/crm/system/dto/UserSimple.java diff --git a/backend/crm/src/main/java/cn/cordys/common/constants/FormKey.java b/backend/crm/src/main/java/cn/cordys/common/constants/FormKey.java index 6a54b1957f..830efeb1bb 100644 --- a/backend/crm/src/main/java/cn/cordys/common/constants/FormKey.java +++ b/backend/crm/src/main/java/cn/cordys/common/constants/FormKey.java @@ -1,9 +1,11 @@ package cn.cordys.common.constants; import lombok.Getter; +import org.apache.commons.lang3.Strings; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -79,4 +81,17 @@ public enum FormKey { public static List allKeys() { return Arrays.stream(FormKey.values()).map(FormKey::getKey).collect(Collectors.toList()); } + + public static FormKey ofKey(String key) { + for (FormKey formKey : FormKey.values()) { + if (Strings.CI.equals(formKey.getKey(), key)) { + return formKey; + } + } + return null; + } + + public boolean hasSnapshot() { + return Strings.CI.equalsAny(this.key, CONTRACT.getKey(), INVOICE.getKey(), QUOTATION.getKey()); + } } diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/aspect/HitApprovalAspect.java b/backend/crm/src/main/java/cn/cordys/crm/approval/aspect/HitApprovalAspect.java index 6b31898f47..1b9f024ed2 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/aspect/HitApprovalAspect.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/aspect/HitApprovalAspect.java @@ -75,7 +75,7 @@ public void handleHitApproval(JoinPoint joinPoint, Object retValue) { if (hit) { // 命中审批流, 修改业务资源审批状态为待提审 - approvalResourceService.updateApprovalStatus(annotation.formKey(), resourceId, ApprovalStatus.PENDING.name()); + approvalResourceService.updateResourceApprovalStatus(annotation.formKey(), resourceId, ApprovalStatus.PENDING.name()); } } catch (Exception e) { log.error("审批流执行时机匹配失败,error:{}", e.getMessage(), e); diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalRecord.java b/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalRecord.java index 5ce1db1e97..04f744dbaf 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalRecord.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalRecord.java @@ -18,6 +18,9 @@ public class ApprovalRecord extends BaseModel { @Schema(description = "任务ID") private String taskId; + @Schema(description = "节点轮次") + private Integer nodeRound; + @Schema(description = "节点ID") private String nodeId; diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalTask.java b/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalTask.java index 6d3806bfea..f6bd990990 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalTask.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/domain/ApprovalTask.java @@ -15,6 +15,9 @@ public class ApprovalTask extends BaseModel { @Schema(description = "节点ID") private String nodeId; + @Schema(description = "节点轮次") + private Integer nodeRound; + @Schema(description = "审批实例ID") private String instanceId; diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalInstanceDetail.java b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalInstanceDetail.java index c1e34042d4..cd79ce4c4f 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalInstanceDetail.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalInstanceDetail.java @@ -14,16 +14,16 @@ public class ApprovalInstanceDetail { @Schema(description = "提交人ID") private String submitterId; + @Schema(description = "提交人头像") + private String submitAvatar; + @Schema(description = "提交人") private String submitter; @Schema(description = "提交时间") private Long submitTime; - @Schema(description = "审批结果") - private String result; - - @Schema(description = "审批实例状态") + @Schema(description = "审批状态") private String approvalStatus; @Schema(description = "当前节点ID") diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalRecordNode.java b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalRecordNode.java index aca23e4d2e..dd654efac5 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalRecordNode.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalRecordNode.java @@ -17,6 +17,9 @@ public class ApprovalRecordNode { @Schema(description = "节点ID") private String nodeId; + @Schema(description = "节点轮次") + private Integer nodeRound; + @Schema(description = "审批状态") private String approvalStatus; diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalTaskNode.java b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalTaskNode.java index e655b03943..c0ad3c1ba9 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalTaskNode.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ApprovalTaskNode.java @@ -43,5 +43,5 @@ public class ApprovalTaskNode { private Long approvalTime; @Schema(description = "是否加签任务") - private boolean isAddSign; + private boolean sign; } diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ResourceSnapshotApprovalParam.java b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ResourceSnapshotApprovalParam.java new file mode 100644 index 0000000000..15d5588542 --- /dev/null +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/dto/ResourceSnapshotApprovalParam.java @@ -0,0 +1,12 @@ +package cn.cordys.crm.approval.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ResourceSnapshotApprovalParam { + + private String resourceId; + private String approvalStatus; +} diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.java b/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.java index 563d7ee3d3..99d99b1ccd 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.java @@ -22,4 +22,14 @@ void updateApprovalStatus(@Param("sourceTable") String sourceTable, @Param("id") String selectBusinessName(@Param("sourceTable") String sourceTable, @Param("id") String id); String getResourceOwner(@Param("sourceTable")String sourceTable, @Param("id")String id); + + /** + * 获取节点下一个执行轮次 + * 从 approval_task 和 approval_record 中一起获取,取最大轮次 +1 + * + * @param instanceId 审批实例ID + * @param nodeId 节点ID + * @return 下一个轮次 + */ + Integer getNextNodeRound(@Param("instanceId") String instanceId, @Param("nodeId") String nodeId); } diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.xml b/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.xml index 1b536c2cff..73ebf37cee 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.xml +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/mapper/ExtApprovalInstanceMapper.xml @@ -15,4 +15,11 @@ + + \ No newline at end of file diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalActionService.java b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalActionService.java index ece1846d37..bbefa920e1 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalActionService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalActionService.java @@ -6,6 +6,7 @@ import cn.cordys.common.exception.GenericException; import cn.cordys.common.uid.IDGenerator; import cn.cordys.common.util.BeanUtils; +import cn.cordys.common.util.CommonBeanFactory; import cn.cordys.common.util.Translator; import cn.cordys.crm.approval.constants.*; import cn.cordys.crm.approval.domain.*; @@ -92,11 +93,11 @@ public void sign(ApprovalAddSignRequest request, String userId, String orgId) { throw new GenericException(Translator.get("no.operation.permission")); } ApprovalInstance instance = approvalInstanceMapper.selectByPrimaryKey(request.getInstanceId()); + // 刷新被加签任务状态 && 插入审批记录 + ApprovalTask currentTask = saveActionTask(request, ApprovalAction.SIGN, userId, orgId, ApprovalAddSignType.valueOf(request.getType())); // 加签操作的待办任务 - ApprovalTask appendActionTask = appendSignTask(request, userId); + ApprovalTask appendActionTask = appendSignTask(request, userId, currentTask.getNodeRound()); ApprovalAddSignTask addSignTask = saveAddSignTask(request, appendActionTask.getId()); - // 刷新被加签任务状态 && 插入审批记录 - saveActionTask(request, ApprovalAction.SIGN, userId, orgId, ApprovalAddSignType.valueOf(request.getType())); // 之后加签(多人或签), 需要刷新实例当前审批节点 if (ApprovalAddSignType.valueOf(request.getType()) == ApprovalAddSignType.AFTER && isMultiAnyMode(appendActionTask.getNodeId(), userId, orgId)) { ApprovalNodeResponse nextNode = approvalFlowService.getTaskNextNode(appendActionTask, instance, orgId); @@ -231,10 +232,11 @@ private void saveInstanceAttachment(List attachmentIds, String instanceI * @param request 加签参数 * @param userId 当前用户 */ - private ApprovalTask appendSignTask(ApprovalActionRequest request, String userId) { + private ApprovalTask appendSignTask(ApprovalActionRequest request, String userId, int round) { ApprovalTask approvalTask = new ApprovalTask(); BeanUtils.copyBean(approvalTask, request); approvalTask.setId(IDGenerator.nextStr()); + approvalTask.setNodeRound(round); approvalTask.setCreateTime(System.currentTimeMillis()); approvalTask.setUpdateTime(System.currentTimeMillis()); approvalTask.setCreateUser(userId); @@ -287,6 +289,7 @@ private void saveApprovalRecord(ApprovalTask currentTask, String comment, List approvalTasks = getNodeApproverTasks((ApprovalNodeApproverResponse) node, instance.getId(), currentUserId, ApprovalTaskType.NL.name()); List ccTasks = getNodeCcTasks((ApprovalNodeApproverResponse) node, instance.getId(), currentUserId); @@ -882,9 +893,10 @@ public List getNodeApproverTasks(ApprovalNodeApproverResponse appr if (approverNode == null) { return approvalTasks; } + Integer nextRound = extApprovalInstanceMapper.getNextNodeRound(instanceId, approverNode.getId()); if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(approverNode.getApproverList())) { approverNode.getApproverList().forEach(approver -> { - ApprovalTask approvalTask = buildTask(approverNode.getId(), instanceId, approver, taskType, userId); + ApprovalTask approvalTask = buildTask(approverNode.getId(), instanceId, approver, taskType, userId, nextRound); approvalTasks.add(approvalTask); }); } @@ -896,19 +908,21 @@ public List getNodeCcTasks(ApprovalNodeApproverResponse approverNo if (approverNode == null) { return approvalTasks; } + Integer nextRound = extApprovalInstanceMapper.getNextNodeRound(instanceId, approverNode.getId()); if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(approverNode.getCcList())) { approverNode.getCcList().forEach(cc -> { - ApprovalTask approvalTask = buildTask(approverNode.getId(), instanceId, cc, ApprovalTaskType.CC.name(), userId); + ApprovalTask approvalTask = buildTask(approverNode.getId(), instanceId, cc, ApprovalTaskType.CC.name(), userId, nextRound); approvalTasks.add(approvalTask); }); } return approvalTasks; } - public ApprovalTask buildTask(String nodeId, String instanceId, String approverId, String taskType, String currentUserId) { + public ApprovalTask buildTask(String nodeId, String instanceId, String approverId, String taskType, String currentUserId, Integer round) { ApprovalTask approvalTask = new ApprovalTask(); approvalTask.setId(IDGenerator.nextStr()); approvalTask.setNodeId(nodeId); + approvalTask.setNodeRound(round); approvalTask.setInstanceId(instanceId); approvalTask.setApproverId(approverId); approvalTask.setStatus(ApprovalStatus.APPROVING.name()); @@ -934,14 +948,15 @@ private List getNodeApproverTasks(String currentNodeId, String ins List approvalTasks = new ArrayList<>(); ApprovalNodeApprover approvalNodeApprover = approvalNodeApproverMapper.selectByPrimaryKey(currentNodeId); List approvers = approvalFlowService.getCurrentNodeApproverList(currentNodeId, userId, currentOrgId); + Integer nextRound = extApprovalInstanceMapper.getNextNodeRound(instanceId, currentNodeId); if (Strings.CI.equals(approvalNodeApprover.getMultiApproverMode(), MultiApproverModeEnum.SEQUENTIAL.name()) || approvers.size() == 1) { // 单人或者依次审批, 只会产生一条待办任务 User approverUser = approvers.getFirst(); - approvalTasks.add(buildTask(currentNodeId, instanceId, approverUser.getId(), taskType, userId)); + approvalTasks.add(buildTask(currentNodeId, instanceId, approverUser.getId(), taskType, userId, nextRound)); } else { // 多人审批, 且为会签或签方式 approvers.forEach(approver -> { - ApprovalTask approvalTask = buildTask(currentNodeId, instanceId, approver.getId(), taskType, userId); + ApprovalTask approvalTask = buildTask(currentNodeId, instanceId, approver.getId(), taskType, userId, nextRound); approvalTasks.add(approvalTask); }); } diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalFlowService.java b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalFlowService.java index 120b6d4f48..3245e32ebc 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalFlowService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalFlowService.java @@ -31,6 +31,7 @@ import cn.cordys.crm.approval.dto.response.*; import cn.cordys.crm.approval.log.ApprovalFlowLogDTO; import cn.cordys.crm.approval.mapper.ExtApprovalFlowMapper; +import cn.cordys.crm.approval.mapper.ExtApprovalInstanceMapper; import cn.cordys.crm.system.domain.Department; import cn.cordys.crm.system.domain.OrganizationUser; import cn.cordys.crm.system.domain.User; @@ -99,7 +100,7 @@ public class ApprovalFlowService { @Resource private UserViewService userViewService; @Resource - private BaseMapper approvalTaskMapper; + private ExtApprovalInstanceMapper extApprovalInstanceMapper; /** * 根据表单类型获取审批流状态权限配置 @@ -1653,7 +1654,7 @@ private ApprovalNodeResponse getNextNodeWithExceptionHandler(ApprovalInstance in return getNextNodeWithExceptionHandler(instance, nodeId, fieldValues, currentOrgId); } if (ApprovalTypeEnum.valueOf(nextApproverNode.getApprovalType()) == ApprovalTypeEnum.AUTO_REJECT) { - // 自动驳回, 插入审批记录, + // 自动驳回, 插入审批记录 saveAutoRecord(instance.getId(), nodeId, ApprovalStatus.UNAPPROVED, null); ApprovalNodeExceptionResponse exNode = BeanUtils.copyBean(new ApprovalNodeExceptionResponse(), nextApproverNode); exNode.setNodeType(ApprovalNodeTypeEnum.EXCEPTION.name()); @@ -1706,10 +1707,12 @@ private ApprovalNodeResponse getNextNodeWithExceptionHandler(ApprovalInstance in * @param approvalStatus 审批状态 */ private void saveAutoRecord(String instanceId, String nodeId, ApprovalStatus approvalStatus, String comment) { + Integer nextRound = extApprovalInstanceMapper.getNextNodeRound(instanceId, nodeId); ApprovalRecord record = new ApprovalRecord(); record.setId(IDGenerator.nextStr()); record.setInstanceId(instanceId); record.setNodeId(nodeId); + record.setNodeRound(nextRound); record.setResult(approvalStatus.name()); if (StringUtils.isBlank(comment)) { record.setComment(approvalStatus == ApprovalStatus.APPROVED ? Translator.get("auto.approval.passed") : Translator.get("auto.approval.rejected")); diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalInstanceService.java b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalInstanceService.java index 42b86561da..b707ac8028 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalInstanceService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalInstanceService.java @@ -1,6 +1,8 @@ package cn.cordys.crm.approval.service; +import cn.cordys.common.constants.FormKey; import cn.cordys.common.util.BeanUtils; +import cn.cordys.common.util.CommonBeanFactory; import cn.cordys.crm.approval.constants.ApprovalStatus; import cn.cordys.crm.approval.constants.ApprovalTaskType; import cn.cordys.crm.approval.domain.*; @@ -10,19 +12,17 @@ import cn.cordys.crm.approval.dto.ApprovalTaskNode; import cn.cordys.crm.approval.mapper.ExtApprovalInstanceMapper; import cn.cordys.crm.system.domain.Attachment; +import cn.cordys.crm.system.dto.UserSimple; import cn.cordys.crm.system.service.AttachmentService; +import cn.cordys.crm.system.service.UserExtendService; import cn.cordys.mybatis.BaseMapper; import cn.cordys.mybatis.lambda.LambdaQueryWrapper; import jakarta.annotation.Resource; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.Strings; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import static cn.cordys.crm.approval.service.ApprovalResourceService.FORM_APPROVAL_TABLE; @@ -50,6 +50,8 @@ public class ApprovalInstanceService { private ExtApprovalInstanceMapper extApprovalInstanceMapper; @Resource private AttachmentService attachmentService; + @Resource + private UserExtendService userExtendService; /** * 获取资源最新审批实例详情 @@ -61,97 +63,52 @@ public ApprovalInstanceDetail getLatestApprovalInstanceDetail(String resourceId) if (latestInstance == null) { return null; } - + Map simpleUserMap = userExtendService.getAllUserSimpleMap(); ApprovalInstanceDetail instanceDetail = BeanUtils.copyBean(new ApprovalInstanceDetail(), latestInstance); - List approvalTasks = getFilteredTasks(latestInstance); - if (CollectionUtils.isEmpty(approvalTasks)) { - instanceDetail.setNodes(null); - return instanceDetail; + if (simpleUserMap.containsKey(instanceDetail.getSubmitterId())) { + instanceDetail.setSubmitAvatar(simpleUserMap.get(instanceDetail.getSubmitterId()).getAvatar()); + instanceDetail.setSubmitter(simpleUserMap.get(instanceDetail.getSubmitterId()).getName()); } - - Map> signTaskGroupMap = queryAddSignTasks(approvalTasks); - Map returnRecordMap = queryReturnRecords(approvalTasks); - Map taskRecordMap = queryTaskRecords(approvalTasks, signTaskGroupMap); - Map> elementAttachmentsMap = queryAttachments(approvalTasks, taskRecordMap, signTaskGroupMap); - - instanceDetail.setNodes(buildApprovalRecordNodeList(approvalTasks, signTaskGroupMap, returnRecordMap, taskRecordMap, elementAttachmentsMap)); + List tasks = getAllTasks(latestInstance); + List records = getAllRecords(latestInstance); + Map> elementAttachmentsMap = queryAttachments(records); + instanceDetail.setNodes(buildApprovalRecordNodeList(tasks, records, elementAttachmentsMap, simpleUserMap)); return instanceDetail; } /** - * 获取最新审批实例 + * 获取资源最新审批实例 */ public ApprovalInstance getLatestInstance(String resourceId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ApprovalInstance::getResourceId, resourceId).orderByDesc(ApprovalInstance::getSubmitTime); + wrapper.eq(ApprovalInstance::getResourceId, resourceId); List list = approvalInstanceMapper.selectListByLambda(wrapper); - return CollectionUtils.isEmpty(list) ? null : list.getFirst(); + return CollectionUtils.isEmpty(list) ? null : list.stream().sorted(Comparator.comparing(ApprovalInstance::getSubmitTime)).toList().getLast(); } /** - * 获取过滤后的任务列表(按节点分组取最新) + * 获取所有的任务列表 */ - private List getFilteredTasks(ApprovalInstance latestInstance) { + private List getAllTasks(ApprovalInstance latestInstance) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ApprovalTask::getInstanceId, latestInstance.getId()).orderByDesc(ApprovalTask::getCreateTime); - List allTasks = approvalTaskMapper.selectListByLambda(wrapper); - if (CollectionUtils.isEmpty(allTasks)) { - return null; - } - return allTasks.stream().collect(Collectors.groupingBy(ApprovalTask::getNodeId)) - .values().stream().map(List::getFirst) - .sorted(Comparator.comparing(ApprovalTask::getCreateTime)).toList(); - } - - /** - * 查询加签任务并分组 - */ - private Map> queryAddSignTasks(List tasks) { - List ids = tasks.stream().filter(task -> Strings.CI.equals(task.getType(), ApprovalTaskType.SN.name())).map(ApprovalTask::getId).toList(); - if (CollectionUtils.isEmpty(ids)) { - return Map.of(); - } - List list = approvalAddSignTaskMapper.selectListByLambda( - new LambdaQueryWrapper().in(ApprovalAddSignTask::getTaskId, ids)); - return list.stream().collect(Collectors.groupingBy(ApprovalAddSignTask::getTaskId)); - } - - /** - * 查询退回记录 - */ - private Map queryReturnRecords(List tasks) { - List ids = tasks.stream().filter(task -> Strings.CI.equals(task.getType(), ApprovalTaskType.BK.name())).map(ApprovalTask::getId).toList(); - if (CollectionUtils.isEmpty(ids)) { - return Map.of(); - } - List list = approvalReturnBackRecordMapper.selectListByLambda( - new LambdaQueryWrapper().in(ApprovalReturnBackRecord::getTaskId, ids)); - return list.stream().collect(Collectors.toMap(ApprovalReturnBackRecord::getTaskId, r -> r, (v1, v2) -> v1)); + wrapper.eq(ApprovalTask::getInstanceId, latestInstance.getId()); + return approvalTaskMapper.selectListByLambda(wrapper); } /** - * 查询任务执行记录 + * 获取所有的记录列表 */ - private Map queryTaskRecords(List tasks, Map> signTaskGroupMap) { - List taskIds = tasks.stream().map(ApprovalTask::getId).toList(); - List signIds = signTaskGroupMap.values().stream().flatMap(List::stream) - .map(ApprovalAddSignTask::getTaskId).toList(); - List records = approvalRecordMapper.selectByIds(ListUtils.union(taskIds, signIds)); - return records.stream().collect(Collectors.toMap(ApprovalRecord::getTaskId, r -> r)); + private List getAllRecords(ApprovalInstance latestInstance) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ApprovalRecord::getInstanceId, latestInstance.getId()); + return approvalRecordMapper.selectListByLambda(wrapper); } /** - * 查询附件信息并按元素ID分组 + * 查询附件信息并按节点ID分组 */ - private Map> queryAttachments(List tasks, Map taskRecordMap, - Map> signTaskGroupMap) { - List elementIds = new ArrayList<>(); - // elementIds.addAll(taskRecordMap.values().stream().map(ApprovalRecord::getId).toList()); - // elementIds.addAll(signTaskGroupMap.values().stream().flatMap(List::stream) - // .filter(t -> ApprovalStatus.APPROVING.name().equals(t.getStatus())).map(ApprovalAddSignTask::getId).toList()); - // elementIds.addAll(tasks.stream().filter(t -> t.getIsReturn() && ApprovalStatus.APPROVING.name().equals(t.getTaskStatus())) - // .map(ApprovalTask::getId).toList()); - + private Map> queryAttachments(List records) { + List elementIds = new ArrayList<>(records.stream().map(ApprovalRecord::getId).toList()); if (CollectionUtils.isEmpty(elementIds)) { return Map.of(); } @@ -171,16 +128,14 @@ private Map> queryAttachments(List tasks, /** * 构建审批记录节点列表 - * @param approvalTasks 审批任务列表 - * @param signTaskGroupMap 加签任务分组 - * @param returnRecordMap 退回记录映射 - * @param taskRecordMap 任务执行记录映射 - * @param attachmentsMap 元素附件映射 + * @param tasks 审批任务列表 + * @param records 记录列表 + * @param attachmentsMap 附件集合 + * @param simpleUserMap 用户信息集合 * @return 审批记录节点列表 */ - private List buildApprovalRecordNodeList(List approvalTasks, Map> signTaskGroupMap, Map returnRecordMap, - Map taskRecordMap, - Map> attachmentsMap) { + private List buildApprovalRecordNodeList(List tasks, List records, + Map> attachmentsMap, Map simpleUserMap) { /* * 处理不同类型的任务: * 1. 加签任务: 需单独查询加签任务表, 并按位次配置追加。 @@ -188,22 +143,96 @@ private List buildApprovalRecordNodeList(List * 3. 抄送任务: 查询该任务节点下所有的抄送任务, 并一起返回。 */ List nodes = new ArrayList<>(); - Map> nodeTasks = approvalTasks.stream().collect(Collectors.groupingBy(ApprovalTask::getNodeId)); - nodeTasks.forEach((nodeId, tasks) -> { - List ccTasks = tasks.stream().filter(task -> ApprovalTaskType.valueOf(task.getType()) == ApprovalTaskType.CC).toList(); - List ccNodes = ccTasks.stream().map(ccTask -> new ApprovalCcNode(ccTask.getApproverId(), ccTask.getApproverId(), ccTask.getApproverId())).toList(); - List normalTasks = tasks.stream().filter(task -> ApprovalTaskType.valueOf(task.getType()) != ApprovalTaskType.NL).toList(); - List taskNodes = normalTasks.stream().map(task -> buildTaskNode(task, taskRecordMap, attachmentsMap)).toList(); - ApprovalRecordNode recordNode = ApprovalRecordNode.builder().ccNodes(ccNodes).taskNodes(taskNodes).build(); - nodes.add(recordNode); + Map recordMap = records.stream().collect(Collectors.toMap(ApprovalRecord::getTaskId, r -> r)); + List nTasks = tasks.stream().filter(task -> ApprovalTaskType.valueOf(task.getType()) == ApprovalTaskType.NL).toList(); + Map nodeMaxRoundMap = mergeNodeMaxRound(nTasks, records); + List sortHisNodes = sortNodeRoundMap(nodeMaxRoundMap, nTasks, records); + sortHisNodes.forEach(hisNode -> { + Integer maxRound = nodeMaxRoundMap.get(hisNode); + // 获取节点下最后一轮正常待办任务 + List nlTasks = tasks.stream().filter(task -> ApprovalTaskType.valueOf(task.getType()) == ApprovalTaskType.NL + && Strings.CI.equals(task.getNodeId(), hisNode) && task.getNodeRound().equals(maxRound)).toList(); + List taskNodes = nlTasks.stream().map(nTask -> buildTaskNode(nTask, recordMap, attachmentsMap, simpleUserMap)).toList(); + // 获取节点下最后一轮抄送任务 + List ccTasks = tasks.stream().filter(task -> ApprovalTaskType.valueOf(task.getType()) == ApprovalTaskType.CC + && Strings.CI.equals(task.getNodeId(), hisNode) && task.getNodeRound().equals(maxRound)).toList(); + List ccNodes = ccTasks.stream().map(cc -> { + ApprovalCcNode ccNode = new ApprovalCcNode(); + ccNode.setCcUserId(cc.getApproverId()); + if (simpleUserMap.containsKey(cc.getApproverId())) { + ccNode.setCcUserName(simpleUserMap.get(cc.getApproverId()).getName()); + ccNode.setCcUserAvatar(simpleUserMap.get(cc.getApproverId()).getAvatar()); + } + return ccNode; + }).toList(); + ApprovalRecordNode recordNode = ApprovalRecordNode.builder().nodeId(hisNode).nodeRound(maxRound).taskNodes(taskNodes).ccNodes(ccNodes).build(); + nodes.addLast(recordNode); }); return nodes; } - private ApprovalTaskNode buildTaskNode(ApprovalTask task, Map taskRecordMap, Map> attachmentsMap) { + /** + * 获取最终的节点顺序 (同一节点可能执行多轮) + * @param tasks 节点待办 + * @param records 节点执行记录 + * @return 节点ID -> 最大轮次 + */ + private Map mergeNodeMaxRound(List tasks, List records) { + // 待办中提取节点和最大轮次 + Map taskNodeMaxRoundMap = tasks.stream().collect(Collectors.toMap(ApprovalTask::getNodeId, ApprovalTask::getNodeRound, Math::max)); + // 记录中提取节点和最大轮次 + Map recordNodeMaxRoundMap = records.stream().collect(Collectors.toMap(ApprovalRecord::getNodeId, ApprovalRecord::getNodeRound, Math::max)); + // 合并节点集合, 按照最大轮次保留 + Map mergedMap = new HashMap<>(recordNodeMaxRoundMap); + taskNodeMaxRoundMap.forEach((nodeId, nodeRound) -> { + mergedMap.merge(nodeId, nodeRound, Math::max); + }); + return mergedMap; + } + + /** + * 轮次节点的最终执行排序 + * 按照该节点的最大轮次中的最小创建时间来排序,早的在前面 + * @param nodeRoundMap 节点ID -> 最大轮次 + * @param tasks 所有待办 + * @param records 所有记录 + * @return 排序后的节点ID列表 + */ + private List sortNodeRoundMap(Map nodeRoundMap, List tasks, List records) { + // 找出每个节点最大轮次中的最小创建时间 + Map nodeMinCreateTimeMap = new HashMap<>(nodeRoundMap.size()); + + // 从待办中找:筛选出每个节点最大轮次的待办,取最小创建时间 + tasks.forEach(task -> { + Integer maxRound = nodeRoundMap.get(task.getNodeId()); + if (task.getNodeRound() != null && task.getNodeRound().equals(maxRound)) { + nodeMinCreateTimeMap.merge(task.getNodeId(), task.getCreateTime(), Math::min); + } + }); + + // 从记录中找:筛选出每个节点最大轮次的记录,取最小创建时间 + records.forEach(record -> { + Integer maxRound = nodeRoundMap.get(record.getNodeId()); + if (record.getNodeRound() != null && record.getNodeRound().equals(maxRound)) { + nodeMinCreateTimeMap.merge(record.getNodeId(), record.getCreateTime(), Math::min); + } + }); + + // 按最小创建时间升序排序 + return nodeMinCreateTimeMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + + private ApprovalTaskNode buildTaskNode(ApprovalTask task, Map taskRecordMap, Map> attachmentsMap, Map simpleUserMap) { ApprovalRecord record = taskRecordMap.get(task.getId()); ApprovalTaskNode taskNode = ApprovalTaskNode.builder().taskId(task.getId()).approverId(task.getApproverId()).approvalStatus(task.getStatus()).approvalTime(task.getUpdateTime()).build(); + if (simpleUserMap.containsKey(task.getApproverId())) { + taskNode.setApprover(simpleUserMap.get(task.getApproverId()).getName()); + taskNode.setApproverAvatar(simpleUserMap.get(task.getApproverId()).getAvatar()); + } if (record != null) { taskNode.setComment(record.getComment()); taskNode.setAttachments(attachmentsMap.get(record.getId())); @@ -240,8 +269,10 @@ public void clearApprovingInstanceOfFlow(String flowId) { .eq(ApprovalInstance::getApprovalStatus, ApprovalStatus.APPROVING.name()); List instances = approvalInstanceMapper.selectListByLambda(instanceLambdaQueryWrapper); instances.forEach(instance -> { - String tableName = FORM_APPROVAL_TABLE.get(instance.getType()); - extApprovalInstanceMapper.updateApprovalStatus(tableName, instance.getResourceId(), ApprovalStatus.NONE.name()); + ApprovalResourceService resourceService = CommonBeanFactory.getBean(ApprovalResourceService.class); + if (resourceService != null) { + resourceService.updateResourceApprovalStatus(FormKey.ofKey(instance.getType()), instance.getResourceId(), ApprovalStatus.NONE.name()); + } }); approvalInstanceMapper.deleteByLambda(instanceLambdaQueryWrapper); // 删除审批实例相关数据: 待办任务、加签任务、退回记录、执行记录、实例附件等 diff --git a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java index f228981b00..0fca8d8894 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/approval/service/ApprovalResourceService.java @@ -11,6 +11,7 @@ import cn.cordys.crm.approval.domain.ApprovalRecord; import cn.cordys.crm.approval.domain.ApprovalTask; import cn.cordys.crm.approval.dto.ApprovalResourceBaseParam; +import cn.cordys.crm.approval.dto.ResourceSnapshotApprovalParam; import cn.cordys.crm.approval.dto.response.ApprovalNodeApproverResponse; import cn.cordys.crm.approval.dto.response.ApprovalNodeResponse; import cn.cordys.crm.approval.dto.response.ResourceApprovalResponse; @@ -20,18 +21,22 @@ import cn.cordys.mybatis.lambda.LambdaQueryWrapper; import cn.cordys.security.UserApprovalDTO; import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ListUtils; import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.lang.reflect.Method; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Service @Transactional(rollbackFor = Exception.class) +@Slf4j public class ApprovalResourceService { @Resource @@ -45,6 +50,8 @@ public class ApprovalResourceService { @Resource private ExtApprovalInstanceMapper extApprovalInstanceMapper; @Resource + private ApplicationContext applicationContext; + @Resource private ApprovalFlowService approvalFlowService; @Resource private ApprovalInstanceService instanceService; @@ -153,18 +160,55 @@ public ResourceApprovalResponse resourceDetail(String resourceId) { } /** - * 更新业务表的审批状态 + * 更新业务表及快照的审批状态 * * @param formKey 表单类型 * @param resourceId 资源ID * @param approvalStatus 审批状态 */ - public void updateApprovalStatus(FormKey formKey, String resourceId, String approvalStatus) { + public void updateResourceApprovalStatus(FormKey formKey, String resourceId, String approvalStatus) { + if (formKey == null) { + throw new GenericException(Translator.get("module.form.illegal")); + } String tableName = FORM_APPROVAL_TABLE.get(formKey.getKey()); if (StringUtils.isBlank(tableName)) { throw new GenericException(Translator.get("module.form.illegal")); } extApprovalInstanceMapper.updateApprovalStatus(tableName, resourceId, approvalStatus); + // 存在快照表, 需要同步刷新审批状态 + if (formKey.hasSnapshot()) { + updateSnapshotApprovalStatus(formKey, resourceId, approvalStatus); + } + } + + /** + * 更新业务快照审批状态值 + * + * @param formKey 表单类型 + * @param resourceId 资源ID + * @param approvalStatus 审批状态 + */ + private void updateSnapshotApprovalStatus(FormKey formKey, String resourceId, String approvalStatus) { + // FormKey 与 Service Bean 名称的映射 + Map snapshotServiceMap = Map.of( + FormKey.INVOICE, "contractInvoiceService", + FormKey.QUOTATION, "opportunityQuotationService", + FormKey.CONTRACT, "customerContactService" + ); + + String serviceBeanName = snapshotServiceMap.get(formKey); + if (StringUtils.isBlank(serviceBeanName)) { + return; + } + + Object service = applicationContext.getBean(serviceBeanName); + try { + Method method = service.getClass().getMethod("updateSnapshotApprovalStatus", ResourceSnapshotApprovalParam.class); + ResourceSnapshotApprovalParam param = ResourceSnapshotApprovalParam.builder().resourceId(resourceId).approvalStatus(approvalStatus).build(); + method.invoke(service, param); + } catch (Exception e) { + log.error("更新业务数据快照失败", e); + } } @@ -174,11 +218,6 @@ public void updateApprovalStatus(FormKey formKey, String resourceId, String appr * @param param 提审参数 */ public void push(ApprovalResourceBaseParam param, String currentOrgId, String currentUserId) { - // 更新业务表 approval_status - String tableName = FORM_APPROVAL_TABLE.get(param.getFormKey()); - if (StringUtils.isBlank(tableName)) { - throw new GenericException(Translator.get("module.form.illegal")); - } ApprovalFlowVersion approvalFlowVersion = approvalFlowService.getEnabledFlow(param.getFormKey(), currentOrgId); if (approvalFlowVersion == null) { throw new GenericException(Translator.get("approval_flow.not.exist")); @@ -190,7 +229,7 @@ public void push(ApprovalResourceBaseParam param, String currentOrgId, String cu instance.setCurrentNodeId(firstApprovalNode.getId()); if (ApprovalNodeTypeEnum.valueOf(firstApprovalNode.getNodeType()) == ApprovalNodeTypeEnum.EXCEPTION) { // 异常节点, 目前只有自动拒绝的场景, 直接驳回 - extApprovalInstanceMapper.updateApprovalStatus(tableName, param.getResourceId(), ApprovalStatus.UNAPPROVED.name()); + updateResourceApprovalStatus(FormKey.ofKey(param.getFormKey()), param.getResourceId(), ApprovalStatus.UNAPPROVED.name()); instance.setApprovalStatus(ApprovalStatus.UNAPPROVED.name()); instance.setApprovalTime(System.currentTimeMillis()); approvalInstanceMapper.insert(instance); @@ -198,7 +237,7 @@ public void push(ApprovalResourceBaseParam param, String currentOrgId, String cu } if (ApprovalNodeTypeEnum.valueOf(firstApprovalNode.getNodeType()) == ApprovalNodeTypeEnum.END) { // 直接结束 - extApprovalInstanceMapper.updateApprovalStatus(tableName, param.getResourceId(), ApprovalStatus.APPROVED.name()); + updateResourceApprovalStatus(FormKey.ofKey(param.getFormKey()), param.getResourceId(), ApprovalStatus.APPROVED.name()); instance.setApprovalStatus(ApprovalStatus.APPROVED.name()); instance.setApprovalTime(System.currentTimeMillis()); approvalInstanceMapper.insert(instance); @@ -211,7 +250,7 @@ public void push(ApprovalResourceBaseParam param, String currentOrgId, String cu * 3. 创建审批待办任务 * 4. 抄送任务 */ - extApprovalInstanceMapper.updateApprovalStatus(tableName, param.getResourceId(), ApprovalStatus.APPROVING.name()); + updateResourceApprovalStatus(FormKey.ofKey(param.getFormKey()), param.getResourceId(), ApprovalStatus.APPROVING.name()); approvalInstanceMapper.insert(instance); ApprovalNodeApproverResponse approverNode = (ApprovalNodeApproverResponse) firstApprovalNode; List approvalTasks = approvalActionService.getNodeApproverTasks(approverNode, instance.getId(), currentUserId, null); @@ -223,12 +262,8 @@ public void push(ApprovalResourceBaseParam param, String currentOrgId, String cu } public void revoke(ApprovalResourceBaseParam param, String currentUserId) { - // 更新业务表 approval_status - String tableName = FORM_APPROVAL_TABLE.get(param.getFormKey()); - if (StringUtils.isBlank(tableName)) { - throw new GenericException(Translator.get("module.form.illegal")); - } - extApprovalInstanceMapper.updateApprovalStatus(tableName, param.getResourceId(), ApprovalStatus.REVOKED.name()); + // 更新业务资源审批状态 + updateResourceApprovalStatus(FormKey.ofKey(param.getFormKey()), param.getResourceId(), ApprovalStatus.REVOKED.name()); // 更新审批实例状态 ApprovalInstance instance = instanceService.getLatestInstance(param.getResourceId()); instance.setApprovalStatus(ApprovalStatus.REVOKED.name()); diff --git a/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractInvoiceService.java b/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractInvoiceService.java index 1dc851416e..17cde84398 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractInvoiceService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractInvoiceService.java @@ -25,16 +25,15 @@ import cn.cordys.crm.approval.constants.ApprovalFormTypeEnum; import cn.cordys.crm.approval.constants.ApprovalStatus; import cn.cordys.crm.approval.constants.ExecuteTimingEnum; +import cn.cordys.crm.approval.dto.ResourceSnapshotApprovalParam; import cn.cordys.crm.approval.service.ApprovalFlowService; import cn.cordys.crm.contract.constants.BusinessTitleConstants; import cn.cordys.crm.contract.constants.ContractApprovalStatus; -import cn.cordys.crm.contract.domain.BusinessTitle; -import cn.cordys.crm.contract.domain.Contract; -import cn.cordys.crm.contract.domain.ContractInvoice; -import cn.cordys.crm.contract.domain.ContractInvoiceSnapshot; +import cn.cordys.crm.contract.domain.*; import cn.cordys.crm.contract.dto.request.ContractInvoiceAddRequest; import cn.cordys.crm.contract.dto.request.ContractInvoicePageRequest; import cn.cordys.crm.contract.dto.request.ContractInvoiceUpdateRequest; +import cn.cordys.crm.contract.dto.response.ContractGetResponse; import cn.cordys.crm.contract.dto.response.ContractInvoiceGetResponse; import cn.cordys.crm.contract.dto.response.ContractInvoiceListResponse; import cn.cordys.crm.contract.mapper.ExtContractInvoiceMapper; @@ -579,4 +578,20 @@ public ModuleFormConfigDTO getBusinessFormConfig(String organizationId) { } return businessFormConfig; } + + /** + * 由审批执行操作统一调用, 勿修改 + * @param param 参数 + */ + public void updateSnapshotApprovalStatus(ResourceSnapshotApprovalParam param) { + ContractInvoiceSnapshot snapshotCriteria = new ContractInvoiceSnapshot(); + snapshotCriteria.setInvoiceId(param.getResourceId()); + ContractInvoiceSnapshot snapshot = snapshotBaseMapper.selectOne(snapshotCriteria); + if (snapshot != null) { + ContractInvoiceGetResponse response = JSON.parseObject(snapshot.getInvoiceValue(), ContractInvoiceGetResponse.class); + response.setApprovalStatus(param.getApprovalStatus()); + snapshot.setInvoiceValue(JSON.toJSONString(response)); + snapshotBaseMapper.update(snapshot); + } + } } diff --git a/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractService.java b/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractService.java index 795a144e4c..e12b6c2ff1 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/contract/service/ContractService.java @@ -30,6 +30,7 @@ import cn.cordys.crm.approval.constants.ApprovalStatus; import cn.cordys.crm.approval.constants.ApprovalFormTypeEnum; import cn.cordys.crm.approval.constants.ExecuteTimingEnum; +import cn.cordys.crm.approval.dto.ResourceSnapshotApprovalParam; import cn.cordys.crm.approval.service.ApprovalFlowService; import cn.cordys.crm.contract.constants.ContractApprovalStatus; import cn.cordys.crm.contract.constants.ContractStage; @@ -48,6 +49,8 @@ import cn.cordys.crm.contract.mapper.ExtContractMapper; import cn.cordys.crm.contract.mapper.ExtContractStageConfigMapper; import cn.cordys.crm.customer.domain.Customer; +import cn.cordys.crm.opportunity.domain.OpportunityQuotationSnapshot; +import cn.cordys.crm.opportunity.dto.response.OpportunityQuotationGetResponse; import cn.cordys.crm.system.constants.DictModule; import cn.cordys.crm.system.constants.NotificationConstants; import cn.cordys.crm.system.domain.MessageTaskConfig; @@ -675,6 +678,22 @@ private void updateStatusSnapshot(String id, String stage, String approvalStatus } } + /** + * 由审批执行操作统一调用, 勿修改 + * @param param 参数 + */ + public void updateSnapshotApprovalStatus(ResourceSnapshotApprovalParam param) { + ContractSnapshot snapshotCriteria = new ContractSnapshot(); + snapshotCriteria.setContractId(param.getResourceId()); + ContractSnapshot snapshot = snapshotBaseMapper.selectOne(snapshotCriteria); + if (snapshot != null) { + ContractGetResponse response = JSON.parseObject(snapshot.getContractValue(), ContractGetResponse.class); + response.setApprovalStatus(param.getApprovalStatus()); + snapshot.setContractValue(JSON.toJSONString(response)); + snapshotBaseMapper.update(snapshot); + } + } + public CustomerContractStatisticResponse calculateContractStatisticByCustomerId(String customerId, String userId, String orgId, DeptDataPermissionDTO deptDataPermission) { return extContractMapper.calculateContractStatisticByCustomerId(customerId, userId, orgId, deptDataPermission); } diff --git a/backend/crm/src/main/java/cn/cordys/crm/opportunity/service/OpportunityQuotationService.java b/backend/crm/src/main/java/cn/cordys/crm/opportunity/service/OpportunityQuotationService.java index 6a248c7e7a..40909f815e 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/opportunity/service/OpportunityQuotationService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/opportunity/service/OpportunityQuotationService.java @@ -24,6 +24,7 @@ import cn.cordys.crm.approval.constants.ApprovalState; import cn.cordys.crm.approval.constants.ApprovalStatus; import cn.cordys.crm.approval.constants.ExecuteTimingEnum; +import cn.cordys.crm.approval.dto.ResourceSnapshotApprovalParam; import cn.cordys.crm.approval.service.ApprovalFlowService; import cn.cordys.crm.contract.constants.ContractApprovalStatus; import cn.cordys.crm.contract.domain.ContractField; @@ -456,6 +457,22 @@ private void updateSnapshot(String id, String approvalStatus, ModuleFormConfigDT } } + /** + * 由审批执行操作统一调用, 勿修改 + * @param param 参数 + */ + public void updateSnapshotApprovalStatus(ResourceSnapshotApprovalParam param) { + OpportunityQuotationSnapshot snapshotCriteria = new OpportunityQuotationSnapshot(); + snapshotCriteria.setQuotationId(param.getResourceId()); + OpportunityQuotationSnapshot snapshot = snapshotBaseMapper.selectOne(snapshotCriteria); + if (snapshot != null) { + OpportunityQuotationGetResponse response = JSON.parseObject(snapshot.getQuotationValue(), OpportunityQuotationGetResponse.class); + response.setApprovalStatus(param.getApprovalStatus()); + snapshot.setQuotationValue(JSON.toJSONString(response)); + snapshotBaseMapper.update(snapshot); + } + } + /** * 作废报价快照 * @param id diff --git a/backend/crm/src/main/java/cn/cordys/crm/system/dto/UserSimple.java b/backend/crm/src/main/java/cn/cordys/crm/system/dto/UserSimple.java new file mode 100644 index 0000000000..24f899f6a4 --- /dev/null +++ b/backend/crm/src/main/java/cn/cordys/crm/system/dto/UserSimple.java @@ -0,0 +1,17 @@ +package cn.cordys.crm.system.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class UserSimple { + + @Schema(description = "用户ID") + private String id; + @Schema(description = "用户名") + private String name; + @Schema(description = "头像") + private String avatar; +} diff --git a/backend/crm/src/main/java/cn/cordys/crm/system/service/UserExtendService.java b/backend/crm/src/main/java/cn/cordys/crm/system/service/UserExtendService.java index cbf83bcc53..7c3641c7d3 100644 --- a/backend/crm/src/main/java/cn/cordys/crm/system/service/UserExtendService.java +++ b/backend/crm/src/main/java/cn/cordys/crm/system/service/UserExtendService.java @@ -6,8 +6,10 @@ import cn.cordys.crm.system.constants.ScopeKey; import cn.cordys.crm.system.domain.OrganizationUser; import cn.cordys.crm.system.domain.User; +import cn.cordys.crm.system.domain.UserExtend; import cn.cordys.crm.system.domain.UserRole; import cn.cordys.crm.system.dto.ScopeNameDTO; +import cn.cordys.crm.system.dto.UserSimple; import cn.cordys.crm.system.mapper.ExtUserExtendMapper; import cn.cordys.mybatis.BaseMapper; import cn.cordys.mybatis.lambda.LambdaQueryWrapper; @@ -38,6 +40,8 @@ public class UserExtendService { private BaseMapper userRoleMapper; @Resource private BaseMapper userMapper; + @Resource + private BaseMapper userExtendMapper; /** * 获取范围的所有负责人ID @@ -370,4 +374,16 @@ private void collectDeptNodes(List nodes, Map dept } } + /** + * 获取用户头像集合 + * @return 用户头像集合 + */ + public Map getAllUserSimpleMap() { + List userExtends = userExtendMapper.selectAll(null); + List users = userMapper.selectAll(null); + Map avatarMap = userExtends.stream().collect(Collectors.toMap(UserExtend::getId, UserExtend::getAvatar)); + List simples = users.stream().map(user -> new UserSimple(user.getId(), user.getName(), avatarMap.get(user.getId()))).toList(); + return simples.stream().collect(Collectors.toMap(UserSimple::getId, u -> u)); + } + } diff --git a/backend/crm/src/main/resources/migration/1.7.0/ddl/V1.7.0_2__ga_ddl.sql b/backend/crm/src/main/resources/migration/1.7.0/ddl/V1.7.0_2__ga_ddl.sql index c53dc9220f..81e63b96cf 100644 --- a/backend/crm/src/main/resources/migration/1.7.0/ddl/V1.7.0_2__ga_ddl.sql +++ b/backend/crm/src/main/resources/migration/1.7.0/ddl/V1.7.0_2__ga_ddl.sql @@ -133,6 +133,7 @@ CREATE INDEX idx_submitter_id ON approval_instance(submitter_id ASC); CREATE TABLE approval_task( `id` VARCHAR(32) NOT NULL COMMENT 'ID' , `node_id` VARCHAR(32) NOT NULL COMMENT '节点ID' , + `node_round` INT NOT NULL COMMENT '节点轮次' , `instance_id` VARCHAR(32) NOT NULL COMMENT '审批实例ID' , `approver_id` VARCHAR(20) NOT NULL COMMENT '审批人ID' , `status` VARCHAR(20) NOT NULL COMMENT '任务状态; 待审批: PENDING, 审批中: APPROVING, 已通过: APPROVED, 已驳回: UNAPPROVED, 已撤销: REVOKED' , @@ -191,6 +192,7 @@ CREATE TABLE approval_record( `instance_id` VARCHAR(32) NOT NULL COMMENT '审批实例ID' , `task_id` VARCHAR(32) COMMENT '任务ID' , `node_id` VARCHAR(32) NOT NULL COMMENT '节点ID' , + `node_round` INT NOT NULL COMMENT '节点轮次' , `result` VARCHAR(255) COMMENT '审批结果' , `comment` VARCHAR(255) COMMENT '审批意见' , `create_time` BIGINT NOT NULL COMMENT '创建时间' , diff --git a/backend/crm/src/test/resources/dml/init_approval_todo_cc_initiated_test.sql b/backend/crm/src/test/resources/dml/init_approval_todo_cc_initiated_test.sql index 0885bb2618..a707ed5753 100644 --- a/backend/crm/src/test/resources/dml/init_approval_todo_cc_initiated_test.sql +++ b/backend/crm/src/test/resources/dml/init_approval_todo_cc_initiated_test.sql @@ -15,8 +15,8 @@ VALUES ('todo_cc_inst_002', 'approval_flow_test_001', 'quotation', 'todo_cc_resource_002', 'admin', 'node_cc_002', 'APPROVED', 1736244043609, 1736245043609, 1736244043609, 1736245043609, 'admin', 'admin'), ('todo_cc_inst_003', 'approval_flow_test_001', 'order', 'todo_cc_resource_003', 'other_user', 'node_cc_003', 'APPROVING', 1736246043609, NULL, 1736246043609, 1736246043609, 'admin', 'admin'); -INSERT INTO approval_task (`id`, `node_id`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) +INSERT INTO approval_task (`id`, `node_id`, `node_round`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) VALUES - ('todo_cc_task_001', 'node_cc_001', 'todo_cc_inst_001', 'admin', 'PENDING', 'cc', 'READ', 1736243043609, 1736243043609, 'admin', 'admin'), - ('todo_cc_task_002', 'node_cc_002', 'todo_cc_inst_002', 'admin', 'APPROVED', 'approve', 'APPROVED', 1736245043609, 1736245043609, 'admin', 'admin'), - ('todo_cc_task_003', 'node_cc_003', 'todo_cc_inst_003', 'admin', 'PENDING', 'CC', 'READ', 1736246043609, 1736246043609, 'admin', 'admin'); + ('todo_cc_task_001', 'node_cc_001', 1,'todo_cc_inst_001', 'admin', 'PENDING', 'cc', 'READ', 1736243043609, 1736243043609, 'admin', 'admin'), + ('todo_cc_task_002', 'node_cc_002', 1,'todo_cc_inst_002', 'admin', 'APPROVED', 'approve', 'APPROVED', 1736245043609, 1736245043609, 'admin', 'admin'), + ('todo_cc_task_003', 'node_cc_003', 1, 'todo_cc_inst_003', 'admin', 'PENDING', 'CC', 'READ', 1736246043609, 1736246043609, 'admin', 'admin'); diff --git a/backend/crm/src/test/resources/dml/init_approval_todo_list_test.sql b/backend/crm/src/test/resources/dml/init_approval_todo_list_test.sql index 7f8a823cc0..9dd29df4ad 100644 --- a/backend/crm/src/test/resources/dml/init_approval_todo_list_test.sql +++ b/backend/crm/src/test/resources/dml/init_approval_todo_list_test.sql @@ -13,8 +13,8 @@ VALUES ('todo_list_inst_contract', 'approval_flow_test_001', 'contract', 'todo_list_resource_contract', 'admin', 'node_contract_current',1736240043609, NULL, NULL, 1736240043609, 1736240043609, 'admin', 'admin'), ('todo_list_inst_quote', 'approval_flow_test_001', 'quotation', 'todo_list_resource_quote', 'admin', 'node_quote_current',1736241043609, NULL, NULL, 1736241043609, 1736241043609, 'admin', 'admin'); -INSERT INTO approval_task (`id`, `node_id`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) +INSERT INTO approval_task (`id`, `node_id`, `node_round`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) VALUES - ('todo_list_task_contract', 'node_contract_current', 'todo_list_inst_contract', 'admin', 'PENDING', 'approve', 'PENDING', 1736240043609, 1736240043609, 'admin', 'admin'), - ('todo_list_task_quote', 'node_quote_current', 'todo_list_inst_quote', 'admin', 'PENDING', 'approve', 'PENDING', 1736241043609, 1736241043609, 'admin', 'admin'), - ('todo_list_task_old_node', 'node_contract_old', 'todo_list_inst_contract', 'admin', 'PENDING', 'approve', 'PENDING', 1736242043609, 1736242043609, 'admin', 'admin'); + ('todo_list_task_contract', 'node_contract_current', 1,'todo_list_inst_contract', 'admin', 'PENDING', 'approve', 'PENDING', 1736240043609, 1736240043609, 'admin', 'admin'), + ('todo_list_task_quote', 'node_quote_current', 1,'todo_list_inst_quote', 'admin', 'PENDING', 'approve', 'PENDING', 1736241043609, 1736241043609, 'admin', 'admin'), + ('todo_list_task_old_node', 'node_contract_old', 1,'todo_list_inst_contract', 'admin', 'PENDING', 'approve', 'PENDING', 1736242043609, 1736242043609, 'admin', 'admin'); diff --git a/backend/crm/src/test/resources/dml/init_approval_todo_processed_test.sql b/backend/crm/src/test/resources/dml/init_approval_todo_processed_test.sql index 9778ebf959..77965fdfd7 100644 --- a/backend/crm/src/test/resources/dml/init_approval_todo_processed_test.sql +++ b/backend/crm/src/test/resources/dml/init_approval_todo_processed_test.sql @@ -6,7 +6,7 @@ VALUES ('todo_processed_inst_001', 'approval_flow_test_001', 'contract', 'todo_processed_resource_001', 'admin', 'node_done', 'APPROVED', 1736240043609, 1736241043609, 1736240043609, 1736241043609, 'admin', 'admin'), ('todo_pending_inst_001', 'approval_flow_test_001', 'contract', 'todo_pending_resource_001', 'admin', 'node_wait','APPROVED',1736242043609, 1736241043609,1736242043609, 1736242043609, 'admin', 'admin'); -INSERT INTO approval_task (`id`, `node_id`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) +INSERT INTO approval_task (`id`, `node_id`, `node_round`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) VALUES - ('todo_processed_task_001', 'node_done', 'todo_processed_inst_001', 'admin', 'APPROVED', 'approve', 'APPROVED', 1736241043609, 1736241043609, 'admin', 'admin'), - ('todo_pending_task_001', 'node_wait', 'todo_pending_inst_001', 'admin', 'PENDING', 'approve', 'PENDING', 1736242043609, 1736242043609, 'admin', 'admin'); + ('todo_processed_task_001', 'node_done', 1,'todo_processed_inst_001', 'admin', 'APPROVED', 'approve', 'APPROVED', 1736241043609, 1736241043609, 'admin', 'admin'), + ('todo_pending_task_001', 'node_wait', 1,'todo_pending_inst_001', 'admin', 'PENDING', 'approve', 'PENDING', 1736242043609, 1736242043609, 'admin', 'admin'); diff --git a/backend/crm/src/test/resources/dml/init_resource_approval_test.sql b/backend/crm/src/test/resources/dml/init_resource_approval_test.sql index 961788e2b5..9e800bff3d 100644 --- a/backend/crm/src/test/resources/dml/init_resource_approval_test.sql +++ b/backend/crm/src/test/resources/dml/init_resource_approval_test.sql @@ -12,12 +12,12 @@ INSERT INTO approval_instance (`id`, `flow_version_id`, `type`, `resource_id`, ` VALUES ('approval_instance_test_001', 'approval_flow_test_001', 'contract', 'approval_resource_test_001', 'admin', 'node_current', 'APPROVING', 1736240043609, NULL, 1736240043609, 1736240043609, 'admin', 'admin'); -INSERT INTO approval_task (`id`, `node_id`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) +INSERT INTO approval_task (`id`, `node_id`, `node_round`, `instance_id`, `approver_id`, `status`, `type`, `action`, `create_time`, `update_time`, `create_user`, `update_user`) VALUES - ('approval_task_current', 'node_current', 'approval_instance_test_001', 'appr_user_curr', 'APPROVED', 'NL', 'APPROVE', 1736240043609, 1736240043609, 'admin', 'admin'), - ('approval_task_other', 'node_other', 'approval_instance_test_001', 'appr_user_othr', 'APPROVED', 'NL', 'APPROVE',1736241043609, 1736241043609, 'admin', 'admin'); + ('approval_task_current', 'node_current', 1, 'approval_instance_test_001', 'appr_user_curr', 'APPROVED', 'NL', 'APPROVE', 1736240043609, 1736240043609, 'admin', 'admin'), + ('approval_task_other', 'node_other', 1, 'approval_instance_test_001', 'appr_user_othr', 'APPROVED', 'NL', 'APPROVE',1736241043609, 1736241043609, 'admin', 'admin'); -INSERT INTO approval_record (`id`, `instance_id`, `task_id`, `node_id`, `result`, `comment`, `create_time`, `update_time`, `create_user`, `update_user`) +INSERT INTO approval_record (`id`, `instance_id`, `task_id`, `node_id`, `node_round`, `result`, `comment`, `create_time`, `update_time`, `create_user`, `update_user`) VALUES - ('approval_record_current', 'approval_instance_test_001', 'approval_task_current', 'node_current', 'APPROVED', 'current-node-comment', 1736240043609, 1736240043609, 'admin', 'admin'), - ('approval_record_other', 'approval_instance_test_001', 'approval_task_other', 'node_other', 'UNAPPROVED', 'other-node-comment', 1736241043609, 1736241043609, 'admin', 'admin'); + ('approval_record_current', 'approval_instance_test_001', 'approval_task_current', 'node_current', 1, 'APPROVED', 'current-node-comment', 1736240043609, 1736240043609, 'admin', 'admin'), + ('approval_record_other', 'approval_instance_test_001', 'approval_task_other', 'node_other', 1, 'UNAPPROVED', 'other-node-comment', 1736241043609, 1736241043609, 'admin', 'admin');