Sfoglia il codice sorgente

fix(workhours): 审批回写修 resolveDetailRows NPE + 加同步状态字段(v0.6+v0.7)

v0.6 修 NPE: resolveDetailRows 当 inlineRows==null(审批单不含某子表 key,如其他工时审批单无
BILLABLE/NON_BILLABLE,或工时审批单仅填项目工时未填非项目)时不再调 queryDetails,基座
_queryDetails 对宜搭返回 data=null 的空子表查询会 ArrayList.addAll(null) NPE 致接口 500
/汇总表完全不更新;queryDetails 返回 null 同样兜底空 list。规范沉淀进 yida-serverside.md
§子表取数控制(新增 inlineRows==null 第三档分支)。

v0.7 加同步状态字段+sleep 2s 避同步锁:
- ApprovalWriteBackResult 加 syncStatus(成功/部分成功/失败) + syncTotal + failCount 三字段
- WHConf 加 6 个 fieldId 配置(工时/其他工时审批各 3 个),application-dev.yml 占位空串
- 汇总表全部回写完毕后 sleep 2s 等宜搭审批结束的内部同步索引/锁释放,
  再 update 原审批单写 3 字段(writebackSyncFields)
- fieldId 任一为空跳过该类别回写(向后兼容,业务方分阶段建字段)
- 回写异常仅 error 日志不抛(同步字段不影响主流程返回)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
malk 1 settimana fa
parent
commit
21a98a61b8

+ 17 - 0
mjava-akdsbeisen/src/main/java/com/malk/server/workhours/ApprovalWriteBackResult.java

@@ -54,4 +54,21 @@ public class ApprovalWriteBackResult {
      * 处理失败的记录数
      */
     private int failRecords;
+
+    /**
+     * 同步状态:成功 / 部分成功 / 失败
+     * <p>
+     * 计算口径:failCount==0 → 成功;failCount<syncTotal → 部分成功;failCount==syncTotal → 失败
+     */
+    private String syncStatus;
+
+    /**
+     * 同步总数 = 需要处理的汇总表「人+天」日记录组数 (== groups)
+     */
+    private int syncTotal;
+
+    /**
+     * 失败数量 = 线程执行/重试耗尽后的失败组数 (== failRecords,独立字段便于排查/对外可见)
+     */
+    private int failCount;
 }

+ 14 - 0
mjava-akdsbeisen/src/main/java/com/malk/server/workhours/WHConf.java

@@ -27,4 +27,18 @@ public class WHConf {
     private String yidaAppType;
 
     private String yidaSystemToken;
+
+    // prd 审批回写同步状态字段:写回原审批单(工时审批 / 其他工时审批 各 3 个 fieldId)
+    // fixme 6 个 fieldId 任一为空则跳过该类别的同步字段回写(向后兼容,允许业务方分阶段建字段)
+    private String approvalSyncStatusField;        // 工时审批 - 同步状态(单选/文本)
+
+    private String approvalSyncTotalField;         // 工时审批 - 同步总数(数字)
+
+    private String approvalSyncFailField;          // 工时审批 - 失败数量(数字)
+
+    private String otherApprovalSyncStatusField;   // 其他工时审批 - 同步状态(单选/文本)
+
+    private String otherApprovalSyncTotalField;    // 其他工时审批 - 同步总数(数字)
+
+    private String otherApprovalSyncFailField;     // 其他工时审批 - 失败数量(数字)
 }

+ 61 - 4
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/ApprovalWriteBackService.java

@@ -13,6 +13,7 @@ import com.malk.utils.UtilMap;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
@@ -125,6 +126,7 @@ public class ApprovalWriteBackService {
      * @param result            审批结果:0=拒绝/撤销,1=同意
      * @return 回写结果统计
      */
+    @Async
     public ApprovalWriteBackResult writeBack(String formInstanceId, int result) {
         McException.assertAccessException(StringUtils.isBlank(formInstanceId), "实例ID不能为空");
 
@@ -209,6 +211,12 @@ public class ApprovalWriteBackService {
             executor.shutdown();
         }
 
+        // 6. 同步状态汇总(syncTotal=分组数;failCount=失败组数;status=成功/部分成功/失败)
+        int syncTotal = groups.size();
+        int failCount = failRecords.get();
+        String syncStatus = (syncTotal == 0 || failCount == 0) ? "成功"
+                : (failCount < syncTotal ? "部分成功" : "失败");
+
         ApprovalWriteBackResult stats = ApprovalWriteBackResult.builder()
                 .formType(isOther ? "其他工时审批" : "工时审批")
                 .formInstanceId(formInstanceId)
@@ -218,12 +226,53 @@ public class ApprovalWriteBackService {
                 .hitRecords(hitRecords.get())
                 .updatedRows(updatedRows.get())
                 .missRows(missRows.get())
-                .failRecords(failRecords.get())
+                .failRecords(failCount)
+                .syncStatus(syncStatus)
+                .syncTotal(syncTotal)
+                .failCount(failCount)
                 .build();
         log.info("[审批回写] 完成 {}", stats);
+
+        // 7. 延迟 2s 等宜搭审批结束的内部同步索引/锁释放,再回写审批单同步字段,避免触发同步锁失败
+        sleep(2000);
+        writebackSyncFields(isOther, formInstanceId, syncStatus, syncTotal, failCount);
+
         return stats;
     }
 
+    /**
+     * 把同步状态/同步总数/失败数量回写到原审批单(fieldId 走 WHConf 配置)
+     * fixme 三个 fieldId 任一为空则跳过该类别回写,允许业务方分阶段在宜搭后台建字段
+     */
+    private void writebackSyncFields(boolean isOther, String formInstanceId,
+                                     String syncStatus, int syncTotal, int failCount) {
+        String statusField = isOther ? whConf.getOtherApprovalSyncStatusField() : whConf.getApprovalSyncStatusField();
+        String totalField = isOther ? whConf.getOtherApprovalSyncTotalField() : whConf.getApprovalSyncTotalField();
+        String failField = isOther ? whConf.getOtherApprovalSyncFailField() : whConf.getApprovalSyncFailField();
+        if (StringUtils.isAnyBlank(statusField, totalField, failField)) {
+            log.info("[审批回写] 跳过审批单同步字段回写(配置未启用) isOther={} formInstanceId={}", isOther, formInstanceId);
+            return;
+        }
+        try {
+            Map<String, Object> sync = new HashMap<>();
+            sync.put(statusField, syncStatus);
+            sync.put(totalField, syncTotal);
+            sync.put(failField, failCount);
+            ydClient.operateData(YDParam.builder()
+                    .appType(whConf.getYidaAppType())
+                    .systemToken(whConf.getYidaSystemToken())
+                    .formInstanceId(formInstanceId)
+                    .updateFormDataJson(JSON.toJSONString(sync))
+                    .ignoreEmpty(false)
+                    .useLatestVersion(true)
+                    .build(), YDConf.FORM_OPERATION.update);
+            log.info("[审批回写] 审批单同步字段已回写 isOther={} formInstanceId={} status={} total={} fail={}",
+                    isOther, formInstanceId, syncStatus, syncTotal, failCount);
+        } catch (Exception ex) {
+            log.error("[审批回写] 审批单同步字段回写失败 isOther={} formInstanceId={}", isOther, formInstanceId, ex);
+        }
+    }
+
     /**
      * 处理单条汇总表日记录
      *
@@ -378,20 +427,28 @@ public class ApprovalWriteBackService {
 
     /**
      * 子表取数控制:主表详情已内联返回子表时,行数 &lt;50 视为完整直接用;
-     * ==50(可能被宜搭截断)或内联缺失,才调 {@link YDService#queryDetails} 递归取全,避免无效请求。
+     * ==50(可能被宜搭截断)才调 {@link YDService#queryDetails} 递归取全,避免无效请求。
      * 复用 ydService 封装,不重复造分页轮子。
+     * <p>
+     * fixme inlineRows==null 表示审批单不含该子表(如其他工时审批单无 BILLABLE/NON_BILLABLE,或工时审批单某一子表未填):
+     * 直接返回空 list,不再调 queryDetails——基座 _queryDetails 对宜搭返回 data=null 的空子表查询会触发
+     * ArrayList.addAll(null) NPE。queryDetails 返回 null 同样兜底空 list。
      */
     private List<Map> resolveDetailRows(String formInstanceId, String tableId, List<Map> inlineRows) {
-        if (inlineRows != null && inlineRows.size() < 50) {
+        if (inlineRows == null) {
+            return Collections.emptyList();
+        }
+        if (inlineRows.size() < 50) {
             return inlineRows;
         }
-        return ydService.queryDetails(YDParam.builder()
+        List<Map> full = ydService.queryDetails(YDParam.builder()
                 .appType(whConf.getYidaAppType())
                 .systemToken(whConf.getYidaSystemToken())
                 .formInstanceId(formInstanceId)
                 .tableFieldId(tableId)
                 .pageNumber(1)
                 .build());
+        return full != null ? full : Collections.emptyList();
     }
 
     /**

+ 7 - 0
mjava-akdsbeisen/src/main/resources/application-dev.yml

@@ -76,3 +76,10 @@ workhours:
   formUuidOtherApproval: "FORM-4828E0E40CD34038825E8C6E25417B2718NF"   # 正式 FORM-BA14F6322DBD470F8CF84CCE131DEA31TIJS
   yidaAppType: "APP_ZQ3I7XO2RSHDJ4QDEVNB"
   yidaSystemToken: "FOD66381NOS25MERLN2UK92FY96Y21UMHD7LM36S"
+  # 审批回写同步状态字段:业务方在工时审批 / 其他工时审批表单上各加 3 个字段(同步状态-文本/单选 / 同步总数-数字 / 失败数量-数字),把 fieldId 填到下方即可启用回写;任一为空则跳过该类别的同步字段回写
+  approvalSyncStatusField: ""
+  approvalSyncTotalField: ""
+  approvalSyncFailField: ""
+  otherApprovalSyncStatusField: ""
+  otherApprovalSyncTotalField: ""
+  otherApprovalSyncFailField: ""