Compare commits

2 Commits

Author SHA1 Message Date
b2894lxlx 0797785176 项目、合同、运单、预结算、正式结算、付款对接mk 2026-09-21 10:36:00 +08:00
b2894lxlx 86adb47392 调试mk 2026-09-20 19:03:13 +08:00
17 changed files with 1162 additions and 121 deletions
@@ -0,0 +1,20 @@
package org.springblade.transport.feign;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* MK 业务流程同步 Feign
*/
@FeignClient(value = "blade-transport")
public interface IMkProcessClient {
String API_PREFIX = "/feign/client/mkProcess";
String APPLY = API_PREFIX + "/apply";
@PostMapping(APPLY)
FR<Boolean> apply(@RequestBody MkProcessSyncDTO param);
}
@@ -0,0 +1,33 @@
package org.springblade.transport.pojo.dto;
import io.swagger.v3.oas.annotations.Schema;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* MK 流程节点同步参数
*/
@Data
@Schema(description = "MK 流程节点同步参数")
public class MkProcessSyncDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "业务类型,如 project-apply")
private String bizType;
@Schema(description = "业务主键")
private Long id;
@Schema(description = "动作:sync / approve / reject")
private String action;
@Schema(description = "处理人姓名")
private String processorName;
@Schema(description = "MK 流程实例详情")
private Object processInfo;
}
@@ -60,6 +60,12 @@ public class BusinessProcessController extends BladeController {
return R.data(businessProcessService.processSubmit(param));
}
@PostMapping("/processDelete")
@ApiOperationSupport(order = 7)
public R<Boolean> processDelete(@RequestBody MKProcessCreateDTO param) {
return R.data(businessProcessService.processDelete(param == null ? null : param.getFormInstanceId()));
}
@GetMapping("/isEditView")
@ApiOperationSupport(order = 3)
@Operation(summary = "是否编辑页", description = "传入业务id")
@@ -34,6 +34,14 @@ public interface IBusinessProcessService extends IService<BusinessProcess> {
*/
String processSubmit(MKProcessCreateDTO param);
/**
* 按业务表单实例 id 删除 MK 流程(不删除本地业务流程记录,供驳回后重新提交使用)
*
* @param formInstanceId 业务表单实例 id
* @return 是否成功
*/
boolean processDelete(String formInstanceId);
/**
* 获取流程当前节点详情
*
@@ -43,7 +43,9 @@ import org.springblade.thirdparty.mk.pojo.dto.sort.*;
import org.springblade.thirdparty.mk.pojo.vo.*;
import org.springblade.thirdparty.mk.service.IMKService;
import org.springblade.transport.feign.ICustomerArchiveClient;
import org.springblade.transport.feign.IMkProcessClient;
import org.springblade.transport.pojo.dto.CustomerProcessNodeSyncDTO;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -69,6 +71,7 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
private final ApprovalConvert approvalConvert;
private final IUserService userService;
private final ICustomerArchiveClient customerArchiveClient;
private final IMkProcessClient mkProcessClient;
@Transactional(rollbackFor = Exception.class)
@Override
@@ -133,8 +136,17 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法提交审核流");
param.setSubmitIdentity(loginName);
param.setLoginName(loginName);
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(param));
String processInstanceId = mkService.processSubmit(param);
String bizType = param.getBizType();
MKProcessCreateDTO mkParam = new MKProcessCreateDTO();
mkParam.setFormInstanceId(param.getFormInstanceId());
mkParam.setSubject(param.getSubject());
mkParam.setSubmitIdentity(loginName);
mkParam.setLoginName(loginName);
mkParam.setTemplateCode(param.getTemplateCode());
mkParam.setFormValues(param.getFormValues());
mkParam.setTempVarData(param.getTempVarData());
log.info("调用mk processSubmit 参数:{}", JSON.toJSONString(mkParam));
String processInstanceId = mkService.processSubmit(mkParam);
AssertUtils.notBlank(processInstanceId, "提交流程失败,未返回流程实例id");
Long bizId;
@@ -170,10 +182,41 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
this.saveOrUpdate(businessProcess);
this.getCurrentNodes(processInstanceId, loginName);
Object processInfo = this.getProcessInfo(processInstanceId, loginName);
this.syncCustomerArchiveFromProcessInfo(param.getFormInstanceId(), processInfo);
this.syncBizFromProcessInfo(bizType, param.getFormInstanceId(), processInfo);
return processInstanceId;
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean processDelete(String formInstanceId) {
AssertUtils.notBlank(formInstanceId, "表单实例id不能为空");
Long bizId;
try {
bizId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
throw new ServiceException("表单实例id格式不正确");
}
BusinessProcess businessProcess = this.getOne(Wrappers.<BusinessProcess>lambdaQuery()
.eq(BusinessProcess::getBizId, bizId)
);
if (businessProcess == null || StringUtil.isBlank(businessProcess.getProcessInstanceId())) {
log.warn("客商驳回后删除流程跳过,未找到流程实例 formInstanceId={}", formInstanceId);
return true;
}
String loginName = businessProcess.getPromoterLoginName();
if (StringUtil.isBlank(loginName)) {
loginName = resolveCurrentUserPhone();
}
AssertUtils.notBlank(loginName, "当前用户手机号为空,无法删除审核流");
log.info("调用mk processDelete processInstanceId={} loginName={} formInstanceId={}",
businessProcess.getProcessInstanceId(), loginName, formInstanceId);
boolean deleted = mkService.processDelete(businessProcess.getProcessInstanceId(), loginName);
if (!deleted) {
throw new ServiceException("删除MK流程失败");
}
return true;
}
@Override
public List<?> getCurrentNodes(String processInstanceId, String loginName) {
if (StringUtils.isBlank(processInstanceId)) {
@@ -226,6 +269,37 @@ public class BusinessProcessServiceImpl extends ServiceImpl<BusinessProcessMappe
}
}
private void syncBizFromProcessInfo(String bizType, String formInstanceId, Object processInfo) {
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
log.warn("同步业务当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
return;
}
Long bizId;
try {
bizId = Long.valueOf(formInstanceId);
} catch (NumberFormatException e) {
log.warn("同步业务当前节点失败,表单实例id不是数字:{}", formInstanceId);
return;
}
String resolvedBizType = StringUtils.isBlank(bizType) ? "customer-archive" : bizType;
if ("customer-archive".equals(resolvedBizType)) {
this.syncCustomerArchiveFromProcessInfo(formInstanceId, processInfo);
return;
}
MkProcessSyncDTO param = new MkProcessSyncDTO();
param.setBizType(resolvedBizType);
param.setId(bizId);
param.setAction("sync");
param.setProcessInfo(processInfo);
try {
FR<Boolean> result = mkProcessClient.apply(param);
log.info("同步业务当前节点完成 bizType={} bizId={} result={}",
resolvedBizType, bizId, JSON.toJSONString(result));
} catch (Exception e) {
log.error("同步业务当前节点异常 bizType={} bizId={}", resolvedBizType, bizId, e);
}
}
private void syncCustomerArchiveFromProcessInfo(String formInstanceId, Object processInfo) {
if (StringUtils.isBlank(formInstanceId) || processInfo == null) {
log.warn("同步客商当前节点跳过,formInstanceId或流程实例详情为空 formInstanceId={}", formInstanceId);
@@ -35,13 +35,11 @@ import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
import org.springblade.transport.pojo.vo.CustomerChangeRecordVO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.impl.CustomerArchivePublicProcessService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -49,9 +47,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
@@ -69,7 +64,7 @@ import java.util.Map;
public class CustomerArchivePublicController {
private final ICustomerArchiveService customerArchiveService;
private final IBusinessProcessClient businessProcessClient;
private final CustomerArchivePublicProcessService customerArchivePublicProcessService;
/**
* 公开详情
@@ -93,121 +88,15 @@ public class CustomerArchivePublicController {
}
/**
* 公开接收流程页 postMessage 数据(当前仅打印,便于联调)
* 公开接收流程页 postMessage 数据
*/
@PostMapping("/process-message")
@ApiOperationSupport(order = 3)
@Operation(summary = "公开接收流程消息", description = "无需登录,接收后查询当前节点流程实例详情并同步客商当前节点")
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点5秒后查询流程实例详情并同步客商")
public R processMessage(@RequestBody Map<String, Object> body) {
log.info("客商公开页收到流程消息:{}", JSON.toJSONString(body));
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("客商公开页流程消息未找到 processId,跳过查询当前节点");
return R.success("ok");
}
String customerIdText = firstText(asMap(body == null ? null : body.get("formData")), "id");
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
Object nodeData = result == null ? null : result.getData();
log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(nodeData));
} catch (Exception e) {
log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e);
}
try {
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
log.info("客商公开页流程消息流程实例详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(customerIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
applyProcessResult(customerIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(customerIdText)) {
try {
customerArchiveService.syncProcessNodeFromProcessInfo(Long.valueOf(customerIdText), processInfo);
} catch (NumberFormatException e) {
log.warn("客商公开页流程消息客商id格式不正确:{}", customerIdText);
}
}
} catch (Exception e) {
log.error("客商公开页查询流程实例详情失败 processId={} loginName={}", processId, loginName, e);
}
customerArchivePublicProcessService.handleProcessMessage(body);
return R.success("ok");
}
private static boolean isProcessFinished(Object processInfo) {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(String customerIdText, String processId, Map<String, Object> formValues,
boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
if (StringUtil.isBlank(customerIdText)) {
log.warn("流程{}但未找到客商id,跳过同步 processId={}", action, processId);
return;
}
try {
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
Long customerId = Long.valueOf(customerIdText);
if (approved) {
customerArchiveService.approveFromProcess(customerId, processorName);
} else {
customerArchiveService.rejectFromProcess(customerId, processorName);
}
} catch (NumberFormatException e) {
log.warn("流程{}但客商id格式不正确:{}", action, customerIdText);
}
}
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
}
@@ -0,0 +1,55 @@
package org.springblade.transport.controller;
import com.alibaba.fastjson2.JSON;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.R;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* MK 业务流程公开查看
*/
@Slf4j
@RestController
@AllArgsConstructor
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@RequestMapping("/mk-process/public")
@Tag(name = "MK业务流程公开查看", description = "MK业务流程公开查看")
public class MkProcessPublicController {
private final MkProcessMessageService mkProcessMessageService;
@GetMapping("/{bizType}/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "公开详情", description = "无需登录")
public R detail(@PathVariable String bizType,
@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(mkProcessMessageService.handler(bizType).publicDetail(id));
}
@PostMapping("/{bizType}/process-message")
@ApiOperationSupport(order = 2)
@Operation(summary = "公开接收流程消息", description = "无需登录,立即查询当前节点,5秒后查询流程实例详情并同步业务")
public R processMessage(@PathVariable String bizType, @RequestBody Map<String, Object> body) {
log.info("公开页收到流程消息 bizType={} body={}", bizType, JSON.toJSONString(body));
mkProcessMessageService.handleProcessMessage(bizType, body);
return R.success("ok");
}
}
@@ -105,7 +105,7 @@ public class ProjectApplyController extends BladeController {
@ApiOperationSupport(order = 4)
@Operation(summary = "新增或修改", description = "传入projectApply")
public R submit(@RequestBody ProjectApply projectApply) {
return R.status(projectApplyService.submit(projectApply));
return projectApplyService.submit(projectApply) ? R.data(projectApply) : R.fail("保存失败");
}
@PostMapping("/submit-approval")
@@ -213,7 +213,7 @@ public class WaybillController extends BladeController {
@ApiOperationSupport(order = 9)
@Operation(summary = "新增或修改", description = "传入waybill")
public R submit(@RequestBody Waybill waybill) {
return R.status(waybillService.submit(waybill));
return waybillService.submit(waybill) ? R.data(waybill) : R.fail("保存失败");
}
@PostMapping("/save-draft")
@@ -0,0 +1,42 @@
package org.springblade.transport.feign;
import io.swagger.v3.oas.annotations.Hidden;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.constant.AuthConstant;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.transport.mk.IMkProcessBizHandler;
import org.springblade.transport.mk.MkProcessMessageService;
import org.springblade.transport.pojo.dto.MkProcessSyncDTO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* MK 业务流程同步 Feign 实现
*/
@Hidden
@RestController
@AllArgsConstructor
public class MkProcessClient implements IMkProcessClient {
private final MkProcessMessageService mkProcessMessageService;
@TenantIgnore
@PreAuth(AuthConstant.PERMIT_ALL)
@PostMapping(APPLY)
@Override
public FR<Boolean> apply(@RequestBody MkProcessSyncDTO param) {
if (param == null || param.getId() == null) {
return FR.data(false);
}
IMkProcessBizHandler handler = mkProcessMessageService.handler(param.getBizType());
String action = param.getAction() == null ? "sync" : param.getAction();
return switch (action) {
case "approve" -> FR.data(handler.approveFromProcess(param.getId(), param.getProcessorName()));
case "reject" -> FR.data(handler.rejectFromProcess(param.getId(), param.getProcessorName()));
default -> FR.data(handler.syncFromProcessInfo(param.getId(), param.getProcessInfo()));
};
}
}
@@ -0,0 +1,17 @@
package org.springblade.transport.mk;
/**
* MK 业务流程处理器
*/
public interface IMkProcessBizHandler {
String bizType();
Object publicDetail(Long id);
boolean syncFromProcessInfo(Long id, Object processInfo);
boolean approveFromProcess(Long id, String processorName);
boolean rejectFromProcess(Long id, String processorName);
}
@@ -0,0 +1,439 @@
package org.springblade.transport.mk;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* 各业务 MK 流程处理器
*/
public final class MkProcessBizHandlers {
private MkProcessBizHandlers() {
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class CustomerHandler implements IMkProcessBizHandler {
private final ICustomerArchiveService service;
@Override
public String bizType() {
return "customer-archive";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
return service.syncProcessNodeFromProcessInfo(id, processInfo);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
return service.approveFromProcess(id, processorName);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
return service.rejectFromProcess(id, processorName);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ProjectHandler implements IMkProcessBizHandler {
private final IProjectApplyService service;
@Override
public String bizType() {
return "project-apply";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ProjectApply entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
entity.setApprovalStatus("reviewing");
}
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ProjectApply entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())
|| "change_approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setEffectiveType("formal");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ProjectApply entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|| "change_rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ContractHandler implements IMkProcessBizHandler {
private final IContractManageService service;
@Override
public String bizType() {
return "contract-manage";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ContractManage entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
if (!"change_reviewing".equals(entity.getApprovalStatus())) {
entity.setApprovalStatus("reviewing");
}
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ContractManage entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())
|| "change_approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_approved" : "approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ContractManage entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())
|| "change_rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean change = "change_reviewing".equals(entity.getApprovalStatus());
entity.setApprovalStatus(change ? "change_rejected" : "rejected");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class WaybillHandler implements IMkProcessBizHandler {
private final IWaybillService service;
@Override
public String bizType() {
return "waybill-manage";
}
@Override
public Object publicDetail(Long id) {
return service.getById(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
if (Func.isEmpty(node)) {
return false;
}
entity.setCurrentProcessNode(node);
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核通过");
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
Waybill entity = service.getById(id);
if (entity == null) {
return false;
}
entity.setCurrentProcessNode("审核不通过");
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class PreSettlementHandler implements IMkProcessBizHandler {
private final IPreSettlementService service;
@Override
public String bizType() {
return "pre-settlement";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
PreSettlement entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
PreSettlement entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
PreSettlement entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class FormalSettlementHandler implements IMkProcessBizHandler {
private final IFormalSettlementService service;
@Override
public String bizType() {
return "formal-settlement";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
FormalSettlement entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
FormalSettlement entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
entity.setApprovedTime(LocalDateTime.now());
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
FormalSettlement entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class PaymentHandler implements IMkProcessBizHandler {
private final IPaymentApplicationService service;
@Override
public String bizType() {
return "payment-application";
}
@Override
public Object publicDetail(Long id) {
return service.detail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
PaymentApplication entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
PaymentApplication entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("approved");
entity.setCurrentNode("审核通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
PaymentApplication entity = service.getById(id);
if (entity == null || "returned".equals(entity.getApprovalStatus())) {
return entity != null;
}
entity.setApprovalStatus("returned");
entity.setCurrentNode("审核不通过");
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
return service.updateById(entity);
}
}
}
@@ -0,0 +1,191 @@
package org.springblade.transport.mk;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 公开页流程消息:当前节点立即查询,流程实例详情延迟 5 秒查询后同步业务状态。
*/
@Slf4j
@Service
@TenantIgnore
@RequiredArgsConstructor
public class MkProcessMessageService {
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
private final IBusinessProcessClient businessProcessClient;
private final List<IMkProcessBizHandler> handlers;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
Thread thread = new Thread(runnable, "mk-process-info-" + THREAD_INDEX.incrementAndGet());
thread.setDaemon(true);
return thread;
});
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
public IMkProcessBizHandler handler(String bizType) {
Map<String, IMkProcessBizHandler> mapping = handlers.stream()
.collect(Collectors.toMap(IMkProcessBizHandler::bizType, Function.identity(), (a, b) -> a));
IMkProcessBizHandler handler = mapping.get(bizType);
if (handler == null) {
throw new IllegalArgumentException("不支持的MK业务类型:" + bizType);
}
return handler;
}
public void handleProcessMessage(String bizType, Map<String, Object> body) {
IMkProcessBizHandler handler = handler(bizType);
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("公开页流程消息未找到 processId,跳过查询当前节点 bizType={}", bizType);
return;
}
String bizIdText = firstText(asMap(body == null ? null : body.get("formData")), "id", "formInstanceId");
queryCurrentNodes(bizType, processId, loginName);
scheduleProcessInfo(handler, bizType, processId, loginName, bizIdText, formValues);
}
private void queryCurrentNodes(String bizType, String processId, String loginName) {
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
Object nodeData = result == null ? null : result.getData();
log.info("公开页流程消息当前节点详情 bizType={} processId={} loginName={} result={}",
bizType, processId, loginName, JSON.toJSONString(nodeData));
} catch (Exception e) {
log.error("公开页查询当前节点失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
}
}
private void scheduleProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
String loginName, String bizIdText, Map<String, Object> formValues) {
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
log.info("公开页将在{}秒后查询流程实例详情 bizType={} processId={}", PROCESS_INFO_DELAY_SECONDS, bizType, processId);
scheduler.schedule(
() -> queryProcessInfo(handler, bizType, processId, loginName, bizIdText, formValuesCopy),
PROCESS_INFO_DELAY_SECONDS,
TimeUnit.SECONDS
);
}
private void queryProcessInfo(IMkProcessBizHandler handler, String bizType, String processId,
String loginName, String bizIdText, Map<String, Object> formValues) {
try {
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
log.info("公开页流程消息流程实例详情 bizType={} processId={} loginName={} result={}",
bizType, processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
applyProcessResult(handler, bizType, bizIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(bizIdText)) {
try {
handler.syncFromProcessInfo(Long.valueOf(bizIdText), processInfo);
} catch (NumberFormatException e) {
log.warn("公开页流程消息业务id格式不正确 bizType={} id={}", bizType, bizIdText);
}
}
} catch (Exception e) {
log.error("公开页查询流程实例详情失败 bizType={} processId={} loginName={}", bizType, processId, loginName, e);
}
}
private static boolean isProcessFinished(Object processInfo) {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(IMkProcessBizHandler handler, String bizType, String bizIdText,
String processId, Map<String, Object> formValues, boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
if (StringUtil.isBlank(bizIdText)) {
log.warn("流程{}但未找到业务id,跳过同步 bizType={} processId={}", action, bizType, processId);
return;
}
try {
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
Long bizId = Long.valueOf(bizIdText);
if (approved) {
handler.approveFromProcess(bizId, processorName);
} else {
handler.rejectFromProcess(bizId, processorName);
}
} catch (NumberFormatException e) {
log.warn("流程{}但业务id格式不正确 bizType={} id={}", action, bizType, bizIdText);
}
}
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
}
@@ -0,0 +1,76 @@
package org.springblade.transport.mk;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 解析 MK 流程实例详情中的当前节点、当前处理人
*/
public final class MkProcessNodeHelper {
private MkProcessNodeHelper() {
}
public static String currentProcessor(Object processInfo) {
return joinDistinct(extractTexts(asMap(processInfo).get("currentHandlers"), "fdName", "name"), "");
}
public static String currentNode(Object processInfo) {
Map<String, Object> taskInfo = asMap(asMap(processInfo).get("fdTaskInfo"));
return joinDistinct(extractTexts(taskInfo.get("handlerInfos"), "nodeName"), "");
}
public static String text(String value, String fallback) {
return Func.isEmpty(value) ? fallback : value.trim();
}
public static Map<String, Object> asMap(Object value) {
if (value instanceof Map<?, ?> map) {
Map<String, Object> result = new LinkedHashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
if (value == null) {
return Map.of();
}
try {
Map<String, Object> parsed = JsonUtil.toMap(JsonUtil.toJson(value));
return parsed == null ? Map.of() : parsed;
} catch (Exception ignored) {
return Map.of();
}
}
private static List<String> extractTexts(Object listObj, String... keys) {
List<String> result = new ArrayList<>();
if (!(listObj instanceof Collection<?> collection) || keys == null) {
return result;
}
for (Object item : collection) {
Map<String, Object> map = asMap(item);
for (String key : keys) {
String text = Func.toStr(map.get(key), "").trim();
if (Func.isNotEmpty(text)) {
result.add(text);
break;
}
}
}
return result;
}
private static String joinDistinct(List<String> values, String delimiter) {
return values.stream().filter(Func::isNotEmpty).distinct().collect(Collectors.joining(delimiter));
}
}
@@ -0,0 +1,182 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.service.impl;
import com.alibaba.fastjson2.JSON;
import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.api.FR;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.process.feign.IBusinessProcessClient;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 客商公开页流程消息处理:当前节点立即查询,流程实例详情延迟查询。
*/
@Slf4j
@Service
@TenantIgnore
@RequiredArgsConstructor
public class CustomerArchivePublicProcessService {
private static final long PROCESS_INFO_DELAY_SECONDS = 5L;
private final ICustomerArchiveService customerArchiveService;
private final IBusinessProcessClient businessProcessClient;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2, runnable -> {
Thread thread = new Thread(runnable, "customer-archive-process-info-" + THREAD_INDEX.incrementAndGet());
thread.setDaemon(true);
return thread;
});
private static final AtomicInteger THREAD_INDEX = new AtomicInteger();
@PreDestroy
public void shutdown() {
scheduler.shutdown();
}
public void handleProcessMessage(Map<String, Object> body) {
Map<String, Object> formValues = asMap(body == null ? null : body.get("formValues"));
String processId = firstText(formValues, "processId");
if (StringUtil.isBlank(processId) && body != null) {
processId = firstText(body, "processId");
}
String loginName = firstText(formValues, "mkLoginName", "loginName");
if (StringUtil.isBlank(processId)) {
log.warn("客商公开页流程消息未找到 processId,跳过查询当前节点");
return;
}
String customerIdText = firstText(asMap(body == null ? null : body.get("formData")), "id");
queryCurrentNodes(processId, loginName);
scheduleProcessInfo(processId, loginName, customerIdText, formValues);
}
private void queryCurrentNodes(String processId, String loginName) {
try {
FR<Object> result = businessProcessClient.getCurrentNodes(processId, loginName);
Object nodeData = result == null ? null : result.getData();
log.info("客商公开页流程消息当前节点详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(nodeData));
} catch (Exception e) {
log.error("客商公开页查询当前节点失败 processId={} loginName={}", processId, loginName, e);
}
}
private void scheduleProcessInfo(String processId, String loginName, String customerIdText,
Map<String, Object> formValues) {
Map<String, Object> formValuesCopy = new HashMap<>(formValues == null ? Map.of() : formValues);
log.info("客商公开页将在{}秒后查询流程实例详情 processId={}", PROCESS_INFO_DELAY_SECONDS, processId);
scheduler.schedule(
() -> queryProcessInfo(processId, loginName, customerIdText, formValuesCopy),
PROCESS_INFO_DELAY_SECONDS,
TimeUnit.SECONDS
);
}
private void queryProcessInfo(String processId, String loginName, String customerIdText,
Map<String, Object> formValues) {
try {
FR<Object> processInfoResult = businessProcessClient.getProcessInfo(processId, loginName);
Object processInfo = processInfoResult == null ? null : processInfoResult.getData();
log.info("客商公开页流程消息流程实例详情 processId={} loginName={} result={}",
processId, loginName, JSON.toJSONString(processInfo));
if (isProcessFinished(processInfo)) {
applyProcessResult(customerIdText, processId, formValues, true);
} else if (isProcessRejected(processInfo)) {
applyProcessResult(customerIdText, processId, formValues, false);
} else if (StringUtil.isNotBlank(customerIdText)) {
try {
customerArchiveService.syncProcessNodeFromProcessInfo(Long.valueOf(customerIdText), processInfo);
} catch (NumberFormatException e) {
log.warn("客商公开页流程消息客商id格式不正确:{}", customerIdText);
}
}
} catch (Exception e) {
log.error("客商公开页查询流程实例详情失败 processId={} loginName={}", processId, loginName, e);
}
}
private static boolean isProcessFinished(Object processInfo) {
return "30".equals(firstText(asMap(processInfo), "fdProcessStatus"));
}
private static boolean isProcessRejected(Object processInfo) {
Map<String, Object> info = asMap(processInfo);
if (!"20".equals(firstText(info, "fdProcessStatus"))) {
return false;
}
if (hasItems(info.get("currentHandlers"))) {
return false;
}
return !hasItems(asMap(info.get("fdTaskInfo")).get("handlerInfos"));
}
private static boolean hasItems(Object value) {
return value instanceof Collection<?> collection && !collection.isEmpty();
}
private void applyProcessResult(String customerIdText, String processId, Map<String, Object> formValues,
boolean approved) {
String action = approved ? "审核通过" : "审核驳回";
if (StringUtil.isBlank(customerIdText)) {
log.warn("流程{}但未找到客商id,跳过同步 processId={}", action, processId);
return;
}
try {
String processorName = firstText(formValues, "mkUserName", "mkLoginName");
Long customerId = Long.valueOf(customerIdText);
if (approved) {
customerArchiveService.approveFromProcess(customerId, processorName);
} else {
customerArchiveService.rejectFromProcess(customerId, processorName);
}
} catch (NumberFormatException e) {
log.warn("流程{}但客商id格式不正确:{}", action, customerIdText);
}
}
private static Map<String, Object> asMap(Object value) {
if (!(value instanceof Map<?, ?> map)) {
return Collections.emptyMap();
}
Map<String, Object> result = new HashMap<>();
map.forEach((key, nested) -> {
if (key != null) {
result.put(String.valueOf(key), nested);
}
});
return result;
}
private static String firstText(Map<String, Object> source, String... keys) {
if (source == null || keys == null) {
return null;
}
for (String key : keys) {
Object value = source.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (StringUtil.isNotBlank(text) && !"null".equalsIgnoreCase(text)) {
return text;
}
}
return null;
}
}
@@ -1,6 +1,7 @@
package org.springblade.thirdparty.mk.pojo.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serial;
@@ -44,4 +45,9 @@ public class MKProcessCreateDTO implements Serializable {
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
private Object tempVarData;
/**
* 业务类型,仅 ERP 内部使用,不传给 MK
*/
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String bizType;
}
+3
View File
@@ -218,11 +218,14 @@ blade:
- /blade-auth/oauth/logout/**
- /blade-transport/customer-archive/public/**
- /customer-archive/public/**
- /blade-transport/mk-process/public/**
- /mk-process/public/**
- /blade-openapi/openApi/mk/process/commonCallback
- /openApi/mk/process/commonCallback
- /feign/client/businessProcess/getCurrentNodes
- /feign/client/businessProcess/getProcessInfo
- /feign/client/customerArchive/syncProcessNode
- /feign/client/mkProcess/**
#授权认证配置
auth:
- method: ALL