Browse Source

项目更新,格屋\快客利\丰凯利\航食

pruple_boy 2 years ago
parent
commit
150cc3ebea
77 changed files with 3132 additions and 682 deletions
  1. 0 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/Boot.java
  2. 22 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/DDController.java
  3. 112 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/GTZZController.java
  4. 5 20
      mjava-cloudpure/src/main/java/com.malk.cloudpure/controller/XBBController.java
  5. 55 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/delegate/DDDelegate.java
  6. 86 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/schedule/CPScheduleTask.java
  7. 42 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/service/CPClient.java
  8. 361 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/service/impl/CPImplClient.java
  9. 14 10
      mjava-cloudpure/src/main/resources/application-dev.yml
  10. 18 12
      mjava-cloudpure/src/main/resources/application-prod.yml
  11. 14 10
      mjava-cloudpure/target/classes/application-dev.yml
  12. 18 12
      mjava-cloudpure/target/classes/application-prod.yml
  13. 41 56
      mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java
  14. 35 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/schedule/FKLScheduleTask.java
  15. 5 5
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/impl/FKLImplService.java
  16. 2 2
      mjava-fengkaili/src/main/resources/application-dev.yml
  17. 2 0
      mjava-fengkaili/src/main/resources/application-prod.yml
  18. BIN
      mjava-fengkaili/src/main/resources/templates/Template_days.xlsx
  19. BIN
      mjava-fengkaili/src/main/resources/templates/Template_month.xlsx
  20. 2 2
      mjava-fengkaili/target/classes/application-dev.yml
  21. 2 0
      mjava-fengkaili/target/classes/application-prod.yml
  22. 9 85
      mjava-gewu/src/main/java/com/malk/gewu/controller/GWController.java
  23. 8 3
      mjava-gewu/src/main/java/com/malk/gewu/schedule/DDScheduleTask.java
  24. 9 0
      mjava-gewu/src/main/java/com/malk/gewu/service/GWService.java
  25. 102 0
      mjava-gewu/src/main/java/com/malk/gewu/service/impl/GWImplService.java
  26. 5 5
      mjava-gewu/src/main/resources/application-prod.yml
  27. 855 0
      mjava-gewu/src/main/resources/static/json/personnel.json
  28. 5 5
      mjava-gewu/target/classes/application-prod.yml
  29. 27 2
      mjava-guyuan/src/main/java/com/malk/guyuan/controller/IVController.java
  30. 2 1
      mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/impl/TXYImplInvoice.java
  31. 2 2
      mjava-guyuan/src/main/resources/application-dev.yml
  32. 2 2
      mjava-guyuan/target/classes/application-dev.yml
  33. 13 13
      mjava-hangshi/src/main/java/com/malk/hangshi/controller/HSController.java
  34. 35 0
      mjava-hangshi/src/main/java/com/malk/hangshi/schedule/HSScheduleTask.java
  35. 16 0
      mjava-hangshi/src/main/java/com/malk/hangshi/service/HSService.java
  36. 80 0
      mjava-hangshi/src/main/java/com/malk/hangshi/service/impl/HSImplService.java
  37. 54 0
      mjava-kuaikeli/pom.xml
  38. 32 0
      mjava-kuaikeli/src/main/java/com/malk/kuaikeli/Boot.java
  39. 70 0
      mjava-kuaikeli/src/main/java/com/malk/kuaikeli/controller/KKLController.java
  40. 39 0
      mjava-kuaikeli/src/main/resources/application-dev.yml
  41. 39 0
      mjava-kuaikeli/src/main/resources/application-prod.yml
  42. 35 0
      mjava-kuaikeli/src/test/resources/server.sh
  43. 25 49
      mjava-pake/src/main/java/com/malk/pake/controller/SFController.java
  44. 8 4
      mjava-pake/src/main/resources/application-dev.yml
  45. 10 5
      mjava-pake/src/main/resources/application-prod.yml
  46. 6 2
      mjava/src/main/java/com/malk/config/WebConfiguration.java
  47. 7 0
      mjava/src/main/java/com/malk/controller/DDCallbackController.java
  48. 41 0
      mjava/src/main/java/com/malk/server/aliwork/YDConf.java
  49. 0 1
      mjava/src/main/java/com/malk/server/aliwork/YDParam.java
  50. 1 0
      mjava/src/main/java/com/malk/server/common/VenR.java
  51. 3 0
      mjava/src/main/java/com/malk/server/dingtalk/DDConf.java
  52. 24 0
      mjava/src/main/java/com/malk/server/dingtalk/DDInterActiveCard.java
  53. 2 3
      mjava/src/main/java/com/malk/server/dingtalk/crypto/DingCallbackCrypto.java
  54. 26 0
      mjava/src/main/java/com/malk/server/vika/VKConf.java
  55. 34 0
      mjava/src/main/java/com/malk/server/vika/VKR.java
  56. 8 3
      mjava/src/main/java/com/malk/server/xbongbong/XBBConf.java
  57. 25 5
      mjava/src/main/java/com/malk/service/aliwork/YDService.java
  58. 106 40
      mjava/src/main/java/com/malk/service/aliwork/impl/YDServiceImpl.java
  59. 2 0
      mjava/src/main/java/com/malk/service/dingtalk/DDClient_Contacts.java
  60. 40 0
      mjava/src/main/java/com/malk/service/dingtalk/DDClient_Extension.java
  61. 14 0
      mjava/src/main/java/com/malk/service/dingtalk/DDClient_Log.java
  62. 13 2
      mjava/src/main/java/com/malk/service/dingtalk/DDService.java
  63. 7 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Contacts.java
  64. 69 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Extension.java
  65. 35 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Log.java
  66. 24 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplService.java
  67. 19 0
      mjava/src/main/java/com/malk/service/vika/VKClient.java
  68. 38 0
      mjava/src/main/java/com/malk/service/vika/impl/VKImplClient.java
  69. 9 1
      mjava/src/main/java/com/malk/service/xbongbong/XBBClient.java
  70. 19 4
      mjava/src/main/java/com/malk/service/xbongbong/impl/XBBImplClient.java
  71. 49 4
      mjava/src/main/java/com/malk/utils/UtilDateTime.java
  72. 5 1
      mjava/src/main/java/com/malk/utils/UtilExcel.java
  73. 101 96
      mjava/src/main/java/com/malk/utils/UtilFile.java
  74. 62 5
      mjava/src/main/java/com/malk/utils/UtilMap.java
  75. 9 0
      mjava/src/main/java/com/malk/utils/UtilMc.java
  76. 15 210
      mjava/target/classes/META-INF/spring-configuration-metadata.json
  77. 1 0
      pom.xml

mjava-cloudpure/src/main/java/com.malk.cloudpure/Boot.java → mjava-cloudpure/src/main/java/com/malk/cloudpure/Boot.java


+ 22 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/DDController.java

@@ -0,0 +1,22 @@
+package com.malk.cloudpure.controller;
+
+import com.malk.controller.DDCallbackController;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 钉钉事件回调 3_1
+ * -
+ * [子项目直接继承即可有调用, 无需实现]
+ * -
+ * 注解 @RequestMapping 路径不能重复 [主子项目属同一个项目];
+ * 获取项目回调请求地址, https://mc.cloudpure.cn/frp/mc/dd/callback [调试代理: frp + nginx]
+ */
+@Slf4j
+@RestController
+@RequestMapping("/cp/dd")
+public class DDController extends DDCallbackController {
+
+
+}

+ 112 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/GTZZController.java

@@ -0,0 +1,112 @@
+package com.malk.cloudpure.controller;
+
+import com.malk.cloudpure.service.CPClient;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Extension;
+import com.malk.service.vika.VKClient;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilMap;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.map.HashedMap;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.net.URLDecoder;
+import java.time.LocalDate;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping("gtzz")
+public class GTZZController {
+
+    @Autowired
+    private CPClient cpClient;
+
+    /**
+     * OA 审批 连接器
+     */
+    @PostMapping("oa-update")
+    McR update(@RequestBody Map data) {
+
+        McException.assertParamException_Null(data, "userId", "type");
+        String compId = "";
+        if ("售前".equals(data.get("type"))) {
+            compId = "numberField_llw07mxz";
+        }
+        if ("转单".equals(data.get("type"))) {
+            compId = "numberField_llw07mxx";
+        }
+        if (StringUtils.isNotBlank(compId)) {
+            String date = UtilDateTime.formatDate(new Date());
+            cpClient.upsertDailyRecord(data.get("userId").toString(), date, UtilMap.map(compId + ", type", 1, compId));
+        }
+        return McR.success();
+    }
+
+    /**
+     * 酷应用 连接器 [卡片实例数据源]
+     */
+//    @PostMapping("application")
+//    Map application() {
+//
+//        return cpClient.applicationData();
+//    }
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Extension ddClient_extension;
+
+    @Autowired
+    private DDConf ddConf;
+
+    /// 酷应用卡片推送, 宜搭导入完成率
+    @SneakyThrows
+    @PostMapping("robot")
+    McR robot() {
+
+        Thread.sleep(3000);
+        String tDate = UtilDateTime.formatLocalDate(LocalDate.now());
+        cpClient.sendCardMessage(tDate, "组织数字化团队", "业务数字化团队", "协同数字化团队, 测试群");
+        return McR.success();
+    }
+
+    @Autowired
+    private VKClient vkClient;
+
+    @SneakyThrows
+    @PostMapping("test")
+    McR test() {
+
+//        cpClient.syncBusinessUserInfo();
+//        cpClient.syncXBongBongForRecord_all(LocalDate.now());
+
+        //        ddClient_extension.sendGroupMessages(ddClient.getAccessToken(), UtilMap.map("content", "xxx12"), "sampleText", "cid/Y3nHe5fkCnHg2qsP43loQ==", ddConf.getRobotCode(), "");
+
+
+//        return McR.success(vkClient.getRecords("dst6ip677mZUxCnLiS", UtilMap.map("filterByFormula", "find('钉专 CSM', 程婕) > 0")));
+
+        String condition = URLDecoder.decode("OR(find(%22%E7%A8%8B%E5%A9%95%22%2C%20%7B%E9%92%89%E4%B8%93%20CSM%7D)%20%3E%200%2C%20find(%22%E7%A8%8B%E5%A9%95%22%2C%20%7B%E9%92%89%E4%B8%93%20CSM%7D)%20%3E%200)", "UTF-8");
+        log.info("xxxx, {}", condition);
+        Map data = new HashedMap();
+        data.put("filterByFormula", condition);
+        return McR.success(vkClient.getRecords("dst6ip677mZUxCnLiS", data));
+    }
+
+}
+
+

+ 5 - 20
mjava-cloudpure/src/main/java/com.malk.cloudpure/controller/XBBController.java

@@ -1,12 +1,9 @@
 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;
 import com.malk.server.common.McR;
 import com.malk.server.xbongbong.XBBConf;
-import com.malk.service.aliwork.YDClient;
 import com.malk.service.xbongbong.XBBClient;
 import com.malk.utils.UtilMap;
 import lombok.extern.slf4j.Slf4j;
@@ -43,8 +40,8 @@ public class XBBController {
         McException.assertParamException_Null(data, "name, userId");
 
         List<Map> customers = new ArrayList<>();
-        customers.addAll(_likeCustomerList(XBBConf.API_LIST_CUSTOMER, "钉钉", data.get("name"), data.get("userId")));
-        customers.addAll(_likeCustomerList(XBBConf.API_LIST_CUSTOMER, "Teambition", data.get("name"), data.get("userId")));
+        customers.addAll(_likeCustomerList(XBBConf.API_LIST_customer, "钉钉", data.get("name"), data.get("userId")));
+        customers.addAll(_likeCustomerList(XBBConf.API_LIST_customer, "Teambition", data.get("name"), data.get("userId")));
 
         log.info("客户响应, {}", customers);
         return McR.success(customers);
@@ -99,8 +96,8 @@ public class XBBController {
         McException.assertParamException_Null(data, "name, userId");
 
         List<Map> customers = new ArrayList<>();
-        customers.addAll(_likeContractList(XBBConf.API_LIST_CONTRACT, "钉钉", data.get("name"), data.get("userId")));
-        customers.addAll(_likeContractList(XBBConf.API_LIST_CONTRACT, "Teambition", data.get("name"), data.get("userId")));
+        customers.addAll(_likeContractList(XBBConf.API_LIST_contract, "钉钉", data.get("name"), data.get("userId")));
+        customers.addAll(_likeContractList(XBBConf.API_LIST_contract, "Teambition", data.get("name"), data.get("userId")));
 
         log.info("合同响应, {}", customers);
         return McR.success(customers);
@@ -118,22 +115,10 @@ public class XBBController {
     }
 
 
-    @Autowired
-    private YDClient ydClient;
-
     @PostMapping("test")
     McR test() {
 
-//        return McR.success(xbbClient.getFormDefine(864843, 0)); // 钉钉 864836 tb 1003094
-//        return McR.success(xbbClient.getFormList("", 1, 201));
-//        return McR.success(likeCusterList("钉钉客户", "计算机", "16808499470903077"));
-
-//        return McR.success(xbbClient.getFormList("", 1, 601));
-//        return McR.success(xbbClient.getFormDefine(864840, 0));
-
-        return McR.success(ydClient.queryData(YDParam.builder()
-                .formUuid("FORM-MP966K81ET7DZ7ZFFY4Z770D3U9F2TV2HD3LL9").build(), YDConf.FORM_QUERY.retrieve_search_form));
-
+        return McR.success();
     }
 }
 

+ 55 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/delegate/DDDelegate.java

@@ -0,0 +1,55 @@
+package com.malk.cloudpure.delegate;
+
+import com.malk.delegate.DDEvent_Delegate;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Primary;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+/**
+ * OA审批事件 [主项目也有实现, 添加 @Primary 优先注入主项目实现]
+ * -
+ * 取消方案: 撤销和拒绝流程不继续执行连接器, 因此不使用连接器与轮询审批记录方案 [低效而且占用较高钉钉api调次数];
+ * 优化方案: 通过事件订阅实现实时同步所有审批状态, 定时查询钉钉回调失败记录 [配置钉钉事件Delegate, 添加定时任务]
+ */
+@Slf4j
+@Service
+@Primary
+public class DDDelegate implements DDEvent_Delegate {
+
+    // 审批任务回调执行业务逻辑
+    @Async
+    @Override
+    public void executeEvent_Task_Finish(String processInstanceId, String processCode, boolean isAgree, String remark) {
+        log.info("executeEvent_Task_Finish");
+    }
+
+    @Async
+    @Override
+    public void executeEvent_Task_Start(String processInstanceId, String processCode) {
+        log.info("executeEvent_Task_Start");
+    }
+
+    @Async
+    @Override
+    public void executeEvent_Task_Redirect(String processInstanceId, String processCode) {
+        log.info("executeEvent_Task_Redirect");
+    }
+
+    @Async
+    @Override
+    public void executeEvent_Instance_Start(String processInstanceId, String processCode) {
+        log.info("executeEvent_Instance_Start");
+    }
+
+
+    // 审批实例回调执行业务逻辑
+    @Async
+    @Override
+    public void executeEvent_Instance_Finish(String processInstanceId, String processCode, boolean isAgree, boolean isTerminate) {
+        log.info("executeEvent_Instance_Finish");
+        String approveResult = isAgree ? "agree" : "refuse";
+        if (isTerminate) approveResult = "terminated";
+        log.info("审批实例回调执行业务逻辑, {}", approveResult);
+    }
+}

+ 86 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/schedule/CPScheduleTask.java

@@ -0,0 +1,86 @@
+package com.malk.cloudpure.schedule;
+
+import com.malk.cloudpure.service.CPClient;
+import com.malk.utils.UtilDateTime;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.time.LocalDate;
+import java.util.Date;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class CPScheduleTask {
+
+    @Autowired
+    private CPClient cpClient;
+
+    /**
+     * 同步销售部门人员 [每日13.30执行1次]
+     */
+    @Scheduled(cron = "0 30 13 * * ?  ")
+    public void task_1() {
+        try {
+            cpClient.syncBusinessUserInfo();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 同步钉钉日报数据 [18-23点, 每59执行1次]
+     */
+    @Scheduled(cron = "0 59 18-23 * * ? ")
+    public void task_2() {
+        try {
+            String sDate = UtilDateTime.formatDate(new Date());
+            cpClient.syncDingTalkLogForRecord(sDate);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 同步销帮帮跟进记录, 商机, 合同, 回款 - [全量] [09-23点, 每59执行1次]
+     */
+    @Scheduled(cron = "0 59 09-23 * * ? ")
+    public void task_3() {
+        try {
+            LocalDate date = LocalDate.now();
+            cpClient.syncXBongBongForRecord_all(date);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 推送群消息卡片 [9.30推前一天, 20.30推当日]
+     */
+    @Scheduled(cron = "0 30 9,20 * * ? ")
+    public void task_5() {
+
+        LocalDate dateNow = LocalDate.now();
+        if (new Date().getHours() < 12) {
+            dateNow = dateNow.plusDays(-1);
+        }
+        try {
+            String tDate = UtilDateTime.formatLocalDate(dateNow);
+            cpClient.sendCardMessage(tDate, "组织数字化团队", "业务数字化团队", "协同数字化团队");
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 42 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/service/CPClient.java

@@ -0,0 +1,42 @@
+package com.malk.cloudpure.service;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+public interface CPClient {
+
+    /**
+     * 创建每日数据 [增量更新]
+     */
+    void upsertDailyRecord(String userId, String tDate, Map formData);
+
+    /**
+     * 同步日报数据 [日报模板]
+     */
+    void syncDingTalkLogForRecord(String sDate);
+
+    /**
+     * 同步销帮帮客户数, 跟进记录, 商机, 合同, 回款
+     */
+    void syncXBongBongForRecord(String userId, long start, long end);
+
+    /**
+     * 同步销帮帮客户数, 跟进记录, 商机, 合同, 回款 - [全量]
+     */
+    void syncXBongBongForRecord_all(LocalDate date);
+
+    /**
+     * 同步销售部门人员 [服务销帮帮]
+     */
+    void syncBusinessUserInfo();
+
+    /**
+     * 酷应用数据 [定时推送, 非轮询不缓存]
+     */
+    Map applicationData(String tDate);
+
+    /**
+     * 酷应用消息卡片推送
+     */
+    void sendCardMessage(String tDate, String... teams);
+}

+ 361 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/service/impl/CPImplClient.java

@@ -0,0 +1,361 @@
+package com.malk.cloudpure.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.cloudpure.service.CPClient;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDInterActiveCard;
+import com.malk.server.xbongbong.XBBConf;
+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_Extension;
+import com.malk.service.dingtalk.DDClient_Log;
+import com.malk.service.xbongbong.XBBClient;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilNumber;
+import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class CPImplClient implements CPClient {
+
+    @Autowired
+    private YDClient ydClient;
+
+    @Autowired
+    private YDService ydService;
+
+    /**
+     * 创建每日数据
+     */
+    @Synchronized
+    @Override
+    public void upsertDailyRecord(String userId, String tDate, Map formData) {
+        LocalDate nowDate = UtilDateTime.parseLocalDate(tDate);
+        YDParam ydParam = YDParam.builder()
+                .formUuid("FORM-NT766881IOWD4HLW6372V8H6WS9K2OZJV74ML7")
+                .searchFieldJson(JSON.toJSONString(UtilMap.map("employeeField_llw07mxm, textField_lm480aco", userId, tDate)))
+                .build();
+        //  查询是否存在
+        List<Map> dataList = (List<Map>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form).getData();
+        if (dataList.size() > 0) {
+            // OA审批连接器, 累计数字
+            if (formData.containsKey("type")) {
+                String compId = formData.get("type").toString();
+                int num = UtilMap.getInt(((Map) dataList.get(0).get("formData")), compId);
+                formData.put(compId, num + 1);
+            }
+            ydParam.setFormInstanceId(dataList.get(0).get("formInstanceId").toString());
+            ydParam.setUpdateFormDataJson(JSON.toJSONString(formData));
+            ydClient.operateData(ydParam, YDConf.FORM_OPERATION.update);
+            return;
+        }
+        // 日期格式记录
+        Map dateForm = UtilMap.map("employeeField_llw07mxm, dateField_llw07myh, textField_lm480aco, textField_lm480acu, textField_lm480acv, textField_lmdjpnoa",
+                Arrays.asList(userId), UtilDateTime.getLocalDateTimeTimeStamp(LocalDateTime.of(nowDate, LocalTime.MIN)), UtilDateTime.formatLocalDate(nowDate), UtilDateTime.formatLocal(nowDate, "yyyy-MM"), UtilDateTime.formatLocalQuarter(nowDate), userId);
+        formData.putAll(dateForm);
+        ydParam.setFormDataJson(JSON.toJSONString(formData));
+        ydClient.operateData(ydParam, YDConf.FORM_OPERATION.create);
+    }
+
+    @Autowired
+    private DDClient_Log ddClient_log;
+
+    @Autowired
+    private DDClient ddClient;
+
+    /**
+     * 同步日报数据
+     */
+    @Override
+    public void syncDingTalkLogForRecord(String sDate) {
+        // 模板名称 + 取值字段
+        List<Map<String, String>> names = Arrays.asList(UtilMap.map("template, category", "日报-组织团队, 有效跟进记录数量"), UtilMap.map("template, category", "业务数字化团队-日报, 当天联系客户数量"));
+        long sStart = UtilDateTime.parseDateTime(sDate + " 00:00:00").getTime();
+        long sEnd = UtilDateTime.parseDateTime(sDate + " 23:59:59").getTime();
+        for (Map name : names) {
+            List<Map> dataList = ddClient_log.reportList(ddClient.getAccessToken(), sStart, sEnd, UtilMap.map("template_name", name.get("template")));
+            for (Map data : dataList) {
+                List<Map> contents = (List<Map>) data.get("contents");
+                Optional optional = contents.stream().filter(item -> name.get("category").equals(item.get("key")) || "".equals(item.get("key"))).findAny();
+                if (optional.isPresent()) {
+                    String userId = data.get("creator_id").toString();
+                    // 有效跟进
+                    String value = String.valueOf(((Map) optional.get()).get("value"));
+                    this.upsertDailyRecord(userId, sDate, UtilMap.map("numberField_llw07mxo", value));
+                }
+            }
+        }
+    }
+
+    @Autowired
+    private XBBClient xbbClient;
+
+    // 客户数
+    private int _queryCustomerList(String url, String userId) {
+
+        //xbbClient.testDefine("", 1, 100); // 客户
+        // 查询条件: formId 钉钉客户:864836 Teambition客户:1003094, 负责人 ownerId [ include 包含任一, in 等于任一 ]
+        List<Map> conditions = Arrays.asList(
+                XBBConf.getConditionMap("ownerId", "in", userId));
+        Map data1 = xbbClient.getDataResult(url, 864836, conditions, UtilMap.map("pageSize", 1));
+        Map data2 = xbbClient.getDataResult(url, 1003094, conditions, UtilMap.map("pageSize", 1));
+        // 客户数合计
+        return UtilMap.getInt(data1, "totalCount") + UtilMap.getInt(data2, "totalCount");
+    }
+
+    // 跟进记录
+    private List<Map> _queryFollowupList(String url, String userId, long start, long end) {
+
+        //xbbClient.testDefine("", 1, 501); // 跟进记录
+        // 查询条件: formId 864839, 创建时间 addTime, 创建人 creatorId
+        List<Map> dataList = xbbClient.getDataList(url, 864839, Arrays.asList(
+                XBBConf.getConditionMap("creatorId", "equal", userId),
+                XBBConf.getConditionMap("addTime", "range", start, end)), null);
+        // 数据筛选: 跟进内容 text_6, 跟进方式 text_4 [上门拜访 1, 远程会议 484f3265-77a4-5783-9c17-dbecfbf1b816]
+        return dataList.stream().map(item -> ((Map) item.get("data"))).collect(Collectors.toList());
+    }
+
+    // 销售机会
+    private List<Map> _queryOpportunityList(String url, String userId, long start, long end) {
+
+        //xbbClient.testDefine("", 1, 301); // 销售机会
+        // 查询条件: formId 864841, 创建时间 addTime, 创建人 creatorId
+        List<Map> dataList = xbbClient.getDataList(url, 864839, Arrays.asList(
+                XBBConf.getConditionMap("creatorId", "equal", userId),
+                XBBConf.getConditionMap("addTime", "range", start, end)), null);
+        return dataList.stream().map(item -> ((Map) item.get("data"))).collect(Collectors.toList());
+    }
+
+    // 合同订单
+    private List<Map> _queryContractList(String url, String userId, long start, long end) {
+
+        //xbbClient.testDefine("", 1, 201); // 合同订单
+        // 查询条件: formId 钉钉部门:864843 Teambition部门合同:1283232, , 创建时间 addTime, 创建人 creatorId
+        List<Map> conditions = Arrays.asList(
+                XBBConf.getConditionMap("creatorId", "equal", userId),
+                XBBConf.getConditionMap("addTime", "range", start, end));
+        List<Map> dataList = xbbClient.getDataList(url, 864843, conditions, null);
+        List<Map> dataList2 = xbbClient.getDataList(url, 1283232, conditions, null);
+        dataList.addAll(dataList2);
+        // 数据筛选: 合同金额 num_1
+        return dataList.stream().map(item -> ((Map) item.get("data"))).collect(Collectors.toList());
+    }
+
+    // 回款单
+    private List<Map> _queryPaymentSheetList(String url, String userId, long start, long end) {
+
+        //xbbClient.testDefine("", 1, 702); // 回款单
+        // 查询条件: formId 864859, 创建时间 addTime, 创建人 creatorId
+        List<Map> dataList = xbbClient.getDataList(url, 864859, Arrays.asList(
+                XBBConf.getConditionMap("creatorId", "equal", userId),
+                XBBConf.getConditionMap("addTime", "range", start, end)), null);
+        // 数据筛选: 回款金额 num_1
+        return dataList.stream().map(item -> ((Map) item.get("data"))).collect(Collectors.toList());
+    }
+
+    /**
+     * 同步销帮帮跟进记录, 商机, 合同, 回款
+     */
+    @Override
+    public void syncXBongBongForRecord(String userId, long start, long end) {
+
+        // 用户数
+        int totalCount = _queryCustomerList(XBBConf.API_LIST_customer, userId);
+
+        // prd 跟进记录: 远程+上门, 跟进记录数(10字以上)
+        List<Map> dataList1 = _queryFollowupList(XBBConf.API_LIST_communicate, userId, start, end);
+        int project = dataList1.stream().filter(item ->
+                Arrays.asList("1", "484f3265-77a4-5783-9c17-dbecfbf1b816").contains(item.get("text_4"))
+        ).collect(Collectors.toList()).size();
+        int followup = dataList1.stream().filter(item ->
+                UtilMap.getString(item, "text_6").length() >= 10
+        ).collect(Collectors.toList()).size();
+
+        // 销售机会: 新建商机数 & 商机金额
+        List<Map> dataList2 = _queryOpportunityList(XBBConf.API_LIST_opportunity, userId, start, end);
+        int opportunity = dataList2.size();
+        double budget = dataList2.stream().mapToDouble(item -> UtilMap.getFloat(item, "num_1"))
+                .reduce(0.f, (a, b) -> a + b);
+        String budgetT = UtilNumber.formatPrecisionString(budget);
+
+        // 合同订单: 成交合同额
+        List<Map> dataList3 = _queryContractList(XBBConf.API_LIST_contract, userId, start, end);
+        double amount = dataList3.stream().mapToDouble(item -> UtilMap.getFloat(item, "num_1"))
+                .reduce(0.f, (a, b) -> a + b);
+        String amountT = UtilNumber.formatPrecisionString(amount);
+
+        // 回款单: 回款额
+        List<Map> dataList4 = _queryPaymentSheetList(XBBConf.API_LIST_paymentSheet, userId, start, end);
+        double payment = dataList4.stream().mapToDouble(item -> UtilMap.getFloat(item, "num_1"))
+                .reduce(0.f, (a, b) -> a + b
+                );
+        String paymentT = UtilNumber.formatPrecisionString(payment);
+
+        // 销帮帮统计
+        this.upsertDailyRecord(userId, UtilDateTime.formatDate(new Date(start * 1000L)), UtilMap.map("numberField_llw07mxt, numberField_llw07mxu, numberField_llw07mxv, numberField_lmee0oez, numberField_llw07myc, numberField_llw07my7, numberField_lmee0oey", project, followup, opportunity, budgetT, amountT, paymentT, totalCount));
+    }
+
+    /**
+     * 同步销帮帮跟进记录, 商机, 合同, 回款 - [全量]
+     */
+    @Override
+    public void syncXBongBongForRecord_all(LocalDate date) {
+
+        // 查询时间条件为秒级
+        long start = UtilDateTime.getLocalDateTimeTimeStamp(LocalDateTime.of(date, LocalTime.MAX.MIN)) / 1000;
+        long end = UtilDateTime.getLocalDateTimeTimeStamp(LocalDateTime.of(date, LocalTime.MAX)) / 1000;
+        // 宜搭存量人员数据
+        YDParam ydParam = YDParam.builder()
+                .formUuid("FORM-FDA66N818M1EBF27B7Z5H5JMK3562D8F7FDMLE")
+                .build();
+        List<Map> formList = (List<Map>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form).getData();
+        List<String> tUserIds = formList.stream().map(itme -> ((Map) itme.get("formData")).get("textField_lmdgtqop").toString())
+                .collect(Collectors.toList());
+        for (String userId : tUserIds) {
+            syncXBongBongForRecord(userId, start, end);
+        }
+    }
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    /**
+     * 同步销售部门人员 [服务销帮帮]
+     */
+    @Override
+    public void syncBusinessUserInfo() {
+
+        // 宜搭存量人员数据
+        YDParam ydParam = YDParam.builder()
+                .formUuid("FORM-FDA66N818M1EBF27B7Z5H5JMK3562D8F7FDMLE")
+                .build();
+        List<Map> formList = (List<Map>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form).getData();
+        List<String> tUserIds = formList.stream().map(itme -> ((Map) itme.get("formData")).get("textField_lmdgtqop").toString())
+                .collect(Collectors.toList());
+
+        // 钉钉事业部 - 商业团队: 94295408 [钉钉事业部 75385376] 客户成功团队: 844485110
+        List<Map> deptList = ddClient_contacts.listSubDepartmentDetail(ddClient.getAccessToken(), 94295408L);
+        deptList.add(ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), 844485110L));
+        for (Map deptInfo : deptList) {
+            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), UtilMap.getLong(deptInfo, "dept_id"));
+            for (String userId : userIds) {
+                if (tUserIds.contains(userId)) {
+                    continue;
+                }
+                // 增量更新宜搭人员数据
+                Map userInfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
+                ydClient.operateData(YDParam.builder()
+                        .formUuid("FORM-FDA66N818M1EBF27B7Z5H5JMK3562D8F7FDMLE")
+                        .formDataJson(JSON.toJSONString(UtilMap.map("employeeField_lmdf7uib, textField_lmdf7uic, textField_lmdgtqop, textField_lmio2az4, textField_lmio2az5, radioField_lmiojmqr", Arrays.asList(userId), deptInfo.get("name"), userId, userInfo.get("name"), userInfo.get("avatar"), "是")))
+                        .build(), YDConf.FORM_OPERATION.create);
+            }
+        }
+    }
+
+    /**
+     * 酷应用数据
+     */
+    @Override
+    public Map applicationData(String tDate) {
+
+        List<Map> dataListRank = new ArrayList<>();
+        List<Map> dataListDaily = new ArrayList<>();
+        // 人员档案
+        List<Map> userList = ydService.queryDataList_FormData("FORM-FDA66N818M1EBF27B7Z5H5JMK3562D8F7FDMLE", UtilMap.map("radioField_lmiojmqr", "是"));
+        // 每日数据
+        List<Map> dailyList = ydService.queryDataList_FormData("FORM-NT766881IOWD4HLW6372V8H6WS9K2OZJV74ML7", UtilMap.map("textField_lm480aco", tDate));
+        // 排名数据
+        List<Map> rankList = ydService.queryDataList_FormData("FORM-TD966Z81MI1EO8BB8K3J14HDFOS42K2W79EMLF", UtilMap.map("textField_lm480acu", UtilDateTime.format(UtilDateTime.parseDate(tDate), "yyyy-MM")));
+        for (Map data : userList) {
+            float value = 0;
+            if ("客户成功团队".equals(data.get("textField_lmdf7uic"))) {
+                continue;
+            }
+            Map userInfo = UtilMap.map("icon, name", data.get("textField_lmio2az5"), data.get("textField_lmio2az4"));
+            // 跟进记录排名
+            Optional optional = dailyList.stream().filter(item -> data.get("textField_lmdgtqop").equals(item.get("textField_lmdjpnoa"))).findAny();
+            if (optional.isPresent()) {
+                Map form = (Map) optional.get();
+                value = UtilMap.getFloat(form, "numberField_llw07mxu"); // 跟进记录
+            }
+            dataListDaily.add(UtilMap.map("value, appItem, team", value, userInfo, data.get("textField_lmdf7uic")));
+            // 完成率排名
+            optional = rankList.stream().filter(item -> data.get("textField_lmdgtqop").equals(item.get("textField_lmdjpnoa"))).findAny();
+            if (optional.isPresent()) {
+                Map form = (Map) optional.get();
+                value = UtilMap.getFloat(form, "numberField_lme99e3l"); // 季度排名
+            }
+            dataListRank.add(UtilMap.map("value, appItem, team", value, userInfo, data.get("textField_lmdf7uic")));
+        }
+        // 排序两种形式
+        Collections.sort(dataListDaily, Comparator.comparingDouble(o -> UtilMap.getFloat(o, "value")));
+        Collections.reverse(dataListDaily);
+        Collections.sort(dataListRank, (o1, o2) -> (int) (UtilMap.getFloat(o2, "value") - UtilMap.getFloat(o1, "value")));
+        // 酷应用 table 组件必须要返回表头, 否则数据不能解析
+        List<Map> meteList = new ArrayList<>();
+        meteList.add(UtilMap.map("aliasName, dataType, alias, weight", "销售", "MICROAPP", "appItem", "30"));
+        meteList.add(UtilMap.map("aliasName, dataType, alias, weight", "排名", "STRING", "value", "20"));
+
+        return UtilMap.map("daily, rank, meta", dataListRank, dataListDaily, meteList);
+    }
+
+    @Autowired
+    private DDClient_Extension ddClient_extension;
+
+    @Autowired
+    private DDConf ddConf;
+
+    /**
+     * 酷应用消息卡片推送
+     */
+    @Override
+    public void sendCardMessage(String tDate, String... chatNames) {
+        
+        // 推送群配置表
+        List<Map> chatIdList = ydService.queryDataList_FormData("FORM-WV866IC1ZMDEQAWTAXYSFBJL2XMM2QY4WRPML0", null);
+        // 全量查询, 分团队推送
+        Map data = applicationData(tDate);
+        for (String name : chatNames) {
+            Optional optional = chatIdList.stream().filter(item -> name.equals(item.get("textField_lmprwfmw"))).findAny();
+            if (optional.isPresent()) {
+                _sendCardMessage(data, (Map) optional.get());
+            }
+        }
+    }
+
+    /// 发送卡片消息, 微应用是否配置会话推送不强制
+    void _sendCardMessage(Map data, Map<String, String> chat) {
+
+        // 酷应用卡片模板ID
+        String cardTemplateId = "bcb35ab8-e1cd-49ba-9081-950357dcf910.schema";
+        // 微应用配置机器人编码
+        String robotCode = ddConf.getRobotCode();
+        // 群内添加酷应用后, 酷应用订阅事件回调群ID
+        String openConversationId = chat.get("textField_lmprwfmx");
+        Map map = UtilMap.map("meta", data.get("meta"));
+        if ("是".equals(chat.get("radioField_lmprwfmz"))) {
+            map.put("rank", UtilMap.getList(data, "rank").stream().filter(item -> chat.get("textField_lmprwfmw").equals(((Map) item).get("team"))).collect(Collectors.toList()));
+            map.put("daily", UtilMap.getList(data, "daily").stream().filter(item -> chat.get("textField_lmprwfmw").equals(((Map) item).get("team"))).collect(Collectors.toList()));
+        } else {
+            map.putAll(data);
+        }
+        Map cardData = DDInterActiveCard.formCardDataForTable(map, "rank", "daily");
+        Map extInfo = UtilMap.map("cardOptions", UtilMap.map("supportForward", true)); // 允许转发
+        extInfo.put("atOpenIds", UtilMap.map("key", UtilMap.map("**@ALL**", "**@ALL**"))); // @所有人
+        ddClient_extension.sendInteractiveCards(ddClient.getAccessToken(), cardTemplateId, String.valueOf(new Date().getTime()), 1, robotCode, "", openConversationId, cardData, extInfo);
+    }
+}

+ 14 - 10
mjava-cloudpure/src/main/resources/application-dev.yml

@@ -49,18 +49,19 @@ logging:
 
 # dingtalk
 dingtalk:
-  agentId: 1963716187
-  appKey: ding8qyulwwmad6j7k6c
-  appSecret: e_hRHuubw-Xi0OuPJOYdXSElVzOC0IPgMrHQTBuAM9BqW-DFnrcsSyBHi7Me3xSv
-  corpId: ding321c72787fffc78b35c2f4657eb6378f
-  aesKey:
-  token:
-  operator: "095358016629044412"   # 牧语[开头需要转一下字符串], OA管理员账号
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey: 6kaXxySzRcBlu8nvEJji7bg73rSX5F9ieXdVunqWvXc
+  token: Mv4daF2LqpmoIOe5XMeipdl0YT
+  robotCode: dingozv6fzkpqkiupd3d
+  operator: "16608972969409067"   # 牧语[开头0, 需要转一下字符串]
 
 # aliwork
 aliwork:
-  appType: APP_YH7W0E5637YUBU5UJ837
-  systemToken: IC766WA11EW8BMOPBEZZMBA4MPUQ214JDL7FLC9
+  appType: APP_YJO0WE6WJ3YUO62PEB2F
+  systemToken: JS766JD1CQUDWUMTAPE6AD49XWBI2P7LT74MLH6
 
 # xbongbong
 xbongbong:
@@ -69,4 +70,7 @@ xbongbong:
   userId: 102314374732747224 # 汐瑶
   callbackToken:
 
-
+# vika
+vika:
+  apiToken: uskk7kCIZZtZf06H8RCjLN2
+  spaceId: spcEX0JJDTVt0

+ 18 - 12
mjava-cloudpure/src/main/resources/application-prod.yml

@@ -16,30 +16,36 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: cp-root@2022++
-    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect
 
 # dingtalk
 dingtalk:
-  agentId: 1963716187
-  appKey: ding8qyulwwmad6j7k6c
-  appSecret: e_hRHuubw-Xi0OuPJOYdXSElVzOC0IPgMrHQTBuAM9BqW-DFnrcsSyBHi7Me3xSv
-  corpId: ding321c72787fffc78b35c2f4657eb6378f
-  aesKey:
-  token:
-  operator: "095358016629044412"   # 牧语[开头需要转一下字符串], OA管理员账号
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey: 6kaXxySzRcBlu8nvEJji7bg73rSX5F9ieXdVunqWvXc
+  token: Mv4daF2LqpmoIOe5XMeipdl0YT
+  robotCode: dingozv6fzkpqkiupd3d
+  operator: "16608972969409067"   # 牧语[开头0, 需要转一下字符串]
 
 # aliwork
 aliwork:
-  appType: APP_YH7W0E5637YUBU5UJ837
-  systemToken: IC766WA11EW8BMOPBEZZMBA4MPUQ214JDL7FLC9
+  appType: APP_YJO0WE6WJ3YUO62PEB2F
+  systemToken: JS766JD1CQUDWUMTAPE6AD49XWBI2P7LT74MLH6
 
 # xbongbong
 xbongbong:
   corpid: dingcc1b1ffad0d5ca1d
   token: f760ea3d154e45c839b2169f09b76f23
   userId: 102314374732747224 # 汐瑶
-  callbackToken:
+  callbackToken:
+
+# vika
+vika:
+  apiToken: uskk7kCIZZtZf06H8RCjLN2
+  spaceId: spcEX0JJDTVt0

+ 14 - 10
mjava-cloudpure/target/classes/application-dev.yml

@@ -49,18 +49,19 @@ logging:
 
 # dingtalk
 dingtalk:
-  agentId: 1963716187
-  appKey: ding8qyulwwmad6j7k6c
-  appSecret: e_hRHuubw-Xi0OuPJOYdXSElVzOC0IPgMrHQTBuAM9BqW-DFnrcsSyBHi7Me3xSv
-  corpId: ding321c72787fffc78b35c2f4657eb6378f
-  aesKey:
-  token:
-  operator: "095358016629044412"   # 牧语[开头需要转一下字符串], OA管理员账号
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey: 6kaXxySzRcBlu8nvEJji7bg73rSX5F9ieXdVunqWvXc
+  token: Mv4daF2LqpmoIOe5XMeipdl0YT
+  robotCode: dingozv6fzkpqkiupd3d
+  operator: "16608972969409067"   # 牧语[开头0, 需要转一下字符串]
 
 # aliwork
 aliwork:
-  appType: APP_YH7W0E5637YUBU5UJ837
-  systemToken: IC766WA11EW8BMOPBEZZMBA4MPUQ214JDL7FLC9
+  appType: APP_YJO0WE6WJ3YUO62PEB2F
+  systemToken: JS766JD1CQUDWUMTAPE6AD49XWBI2P7LT74MLH6
 
 # xbongbong
 xbongbong:
@@ -69,4 +70,7 @@ xbongbong:
   userId: 102314374732747224 # 汐瑶
   callbackToken:
 
-
+# vika
+vika:
+  apiToken: uskk7kCIZZtZf06H8RCjLN2
+  spaceId: spcEX0JJDTVt0

+ 18 - 12
mjava-cloudpure/target/classes/application-prod.yml

@@ -16,30 +16,36 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: cp-root@2022++
-    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect
 
 # dingtalk
 dingtalk:
-  agentId: 1963716187
-  appKey: ding8qyulwwmad6j7k6c
-  appSecret: e_hRHuubw-Xi0OuPJOYdXSElVzOC0IPgMrHQTBuAM9BqW-DFnrcsSyBHi7Me3xSv
-  corpId: ding321c72787fffc78b35c2f4657eb6378f
-  aesKey:
-  token:
-  operator: "095358016629044412"   # 牧语[开头需要转一下字符串], OA管理员账号
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey: 6kaXxySzRcBlu8nvEJji7bg73rSX5F9ieXdVunqWvXc
+  token: Mv4daF2LqpmoIOe5XMeipdl0YT
+  robotCode: dingozv6fzkpqkiupd3d
+  operator: "16608972969409067"   # 牧语[开头0, 需要转一下字符串]
 
 # aliwork
 aliwork:
-  appType: APP_YH7W0E5637YUBU5UJ837
-  systemToken: IC766WA11EW8BMOPBEZZMBA4MPUQ214JDL7FLC9
+  appType: APP_YJO0WE6WJ3YUO62PEB2F
+  systemToken: JS766JD1CQUDWUMTAPE6AD49XWBI2P7LT74MLH6
 
 # xbongbong
 xbongbong:
   corpid: dingcc1b1ffad0d5ca1d
   token: f760ea3d154e45c839b2169f09b76f23
   userId: 102314374732747224 # 汐瑶
-  callbackToken:
+  callbackToken:
+
+# vika
+vika:
+  apiToken: uskk7kCIZZtZf06H8RCjLN2
+  spaceId: spcEX0JJDTVt0

+ 41 - 56
mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java

@@ -9,15 +9,10 @@ import com.malk.fengkaili.service.FKLService;
 import com.malk.server.common.McException;
 import com.malk.server.common.McPage;
 import com.malk.server.common.McR;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.service.dingtalk.DDClient;
-import com.malk.service.dingtalk.DDClient_Attendance;
-import com.malk.service.dingtalk.DDClient_Contacts;
-import com.malk.utils.UtilExcel;
-import com.malk.utils.UtilList;
-import com.malk.utils.UtilMap;
-import com.malk.utils.UtilMc;
+import com.malk.service.dingtalk.DDService;
+import com.malk.utils.*;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -25,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -37,36 +33,24 @@ import java.util.stream.Collectors;
 @RequestMapping
 public class FKLController {
 
-    @Autowired
-    private DDClient ddClient;
-
-    @Autowired
-    private DDClient_Attendance ddClient_attendance;
-
-    @Autowired
-    private DDClient_Contacts ddClient_contacts;
-
     @Autowired
     private FKLService fklService;
 
-    @Autowired
-    private DDConf ddConf;
-
     /**
      * 同步用户 & 部门
      */
     @PostMapping("syncUserInfo")
     McR syncUserInfo() {
-
         fklService.syncUserInfo();
         return McR.success();
     }
 
-
     /// 考勤汇总
-    private McPage _getAttendanceList(Map data, List<String> days) {
-        log.info("考勤汇总, {}", data);
+    private McPage _getAttendanceList(Map data, List<String> days, HttpServletRequest request) {
+        log.info("考勤汇总, {}", UtilServlet.getHeaders(request).get("authorization"));
+        log.info("考勤汇总, {}", UtilServlet.getHeaders(request));
         McException.assertParamException_Null(data, "startTime", "endTime");
+        McException.assertAccessException(StringUtils.isBlank(UtilServlet.getHeaders(request).get("authorization")), "该账户无操作权限");
 
         List<Long> dpetIds = (List<Long>) data.get("deptId");
         // 基于用户分页
@@ -83,30 +67,28 @@ public class FKLController {
      * 查询考勤汇总
      */
     @PostMapping("queryAttendanceList")
-    McR queryAttendanceList(@RequestBody Map data) {
-
-        return McR.success(_getAttendanceList(data, null));
+    McR queryAttendanceList(@RequestBody Map data, HttpServletRequest request) {
+        return McR.success(_getAttendanceList(data, null, request));
     }
 
     /**
      * 查询考勤汇总 [天]
      */
     @PostMapping("queryAttendanceDays")
-    McR queryAttendanceDays(@RequestBody Map data) {
+    McR queryAttendanceDays(@RequestBody Map data, HttpServletRequest request) {
         List<String> days = new ArrayList<>();
-
-        return McR.success(UtilMap.map("page, prop", _getAttendanceList(data, days), days));
+        return McR.success(UtilMap.map("page, prop", _getAttendanceList(data, days, request), days));
     }
 
     /**
      * 导出考勤汇总
      */
     @PostMapping("exportAttendanceList")
-    void exportAttendanceList(@RequestBody Map data, HttpServletResponse response) {
+    void exportAttendanceList(@RequestBody Map data, HttpServletResponse response, HttpServletRequest request) {
 
         data.put("page", 1);
         data.put("size", Integer.MAX_VALUE);
-        List<Map> dataList = _getAttendanceList(data, null).getList();
+        List<Map> dataList = _getAttendanceList(data, null, request).getList();
         //  获取出现最多次作为法定应出勤天数, 考勤应出勤天数和班组 + 人员挂钩 [班次详情应出勤不准确]
         float workdays = (Float) UtilList.maxFrequencyObject(dataList.stream().map(item -> {
             float val = 0.f;
@@ -119,7 +101,7 @@ public class FKLController {
         String range = ("核算周期: " + data.get("startTime").toString().split(" ")[0].replace("-", ".") + "-" + data.get("endTime").toString().split(" ")[0].replace("-", "."));
         String attendance = workdays + "天*8小时*60分钟=";
         Map dataMain = UtilMap.map("核算周期, 应出勤天数, 应出勤分钟", range, attendance, workdays * 8f * 60f);
-
+        dataMain.put("date", UtilDateTime.format(UtilDateTime.parseDateTime(UtilMap.getString(data, "endTime")), "yyyy年MM月"));
         UtilExcel.exportMapAndListByTemplate(response, dataMain, dataList, Map.class, "月度汇总", "Template_month.xlsx");
     }
 
@@ -127,43 +109,46 @@ public class FKLController {
      * 导出考勤汇总 [天]
      */
     @PostMapping("exportAttendanceDays")
-    void exportAttendanceDays(@RequestBody Map data, HttpServletResponse response) {
-
+    void exportAttendanceDays(@RequestBody Map data, HttpServletResponse response, HttpServletRequest request) {
         data.put("page", 1);
         data.put("size", Integer.MAX_VALUE);
 
-//        List<String> days = new ArrayList<>();
-//        List<Map> dataList = _getAttendanceList(data, days).getList();
-//        List<String> columnHeader = UtilList.asList("序号", "姓名", "工号", "部门", "出勤天数", "年假(天)", "事假(小时)", "调休(小时)", "产假(天)", "陪产假(天)", "婚假(天)", "丧假(天)", "哺乳假(小时)", "病假(天)", "育儿假(天)");
-//        List<String> columnProp = UtilList.asList("序号", "员工姓名", "员工工号", "所属部门", "出勤天数", "年假", "事假", "调休", "产假", "陪产假", "婚假", "丧假", "哺乳假", "病假", "育儿假");
-//        // 动态列头名称
-//        columnHeader.addAll(4, days);
-//        columnProp.addAll(4, days);
-//
-//        UtilExcel.builder().heardList(columnHeader.toArray(new String[0])).heardKey(columnProp.toArray(new String[0]))
-//                .data(dataList).fileName("月度明细").build()
-//                .exportExcelByPoi(response);
-
         // 动态表头模板导出
         List<String> days = new ArrayList<>();
-        List<Map> dataList = _getAttendanceList(data, days).getList();
+        List<Map> dataList = _getAttendanceList(data, days, request).getList();
         Map dataMain = new HashMap();
         days.forEach(UtilMc.consumerWithIndex((item, index) -> {
             dataMain.put("day" + (index + 1), item);
         }));
+        dataMain.put("date", UtilDateTime.format(UtilDateTime.parseDateTime(UtilMap.getString(data, "endTime")), "yyyy年MM月"));
         UtilExcel.exportMapAndListByTemplate(response, dataMain, dataList, Map.class, "月度明细", "Template_days.xlsx");
+    }
+
+    @Autowired
+    private DDService ddService;
 
+    public static String jsApi_nonceStr = "720F4HNA579C0ZHEDR";
+    public static String jsApi_url = "http://localhost:8001";
+
+    /**
+     * jsApi 注册
+     */
+    @PostMapping("register")
+    McR register() {
+        return McR.success(ddService.registerJsApi(jsApi_url, jsApi_nonceStr));
+    }
+
+    /**
+     * jsApi 免登
+     */
+    @PostMapping("user/code")
+    McR userCodeAuth(@RequestBody Map<String, String> data) {
+        McException.assertParamException_Null(data, "code");
+        return McR.success(ddService.getUserInfoByCode(data.get("code")));
     }
 
     @PostMapping("test")
     McR test() {
-
-//        return McR.success(ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), 711600043L));
-//        return McR.success(fklService.queryUserInfos(1, Integer.MAX_VALUE, null, null));
-//        return McR.success(ddClient_attendance.getAttendanceShiftDetail(ddClient.getAccessToken(), ddConf.getOperator(), "237885247"));
-//        return McR.success(ddClient_attendance.getAttendanceGroupSearch(ddClient.getAccessToken(), ddConf.getOperator(), "自动考勤"));
-//        return McR.success(ddClient_attendance.getAttendanceGroupDetail(ddClient.getAccessToken(), ddConf.getOperator(), "343575114"));
-//        return McR.success(ddClient_contacts.getLeaveEmployeeRecords(ddClient.getAccessToken(), UtilDateTime.parseDate("2023-07-01"), null));
-        return McR.success(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), "0953580166811961653"));
+        return McR.success();
     }
 }

+ 35 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/schedule/FKLScheduleTask.java

@@ -0,0 +1,35 @@
+package com.malk.fengkaili.schedule;
+
+import com.malk.fengkaili.service.FKLService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class FKLScheduleTask {
+
+    @Autowired
+    private FKLService fklService;
+
+    /**
+     * 每天凌晨4点同步
+     */
+    @Scheduled(cron = "* * 4 * * ? ")
+    public void syncDingTalkFailedList() {
+        try {
+            fklService.syncUserInfo();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 5 - 5
mjava-fengkaili/src/main/java/com/malk/fengkaili/service/impl/FKLImplService.java

@@ -266,7 +266,7 @@ public class FKLImplService implements FKLService {
                             // 兼容早退和迟到情况下, 还存在请假情况
                             if (result.contains("迟到") && !result.contains("补卡申请")) {
                                 type = "迟到"; // 迟到状态标记
-                                float exception_duration = Float.valueOf((result.split(",")[0].split("分钟")[0].replace("上班迟到", "")));
+                                float exception_duration = Float.valueOf((result.split(",")[0].split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
                                 if (exception_duration >= 180f) {
                                     // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
                                     day_1 = "迟到";
@@ -286,8 +286,8 @@ public class FKLImplService implements FKLService {
                             day_2 = "";
                             // 请假 & 出差
                             for (String status : result.split(",")) {
-                                /// 过滤异常情况 &  未打卡判定为status, 非result
-                                if (status.contains("补卡申请") || status.contains("正常") || status.equals("未打卡")) {
+                                /// 过滤异常情况 &  未打卡判定为status, 非result & 外勤又外出情况
+                                if (status.contains("补卡申请") || status.contains("正常") || status.equals("未打卡") || status.contains("外勤")) {
                                     continue;
                                 }
                                 if (status.contains("缺卡") || status.equals("未打卡") || status.contains("迟到") || status.contains("早退")) {
@@ -303,7 +303,7 @@ public class FKLImplService implements FKLService {
                                     if (status.contains("迟到") || status.contains("早退")) {
                                         if (status.contains("迟到")) {
                                             type = "迟到"; // 迟到状态标记
-                                            float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("上班迟到", "")));
+                                            float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
                                             if (exception_duration >= 180f) {
                                                 // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
                                                 day_1 = "迟到";
@@ -459,7 +459,7 @@ public class FKLImplService implements FKLService {
             if (ObjectUtils.isNotEmpty(po.getHiredDate()) && UtilDateTime.beforeAndEqual(UtilDateTime.parseDateTime(start), po.getHiredDate()) && UtilDateTime.afterAndEqual(UtilDateTime.parseDateTime(end), po.getHiredDate())) {
                 Optional optional = Arrays.stream(attendanceInfo.get("考勤结果").toString().split("; ")).filter(item -> item.contains("迟到") && item.contains(UtilDateTime.formatDate(po.getHiredDate()))).findAny();
                 if (optional.isPresent()) {
-                    exception_duration = Float.valueOf((optional.get().toString().split("分钟")[0].replace("上班迟到", "")));
+                    exception_duration = Float.valueOf((optional.get().toString().split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
                     attendanceInfo.put("迟到次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "迟到次数") - 1));
                 }
                 attendanceInfo.put("考勤结果", "入职日期" + UtilDateTime.formatDate(po.getHiredDate()) + "; " + attendanceInfo.get("考勤结果"));

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

@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: mu123
-    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
     # 主库
     primary:
       username: root

+ 2 - 0
mjava-fengkaili/src/main/resources/application-prod.yml

@@ -21,6 +21,8 @@ spring:
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect
+    hibernate:
+      ddl-auto: none
 
 # dingtalk
 dingtalk:

BIN
mjava-fengkaili/src/main/resources/templates/Template_days.xlsx


BIN
mjava-fengkaili/src/main/resources/templates/Template_month.xlsx


+ 2 - 2
mjava-fengkaili/target/classes/application-dev.yml

@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: mu123
-    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
     # 主库
     primary:
       username: root

+ 2 - 0
mjava-fengkaili/target/classes/application-prod.yml

@@ -21,6 +21,8 @@ spring:
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect
+    hibernate:
+      ddl-auto: none
 
 # dingtalk
 dingtalk:

+ 9 - 85
mjava-gewu/src/main/java/com/malk/gewu/controller/GWController.java

@@ -4,26 +4,15 @@ package com.malk.gewu.controller;
  * 错误抛出与拦截详见 CatchException
  */
 
-import cn.hutool.json.JSONUtil;
-import com.alibaba.fastjson.JSON;
-import com.malk.server.aliwork.YDConf;
-import com.malk.server.aliwork.YDParam;
+import com.malk.gewu.service.GWService;
 import com.malk.server.common.McR;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.service.aliwork.YDClient;
-import com.malk.service.dingtalk.DDClient;
-import com.malk.service.dingtalk.DDClient_Contacts;
-import com.malk.service.dingtalk.DDClient_Personnel;
 import com.malk.utils.UtilFile;
-import com.malk.utils.UtilMap;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.*;
-
 @Slf4j
 @RestController
 @RequestMapping
@@ -31,19 +20,7 @@ public class GWController {
 
 
     @Autowired
-    private DDClient ddClient;
-
-    @Autowired
-    private DDConf ddConf;
-
-    @Autowired
-    private DDClient_Contacts ddClient_contacts;
-
-    @Autowired
-    private DDClient_Personnel ddClient_personnel;
-
-    @Autowired
-    private YDClient ydClient;
+    private GWService gwService;
 
     /**
      * 同步花名册信息
@@ -51,62 +28,7 @@ public class GWController {
     @PostMapping("sync")
     McR syncRoster() {
 
-        // 花名册元数据
-        List<Map> metaList = (List<Map>) UtilFile.readJsonObjectFromFile("static/json/personnel"); // 本地匹配了宜搭组件ID
-//        List<Map> metaList = ddClient_personnel.getPersonnelMeta(ddClient.getAccessToken(), ddConf.getAgentId());
-        // 同步全量人员
-        ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true).forEach(deptId -> {
-            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
-            log.info("dept, {}, userIds, {}", deptId, userIds.size());
-            // 员工花名册信息
-            ddClient_personnel.getEmployeeInfos(ddClient.getAccessToken(), userIds, ddConf.getAgentId(), null).forEach(employeeInfo -> {
-                // 通过元数据字段code, 匹配员工花名册value
-                List<Map> employeeField = (List<Map>) employeeInfo.get("field_data_list");
-                // 宜搭表单数据
-                Map formData = UtilMap.map("employeeField_limrznyp", Arrays.asList(employeeInfo.get("userid"))); // 成员权限
-                metaList.forEach(meta -> {
-                    boolean isDetail = UtilMap.getBoolean(meta, "detail");
-                    List<Map> metaField = (List<Map>) meta.get("field_meta_info_list");
-                    Map detail = new HashMap(); // 明细行
-                    metaField.forEach(field -> {
-                        // 元数据内一些系统字段无 field_code, sys00 基本信息分组下 使用 field_name
-                        Optional optional = employeeField.stream().filter(employee -> field.get("field_code").equals(employee.get("field_code")) || employee.get("field_name").equals(field.get("field_name"))).findAny();
-                        if (optional.isPresent()) {
-                            // 数据组装
-                            Map employee = (Map) optional.get();
-                            String value = UtilMap.getString(((List<Map>) employee.get("field_value_list")).get(0), "label");
-                            log.info("分组 -> {}, 是否明细 -> {}; 字段 -> {}, 值 -> {}", meta.get("group_name"), meta.get("detail"), field.get("field_name"), value);
-                            // 值处理
-                            if (field.containsKey("comp_id")) {
-                                if (isDetail) {
-                                    detail.put(field.get("comp_id"), value);
-                                } else {
-                                    formData.put(field.get("comp_id"), value);
-                                }
-                            }
-                        }
-                    });
-                    // 明细表
-                    if (isDetail && meta.containsKey("comp_id")) {
-                        formData.put(meta.get("comp_id"), Arrays.asList(detail));
-                    }
-                });
-                // 宜搭更新
-                YDParam ydParam = YDParam.builder()
-                        .searchFieldJson(JSON.toJSONString(UtilMap.map("employeeField_limrznyp", formData.get("employeeField_limrznyp"))))
-                        .formUuid("FORM-EA866E71M5ICA9TSABIFG9V1QMRN2PFL786KL8")
-                        .build();
-                List<String> formInstIds = (List<String>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form_id).getData();
-                if (formInstIds.size() > 0) {
-                    ydParam.setFormInstanceId(formInstIds.get(0));
-                    ydParam.setUpdateFormDataJson(JSON.toJSONString(formData));
-                    ydClient.operateData(ydParam, YDConf.FORM_OPERATION.update);
-                } else {
-                    ydParam.setFormDataJson(JSON.toJSONString(formData));
-                    ydClient.operateData(ydParam, YDConf.FORM_OPERATION.create);
-                }
-            });
-        });
+        gwService.syncRoster();
         return McR.success();
     }
 
@@ -116,12 +38,14 @@ public class GWController {
     @PostMapping("test")
     McR test() {
 
-        String path = JSONUtil.class.getClassLoader().getResource("static/json/personnel").getPath();
-        String json = UtilFile.readJsonStringFromFile(path);
-
+//        String path = JSONUtil.class.getClassLoader().getResource("templates/personnel").getPath();
+//        log.info("xxx, {}", path);
+//        String json = UtilFile.readJsonStringFromFile(path);
+//
 //        return McR.success(JSON.parse(json));
 
-        return McR.success(UtilFile.readJsonObjectFromFile("static/json/personnel"));
+//        return McR.success(UtilFile.readJsonObjectFromResource("templates/personnel"));
+        return McR.success(UtilFile.readJsonObjectFromResource("static/json/personnel.json"));
 
 //        return McR.success(ddClient_personnel.getPersonnelMeta(ddClient.getAccessToken(), ddConf.getAgentId()));
     }

+ 8 - 3
mjava-gewu/src/main/java/com/malk/gewu/schedule/DDScheduleTask.java

@@ -1,6 +1,8 @@
 package com.malk.gewu.schedule;
 
+import com.malk.gewu.service.GWService;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.scheduling.annotation.EnableScheduling;
@@ -13,15 +15,18 @@ import org.springframework.scheduling.annotation.Scheduled;
 @Configuration
 @EnableScheduling
 @ConditionalOnProperty(name = {"spel.scheduling"})
-public class DDScheduleTask {
+public class GWScheduleTask {
+
+    @Autowired
+    private GWService gwService;
 
     /**
      * 每天凌晨4点同步
      */
-    @Scheduled(cron = "* * 4 * * ? ")
+    @Scheduled(cron = "0 0 4 * * ? ")
     public void syncDingTalkFailedList() {
         try {
-
+            gwService.syncRoster();
         } catch (Exception e) {
             // 记录错误信息
             e.printStackTrace();

+ 9 - 0
mjava-gewu/src/main/java/com/malk/gewu/service/GWService.java

@@ -0,0 +1,9 @@
+package com.malk.gewu.service;
+
+public interface GWService {
+
+    /**
+     * 同步花名册信息
+     */
+    void syncRoster();
+}

+ 102 - 0
mjava-gewu/src/main/java/com/malk/gewu/service/impl/GWImplService.java

@@ -0,0 +1,102 @@
+package com.malk.gewu.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.gewu.service.GWService;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDClient_Personnel;
+import com.malk.utils.UtilFile;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service
+@Slf4j
+public class GWImplService implements GWService {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDConf ddConf;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private DDClient_Personnel ddClient_personnel;
+
+    @Autowired
+    private YDClient ydClient;
+
+    /**
+     * 同步花名册信息
+     */
+    @Override
+    public void syncRoster() {
+
+        // 花名册元数据
+        List<Map> metaList = (List<Map>) UtilFile.readJsonObjectFromResource("static/json/personnel.json"); // 本地匹配了宜搭组件ID
+//        List<Map> metaList = ddClient_personnel.getPersonnelMeta(ddClient.getAccessToken(), ddConf.getAgentId());
+        // 同步全量人员
+        ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true).forEach(deptId -> {
+            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
+            log.info("dept, {}, userIds, {}", deptId, userIds.size());
+            // 员工花名册信息
+            ddClient_personnel.getEmployeeInfos(ddClient.getAccessToken(), userIds, ddConf.getAgentId(), null).forEach(employeeInfo -> {
+                // 通过元数据字段code, 匹配员工花名册value
+                List<Map> employeeField = (List<Map>) employeeInfo.get("field_data_list");
+                // 宜搭表单数据
+                Map formData = UtilMap.map("employeeField_limrznyp", Arrays.asList(employeeInfo.get("userid"))); // 成员权限
+                metaList.forEach(meta -> {
+                    boolean isDetail = UtilMap.getBoolean(meta, "detail");
+                    List<Map> metaField = (List<Map>) meta.get("field_meta_info_list");
+                    Map detail = new HashMap(); // 明细行
+                    metaField.forEach(field -> {
+                        // 元数据内一些系统字段无 field_code, sys00 基本信息分组下 使用 field_name
+                        Optional optional = employeeField.stream().filter(employee -> field.get("field_code").equals(employee.get("field_code")) || employee.get("field_name").equals(field.get("field_name"))).findAny();
+                        if (optional.isPresent()) {
+                            // 数据组装
+                            Map employee = (Map) optional.get();
+                            String value = UtilMap.getString(((List<Map>) employee.get("field_value_list")).get(0), "label");
+                            log.info("分组 -> {}, 是否明细 -> {}; 字段 -> {}, 值 -> {}", meta.get("group_name"), meta.get("detail"), field.get("field_name"), value);
+                            // 值处理
+                            if (field.containsKey("comp_id")) {
+                                if (isDetail) {
+                                    detail.put(field.get("comp_id"), value);
+                                } else {
+                                    formData.put(field.get("comp_id"), value);
+                                }
+                            }
+                        }
+                    });
+                    // 明细表
+                    if (isDetail && meta.containsKey("comp_id")) {
+                        formData.put(meta.get("comp_id"), Arrays.asList(detail));
+                    }
+                });
+                // 宜搭更新
+                YDParam ydParam = YDParam.builder()
+                        .searchFieldJson(JSON.toJSONString(UtilMap.map("employeeField_limrznyp", formData.get("employeeField_limrznyp"))))
+                        .formUuid("FORM-EA866E71M5ICA9TSABIFG9V1QMRN2PFL786KL8")
+                        .build();
+                List<String> formInstIds = (List<String>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form_id).getData();
+                if (formInstIds.size() > 0) {
+                    ydParam.setFormInstanceId(formInstIds.get(0));
+                    ydParam.setUpdateFormDataJson(JSON.toJSONString(formData));
+                    ydClient.operateData(ydParam, YDConf.FORM_OPERATION.update);
+                } else {
+                    ydParam.setFormDataJson(JSON.toJSONString(formData));
+                    ydClient.operateData(ydParam, YDConf.FORM_OPERATION.create);
+                }
+            });
+        });
+    }
+}

+ 5 - 5
mjava-gewu/src/main/resources/application-prod.yml

@@ -1,13 +1,13 @@
 # 环境配置
 server:
-  port: 9010
+  port: 9015
   servlet:
     context-path: /api/gewu
 
 # condition
 spel:
-  scheduling: true        # 定时任务是否执行
-  multiSource: false      # 是否多数据源配置
+  scheduling: true         # 定时任务是否执行
+  multiSource: false       # 是否多数据源配置
 
 spring:
   # database
@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: cp-root@2022++
-    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect

File diff suppressed because it is too large
+ 855 - 0
mjava-gewu/src/main/resources/static/json/personnel.json


+ 5 - 5
mjava-gewu/target/classes/application-prod.yml

@@ -1,13 +1,13 @@
 # 环境配置
 server:
-  port: 9010
+  port: 9015
   servlet:
     context-path: /api/gewu
 
 # condition
 spel:
-  scheduling: true        # 定时任务是否执行
-  multiSource: false      # 是否多数据源配置
+  scheduling: true         # 定时任务是否执行
+  multiSource: false       # 是否多数据源配置
 
 spring:
   # database
@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: cp-root@2022++
-    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     database: MYSQL
     database-platform: org.hibernate.dialect.MySQL57Dialect

+ 27 - 2
mjava-guyuan/src/main/java/com/malk/guyuan/controller/IVController.java

@@ -11,6 +11,7 @@ import com.malk.server.common.FilePath;
 import com.malk.server.common.McException;
 import com.malk.server.common.McR;
 import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDService;
 import com.malk.utils.*;
 import com.spire.pdf.PdfCompressionLevel;
 import com.spire.pdf.PdfDocument;
@@ -45,7 +46,7 @@ import java.util.stream.Collectors;
 @RestController
 @RequestMapping
 public class IVController {
-    
+
     @Autowired
     private TXYInvoice txyInvoice;
 
@@ -276,6 +277,10 @@ public class IVController {
         return McR.success("发票查重, 验真响应");
     }
 
+
+    @Autowired
+    private YDService ydService;
+
     /**
      * 发票状态更新: 服务注册 // todo 通用服务, { 组件Id: 值, 组件id: 值 }
      */
@@ -300,7 +305,9 @@ public class IVController {
         if (compId.equals("selectField_liihyrt6")) {
             update.put("radioField_liw7rb2q", "否"); // 提交后, 更新是否退回标识为否
         }
-        ydClient.operateData(YDParam.builder()
+
+        // prd 9.10 更新报销单, 关联到发票:: todo 宜搭服务注册拿不到系统默认字段, 先查询解决:::: 可能是提交校验的原因
+        ydService.operateData2(data, update, YDParam.builder()
                 .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
                 .formInstanceIdList(formInstanceIds)
                 .updateFormDataJson(JSON.toJSONString(update))
@@ -342,4 +349,22 @@ public class IVController {
 
         return McR.success();
     }
+
+    @PostMapping("test")
+    McR test() {
+
+
+//        List<Map> process = (List<Map>) ydClient.queryData(YDParam.builder()
+//                .formUuid("W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
+//                .formInstId("FINST-NGA66WA1FV4EB7QJC3OATA3EV8MK35Z9COEMLFR22")
+//                .build(), YDConf.FORM_QUERY.retrieve_id).getData();
+
+        List<Map> process = (List<Map>) ydClient.queryData(YDParam.builder()
+                .formUuid("FORM-0IA66C71F6NBAETREO8DE9SSN43D3YIZ0AYILC")
+                .searchFieldJson(JSON.toJSONString(UtilMap.map("textField_lmewsobs", "Y16668919W4E4FHQ6123ADDHB8XK3S709YEMLXWF")))
+                .build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
+
+        return McR.success();
+    }
+
 }

+ 2 - 1
mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/impl/TXYImplInvoice.java

@@ -98,7 +98,8 @@ public class TXYImplInvoice implements TXYInvoice {
         ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
         OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
 
-        if (StringUtils.isNotBlank(checkCode)) {
+        // 一些特殊发票校验码只有5位, 兼容
+        if (StringUtils.isNotBlank(checkCode) && checkCode.length() > 6) {
             checkCode = checkCode.substring(checkCode.length() - 6);
         }
 

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

@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: mu123
-    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     hibernate:
       ddl-auto: none      # JPA对表没有任何操作

+ 2 - 2
mjava-guyuan/target/classes/application-dev.yml

@@ -16,8 +16,8 @@ spring:
       connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
     driver-class-name: com.mysql.cj.jdbc.Driver
     username: root
-    password: mu123
-    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    password: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
   jpa:
     hibernate:
       ddl-auto: none      # JPA对表没有任何操作

+ 13 - 13
mjava-hangshi/src/main/java/com/malk/hangshi/controller/HSController.java

@@ -1,15 +1,15 @@
 package com.malk.hangshi.controller;
 
 import com.alibaba.fastjson.JSON;
+import com.malk.hangshi.service.HSService;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
 import com.malk.server.common.McException;
 import com.malk.server.common.McR;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.server.dingtalk.DDConfigSign;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDService;
 import com.malk.utils.UtilMap;
 import com.malk.utils.UtilServlet;
 import lombok.extern.slf4j.Slf4j;
@@ -21,7 +21,6 @@ import org.springframework.web.bind.annotation.RestController;
 
 import javax.servlet.http.HttpServletRequest;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -40,14 +39,21 @@ public class HSController {
     @Autowired
     private DDClient_Contacts ddClient_contacts;
 
+    @Autowired
+    private HSService hsService;
+
     /**
      * 获取员工人数
      */
     @PostMapping("user/count")
     McR queryCount() {
-        return McR.success(ddClient_contacts.getUserCount(ddClient.getAccessToken(), true));
+        return McR.success(hsService.getCorpCount());
     }
 
+    @PostMapping("user/deptInfo")
+    McR queryDeptInfo() {
+        return McR.success(hsService.getDeptInfo(false));
+    }
 
     @Autowired
     private YDClient ydClient;
@@ -81,7 +87,7 @@ public class HSController {
     }
 
     @Autowired
-    private DDConf ddConf;
+    private DDService ddService;
 
     /**
      * jsApi 注册
@@ -90,12 +96,7 @@ public class HSController {
     McR register(@RequestBody Map<String, String> data) {
 
         McException.assertParamException_Null(data, "url", "nonceStr");
-        String jsTicket = ddClient.getJsApiTicket(ddClient.getAccessToken());
-        String nonceStr = data.get("nonceStr");
-        long timeStamp = new Date().getTime();
-        String url = data.get("url");
-        String signature = DDConfigSign.sign(jsTicket, nonceStr, timeStamp, url);
-        return McR.success(UtilMap.map("nonceStr, agentId, timeStamp, corpId, signature", nonceStr, ddConf.getAgentId(), timeStamp, ddConf.getCorpId(), signature));
+        return McR.success(ddService.registerJsApi(data.get("url"), data.get("nonceStr")));
     }
 
     /**
@@ -104,8 +105,7 @@ public class HSController {
     @PostMapping("user/code")
     McR userCodeAuth(@RequestBody Map<String, String> data) {
         McException.assertParamException_Null(data, "code");
-        Map rsp = ddClient.getUserInfoByCode(ddClient.getAccessToken(), data.get("code"));
-        return McR.success(ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), rsp.get("userid").toString()));
+        return McR.success(ddService.getUserInfoByCode(data.get("code")));
     }
 
 }

+ 35 - 0
mjava-hangshi/src/main/java/com/malk/hangshi/schedule/HSScheduleTask.java

@@ -0,0 +1,35 @@
+package com.malk.hangshi.schedule;
+
+import com.malk.hangshi.service.HSService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class HSScheduleTask {
+
+    @Autowired
+    private HSService hsService;
+
+    /**
+     * 每天凌晨4点同步
+     */
+    @Scheduled(cron = "* * 4 * * ? ")
+    public void syncDingTalkFailedList() {
+        try {
+            hsService.getDeptInfo(true);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 16 - 0
mjava-hangshi/src/main/java/com/malk/hangshi/service/HSService.java

@@ -0,0 +1,16 @@
+package com.malk.hangshi.service;
+
+import java.util.Map;
+
+public interface HSService {
+
+    /**
+     * 部门人数
+     */
+    Map getDeptInfo(boolean isClear);
+
+    /**
+     * 组织人数
+     */
+    int getCorpCount();
+}

+ 80 - 0
mjava-hangshi/src/main/java/com/malk/hangshi/service/impl/HSImplService.java

@@ -0,0 +1,80 @@
+package com.malk.hangshi.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.malk.hangshi.service.HSService;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.utils.UtilMap;
+import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class HSImplService implements HSService {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    private Map DEPT_INFO;
+
+    @Override
+    @Synchronized
+    public Map getDeptInfo(boolean isClear) {
+
+        if (isClear) {
+            DEPT_INFO = null;
+        }
+
+        if (ObjectUtil.isNull(DEPT_INFO)) {
+            int total = 0;
+            Map deptInfo = UtilMap.map("东方航空食品投资有限公司", ddClient_contacts.getUserCount(ddClient.getAccessToken(), true));
+            // 一级部门累计
+            List<Map> deptInfos = ddClient_contacts.listSubDepartmentDetail(ddClient.getAccessToken(), DDConf.TOP_DEPARTMENT);
+            for (Map dept : deptInfos) {
+                String deptName = String.valueOf(dept.get("name"));
+                if (!deptName.contains("航食")) {
+                    continue;
+                }
+                long deptId = UtilMap.getLong(dept, "dept_id");
+                int count = 0;
+                for (long sub : ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true, deptId)) {
+                    count += ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), sub).size();
+                }
+                total += count;
+                deptInfo.put(dept.get("name"), count);
+                // 上海航食处理
+                if (deptName.equals("上海航食")) {
+                    List<Map> subDeptInfos = ddClient_contacts.listSubDepartmentDetail(ddClient.getAccessToken(), deptId);
+                    for (Map subDept : subDeptInfos) {
+                        if (Arrays.asList("虹桥中心", "浦东中心").contains(subDept.get("name"))) {
+                            int num = 0;
+                            for (long sub : ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true, UtilMap.getLong(subDept, "dept_id"))) {
+                                num += ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), sub).size();
+                            }
+                            deptInfo.put(subDept.get("name"), num);
+                        }
+                    }
+                    deptInfo.put("各职能业务部门及事业部", UtilMap.getInt(deptInfo, "上海航食") - UtilMap.getInt(deptInfo, "虹桥中心") - UtilMap.getInt(deptInfo, "浦东中心"));
+                }
+            }
+//            deptInfo.put("航食", total);
+            DEPT_INFO = deptInfo;
+        }
+        return DEPT_INFO;
+    }
+
+    @Override
+    public int getCorpCount() {
+        return ddClient_contacts.getUserCount(ddClient.getAccessToken(), true);
+    }
+}

+ 54 - 0
mjava-kuaikeli/pom.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>java-mcli</artifactId>
+        <groupId>com.malk</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>mjava-kuaikeli</artifactId>
+    <description>快客利宜搭开发</description>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <!-- 核心模块-->
+        <dependency>
+            <groupId>com.malk</groupId>
+            <artifactId>mjava</artifactId>
+            <version>${mjava.version}</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                    <!-- 如果没有该配置,devtools不会生效: 打包时关闭 -->
+                    <fork>false</fork>
+                    <!-- 避免中文乱码 -->
+                    <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
+                </configuration>
+                <!-- 允许生成可运行jar -->
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+</project>

+ 32 - 0
mjava-kuaikeli/src/main/java/com/malk/kuaikeli/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.kuaikeli;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+import javax.persistence.EntityManager;
+
+/**
+ * corp项目: 扫描公共模块
+ * -
+ * 若是无需数据库模块, 配置无效地址也可启动, 引入mjava不支持直接 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 配置
+ * 需要配置 jpa.hibernate.ddl-auto 为 none. 标识对表没有任何操作. 若不设置为 non, flyway.enabled 配置会无效, 在没有数库连接情况下程序无法启动
+ */
+@EnableJpaAuditing
+@SpringBootApplication(scanBasePackages = {"com.malk"})
+public class Boot {
+
+    public static void main(String... args) {
+        SpringApplication.run(Boot.class, args);
+    }
+
+    /**
+     * 让Spring管理JPAQueryFactory [不使用Qualifier详见mjava-Boot]
+     */
+    @Bean
+    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
+        return new JPAQueryFactory(entityManager);
+    }
+}

+ 70 - 0
mjava-kuaikeli/src/main/java/com/malk/kuaikeli/controller/KKLController.java

@@ -0,0 +1,70 @@
+package com.malk.kuaikeli.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.common.McR;
+import com.malk.service.aliwork.YDService;
+import com.malk.utils.UtilMc;
+import com.malk.utils.UtilServlet;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping()
+public class KKLController {
+
+    @Autowired
+    private YDService ydService;
+
+    @PostMapping("supplier")
+    McR supplier(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("supplier, {}", JSON.toJSONString(data));
+
+        // ppExt: 供应商区分发起: 当前表:供应供子表组件ID,子表内供应商编号,名称,负责人
+        String[] compIds_supplier = String.valueOf(data.get("compId_supplier")).split(", ");
+        String compId_supplier = compIds_supplier[1];
+
+        Map formData = (Map) JSON.parse(String.valueOf(data.get("formData")));
+        List<Map> details = (List<Map>) formData.get(compIds_supplier[0]);
+
+        // 供应商编号, 数据去重: 提取数据区分供应商发起
+        List<Map> suppliers = UtilMc.distinctByKey(details, compId_supplier);
+        for (Map supplier : suppliers) {
+            List<Map> dataList = details.stream().filter(item -> supplier.get(compId_supplier).equals(item.get(compId_supplier))).collect(Collectors.toList());
+            formData.put(compIds_supplier[0], dataList);
+            // ppExt: 参数传递, 目标表组件ID, 对照当前表子表内的子表内供应商编号,名称,负责人已放置主表字段, 此处公共内容提前首条数据进行赋值
+            for (int i = 1; i < compIds_supplier.length; i++) {
+                formData.put(compIds_supplier[i], YDConf.getDataByCompId(dataList.get(0), compIds_supplier[i]));
+            }
+            data.put("formData", JSON.toJSONString(formData));
+            ydService.copyFormData(data); // 发起流程
+        }
+        return McR.errorToken();
+    }
+
+    @PostMapping("test")
+    McR test(HttpServletRequest request) {
+
+
+        Map data = UtilServlet.getParamMap(request);
+
+        log.info("xxxx, {}", data);
+
+        return McR.success();
+    }
+
+}

+ 39 - 0
mjava-kuaikeli/src/main/resources/application-dev.yml

@@ -0,0 +1,39 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/kuaikeli
+
+# condition
+spel:
+  scheduling: false        # 定时任务是否执行
+  multiSource: false      # 是否多数据源配置
+
+spring:
+  # database
+  datasource:
+    hikari:
+      connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: root
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# dingtalk
+dingtalk:
+  agentId: 2737389469
+  appKey: dingbdo9neuaibv5yjzb
+  appSecret: Gqy7vUkifTDQMCy5kQH8NaNVu1JepPr0Ci66XX5PiNts_mo6kNJEzYPumf3WcnmF
+  corpId: ding7071108579851504f5bf40eda33b7ba0
+  aesKey:
+  token:
+  robotCode:
+  operator: ""   # 管理员 [开头0, 需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_OHSG8KMIP9SGXV32XUQJ
+  systemToken: 9F766B81QHGDDLQ89X17X5GXVPYM20V729KLLPH

+ 39 - 0
mjava-kuaikeli/src/main/resources/application-prod.yml

@@ -0,0 +1,39 @@
+# 环境配置
+server:
+  port: 9016
+  servlet:
+    context-path: /api/kuaikeli
+
+# condition
+spel:
+  scheduling: false        # 定时任务是否执行
+  multiSource: false      # 是否多数据源配置
+
+spring:
+  # database
+  datasource:
+    hikari:
+      connection-init-sql: SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci           # SqlServer, Oracle 无需设置类型
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    username: root
+    password: mu123
+    url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# dingtalk
+dingtalk:
+  agentId: 2737389469
+  appKey: dingbdo9neuaibv5yjzb
+  appSecret: Gqy7vUkifTDQMCy5kQH8NaNVu1JepPr0Ci66XX5PiNts_mo6kNJEzYPumf3WcnmF
+  corpId: ding7071108579851504f5bf40eda33b7ba0
+  aesKey:
+  token:
+  robotCode:
+  operator: ""   # 管理员 [开头0, 需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_OHSG8KMIP9SGXV32XUQJ
+  systemToken: 9F766B81QHGDDLQ89X17X5GXVPYM20V729KLLPH

+ 35 - 0
mjava-kuaikeli/src/test/resources/server.sh

@@ -0,0 +1,35 @@
+#!/bin/bash
+appname='mjava-kuaikeli'
+if [ "$1" == "dev" ]; then
+  java -Xms256m -Xmx256m -jar $appname.jar --spring.profiles.active=dev
+else
+  if [ "$1" == "start" ]; then
+    nohup java -Xms256m -Xmx256m -jar $appname.jar &
+    echo "server prod is starting"
+  else
+    if [ "$1" == "test" ]; then
+      nohup java -Xms256m -Xmx256m -jar $appname.jar --spring.profiles.active=test &
+      echo "server test is starting"
+    else
+      if [ "$1" == "stop" ]; then
+        PID=$(ps -ef | grep $appname.jar | grep -v grep | awk '{ print $2 }')
+        if [ -z "$PID" ]; then
+          echo "server is already stopped"
+        else
+          echo kill $PID
+          kill $PID
+        fi
+      else
+        if [ "$1" == "status" ]; then
+          PID=$(ps -ef | grep $appname.jar | grep -v grep | awk '{ print $2 }')
+          if [ -z "$PID" ]; then
+            echo "server is stopped"
+          else
+            echo "server is running"
+            echo $PID
+          fi
+        fi
+      fi
+    fi
+  fi
+fi

+ 25 - 49
mjava-pake/src/main/java/com/malk/pake/controller/SFController.java

@@ -4,22 +4,22 @@ package com.malk.pake.controller;
  * 错误抛出与拦截详见 CatchException
  */
 
+import com.alibaba.fastjson.JSON;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
 import com.malk.server.common.McR;
-import com.malk.service.dingtalk.DDClient;
-import com.malk.service.dingtalk.DDClient_Attendance;
-import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.aliwork.YDService;
 import com.malk.utils.UtilMap;
+import com.malk.utils.UtilServlet;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import java.util.Arrays;
-import java.util.HashMap;
+import javax.servlet.http.HttpServletRequest;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 @Slf4j
 @RestController
@@ -27,54 +27,30 @@ import java.util.stream.Collectors;
 public class SFController {
 
     @Autowired
-    private DDClient ddClient;
+    private YDService ydService;
 
     @Autowired
-    private DDClient_Attendance ddClient_attendance;
-
-    @Autowired
-    private DDClient_Contacts ddClient_contacts;
+    private YDConf ydConf;
+
+    /**
+     * 关联表单自关联, 业务自动化下传
+     */
+    @PostMapping("association")
+    McR association(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("关联表单自关联, 业务自动化下传, {}", data);
+        List<Map> association = ydConf.associationForm(data.get("formUuid").toString(), data.get("formInstId").toString(), data.get("title").toString(), UtilMap.getString(data, "subTitle"), true);
+        ydService.operateData(YDParam.builder()
+                .formInstId(String.valueOf(data.get("formInstId")))
+                .updateFormDataJson(JSON.toJSONString(UtilMap.map(data.get("compId").toString(), association)))
+                .build(), YDConf.FORM_OPERATION.update);
+        return McR.success();
+    }
 
     @PostMapping("test")
     McR test() {
 
-        List<String> columnNames = Arrays.asList("考勤结果", "下班1打卡结果", "下班1打卡时间", "旷工天数", "出勤天数", "休息天数", "工作时长", "考勤结果", "出差时长", "迟到次数", "早退次数", "下班缺卡次数", "上班缺卡次数", "外出时长", "休息日加班", "工作日加班", "节假日加班", "严重迟到次数", "应出勤天数");
-        List<Map> columns = ddClient_attendance.getAttColumns(ddClient.getAccessToken());
-        Map columnIds = new HashMap(); // 列id通过map提取, 月度汇总接口仅返回id, 存储映射关系
-        List<String> leaveNames = columns.stream().filter(column -> {
-                    if (columnNames.contains(column.get("name"))) {
-                        columnIds.put(column.get("id"), column.get("name"));
-                    }
-                    return column.get("alias").equals("leave_");
-                }
-        ).map(column -> String.valueOf(column.get("name"))).collect(Collectors.toList());
-
-//        ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true).forEach(deptId -> {
-//
-//            
-//        });
-
-
-        String start = "2023-07-01 00:00:00";
-        String end = "2023-07-22 23:59:59";
-        String userId = "196230404821883307";
-
-        Map userInfo = UtilMap.map("userId, userName", "196230404821883307", "吴迎梅");
-        ddClient_attendance.getAttColumnVal(ddClient.getAccessToken(), userId, (List<String>) columnIds.keySet(), start, end).forEach(attendance -> {
-
-            String columnId = ((Map) attendance.get("column_vo")).get("id").toString();
-            userInfo.put(columnIds.get(columnId), "");
-
-
-        });
-
-
-        List<Map> leaveList = ddClient_attendance.getLeaveTimeByNames(ddClient.getAccessToken(), userId, leaveNames, start, end);
-
-
-//        getLeaveTimeByNames
-//        ddClient_attendance.getAttColumns(ddClient.getAccessToken());
-//        getAttColumnVal
-        return McR.success(columns);
+        return McR.success();
     }
 }

+ 8 - 4
mjava-pake/src/main/resources/application-dev.yml

@@ -49,12 +49,16 @@ logging:
 
 # dingtalk
 dingtalk:
-  agentId: 2664525556
-  appKey: dingmcbz0lceeusy2kk4
-  appSecret: nhNVAbjjtb1Y_Q3WM1SkV4Wk3qDxTjfKEVUZd2iMrF5DtlGVcVDi5aIK-8CundeZ
-  corpId: dingade22a8c4fd34b8535c2f4657eb6378f
+  agentId: 2664607479
+  appKey: dingn3i8b1htbuealing
+  appSecret: yrKb6sUl2sAT0YoQJ-1us1xYY9AWR4RG5RLlBA1Uaz1cVsbbfwcklVxVTeyDa1y_
+  corpId: ding2ef56b69014fbfec35c2f4657eb6378f
   aesKey:
   token:
   operator: ""   # OA管理员账号 [0开头需要转一下字符串]
 
+# aliwork
+aliwork:
+  appType: APP_UNSAR4O4Y7NBDUYXLIP4
+  systemToken: HP666C71JQMCDBUHBETIS992J3SW3XKUKBCKL53
 

+ 10 - 5
mjava-pake/src/main/resources/application-prod.yml

@@ -24,10 +24,15 @@ spring:
 
 # dingtalk
 dingtalk:
-  agentId: 2664525556
-  appKey: dingmcbz0lceeusy2kk4
-  appSecret: nhNVAbjjtb1Y_Q3WM1SkV4Wk3qDxTjfKEVUZd2iMrF5DtlGVcVDi5aIK-8CundeZ
-  corpId: dingade22a8c4fd34b8535c2f4657eb6378f
+  agentId: 2664607479
+  appKey: dingn3i8b1htbuealing
+  appSecret: yrKb6sUl2sAT0YoQJ-1us1xYY9AWR4RG5RLlBA1Uaz1cVsbbfwcklVxVTeyDa1y_
+  corpId: ding2ef56b69014fbfec35c2f4657eb6378f
   aesKey:
   token:
-  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+  operator: ""   # OA管理员账号 [0开头需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_UNSAR4O4Y7NBDUYXLIP4
+  systemToken: HP666C71JQMCDBUHBETIS992J3SW3XKUKBCKL53

+ 6 - 2
mjava/src/main/java/com/malk/config/WebConfiguration.java

@@ -21,6 +21,7 @@ public class WebConfiguration implements WebMvcConfigurer {
         logger.info("拦截器 ▷ 初始化");
         registry.addInterceptor(new RequestInterceptor())
                 .addPathPatterns("/**")
+                // ppExt: 若无需对外访问, 不用添加路径. static/public 为默认
                 .excludePathPatterns("/assets/**", "/templates/**");
     }
 
@@ -41,12 +42,15 @@ public class WebConfiguration implements WebMvcConfigurer {
      * -
      * 默认的静态资源路径为: classpath:/META-INF/resources/, classpath:/resources/,classpath:/static/, classpath:/public [默认路径不会进拦截器]
      * 读取的是target内容, 访问路径 assets: http://localhost:9001/api/assets/logo/logo-text.png [自定义拦截器添加路径排除: excludePathPatterns]
-     * web网页路径: http://106.14.201.187:9011/api/cusName/web/index.html#/zhuogao/home [前后端模块化, 不配置Nginx从Tomcat透出vue页面]
+     * web网页路径: http://localhost:9011/api/项目路径/web/index.html#/模块名称/home [前后端模块化, 不配置Nginx从Tomcat透出vue页面]
      * 当在SpringBoot项目内添加网页资源时,在windows服务器,需要C:\Windows\System32下添加tomcat-native-1.2.14-win32-bin.zip内x64下两个文件, 重启项目
+     * -
+     * ppExt: ClassPathResource, 需要打包/编译后才能访问到. 识别不是架包内内容 [static 可自动过滤, 自动识别子文件夹作为 path]
+     * [两个示例]: mjs http://localhost:9001/api/项目路径/mjs/mjs.min.js / http://localhost:9001/api/项目路径/json/personnel.json
      */
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
-        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/static/");
+        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
         registry.addResourceHandler("/web/**").addResourceLocations("classpath:/static/web/");
         registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/assets/");
         registry.addResourceHandler("/templates/**").addResourceLocations("classpath:/templates/");

+ 7 - 0
mjava/src/main/java/com/malk/controller/DDCallbackController.java

@@ -2,6 +2,7 @@ package com.malk.controller;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.malk.server.common.McR;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.server.dingtalk.crypto.DingCallbackCrypto;
 import com.malk.service.dingtalk.DDClient_Event;
@@ -62,4 +63,10 @@ public class DDCallbackController {
         log.info("----- [DD]已注册, 未处理的其它回调 -----, {}", eventJson);
         return success;
     }
+
+    @PostMapping("robot")
+    McR robot(@RequestBody Map data) {
+        log.info("xxx, {}", data);
+        return McR.success();
+    }
 }

+ 41 - 0
mjava/src/main/java/com/malk/server/aliwork/YDConf.java

@@ -1,12 +1,20 @@
 package com.malk.server.aliwork;
 
+import com.alibaba.fastjson.JSON;
+import com.malk.utils.UtilMap;
 import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
 
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
 @Data
 @Component
 @ConfigurationProperties(prefix = "aliwork")
+@Slf4j
 public class YDConf {
 
     private String appType;
@@ -67,6 +75,39 @@ public class YDConf {
         start,                      // 发起流程
     }
 
+    /**
+     * 关联表单处理
+     *
+     * @param formType: "receipt" 跳转为表单【若是流程,则权限体系会失效,但也会显示审批节点】; formType: "process" 为流程
+     */
+    public List<Map> associationForm(String formUuid, String formInstanceId, String title, String subTitle, boolean isProcess) {
+        String formType = isProcess ? "process" : "receipt";
+        return Arrays.asList(UtilMap.map("appType, formUuid, instanceId, title, subTitle, formType", appType, formUuid, formInstanceId, title, subTitle, formType));
+    }
+
+    /**
+     * 组件格式化取值 [取值] todo 明细表递归
+     */
+    public static Object getDataByCompId(Map formData, String compId_cur) {
+        if (compId_cur.contains("associationFormField_")) {
+            // 服务注册 #{_yida_all_data} 直接组件Id
+            if (!formData.containsKey(compId_cur)) {
+                // 接口返回数据, 关联组件ID, 不会返回组件id字段
+                compId_cur = compId_cur + "_id";
+            }
+            return JSON.parse(String.valueOf(formData.get(compId_cur)));
+        } else if (compId_cur.contains("employeeField_")) {
+            if (!formData.containsKey(compId_cur + "_id")) {
+                // 服务注册 #{_yida_all_data} 返回JsonString userid 数组
+                return JSON.parse(String.valueOf(formData.get(compId_cur)));
+            } else {
+                // 成员组件, 接口返回组件id返回为姓名, _id返回是userId
+                return formData.get(compId_cur + "_id");
+            }
+        }
+        return formData.get(compId_cur);
+    }
+
     ////////////////////////// 老版本API //////////////////////////
 
     /**

+ 0 - 1
mjava/src/main/java/com/malk/server/aliwork/YDParam.java

@@ -159,7 +159,6 @@ public class YDParam extends BaseDto {
     @Builder.Default
     boolean useLatestFormSchemaVersion = true;
 
-
     /**
      * 分组校验
      *

+ 1 - 0
mjava/src/main/java/com/malk/server/common/VenR.java

@@ -31,6 +31,7 @@ public class VenR extends BaseDto {
     public static final String RC_FXK = "com.malk.server.fxiaoke.FXKR ";
     public static final String RC_CY = "com.malk.server.h3yun.CYR";
     public static final String RC_XBB = "com.malk.server.xbongbong.XBBR";
+    public static final String RC_VK = "com.malk.server.vika.VKR";
 
     /**
      * 通用post请求

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

@@ -30,6 +30,9 @@ public class DDConf {
     // 操作人, 需要为OA后台管理员
     private String operator;
 
+    // 机器人编号
+    private String robotCode;
+
     /**
      * 钉钉一级部门: 1
      */

+ 24 - 0
mjava/src/main/java/com/malk/server/dingtalk/DDInterActiveCard.java

@@ -0,0 +1,24 @@
+package com.malk.server.dingtalk;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.malk.utils.UtilMap;
+import org.apache.commons.collections4.map.HashedMap;
+
+import java.util.Map;
+
+public class DDInterActiveCard {
+
+    /**
+     * 格式交互卡片, 表格数据类型 [兼容多表格], 格式详见 DDClient_Extension 发送卡片
+     */
+    public static Map formCardDataForTable(Map data, String... props) {
+
+        Map cardData = new HashedMap();
+        for (String prop : props) {
+            cardData.put(prop, UtilMap.map("data, meta", data.get(prop), data.get("meta")));
+        }
+        /// fastjson 避免循环引用: 当一个对象包含另一个对象时 或 当一个对象和另一个对象完全相同时
+        return UtilMap.map("cardParamMap", UtilMap.map("sys_full_json_obj", JSON.toJSONString(cardData, SerializerFeature.DisableCircularReferenceDetect)));
+    }
+}

+ 2 - 3
mjava/src/main/java/com/malk/server/dingtalk/crypto/DingCallbackCrypto.java

@@ -1,6 +1,5 @@
 package com.malk.server.dingtalk.crypto;
 
-import com.alibaba.fastjson.JSON;
 import org.apache.commons.codec.binary.Base64;
 
 import javax.crypto.Cipher;
@@ -206,13 +205,13 @@ public class DingCallbackCrypto {
         try {
             String[] array = new String[]{token, timestamp, nonce, encrypt};
             Arrays.sort(array);
-            System.out.println(JSON.toJSONString(array));
+//            System.out.println(JSON.toJSONString(array));
             StringBuffer sb = new StringBuffer();
             for (int i = 0; i < 4; i++) {
                 sb.append(array[i]);
             }
             String str = sb.toString();
-            System.out.println(str);
+//            System.out.println(str);
             MessageDigest md = MessageDigest.getInstance("SHA-1");
             md.update(str.getBytes());
             byte[] digest = md.digest();

+ 26 - 0
mjava/src/main/java/com/malk/server/vika/VKConf.java

@@ -0,0 +1,26 @@
+package com.malk.server.vika;
+
+import com.malk.utils.UtilMap;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 读取配置文件参考FilePah
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "vika")
+public class VKConf {
+
+    private String apiToken;
+
+    /**
+     * api token授权参数
+     */
+    public Map initTokenHeader() {
+        return UtilMap.map("Authorization", "Bearer " + apiToken);
+    }
+}

+ 34 - 0
mjava/src/main/java/com/malk/server/vika/VKR.java

@@ -0,0 +1,34 @@
+package com.malk.server.vika;
+
+import com.malk.server.common.McException;
+import com.malk.server.common.VenR;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 返回值配置参考McR
+ */
+@Data
+@NoArgsConstructor
+public class VKR<T> extends VenR {
+
+
+    private int code;
+
+    private String message;
+
+    private boolean success;
+
+    private T data;
+
+    // 成功状态标记
+    private final static int SUC_CODE = 200;
+
+    /**
+     * 断言错误信息
+     */
+    @Override
+    public void assertSuccess() {
+        McException.assertException(code != SUC_CODE || !isSuccess(), String.valueOf(code), message, "vika");
+    }
+}

+ 8 - 3
mjava/src/main/java/com/malk/server/xbongbong/XBBConf.java

@@ -42,13 +42,18 @@ public class XBBConf {
     /**
      * 获取查询条件
      *
+     * @implSpec 查询时间条件为秒级; 查询条件值为集合类型, 若是区间通过指定range, 值依然是集合.
      * @apiNote https://profapi.xbongbong.com/#/params?anchor=%E5%85%A5%E5%8F%82%E8%AF%B4%E6%98%8E
      */
-    public static final Map getConditionMap(String attr, String symbol, String... values) {
+    public static final Map getConditionMap(String attr, String symbol, Object... values) {
         return UtilMap.map("attr, symbol, value", attr, symbol, Arrays.asList(values));
     }
 
     /// API
-    public static final String API_LIST_CUSTOMER = "https://proapi.xbongbong.com/pro/v2/api/customer/list";
-    public static final String API_LIST_CONTRACT = "https://proapi.xbongbong.com/pro/v2/api/contract/list";
+    public static final String API_LIST_customer = "https://proapi.xbongbong.com/pro/v2/api/customer/list"; // 客户列表
+    public static final String API_LIST_contract = "https://proapi.xbongbong.com/pro/v2/api/contract/list"; // 合同列表
+    public static final String API_LIST_communicate = "https://proapi.xbongbong.com/pro/v2/api/communicate/list"; // 跟进记录
+    public static final String API_LIST_opportunity = "https://proapi.xbongbong.com/pro/v2/api/opportunity/list"; // 销售机会
+    public static final String API_LIST_paymentSheet = "https://proapi.xbongbong.com/pro/v2/api/paymentSheet/list"; // 回款单
+
 }

+ 25 - 5
mjava/src/main/java/com/malk/service/aliwork/YDService.java

@@ -14,16 +14,36 @@ public interface YDService {
      */
     Object operateData(YDParam param, YDConf.FORM_OPERATION type);
 
+    /**
+     * 操作数据 [异步]
+     */
+    Object operateData2(Map data, Map update, YDParam param, YDConf.FORM_OPERATION type);
+
     /**
      * 查询数据 [子表]
      */
     List<Map> queryDetails(YDParam ydParam);
 
     /**
-     * 全表复制 [镜像] todo, 明细超过20条未处理, 组件替换和忽略未处理
-     *
-     * @param cParam 当前表数据, 实例ID
-     * @param sParam 目标表数据, 表单ID/流程ID
+     * 查询全部 [主表]
+     */
+    List<Map> queryAllFormData(YDParam YDParam);
+
+    /**
+     * 查询宜搭数据
+     */
+    List queryDataList_FormData(String formUuid, Map conditions);
+
+    /**
+     * 全表复制 [服务注册传递参数都是 string 格式]
+     * -
+     * fixme: 服务注册
+     * 1. 在提交校验,因为数据还未执行, 除 #{_yida_all_data} 外,绝大部分字段都是为空,且若是子表字段数据:会拆为为单一字段,数据为该字段下明细数据数组
+     * 2. 但在在业务规则内配置,可以正常获取. 若是子表单, 除返回提交下该字段明细数据数组外, 也会返回子表组件, 及子表内对应的数据 [可有效避免触发何查询操作]
+     * 业务规则场景: 若是写入关联表单,自关联后,通过自动化传递。若是数据全表复制,可获取全量数据进行写入操作
+     * ppExt: 组件格式
+     * 1. { "cur": "目标表, 主表组件ID[包含子表组件ID]", "src": "当前表, 主表组件ID[包含子表组件ID]", "子表组件ID + cur/src": "子表内的组件ID" }
+     * 2. 所有组件通过英文逗号 + 空格区分, 子表组件为避免组件id相同但子表内组件不一致的情况, 在单独子表组件ID后添加 src/cur 区分来源
      */
-    Object copyFormData(YDParam cParam, YDParam sParam);
+    Object copyFormData(Map data);
 }

+ 106 - 40
mjava/src/main/java/com/malk/service/aliwork/impl/YDServiceImpl.java

@@ -3,16 +3,23 @@ package com.malk.service.aliwork.impl;
 import com.alibaba.fastjson.JSON;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
+import com.malk.server.common.McException;
 import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.aliwork.YDService;
+import com.malk.utils.UtilMap;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.map.HashedMap;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Service
@@ -33,6 +40,28 @@ public class YDServiceImpl implements YDService {
     }
 
 
+    @Async
+    @SneakyThrows
+    @Override
+    public Object operateData2(Map data, Map update, YDParam ydParam, YDConf.FORM_OPERATION type) {
+
+        // prd 9.10 更新报销单, 关联到发票:: todo 宜搭服务注册拿不到系统默认字段, 先查询解决
+        if (data.containsKey("aUuid")) {
+            Thread.sleep(3000);
+            List<Map> process = (List<Map>) ydClient.queryData(YDParam.builder()
+                    .formUuid(data.get("aFormUuid").toString())
+                    .searchFieldJson(JSON.toJSONString(UtilMap.map("textField_lmewsobs", data.get("aUuid"))))
+                    .build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
+
+            update.put(data.get("aCompId"),
+                    Arrays.asList(UtilMap.map("appType, formType, instanceId, formUuid, title",
+                            "APP_FKRK7Y94DPI1S9DV1605", "process", process.get(0).get("formInstanceId"), data.get("aFormUuid"), process.get(0).get("title"))));
+            ydParam.setUpdateFormDataJson(JSON.toJSONString(update));
+        }
+        return ydClient.operateData(ydParam, type);
+    }
+
+
     /**
      * 子表全部数据获取, 最大分页为50 [可考虑中间表思路]
      */
@@ -41,7 +70,7 @@ public class YDServiceImpl implements YDService {
         return _queryDetails(ydParam, new ArrayList<>());
     }
 
-    // 递归查询
+    // 递归查询 todo 如果查询中, totalCount 发生变化, 就会进入死循环
     private List<Map> _queryDetails(YDParam ydParam, List<Map> details) {
         ydParam.setPageSize(50);
         DDR_New ddr_new = ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_details);
@@ -54,52 +83,89 @@ public class YDServiceImpl implements YDService {
     }
 
     /**
-     * 全表复制
+     * 查询全部 [主表]
      */
     @Override
-    public Object copyFormData(YDParam cParam, YDParam sParam) {
-
-        Map<String, Object> formData = ydClient.queryData(cParam, YDConf.FORM_QUERY.retrieve_id).getFormData();
-
-        Map newFormData = new HashMap();
-        for (String compId : formData.keySet()) {
-            if (compId.endsWith("_id") || compId.endsWith("_value")) {
-                continue;
-            }
-            newFormData.put(compId, _formatCompData(compId, formData));
+    public List<Map> queryAllFormData(YDParam ydParam) {
+        float pageSize = YDConf.PAGE_SIZE_LIMIT;
+        // 查询数据量
+        ydParam.setPageSize(1);
+        long totalCount = ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form).getTotalCount();
+        // 轮询累计数据
+        List<Map> dataList = new ArrayList<>();
+        ydParam.setCurrentPage(1);
+        ydParam.setPageSize((int) pageSize);
+        for (int page = 1; page <= Math.ceil(totalCount / pageSize); page++) {
+            ydParam.setCurrentPage(page);
+            dataList.addAll((List<Map>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form).getData());
         }
-
-        newFormData.put("employeeField_lia74rki", Arrays.asList(YDConf.PUB_ACCOUNT));
-
-        sParam.setFormDataJson(JSON.toJSONString(newFormData));
-        ydClient.operateData(sParam, YDConf.FORM_OPERATION.start);
-
-        return null;
+        return dataList;
     }
 
-    /// 格式化组件
-    Object _formatCompData(String compId, Map<String, Object> formData) {
-
-        // todo: 成员, 附件, 关联表单 ignoreDetails 忽略明细
-//        List<Map> association = JSON.parseArray(String.valueOf(JSON.parse(String.valueOf(formData.get("associationFormField_lg0gmbi8_id")))), Map.class);
-//        String formInstId = String.valueOf(association.get(0).get("instanceId"));
-
+    /**
+     * 查询宜搭数据
+     */
+    @Override
+    public List queryDataList_FormData(String formUuid, Map conditions) {
+        conditions = UtilMap.empty(conditions);
+        List<Map> dataList = (List<Map>) ydClient.queryData(YDParam.builder()
+                        .formUuid(formUuid)
+                        .searchFieldJson(JSON.toJSONString(conditions)).build(),
+                YDConf.FORM_QUERY.retrieve_search_form).getData();
+        return dataList.stream().map(item -> (Map) item.get("formData")).collect(Collectors.toList());
+    }
 
-        if (compId.startsWith("tableField_")) {
-            List<Map> details = (List<Map>) formData.get(compId);
-            for (Map<String, Object> detail : details) {
-                for (String compId_sub : detail.keySet()) {
-                    detail.put(compId_sub, _formatCompData(compId_sub, detail));
+    /**
+     * 全表复制
+     */
+    @Override
+    public Object copyFormData(Map data) {
+
+        McException.assertParamException_Null(data, "formData, formUuid, processCode, compIds");
+        Map compIds = (Map) JSON.parse(String.valueOf(data.get("compIds")));
+        McException.assertParamException_Null(compIds, "cur, src"); // compIds 包含全部组件
+        Map formData = (Map) JSON.parse(String.valueOf(data.get("formData")));
+
+        String[] compIds_cur = UtilMap.getString(compIds, "cur").split(", ");
+        String[] compIds_src = UtilMap.getString(compIds, "src").split(", ");
+        McException.assertAccessException(compIds_cur.length != compIds_src.length, "主表的字段数量不一致, 请检查配置");
+
+        // 目标表数据处理
+        Map dataForm = new HashedMap();
+        for (int i = 0; i < compIds_cur.length; i++) {
+            String compId_cur = compIds_cur[i];
+            String compId_src = compIds_src[i];
+            // 子表的顺序要保持一致
+            if (compId_cur.contains("tableField_") || compId_src.contains("tableField_")) {
+                boolean isAssert = (compId_cur.contains("tableField_") && !compId_src.contains("tableField_")) || (!compId_cur.contains("tableField_") && compId_src.contains("tableField_"));
+                McException.assertAccessException(isAssert, "子表组件类型不一致, 请检查配置");
+                String[] compIds_cur_detail = UtilMap.getString(compIds, compId_cur + "_cur").split(", ");
+                String[] compIds_src_detail = UtilMap.getString(compIds, compId_src + "_src").split(", ");
+                McException.assertAccessException(compIds_cur_detail.length != compIds_src_detail.length, "子表的字段数量不一致, 请检查配置");
+                // 子表数据处理
+                List<Map> details = new ArrayList<>();
+                for (Map detail : (List<Map>) UtilMap.getList(formData, compId_cur)) {
+                    Map rowData = new HashedMap();
+                    // 子表组件处理
+                    for (int j = 0; j < compIds_cur_detail.length; j++) {
+                        String compId_cur_detail = compIds_cur_detail[j];
+                        String compId_src_detail = compIds_src_detail[j];
+                        rowData.put(compId_src_detail, YDConf.getDataByCompId(detail, compId_cur_detail));
+                    }
+                    details.add(rowData);
                 }
+                dataForm.put(compId_cur, details);
+            } else {
+                // 主表数据处理
+                dataForm.put(compId_src, YDConf.getDataByCompId(formData, compId_cur));
             }
         }
-        if (compId.startsWith("employeeField_")) {
-            return formData.get(compId + "_id");
-        }
-        if (compId.startsWith("associationFormField_")) {
-            return JSON.parse(String.valueOf(formData.get(compId + "_id")));
-        }
-
-        return formData.get(compId);
+        // 发起流程/创建表单
+        YDConf.FORM_OPERATION type = data.containsKey("processCode") ? YDConf.FORM_OPERATION.start : YDConf.FORM_OPERATION.create;
+        return ydClient.operateData(YDParam.builder()
+                .formUuid(UtilMap.getString(data, "formUuid"))
+                .processCode(UtilMap.getString(data, "processCode"))
+                .formDataJson(JSON.toJSONString(dataForm))
+                .build(), type);
     }
 }

+ 2 - 0
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Contacts.java

@@ -26,6 +26,8 @@ public interface DDClient_Contacts {
      */
     List<Long> getDepartmentId_all(String access_token, boolean containsTop);
 
+    List<Long> getDepartmentId_all(String access_token, boolean containsTop, long deptId);
+
     /**
      * 获取全部架构内部门_全部 detail [是否包含一级部门]
      */

+ 40 - 0
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Extension.java

@@ -0,0 +1,40 @@
+package com.malk.service.dingtalk;
+
+import java.util.Map;
+
+public interface DDClient_Extension {
+
+    /**
+     * 发送钉钉互动卡片 [微应用机器人创建后, 需要次日才能使用, 期间会出现机器人不存在提示]
+     *
+     * @param cardTemplateId   互动卡片的消息模板ID
+     * @param outTrackId       是由开发者自己生成并作为入参传递给钉钉的,钉钉只在对应使用到outTrackId的场景,帮助开发者对TrackId进行记录
+     * @param conversationType 0:单聊 1:群聊 [ fixme: openConversationId 必须填写, 通过钉钉事件回调\机器人消息回调\酷应用快捷入口进行获取. 微应用是否配置会话推送不强制 ]
+     * @param robotCode        场景群使用     [ 非场景群的企业内部开发-机器人发送单聊:chatBotId和robotCode都不填写,直接用支持单聊的机器人应用来发送 ]
+     * @param chatBotId        非场景群的企业内部开发-机器人发送群聊 [ fixme: cardData 内数据参考酷应用卡片内的 mock 格式, 若是表格需要传递 meta 表头 ]
+     * @param cardData         卡片数据 { cardData: { cardParamMap : { sys_full_json_obj: JSONString } } }: sys_full_json_obj, 将所有非 String 参数构建成一个 JSONObject
+     * @param extInfo          extInfo.put("atOpenIds", UtilMap.map("key", UtilMap.map("**@ALL**", "**@ALL**"))); // 所有人。UtilMap.map("cardOptions", UtilMap.map("supportForward", true)); // 允许转发
+     * @implSpec fixme: 注意检查是否没有更换 outTrackId。一般情况下,如果使用了新的 cardTemplateId 或 cardData 等参数,则需要生成一个全新的 outTrackId,否则更改不会生效
+     */
+    Map sendInteractiveCards(String accessToken, String cardTemplateId, String outTrackId, int conversationType, String robotCode, String chatBotId, String openConversationId, Map cardData, Map extInfo);
+
+    /**
+     * 注册互动卡片回调地址
+     */
+    Map registerInterActiveCard(String access_token, String callback_url, Map extInfo);
+
+    /**
+     * 机器人发送群聊消息 [其它参数公司, 参考 发送钉钉互动卡片]
+     *
+     * @param coolAppCode 当使用群聊酷应用的方式安装机器人时,必须传入此参数
+     */
+    Map sendGroupMessages(String accessToken, Map msgParam, String msgKey, String openConversationId, String robotCode, String coolAppCode);
+
+    /**
+     * 自定义机器人发送群消息
+     *
+     * @param extInfo 区分消息类型作为不同参数. 另 at { isAtAll: bool } 是否@所有人。
+     */
+    Map sendMessages(String accessToken, Map msgType, Map extInfo);
+
+}

+ 14 - 0
mjava/src/main/java/com/malk/service/dingtalk/DDClient_Log.java

@@ -0,0 +1,14 @@
+package com.malk.service.dingtalk;
+
+import java.util.List;
+import java.util.Map;
+
+public interface DDClient_Log {
+
+    /**
+     * 获取用户发出的日志列表
+     *
+     * @param extInfo size 每页数据量,最大值为20; 查询游标,初始传入0,后续从上一次的返回值中获取
+     */
+    List<Map> reportList(String access_token, long start_time, long end_time, Map extInfo);
+}

+ 13 - 2
mjava/src/main/java/com/malk/service/dingtalk/DDService.java

@@ -30,8 +30,7 @@ public interface DDService {
      */
     Map uploadFileFormUrl(String accessToken, String userId, String filePath);
 
-    
-    // todo 通讯录部门结构返回; 通讯录全量数据同步
+    // todo 通讯录部门结构返回; 通讯录全量数据同步; 限流控制
 
     /**
      * 判断员工是否在指定部门
@@ -47,4 +46,16 @@ public interface DDService {
      * 获取员工所属部门层级路径 [名称拼接]
      */
     String getUserDepartmentHierarchyJoin(String access_token, String userId, String jon);
+
+    /**
+     * jsApi 注册
+     *
+     * @implNote H5无需配置, 但宜搭内免得需要配置鉴权, 才能获取到code. 宜搭非免登页面不会执行调用
+     */
+    Map registerJsApi(String url, String nonceStr);
+
+    /**
+     * 免登code获取用户信息
+     */
+    Map getUserInfoByCode(String code);
 }

+ 7 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Contacts.java

@@ -67,6 +67,13 @@ public class DDImplClient_Contacts implements DDClient_Contacts {
                 .stream().map(detpId -> Long.valueOf(String.valueOf(detpId))).collect(Collectors.toList());
     }
 
+    @Override
+    public List<Long> getDepartmentId_all(String access_token, boolean containsTop, long deptId) {
+        List<Long> deptList = containsTop ? UtilList.asList(deptId) : new ArrayList<>();
+        return (List<Long>) _getDepartment_all(access_token, deptId, deptList, true)
+                .stream().map(detpId -> Long.valueOf(String.valueOf(detpId))).collect(Collectors.toList());
+    }
+
     /// 递归查询
     private List _getDepartment_all(String access_token, long deptId, List deptIdList, boolean isIdList) {
         List tList = isIdList ?

+ 69 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Extension.java

@@ -0,0 +1,69 @@
+package com.malk.service.dingtalk.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDR;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.service.dingtalk.DDClient_Extension;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+@Slf4j
+public class DDImplClient_Extension implements DDClient_Extension {
+
+    /**
+     * 发送钉钉互动卡片
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/send-interactive-dynamic-cards-1
+     */
+    @Override
+    public Map sendInteractiveCards(String accessToken, String cardTemplateId, String outTrackId, int conversationType, String robotCode, String chatBotId, String openConversationId, Map cardData, Map extInfo) {
+
+        Map data = UtilMap.map("cardTemplateId, outTrackId, conversationType, robotCode, chatBotId, openConversationId, cardData", cardTemplateId, outTrackId, conversationType, robotCode, chatBotId, openConversationId, cardData);
+        UtilMap.putAll(data, extInfo);
+        return (Map) DDR_New.doPost("https://api.dingtalk.com/v1.0/im/interactiveCards/send", DDConf.initTokenHeader(accessToken), null, data).getResult();
+    }
+
+    /**
+     * 注册互动卡片回调地址
+     *
+     * @implNote https://open.dingtalk.com/document/orgapp/register-card-callback-address
+     */
+    @Override
+    public Map registerInterActiveCard(String access_token, String callback_url, Map extInfo) {
+
+        Map data = UtilMap.map("callback_url", callback_url);
+        UtilMap.putAll(data, extInfo);
+        return (Map) DDR.doPost("https://oapi.dingtalk.com/topapi/im/chat/scencegroup/interactivecard/callback/register", null, DDConf.initTokenParams(access_token), data).getResult();
+    }
+
+    /**
+     * 机器人发送群聊消息 [其它参数公司, 参考 发送钉钉互动卡片]
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/the-robot-sends-a-group-message
+     * @apiNote https://open.dingtalk.com/document/orgapp/types-of-messages-sent-by-robots
+     */
+    @Override
+    public Map sendGroupMessages(String accessToken, Map msgParam, String msgKey, String openConversationId, String robotCode, String coolAppCode) {
+
+        Map data = UtilMap.map("msgParam, msgKey, openConversationId, robotCode, coolAppCode", JSON.toJSONString(msgParam), msgKey, openConversationId, robotCode, coolAppCode);
+        return (Map) DDR_New.doPost("https://api.dingtalk.com/v1.0/robot/groupMessages/send", DDConf.initTokenHeader(accessToken), null, data).getResult();
+    }
+
+    /**
+     * 自定义机器人发送群消息
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/custom-robots-send-group-messages
+     */
+    @Override
+    public Map sendMessages(String accessToken, Map msgType, Map extInfo) {
+
+        Map data = UtilMap.map("msgtype", msgType);
+        UtilMap.putAll(data, extInfo);
+        return (Map) DDR.doPost("https://oapi.dingtalk.com/robot/send", null, DDConf.initTokenParams(accessToken), extInfo).getResult();
+    }
+}

+ 35 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_Log.java

@@ -0,0 +1,35 @@
+package com.malk.service.dingtalk.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDR;
+import com.malk.service.dingtalk.DDClient_Log;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class DDImplClient_Log implements DDClient_Log {
+
+    /**
+     * 获取用户发出的日志列表
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/query-logs-sent-by-an-employee
+     */
+    @Override
+    public List<Map> reportList(String access_token, long start_time, long end_time, Map extInfo) {
+        Map body = UtilMap.map("start_time, end_time, size", start_time, end_time, 20);
+        if (ObjectUtil.isNotNull(extInfo)) {
+            body.putAll(extInfo);
+        }
+        if (!body.containsKey("cursor")) {
+            body.put("cursor", 0);
+        }
+        Map result = (Map) DDR.doPost("https://oapi.dingtalk.com/topapi/report/list", null, DDConf.initTokenParams(access_token), body).getResult();
+        return (List<Map>) result.get("data_list");
+    }
+}

+ 24 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplService.java

@@ -3,6 +3,7 @@ package com.malk.service.dingtalk.impl;
 import cn.hutool.core.util.ObjectUtil;
 import com.malk.server.common.FilePath;
 import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDConfigSign;
 import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.dingtalk.*;
 import com.malk.utils.UtilFile;
@@ -43,6 +44,9 @@ public class DDImplService implements DDService {
     @Autowired
     private DDConf ddConf;
 
+    @Autowired
+    private DDClient ddClient;
+
     /**
      * 新发起审批15s内不允许撤销, 异步执行 [审批同意/拒绝只能通过节点操作, 系统无法直接介入]   -- 异步需要中转一层进行触发, client为原子接口
      */
@@ -182,4 +186,24 @@ public class DDImplService implements DDService {
     public String getUserDepartmentHierarchyJoin(String access_token, String userId, String delimiter) {
         return String.join(delimiter, getUserDepartmentHierarchy(access_token, userId).stream().map(dept -> dept.get("name").toString()).collect(Collectors.toList()));
     }
+
+    /**
+     * jsApi 注册
+     */
+    @Override
+    public Map registerJsApi(String url, String nonceStr) {
+        String jsTicket = ddClient.getJsApiTicket(ddClient.getAccessToken());
+        long timeStamp = new Date().getTime();
+        String signature = DDConfigSign.sign(jsTicket, nonceStr, timeStamp, url);
+        return UtilMap.map("nonceStr, agentId, timeStamp, corpId, signature", nonceStr, ddConf.getAgentId(), timeStamp, ddConf.getCorpId(), signature);
+    }
+
+    /**
+     * 免登code获取用户信息
+     */
+    @Override
+    public Map getUserInfoByCode(String code) {
+        Map rsp = ddClient.getUserInfoByCode(ddClient.getAccessToken(), code);
+        return ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), rsp.get("userid").toString());
+    }
 }

+ 19 - 0
mjava/src/main/java/com/malk/service/vika/VKClient.java

@@ -0,0 +1,19 @@
+package com.malk.service.vika;
+
+import java.util.List;
+import java.util.Map;
+
+public interface VKClient {
+
+    /**
+     * 获取记录
+     *
+     * @implSpec pageSize 默认100, 取值1~1000. pageNum 分页
+     */
+    Map getRecords(String datasheetId, Map extInfo);
+
+    /**
+     * 获取记录 [最大分页]
+     */
+    List<Map> getRecordsAll(String datasheetId, Map extInfo);
+}

+ 38 - 0
mjava/src/main/java/com/malk/service/vika/impl/VKImplClient.java

@@ -0,0 +1,38 @@
+package com.malk.service.vika.impl;
+
+import com.malk.server.vika.VKConf;
+import com.malk.server.vika.VKR;
+import com.malk.service.vika.VKClient;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+@Slf4j
+public class VKImplClient implements VKClient {
+
+    @Autowired
+    private VKConf vkConf;
+
+    /**
+     * 获取记录
+     */
+    @Override
+    public Map getRecords(String datasheetId, Map extInfo) {
+        VKR vkr = (VKR) VKR.doGet("https://vika.cn/fusion/v1/datasheets/" + datasheetId + "/records", vkConf.initTokenHeader(), extInfo, VKR.RC_VK);
+        return (Map) vkr.getData();
+    }
+
+    /**
+     * 获取记录 [最大分页]
+     */
+    @Override
+    public List<Map> getRecordsAll(String datasheetId, Map extInfo) {
+        UtilMap.putNotNull(extInfo, "pageSize", 1000);
+        return (List<Map>) getRecords(datasheetId, extInfo).get("records");
+    }
+}

+ 9 - 1
mjava/src/main/java/com/malk/service/xbongbong/XBBClient.java

@@ -24,10 +24,18 @@ public interface XBBClient {
     List<Map> getFormDefine(@NotNull long formId, int subBusinessType);
 
     /**
-     * 客户列表接口/合同接口列表
+     * 表单列表接口
      *
      * @param conditions 详见 XBBConf.getCondition 格式说明
      * @param extInfo    其他非必填参数 [例: 每页数量,默认为20,最大值100]
      */
+    Map getDataResult(String url, @NotNull long formId, List<Map> conditions, Map extInfo);
+
+    /**
+     * 表单列表接口 [返回list]
+     */
     List<Map> getDataList(String url, @NotNull long formId, List<Map> conditions, Map extInfo);
+
+    /// 获取表单以及表单定义, 查询与数据处理, 获取后可在程序内固定, 避免无效调用
+    List<Map> testDefine(String name, @NotNull int saasMark, int businessType);
 }

+ 19 - 4
mjava/src/main/java/com/malk/service/xbongbong/impl/XBBImplClient.java

@@ -72,12 +72,12 @@ public class XBBImplClient implements XBBClient {
     }
 
     /**
-     * 客户列表接口/合同列表接口
+     * 表单列表接口
      *
      * @apiNote https://profapi.xbongbong.com/#/apilist/181
      */
     @Override
-    public List<Map> getDataList(String url, @NotNull long formId, List<Map> conditions, Map extInfo) {
+    public Map getDataResult(String url, @NotNull long formId, List<Map> conditions, Map extInfo) {
         Map body = UtilMap.map("formId, conditions, corpid, userId", formId, conditions, xbbConf.getCorpid(), xbbConf.getUserId());
         if (ObjectUtil.isNotNull(extInfo)) {
             body.putAll(extInfo);
@@ -85,7 +85,22 @@ public class XBBImplClient implements XBBClient {
             body.put("pageSize", 100);
         }
         XBBR xbbr = (XBBR) XBBR.doPost(url, getHeaderSign(body), null, body, VenR.RC_XBB);
-        Map<String, List<Map>> rsp = (Map<String, List<Map>>) xbbr.getResult();
-        return rsp.get("list");
+        return (Map) xbbr.getResult();
+    }
+
+    /**
+     * 表单列表接口 [返回list]
+     */
+    @Override
+    public List<Map> getDataList(String url, @NotNull long formId, List<Map> conditions, Map extInfo) {
+        return (List<Map>) getDataResult(url, formId, conditions, extInfo).get("list");
+    }
+
+    /// 获取表单以及表单定义, 查询与数据处理, 获取后可在程序内固定, 避免无效调用
+    @Override
+    public List<Map> testDefine(String name, @NotNull int saasMark, int businessType) {
+        List<Map> rsp = this.getFormList(name, saasMark, businessType);
+        long formId = UtilMap.getLong(rsp.get(0), "formId");
+        return this.getFormDefine(formId, businessType); // 表单定义
     }
 }

+ 49 - 4
mjava/src/main/java/com/malk/utils/UtilDateTime.java

@@ -9,6 +9,7 @@ import java.time.*;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAdjusters;
+import java.util.Calendar;
 import java.util.Date;
 
 /**
@@ -46,8 +47,8 @@ public abstract class UtilDateTime {
     }
 
     // 获取时间段内小时
-    public static float betweenHour(Temporal startInclusive, Temporal endExclusive) {
-        return Duration.between(startInclusive, endExclusive).toMillis() / 60f;
+    public static double betweenHour(Temporal startInclusive, Temporal endExclusive) {
+        return UtilNumber.formatPrecisionValue(Duration.between(startInclusive, endExclusive).toMinutes() / 60f);
     }
 
     // 获取上月第一天0点
@@ -56,6 +57,7 @@ public abstract class UtilDateTime {
         int year = dateTime.getYear();
         if (month == 0) {
             month = 12;
+            year -= 1;
         }
         return LocalDateTime.of(year, month, 1, 0, 0, 0);
     }
@@ -80,12 +82,17 @@ public abstract class UtilDateTime {
         return LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
     }
 
+    // 转中国时区时间戳
+    public static long getLocalDateTimeTimeStamp(LocalDateTime dataTime) {
+        return dataTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
+    }
+
     // 时间戳转为本地时间日期
     public static LocalDateTime getLocalDateTimeFromTimestamp(long millisecond) {
         return LocalDateTime.ofEpochSecond(millisecond / 1000, 0, ZoneOffset.ofHours(8));
     }
 
-    //// Date  ////
+    //// Date ////
 
     public static String formatDateTime(Date dateTime) {
         if (ObjectUtil.isNull(dateTime)) return "";
@@ -104,6 +111,11 @@ public abstract class UtilDateTime {
         return new SimpleDateFormat(pattern).format(dateTime);
     }
 
+    public static String formatQuarter(Date date) {
+        // 月份从0开始
+        return date.getYear() + "-" + (date.getMonth() / 3 + 1);
+    }
+
     @SneakyThrows
     public static Date parseDateTime(String dateStr) {
         if (StringUtils.isBlank(dateStr)) {
@@ -142,10 +154,19 @@ public abstract class UtilDateTime {
         return DateTimeFormatter.ofPattern(TIME_PATTERN).format(dateTime);
     }
 
-    public static String formatLocal(LocalTime dateTime, String pattern) {
+    public static String formatLocal(LocalDateTime dateTime, String pattern) {
         return DateTimeFormatter.ofPattern(pattern).format(dateTime);
     }
 
+    public static String formatLocal(LocalDate dateTime, String pattern) {
+        return DateTimeFormatter.ofPattern(pattern).format(dateTime);
+    }
+
+    public static String formatLocalQuarter(LocalDate date) {
+        // 月份从1开始
+        return date.getYear() + "-Q" + ((date.getMonth().getValue() - 1) / 3 + 1);
+    }
+
     public static LocalDateTime parseLocalDateTime(String dateStr) {
         if (StringUtils.isBlank(dateStr)) {
             return LocalDateTime.now();
@@ -164,4 +185,28 @@ public abstract class UtilDateTime {
     public static LocalTime parseLocal(String dateStr, String pattern) {
         return LocalTime.parse(dateStr, DateTimeFormatter.ofPattern(pattern));
     }
+
+
+    //// Calendar ////
+
+    // 获取上月最后一天
+    public static String lastDayOfNextMonth(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        // 指定日期月份减去一
+        cal.add(Calendar.MONTH, -1);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        cal.roll(Calendar.DAY_OF_MONTH, -1);
+        return formatDate(cal.getTime()) + " 23:59:59";
+    }
+
+    // 获取上月第一天
+    public static String firstDayOfNextMonth(Date date) {
+        Calendar cal = Calendar.getInstance();
+        cal.setTime(date);
+        // 指定日期月份减去一
+        cal.add(Calendar.MONTH, -1);
+        cal.set(Calendar.DAY_OF_MONTH, 1);
+        return formatDate(cal.getTime()) + " 00:00:00";
+    }
 }

+ 5 - 1
mjava/src/main/java/com/malk/utils/UtilExcel.java

@@ -51,7 +51,7 @@ public class UtilExcel {
         // 文件名兼容中文
         fileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
         response.setHeader("Content-Disposition", "attachment;filename*=utf-8" + fileName);
-        // 想要让客户端可以访问到其他的首部信息,服务器不仅要在header里加入该首部,还要将它们在 Access-Control-Expose-Headers 里面列出来
+        // 想要让客户端CellStyleModel可以访问到其他的首部信息,服务器不仅要在header里加入该首部,还要将它们在 Access-Control-Expose-Headers 里面列出来
         response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
     }
 
@@ -69,6 +69,8 @@ public class UtilExcel {
     // 字体大小
     @Builder.Default
     private int fontSize = 12;
+    @Builder.Default
+    private String fontName = "微软雅黑";
     // 行高
     @Builder.Default
     private int rowHeight = 30;
@@ -116,6 +118,7 @@ public class UtilExcel {
         // 设置表头字体
         XSSFFont fontHeader = wb.createFont();
         fontHeader.setFontHeightInPoints((short) fontSize);
+        fontHeader.setFontName(fontName);
         fontHeader.setBold(true);
         styleHeader.setFont(fontHeader);
 
@@ -133,6 +136,7 @@ public class UtilExcel {
         // 设置表格字体
         XSSFFont fontBody = wb.createFont();
         fontBody.setFontHeightInPoints((short) fontSize);
+        fontBody.setFontName(fontName);
         fontBody.setBold(false);
         bodyStyle.setFont(fontBody);
 

+ 101 - 96
mjava/src/main/java/com/malk/utils/UtilFile.java

@@ -1,6 +1,5 @@
 package com.malk.utils;
 
-import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import lombok.SneakyThrows;
 import org.apache.commons.codec.binary.Base64;
@@ -13,6 +12,8 @@ import java.util.Date;
 
 public abstract class UtilFile {
 
+    ////////////////////////////// File Path //////////////////////////////
+
     /**
      * 匹配路径: 自动追加年月日作为目录
      * -
@@ -29,6 +30,77 @@ public abstract class UtilFile {
         return file;
     }
 
+    /**
+     * 根据路径删除指定的目录或文件,无论存在与否
+     */
+    public static boolean DeleteFolder(String sPath) {
+        boolean flag = false;
+        File file = new File(sPath);
+        // 判断目录或文件是否存在
+        if (!file.exists()) {  // 不存在返回 false
+            return flag;
+        } else {
+            // 判断是否为文件
+            if (file.isFile()) {  // 为文件时调用删除文件方法
+                return deleteFile(sPath);
+            } else {  // 为目录时调用删除目录方法
+                return deleteDirectory(sPath);
+            }
+        }
+    }
+
+    /**
+     * 删除单个文件
+     */
+    public static boolean deleteFile(String sPath) {
+        boolean flag = false;
+        File file = new File(sPath);
+        // 路径为文件且不为空则进行删除
+        if (file.isFile() && file.exists()) {
+            file.delete();
+            flag = true;
+        }
+        return flag;
+    }
+
+    /**
+     * 删除目录(文件夹)以及目录下的文件
+     */
+    public static boolean deleteDirectory(String sPath) {
+        // 如果sPath不以文件分隔符结尾,自动添加文件分隔符
+        if (!sPath.endsWith(File.separator)) {
+            sPath = sPath + File.separator;
+        }
+        File dirFile = new File(sPath);
+        // 如果dir对应的文件不存在,或者不是一个目录,则退出
+        if (!dirFile.exists() || !dirFile.isDirectory()) {
+            return false;
+        }
+        boolean flag = true;
+        // 删除文件夹下的所有文件(包括子目录)
+        File[] files = dirFile.listFiles();
+        for (int i = 0; i < files.length; i++) {
+            // 删除子文件
+            if (files[i].isFile()) {
+                flag = deleteFile(files[i].getAbsolutePath());
+                if (!flag) break;
+            } // 删除子目录
+            else {
+                flag = deleteDirectory(files[i].getAbsolutePath());
+                if (!flag) break;
+            }
+        }
+        if (!flag) return false;
+        // 删除当前目录
+        if (dirFile.delete()) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    ////////////////////////////// File & 字节/流 //////////////////////////////
+
     /**
      * 文件转化为byte字节数组
      */
@@ -116,31 +188,13 @@ public abstract class UtilFile {
         }
     }
 
-    /**
-     * 本地路径转json string
-     */
-    @SneakyThrows
-    public static String readJsonStringFromFile(String path) {
-        File jsonFile = new File(path);
-        FileReader fileReader = new FileReader(jsonFile);
-
-        Reader reader = new InputStreamReader(new FileInputStream(jsonFile), "utf-8");
-        int ch = 0;
-        StringBuffer sb = new StringBuffer();
-        while ((ch = reader.read()) != -1) {
-            sb.append((char) ch);
-        }
-        fileReader.close();
-        reader.close();
-        return sb.toString();
-    }
+    ////////////////////////////// Resource 读取 //////////////////////////////
 
     /**
-     * 项目包文件, 读取 [服务Excel导出]
-     * [ppExt]
+     * 项目包文件, 读取 [ ppExt: 读取 Resource 必须 ]
+     * -
      * 1. ClassPathResource, 需要打包/编译后才能访问到. 识别不是架包内内容
-     * 2. 路径若 WebConfiguration 配置, 可使用配置别名
-     * 3. 若是读取本地json, 文件不能使用 .josn 后缀, 会被转义导致解析异常
+     * 2. 路径若 WebConfiguration 配置, 可使用配置别名. [详见 WebConfiguration]
      */
     @SneakyThrows
     public static InputStream readPackageResource(String path) {
@@ -150,86 +204,37 @@ public abstract class UtilFile {
     }
 
     /**
-     * 项目包文件, 读取 [服务读取本地json string]
-     */
-    public static String readPackageResourcePath(String path) {
-        return JSONUtil.class.getClassLoader().getResource(path).getPath();
-    }
-
-    /**
-     * 项目包文件, 读取 [服务读取本地json]
-     */
-    public static Object readJsonObjectFromFile(String path) {
-        String json = readJsonStringFromFile(readPackageResourcePath(path));
-        return JSON.parse(json);
-    }
-
-    /**
-     * 根据路径删除指定的目录或文件,无论存在与否
+     * 项目包JSON, 读取 [ ppExt: 读取 Resource 必须 ]
      */
-    public static boolean DeleteFolder(String sPath) {
-        boolean flag = false;
-        File file = new File(sPath);
-        // 判断目录或文件是否存在
-        if (!file.exists()) {  // 不存在返回 false
-            return flag;
-        } else {
-            // 判断是否为文件
-            if (file.isFile()) {  // 为文件时调用删除文件方法
-                return deleteFile(sPath);
-            } else {  // 为目录时调用删除目录方法
-                return deleteDirectory(sPath);
-            }
-        }
+    public static Object readJsonObjectFromResource(String path) {
+        return _readJsonObjectFromStream(readPackageResource(path));
     }
 
-    /**
-     * 删除单个文件
-     */
-    public static boolean deleteFile(String sPath) {
-        boolean flag = false;
-        File file = new File(sPath);
-        // 路径为文件且不为空则进行删除
-        if (file.isFile() && file.exists()) {
-            file.delete();
-            flag = true;
+    /// ppExt: 文件流, 转JSONObject
+    /// 1. 使用 JSONUtil.class.getClassLoader().getResource(path).getPath(); 本地访问路径正常, 部署服务器路径访问访问, 需要读取 Resource
+    /// 2. 若是读取本地json, 文件不能使用 .josn 后缀, 会被转义导致解析异常 / 但若是 Resource 资源则不受影响, [访问路径详见 WebConfiguration]
+    @SneakyThrows
+    private static Object _readJsonObjectFromStream(InputStream inputStream) {
+        Reader reader = new InputStreamReader(inputStream, "utf-8");
+        int ch = 0;
+        StringBuffer sb = new StringBuffer();
+        while ((ch = reader.read()) != -1) {
+            sb.append((char) ch);
         }
-        return flag;
+        reader.close();
+        return JSON.parse(sb.toString());
     }
 
     /**
-     * 删除目录(文件夹)以及目录下的文件
+     * 文件绝对路径转json
      */
-    public static boolean deleteDirectory(String sPath) {
-        // 如果sPath不以文件分隔符结尾,自动添加文件分隔符
-        if (!sPath.endsWith(File.separator)) {
-            sPath = sPath + File.separator;
-        }
-        File dirFile = new File(sPath);
-        // 如果dir对应的文件不存在,或者不是一个目录,则退出
-        if (!dirFile.exists() || !dirFile.isDirectory()) {
-            return false;
-        }
-        boolean flag = true;
-        // 删除文件夹下的所有文件(包括子目录)
-        File[] files = dirFile.listFiles();
-        for (int i = 0; i < files.length; i++) {
-            // 删除子文件
-            if (files[i].isFile()) {
-                flag = deleteFile(files[i].getAbsolutePath());
-                if (!flag) break;
-            } // 删除子目录
-            else {
-                flag = deleteDirectory(files[i].getAbsolutePath());
-                if (!flag) break;
-            }
-        }
-        if (!flag) return false;
-        // 删除当前目录
-        if (dirFile.delete()) {
-            return true;
-        } else {
-            return false;
-        }
+    @SneakyThrows
+    public static Object readJsonObjectFromFile(String absolutePath) {
+        File jsonFile = new File(absolutePath);
+        FileReader fileReader = new FileReader(jsonFile);
+        // 通过文件, 读取流转json对象
+        Object jsonObject = _readJsonObjectFromStream(new FileInputStream(jsonFile));
+        fileReader.close();
+        return jsonObject;
     }
 }

+ 62 - 5
mjava/src/main/java/com/malk/utils/UtilMap.java

@@ -3,11 +3,10 @@ package com.malk.utils;
 import cn.hutool.core.util.ObjectUtil;
 import com.malk.server.common.McException;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.map.HashedMap;
 import org.apache.commons.lang3.StringUtils;
 
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 
 @Slf4j
 public abstract class UtilMap {
@@ -68,6 +67,19 @@ public abstract class UtilMap {
         sMap.putAll(map(skeys, ckeys, cMap));
     }
 
+    /// 创建空对象
+    public static Map empty() {
+        return new HashedMap();
+    }
+
+    /// 判定 & 创建空对象
+    public static Map empty(Map data) {
+        if (ObjectUtil.isNotNull(data)) {
+            return data;
+        }
+        return new HashedMap();
+    }
+
     /************* 赋值 ************/
 
     /**
@@ -83,6 +95,20 @@ public abstract class UtilMap {
         return data;
     }
 
+    /**
+     * 非空对象全量合并
+     */
+    public static Map putAll(Map data, Map value) {
+        if (ObjectUtil.isNull(data)) {
+            return value;
+        }
+        if (ObjectUtil.isNotNull(value)) {
+            data.putAll(value);
+        }
+        return data;
+    }
+
+
     /**
      * 赋值 [值为0, 忽略]
      */
@@ -106,6 +132,9 @@ public abstract class UtilMap {
      */
     public static String getString(Map data, String key) {
         if (data.containsKey(key)) {
+            if (ObjectUtil.isNull(data.get(key))) {
+                return "";
+            }
             return String.valueOf(data.get(key));
         }
         return "";
@@ -123,14 +152,22 @@ public abstract class UtilMap {
      * 取值 [转为 int]
      */
     public static int getInt(Map data, String key) {
-        return Integer.valueOf(getString(data, key));
+        String txt = getString(data, key);
+        if (StringUtils.isBlank(txt)) {
+            return 0;
+        }
+        return Integer.valueOf(txt);
     }
 
     /**
      * 取值 [转为 float]
      */
     public static Float getFloat(Map data, String key) {
-        return Float.valueOf(getString(data, key));
+        String txt = getString(data, key);
+        if (StringUtils.isBlank(txt)) {
+            return 0f;
+        }
+        return Float.valueOf(txt);
     }
 
     /**
@@ -162,6 +199,26 @@ public abstract class UtilMap {
         return value;
     }
 
+    /**
+     * 取值 List
+     */
+    public static List getList(Map data, String key) {
+        if (data.containsKey(key)) {
+            return (List) data.get(key);
+        }
+        return new ArrayList();
+    }
+
+    /**
+     * 取值 List
+     */
+    public static Map getMap(Map data, String key) {
+        if (data.containsKey(key)) {
+            return (Map) data.get(key);
+        }
+        return new HashedMap();
+    }
+
     /************* 判定 ************/
 
     /**

+ 9 - 0
mjava/src/main/java/com/malk/utils/UtilMc.java

@@ -1,5 +1,6 @@
 package com.malk.utils;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
@@ -7,6 +8,7 @@ import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 public class UtilMc {
 
@@ -25,6 +27,13 @@ public class UtilMc {
         return t -> Objects.isNull(seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE));
     }
 
+    /**
+     * 集合内Map唯一值, 返回集合 [返回去重后数据本身]
+     */
+    public static List<Map> distinctByKey(List<Map> dataList, String key) {
+        return dataList.stream().filter(distinctByKey(item -> item.get(key))).collect(Collectors.toList());
+    }
+
     /**
      * 工具方法 [forEach 索引]
      */

+ 15 - 210
mjava/target/classes/META-INF/spring-configuration-metadata.json

@@ -40,41 +40,6 @@
       "type": "com.malk.server.common.FilePath$Path",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path",
-      "type": "com.malk.server.common.FilePath$Path",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
     {
       "name": "file.source",
       "type": "com.malk.server.common.FilePath$Source",
@@ -90,41 +55,6 @@
       "type": "com.malk.server.common.FilePath$Source",
       "sourceType": "com.malk.server.common.FilePath$Source"
     },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source",
-      "type": "com.malk.server.common.FilePath$Source",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
     {
       "name": "fxiaoke",
       "type": "com.malk.server.fxiaoke.FXKConf",
@@ -142,6 +72,11 @@
       "sourceType": "com.malk.config.mutilSource.DataSourceConfig",
       "sourceMethod": "slaveDataSource()"
     },
+    {
+      "name": "vika",
+      "type": "com.malk.server.vika.VKConf",
+      "sourceType": "com.malk.server.vika.VKConf"
+    },
     {
       "name": "xbongbong",
       "type": "com.malk.server.xbongbong.XBBConf",
@@ -213,6 +148,11 @@
       "type": "java.lang.String",
       "sourceType": "com.malk.server.dingtalk.DDConf"
     },
+    {
+      "name": "dingtalk.robot-code",
+      "type": "java.lang.String",
+      "sourceType": "com.malk.server.dingtalk.DDConf"
+    },
     {
       "name": "dingtalk.token",
       "type": "java.lang.String",
@@ -254,76 +194,6 @@
       "type": "java.lang.String",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.file",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.image",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
     {
       "name": "file.path.image",
       "type": "java.lang.String",
@@ -344,76 +214,6 @@
       "type": "java.lang.String",
       "sourceType": "com.malk.server.common.FilePath$Path"
     },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.path.tmp",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Path"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
-    {
-      "name": "file.source.fonts",
-      "type": "java.lang.String",
-      "sourceType": "com.malk.server.common.FilePath$Source"
-    },
     {
       "name": "file.source.fonts",
       "type": "java.lang.String",
@@ -444,6 +244,11 @@
       "type": "java.lang.String",
       "sourceType": "com.malk.server.fxiaoke.FXKConf"
     },
+    {
+      "name": "vika.api-token",
+      "type": "java.lang.String",
+      "sourceType": "com.malk.server.vika.VKConf"
+    },
     {
       "name": "xbongbong.callback-token",
       "type": "java.lang.String",

+ 1 - 0
pom.xml

@@ -21,6 +21,7 @@
         <module>mjava-shangfeng</module>
         <module>mjava-pake</module>
         <module>mjava-yangu</module>
+        <module>mjava-kuaikeli</module>
     </modules>
     <packaging>pom</packaging>