Browse Source

艾为知识库版本

pruple_boy 1 year ago
parent
commit
428f7a07c8

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

@@ -59,7 +59,7 @@ public class TBxYDController {
 
         Map<String, ?> data = UtilServlet.getParamMap(request);
         log.info("检查项check回调, {}", data);
-        McException.assertParamException_Null(data, "taskId, creatorId, formInstId");
+        McException.assertParamException_Null(data, "taskId, formInstId");
 
         awClint.checked(data);
         return McR.success();
@@ -227,6 +227,20 @@ public class TBxYDController {
         return McR.success(dataList);
     }
 
+    /**
+     * 知识库版本管理
+     */
+    @PostMapping("approve/version")
+    McR approveVersion(HttpServletRequest request) {
+
+        Map<String, ?> data = UtilServlet.getParamMap(request);
+        log.info("知识库版本管理, {}", data);
+        McException.assertParamException_Null(data, "taskId", "pCode");
+
+        awClint.approveVersion(UtilMap.getString(data, "taskId"), UtilMap.getString(data, "pCode"));
+        return McR.success();
+    }
+
     //////// test ////////
 
     @Autowired
@@ -281,7 +295,7 @@ public class TBxYDController {
 
     @GetMapping("tmp")
     McR tmp(String programIds) {
-        
+
         return McR.success();
     }
 }

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

@@ -26,6 +26,7 @@ public class AWServer {
     public static final String TASK_CHECK_STATUS = "技术检查项检查状态";
 
     public static final String TASK_APPROVE_ATTACHMENT = "交付件";
+    public static final String TASK_APPROVE_VERSION = "交付件版本";
     public static final String TASK_APPROVE_LINK = "交付件审批流程";
     public static final String TASK_APPROVE_DESC = "交付件描述";
 

+ 5 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/AWClint.java

@@ -93,5 +93,10 @@ public interface AWClint {
      */
     List<Map> syncVerifier(String projectCode);
 
+    /**
+     * 知识库版本管理
+     */
+    void approveVersion(String taskId, String pCode);
+
     void test();
 }

+ 237 - 9
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWImplClient.java

@@ -9,9 +9,13 @@ import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
 import com.malk.server.common.McException;
 import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDR_New;
 import com.malk.server.teambition.TBConf;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.aliwork.YDService;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDClient_Storage;
 import com.malk.service.teambition.TBClient;
 import com.malk.utils.*;
 import lombok.Synchronized;
@@ -43,6 +47,7 @@ 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"); // 项目主数据
@@ -252,9 +257,11 @@ public class AWImplClient implements AWClint {
 
     /**
      * 获取知识库附件传递到宜搭
-     * - ppExt -
+     * -
      * 1. 宜搭附件传递 downloadUrl 和 name 即可实现在线预览
      * 2. 知识库绑定没有文件后缀, 宜搭识别目前仅能点击下载跳转预览, 添加统一docx后缀, 文档会自行区分
+     * -
+     * ppExt: 获取任务字段需要传递 AWServer.TASK_APPROVE_ATTACHMENT 以获取ID, 另外添加 .doc 只是重定向若是文件夹也不影响访问
      */
     List<Map> _getDocs(Map taskData) {
         Map rTask = UtilMap.getMap(taskData, "task");
@@ -262,7 +269,9 @@ public class AWImplClient implements AWClint {
         String deliverable = String.valueOf(taskData.get(AWServer.TASK_APPROVE_ATTACHMENT + "_id")); // 交付物任务字段ID
         List<Map> attachments = TBConf.getTaskFieldValue(customfields, deliverable).stream().map(item -> {
             Map link = (Map) JSON.parse(String.valueOf(item.get("metaString")));
-            return UtilMap.map("downloadUrl, name", link.get("url"), link.get("title") + ".docx");
+            Map row = UtilMap.map("downloadUrl, name", link.get("url"), link.get("title") + ".docx");
+            row.putAll(link); // 保留原数据作为知识库版本管理相关参数
+            return row;
         }).collect(Collectors.toList());
         return attachments;
     }
@@ -605,12 +614,12 @@ public class AWImplClient implements AWClint {
             tbClient.updateProjectStatusField(projectId, tbConf.getOperatorId(), TBConf.assembleCustomFieldName("项目重要等级", UtilMap.getString(formData, "textField_lwj1r7n6")));
         }
         // 项目添加到项目集
-        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());
-        }
-        
+//        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());
+//        }
+
         log.info("TB项目信息, {}, {}", projectId, roleIds.size());
     }
 
@@ -1078,6 +1087,225 @@ public class AWImplClient implements AWClint {
 
     }
 
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private DDClient_Storage ddClient_storage;
+
+    /// fixme: 文件/文件夹名称若重复会自动添加(1)\(2)\..., 目前通过自建文件夹/文件数据表控制 [fileType - 文件/文件夹]
+    private Map _upsertFileAbsolutePath(String fileType, String fileName, String pCode, String pId, String workspaceId, String rootNodeId, String unionId) {
+
+        // 避免模糊匹配, 知识库ID + 文件/文件夹名称全路径
+        List<Map> conditions = new ArrayList<>();
+        Map work = new HashMap();
+        work.put("key", "textField_lx2yekqj");
+        work.put("value", workspaceId);
+        work.put("type", "TEXT");
+        work.put("operator", "eq");
+        work.put("componentName", "TextField");
+        conditions.add(work);
+        Map file = new HashMap();
+        file.put("key", "textField_lx36gdpp");
+        file.put("value", fileName);
+        file.put("type", "TEXT");
+        file.put("operator", "eq");
+        file.put("componentName", "TextField");
+        conditions.add(file);
+
+        List<Map> dataList = (List<Map>) ydClient.queryData(YDParam.builder()
+                .formUuid(_matchFormUuid("DENTRY"))
+                .searchCondition(JSON.toJSONString(conditions))
+                .build(), YDConf.FORM_QUERY.retrieve_list).getData();
+        String formInstId = "";
+        int version = 0;
+        if (dataList.isEmpty()) {
+            Map formData = UtilMap.map("radioField_lx36gdpm, textField_lx36gdpp, textField_lrj7vnxb, textField_lqxtykce", fileType, fileName, pCode, pId);
+            formData.put("textField_lx2yekqj", workspaceId);
+            if ("文件夹".equals(fileType)) {
+                if (fileName.contains("/")) {
+                    fileName = String.valueOf(UtilList.getLast(fileName.split("/")));
+                }
+                DDR_New ddr_new = ddClient_storage.createFolder(ddClient.getAccessToken(), unionId, workspaceId, fileName, "FOLDER", rootNodeId, null);
+                rootNodeId = ddr_new.getNodeId(); // ppExt: 文件夹创后, 更新 rootNodeId, 用于创建下级目录
+                formData.put("textField_lx36gdpn", ddr_new.getDentryUuid());
+                formData.put("textField_lx36gdpo", ddr_new.getUrl());
+                formData.put("textField_lx342bvd", rootNodeId);
+            } else {
+                formData.put("numberField_lx36gdpz", version);
+            }
+            formInstId = (String) ydClient.operateData(YDParam.builder()
+                    .formUuid(_matchFormUuid("DENTRY"))
+                    .formDataJson(JSON.toJSONString(formData))
+                    .build(), YDConf.FORM_OPERATION.create);
+        } else {
+            formInstId = UtilMap.getString(dataList.get(0), "formInstanceId");
+            rootNodeId = UtilMap.getString(UtilMap.getMap(dataList.get(0), "formData"), "textField_lx342bvd");
+            version = UtilMap.getInt(UtilMap.getMap(dataList.get(0), "formData"), "numberField_lx36gdpz");
+        }
+        // 返回 formInstId 用于记录文件版本号, 以及复制后文件访问链接
+        return UtilMap.map("rootNodeId, formInstId, version", rootNodeId, formInstId, version);
+    }
+
+    /**
+     * 知识库复制操作 sop [ fixme: 文件复制后, 变更记录不会保存 ]
+     * -
+     * 1. TB 任务上未返回真实 workspaceId
+     * - 现状: 返回的 WorkspaceId 其实是 spaceId, instanceId 其实是 dentryId.
+     * - 临时: 知识库文件/文件夹复制接口需要传递来源 workplaceId, 但测试下来传递目标 workplaceId 也可以, 包括从我的文档内拷贝均验证通过
+     * - 解法: 官方已做了兼容
+     * -
+     * 2. TB 任务上返回链接格式不一致
+     * - 原因: 知识库复制需要同时有文件与\复制目标文件夹下编辑权限. 目前外部使用不能提供公共操作账号, 只能通过 dentryUuid 给来源文件添加编辑权限, 再进行复制操作
+     * - 现状: 从最近浏览选择的文件, 地址会被转换, 点击以后会重定向到知识库真实地址, 地址带有 node 与 dentryUuid
+     * - 官方: 文档选择器 有最近浏览列表 以及 选知识库后的目录树,背后都是对接的钉钉文档开放API,不过前者是V1版本的,后者是V2版本的,文档他们好像又出了V3版本,总之不同版本接口返回的链接可能是不一样的
+     * - 临时: 目前控制用户不能选择最近浏览
+     * - 建议: 权限控制需要通过 dentryUuid, TB是否可反回, 或可提供通过 dentry 查询 dentryUuid接口; 或者使用现有链接反查接口, 是否确保链接格式内包含 dentryUuid [URL下node后字符串]
+     * - 解法: 提供通过 spaceId + dentryId 查询详情接口, 返回有创建人\dentryUuid信息
+     * -
+     * 3. 通过接口添加知识库权限操作人需要有权限
+     * 说明: 详见2 [fixme: 公共账号拥有所有项目知识库的编辑以上权限]
+     * 解法: 通过提供通过 spaceId + dentryId 查询创建人, 再由创建人为公共账号添加编辑权限, 再通过公共账号复制 [ppExt: 知识库可为其他成员添加不高于自身的权限]
+     * -
+     * 4. 需要通过模板自动创建知识库, TB 134版本已支持
+     * 1. 调整: fixme 知识库列表接口, 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库
+     * 2. 方案: 依赖同3, 公共账号 TBManager 默认是项目拥有者, 会默认同步到知识库成员, 查询权限后通过知识库创建者添加编辑, 用于复制文件 [ppExt: 直接添加知识库根节点 rootNodeId 编辑权限]
+     */
+    @Override
+    public void approveVersion(String taskId, String pCode) {
+
+        if (!"A240407DryRun".equals(pCode)) {
+            return;
+        }
+
+        String result = "";
+
+        // 通用账户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));
+        pList = pList.stream().filter(item -> pCode.equals(item.get("textField_lrj7vnxb"))).collect(Collectors.toList());
+        if (pList.isEmpty()) {
+            result = "未匹配到项目主数据";
+        } else {
+            workspaceId = UtilMap.getString(pList.get(0), "textField_lx2yekqj");
+            rootNodeId = UtilMap.getString(pList.get(0), "textField_lx342bvd");
+            creatorId = UtilMap.getString(pList.get(0), "textField_lx8ra1bs");
+            if (StringUtils.isBlank(workspaceId)) {
+                // 获取知识库空间Id [ ppExt: 知识库列表接口, 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库 ]
+                List<Map> workspaces = ddClient_storage.searchWorkspaces(ddClient.getAccessToken(), unionId, pCode, null);
+                if (workspaces.isEmpty()) {
+                    result = "项目知识库未找到";
+                } else {
+                    workspaceId = UtilMap.getString(workspaces.get(0), "workspaceId");
+                    Map space = ddClient_storage.getWorkspaceDetail(ddClient.getAccessToken(), unionId, workspaceId, false);
+                    rootNodeId = UtilMap.getString(space, "rootNodeId"); // 知识库返回根节点 rootNodeId, 即根目录的 dentryUuid
+                    creatorId = UtilMap.getString(space, "creatorId");
+                    // 更新知识库信息到项目档案
+                    ydClient.operateData(YDParam.builder()
+                            .formInstanceId(UtilMap.getString(pList.get(0), "instanceId"))
+                            .updateFormDataJson(JSON.toJSONString(UtilMap.map("textField_lx2yekqj, textField_lx342bvd, textField_lx8ra1bs, textField_lx8ra1bt", workspaceId, rootNodeId, creatorId, space.get("url"))))
+                            .build(), YDConf.FORM_OPERATION.update);
+                }
+            }
+        }
+        // 权限 & 文件复制 & 版本管理
+        if (StringUtils.isBlank(result)) {
+            try {
+                // 知识库返回根节点 rootNodeId, 即根目录的 dentryUuid, 可添加整个知识库编辑权限, 通过知识库创建者赋权
+                List<Map> members = Arrays.asList(UtilMap.map("type, id", "USER", ddConf.getOperator()));
+                creatorId = String.valueOf(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), creatorId).get("unionid"));
+                ddClient_storage.updateDentryPermissions(ddClient.getAccessToken(), rootNodeId, creatorId, "EDITOR", members, null);
+            } catch (McException e) {
+                // ppExt: 若是创建人赋值编辑权限, 也会是500异常
+                log.error(e.getMessage(), e);
+            }
+            // 知识库文件夹 & 拷贝处理 [prd 文件目录结构 TR评审节点/资源名称]
+            Map taskData = _getTaskFieldMap(taskId, AWServer.TASK_APPROVE_ATTACHMENT, AWServer.TASK_STAGE, AWServer.TASK_ROLE);
+            Map rTask = UtilMap.getMap(taskData, "task");
+
+            // 文件/文件夹版本控制: fileType, fileName, pCode, pId, workspaceId, rootNodeId, unionId
+            String fileName = "项目交付件";
+            Map record = _upsertFileAbsolutePath("文件夹", fileName, pCode, UtilMap.getString(rTask, "projectId"), workspaceId, rootNodeId, unionId);
+            rootNodeId = UtilMap.getString(record, "rootNodeId"); // 最新文件路径下ID
+
+            String rName = UtilMap.getString(taskData, AWServer.TASK_STAGE);
+            if (StringUtils.isBlank(rName)) {
+                rName = "其他";
+            }
+            fileName = fileName + "/" + rName;
+            record = _upsertFileAbsolutePath("文件夹", fileName, pCode, UtilMap.getString(rTask, "projectId"), workspaceId, rootNodeId, unionId);
+            rootNodeId = UtilMap.getString(record, "rootNodeId"); // 最新文件路径下ID
+
+            String tName = UtilMap.getString(taskData, AWServer.TASK_ROLE);
+            if (StringUtils.isBlank(tName)) {
+                tName = "其他";
+            }
+            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<>();
+            for (Map doc : docs) {
+
+                // 文件/文件夹版本控制: fileType, fileName, pCode, pId, workspaceId, rootNodeId, unionId
+                String title = UtilMap.getString(doc, "title");
+                fileName = fileName + "/" + title;
+                Map version = _upsertFileAbsolutePath("文件", fileName, pCode, UtilMap.getString(rTask, "projectId"), workspaceId, rootNodeId, unionId);
+
+                // 通过提供通过 spaceId + dentryId 查询创建人, 再由创建人为公共账号添加编辑权限, 再通过公共账号复制 [ppExt: 知识库可为其他成员添加不高于自身的权限]
+                DDR_New ddr_dentry = ddClient_storage.getSpaceDentryDetail(ddClient.getAccessToken(), unionId, UtilMap.getString(doc, "instanceId"), UtilMap.getString(doc, "instanceId"), false);
+                try {
+                    String creatorId_t = UtilMap.getString(ddr_dentry.getUpdater(), "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) {
+                    log.error(e.getMessage(), e);
+                    // ppExt: 若是创建人权限被管理员拿走, 会导致500异常, 需要兼容
+                    continue;
+                }
+
+                // 复制文件/文件夹, 知识库文件添加版本标识
+                int order = UtilMap.getInt(version, "version");
+                order += 1;
+                String docName = title + " v" + order + ".0";
+                DDR_New ddr_new = ddClient_storage.copyDentries(ddClient.getAccessToken(), unionId, workspaceId, UtilMap.getString(doc, "instanceId"), workspaceId, rootNodeId, docName);
+
+                // 更新最新版本记录, 叠加版本号
+                Map formData = UtilMap.map("textField_lx36gdpp, numberField_lx36gdpz", fileName, order);
+                formData.put("textField_lx342bvd", ddr_new.getDentryId());
+                formData.put("textField_lx36gdpn", ddr_new.getDentryUuid());
+                formData.put("textField_lx36gdpo", ddr_new.getUrl());
+                ydClient.operateData(YDParam.builder()
+                        .formInstanceId(UtilMap.getString(version, "formInstId"))
+                        .updateFormDataJson(JSON.toJSONString(formData))
+                        .build(), YDConf.FORM_OPERATION.update);
+                // 更新版本字段
+                Map meta = UtilMap.map("title, url, instanceId, workspaceId, image", docName, ddr_new.getUrl(), ddr_new.getDentryId(), ddr_new.getSpaceId(), doc.get("image"));
+                attas.add(UtilMap.map("title, metaString", docName, JSON.toJSONString(meta)));
+            }
+            if (attas.isEmpty()) {
+                result = "无编辑权限, 复制失败";
+            } else {
+                // 多文件更新回写 [ fixme: 更新任务自定义字段值 ]
+                tbClient.updateTaskCustomField(taskId, tbConf.getOperatorId(), UtilMap.map("customfieldName, value", AWServer.TASK_APPROVE_VERSION, attas));
+            }
+        }
+        if (StringUtils.isNotBlank(result)) {
+            Map body = TBConf.assembleCustomFieldName(AWServer.TASK_APPROVE_VERSION, result);
+            tbClient.updateTaskCustomField(taskId, tbConf.getOperatorId(), body);
+        }
+        // todo: 业务待定, 若变更有单独BPM流程, TBx宜搭只是作为归档管理, 不存在变更版本审批拒绝场景
+        log.info("知识库版本管理, {}", result);
+    }
+
     @Override
     public void test() {
 
@@ -1123,7 +1351,7 @@ public class AWImplClient implements AWClint {
                     .build(), YDConf.FORM_OPERATION.update);
         });
     }
-
 }
 
 
+

+ 2 - 12
mjava-aiwei/src/main/resources/application-dev.yml

@@ -55,7 +55,7 @@ dingtalk:
   corpId: ding321c72787fffc78b35c2f4657eb6378f
   aesKey:
   token:
-  operator: ""   # OA管理员账号
+  operator: "095358016629044412"   # OA管理员账号
 
 # aliwork
 #aliwork:
@@ -69,17 +69,7 @@ teambition:
   TenantId: 6034c885e71842e1e5bb5218        # 管理后台 - 企业xx - 企业ID
   OperatorId: 5e698cca21f5ad70dfba7d2b      # 公共账号, 需要有操作权限 [牧语]
 
-# dingtalk
-#dingtalk:
-#  agentId: 2848797049
-#  appKey: dingbqy1qugrihao43dl
-#  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
-#  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
-#  aesKey:
-#  token:
-#  operator: ""   # OA管理员账号
-
-# aliwork
+# aliwork - prod
 aliwork:
   appType: "APP_R5EBUF2FPN3Y8DRF93M4"
   systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"

+ 2 - 1
mjava/src/main/java/com/malk/server/common/McException.java

@@ -27,7 +27,8 @@ public class McException extends RuntimeException {
     private String message;
 
     // 错误来源
-    private String source;
+    @Builder.Default
+    private String source = "MC";
 
     public McException(McREnum rEnum) {
         this(rEnum.isSuc(), rEnum.getCode(), rEnum.getMsg());

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

@@ -106,6 +106,27 @@ public class DDR_New<T> extends VenR {
     /// 文件或文件夹列表
     private List<Map> dentries;
 
+    /// 知识库
+    private List<Map> workspaces;
+
+    private List<Map> items;
+
+    private Map workspace;
+
+    // 知识库 [创建文件/文件夹]
+    private String workspaceId;
+    private String dentryUuid;
+    private String nodeId;
+    private String url;
+
+    // 知识库 [复制文件/文件夹]
+    private String dentryId; // nodeId
+    private String spaceId;
+    private String name;
+
+    // 知识库 [节点信息]
+    private Map creator;
+    private Map updater;
 
     ////  专属钉  ////
 

+ 1 - 1
mjava/src/main/java/com/malk/server/teambition/TBConf.java

@@ -71,7 +71,7 @@ public class TBConf {
         return "";
     }
 
-    /// 更新任务自定义字段值 [ppExt: 富文本不能解析, 知识库可写入]
+    /// 更新任务自定义字段值 [ppExt: 富文本不能解析, 知识库可写入] - todo 不支持多参, 参考知识库版本管理更新
     public static Map assembleCustomField(String fieldName, String fieldValue, String value, Object meta) {
         Map body = UtilMap.map(fieldName, fieldValue);
         Map data = UtilMap.map("title", (Object) value);

+ 75 - 0
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Storage.java

@@ -40,6 +40,81 @@ public interface DDClient_Storage {
      */
     Map addFolders(String accessToken, String unionId, String spaceId, String parentId, String name, Map option);
 
+    ///////////////////////// 知识库 /////////////////////////
+
+    /**
+     * 获取知识库列表 [ ppExt: 查询用户有权限的知识库列表, 接口已废弃. 现使用查询接口, 同样会受到权限控制 ]
+     *
+     * @param operatorId fixme 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库
+     * @apiNote https://open.dingtalk.com/document/orgapp/get-knowledge-base-list
+     */
+    List<Map> getWorkspaces(String accessToken, String operatorId, Map extInfo);
+
+    /**
+     * 搜索知识库 [ ppExt: 知识库列表接口, 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库 ]
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/search-knowledge-base
+     */
+    List<Map> searchWorkspaces(String accessToken, String operatorId, String keyword, Map option);
+
+    /**
+     * 获取知识库详情
+     * ppExt: 若是权限外, 返回 403 无访问权限
+     * 1. 知识库返回根节点 rootNodeId, 即根目录的 dentryUuid, 可添加整个知识库编辑权限, 通过知识库创建者赋权
+     * 2. fixme 知识库列表接口, 查询受到 operatorId 权限控制, 仅会返回操作者有权限知识库;
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/get-knowledge-base
+     */
+    Map getWorkspaceDetail(String accessToken, String operatorId, String workspaceId, boolean withPermissionRole);
+
+    /**
+     * 知识库复制文件或文件夹 [ppExt: 艾为灰度接口, 支持文件夹的复制]
+     * fixme: 文件复制后, 变更记录不会保存
+     * 1.知识库文件/文件夹复制接口需要传递来源 workplaceId, 但测试下来传递目标 workplaceId 也可以, 包括从我的文档内拷贝均验证通过
+     * 2.知识库复制需要同时有文件与\复制目标文件夹下编辑权限. 目前外部使用不能提供公共操作账号, 只能通过 dentryUuid 给来源文件添加编辑权限, 再进行复制操作
+     */
+    DDR_New copyDentries(String accessToken, String operatorId, String workspaceId, String fileDentryId, String targetSpaceId, String toParentDentryId, String fileName);
+
+    /**
+     * 创建知识库文档/文件夹 [ppExt: 文件/文件夹名称若重复会自动添加(1)\(2)\..., 目前通过自建文件夹/文件数据表控制]
+     *
+     * @param name    文件名称 / 文件夹名称
+     * @param docType DOC:文字, WORKBOOK:表格, MIND:脑图, FOLDER:文件夹
+     * @apiNote https://open.dingtalk.com/document/isvapp/create-team-space-document
+     */
+    DDR_New createFolder(String accessToken, String operatorId, String workspaceId, String name, String docType, String parentNodeId, Map extInfo);
+
+    /**
+     * 查询知识库节点信息
+     * ppExt: 历史接口, 可兼容当前TB返回
+     * 1. TB 任务上未返回真实 workspaceId, 返回的 WorkspaceId 其实是 spaceId, instanceId 其实是 dentryId.
+     * 2. 通过提供通过 spaceId + dentryId 查询创建人, 再由创建人为公共账号添加编辑权限, 再通过公共账号复制 [ppExt: 知识库可为其他成员添加不高于自身的权限]
+     *
+     * @apiNote https://open.dingtalk.com/document/isvapp/query-knowledge-base-node-information?spm=a2q3p.21071111.0.0.93ffaiEWaiEWOI
+     */
+    DDR_New getSpaceDentryDetail(String accessToken, String operatorId, String spaceId, String dentryId, boolean includeSpace);
+
+
+    ///////////////////////// 储存管理 /////////////////////////
+
+    /**
+     * 搜索文件 [ppExt: 支持知识库 + 文档检索]
+     * fixme: 不实用, 虽然可以返回修改权限所依赖的 dentryUuid, 但搜索不支持知识库检索, 且重名文件无法区分
+     *
+     * @apiNote https://open-dev.dingtalk.com/apiExplorer?spm=ding_open_doc.document.0.0.44862e9ajnDDHC#/?devType=org&api=storage_2.0%23SearchDentries
+     */
+    List<Map> searchDentry(String accessToken, String operatorId, String keyword, Map option);
+
+    /**
+     * 修改权限
+     * ppExt: fixme: 若是创建人权限被管理员拿走, 会导致500异常, 需要兼容; 若是创建人赋值编辑权限, 也会是500异常
+     * 1. 依赖 dentryUuid, 目前创建文件/文件夹, 复制文件/文件夹, 以及知识库链接 node 后字符串 或者是通过链接反查 nodeId 都是 dentryUuid
+     * 2. 知识库复制需要同时有文件与\复制目标文件夹下编辑权限, 知识库可为其他成员添加不高于自身的权限, 可通过反查 dentryUuid 通过创建人添加编辑权限, 再进行复制
+     *
+     * @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);
+
     ///////////////////////// OA审批附件 /////////////////////////
 
     /**

+ 69 - 1
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Storage.java

@@ -118,10 +118,78 @@ public class DDImplClient_Storage implements DDClient_Storage {
         if (ObjectUtil.isNotNull(option)) {
             body.put("option", option);
         }
-        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com//v1.0/storage/spaces/" + spaceId + "/dentries/" + parentId + "/folders", DDConf.initTokenHeader(accessToken), param, body);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v1.0/storage/spaces/" + spaceId + "/dentries/" + parentId + "/folders", DDConf.initTokenHeader(accessToken), param, body);
         return ddr_new.getDentry();
     }
 
+    ///////////////////////// 知识库 /////////////////////////
+
+    @Override
+    public List<Map> getWorkspaces(String accessToken, String operatorId, Map extInfo) {
+        Map param = UtilMap.map("operatorId", operatorId);
+        UtilMap.putAll(param, extInfo);
+        DDR_New ddr_new = DDR_New.doGet("https://api.dingtalk.com/v2.0/wiki/workspaces", DDConf.initTokenHeader(accessToken), param);
+        return ddr_new.getWorkspaces();
+    }
+
+    @Override
+    public List<Map> searchWorkspaces(String accessToken, String operatorId, String keyword, Map option) {
+        Map param = UtilMap.map("operatorId", operatorId);
+        Map body = UtilMap.map("keyword", keyword);
+        UtilMap.putNotNull(body, "option", option);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v2.0/storage/workspaces/search", DDConf.initTokenHeader(accessToken), param, body);
+        return ddr_new.getItems();
+    }
+
+    @Override
+    public Map getWorkspaceDetail(String accessToken, String operatorId, String workspaceId, boolean withPermissionRole) {
+        Map param = UtilMap.map("operatorId, withPermissionRole", operatorId, withPermissionRole);
+        DDR_New ddr_new = DDR_New.doGet("https://api.dingtalk.com/v2.0/wiki/workspaces/" + workspaceId, DDConf.initTokenHeader(accessToken), param);
+        return ddr_new.getWorkspace();
+
+    }
+
+    @Override
+    public DDR_New copyDentries(String accessToken, String operatorId, String workspaceId, String fileDentryId, String targetSpaceId, String toParentDentryId, String fileName) {
+        Map body = UtilMap.map("targetSpaceId, toParentDentryId, operatorId, name", targetSpaceId, toParentDentryId, operatorId, fileName);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v2.0/doc/spaces/" + workspaceId + "/dentries/" + fileDentryId + "/copy", DDConf.initTokenHeader(accessToken), null, body);
+        return ddr_new;
+    }
+
+    @Override
+    public DDR_New createFolder(String accessToken, String operatorId, String workspaceId, String name, String docType, String parentNodeId, Map extInfo) {
+        Map body = UtilMap.map("name, docType, operatorId, parentNodeId", name, docType, operatorId, parentNodeId);
+        UtilMap.putAll(body, extInfo);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v1.0/doc/workspaces/" + workspaceId + "/docs", DDConf.initTokenHeader(accessToken), null, body);
+        return ddr_new;
+    }
+
+    @Override
+    public DDR_New getSpaceDentryDetail(String accessToken, String operatorId, String spaceId, String dentryId, boolean includeSpace) {
+        Map param = UtilMap.map("operatorId, includeSpace", operatorId, includeSpace);
+        DDR_New ddr_new = DDR_New.doGet("https://api.dingtalk.com/v2.0/doc/spaces/" + spaceId + "/dentries/" + dentryId, DDConf.initTokenHeader(accessToken), param);
+        return ddr_new;
+    }
+
+    ///////////////////////// 存储空间 /////////////////////////
+
+    @Override
+    public List<Map> searchDentry(String accessToken, String operatorId, String keyword, Map option) {
+        Map param = UtilMap.map("operatorId", operatorId);
+        Map body = UtilMap.map("keyword", keyword);
+        UtilMap.putNotNull(body, "option", option);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v2.0/storage/dentries/search", DDConf.initTokenHeader(accessToken), param, body);
+        return ddr_new.getItems();
+    }
+
+    @Override
+    public boolean updateDentryPermissions(String accessToken, String dentryUuid, String unionId, String roleId, List<Map> members, Map option) {
+        Map param = UtilMap.map("unionId", unionId);
+        Map body = UtilMap.map("roleId, members, option", roleId, members, option);
+        DDR_New ddr_new = DDR_New.doPost("https://api.dingtalk.com/v2.0/storage/spaces/dentries/" + dentryUuid + "/permissions", DDConf.initTokenHeader(accessToken), param, body);
+        return ddr_new.isSuccess();
+    }
+
     ///////////////////////// OA审批附件 /////////////////////////
 
     /**