新增手动拉取钉钉审批结果功能,支持强制重跑集成,优化审批台账管理。涉及多个文件的更新,包括API接口、服务逻辑及前端页面。

This commit is contained in:
geht
2026-07-30 16:58:10 +08:00
parent 7a9c19e4f3
commit 3f22eb346e
10 changed files with 299 additions and 16 deletions

View File

@@ -1248,7 +1248,7 @@ jeecgboot-vue3/src/views/xslmes/mesXslDryingProcessSwitch/MesXslDryingProcessSwi
jeecgboot-vue3/src/views/xslmes/mesXslDryingProcessSwitch/MesXslDryingProcessSwitch.api.ts
-- author:jiangxh---date:20260708--for: 【MES】烘胶流程开启总开关+二次确认,不校验烘胶房日期) ---
-- author:xsl---date:20260720--for: <20><>APPԭ<50><D4AD><EFBFBD><EFBFBD><EFBFBD>ͼ졿<CDBC>ֳֶ˰<D6B6><CBB0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѯ/<2F>ͼ<EFBFBD><><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD> ---
-- author:xsl---date:20260720--for: <20><>APPԭ<50><D4AD><EFBFBD><EFBFBD><EFBFBD>ͼ졿<CDBC>ֳֶ˰<D6B6><CBB0><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѯ/<2F>ͼ<EFBFBD><><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ӿ<EFBFBD><D3BF><EFBFBD>ҳ<EFBFBD><D2B3> ---
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/vo/MesXslRawMaterialInspectAppScanVo.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/controller/MesXslRawMaterialInspectAppController.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/service/IMesXslRawMaterialInspectRecordService.java
@@ -1257,3 +1257,14 @@ JeecgUniapp-master/src/service/xslmes/rawMaterialInspect.ts
JeecgUniapp-master/src/pages/rawMaterialInspect/rawMaterialInspect.vue
JeecgUniapp-master/src/pages/rawMaterialInspect/inspectResult.vue
JeecgUniapp-master/src/common/work.ts
-- author:GHT---date:20260730--for: <20><>XSLMES-20260730-DING-PULL<4C><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̨<EFBFBD><CCA8><EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD>ܼ<EFBFBD><DCBC><EFBFBD> ---
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationForceReplayHolder.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/integration/engine/IntegrationOrchestrator.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingApprovalManualPullService.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/dingtalk/stream/DingBpmsEventProcessor.java
jeecg-boot-module/jeecg-module-xslmes/src/main/java/org/jeecg/modules/xslmes/approval/controller/MesXslApprovalRecordController.java
jeecg-module-system/jeecg-system-start/src/main/resources/flyway/sql/mysql/V3.9.2_164__mes_xsl_approval_record_pull_ding_result.sql
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecordList.vue
jeecgboot-vue3/src/views/xslmes/approval/mesXslApprovalRecord/MesXslApprovalRecord.api.ts
-- author:GHT---date:20260730--for: <20><>XSLMES-20260730-DING-PULL<4C><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̨<EFBFBD><CCA8><EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD><EFBFBD>ܼ<EFBFBD><DCBC><EFBFBD> ---

View File

@@ -13,8 +13,10 @@ import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
import org.jeecg.modules.xslmes.dingtalk.stream.DingApprovalManualPullService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -35,6 +37,9 @@ public class MesXslApprovalRecordController extends JeecgController<MesXslApprov
@Autowired
private IMesXslApprovalRecordService mesXslApprovalRecordService;
@Autowired
private DingApprovalManualPullService dingApprovalManualPullService;
@Operation(summary = "MES审批台账-分页列表查询")
@RequiresPermissions("xslmes:mes_xsl_approval_record:list")
@GetMapping(value = "/list")
@@ -61,6 +66,16 @@ public class MesXslApprovalRecordController extends JeecgController<MesXslApprov
return Result.OK(entity);
}
//update-begin---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】台账手动拉取钉钉结果并强制重跑集成-----
@Operation(summary = "MES审批台账-手动拉取钉钉审批结果并强制重跑集成")
// 与「补发IM卡片」一致复用 list 权限,避免新权限未入库/未赋权导致管理员也无法操作
@RequiresPermissions("xslmes:mes_xsl_approval_record:list")
@PostMapping(value = "/pullDingResult")
public Result<String> pullDingResult(@RequestParam(name = "id") String id) {
return dingApprovalManualPullService.pullAndForceReplay(id);
}
//update-end---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】台账手动拉取钉钉结果并强制重跑集成-----
@RequiresPermissions("xslmes:mes_xsl_approval_record:exportXls")
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, MesXslApprovalRecord model) {

View File

@@ -0,0 +1,25 @@
package org.jeecg.modules.xslmes.approval.integration.engine;
/**
* 手动强制重跑集成时的线程标记:旁路幂等跳过,允许假 success 后再次执行。
*/
public final class IntegrationForceReplayHolder {
private static final ThreadLocal<Boolean> FORCE = ThreadLocal.withInitial(() -> Boolean.FALSE);
private IntegrationForceReplayHolder() {
}
public static boolean isForced() {
return Boolean.TRUE.equals(FORCE.get());
}
public static void runForced(Runnable action) {
FORCE.set(Boolean.TRUE);
try {
action.run();
} finally {
FORCE.remove();
}
}
}

View File

@@ -223,11 +223,16 @@ public class IntegrationOrchestrator {
IIntegrationActionExecutor executor = findExecutor(action.getActionType());
String response = executor.execute(ctx, action);
long ms = System.currentTimeMillis() - t0;
writeLog(ctx, action, idempotentKey, "success", null, response, snapshot, null, ms);
successCount++;
//update-begin---author:GHT ---date:20260608 for【审核集成】动作成功后刷新源单快照供后续动作使用最新字段-----------
refreshSourceRecord(ctx);
//update-end---author:GHT ---date:20260608 for【审核集成】动作成功后刷新源单快照供后续动作使用最新字段-----------
//update-begin---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】条件不符的「跳过」不记 success避免假成功堵死重试-----------
if (oConvertUtils.isNotEmpty(response) && response.startsWith("跳过:")) {
writeLog(ctx, action, idempotentKey, "skipped", null, response, snapshot, null, ms);
log.info("[集成引擎] 动作条件跳过 action={} response={}", action.getActionName(), response);
} else {
writeLog(ctx, action, idempotentKey, "success", null, response, snapshot, null, ms);
successCount++;
refreshSourceRecord(ctx);
}
//update-end---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】条件不符的「跳过」不记 success避免假成功堵死重试-----------
} catch (Exception e) {
long ms = System.currentTimeMillis() - t0;
String errMsg = e.getMessage();
@@ -386,8 +391,26 @@ public class IntegrationOrchestrator {
/**
* 回退类动作:历史 success 但源单 status 仍未到 targetStage 时,允许再次执行。
* 环节同步:历史 success 但源单仍未到 statusAfter 时,允许再次执行(修复假成功堵死)。
* 手动强制重跑:一律旁路。
*/
private boolean shouldBypassIdempotentSkip(IntegrationContext ctx, MesXslIntegrationAction action) {
//update-begin---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】强制重跑+环节同步未达目标旁路幂等-----------
if (IntegrationForceReplayHolder.isForced()) {
return true;
}
if ("REGISTRY_STAGE_SYNC".equals(action.getActionType())) {
String statusAfter = resolveSyncStatusAfter(ctx, action);
if (oConvertUtils.isEmpty(statusAfter)) {
return false;
}
String currentStatus = readSourceStatus(ctx);
if (oConvertUtils.isEmpty(currentStatus)) {
return true;
}
return !statusAfter.equals(currentStatus);
}
//update-end---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】强制重跑+环节同步未达目标旁路幂等-----------
if (!"REGISTRY_STAGE_REVERT".equals(action.getActionType())) {
return false;
}
@@ -399,6 +422,13 @@ public class IntegrationOrchestrator {
return !targetStage.equals(currentStatus);
}
//update-begin---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】解析环节同步目标状态-----------
private String resolveSyncStatusAfter(IntegrationContext ctx, MesXslIntegrationAction action) {
String stage = IntegrationActionConfigHelper.resolveStage(action, ctx.getPlan());
return IntegrationActionConfigHelper.resolveStatusAfter(action, stage);
}
//update-end---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】解析环节同步目标状态-----------
private String resolveRevertTargetStage(MesXslIntegrationAction action) {
String target = IntegrationActionConfigHelper.resolveTargetStage(action);
if (oConvertUtils.isNotEmpty(target)) {

View File

@@ -0,0 +1,143 @@
package org.jeecg.modules.xslmes.dingtalk.stream;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.xslmes.approval.constant.ApprovalRecordConstants;
import org.jeecg.modules.xslmes.approval.entity.MesXslApprovalRecord;
import org.jeecg.modules.xslmes.approval.integration.engine.IntegrationForceReplayHolder;
import org.jeecg.modules.xslmes.approval.service.IMesXslApprovalRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 审批台账手动拉取钉钉结果并强制重跑集成。
* <p>
* 用于 Stream 接收节点未在线导致漏推、或集成假 success 后幂等卡死的人工修复。
*/
@Slf4j
@Service
public class DingApprovalManualPullService {
private static final String LOG_TAG = DingTalkStreamSdkRunner.LOG_TAG;
@Autowired
private IMesXslApprovalRecordService approvalRecordService;
@Autowired
private DingTalkWorkflowService workflowService;
@Autowired
private DingBpmsEventProcessor eventProcessor;
//update-begin---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】台账手动拉取钉钉审批结果并强制重跑集成-----
/**
* 按台账 ID 拉取钉钉实例,强制重放已同意中间节点集成,终态时再触发 onInstanceChange。
*/
public Result<String> pullAndForceReplay(String recordId) {
if (oConvertUtils.isEmpty(recordId)) {
return Result.error("台账ID不能为空");
}
MesXslApprovalRecord record = approvalRecordService.getById(recordId);
if (record == null) {
return Result.error("未找到审批台账");
}
if (!ApprovalRecordConstants.CHANNEL_DINGTALK.equals(record.getChannel())) {
return Result.error("仅支持钉钉通道台账");
}
if (oConvertUtils.isEmpty(record.getExternalInstanceId())) {
return Result.error("台账无钉钉实例ID无法拉取");
}
if (oConvertUtils.isEmpty(record.getBizTable()) || oConvertUtils.isEmpty(record.getBizDataId())) {
return Result.error("台账缺少业务表或业务ID");
}
String instanceId = record.getExternalInstanceId();
JSONObject instance = workflowService.getProcessInstance(instanceId);
if (instance == null) {
return Result.error("拉取钉钉审批实例失败,请稍后重试");
}
String dingStatus = instance.getString("status");
String dingResult = instance.getString("result");
log.info("{} 手动拉取开始 recordId={} instanceId={} dingStatus={} dingResult={}",
LOG_TAG, recordId, instanceId, dingStatus, dingResult);
int[] nodeHolder = {0};
boolean[] terminalHolder = {false};
String[] errHolder = {null};
IntegrationForceReplayHolder.runForced(() -> {
try {
nodeHolder[0] = eventProcessor.forceReconcileAgreedNodes(record, instance);
JSONObject synthetic = buildSyntheticEvent(instance, instanceId);
if (synthetic != null) {
eventProcessor.onInstanceChange(synthetic);
terminalHolder[0] = true;
}
} catch (Exception e) {
log.error("{} 手动拉取强制重跑失败 recordId={} instanceId={}: {}",
LOG_TAG, recordId, instanceId, e.getMessage(), e);
errHolder[0] = e.getMessage();
}
});
if (errHolder[0] != null) {
return Result.error("拉取成功但重跑集成失败: " + errHolder[0]);
}
StringBuilder msg = new StringBuilder("已拉取钉钉结果并强制重跑集成");
msg.append("(钉钉状态=").append(dingStatus);
if (oConvertUtils.isNotEmpty(dingResult)) {
msg.append(",结果=").append(dingResult);
}
msg.append("");
msg.append(";中间节点重放 ").append(nodeHolder[0]).append("");
if (terminalHolder[0]) {
msg.append(";已触发终态回调");
} else if ("RUNNING".equalsIgnoreCase(dingStatus)) {
msg.append(";钉钉仍流转中,仅补偿已同意节点");
} else {
msg.append(";未映射到可处理终态,请核对钉钉实例");
}
log.info("{} 手动拉取完成 recordId={} {}", LOG_TAG, recordId, msg);
return Result.OK(msg.toString());
}
/**
* 将钉钉实例状态映射为 onInstanceChange 所需的合成事件(与补偿扫描一致)。
*/
private JSONObject buildSyntheticEvent(JSONObject instance, String processInstanceId) {
String dingStatus = instance.getString("status");
String dingResult = instance.getString("result");
if ("RUNNING".equalsIgnoreCase(dingStatus)) {
return null;
}
JSONObject event = new JSONObject();
event.put("processInstanceId", processInstanceId);
if ("COMPLETED".equalsIgnoreCase(dingStatus)) {
if ("agree".equalsIgnoreCase(dingResult)) {
event.put("type", "finish");
event.put("result", "agree");
} else if ("refuse".equalsIgnoreCase(dingResult)) {
event.put("type", "finish");
event.put("result", "refuse");
} else {
event.put("type", "finish");
event.put("result", oConvertUtils.isEmpty(dingResult) ? "unknown" : dingResult);
}
return event;
}
if ("TERMINATED".equalsIgnoreCase(dingStatus) || "CANCELED".equalsIgnoreCase(dingStatus)) {
event.put("type", "terminate");
return event;
}
return null;
}
//update-end---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】台账手动拉取钉钉审批结果并强制重跑集成-----
}

View File

@@ -491,10 +491,22 @@ public class DingBpmsEventProcessor {
* @return 本次补偿触发的节点数
*/
public int reconcileIntermediateNodes(MesXslApprovalRecord record, JSONObject instance) {
return reconcileAgreedNodes(record, instance, false);
}
//update-begin---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】强制重放已同意中间节点不限台账流转态-----
/**
* 手动拉取场景:忽略台账是否已终态、忽略历史集成 success强制按序重放已同意节点。
*/
public int forceReconcileAgreedNodes(MesXslApprovalRecord record, JSONObject instance) {
return reconcileAgreedNodes(record, instance, true);
}
private int reconcileAgreedNodes(MesXslApprovalRecord record, JSONObject instance, boolean force) {
if (record == null || instance == null) {
return 0;
}
if (!ApprovalRecordConstants.STATUS_RUNNING.equals(record.getStatus())) {
if (!force && !ApprovalRecordConstants.STATUS_RUNNING.equals(record.getStatus())) {
return 0;
}
MesXslApprovalFlow flow = loadFlow(record.getFlowId());
@@ -504,8 +516,8 @@ public class DingBpmsEventProcessor {
List<NodePair> pairs = instanceStageExtractor.alignMesNodesWithTasks(instance, flow.getFlowConfig());
if (pairs.isEmpty()) {
log.info("{} 中间节点补偿:无节点对齐 instanceId={} recordId={}",
LOG_TAG, record.getExternalInstanceId(), record.getId());
log.info("{} 中间节点补偿:无节点对齐 instanceId={} recordId={} force={}",
LOG_TAG, record.getExternalInstanceId(), record.getId(), force);
return 0;
}
@@ -519,7 +531,7 @@ public class DingBpmsEventProcessor {
}
String planId = resolveNodeIntegrationPlanId(pair.getMesNode());
if (oConvertUtils.isNotEmpty(planId) && hasIntegrationSuccess(record.getId(), planId)) {
if (!force && oConvertUtils.isNotEmpty(planId) && hasIntegrationSuccess(record.getId(), planId)) {
continue;
}
if (oConvertUtils.isEmpty(planId)) {
@@ -529,8 +541,8 @@ public class DingBpmsEventProcessor {
JSONObject mesNode = pair.getMesNode();
String nodeName = mesNode != null ? mesNode.getString("name") : null;
log.info("{} 中间节点补偿:触发集成 instanceId={} recordId={} nodeIndex={} nodeName={} planId={}",
LOG_TAG, record.getExternalInstanceId(), record.getId(), nodeIndex, nodeName, planId);
log.info("{} 中间节点补偿:触发集成 instanceId={} recordId={} nodeIndex={} nodeName={} planId={} force={}",
LOG_TAG, record.getExternalInstanceId(), record.getId(), nodeIndex, nodeName, planId, force);
try {
fireCompensatedNodeApproved(record, instance, pair, nodeIndex, decision);
@@ -542,6 +554,7 @@ public class DingBpmsEventProcessor {
}
return compensated;
}
//update-end---author:GHT ---date:20260730 for【XSLMES-20260730-DING-PULL】强制重放已同意中间节点不限台账流转态-----
private void fireCompensatedNodeApproved(MesXslApprovalRecord record, JSONObject instance,
NodePair pair, int nodeIndex, NodeTaskDecision decision) {