Przeglądaj źródła

中车-增加附件同步文件库

xmy 1 miesiąc temu
rodzic
commit
6e5a46a69b

+ 69 - 7
mjava-ounuo/src/main/java/com/malk/tuosi/service/impl/TbServiceImpl.java

@@ -15,10 +15,10 @@ import com.malk.server.dingtalk.DDConf;
 import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Extension;
+import com.malk.service.dingtalk.DDClient_Notice;
 import com.malk.tuosi.service.TBService;
 import com.malk.utils.UtilHttp;
 import com.malk.utils.UtilMap;
-import com.sun.crypto.provider.PBEWithMD5AndDESCipher;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import lombok.var;
@@ -284,7 +284,7 @@ public class TbServiceImpl implements TBService {
             if (sfcId.equals("6878b3c0669d175c3c6bc679") || sfcId.equals("6878b3ceb5ffcf500365d17a") || sfcId.equals("6878b3d8f40485da4c5f99a2")
                 || sfcId.equals("6878b39a27ae5f3cac355fb9") || sfcId.equals("6878b3a4090e6872fd6b78c6") || sfcId.equals("6878b3ad9e17ac8a027c6d37")
                 || sfcId.equals("68999dc2d1cb63e672e0b503") || sfcId.equals("68999dcf3071884859cacb87") || sfcId.equals("6878b3b60d49d89e7955726f")
-                || sfcId.equals("68999dd990de390188d8bee6")){
+                || sfcId.equals("68999dd990de390188d8bee6") || sfcId.equals("6a7c2663e9cc163c31e5cd62")){
                 String taskId = data.getString("taskId");
                 HashMap header = new HashMap();
                 header.put("x-operator-id","622ee3450cf3bb5e1a486f1f");//xmy
@@ -326,13 +326,13 @@ public class TbServiceImpl implements TBService {
                     case "68999dd990de390188d8bee6":
                         nodeId = "6880912c9c778244636507f8";
                         break;
+                    case "6a7c2663e9cc163c31e5cd62":
+                        nodeId = "6875f3cf4b7eb5199098892c";
+                        break;
                 }
                 body_AAA.put("nodeId",nodeId);
-                String doPut = UtilHttp.doPut(
-                        "https://open.teambition.com/api/v3/task/" + taskId + "/node/complete",
-                        header,
-                        null,
-                        body_AAA);
+                /*TODO:TB主从同步延迟,任务刚创建时从库可能查询不到,接口返回 errorCode=10002(参数有误: task),故最多尝试3次,仍失败则钉钉告警宁学刚*/
+                String doPut = doNodeCompleteWithRetry(header, taskId, body_AAA);
                 log.info("自动流程确认,任务id: {}",taskId );
                 log.info("[创建任务自动更新确认流程],{}",doPut);
             }
@@ -686,6 +686,8 @@ public class TbServiceImpl implements TBService {
     private DDConf ddConf;
     @Autowired
     private DDClient ddClient;
+    @Autowired
+    private DDClient_Notice ddClient_notice;   // 钉钉工作通知,用于告警推送
 
     @Override
     public McR oaCallbackTB(Map body) throws JsonProcessingException {
@@ -888,4 +890,64 @@ public class TbServiceImpl implements TBService {
         return html.toString();
     }
 
+    /**
+     * TB节点完成接口调用(带重试): 最多尝试3次, 仍失败则发送钉钉告警
+     * 背景: TB接口走主从分离, 任务创建后立即调用节点完成接口, 从库可能尚未同步到该任务,
+     *       返回 errorCode=10002 "参数有误: task"(官方建议加延迟/重试)
+     */
+    private String doNodeCompleteWithRetry(HashMap header, String taskId, HashMap body) throws InterruptedException {
+        final int MAX_ATTEMPT = 3;
+        // 每次尝试前的等待时间: 第1次立即执行, 之后递增等待, 给从库留同步时间
+        final long[] DELAY_MILLIS = {0L, 3000L, 5000L};
+        String doPut = "";
+        for (int attempt = 1; attempt <= MAX_ATTEMPT; attempt++) {
+            if (DELAY_MILLIS[attempt - 1] > 0) {
+                log.info("[创建任务自动更新确认流程] 第{}次尝试前等待{}ms, 任务id: {}", attempt - 1, DELAY_MILLIS[attempt - 1], taskId);
+                Thread.sleep(DELAY_MILLIS[attempt - 1]);
+            }
+            doPut = UtilHttp.doPut(
+                    "https://open.teambition.com/api/v3/task/" + taskId + "/node/complete",
+                    header,
+                    null,
+                    body);
+            log.info("[创建任务自动更新确认流程] 第{}次尝试, 任务id: {}, 响应: {}", attempt, taskId, doPut);
+            // errorCode 为空视为成功; 10002等错误码视为失败进入重试
+            JSONObject rsp = JSON.parseObject(doPut);
+            String errorCode = (rsp == null) ? null : rsp.getString("errorCode");
+            if (rsp != null && (errorCode == null || errorCode.trim().isEmpty() || "0".equals(errorCode))) {
+                log.info("[创建任务自动更新确认流程] 任务id: {} 第{}次尝试成功", taskId, attempt);
+                return doPut;
+            }
+            log.warn("[创建任务自动更新确认流程] 任务id: {} 第{}次尝试失败, errorCode: {}, errorMessage: {}",
+                    taskId, attempt, errorCode, (rsp == null ? "响应为空" : rsp.getString("errorMessage")));
+        }
+        // 3次均失败: 发送钉钉告警给宁学刚
+        sendNodeCompleteAlert(taskId, String.valueOf(body.get("nodeId")), doPut);
+        return doPut;
+    }
+
+    /**
+     * 节点完成接口3次重试均失败后, 发送钉钉工作通知告警给宁学刚
+     */
+    private void sendNodeCompleteAlert(String taskId, String nodeId, String lastResponse) {
+        try {
+            // 响应体过长时截断, 避免钉钉消息超限
+            if (lastResponse != null && lastResponse.length() > 500) {
+                lastResponse = lastResponse.substring(0, 500) + "...";
+            }
+            String tips = "【TB自动确认流程告警】任务节点完成接口重试3次仍失败,请人工处理!\n"
+                    + "任务ID:" + taskId + "\n"
+                    + "节点ID:" + nodeId + "\n"
+                    + "最后响应:" + lastResponse + "\n"
+                    + "发生时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+            Map message = UtilMap.map("msgtype", "text");
+            message.put("text", UtilMap.map("content", tips));
+            String result = ddClient_notice.sendNotification(ddClient.getAccessToken(),
+                    Arrays.asList("041114023523257781"), null, false, message);
+            log.info("[创建任务自动更新确认流程] 已发送钉钉告警至宁学刚(041114023523257781), 任务id: {}, 告警返回task_id: {}", taskId, result);
+        } catch (Exception e) {
+            log.error("[创建任务自动更新确认流程] 钉钉告警发送失败, 任务id: {}", taskId, e);
+        }
+    }
+
 }

+ 129 - 59
mjava-zhongche/src/main/java/com/malk/zhongche/service/impl/ZhongcheServiceImpl.java

@@ -191,9 +191,10 @@ public class ZhongcheServiceImpl implements ZhongcheService {
         List<Map> result = UtilMap.getList(jsonMap, "result");
         for (Map map1 : result) {
             String activityId = UtilMap.getString(map1, "activityId");
+            String actionExit = UtilMap.getString(map1,"actionExit");
 
             //管理员审批节点
-            if ("node_ocmouwh06v1".equals(activityId)){
+            if ("node_ocmouwh06v1".equals(activityId) && "agree".equals(actionExit)){
                 String operatorUserId = UtilMap.getString(map1, "operatorUserId");
 
                 Map jsonMap2 = (Map) JSONObject.parse(UtilHttp.doGet("https://open.teambition.com/api/idmap/dingtalk/getTbUserId", initHeaderToken(), UtilMap.map("dingUserIds", operatorUserId)));
@@ -380,75 +381,88 @@ public class ZhongcheServiceImpl implements ZhongcheService {
             }
         }
 
-        if (Objects.nonNull(formData.get("attachmentField_mpzbe3w8"))){
+        //优先收集"问题表"各行的【存在问题附件】,合并结果为空则回退取默认附件字段
+        List<Map> fileList = new ArrayList<>();
+        List<Map> problemRows = parseListField(formData.get("tableField_m9w8qyii"));//问题表
+        for (Map problemRow : problemRows) {
+            fileList.addAll(parseListField(problemRow.get("attachmentField_madl2j0j")));//存在问题附件
+        }
+        if (fileList.isEmpty()){
+            fileList = parseListField(formData.get("attachmentField_mpzbe3w8"));//默认附件字段
+        }
+
+        if (!fileList.isEmpty()){
             List<Map> addValues = new ArrayList<>();
+            List<String> fileTokens = new ArrayList<>();
 
-            List<Map> fileList = (List<Map>) JSONArray.parse(String.valueOf(formData.get("attachmentField_mpzbe3w8")));
             for (Map fileMap : fileList) {
-                String ydDownloadUrl = fileMap.get("downloadUrl").toString();
-                String fileName = fileMap.get("name").toString();
-
-                try {
-                    //下载宜搭附件
-                    downloadYdFile(ydDownloadUrl, filePath + fileName);
-                    System.out.println("文件下载成功!");
-
-                    //TB文件创建文件上传凭证
-                    Path path = Paths.get(filePath + fileName);
-                    String mimeType = Files.probeContentType(path);
-                    System.out.println(mimeType); // application/pdf
-
-                    File file = new File(fileName);
-                    long fileSize = file.length();
-
-                    Map body = new HashMap();
-                    body.put("scope", "task:"+taskId+"/attachment");
-                    body.put("fileSize", fileSize);
-                    body.put("fileType", mimeType);
-                    body.put("fileName", fileName);
-                    body.put("category", "attachment");
-
-                    TBR tbr = (TBR) UtilHttp.doPost("https://open.teambition.com/api/v3/awos/upload-token", initHeaderToken(), null, body, TBR.class);
-                    Map result = (Map) tbr.getResult();
-                    String uploadUrl = UtilMap.getString(result, "uploadUrl");
-                    String token = UtilMap.getString(result, "token");
-
-                    //上传文件
-                    uploadFile(filePath + fileName, uploadUrl, mimeType);
-
-                    Map value = new HashMap();
-                    value.put("title",fileName);
-                    value.put("metaString",JSONObject.toJSONString(UtilMap.map("fileToken",token)));
-
-                    addValues.add(value);
-                } catch (IOException e) {
-                    e.printStackTrace();
+                    String ydDownloadUrl = fileMap.get("downloadUrl").toString();
+                    String fileName = fileMap.get("name").toString();
+
+                    try {
+                        //下载宜搭附件
+                        downloadYdFile(ydDownloadUrl, filePath + fileName);
+                        System.out.println("文件下载成功!");
+
+                        //TB文件创建文件上传凭证
+                        Path path = Paths.get(filePath + fileName);
+                        String mimeType = Files.probeContentType(path);
+                        System.out.println(mimeType); // application/pdf
+
+                        File file = new File(filePath + fileName);
+                        long fileSize = file.length();
+
+                        Map body = new HashMap();
+                        body.put("scope", "task:"+taskId+"/attachment");
+                        body.put("fileSize", fileSize);
+                        body.put("fileType", mimeType);
+                        body.put("fileName", fileName);
+                        body.put("category", "attachment");
+
+                        TBR tbr = (TBR) UtilHttp.doPost("https://open.teambition.com/api/v3/awos/upload-token", initHeaderToken(), null, body, TBR.class);
+                        Map result = (Map) tbr.getResult();
+                        String uploadUrl = UtilMap.getString(result, "uploadUrl");
+                        String token = UtilMap.getString(result, "token");
+
+                        //上传文件
+                        uploadFile(filePath + fileName, uploadUrl, mimeType);
+
+                        Map value = new HashMap();
+                        value.put("title",fileName);
+                        value.put("metaString",JSONObject.toJSONString(UtilMap.map("fileToken",token)));
+
+                        addValues.add(value);
+                        fileTokens.add(token);
+                    } catch (IOException e) {
+                        throw new IllegalStateException("同步宜搭附件到 Teambition 失败: " + fileName, e);
+                    }
                 }
-            }
 
-            //更新tb任务附件
-            Map body = new HashMap();
-            body.put("customfieldId", "67e372b0142aea12588c2d22");
-            body.put("addValues",addValues);
+                //更新tb任务附件
+                Map body = new HashMap();
+                body.put("customfieldId", "67e372b0142aea12588c2d22");
+                body.put("addValues",addValues);
+
+                Map map2 = tbClient.updateTaskCustomField(taskId, tbOperatorId, body);
+                log.info("map2:{}",JSONObject.toJSONString(map2));
 
-            Map map2 = tbClient.updateTaskCustomField(taskId, tbOperatorId, body);
-            log.info("map2:{}",JSONObject.toJSONString(map2));
+                createProjectLibraryFiles(projectId, fileTokens, tbOperatorId);
 
-            //查询任务所在工作流的任务状态列表
-            TBR tbr = (TBR) UtilHttp.doGet("https://open.teambition.com/api/v3/task/" + taskId + "/tfs", initHeaderToken(), null, TBR.class);
+                //查询任务所在工作流的任务状态列表
+                TBR tbr = (TBR) UtilHttp.doGet("https://open.teambition.com/api/v3/task/" + taskId + "/tfs", initHeaderToken(), null, TBR.class);
 
-            List<Map> tfsList = (List<Map>) tbr.getResult();
+                List<Map> tfsList = (List<Map>) tbr.getResult();
 
-            for (Map tfsMap : tfsList) {
-                String tfsName = UtilMap.getString(tfsMap, "name");
+                for (Map tfsMap : tfsList) {
+                    String tfsName = UtilMap.getString(tfsMap, "name");
 
-                if ("已完成".equals(tfsName)){
-                    String tfsId = UtilMap.getString(tfsMap, "id");
-                    //完成tb任务
-                    tbClient.updateTaskFlowStatus(taskId, tbOperatorId, tfsId,null);
+                    if ("已完成".equals(tfsName)){
+                        String tfsId = UtilMap.getString(tfsMap, "id");
+                        //完成tb任务
+                        tbClient.updateTaskFlowStatus(taskId, tbOperatorId, tfsId,null);
+                    }
                 }
             }
-        }
 
         return McR.success();
     }
@@ -481,6 +495,62 @@ public class ZhongcheServiceImpl implements ZhongcheService {
         return McR.success(map);
     }
 
+    /**
+     * 解析宜搭"附件/子表单"类字段值,兼容 String(JSON) 与 List 两种返回形态。
+     * @param raw 宜搭表单字段原始值(可能为 null、JSON 字符串或 List)
+     * @return 由 Map 组成的列表;无值或解析失败时返回空列表,不抛异常
+     */
+    private List<Map> parseListField(Object raw) {
+        List<Map> list = new ArrayList<>();
+        if (Objects.isNull(raw)){
+            return list;
+        }
+        List<?> source;
+        if (raw instanceof List){
+            source = (List<?>) raw;
+        }else if (raw instanceof String && StringUtils.isNotBlank((String) raw)){
+            try {
+                source = JSONArray.parseArray((String) raw);
+            } catch (Exception e) {
+                log.warn("解析宜搭字段JSON失败, raw:{}", raw, e);
+                return list;
+            }
+        }else {
+            return list;
+        }
+        for (Object item : source) {
+            if (item instanceof Map){
+                list.add((Map) item);
+            }
+        }
+        return list;
+    }
+
+    private void createProjectLibraryFiles(String projectId, List<String> fileTokens, String operatorId) {
+        if (!Strings.isNotBlank(projectId)) {
+            throw new IllegalArgumentException("项目ID不能为空,无法同步项目文件库");
+        }
+        if (fileTokens.isEmpty()) {
+            return;
+        }
+
+        Map body = new HashMap();
+        body.put("projectId", projectId);
+        body.put("fileTokens", fileTokens);
+
+        TBR response = (TBR) UtilHttp.doPost(
+                "https://open.teambition.com/api/v3/work/create",
+                initHeaderToken(operatorId),
+                null,
+                body,
+                TBR.class);
+        if (Objects.isNull(response)) {
+            throw new IllegalStateException("创建项目文件库文件失败:未收到 Teambition 响应");
+        }
+        response.assertSuccess();
+        log.info("已同步 {} 个附件到项目文件库, projectId:{}", fileTokens.size(), projectId);
+    }
+
     /**
      * 上传文件(核心方法)
      * @param filePath 本地文件路径(如"2.png")
@@ -650,7 +720,7 @@ public class ZhongcheServiceImpl implements ZhongcheService {
     // 操作者ID: 若是查询, 以操作人视角作为权限
     private Map<String, String> initHeaderToken(String operatorId) {
         Map header = initHeaderToken();
-        header.put("x-operator-id", operatorId);
+        header.put("X-Operator-Id", operatorId);
         return header;
     }
 

+ 1 - 1
mjava-zhongche/src/main/resources/application.yml

@@ -1,6 +1,6 @@
 spring:
   profiles:
-    active: dev
+    active: prod
   servlet:
     multipart:
       max-file-size: 100MB