pruple_boy vor 1 Jahr
Commit
b9fcf7e928
100 geänderte Dateien mit 10472 neuen und 0 gelöschten Zeilen
  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. 69 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. 60 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. 21 0
      mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBController.java
  24. 281 0
      mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBxYDController.java
  25. 48 0
      mjava-aiwei/src/main/java/com/malk/aiwei/delegate/TBDelegate.java
  26. 87 0
      mjava-aiwei/src/main/java/com/malk/aiwei/schedule/AWScheduleTask.java
  27. 60 0
      mjava-aiwei/src/main/java/com/malk/aiwei/server/AWServer.java
  28. 97 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/AWClint.java
  29. 9 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/AwDingService.java
  30. 1071 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWImplClient.java
  31. 79 0
      mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AwDingServiceImpl.java
  32. 86 0
      mjava-aiwei/src/main/resources/application-dev.yml
  33. 46 0
      mjava-aiwei/src/main/resources/application-prod.yml
  34. 38 0
      mjava-aiwei/src/main/resources/application-test.yml
  35. 35 0
      mjava-aiwei/src/test/resources/server.sh
  36. 54 0
      mjava-cloudpure/pom.xml
  37. 32 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/Boot.java
  38. 21 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/DDController.java
  39. 112 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/GTZZController.java
  40. 135 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/controller/XBBController.java
  41. 55 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/delegate/DDDelegate.java
  42. 86 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/schedule/CPScheduleTask.java
  43. 42 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/service/CPClient.java
  44. 361 0
      mjava-cloudpure/src/main/java/com/malk/cloudpure/service/impl/CPImplClient.java
  45. 76 0
      mjava-cloudpure/src/main/resources/application-dev.yml
  46. 51 0
      mjava-cloudpure/src/main/resources/application-prod.yml
  47. 35 0
      mjava-cloudpure/src/test/resources/server.sh
  48. 54 0
      mjava-dongfangxinhua/pom.xml
  49. 32 0
      mjava-dongfangxinhua/src/main/java/com/malk/dongfangxinhua/Boot.java
  50. 161 0
      mjava-dongfangxinhua/src/main/java/com/malk/dongfangxinhua/controller/DFXHController.java
  51. 65 0
      mjava-dongfangxinhua/src/main/resources/application-dev.yml
  52. 38 0
      mjava-dongfangxinhua/src/main/resources/application-prod.yml
  53. 39 0
      mjava-dongfangxinhua/src/test/resource/server.sh
  54. 54 0
      mjava-fengkaili/pom.xml
  55. 32 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/Boot.java
  56. 152 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/controller/FKLController.java
  57. 23 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/dao/FKLDdContactDao.java
  58. 72 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/repository/entity/FKLDdContactPo.java
  59. 35 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/schedule/FKLScheduleTask.java
  60. 28 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/FKLService.java
  61. 634 0
      mjava-fengkaili/src/main/java/com/malk/fengkaili/service/impl/FKLImplService.java
  62. 60 0
      mjava-fengkaili/src/main/resources/application-dev.yml
  63. 35 0
      mjava-fengkaili/src/main/resources/application-prod.yml
  64. BIN
      mjava-fengkaili/src/main/resources/templates/Template_days.xlsx
  65. BIN
      mjava-fengkaili/src/main/resources/templates/Template_month.xlsx
  66. 39 0
      mjava-fengkaili/src/test/resource/server.sh
  67. 54 0
      mjava-gewu/pom.xml
  68. 32 0
      mjava-gewu/src/main/java/com/malk/gewu/Boot.java
  69. 113 0
      mjava-gewu/src/main/java/com/malk/gewu/controller/GWController.java
  70. 35 0
      mjava-gewu/src/main/java/com/malk/gewu/schedule/GWScheduleTask.java
  71. 20 0
      mjava-gewu/src/main/java/com/malk/gewu/service/GWService.java
  72. 131 0
      mjava-gewu/src/main/java/com/malk/gewu/service/impl/GWImplService.java
  73. 65 0
      mjava-gewu/src/main/resources/application-dev.yml
  74. 38 0
      mjava-gewu/src/main/resources/application-prod.yml
  75. 855 0
      mjava-gewu/src/main/resources/static/json/personnel
  76. 840 0
      mjava-gewu/src/main/resources/static/json/personnel.json
  77. 39 0
      mjava-gewu/src/test/resource/server.sh
  78. 82 0
      mjava-guyuan/pom.xml
  79. 32 0
      mjava-guyuan/src/main/java/com/malk/guyuan/Boot.java
  80. 148 0
      mjava-guyuan/src/main/java/com/malk/guyuan/controller/GYController.java
  81. 443 0
      mjava-guyuan/src/main/java/com/malk/guyuan/controller/IVController.java
  82. 27 0
      mjava-guyuan/src/main/java/com/malk/guyuan/filter/CatchException_YXY.java
  83. 214 0
      mjava-guyuan/src/main/java/com/malk/guyuan/server/model/McInvoiceDto.java
  84. 67 0
      mjava-guyuan/src/main/java/com/malk/guyuan/server/model/McInvoiceKind.java
  85. 36 0
      mjava-guyuan/src/main/java/com/malk/guyuan/server/tencent/TXYConf.java
  86. 39 0
      mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/TXYInvoice.java
  87. 162 0
      mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/impl/TXYImplInvoice.java
  88. 60 0
      mjava-guyuan/src/main/resources/application-dev.yml
  89. 45 0
      mjava-guyuan/src/main/resources/application-prod.yml
  90. 54 0
      mjava-hake/pom.xml
  91. 32 0
      mjava-hake/src/main/java/com/malk/hake/Boot.java
  92. 140 0
      mjava-hake/src/main/java/com/malk/hake/controller/HKController.java
  93. 71 0
      mjava-hake/src/main/java/com/malk/hake/schedule/HKScheduleTask.java
  94. 26 0
      mjava-hake/src/main/java/com/malk/hake/service/HKClient.java
  95. 288 0
      mjava-hake/src/main/java/com/malk/hake/service/impl/HKImplClient.java
  96. 51 0
      mjava-hake/src/main/resources/application-dev.yml
  97. 38 0
      mjava-hake/src/main/resources/application-prod.yml
  98. 38 0
      mjava-hake/src/main/resources/application-test.yml
  99. 341 0
      mjava-hake/src/main/resources/static/json/form.json
  100. 0 0
      mjava-hake/src/test/resource/server.sh

+ 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;
+    }
+}

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

@@ -0,0 +1,69 @@
+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.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;
+
+    @PostMapping("test")
+    McR test() {
+
+        log.info("11111");
+
+        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 {
+
+
+}

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

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

+ 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);
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 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);
+    }
+}

+ 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 {
+    
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 281 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/controller/TBxYDController.java


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

@@ -0,0 +1,48 @@
+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.doApprove(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();
+        }
+    }
+}

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

@@ -0,0 +1,60 @@
+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 = "产品版本";
+
+    // prd 23.02.29 字段从 预检项 变更为 技术检查项
+    public static final String TASK_CHECK_LINK = "技术检查项";
+    public static final String TASK_CHECK_STATUS = "技术检查项检查状态";
+
+    public static final String TASK_APPROVE_ATTACHMENT = "交付件";
+    public static final String TASK_APPROVE_LINK = "交付件审批流程";
+    public static final String TASK_APPROVE_DESC = "交付件描述";
+
+    // 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");
+    }
+}

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

@@ -0,0 +1,97 @@
+package com.malk.aiwei.service;
+
+import com.malk.server.aliwork.YDParam;
+
+import java.util.List;
+import java.util.Map;
+
+public interface AWClint {
+
+    /**
+     * 交付物审批
+     */
+    void doApprove(Map data);
+
+    /**
+     * 交付物审批回调
+     */
+    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);
+
+    /**
+     * 项目迁移: 删除依赖项
+     */
+    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);
+
+    /**
+     * 提供verifier数据读取服务
+     */
+    List<Map> syncVerifier(String projectCode);
+
+    void test();
+}

+ 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);
+
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 1071 - 0
mjava-aiwei/src/main/java/com/malk/aiwei/service/impl/AWImplClient.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);
+    }
+}

+ 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: ""   # OA管理员账号
+
+# aliwork
+#aliwork:
+#  appType: "APP_H7WUJTKB448F9IBDC6C4"
+#  systemToken: "DHA66081DN6GRFNC6GTRW5NIJS082ZF0UN9PLLF"
+
+# teambition
+teambition:
+  AppID: 65956b5dd0ac095d62d0e592
+  AppSecret: gjQUoqKa1PHjTiyQFFuachfqKPyNeacA
+  TenantId: 6034c885e71842e1e5bb5218        # 管理后台 - 企业xx - 企业ID
+  OperatorId: 5e698cca21f5ad70dfba7d2b      # 公共账号, 需要有操作权限 [牧语]
+
+# dingtalk
+#dingtalk:
+#  agentId: 2848797049
+#  appKey: dingbqy1qugrihao43dl
+#  appSecret: UUaTKTWgLdduHvMSl0ipm19f_PDarHLHqnpz4vFZXjkkmFNmfWuwoPF1evjIRwvd
+#  corpId: ding5fcad818b0d9f62c35c2f4657eb6378f
+#  aesKey:
+#  token:
+#  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_R5EBUF2FPN3Y8DRF93M4"
+  systemToken: "ON566NC1VNIHPANP9TNVHB3TBIWS3E0TUZ5RLF3"
+

+ 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        # 私有部署

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

@@ -0,0 +1,38 @@
+# 环境配置
+server:
+  port: 9023
+  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: 2691784047
+  appKey: dinghbynhnd2dbgypmsa
+  appSecret: Kl5Xw8x0TlEIlvcJuUkYZD18UTTShJmfdKrAIpY8oX-Q_tazyUKA28nQh7dG5-mq
+  corpId: ding321c72787fffc78b35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_H7WUJTKB448F9IBDC6C4"
+  systemToken: "DHA66081DN6GRFNC6GTRW5NIJS082ZF0UN9PLLF"

+ 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 {
+
+}

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

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

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

@@ -0,0 +1,135 @@
+package com.malk.cloudpure.controller;
+
+import com.alibaba.fastjson.JSON;
+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.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.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 中台停用
+        //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;
+
+    @PostMapping("test")
+    McR test() {
+
+        ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), "16608972969409067");
+        return McR.success();
+    }
+}
+
+

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

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

+ 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);
+}

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

@@ -0,0 +1,361 @@
+package com.malk.cloudpure.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.cloudpure.service.CPClient;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDInterActiveCard;
+import com.malk.server.xbongbong.XBBConf;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDService;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
+import com.malk.service.dingtalk.DDClient_Extension;
+import com.malk.service.dingtalk.DDClient_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);
+        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);
+}

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

@@ -0,0 +1,634 @@
+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("休息,", ""); // 休息有外出/出差;
+                        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;
+                        } else if (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("天", "")));
+                                        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 (sstart >= 1200 && sDate) {
+                                                day_2 = type;
+                                            } else {
+                                                day_1 = type;
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                        // 日期动态列头
+                        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;
+
+    /**
+     * 同步花名册信息
+     */
+    @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());
+    }
+}

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

@@ -0,0 +1,65 @@
+# 环境配置
+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
+dingtalk:
+  agentId: 2691784047
+  appKey: dinghbynhnd2dbgypmsa
+  appSecret: Kl5Xw8x0TlEIlvcJuUkYZD18UTTShJmfdKrAIpY8oX-Q_tazyUKA28nQh7dG5-mq
+  corpId: ding321c72787fffc78b35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+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

Datei-Diff unterdrückt, da er zu groß ist
+ 855 - 0
mjava-gewu/src/main/resources/static/json/personnel


Datei-Diff unterdrückt, da er zu groß ist
+ 840 - 0
mjava-gewu/src/main/resources/static/json/personnel.json


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

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

+ 82 - 0
mjava-guyuan/pom.xml

@@ -0,0 +1,82 @@
+<?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-guyuan</artifactId>
+    <description>谷元发票功能开发, 查重验真, 混票识别</description>
+
+    <properties>
+        <maven.compiler.source>8</maven.compiler.source>
+        <maven.compiler.target>8</maven.compiler.target>
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>com.e-iceblue</id>
+            <url>https://repo.e-iceblue.cn/repository/maven-public/</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <!-- 核心模块-->
+        <dependency>
+            <groupId>com.malk</groupId>
+            <artifactId>mjava</artifactId>
+            <version>${mjava.version}</version>
+        </dependency>
+        <!-- 腾讯云 [go to https://search.maven.org/search?q=tencentcloud-sdk-java and getDefault the latest version.] -->
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java</artifactId>
+        </dependency>
+        <!-- 图片压缩 -->
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <version>0.4.8</version>
+        </dependency>
+        <!-- PDF压缩 -->
+        <dependency>
+            <groupId>com.twelvemonkeys.imageio</groupId>
+            <artifactId>imageio-tiff</artifactId>
+            <version>3.5</version>
+        </dependency>
+        <dependency>
+            <groupId>e-iceblue</groupId>
+            <artifactId>spire.pdf.free</artifactId>
+            <version>5.1.0</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-guyuan/src/main/java/com/malk/guyuan/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.guyuan;
+
+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);
+    }
+}

+ 148 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/controller/GYController.java

@@ -0,0 +1,148 @@
+package com.malk.guyuan.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.server.dingtalk.DDR_New;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliyun.ALYInvoice;
+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.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 错误抛出与拦截详见CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping("/")
+public class GYController {
+
+    @Autowired
+    private ALYInvoice invoice;
+
+    @Autowired
+    private YDClient ydClient;
+
+    /**
+     * 阿里发票验真
+     */
+    @PostMapping("/invoice/check")
+    McR invoiceCheckPDF(@RequestBody Map<String, String> param) {
+        log.info("阿里发票验真, 文件, {}", param);
+        McException.assertParamException_Null(param, "url");
+        return McR.success(invoice.invoiceCheckPDF("4e2c048bfe1d4feea7354a66c7944fd1", param.get("url")));
+    }
+
+    /**
+     * 阿里发票验真: image
+     */
+    @PostMapping("invoice/ocr")
+    McR invoiceCheckImage(@RequestBody Map<String, String> param) {
+        log.info("阿里发票验真, 图片, {}", param);
+        return McR.success(invoice.invoiceCheckOCR("4e2c048bfe1d4feea7354a66c7944fd1", param.get("url")));
+    }
+
+    /**
+     * 宜搭临时免登地址: 钉钉平台
+     */
+    @PostMapping("openUrl")
+    McR openUrl(@RequestBody Map<String, String> param) {
+        McException.assertParamException_Null(param, "url");
+        return McR.success(ydClient.convertTemporaryUrl(param.get("url")));
+    }
+
+    /**
+     * 全局查询子表单
+     */
+    @PostMapping("queryAll")
+    McR queryAll(@RequestBody YDParam ydParam) {
+        return McR.success(ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_list_all));
+    }
+
+    /**
+     * 全局查询(不匹配子表单)
+     */
+    @PostMapping("validate")
+    McR queryAll(HttpServletRequest request) {
+        Map<String, ?> param = UtilServlet.getParamMap(request);
+        log.info("全局查询(不匹配子表单), {}", param);
+        if (ObjectUtil.isNull(param.get("uniques"))) {
+            return McR.success();
+        }
+        McException.assertParamException_Null(param, "uniques", "formUuid", "compId");
+        // 容错 - 尾部分号的空格会被输入框忽略
+        String[] uniques = String.valueOf(param.get("uniques")).replace("; ", ";").split(";");
+        for (String val : uniques) {
+            // 查重校验: 单张发票唯一标识 + 审批已通过 / 审批中
+            List<Map> conditions = new ArrayList<>();
+            Map unique = new HashMap();
+            unique.put("key", param.get("compId"));
+            unique.put("value", val.split(": ")[1]);
+            unique.put("type", "TEXT");
+            unique.put("operator", "like");
+            unique.put("componentName", "TextField");
+            conditions.add(unique);
+
+            Map approve = new HashMap();
+            approve.put("key", "processApprovedResult");
+            approve.put("value", new String[]{"agree"});
+            approve.put("type", "ARRAY");
+            approve.put("operator", "in");
+            approve.put("componentName", "SelectField");
+            conditions.add(approve);
+            YDParam ydParam = YDParam.builder()
+                    .appType("APP_FKRK7Y94DPI1S9DV1605")
+                    .systemToken("FN7666A1ZD0STZZ75W4CKD1GD07X3PUW2FBRKT")
+                    .formUuid(String.valueOf(param.get("formUuid")))
+                    .searchCondition(JSON.toJSONString(conditions))
+                    .build();
+            DDR_New ddr_new = ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_list);
+            log.info("审批通过匹配结果, {}, {}", ddr_new.getTotalCount(), ddr_new.getData());
+            if (ddr_new.getTotalCount() > 0) {
+                return McR.errorAccess("发票已被使用, 请勿重复提交!");
+            }
+
+            conditions.remove(approve);
+            Map status = new HashMap();
+            status.put("key", "processInstanceStatus");
+            status.put("value", new String[]{"RUNNING"});
+            status.put("type", "ARRAY");
+            status.put("operator", "in");
+            status.put("componentName", "SelectField");
+            conditions.add(status);
+            ydParam.setSearchCondition(JSON.toJSONString(conditions));
+            ddr_new = ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_list);
+            log.info("审批通过匹配结果, {}, {}", ddr_new.getTotalCount(), ddr_new.getData());
+            if (ddr_new.getTotalCount() > 0) {
+                return McR.errorAccess("发票已在流程中, 请勿重复提交!");
+            }
+        }
+        return McR.success();
+    }
+
+    /**
+     * 服务状态返回
+     */
+    @PostMapping("validateTips")
+    McR validateTaxNo(HttpServletRequest request) {
+        Map data = UtilServlet.getParamMap(request);
+        if (!data.get("status").equals(1)) {
+            return McR.errorAccess("error");
+        }
+        return McR.success(data);
+    }
+}

+ 443 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/controller/IVController.java

@@ -0,0 +1,443 @@
+package com.malk.guyuan.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.guyuan.server.model.McInvoiceDto;
+import com.malk.guyuan.server.model.McInvoiceKind;
+import com.malk.guyuan.server.tencent.TXYConf;
+import com.malk.guyuan.service.tencent.TXYInvoice;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.common.FilePath;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDService;
+import com.malk.utils.*;
+import com.spire.pdf.PdfCompressionLevel;
+import com.spire.pdf.PdfDocument;
+import com.spire.pdf.PdfPageBase;
+import com.spire.pdf.exporting.PdfImageInfo;
+import com.spire.pdf.graphics.PdfBitmap;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import net.coobird.thumbnailator.Thumbnails;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.util.IOUtils;
+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.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 错误抛出与拦截详见CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping
+public class IVController {
+
+    @Autowired
+    private TXYInvoice txyInvoice;
+
+    @Autowired
+    private YDClient ydClient;
+
+    /// 优先获取字段, 新版本接口已支持字段返回
+    private String findValue(List<Map<String, String>> infos, String... names) {
+        for (String name : names) {
+            Optional optional = infos.stream().filter(info -> info.get("Name").equals(name)).findAny();
+            if (optional.isPresent()) {
+                return String.valueOf(((Map) optional.get()).get("Value"));
+            }
+        }
+        return "";
+    }
+
+    // 兼容历史配置, 格式 (谷元)
+    private String guyuanNameRepalce(String name) {
+        if (name.contains("谷元")) {
+            return UtilString.replaceBracketIsWhole(name);
+        } else {
+            return UtilString.replaceBracketIsSemiangle(name);
+        }
+    }
+
+    /// url压缩转base64
+    @SneakyThrows
+    private String imageUrlConvertBase64(String imageUrl) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        // scale(比例), outputQuality(质量)
+        Thumbnails.fromURLs(Arrays.asList(new URL(imageUrl))).scale(0.5f).outputQuality(0.25f).toOutputStream(out);
+        InputStream inputStream = new ByteArrayInputStream(out.toByteArray());
+        //转换为base64
+        byte[] bytes = IOUtils.toByteArray(inputStream);
+        return Base64.getEncoder().encodeToString(bytes);
+    }
+
+    @Autowired
+    private FilePath filePath;
+
+    /// PDF压缩转base64
+    @SneakyThrows
+    private String pdfUrlConvertBase64(String pdfUrl) {
+        String fileName = "tmp_" + new Date().getTime() + ".pdf";
+        // 下载文件
+        File file = UtilFile.mkdirIfNot(fileName, filePath.getPath().getTmp());
+        UtilHttp.doDownload(pdfUrl, file);
+
+        // PDF压缩
+        PdfDocument doc = new PdfDocument(); // 创建PdfDocument类的对象
+        doc.loadFromFile(file.getAbsolutePath()); // 加载PDF文档
+        doc.getFileInfo().setIncrementalUpdate(false); // 禁用增量更新
+        doc.setCompressionLevel(PdfCompressionLevel.Normal); // 将压缩级别设置为最佳
+
+        // 遍历文档页面
+        for (int i = 0; i < doc.getPages().getCount(); i++) {
+            PdfPageBase page = doc.getPages().get(i);  // 获取指定页面
+            PdfImageInfo[] images = page.getImagesInfo(); // 获取每个页面的图像信息集合
+            // 遍历集合中的所有项目
+            if (images != null && images.length > 0)
+                for (int j = 0; j < images.length; j++) {
+                    PdfImageInfo image = images[j];  // 获取指定图片
+                    PdfBitmap bp = new PdfBitmap(image.getImage());
+                    bp.setQuality(30); // 设置压缩质量
+                    page.replaceImage(j, bp); // 将原始图像替换为压缩图像
+                }
+        }
+        // 将结果文档保存至另一个PDF文档中: 覆盖
+        doc.saveToFile(file.getAbsolutePath());
+        doc.close();
+        // PDF转base64, 无需透出本地文件地址
+        String base64 = UtilFile.fileToBase64(file.getAbsolutePath());
+        // 删除临时PDF文件
+        UtilFile.deleteFile(file.getAbsolutePath());
+        return base64;
+    }
+
+    // prd 校验发票抬头, 购买方范围
+    private void validateBuyer(String BuyerName, String tips) {
+        List<String> corpNames = Arrays.asList(
+                "谷元(上海)文化科技有限责任公司",
+                "上海爱鱼文化传媒有限公司",
+                "上海渔米可禧文化传媒有限公司",
+                "上海巧豆文化传媒有限公司",
+                "钰鸿文化创意(上海)有限公司",
+                "上海攸元文化科技有限公司",
+                "上海盈田演出经纪有限公司",
+                "上海小蝠文化科技有限公司",
+                "沐田居海(厦门)文化传媒有限公司",
+                "北京元环文化科技有限责任公司",
+                "厦门攸元文化科技有限公司",
+                "厦门亨有文化科技有限公司",
+                "厦门银雀思汀文化传媒有限公司",
+                "上海渝泽信息科技有限公司",
+                "厦门神谷飞流影视传媒有限公司",
+                "厦门谷钛数字科技有限公司",
+                "上海观情科技有限公司",
+                "渔米可禧文化传媒(香港)有限公司",
+                "杭州渔米可禧文化传媒有限公司");
+        McException.assertAccessException(!corpNames.contains(BuyerName), tips + ", 购买方名称不合法!");
+    }
+
+    /**
+     * 混票识别 [新版本]
+     */
+    @PostMapping("invoice-iv2")
+    McR invoice_iv2(@RequestBody Map<String, String> data) throws TencentCloudSDKException {
+
+        McException.assertParamException_Null(data, "url");
+        String image = ydClient.convertTemporaryUrl(data.get("url"));
+        log.info("混票识别, 免登地址, {}", image);
+        // 非PDF, 且内存大于3M, 压缩后上传
+        if (UtilMap.getFloat(data, "size") > 3.0f && !UtilMap.getBoolean(data, "isPdf")) {
+            image = imageUrlConvertBase64(image);
+        }
+        if (UtilMap.getFloat(data, "size") > 6.0f && UtilMap.getBoolean(data, "isPdf")) {
+            image = pdfUrlConvertBase64(image);
+        }
+        List<Map> invoices = (List<Map>) txyInvoice.doRecognizeGeneralInvoice(image).get("MixedInvoiceItems");
+        List<McInvoiceDto> result = invoices.stream().map(item -> {
+            Map prop = UtilMap.getMap(UtilMap.getMap(item, "SingleInvoiceInfos"), UtilMap.getString(item, "SubType"));
+            // ppExt: 通用字段定义
+            McInvoiceDto invoiceDto = McInvoiceDto.builder()
+                    .name(UtilMap.getString(item, "SubTypeDescription"))
+                    .kindName(UtilMap.getString(item, "TypeDescription"))
+                    .kind(UtilMap.getInt(item, "Type"))
+                    .code(UtilMap.getString(prop, "Code"))
+                    .serial(UtilMap.getString(prop, "Number"))
+                    .date(UtilString.replaceDateZH_cn(UtilMap.getString(prop, "Date")))
+                    .checkCode(UtilMap.getString(prop, "CheckCode"))
+                    // ppExt: 多明细行时, 优先取值合计 [全电票返回了subTotal字段, 但值为空]
+                    .amount(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "SubTotal", "Total")))
+                    .tax(UtilNumber.setBigDecimal(UtilMap.getString_first(prop, "SubTax", "Tax")))
+                    .excludingTax(UtilNumber.setBigDecimal(UtilMap.getString(prop, "PretaxAmount")))
+                    .buyerName(guyuanNameRepalce(UtilMap.getString(prop, "Buyer")))
+                    // ppExt: 中央非税未返回税号官方说明: 非税发票理论是没有税号的,图片中属于信用代码
+                    .buyerTaxId(UtilMap.getString(prop, "BuyerTaxID"))
+                    .sellerName(guyuanNameRepalce(UtilMap.getString_first(prop, "Seller", "Issuer")))                            // 行程单: 填开单位
+                    .sellerTaxId(UtilMap.getString_first(prop, "SellerTaxID", "AgentCode"))                       // 行程单: 销售单位代号
+                    .passengerName(UtilMap.getString_first(prop, "Name", "UserName"))                             // 火车票, 行程单
+                    // 交通出行
+                    .seatType(UtilMap.getString(prop, "Seat"))
+                    .departureTime(UtilString.replaceDateZH_cn(UtilMap.getString(prop, "DateGetOn")) + " " + UtilMap.getString(prop, "TimeGetOn"))
+                    .departurePort(UtilMap.getString_first(prop, "StationGetOn", "Entrance", "Place"))            // 火车票: 出发车站, 过路过桥费: 入口, 出租车: 发票所在地
+                    .arrivePort(UtilMap.getString_first(prop, "StationGetOff", "Exit"))                           // 行程单, 火车票: 到达车站, 过路过桥费: 出口
+                    .trainNo(UtilMap.getString_first(prop, "TrainNumber", "LicensePlate"))                        // 火车票: 车次, 出租车: 车牌号
+                    .insuranceCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "Insurance"))))               // 行程单: 保险费
+                    .fuelCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "FuelSurcharge"))))                // 行程单: 燃油附加费
+                    .constructionCosts(UtilNumber.setBigDecimal((UtilMap.getString(prop, "AirDevelopmentFund"))))   // 行程单: 民航发展基金
+                    .build();
+            // ppExt: 机票行程单, 行程与座位信息在明细内
+            if ("机票行程单".equals(item.get("TypeDescription"))) {
+                Map flight = (Map) UtilMap.getList(prop, "FlightItems").get(0);
+                invoiceDto.setDepartureTime(UtilString.replaceDateZH_cn(UtilMap.getString(item, "DateGetOn")) + " " + UtilMap.getString(prop, "TimeGetOn"));
+                invoiceDto.setDeparturePort(UtilMap.getString(flight, "StationGetOn"));
+                invoiceDto.setArrivePort(UtilMap.getString(flight, "StationGetOff"));
+                invoiceDto.setSeatType(UtilMap.getString(flight, "Seat"));
+            }
+            if ("出租车发票".equals(item.get("TypeDescription"))) {
+                // 上下车时间
+                invoiceDto.setDepartureTime(UtilMap.getString(prop, "TimeGetOn") + " ~ " + UtilMap.getString(prop, "TimeGetOff"));
+            }
+            return invoiceDto;
+        }).collect(Collectors.toList());
+        return McR.success(McInvoiceDto.formatResponse(result));
+    }
+
+    /**
+     * 混票识别 [旧版本, 已废弃]
+     */
+    @PostMapping("invoice-iv")
+    McR invoice_iv(@RequestBody Map<String, String> data) throws TencentCloudSDKException {
+
+        McException.assertParamException_Null(data, "url");
+        String image = ydClient.convertTemporaryUrl(data.get("url"));
+        log.info("混票识别, 免登地址, {}", image);
+        // 非PDF, 且内存大于3M, 压缩后上传
+        if (UtilMap.getFloat(data, "size") > 3.0f && !UtilMap.getBoolean(data, "isPdf")) {
+            image = imageUrlConvertBase64(image);
+        }
+        if (UtilMap.getFloat(data, "size") > 6.0f && UtilMap.getBoolean(data, "isPdf")) {
+            image = pdfUrlConvertBase64(image);
+        }
+        // ppExt: 通用字段定义
+        List<Map> invoices = (List<Map>) txyInvoice.doMixedInvoiceOCR(image).get("MixedInvoiceItems");
+        List<McInvoiceDto> result = invoices.stream().map(item -> {
+            String kind = TXYConf.TYPE_INVOICE.get(item.get("Type").toString());
+            List<Map<String, String>> infos = (List<Map<String, String>>) item.get("SingleInvoiceInfos");
+
+            McInvoiceDto.assertSuccess(item, kind); // 响应断言
+            String invoiceName = findValue(infos, "发票名称");
+            if (kind.equals("全电发票")) {
+                kind = invoiceName.contains("增值税专用发票") ? "全电专用发票" : "全电普通发票";
+            }
+            if (kind.equals("增值税发票")) {
+                kind = invoiceName.contains("增值税专用发票") ? "增值税专用发票" : "增值税普通发票";
+                if (invoiceName.contains("增值税电子")) {
+                    kind = invoiceName.contains("专用发票") ? "增值税电子专用发票" : "增值税电子普通发票";
+                }
+            }
+            McInvoiceDto invoiceDto = McInvoiceDto.builder()
+                    .name(invoiceName)
+                    .kindName(kind)
+                    .kind(McInvoiceKind.getKindCode(kind))
+                    .code(findValue(infos, "发票代码", "票据代码")) // 发票, 非税发票
+                    // 储存唯一ID [发票, 火车票, 行程单]
+                    .serial(findValue(infos, "发票号码", "编号", "电子客票号码", "票据号码").replace("No", "")) // 发票, 非税发票
+                    .date(findValue(infos, "开票日期").replace("年", "-").replace("月", "-").replace("日", ""))
+                    .checkCode(findValue(infos, "校验码"))
+                    .amount(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "小写金额", "价税合计(小写)", "合计金额", "票价", "金额"))) // 发票, 全电票, 行程单, 火车票, 过路过桥费
+                    .excludingTax(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "合计金额", "金额", "票价", "小写金额"))) // [ppExt: 多明细行时, 优先取值合计] 行程单, 火车票, 定额发票
+                    .tax(UtilNumber.replaceCurrencyCHYToDecimal(findValue(infos, "合计税额"))) // 增值税发票
+                    .buyerName(guyuanNameRepalce(findValue(infos, "购买方名称", "交款人"))) // 发票, 非税发票
+                    .buyerTaxId(findValue(infos, "购买方识别号", "购买方统一社会信用代码/纳税人识别号", "交款人统一社会信用代码")) // 发票, 全电票, 非税发票
+                    .sellerName(guyuanNameRepalce(findValue(infos, "销售方名称", "填开单位"))) // 行程单
+                    .sellerTaxId(findValue(infos, "销售方识别号", "销售方统一社会信用代码/纳税人识别号", "销售单位代号")) // 发票, 全电票, 行程单
+                    .passengerName(findValue(infos, "旅客姓名", "姓名")) // 行程单, 火车票
+                    .seatType(findValue(infos, "座位等级", "席别")) // 行程单, 火车票
+                    .departurePort(findValue(infos, "始发地", "出发站", "入口")) // 行程单, 火车票, 过路过桥费
+                    .arrivePort(findValue(infos, "目的地", "到达站", "出口")) // 行程单, 火车票, 过路过桥费
+                    .trainNo(findValue(infos, "航班号", "车次", "车牌号")) // 行程单, 火车票, 出租车
+                    .insuranceCosts(UtilNumber.setBigDecimal((findValue(infos, "保险费")))) // 行程单
+                    .fuelCosts(UtilNumber.setBigDecimal((findValue(infos, "燃油附加费")))) // 行程单
+                    .constructionCosts(UtilNumber.setBigDecimal((findValue(infos, "民航发展基金")))) // 行程单
+                    .build();
+            // 价格不一致情况下, 通过合计返回
+            if (!UtilNumber.equalBigDecimal(invoiceDto.getAmount(), invoiceDto.getExcludingTax().add(invoiceDto.getTax()))) {
+                invoiceDto.setAmount(invoiceDto.getExcludingTax().add(invoiceDto.getTax()));
+            }
+            // 机票行程单
+            if (kind.equals(McInvoiceKind.JP.getDesc())) {
+                String date = findValue(infos, "日期").replace("年", "-").replace("月", "-").replace("日", " ");
+                invoiceDto.setDepartureTime(date + " " + findValue(infos, "时间"));
+            }
+            // 火车票
+            if (kind.equals(McInvoiceKind.HC.getDesc())) {
+                invoiceDto.setDepartureTime(findValue(infos, "出发时间").replace("年", "-").replace("月", "-").replace("日", " "));
+            }
+            // 出租车
+            if (kind.equals(McInvoiceKind.CZC.getDesc())) {
+                String date = findValue(infos, "日期").replace("年", "-").replace("月", "-").replace("日", " ");
+                invoiceDto.setDepartureTime(date + " " + findValue(infos, "上车"));
+            }
+            return invoiceDto;
+        }).collect(Collectors.toList());
+        return McR.success(McInvoiceDto.formatResponse(result));
+    }
+
+    /**
+     * 发票查重, 验真
+     */
+    @PostMapping("invoice-va")
+    McR invoice_va(@RequestBody Map data) {
+        McException.assertParamException_Null(data, "param");
+        List<McInvoiceDto> invoices = JSON.parseArray(JSON.toJSONString(data.get("param")), McInvoiceDto.class);
+
+        log.info("发票查重, 验真, {}", invoices);
+        invoices.forEach(UtilMc.consumerWithIndex((item, index) -> {
+
+            McInvoiceDto dto = (McInvoiceDto) item;
+            String invoiceNo = dto.getSerial(); // 唯一标识, 发票号码
+
+            String serial = "第【" + (index + 1) + "】张发票";
+            validateBuyer(dto.getBuyerName(), serial + "有疑问");
+            McException.assertAccessException(StringUtils.isBlank(invoiceNo), serial + ", 识别结果为空, 请检查!");
+            YDParam ydParam = YDParam.builder()
+                    .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
+                    .searchFieldJson(JSON.toJSONString(UtilMap.map("radioField_liihyrtb, textField_liihyrt8", "否", invoiceNo)))
+                    .build();
+            List<String> idList = (List<String>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form_id).getData();
+            if (idList.size() > 0) {
+                McException.exceptionAccess(serial + "已存在, 请勿重复提交!");
+            }
+            // prd 仅仅识别增值税普通发票
+            if (dto.getName().contains("普通发票")) {
+                String serialTips = serial + "有疑问";
+                try {
+                    // ppExt: 识别与验真后抬头对比 [全电票, 新版本识别接口, 返回名称为: 电子发票(普通发票) 不包含全电标识, 发类型为: 全电发票. 注意取值]
+                    Map rsp = txyInvoice.doVatInvoiceVerifyNew(dto.getKindName(), dto.getCode(), invoiceNo, dto.getDate(), String.valueOf(dto.getAmount()), dto.getCheckCode(), String.valueOf(dto.getExcludingTax()), serialTips);
+                    Map invoice = (Map) rsp.get("Invoice");
+                    McException.assertAccessException(!dto.getBuyerName().equals(guyuanNameRepalce(invoice.get("BuyerName").toString())), serialTips + ", 购买方名称不匹配!");
+                    McException.assertAccessException(!dto.getBuyerTaxId().equals(invoice.get("BuyerTaxCode")), serialTips + ", 购买方税号不匹配!");
+                    McException.assertAccessException(!dto.getSellerName().equals(guyuanNameRepalce(invoice.get("SellerName").toString())), serialTips + ", 销售方名称不匹配!");
+                    McException.assertAccessException(!dto.getSellerTaxId().equals(invoice.get("SellerTaxCode")), serialTips + ", 销售方税号不匹配!");
+                } catch (TencentCloudSDKException e) {
+                    log.error(e.getMessage(), e);
+                    // prd: 上传发票为假发票时,提示:该发票有疑问,请联系财务人员
+                    String message = e.getMessage();
+                    // ppExt: 已经是新版本接口, 过滤提示 [官方答复: 提示不会检测您是否使用的是新版,所有的用户都会提示, 忽略即可]
+                    if (message.contains("温馨提示")) {
+                        message = message.split("温馨提示")[0];
+                    }
+                    if (message.contains("发票不存在")) {
+                        message = "有疑问,请联系财务人员";
+                    }
+                    McException.exceptionAccess(serial + message);
+                }
+            }
+        }));
+        return McR.success();
+    }
+
+
+    @Autowired
+    private YDService ydService;
+
+    /**
+     * 发票状态更新: 服务注册
+     */
+    @PostMapping("invoice-up")
+    McR invoice_va(HttpServletRequest request) {
+        Map data = UtilServlet.getParamMap(request);
+        log.info("发票状态更新: 服务注册, {}", data);
+
+        String compId = UtilMap.getString(data, "compId");
+        String status = UtilMap.getString(data, "status");
+
+        // 读取关联表单
+        List<String> associationForm = (List<String>) JSON.parse(UtilMap.getString(data, "multiAssociation"));
+        List<String> formInstanceIds = new ArrayList<>();
+        for (String record : associationForm) {
+            // 解析关联表单
+            List<Map> associationData = (List<Map>) JSON.parse(record);
+            formInstanceIds.addAll(associationData.stream().map(form -> UtilMap.getString(form, "instanceId")).collect(Collectors.toList()));
+        }
+        // 宜搭批量更新
+        Map update = UtilMap.map(compId, status);
+        if (compId.equals("selectField_liihyrt6")) {
+            update.put("radioField_liw7rb2q", "否"); // 提交后, 更新是否退回标识为否
+        }
+
+        // prd 9.10 更新报销单, 关联到发票:: ppExt 宜搭服务注册, 提交规则系统默认字段 [详见 YDService]
+        ydService.operateData2(data, update, YDParam.builder()
+                .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
+                .formInstanceIdList(formInstanceIds)
+                .updateFormDataJson(JSON.toJSONString(update))
+                .build(), YDConf.FORM_OPERATION.multi_update);
+
+        return McR.success();
+    }
+
+    /**
+     * 发票状态更新: 退回提交
+     */
+    @PostMapping("invoice-zy")
+    McR invoice_zy(@RequestBody Map data) {
+        log.info("发票状态更新: 退回提交, {}", data);
+
+        List<String> pre_ids = (List<String>) data.get("pre_ids"); // 释放修改前
+        List<String> cur_ids = (List<String>) data.get("cur_ids"); // 占用修改后
+
+        // [前端调用添加] 退回为监听宜搭dom事件, 先执行接口调用, 才会校验宜搭必填, 过滤无效调用
+        if (cur_ids.size() == 0) {
+            return McR.success();
+        }
+
+        Map pre_update = (Map) data.get("pre_update");
+        Map cur_update = (Map) data.get("cur_update");
+
+        // 宜搭批量更新
+        ydClient.operateData(YDParam.builder()
+                .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
+                .formInstanceIdList(pre_ids)
+                .updateFormDataJson(JSON.toJSONString(pre_update))
+                .build(), YDConf.FORM_OPERATION.multi_update);
+
+        ydClient.operateData(YDParam.builder()
+                .formUuid("FORM-W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
+                .formInstanceIdList(cur_ids)
+                .updateFormDataJson(JSON.toJSONString(cur_update))
+                .build(), YDConf.FORM_OPERATION.multi_update);
+
+        return McR.success();
+    }
+
+    @PostMapping("test")
+    McR test() {
+
+
+//        List<Map> process = (List<Map>) ydClient.queryData(YDParam.builder()
+//                .formUuid("W2A66Z910O9B3LP9C6IYUDPRVWY62DO0YHIILY")
+//                .formInstId("FINST-NGA66WA1FV4EB7QJC3OATA3EV8MK35Z9COEMLFR22")
+//                .build(), YDConf.FORM_QUERY.retrieve_id).getData();
+
+        List<Map> process = (List<Map>) ydClient.queryData(YDParam.builder()
+                .formUuid("FORM-0IA66C71F6NBAETREO8DE9SSN43D3YIZ0AYILC")
+                .searchFieldJson(JSON.toJSONString(UtilMap.map("textField_lmewsobs", "Y16668919W4E4FHQ6123ADDHB8XK3S709YEMLXWF")))
+                .build(), YDConf.FORM_QUERY.retrieve_search_form).getData();
+
+        return McR.success();
+    }
+
+}

+ 27 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/filter/CatchException_YXY.java

@@ -0,0 +1,27 @@
+package com.malk.guyuan.filter;
+
+import com.malk.filter.CatchException;
+import com.malk.server.common.McR;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+/**
+ * 腾讯云 [通用拦截参考 CatchException]
+ */
+@Slf4j
+@RestControllerAdvice(annotations = RestController.class)
+public class CatchException_YXY extends CatchException {
+
+    /**
+     * 错误类抛出
+     */
+    @ExceptionHandler(TencentCloudSDKException.class)
+    public McR TencentCloudSDKException(TencentCloudSDKException e) {
+        log.error(e.getMessage(), e);  // 记录错误日志
+        // prd, 统一中文提示 return McR.errorVendor(e.getMessage(), "tencent");
+        return McR.errorVendor("发票识别异常!", "tencent");
+    }
+}

+ 214 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/server/model/McInvoiceDto.java

@@ -0,0 +1,214 @@
+package com.malk.guyuan.server.model;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.malk.base.BaseDto;
+import com.malk.server.common.McException;
+import com.malk.server.common.McREnum;
+import com.malk.utils.UtilMap;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+
+import java.math.BigDecimal;
+import java.util.Map;
+
+/**
+ * ppExt: 新版本官方返回数据已结构化, 以官方为准
+ *
+ * @apiNote https://cloud.tencent.com/document/api/866/33527#AirTransport 数据结构
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class McInvoiceDto extends BaseDto {
+
+
+    /**************** 发票 ****************/
+
+    /**
+     * 发票名称 [类型全称]
+     */
+    private String name;
+
+    // 兼容: 不为空
+    public String getName() {
+        if (StringUtils.isBlank(name)) {
+            return kindName;
+        }
+        return name;
+    }
+
+    /**
+     * 发票类型 [类型简称]
+     * -
+     * ppExt: 全电票, 新版本识别接口, 返回名称为: 电子发票(普通发票) 不包含全电标识, 发类型为: 全电发票. 注意取值
+     */
+    private String kindName;
+
+    /**
+     * 发票类型 [编码]
+     */
+    private int kind;
+
+    /**
+     * 发票代码
+     */
+    private String code;
+
+    /**
+     * 发票号码
+     */
+    private String serial;
+
+    /**
+     * 开票日期 [yyyy-MM-dd]
+     */
+    private String date;
+
+    /**
+     * 校验码
+     */
+    private String checkCode;
+
+    /**
+     * 价税合计
+     */
+    private BigDecimal amount;
+
+    /**
+     * 不含税金额
+     */
+    private BigDecimal excludingTax;
+
+    /// 没有税额时, 税额可能没传递 [因此前提是不含税为空],不含税金额等于价税合计
+    public BigDecimal getExcludingTax() {
+        if (ObjectUtil.isNull((excludingTax)) && (ObjectUtil.isNull(tax) || BigDecimal.ZERO.compareTo(tax) == 0)) {
+            return amount;
+        }
+        return excludingTax;
+    }
+
+    /**
+     * 不含税金额
+     */
+    private BigDecimal tax;
+
+    /// 取值为null, 合计会异常
+    public BigDecimal getTax() {
+        if (ObjectUtil.isNull(tax)) {
+            return BigDecimal.ZERO;
+        }
+        return tax;
+    }
+
+    /**
+     * 购买方名称
+     */
+    private String buyerName;
+
+    /**
+     * 购买方税号
+     */
+    private String buyerTaxId;
+
+    /**
+     * 销售方名称
+     */
+    private String sellerName;
+
+    /**
+     * 销售方税号
+     */
+    private String sellerTaxId;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**************** 交通 ****************/
+
+    /**
+     * 乘客姓名
+     */
+    private String passengerName;
+
+    /**
+     * 座位类型
+     */
+    private String seatType;
+
+    /**
+     * 出发地
+     */
+    private String departurePort;
+
+    /**
+     * 到达地
+     */
+    private String arrivePort;
+
+    /**
+     * 出发时间 [yyyy-MM-dd HH:mm]
+     */
+    private String departureTime;
+
+    /**
+     * 车次编号/航班号
+     */
+    private String trainNo;
+
+    /**
+     * 保险费
+     */
+    private BigDecimal insuranceCosts;
+
+    /**
+     * 燃油附加费
+     */
+    private BigDecimal fuelCosts;
+
+    /**
+     * 民航发展基金
+     */
+    private BigDecimal constructionCosts;
+
+    /**************** 格式化 ****************/
+
+    /**
+     * 实例Map, 服务宜搭组件映射 [明细组件获取表头, 通过名称转换字段]
+     *
+     * @implSpec const headers = this.$("tableField_liv5f4d2").props.children[0].props.children.map(({ props: { label, fieldId } }) => ({ label, compId: fieldId }))
+     */
+    public static Map formatDtoLabelAndProp() {
+
+        Map data = UtilMap.map("发票名称, 发票类型, 发票代码, 发票号码, 开票日期, 校验码, 价税合计, 不含税金额, 税额", "name, kindName, code, serial, date, checkCode, amount, excludingTax, tax");
+        data.putAll(UtilMap.map("购买方名称, 购买方税号, 销售方名称, 销售方税号", "buyerName, buyerTaxId, sellerName, sellerTaxId"));
+        data.putAll(UtilMap.map("乘客姓名, 座位类型, 出发地, 到达地, 出发时间, 车次编号/航班号, 保险费, 燃油附加费, 民航发展基金", "passengerName, seatType, departurePort, arrivePort, departureTime, trainNo, insuranceCosts, fuelCosts, constructionCosts"));
+        return data;
+    }
+
+    /**
+     * 格式化返回
+     */
+    public static Map formatResponse(Object result) {
+        return UtilMap.map("result, dto", result, formatDtoLabelAndProp());
+    }
+
+    /**************** 返回值 ****************/
+
+    // 成功状态标记
+    private final static String SUC_CODE = "OK";
+
+    /**
+     * 断言错误信息
+     */
+    public static void assertSuccess(Map result, String kind) {
+        String code = UtilMap.getString(result, "Code");
+        McException.assertException(!SUC_CODE.equals(code), McREnum.VENDOR_ERROR.getCode(), kind, "tencent");
+    }
+}
+

+ 67 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/server/model/McInvoiceKind.java

@@ -0,0 +1,67 @@
+package com.malk.guyuan.server.model;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+/**
+ * 定义返回值和对应状态的信息 [旧版本]
+ * ppExt: 新版本官方返回数据已结构化, 以官方为准
+ */
+@Deprecated
+@Slf4j
+public enum McInvoiceKind {
+
+    PP("增值税普通发票", 1),
+    ZP("增值税专用发票", 2),
+    DP("增值税电子普通发票", 3),
+    DZ("增值税电子专用发票", 4),
+    QP("全电普通发票", 5),
+    QZ("全电专用发票", 6),
+
+    HC("火车票", 11),
+    JP("机票行程单", 12),
+    CZC("出租车发票", 13),
+    DE("定额发票", 14),
+    GLGQ("过路过桥费发票", 15),
+
+    FS("非税发票", 21),
+    OT("其它特殊发票", 22),
+
+    UN("未知类型", -1);
+
+    @Getter
+    private String desc;
+    @Getter
+    private int code;
+
+    /**
+     * 根据code查找
+     */
+    public final static String getKindName(int code) {
+        Optional optional = Arrays.stream(McInvoiceKind.values()).filter(item -> item.code == code).findAny();
+        if (optional.isPresent()) {
+            return ((McInvoiceKind) optional.get()).desc;
+        }
+        return UN.getDesc();
+    }
+
+    /**
+     * 根据name查找
+     */
+    public final static int getKindCode(String name) {
+        Optional optional = Arrays.stream(McInvoiceKind.values()).filter(item -> item.desc.equals(name)).findAny();
+        //log.info("发票类型, {}", Arrays.stream(McInvoiceKind.values()).map(item -> item.desc).collect(Collectors.toList()));
+        if (optional.isPresent()) {
+            return ((McInvoiceKind) optional.get()).code;
+        }
+        return UN.getCode();
+    }
+
+    McInvoiceKind(String name, int code) {
+        this.desc = name;
+        this.code = code;
+    }
+}

+ 36 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/server/tencent/TXYConf.java

@@ -0,0 +1,36 @@
+package com.malk.guyuan.server.tencent;
+
+import com.malk.utils.UtilMap;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 读取配置文件参考FilePah
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "tencent")
+public class TXYConf {
+
+    private String APPID;
+
+    private String SecretId;
+
+    private String SecretKey;
+
+    private String Region;
+
+    /**
+     * 票据类型
+     * -
+     * 中央非税未返回税号官方说明: 非税发票理论是没有税号的,图片中那个属于信用代码 [两个版本接口均为返回]
+     * ppExt: 新版本官方返回数据已结构化, 以官方为准
+     */
+    @Deprecated
+    public static final Map<String, String> TYPE_INVOICE = UtilMap.map("-1, 0, 1, 2, 3, 5, 8, 9, 10, 11, 12, 13, 15, 16",
+            "未知类型, 出租车发票, 定额发票, 火车票, 增值税发票, 机票行程单, 通用机打发票, 汽车票, 轮船票, 增值税发票(卷票) , 购车发票, 过路过桥费发票, 非税发票, 全电发票");
+
+}

+ 39 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/TXYInvoice.java

@@ -0,0 +1,39 @@
+package com.malk.guyuan.service.tencent;
+
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+
+import java.util.Map;
+
+public interface TXYInvoice {
+
+    /**
+     * 混贴票据识别 [将弃用, 直接返回识别数据, ppExt: 官方已停止更新, PDF多张异常]
+     *
+     * @param image 下载图片经Base64编码后不超过 7M。图片下载时间不超过 3 秒
+     */
+    @Deprecated
+    Map doMixedInvoiceOCR(String image) throws TencentCloudSDKException;
+
+    /**
+     * 通用票据识别(高级版)[返回结构化数据] ppExt: 新版本官方返回数据已结构化, 以官方为准
+     *
+     * @param image 下载图片经Base64编码后不超过 8M。图片下载时间不超过 3 秒
+     */
+    Map doRecognizeGeneralInvoice(String image) throws TencentCloudSDKException;
+
+    /**
+     * 发票验真[新版]
+     *
+     * @param invoiceKind         ppExt: 全电票, 新版本识别接口, 返回名称为: 电子发票(普通发票) 不包含全电标识, 发类型为: 全电发票. 注意取值
+     * @param invoiceCode         票代码(10或12 位),全电发票为空
+     * @param checkCode           校验码后 6 位,增值税普通发票、增值税电子普通发票、增值税普通发票(卷式)、增值税电子普通发票(通行费)时必填;
+     * @param excludingTax/amount 不含税金额,增值税专用发票、增值税电子专用发票、机动车销售统一发票、二手车销售统一发票、区块链发票时必填; 全电发票为价税合计(含税金额)
+     */
+    Map doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException;
+
+    /**
+     * 名片识别
+     */
+    Map doBusinessCardOCR(String image) throws TencentCloudSDKException;
+
+}

+ 162 - 0
mjava-guyuan/src/main/java/com/malk/guyuan/service/tencent/impl/TXYImplInvoice.java

@@ -0,0 +1,162 @@
+package com.malk.guyuan.service.tencent.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.guyuan.server.tencent.TXYConf;
+import com.malk.guyuan.service.tencent.TXYInvoice;
+import com.malk.server.common.McException;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilNumber;
+import com.tencentcloudapi.common.Credential;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import com.tencentcloudapi.common.profile.ClientProfile;
+import com.tencentcloudapi.common.profile.HttpProfile;
+import com.tencentcloudapi.ocr.v20181119.OcrClient;
+import com.tencentcloudapi.ocr.v20181119.models.*;
+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.util.Map;
+
+@Slf4j
+@Service
+public class TXYImplInvoice implements TXYInvoice {
+
+    @Autowired
+    private TXYConf txyConf;
+
+    // 创建请求描述
+    private ClientProfile doRequest(String endPoint) {
+
+        HttpProfile httpProfile = new HttpProfile();
+        httpProfile.setEndpoint(endPoint);
+        ClientProfile clientProfile = new ClientProfile();
+        clientProfile.setHttpProfile(httpProfile);
+        return clientProfile;
+    }
+
+    /**
+     * 混贴票据识别
+     *
+     * @apiNote https://cloud.tencent.com/document/product/866/37835
+     */
+    @Override
+    public Map doMixedInvoiceOCR(String image) throws TencentCloudSDKException {
+
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        MixedInvoiceOCRRequest req = new MixedInvoiceOCRRequest();
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
+        req.setReturnMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
+        //log.debug("请求参数, {}", JSON.toJSONString(req));
+        MixedInvoiceOCRResponse resp = client.MixedInvoiceOCR(req);
+        String result = MixedInvoiceOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
+
+    /**
+     * 通用票据识别(高级版)
+     *
+     * @apiNote https://cloud.tencent.com/document/api/866/90802
+     */
+    @Override
+    public Map doRecognizeGeneralInvoice(String image) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        RecognizeGeneralInvoiceRequest req = new RecognizeGeneralInvoiceRequest();
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
+        req.setEnableMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
+        req.setEnableCutImage(false); // 返回切割图片base64
+        RecognizeGeneralInvoiceResponse resp = client.RecognizeGeneralInvoice(req);
+        String result = MixedInvoiceOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
+
+    /**
+     * 发票验真[新版]
+     *
+     * @apiNote https://cloud.tencent.com/document/product/866/73674
+     */
+    @Override
+    public Map doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String tips) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        // 一些特殊发票校验码只有5位, 兼容
+        if (StringUtils.isNotBlank(checkCode) && checkCode.length() > 6) {
+            checkCode = checkCode.substring(checkCode.length() - 6);
+        }
+
+        VatInvoiceVerifyNewRequest req = new VatInvoiceVerifyNewRequest();
+        req.setInvoiceNo(invoiceNo);
+        req.setInvoiceDate(invoiceDate);
+        req.setInvoiceCode(invoiceCode);
+        req.setCheckCode(checkCode);
+        // ppExt: 全电票, 新版本识别接口, 返回名称为: 电子发票(普通发票) 不包含全电标识, 发类型为: 全电发票. 注意取值
+        if (invoiceKind.contains("全电")) {
+            // 全电票, 需要价税合计且无发票代码与校验码
+            req.setCheckCode(null);
+            req.setInvoiceCode(null);
+            req.setAmount(amount);
+        } else {
+            req.setAmount(null);
+            req.setAmount(excludingTax);
+        }
+        log.debug("发票验真, {}, {}", invoiceKind, JSON.toJSONString(req));
+        VatInvoiceVerifyNewResponse resp = client.VatInvoiceVerifyNew(req);
+        String result = VatInvoiceVerifyNewResponse.toJsonString(resp);
+        Map rsp = (Map) JSON.parse(result);
+
+        // 因全电票取值, 取值价税合计, 单独校验下金额
+        Map invoice = (Map) rsp.get("Invoice");
+        if (StringUtils.isBlank(tips)) {
+            tips = "发票有疑问";
+        }
+        log.debug("请求响应, {}", result);
+        McException.assertAccessException(!UtilNumber.equalBigDecimal(UtilMap.getString(invoice, "AmountWithTax"), amount), tips + ", 价税合计金额不匹配!");
+        // ppExt: 增值税卷票: 票面无税率, 税额. 但接口验证返回或本质上发票是有税率, 税额. 因此取消后置判断
+        McException.assertAccessException(!invoiceKind.contains("卷票") && !UtilNumber.equalBigDecimal(UtilMap.getString(invoice, "AmountWithoutTax"), excludingTax), tips + ", 不含税金额不匹配!");
+        return rsp;
+    }
+
+    /**
+     * 名片识别
+     *
+     * @apiNote https://console.cloud.tencent.com/api/explorer?Product=ocr&Version=2018-11-19&Action=BusinessCardOCR
+     */
+    @Override
+    public Map doBusinessCardOCR(String image) throws TencentCloudSDKException {
+        Credential cred = new Credential(txyConf.getSecretId(), txyConf.getSecretKey());
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, txyConf.getRegion(), clientProfile);
+
+        BusinessCardOCRRequest req = new BusinessCardOCRRequest();
+        if (image.startsWith("http")) {
+            req.setImageUrl(image);
+        } else {
+            req.setImageBase64(image);
+        }
+        req.setConfig(JSON.toJSONString(UtilMap.map("RetImageType", "PROPROCESS")));
+        //log.debug("请求参数, {}", JSON.toJSONString(req));
+        BusinessCardOCRResponse resp = client.BusinessCardOCR(req);
+        String result = BusinessCardOCRResponse.toJsonString(resp);
+        log.debug("请求响应, {}", result);
+        return (Map) JSON.parse(result);
+    }
+}

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

@@ -0,0 +1,60 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/guyuan
+
+# 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
+  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/file
+    image: /Users/malk/server/_Tool/var/mjava/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: 2356105659
+  appKey: dinghov5fyfjlhzk1ej1
+  appSecret: QR_1_7D0UUYtfdtMlDlmnjNSkvU2Uo3gOyabo2h09rmvj5_x0uk0Lw7N9pCy7sf-
+  corpId: dinge281bf50c488ebfd35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: APP_FKRK7Y94DPI1S9DV1605
+  systemToken: FN7666A1ZD0STZZ75W4CKD1GD07X3PUW2FBRKT
+
+# tencent [腾讯云]
+tencent:
+  APPID: 1309939821
+  SecretId: AKID2uqoryukbO2XuBThuxzdEpnmnmoocuCH
+  SecretKey: wnmgYHo8wrmjlldKoHnIkDZlqvrVDpOz
+  Region: ap-shanghai

+ 45 - 0
mjava-guyuan/src/main/resources/application-prod.yml

@@ -0,0 +1,45 @@
+# 环境配置
+server:
+  port: 9004
+  servlet:
+    context-path: /api/guyuan
+
+# 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
+
+# dingtalk
+dingtalk:
+  agentId: 2356105659
+  appKey: dinghov5fyfjlhzk1ej1
+  appSecret: QR_1_7D0UUYtfdtMlDlmnjNSkvU2Uo3gOyabo2h09rmvj5_x0uk0Lw7N9pCy7sf-
+  corpId: dinge281bf50c488ebfd35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: APP_FKRK7Y94DPI1S9DV1605
+  systemToken: FN7666A1ZD0STZZ75W4CKD1GD07X3PUW2FBRKT
+
+# tencent [腾讯云]
+tencent:
+  APPID: 1309939821
+  SecretId: AKID2uqoryukbO2XuBThuxzdEpnmnmoocuCH
+  SecretKey: wnmgYHo8wrmjlldKoHnIkDZlqvrVDpOz
+  Region: ap-shanghai

+ 54 - 0
mjava-hake/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-hake</artifactId>
+    <description>宜搭对接monitor</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-hake/src/main/java/com/malk/hake/Boot.java

@@ -0,0 +1,32 @@
+package com.malk.hake;
+
+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);
+    }
+}

+ 140 - 0
mjava-hake/src/main/java/com/malk/hake/controller/HKController.java

@@ -0,0 +1,140 @@
+package com.malk.hake.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.hake.service.HKClient;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.common.McException;
+import com.malk.server.common.McR;
+import com.malk.server.common.McREnum;
+import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDService;
+import com.malk.utils.UtilDateTime;
+import com.malk.utils.UtilMap;
+import com.malk.utils.UtilServlet;
+import lombok.SneakyThrows;
+import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 错误抛出与拦截详见 CatchException
+ */
+@Slf4j
+@RestController
+@RequestMapping
+public class HKController {
+
+    @Autowired
+    private HKClient hkClient;
+
+    /**
+     * 通用流程发起
+     */
+    @SneakyThrows
+    @PostMapping("process/start")
+    McR startProcess(@RequestBody Map data, HttpServletRequest request, @RequestParam String code) {
+
+        Map header = UtilServlet.getHeaders(request);
+        log.info("流程发起, {}, {}, {}", code, data, header);
+        McException.assertException(!"dinge61fe69900ea236b35c2f4657eb6378f".equals(header.get("authorization")), McREnum.NOT_AUTHORIZED);
+        Map result = hkClient.startProcess(code, data);
+        return McR.success(result);
+    }
+
+    /**
+     * 通用审批校验 [区分类型, 宜搭提示不同报错信息]
+     */
+    @PostMapping("process/validate")
+    McR validateProcess(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("审批校验, {}", JSON.toJSONString(data));
+        // 拒绝审批意见必填校验 prd 4.17 取消拒绝校验, 统一返回 [审核人xxx拒绝], monitor通过活动跳转
+        //McException.assertParamException_Null(data, "Remark");
+        return McR.success();
+    }
+
+    /**
+     * 通用审批回调 [区分类型, 宜搭提示不同报错信息]
+     */
+    @PostMapping("process/callback")
+    McR callbackProcess(HttpServletRequest request) {
+
+        Map data = UtilServlet.getParamMap(request);
+        log.info("审批回调, {}", JSON.toJSONString(data));
+        hkClient.callbackProcess(data);
+        return McR.success();
+    }
+
+    /**
+     * 通讯录同步
+     */
+    @Synchronized
+    @PostMapping("contact/sync")
+    McR syncContact() {
+
+        log.info("手动触发, 通讯录同步");
+        hkClient.syncContact();
+        return McR.success();
+    }
+
+    @PostMapping("monitor/sync")
+    McR pay(String code) {
+
+        log.info("手动触发, 拉取Monitor同步");
+        hkClient.syncMonitor(code);
+        return McR.success();
+    }
+
+
+    @Autowired
+    private YDClient ydClient;
+
+    @Autowired
+    private YDService ydService;
+
+    @GetMapping("test")
+    McR test() {
+
+//        ydClient.operateData(YDParam.builder()
+//                .formInstanceId("125e59bb-5f1f-4307-b809-79d0add5e28e")
+//                .updateFormDataJson(JSON.toJSONString(UtilMap.map("textareaField_lntocd8u", "管理员拒绝")))
+//                .build(), YDConf.FORM_OPERATION.update);
+
+
+        /// 衣念
+        List<Map> dataList = (List<Map>) ydService.queryFormData_all(YDParam.builder()
+                .appType("APP_ZTKQXN2ISL7TWFDR6PSK")
+                .systemToken("GM866LB1OJTBPZB471GENBEO35YB270I085JLN1")
+                .formUuid("FORM-PR8667D1F4NBYLKI9ZKT9AEDZ6SP2CU4OA5JLQ")
+                .searchFieldJson(JSON.toJSONString(UtilMap.map("selectField_lk82ds02, dateField_lili3nyt", "是", Arrays.asList(UtilDateTime.parseDate("2024-04-01").getTime(), new Date().getTime()))))
+                .useLatestVersion(true)
+                .build());
+        log.info("dataList, {}", dataList.size());
+
+        dataList.forEach(item -> {
+            List<Map> details = (List<Map>) item.get("tableField_lili3nz6");
+            Map formData = UtilMap.map("tableField_lili3nz6", details.stream().map(row -> {
+                row.put("textField_lvf16s5q", String.valueOf(row.get("selectField_lk2byq2q")).replace("-1", "").replace("-2", "").replace("-3", "").replace("-4", "").replace("-5", ""));
+                return row;
+            }).collect(Collectors.toList()));
+            log.info("------, {}", item.get("formInstanceId"));
+            ydClient.operateData(YDParam.builder()
+                    .appType("APP_ZTKQXN2ISL7TWFDR6PSK")
+                    .systemToken("GM866LB1OJTBPZB471GENBEO35YB270I085JLN1")
+                    .formInstanceId(UtilMap.getString(item, "formInstanceId"))
+                    .updateFormDataJson(JSON.toJSONString(formData))
+                    .build(), YDConf.FORM_OPERATION.update);
+        });
+        return McR.success();
+    }
+}

+ 71 - 0
mjava-hake/src/main/java/com/malk/hake/schedule/HKScheduleTask.java

@@ -0,0 +1,71 @@
+package com.malk.hake.schedule;
+
+import com.malk.hake.service.HKClient;
+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 HKScheduleTask {
+
+    @Autowired
+    private HKClient hkClient;
+
+    /**
+     * 同步通讯录: 早上6:00  中午11:30    下午16:00    晚上19:30
+     */
+    @Scheduled(cron = "0 0 6,16 * * ? ")
+    public void timer_1() {
+        try {
+            hkClient.syncContact();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    @Scheduled(cron = "0 30 11,19 * * ? ")
+    public void timer_2() {
+        try {
+            hkClient.syncContact();
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 同步付款: 每次凌晨1点
+     */
+    @Scheduled(cron = "0 0 1 * * ? ")
+    public void timer_3() {
+        try {
+//            hkClient.syncMonitor("FKSQ");
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 同步预付款: 每次凌晨2点
+     */
+    @Scheduled(cron = "0 0 2 * * ? ")
+    public void timer_4() {
+        try {
+//            hkClient.syncMonitor("YFKSQ");
+        } catch (Exception e) {
+            // 记录错误信息
+            e.printStackTrace();
+        }
+    }
+}

+ 26 - 0
mjava-hake/src/main/java/com/malk/hake/service/HKClient.java

@@ -0,0 +1,26 @@
+package com.malk.hake.service;
+
+import java.util.Map;
+
+public interface HKClient {
+
+    /**
+     * 通用流程发起
+     */
+    Map<String, Object> startProcess(String code, Map data);
+
+    /**
+     * 通用审批回调
+     */
+    void callbackProcess(Map data);
+
+    /**
+     * 通讯录同步
+     */
+    void syncContact();
+
+    /**
+     * 付款数据同步
+     */
+    void syncMonitor(String code);
+}

+ 288 - 0
mjava-hake/src/main/java/com/malk/hake/service/impl/HKImplClient.java

@@ -0,0 +1,288 @@
+package com.malk.hake.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.fastjson.JSON;
+import com.malk.hake.service.HKClient;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+import com.malk.server.common.McException;
+import com.malk.server.dingtalk.DDConf;
+import com.malk.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.DDService;
+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.*;
+
+@Service
+@Slf4j
+public class HKImplClient implements HKClient {
+
+    @Autowired
+    private YDClient ydClient;
+
+    // monitor接口ip
+    String _getEnvApi(String path) {
+        if (UtilEnv.getActiveProfile().equals(UtilEnv.ENV_PROD)) {
+            return "http://172.16.20.13:10002/api/public" + path;
+        }
+        return "http://116.228.113.106:10001/api/public" + path;
+    }
+
+    private Map MATE;
+
+    private Map _getMeta() {
+        if (ObjectUtil.isNull(MATE)) {
+            MATE = (Map) UtilFile.readJsonObjectFromResource("static/json/form.json"); // 本地匹配宜搭组件ID
+        }
+        return MATE;
+    }
+
+    /**
+     * 创建/更新数据
+     */
+    private String upsertData(String code, Map data) {
+        // 组织单据数据
+        Map meta = _getMeta();
+        Map<String, ?> component = (Map) ((Map) meta.get(code)).get("compIds");
+        Map formData = UtilMap.empty();
+        for (String key : component.keySet()) {
+            if (key.startsWith("employeeField_")) {
+                // getString, 避免 [null] 数据类型
+                formData.put(key, Arrays.asList(UtilMap.getString(data, (String) component.get(key))));
+            } else if (key.startsWith("tableField_")) {
+                List<Map> rows = (List<Map>) data.get("rows");
+                List<Map> details = new ArrayList<>();
+                Map<String, String> compIds = (Map) component.get(key);
+                for (Map row : rows) {
+                    Map detail = UtilMap.empty();
+                    for (String prop : compIds.keySet()) {
+                        detail.put(prop, row.get(compIds.get(prop)));
+                    }
+                    details.add(detail);
+                }
+                formData.put(key, details);
+            } else {
+                Object value = data.get(component.get(key));
+                formData.put(key, value);
+            }
+        }
+        // 发起人匹配规则
+        String userId = _getUserId(code, meta, data);
+        YDConf.FORM_OPERATION operation = YDConf.FORM_OPERATION.start;
+        String processCode = String.valueOf(((Map) meta.get(code)).get("processCode"));
+        if (UtilString.isBlankCompatNull(processCode)) {
+            operation = YDConf.FORM_OPERATION.create;
+        }
+        String formInstanceId = UtilMap.getString(data, "instanceId");
+        YDParam ydParam = YDParam.builder()
+                .formUuid(String.valueOf(((Map) meta.get(code)).get("formUuid")))
+                .processCode(processCode)
+                .userId(userId)
+                .formDataJson(JSON.toJSONString(formData))
+                .updateFormDataJson(JSON.toJSONString(formData))
+                .useLatestVersion(true)
+                .build();
+        // 匹配更新机制: Upsert操作
+        if (StringUtils.isNotBlank(formInstanceId)) {
+            ydParam.setFormInstanceId(formInstanceId);
+            operation = YDConf.FORM_OPERATION.update;
+        }
+        Object result = ydClient.operateData(ydParam, operation);
+        return StringUtils.isNotBlank(formInstanceId) ? formInstanceId : (String) result;
+    }
+
+    /// 发起人处理
+    String _getUserId(String code, Map meta, Map data) {
+        String userId = String.valueOf(data.get(String.valueOf(((Map) meta.get(code)).get("creator"))));
+        // 报价单
+        if ("BJ".equals(code)) {
+            if (Arrays.asList("样品", "大货").contains(data.get("quoteType"))) {
+                userId = String.valueOf(data.get("dtOurReferencePersonId"));
+            } else {
+                userId = String.valueOf(data.get("dtSellerPersonId"));
+            }
+        }
+        if (UtilString.isBlankCompatNull(userId)) {
+            userId = YDConf.PUB_ACCOUNT;
+        }
+        return userId;
+    }
+
+    /**
+     * 通用流程发起
+     */
+    @SneakyThrows
+    @Override
+    public Map<String, Object> startProcess(String code, Map data) {
+        String instanceId = upsertData(code, data);
+        Map meta = _getMeta();
+        // 回传钉钉单据号
+        Thread.sleep(800);
+        Map form = ydClient.queryData(YDParam.builder()
+                .formInstanceId(String.valueOf(instanceId))
+                .build(), YDConf.FORM_QUERY.retrieve_id).getFormData();
+        String url = "https://pxi03f.aliwork.com/APP_QWUVLI1R6XYUXWAOPF6O/processDetail?procInsId=" + instanceId;
+        return UtilMap.map("ddProcessId, dtNumber, dtUrl", instanceId, form.get(meta.get(code + "_RC")), url);
+    }
+
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
+    @Autowired
+    private DDService ddService;
+
+    private static final String AUTH = "bbb76928-a9db-42fa-ac83-cc9a2ed75162";
+
+    /**
+     * 通用审批回调 [区分类型, 宜搭提示不同报错信息]
+     */
+    @Override
+    public void callbackProcess(Map data) {
+
+        // 拒绝审批意见必填校验
+        McException.assertParamException_Null(data, "Result");
+        // prd 4.17 取消拒绝校验, 统一返回 [审核人xxx拒绝], monitor通过活动跳转
+        //if ("0".equals(data.get("Result"))) {
+        // McException.assertParamException_Null(data, "Remark");
+        //}
+        // 回调monitor审批信息
+        data.put("Url", "https://pxi03f.aliwork.com/APP_QWUVLI1R6XYUXWAOPF6O/processDetail?procInsId=" + data.get("DdProcessId"));
+        data.put("ApprovalDate", UtilDateTime.formatLocalDateTime(LocalDateTime.now()));
+        Map UserInfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), String.valueOf(data.get("Approver")));
+        // prd 4.17 取消拒绝校验, 统一返回 [审核人xxx拒绝], monitor通过活动跳转
+        if ("0".equals(data.get("Result")) && !data.containsKey("Remark")) {
+            data.put("Remark", "审批人 【" + UserInfo.get("name") + "】拒绝");
+        }
+        data.put("Approver", String.valueOf(UserInfo.get("name")));
+        log.info("审批回调, {}", JSON.toJSONString(data));
+        // prd: 通过 http status 判定, 200 即为成功
+        String rsp = UtilHttp.doPost(_getEnvApi("/Approval"), UtilMap.map("Authorization", AUTH), UtilMap.map("Code", data.get("Code")), data);
+        Map result = (Map) JSON.parse(rsp);
+        McException.assertException(result.containsKey("errorMsg"), String.valueOf(result.get("status")), String.valueOf(result.get("errorMsg")), "Monitor");
+    }
+
+    /**
+     * 通讯录同步
+     */
+    @Override
+    @Synchronized
+    public void syncContact() {
+
+        // prd: 非全量同步: 总部\哈克\负责人
+        List<Long> deptList = UtilList.asList(DDConf.TOP_DEPARTMENT);
+        deptList.addAll(ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true, 129113277L)); // 总部
+        deptList.addAll(ddClient_contacts.getDepartmentId_all(ddClient.getAccessToken(), true, 128984162L)); // 哈克
+        for (long deptId : deptList) {
+            List<String> userIds = ddClient_contacts.listDepartmentUserId(ddClient.getAccessToken(), deptId);
+            if (userIds.size() == 0) {
+                continue;
+            }
+            // prd: 取第3层作为部门, 为空取值总经办
+            String dpetCascade = ddService.getUserDepartmentHierarchyJoin(ddClient.getAccessToken(), userIds.get(0), "-");
+            String deptName = "总经办";
+            String[] depts = dpetCascade.split("-");
+            if (depts.length >= 3) {
+                deptName = depts[2];
+            }
+            for (String userId : userIds) {
+                Map userInfo = ddClient_contacts.getUserInfoById(ddClient.getAccessToken(), userId);
+                Map data = UtilMap.map("employeeNumber, employeeName, dtPersonId, lastName", userInfo.get("job_number"), userInfo.get("name"), userInfo.get("userid"), userInfo.get("title"));
+                // prd: 手机号花名册未展示, 因此添加判空, 避免覆盖Monitor
+                if (StringUtils.isNotBlank(String.valueOf(userInfo.get("mobile")))) {
+                    data.put("mobilePhone", userInfo.get("mobile"));
+                }
+                if (userInfo.containsKey("org_email")) {
+                    data.put("email", userInfo.get("org_email"));
+                }
+                data.put("employeeStartDate", UtilDateTime.formatDate(new Date(UtilMap.getLong(userInfo, "hired_date"))));
+                data.put("employeeDept", deptName);
+                data.put("dtDepts", dpetCascade);
+                data.put("dtPersonGroup", "");
+                log.info("同步人员, {}", JSON.toJSONString(UtilMap.map("employees", Arrays.asList(data))));
+                _syncContact(data, userId);
+            }
+        }
+
+        // 离职人员同步
+        Date sDate = UtilDateTime.convertToDateFromLocalDateTime(LocalDateTime.now().minusDays(1));
+        List<Map<String, String>> maps = ddClient_contacts.getLeaveEmployeeRecords(ddClient.getAccessToken(), sDate, null);
+        for (Map<String, String> map : maps) {
+            // prd 钉钉离职数据获取不到工号, 通过99999 monitor判断, 若存在则更新部门, 负责忽略
+            Map data = UtilMap.map("employeeNumber, employeeName, dtPersonId, mobilePhone, employeeDept", "99999", map.get("name"), map.get("userId"), map.get("mobile"), "离职");
+            data.put("employeeFinishDate", map.get("leaveTime").split("T")[0]);
+            _syncContact(data, map.get("userId"));
+            log.info("离职人员, {}", JSON.toJSONString(UtilMap.map("employees", Arrays.asList(data))));
+        }
+    }
+
+    /// 员工信息写入monitor
+    void _syncContact(Map data, String userId) {
+
+        // prd: 通过 http status 判定, 200 即为成功
+        String rsp = UtilHttp.doPost(_getEnvApi("/EmployeeImport"), UtilMap.map("Authorization", AUTH), null, UtilMap.map("employees", Arrays.asList(data)));
+        Map result = (Map) JSON.parse(rsp);
+        if (UtilList.isNotEmpty(UtilMap.getList(result, "fail"))) {
+            result = (Map) UtilMap.getList(result, "fail").get(0); // 单条同步, 记录错误人员
+        }
+        //McException.assertException(result.containsKey("msg"), String.valueOf(result.get("status")), String.valueOf(result.get("msg")), "Monitor");
+        if (result.containsKey("msg")) {
+            ydClient.operateData(YDParam.builder()
+                    .formUuid("FORM-W2A66Z91912F39B2AHY0VD4CRK9F3C4JB74OLN")
+                    .formDataJson(JSON.toJSONString(UtilMap.map("dateField_lo47byyd, employeeField_lo47byyj, textareaField_lo47byyo, textareaField_lo47sanj", new Date().getTime(), userId, result, data)))
+                    .build(), YDConf.FORM_OPERATION.create);
+        }
+    }
+
+    @Autowired
+    private YDService ydService;
+
+    /**
+     * 付款数据同步
+     */
+    @Override
+    public void syncMonitor(String code) {
+
+        McException.assertAccessException(!Arrays.asList("FKSQ", "YFKSQ").contains(code), "单据编码未匹配");
+        boolean isYF = "YFKSQ".equals(code);
+
+        String rsp = UtilHttp.doPost(_getEnvApi(isYF ? "/PrePayment" : "/Payment"), UtilMap.map("Authorization", AUTH), null, UtilMap.empty());
+        Map result = (Map) JSON.parse(rsp);
+        List<Map> dataList = (List<Map>) UtilMap.getList(result, isYF ? "orders" : "rows");
+        log.info("付款数据同步, {} {}", code, dataList.size());
+        // prd: 通过 http status 判定, 200 即为成功
+        for (Map row : dataList) {
+            Map search = null;
+            if (isYF) {
+                search = YDConf.searchCondition_TextFiled("textField_lnmyiqan", UtilMap.getString(row, "orderNumber"), "eq");
+            } else {
+                search = YDConf.searchCondition_TextFiled("textField_lnmzbyzu", UtilMap.getString(row, "invoiceNumber"), "eq");
+            }
+            // 匹配是否新增
+            List<Map> formList = ydService.queryFormData(isYF ? "FORM-FCD09241E15D4E53A901D54CA3FBB084PQ6P" : "FORM-4E69EBA1416047078D14601AF17A05643350", JSON.toJSONString(Arrays.asList(search)));
+            if (formList.size() == 0) {
+                // 宜搭付款控制字段
+                row.put("status", "否");       // 反馈中
+                row.put("payed", 0);           // 已付款
+            } else {
+                Map record = formList.get(0);
+                row.put("status", record.get("radioField_ls3ddkie"));       // 反馈中
+                row.put("payed", record.get("numberField_ls3ddkif"));       // 已付款
+                row.put("instanceId", record.get("instanceId"));
+            }
+            upsertData(code, row);
+        }
+    }
+}

+ 51 - 0
mjava-hake/src/main/resources/application-dev.yml

@@ -0,0 +1,51 @@
+# 环境配置
+server:
+  port: 9001
+  servlet:
+    context-path: /api/hake
+
+# 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
+
+# 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: 2757249888
+  appKey: ding3zk0vhfifznseopg
+  appSecret: Eso_p9HrVrJClEqcwbYuuOeDbS5Lb0e8Qq_HWtJm4_GYR38E-O5UEvakWpxXAvqq
+  corpId: dinge61fe69900ea236b35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_QWUVLI1R6XYUXWAOPF6O"
+  systemToken: "SE766NA1XW0F9PG19UX926VYF9RS31DVAYMNLJ27"
+

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

@@ -0,0 +1,38 @@
+# 环境配置
+server:
+  port: 9011
+  servlet:
+    context-path: /api/hake
+
+# 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: 2757249888
+  appKey: ding3zk0vhfifznseopg
+  appSecret: Eso_p9HrVrJClEqcwbYuuOeDbS5Lb0e8Qq_HWtJm4_GYR38E-O5UEvakWpxXAvqq
+  corpId: dinge61fe69900ea236b35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_QWUVLI1R6XYUXWAOPF6O"
+  systemToken: "SE766NA1XW0F9PG19UX926VYF9RS31DVAYMNLJ27"

+ 38 - 0
mjava-hake/src/main/resources/application-test.yml

@@ -0,0 +1,38 @@
+# 环境配置
+server:
+  port: 9019
+  servlet:
+    context-path: /api/hake
+
+# 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: 2757249888
+  appKey: ding3zk0vhfifznseopg
+  appSecret: Eso_p9HrVrJClEqcwbYuuOeDbS5Lb0e8Qq_HWtJm4_GYR38E-O5UEvakWpxXAvqq
+  corpId: dinge61fe69900ea236b35c2f4657eb6378f
+  aesKey:
+  token:
+  operator: ""   # OA管理员账号
+
+# aliwork
+aliwork:
+  appType: "APP_QWUVLI1R6XYUXWAOPF6O"
+  systemToken: "SE766NA1XW0F9PG19UX926VYF9RS31DVAYMNLJ27"

+ 341 - 0
mjava-hake/src/main/resources/static/json/form.json

@@ -0,0 +1,341 @@
+{
+  "BJ": {
+    "formUuid": "FORM-4IA66891ZYZEIU2K9N4HD60ZUL3F2QJGHYMNL7",
+    "processCode": "TPROC--4IA66891ZYZEIU2K9N4HD60ZUL3F2RJGHYMNL8",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnmyiqad": "orderNumber",
+      "textField_lnmyiqae": "customerNumber",
+      "textField_lnmyiqaf": "customerStatus",
+      "textField_lnmyiqag": "customerName",
+      "textField_lnmyiqai": "deliveryAddress",
+      "textField_lnmyiqak": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "textField_lnwqhgoy": "dtOurReferencePersonId",
+      "textField_lnwqhgoz": "dtSellerPersonId",
+      "textField_lnwqhgp2": "quoteType",
+      "employeeField_lnwqhgox": "dtPersonId",
+      "employeeField_lnwqhgp0": "dtOurReferencePersonId",
+      "employeeField_lnwqhgp1": "dtSellerPersonId",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqav": "partNumber",
+        "textField_lnmyiqaw": "partName",
+        "numberField_lnmyiqax": "quantity",
+        "textField_lnmyiqaz": "currency",
+        "textField_lsts7vew": "unit",
+        "numberField_lnmyiqb1": "exchangeRate",
+        "numberField_lnmyiqb2": "priceInCompanyCurrencyExTax",
+        "numberField_lnmyiqb3": "priceInTax",
+        "numberField_lt60a87j": "standardPrice",
+        "numberField_lnmyiqb4": "",
+        "numberField_lnmyiqb5": "",
+        "textField_lnmyiqb6": "deliveryDate",
+        "textField_lu12hl9n": "circleRate"
+      }
+    }
+  },
+  "BJ_RC": "serialNumberField_lntocd8t",
+  "XSDD": {
+    "formUuid": "FORM-NO766591M91FUF566DE1X4733DB83URWQYMNL1",
+    "processCode": "TPROC--NO766591M91FUF566DE1X4733DB831TWQYMNL2",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnmyiqad": "orderNumber",
+      "textField_lnmyiqae": "customerNumber",
+      "textField_lnmyiqag": "customerName",
+      "textField_lnmyiqai": "deliveryAddress",
+      "textField_lnmyiqaf": "seller",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5g6p79": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "numberField_lnmyrzwx": "reason",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqav": "partNumber",
+        "textField_lnmyiqaw": "partName",
+        "numberField_lnmyiqax": "quantity",
+        "textField_lnmyiqaz": "unit",
+        "numberField_lnmyiqb2": "priceExTax",
+        "textField_lnmyrzx0": "deliveryDate",
+        "textField_lnmyrzwy": "currency",
+        "numberField_lnmyiqb1": "exchangeRate",
+        "numberField_lnmyrzwz": "priceInCompanyCurrencyExTax",
+        "textField_lnmyiqb6": "rowType"
+      },
+      "textareaField_lte4l7om": "changes"
+    }
+  },
+  "XSDD_RC": "serialNumberField_lntocd8t",
+  "XSDDTZ": {
+    "formUuid": "FORM-69CF7FD262DF429F8906E049E494DE5BP9AD",
+    "processCode": "TPROC--6L966171NUKG2V3OBRKYAA0QZ96G3HNVDXTPLA",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnmyiqad": "orderNumber",
+      "textField_lnmyiqae": "customerNumber",
+      "textField_lnmyiqag": "customerName",
+      "textField_lnmyiqai": "deliveryAddress",
+      "textField_lnmyiqaf": "seller",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5g6p79": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "numberField_lnmyrzwx": "reason",
+      "textField_lptxghkq": "dtOurReferencePersonId",
+      "employeeField_lptxghks": "dtOurReferencePersonId",
+      "textField_lptxghkr": "dtSellerPersonId",
+      "employeeField_lptxghkt": "dtSellerPersonId",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqav": "partNumber",
+        "textField_lnmyiqaw": "partName",
+        "numberField_lnmyiqax": "quantity",
+        "textField_lnmyiqaz": "unit",
+        "textField_lnmyrzx0": "deliveryDate",
+        "textField_lnmyiqb6": "rowType"
+      }
+    }
+  },
+  "GYSZQ": {
+    "formUuid": "FORM-JD8668C1FYZEYMQ6BY5HA43UFP9T2HUBYYMNL7",
+    "processCode": "TPROC--JD8668C1FYZEYMQ6BY5HA43UFP9T2JVBYYMNL8",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnmyiqad": "orderNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lnmyiqag": "paymentTerm",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5ggrvr": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup"
+    }
+  },
+  "GYSZQ_RC": "serialNumberField_lntocd8t",
+  "CGDD": {
+    "formUuid": "FORM-FDA66N81Q50FAELE9UEQDC4R2OR43ZDP1ZMNL9",
+    "processCode": "TPROC--FDA66N81Q50FAELE9UEQDC4R2OR435FP1ZMNLA",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lo5h6ojh": "orderNumber",
+      "textField_lnmyiqad": "supplierNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lnmyiqag": "seller",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5fmt0u": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqaw": "partNumber",
+        "textField_lnmyiqav": "partName",
+        "textField_lnmyiqaz": "unit",
+        "numberField_lnmyiqax": "quantity",
+        "numberField_lnmyiqb2": "priceExTax",
+        "textField_lnmyrzx0": "deliveryDate",
+        "textField_lnmyrzwy": "currency",
+        "numberField_lnmyiqb1": "exchangeRate",
+        "numberField_lnmyrzwz": "priceInCompanyCurrencyExTax"
+      }
+    }
+  },
+  "CGDD_RC": "serialNumberField_lntocd8t",
+  "CGDDTZ": {
+    "formUuid": "FORM-A3FC62280D774E7A8A877B05B8FD5A2CLD6J",
+    "processCode": "TPROC--IS866QA17HLGKPPV7NRZ15WB8WPB3QW37XTPL0",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lo5h6ojh": "orderNumber",
+      "textField_lnmyiqad": "supplierNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lnmyiqag": "seller",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5fmt0u": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqaw": "partNumber",
+        "textField_lnmyiqav": "partName",
+        "textField_lnmyiqaz": "unit",
+        "numberField_lnmyiqax": "quantity",
+        "textField_lnmyrzx0": "deliveryDate"
+      }
+    }
+  },
+  "CGDDJQ": {
+    "formUuid": "FORM-0K666DC1000FWTLICOT0GAHRLA363XSU5ZMNL9",
+    "processCode": "TPROC--0K666DC1000FWTLICOT0GAHRLA363YTU5ZMNLA",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lo5i25ar": "orderNumber",
+      "textField_lnmyiqad": "supplierNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lnmyiqag": "seller",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5gpzw4": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqaw": "partNumber",
+        "textField_lnmyiqav": "partName",
+        "textField_lnmyiqaz": "unit",
+        "numberField_lnmyiqax": "quantity",
+        "numberField_lnmyiqb2": "priceExTax",
+        "textField_lnmyrzx0": "deliveryDate",
+        "textField_lnmyrzwy": "currency",
+        "numberField_lnmyiqb1": "exchangeRate",
+        "numberField_lnmyrzwz": "priceInCompanyCurrencyExTax"
+      }
+    }
+  },
+  "CGDDJQ_RC": "serialNumberField_lntocd8t",
+  "FKSQ": {
+    "formUuid": "FORM-4E69EBA1416047078D14601AF17A05643350",
+    "creator": "",
+    "compIds": {
+      "textField_lnmyiqad": "supplierNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lnmyiqag": "paymentDate",
+      "textField_lnmyiqak": "paymentMethod",
+      "textField_lnmyiqan": "dueDate",
+      "textField_ls3ddkia": "supplierInvoiceNumber",
+      "textField_lnmzbyzu": "invoiceNumber",
+      "numberField_lnmzbyzz": "paymentAmount",
+      "numberField_lnmzbz00": "invoiceAmount",
+      "numberField_lnmzbz01": "restAmount",
+      "textField_ls3ddkib": "supplierAgentDtId",
+      "employeeField_ls3ddkid": "supplierAgentDtId",
+      "radioField_ls3ddkie": "status",
+      "numberField_ls3ddkif": "payed"
+    }
+  },
+  "FKSQ_RC": "serialNumberField_lntocd8t",
+  "YFKSQ": {
+    "formUuid": "FORM-FCD09241E15D4E53A901D54CA3FBB084PQ6P",
+    "creator": "",
+    "compIds": {
+      "textField_lnmyiqad": "supplierNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lnmyiqag": "paymentDate",
+      "textField_lnmyiqak": "paymentMethod",
+      "textField_lnmyiqan": "orderNumber",
+      "numberField_lnmzbyzz": "orderAmount",
+      "numberField_lnmzbz00": "paymentAmount",
+      "tableField_lnmzeuoe": {
+        "textField_lnmzeuof": "position",
+        "numberField_lnmzeuoh": "rowAmount",
+        "numberField_lnmzeuoi": "rowPaymentAmount",
+        "textField_lnmzeuok": "partNumber",
+        "textField_lnmzeuom": "partName",
+        "numberField_lnmzeuon": "quantity",
+        "numberField_lnmzeuoo": "price",
+        "numberField_lnmzeuop": "vat",
+        "numberField_lnmzeuoq": "discount"
+      },
+      "textField_ls3ddkib": "supplierAgentDtId",
+      "employeeField_ls3egxx6": "supplierAgentDtId",
+      "radioField_ls3ddkie": "status",
+      "numberField_ls3ddkif": "payed"
+    }
+  },
+  "YFKSQ_RC": "serialNumberField_lntocd8t",
+  "SJJY": {
+    "formUuid": "FORM-0X966971YVZEJ2FKE4JL29YKGQ3Q3I0OHZMNL8",
+    "processCode": "TPROC--0X966971YVZEJ2FKE4JL29YKGQ3Q3T1OHZMNL9",
+    "creator": "dtPersonId",
+    "compIds": {
+      "numberField_lnmzi8vv": "reportNumber",
+      "textField_lnmyiqae ": "orderNumber",
+      "numberField_lnmzi8vw": "reportQuantity",
+      "textField_lnmyiqak": "reporter",
+      "textField_lnmyiqan": "operationNumber",
+      "textField_lnmzi8vx": "operationName",
+      "textField_lnmzi8vy": "reportDate",
+      "textField_lnmzi8vz": "dtPersonId",
+      "textField_lnmzi8w0": "dtPersonGroup"
+    }
+  },
+  "SJJY_RC": "serialNumberField_lntocd8t",
+  "SCJS": {
+    "formUuid": "FORM-6L866J71Z00FM70UDURM67IU0YY92JRZ10NNLU",
+    "processCode": "TPROC--6L866J71Z00FM70UDURM67IU0YY92JSZ10NNLV",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnn033p5": "finishedGoodsOpNo",
+      "textField_lnmyiqae ": "finishedInspectionOpNo",
+      "textField_lnn033p6": "orderNumber",
+      "textField_lnmyiqak": "partNumber",
+      "textField_lnmyiqan": "partName",
+      "numberField_lnn033pe": "reportQuantity",
+      "textField_lnn033p7": "reporter",
+      "textField_lnn033p9": "operationNumber",
+      "textField_lnn033pb": "operationName",
+      "textField_lnmzi8vy": "reportDate",
+      "textField_lnmzi8vz": "dtPersonId",
+      "textField_lnmzi8w0": "dtPersonGroup"
+    }
+  },
+  "SCJS_RC": "serialNumberField_lntocd8t",
+  "CGDH": {
+    "formUuid": "FORM-CP766081YIZE7T7NEMTGGDLNRCLX2XLXA0NNLK",
+    "processCode": "TPROC--CP766081YIZE7T7NEMTGGDLNRCLX22NXA0NNLL",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnn033p5": "supplierNumber",
+      "textField_lnmyiqae": "supplierName",
+      "textField_lqo0h0st": "orderNumber",
+      "textField_lnmyiqak": "orderType",
+      "textField_lnmyiqan": "partNumber",
+      "textField_lnn07qeq": "partName",
+      "textField_lnn033p7": "unit",
+      "numberField_lnn0a6sw": "quantity",
+      "textField_lnn033pb": "deliveryDate",
+      "textField_lnmzi8vy": "dtPersonId"
+    }
+  },
+  "CGDH_RC": "serialNumberField_lntocd8t",
+  "XJSP": {
+    "formUuid": "FORM-K5766HA1XYZEAU3BA7C3V95ZODMT36T8E0NNLC",
+    "processCode": "TPROC--K5766HA1XYZEAU3BA7C3V95ZODMT3KU8E0NNLD",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnmyiqad": "orderNumber",
+      "textField_lnmyiqae": "supplierNumber",
+      "textField_lnmyiqaf": "supplierStatus",
+      "textField_lnmyiqag": "supplierName",
+      "textareaField_lq3r0wyk": "deliveryAddress",
+      "textField_lnmyiqak": "dtPersonId",
+      "employeeField_lo5h89wz": "dtPersonId",
+      "textField_lnmyiqan": "dtPersonGroup",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqav": "partNumber",
+        "textField_lnmyiqaw": "partName",
+        "numberField_lnmyiqax": "quantity",
+        "textField_lnmyiqaz": "currency",
+        "textField_lsts7vew": "unit",
+        "numberField_lnmyiqb1": "exchangeRate",
+        "numberField_lnmyiqb2": "priceInCompanyCurrencyExTax",
+        "numberField_lnmyiqb3": "priceInTax",
+        "textField_lnmyiqb6": "deliveryDate"
+      }
+    }
+  },
+  "XJSP_RC": "serialNumberField_lntocd8t",
+  "DJFP": {
+    "formUuid": "FORM-NO766591W11FGLKA7ZIGY5B9FEBB2CZAH0NNL8",
+    "processCode": "TPROC--NO766591W11FGLKA7ZIGY5B9FEBB2U0BH0NNL9",
+    "creator": "dtPersonId",
+    "compIds": {
+      "textField_lnmyiqad": "orderNumber",
+      "textField_lnmyiqae": "customerNumber",
+      "textField_lnmyiqaf": "customerName",
+      "textField_lnmyiqag": "seller",
+      "textField_lnmyiqai": "dtPersonId",
+      "employeeField_lo5hdfsd": "dtPersonId",
+      "textField_lnmyiqak": "dtPersonGroup",
+      "numberField_lnn0i86l": "reason",
+      "tableField_lnmyiqau": {
+        "textField_lnmyiqav": "partNumber",
+        "textField_lnmyiqaw": "partName",
+        "numberField_lnmyiqax": "quantity",
+        "textField_lnmyiqaz": "unit",
+        "numberField_lnmyiqb1": "priceExTax",
+        "textField_lnn0i86n": "currency",
+        "numberField_lnn0i86m": "exchangeRate",
+        "numberField_lnmyiqb2": "priceInCompanyCurrencyExTax"
+      }
+    }
+  },
+  "DJFP_RC": "serialNumberField_lntocd8t"
+}

+ 0 - 0
mjava-hake/src/test/resource/server.sh


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.