|
|
@@ -4,9 +4,12 @@ import com.alibaba.fastjson.JSON;
|
|
|
import com.malk.server.aliwork.YDConf;
|
|
|
import com.malk.server.aliwork.YDParam;
|
|
|
import com.malk.server.dingtalk.DDR_New;
|
|
|
+import com.malk.server.workhours.ApprovalWriteBackResult;
|
|
|
+import com.malk.server.workhours.BackendExceptionLog;
|
|
|
import com.malk.server.workhours.WHConf;
|
|
|
import com.malk.service.aliwork.YDClient;
|
|
|
import com.malk.service.workhours.ApprovalWriteBackService;
|
|
|
+import com.malk.service.workhours.BackendExceptionLogService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
@@ -23,11 +26,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|
|
* 审批回写兜底定时(bug 2 修复)
|
|
|
* <p>
|
|
|
* 每天 04:20、12:20 触发,扫描两类审批单 instanceStatus=COMPLETED、
|
|
|
- * 同步状态 selectField_mq58cd5p 为空的实例,主动调 ApprovalWriteBackService.writeBack 补齐工时汇总表回写。
|
|
|
+ * 同步状态非“全部成功”的实例,主动调 ApprovalWriteBackService.writeBack 补齐工时汇总表回写。
|
|
|
* <p>
|
|
|
* 触发原因场景:
|
|
|
* 1. 审批完成时宜搭端未回调,writeBack 从未被调
|
|
|
- * 2. writeBack 调用但网络/宜搭异常,回写失败(会被 syncStatus 置为"部分失败"或"全部失败",此定时兜底不会重复处理)
|
|
|
+ * 2. writeBack 调用但网络/宜搭异常,回写失败(syncStatus 为“部分失败”或“全部失败”时继续补偿)
|
|
|
* <p>
|
|
|
* 幂等:writeBack 内部 REJECT→0 / APPROVE←hours, 重复执行结果一致
|
|
|
* <p>
|
|
|
@@ -50,11 +53,14 @@ public class ApprovalWriteBackTimer {
|
|
|
@Autowired
|
|
|
private ApprovalWriteBackService approvalWriteBackService;
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private BackendExceptionLogService backendExceptionLogService;
|
|
|
+
|
|
|
private static final AtomicBoolean RUNNING = new AtomicBoolean(false);
|
|
|
|
|
|
/**
|
|
|
* 每天 04:20、12:20 兜底扫描审批单。
|
|
|
- * prd 中午回写先于 12:45 审批补发执行,先消化已完成审批,再检查剩余漏发记录。
|
|
|
+ * prd 中午回写先于 12:55 审批补发执行,先消化已完成审批,再检查剩余漏发记录。
|
|
|
*/
|
|
|
@Scheduled(cron = "0 20 4 * * ?")
|
|
|
@Scheduled(cron = "0 20 12 * * ?")
|
|
|
@@ -64,7 +70,7 @@ public class ApprovalWriteBackTimer {
|
|
|
return;
|
|
|
}
|
|
|
try {
|
|
|
- log.info("[审批回写兜底] 开始扫描 syncStatus 空的 COMPLETED 审批单");
|
|
|
+ log.info("[审批回写兜底] 开始扫描 syncStatus 非全部成功的 COMPLETED 审批单");
|
|
|
AtomicInteger totalScanned = new AtomicInteger(0);
|
|
|
AtomicInteger totalTriggered = new AtomicInteger(0);
|
|
|
AtomicInteger totalFailed = new AtomicInteger(0);
|
|
|
@@ -79,6 +85,7 @@ public class ApprovalWriteBackTimer {
|
|
|
totalScanned.get(), totalTriggered.get(), totalFailed.get());
|
|
|
} catch (Exception e) {
|
|
|
log.error("[审批回写兜底] 定时任务异常", e);
|
|
|
+ recordTimerException(e);
|
|
|
} finally {
|
|
|
RUNNING.set(false);
|
|
|
}
|
|
|
@@ -104,29 +111,44 @@ public class ApprovalWriteBackTimer {
|
|
|
}
|
|
|
for (Map item : data) {
|
|
|
scanned.incrementAndGet();
|
|
|
+ Object processInstanceId = item.get("processInstanceId");
|
|
|
+ String pid = processInstanceId == null ? "" : String.valueOf(processInstanceId);
|
|
|
+ Object formInstanceId = item.get("formInstanceId");
|
|
|
+ String finstId = formInstanceId == null ? pid : String.valueOf(formInstanceId);
|
|
|
// fixme getInstances 当前返回字段名为 data;兼容旧响应中的 formData。
|
|
|
Map fd = extractFormData(item);
|
|
|
if (fd == null) {
|
|
|
+ failed.incrementAndGet();
|
|
|
log.warn("[审批回写兜底] 流程实例缺少表单数据 keys={}", item.keySet());
|
|
|
+ recordMissingFormData(tag, finstId, item);
|
|
|
continue;
|
|
|
}
|
|
|
- String syncStatus = String.valueOf(fd.getOrDefault(whConf.getApprovalSyncStatusField(), ""));
|
|
|
- if (StringUtils.isNotBlank(syncStatus) && !"null".equals(syncStatus)) {
|
|
|
+ String syncStatusField = resolveSyncStatusField(tag);
|
|
|
+ String syncStatus = String.valueOf(fd.getOrDefault(syncStatusField, ""));
|
|
|
+ if (!shouldRetrySyncStatus(syncStatus)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Integer resultCode = resolveResultCode(item.get("approvedResult"));
|
|
|
+ if (resultCode == null) {
|
|
|
+ failed.incrementAndGet();
|
|
|
+ log.warn("[审批回写兜底] 审批结果缺失或未知,跳过并等待下轮 tag={} formInstanceId={} approvedResult={}",
|
|
|
+ tag, finstId, item.get("approvedResult"));
|
|
|
continue;
|
|
|
}
|
|
|
- String pid = String.valueOf(item.get("processInstanceId"));
|
|
|
- Object fInstObj = item.get("formInstanceId");
|
|
|
- String finstId = fInstObj != null ? String.valueOf(fInstObj) : pid;
|
|
|
- String approvedResult = String.valueOf(item.getOrDefault("approvedResult", "agree"));
|
|
|
- int resultCode = "agree".equals(approvedResult) ? 1 : 0;
|
|
|
try {
|
|
|
- approvalWriteBackService.writeBack(finstId, resultCode);
|
|
|
+ ApprovalWriteBackResult result =
|
|
|
+ approvalWriteBackService.writeBack(finstId, resultCode);
|
|
|
triggered.incrementAndGet();
|
|
|
+ if (result == null || !"全部成功".equals(result.getSyncStatus())) {
|
|
|
+ failed.incrementAndGet();
|
|
|
+ recordWriteBackFailure(tag, finstId, result);
|
|
|
+ }
|
|
|
log.info("[审批回写兜底] 触发 writeBack tag={} formInstanceId={} result={}",
|
|
|
tag, finstId, resultCode);
|
|
|
} catch (Exception ex) {
|
|
|
failed.incrementAndGet();
|
|
|
log.error("[审批回写兜底] writeBack 失败 tag={} formInstanceId={}", tag, finstId, ex);
|
|
|
+ recordWriteBackException(tag, finstId, ex);
|
|
|
}
|
|
|
}
|
|
|
if (!hasNextPage(page, PAGE_SIZE, r.getTotalCount(), data.size())) {
|
|
|
@@ -144,6 +166,107 @@ public class ApprovalWriteBackTimer {
|
|
|
return totalCount <= 0 || (long) pageNumber * pageSize < totalCount;
|
|
|
}
|
|
|
|
|
|
+ static boolean shouldRetrySyncStatus(String syncStatus) {
|
|
|
+ return !"全部成功".equals(StringUtils.trimToEmpty(syncStatus));
|
|
|
+ }
|
|
|
+
|
|
|
+ String resolveSyncStatusField(String tag) {
|
|
|
+ return "other".equals(tag)
|
|
|
+ ? whConf.getOtherApprovalSyncStatusField()
|
|
|
+ : whConf.getApprovalSyncStatusField();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recordMissingFormData(String tag, String formInstanceId, Map item) {
|
|
|
+ backendExceptionLogService.record(BackendExceptionLog.builder()
|
|
|
+ .category(BackendExceptionLogService.CATEGORY_DATA_VALIDATION)
|
|
|
+ .sourceSystem(BackendExceptionLogService.SOURCE_YIDA)
|
|
|
+ .sourceModule("ApprovalWriteBackTimer")
|
|
|
+ .sourceAction("scanForm")
|
|
|
+ .sourceDescription("审批回写兜底扫描读取 COMPLETED 流程实例表单数据")
|
|
|
+ .businessObjectType(tag + "审批单")
|
|
|
+ .businessObjectId(formInstanceId)
|
|
|
+ .errorCode("APPROVAL_FORM_DATA_MISSING")
|
|
|
+ .summary("审批流程实例缺少表单数据")
|
|
|
+ .detail("processKeys=" + item.keySet())
|
|
|
+ .retryable(true)
|
|
|
+ .retryCount(1)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recordWriteBackFailure(
|
|
|
+ String tag,
|
|
|
+ String formInstanceId,
|
|
|
+ ApprovalWriteBackResult result) {
|
|
|
+ String detail = result == null
|
|
|
+ ? "writeBackResult=null"
|
|
|
+ : "syncStatus=" + result.getSyncStatus()
|
|
|
+ + ", syncTotal=" + result.getSyncTotal()
|
|
|
+ + ", failCount=" + result.getFailCount()
|
|
|
+ + ", missRows=" + result.getMissRows();
|
|
|
+ backendExceptionLogService.record(BackendExceptionLog.builder()
|
|
|
+ .category(BackendExceptionLogService.CATEGORY_BUSINESS_DATA_MISSING)
|
|
|
+ .sourceSystem(BackendExceptionLogService.SOURCE_AKDS_BACKEND)
|
|
|
+ .sourceModule("ApprovalWriteBackTimer")
|
|
|
+ .sourceAction("writeBack")
|
|
|
+ .sourceDescription("审批完成后回写工时汇总表,结果不是全部成功")
|
|
|
+ .businessObjectType(tag + "审批单")
|
|
|
+ .businessObjectId(formInstanceId)
|
|
|
+ .errorCode("APPROVAL_WRITEBACK_NOT_ALL_SUCCESS")
|
|
|
+ .summary("审批回写未全部成功")
|
|
|
+ .detail(detail)
|
|
|
+ .retryable(true)
|
|
|
+ .retryCount(1)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recordWriteBackException(
|
|
|
+ String tag,
|
|
|
+ String formInstanceId,
|
|
|
+ Exception exception) {
|
|
|
+ backendExceptionLogService.record(BackendExceptionLog.builder()
|
|
|
+ .category(BackendExceptionLogService.CATEGORY_PROGRAM)
|
|
|
+ .sourceSystem(BackendExceptionLogService.SOURCE_AKDS_BACKEND)
|
|
|
+ .sourceModule("ApprovalWriteBackTimer")
|
|
|
+ .sourceAction("writeBack")
|
|
|
+ .sourceDescription("审批回写兜底任务调用审批回写服务")
|
|
|
+ .businessObjectType(tag + "审批单")
|
|
|
+ .businessObjectId(formInstanceId)
|
|
|
+ .errorCode("APPROVAL_WRITEBACK_EXCEPTION")
|
|
|
+ .summary("审批回写服务执行异常")
|
|
|
+ .detail(exception.getClass().getName() + ": " + exception.getMessage())
|
|
|
+ .retryable(true)
|
|
|
+ .retryCount(1)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+
|
|
|
+ private void recordTimerException(Exception exception) {
|
|
|
+ backendExceptionLogService.record(BackendExceptionLog.builder()
|
|
|
+ .category(BackendExceptionLogService.CATEGORY_SCHEDULED_TASK)
|
|
|
+ .sourceSystem(BackendExceptionLogService.SOURCE_AKDS_BACKEND)
|
|
|
+ .sourceModule("ApprovalWriteBackTimer")
|
|
|
+ .sourceAction("scanAndWriteBack")
|
|
|
+ .sourceDescription("每天 04:20、12:20 扫描两类 COMPLETED 审批并补偿回写")
|
|
|
+ .errorCode("APPROVAL_WRITEBACK_TIMER_EXCEPTION")
|
|
|
+ .summary("审批回写兜底定时任务异常")
|
|
|
+ .detail(exception.getClass().getName() + ": " + exception.getMessage())
|
|
|
+ .retryable(true)
|
|
|
+ .retryCount(1)
|
|
|
+ .build());
|
|
|
+ }
|
|
|
+
|
|
|
+ static Integer resolveResultCode(Object approvedResult) {
|
|
|
+ String value = approvedResult == null
|
|
|
+ ? "" : StringUtils.trimToEmpty(String.valueOf(approvedResult)).toLowerCase(Locale.ROOT);
|
|
|
+ if ("agree".equals(value)) {
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ if ("refuse".equals(value) || "reject".equals(value)
|
|
|
+ || "terminate".equals(value) || "cancel".equals(value)) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
@SuppressWarnings("unchecked")
|
|
|
static Map extractFormData(Map item) {
|
|
|
if (item == null) {
|