pruple_boy 1 年之前
父节点
当前提交
4d26836383

+ 1 - 2
mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBxYDController.java

@@ -109,7 +109,7 @@ public class TBxYDController {
 
         Map<String, ?> data = UtilServlet.getParamMap(request);
         log.info("交付物变更发起, {}", data);
-        
+
         // prd 618 通过任务扩展按钮变更情况, 提交后再触发审批回写
         if (UtilMap.getBoolean(data, "isChange")) {
             awClint.changeApprove(UtilMap.getString(data, "taskId"), UtilMap.getString(data, "formInstId"));
@@ -243,7 +243,6 @@ public class TBxYDController {
         return McR.success();
     }
 
-
     /**
      * 提供verifier数据读取服务
      */

+ 0 - 1
mjava-aiwei/src/main/java/com/malk/aiwei/server/AWServer.java

@@ -45,7 +45,6 @@ public class AWServer {
             "65f269605a2065ad7ad65d18"  // ak类复制项目
     );
 
-
     /**
      * 艾为网关接口
      */

+ 173 - 55
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWImplClient.java

@@ -2,6 +2,7 @@ package com.malk.aiwei.service.impl;
 
 import cn.hutool.core.util.ObjectUtil;
 import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Lists;
 import com.malk.aiwei.server.AWServer;
 import com.malk.aiwei.service.AWClint;
 import com.malk.aiwei.service.AwDingService;
@@ -46,21 +47,22 @@ public class AWImplClient implements AWClint {
     // 项目主数据表
     String _matchFormUuid(String code) {
         Map<String, String> formUuid = UtilMap.empty();
-        if (true || UtilEnv.getActiveProfile().equals(UtilEnv.ENV_PROD)) {
-            formUuid.put("DENTRY", "FORM-BD73A57B62EA4153B896C9BB3EA14D28GWSQ"); // 文件夹/文件版本记录
-            formUuid.put("REVIEW", "FORM-812FD46AF391449A8F206EDB3221B38840UQ"); // 交付物审批记录
-            formUuid.put("REVIEW_PROCESS", "TPROC--RJC66SC1NEFHXJ0H770K0CF4WN1K21HQ706RL5"); // 交付物审批记录
-            formUuid.put("PROJECT", "FORM-141E21DF183846028E21727CE43CD1C75CLZ"); // 项目主数据
-            formUuid.put("APPROVE", "FORM-A25299893F614A6EAA672514D3A76BB0QDBF"); // 交付物审批矩阵
-            formUuid.put("CHECK", "FORM-1A5D4D7FBF88409B956EBE51F9342A6BKOLP"); // 预检项
-            formUuid.put("RECORD", "FORM-6E2C0D1197264B8AA23EB3FECAE7344B00BN"); // 预检项记录
-            formUuid.put("ROLE", "FORM-3C7396A12ADB48A8833EBD90089C93833R21"); // 项目角色
-            formUuid.put("ROLE_PDT", "FORM-69B6A8151F3346DCA20B0ED54F5380675LJX"); // PDT角色
-            formUuid.put("CRM_LOG", "FORM-16DD578308E64763AE1539D8176CCCD0GX6F"); // crm推送日志
-            formUuid.put("TEMPLATE", "FORM-24EFE5C1BCE54E7192E53B33ADFBF1C52NS3"); // TB模板与项目类型映射表
-            formUuid.put("DOMAIN", "https://yida.awinic.com/"); // 宜搭域名
+        if (UtilEnv.getActiveProfile().equals(UtilEnv.ENV_PROD)) {
+            formUuid.put("DENTRY", "FORM-BD73A57B62EA4153B896C9BB3EA14D28GWSQ");                // 文件夹/文件版本记录
+            formUuid.put("REVIEW", "FORM-812FD46AF391449A8F206EDB3221B38840UQ");                // 交付物审批记录
+            formUuid.put("REVIEW_PROCESS", "TPROC--RJC66SC1NEFHXJ0H770K0CF4WN1K21HQ706RL5");    // 交付物审批记录
+            formUuid.put("PROJECT", "FORM-141E21DF183846028E21727CE43CD1C75CLZ");               // 项目主数据
+            formUuid.put("APPROVE", "FORM-A25299893F614A6EAA672514D3A76BB0QDBF");               // 交付物审批矩阵
+            formUuid.put("CHECK", "FORM-1A5D4D7FBF88409B956EBE51F9342A6BKOLP");                 // 预检项
+            formUuid.put("RECORD", "FORM-6E2C0D1197264B8AA23EB3FECAE7344B00BN");                // 预检项记录
+            formUuid.put("ROLE", "FORM-3C7396A12ADB48A8833EBD90089C93833R21");                  // 项目角色
+            formUuid.put("ROLE_PDT", "FORM-69B6A8151F3346DCA20B0ED54F5380675LJX");              // PDT角色
+            formUuid.put("CRM_LOG", "FORM-16DD578308E64763AE1539D8176CCCD0GX6F");               // crm推送日志
+            formUuid.put("TEMPLATE", "FORM-24EFE5C1BCE54E7192E53B33ADFBF1C52NS3");              // TB模板与项目类型映射表
+            formUuid.put("DOMAIN", "https://yida.awinic.com/");                                 // 宜搭域名
         } else {
             // [ fixme: 测试环境添加主数据, 匹配不同架构下企业成员 ]
+            formUuid.put("DENTRY", "FORM-AA1237FC3B5A4DEBA7C823EDB48CCDEBZ1SV");
             formUuid.put("REVIEW", "FORM-FBC1A390B4C348089020C763938A6F54RUNY");
             formUuid.put("REVIEW_PROCESS", "TPROC--YU966T91PIDH4XDR82OJC8GVOP7Z19PODTXQL6");
             formUuid.put("PROJECT", "FORM-84EF78C7DBA047E58A8C8511106F91D5WNVI");
@@ -86,6 +88,14 @@ public class AWImplClient implements AWClint {
     @Override
     public Map doApprove(Map data, boolean isChange) {
 
+        log.info("交付物审批, {}", data);
+        String taskId = UtilMap.getString(data, "taskId");
+
+        // prd 618 通过任务扩展按钮触发, 接口仅需传递 taskId 即可, 相关信息直接从任务详情取值
+        Map taskData = _getTaskFieldMap(taskId, AWServer.TASK_CODE, AWServer.TASK_APPROVE_ATTACHMENT, AWServer.TASK_APPROVE_DESC, AWServer.TASK_PRODUCT, AWServer.TASK_PRODUCT_VERSION, AWServer.TASK_CHECK_STATUS);
+        Map rTask = UtilMap.getMap(taskData, "task");
+        String pCode = UtilMap.getString(rTask, "projectId");
+
         // 通过工作流触发: prd 618 通过任务扩展按钮触发, fixme tb任务完成态工作流工作会触发完成时间更新, 导致逾期问题
         String type = isChange ? "变更" : "发布"; // 回调 发布 变更
         boolean isCallback = data.containsKey("tfsId");
@@ -96,23 +106,27 @@ public class AWImplClient implements AWClint {
                 return null;
             }
             type = "回调";
+        } else {
+            // prd 发起评审先修改工作流,成功后再执行相关逻辑,避免前后置依赖与子任务未完成导致不能关闭
+            String workFlowId_c = _getWorkFlowStatus(pCode, "审批通过");
+            if (!isChange) {
+                McException.assertAccessException(StringUtils.isBlank(UtilMap.getString(taskData, AWServer.TASK_CHECK_STATUS)), "请核对, 技术检查项检查状态!");
+                // prd: 不过滤审批中, 避免撤销后重新填写交付件情况
+                McException.assertAccessException(workFlowId_c.equals(rTask.get("tfsId")), "已完成任务, 不允许提交发布!");
+                String workFlowId = _getWorkFlowStatus(pCode, "审批中");
+                tbClient.updateTaskFlowStatus(taskId, tbConf.getOperatorId(), workFlowId, "审批中");
+            } else {
+                McException.assertAccessException(!rTask.get("tfsId").equals(workFlowId_c), "非已完成任务, 不允许提交变更!");
+            }
         }
-        // todo 已变更工作流, 不允许再提交发布; 没有发布状态也不允许做变更
-
-        log.info("交付物审批, {}", data);
-        McException.assertParamException_Null(data, "taskId");
-        String taskId = UtilMap.getString(data, "taskId");
-        Map taskData = _getTaskFieldMap(taskId, AWServer.TASK_CODE, AWServer.TASK_APPROVE_ATTACHMENT, AWServer.TASK_APPROVE_DESC, AWServer.TASK_PRODUCT, AWServer.TASK_PRODUCT_VERSION);
-        List<Map> docs = _getDocs(taskData);
         // ppExt: TB有卡控, 校验交付物不能为空, 同时兼容非交付件任务类型
+        List<Map> docs = _getDocs(taskData);
         McException.assertAccessException(!isCallback && docs.size() == 0, "任务交付件不能为空, 请填写后再操作!");
-        // prd 618 通过任务扩展按钮触发, 接口仅需传递 taskId 即可, 相关信息直接从任务详情取值
-        Map rTask = UtilMap.getMap(taskData, "task");
-        String pCode = UtilMap.getString(rTask, "projectId");
         String executorId = UtilMap.getString(rTask, "executorId");
         if (!isCallback) {
             McException.assertAccessException(StringUtils.isBlank(executorId), "任务执行人未分配!");
         }
+
         // 返回userId, 若非执行人权限, 页面控制弹出框
         String userId = "";
         String tCode = UtilMap.getString(taskData, AWServer.TASK_CODE);
@@ -207,7 +221,9 @@ public class AWImplClient implements AWClint {
                     formData.put("result", result);
                     formData.put("userId", userId);
                     List<String> pmUserIds = (List<String>) JSON.parse(UtilMap.getString(rProject, "employeeField_ltzn872j_id"));
-                    formData.put("PMName", ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), pmUserIds.get(0)).get("name"));
+                    if (ObjectUtil.isNotNull(pmUserIds)) {
+                        formData.put("PMName", ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), pmUserIds.get(0)).get("name"));
+                    }
                     return formData;
                 }
             }
@@ -240,17 +256,23 @@ public class AWImplClient implements AWClint {
     public void approved(Map data) {
 
         log.info("交付物审批回调, {}", data);
-        String result = UtilMap.getString(data, "approve");
         String type = UtilMap.getString(data, "type");
+        String projectId = UtilMap.getString(data, "projectId");
+        String taskId = UtilMap.getString(data, "taskId");
+        String result = UtilMap.getString(data, "approve");
         if ("变更".equals(type)) {
             Map body = TBConf.assembleCustomFieldName(AWServer.TASK_APPROVE_STATE, "已变更");
-            tbClient.updateTaskCustomField(UtilMap.getString(data, "taskId"), tbConf.getOperatorId(), body);
+            tbClient.updateTaskCustomField(taskId, tbConf.getOperatorId(), body);
+        }
+        // prd 618 兼容原逻辑, 保留回调类更新任务工作流. 新版本通过任务扩展按钮触发, 发布在创建时触发\变更不更新工作流避免导致逾期情况
+        if ("回调".equals(type)) {
+            String workFlowId = _getWorkFlowStatus(projectId, result);
+            tbClient.updateTaskFlowStatus(taskId, tbConf.getOperatorId(), workFlowId, result);
+        }
+        if ("发布".equals(type)) {
+            String workFlowId = _getWorkFlowStatus(projectId, result);
+            tbClient.updateTaskFlowStatus(taskId, tbConf.getOperatorId(), workFlowId, result);
         }
-        // todo 任务工作流更新, tb需要调整
-//        if ("回调".equals(UtilMap.getString(data, "type"))) {
-        String workFlowId = _getWorkFlowStatus(String.valueOf(data.get("projectId")), result);
-        tbClient.updateTaskFlowStatus(String.valueOf(data.get("taskId")), tbConf.getOperatorId(), workFlowId, String.valueOf(data.get("approve")));
-//        }
     }
 
     @Autowired
@@ -544,6 +566,7 @@ public class AWImplClient implements AWClint {
             if (StringUtils.isNotBlank(projectId) && StringUtils.isBlank(projectCode)) {
                 try {
                     _syncProjectRole(formData, projectId, false);
+                    _assembleBreakPermission(formData);
                 } catch (Exception e) {
                     // 记录错误信息
                     e.printStackTrace();
@@ -565,15 +588,12 @@ public class AWImplClient implements AWClint {
     public void createProject(String projectCode, String templateId) {
 
         log.info("通过模板创建项目, {}, {}", projectCode, templateId);
-
         // 虽然先创建TB, 可以一次性同步项目数据, 但避免执行异常, 因此成功创建TB项目再回写
         Map formData = syncProject(projectCode);
         String projectId = UtilMap.getString(formData, "textField_lqxtykce");
         log.info("项目主数据, {}, {}", projectId, formData);
         boolean isCreate = StringUtils.isBlank(projectId);
         if (isCreate) {
-            //awDingService.saveGroup(upMap,formData);
-
             // prd 多模板适配: [templateId 为空, 触发项目类型匹配]
             if (StringUtils.isBlank(templateId)) {
                 List<Map> dataList = ydService.queryDataList_FormData(_matchFormUuid("TEMPLATE"), null);
@@ -604,10 +624,11 @@ public class AWImplClient implements AWClint {
         String projectId = UtilMap.getString(formData, "textField_lqxtykce");
         if (StringUtils.isNotBlank(projectId)) { // 屏蔽未创建项目, 出现新增
             _syncProjectRole(formData, projectId, false);
+            _assembleBreakPermission(formData);
         }
     }
 
-    // 同步TB项目成员/项目分组/项目概览 todo 群成员
+    // 同步TB项目成员/项目分组/项目概览
     void _syncProjectRole(Map formData, String projectId, boolean isCreate) {
 
         // prd TB项目成员, 项目经理设置为管理者
@@ -616,6 +637,10 @@ public class AWImplClient implements AWClint {
         List<String> representativeIds = new ArrayList<>();
 
         String representative = _getProjectRoleId(projectId, "项目各代表");
+        // prd 6.26 L类项目使用 项目各代表_L, TB配置拥有邀请成员权限
+        if ("生命周期维护项目".equals(formData.get("textField_ltwcq7s6"))) {
+            representative = _getProjectRoleId(projectId, "项目各代表_L");
+        }
         String pmRoleId = _getProjectRoleId(projectId, AWServer.PROJECT_PM_ROLE);
         String staffRoleId = _getProjectRoleId(projectId, "项目成员");
 
@@ -679,7 +704,8 @@ public class AWImplClient implements AWClint {
         List<Map> groupList = tbClient.queryProgramList_all();
         Optional optional = groupList.stream().filter(item -> groupName.equals(item.get("name"))).findAny();
         if (optional.isPresent()) {
-            tbClient.upsertProgramProject(UtilMap.getString((Map) optional.get(), "id"), Arrays.asList(projectId), tbConf.getOperatorId());
+            Map item = (Map) optional.get(); // 权限
+            tbClient.upsertProgramProject(UtilMap.getString(item, "id"), Arrays.asList(projectId), UtilMap.getString(item, "creatorId"));
         }
     }
 
@@ -763,15 +789,15 @@ public class AWImplClient implements AWClint {
                 // prd 6.17 任务分配:资源匹配多人时,取值第一人作为执行人,其他人员作为参与者
                 if (roleIds.size() > 1) {
                     String executorId = UtilMap.getString(task, "executorId");
-                    List<String> tbUserIds = roleIds.stream().map(item -> _convertUserId(item, false)).collect(Collectors.toList());
+                    List<String> tbUserIds = _convertUserId(roleIds, false);
                     // prd 多人情况避免, 手动指定后被覆盖. 判定当前任务执行人在角色范围内则不支持更新
                     if (StringUtils.isNotBlank(executorId) && tbUserIds.contains(executorId)) {
+                        log.info("多人情况跳过, {}", tbUserIds.contains(executorId));
                         continue;
                     }
                     if (tbUserIds.size() > 0) {
                         tbClient.updateTaskExecutor(UtilMap.getString(task, "id"), tbConf.getOperatorId(), tbUserIds.get(0), false, false);
                     }
-                    roleIds.remove(0);
                     tbClient.updateTaskInvolveMembers(UtilMap.getString(task, "id"), tbConf.getOperatorId(), tbUserIds, null, false, false);
                 }
             }
@@ -810,7 +836,7 @@ public class AWImplClient implements AWClint {
         });
     }
 
-    // TB与宜搭userId转换 fixme 提取 tbService
+    // TB与宜搭userId转换 fixme 提取 tbService [ ppExt: 若TB未启用人员返回为空 ]
     private String _convertUserId(String userId, boolean isTBID) {
         List<Map> tbMap = tbClient.idMapQuery(userId, isTBID);
         // 过滤未匹配人员信息 [ppExt: TB人员未匹配, 不执行修改也无报错]
@@ -826,7 +852,7 @@ public class AWImplClient implements AWClint {
         }
     }
 
-    // TB与宜搭userId转换 fixme 提取 tbService
+    // TB与宜搭userId转换 fixme 提取 tbService [ ppExt: 若TB未启用人员返回为空 ]
     private List<String> _convertUserId(List<String> userIds, boolean isTBID) {
         return userIds.stream().distinct()
                 .map(id -> _convertUserId(id, isTBID))
@@ -1090,7 +1116,7 @@ public class AWImplClient implements AWClint {
         }
 
         List<Map> dataList = curList.stream().filter(item -> "启用".equals(item.get("radioField_lrnddfq6")) && !item.containsKey("status")).collect(Collectors.toList());
-        log.info(" 兼容删除场景, 标记状态为停用, {}", dataList.size());
+        log.info("兼容删除场景, 标记状态为停用, {}", dataList.size());
         dataList.forEach(item -> {
             ydClient.operateData(YDParam.builder()
                     .formInstanceId(UtilMap.getString(item, "formInstanceId"))
@@ -1195,6 +1221,7 @@ public class AWImplClient implements AWClint {
                 .searchCondition(JSON.toJSONString(conditions))
                 .build(), YDConf.FORM_QUERY.retrieve_list).getData();
         String formInstId = "";
+        String dentryUuid = "";
         int version = 0;
         if (dataList.isEmpty()) {
             Map formData = UtilMap.map("radioField_lx36gdpm, textField_lx36gdpp, textField_lrj7vnxb, textField_lqxtykce", fileType, fileName, pCode, pId);
@@ -1208,6 +1235,7 @@ public class AWImplClient implements AWClint {
                 formData.put("textField_lx36gdpn", ddr_new.getDentryUuid());
                 formData.put("textField_lx36gdpo", ddr_new.getUrl());
                 formData.put("textField_lx342bvd", rootNodeId);
+                dentryUuid = ddr_new.getDentryUuid(); // 用于权限节点打断
             } else {
                 formData.put("numberField_lx36gdpz", version);
             }
@@ -1221,7 +1249,7 @@ public class AWImplClient implements AWClint {
             version = UtilMap.getInt(UtilMap.getMap(dataList.get(0), "formData"), "numberField_lx36gdpz");
         }
         // 返回 formInstId 用于记录文件版本号, 以及复制后文件访问链接
-        return UtilMap.map("rootNodeId, formInstId, version", rootNodeId, formInstId, version);
+        return UtilMap.map("rootNodeId, formInstId, version, dentryUuid", rootNodeId, formInstId, version, dentryUuid);
     }
 
     /**
@@ -1238,7 +1266,7 @@ public class AWImplClient implements AWClint {
      * - 官方: 文档选择器 有最近浏览列表 以及 选知识库后的目录树,背后都是对接的钉钉文档开放API,不过前者是V1版本的,后者是V2版本的,文档他们好像又出了V3版本,总之不同版本接口返回的链接可能是不一样的
      * - 临时: 目前控制用户不能选择最近浏览
      * - 建议: 权限控制需要通过 dentryUuid, TB是否可反回, 或可提供通过 dentry 查询 dentryUuid接口; 或者使用现有链接反查接口, 是否确保链接格式内包含 dentryUuid [URL下node后字符串]
-     * - 解法: 提供通过 spaceId + dentryId 查询详情接口, 返回有创建人\dentryUuid信息
+     * - 解法: 提供通过 spaceId + dentryId 查询详情接口, 返回有创建人\dentryUuid信息 [ fixme 特别说明: 知识库 creatorId 就是所有者, 转交后 creatorId 会自动同步更新 ]
      * -
      * 3. 通过接口添加知识库权限操作人需要有权限
      * 说明: 详见2 [fixme: 公共账号拥有所有项目知识库的编辑以上权限]
@@ -1253,15 +1281,11 @@ public class AWImplClient implements AWClint {
     @Override
     public void approveVersion(String taskId, String pCode) {
 
-        if (!"A240407DryRun".equals(pCode)) {
-            return;
-        }
-
         String result = "";
-
+        String formInstanceId = "";
+        List<String> staffIds = new ArrayList<>();
         // 通用账户unionId
         String unionId = String.valueOf(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), ddConf.getOperator()).get("unionid"));
-
         // 知识库 & 项目信息处理
         String workspaceId = "", rootNodeId = "", creatorId = "";
         List<Map> pList = ydService.queryDataList_FormData(_matchFormUuid("PROJECT"), UtilMap.map("textField_lrj7vnxb", pCode));
@@ -1272,6 +1296,12 @@ public class AWImplClient implements AWClint {
             workspaceId = UtilMap.getString(pList.get(0), "textField_lx2yekqj");
             rootNodeId = UtilMap.getString(pList.get(0), "textField_lx342bvd");
             creatorId = UtilMap.getString(pList.get(0), "textField_lx8ra1bs");
+            List<Map> details = UtilMap.getList(pList.get(0), "tableField_lqxtykcf");
+            for (Map detail : details) {
+                if (ObjectUtil.isNotNull(detail.get("employeeField_lqxtykch_id"))) {
+                    staffIds.addAll((List) detail.get("employeeField_lqxtykch_id"));
+                }
+            }
             if (StringUtils.isBlank(workspaceId)) {
                 // 获取知识库空间Id [ ppExt: 知识库列表接口, 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库 ] todo tb提供接口返回知识库相关信息
                 List<Map> workspaces = ddClient_storage.searchWorkspaces(ddClient.getAccessToken(), unionId, pCode, null);
@@ -1283,8 +1313,9 @@ public class AWImplClient implements AWClint {
                     rootNodeId = UtilMap.getString(space, "rootNodeId"); // 知识库返回根节点 rootNodeId, 即根目录的 dentryUuid
                     creatorId = UtilMap.getString(space, "creatorId");
                     // 更新知识库信息到项目档案
+                    formInstanceId = UtilMap.getString(pList.get(0), "instanceId");
                     ydClient.operateData(YDParam.builder()
-                            .formInstanceId(UtilMap.getString(pList.get(0), "instanceId"))
+                            .formInstanceId(formInstanceId)
                             .updateFormDataJson(JSON.toJSONString(UtilMap.map("textField_lx2yekqj, textField_lx342bvd, textField_lx8ra1bs, textField_lx8ra1bt", workspaceId, rootNodeId, creatorId, space.get("url"))))
                             .build(), YDConf.FORM_OPERATION.update);
                 }
@@ -1307,11 +1338,16 @@ public class AWImplClient implements AWClint {
             // fixme: 知识库所有操作和权限都与依赖于权限, 通过 spaceId + dentryId 查询创建人需要传递操作者, 操作者需要有查看以上权限. todo 目前临时读取执行人, tb文档相关信息需要返回创建人信息
             Map<String, String> extra = (Map) tbClient.idMapQuery(UtilMap.getString(rTask, "executorId"), "dingTalk-user", ddConf.getCorpId()).get(0).get("extra");
             String executorIdUnionId = String.valueOf(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), extra.get("userId")).get("unionid"));
+            String workRootNodeId = rootNodeId; // 知识库成员权限同步
             // 文件/文件夹版本控制: fileType, fileName, pCode, pId, workspaceId, rootNodeId, unionId
-            String fileName = "项目交付件";
+            String fileName = "文档版本库";
             Map record = _upsertFileAbsolutePath("文件夹", fileName, pCode, UtilMap.getString(rTask, "projectId"), workspaceId, rootNodeId, unionId);
             rootNodeId = UtilMap.getString(record, "rootNodeId"); // 最新文件路径下ID
-
+            /// ppExt 一级目录设置权限打断: 后续知识库成员调整, 非管理者与所有者人员不会传递到当前路径下, 即该文件若要可见需要重新对成员赋权
+            log.info("{}, {}", fileName, record);
+            if (StringUtils.isNotBlank(UtilMap.getString(record, "dentryUuid"))) {
+                _breakPermission(workRootNodeId, UtilMap.getString(record, "dentryUuid"), staffIds, creatorId, formInstanceId);
+            }
             String rName = UtilMap.getString(taskData, AWServer.TASK_STAGE);
             if (StringUtils.isBlank(rName)) {
                 rName = "其他";
@@ -1327,7 +1363,6 @@ public class AWImplClient implements AWClint {
             fileName = fileName + "/" + tName;
             record = _upsertFileAbsolutePath("文件夹", fileName, pCode, UtilMap.getString(rTask, "projectId"), workspaceId, rootNodeId, unionId);
             rootNodeId = UtilMap.getString(record, "rootNodeId"); // 最新文件路径下ID
-
             List<Map> docs = _getDocs(taskData);
             log.info("docs, {}", docs);
             List<Map> attas = new ArrayList<>();
@@ -1340,14 +1375,13 @@ public class AWImplClient implements AWClint {
                 // 通过提供 spaceId + dentryId 查询创建人, 再由创建人为公共账号添加编辑权限, 再通过公共账号复制 [ppExt: 知识库可为其他成员添加不高于自身的权限]
                 DDR_New ddr_dentry = ddClient_storage.getSpaceDentryDetail(ddClient.getAccessToken(), executorIdUnionId, UtilMap.getString(doc, "instanceId"), UtilMap.getString(doc, "instanceId"), false);
                 try {
-                    String creatorId_t = UtilMap.getString(ddr_dentry.getUpdater(), "unionId");
+                    String creatorId_t = UtilMap.getString(ddr_dentry.getCreator(), "unionId");
                     List<Map> members_t = Arrays.asList(UtilMap.map("type, id", "USER", ddConf.getOperator()));
                     ddClient_storage.updateDentryPermissions(ddClient.getAccessToken(), ddr_dentry.getDentryUuid(), creatorId_t, "EDITOR", members_t, null);
                 } catch (McException e) {
                     // ppExt: 若是创建人权限被管理员拿走, 会导致500异常, 需要兼容
                     log.error(e.getMessage(), e);
                 }
-
                 // 复制文件/文件夹, 知识库文件添加版本标识
                 int order = UtilMap.getInt(version, "version");
                 order += 1;
@@ -1388,9 +1422,85 @@ public class AWImplClient implements AWClint {
         log.info("知识库版本管理, {}", result);
     }
 
+    /// 更新一级归档目录 dentryUuid 权限打断, 并更新项目成员只读权限, 且更新记录实例ID [ 用于后续项目全量同步时, 刷新权限 ]
+    private void _breakPermission(String rootNodeId, String dentryUuid, List<String> staffIds, String
+            creatorUnionId, String formInstanceId) {
+
+        ddClient_storage.updateDentryPermissionsInheritances(ddClient.getAccessToken(), dentryUuid, creatorUnionId, "BREAK");
+        try {  // todo 目前权限绑定群聊会异常, BUG
+            /// 原存量用户权限处理 & 管理者, 所有者更新异常处理 [ fixme: 需要获取知识库根目录 dentryUuid 成员列表作为权限原始数据 ]
+            List<Map> allMembers = ddClient_storage.queryDentryPermissions(ddClient.getAccessToken(), rootNodeId, creatorUnionId, null);
+            for (Map member : allMembers) {
+                Map<String, String> team = UtilMap.getMap(member, "member");
+                // ppExt: 管理员和所有者, 赋值编辑权限, 会报500异常需要兼容 [ 原只读权限不做调整 ]
+                if ("USER".equals(UtilMap.getString(team, "type"))) {
+                    String roleId = UtilMap.getString(UtilMap.getMap(member, "role"), "id");
+                    String userId = UtilMap.getString(team, "id");
+                    if (Arrays.asList("OWNER", "MANAGER", "READER").contains(roleId)) {
+                        staffIds.remove(userId);
+                    } else {
+                        /// prd 存量的成员处理办法:第一次更新的时候刷为只读权限,后续知识库成员手动导致的变更不再单独添加。文件权限读取权限矩阵
+                        if (StringUtils.isNotBlank(formInstanceId)) {
+                            staffIds.add(userId); // ppExt: 原存量的用户, 改为查看权限 [兼容]
+                        }
+                    }
+                }
+            }
+        } catch (McException e) {
+            log.error(e.getMessage(), e);
+        }
+        // ppExt: 不移除 TBManager 编辑权限, 避免被覆盖
+        if (staffIds.contains(ddConf.getOperator())) {
+            staffIds.remove(ddConf.getOperator());
+        }
+        log.info("更新一级归档目录 {}, {}, {}", dentryUuid, staffIds, formInstanceId);
+        // 权限成员列表,最大size30, 通过 partition 快速切割[ 只读权限]
+        List<List<String>> parts = Lists.partition(staffIds, DDConf.WORKSPACE_PERMISSION_SIZE);
+        for (List<String> ids : parts) {
+            List<Map> members = ids.stream().map(userId -> UtilMap.map("type, id", "USER", userId)).collect(Collectors.toList());
+            ddClient_storage.updateDentryPermissions(ddClient.getAccessToken(), dentryUuid, creatorUnionId, "READER", members, null);
+        }
+        if (StringUtils.isNotBlank(formInstanceId)) {
+            ydClient.operateData(YDParam.builder()
+                    .formInstanceId(formInstanceId)
+                    .updateFormDataJson(JSON.toJSONString(UtilMap.map("textField_lxmw6z7k", dentryUuid)))
+                    .build(), YDConf.FORM_OPERATION.update);
+        }
+    }
+
+    /// 同步知识库成员权限, 非初始化
+    private void _assembleBreakPermission(Map formData) {
+        List<String> staffIds = new ArrayList<>();
+        String workspaceId = UtilMap.getString(formData, "textField_lx2yekqj");
+        if (StringUtils.isNotBlank(workspaceId)) {
+            /// ppExt 一级目录设置权限打断: 后续知识库成员调整, 非管理者与所有者人员不会传递到当前路径下, 即该文件若要可见需要重新对成员赋权
+            String dentryUuid = UtilMap.getString(formData, "textField_lxmw6z7k");
+            String creatorId = UtilMap.getString(formData, "textField_lx8ra1bs");
+            List<Map> details = UtilMap.getList(formData, "tableField_lqxtykcf");
+            for (Map detail : details) {
+                if (ObjectUtil.isNotNull(detail.get("employeeField_lqxtykch_id"))) {
+                    staffIds.addAll((List) detail.get("employeeField_lqxtykch_id"));
+                }
+            }
+            String creatorUnionId = String.valueOf(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), creatorId).get("unionid"));
+            _breakPermission(UtilMap.getString(formData, "textField_lx342bvd"), dentryUuid, staffIds, creatorUnionId, null);
+        }
+    }
+
     @Override
     public void test() {
+//        approveVersion("659a681d44ade3345fdc0d39", "99999");
 
+        String pCode = "99999";
+//        String pCode = "A240407DryRun";
+//
+        List<Map> pList = ydService.queryDataList_FormData(_matchFormUuid("PROJECT"), UtilMap.map("textField_lrj7vnxb", pCode));
+        pList = pList.stream().filter(item -> pCode.equals(item.get("textField_lrj7vnxb"))).collect(Collectors.toList());
+        _assembleBreakPermission(pList.get(0));
+
+//        String creatorUnionId = String.valueOf(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), "095358016629044412").get("unionid"));
+//        List<Map> allMembers = ddClient_storage.queryDentryPermissions(ddClient.getAccessToken(), "14lgGw3P8vMDNPvMC7aw11LzJ5daZ90D", creatorUnionId, null);
+//        log.info("xxxx, {}", allMembers);
     }
 
     @Override
@@ -1398,6 +1508,14 @@ public class AWImplClient implements AWClint {
 //        this.tmp_617();
     }
 
+    /// tmp: 6.18 临时修改权限
+    private void tmp618() {
+        // 知识库返回根节点 rootNodeId, 即根目录的 dentryUuid, 可添加整个知识库编辑权限, 通过知识库创建者赋权
+        List<Map> members = Arrays.asList(UtilMap.map("type, id", "USER", ddConf.getOperator()));
+        String creatorId = String.valueOf(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), "Fra2CxAD27BuTqzaEvZ62bDZXBMTPsVg5gIxaxfVS0s").get("unionid"));
+        ddClient_storage.updateDentryPermissions(ddClient.getAccessToken(), "Y1OQX0akWmlxny6jF3lrp9om8GlDd3mE", creatorId, "EDITOR", members, null);
+    }
+
     /// tmp: 6.17 处理审批通过数据 [新项目工作量配置异常, 审批通过配置为审核通过]
     private void tmp_617() {
 

+ 18 - 8
mjava-aiwei/src/main/resources/application-dev.yml

@@ -57,10 +57,10 @@ dingtalk:
   token:
   operator: "095358016629044412"   # OA管理员账号
 
-# aliwork
-#aliwork:
-#  appType: "APP_H7WUJTKB448F9IBDC6C4"
-#  systemToken: "DHA66081DN6GRFNC6GTRW5NIJS082ZF0UN9PLLF"
+#  aliwork
+aliwork:
+  appType: "APP_H7WUJTKB448F9IBDC6C4"
+  systemToken: "DHA66081DN6GRFNC6GTRW5NIJS082ZF0UN9PLLF"
 
 # teambition
 teambition:
@@ -69,8 +69,18 @@ teambition:
   TenantId: 6034c885e71842e1e5bb5218        # 管理后台 - 企业xx - 企业ID
   OperatorId: 5e698cca21f5ad70dfba7d2b      # 公共账号, 需要有操作权限 [牧语]
 
-# aliwork - prod
-aliwork:
-  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
-  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+## aliwork - prod
+#aliwork:
+#  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+#  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+#
+## dingtalk
+#dingtalk:
+#  agentId: 2848797049
+#  appKey: dingbqy1qugrihao43dl
+#  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+#  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+#  aesKey:
+#  token:
+#  operator: "0249EDD1-754E-44C8-87F0-255B0E32021F"   # OA管理员账号
 

+ 36 - 1
mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/TSController.java

@@ -1,5 +1,6 @@
 package com.malk.cloudpure.controller;
 
+import com.alibaba.fastjson.JSON;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
 import com.malk.server.common.McException;
@@ -17,6 +18,7 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -46,12 +48,45 @@ public class TSController {
     McR test() {
 
         //updatePosition("2445432306884832");
-        tianhua618();
+//        tianhua618();
+
+        updateVersion("7d59681c-b898-448f-ac2a-d2f411a45161");
+//        lanyun621();
 
         return McR.success();
     }
 
 
+    // 蓝云
+    private void updateVersion(String formInstId) {
+        ydClient.operateData(YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                .formInstanceId(formInstId)
+                .updateFormDataJson(JSON.toJSONString(new HashMap<>()))
+                .useLatestVersion(true)
+                .build(), YDConf.FORM_OPERATION.update);
+
+    }
+
+    /// 6.21 蓝云数据版本问题
+    private void lanyun621() {
+
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                .formUuid("FORM-5A6C8B23015F44949F66543E5E684F5EF3SK")
+                .build());
+        dataList.forEach(item -> {
+            updatePosition(UtilMap.getString(item, "textField_lxjl5kwm"));
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                    .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                    .formInstanceId(UtilMap.getString(item, "formInstanceId"))
+                    .build(), YDConf.FORM_OPERATION.update);
+        });
+    }
+
     /// 6.18 苏州天华更新部门职位
     private void tianhua618() {
         List<Map> dataList = ydService.queryFormData_all(YDParam.builder()

+ 5 - 0
mjava/src/main/java/com/malk/server/dingtalk/DDConf.java

@@ -40,6 +40,11 @@ public class DDConf {
      */
     public static final long TOP_DEPARTMENT = 1L;
 
+    /**
+     * 知识库权限设置成员上限: 30
+     */
+    public static final Integer WORKSPACE_PERMISSION_SIZE = 30;
+
     /**
      * 钉钉回调响应
      */

+ 2 - 0
mjava/src/main/java/com/malk/server/dingtalk/DDR_New.java

@@ -128,6 +128,8 @@ public class DDR_New<T> extends VenR {
     private Map creator;
     private Map updater;
 
+    List<Map> permissions; // 权限列表以及人员
+
     ////  专属钉  ////
 
     // 避免无数据返回空

+ 27 - 1
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Storage.java

@@ -59,10 +59,12 @@ public interface DDClient_Storage {
 
     /**
      * 获取知识库详情
-     * ppExt: 若是权限外, 返回 403 无访问权限
+     * ppExt: 若是权限外, 返回 403 无访问权限 [ fixme: 注意创目录接口, 会单独返回 dentryUuid, 返回的 rootNodeId 本质上是 dentryId ]
      * 1. 知识库返回根节点 rootNodeId, 即根目录的 dentryUuid, 可添加整个知识库编辑权限, 通过知识库创建者赋权
      * 2. fixme 知识库列表接口, 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库;
+     * 3. ppExt 特别说明: 知识库 creatorId 就是所有者, 转交后 creatorId 会自动同步更新
      *
+     * @param withPermissionRole 仅会返回 operatorId 对应知识库的权限, permissionRole
      * @apiNote https://open.dingtalk.com/document/orgapp/get-knowledge-base
      */
     Map getWorkspaceDetail(String accessToken, String operatorId, String workspaceId, boolean withPermissionRole);
@@ -78,6 +80,8 @@ public interface DDClient_Storage {
 
     /**
      * 创建知识库文档/文件夹 [ppExt: 文件/文件夹名称若重复会自动添加(1)\(2)\..., 目前通过自建文件夹/文件数据表控制]
+     * -
+     * [ fixme: 注意创目录接口, 会单独返回 dentryUuid, 返回的 rootNodeId 本质上是 dentryId ]
      *
      * @param name    文件名称 / 文件夹名称
      * @param docType DOC:文字, WORKBOOK:表格, MIND:脑图, FOLDER:文件夹
@@ -113,10 +117,32 @@ public interface DDClient_Storage {
      * 1. 依赖 dentryUuid, 目前创建文件/文件夹, 复制文件/文件夹, 以及知识库链接 node 后字符串 或者是通过链接反查 nodeId 都是 dentryUuid
      * 2. 知识库复制需要同时有文件与\复制目标文件夹下编辑权限, 知识库可为其他成员添加不高于自身的权限, 可通过反查 dentryUuid 通过创建人添加编辑权限, 再进行复制
      *
+     * @param members 权限成员列表,最大size30。[ fixme 可通过 partition 快速切割: List<List<String>> parts = Lists.partition(staffIds, 30);]
+     * @param option  duration 目前仅OwnerType为APP的Space支持临时权限
      * @apiNote https://open.dingtalk.com/document/orgapp/modify-permissions-file
      */
     boolean updateDentryPermissions(String accessToken, String dentryUuid, String unionId, String roleId, List<Map> members, Map option);
 
+    /**
+     * 设置权限继承模式
+     * ppExt: 设置为打断模式, 与手动修改权限效果一致. 后续知识库成员调整, 非管理者与所有者人员不会传递到当前路径下, 即该文件若要可见需要重新对成员赋权
+     *
+     * @param inheritance 不支持OWNER和MANAGER的打断, 默认权限继承模式
+     *                    BREAK: 打断 权限的传递在当前节点做一个打断;
+     *                    PASS_ON: 传递 当前文件(夹)会继承所有父节点的权限, 然后结合当前文件(夹)上的权限, 相同成员权限取最大
+     * @apiNote https://open.dingtalk.com/document/orgapp/set-permission-inheritance-mode
+     */
+    boolean updateDentryPermissionsInheritances(String accessToken, String dentryUuid, String unionId, String inheritance);
+
+    /**
+     * 获取权限列表 [ fixme: 需要获取知识库根目录 dentryUuid 成员列表作为权限原始数据 ]
+     *
+     * @param option 原 option 参数: nextToken: 分页游标, maxResults: 分页大小,默认值30; filterRoleIds: 过滤角色 [ OWNER, MANAGER, EDITOR, DOWNLOADER, READER ]
+     * @apiNote https://open.dingtalk.com/document/orgapp/get-permission-list
+     */
+    List<Map> queryDentryPermissions(String accessToken, String dentryUuid, String unionId, Map option);
+
+
     ///////////////////////// OA审批附件 /////////////////////////
 
     /**

+ 30 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Storage.java

@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.dingtalk.DDClient_Storage;
+import com.malk.utils.UtilHttp;
 import com.malk.utils.UtilMap;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -190,6 +191,35 @@ public class DDImplClient_Storage implements DDClient_Storage {
         return ddr_new.isSuccess();
     }
 
+    @Override
+    public boolean updateDentryPermissionsInheritances(String accessToken, String dentryUuid, String unionId, String inheritance) {
+        Map param = UtilMap.map("unionId", unionId);
+        Map body = UtilMap.map("inheritance", inheritance);
+        DDR_New ddr_new = (DDR_New) UtilHttp.doPut("https://api.dingtalk.com/v2.0/storage/spaces/dentries/" + dentryUuid + "/permissions/inheritances", DDConf.initTokenHeader(accessToken), param, body, DDR_New.class);
+        return ddr_new.isSuccess();
+    }
+
+    @Override
+    public List<Map> queryDentryPermissions(String accessToken, String dentryUuid, String unionId, Map option) {
+        return _getDentryPermissions(accessToken, dentryUuid, unionId, option, new ArrayList<>());
+    }
+
+    /// 递归知识库成员列表
+    private List<Map> _getDentryPermissions(String accessToken, String dentryUuid, String unionId, Map option, List<Map> dataList) {
+
+        Map param = UtilMap.map("unionId", unionId);
+        option = UtilMap.put(option, "maxResults", DDConf.WORKSPACE_PERMISSION_SIZE);
+        Map body = UtilMap.map("option", option);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v2.0/storage/spaces/dentries/" + dentryUuid + "/permissions/query", DDConf.initTokenHeader(accessToken), param, body);
+        dataList.addAll(ddr_new.getPermissions());
+        // 返回空字符串, 过滤
+        if (StringUtils.isNotBlank(ddr_new.getNextToken())) {
+            option.put("nextToken", ddr_new.getNextToken());
+            _getDentryPermissions(accessToken, dentryUuid, unionId, option, dataList);
+        }
+        return dataList;
+    }
+
     ///////////////////////// OA审批附件 /////////////////////////
 
     /**

+ 1 - 1
mjava/src/main/java/com/malk/service/teambition/TBClient.java

@@ -259,7 +259,7 @@ public interface TBClient {
     List<Map> queryProgramList_all();
 
     /**
-     * 批量添加项目到项目集
+     * 批量添加项目到项目集 [ppExt: 添加项目集 operatorId 需要有项目集合管理权限, 可使用项目集合返回创建人更新 ]
      *
      * @apiNote https://open.teambition.com/docs/apis/64e810fe912d20d3b58f9ff9
      */

+ 3 - 1
mjava/src/main/java/com/malk/utils/UtilMap.java

@@ -164,7 +164,9 @@ public abstract class UtilMap {
      * 赋值 [原值为空赋值默认值]
      */
     public static Map put(Map data, String key, Object defaultValue) {
-        data = empty(data);
+        if (ObjectUtil.isNull(data)) {
+            data = empty(data);
+        }
         Object value = data.get(key);
         if (ObjectUtil.isNull(value)) {
             value = defaultValue;