malk 2 kuukautta sitten
commit
8016dd7a94
100 muutettua tiedostoa jossa 10285 lisäystä ja 0 poistoa
  1. 42 0
      .gitignore
  2. 60 0
      mjava-aipocloud/pom.xml
  3. 65 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/Boot.java
  4. 77 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/controller/ABController.java
  5. 22 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/controller/DDController.java
  6. 68 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/delegate/DDDelegate.java
  7. 26 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/dao/ABEventDao.java
  8. 17 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/dao/ABUserDao.java
  9. 151 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/entity/AbEventPo.java
  10. 40 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/entity/AbUserPo.java
  11. 52 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/schedule/ABScheduleTask.java
  12. 21 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/service/ABClient.java
  13. 255 0
      mjava-aipocloud/src/main/java/com/malk/aipocloud/service/impl/ABImplClient.java
  14. 65 0
      mjava-aipocloud/src/main/resources/application-dev.yml
  15. 40 0
      mjava-aipocloud/src/main/resources/application-prod.yml
  16. 39 0
      mjava-aipocloud/src/main/resources/application-test.yml
  17. BIN
      mjava-aipocloud/src/main/resources/ssl/cp.100ali.com.jks
  18. BIN
      mjava-aipocloud/src/main/resources/ssl/server.jks
  19. 100 0
      mjava-aipocloud/src/main/resources/static/json/query.json
  20. 35 0
      mjava-aipocloud/src/test/resources/server.sh
  21. 54 0
      mjava-aiwei/pom.xml
  22. 32 0
      mjava-aiwei/src/main/java/com/malk/aiwei/Boot.java
  23. 72 0
      mjava-aiwei/src/main/java/com/malk/aiwei/controller/PLController.java
  24. 21 0
      mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBController.java
  25. 629 0
      mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBxYDController.java
  26. 53 0
      mjava-aiwei/src/main/java/com/malk/aiwei/delegate/TBDelegate.java
  27. 87 0
      mjava-aiwei/src/main/java/com/malk/aiwei/schedule/AWScheduleTask.java
  28. 43 0
      mjava-aiwei/src/main/java/com/malk/aiwei/schedule/Timer.java
  29. 71 0
      mjava-aiwei/src/main/java/com/malk/aiwei/server/AWServer.java
  30. 88 0
      mjava-aiwei/src/main/java/com/malk/aiwei/server/YDSearch.java
  31. 130 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/AWClint.java
  32. 59 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/AWYDClient.java
  33. 9 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/AwDingService.java
  34. 13 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/GZT_PN.java
  35. 2100 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWImplClient.java
  36. 1018 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWYDImplClient.java
  37. 79 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AwDingServiceImpl.java
  38. 120 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/GZT_PNImpl.java
  39. 86 0
      mjava-aiwei/src/main/resources/application-dev.yml
  40. 45 0
      mjava-aiwei/src/main/resources/application-local.yml
  41. 46 0
      mjava-aiwei/src/main/resources/application-prod.yml
  42. 46 0
      mjava-aiwei/src/main/resources/application-test.yml
  43. 46 0
      mjava-aiwei/src/main/resources/application-tmp.yml
  44. BIN
      mjava-aiwei/src/main/resources/templates/templates_checked_list.xlsx
  45. BIN
      mjava-aiwei/src/main/resources/templates/templates_project_requirements.xlsx
  46. BIN
      mjava-aiwei/src/main/resources/templates/templates_project_requirements_exception.xlsx
  47. BIN
      mjava-aiwei/src/main/resources/templates/templates_project_requirements_test.xlsx
  48. BIN
      mjava-aiwei/src/main/resources/templates/templates_project_specification.xlsx
  49. BIN
      mjava-aiwei/src/main/resources/templates/templates_project_specification_exception.xlsx
  50. BIN
      mjava-aiwei/src/main/resources/templates/templates_test_specification.xlsx
  51. BIN
      mjava-aiwei/src/main/resources/templates/templates_test_specification_exception.xlsx
  52. BIN
      mjava-aiwei/src/main/resources/templates/templates_xqrk.xlsx
  53. BIN
      mjava-aiwei/src/main/resources/templates/templates_xqwh.xlsx
  54. BIN
      mjava-aiwei/src/main/resources/templates/templates_xqwh_result.xlsx
  55. 79 0
      mjava-aiwei/src/test/java/com/malk/aiwei/AwTbTest.java
  56. 89 0
      mjava-aiwei/src/test/java/com/malk/aiwei/DataHandingTest.java
  57. 60 0
      mjava-aiwei/src/test/java/testLocal.java
  58. 35 0
      mjava-aiwei/src/test/resources/server.sh
  59. 54 0
      mjava-cloudpure/pom.xml
  60. 32 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/Boot.java
  61. 21 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/DDController.java
  62. 113 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/GTZZController.java
  63. 219 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/TLYController.java
  64. 518 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/TSController.java
  65. 146 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/XBBController.java
  66. 63 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/delegate/DDDelegate.java
  67. 86 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/schedule/CPScheduleTask.java
  68. 42 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/service/CPClient.java
  69. 362 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/service/impl/CPImplClient.java
  70. 76 0
      mjava-cloudpure/src/main/resources/application-dev.yml
  71. 51 0
      mjava-cloudpure/src/main/resources/application-prod.yml
  72. 35 0
      mjava-cloudpure/src/test/resources/server.sh
  73. 54 0
      mjava-dongfangxinhua/pom.xml
  74. 32 0
      mjava-dongfangxinhua/src/main/java/com/malk/dongfangxinhua/Boot.java
  75. 161 0
      mjava-dongfangxinhua/src/main/java/com/malk/dongfangxinhua/controller/DFXHController.java
  76. 65 0
      mjava-dongfangxinhua/src/main/resources/application-dev.yml
  77. 38 0
      mjava-dongfangxinhua/src/main/resources/application-prod.yml
  78. 39 0
      mjava-dongfangxinhua/src/test/resource/server.sh
  79. 54 0
      mjava-fengkaili/pom.xml
  80. 32 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/Boot.java
  81. 152 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java
  82. 23 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/dao/FKLDdContactDao.java
  83. 72 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/entity/FKLDdContactPo.java
  84. 35 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/schedule/FKLScheduleTask.java
  85. 28 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/FKLService.java
  86. 659 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/impl/FKLImplService.java
  87. 60 0
      mjava-fengkaili/src/main/resources/application-dev.yml
  88. 35 0
      mjava-fengkaili/src/main/resources/application-prod.yml
  89. BIN
      mjava-fengkaili/src/main/resources/templates/Template_days.xlsx
  90. BIN
      mjava-fengkaili/src/main/resources/templates/Template_month.xlsx
  91. 39 0
      mjava-fengkaili/src/test/resource/server.sh
  92. 54 0
      mjava-gewu/pom.xml
  93. 32 0
      mjava-gewu/src/main/java/com/malk/gewu/Boot.java
  94. 113 0
      mjava-gewu/src/main/java/com/malk/gewu/controller/GWController.java
  95. 35 0
      mjava-gewu/src/main/java/com/malk/gewu/schedule/GWScheduleTask.java
  96. 20 0
      mjava-gewu/src/main/java/com/malk/gewu/service/GWService.java
  97. 131 0
      mjava-gewu/src/main/java/com/malk/gewu/service/impl/GWImplService.java
  98. 81 0
      mjava-gewu/src/main/resources/application-dev.yml
  99. 38 0
      mjava-gewu/src/main/resources/application-prod.yml
  100. 0 0
      mjava-gewu/src/main/resources/static/json/personnel

+ 42 - 0
.gitignore

@@ -0,0 +1,42 @@
+# IntelliJ project files
+.idea
+*.iml
+out
+gen
+### Java template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+/log/
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+mvnw
+mvnw.cmd
+
+# tmp file
+.tmp
+*/tmp
+*/target
+
+# web2 file
+*/src/main/resources/static/web
+*/src/main/resources/static/mjs

+ 60 - 0
mjava-aipocloud/pom.xml

@@ -0,0 +1,60 @@
+<?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-aipocloud</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>
+        <!-- sqlserver 依赖-->
+        <dependency>
+            <groupId>com.microsoft.sqlserver</groupId>
+            <artifactId>mssql-jdbc</artifactId>
+            <scope>runtime</scope>
+        </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>

+ 65 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/Boot.java

@@ -0,0 +1,65 @@
+package com.malk.aipocloud;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import org.apache.catalina.connector.Connector;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
+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. 标识对表没有任何操作. 若不设置为 none, 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);
+    }
+
+    /**
+     * todo 是否集成到基础框架
+     * 配置Tomcat下Https [访问格式为: https + 域名 + 端口 + 路径]
+     * 1. 在resource下添加/ssl/证书, jsk后缀文件 (证书类型为jks, 非Tomcat下pfx)
+     * 2. yml文件配置ssl: 路径\密码\类型(JKS)  [密码\路径不对程序启动异常]
+     * 3. 启动类重定向和端口 (http服务于默认端口服务不能是同一个端口, 相当于是同时启动两个端口)
+     */
+    @Bean
+    public ServletWebServerFactory servletContainer() {
+
+        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
+        tomcat.addAdditionalTomcatConnectors(createHTTPConnector());
+        return tomcat;
+    }
+
+    @Value("${server.ssl.http}")
+    private int httpPort;
+
+    private Connector createHTTPConnector() {
+
+        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
+        // ppExt: 同时启用http服务端口, 原默认端口只能https访问, 此处http端口仅支持http访问 [证书匹配后, localhost/127.0.0.1仅支持https访问]
+        connector.setScheme("http");
+        connector.setSecure(false);
+        connector.setPort(httpPort);
+        connector.setRedirectPort(httpPort);
+        return connector;
+    }
+}

+ 77 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/controller/ABController.java

@@ -0,0 +1,77 @@
+package com.malk.aipocloud.controller;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+
+import com.malk.aipocloud.repository.dao.ABEventDao;
+import com.malk.aipocloud.service.ABClient;
+import com.malk.delegate.McDelegate;
+import com.malk.server.common.McR;
+import com.malk.utils.UtilDateTime;
+import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Slf4j
+@RestController
+@RequestMapping
+public class ABController {
+
+    @Autowired
+    private ABClient abClient;
+
+    @Autowired
+    private ABEventDao abEventDao;
+
+    /**
+     * 同步审批单
+     */
+    @PostMapping("process/sync")
+    McR syncProcess() {
+
+        abClient.syncProcess(UtilDateTime.parseLocalDateTime("2023-11-29 00:00:00"), UtilDateTime.parseLocalDateTime("2023-11-29 23:59:59"));
+//        LocalDateTime now = LocalDateTime.now().minusHours(8);
+//        abClient.syncProcess(now.minusMinutes(5), now);
+        return McR.success();
+    }
+
+    /**
+     * 通讯录同步
+     */
+    @Synchronized
+    @PostMapping("contact/sync")
+    McR syncContact() {
+
+        log.info("手动触发, 通讯录同步");
+        abClient.syncContact();
+        return McR.success();
+    }
+
+
+    @Autowired
+    private McDelegate mcDelegate;
+
+    @Value("${server.ssl.key-store}")
+    private String keyStore;
+
+    @Value("${server.ssl.key-store-password}")
+    private String keyStorePassword;
+
+    @GetMapping("test")
+    McR test() {
+
+        log.info("11111, {}, {}", keyStore, keyStorePassword);
+
+        mcDelegate.setTimeout(() -> {
+
+            log.info("22222");
+        }, 5000);
+        return McR.success();
+    }
+}

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

@@ -0,0 +1,22 @@
+package com.malk.aipocloud.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/xxx/dd/callback [调试代理: frp + nginx]
+ */
+@Slf4j
+@RestController
+@RequestMapping("/dd")
+public class DDController extends DDCallbackController {
+
+
+}

+ 68 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/delegate/DDDelegate.java

@@ -0,0 +1,68 @@
+package com.malk.aipocloud.delegate;
+
+import com.malk.aipocloud.service.ABClient;
+import com.malk.delegate.DDEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Primary;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+/**
+ * OA审批事件 [主项目也有实现, 添加 @Primary 优先注入主项目实现]
+ * -
+ * 取消方案: 撤销和拒绝流程不继续执行连接器, 因此不使用连接器与轮询审批记录方案 [低效而且占用较高钉钉api调次数];
+ * 优化方案: 通过事件订阅实现实时同步所有审批状态, 定时查询钉钉回调失败记录 [配置钉钉事件Delegate, 添加定时任务]
+ */
+@Slf4j
+@Service
+@Primary
+public class DDDelegate implements DDEvent {
+
+    // 审批任务回调执行业务逻辑
+    @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");
+    }
+
+    @Autowired
+    private ABClient abClient;
+
+    // 审批实例回调执行业务逻辑
+    @Async
+    @Override
+    public void executeEvent_Instance_Finish(String processInstanceId, String processCode, boolean isAgree, boolean isTerminate, String staffId) {
+        log.info("executeEvent_Instance_Finish");
+        String approveResult = isAgree ? "agree" : "refuse";
+        if (isTerminate) approveResult = "terminated";
+        log.info("审批实例回调执行业务逻辑, {}", approveResult);
+        abClient.callbackApprove(processCode, processInstanceId, approveResult, staffId);
+    }
+
+    // 考勤打卡事件回调
+    @Override
+    public void executeEvent_attendance_check(Map<String, ?> record) {
+        log.info("executeEvent_attendance_check");
+    }
+}

+ 26 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/dao/ABEventDao.java

@@ -0,0 +1,26 @@
+package com.malk.aipocloud.repository.dao;
+
+import com.malk.aipocloud.repository.entity.AbEventPo;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 钉钉花名册同步
+ */
+@Transactional
+public interface ABEventDao extends JpaRepository<AbEventPo, Long> {
+
+    AbEventPo findByInstanceId(String instanceId);
+
+    @Query("SELECT m from AbEventPo m WHERE (m.detectDateTime >= ?1 AND m.detectDateTime <= ?2) AND m.channelType = ?3 AND m.logonName = ?4 and (m.state = 'process' or (m.state = 'agree' and m.expire > ?5 ))")
+    List<AbEventPo> queryList(Date sTime, Date eTime, int channel, String name, Date expire);
+
+    @Modifying
+    @Query("update AbEventPo m set m.state = ?2, m.sync = ?3 where m.instanceId = ?1")
+    void updateState(String instanceId, String state, String sync);
+}

+ 17 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/dao/ABUserDao.java

@@ -0,0 +1,17 @@
+package com.malk.aipocloud.repository.dao;
+
+import com.malk.aipocloud.repository.entity.AbUserPo;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import javax.transaction.Transactional;
+
+/**
+ * 钉钉花名册同步
+ */
+@Transactional
+public interface ABUserDao extends JpaRepository<AbUserPo, Long> {
+
+    AbUserPo findByEmail(String email);
+
+    AbUserPo findByUserId(String userId);
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 151 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/entity/AbEventPo.java


+ 40 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/repository/entity/AbUserPo.java

@@ -0,0 +1,40 @@
+package com.malk.aipocloud.repository.entity;
+
+import com.malk.base.BasePo;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Table;
+
+@Entity
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Table(name = "aipocloud_user")
+public class AbUserPo extends BasePo {
+
+    /**
+     * 员工Id
+     */
+    private String userId;
+    
+    /**
+     * 员工姓名
+     */
+    private String name;
+
+    /**
+     * email
+     */
+    private String email;
+
+
+    /**
+     * 部门名称
+     */
+    private String dept;
+}

+ 52 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/schedule/ABScheduleTask.java

@@ -0,0 +1,52 @@
+package com.malk.aipocloud.schedule;
+
+import com.malk.aipocloud.service.ABClient;
+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.LocalDateTime;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class ABScheduleTask {
+
+    @Autowired
+    private ABClient abClient;
+
+    /**
+     * 同步审批单: 查询5min, 避免拒绝\撤销重复推送问题
+     */
+    @Scheduled(cron = "0 1/5 * * * ?")
+    public void timer_1() {
+        try {
+            // todo utc时间和gmt+8时间,是utc加上8小时就是gmt时间
+            LocalDateTime now = LocalDateTime.now().minusHours(8);
+            abClient.syncProcess(now.minusMinutes(5), now);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 同步通讯录: 每小时3分执行, 错开审批单同步
+     */
+    @Scheduled(cron = "0 3 * * * ?")
+    public void timer_2() {
+        try {
+            abClient.syncContact();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 21 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/service/ABClient.java

@@ -0,0 +1,21 @@
+package com.malk.aipocloud.service;
+
+import java.time.LocalDateTime;
+
+public interface ABClient {
+
+    /**
+     * 通讯录同步
+     */
+    void syncContact();
+
+    /**
+     * 同步审批单 [prd 审批去重逻辑:用户 + 通道 + md5 + 有效期(审批通过且在生效时间内)] - 查询5min, 避免拒绝\撤销重复推送问题
+     */
+    void syncProcess(LocalDateTime sTime, LocalDateTime eTime);
+
+    /**
+     * 审批回调
+     */
+    void callbackApprove(String processCode, String processInstanceId, String state, String staffId);
+}

+ 255 - 0
mjava-aipocloud/src/main/java/com/malk/aipocloud/service/impl/ABImplClient.java

@@ -0,0 +1,255 @@
+package com.malk.aipocloud.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.malk.aipocloud.repository.dao.ABEventDao;
+import com.malk.aipocloud.repository.dao.ABUserDao;
+import com.malk.aipocloud.repository.entity.AbEventPo;
+import com.malk.aipocloud.repository.entity.AbUserPo;
+import com.malk.aipocloud.service.ABClient;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDFormComponentDto;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDClient_Notice;
+import com.malk.service.dingtalk.DDClient_Workflow;
+import com.malk.utils.*;
+import lombok.SneakyThrows;
+import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class ABImplClient implements ABClient {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private ABUserDao abUserDao;
+
+    /**
+     * 通讯录同步
+     */
+    @Override
+    @Synchronized
+    public void syncContact() {
+
+        List<Long> deptList = ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true, DDConf.TOP_DEPARTMENT);
+        for (long deptId : deptList) {
+            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
+            if (userIds.size() == 0) {
+                continue;
+            }
+            String deptName = String.valueOf(ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), deptId).get("name"));
+            for (String userId : userIds) {
+                Map userInfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
+                log.info("同步人员, {}", userInfo);
+                if (StringUtils.isBlank(UtilMap.getString(userInfo, "email"))) {
+                    continue;
+                }
+                AbUserPo userPo = AbUserPo.builder()
+                        .userId(userId)
+                        .name(String.valueOf(userInfo.get("name")))
+                        .dept(deptName)
+                        .email(String.valueOf(userInfo.get("email")))
+                        .build();
+                userPo.upsert(abUserDao.findByUserId(userId)); // 匹配更新
+                abUserDao.save(userPo);
+            }
+        }
+    }
+
+    @Autowired
+    private DDClient_Workflow ddClient_workflow;
+
+    @Autowired
+    private ABEventDao abEventDao;
+
+    @Autowired
+    private DDConf ddConf;
+
+    // DLP
+    private static final String CODE = "PROC-FA464E73-56A4-43DF-A9DD-CCE11114C129";
+    //private static final String CODE = "PROC-B501FF97-E6FC-41D5-8704-D988D4878F45"; // poc
+    private static final String USER = "dingTalk";
+    private static final int DLP_DAYS = 1;
+    private static final int DLP_NUM = 10;
+
+    private String _getUrl(String path) {
+        return "https://10.14.2.5:8443" + path; // 内网
+//        return "https://ucss.aipocloud.com:8443" + path; // 公网
+    }
+
+    // 认证逻辑: token作为后续接口密码
+    private String _getToken() {
+
+        Map header = UtilMap.map("content-type, user-agent", "application/json, QKAct-External-Client");
+        Map body = UtilMap.map("client-id", USER);
+        String rsp = UtilHttp.doRequest(UtilHttp.METHOD.POST, _getUrl("/qkact/v0/checkin"),
+                header, null, body, null, "dingtalk_dlp", "SkyP@ssw0rd1!");
+        Map data = (Map) JSON.parse(rsp);
+        return UtilMap.getString(data, "access-token");
+    }
+
+    /**
+     * 同步审批单 [prd 审批去重逻辑:用户 + 通道 + md5 + 有效期(审批通过且在生效时间内)] - 查询5min, 避免拒绝\撤销重复推送问题
+     */
+    @Override
+    @Synchronized
+    public void syncProcess(LocalDateTime sTime, LocalDateTime eTime) {
+
+        String parttern = "yyyy-MM-dd'T'HH:mm:ss";
+        String pwd = _getToken();
+        Map query = (Map) UtilFile.readJsonObjectFromResource("static/json/query.json");  // 事件查询条件: 查询5min, 避免拒绝\撤销重复推送问题
+        query = (Map) JSON.parse(JSON.toJSONString(query).replace("xxxx-xx-xxSxx:xx:xx", UtilDateTime.formatLocal(sTime, parttern)).replace("xxxx-xx-xxExx:xx:xx", UtilDateTime.formatLocal(eTime, parttern)));
+
+        Map header = UtilMap.map("content-type, user-agent", "application/json, QKAct-External-Client");
+        String rsp = UtilHttp.doRequest(UtilHttp.METHOD.POST, _getUrl("/sps/v1/proxy/dlp-endpoint/_search"), header, null, query, null, USER, pwd);
+        Map data = (Map) JSON.parse(rsp);
+        List<Map> list = (List<Map>) UtilMap.getList(UtilMap.getMap(data, "hits"), "hits");
+        for (Map record : list) {
+
+            record = UtilMap.getMap(record, "_source");
+            List<Map> attachments = (List<Map>) record.get("attachments");
+            if (UtilList.isEmpty(attachments)) {
+                continue;
+            }
+            // prd 审批去重逻辑:用户 + 通道 + md5 + 有效期(审批通过且在生效时间内)
+            Map<String, String> source = UtilMap.getMap(record, "source");
+            int channel = UtilMap.getInt(record, "channelType");
+            Date detect = UtilDateTime.parse(String.valueOf(record.get("detectDateTime")), "yyyy-MM-dd'T'HH:mm:ss.SSS");
+            Date expire = new Date(detect.getTime() + DLP_DAYS * 24 * 60 * 60 * 1000L);
+            List<AbEventPo> eventPoList = abEventDao.queryList(UtilDateTime.convertToDateFromLocalDateTime(sTime), UtilDateTime.convertToDateFromLocalDateTime(eTime), channel, source.get("logonName"), expire);
+            if (eventPoList.stream().filter(item -> attachments.get(0).get("fileMd5").equals(item.getAttachmentsDisplay().get(0).get("fileMd5"))).findAny().isPresent()) {
+                log.info("记录已存在, {}", record);
+                continue;
+            }
+            // 邮箱错误通知管理员
+            String userId = "";
+            AbUserPo userPo = abUserDao.findByEmail(source.get("mail"));
+            if (ObjectUtil.isNull(userPo)) {
+                Map message = UtilMap.map("msgtype", "text");
+                message.put("text", UtilMap.map("content", "「" + source.get("mail") + "」: 未匹配到客户,需要维护花名册邮箱信息。"));
+                ddClient_notice.sendNotification(ddClient.getAccessToken(), Arrays.asList(ddConf.getOperator()), null, false, message);
+                continue;
+            }
+            userId = userPo.getUserId();
+            //userId = ddConf.getOperator(); // test
+            List<Map<String, String>> policies = UtilMap.getList(record, "matchedPolicies");
+            List<String> tPolicies = policies.stream().map(item -> item.get("name")).collect(Collectors.toList());
+            List<Map<String, String>> rules = UtilMap.getList(record, "extendIncidentRules");
+            List<String> tRules = rules.stream().map(item -> item.get("name")).collect(Collectors.toList());
+            List<Map<String, String>> destinations = UtilMap.getList(record, "destinations");
+            List<String> tDestinations = destinations.stream().map(item -> item.get("displayName")).collect(Collectors.toList());
+
+            AbEventPo eventPo = AbEventPo.builder()
+                    .serialId(UtilMap.getInt(record, "serialId"))
+                    .userId(userId)
+                    .userName(userPo.getName())
+                    .logonName(source.get("logonName"))
+                    .department(source.get("department"))
+                    .fqdn(source.get("fqdn"))
+                    .ipAddress(source.get("ipAddress"))
+                    .email(source.get("mail"))
+                    .detectDateTime(detect)
+                    .expire(expire)
+                    .channelType(channel)
+                    .channelTypeDisplay(AbEventPo.convertChannelTypeDisplay(channel))
+                    .policiesName(String.join(", ", tPolicies))
+                    .classificationLevelNames(String.join(", ", UtilMap.getList(record, "classificationLevelNames")))
+                    .rulesName(String.join(", ", tRules))
+                    .tagNames(String.join(", ", UtilMap.getList(record, "tagNames")))
+                    .displayName(String.join(", ", tDestinations))
+                    .attachments(JSON.toJSONString(attachments))
+                    .attachmentsDisplay(attachments)
+                    .build();
+            // 组件数据格式化: prd 发送通道钉钉取值发送的目标名称
+            Map ruleForm = UtilMap.map("logonName, department, fqdn, detectDateTime, displayName, tagNames",
+                    "用户姓名, 用户所在部门, 计算机名称, DLP事件产生时间, 发送通道, 文件标签名称");
+            Map ruleDetail = UtilMap.map("attachmentsDisplay", UtilMap.map("filename, fileSize, fileMd5", "附件名称, 附件大小, 附件MD5"));
+            ruleForm.put("attachmentsDisplay", "附件信息");
+
+            // 推送钉钉审批
+            Map formData = (Map<String, ?>) JSON.parse(JSON.toJSONString(eventPo));
+            detect = new Date(detect.getTime() + 8 * 60 * 60 * 1000L); // prd utc转gmt显示
+            formData.putAll(UtilMap.map("detectDateTime, logonName", UtilDateTime.formatDateTime(detect), JSON.toJSONString(Arrays.asList(userId)))); // 默认值
+            List<Map> formValues = DDFormComponentDto.formatComponentValues(formData, ruleForm, ruleDetail);
+            Map extInfo = UtilMap.map("dept_id", DDConf.TOP_DEPARTMENT);
+            String processInstanceId = ddClient_workflow.doProcessInstances(ddClient.getAccessToken(), userId, CODE, formValues, extInfo);
+            eventPo.setInstanceId(processInstanceId);
+            eventPo.setState("process");
+            abEventDao.save(eventPo);
+            log.info("推送钉钉审批, {}", eventPo);
+        }
+    }
+
+    @Autowired
+    private DDClient_Notice ddClient_notice;
+
+    /**
+     * 审批回调
+     */
+    @SneakyThrows
+    @Override
+    public void callbackApprove(String processCode, String processInstanceId, String state, String staffId) {
+        // 回调结果
+        if (CODE.equals(processCode)) {
+
+            if (!"agree".equals(state)) {
+                abEventDao.updateState(processInstanceId, state, null);
+                return;
+            }
+            // prd: 审批去重逻辑:用户 + 通道 + md5 +有效期(审批通过且在生效时间内)
+            AbEventPo abEventPo = abEventDao.findByInstanceId(processInstanceId);
+            List<Map> attachments = abEventPo.getAttachmentsDisplay();
+            if (attachments.isEmpty()) {
+                return;
+            }
+            // 匹配审批人信息
+            Map userInfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), staffId);
+            Map data = UtilMap.map("approver, submitter_fqdn, approved_time, forensic", userInfo.get("name"), abEventPo.getFqdn(), new Date().getTime() / 1000L, 1);
+            data.putAll(UtilMap.map("submitter_name, submitter_ip, submitter_email", abEventPo.getLogonName(), abEventPo.getIpAddress().split(",")[0], abEventPo.getEmail()));
+            Map attachment = attachments.get(0);
+            data.putAll(UtilMap.map("file_size, file_md5, file_name", attachment.get("fileSize"), attachment.get("fileMd5"), attachment.get("filename")));
+            // prd channel 号 是100-106 的对应成22 通道进行回传
+            int channel = abEventPo.getChannelType();
+            if (channel >= 100 && channel <= 106) {
+                channel = 22;
+            }
+            data.put("channel", Arrays.asList(channel));
+            // 匹配单据必填项
+            //List<Map<String, String>> formComponentValues = UtilMap.getList(instance, "formComponentValues");
+            //String expired = formComponentValues.stream().filter(item -> "过期时间".equals(item.get("name"))).findAny().get().get("value");
+            //data.put("expired_time", UtilDateTime.parse(expired, "yyyy-MM-dd HH:mm").getTime());
+            //data.put("max_num", formComponentValues.stream().filter(item -> "可发送次数".equals(item.get("name"))).findAny().get().get("value"));
+            data.put("expired_time", abEventPo.getExpire().getTime() / 1000L);
+            data.put("max_num", DLP_NUM);
+
+            Map body = UtilMap.map("data", Arrays.asList(data));
+            Map header = UtilMap.map("content-type, user-agent", "application/json, QKAct-External-Client");
+            String rsp = UtilHttp.doRequest(UtilHttp.METHOD.POST, _getUrl("/qkact/v0/dlp/incident/approval"),
+                    header, null, body, null, USER, _getToken());
+            abEventDao.updateState(processInstanceId, state, rsp);
+            // 延迟5分钟通知
+            Thread.sleep(5 * 60 * 1000);
+            Map message = UtilMap.map("msgtype", "text");
+            String tips = "「" + abEventPo.getUserName() + "」,您的文件:「" + attachment.get("filename") + "」通过「" + abEventPo.getDisplayName() + "」外发的审批已通过,截止「" + UtilDateTime.formatDateTime(abEventPo.getExpire()) + "」前有效,请重新发起文件外发。";
+            message.put("text", UtilMap.map("content", tips));
+            ddClient_notice.sendNotification(ddClient.getAccessToken(), Arrays.asList(abEventPo.getUserId()), null, false, message);
+        }
+    }
+}

+ 65 - 0
mjava-aipocloud/src/main/resources/application-dev.yml

@@ -0,0 +1,65 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/aipocloud
+  ssl:
+    key-store: classpath:ssl/cp.100ali.com.jks
+    key-store-password: n91xkery
+    keyStoreType: JKS      # 证书类型为jks, 非Tomcat下pfx [密码\路径不对程序启动异常]
+    http: 9000             # 同时启用http服务端口, 原默认端口只能https访问, 此处http端口仅支持http访问
+
+# 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
+    # 主库
+    primary:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+# dingtalk
+dingtalk:
+  agentId: 2704247534
+  appKey: dingqmijjypfepl0tsuq
+  appSecret: dixOqjK4Zw8PajvrtY1mbKxs4DIJJJmq6WvqdSDTCStBWAPyTeobgQFxZ1VhH-Z3
+  corpId: ding321c72787fffc78b35c2f4657eb6378f
+  aesKey: vBtjZT6yIJXPywnOqmMHYUyPBpglTostOMpdQIMrSHk
+  token: ngnPYIwW5RfZwxnb1OjQJMb6U62NYKbvxGtcVaYe1hRaaKM1j8qG
+  operator: "095358016629044412"   # OA管理员账号
+
+

+ 40 - 0
mjava-aipocloud/src/main/resources/application-prod.yml

@@ -0,0 +1,40 @@
+# 环境配置
+server:
+  port: 9021
+  servlet:
+    context-path: /api/aipocloud
+  ssl:
+    key-store: classpath:ssl/cp.100ali.com.jks
+    key-store-password: n91xkery
+    keyStoreType: JKS      # 证书类型为jks, 非Tomcat下pfx [密码\路径不对程序启动异常]
+    http: 9022             # 同时启用http服务端口, 原默认端口只能https访问, 此处http端口仅支持http访问
+
+# condition
+spel:
+  scheduling: true         # 定时任务是否执行
+  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: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+    hibernate:
+      ddl-auto: none
+
+# dingtalk
+dingtalk:
+  agentId: 2791266100
+  appKey: ding0dmmeh4ufremtbkp
+  appSecret: RHE6gWY-0PBUSFKNF6j__v1II9WD5h0u2TZDK3QwKKczivXid2br_SMWK-EKSDLT
+  corpId: dingbc79c496d04b812f35c2f4657eb6378f
+  aesKey: vBtjZT6yIJXPywnOqmMHYUyPBpglTostOMpdQIMrSHk
+  token: ngnPYIwW5RfZwxnb1OjQJMb6U62NYKbvxGtcVaYe1hRaaKM1j8qG
+  operator: "0220663466860353"   # OA管理员账号

+ 39 - 0
mjava-aipocloud/src/main/resources/application-test.yml

@@ -0,0 +1,39 @@
+# 环境配置
+server:
+  port: 9091
+  servlet:
+    context-path: /api/aipocloud
+  ssl:
+    key-store: classpath:ssl/server.jks
+    key-store-password: jJFTKCDX5mP9uvFb
+    keyStoreType: JKS      # 证书类型为jks, 非Tomcat下pfx [密码\路径不对程序启动异常]
+    http: 9090             # 同时启用http服务端口, 原默认端口只能https访问, 此处http端口仅支持http访问
+
+# condition
+spel:
+  scheduling: true         # 定时任务是否执行
+  multiSource: false       # 是否多数据源配置
+
+spring:
+  # database
+  datasource:
+    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
+    url: jdbc:sqlserver://127.0.0.1:1433;SelectMethod=cursor;DatabaseName=dingtalk
+    username: DingTalk
+    password: Dingding2023@#
+    # JPA
+    jpa:
+      database: sql_server
+      properties:
+        hibernate:
+          default_schema: dbo
+
+# dingtalk
+dingtalk:
+  agentId: 2791266100
+  appKey: ding0dmmeh4ufremtbkp
+  appSecret: RHE6gWY-0PBUSFKNF6j__v1II9WD5h0u2TZDK3QwKKczivXid2br_SMWK-EKSDLT
+  corpId: dingbc79c496d04b812f35c2f4657eb6378f
+  aesKey: vBtjZT6yIJXPywnOqmMHYUyPBpglTostOMpdQIMrSHk
+  token: ngnPYIwW5RfZwxnb1OjQJMb6U62NYKbvxGtcVaYe1hRaaKM1j8qG
+  operator: "0220663466860353"   # OA管理员账号

BIN
mjava-aipocloud/src/main/resources/ssl/cp.100ali.com.jks


BIN
mjava-aipocloud/src/main/resources/ssl/server.jks


+ 100 - 0
mjava-aipocloud/src/main/resources/static/json/query.json

@@ -0,0 +1,100 @@
+{
+  "from": 0,
+  "size": 1000,
+  "query": {
+    "bool": {
+      "must": [
+        {
+          "range": {
+            "detectDateTime": {
+              "from": "xxxx-xx-xxSxx:xx:xx.000+0000",
+              "to": "xxxx-xx-xxExx:xx:xx.000+0000",
+              "include_lower": true,
+              "include_upper": true,
+              "boost": 1
+            }
+          }
+        },
+        {
+          "terms": {
+            "actionType": [
+              13
+            ],
+            "boost": 1
+          }
+        },
+        {
+          "terms": {
+            "isVisible": [
+              true
+            ],
+            "boost": 1
+          }
+        },
+        {
+          "bool": {
+            "should": [
+              {
+                "terms": {
+                  "isOrdinary": [
+                    true
+                  ],
+                  "boost": 1
+                }
+              },
+              {
+                "bool": {
+                  "must_not": [
+                    {
+                      "exists": {
+                        "field": "isOrdinary",
+                        "boost": 1
+                      }
+                    }
+                  ],
+                  "adjust_pure_negative": true,
+                  "boost": 1
+                }
+              }
+            ],
+            "adjust_pure_negative": true,
+            "boost": 1
+          }
+        }
+      ],
+      "adjust_pure_negative": true,
+      "boost": 1
+    }
+  },
+  "_source": {
+    "includes": [
+      "serialId",
+      "source.logonName",
+      "source.department",
+      "source.fqdn",
+      "detectDateTime",
+      "channelType",
+      "source.mail",
+      "source.ipAddress",
+      "matchedPolicies.name",
+      "classificationLevelNames",
+      "extendIncidentRules.name",
+      "tagNames",
+      "destinations.displayName",
+      "attachments.filename",
+      "attachments.fileSize",
+      "attachments.fileMd5"
+    ],
+    "excludes": [
+      "matchedPolicies.matchedRules.matchedConditions"
+    ]
+  },
+  "sort": [
+    {
+      "detectDateTime": {
+        "order": "desc"
+      }
+    }
+  ],
+  "track_total_hits": 2147483647
+}

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

@@ -0,0 +1,35 @@
+#!/bin/bash
+appname='mjava-aipocloud'
+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

+ 54 - 0
mjava-aiwei/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-aiwei</artifactId>
+    <description>艾为电子, TBx宜搭</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-aiwei/src/main/java/com/malk/aiwei/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.aiwei;
+
+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. 标识对表没有任何操作. 若不设置为 none, 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);
+    }
+}

+ 72 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/controller/PLController.java

@@ -0,0 +1,72 @@
+package com.malk.aiwei.controller;
+
+
+import com.malk.aiwei.service.GZT_PN;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+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.RequestBody;
+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;
+
+@Slf4j
+@RestController
+@RequestMapping("pl")
+public class PLController {
+
+    @Autowired
+    private GZT_PN gzt_pn;
+
+    /**
+     * jsApi 注册
+     */
+    @PostMapping("register")
+    McR register(@RequestBody Map<String, String> data) {
+
+        McException.assertParamException_Null(data, "url", "nonceStr");
+        return McR.success(gzt_pn.register(data));
+    }
+
+    /**
+     * 工作台数据 - 资讯中心
+     */
+    @PostMapping("portal/ZXZX")
+    List<Map> portal_ZXZZ(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("工作台数据, {}", data);
+        return gzt_pn.getPortalList("派能资讯");
+    }
+
+    @PostMapping("portal/NLPS")
+    List<Map> portal_NLPS(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("工作台数据, {}", data);
+        return gzt_pn.getPortalList("能量派送");
+    }
+
+    @PostMapping("portal/RZZX")
+    List<Map> portal_RZZX(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("工作台数据, {}", data);
+        return gzt_pn.getPortalList("人资资讯");
+    }
+
+    @PostMapping("portal/NBZX")
+    List<Map> portal_NBZX(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("工作台数据, {}", data);
+        return gzt_pn.getPortalList("内部资讯");
+    }
+
+}

+ 21 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBController.java

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

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 629 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBxYDController.java


+ 53 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/delegate/TBDelegate.java

@@ -0,0 +1,53 @@
+package com.malk.aiwei.delegate;
+
+import com.alibaba.fastjson.JSONObject;
+import com.malk.aiwei.service.AWClint;
+import com.malk.delegate.TBEvent;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+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 TBDelegate implements TBEvent {
+
+    @Autowired
+    private AWClint awClint;
+
+    @Async
+    @Override
+    @SneakyThrows
+    public void callBackTask(JSONObject eventJson) {
+        String eventName = eventJson.getString("event");
+        log.info("callBackTask, {}, {}", eventName, eventJson);
+
+        if ("v3.task.taskflowstatus.update".equals(eventName)) {
+            JSONObject data = eventJson.getJSONObject("data");
+            awClint.taskLevelValidate(data);
+            awClint.doApprove(data, false);
+        } else if ("v3.task.customfield.update".equals(eventName)) {
+            JSONObject data = eventJson.getJSONObject("data");
+            System.out.println("1" + JSONObject.toJSONString(data));
+            awClint.custFieldUpdate(data);
+        }
+    }
+
+    @Async
+    @Override
+    @SneakyThrows
+    public void callBackProject(JSONObject eventJson) {
+        String eventName = eventJson.getString("event");
+        log.info("callBackProject, {}", eventJson);
+    }
+
+}

+ 87 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/schedule/AWScheduleTask.java

@@ -0,0 +1,87 @@
+package com.malk.aiwei.schedule;
+
+import com.malk.aiwei.service.AWClint;
+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 AWScheduleTask {
+
+    @Autowired
+    private AWClint awClint;
+
+    /**
+     * 每天12.30,00.30点同步, 项目主数据
+     */
+    @Scheduled(cron = "0 30 0,12 * * ? ")
+    public void sync_4() {
+        try {
+            awClint.syncProject(null);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 每天凌晨1点同步, 预检项
+     */
+    @Scheduled(cron = "0 0 1 * * ? ")
+    public void sync_1() {
+        try {
+            awClint.syncCheckList(0);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 每天凌晨1.30点同步. 预检项
+     */
+    @Scheduled(cron = "0 30 1 * * ? ")
+    public void sync_2() {
+        try {
+            awClint.syncCheckList(1);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 每天凌晨2点同步, 预检项
+     */
+    @Scheduled(cron = "0 0 2 * * ? ")
+    public void sync_3() {
+        try {
+            awClint.syncCheckList(2);
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 每天20点同步, 全量同步CRM
+     */
+    @Scheduled(cron = "0 0 20 * * ? ")
+    public void sync_5() {
+        try {
+            awClint.syncBaseLineForCRM();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 43 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/schedule/Timer.java

@@ -0,0 +1,43 @@
+package com.malk.aiwei.schedule;
+
+import com.malk.aiwei.service.GZT_PN;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Configuration
+@EnableScheduling
+public class Timer {
+
+    @Autowired
+    private GZT_PN gzt_pn;
+
+
+    private Map portal_cache = new HashMap();
+
+    @Scheduled(cron = "0 */5 * * * ?")//工作台数据5分钟更新一次
+    public void hzmhByType() {
+        try {
+            gzt_pn.getDataList();
+//            HttpUtil.get("http://aservice.risechina.com:9020/ruisi/hzmh/byType");
+//            portal_cache = new HashMap();
+//            rsService.getPortalList("财务专区");
+//            rsService.getPortalList("公司头条");
+//            rsService.getPortalList("企业文化");
+//            rsService.getPortalList("员工自助");
+//            rsService.getPortalList("规章制度");
+
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+}
+

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

@@ -0,0 +1,71 @@
+package com.malk.aiwei.server;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.utils.UtilHttp;
+import com.malk.utils.UtilMap;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class AWServer {
+
+    /**
+     * 任务卡片字段名称
+     */
+    public static final String TASK_ROLE = "资源名称";
+    public static final String TASK_CODE = "任务编号";
+    public static final String TASK_STAGE = "TR评审节点";
+    public static final String TASK_STAGE_BLANK = "无TR评审节点";
+    public static final String TASK_PRODUCT = "产品型号";
+    public static final String TASK_PRODUCT_VERSION = "产品版本";
+    public static final String TASK_WAFER = "wafer";
+
+    public static final String TASK_TRANSMIT = "下达状态";
+
+    // prd 23.02.29 字段从 预检项 变更为 技术检查项
+    public static final String TASK_CHECK_LINK = "技术检查项";
+    public static final String TASK_CHECK_STATUS = "技术检查项检查状态";
+
+    // prd 7.17 技术检查项确认状态
+    public static final String TASK_CHECK_LINK_OK = "技术检查确认";
+
+    public static final String TASK_APPROVE_ATTACHMENT = "交付件";
+    public static final String TASK_APPROVE_VERSION = "交付件版本";
+    public static final String TASK_APPROVE_LINK = "交付件审批流程";
+    public static final String TASK_APPROVE_STATE = "变更状态";
+    public static final String TASK_APPROVE_DESC = "交付件描述";
+
+    // prd 8.6 产品型号、版本选择添加字段
+    public static final String SELECT_TASK_PRODUCT = "选择产品型号";
+    public static final String SELECT_TASK_PRODUCT_VERSION = "选择产品版本";
+
+    // ppExt: 注意不同任务类型名称唯一性, 如通用已完成不要使用
+    public static final String WORKFLOW_APPROVE = "已提交";
+    public static final List<String> WORKFLOW_INITIAL = Arrays.asList("未完成", "待处理");
+
+    public static final String PROJECT_PM_ROLE = "项目管理员";
+    public static final String PROJECT_PM_NAME = "项目经理";
+
+    // 预检项不加载白名单 [项目模板/供复制项目]
+    public static final List<String> PROJECT_IGNORE_ID = Arrays.asList(
+            "65fac2fc386eb113f1adac83", // ak类正式模板
+            "65fac3af386eb113f1adacc0", // ak类迁移模板
+            "65f269605a2065ad7ad65d18"  // ak类复制项目
+    );
+
+    /**
+     * 艾为网关接口
+     */
+    private static final String appKey = "AW394034j";
+    private static final String secret = "RJbH7RMH3vllt4KDri303Mlw@df3434k";
+
+    public static String getToken() {
+
+        Map body = UtilMap.map("appKey, secret", appKey, secret);
+        String rsp = UtilHttp.doPost("https://apigateway.awinic.com/token/getToken", null, body, new HashMap());
+        Map result = (Map) JSON.parse(rsp);
+        return UtilMap.getString(result, "token");
+    }
+}

+ 88 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/server/YDSearch.java

@@ -0,0 +1,88 @@
+package com.malk.aiwei.server;
+
+import lombok.Data;
+
+@Data
+public class YDSearch {
+
+    // YidaSearch yidaSearch=new YidaSearch("textField_lk7v5k5f",contract.getContractNo(),"同步合同档案台账", YidaSearch.Type.TEXT_FIELD,YidaSearch.Operator.EQ);
+
+
+    private String key;
+    private Object value;
+    // 组件值类型
+    private String type;
+    // 操作符
+    private String operator;
+    // 组件名称
+    private String componentName;
+    // 查询子表使用
+    private String parentId;
+
+    public YDSearch(String key, Object value, String type, String operator, String componentName){
+        this.key=key;
+        this.value=value;
+        this.type=type;
+        this.operator=operator;
+        this.componentName=componentName;
+    }
+
+    public YDSearch(String key, Object value, Type type, Operator operator, String componentName, String parentId) {
+        this.key = key;
+        this.value = value;
+        this.type = type.type;
+        this.operator = operator.operator;
+        this.componentName = componentName;
+        this.parentId = parentId;
+    }
+
+    public YDSearch(String key, Object value, String componentName, Type type, Operator operator){
+        this.key=key;
+        this.value=value;
+        this.type=type.type;
+        this.operator=operator.operator;
+        this.componentName=componentName;
+    }
+
+    // 缺少来这里找 https://open.dingtalk.com/document/orgapp-server/use-the-filter-conditions-in-the-form-data-management-for
+    public enum Type{
+        TEXT_FIELD("TEXT"),
+        TEXTAREA_FIELD("TEXT"),
+        NUMBER_FIELD("DOUBLE"),
+        RATE_FIELD("DOUBLE"),
+        RADIO_FIELD("ARRAY"),
+        CHECKBOX_FIELD("ARRAY"),
+        MULTISELECT_FIELD("ARRAY"),
+        CASCADESELECT_FIELD("STRING"),
+        DATE_FIELD("DOUBLE"),
+        CASCADEDATE_FIELD("DOUBLE"),
+        IMAGE_FIELD("TEXT"),
+        ATTACHMENT_FIELD("TEXT"),
+        EMPLOYEE_FIELD("STRING"),
+        DEPARTMENTSELECT_FIELD("ARRAY");
+
+        public final String type;
+
+        Type(String type){
+            this.type=type;
+        }
+    }
+
+    public enum Operator{
+        EQ("eq"),
+        LIKE("like"),
+        GT("gt"),
+        GE("ge"),
+        LT("lt"),
+        LE("le"),
+        BETWEEN("between"),
+        CONTAINS("contains");
+
+        public final String operator;
+
+        Operator(String operator){
+            this.operator=operator;
+        }
+    }
+
+}

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

@@ -0,0 +1,130 @@
+package com.malk.aiwei.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.malk.server.aliwork.YDParam;
+
+import java.util.List;
+import java.util.Map;
+
+public interface AWClint {
+
+    /**
+     * 交付物审批
+     */
+    Map doApprove(Map data, boolean isChange);
+
+    /**
+     * 交付物审批变更发起
+     */
+    void changeApprove(String taskId, String instanceId, String title);
+
+    /**
+     * 交付物审批回调
+     */
+    void approved(Map data);
+
+    /**
+     * 检查项check
+     */
+    Map doCheck(String taskId, boolean isTask);
+
+    /**
+     * 检查项回调
+     */
+    void checked(Map data);
+
+    /**
+     * 同步项目主数据
+     */
+    Map syncProject(String projectCode);
+
+    /**
+     * 通过模板创建项目 [templateId 为空, 触发项目类型匹配]
+     */
+    void createProject(String projectCode, String templateId);
+
+    /**
+     * 项目主数据增量更新
+     */
+    void updateProject(String projectCode);
+
+    /**
+     * 分配项目角色 prd 若是一人直接指定, 多人情况下忽略
+     */
+    void updateProjectRole(String projectId, List<String> trNode, String production, String version);
+
+    /**
+     * 项目迁移: 删除依赖项
+     */
+    void removeDependencies(String projectId, List<String> trNode);
+
+    /**
+     * 增量同步crm基线
+     */
+    void syncBaseLineForCRM(String projectId);
+
+    /**
+     * 全量同步crm基线
+     */
+    void syncBaseLineForCRM();
+
+    /**
+     * 修改任务自定义字段内容
+     *
+     * @param projectId 1. 若为空, 触发全量修改; 2. 仅修改非未完成任务
+     */
+    void batchUpdate(String fieldName, String preName, String modifyName, String projectId);
+
+    /**
+     * 同步预检项 [实现]
+     *
+     * @param srcParam          [appType, formUuid, systemToken]
+     * @param compIds           映射表: 当前组件, 来源组件
+     * @param taskCompId        任务号, 忽略为空记录
+     * @param codeCompId        来源表唯一标识
+     * @param checkType         预检项分类(TR评审要素表、经验库、IC技术检查表)
+     * @param associationCompId 预检项分类对应关联组件
+     */
+    void syncCheckList(YDParam srcParam, Map<String, ?> compIds, String taskCompId, String codeCompId, String checkType, String associationCompId);
+
+    /**
+     * 同步预检项 [通用]
+     *
+     * @param type 预检项分类(0-经验库、1-IC技术检查表, 2-TR评审要素表)
+     */
+    void syncCheckList(int type);
+
+    /**
+     * 知识库版本管理
+     */
+    void approveVersion(String taskId, String pCode);
+
+    /**
+     * 获取主数据中产品列表
+     */
+    List<Map> getProductList(String projectId, String q);
+
+    /**
+     * 获取主数据中产品版本
+     */
+    List<Map> getProductList(String projectId, String q, String taskId);
+
+    /**
+     * 选择产品\版本后, 回调更新对应原文本字段, 兼容之前字段逻辑
+     */
+    void custFieldUpdate(JSONObject data);
+
+    /**
+     * 检查项导出, 全部检查项左关联已提交数据
+     */
+    List<Map> exportCheckList(String pCode, String proType);
+
+    /**
+     * 任务NA校验, 一二级不允许NA
+     */
+    void taskLevelValidate(Map data);
+
+    void test();
+
+    void tmp();
+}

+ 59 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/AWYDClient.java

@@ -0,0 +1,59 @@
+package com.malk.aiwei.service;
+
+import java.util.List;
+import java.util.Map;
+
+public interface AWYDClient {
+
+
+    /**
+     * 提供verifier数据读取服务
+     */
+    Map syncVerifier(String projectCode, String productCode, String productVersion);
+
+    /**
+     * 提供verifier数据回传服务
+     */
+    void backVerifier(Map data);
+
+    /**
+     * 项目需求导入校验
+     */
+    Map checkImportData_projectRequirements(String charter, List<Map> dataList, boolean isSubmit);
+
+    /**
+     * 项目需求审批通过后, 回写需求编号与关联表单, ppExt 并覆盖项目需求档案需求列表
+     */
+    void dealApprovedData_projectRequirements(String pCode, String instanceId, Map data);
+
+    /**
+     * tmp 覆盖项目需求档案需求列表, 兼容 dealApprovedData_projectRequirements
+     */
+    void archiveProjectRequirements(String pCode, String instanceId);
+
+    /**
+     * 项目规格导入校验
+     */
+    Map checkImportData_projectSpecification(String charter, List<Map> dataList, List<String> prList, boolean isSubmit);
+
+    /**
+     * 项目规格审批通过后, 回写规格编号与关联表单
+     */
+    void dealApprovedData_projectSpecification(String instanceId, Map data);
+
+    /**
+     * 测试规格导入校验
+     */
+    Map checkImportData_testSpecification(String charter, List<Map> dataList, List<String> prList, boolean isSubmit);
+
+    /**
+     * 测试规格审批通过后, 回写规格编号与关联表单
+     */
+    void dealApprovedData_testSpecification(String instanceId, Map data);
+
+    
+    void test();
+
+    // 0906 项目需求导入逻辑变更, 弃用保留
+    Map checkImportData(Map data);
+}

+ 9 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/AwDingService.java

@@ -0,0 +1,9 @@
+package com.malk.aiwei.service;
+
+import java.util.Map;
+
+public interface AwDingService {
+
+    void saveGroup(Map upMap,Map formData);
+
+}

+ 13 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/GZT_PN.java

@@ -0,0 +1,13 @@
+package com.malk.aiwei.service;
+
+import java.util.List;
+import java.util.Map;
+
+public interface GZT_PN {
+
+    List<Map> getPortalList(String section);
+
+    void getDataList();
+
+    Map register(Map<String, String> data);
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 2100 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWImplClient.java


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1018 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWYDImplClient.java


+ 79 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AwDingServiceImpl.java

@@ -0,0 +1,79 @@
+package com.malk.aiwei.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.malk.aiwei.service.AwDingService;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Group;
+import com.malk.utils.UtilEnv;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.formula.functions.T;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Slf4j
+@Service
+public class AwDingServiceImpl implements AwDingService {
+
+    @Autowired
+    private DDClient_Group ddClient_group;
+    @Autowired
+    private DDClient ddClient;
+
+    String _matchFormData(String code) {
+        Map<String, String> formUuid = UtilMap.empty();
+        if (true || UtilEnv.getActiveProfile().equals(UtilEnv.ENV_PROD)) {
+            formUuid.put("OWNER_USER_ID", "153620324221442254");// todo 需要替换艾为tbmanager userId
+            formUuid.put("TEMPLATE_ID", "17d5c0fc-79f8-43bf-86ab-bad1e254ec2d");// todo 需要在艾为组织创建场景群模板,并替换模板ID
+        } else {
+            formUuid.put("OWNER_USER_ID", "153620324221442254");
+            formUuid.put("TEMPLATE_ID", "17d5c0fc-79f8-43bf-86ab-bad1e254ec2d");
+        }
+        return formUuid.get(code);
+    }
+
+    @Override
+    public void saveGroup(Map upMap, Map formData) {
+        try {
+            String ddGroupId=String.valueOf(formData.get("textField_luqvxhec"));// 群ID
+            List<Map> list= (List<Map>) formData.get("tableField_lqxtykcf");// 项目角色子表单
+            String newUsers="";
+            for (Map map:list){
+                newUsers=newUsers.concat(String.join(",",UtilMap.getList(map,"employeeField_lqxtykch_id"))).concat(",");
+            }
+            newUsers=newUsers.substring(0,newUsers.length()-1);
+            if(StrUtil.isBlank(ddGroupId)){
+                String name=String.valueOf(formData.get("textField_lqxtykcd")).concat("项目群");// todo 这里是群名称
+                ddGroupId=ddClient_group.createGroupByTemp(ddClient.getAccessToken(),name,_matchFormData("TEMPLATE_ID"),_matchFormData("OWNER_USER_ID"),newUsers);
+                // 创建群
+                upMap.put("textField_luqvxhec",ddGroupId);// 群ID
+                upMap.put("textField_luqvxhed",newUsers);// 群成员ID
+            }else{
+                // 判断并更新群成员
+                String oldUsers=String.valueOf(formData.get("textField_luqvxhed"));
+                // 删除群成员
+                List delList=findDifferentElements(Arrays.asList(oldUsers.split(",")),Arrays.asList(newUsers.split(",")));
+                if(delList.size()>0){
+                    ddClient_group.delGroupUser(ddClient.getAccessToken(),ddGroupId,String.join(",",delList));
+                }
+                // 新增群成员
+                List addList=findDifferentElements(Arrays.asList(newUsers.split(",")),Arrays.asList(oldUsers.split(",")));
+                if(addList.size()>0){
+                    ddClient_group.addGroupUser(ddClient.getAccessToken(),ddGroupId,String.join(",",addList));
+                }
+                upMap.put("textField_luqvxhed",newUsers);// 群成员ID
+            }
+        }catch (Exception e){
+            log.error("创建钉钉项目群异常了,{}",e);
+            e.printStackTrace();
+        }
+    }
+
+    private List findDifferentElements(List firstArray,List secondArray){
+        Set<T> set1 = new HashSet<>(firstArray);
+        set1.removeAll(secondArray);
+        return new ArrayList<>(set1);
+    }
+}

+ 120 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/GZT_PNImpl.java

@@ -0,0 +1,120 @@
+package com.malk.aiwei.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.aiwei.service.GZT_PN;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDService;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@EnableScheduling
+@Service
+@Slf4j
+public class GZT_PNImpl implements GZT_PN {
+
+    private Map portal_cache = new HashMap();
+    @Autowired
+    private YDClient ydClient;
+
+    @Override
+    public List<Map> getPortalList(String section) {
+        return _portalList(section);
+    }
+
+    @Override
+    public void getDataList() {
+        try {
+
+            portal_cache = new HashMap();
+            getPortalList("派能资讯");
+            getPortalList("能量派送");
+            getPortalList("人资资讯");
+            getPortalList("内部资讯");
+
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    private List<Map> _portalList(String type) {
+        List<Map> pList = UtilMap.getList(portal_cache, type);
+        try {
+            if (pList.isEmpty()) {
+                List<Map> dataList = (List<Map>) ydClient.queryData(YDParam.builder()
+                        .appType("APP_OM7HTYCXYQYCKJ046D9V")
+                        .systemToken("BN766KC1CS9Q2LJQDAKIN82VVUHT2J07ALF3M7R2")
+                        .formUuid("FORM-918FE77C2E8248069B7CA497C6B2E4C6XD4B")
+                        .pageSize(5)
+                        .searchFieldJson(JSONObject.toJSONString(UtilMap.map("selectField_m17kv4an", type)))
+                        .build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
+
+                dataList.sort(Comparator.comparingInt(item -> UtilMap.getInt(item, "numberField_m3s6swzt"))); // 排序
+                pList = dataList.stream().map(item -> {
+                    Map formData = UtilMap.getMap(item, "formData");
+
+                    Map row = UtilMap.map("title, source, link", UtilMap.getString(formData, "textField_m17kv4aq"), UtilMap.getString(formData, "textField_m17kv4as"), UtilMap.getString(formData, "textField_m17kv4at"));
+                    long date = UtilMap.getLong(formData, "dateField_m17kv4ar");
+                    if (date > 0L) {
+                        row.put("dateTime", UtilDateTime.format(new Date(date), "yyyy-MM-dd HH:mm"));
+                    }
+                    // 图片免登处理
+                    String image = UtilMap.getString(formData, "imageField_m1abjxl0");
+                    if (StringUtils.isNotBlank(image)) {
+                        List<Map> attas = (List<Map>) JSON.parse(image);
+                        row.put("image", convertTemporaryUrl_PN(UtilMap.getString(attas.get(0), "url")));
+                        Object image1 = row.get("image");
+                        //   System.out.println("image=========" + UtilMap.getString(attas.get(0), "url"));
+//                    System.out.println("image1========="+image1);
+                    }
+                    return row;
+                }).collect(Collectors.toList());
+                portal_cache.put(type, pList);
+            }
+        } catch (Exception e) {
+
+        }
+        log.info("type: {},list:{}", type, pList);
+        return pList;
+    }
+
+    @Autowired
+    private DDClient ddClient;
+
+    /**
+     * 派能
+     */
+    public String convertTemporaryUrl_PN(String url) {
+        String ddapptonken = ddClient.getAccessToken("dingmpxci8bolc3jpima", "Y_k3jpKNHbGvb9S9As2Y61ZaUFNglm7SCqquIkcowLBRoc4ZpH7DG0ZTn8LyHMwI");
+        Map param = new HashMap();
+        param.put("systemToken", "BN766KC1CS9Q2LJQDAKIN82VVUHT2J07ALF3M7R2");
+        param.put("userId", YDConf.PUB_ACCOUNT);
+        param.put("fileUrl", url);          // URL在param上时, 需要编码 [UtilHttp已经做了编码] - URLEncoder.encode(url, "UTF-8")
+        param.put("timeout", 60000);      // 默认1分钟, 最大24小时 [毫秒]
+        // System.out.println("ssss:"+(String) DDR_New.doGet("https://api.dingtalk.com/v1.0/yida/apps/temporaryUrls/APP_G951QZ32AUJNJUE4G127" , ddClient.initTokenHeader_PN(), param).getResult());
+        return (String) DDR_New.doGet("https://api.dingtalk.com/v1.0/yida/apps/temporaryUrls/APP_OM7HTYCXYQYCKJ046D9V", DDConf.initTokenHeader(ddapptonken), param).getResult();
+    }
+
+    @Autowired
+    private DDService ddService;
+
+    @Override
+    public Map register(Map<String, String> data) {
+        String ddapptonken = ddClient.getAccessToken("dingmpxci8bolc3jpima", "Y_k3jpKNHbGvb9S9As2Y61ZaUFNglm7SCqquIkcowLBRoc4ZpH7DG0ZTn8LyHMwI");
+        return ddService.registerJsApi(ddapptonken, data.get("url"), data.get("nonceStr"));
+    }
+}

+ 86 - 0
mjava-aiwei/src/main/resources/application-dev.yml

@@ -0,0 +1,86 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/aiwei
+
+# 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
+    # 主库
+    primary:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+# dingtalk
+dingtalk:
+  agentId: 2691784047
+  appKey: dinghbynhnd2dbgypmsa
+  appSecret: Kl5Xw8x0TlEIlvcJuUkYZD18UTTShJmfdKrAIpY8oX-Q_tazyUKA28nQh7dG5-mq
+  corpId: ding321c72787fffc78b35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "095358016629044412"   # OA管理员账号
+
+##  aliwork
+#aliwork:
+#  appType: "APP_H7WUJTKB448F9IBDC6C4"
+#  systemToken: "DHA66081DN6GRFNC6GTRW5NIJS082ZF0UN9PLLF"
+
+# teambition
+teambition:
+  AppID: 65956b5dd0ac095d62d0e592
+  AppSecret: gjQUoqKa1PHjTiyQFFuachfqKPyNeacA
+  TenantId: 6034c885e71842e1e5bb5218        # 管理后台 - 企业xx - 企业ID
+  OperatorId: 5e698cca21f5ad70dfba7d2b      # 公共账号, 需要有操作权限 [牧语]
+
+# aliwork - prod
+aliwork:
+  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+#
+## dingtalk
+#dingtalk:
+#  agentId: 2848797049
+#  appKey: dingbqy1qugrihao43dl
+#  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+#  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+#  aesKey:
+#  token:
+#  operator: "0249EDD1-754E-44C8-87F0-255B0E32021F"   # OA管理员账号
+

+ 45 - 0
mjava-aiwei/src/main/resources/application-local.yml

@@ -0,0 +1,45 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/aiwei
+
+# 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: 2848797049
+  appKey: dingbqy1qugrihao43dl
+  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "0249EDD1-754E-44C8-87F0-255B0E32021F"   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+
+# teambition
+teambition:
+  AppID: 66b1dabf408d68cc7f672ecd
+  AppSecret: ZNRGnvMGW3n6qtmsZId8G2YUrVWxMNQF
+  TenantId: 623ef800ad6674f7257f0eae                # 管理后台 - 企业xx - 企业ID
+  OperatorId: 6503d14c2efed4b3063f3952              # 公共账号, 需要有操作权限 [x]

+ 46 - 0
mjava-aiwei/src/main/resources/application-prod.yml

@@ -0,0 +1,46 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/aiwei
+
+# condition
+spel:
+  scheduling: true         # 定时任务是否执行
+  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: 2848797049
+  appKey: dingbqy1qugrihao43dl
+  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "0249EDD1-754E-44C8-87F0-255B0E32021F"   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+
+# teambition
+teambition:
+  AppID: 659cf4922fefb4a7ec89137b
+  AppSecret: ploT7pyTcEVz91i5IIBZ8Pw7LbrOWPYD
+  TenantId: 655f1512ad7db5a6a70cf7b1                # 管理后台 - 企业xx - 企业ID
+  OperatorId: 65682c174655a82b4fa04dfe              # 公共账号, 需要有操作权限 [x]
+  ApiHost: https://tb.awinic.com:443/gateway        # 私有部署

+ 46 - 0
mjava-aiwei/src/main/resources/application-test.yml

@@ -0,0 +1,46 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/aiwei
+
+# 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: 2848797049
+  appKey: dingbqy1qugrihao43dl
+  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "0249EDD1-754E-44C8-87F0-255B0E32021F"   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+
+# teambition
+teambition:
+  AppID: 66b5cec1dbcd57b14046af05
+  AppSecret: jyd7DVu3MGYwzt562qPmLAaacV65FIyQ
+  TenantId: 658e7239b580fdc87e4a7ca4                # 管理后台 - 企业xx - 企业ID
+  OperatorId: 667e8282df3131f8e588526e              # 公共账号, 需要有操作权限 [x]
+  ApiHost: https://tbtest.awinic.com:443/gateway        # 私有部署

+ 46 - 0
mjava-aiwei/src/main/resources/application-tmp.yml

@@ -0,0 +1,46 @@
+# 环境配置
+server:
+  port: 9011
+  servlet:
+    context-path: /api/aiwei
+
+# 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: 2848797049
+  appKey: dingbqy1qugrihao43dl
+  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "0249EDD1-754E-44C8-87F0-255B0E32021F"   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+
+# teambition
+teambition:
+  AppID: 66b5cec1dbcd57b14046af05
+  AppSecret: jyd7DVu3MGYwzt562qPmLAaacV65FIyQ
+  TenantId: 658e7239b580fdc87e4a7ca4                # 管理后台 - 企业xx - 企业ID
+  OperatorId: 667e8282df3131f8e588526e              # 公共账号, 需要有操作权限 [x]
+  ApiHost: https://tbtest.awinic.com:443/gateway    # 私有部署

BIN
mjava-aiwei/src/main/resources/templates/templates_checked_list.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_project_requirements.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_project_requirements_exception.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_project_requirements_test.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_project_specification.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_project_specification_exception.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_test_specification.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_test_specification_exception.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_xqrk.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_xqwh.xlsx


BIN
mjava-aiwei/src/main/resources/templates/templates_xqwh_result.xlsx


+ 79 - 0
mjava-aiwei/src/test/java/com/malk/aiwei/AwTbTest.java

@@ -0,0 +1,79 @@
+package com.malk.aiwei;
+
+import com.alibaba.fastjson.JSONObject;
+import com.malk.server.teambition.TBConf;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.teambition.TBClient;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@Slf4j
+@SpringBootTest
+@RunWith(SpringRunner.class)
+public class AwTbTest {
+
+    @Autowired
+    private TBClient tbClient;
+    @Autowired
+    private TBConf tbConf;
+
+    @Test
+    public void test() {
+        List<String> users = Arrays.asList("5f3a250040ce230ba377bf3c", "5e698cca21f5ad70dfba7d2b", "616fb6f78ad4104a10515809");
+        String programId = "662a69aceda740b4e63d3976";
+//        tbClient.createProgramMember(programId,tbConf.getOperatorId(),users);
+        List addUsers = new ArrayList();
+        for (String userId : users) {
+            List resut = tbClient.queryProgramMember(programId, tbConf.getOperatorId(), UtilMap.map("userIds", userId));
+            if (resut.size() < 1) addUsers.add(userId);
+        }
+        log.info("需要添加到项目集的userId:{}", tbClient);
+        if (addUsers.size() > 0) tbClient.createProgramMember(programId, tbConf.getOperatorId(), addUsers);
+    }
+
+    @Autowired
+    private YDClient ydClient;
+
+    // this.initOptions("FORM-IQ8666B1PL275QG5BSSDG6ZD9X3Q29X881HCL4", JSON.stringify({'textField_ljsd7bjn':value}), //
+    //    "8F966HB12J27MQJM6V4IQDYHYTPA2G4GTZGCLN1", "APP_QBWQITQBSPJNYTUTNPDK",
+    //    'yida_pub_account', "selectField_lchafcgt", "selectField_lchafcgt", "selectField_lywdbrk1", 100);
+
+
+    @Test
+    public void adad() {
+//        YDSearch ydSearch=new YDSearch("textField_lha7mqbs","G","物料子类", YDSearch.Type.TEXT_FIELD,YDSearch.Operator.EQ);
+//        YDSearch ydSearch2=new YDSearch("textField_lha7mqbn","A2443","项目号", YDSearch.Type.TEXT_FIELD,YDSearch.Operator.EQ);
+//        List<Map> list =(List<Map>) ydClient.queryData(YDParam.builder().formUuid("FORM-4W8667D17CAAGFXI9VO0J9J7RFNL2QDFL7AHLJ")
+//                .appType("APP_QBWQITQBSPJNYTUTNPDK").systemToken("8F966HB12J27MQJM6V4IQDYHYTPA2G4GTZGCLN1")
+//                .searchCondition(JSONObject.toJSONString(Arrays.asList(ydSearch,ydSearch2)))
+//                .pageSize(100).build(), YDConf.FORM_QUERY.retrieve_list).getData();
+//        List<Map> result=new ArrayList<>();
+//        Map<String,String> keyMap=new HashMap<>();
+//        for (Map map:list){
+//            String data=UtilMap.getString(UtilMap.getMap(map,"formData"),("textField_lhsxurnt"));
+//            if(!StringUtils.isBlank(data)&&!keyMap.containsKey(data)){
+//                result.add(UtilMap.map("title",data));
+//            }
+//        }
+//        print(result);
+//        DDR_New ddr_new = ydClient.queryData(YDParam.builder()
+//                .appType("APP_QBWQITQBSPJNYTUTNPDK").systemToken("8F966HB12J27MQJM6V4IQDYHYTPA2G4GTZGCLN1")
+//                .pageSize(100)
+//                .build(), YDConf.FORM_QUERY.retrieve_forms);
+//        print(ddr_new);
+    }
+
+    private void print(Object o) {
+        System.out.println(JSONObject.toJSONString(o));
+    }
+
+}

+ 89 - 0
mjava-aiwei/src/test/java/com/malk/aiwei/DataHandingTest.java

@@ -0,0 +1,89 @@
+package com.malk.aiwei;
+
+import com.alibaba.fastjson.JSONObject;
+import com.malk.aiwei.server.AWServer;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.teambition.TBConf;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDService;
+import com.malk.service.teambition.TBClient;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.platform.commons.util.StringUtils;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@SpringBootTest
+@RunWith(SpringRunner.class)
+public class DataHandingTest {
+
+    @Autowired
+    private TBClient tbClient;
+    @Autowired
+    private YDClient ydClient;
+    @Autowired
+    private YDService ydService;
+
+    // 处理数据
+    @Test
+    public void test(){
+        List<Map> data = ydService.queryFormData_all(YDParam.builder().formUuid("FORM-6E2C0D1197264B8AA23EB3FECAE7344B00BN").build());
+        List<Map> errorList = new ArrayList<>();
+        for (Map item:data){
+            String instanceId=UtilMap.getString(item,("instanceId"));
+            String tbTaskId=UtilMap.getString(item,("textField_lr3dlwsa"));
+            if(StringUtils.isBlank(tbTaskId)){
+                continue;
+            }
+            try {
+                Map taskData = _getTaskFieldMap(tbTaskId, "wafer");
+                Map rTask = UtilMap.getMap(taskData, "task");
+                Map updateMap=new HashMap();
+                updateMap.put("textField_m0yo7tsj",taskData.get("wafer"));
+                updateMap.put("textField_m0yo7tsk",_getWorkFlowStatusList(UtilMap.getString(rTask,"projectId"),UtilMap.getString(rTask,"tfsId"))); // 任务状态
+                ydClient.operateData(YDParam.builder().formInstId(instanceId).updateFormDataJson(JSONObject.toJSONString(updateMap)).build(), YDConf.FORM_OPERATION.update);
+            }catch (Exception e){
+                e.printStackTrace();
+                errorList.add(UtilMap.map("instanceId, tbTaskId",instanceId,tbTaskId));
+            }
+        }
+        System.out.println(JSONObject.toJSONString(errorList));
+    }
+
+    private Map _getTaskFieldMap(String taskId, String... fieldNames) {
+        // 查询任务详情
+        Map rTask = tbClient.queryTaskDetail(taskId, null, null).get(0);
+        // 查询项目任务ID
+        List<Map> customField = tbClient.queryProjectCustomField(String.valueOf(rTask.get("projectId")), null);
+        return __getTaskMap(rTask, customField, fieldNames);
+    }
+
+    // 组装任务数据
+    private Map __getTaskMap(Map task, List<Map> projectCustomField, String... fieldNames) {
+        Map result = UtilMap.map("task", task);
+        List<Map> taskCustomField = (List<Map>) task.get("customfields");
+        for (String filed : fieldNames) {
+            Optional optional = projectCustomField.stream().filter(item -> filed.equals(item.get("name"))).findAny();
+            if (optional.isPresent()) {
+                Map map = (Map) optional.get();
+                result.put(filed, TBConf.getTaskFieldValue_First(taskCustomField, String.valueOf(map.get("id"))));
+                result.put(filed + "_id", map.get("id")); // ppExt: 返回字段ID
+            }
+        }
+        return result;
+    }
+
+    private String _getWorkFlowStatusList(String projectId,String tfsId) {
+        List<Map> customFlowStatus = tbClient.queryProjectCustomFlowStatus(projectId, UtilMap.map("tfsIds",tfsId));
+        return UtilMap.getString(customFlowStatus.get(0),"name");
+    }
+
+}

+ 60 - 0
mjava-aiwei/src/test/java/testLocal.java

@@ -0,0 +1,60 @@
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.aiwei.Boot;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.service.aliwork.YDClient;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * @author floe
+ * @date 2024/7/22 22:29
+ */
+@Slf4j
+@SpringBootTest(classes = Boot.class)
+@RunWith(SpringRunner.class)
+public class testLocal {
+
+    @Autowired
+    private YDClient ydClient;
+
+
+
+    @Test
+    public void test(){
+        String token="Bearer ".concat(getToken());
+        List<String> ids= FileUtil.readLines("/home/ids.txt","utf-8");
+//        List<String> ids= Arrays.asList("66697d643f7c5aef23f3a326###FINST-TM966BD1ISXM7XLDC9AGM4N92MDV325WL1YYLV9B","6699cef58e173e99e22d8de4###FINST-MBC668915E4NNTTUAFIFSDQZTPYT2T3WIV0ZLBF6");
+        for (int i = 0; i < ids.size(); i++) {
+            String result=HttpUtil.createPost("https://apigateway.awinic.com/sys/teambition/api/aiwei/do-check?taskId="+ids.get(i).split("###")[0])
+                    .header("authorization",token).execute().body();
+            System.out.println(result);
+            JSONObject resultObj=JSONObject.parseObject(result);
+            if(!resultObj.getBoolean("success")){
+                continue;
+            }
+            JSONObject data=resultObj.getJSONObject("data");
+            data.remove("tableField_lqxxgj4s");
+            data.remove("userId");
+            ydClient.operateData(YDParam.builder().updateFormDataJson(JSONObject.toJSONString(data)).formInstanceId(ids.get(i).split("###")[1]).build()
+             , YDConf.FORM_OPERATION.update);
+        }
+    }
+
+    private String getToken(){
+        String a=HttpUtil.post("https://apigateway.awinic.com/token/getToken", JSONObject.toJSONString(UtilMap.map("appKey, secret","AW394034j","RJbH7RMH3vllt4KDri303Mlw@df3434k")));
+        System.out.println(a);
+        return JSONObject.parseObject(a).getJSONObject("data").getString("token");
+    }
+}

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

@@ -0,0 +1,35 @@
+#!/bin/bash
+appname='mjava-aiwei'
+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

+ 54 - 0
mjava-cloudpure/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-cloudpure</artifactId>
+    <description>云璞环境, 销帮帮对接, yd-tb方案</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-cloudpure/src/main/java/com/malk/cloudpure/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.cloudpure;
+
+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. 标识对表没有任何操作. 若不设置为 none, 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);
+    }
+}

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

@@ -0,0 +1,21 @@
+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 {
+
+}

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

@@ -0,0 +1,113 @@
+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));
+    }
+
+
+}
+
+

+ 219 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/TLYController.java

@@ -0,0 +1,219 @@
+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.McR;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDService;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Workflow;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.map.HashedMap;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping("lanyun")
+public class TLYController {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private YDClient ydClient;
+
+    @Autowired
+    private DDClient_Workflow ddClient_workflow;
+
+    /**
+     * 推送审批
+     */
+    @PostMapping("do-approve")
+    public McR doApprove(String processInstanceId, String type) {
+
+        log.info("推送审批, {} {}", type, processInstanceId);
+        // OA组件name, 匹配宜搭组件ID
+        if ("出差".equals(type)) {
+            Map<String, String> compsId_main = UtilMap.map("出差类型, 预算, 出差天数, 出差事由, 出差备注", "textField_lygo4abp, numberField_lygo9owt, numberField_lygo4abs, textareaField_lygo9owv, textareaField_lygo9ox1");
+            Map<String, String> compsId_itinerary = UtilMap.map("交通工具, 单程往返, 出发城市, 目的城市, 开始时间, 结束时间, 时长", "textField_lygo9oww, textField_lygo9owx, textField_lygo9owy, textField_lygo9owz, textField_lygvu31i, textField_lygvu31j, numberField_lygo9ox0");
+            compsId_main.put("出差明细", "tableField_lygo9owu"); // 子表组件
+            syncYD(processInstanceId, "FORM-210DA087671044F8A5CD72F0E9E89060SZ8Q", compsId_main, compsId_itinerary, "itinerary");
+        }
+        if ("加班".equals(type)) {
+            Map<String, String> compsId_main = UtilMap.map("加班人, 加班类型, 开始时间, 结束时间, 时长, 甲方付费, 加班原因", "employeeField_lygnp948, textField_lygnetwa, textField_lygwcloa, textField_lygwclob, numberField_lygnetwe, textField_lygnetwf, textareaField_lygnetwg");
+            Map<String, String> compsId_itinerary = UtilMap.map("加班时间, overtimeDuration", "textField_lygwclo8, numberField_lygnetwd");
+            compsId_main.put("明细", "tableField_lygnetwc"); // 子表组件
+            syncYD(processInstanceId, "FORM-B33388047A42479BB6387F9E0B453588EZ4P", compsId_main, compsId_itinerary, "everyDayDuration");
+        }
+        if ("请假".equals(type)) {
+            Map<String, String> compsId_main = UtilMap.map("DDHolidayField, 请假事由, 代班人", "textField_lyh0xukm / textField_lyh0xukn / numberField_lygo4abs / radioField_lyh0xuko / textField_lygo4abp, textareaField_lygo4abt, employeeField_lygo4abu");
+            syncYD(processInstanceId, "FORM-FB29F35DF0A04DC2A5F2860E9593C9C6XCMT", compsId_main, null, "");
+        }
+        if ("调班".equals(type)) {
+            Map<String, String> compsId_main = UtilMap.map("申请类型, 原班次名称, 原考勤时间, 新班次考勤时间, 新班次有效时长, 处理状态", "selectField_lygnsggb, textField_lygnsgg6, textField_lygnsgg7, textField_lygnsgg8, textField_lygnsgg9, selectField_lygnsgga");
+            syncYD(processInstanceId, "FORM-777EB143B87D45F2ACB228A024290E1A9GSF", compsId_main, null, "");
+
+        }
+        return McR.success();
+    }
+
+    /// dingtalk
+    final String APP_EKY = "dingfwn4kpmb4g3dy4fj";
+    final String APP_SECRET = "OwdkUc9nvBivpwsg0AiaEcyWLtZ678fskqBJwP7B5CQNrycIoyKuWslFTCOOaZG4";
+    /// aliwork
+    final String APP_TYPE = "APP_ERBDTFS82HOVBPL3NFH0";
+    final String SYSTRM_TOKEN = "RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP";
+
+    /// 同步OA单据到宜搭
+    public void syncYD(String processInstanceId, String formUuid, Map<String, String> compsId_main, Map<String, String> compsId_itinerary, String compId_sub_oa) {
+
+        String token = ddClient.getAccessToken(APP_EKY, APP_SECRET);
+
+        Map processData = ddClient_workflow.getProcessInstanceId(token, processInstanceId);
+        List<Map> formComponentValues = (List<Map>) processData.get("formComponentValues");
+
+        String userId = String.valueOf(processData.get("originatorUserId"));
+        long cDate = UtilDateTime.parse(UtilMap.getString(processData, "createTime"), "yyyy-MM-dd'T'HH:mm").getTime();
+        Map formData = UtilMap.map("employeeField_ltxqs53k, departmentSelectField_lu20ayky, dateField_ltxqs53j, textField_lygnetw9", Arrays.asList(userId), Arrays.asList(processData.get("originatorDeptId")), cDate, UtilMap.getString(processData, "businessId"));
+
+        for (String name : compsId_main.keySet()) {
+            String compId = compsId_main.get(name);
+            // 判定是否子表 [宜搭]
+            if (compId.startsWith("tableField_")) {
+                List<Map> details = new ArrayList<>();
+                // 兼容明细组件, 存在多条情况 [加班跨天才有有明细]
+                Optional optional = formComponentValues.stream().filter(item -> compId_sub_oa.equals(item.get("bizAlias"))).findAny();
+                if (!optional.isPresent()) {
+                    continue;
+                }
+                String schedule = UtilMap.getString((Map) optional.get(), "value");
+                List<Map> itineraryList = ((List<Map>) JSON.parse(schedule));
+                // 循环明细数据
+                for (Map itinerary : itineraryList) {
+                    List<Map> rowValue = (List<Map>) itinerary.get("rowValue");
+                    Map rowData = new HashedMap();
+                    // 循环子表组件
+                    for (String subName : compsId_itinerary.keySet()) {
+                        log.info("子表字段, {}", subName);
+                        //  加班单跨天 [子表label为空]
+                        rowData.put(compsId_itinerary.get(subName), rowValue.stream().filter(item -> subName.equals(item.get("bizAlias")) || subName.equals(item.get("label"))).findAny().get().get("value"));
+                    }
+                    details.add(rowData);
+                }
+                formData.put(compId, details);
+                continue;
+            }
+            log.info("主表字段, {}", name);
+            // 请假套件: 开始时间 / 结束时间 / 时长 / 单位 / 请假类型
+            if ("DDHolidayField".equals(name)) {
+                Optional optional = formComponentValues.stream().filter(item -> "DDHolidayField".equals(item.get("componentType"))).findAny();
+                if (optional.isPresent()) {
+                    String[] ids = compId.split(" / ");
+                    List vas = (List) JSON.parse(UtilMap.getString((Map) optional.get(), "value"));
+                    for (int i = 0; i < ids.length; i++) {
+                        formData.put(ids[i], vas.get(i));
+                    }
+                }
+                continue;
+            }
+            Map formComp = formComponentValues.stream().filter(item -> name.equals(item.get("name"))).findAny().get();
+            Object value = formComp.get("value");
+            // 成员组件, 数据处理
+            if ("InnerContactField".equals(formComp.get("componentType")) && formComp.containsKey("value")) {
+                List<Map> empInfos = (List<Map>) JSON.parse(String.valueOf(formComp.get("extValue")));
+                List<String> emplsId = new ArrayList<>();
+                for (Map empInfo : empInfos) {
+                    emplsId.add(String.valueOf(empInfo.get("emplId")));
+                }
+                value = emplsId; // 成员多选
+            }
+            formData.put(compId, value);
+        }
+
+        // 用于审批回传
+        List<Map> tasks = UtilMap.getList(processData, "tasks");
+        formData.put("textField_lygvvyd9", tasks.get(0).get("taskId"));
+        formData.put("textField_lygvvyda", tasks.get(0).get("userId"));
+        formData.put("textField_lyh4y3th", processInstanceId);
+        formData.put("selectField_lyo1uao4", "否"); // 出差是否报销, 否
+        formData.put("selectField_lyo1zprd", "否"); // 同步存量数据, 否
+
+        log.info("审批数据, {}", JSON.toJSONString(formData));
+        ydClient.operateData(YDParam.builder()
+                .appType(APP_TYPE)
+                .systemToken(SYSTRM_TOKEN)
+                .formUuid(formUuid)
+                .formDataJson(JSON.toJSONString(formData))
+                .userId(userId)
+                .build(), YDConf.FORM_OPERATION.start);
+    }
+
+    /**
+     * 审批回调
+     */
+    @PostMapping("approved")
+    public McR approved(String processInstanceId, String userId, String taskId, String result) {
+        log.info("审批回调, {} {}", processInstanceId, result);
+        String accessToken = ddClient.getAccessToken(APP_EKY, APP_SECRET);
+        ddClient_workflow.executeRunningApprove(accessToken, processInstanceId, userId, taskId, result, "", null);
+        return McR.success();
+    }
+
+    /**
+     * 删除/撤销
+     */
+    @PostMapping("terminate")
+    public McR terminate(String processInstanceId, String userId) {
+        log.info("删除/撤销, {} {}", processInstanceId, userId);
+
+        String accessToken = ddClient.getAccessToken(APP_EKY, APP_SECRET);
+        ddClient_workflow.terminateRunningApprove(accessToken, processInstanceId, true, "发起人撤销", userId);
+        return McR.success();
+    }
+
+
+    @Autowired
+    private YDService ydService;
+
+    @GetMapping("test")
+    McR test() {
+
+
+//        long beginTime = UtilDateTime.parseDateTime("2024-06-01 00:00:00").getTime();
+//        long finishTime = UtilDateTime.parseDateTime("2024-07-17 00:00:00").getTime();
+//
+//        Map extInfo = UtilMap.map("statuses", Arrays.asList("COMPLETED"));
+//        List<String> dataList = ddClient_workflow.getInstanceIds_all(ddClient.getAccessToken(), "PROC-6E40CB5E-F2AE-4CE6-9864-F5C3848D0C7E", beginTime, finishTime, extInfo);
+
+
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .formUuid("FORM-210DA087671044F8A5CD72F0E9E89060SZ8Q")
+                .systemToken(SYSTRM_TOKEN)
+                .appType(APP_TYPE)
+                .build());
+        for (Map map : dataList) {
+
+            if (UtilMap.isBlankString(map, "selectField_lyo1uao4")) {
+                ydClient.operateData(YDParam.builder()
+                        .formInstanceId(UtilMap.getString(map, "instanceId"))
+                        .updateFormDataJson(JSON.toJSONString(UtilMap.map("selectField_lyo1uao4, selectField_lyo1zprd", "否", "否")))
+                        .build(), YDConf.FORM_OPERATION.update);
+            }
+        }
+        return McR.success();
+    }
+}
+
+

+ 518 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/TSController.java

@@ -0,0 +1,518 @@
+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.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_Workflow;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilList;
+import com.malk.utils.UtilMap;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping("test")
+public class TSController {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private YDService ydService;
+
+    @Autowired
+    private YDClient ydClient;
+
+    @SneakyThrows
+    @GetMapping("test")
+    McR test() {
+
+        //updatePosition("2445432306884832");
+//        tianhua618();
+
+//        updateVersion("e6884e9b-fb5a-4e8b-822f-7cfb8ee4bfea");
+//        lanyun621();
+
+//        lanyun630();
+
+        _lanyun0724();
+
+        return McR.success();
+    }
+
+    // 12.09 蓝云账龄表
+
+    /**
+     * 同步开票档案账龄表
+     */
+
+    private YDParam.YDParamBuilder _initLYParam() {
+        return YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP");
+    }
+
+    /**
+     * 蓝云, 同步催款函
+     */
+    @GetMapping("syncCallLetters")
+    public McR syncCallLetters() {
+        List<Map> dataList = ydService.queryFormData_all(_initLYParam()
+                .formUuid("FORM-EC785A5AB2B9432C892062823EB7C62A9NTL")
+                .searchFieldJson(JSON.toJSONString(UtilMap.map("selectField_lvituew9, radioField_m4qrz687", "正常", "否")))
+                .build());
+        // 更新数据版本, upsert不支持版本更新
+//        if (true) {
+//            for (Map data : dataList) {
+//                ydClient.operateData(_initLYParam()
+//                        .formInstanceId(UtilMap.getString(data, "formInstanceId"))
+//                        .updateFormDataJson(JSON.toJSONString(UtilMap.empty()))
+//                        .useLatestVersion(true)
+//                        .build(), YDConf.FORM_OPERATION.delete);
+//            }
+//            return McR.success();
+//        }
+
+        // 公司主体
+        List<Map> subjectList = ydService.queryFormData_all(_initLYParam()
+                .formUuid("FORM-B1425F3ECC294B858B267A806CB5AEDEFTND")
+                .build());
+        log.info("同步账龄表数据, {}", dataList.size());
+        // 匹配逻辑: 客户 + 蓝云主体【明细按照开票周期】
+        Map<String, Map> setMap = new HashMap();
+        for (Map data : dataList) {
+            String kpgs = UtilMap.getString(data, "selectField_lvc9x4vo");  // 开票公司
+            String khmc = UtilMap.getString(data, "selectField_lvdojfui");  // 客户名称
+            String unique = kpgs + "_" + khmc;
+            Map formData = UtilMap.getMap(setMap, unique); // 累计标识
+            if (!setMap.containsKey(unique)) {
+                // 主表信息: 由 --> 至, 合计金额
+                formData = UtilMap.map("textField_m06ij3z7, textField_m06ij3z8, numberField_m06lmogz", kpgs, khmc, UtilMap.getFloat(data, "numberField_m4qrz685"));
+                List<Map> subjects = subjectList.stream().filter(item -> kpgs.equals(UtilMap.getString(item, "textField_lrintpap"))).collect(Collectors.toList()); // 精准匹配
+                if (subjects.size() == 0) {
+                    continue;
+                }
+                formData.putAll(UtilMap.map("textField_m06ij3zr, textField_m06ij3zs", kpgs, UtilMap.getString(subjects.get(0), "textField_m4qrp33v")));
+                // 付款信息, 指定主体下账号
+                List<Map> details = (List<Map>) UtilMap.getList(subjects.get(0), "tableField_luq9ph7m").stream().filter(item -> "是".equals(((Map) item).get("radioField_m4s770ep"))).collect(Collectors.toList());
+                if (details.size() > 0) {
+                    // 开户行, 银行账号
+                    formData.putAll(UtilMap.map("textField_m06ij3zt, textField_m06ij3zu", "textField_lrpq6es0, textField_lrpq6es1, ", details.get(0)));
+                }
+                // 开票子表: 业务内容-开票内容, 发票号码, 开票时间-开票周期(文本), 项目名称, 金额-未回款金额, 备注-差异说明
+                Map sub = UtilMap.map("textField_m06ij3zf, textField_m06ij3zg, textField_m06ij3zh, textField_m06ij3zi, numberField_m06ij3zk, textField_m06ij3zj", "textField_lvdnme0u, textField_lvd8pp35, textField_m25j5gxv, textField_lvdntzul, numberField_m4qrz685, textareaField_m25j5gyc", data);
+                sub.put("textField_m06ij3ze", 1); // 序号
+                formData.put("tableField_m06ij3zd", UtilList.asList(sub));
+            } else {
+                List<Map> table = UtilMap.getList(formData, "tableField_m06ij3zd");
+                // 开票子表: 业务内容-开票内容, 发票号码, 开票时间-开票周期(文本), 项目名称, 金额-未回款金额, 备注-差异说明
+                Map sub = UtilMap.map("textField_m06ij3zf, textField_m06ij3zg, textField_m06ij3zh, textField_m06ij3zi, numberField_m06ij3zk, textField_m06ij3zj", "textField_lvdnme0u, textField_lvd8pp35, textField_m25j5gxv, textField_lvdntzul, numberField_m4qrz685, textareaField_m25j5gyc", data);
+                sub.put("textField_m06ij3ze", table.size() + 1); // 序号
+                table.add(sub);
+                formData.put("tableField_m06ij3zd", table);
+                // 累计合计金额
+                formData.put("numberField_m06lmogz", UtilMap.getFloat(formData, "numberField_m06lmogz") + UtilMap.getFloat(data, "numberField_m4qrz685"));
+            }
+            setMap.put(unique, formData);
+        }
+
+        log.info("催款函, {}", setMap.size());
+        for (String unique : setMap.keySet()) {
+            Map formData = UtilMap.getMap(setMap, unique);
+            formData.put("textField_m4qxo07x", unique + "_" + UtilMap.getFloat(formData, "numberField_m06lmogz"));
+            ydClient.operateData(_initLYParam()
+                    .formUuid("FORM-738D89FEC34740EC92B08BF6D7B9470DEQY6")
+                    .processCode("TPROC--IMD665A1KDXNJ2V47QQ9UCM9I9F827DEEI60M6")
+                    .formDataJson(JSON.toJSONString(formData))
+                    .userId("396511732") // 通过宜搭平台发起的,不会有待办, 通过企业账号发送 (system)
+                    .build(), YDConf.FORM_OPERATION.start);
+        }
+        return McR.success();
+    }
+
+
+    /**
+     * 蓝云, 全量同步账龄表 [后续可调整为同步前一天更新数据]
+     */
+    @GetMapping("syncAgingSchedule")
+    public void syncAgingSchedule() {
+
+        List<Map> dataList = ydService.queryFormData_all(_initLYParam()
+                .formUuid("FORM-6603375ED27B4D059CBB919C2BEFA44BZVOL")
+//                .searchFieldJson(JSON.toJSONString(UtilMap.map("textField_lvdosccc", "KP_2024121301677")))
+                .build());
+
+        // 合同业务类型: 日保一次性与小业主一致
+        Map compIds = UtilMap.map("大业主, 小业主, 工程订单, 日保一次性", "tableField_lvc9x4vt, tableField_lvdnme13, tableField_lvd8pp44, tableField_lvdnme13");
+
+        // 账龄表主表: 1 开票档案, 合同业务类型, 开票编号
+        Map compId_main = UtilMap.map("associationFormField_m25j5gxb, selectField_lvc9x4vn, textField_lvdosccc", "kpdh_gl, selectField_lvc9x4vn, textField_lvdosccc");
+        // 账龄表主表: 2 开票公司, 公司编码, 开票类型,
+        compId_main.putAll(UtilMap.map("selectField_lvc9x4vo, textField_m065jst5, selectField_lvc9x4vp", "selectField_lvc9x4vo, textField_m065jst5, selectField_lvc9x4vp"));
+        // 账龄表主表: 3 项目点名称, 项目点编号, 收款协议
+        compId_main.putAll(UtilMap.map("textField_lvdntzul, textField_m25j5gxk, selectField_lvc9x4vq", "textField_m4h15mt7, textField_lvdntzul, selectField_lvc9x4vq"));
+        // 账龄表主表: 4 客户名称, 客户编号, 实际开票日期
+        compId_main.putAll(UtilMap.map("selectField_lvdojfui, textField_lvdojfuj, dateField_lw5ud9bk", "selectField_lvdojfui, textField_lvdojfuj, dateField_lw5ud9bk"));
+        // 账龄表主表: 5 实例ID, 发票号码, 发票状态
+        compId_main.putAll(UtilMap.map("textField_m25j5gyi, textField_lvd8pp35, selectField_lvituew9", "formInstanceId, textField_lvd8pp35, selectField_lvituew9"));
+
+        // 账龄表子表: 1 开票周期, 开票周期-文本, 开票内容, 含税单价, 数量, 税率
+        List<String> compId_detail = UtilList.asList("dateField_m25j5gxu", "textField_m25j5gxv", "textField_lvdnme0u", "numberField_lvdnme0w", "numberField_lvdnme0x", "numberField_lvdnme0y");
+        // 账龄表子表: 2 含税小计, 税额, 无税金额, 差异说明, 已回款金额, 回款周期, 回款周期-文本
+        compId_detail.addAll(Arrays.asList("numberField_lvdnme0z", "numberField_lvdnme10", "numberField_lvdnme11", "textareaField_m25j5gyc", "numberField_lvg084l9", "dateField_m0dp1g0e", "textField_m25j5gyh"));
+        // 账龄表子表: 匹配来源数据表
+        Map compIds_tab = UtilMap.map("tableField_lvc9x4vt, tableField_lvd8pp44, tableField_lvdnme13",
+                // 大业主对应子表
+                Arrays.asList("dateField_lw5uybgq", "dateField_lw5uybgq_wb", "textField_lvd8pp2t", "numberField_lvd8pp2v", "numberField_lvd8pp2w", "numberField_lvd8pp2x", "numberField_lvd8pp2y", "numberField_lvd8pp2z", "numberField_lvd8pp30", "textField_lvdnme0g", "numberField_lvg084lb", "dateField_m0f55roc", "dateField_m0f55roc_wb"),
+                // 工程订单对应子表
+                Arrays.asList("dateField_lvd8pp45", "ateField_lvd8pp45_wb", "textField_lvd8pp3w", "numberField_lvd8pp3y", "numberField_lvd8pp3z", "numberField_lvd8pp40", "numberField_lvd8pp41", "numberField_lvd8pp42", "numberField_lvd8pp43", "textField_lvdnme0h", "numberField_lvg084l9", "dateField_m0dp1g0e", "dateField_m0dp1g0e_wb"),
+                // 小业主\日报一次性对应子表
+                Arrays.asList("dateField_lvdnme0t", "dateField_lvdnme0t_wb", "textField_lvdnme0u", "numberField_lvdnme0w", "numberField_lvdnme0x", "numberField_lvdnme0y", "numberField_lvdnme0z", "numberField_lvdnme10", "numberField_lvdnme11", "textField_lvdnme12", "numberField_lvg084la", "dateField_m0f55roe", "dateField_m0f55roe_wb"));
+
+        // 明细处理为主表记录, 以明细形式写入
+        for (Map formData : dataList) {
+
+            String compId_type = UtilMap.getString(compIds, UtilMap.getString(formData, "selectField_lvc9x4vn"));
+            List<Map> details = UtilMap.getList(formData, compId_type);
+            log.info("账龄表同步, {}, {}, {}", UtilMap.getString(formData, "selectField_lvc9x4vn"), formData.get("formInstanceId"), details.size());
+            if (details.isEmpty()) {
+                continue;
+            }
+            // 主表字段匹配 kpdh_gl
+            Map dataForm = UtilMap.empty();
+            for (Object key : compId_main.keySet()) {
+                if ("kpdh_gl".equals(compId_main.get(key))) {
+                    dataForm.put(key, YDConf.associationForm("APP_ERBDTFS82HOVBPL3NFH0", "FORM-6603375ED27B4D059CBB919C2BEFA44BZVOL", UtilMap.getString(formData, "formInstanceId"), UtilMap.getString(formData, "textField_lvdosccc"), null, false));
+                } else {
+                    dataForm.put(key, formData.get(compId_main.get(key)));
+                }
+            }
+            // 子表数据匹配 _wb
+            for (Map detail : details) {
+                // 唯一条件: 单据编号 + 业务类型 + 开票周期
+                String compId_date = (String) UtilMap.getList(compIds_tab, compId_type).get(0);
+                long kpzq = UtilMap.getLong(detail, compId_date);
+                if (kpzq == 0) {
+                    continue;
+                }
+                List<String> arrCompId = UtilMap.getList(compIds_tab, compId_type);
+                for (int i = 0; i < arrCompId.size(); i++) {
+                    String key = compId_detail.get(i);
+                    if (arrCompId.get(i).contains("dateField_")) {
+                        String cId = arrCompId.get(i).replace("_wb", "");
+                        if (!detail.containsKey(cId)) {
+                            continue;
+                        }
+                        long tm = UtilMap.getLong(detail, cId);
+                        if (tm == 0) {
+                            continue;
+                        }
+                        if (arrCompId.get(i).contains("_wb")) {
+                            dataForm.put(key, UtilDateTime.format(new Date(tm), "yyyy-MM"));
+                        } else {
+                            dataForm.put(key, tm);
+                        }
+                    } else {
+                        dataForm.put(key, detail.get(arrCompId.get(i)));
+                    }
+                }
+                String kpzq_wb = UtilDateTime.format(new Date(kpzq), "yyyy-MM");
+                List<Map> searchCondition = Arrays.asList(YDConf.searchCondition_TextFiled("textField_lvdosccc", UtilMap.getString(formData, "textField_lvdosccc"), "eq"),
+                        YDConf.searchCondition_TextFiled("selectField_lvc9x4vn", UtilMap.getString(formData, "selectField_lvc9x4vn"), "eq"),
+                        YDConf.searchCondition_TextFiled("textField_m25j5gxv", kpzq_wb, "eq"));
+                // 回款状态与未回款金额记录, 用于催款函查询
+                float figure = UtilMap.getFloat(dataForm, "numberField_lvdnme0z") - UtilMap.getFloat(dataForm, "numberField_lvg084l9");
+                dataForm.put("radioField_m4qrz687", figure == 0 ? "是" : "否");
+                dataForm.put("numberField_m4qrz685", figure);
+                ydClient.operateData(_initLYParam()
+                        .searchCondition(JSON.toJSONString(searchCondition))
+                        .formUuid("FORM-EC785A5AB2B9432C892062823EB7C62A9NTL")
+                        .formDataJson(JSON.toJSONString(dataForm))
+                        .build(), YDConf.FORM_OPERATION.upsert_v2);
+            }
+        }
+    }
+
+    // 12.17 JL华南用户加入一分
+    @GetMapping("jll/invite")
+    public McR JLL_invite() {
+
+        // 授权专属账号允许加入一分组织
+//        String token = ddClient.getAccessToken("dingy8h3bgoqsdparydl", "Zshlws58uFmezijH7TAn2LgXvh2qqXUUtpObXJQODlE5SeWAQYmvKbgIX_ona0_t");
+//        ddClient_contacts.multiOrgPermissions(token, "dingc54a4bb4def9663f35c2f4657eb6378f", null);
+
+        // 0109 授权专属账号允许加入上海分组织 (华东)
+        String token = ddClient.getAccessToken("dingy8h3bgoqsdparydl", "Zshlws58uFmezijH7TAn2LgXvh2qqXUUtpObXJQODlE5SeWAQYmvKbgIX_ona0_t");
+        //ddClient_contacts.multiOrgPermissions(token, "dingc54a4bb4def9663f35c2f4657eb6378f", null);
+        ddClient_contacts.multiOrgPermissions(token, "ding120e2a852a39a5df35c2f4657eb6378f", null);
+
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_GW0W0VPQ0FR53R258J6G")
+                .systemToken("WO966N71H84IUHE3AYRISBIWQTIH39K5R1ZRL32")
+                .formUuid("FORM-F521892655A047C693B70982B97F8027WZRN")
+                .build());
+        log.info("用户列表, {}", dataList.size());
+
+//        String ddToken = ddClient.getAccessToken("dingz2yea3codfo8c7lm", "JyytgtZZPmPVKifyiWO34cuQJZp6c99NMEJ7CfPoSVR8WceDRBPlnCU9_Zf1sQKT");
+        // 0109 专属钉账号加入上海分, 保留华东原架构
+        String ddToken = ddClient.getAccessToken("dingljptjms8t3xdmieo", "oJmnvcLjTKiwRArUqvi5K9lfGyRx1yitkPtrAIvJ_RhWlbrwUZnrF8cMkf94kOxo");
+//        dataList = Arrays.asList(dataList.get(0));
+        for (Map data : dataList) {
+            if (UtilMap.getString(data, "radioField_m3zdctzd").equals("是")) {
+                continue;
+            }
+            String name = UtilMap.getString(data, "textField_p2em9tg") + "(企)";
+            //List<Long> deptId = Arrays.asList(UtilMap.getLong(data, "textField_fngklyc"));
+//            List<Long> deptId = Arrays.asList(980751899L); // 使用隐藏部门做中转
+            List<Long> deptId = Arrays.asList(1L); // 使用隐藏部门做中转
+            // userId、姓名、工号、职位、手机号、邮箱
+//            Map extInfo = UtilMap.map("userid, job_number, title, email, exclusive_mobile", "textField_disml2c, textField_8en47ss, textField_qasx8rm, textField_vyo7anl, textField_t6o72ey", data);
+            Map extInfo = UtilMap.map("userid, job_number, title, exclusive_mobile, email", "textField_disml2c, textField_8en47ss, textField_qasx8rm, textField_t6o72e0y, textField_vyo7anl", data);
+            extInfo.put("userid", UtilMap.getString(data, "textField_disml2c"));
+//            extInfo.put("userid", UtilMap.getString(data, "textField_disml2c") + "d");
+            if (data.containsKey("dateField_m54rr91h") && UtilMap.getLong(data, "dateField_m54rr91h") > 0) {
+                extInfo.put("hired_date", UtilMap.getLong(data, "dateField_m54rr91h"));
+            }
+            Map result = UtilMap.map("radioField_m3zdctzd, textareaField_m3zajr2x", "是", "");
+            try {
+                ddClient_contacts.inviteExclusiveUser(ddToken, name, deptId, "dingf475a1e690748017acaaa37764f94726", UtilMap.getString(data, "textField_disml2c"), extInfo);
+            } catch (McException e) {
+                result.put("radioField_m3zdctzd", "否");
+                result.put("textareaField_m3zajr2x", e.getMessage());
+            }
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_GW0W0VPQ0FR53R258J6G")
+                    .systemToken("WO966N71H84IUHE3AYRISBIWQTIH39K5R1ZRL32")
+                    .formInstanceId(UtilMap.getString(data, "formInstanceId"))
+                    .updateFormDataJson(JSON.toJSONString(result))
+                    .useLatestVersion(true)
+                    .build(), YDConf.FORM_OPERATION.update);
+        }
+
+
+        return McR.success();
+    }
+
+    // 11.27 JLL用户更新
+    @GetMapping("jll/contact")
+    public McR JLL_contact() {
+
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_GW0W0VPQ0FR53R258J6G")
+                .systemToken("WO966N71H84IUHE3AYRISBIWQTIH39K5R1ZRL32")
+                .formUuid("FORM-2DD2B036B7DF4DDCA329E19BD5285EEBNCB8")
+                .build());
+        log.info("用户列表, {}", dataList.size());
+
+        // 更新企业账号信息
+        String accessToken = ddClient.getAccessToken("dingy8h3bgoqsdparydl", "Zshlws58uFmezijH7TAn2LgXvh2qqXUUtpObXJQODlE5SeWAQYmvKbgIX_ona0_t");
+        dataList.forEach(formData -> {
+            if (UtilMap.getString(formData, "radioField_m3zdctzd").equals("是")) {
+                return;
+            }
+            String userId = UtilMap.getString(formData, "textField_m3zajr2t");
+            Map userInfo = null;
+            Map result = UtilMap.map("radioField_m3zdctzd", "是");
+            try {
+                userInfo = ddClient_contacts.getUserInfoById(accessToken, userId);
+            } catch (McException e) {
+                result.put("radioField_m3zdctzd", "是");
+                result.put("textareaField_m3zajr2x", e.getMessage());
+            }
+            if (!Objects.isNull(userInfo)) {
+                List<Long> deptIds = (List<Long>) UtilMap.getList(userInfo, "dept_id_list").stream().map(item -> Long.valueOf(String.valueOf(item))).collect(Collectors.toList());
+                Map update = UtilMap.map("exclusive_mobile, email", UtilMap.getString(formData, "textField_m3zajr2u"), UtilMap.getString(formData, "textField_m3zajr2v"));
+                try {
+                    ddClient_contacts.updateUser_dingTalk(accessToken, userId, deptIds, update);
+                } catch (McException e) {
+                    result.put("radioField_m3zdctzd", "否");
+                    result.put("textareaField_m3zajr2x", e.getMessage());
+                }
+            }
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_GW0W0VPQ0FR53R258J6G")
+                    .systemToken("WO966N71H84IUHE3AYRISBIWQTIH39K5R1ZRL32")
+                    .formInstanceId(UtilMap.getString(formData, "formInstanceId"))
+                    .updateFormDataJson(JSON.toJSONString(result))
+                    .useLatestVersion(true)
+                    .build(), YDConf.FORM_OPERATION.update);
+        });
+        return McR.success();
+    }
+
+
+    // 蓝云 [采购付款更新]
+    private void _lanyun0724() {
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                .formUuid("FORM-BF9735C77D224D01B69BC1C517A89755OV4M")
+                .build());
+        log.info("项目点数据, {}", dataList.size());
+        dataList.forEach(formData -> {
+            updateVersion(UtilMap.getString(formData, "instanceId"));
+        });
+    }
+
+    @Autowired
+    private DDClient_Workflow ddClient_workflow;
+
+    @PostMapping("tmp")
+    public void tmp() {
+//        String token = ddClient.getAccessToken("dingfwn4kpmb4g3dy4fj", "OwdkUc9nvBivpwsg0AiaEcyWLtZ678fskqBJwP7B5CQNrycIoyKuWslFTCOOaZG4");
+
+//        ddClient_workflow.getProcessInstanceId(token, "QVxpoRomSfOqOeuUqzwXvQ04851720676197");
+//        ddClient_workflow.executeRunningApprove(token, "QVxpoRomSfOqOeuUqzwXvQ04851720676197", "396511732", "87755078200", "agree", "", null);
+
+    }
+
+    // 蓝云
+    private void updateVersion(String formInstId) {
+        ydClient.operateData(YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                .formInstanceId(formInstId)
+                .updateFormDataJson(JSON.toJSONString(new HashMap<>()))
+                .useLatestVersion(true)
+                .build(), YDConf.FORM_OPERATION.update);
+
+    }
+
+    /// 630 蓝云项目点数据更新
+    private void lanyun630() {
+
+
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                .formUuid("FORM-97BA6C6C8C50416A9E5854F2F4B5C3F3R4WC")
+//                .searchFieldJson(JSON.toJSONString(UtilMap.map("textField_lrr3rg6h", "XM00363")))
+                .build());
+        log.info("项目点数据, {}", dataList.size());
+        dataList.forEach(formData -> {
+            List<Map> details = UtilMap.getList(formData, "tableField_lrru2tnq");
+//            details = details.stream().filter(item -> UtilMap.isNotBlankString(item, "dateField_lrru2tnr")).collect(Collectors.toList());
+            // 数据格式化
+//            details.forEach(row -> {
+//                row.put("textField_luh0k82j", UtilDateTime.format(new Date(UtilMap.getLong(row, "dateField_lrru2tnr")), "yyyy-MM"));
+//            }
+//            });
+//            Collections.sort(details, Comparator.comparingLong(o -> UtilMap.getLong(o, "dateField_lrru2tnr")));
+
+            // 查找当月数据
+            LocalDateTime currentDate = UtilDateTime.firstDayOfLastMonth(LocalDateTime.now());
+            Optional optional = details.stream().filter(item -> UtilDateTime.getLocalDateTimeTimeStamp(currentDate) == UtilMap.getLong(item, "dateField_lrru2tnr")).findAny();
+            log.info("上个月数据, {}, {}", currentDate, optional);
+            if (!optional.isPresent() || StringUtils.isNotBlank(UtilMap.getString((Map) optional.get(), "numberField_lrru2tnz"))) {
+                return;
+            }
+
+            LocalDateTime lastMonthDate = UtilDateTime.firstDayOfLastMonth(currentDate);
+            Map currentRow = (Map) optional.get();
+            optional = details.stream().filter(item -> UtilDateTime.getLocalDateTimeTimeStamp(lastMonthDate) == UtilMap.getLong(item, "dateField_lrru2tnr")).findAny();
+            log.info("上上月数据, {}, {}", lastMonthDate, optional);
+
+            float price;
+            if (!optional.isPresent() || StringUtils.isBlank(UtilMap.getString((Map) optional.get(), "numberField_lrru2tnz"))) {
+                price = UtilMap.getFloat(currentRow, "numberField_lrru2tns");
+                currentRow.put("numberField_lrru2tnz", price); // 取本月收入定额
+            } else {
+                price = UtilMap.getFloat((Map) optional.get(), "numberField_lrru2tnz");
+                currentRow.put("numberField_lrru2tnz", price); // 取上月收入预估
+            }
+            price += UtilMap.getFloat(formData, "numberField_lutjev0u");
+            log.info("price, {}", price);
+            // 合计初始化
+//            price = details.stream().map(item -> UtilMap.getFloat(item, "numberField_lrru2tnz")).collect(Collectors.toList()).stream().reduce(0f, (acc, cur) -> acc + cur);
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                    .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                    .formInstanceId(UtilMap.getString(formData, "formInstanceId"))
+//                    .useLatestVersion(true)
+                    .updateFormDataJson(JSON.toJSONString(UtilMap.map("tableField_lrru2tnq, numberField_lutjev0u", details, price)))
+                    .build(), YDConf.FORM_OPERATION.update);
+        });
+    }
+
+    /// 6.21 蓝云数据版本问题
+    private void lanyun621() {
+
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                .formUuid("FORM-5A6C8B23015F44949F66543E5E684F5EF3SK")
+                .build());
+        dataList.forEach(item -> {
+            updatePosition(UtilMap.getString(item, "textField_lxjl5kwm"));
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_ERBDTFS82HOVBPL3NFH0")
+                    .systemToken("RRB66F91T97H1WN89QZYC47PKLZO2ZQOUMOQLP")
+                    .formInstanceId(UtilMap.getString(item, "formInstanceId"))
+                    .useLatestVersion(true)
+                    .updateFormDataJson(JSON.toJSONString(new HashMap<>()))
+                    .build(), YDConf.FORM_OPERATION.update);
+        });
+    }
+
+    /// 6.18 苏州天华更新部门职位
+    private void tianhua618() {
+        List<Map> dataList = ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_GW0W0VPQ0FR53R258J6G")
+                .systemToken("WO966N71H84IUHE3AYRISBIWQTIH39K5R1ZRL32")
+                .formUuid("FORM-120A6337A2034F4FB0E44949DB05C3BE5FTC")
+                .build());
+        dataList.forEach(item -> {
+            updatePosition(UtilMap.getString(item, "textField_lxjl5kwm"));
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_GW0W0VPQ0FR53R258J6G")
+                    .systemToken("WO966N71H84IUHE3AYRISBIWQTIH39K5R1ZRL32")
+                    .formInstanceId(UtilMap.getString(item, "formInstanceId"))
+                    .build(), YDConf.FORM_OPERATION.delete);
+        });
+    }
+
+    private void updatePosition(String userId) {
+        String token = ddClient.getAccessToken("ding4h80bxhmsujcmkad", "gO9i1LvoExN3ozhKbJ6ZzZhHgvAXEfUfwJMJvYyaP0H5zt5jk2zhiWQLXQMCz9yd");
+        Map userInfo = null;
+        try {
+            userInfo = ddClient_contacts.getUserInfoById(token, userId);
+        } catch (McException e) {
+            log.error(e.getMessage(), e);
+            return;
+        }
+        String title = UtilMap.getString(userInfo, "title");
+        if (StringUtils.isBlank(title)) {
+            log.info("更新异常, {}, {}", userInfo);
+            return;
+        }
+        List<Long> deptIds = (List<Long>) UtilMap.getList(userInfo, "dept_id_list").stream().map(item -> Long.parseLong(String.valueOf(item))).collect(Collectors.toList());
+        List<Map> titles = deptIds.stream().map(dept_id -> UtilMap.map("dept_id, title", dept_id, title)).collect(Collectors.toList());
+        ddClient_contacts.updateUser_dingTalk(token, userId, deptIds, UtilMap.map("dept_title_list", titles));
+    }
+
+}
+
+

+ 146 - 0
mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/XBBController.java

@@ -0,0 +1,146 @@
+package com.malk.cloudpure.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.cloudpure.service.CPClient;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.server.xbongbong.XBBConf;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.xbongbong.XBBClient;
+import com.malk.utils.UtilDateTime;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping("crm")
+public class XBBController {
+
+    @Autowired
+    private XBBClient xbbClient;
+
+    /**
+     * 模糊查询归属权限下客户列表
+     */
+    @PostMapping("list/customer")
+    McR customerList(@RequestBody Map<String, String> data) {
+
+        log.info("客户列表, {}", data);
+        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")));
+
+        log.info("客户响应, {}", customers);
+        return McR.success(customers);
+    }
+
+    /**
+     * 模糊查询归属权限下, 指定字段类型 [后置查询]
+     */
+    private List<Map> _likeCustomerList(String url, String type, String name, String userId) {
+        List<Map> rsp = xbbClient.getFormList(type, 1, 100);
+        long formId = UtilMap.getLong(rsp.get(0), "formId");
+        rsp = xbbClient.getFormDefine(formId, 0); // 表单定义
+        String nameField = (rsp.stream().filter(item -> item.get("attrName").equals("客户名称")).findAny().get()).get("attr").toString();
+        rsp = xbbClient.getDataList(url, formId, Arrays.asList(XBBConf.getConditionMap(nameField, "like", name)), null);
+        return rsp.stream().filter(item -> {
+            List<String> ownerId = (List<String>) JSON.parse(String.valueOf(((Map) item.get("data")).get("ownerId")));
+            List<String> coUserId = (List<String>) JSON.parse(String.valueOf(((Map) item.get("data")).get("coUserId")));
+            // 后置查询, 属于客户负责人和协同人
+            return ownerId.contains(userId) || coUserId.contains(userId);
+        }).map(item -> UtilMap.map("name", String.valueOf(((Map) item.get("data")).get(nameField)))).collect(Collectors.toList());
+    }
+
+    /**
+     * 模糊查询归属权限下, 指定字段类型 [后置查询]
+     */
+    private List<Map> _likeContractList(String url, String type, String name, String userId) {
+        List<Map> rsp = xbbClient.getFormList(type, 1, 201);
+        long formId = UtilMap.getLong(rsp.get(0), "formId");
+        rsp = xbbClient.getFormDefine(formId, 0); // 表单定义
+        String nameField = (rsp.stream().filter(item -> item.get("attrName").equals("合同名称")).findAny().get()).get("attr").toString();
+        String codeField = (rsp.stream().filter(item -> item.get("attrName").equals("合同编号")).findAny().get()).get("attr").toString();
+        String cashField = (rsp.stream().filter(item -> item.get("attrName").equals("合同金额")).findAny().get()).get("attr").toString();
+        rsp = xbbClient.getDataList(url, formId, Arrays.asList(XBBConf.getConditionMap(nameField, "like", name)), null);
+        return rsp.stream().filter(item -> {
+            List<String> ownerId = (List<String>) JSON.parse(String.valueOf(((Map) item.get("data")).get("ownerId")));
+            List<String> coUserId = (List<String>) JSON.parse(String.valueOf(((Map) item.get("data")).get("coUserId")));
+            // 后置查询, 属于客户负责人和协同人
+            return ownerId.contains(userId) || coUserId.contains(userId);
+        }).map(item -> {
+            Map data = (Map) item.get("data");
+            return UtilMap.map("name, value", data.get(nameField), data.get(codeField) + "/" + data.get(cashField));
+        }).collect(Collectors.toList());
+    }
+
+    /**
+     * 模糊查询归属权限下合同列表
+     */
+    @PostMapping("list/contract")
+    McR contractList(@RequestBody Map<String, String> data) {
+
+        log.info("合同列表, {}", data);
+        McException.assertParamException_Null(data, "name, userId");
+
+        List<Map> customers = new ArrayList<>();
+        customers.addAll(_likeContractList(XBBConf.API_LIST_contract, "钉钉", data.get("name"), data.get("userId")));
+        // prd 5.9 Teambition合同中台停用
+        //customers.addAll(_likeContractList(XBBConf.API_LIST_contract, "Teambition", data.get("name"), data.get("userId")));
+
+        log.info("合同响应, {}", customers);
+        return McR.success(customers);
+    }
+
+    /**
+     * 自动填充合同编号/合同金额
+     */
+    @PostMapping("code/contract")
+    McR contractCode(@RequestBody Map<String, String> data) {
+
+        String[] content = data.get("code").split("/");
+        log.info("自动填充, {}, {}", content);
+        return McR.success(UtilMap.map("code, cash", content[0], content[1]));
+    }
+
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private CPClient cpClient;
+
+    @PostMapping("test")
+    McR test() {
+
+        //ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), "16608972969409067");
+        LocalDate date = UtilDateTime.parseLocalDate("2024-05-15");
+        cpClient.syncXBongBongForRecord_all(date);
+
+        String sDate = UtilDateTime.formatDate(UtilDateTime.parseDate("2024-05-15"));
+        cpClient.syncDingTalkLogForRecord(sDate);
+        return McR.success();
+    }
+}
+
+

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

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

+ 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 * * 1,2,3,4,5")
+    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);
+}

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

@@ -0,0 +1,362 @@
+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_Report;
+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_Report 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);
+        // prd 5.9 Teambition合同中台停用
+        //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);
+    }
+}

+ 76 - 0
mjava-cloudpure/src/main/resources/application-dev.yml

@@ -0,0 +1,76 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/cloudpure
+
+# 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
+    # 主库
+    primary:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+# dingtalk
+dingtalk:
+  agentId: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey: 6kaXxySzRcBlu8nvEJji7bg73rSX5F9ieXdVunqWvXc
+  token: Mv4daF2LqpmoIOe5XMeipdl0YT
+  robotCode: dingozv6fzkpqkiupd3d
+  operator: "16608972969409067"   # 牧语 [开头0, 需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_YJO0WE6WJ3YUO62PEB2F
+  systemToken: JS766JD1CQUDWUMTAPE6AD49XWBI2P7LT74MLH6
+
+# xbongbong
+xbongbong:
+  corpid: dingcc1b1ffad0d5ca1d
+  token: f760ea3d154e45c839b2169f09b76f23
+  userId: 102314374732747224 # 汐瑶
+  callbackToken:
+
+# vika
+vika:
+  apiToken: uskk7kCIZZtZf06H8RCjLN2
+  spaceId: spcEX0JJDTVt0

+ 51 - 0
mjava-cloudpure/src/main/resources/application-prod.yml

@@ -0,0 +1,51 @@
+# 环境配置
+server:
+  port: 9011
+  servlet:
+    context-path: /api/cloudpure
+
+# condition
+spel:
+  scheduling: true        # 定时任务是否执行
+  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: 2673435445
+  appKey: dingozv6fzkpqkiupd3d
+  appSecret: bO4AA6ujXj8xgLBJI5pR7ns0vRsHCn8Ng9fTf9WF95HTOlCW0oybYpHsuxXuBPiO
+  corpId: dingcc1b1ffad0d5ca1d
+  aesKey: 6kaXxySzRcBlu8nvEJji7bg73rSX5F9ieXdVunqWvXc
+  token: Mv4daF2LqpmoIOe5XMeipdl0YT
+  robotCode: dingozv6fzkpqkiupd3d
+  operator: "16608972969409067"   # 牧语 [开头0, 需要转一下字符串]
+
+# aliwork
+aliwork:
+  appType: APP_YJO0WE6WJ3YUO62PEB2F
+  systemToken: JS766JD1CQUDWUMTAPE6AD49XWBI2P7LT74MLH6
+
+# xbongbong
+xbongbong:
+  corpid: dingcc1b1ffad0d5ca1d
+  token: f760ea3d154e45c839b2169f09b76f23
+  userId: 102314374732747224 # 汐瑶
+  callbackToken:
+
+# vika
+vika:
+  apiToken: uskk7kCIZZtZf06H8RCjLN2
+  spaceId: spcEX0JJDTVt0

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

@@ -0,0 +1,35 @@
+#!/bin/bash
+appname='mjava-cloudpure'
+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

+ 54 - 0
mjava-dongfangxinhua/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-dongfangxinhua</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-dongfangxinhua/src/main/java/com/malk/dongfangxinhua/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.dongfangxinhua;
+
+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. 标识对表没有任何操作. 若不设置为 none, 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);
+    }
+}

+ 161 - 0
mjava-dongfangxinhua/src/main/java/com/malk/dongfangxinhua/controller/DFXHController.java

@@ -0,0 +1,161 @@
+package com.malk.dongfangxinhua.controller;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+
+import cn.hutool.core.util.XmlUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.XML;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilServlet;
+import lombok.extern.slf4j.Slf4j;
+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 javax.servlet.http.HttpServletRequest;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping
+public class DFXHController {
+
+    /// doc: http://47.111.179.216/interface/main.html#
+    private static final String API = "http://27.115.15.66:8098/BisOutWeb/payCenter/dealReq.srv";
+
+    /// xml请求
+    private JSONObject _doPost(String tradeCode, Map condition) {
+
+        // xml请求格式数据处理
+        Map head = UtilMap.map("erpSysCode, custNo, tradeName", "XYYGLXT", "300200632", tradeCode);
+        Map body = UtilMap.map("head, map", head, condition);
+        String data = XmlUtil.mapToXmlStr(body, "body");
+        log.info("请求入参, {}", XML.toJSONObject(data));
+
+        /// prd 财资固定格式, xml头数据前需要添加类型code, 且不能换行
+        data = "0000012001" + tradeCode + data;
+        String rsp = HttpRequest.post(API).body(data).execute().body();
+        JSONObject result = XML.toJSONObject(rsp);
+        log.info("请求响应, {}", result);
+        result = result.getJSONObject("body");
+
+        // 错误拦截, 统一返回
+        String message = result.getJSONObject("head").getStr("retMsg");
+        McException.assertAccessException(!"查询成功".equals(message), message);
+
+        return result;
+    }
+
+    /**
+     * 查询联行号
+     */
+    @PostMapping("bank")
+    McR bank(String name) {
+
+        JSONObject rsp = _doPost("BY0004", UtilMap.map("bankTypeName, bankName", "银行", name));
+        JSONArray list = rsp.getJSONObject("loopData").getJSONArray("row");
+
+        return McR.success(list);
+    }
+
+
+    /**
+     * 付款申请
+     */
+    @PostMapping("payment")
+    McR payment(@RequestBody Map data) {
+
+        // 付款: payerAccNo, payerCorpName; 领款: payeeAccNo, payeeAccName, payeeBankName, payeeBankCode; 金额: payMoney, payPurpose, erpReqNo
+        JSONObject rsp = _doPost("BY0001", data);
+        JSONObject state = rsp.getJSONObject("loopData").getJSONObject("row");
+
+        return McR.success(state);
+    }
+
+    /**
+     * 付款状态
+     */
+    @PostMapping("state")
+    McR payment(String payNo) {
+
+        JSONObject rsp = _doPost("BY0003", UtilMap.map("erpReqNo", payNo));
+        JSONObject state = rsp.getJSONObject("loopData").getJSONObject("row");
+
+        return McR.success(state);
+    }
+
+
+    /**
+     * 付款账号同步
+     */
+    @PostMapping("sync")
+    McR sync(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("付款账号同步, {}", data);
+
+        // 查询付款账户
+
+
+        // 更新付款信息
+        
+
+        return McR.success();
+    }
+
+
+    @PostMapping(value = "test")
+    McR test(String name, HttpServletRequest request) {
+
+//        XmlUtil.;
+
+//        String rsp = UtilHttp.doPost(API, null, null, null, null);
+
+//        String xmlData = "";
+//        Document document = XmlUtil.parseXml(xmlData);
+
+        StringBuffer reqXmlData = new StringBuffer();
+        try {
+            InputStream inputStream = request.getInputStream();
+            String s;
+            BufferedReader in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+            while ((s = in.readLine()) != null) {
+                reqXmlData.append(s);
+            }
+            in.close();
+            inputStream.close();
+        } catch (IOException e) {
+            System.out.println("流解析xml数据异常!");
+            e.printStackTrace();
+        }
+        //判断请求数据是否为空
+        if (reqXmlData.length() <= 0) {
+            System.out.println("请求数据为空!");
+        }
+
+        //json类型数据 [自动去掉标识]
+        JSONObject jsonObject = XML.toJSONObject(reqXmlData.toString());
+        log.info("xxxx,, {}", jsonObject);
+
+        String data = reqXmlData.toString();
+//        UtilHttp.doRequest(UtilHttp.METHOD.POST, API, null, null, jsonObject);
+
+        log.info("xxx, {}", "0000012001BY0004<?xml version=\\\"1.0\\\" encoding=\\\"UTF-8\\\"?>\\n\"" + jsonObject.toString());
+        String rsp = HttpRequest.post(API).body(data).execute().body();
+        log.info("xxx, {}", rsp);
+
+        return McR.success(jsonObject.toString());
+    }
+}

+ 65 - 0
mjava-dongfangxinhua/src/main/resources/application-dev.yml

@@ -0,0 +1,65 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/dongfangxinhua
+
+# 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
+    # 主库
+    primary:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+# dingtalk
+dingtalk:
+  agentId: 2660236361
+  appKey: dinguuieqv4lkvp3vkaf
+  appSecret: N5JjPU9RDk77pTze5vRWmiWLDjPKeYJV3sQrmYgN_SC57nOALmj570rVB0SGGcQQ
+  corpId: dingec9ee223c2b3a671
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_LNBUWW7ZBXRUWE29PCOV"
+  systemToken: "WC866ZA1PI1B2L1JDNL334F44KI03GREIV1ILZ13"
+
+

+ 38 - 0
mjava-dongfangxinhua/src/main/resources/application-prod.yml

@@ -0,0 +1,38 @@
+# 环境配置
+server:
+  port: 9022
+  servlet:
+    context-path: /api/dongfangxinhua
+
+# 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: 2660236361
+  appKey: dinguuieqv4lkvp3vkaf
+  appSecret: N5JjPU9RDk77pTze5vRWmiWLDjPKeYJV3sQrmYgN_SC57nOALmj570rVB0SGGcQQ
+  corpId: dingec9ee223c2b3a671
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_LNBUWW7ZBXRUWE29PCOV"
+  systemToken: "WC866ZA1PI1B2L1JDNL334F44KI03GREIV1ILZ13"

+ 39 - 0
mjava-dongfangxinhua/src/test/resource/server.sh

@@ -0,0 +1,39 @@
+#!/bin/bash
+
+appname='mjava-dongfangxinhua'
+
+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"
+    tail -f log/info.log
+  else
+    if [ "$1" == "test" ]; then
+      nohup java -Xms256m -Xmx256m -jar $appname.jar --spring.profiles.active=test &
+      echo "server test is starting"
+      tail -f log/info.log
+    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

+ 54 - 0
mjava-fengkaili/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-fengkaili</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-fengkaili/src/main/java/com/malk/fengkaili/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.fengkaili;
+
+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. 标识对表没有任何操作. 若不设置为 none, 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);
+    }
+}

+ 152 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java

@@ -0,0 +1,152 @@
+package com.malk.fengkaili.controller;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+
+import com.malk.fengkaili.repository.entity.FKLDdContactPo;
+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.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;
+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.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@RestController
+@RequestMapping
+public class FKLController {
+
+    @Autowired
+    private FKLService fklService;
+
+    /**
+     * 同步用户 & 部门
+     */
+    @PostMapping("syncUserInfo")
+    McR syncUserInfo() {
+        fklService.syncUserInfo();
+        return McR.success();
+    }
+
+    /// 考勤汇总
+    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");
+        // 基于用户分页
+        Date sDate = UtilDateTime.parseDate(UtilMap.getString(data, "startTime"));
+        Page page = fklService.queryUserInfos(UtilMap.getInt(data, "page"), UtilMap.getInt(data, "size"), UtilMap.getString(data, "name"), dpetIds, sDate);
+        McException.assertAccessException(page.getTotalElements() == 0, "查询用户为空!");
+
+        List<FKLDdContactPo> userInfos = page.getContent();
+        List<Map> dataList = fklService.queryAttendanceList(data.get("startTime").toString(), data.get("endTime").toString(), userInfos, days);
+        log.info("汇总数量, {}", dataList.size());
+        return McPage.page(page, dataList);
+    }
+
+    /**
+     * 查询考勤汇总
+     */
+    @PostMapping("queryAttendanceList")
+    McR queryAttendanceList(@RequestBody Map data, HttpServletRequest request) {
+        return McR.success(_getAttendanceList(data, null, request));
+    }
+
+    /**
+     * 查询考勤汇总 [天]
+     */
+    @PostMapping("queryAttendanceDays")
+    McR queryAttendanceDays(@RequestBody Map data, HttpServletRequest request) {
+        List<String> days = new ArrayList<>();
+        return McR.success(UtilMap.map("page, prop", _getAttendanceList(data, days, request), days));
+    }
+
+    /**
+     * 导出考勤汇总
+     */
+    @PostMapping("exportAttendanceList")
+    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, request).getList();
+        //  获取出现最多次作为法定应出勤天数, 考勤应出勤天数和班组 + 人员挂钩 [班次详情应出勤不准确]
+        float workdays = (Float) UtilList.maxFrequencyObject(dataList.stream().map(item -> {
+            float val = 0.f;
+            // 数据内0字段被忽略, 兼容处理
+            if (item.containsKey("出勤天数")) {
+                val = UtilMap.getFloat(item, "出勤天数");
+            }
+            return val;
+        }).collect(Collectors.toList()));
+        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");
+    }
+
+    /**
+     * 导出考勤汇总 [天]
+     */
+    @PostMapping("exportAttendanceDays")
+    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, 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();
+    }
+}

+ 23 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/dao/FKLDdContactDao.java

@@ -0,0 +1,23 @@
+package com.malk.fengkaili.repository.dao;
+
+import com.malk.fengkaili.repository.entity.FKLDdContactPo;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+import java.util.Date;
+
+/**
+ * 钉钉花名册同步
+ */
+@Transactional
+public interface FKLDdContactDao extends JpaRepository<FKLDdContactPo, Long>, JpaSpecificationExecutor<FKLDdContactPo> {
+
+    FKLDdContactPo findByUserId(String userId);
+
+    @Modifying
+    @Query("update FKLDdContactPo set leaveDate = ?2 where userId = ?1")
+    void updateLeaveDate(String userId, Date leaveDate);
+}

+ 72 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/entity/FKLDdContactPo.java

@@ -0,0 +1,72 @@
+package com.malk.fengkaili.repository.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.malk.base.BasePo;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import javax.persistence.Table;
+import javax.persistence.Temporal;
+import javax.persistence.TemporalType;
+import java.util.Date;
+
+@Entity
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Table(name = "fkl_dd_contact")
+public class FKLDdContactPo extends BasePo {
+
+    /**
+     * 员工Id
+     */
+    private String userId;
+
+    /**
+     * 员工姓名
+     */
+    private String name;
+
+    /**
+     * 员工工号
+     */
+    private String jobNumber;
+
+    /**
+     * 手机号
+     */
+    private String mobile;
+
+    /**
+     * 部门Id
+     */
+    private long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 入职日期
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date hiredDate;
+
+    /**
+     * 离职日期
+     */
+    @Temporal(TemporalType.TIMESTAMP)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date leaveDate;
+
+    /**
+     * 考勤备注 [无需打卡]
+     */
+    private String remark;
+}

+ 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 = "0 0 4 * * ? ")
+    public void syncDingTalkFailedList() {
+        try {
+            fklService.syncUserInfo();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 28 - 0
mjava-fengkaili/src/main/java/com/malk/fengkaili/service/FKLService.java

@@ -0,0 +1,28 @@
+package com.malk.fengkaili.service;
+
+import com.malk.fengkaili.repository.entity.FKLDdContactPo;
+import org.springframework.data.domain.Page;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public interface FKLService {
+
+    /**
+     * 同步用户信息
+     */
+    void syncUserInfo();
+
+    /**
+     * 查询用户列表
+     */
+    Page<FKLDdContactPo> queryUserInfos(int page, int size, String name, List<Long> deptIds, Date sDate);
+
+    /**
+     * 考勤数据统计
+     *
+     * @param days 考勤明细日期表头
+     */
+    List<Map> queryAttendanceList(String start, String end, List<FKLDdContactPo> userInfos, List<String> days);
+}

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

@@ -0,0 +1,659 @@
+package com.malk.fengkaili.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.malk.fengkaili.repository.dao.FKLDdContactDao;
+import com.malk.fengkaili.repository.entity.FKLDdContactPo;
+import com.malk.fengkaili.service.FKLService;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Attendance;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDService;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilList;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilNumber;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+import javax.persistence.criteria.Predicate;
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class FKLImplService implements FKLService {
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private FKLDdContactDao fklDdContactDao;
+
+    @Autowired
+    private DDClient_Attendance ddClient_attendance;
+
+    @Autowired
+    private DDService ddService;
+
+    /**
+     * 同步用户信息
+     */
+    @Override
+    public void syncUserInfo() {
+        // 匹配部门信息, 全量
+        ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true).forEach(deptId -> {
+            // String deptName = ddClient_contacts.getDepartmentInfo(ddClient.getAccessToken(), deptId).getDefault("name").toString();
+            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
+            if (userIds.size() > 0) {
+                // 获取部门层级拼接
+                String deptName = ddService.getUserDepartmentHierarchyJoin(ddClient.getAccessToken(), userIds.get(0), "-");
+                for (String userId : userIds) {
+                    // 牧语
+                    if ("0953580166811961653".equals(userId)) {
+                        continue;
+                    }
+                    Map userinfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
+                    // 员工信息表, 落库
+                    FKLDdContactPo contactPo = FKLDdContactPo.builder()
+                            .userId(userId)
+                            .name(UtilMap.getString(userinfo, "name"))
+                            .jobNumber(UtilMap.getString(userinfo, "job_number"))
+                            .deptId(deptId)
+                            .deptName(deptName)
+                            .mobile(UtilMap.getString(userinfo, "mobile"))
+                            .hiredDate(userinfo.containsKey("hired_date") ? new Date(UtilMap.getLong(userinfo, "hired_date")) : null)
+                            .remark(UtilMap.getString(userinfo, "remark")) // 无需打卡 标记
+                            .build();
+                    FKLDdContactPo po = fklDdContactDao.findByUserId(userId);
+                    // 员工更新, 组织架构调整
+                    if (ObjectUtil.isNotNull(po)) {
+                        contactPo.id = po.id;
+                        contactPo.setCreateTime(po.getCreateTime());
+                    }
+                    fklDdContactDao.save(contactPo);
+                    log.info("同步人员, {}", contactPo);
+                }
+            }
+        });
+
+        // 同步离职人员, 标记离职日期
+        Date start = UtilDateTime.convertToDateFromLocalDateTime(UtilDateTime.firstDayOfLastMonth(LocalDateTime.now()));
+        ddClient_contacts.getLeaveEmployeeRecords(ddClient.getAccessToken(), start, null).forEach(item -> {
+            log.info("同步#离职人员, {}", item);
+            fklDdContactDao.updateLeaveDate(item.get("userId"), UtilDateTime.parse(item.get("leaveTime"), "yyyy-MM-dd'T'HH:mm:ss"));
+        });
+    }
+
+    /**
+     * 查询用户列表
+     */
+    @Override
+    public Page<FKLDdContactPo> queryUserInfos(int page, int size, String name, List<Long> deptIds, Date sDate) {
+
+        // 分页 & 排序
+        Sort sort = Sort.by(Sort.Direction.ASC, "deptName");
+        Pageable pageable = PageRequest.of(page - 1, size, sort);
+
+        // 查询条件: 姓名, 所属部门
+        Specification<FKLDdContactPo> specification = (root, criteriaQuery, criteriaBuilder) -> {
+            List<Predicate> predicateList = new ArrayList<>();
+            if (StringUtils.isNotBlank(name)) {
+                predicateList.add(criteriaBuilder.equal(root.get("name"), name));
+            }
+            if (UtilList.isNotEmpty(deptIds)) {
+                predicateList.add(criteriaBuilder.in(root.get("deptId")).value(deptIds));
+            }
+            // 2月前离职人员过滤 [or语法]
+            predicateList.add(criteriaBuilder.or(criteriaBuilder.isNull(root.get("leaveDate")), criteriaBuilder.greaterThan(root.get("leaveDate"), sDate)));
+            return criteriaBuilder.and(predicateList.toArray(new javax.persistence.criteria.Predicate[predicateList.size()]));
+        };
+        // 无数据时返回空列表
+        return fklDdContactDao.findAll(specification, pageable);
+    }
+
+    /// 累计月度汇总数字
+    private Object _reduceAttendance(Map column, String name, String keyList) {
+        Object value;
+        List<Map> vals = (List<Map>) column.get(keyList);
+        // 异常信息, 保留备注
+        if (name.equals("考勤结果")) {
+            List<String> tmps = new ArrayList<>(); // 同行出差会重复, 考勤结果要过滤
+            vals.stream().forEach(item -> {
+                // prd 异常补录当前日期
+                String content = UtilMap.getString(item, "value");
+                String svalue = content;
+                if (!content.contains("-")) {
+                    content += UtilMap.getString(item, "date").split(" ")[0];
+                }
+                content = content.replace("未打卡,", "").replace("正常,", "").replace("休息并打卡,", "").replace("休息,", "");
+                // 休息有外出/出差 , 正常带其他状态情况 || 超过90未打卡静默用户 || 被添加为协同人后, 钉钉也会记录一条出差
+                if (content.contains("出差")) {
+                    // 兼容出差中还有其他考勤结果, 以及还存在跨天的情况下
+                    List<String> arr = new ArrayList<>();
+                    for (String t : content.split(",")) {
+                        if (!arr.contains(t) && !tmps.stream().filter(s -> s.contains(t)).findAny().isPresent()) {
+                            arr.add(t);
+                        }
+                    }
+                    if (arr.size() == 0) {
+                        return;
+                    }
+                    content = String.join(",", arr);
+                }
+                boolean isFuture = UtilDateTime.parseLocalDateTime(UtilMap.getString(item, "date")).isAfter(LocalDateTime.now());
+                if (!isFuture && StringUtils.isNotBlank(svalue) && !tmps.contains(content) && !content.contains("休息") && !svalue.equals("正常") && !svalue.equals("未打卡")) {
+                    tmps.add(content);
+                }
+            });
+            value = String.join("; ", tmps);
+        } else {
+            value = vals.stream().map(item -> UtilMap.getFloat(item, "value")).reduce(0.f, (a, b) -> {
+                // ddExt: 出差默认是可重复提交, 且若被添加为协同人, 也会多累计一天出差 [但工作时长是正常]. 可开启不允许重复提交, 同样的同行人会冲突
+                if (name.equals("出差时长") && b > 1.0f) {
+                    b = 1.0f;
+                }
+                return a + b;
+            });
+        }
+        return value;
+    }
+
+    /// 缓存考勤自定义列
+    private List<Map> columns;
+
+    List<Map> getColumns() {
+        if (UtilList.isEmpty(columns)) {
+            columns = ddClient_attendance.getAttColumns(ddClient.getAccessToken());
+        }
+        return columns;
+    }
+
+    /**
+     * 考勤数据统计
+     */
+    @Override
+    public List<Map> queryAttendanceList(String start, String end, List<FKLDdContactPo> userInfos, List<String> days) {
+
+        // 考勤列, 假期信息定义
+        List<String> columnNames = Arrays.asList("旷工天数", "出勤天数", "工作时长", "考勤结果", "出差时长", "迟到次数", "早退次数", "下班缺卡次数", "上班缺卡次数", "外出时长", "休息日加班", "工作日加班", "节假日加班", "严重迟到次数", "应出勤天数");
+        AtomicReference<String> fileId_attendance_days = new AtomicReference<>(""); // 出勤天数字段id
+        AtomicReference<String> fileId_attendance_result = new AtomicReference<>(""); // 考勤结果字段id
+        List<Map> columns = getColumns();
+        Map columnIds = new HashMap();
+        // 假期单独返回, 钉钉产品规则
+        List<String> leaveNames = columns.stream().filter(column -> {
+                    if ("出勤天数".equals(column.get("name"))) {
+                        fileId_attendance_days.set(String.valueOf(column.get("id")));
+                    }
+                    if ("考勤结果".equals(column.get("name"))) {
+                        fileId_attendance_result.set(String.valueOf(column.get("id")));
+                    }
+                    // 列类型储存id映射名称为map, 考勤数据返回仅保留列id
+                    if (columnNames.contains(column.get("name"))) {
+                        columnIds.put(column.get("id").toString(), column.get("name"));
+                        return false;
+                    }
+                    return column.get("alias").equals("leave_");
+                }
+        ).map(column -> String.valueOf(column.get("name"))).collect(Collectors.toList());
+
+        // 考勤汇总数据
+        List<Map> attendanceInfos = new ArrayList<>();
+        List<String> queryIds = new ArrayList<>(columnIds.keySet()); // 考勤列定义
+        userInfos.forEach(po -> {
+            Map attendanceInfo = UtilMap.map("员工ID, 员工姓名, 员工工号, 所属部门, 考勤状态", po.getUserId(), po.getName(), po.getJobNumber(), po.getDeptName(), po.getRemark());
+            // 累计月度汇总
+            List<Map> attendanceList = ddClient_attendance.getAttColumnVal(ddClient.getAccessToken(), po.getUserId(), queryIds, start, end);
+            attendanceList.forEach(column -> {
+                String id = ((Map) column.get("column_vo")).get("id").toString();
+                String name = String.valueOf(columnIds.get(id)); // 接口仅返回列id, 通过map映射
+                attendanceInfo.put(name, _reduceAttendance(column, name, "column_vals"));
+                // prd [sheet2]每天考勤结果统计
+                if (!Objects.isNull(days) && name.equals("考勤结果")) {
+                    List<Map> vals = (List<Map>) column.get("column_vals");
+                    int index = 0;
+                    for (Map<String, String> val : vals) {
+                        index++;
+                        String date = val.get("date").replace(" 00:00:00", "").replace(LocalDate.now().getYear() + "-", "");
+                        String result = val.get("value").replace("休息并打卡,", "").replace("休息,", ""); // 休息有外出/出差;
+                        // prd 5.27 人员到岗后, 未办理入职但已经有出差
+                        if (result.contains("不在考勤组并打卡") && !result.equals("不在考勤组并打卡")) {
+                            result = result.replace("不在考勤组并打卡,", "");
+                        }
+                        // prd 11.26 未排班并打卡 场景兼容
+                        if (result.contains("未排班并打卡")) {
+                            result = "";
+                        }
+                        log.info("人员明细, {} - {}, {}", date, po.getName(), val.get("value"));
+                        String day_1 = "zc", day_2 = "zc", type = "zc"; // 异常类型
+                        if (result.contains("休息") || result.contains("加班") || (val.get("value").contains("休息,") && (!result.contains("出差") && !result.contains("婚假") && !result.contains("产假")))) {
+                            type = "公假"; // 包含休息, 休息加班打卡, 忽略跨休息日连续请假情况, prd 钉钉后台配置: 产假, 婚假按自然日
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (StringUtils.isBlank(result) || result.equals("不在考勤组并打卡")) {
+                            if (StringUtils.isBlank(result)) {
+                                type = "/"; // 新入职
+                            } else {
+                                type = "zc"; // 新入职
+                            }
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (result.equals("正常") || (result.split(",").length == 2 && result.contains("外勤") && result.contains("补卡")) || result.equals("下班外勤") || result.equals("上班外勤") || result.equals("上班外勤,下班外勤")) {
+                            // 包含补卡, 一次外勤补卡, 外勤考勤情况 [调休会被标识为考勤正常]
+                            type = "zc";
+                            day_1 = type;
+                            day_2 = type;
+                        }
+                        // prd 250205 请假为半天模式, 全天计算有误
+//                        else if (result.contains("产假") || result.contains("陪产假") || result.contains("婚假") || result.contains("丧假") || result.contains("育儿假")) {
+//                            type = result.split("假")[0] + "假"; // 按天请假
+//                            day_1 = type;
+//                            day_2 = type;
+//                        }
+                        else if (result.contains("旷工") || result.equals("未打卡")) {
+                            type = "旷工"; // 兼容异常情况
+                            day_1 = type;
+                            day_2 = type;
+                        } else if (result.contains("缺卡") && !result.contains("到")) {
+                            // prd 8点上班, 8点后请假或外出都是缺卡记录
+                            if (result.equals("上班缺卡")) {
+                                type = "缺卡";
+                                day_1 = type;
+                            }
+                            if (result.equals("下班缺卡")) {
+                                // prd 离职操作是直接删除, 会有一次打卡, 符合标记为zc
+                                if (ObjectUtil.isNotNull(po.getLeaveDate()) && date.equals(UtilDateTime.format(po.getLeaveDate(), "MM-dd"))) {
+                                    type = type.length() > 0 ? type : "zc";
+                                    day_2 = "zc";
+                                } else {
+                                    type = "缺卡";
+                                    day_2 = type;
+                                }
+                            }
+                        } else if (result.split(",").length <= 2 && (result.contains("迟到") || result.contains("早退"))) {
+                            // 兼容早退和迟到情况下, 还存在请假情况
+                            if (result.contains("迟到") && !result.contains("补卡申请")) {
+                                type = "迟到"; // 迟到状态标记
+                                float exception_duration = Float.valueOf((result.split(",")[0].split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
+                                if (exception_duration >= 180f) {
+                                    // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                    day_1 = "迟到";
+                                }
+                            }
+                            if (result.contains("早退") && !result.contains("补卡申请")) {
+                                type += type.length() > 0 ? " 早退" : "早退"; // 早退状态标记
+                                float exception_duration = Float.valueOf((result.split(",")[result.split(",").length - 1].split("分钟")[0].replace("下班早退", "")));
+                                if (exception_duration >= 180f) {
+                                    // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                    day_2 = "早退";
+                                }
+                            }
+                        } else {
+                            type = "";
+                            day_1 = "";
+                            day_2 = "";
+                            // 请假 & 出差
+                            for (String status : result.split(",")) {
+                                /// 过滤异常情况 &  未打卡判定为status, 非result & 外勤又外出情况
+                                if (status.contains("补卡申请") || status.contains("正常") || status.equals("未打卡") || status.contains("外勤")) {
+                                    continue;
+                                }
+                                if (status.contains("缺卡") || status.equals("未打卡") || status.contains("迟到") || status.contains("早退")) {
+                                    if (status.equals("上班缺卡")) {
+                                        type = "缺卡";
+                                        day_1 = "缺卡";
+                                    }
+                                    if (status.equals("下班缺卡")) {
+                                        type += "缺卡";
+                                        day_2 = "缺卡";
+                                    }
+                                    // 兼容早退和迟到情况下, 还存在请假情况
+                                    if (status.contains("迟到") || status.contains("早退")) {
+                                        if (status.contains("迟到")) {
+                                            type = "迟到"; // 迟到状态标记
+                                            float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("上班迟到", "").replace("上班严重迟到", "")));
+                                            if (exception_duration >= 180f) {
+                                                // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                                day_1 = "迟到";
+                                            }
+                                        }
+                                        if (status.contains("早退")) {
+                                            type += type.length() > 0 ? " 早退" : "早退"; // 早退状态标记
+                                            float exception_duration = Float.valueOf((status.split(",")[0].split("分钟")[0].replace("下班早退", "")));
+                                            if (exception_duration >= 180f) {
+                                                // 设置早退、迟到3小时以上则在表中记录为早退、迟到. 保留迟到次数记录
+                                                day_2 = "早退";
+                                            }
+                                        }
+                                    }
+                                } else {
+                                    /// 请假数据处理 [小时情况]
+                                    String tmp = status.contains("调休") ? "调休" : status.split("假")[0] + "假"; // 异常类型
+                                    if (status.contains("外出") || Arrays.asList("调休", "哺乳假", "事假").contains(tmp)) {
+                                        if (result.contains("外出")) {
+                                            tmp = "外出";
+                                        }
+                                        // 外出, 调休, 事假, 哺乳假: 兼容9点申请, 排班是8点情况, 不记录缺卡
+                                        if (day_1.equals("缺卡") && result.contains("09:00")) {
+                                            day_1 = "";
+                                        }
+                                        // prd 请假3小时以内标记为zc, 按照小时请假 [调休, 哺乳假, 事假];
+                                        String[] arr = status.split(" ");
+                                        float hour = Float.valueOf((arr[arr.length - 1].replace("小时", "")));
+                                        if (hour < 3.0f && !tmp.equals("外出")) { // <3 同时9点申请标识zc, 避免不能统计外出情况
+                                            continue;
+                                        } else {
+                                            // prd 请假3小时以内标记为zc, 区分上午与下午, 午休从12-13分割
+                                            String sStart = status.split(" ")[1].split("到")[0].replace(":", "");
+                                            type = type.length() > 0 && !tmp.equals(type) ? type + " " + tmp : tmp; // 兼容一天提交两次外出情况
+                                            // 兼容跨天请假场景
+                                            boolean sDate = date.equals(status.split(" ")[0].replace(tmp, ""));
+                                            boolean eDate = date.equals(status.split(" ")[1].split("到")[1]);
+                                            if (Integer.valueOf(sStart) >= 1200 && sDate) {
+                                                day_2 = tmp;
+                                            } else {
+                                                String sEnd = status.split(" ")[2].replace(":", "");
+                                                if (Integer.valueOf(sStart) < 800 || !sDate) {
+                                                    sStart = "0800";
+                                                }
+                                                float hourZao = Duration.between(UtilDateTime.parseLocal(sStart, "HHmm"), UtilDateTime.parseLocal("1200", "HHmm")).toMinutes() / 60f;
+                                                if (hourZao >= 3.0f || (hourZao > 0f && tmp.equals("外出"))) {
+                                                    day_1 += day_1.length() > 0 ? " " + tmp : tmp;
+                                                }
+                                                if (Integer.valueOf(sEnd) > 1700 || !eDate) {
+                                                    sEnd = "1700";
+                                                }
+                                                float hourWan = Duration.between(UtilDateTime.parseLocal("1300", "HHmm"), UtilDateTime.parseLocal(sEnd, "HHmm")).toMinutes() / 60f;
+                                                if (hourWan > 3.0f || (hourWan > 0f && tmp.equals("外出"))) {
+                                                    day_2 += day_2.length() > 0 ? " " + tmp : tmp;
+                                                }
+                                            }
+                                        }
+                                    } else if (status.contains("出差")) {
+                                        // 出差兼容, 半天, 外出, 请假等情况
+                                        type += type.length() > 0 ? (type.contains("出差") ? "" : " 出差") : "出差";
+                                        // 半天出差场景以及被添加为协同人后, 钉钉也会记录一条出差; 均循环进行处理, 即时出差覆盖即当天多次出差也可兼容
+                                        int sStart = Integer.valueOf(status.split(" ")[1].split("到")[0].replace(":", ""));
+                                        int sEnd = Integer.valueOf(status.split(" ")[2].replace(":", ""));
+                                        if (val.get("value").contains("休息")) {
+                                            day_1 = day_1.equals("") ? "公假" : day_1;
+                                            day_2 = day_2.equals("") ? "公假" : day_2;
+                                        }
+                                        if (sStart >= 1200 && date.equals(status.split(" ")[0].replace("出差", ""))) {
+                                            // 跨天: 日期相等, 且下午时间
+                                            day_2 = "出差";
+                                        } else if (sEnd <= 1300 && date.equals(status.split(" ")[1].split("到")[1])) {
+                                            // 跨天: 日期相等, 且上午时间
+                                            day_1 = "出差";
+                                        } else {
+                                            day_1 = "出差";
+                                            day_2 = "出差";
+                                        }
+                                    } else {
+                                        /// 非小时假, 请假数据处理 [半天情况]
+                                        String[] arr = status.split(" ");
+                                        int sstart = Integer.valueOf(status.split(" ")[1].split("到")[0].replace(":", ""));
+                                        float day = Float.valueOf((arr[arr.length - 1].replace("天", "")));
+                                        // prd 250205 部分返回异常数据日期含有年, 需去除
+                                        if (date.split("-").length > 2) {
+                                            date = date.split("-")[1] + "-" + date.split("-")[2];
+                                        }
+                                        boolean sDate = date.equals(status.split(" ")[0].replace(tmp, ""));
+                                        boolean eDate = date.equals(status.split(" ")[1].split("到")[1]);
+                                        type = tmp;
+                                        // 兼容跨天请假场景
+                                        int sEnd = Integer.valueOf(status.split(" ")[2].replace(":", ""));
+                                        if ((day >= 1.0f && ((sDate && eDate) || (sDate & sstart <= 800) || (eDate && sEnd >= 1700))) || (!sDate && !eDate)) {
+                                            day_1 = type;
+                                            day_2 = type;
+                                        } else {
+                                            if (sEnd >= 1700) {
+                                                day_2 = type;
+                                            } else {
+                                                day_1 = type;
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        // prd 9/29 存在请假半天, 早退或迟到情况, 3小时以上才算次数 [兼容早退和迟到情况下, 还存在请假情况]
+                        if (type.contains(",")) {
+                            for (String tmp : type.split(",")) {
+                                if (tmp.contains("迟到") || tmp.contains("早退")) {
+                                    continue;
+                                }
+                                type = tmp;
+                                day_1 = tmp;
+                                day_2 = tmp;
+                            }
+                        }
+                        // 日期动态列头
+                        if (!days.contains(date)) {
+                            days.add(date);
+                        }
+                        attendanceInfo.put(date, type.length() == 0 ? "zc" : type);
+                        attendanceInfo.put("day" + index + "_1", day_1.length() == 0 ? "zc" : day_1);
+                        attendanceInfo.put("day" + index + "_2", day_2.length() == 0 ? "zc" : day_2);
+                    }
+                }
+            });
+            // 累计假期数据
+            float leave_duration = 0f;  // 法定假期调整时长: 分钟
+            float leave_all = 0f;   // 请假总时长: 天 [屏蔽3小时以内] - 新, 仅事假扣除出勤, 通过字段配置解决
+            for (Map column : ddClient_attendance.getLeaveTimeByNames(ddClient.getAccessToken(), po.getUserId(), leaveNames, start, end)) {
+                String name = ((Map) column.get("columnvo")).get("name").toString(); // 接口返回列名称
+                float value = (Float) _reduceAttendance(column, name, "columnvals");
+                // prd 法定假期[除病假、事件、调休、产假外]请假时长 [调休, 事假, 哺乳假为小时, 其余半天为最小单位]
+                if (!Arrays.asList("病假", "事假", "调休", "产假").contains(name)) {
+                    if (name.equals("哺乳假")) {
+                        leave_duration += value * 60f;
+                    } else {
+                        leave_duration += value * 60f * 8f;
+                    }
+                }
+                // prd 病假,产假,事假扣除出勤天数. 因事假按照小时请假, 3小时内记录为出勤, 3-4小时为半天, 4小时以上记录为一天, 因此钉钉后台未设置自动扣减
+                if (Arrays.asList("病假", "产假", "事假").contains(name)) {
+                    if (name.equals("事假")) {
+                        // 系统已自动过滤, 午休时间 [跨天场景]
+                        if (value > 8f) {
+                            leave_all += Math.floor(value / 8f);
+                        }
+                        float hours = value % 8;
+                        if (hours > 0f) {
+                            // prd 1. 3小时以下不扣除;  2. 大于等于3,小于6为半天;  3. 大于等于6为1天
+                            if (hours >= 6.0f) {
+                                leave_all += 1.0f;
+                            } else if (hours >= 3f) {
+                                leave_all += 0.5f;
+                            }
+                        }
+                    } else {
+                        leave_all += value;
+                    }
+                }
+
+                attendanceInfo.put(name, value);
+            }
+            // 数据处理, 请假折算天
+            float overTime = UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班") + UtilMap.getFloat(attendanceInfo, "节假日加班");
+            attendanceInfo.put("加班总时长", UtilNumber.formatPrecisionValue(overTime));
+            attendanceInfo.put("事假天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "事假") / 8f));
+            attendanceInfo.put("哺乳假天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "哺乳假") / 8f));
+            attendanceInfo.put("调休天", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "调休") / 8f));
+            // prd 标记人离职时间, 提示异常考勤
+            float exception_duration = 0f;
+            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("上班迟到", "").replace("上班严重迟到", "")));
+                    attendanceInfo.put("迟到次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "迟到次数") - 1));
+                }
+                attendanceInfo.put("考勤结果", "入职日期" + UtilDateTime.formatDate(po.getHiredDate()) + "; " + attendanceInfo.get("考勤结果"));
+            }
+            attendanceInfo.put("缺卡次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "上班缺卡次数") + UtilMap.getFloat(attendanceInfo, "下班缺卡次数")));
+            attendanceInfo.put("出勤天数_sys", attendanceInfo.get("出勤天数")); // prd 离职1号计算, 请假扣除, 部分员工旷工算出勤, 扣除休息打卡出勤
+            if (ObjectUtils.isNotEmpty(po.getLeaveDate()) && UtilDateTime.parseDateTime(start).before(po.getLeaveDate()) && UtilDateTime.parseDateTime(end).after(po.getLeaveDate())) {
+                // prd 离职员工出勤天数是否可以只记录员工离职当月1号
+                Optional option = attendanceList.stream().filter(item -> {
+                    /// 线程安全, 对象获取值
+                    String id = (((Map) item.get("column_vo"))).get("id").toString();
+                    return fileId_attendance_days.get().equals(id);
+                }).findAny();
+                if (option.isPresent()) {
+                    List<Map> dataList = (List<Map>) ((Map) option.get()).get("column_vals");
+                    for (Map data : dataList) {
+                        if (UtilDateTime.parseDate(data.get("date").toString()).getMonth() != UtilDateTime.parseDate(end).getMonth()) {
+                            log.info("离职从1号计算出勤, {}, {}, {}, {}", po.getName(), data.get("date"), UtilMap.getFloat(attendanceInfo, "出勤天数_sys"), UtilMap.getFloat(data, "value"));
+                            attendanceInfo.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数_sys") - UtilMap.getFloat(data, "value")));
+                        }
+                    }
+                }
+                // 缺卡补录
+                Optional optional = Arrays.stream(attendanceInfo.get("考勤结果").toString().split("; ")).filter(item -> item.equals("下班缺卡" + UtilDateTime.formatDate(po.getLeaveDate()))).findAny();
+                if (optional.isPresent()) {
+                    exception_duration = 480f;
+                    attendanceInfo.put("缺卡次数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "缺卡次数") - 1));
+                }
+                attendanceInfo.put("考勤结果", "离职日期" + UtilDateTime.formatDate(po.getLeaveDate()) + "; " + attendanceInfo.get("考勤结果"));
+            }
+            attendanceInfo.put("缺卡调整时长", UtilNumber.formatPrecisionValue(exception_duration));
+            // prd 总时长 = 工作时长 + 法定假期[除病假、事件、调休、产假外]请假时长 + 调休时长 - 加班时长【出差、外出不考勤但需要计入总工时,以申请时长为准,但外出可能为不足一天情况, 当天还有打卡: 目前先取系统默认】
+            float system_duration = UtilMap.getFloat(attendanceInfo, "工作时长");
+            float tiaoxiu_duration = UtilMap.getFloat(attendanceInfo, "调休") * 60f;
+            attendanceInfo.put("调休时长", UtilNumber.formatPrecisionValue(tiaoxiu_duration));
+            attendanceInfo.put("法定假调整时长", UtilNumber.formatPrecisionValue(leave_duration));
+            // prd [新] 汇总表: 不取系统调休。总时长计算取 0,返回列表也为 0
+            attendanceInfo.put("总时长", UtilNumber.formatPrecisionValue(system_duration + leave_duration + exception_duration - overTime));
+            // prd 请假扣除出勤天数  ppExt 钉钉接口休息如出差半天系统也返回出勤天数1, 存在异常; 休息日加班也会记录为出勤, 考勤字段调整无效
+            attendanceInfo.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数_sys") - leave_all));
+            // prd 公假打卡的天数无需记录到出勤天数中, 包含出差部分
+            Optional optional = attendanceList.stream().filter(item -> {
+                /// 线程安全, 对象获取值
+                String id = (((Map) item.get("column_vo"))).get("id").toString();
+                return fileId_attendance_result.get().equals(id);
+            }).findAny();
+            if (optional.isPresent()) {
+                List<Map> dataList = (List<Map>) ((Map) optional.get()).get("column_vals");
+                int days_overTime = dataList.stream().filter(item -> String.valueOf(item.get("value")).contains("休息并打卡") || String.valueOf(item.get("value")).contains("休息,出差")).collect(Collectors.toList()).size();
+                attendanceInfo.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendanceInfo, "出勤天数_sys") - days_overTime));
+            }
+            attendanceInfos.add(attendanceInfo);
+        });
+        // prd 26-25周期非自然月逻辑 [获取出现最多次作为法定应出勤天数] 考勤应出勤天数和班组 + 人员挂钩, ppExt 排班天数钉钉查询没有接口
+        float workMin = (Float) UtilList.maxFrequencyObject(attendanceInfos.stream().map(item -> UtilMap.getFloat(item, "出勤天数")).collect(Collectors.toList())) * 60 * 8;
+
+        // prd 数据处理 [ppExt 月度汇总统计真实数据, 月度明细按照zc规则统计]
+        int order = 0;
+        for (Map attendance : attendanceInfos) {
+            if (attendance.containsKey("总时长") && workMin > 0) {
+                attendance.put("勤勉度系数", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendance, "总时长") / workMin));
+            }
+            order++;
+            attendance.put("序号", String.valueOf(order));
+            // 调休按照半天\一天进行取整, 补充尾差
+            attendance.put("出勤天数_sys", UtilNumber.roundHalf(UtilMap.getFloat(attendance, "出勤天数_sys")));
+            // prd 月度汇总表和月度明细表是否可实现部分无需打卡的员工
+            if ("无需打卡".equals(attendance.get("考勤状态"))) {
+                if (!Objects.isNull(days)) {
+                    attendance.put("旷工天数", 0);
+                    attendance.put("缺卡次数", 0);
+                    attendance.put("上班缺卡次数", 0);
+                    attendance.put("上班缺卡次数", 0);
+                    attendance.put("下班缺卡次数", 0);
+                    attendance.put("迟到次数", 0);
+                    attendance.put("早退次数", 0);
+                    for (Object key : attendance.keySet()) {
+                        String prop = String.valueOf(key);
+                        if (prop.contains("_") || prop.contains("-")) {
+                            String val = String.valueOf(attendance.get(prop)).replace("旷工", "").replace("缺卡", "").replace("迟到", "").replace("早退", "").trim();
+                            // 忽略考勤异常 | 考勤静默用户
+                            if (StringUtils.isBlank(val) || val.equals("/")) {
+                                attendance.put(prop, "zc");
+                            } else {
+                                attendance.put(prop, val);
+                            }
+                        }
+                    }
+                } else {
+                    List<String> vals = new ArrayList<>();
+                    for (String cont : String.valueOf(attendance.get("考勤结果")).split("; ")) {
+                        // 缺卡情况下, 存在请假, 需要保留
+                        if (cont.contains("缺卡,") || (!cont.contains("旷工") && !cont.contains("缺卡") && !cont.contains("迟到") && !cont.contains("早退"))) {
+                            vals.add(cont);
+                        }
+                    }
+                    attendance.put("考勤结果", String.join("; ", vals));
+                    // prd 部分无需打卡的员工旷工、缺卡、迟到、早退的天数需要记录到出勤天数中
+                    attendance.put("出勤天数_sys", UtilNumber.formatPrecisionValue(UtilMap.getFloat(attendance, "出勤天数_sys") + UtilMap.getFloat(attendance, "旷工天数")));
+                }
+            }
+            if (!Objects.isNull(days)) {
+                // prd 异常与假期统计对应状态数据, 出勤天数就是实际到公司工作的天数[zc状态], 兼容3小时需求逻辑
+                AtomicReference<Float> days_rest = new AtomicReference<>(0f);
+                AtomicReference<Float> days_tiaoxiu = new AtomicReference<>(0f);
+                AtomicReference<Float> days_shijia = new AtomicReference<>(0f);
+                AtomicReference<Float> days_burujia = new AtomicReference<>(0f);
+                AtomicReference<Float> days_chidao = new AtomicReference<>(0f);
+                AtomicReference<Float> days_zaotui = new AtomicReference<>(0f);
+                AtomicReference<Float> days_kuangong = new AtomicReference<>(0f);
+                AtomicReference<Float> days_addition = new AtomicReference<>(0f);
+                attendance.put("出勤天数_prd", attendance.keySet().stream().reduce(0f, (acc, cur) -> {
+                    if (cur.toString().contains("_")) {
+                        // prd 2.29 新增 = 出差 + 外出 + zc [Excel添加公式导出不直接显示, 需要触发回车才会更新]
+                        if (Arrays.asList("zc", "外出", "出差").contains(attendance.get(cur))) {
+                            days_addition.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        }
+                        // 累计汇总天数 [出勤天数包含 外出, 缺卡, zc]; ppExt 未加入考勤组人员, 会全月统计为zc, 钉钉返回为空, 没有判断条件. 因此需要手动调整 [极端情况]
+                        if (Arrays.asList("zc", "缺卡", "外出").contains(attendance.get(cur))) {
+                            return Float.valueOf(String.valueOf(acc)) + 0.5;
+                        } else if (attendance.get(cur).equals("公假")) {
+                            days_rest.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        } else if (attendance.get(cur).equals("调休")) {
+                            days_tiaoxiu.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        } else if (attendance.get(cur).equals("事假")) {
+                            days_shijia.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        } else if (attendance.get(cur).equals("哺乳假")) {
+                            days_burujia.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        } else if (attendance.get(cur).equals("迟到")) {
+                            days_chidao.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        } else if (attendance.get(cur).equals("早退")) {
+                            days_zaotui.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        } else if (attendance.get(cur).equals("旷工")) {
+                            days_kuangong.updateAndGet(v -> new Float((float) (v + 0.5)));
+                        }
+                    }
+                    return acc;
+                }));
+                attendance.put("公假(新增)_prd", days_addition.get());
+                attendance.put("公假天数_prd", days_rest.get());
+                attendance.put("调休天数_prd", days_tiaoxiu.get());
+                attendance.put("事假天数_prd", days_shijia.get());
+                attendance.put("哺乳假天数_prd", days_burujia.get());
+                attendance.put("迟到次数_prd", days_chidao.get());
+                attendance.put("早退次数_prd", days_zaotui.get());
+                attendance.put("旷工天数_prd", days_kuangong.get());
+            }
+        }
+        // 记录月度明细日期, 进行排序 [接口返回已排序]
+//        if (UtilList.isNotEmpty(days)) {
+//            Collections.sort(days, Comparator.comparingLong(o -> Long.valueOf(o.replace("-", ""))));
+//        }
+        return UtilList.ignoreListMapZero(attendanceInfos);
+    }
+}

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

@@ -0,0 +1,60 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/fengkaili
+
+# 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: 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
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+# dingtalk
+dingtalk:
+  agentId: 2664525556
+  appKey: dingmcbz0lceeusy2kk4
+  appSecret: nhNVAbjjtb1Y_Q3WM1SkV4Wk3qDxTjfKEVUZd2iMrF5DtlGVcVDi5aIK-8CundeZ
+  corpId: dingade22a8c4fd34b8535c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "0504284411785810"   # 徐欢, OA管理员账号 [0开头需要转一下字符串]
+
+

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

@@ -0,0 +1,35 @@
+# 环境配置
+server:
+  port: 9012
+  servlet:
+    context-path: /api/fengkaili
+
+# condition
+spel:
+  scheduling: true        # 定时任务是否执行
+  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: cp-root@2022++
+    url: jdbc:mysql://47.97.181.40:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+    hibernate:
+      ddl-auto: none
+
+# dingtalk
+dingtalk:
+  agentId: 2664525556
+  appKey: dingmcbz0lceeusy2kk4
+  appSecret: nhNVAbjjtb1Y_Q3WM1SkV4Wk3qDxTjfKEVUZd2iMrF5DtlGVcVDi5aIK-8CundeZ
+  corpId: dingade22a8c4fd34b8535c2f4657eb6378f
+  aesKey:
+  token:
+  operator: "0504284411785810"   # 徐欢, OA管理员账号 [0开头需要转一下字符串]

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


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


+ 39 - 0
mjava-fengkaili/src/test/resource/server.sh

@@ -0,0 +1,39 @@
+#!/bin/bash
+
+appname='mjava-fengkaili'
+
+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"
+    tail -f log/info.log
+  else
+    if [ "$1" == "test" ]; then
+      nohup java -Xms256m -Xmx256m -jar $appname.jar --spring.profiles.active=test &
+      echo "server test is starting"
+      tail -f log/info.log
+    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

+ 54 - 0
mjava-gewu/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-gewu</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-gewu/src/main/java/com/malk/gewu/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.gewu;
+
+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. 标识对表没有任何操作. 若不设置为 none, 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);
+    }
+}

+ 113 - 0
mjava-gewu/src/main/java/com/malk/gewu/controller/GWController.java

@@ -0,0 +1,113 @@
+package com.malk.gewu.controller;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+
+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.common.McR;
+import com.malk.service.aliwork.YDClient;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilServlet;
+import lombok.Synchronized;
+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.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping
+public class GWController {
+    
+    @Autowired
+    private GWService gwService;
+
+    /**
+     * 同步花名册信息
+     */
+    @Synchronized
+    @PostMapping("sync")
+    McR syncRoster() {
+
+        gwService.syncRoster();
+        return McR.success();
+    }
+
+    @Autowired
+    private YDClient ydClient;
+
+    /**
+     * 拆分培训记录表
+     */
+    @Synchronized
+    @PostMapping("copy")
+    McR copyTrain(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("拆分培训记录表, {}", data);
+        Map formData = (Map) JSON.parse(String.valueOf(data.get("formData")));
+        List<String> userIds = (List<String>) JSON.parse(String.valueOf(formData.get("employeeField_limyct1y")));
+        for (String userId : userIds) {
+            Map form = UtilMap.putAll(UtilMap.empty(), formData);
+            form.put("employeeField_limyct1y", Arrays.asList(userId));
+            ydClient.operateData(YDParam.builder()
+                    .formUuid("FORM-AC666081OYZE6LMAELILADKXFZO8231E9GLNL5")
+                    .formDataJson(JSON.toJSONString(form))
+                    .build(), YDConf.FORM_OPERATION.create);
+        }
+
+        return McR.success();
+    }
+
+    /**
+     * 创建提醒日程
+     */
+    @PostMapping("schedule")
+    McR schedule(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("创建提醒日程, {}", data);
+
+        List<String> userIds = (List<String>) JSON.parse(UtilMap.getString(data, "userIds"));
+        Date sTime = UtilDateTime.parseDateTime(UtilDateTime.formatDate(new Date(UtilMap.getLong(data, "sTime"))) + " 09:00:000");
+        Date eTime = UtilDateTime.parseDateTime(UtilDateTime.formatDate(new Date(UtilMap.getLong(data, "eTime"))) + " 18:00:000");
+        String organizer = UtilMap.getString(data, "organizer");
+
+        gwService.createSchedule(UtilMap.getString(data, "summary"), UtilMap.getString(data, "description"), userIds, sTime, eTime, organizer);
+        return McR.success();
+    }
+
+
+    /**
+     * test
+     */
+    @PostMapping("test")
+    McR test() {
+
+//        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.readJsonObjectFromResource("templates/personnel"));
+//        return McR.success(UtilFile.readJsonObjectFromResource("static/json/personnel.json"));
+
+        gwService.test();
+        return McR.success();
+    }
+
+
+}

+ 35 - 0
mjava-gewu/src/main/java/com/malk/gewu/schedule/GWScheduleTask.java

@@ -0,0 +1,35 @@
+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;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * @EnableScheduling 开启定时任务 [配置参考McScheduleTask]
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = {"spel.scheduling"})
+public class GWScheduleTask {
+
+    @Autowired
+    private GWService gwService;
+
+    /**
+     * 每天凌晨4点同步
+     */
+    @Scheduled(cron = "0 0 4 * * ? ")
+    public void syncDingTalkFailedList() {
+        try {
+            gwService.syncRoster();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

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

@@ -0,0 +1,20 @@
+package com.malk.gewu.service;
+
+import java.util.Date;
+import java.util.List;
+
+public interface GWService {
+
+    /**
+     * 同步花名册信息
+     */
+    void syncRoster();
+
+    /**
+     * 创建钉钉日程
+     */
+    void createSchedule(String summary, String description, List<String> usrIds, Date sTime, Date eTIme, String organizer);
+
+    // test
+    void test();
+}

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

@@ -0,0 +1,131 @@
+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.service.dingtalk.DDClient_Schedule;
+import com.malk.utils.UtilDateTime;
+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;
+
+    /**
+     * 同步花名册信息 ppExt:: 格屋临时添加, 后续上线限流重试功能
+     */
+    @Override
+    public void syncRoster() {
+
+        // 花名册元数据
+        List<Map> metaList = (List<Map>) UtilFile.readJsonObjectFromResource("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());
+            if (userIds.size() == 0) {
+                return;
+            }
+            // 员工花名册信息
+            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);
+                }
+            });
+        });
+    }
+
+    @Autowired
+    private DDClient_Schedule ddClient_schedule;
+
+    /**
+     * 创建钉钉日程
+     */
+    @Override
+    public void createSchedule(String summary, String description, List<String> usrIds, Date sTime, Date eTIme, String organizer) {
+        String startTime = UtilDateTime.format(sTime, UtilDateTime.DATE_TIME_ISO).replace("+0800", "+08:00");
+        String endTime = UtilDateTime.format(eTIme, UtilDateTime.DATE_TIME_ISO).replace("+0800", "+08:00");
+        // ppExt: start 与 end 不能使用同一个map对象, 会报错date不能为空.
+        Map start = UtilMap.map("dateTime, timeZone", startTime, "Asia/Shanghai");
+        Map end = UtilMap.map("dateTime, timeZone", endTime, "Asia/Shanghai");
+        Map body = UtilMap.map("summary, description, start, end, userIds", summary, description, start, end, usrIds);
+        ddClient_schedule.eventsSchedule(ddClient.getAccessToken(), organizer, body);
+    }
+
+    /// test
+    @Override
+    public void test() {
+
+        ddClient_personnel.getPersonnelMeta(ddClient.getAccessToken(), ddConf.getAgentId());
+    }
+}

+ 81 - 0
mjava-gewu/src/main/resources/application-dev.yml

@@ -0,0 +1,81 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/gewu
+
+# 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
+    # 主库
+    primary:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+    # 从库
+    slave:
+      username: root
+      password: mu123
+      jdbc-url: jdbc:mysql://127.0.0.1:3306/mjava_slave?serverTimezone=Asia/Shanghai&useUnicode=yes&characterEncoding=UTF-8&useSSL=true
+  jpa:
+    hibernate:
+      ddl-auto: none      # JPA对表没有任何操作
+    show-sql: true
+    database: MYSQL
+    database-platform: org.hibernate.dialect.MySQL57Dialect
+
+# filepath
+file:
+  path:
+    file: /Users/malk/server/_Tool/var/mjava/tmp/file/
+    image: /Users/malk/server/_Tool/var/mjava/tmp/image/
+    tmp: /Users/malk/server/_Tool/var/mjava/tmp/
+  source:
+    fonts: /Users/malk/server/_Tool/fonts/simsun.ttc
+logging:
+  file:
+    path: /Users/malk/server/_Tool/var/mjava/log
+
+## dingtalk - dev
+#dingtalk:
+#  agentId: 2691784047
+#  appKey: dinghbynhnd2dbgypmsa
+#  appSecret: Kl5Xw8x0TlEIlvcJuUkYZD18UTTShJmfdKrAIpY8oX-Q_tazyUKA28nQh7dG5-mq
+#  corpId: ding321c72787fffc78b35c2f4657eb6378f
+#  aesKey:
+#  token:
+#  operator: ""   # OA管理员账号
+#
+## aliwork - dev
+#aliwork:
+#  appType: APP_FX4PR3OWW4WCFOHI2ZSR
+#  systemToken: 2G766HA1RDHC1C2CCRY62544NL8L21T5786KL27
+
+
+# dingtalk - prod
+dingtalk:
+  agentId: 2660236361
+  appKey: dinguuieqv4lkvp3vkaf
+  appSecret: N5JjPU9RDk77pTze5vRWmiWLDjPKeYJV3sQrmYgN_SC57nOALmj570rVB0SGGcQQ
+  corpId: dingec9ee223c2b3a671
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork - prod
+aliwork:
+  appType: APP_FX4PR3OWW4WCFOHI2ZSR
+  systemToken: 2G766HA1RDHC1C2CCRY62544NL8L21T5786KL27
+
+

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

@@ -0,0 +1,38 @@
+# 环境配置
+server:
+  port: 9015
+  servlet:
+    context-path: /api/gewu
+
+# condition
+spel:
+  scheduling: true         # 定时任务是否执行
+  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: 2660236361
+  appKey: dinguuieqv4lkvp3vkaf
+  appSecret: N5JjPU9RDk77pTze5vRWmiWLDjPKeYJV3sQrmYgN_SC57nOALmj570rVB0SGGcQQ
+  corpId: dingec9ee223c2b3a671
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: APP_FX4PR3OWW4WCFOHI2ZSR
+  systemToken: 2G766HA1RDHC1C2CCRY62544NL8L21T5786KL27

+ 0 - 0
mjava-gewu/src/main/resources/static/json/personnel


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä