Bläddra i källkod

提交乐盟项目代码

zhangkan 2 veckor sedan
förälder
incheckning
7bd459b908
46 ändrade filer med 5293 tillägg och 6544 borttagningar
  1. 6 0
      .gitignore
  2. 0 0
      README.md
  3. 0 43
      main/java/com/muzhi/huanshi/controller/AccessTokenController.java
  4. 0 43
      main/java/com/muzhi/huanshi/controller/DingOaController.java
  5. 0 14
      main/java/com/muzhi/huanshi/service/AccessTokenService.java
  6. 0 14
      main/java/com/muzhi/huanshi/service/DingOaService.java
  7. 0 86
      main/java/com/muzhi/huanshi/service/impl/AccessTokenServiceImpl.java
  8. 0 2921
      main/java/com/muzhi/huanshi/service/impl/DingOaServiceImpl.java
  9. 442 0
      pom.xml
  10. 4 4
      main/java/com/muzhi/huanshi/HuanshiApplication.java
  11. 77 77
      main/java/com/muzhi/huanshi/common/R.java
  12. 12 12
      main/java/com/muzhi/huanshi/common/ResultCode.java
  13. 298 0
      src/main/java/com/muzhi/lemeng/common/alibaba/AlibabaMailBox.java
  14. 63 0
      src/main/java/com/muzhi/lemeng/common/alibaba/AliyunInvoice.java
  15. 39 0
      src/main/java/com/muzhi/lemeng/common/dingtalkwebhook/DingtalkWebhook.java
  16. 82 0
      src/main/java/com/muzhi/lemeng/common/dingtalkyida/YiDaFormData.java
  17. 395 0
      src/main/java/com/muzhi/lemeng/common/dingtalkyida/YiDaFormDataV2.java
  18. 187 0
      src/main/java/com/muzhi/lemeng/common/tencent/TencentInvoice.java
  19. 121 0
      src/main/java/com/muzhi/lemeng/common/thirdparty/EicFormData.java
  20. 40 0
      src/main/java/com/muzhi/lemeng/common/time/BeijingToUtcConverter.java
  21. 122 0
      src/main/java/com/muzhi/lemeng/common/utilityclass/PackageFlie.java
  22. 48 0
      src/main/java/com/muzhi/lemeng/controller/AccessTokenController.java
  23. 32 0
      src/main/java/com/muzhi/lemeng/controller/AlibabaMailBoxController.java
  24. 39 0
      src/main/java/com/muzhi/lemeng/controller/DingTalkController.java
  25. 39 0
      src/main/java/com/muzhi/lemeng/controller/InvoiceController.java
  26. 40 0
      src/main/java/com/muzhi/lemeng/entity/InvoiceItem.java
  27. 17 0
      src/main/java/com/muzhi/lemeng/service/AccessTokenService.java
  28. 12 0
      src/main/java/com/muzhi/lemeng/service/AliBtripCorpTokenService.java
  29. 17 0
      src/main/java/com/muzhi/lemeng/service/AliBtripTrainTicketService.java
  30. 16 0
      src/main/java/com/muzhi/lemeng/service/AliBtripVatInvoiceService.java
  31. 13 0
      src/main/java/com/muzhi/lemeng/service/AlibabaMailBoxService.java
  32. 17 0
      src/main/java/com/muzhi/lemeng/service/DingTalkService.java
  33. 19 0
      src/main/java/com/muzhi/lemeng/service/InvoiceService.java
  34. 121 0
      src/main/java/com/muzhi/lemeng/service/impl/AccessTokenServiceImpl.java
  35. 110 0
      src/main/java/com/muzhi/lemeng/service/impl/AliBtripCorpTokenServiceImpl.java
  36. 142 0
      src/main/java/com/muzhi/lemeng/service/impl/AliBtripTrainTicketServiceImpl.java
  37. 126 0
      src/main/java/com/muzhi/lemeng/service/impl/AliBtripVatInvoiceServiceImpl.java
  38. 682 0
      src/main/java/com/muzhi/lemeng/service/impl/AlibabaMailBoxServiceImpl.java
  39. 384 0
      src/main/java/com/muzhi/lemeng/service/impl/DingTalkServiceImpl.java
  40. 570 0
      src/main/java/com/muzhi/lemeng/service/impl/InvoiceServiceImpl.java
  41. 25 8
      main/resources/application.properties
  42. 6 0
      src/main/resources/application.yml
  43. 770 0
      src/test/java/com/muzhi/lemeng/APITest/Test1.java
  44. 1 1
      test/java/com/muzhi/huanshi/TestApplicationTests.java
  45. 159 0
      src/test/java/com/muzhi/lemeng/service/AliBtripInvoiceServiceTest.java
  46. 0 3321
      test/java/com/muzhi/huanshi/APITest/Test1.java

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+.DS_Store
+.idea/
+target/
+invoice/
+*.iml
+*.log

+ 0 - 0
README.md


+ 0 - 43
main/java/com/muzhi/huanshi/controller/AccessTokenController.java

@@ -1,43 +0,0 @@
-package com.muzhi.huanshi.controller;
-
-import com.muzhi.huanshi.service.AccessTokenService;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-/**
- * @Author ZhangKan
- * @Data 15:03
- */
-
-@Api(description = "钉钉授权AccessToken")
-@RestController
-@RequestMapping("/dingservice")
-@CrossOrigin//解决跨域问题
-public class AccessTokenController {
-
-    @Autowired
-    private AccessTokenService AccessTokenService;
-
-    //获取钉钉中的access_token(斯曼尔)
-    @ApiOperation(value = "获取钉钉中的access_token")
-    @GetMapping("getAccessToken")
-    public String getAccessToken(){
-
-        String accessToken = AccessTokenService.getAccessToken();
-        return accessToken;
-    }
-
-    //获取钉钉中的access_token(环世TB应用)
-    @ApiOperation(value = "获取钉钉中的access_token")
-    @GetMapping("getAppToken")
-    public String getAppToken(){
-
-        String AppToken = AccessTokenService.getAppToken();
-        return AppToken;
-    }
-}

+ 0 - 43
main/java/com/muzhi/huanshi/controller/DingOaController.java

@@ -1,43 +0,0 @@
-package com.muzhi.huanshi.controller;
-
-import com.muzhi.huanshi.service.DingOaService;
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.web.bind.annotation.*;
-
-
-@Api(description = "钉钉")
-@RestController
-@RequestMapping("/dingservice")
-//@CrossOrigin//解决跨域问题
-public class DingOaController {
-
-    @Autowired
-    private DingOaService dingOaService;
-
-    //获取钉钉人员-同步至宜搭人员信息表
-    @ApiOperation(value = "获取钉钉人员-同步至宜搭人员信息表")
-    @PostMapping("addDingdingRenyuan")
-//    @Scheduled(cron = "0 10 23 * * ?")//每天23:10触发(也就是凌晨23点10分触发)
-//    @Scheduled(cron = "0 0/10 * * * ?")//每10分钟执行一次
-//    @Scheduled(cron = "0 */10 * * * ?")//每隔10分钟执行一次
-    public String getKaoqin(){
-
-        String addDingdingRenyuan = dingOaService.addDingdingRenyuan();
-        return addDingdingRenyuan;
-    }
-
-    //统计钉钉考勤、请假、TB项目/任务工时
-    @ApiOperation(value = "统计钉钉考勤、请假、TB项目/任务工时表")
-    @PostMapping("allTongji")
-//    @Scheduled(cron = "0 30 10 * * ?")//每天01:30触发(也就是凌晨01点30分触发)
-//    @Scheduled(cron = "0 0/10 * * * ?")//每10分钟执行一次
-//    @Scheduled(cron = "0 */10 * * * ?")//每隔10分钟执行一次
-    public String allTongji(int type){
-
-        String allTongji = dingOaService.allTongji(type);
-        return allTongji;
-    }
-}

+ 0 - 14
main/java/com/muzhi/huanshi/service/AccessTokenService.java

@@ -1,14 +0,0 @@
-package com.muzhi.huanshi.service;
-
-/**
- * @Author ZhangKan
- * @Data 15:34
- */
-public interface AccessTokenService {
-
-    //获取access_token的方法(环世)
-    String getAccessToken();
-
-    //获取access_token的方法(环世TB应用)
-    String getAppToken();
-}

+ 0 - 14
main/java/com/muzhi/huanshi/service/DingOaService.java

@@ -1,14 +0,0 @@
-package com.muzhi.huanshi.service;
-
-/**
- * @Author ZhangKan
- * @Data 15:34
- */
-public interface DingOaService {
-
-    //获取钉钉人员-同步至宜搭人员信息表
-    String addDingdingRenyuan();
-
-    //统计钉钉考勤、请假、TB项目/任务工时
-    String allTongji(int type);
-}

+ 0 - 86
main/java/com/muzhi/huanshi/service/impl/AccessTokenServiceImpl.java

@@ -1,86 +0,0 @@
-package com.muzhi.huanshi.service.impl;
-
-import com.auth0.jwt.JWT;
-import com.auth0.jwt.algorithms.Algorithm;
-import com.dingtalk.api.DefaultDingTalkClient;
-import com.dingtalk.api.request.OapiGettokenRequest;
-import com.dingtalk.api.response.OapiGettokenResponse;
-import com.muzhi.huanshi.service.AccessTokenService;
-import com.taobao.api.ApiException;
-import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-
-import java.util.Date;
-
-
-/**
- * @Author ZhangKan
- * @Data 15:39
- */
-
-@Service
-public class AccessTokenServiceImpl implements AccessTokenService {
-
-    //环世
-    //APPKEY
-    private static String APPKEY = "dingobpyhlo3vioasyo9";
-    //APPSECRET
-    private static String APPSECRET = "RNLnat9hiz8zQGSyOhLf1GEAx9qS66_VoFZFYJTwT_uy2aB_5sEoByTdd7oWc3Y0";
-    //AgenthId:2769663130
-
-    public static String accessToken = null;
-
-
-
-    //获取access_token的方法(环世)
-    @Override
-    public String getAccessToken() {
-        DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
-        OapiGettokenRequest request = new OapiGettokenRequest();
-        request.setAppkey(APPKEY);
-        request.setAppsecret(APPSECRET);
-        request.setHttpMethod("GET");
-        try {
-            OapiGettokenResponse response = client.execute(request);
-            accessToken = response.getAccessToken();
-            System.out.println("环世AccessToken:" + accessToken);
-        } catch (ApiException e) {
-            e.printStackTrace();
-        }
-        return accessToken;
-    }
-
-    public static final Long   EXPIRES_IN = 1 * 3600 * 1000L;
-    public static final String TOKEN_APPID = "_appId";
-
-    public static String appId = "6555d03b73aa2fd874f4cc12";
-    public static String appSecret = "HDMqRuUY08eY1FtEzxs0hNB6A2Tq58aL";
-    //获取access_token的方法(环世TB应用)
-    @Override
-    public String getAppToken() {
-        if (StringUtils.isEmpty(appId) || StringUtils.isEmpty(appSecret)) {
-            return null;
-        }
-
-        Algorithm algorithm = Algorithm.HMAC256(appSecret);
-        long timestamp = System.currentTimeMillis();
-        Date issuedAt = new Date(timestamp);
-        Date expiresAt = new Date(timestamp + EXPIRES_IN);
-
-//        return JWT.create()
-//                .withClaim(TOKEN_APPID, appId)
-//                .withIssuedAt(issuedAt)
-//                .withExpiresAt(expiresAt)
-//                .sign(algorithm);
-
-        String sign = JWT.create()
-                .withClaim(TOKEN_APPID, appId)
-                .withIssuedAt(issuedAt)
-                .withExpiresAt(expiresAt)
-                .sign(algorithm);
-        System.out.println("环世TB应用的AccessToken:" + sign);
-
-        return sign;
-
-    }
-}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 2921
main/java/com/muzhi/huanshi/service/impl/DingOaServiceImpl.java


+ 442 - 0
pom.xml

@@ -0,0 +1,442 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.3.3.RELEASE</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.muzhi.lemeng</groupId>
+    <artifactId>springboot</artifactId>
+<!--    <packaging>jar</packaging>-->
+    <version>lemeng</version>
+    <name>lemeng</name>
+    <description>Demo project for Spring Boot</description>
+
+    <properties>
+        <java.version>1.8</java.version>
+        <fastjson.version>1.2.33</fastjson.version>
+        <gson.version>2.8.2</gson.version>
+        <json.version>20170516</json.version>
+        <poi.version>3.17</poi.version>
+        <jodatime.version>2.10.1</jodatime.version>
+        <swagger.version>2.7.0</swagger.version>
+        <commons-fileupload.version>1.3.1</commons-fileupload.version>
+        <!--        <thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>-->
+        <!-- 布局功能的支持程序 thymeleaf3主程序 layout2以上版本-->
+        <!--        <thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>-->
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>sonatype-nexus-staging</id>
+            <name>Sonatype Nexus Staging</name>
+            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <!-- JAXB API (Java 11+ 需要手动添加) -->
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+            <version>2.3.3</version>
+        </dependency>
+
+        <!-- 用于发送 HTTP 请求 (Java 11+ 可选,也可用内置 HttpClient) -->
+        <!-- 如果你用 Java 8,推荐用 Apache HttpClient -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.14</version>
+        </dependency>
+
+        <!-- Apache HttpClient (Java 8 最常用的 HTTP 客户端) -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.14</version>
+        </dependency>
+
+        <!-- JAXB API (Java 8 内置,但显式声明避免问题) -->
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <version>2.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+            <version>2.3.3</version>
+        </dependency>
+
+
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.4.0</version>
+        </dependency>
+
+        <!-- httpClient -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore</artifactId>
+            <version>4.4.13</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-httpclient</groupId>
+            <artifactId>commons-httpclient</artifactId>
+            <version>3.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.4.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.konghq</groupId>
+            <artifactId>unirest-java</artifactId>
+            <version>2.3.17</version>
+        </dependency>
+
+<!--        <dependency>-->
+<!--            <groupId>org.slf4j</groupId>-->
+<!--            <artifactId>slf4j-api</artifactId>-->
+<!--        </dependency>-->
+
+<!--        <dependency>-->
+<!--            <groupId>commons-io</groupId>-->
+<!--            <artifactId>commons-io</artifactId>-->
+<!--        </dependency>-->
+
+        <!--json 组件 -->
+<!--        <dependency>-->
+<!--            <groupId>com.alibaba</groupId>-->
+<!--            <artifactId>fastjson</artifactId>-->
+<!--            <scope>provided</scope>-->
+<!--        </dependency>-->
+
+        <!--        <dependency>-->
+        <!--            <groupId>org.apache.cxf</groupId>-->
+        <!--            <artifactId>cxf-spring-boot-starter-jaxws</artifactId>-->
+        <!--            <version>3.3.1</version>-->
+        <!--        </dependency>-->
+
+        <!--        <dependency>-->
+        <!--            <groupId>org.springframework.boot</groupId>-->
+        <!--            <artifactId>spring-boot-starter-thymeleaf</artifactId>-->
+        <!--&lt;!&ndash;            <version>2.3.3.RELEASE</version>&ndash;&gt;-->
+        <!--        </dependency>-->
+
+        <!--阿里云OCR识别-->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>ocr_api20210707</artifactId>
+            <version>3.1.3</version>
+        </dependency>
+
+        <!-- 腾讯云OCR识别 -->
+        <dependency>
+            <groupId>com.tencentcloudapi</groupId>
+            <artifactId>tencentcloud-sdk-java-ocr</artifactId>
+            <version>3.1.1210</version>
+        </dependency>
+
+        <!-- 阿里云 SDK Core -->
+<!--        <dependency>-->
+<!--            <groupId>com.aliyun</groupId>-->
+<!--            <artifactId>tea-openapi</artifactId>-->
+<!--            <version>2.4.2</version>-->
+<!--        </dependency>-->
+
+        <!-- 如果使用 OkHttp 作为 HTTP 客户端 -->
+<!--        <dependency>-->
+<!--            <groupId>com.aliyun</groupId>-->
+<!--            <artifactId>tea-util</artifactId>-->
+<!--            <version>0.2.21</version>-->
+<!--        </dependency>-->
+
+        <!--压缩包相关-->
+        <!-- RAR 解压支持 -->
+        <dependency>
+            <groupId>com.github.junrar</groupId>
+            <artifactId>junrar</artifactId>
+            <version>7.5.3</version>
+        </dependency>
+
+        <!--钉钉相关-->
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
+            <version>2.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>dingtalk</artifactId>
+            <version>2.2.34</version>
+        </dependency>
+
+        <!--json相关-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>${fastjson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+            <version>${json.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>${gson.version}</version>
+        </dependency>
+
+        <!--        <dependency>-->
+        <!--            <groupId>net.sf.json-lib</groupId>-->
+        <!--            <artifactId>json-lib</artifactId>-->
+        <!--            <version>2.4</version>-->
+        <!--        </dependency>-->
+
+
+        <!-- JSONObject对象依赖的jar包 -->
+        <dependency>
+            <groupId>commons-beanutils</groupId>
+            <artifactId>commons-beanutils</artifactId>
+            <version>1.9.3</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-collections</groupId>
+            <artifactId>commons-collections</artifactId>
+            <version>3.2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-logging</groupId>
+            <artifactId>commons-logging</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>net.sf.ezmorph</groupId>
+            <artifactId>ezmorph</artifactId>
+            <version>1.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>net.sf.json-lib</groupId>
+            <artifactId>json-lib</artifactId>
+            <version>2.2.3</version>
+            <classifier>jdk15</classifier><!-- 指定jdk版本 -->
+        </dependency>
+
+
+        <!--xls-->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi</artifactId>
+            <version>${poi.version}</version>
+        </dependency>
+        <!--xlsx-->
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>${poi.version}</version>
+        </dependency>
+        <!--文件上传-->
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+            <version>${commons-fileupload.version}</version>
+        </dependency>
+
+        <!--        <dependency>-->
+        <!--            <groupId>org.springframework.boot</groupId>-->
+        <!--            <artifactId>spring-boot-starter-thymeleaf</artifactId>-->
+        <!--        </dependency>-->
+
+        <!--引入web模块-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <version>2.3.3.RELEASE</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+            <version>1.18.10</version>
+        </dependency>
+
+        <!--MD5加密-->
+        <dependency>
+            <groupId>org.apache.directory.studio</groupId>
+            <artifactId>org.apache.commons.codec</artifactId>
+            <version>1.8</version>
+        </dependency>
+
+        <!--Map缓存方法:可设置Map中的Entry在一段时间后自动过期-->
+        <dependency>
+            <groupId>net.jodah</groupId>
+            <artifactId>expiringmap</artifactId>
+            <version>0.5.8</version>
+        </dependency>
+
+<!--        &lt;!&ndash;mybatis-plus数据库连接&ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.baomidou</groupId>-->
+<!--            <artifactId>mybatis-plus-boot-starter</artifactId>-->
+<!--            <version>3.2.0</version>-->
+<!--        </dependency>-->
+<!--        &lt;!&ndash;引入mysql:数据库驱动&ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>mysql</groupId>-->
+<!--            <artifactId>mysql-connector-java</artifactId>-->
+<!--            <scope>runtime</scope>-->
+<!--        </dependency>-->
+<!--        &lt;!&ndash;引入druid数据源&ndash;&gt;-->
+<!--        &lt;!&ndash; https://mvnrepository.com/artifact/com.alibaba/druid &ndash;&gt;-->
+<!--        <dependency>-->
+<!--            <groupId>com.alibaba</groupId>-->
+<!--            <artifactId>druid</artifactId>-->
+<!--            <version>1.1.9</version>-->
+<!--        </dependency>-->
+<!--        <dependency>-->
+<!--            <groupId>com.alibaba</groupId>-->
+<!--            <artifactId>druid-spring-boot-starter</artifactId>-->
+<!--            <version>1.1.9</version>-->
+<!--        </dependency>-->
+
+        <!--引入jpa模块-->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+
+        <!--日期时间工具-->
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+            <version>${jodatime.version}</version>
+        </dependency>
+
+        <!--日期工具-->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>4.6.8</version>
+        </dependency>
+
+        <!--log4j-->
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.17</version>
+        </dependency>
+
+        <!--swagger-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger2</artifactId>
+            <version>${swagger.version}</version>
+        </dependency>
+        <!--swagger ui-->
+        <dependency>
+            <groupId>io.springfox</groupId>
+            <artifactId>springfox-swagger-ui</artifactId>
+            <version>${swagger.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.junit.vintage</groupId>
+                    <artifactId>junit-vintage-engine</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <fork>true</fork>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>2.3.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                    <encoding>utf8</encoding>
+                </configuration>
+            </plugin>
+
+            <!--打包跳过测试-->
+<!--            <plugin>-->
+<!--                <groupId>org.apache.maven.plugins</groupId>-->
+<!--                <artifactId>maven-surefire-plugin</artifactId>-->
+<!--                <version>2.4.2</version>-->
+<!--                <configuration>-->
+<!--                    <skipTests>true</skipTests>-->
+<!--                </configuration>-->
+<!--            </plugin>-->
+        </plugins>
+
+        <!--映射mapper中对应的xml配置文件,如果没有mapper.xml文件则将下列注释-->
+<!--        <resources>-->
+<!--            <resource>-->
+<!--                <directory>src/main/java</directory>-->
+<!--                <includes>-->
+<!--                    <include>**/*.xml</include>-->
+
+<!--                    <include>**/*.properties</include>-->
+<!--                    <include>**/*.tld</include>-->
+<!--                    <include>**/*.xls</include>-->
+<!--                    <include>**/*.xlsx</include>-->
+<!--                </includes>-->
+<!--                <filtering>false</filtering>-->
+<!--            </resource>-->
+<!--        </resources>-->
+    </build>
+
+</project>

+ 4 - 4
main/java/com/muzhi/huanshi/HuanshiApplication.java

@@ -1,4 +1,4 @@
-package com.muzhi.huanshi;
+package com.muzhi.lemeng;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -7,15 +7,15 @@ import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 
-//@MapperScan("com.muzhi.simaner.mapper")
+//@MapperScan("com.muzhi.huaxiao.mapper")
 @EnableAsync//开启异步注解
 @SpringBootApplication(exclude= DataSourceAutoConfiguration.class)//启动时不加载数据库(exclude= DataSourceAutoConfiguration.class)
 @EnableScheduling//开启定时任务
 //@ComponentScan(basePackages = {"com.muzhi"})
-public class HuanshiApplication {
+public class LemengApplication {
 
     public static void main(String[] args){
-        SpringApplication.run(HuanshiApplication.class, args);
+        SpringApplication.run(LemengApplication.class, args);
     }
 
 }

+ 77 - 77
main/java/com/muzhi/huanshi/common/R.java

@@ -1,77 +1,77 @@
-package com.muzhi.huanshi.common;
-
-import io.swagger.annotations.ApiModelProperty;
-import lombok.Data;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @Author ZhangKan
- * @Data 11:20
- */
-
-//统一返回结果类
-@Data
-public class R {
-
-    @ApiModelProperty(value = "是否成功")
-    private Boolean success;
-
-    @ApiModelProperty(value = "返回码")
-    private Integer code;
-
-    @ApiModelProperty(value = "返回消息")
-    private String message;
-
-    @ApiModelProperty(value = "返回数据")
-    private Map<String, Object> data = new HashMap<String, Object>();
-
-    //把构造方法私有
-    private R(){}
-
-    //成功静态方法
-    public static R ok(){
-
-        R r = new R();
-        r.setSuccess(true);
-        r.setCode(ResultCode.SUCCESS);
-        r.setMessage("成功");
-        return r;
-    }
-
-    //失败静态方法
-    public static R error(){
-
-        R r = new R();
-        r.setSuccess(false);
-        r.setCode(ResultCode.ERROR);
-        r.setMessage("失败");
-        return r;
-    }
-
-    public R message(String message){
-        this.setMessage(message);
-        return this;
-    }
-
-    public R code(Integer code){
-        this.setCode(code);
-        return this;
-    }
-
-    public R data(String key, Object value){
-        this.data.put(key, value);
-        return this;
-    }
-
-    public R data(String key, String value){
-        this.data.put(key, value);
-        return this;
-    }
-
-    public R data(Map<String, Object> map){
-        this.setData(map);
-        return this;
-    }
-}
+package com.muzhi.lemeng.common;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @Author ZhangKan
+ * @Data 11:20
+ */
+
+//统一返回结果类
+@Data
+public class R {
+
+    @ApiModelProperty(value = "是否成功")
+    private Boolean success;
+
+    @ApiModelProperty(value = "返回码")
+    private Integer code;
+
+    @ApiModelProperty(value = "返回消息")
+    private String message;
+
+    @ApiModelProperty(value = "返回数据")
+    private Map<String, Object> data = new HashMap<String, Object>();
+
+    //把构造方法私有
+    private R(){}
+
+    //成功静态方法
+    public static R ok(){
+
+        R r = new R();
+        r.setSuccess(true);
+        r.setCode(ResultCode.SUCCESS);
+        r.setMessage("成功");
+        return r;
+    }
+
+    //失败静态方法
+    public static R error(){
+
+        R r = new R();
+        r.setSuccess(false);
+        r.setCode(ResultCode.ERROR);
+        r.setMessage("失败");
+        return r;
+    }
+
+    public R message(String message){
+        this.setMessage(message);
+        return this;
+    }
+
+    public R code(Integer code){
+        this.setCode(code);
+        return this;
+    }
+
+    public R data(String key, Object value){
+        this.data.put(key, value);
+        return this;
+    }
+
+    public R data(String key, String value){
+        this.data.put(key, value);
+        return this;
+    }
+
+    public R data(Map<String, Object> map){
+        this.setData(map);
+        return this;
+    }
+}

+ 12 - 12
main/java/com/muzhi/huanshi/common/ResultCode.java

@@ -1,12 +1,12 @@
-package com.muzhi.huanshi.common;
-
-/**
- * @Author ZhangKan
- * @Data 11:15
- */
-public interface ResultCode {
-
-    public static Integer SUCCESS = 200;//成功
-
-    public static Integer ERROR = 201;//失败
-}
+package com.muzhi.lemeng.common;
+
+/**
+ * @Author ZhangKan
+ * @Data 11:15
+ */
+public interface ResultCode {
+
+    public static Integer SUCCESS = 200;//成功
+
+    public static Integer ERROR = 201;//失败
+}

+ 298 - 0
src/main/java/com/muzhi/lemeng/common/alibaba/AlibabaMailBox.java

@@ -0,0 +1,298 @@
+package com.muzhi.lemeng.common.alibaba;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.utilityclass.PackageFlie;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+public class AlibabaMailBox {
+
+    /**
+     * 搜索邮件
+     * REMARK 接口文档地址:https://mailhelp.aliyun.com/openapi/index.html?spm=a2c4g.11186623.0.0.3375c4f4BlclKG#/operations/alimailpb_message_MailSearchService_SearchMessage
+     *
+     * @param emailAddress 邮箱地址
+     * @param select       筛选字段
+     * @param query        查询条件
+     * @return jsonArray[]
+     */
+    public static JSONArray QueryMail(String emailAddress, String select, String query) {
+        // TODO 搜索邮件
+
+        JSONArray jsonArray = new JSONArray();
+        return jsonArray;
+    }
+
+    /**
+     * 列出邮件的全部附件
+     * REMARK 接口文档地址:https://mailhelp.aliyun.com/openapi/index.html?spm=a2c4g.11186623.0.0.3375c4f4BlclKG#/operations/alimailpb_alimail_mailagent_open_MailService_ListAttachments
+     *
+     * @param emailAddress 邮箱地址
+     * @param emailId      邮件id
+     * @param alibabaMailboxAccessToken 阿里邮箱授权令牌
+     * @return jsonArray[]
+     */
+    public static JSONArray MailAttachments(String emailAddress, String emailId, String alibabaMailboxAccessToken) {
+        log.info("开始获取邮件附件,邮箱: {}, 邮件ID: {}", emailAddress, emailId);
+
+        JSONArray jsonArray = new JSONArray();
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("text/plain");
+            Request request = new Request.Builder()
+                    .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/"+ emailId +"/attachments")
+                    .method("GET", null)
+                    .addHeader("Authorization", "Bearer " + alibabaMailboxAccessToken)
+                    .build();
+            Response response = client.newCall(request).execute();
+            String resStr = response.body().string();
+            JSONObject jsonObject = JSONObject.parseObject(resStr);
+            jsonArray = jsonObject.getJSONArray("attachments") != null ? jsonObject.getJSONArray("attachments") : new JSONArray();
+            System.out.println("列出邮件的发票附件列表[]:" + jsonArray);
+            return jsonArray;
+        }catch (Exception e){
+            log.error("该邮件的发票附件获取异常:{}", e);
+            return jsonArray;
+        }
+    }
+
+    /**
+     * 创建下载会话(附件)
+     * REMARK 接口文档地址:https://mailhelp.aliyun.com/openapi/index.html?spm=a2c4g.11186623.0.0.3375c4f4BlclKG#/operations/alimailpb_alimail_mailagent_open_MailService_CreateAttachmentDownloadSession
+     *
+     * @param type         文件处理类型
+     * @param emailAddress 邮箱地址
+     * @param emailId      邮件id
+     * @param emailFileId  邮件附件id
+     * @param fileName     文件名
+     * @param alibabaMailboxAccessToken 阿里邮箱授权令牌
+     * @return Map key:url; value:fileName
+     */
+    public static Map downLoadMailFile(String type, String emailAddress, String emailId, String emailFileId, String fileName, String alibabaMailboxAccessToken) {
+        log.info("文件处理类型:" + type);
+        log.info("开始获取邮件附件,邮箱: {}, 邮件ID: {}, 邮件附件ID: {}", emailAddress, emailId, emailFileId);
+
+        String timeMillisText = "" + System.currentTimeMillis();
+        System.out.println("当前时间戳:" + timeMillisText);
+        //文件名拼接毫秒级时间戳,应对可能存在的文件名重复的问题导致文件被覆盖
+        fileName = timeMillisText + fileName;
+
+        Map map = new HashMap();
+
+        String fileNewUrl = null;
+        if (type.equals("pdf")){
+            try {
+                OkHttpClient client = new OkHttpClient().newBuilder()
+                        .build();
+                MediaType mediaType = MediaType.parse("text/plain");
+                Request request = new Request.Builder()
+                        .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/"+ emailId +"/attachments/"+ emailFileId +"/$value")
+                        .method("GET", null)
+                        .addHeader("Authorization", "Bearer " + alibabaMailboxAccessToken)
+                        .build();
+                try (Response response = client.newCall(request).execute()) {
+                    if (!response.isSuccessful()) {
+                        System.err.println("下载失败: " + response.code() + " - " + response.message());
+                        // 打印错误 body(可能是 JSON 错误信息)
+                        if (response.body() != null) {
+                            System.err.println("Error body: " + response.body().string());
+                        }
+                        return null;
+                    }
+
+                    String destinationDir = "invoice"; // Linux 路径
+
+                    //获取当前日期,用于创建文件保存目录中增加当前日期的文件夹作区分
+                    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");//设置日期格式 yyyy-MM-dd HH:mm:ss
+                    String format = df.format(new Date());
+                    System.out.println(format);// new Date()为获取当前系统时间
+
+                    String path = destinationDir + File.separatorChar + format;// 指定存放位置,此处使用的是全局变量的文件名称
+                    // 确保目录存在
+                    Path dir = Paths.get(path);
+
+                    Files.createDirectories(dir);
+
+                    // 构建完整文件路径
+                    Path filePath = dir.resolve(fileName);
+
+                    // 从响应流直接写入文件(高效,不加载全内存)
+                    try (ResponseBody body = response.body()) {
+                        if (body == null) {
+                            throw new IOException("响应体为空");
+                        }
+                        Files.copy(body.byteStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
+                    }
+                    System.out.println("✅ 附件已保存至: " + filePath.toAbsolutePath());
+
+                    String yunpuHost = "https://poc.cloudpure.cn/lemeng/invoice/";//STOP YUNPU临时服务器
+                    String lemengHost = "http://112.124.41.243/lemeng/invoice/";//乐檬正式服务器
+
+                    fileNewUrl = lemengHost + format + File.separator + fileName;
+                    System.out.println("服务器访问全地址:" + fileNewUrl);
+
+                    map.put("fileNewUrl", fileNewUrl);
+                    map.put("fileName", fileName);
+
+                    //【Linux下面的路径】//NOTE 如果程序需要直接访问该存储的文件,则需要使用下面的路径
+//                String useFilePath = destinationDir + File.separatorChar + format + File.separator + fileName;
+//                System.out.println("服务器直接访问进行发票解析的地址:" + useFilePath);
+                    return map;
+                } catch (IOException e) {
+                    System.err.println("❌下载异常: " + e.getMessage());
+                    e.printStackTrace();
+                    return null;
+                }
+            }catch (Exception e){
+                e.printStackTrace();
+                return null;
+            }
+        }else if (type.equals("zip")){
+            try {
+                OkHttpClient client = new OkHttpClient().newBuilder()
+                        .build();
+                MediaType mediaType = MediaType.parse("text/plain");
+                Request request = new Request.Builder()
+                        .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/"+ emailId +"/attachments/"+ emailFileId +"/$value")
+                        .method("GET", null)
+                        .addHeader("Authorization", "Bearer " + alibabaMailboxAccessToken)
+                        .build();
+                try (Response response = client.newCall(request).execute()) {
+                    if (!response.isSuccessful()) {
+                        throw new IOException("Failed to download: " + response.code());
+                    }
+
+                    ResponseBody body = response.body();
+                    if (body == null) {
+                        throw new IOException("Empty response body");
+                    }
+
+                    String destinationDir = "invoice"; // Linux 路径
+
+                    //获取当前日期,用于创建文件保存目录中增加当前日期的文件夹作区分
+                    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");//设置日期格式 yyyy-MM-dd HH:mm:ss
+                    String format = df.format(new Date());
+                    System.out.println(format);// new Date()为获取当前系统时间
+
+                    String path = destinationDir + File.separatorChar + format;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                    //fixme 临时测试使用,本机路径
+//                    path = "/Users/zhangkan/IdeaProject/lemeng/invoice/" + format;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                    // 确保目录存在
+                    Path dir = Paths.get(path);
+
+                    Files.createDirectories(dir);
+
+                    try (InputStream inputStream = body.byteStream()) {
+                        // 构建完整文件路径
+                        Path filePath = dir.resolve("");
+                        fileName = PackageFlie.extractPdfFromZipStream(inputStream, filePath);
+
+                        String yunpuHost = "https://poc.cloudpure.cn/lemeng/invoice/";//STOP YUNPU临时服务器
+                        String lemengHost = "http://112.124.41.243/lemeng/invoice/";//乐檬正式服务器
+
+                        fileNewUrl = lemengHost + format + File.separator + fileName;
+                        System.out.println("服务器访问全地址:" + fileNewUrl);
+
+                        map.put("fileNewUrl", fileNewUrl);
+                        map.put("fileName", fileName);
+                        return map;
+                    }catch (Exception e){
+                        // 尝试通过魔数检测(可选增强)
+                        throw new IllegalArgumentException("Unsupported compression format: " + fileName);
+                    }
+                } catch (IOException e) {
+                    System.err.println("❌下载异常: " + e.getMessage());
+                    e.printStackTrace();
+                    return null;
+                }
+            }catch (Exception e){
+                e.printStackTrace();
+                return null;
+            }
+        } else if (type.equals("rar")) {
+            try {
+                OkHttpClient client = new OkHttpClient().newBuilder()
+                        .build();
+                MediaType mediaType = MediaType.parse("text/plain");
+                Request request = new Request.Builder()
+                        .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/"+ emailId +"/attachments/"+ emailFileId +"/$value")
+                        .method("GET", null)
+                        .addHeader("Authorization", "Bearer " + alibabaMailboxAccessToken)
+                        .build();
+                try (Response response = client.newCall(request).execute()) {
+                    if (!response.isSuccessful()) {
+                        throw new IOException("Failed to download: " + response.code());
+                    }
+
+                    ResponseBody body = response.body();
+                    if (body == null) {
+                        throw new IOException("Empty response body");
+                    }
+
+                    String destinationDir = "invoice"; // Linux 路径
+
+                    //获取当前日期,用于创建文件保存目录中增加当前日期的文件夹作区分
+                    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");//设置日期格式 yyyy-MM-dd HH:mm:ss
+                    String format = df.format(new Date());
+                    System.out.println(format);// new Date()为获取当前系统时间
+
+                    String path = destinationDir + File.separatorChar + format;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                    //fixme 临时测试使用,本机路径
+//                    path = "/Users/zhangkan/IdeaProject/lemeng/invoice/" + format;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                    // 确保目录存在
+                    Path dir = Paths.get(path);
+
+                    Files.createDirectories(dir);
+
+                    try (InputStream inputStream = body.byteStream()) {
+                        // 构建完整文件路径
+                        Path filePath = dir.resolve("");
+                        fileName = PackageFlie.extractPdfFromRarStream(inputStream, filePath);
+
+                        String yunpuHost = "https://poc.cloudpure.cn/lemeng/invoice/";//STOP YUNPU临时服务器
+                        String lemengHost = "http://112.124.41.243/lemeng/invoice/";//乐檬正式服务器
+
+                        fileNewUrl = lemengHost + format + File.separator + fileName;
+                        System.out.println("服务器访问全地址:" + fileNewUrl);
+
+                        map.put("fileNewUrl", fileNewUrl);
+                        map.put("fileName", fileName);
+                        return map;
+                    }catch (Exception e){
+                        // 尝试通过魔数检测(可选增强)
+                        throw new IllegalArgumentException("Unsupported compression format: " + fileName);
+                    }
+                } catch (IOException e) {
+                    System.err.println("❌下载异常: " + e.getMessage());
+                    e.printStackTrace();
+                    return null;
+                }
+            }catch (Exception e){
+                e.printStackTrace();
+                return null;
+            }
+        } else {
+            log.info("--->当前文件类型暂不支持处理!");
+            return null;
+        }
+    }
+}

+ 63 - 0
src/main/java/com/muzhi/lemeng/common/alibaba/AliyunInvoice.java

@@ -0,0 +1,63 @@
+package com.muzhi.lemeng.common.alibaba;
+
+import com.aliyun.ocr_api20210707.Client;
+import com.aliyun.ocr_api20210707.models.*;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class AliyunInvoice {
+    private static String accessKeyId;
+    private static String accessKeySecret;
+
+    // 创建一个非静态的 Setter 方法,加上 @Value
+    // Spring 启动时会调用这个方法,将配置文件值传入
+    @Value("${myapp.aliyun.accessKeyId}")
+    public void setAccessKeyId(String value) {
+        AliyunInvoice.accessKeyId = value; // 赋值给静态变量
+    }
+    @Value("${myapp.aliyun.accessKeySecret}")
+    public void setAccessKeySecret(String value) {
+        AliyunInvoice.accessKeySecret = value; // 赋值给静态变量
+    }
+
+    /**
+     * 创建 OCR 客户端
+     *
+     * @param accessKeyId     阿里云 AccessKey ID
+     * @param accessKeySecret 阿里云 AccessKey Secret
+     */
+    private static Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
+        Config config = new Config()
+                .setAccessKeyId(accessKeyId)
+                .setAccessKeySecret(accessKeySecret);
+        // 设置 Endpoint
+        config.endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
+        return new Client(config);
+    }
+
+    /**
+     * 执行 OCR 识别
+     *
+     * @param imageUrl 发票文件公网 URL
+     */
+    public static RecognizeAllTextResponse performOcrRecognition(String imageUrl) throws Exception {
+        // 初始化 OCR 客户端
+        Client client = createClient(accessKeyId, accessKeySecret);
+
+        RecognizeAllTextRequest request = new RecognizeAllTextRequest();
+        request.setUrl(imageUrl);
+        // 使用通用识别类型,SDK 会自动判断内部具体的小票类型
+        request.setType("Advanced");
+
+        // 设置运行时选项(如超时)
+        RuntimeOptions runtime = new RuntimeOptions();
+        runtime.readTimeout = 10000; // 10秒读超时
+        runtime.connectTimeout = 5000; // 5秒连接超时
+        return client.recognizeAllTextWithOptions(request, runtime);
+    }
+}

+ 39 - 0
src/main/java/com/muzhi/lemeng/common/dingtalkwebhook/DingtalkWebhook.java

@@ -0,0 +1,39 @@
+package com.muzhi.lemeng.common.dingtalkwebhook;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+public class DingtalkWebhook {
+
+
+    public static JSONObject dingtalkWebhookConnection(String webhookUrl, String mapStr){
+        log.info("钉钉webhookUrl:" + webhookUrl);
+        log.info("钉钉mapStr:" + mapStr);
+
+        OkHttpClient client = new OkHttpClient().newBuilder()
+                .build();
+        MediaType mediaType = MediaType.parse("text/plain");
+        RequestBody body = RequestBody.create(mediaType, mapStr);
+        Request request = new Request.Builder()
+                .url(webhookUrl)
+                .method("POST", body)
+                .build();
+        JSONObject jsonObject = null;
+        try {
+            Response response = client.newCall(request).execute();
+            String bodyStr = response.body().string();
+            jsonObject = JSON.parseObject(bodyStr);
+            System.out.println("返回体body{}:" + jsonObject);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+        return jsonObject;
+    }
+
+}

+ 82 - 0
src/main/java/com/muzhi/lemeng/common/dingtalkyida/YiDaFormData.java

@@ -0,0 +1,82 @@
+package com.muzhi.lemeng.common.dingtalkyida;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.dingtalkyida_1_0.models.CreateOrUpdateFormDataResponse;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teautil.models.RuntimeOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class YiDaFormData {
+
+    private static String appType;
+    private static String appSecret;
+
+    // 创建一个非静态的 Setter 方法,加上 @Value
+    // Spring 启动时会调用这个方法,将配置文件值传入
+    @Value("${myapp.yida.appType}")
+    public void setAppType(String value) {
+        YiDaFormData.appType = value; // 赋值给静态变量
+    }
+    @Value("${myapp.yida.appSecret}")
+    public void setAppSecret(String value) {
+        YiDaFormData.appSecret = value; // 赋值给静态变量
+    }
+
+    //宜搭接口调用
+    public static com.aliyun.dingtalkyida_1_0.Client createClient() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkyida_1_0.Client(config);
+    }
+
+    /**
+     * 新增或更新表单实例
+     * REMARK 接口文档地址:https://open.dingtalk.com/document/development/api-createorupdateformdata-v2
+     *
+     * @param formUuid        表单编码
+     * @param searchCondition 查询条件
+     * @param formDataJson    表单字段
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static JSONObject CreateOrUpdateFormData(String formUuid, String searchCondition, String formDataJson, String accessToken) {
+
+        JSONObject jsonObject = new JSONObject();
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client11 = createClient();
+            com.aliyun.dingtalkyida_1_0.models.CreateOrUpdateFormDataHeaders createOrUpdateFormDataHeaders = new com.aliyun.dingtalkyida_1_0.models.CreateOrUpdateFormDataHeaders();
+            createOrUpdateFormDataHeaders.xAcsDingtalkAccessToken = accessToken;
+            com.aliyun.dingtalkyida_1_0.models.CreateOrUpdateFormDataRequest createOrUpdateFormDataRequest = new com.aliyun.dingtalkyida_1_0.models.CreateOrUpdateFormDataRequest()
+                    //应用秘钥
+                    .setSystemToken(appSecret)
+                    //表单编码
+                    .setFormUuid(formUuid)
+                    .setUserId("yida_pub_account")//发起人userId,宜搭平台发起默认:yida_pub_account
+                    .setSearchCondition(searchCondition)
+                    //应用编码
+                    .setAppType(appType)
+                    .setFormDataJson(formDataJson);
+
+            CreateOrUpdateFormDataResponse orUpdateFormDataWithOptions = client11.createOrUpdateFormDataWithOptions(createOrUpdateFormDataRequest, createOrUpdateFormDataHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(orUpdateFormDataWithOptions.getBody());
+            jsonObject = JSON.parseObject(s);
+            System.out.println("【创建或更新】表单object对象:" + jsonObject);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        return jsonObject;
+    }
+}

+ 395 - 0
src/main/java/com/muzhi/lemeng/common/dingtalkyida/YiDaFormDataV2.java

@@ -0,0 +1,395 @@
+package com.muzhi.lemeng.common.dingtalkyida;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.dingtalkyida_1_0.models.*;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teautil.models.RuntimeOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class YiDaFormDataV2 {
+
+    private static String appType;
+    private static String appSecret;
+
+    // 创建一个非静态的 Setter 方法,加上 @Value
+    // Spring 启动时会调用这个方法,将配置文件值传入
+    @Value("${myapp.yida.appType}")
+    public void setAppType(String value) {
+        YiDaFormDataV2.appType = value; // 赋值给静态变量
+    }
+    @Value("${myapp.yida.appSecret}")
+    public void setAppSecret(String value) {
+        YiDaFormDataV2.appSecret = value; // 赋值给静态变量
+    }
+
+    //宜搭接口调用
+    public static com.aliyun.dingtalkyida_1_0.Client createClient() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkyida_1_0.Client(config);
+    }
+
+    public static com.aliyun.dingtalkyida_2_0.Client createClient2() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkyida_2_0.Client(config);
+    }
+
+    /**
+     * 查询和更新表单实例
+     * REMARK 接口文档地址:
+     *  【查询表单实例数据】https://open.dingtalk.com/document/development/api-searchformdatas-v2
+     *  【更新表单数据】https://open.dingtalk.com/document/development/api-updateformdata-v2
+     *
+     * @param formUuid        表单编码
+     * @param serialNumber    EIC流水号
+     * @param state           EIC报销付款状态
+     * @param message         EIC回传信息
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static String searchAndUpdateFormData(String formUuid, String serialNumber, String state, String message, String accessToken) {
+
+        String shiliID = null;
+
+        //查询表单实例数据
+        Map mapAll = new HashMap();
+        //EIC流水号
+        mapAll.put("textField_mnh2jodr", serialNumber);
+        String mapAllStr = JSON.toJSONString(mapAll);
+        System.out.println("封装查询的字段参数:" + mapAllStr);
+
+        int size = 100;
+        int page = 1;
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient();
+            com.aliyun.dingtalkyida_1_0.models.SearchFormDatasHeaders searchFormDatasHeaders = new com.aliyun.dingtalkyida_1_0.models.SearchFormDatasHeaders();
+            searchFormDatasHeaders.xAcsDingtalkAccessToken = accessToken;
+            com.aliyun.dingtalkyida_1_0.models.SearchFormDatasRequest searchFormDatasRequest = new com.aliyun.dingtalkyida_1_0.models.SearchFormDatasRequest()
+                    .setSystemToken(appSecret)
+
+                    .setSearchFieldJson(mapAllStr)
+
+                    .setCurrentPage(page)
+                    .setPageSize(size)
+                    .setFormUuid(formUuid)
+                    .setUserId("yida_pub_account")
+                    .setAppType(appType);
+
+            SearchFormDatasResponse searchFormDatasResponse = client.searchFormDatasWithOptions(searchFormDatasRequest, searchFormDatasHeaders, new RuntimeOptions());
+//            System.out.println(JSON.toJSONString(searchFormDatasResponse.getBody()));
+            String s = JSON.toJSONString(searchFormDatasResponse.getBody());
+            JSONObject jsonObject = JSON.parseObject(s);
+//            System.out.println("宜搭数据高级条件查询【报销档案】接口object对象:" + jsonObject);
+            JSONArray data = jsonObject.getJSONArray("data");
+            if (data.isEmpty()) {
+                System.out.println("查询【报销档案】返回数据为空!");
+            } else {
+                //实例ID
+                shiliID = data.getJSONObject(0).getString("formInstanceId");
+                System.out.println("实例ID===>" + shiliID);
+            }
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        }
+
+        if (shiliID != null){
+            try {
+                /**
+                 * 更新表单数据「按照数据版本」
+                 * 注:非表单字段调整不开启
+                 * */
+
+                Map formMap = new HashMap<>();
+                formMap.put("radioField_mm5zuw4o", state);//EIC付款单状态
+                formMap.put("textField_mm5zuw4m", message);//EIC回传信息
+                String formDataJson = JSON.toJSONString(formMap);
+                System.out.println("mapJson:" + formDataJson);
+
+                com.aliyun.dingtalkyida_1_0.Client client = createClient();
+                com.aliyun.dingtalkyida_1_0.models.UpdateFormDataHeaders updateFormDataHeaders = new com.aliyun.dingtalkyida_1_0.models.UpdateFormDataHeaders();
+                updateFormDataHeaders.xAcsDingtalkAccessToken = accessToken;
+                com.aliyun.dingtalkyida_1_0.models.UpdateFormDataRequest updateFormDataRequest = new com.aliyun.dingtalkyida_1_0.models.UpdateFormDataRequest()
+                        .setSystemToken(appSecret)
+                        .setFormInstanceId(shiliID)
+                        .setUserId("yida_pub_account")
+                        .setUseLatestVersion(true)
+                        .setAppType(appType)
+                        .setUpdateFormDataJson(formDataJson);
+
+                client.updateFormDataWithOptions(updateFormDataRequest, updateFormDataHeaders, new RuntimeOptions());
+            } catch (TeaException err) {
+                if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                    // err 中含有 code 和 message 属性,可帮助开发定位问题
+                }
+            } catch (Exception _err) {
+                TeaException err = new TeaException(_err.getMessage(), _err);
+                if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                    // err 中含有 code 和 message 属性,可帮助开发定位问题
+                }
+            }
+        }
+
+        return shiliID;
+    }
+
+    /**
+     * 更新表单实例
+     * REMARK 接口文档地址:https://open.dingtalk.com/document/development/api-updateformdata-v2
+     *
+     * @param shiliID         数据实例id
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static void updateFormData(String shiliID, String formDataJson, String accessToken) {
+        try {
+            /**
+             * 更新表单数据「按照数据版本」
+             * 注:非表单字段调整不开启
+             * */
+
+            com.aliyun.dingtalkyida_1_0.Client client = createClient();
+            com.aliyun.dingtalkyida_1_0.models.UpdateFormDataHeaders updateFormDataHeaders = new com.aliyun.dingtalkyida_1_0.models.UpdateFormDataHeaders();
+            updateFormDataHeaders.xAcsDingtalkAccessToken = accessToken;
+            com.aliyun.dingtalkyida_1_0.models.UpdateFormDataRequest updateFormDataRequest = new com.aliyun.dingtalkyida_1_0.models.UpdateFormDataRequest()
+                    .setSystemToken(appSecret)
+                    .setFormInstanceId(shiliID)
+                    .setUserId("yida_pub_account")
+                    .setUseLatestVersion(true)
+                    .setAppType(appType)
+                    .setUpdateFormDataJson(formDataJson);
+
+            client.updateFormDataWithOptions(updateFormDataRequest, updateFormDataHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+    }
+
+    /**
+     * 根据实例id查询表单实例数据
+     * REMARK 接口文档地址:https://open.dingtalk.com/document/development/api-getformdatabyid-v2
+     *
+     * @param shiliID         数据实例id
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static JSONObject getYidaFormData(String shiliID, String accessToken){
+
+        JSONObject jsonObject = null;
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClient2();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders getFormDataByIDHeaders = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders();
+            getFormDataByIDHeaders.xAcsDingtalkAccessToken = accessToken;
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest getFormDataByIDRequest = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest()
+                    .setSystemToken(appSecret)
+                    .setUserId("yida_pub_account")
+                    .setAppType(appType);
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDResponse formDataByIDWithOptions = client.getFormDataByIDWithOptions(shiliID, getFormDataByIDRequest, getFormDataByIDHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(formDataByIDWithOptions.getBody());
+            jsonObject = JSON.parseObject(s);
+//            System.out.println("获取宜搭表单数据,表单object对象:" + jsonObject);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        }
+        System.out.println("获取宜搭表单数据,表单object对象:" + jsonObject);
+
+        return jsonObject;
+    }
+
+    /**
+     * 根据实例id查询表单实例明细组件的数据
+     * REMARK 接口文档地址:https://open.dingtalk.com/document/development/obtain-child-table-component-data
+     *
+     * @param formUuid        表单uuid
+     * @param shiliID         数据实例id
+     * @param tabeId          明细组件id
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static JSONArray getYidaMX(String formUuid, String shiliID, String tabeId, String accessToken){
+
+        int pageNumber = 1;
+        int pageSize = 50;
+
+        float nowNum = 0;
+        float totalCount = 0;
+        List listAll = new ArrayList();
+
+        do {
+            JSONObject jsonObject = null;
+            try {
+                com.aliyun.dingtalkyida_1_0.Client client = createClient();
+                com.aliyun.dingtalkyida_1_0.models.ListTableDataByFormInstanceIdTableIdHeaders listTableDataByFormInstanceIdTableIdHeaders = new com.aliyun.dingtalkyida_1_0.models.ListTableDataByFormInstanceIdTableIdHeaders();
+                listTableDataByFormInstanceIdTableIdHeaders.xAcsDingtalkAccessToken = accessToken;
+                com.aliyun.dingtalkyida_1_0.models.ListTableDataByFormInstanceIdTableIdRequest listTableDataByFormInstanceIdTableIdRequest = new com.aliyun.dingtalkyida_1_0.models.ListTableDataByFormInstanceIdTableIdRequest()
+                        .setFormUuid(formUuid)
+                        .setPageNumber(pageNumber)
+                        .setAppType(appType)
+                        .setSystemToken(appSecret)
+                        .setPageSize(pageSize)
+                        .setTableFieldId(tabeId)
+                        .setUserId("yida_pub_account");
+
+                ListTableDataByFormInstanceIdTableIdResponse listTableDataByFormInstanceIdTableIdResponse = client.listTableDataByFormInstanceIdTableIdWithOptions(shiliID, listTableDataByFormInstanceIdTableIdRequest, listTableDataByFormInstanceIdTableIdHeaders, new RuntimeOptions());
+                String s = JSON.toJSONString(listTableDataByFormInstanceIdTableIdResponse.getBody());
+                jsonObject = JSON.parseObject(s);
+            } catch (TeaException err) {
+                if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                    // err 中含有 code 和 message 属性,可帮助开发定位问题
+                }
+            } catch (Exception _err) {
+                TeaException err = new TeaException(_err.getMessage(), _err);
+                if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                    // err 中含有 code 和 message 属性,可帮助开发定位问题
+                }
+            }
+            System.out.println("获取宜搭表单数据,明细数据object对象:" + jsonObject);
+
+            JSONArray data = jsonObject.getJSONArray("data");
+            System.out.println("返回的宜搭明细列表data:" + data);
+            for (int j = 0; j < data.size(); j++) {
+                JSONObject jsonObject1 = data.getJSONObject(j);
+                listAll.add(jsonObject1);
+            }
+
+            totalCount = jsonObject.getFloat("totalCount");
+            System.out.println("明细总数:" + totalCount);
+            nowNum = pageNumber * pageSize;
+            System.out.println("当前分页应查询出来的总数:" + nowNum);
+            pageNumber += 1;
+            System.out.println("下次查询的分页:" + pageNumber);
+        }while (nowNum <= totalCount);
+
+        //封装所有的明细分页数据
+        JSONArray jsonArray = JSON.parseArray(listAll.toString());
+        System.out.println("封装所有的明细分页数据jsonArray:" + jsonArray);
+
+        return jsonArray;
+    }
+
+    /**
+     * 查询表单实例数据
+     * REMARK 接口文档地址:【查询表单实例数据】https://open.dingtalk.com/document/development/api-searchformdatas-v2
+     *
+     * @param formUuid        表单编码
+     * @param queryMapAllStr  封装查询的字段参数
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static JSONObject getQueryYidaForm(String formUuid, String queryMapAllStr, String accessToken){
+
+        JSONObject jsonObject = new JSONObject();
+
+        int size = 100;
+        int page = 1;
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient();
+            com.aliyun.dingtalkyida_1_0.models.SearchFormDatasHeaders searchFormDatasHeaders = new com.aliyun.dingtalkyida_1_0.models.SearchFormDatasHeaders();
+            searchFormDatasHeaders.xAcsDingtalkAccessToken = accessToken;
+            com.aliyun.dingtalkyida_1_0.models.SearchFormDatasRequest searchFormDatasRequest = new com.aliyun.dingtalkyida_1_0.models.SearchFormDatasRequest()
+                    .setSystemToken(appSecret)
+
+                    .setSearchFieldJson(queryMapAllStr)
+
+                    .setCurrentPage(page)
+                    .setPageSize(size)
+                    .setFormUuid(formUuid)
+                    .setUserId("yida_pub_account")
+                    .setAppType(appType);
+
+            SearchFormDatasResponse searchFormDatasResponse = client.searchFormDatasWithOptions(searchFormDatasRequest, searchFormDatasHeaders, new RuntimeOptions());
+//            System.out.println(JSON.toJSONString(searchFormDatasResponse.getBody()));
+            String s = JSON.toJSONString(searchFormDatasResponse.getBody());
+            jsonObject = JSON.parseObject(s);
+            System.out.println("宜搭数据高级条件查询到接口object对象:" + jsonObject);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        return jsonObject;
+    }
+
+    /**
+     * 获取宜搭附件免登地址
+     * REMARK 接口文档地址:https://open.dingtalk.com/document/development/obtain-the-temporary-free-access-address-of-yixian-accessories
+     *
+     * @param url       宜搭附件地址
+     * @return String   临时免登地址URL
+     */
+    public static String getYidaTemporaryFileURL(String url, String accessToken) {
+        String fileUrl = "";
+        JSONObject jsonObject = null;
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient();
+            com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders getOpenUrlHeaders = new com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders();
+            getOpenUrlHeaders.xAcsDingtalkAccessToken = accessToken;
+            com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest getOpenUrlRequest = new com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest()
+                    .setSystemToken(appSecret)
+                    .setFileUrl(url)//宜搭附件url
+                    .setUserId("yida_pub_account")
+                    .setTimeout(86400000L);//最大值: 86400000 , 即24小时。不填则默认60000,即1分钟。填0或者负数无效。
+
+            GetOpenUrlResponse appA1U3R89IHBD21BJZVX06 = client.getOpenUrlWithOptions(appType, getOpenUrlRequest, getOpenUrlHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(appA1U3R89IHBD21BJZVX06.getBody());
+            jsonObject = JSON.parseObject(s);
+            System.out.println("获取宜搭附件免登地址:" + jsonObject);
+
+            fileUrl = jsonObject.getString("result");
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        System.out.println("获取宜搭附件免登地址:" + fileUrl);
+        return fileUrl;
+    }
+}

+ 187 - 0
src/main/java/com/muzhi/lemeng/common/tencent/TencentInvoice.java

@@ -0,0 +1,187 @@
+package com.muzhi.lemeng.common.tencent;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+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.lang.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Component
+public class TencentInvoice {
+    private static String secretId;
+    private static String secretKey;
+    private static String region;
+
+    // 创建一个非静态的 Setter 方法,加上 @Value
+    // Spring 启动时会调用这个方法,将配置文件值传入
+    @Value("${myapp.tengxun.secretId}")
+    public void setSecretId(String value) {
+        TencentInvoice.secretId = value; // 赋值给静态变量
+    }
+    @Value("${myapp.tengxun.secretKey}")
+    public void setSecretKey(String value) {
+        TencentInvoice.secretKey = value; // 赋值给静态变量
+    }
+    @Value("${myapp.tengxun.region}")
+    public void setRegion(String value) {
+        TencentInvoice.region = value; // 赋值给静态变量
+    }
+
+    // 创建请求描述
+    private static ClientProfile doRequest(String endPoint) {
+
+        HttpProfile httpProfile = new HttpProfile();
+        httpProfile.setEndpoint(endPoint);
+        ClientProfile clientProfile = new ClientProfile();
+        clientProfile.setHttpProfile(httpProfile);
+        return clientProfile;
+    }
+
+    /**
+     * 通用票据识别(高级版)
+     * REMARK 接口文档地址:https://cloud.tencent.com/document/product/866/37835
+     *
+     * @param invoiceUrl 发票文件公网URL
+     */
+    public static JSONObject doRecognizeGeneralInvoice(String invoiceUrl) throws TencentCloudSDKException{
+        Credential cred = new Credential(secretId, secretKey);
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, region, clientProfile);
+
+        RecognizeGeneralInvoiceRequest req = new RecognizeGeneralInvoiceRequest();
+        if (invoiceUrl.startsWith("http")) {
+            req.setImageUrl(invoiceUrl);
+        } else {
+            req.setImageBase64(invoiceUrl);
+        }
+        req.setEnableMultiplePage(true); // PDF多页识别, 仅支持返回文件前30页
+        req.setEnableCutImage(false); // 返回切割图片base64
+        RecognizeGeneralInvoiceResponse resp = client.RecognizeGeneralInvoice(req);
+        String result = MixedInvoiceOCRResponse.toJsonString(resp);
+        JSONObject jsonObject = JSON.parseObject(result);
+        log.debug("请求响应, {}", result);
+        return jsonObject;
+    }
+
+    /**
+     * 发票验真[新版]
+     * REMARK 接口文档地址:https://cloud.tencent.com/document/product/866/73674
+     *
+     * @param invoiceKind 票种类型
+     * @param invoiceCode 发票代码
+     * @param invoiceNo 发票号码
+     * @param invoiceDate 开票日期
+     * @param amount 价税合计金额
+     * @param checkCode 校验码
+     * @param excludingTax 不含税金额
+     * @param gmfName 购买方名称
+     * @param gmfNo 购买方纳税人识别号
+     */
+    public static JSONObject doVatInvoiceVerifyNew(String invoiceKind, String invoiceCode, String invoiceNo, String invoiceDate, String amount, String checkCode, String excludingTax, String gmfName, String gmfNo) throws TencentCloudSDKException {
+        Credential cred = new Credential(secretId, secretKey);
+        ClientProfile clientProfile = doRequest("ocr.tencentcloudapi.com");
+        OcrClient client = new OcrClient(cred, region, 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));
+        String result = null;
+        String message = null;
+        try {
+            VatInvoiceVerifyNewResponse resp = client.VatInvoiceVerifyNew(req);
+            result = VatInvoiceVerifyNewResponse.toJsonString(resp);
+        }catch (TencentCloudSDKException e){
+            e.printStackTrace();
+            message = e.getMessage();
+            log.error("发票验真失败: " + e.getMessage());
+        }
+        log.info("发票验真, {}, {}", invoiceKind, result);
+
+        // 1. 获取当前日期时间对象
+        LocalDateTime now = LocalDateTime.now();
+        // 2. 定义格式化器 (yyyy-MM-dd HH:mm:ss)
+        // 注意:HH 代表24小时制,mm 代表分钟,ss 代表秒
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        // 3. 格式化输出
+        String formattedDate = now.format(formatter);
+        System.out.println("当前时间: " + formattedDate);
+
+        Map invoiceMapResult = new HashMap();
+        if (result != null){
+            JSONObject jsonObject = JSON.parseObject(result);
+            // 因全电票取值, 取值价税合计, 单独校验金额
+            JSONObject invoice = jsonObject.getJSONObject("Invoice");
+            if (invoiceCode == null){
+                invoiceCode = "";
+            }
+            if (checkCode == null){
+                checkCode = "";
+            }
+            String fpdm = invoice.getString("Code");
+            System.out.println("发票代码: " + fpdm);
+            String fphm = invoice.getString("Number");
+            System.out.println("发票号码: " + fphm);
+            String kprq = invoice.getString("Date");
+            System.out.println("开票日期: " + kprq);
+            float hsje = invoice.getFloat("AmountWithTax");
+            System.out.println("含税金额: " + hsje);
+            float bhsje = invoice.getFloat("AmountWithoutTax");
+            System.out.println("不含税金额: " + bhsje);
+            //将含税金额、不含税金额从文本转成数字
+            float amountFloat = Float.parseFloat(amount);
+            float excludingTaxFloat = Float.parseFloat(excludingTax);
+
+            String gmfmc = invoice.getString("BuyerName");
+            System.out.println("购方名称: " + gmfmc);
+            String gmfnsrsbh = invoice.getString("BuyerTaxCode");
+            System.out.println("购方纳税人识别号: " + gmfnsrsbh);
+
+            if (invoiceCode.equals(fpdm) && invoiceNo.equals(fphm) && invoiceDate.equals(kprq) && amountFloat == hsje && excludingTaxFloat == bhsje && gmfmc.equals(gmfName) && gmfnsrsbh.equals(gmfNo)){
+                invoiceMapResult.put("state", "验真成功");
+                invoiceMapResult.put("message", "✅发票验真成功!执行时间:" + formattedDate);
+            }else {
+                invoiceMapResult.put("state", "验真失败");
+                invoiceMapResult.put("message", "⚠️发票存在疑问!执行时间:" + formattedDate);
+            }
+        }else {
+            invoiceMapResult.put("state", "验真失败");
+            invoiceMapResult.put("message", "⚠️发票存在疑问!执行时间:" + formattedDate + "【失败原因:" + message + "】");
+        }
+
+        String jsonString = JSON.toJSONString(invoiceMapResult);
+        JSONObject invoiceJsonObject = JSONObject.parseObject(jsonString);
+        log.info("发票验真结果: {}", invoiceJsonObject);
+        return invoiceJsonObject;
+    }
+}

+ 121 - 0
src/main/java/com/muzhi/lemeng/common/thirdparty/EicFormData.java

@@ -0,0 +1,121 @@
+package com.muzhi.lemeng.common.thirdparty;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class EicFormData {
+
+    private static String eicUrlAddress;
+
+    // 创建一个非静态的 Setter 方法,加上 @Value
+    // Spring 启动时会调用这个方法,将配置文件值传入
+    @Value("${myapp.eic.eicUrlAddress}")
+    public void setEicUrlAddress(String value) {
+        EicFormData.eicUrlAddress = value; // 赋值给静态变量
+    }
+
+    /**
+     * 创建EIC报销单
+     * REMARK 接口文档地址:【创建EIC报销单】
+     *
+     * @body body           请求参数对象
+     * {
+     *     "data": [
+     *         {
+     *             "serialNumber":"slid-001",//宜搭报销单流水号
+     *             "handler":"18605743616",//EIC用户手机号
+     *             "handlerDep":"10103036802",//EIC部门代码
+     *             "payer":"10101",//付款单位编号
+     *             "title":"出差报销",//报销单据主题
+     *             "way":"银行",//结算方式
+     *             "name":"张三",//账户名称(结算方式是银行,必填)
+     *             "cardNum":"23455556667777",//账户号(结算方式是银行,必填)
+     *             "bank":"建设银行黄浦支行",//开户银行(结算方式是银行,必填)
+     *             "date":"2026-03-31",//结算日期(yyyy-MM-dd)
+     *             "currency":"人民币",//币种(默认人民币;币种:人民币、美元、欧元、美金、日元、港币。(必须按照汉字规则,比如币种是美元,填写“美元”))
+     *             "subjectArr":[//报销科目数组明细
+     *                 {
+     *                     "bearer":"18605743616",//EIC用户手机号
+     *                     "bearerDep":"1010101",//EIC部门代码
+     *                     "amount":200.12,//金额
+     *                     "taxAmount":10.23,//税额
+     *                     "subjectCode":"20010101",//报销科目代码
+     *                     "subjectName":"市内出租车费",//报销科目名称
+     *                     "invoices":[//NOTE 新增
+     *                          {
+     *                              "type":"",//发票类型:专票-纸质,专票-电子,普票-纸质,普票-电子。只有这四种情况
+     *                              "date2":"",//发票日期(yyyy-MM-dd格式)
+     *                              "no":"",//发票号
+     *                              "amount2":"",//发票金额(如果是多张发票,发票合计金额必须和报销科目金额一致)
+     *                              "taxAmount2":"",//发票税额(如果是多张发票,发票合计税额必须和报销科目税额一致)
+     *                              "tax":"",//税率
+     *                              "subjectCode2":""//发票科目代码(必须和eic一致)
+     *                          }
+     *                     ]
+     *                 },
+     *                 {
+     *                     "bearer":"13486641886",
+     *                     "bearerDep":"1010102",
+     *                     "amount":345,
+     *                     "taxAmount":18.45,
+     *                     "subjectCode":"20010101",
+     *                     "subjectName":"市内出租车费",
+     *                     "invoices":[//NOTE 新增
+     *                          {
+     *                              "type":"",//发票类型:专票-纸质,专票-电子,普票-纸质,普票-电子。只有这四种情况
+     *                              "date2":"",//发票日期(yyyy-MM-dd格式)
+     *                              "no":"",//发票号
+     *                              "amount2":"",//发票金额(如果是多张发票,发票合计金额必须和报销科目金额一致)
+     *                              "taxAmount2":"",//发票税额(如果是多张发票,发票合计税额必须和报销科目税额一致)
+     *                              "tax":"",//税率
+     *                              "subjectCode2":""//发票科目代码(必须和eic一致)
+     *                          }
+     *                      ]
+     *                 }
+     *             ],
+     *             "attachment":[//NOTE 新增
+     *                  {
+     *                       "fileUrl":"",//网络文件URL
+     *                       "fileName":"",//指定文件名称,必须包含扩展名。扩展名需要与上传文件扩展名一致。如:合同.doc。长度最大为200个字符。
+     *                       "extension":""//文件扩展名,支持文件类型doc、docx、pdf、xls、xlsx、png、jpg、jpeg
+     *                  }
+     *             ]
+     *         }
+     *     ]
+     * }
+     *
+     * @return jsonObject{}
+     */
+    public static JSONObject createEidFormData(String bodyStr, String accessToken) {
+        log.info("开始接收宜搭报销单传递参数......");
+        log.info("接收宜搭报销单传递的封装body参数:" + bodyStr);
+
+        JSONObject result = null;
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("application/json");
+            RequestBody body = RequestBody.create(mediaType, bodyStr);
+            Request request = new Request.Builder()
+                    .url(eicUrlAddress)
+                    .method("POST", body)
+                    .addHeader("Content-Type", "application/json")
+                    .addHeader("Authorization", "Bearer " + accessToken)
+                    .build();
+            Response response = client.newCall(request).execute();
+            String resStr = response.body().string();
+            result = JSON.parseObject(resStr);
+            log.info("创建EIC报销单返回结果:" + result);
+        }catch (Exception e){
+            log.error("创建EIC报销单异常:" + e.getMessage());
+        }
+
+        return result;
+    }
+}

+ 40 - 0
src/main/java/com/muzhi/lemeng/common/time/BeijingToUtcConverter.java

@@ -0,0 +1,40 @@
+package com.muzhi.lemeng.common.time;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+public class BeijingToUtcConverter {
+
+    /**
+     * 将指定的北京时间日期(如 "2026-01-30")转换为对应的 UTC 起止时间(ISO 8601 + Z)
+     *
+     * @param beijingDateStr 格式:yyyy-MM-dd(表示北京时间的某一天)
+     * @return String[2] {startUtc, endUtc}
+     */
+    public static String[] convertBeijingDateToUtcRange(String beijingDateStr) {
+        // 1. 解析输入日期(视为北京时间的日期)
+        LocalDate date = LocalDate.parse(beijingDateStr);
+
+        // 2. 构造北京时间的 00:00:00 和 23:59:59
+        LocalDateTime startBeijing = date.atStartOfDay(); // 00:00:00
+        LocalDateTime endBeijing = date.atTime(23, 59, 59); // 23:59:59
+
+        // 3. 指定时区为北京
+        ZoneId beijingZone = ZoneId.of("Asia/Shanghai");
+
+        // 4. 转换为 UTC Instant
+        Instant startUtc = startBeijing.atZone(beijingZone).toInstant();
+        Instant endUtc = endBeijing.atZone(beijingZone).toInstant();
+
+        // 5. 格式化为 ISO 8601(带 Z)
+        DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
+
+        return new String[]{
+                formatter.format(startUtc),
+                formatter.format(endUtc)
+        };
+    }
+}

+ 122 - 0
src/main/java/com/muzhi/lemeng/common/utilityclass/PackageFlie.java

@@ -0,0 +1,122 @@
+package com.muzhi.lemeng.common.utilityclass;
+
+import com.github.junrar.Archive;
+import com.github.junrar.rarfile.FileHeader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class PackageFlie {
+
+    // === 从 ZIP 流中提取 PDF ===
+    public static String extractPdfFromZipStream(InputStream zipStream, Path targetDir) throws IOException {
+
+        String flieName = null;
+
+        Files.createDirectories(targetDir);
+        try (ZipInputStream zis = new ZipInputStream(zipStream)) {
+            ZipEntry entry;
+            while ((entry = zis.getNextEntry()) != null) {
+                String name = entry.getName().toLowerCase();
+                if (entry.isDirectory() || !name.endsWith(".pdf")) {
+                    zis.closeEntry();
+                    continue;
+                }
+
+                // 防路径穿越:只取文件名
+                String safeName = Paths.get(entry.getName()).getFileName().toString();
+                Path destPath = targetDir.resolve(safeName);
+
+                System.out.println("Found PDF: " + safeName);
+                flieName = safeName;
+
+                // 避免覆盖(可选)
+                if (Files.exists(destPath)) {
+                    System.out.println("Skip existing: " + safeName);
+                    zis.close();
+                    continue;
+                }
+
+                try (OutputStream os = Files.newOutputStream(destPath)) {
+                    byte[] buffer = new byte[8192];
+                    int len;
+                    while ((len = zis.read(buffer)) > 0) {
+                        os.write(buffer, 0, len);
+                    }
+                    System.out.println("✅ Extracted PDF: " + destPath);
+                }
+                zis.closeEntry();
+            }
+        }
+        return flieName;
+    }
+
+    // === 从 RAR 流中提取 PDF ===
+    public static String extractPdfFromRarStream(InputStream rarStream, Path targetDir) throws IOException {
+
+        String flieName = null;
+
+        Files.createDirectories(targetDir);
+
+        // junrar 需要 RandomAccessFile,但我们可以用 ByteArrayOutputStream 模拟
+        // 更简单方式:先写入临时文件(因 junrar 不支持纯 InputStream)
+        Path tempRar = Files.createTempFile("temp_", ".rar");
+        try {
+            Files.copy(rarStream, tempRar, StandardCopyOption.REPLACE_EXISTING);
+
+            try (Archive archive = new Archive(tempRar.toFile())) {
+                FileHeader fileHeader;
+                while ((fileHeader = archive.nextFileHeader()) != null) {
+                    String name = fileHeader.getFileNameString().toLowerCase();
+                    if (fileHeader.isDirectory() || !name.endsWith(".pdf")) {
+                        continue;
+                    }
+
+                    String safeName = Paths.get(fileHeader.getFileNameString()).getFileName().toString();
+                    Path destPath = targetDir.resolve(safeName);
+
+                    System.out.println("Found PDF: " + safeName);
+                    flieName = safeName;
+
+                    if (Files.exists(destPath)) {
+                        System.out.println("Skip existing: " + safeName);
+                        continue;
+                    }
+
+                    try (OutputStream os = Files.newOutputStream(destPath)) {
+                        archive.extractFile(fileHeader, os);
+                        System.out.println("✅ Extracted PDF: " + destPath);
+                    }
+                }
+            }catch (Exception e){
+                e.printStackTrace();
+            }
+        } finally {
+            // 清理临时文件
+            try {
+                Files.deleteIfExists(tempRar);
+            } catch (IOException e) {
+                System.err.println("Failed to delete temp RAR file: " + e.getMessage());
+            }
+        }
+        return flieName;
+    }
+
+    // === 从 Content-Disposition 或 URL 提取文件名 ===
+    public static String extractFileName(String contentDisposition, String url) {
+        if (contentDisposition != null && contentDisposition.contains("filename=")) {
+            String filename = contentDisposition.substring(contentDisposition.indexOf("filename=") + 9);
+            filename = filename.replaceAll("\"", "").trim();
+            return filename;
+        }
+        // 从 URL 最后一段取
+        return Paths.get(url).getFileName().toString();
+    }
+}

+ 48 - 0
src/main/java/com/muzhi/lemeng/controller/AccessTokenController.java

@@ -0,0 +1,48 @@
+package com.muzhi.lemeng.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @Author ZhangKan
+ * @Data 15:03
+ */
+
+@Api(description = "钉钉授权AccessToken")
+@RestController
+@RequestMapping("/dingservice")
+@CrossOrigin//解决跨域问题
+public class AccessTokenController {
+
+    @Autowired
+    private com.muzhi.lemeng.service.AccessTokenService AccessTokenService;
+
+    //获取钉钉中的access_token(钉钉应用)
+    @ApiOperation(value = "获取钉钉的access_token")
+    @GetMapping("getAccessToken")
+    public String getAccessToken(){
+
+        String accessToken = AccessTokenService.getAccessToken();
+        return accessToken;
+    }
+
+    //获取阿里企业邮箱的access_token
+    @ApiOperation(value = "获取阿里企业邮箱的access_token")
+    @GetMapping("getAlibabaMailboxAccessToken")
+    public String getAlibabaMailboxAccessToken(){
+
+        String mailboxAccessToken = AccessTokenService.getAlibabaMailboxAccessToken();
+        return mailboxAccessToken;
+    }
+
+    //获取EIC的access_token
+    @ApiOperation(value = "获取EIC的access_token")
+    @GetMapping("getEicAccessToken")
+    public String getEicAccessToken(){
+
+        String eicAccessToken = AccessTokenService.getEicAccessToken();
+        return eicAccessToken;
+    }
+}

+ 32 - 0
src/main/java/com/muzhi/lemeng/controller/AlibabaMailBoxController.java

@@ -0,0 +1,32 @@
+package com.muzhi.lemeng.controller;
+
+import com.muzhi.lemeng.common.R;
+import com.muzhi.lemeng.service.AlibabaMailBoxService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.web.bind.annotation.*;
+
+
+@Api(description = "钉钉")
+@RestController
+@RequestMapping("/aliyunMailBoxService")
+@CrossOrigin//解决跨域问题
+public class AlibabaMailBoxController {
+
+    @Autowired
+    private AlibabaMailBoxService alibabaMailBoxService;
+
+    /**搜索邮件*/
+    @ApiOperation(value = "搜索邮件")
+    @GetMapping("queryMessages")
+    @Scheduled(cron = "0 0 6 * * ?")//每天06:00触发(也就是早上06点00分触发)
+//    @Scheduled(cron = "0 30 21 * * ?")//每天22:30触发(也就是晚上21点30分触发)
+//    @Scheduled(cron = "0 0/30 8-20 * * ?")//每天8点~20点,每30分钟执行一次
+    public R queryMessages(){
+
+        R queryMessages = alibabaMailBoxService.queryMessages();
+        return queryMessages;
+    }
+}

+ 39 - 0
src/main/java/com/muzhi/lemeng/controller/DingTalkController.java

@@ -0,0 +1,39 @@
+package com.muzhi.lemeng.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.service.DingTalkService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import com.muzhi.lemeng.common.R;
+
+
+@Api(description = "钉钉")
+@RestController
+@RequestMapping("/dingservice")
+@CrossOrigin//解决跨域问题
+public class DingTalkController {
+
+    @Autowired
+    private DingTalkService dingTalkService;
+
+    /**提供EIC回写宜搭报销付款状态接口*/
+    @ApiOperation(value = "提供EIC回写宜搭报销付款状态接口")
+    @PostMapping("updateState")
+    public R updateState(@RequestBody JSONObject body){
+
+        R updateState = dingTalkService.updateState(body);
+        return updateState;
+    }
+
+    /**获取宜搭报销单信息推送创建EIC报销单*/
+    @ApiOperation(value = "获取宜搭报销单信息推送创建EIC报销单")
+    @PostMapping("createExpenseAccount")
+    public R createExpenseAccount(String shiliID){
+
+        R createExpenseAccount = dingTalkService.createExpenseAccount(shiliID);
+        return createExpenseAccount;
+    }
+}

+ 39 - 0
src/main/java/com/muzhi/lemeng/controller/InvoiceController.java

@@ -0,0 +1,39 @@
+package com.muzhi.lemeng.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.R;
+import com.muzhi.lemeng.service.InvoiceService;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+
+@Api(description = "钉钉")
+@RestController
+@RequestMapping("/invoiceService")
+@CrossOrigin//解决跨域问题
+public class InvoiceController {
+
+    @Autowired
+    private InvoiceService invoiceService;
+
+    /**通用票据识别(高级)提取发票信息*/
+    @ApiOperation(value = "通用票据识别(高级)提取发票信息")
+    @PostMapping("getInvoiceOcr")
+    public R getInvoiceOCR(@RequestBody JSONObject obj) throws TencentCloudSDKException{
+
+        R getInvoiceOcr = invoiceService.getInvoiceOCR(obj);
+        return getInvoiceOcr;
+    }
+
+    /**增值税发票核验(新版)获取发票验真结果*/
+    @ApiOperation(value = "增值税发票核验(新版)获取发票验真结果")
+    @PostMapping("doInvoiceVerification")
+    public JSONObject doInvoiceVerification(@RequestBody JSONObject obj) throws TencentCloudSDKException {
+
+        JSONObject doInvoiceVerification = invoiceService.doInvoiceVerification(obj);
+        return doInvoiceVerification;
+    }
+}

+ 40 - 0
src/main/java/com/muzhi/lemeng/entity/InvoiceItem.java

@@ -0,0 +1,40 @@
+package com.muzhi.lemeng.entity;
+
+public class InvoiceItem {
+    private String itemName;
+    private String quantity;
+    private String unitPrice;
+    private String amount;
+    private String taxRate;
+    private String taxAmount;
+    private String totalTaxIncluded;
+
+    // 构造方法
+    public InvoiceItem(String itemName, String quantity, String unitPrice,
+                       String amount, String taxRate, String taxAmount, String totalTaxIncluded) {
+        this.itemName = itemName;
+        this.quantity = quantity;
+        this.unitPrice = unitPrice;
+        this.amount = amount;
+        this.taxRate = taxRate;
+        this.taxAmount = taxAmount;
+        this.totalTaxIncluded = totalTaxIncluded;
+    }
+
+    // Getter 方法(用于输出)
+    public String getItemName() { return itemName; }
+    public String getQuantity() { return quantity; }
+    public String getUnitPrice() { return unitPrice; }
+    public String getAmount() { return amount; }
+    public String getTaxRate() { return taxRate; }
+    public String getTaxAmount() { return taxAmount; }
+    public String getTotalTaxIncluded() { return totalTaxIncluded; }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "商品: %s | 数量: %s | 单价: %s | 金额: %s | 税率: %s | 税额: %s | 价税合计: %s",
+                itemName, quantity, unitPrice, amount, taxRate, taxAmount, totalTaxIncluded
+        );
+    }
+}

+ 17 - 0
src/main/java/com/muzhi/lemeng/service/AccessTokenService.java

@@ -0,0 +1,17 @@
+package com.muzhi.lemeng.service;
+
+/**
+ * @Author ZhangKan
+ * @Data 15:34
+ */
+public interface AccessTokenService {
+
+    //获取access_token的方法(钉钉应用)
+    String getAccessToken();
+
+    //获取阿里企业邮箱的access_token
+    String getAlibabaMailboxAccessToken();
+
+    //获取EIC的access_token
+    String getEicAccessToken();
+}

+ 12 - 0
src/main/java/com/muzhi/lemeng/service/AliBtripCorpTokenService.java

@@ -0,0 +1,12 @@
+package com.muzhi.lemeng.service;
+
+/**
+ * 阿里商旅企业访问凭证服务。
+ */
+public interface AliBtripCorpTokenService {
+
+    /**
+     * 获取企业访问凭证,凭证有效期内复用缓存。
+     */
+    String getCorpToken();
+}

+ 17 - 0
src/main/java/com/muzhi/lemeng/service/AliBtripTrainTicketService.java

@@ -0,0 +1,17 @@
+package com.muzhi.lemeng.service;
+
+import com.alibaba.fastjson.JSONArray;
+
+/**
+ * 阿里商旅火车票凭证明细服务。
+ */
+public interface AliBtripTrainTicketService {
+
+    /**
+     * 按发票开具时间查询全部火车票凭证明细。
+     *
+     * @param invoiceDateStart 开始时间,格式 yyyy-MM-dd HH:mm:ss
+     * @param invoiceDateEnd   结束时间,格式 yyyy-MM-dd HH:mm:ss
+     */
+    JSONArray queryByInvoiceDate(String invoiceDateStart, String invoiceDateEnd);
+}

+ 16 - 0
src/main/java/com/muzhi/lemeng/service/AliBtripVatInvoiceService.java

@@ -0,0 +1,16 @@
+package com.muzhi.lemeng.service;
+
+import com.alibaba.fastjson.JSONArray;
+
+/**
+ * 阿里商旅增值税发票明细服务。
+ */
+public interface AliBtripVatInvoiceService {
+
+    /**
+     * 按出账日查询全部增值税发票明细。
+     *
+     * @param billDate 出账日,格式 yyyy-MM-dd
+     */
+    JSONArray queryByBillDate(String billDate);
+}

+ 13 - 0
src/main/java/com/muzhi/lemeng/service/AlibabaMailBoxService.java

@@ -0,0 +1,13 @@
+package com.muzhi.lemeng.service;
+
+import com.muzhi.lemeng.common.R;
+
+/**
+ * @Author ZhangKan
+ * @Data 15:34
+ */
+public interface AlibabaMailBoxService {
+
+    /**搜索邮件*/
+    R queryMessages();
+}

+ 17 - 0
src/main/java/com/muzhi/lemeng/service/DingTalkService.java

@@ -0,0 +1,17 @@
+package com.muzhi.lemeng.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.R;
+
+/**
+ * @Author ZhangKan
+ * @Data 15:34
+ */
+public interface DingTalkService {
+
+    /**提供EIC回写宜搭报销付款状态接口*/
+    R updateState(JSONObject body);
+
+    /**获取宜搭报销单信息推送创建EIC报销单*/
+    R createExpenseAccount(String shiliID);
+}

+ 19 - 0
src/main/java/com/muzhi/lemeng/service/InvoiceService.java

@@ -0,0 +1,19 @@
+package com.muzhi.lemeng.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.R;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import org.springframework.web.bind.annotation.RequestBody;
+
+/**
+ * @Author ZhangKan
+ * @Data 15:34
+ */
+public interface InvoiceService {
+
+    /**通用票据识别(高级)提取发票信息*/
+    R getInvoiceOCR(JSONObject obj) throws TencentCloudSDKException;
+
+    /**增值税发票核验(新版)获取发票验真结果*/
+    JSONObject doInvoiceVerification(JSONObject obj) throws TencentCloudSDKException;
+}

+ 121 - 0
src/main/java/com/muzhi/lemeng/service/impl/AccessTokenServiceImpl.java

@@ -0,0 +1,121 @@
+package com.muzhi.lemeng.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.dingtalk.api.DefaultDingTalkClient;
+import com.dingtalk.api.request.OapiGettokenRequest;
+import com.dingtalk.api.response.OapiGettokenResponse;
+import com.muzhi.lemeng.service.AccessTokenService;
+import com.taobao.api.ApiException;
+import okhttp3.*;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @Author ZhangKan
+ * @Data 15:39
+ */
+
+@Service
+public class AccessTokenServiceImpl implements AccessTokenService {
+
+    /**钉钉应用*/
+    //APPKEY
+    private static String APPKEY = "dingzupxqpzcczaaddcw";
+    //APPSECRET
+    private static String APPSECRET = "EpND2Oj4adk2Od9ZwAszSYGC8WL_OWYPq1Uj7b5jR1DI6rCsWTdxzvqTHM4H3Dz6";
+    //AgenthId:
+    //corpId:
+
+    public static String DingtalkAccessToken = null;
+
+    //获取dingtalk授权码(钉钉应用)
+    @Override
+    public String getAccessToken() {
+        DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
+        OapiGettokenRequest request = new OapiGettokenRequest();
+        request.setAppkey(APPKEY);
+        request.setAppsecret(APPSECRET);
+        request.setHttpMethod("GET");
+        try {
+            OapiGettokenResponse response = client.execute(request);
+            DingtalkAccessToken = response.getAccessToken();
+            System.out.println("乐檬钉钉AccessToken:" + DingtalkAccessToken);
+        } catch (ApiException e) {
+            e.printStackTrace();
+        }
+        return DingtalkAccessToken;
+    }
+
+    //获取阿里企业邮箱的access_token
+    @Override
+    public String getAlibabaMailboxAccessToken() {
+
+        String accessToken = null;
+
+        String grant_type = "client_credentials";//固定值
+        String client_id = "tlHiqXxwX6jVFEKs";//应用id
+        String client_secret = "BI9O3IvEBJVYuKC55hEuxkwkpHTdheh8PUSipwBreHkEWq0xDc0rOUQ9pM9lktuE";//应用密钥
+        String bodyStr = "grant_type="+ grant_type +"&client_id="+ client_id +"&client_secret=" + client_secret;
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
+            RequestBody body = RequestBody.create(mediaType, bodyStr);
+            Request request = new Request.Builder()
+                    .url("https://alimail-cn.aliyuncs.com/oauth2/v2.0/token")
+                    .method("POST", body)
+                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
+                    .build();
+            Response response = client.newCall(request).execute();
+            String resStr = response.body().string();
+            JSONObject jsonObject = JSON.parseObject(resStr);
+            accessToken = jsonObject.getString("access_token");
+            System.out.println("阿里企业邮箱的access_token:" + accessToken);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+        return accessToken;
+    }
+
+    //获取EIC的access_token
+    @Override
+    public String getEicAccessToken() {
+
+        String accessToken = null;
+
+        String grant_type = "client_credential";//固定值
+        String appkey = "20180005";//应用id
+        String appsecret = "41846b2a-915d-4079-ae1d-5a96ac5c678f";//应用密钥
+
+        Map map = new HashMap();
+        map.put("grant_type", grant_type);
+        map.put("appkey", appkey);
+        map.put("appsecret", appsecret);
+        String bodyStr = JSON.toJSONString(map);
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("application/json");
+            RequestBody body = RequestBody.create(mediaType, bodyStr);
+            Request request = new Request.Builder()
+                    .url("http://47.111.239.150:8088/wbjk/auth2/getToken")
+                    .method("POST", body)
+                    .addHeader("Content-Type", "application/json")
+                    .build();
+            Response response = client.newCall(request).execute();
+            String resStr = response.body().string();
+            JSONObject jsonObject = JSON.parseObject(resStr);
+            accessToken = jsonObject.getString("token");
+            System.out.println("EIC的access_token:" + accessToken);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+        return accessToken;
+    }
+}

+ 110 - 0
src/main/java/com/muzhi/lemeng/service/impl/AliBtripCorpTokenServiceImpl.java

@@ -0,0 +1,110 @@
+package com.muzhi.lemeng.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.service.AliBtripCorpTokenService;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+@Service
+public class AliBtripCorpTokenServiceImpl implements AliBtripCorpTokenService {
+
+    private static final long REFRESH_ADVANCE_MILLIS = TimeUnit.MINUTES.toMillis(5);
+
+    private final OkHttpClient httpClient = new OkHttpClient();
+    private final Object tokenLock = new Object();
+
+    @Value("${myapp.alibtrip.baseUrl:https://btripopen.alibtrip.com/api}")
+    private String baseUrl;
+
+    @Value("${myapp.alibtrip.appKey:}")
+    private String appKey;
+
+    @Value("${myapp.alibtrip.appSecret:}")
+    private String appSecret;
+
+    @Value("${myapp.alibtrip.corpId:}")
+    private String corpId;
+
+    private volatile String cachedToken;
+    private volatile long refreshAtMillis;
+
+    @Override
+    public String getCorpToken() {
+        long now = System.currentTimeMillis();
+        if (cachedToken != null && now < refreshAtMillis) {
+            return cachedToken;
+        }
+        synchronized (tokenLock) {
+            now = System.currentTimeMillis();
+            if (cachedToken != null && now < refreshAtMillis) {
+                return cachedToken;
+            }
+            return refreshToken(now);
+        }
+    }
+
+    private String refreshToken(long now) {
+        requireConfigured("myapp.alibtrip.appKey", appKey);
+        requireConfigured("myapp.alibtrip.appSecret", appSecret);
+        requireConfigured("myapp.alibtrip.corpId", corpId);
+
+        HttpUrl url = requireBaseUrl().newBuilder()
+                .addPathSegments("btrip-open-auth/v1/corp-token/action/take")
+                .addQueryParameter("app_key", appKey)
+                .addQueryParameter("app_secret", appSecret)
+                .addQueryParameter("corp_id", corpId)
+                .addQueryParameter("type", "0")
+                .build();
+        Request request = new Request.Builder().url(url).get().build();
+
+        try (Response response = httpClient.newCall(request).execute()) {
+            String responseBody = response.body() == null ? "" : response.body().string();
+            if (!response.isSuccessful()) {
+                throw new IllegalStateException("阿里商旅企业访问凭证请求失败,HTTP " + response.code());
+            }
+            JSONObject result = JSON.parseObject(responseBody);
+            if (!result.getBooleanValue("success")) {
+                throw new IllegalStateException("阿里商旅企业访问凭证请求失败:"
+                        + result.getString("code") + " - " + result.getString("message"));
+            }
+            JSONObject module = result.getJSONObject("module");
+            String token = module == null ? null : module.getString("token");
+            if (isBlank(token)) {
+                throw new IllegalStateException("阿里商旅企业访问凭证响应缺少 module.token");
+            }
+            long expireMillis = module.getLongValue("expire");
+            long cacheMillis = Math.max(0L, expireMillis - REFRESH_ADVANCE_MILLIS);
+            cachedToken = token;
+            refreshAtMillis = now + cacheMillis;
+            return token;
+        } catch (IOException e) {
+            throw new IllegalStateException("阿里商旅企业访问凭证请求异常", e);
+        }
+    }
+
+    private HttpUrl requireBaseUrl() {
+        HttpUrl url = HttpUrl.parse(baseUrl);
+        if (url == null) {
+            throw new IllegalStateException("myapp.alibtrip.baseUrl 配置不合法");
+        }
+        return url;
+    }
+
+    private static void requireConfigured(String name, String value) {
+        if (isBlank(value)) {
+            throw new IllegalStateException(name + " 未配置");
+        }
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}

+ 142 - 0
src/main/java/com/muzhi/lemeng/service/impl/AliBtripTrainTicketServiceImpl.java

@@ -0,0 +1,142 @@
+package com.muzhi.lemeng.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.service.AliBtripCorpTokenService;
+import com.muzhi.lemeng.service.AliBtripTrainTicketService;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+@Service
+public class AliBtripTrainTicketServiceImpl implements AliBtripTrainTicketService {
+
+    private static final int PAGE_SIZE = 100;
+    private static final int MAX_PAGE_COUNT = 10000;
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+    private final OkHttpClient httpClient = new OkHttpClient();
+
+    @Autowired
+    private AliBtripCorpTokenService corpTokenService;
+
+    @Value("${myapp.alibtrip.baseUrl:https://btripopen.alibtrip.com/api}")
+    private String baseUrl;
+
+    @Value("${myapp.alibtrip.appKey:}")
+    private String appKey;
+
+    @Override
+    public JSONArray queryByInvoiceDate(String invoiceDateStart, String invoiceDateEnd) {
+        validateDateRange(invoiceDateStart, invoiceDateEnd);
+
+        JSONArray allItems = new JSONArray();
+        int pageNo = 1;
+        int totalPage;
+        int totalSize = -1;
+        do {
+            JSONObject module = queryPage(invoiceDateStart, invoiceDateEnd, pageNo);
+            JSONArray items = module.getJSONArray("items");
+            if (items != null) {
+                allItems.addAll(items);
+            }
+            totalPage = module.getIntValue("total_page");
+            totalSize = module.getIntValue("total_size");
+            validatePage(apiName(), pageNo, totalPage, items);
+            pageNo++;
+        } while (pageNo <= totalPage);
+
+        if (totalSize >= 0 && allItems.size() != totalSize) {
+            throw new IllegalStateException("阿里商旅" + apiName() + "分页数量不一致,预期 "
+                    + totalSize + " 条,实际 " + allItems.size() + " 条");
+        }
+        return allItems;
+    }
+
+    private JSONObject queryPage(String invoiceDateStart, String invoiceDateEnd, int pageNo) {
+        HttpUrl url = buildBaseUrl("scan/v1/train-ticket").newBuilder()
+                .addQueryParameter("app_key", requireAppKey())
+                .addQueryParameter("corp_token", corpTokenService.getCorpToken())
+                .addQueryParameter("invoice_date_start", invoiceDateStart)
+                .addQueryParameter("invoice_date_end", invoiceDateEnd)
+                .addQueryParameter("page_size", String.valueOf(PAGE_SIZE))
+                .addQueryParameter("page_no", String.valueOf(pageNo))
+                .build();
+        return executePageRequest(url);
+    }
+
+    private JSONObject executePageRequest(HttpUrl url) {
+        Request request = new Request.Builder().url(url).get().build();
+        try (Response response = httpClient.newCall(request).execute()) {
+            String responseBody = response.body() == null ? "" : response.body().string();
+            if (!response.isSuccessful()) {
+                throw new IllegalStateException("阿里商旅" + apiName() + "请求失败,HTTP " + response.code());
+            }
+            JSONObject result = JSON.parseObject(responseBody);
+            if (!result.getBooleanValue("success")) {
+                throw new IllegalStateException("阿里商旅" + apiName() + "请求失败:"
+                        + result.getString("code") + " - " + result.getString("message"));
+            }
+            JSONObject module = result.getJSONObject("module");
+            if (module == null) {
+                throw new IllegalStateException("阿里商旅" + apiName() + "响应缺少 module");
+            }
+            return module;
+        } catch (IOException e) {
+            throw new IllegalStateException("阿里商旅" + apiName() + "请求异常", e);
+        }
+    }
+
+    private HttpUrl buildBaseUrl(String path) {
+        HttpUrl url = HttpUrl.parse(baseUrl);
+        if (url == null) {
+            throw new IllegalStateException("myapp.alibtrip.baseUrl 配置不合法");
+        }
+        return url.newBuilder().addPathSegments(path).build();
+    }
+
+    private String requireAppKey() {
+        if (appKey == null || appKey.trim().isEmpty()) {
+            throw new IllegalStateException("myapp.alibtrip.appKey 未配置");
+        }
+        return appKey;
+    }
+
+    private static void validatePage(String apiName, int pageNo, int totalPage, JSONArray items) {
+        if (totalPage < 0 || totalPage > MAX_PAGE_COUNT) {
+            throw new IllegalStateException("阿里商旅" + apiName + "返回非法总页数:" + totalPage);
+        }
+        if (pageNo < totalPage && (items == null || items.isEmpty())) {
+            throw new IllegalStateException("阿里商旅" + apiName + "第 " + pageNo + " 页为空,已停止以避免漏数");
+        }
+    }
+
+    private static void validateDateRange(String startValue, String endValue) {
+        try {
+            LocalDateTime start = LocalDateTime.parse(startValue, DATE_TIME_FORMATTER);
+            LocalDateTime end = LocalDateTime.parse(endValue, DATE_TIME_FORMATTER);
+            if (end.isBefore(start)) {
+                throw new IllegalArgumentException("invoice_date_end 不能早于 invoice_date_start");
+            }
+            if (end.isAfter(start.plusDays(7))) {
+                throw new IllegalArgumentException("发票开具时间范围不能超过 7 天");
+            }
+        } catch (DateTimeParseException e) {
+            throw new IllegalArgumentException("发票开具时间格式必须为 yyyy-MM-dd HH:mm:ss", e);
+        }
+    }
+
+    private static String apiName() {
+        return "火车票凭证明细";
+    }
+}

+ 126 - 0
src/main/java/com/muzhi/lemeng/service/impl/AliBtripVatInvoiceServiceImpl.java

@@ -0,0 +1,126 @@
+package com.muzhi.lemeng.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.service.AliBtripCorpTokenService;
+import com.muzhi.lemeng.service.AliBtripVatInvoiceService;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+
+@Service
+public class AliBtripVatInvoiceServiceImpl implements AliBtripVatInvoiceService {
+
+    private static final int PAGE_SIZE = 100;
+    private static final int MAX_PAGE_COUNT = 10000;
+    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    private final OkHttpClient httpClient = new OkHttpClient();
+
+    @Autowired
+    private AliBtripCorpTokenService corpTokenService;
+
+    @Value("${myapp.alibtrip.baseUrl:https://btripopen.alibtrip.com/api}")
+    private String baseUrl;
+
+    @Value("${myapp.alibtrip.appKey:}")
+    private String appKey;
+
+    @Override
+    public JSONArray queryByBillDate(String billDate) {
+        validateBillDate(billDate);
+
+        JSONArray allItems = new JSONArray();
+        int pageNo = 1;
+        int totalPage;
+        int totalSize = -1;
+        do {
+            JSONObject module = queryPage(billDate, pageNo);
+            JSONArray items = module.getJSONArray("items");
+            if (items != null) {
+                allItems.addAll(items);
+            }
+            totalPage = module.getIntValue("total_page");
+            totalSize = module.getIntValue("total_size");
+            validatePage(pageNo, totalPage, items);
+            pageNo++;
+        } while (pageNo <= totalPage);
+
+        if (totalSize >= 0 && allItems.size() != totalSize) {
+            throw new IllegalStateException("阿里商旅增值税发票明细分页数量不一致,预期 "
+                    + totalSize + " 条,实际 " + allItems.size() + " 条");
+        }
+        return allItems;
+    }
+
+    private JSONObject queryPage(String billDate, int pageNo) {
+        HttpUrl url = buildBaseUrl("scan/v1/vat-invoice").newBuilder()
+                .addQueryParameter("app_key", requireAppKey())
+                .addQueryParameter("corp_token", corpTokenService.getCorpToken())
+                .addQueryParameter("bill_date", billDate)
+                .addQueryParameter("page_size", String.valueOf(PAGE_SIZE))
+                .addQueryParameter("page_no", String.valueOf(pageNo))
+                .build();
+        Request request = new Request.Builder().url(url).get().build();
+        try (Response response = httpClient.newCall(request).execute()) {
+            String responseBody = response.body() == null ? "" : response.body().string();
+            if (!response.isSuccessful()) {
+                throw new IllegalStateException("阿里商旅增值税发票明细请求失败,HTTP " + response.code());
+            }
+            JSONObject result = JSON.parseObject(responseBody);
+            if (!result.getBooleanValue("success")) {
+                throw new IllegalStateException("阿里商旅增值税发票明细请求失败:"
+                        + result.getString("code") + " - " + result.getString("message"));
+            }
+            JSONObject module = result.getJSONObject("module");
+            if (module == null) {
+                throw new IllegalStateException("阿里商旅增值税发票明细响应缺少 module");
+            }
+            return module;
+        } catch (IOException e) {
+            throw new IllegalStateException("阿里商旅增值税发票明细请求异常", e);
+        }
+    }
+
+    private HttpUrl buildBaseUrl(String path) {
+        HttpUrl url = HttpUrl.parse(baseUrl);
+        if (url == null) {
+            throw new IllegalStateException("myapp.alibtrip.baseUrl 配置不合法");
+        }
+        return url.newBuilder().addPathSegments(path).build();
+    }
+
+    private String requireAppKey() {
+        if (appKey == null || appKey.trim().isEmpty()) {
+            throw new IllegalStateException("myapp.alibtrip.appKey 未配置");
+        }
+        return appKey;
+    }
+
+    private static void validatePage(int pageNo, int totalPage, JSONArray items) {
+        if (totalPage < 0 || totalPage > MAX_PAGE_COUNT) {
+            throw new IllegalStateException("阿里商旅增值税发票明细返回非法总页数:" + totalPage);
+        }
+        if (pageNo < totalPage && (items == null || items.isEmpty())) {
+            throw new IllegalStateException("阿里商旅增值税发票明细第 " + pageNo + " 页为空,已停止以避免漏数");
+        }
+    }
+
+    private static void validateBillDate(String billDate) {
+        try {
+            LocalDate.parse(billDate, DATE_FORMATTER);
+        } catch (DateTimeParseException e) {
+            throw new IllegalArgumentException("出账日格式必须为 yyyy-MM-dd", e);
+        }
+    }
+}

+ 682 - 0
src/main/java/com/muzhi/lemeng/service/impl/AlibabaMailBoxServiceImpl.java

@@ -0,0 +1,682 @@
+package com.muzhi.lemeng.service.impl;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.R;
+import com.muzhi.lemeng.common.alibaba.AlibabaMailBox;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormData;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormDataV2;
+import com.muzhi.lemeng.common.tencent.TencentInvoice;
+import com.muzhi.lemeng.common.time.BeijingToUtcConverter;
+import com.muzhi.lemeng.service.AccessTokenService;
+import com.muzhi.lemeng.service.AliBtripTrainTicketService;
+import com.muzhi.lemeng.service.AliBtripVatInvoiceService;
+import com.muzhi.lemeng.service.AlibabaMailBoxService;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+
+@Service
+@Component
+@Slf4j
+public class AlibabaMailBoxServiceImpl implements AlibabaMailBoxService {
+
+    @Autowired
+    private AccessTokenService accessTokenService;
+
+    @Autowired
+    private AliBtripTrainTicketService aliBtripTrainTicketService;
+
+    @Autowired
+    private AliBtripVatInvoiceService aliBtripVatInvoiceService;
+
+    /**
+     * 搜索邮件,并解析发票信息
+     * NOTE 邮件标题匹配各类发票邮件标识:【发票、电子发票、电子票】
+     */
+    @Override
+    public R queryMessages(){
+
+        //TODO 指定代收邮箱地址
+        String emailAddress = "mailassistant@cnlemon.net";
+
+
+        /*搜索邮件*/
+        log.info("开始循环搜索邮件--->");
+        //邮件信息返回字段筛选
+        String select = "toRecipients";//邮件的接收人(可选字段:在URL中增加$select=toRecipients查询参数后返回,如需多个字段使用英文逗号分隔)
+        // 1. 获取当前日期
+//        LocalDate today = LocalDate.now();
+        LocalDate yesterday = LocalDate.now().minusDays(1);//获取昨天的日期
+        // 2. 定义格式化器 (yyyy-MM-dd)
+        DateTimeFormatter formatterInput = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+        // 3. 格式化日期
+        String inputDate = yesterday.format(formatterInput);
+        System.out.println("获取邮箱邮件的日期: " + inputDate);
+
+        //查询时段
+//        inputDate = "2026-08-21"; //TODO 【测试】指定日期,测试完毕需要注释
+
+        Set<String> aliBtripInvoiceNumbers = queryAliBtripInvoiceNumbers(inputDate);
+
+        String[] range = BeijingToUtcConverter.convertBeijingDateToUtcRange(inputDate);
+        System.out.println("北京时间 " + inputDate + " 00:00:00 对应 UTC: " + range[0]);
+        System.out.println("北京时间 " + inputDate + " 23:59:59 对应 UTC: " + range[1]);
+        String startTime = range[0];//邮件发送时间
+        String endTime = range[1];//邮件发送时间
+
+        //TODO 【测试】指定日期,测试完毕需要注释
+//        startTime = "2026-04-21T16:00:00Z";
+//        endTime = "2026-08-05T15:59:59Z";
+
+        //构造查询条件
+        String folderId = "2";//邮件文件夹ID(2:指收件箱)
+        String query = "date>"+ startTime +" AND date<"+ endTime +" AND folderId:"+ folderId + " AND (subject:发票 OR subject:电子发票 OR subject:电子票)";//查询语句
+        //分页参数
+        String cursor = "0";//游标,邮件的起始偏移标识, 其值取自响应中的nextCursor字段。“”表示从头开始
+        int size = 100;//返回邮件数量,最大100
+
+        String nextCursor = "";
+        JSONArray messages = null;
+        do {
+            log.info("第{}次循环搜索邮件==========", cursor);
+            Map bodyMap = new HashMap();
+            bodyMap.put("query", query);
+            bodyMap.put("cursor", cursor);
+            bodyMap.put("size", size);
+            String bodyStr = JSON.toJSONString(bodyMap);
+            System.out.println("请求体body:" + bodyStr);
+            try {
+                OkHttpClient client = new OkHttpClient().newBuilder()
+                        .build();
+                MediaType mediaType = MediaType.parse("application/json");
+                RequestBody body = RequestBody.create(mediaType, bodyStr);
+                Request request = new Request.Builder()
+                        .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/query?$select="+ select)
+                        .method("POST", body)
+                        .addHeader("Content-Type", "application/json")
+                        .addHeader("Authorization", "Bearer " + accessTokenService.getAlibabaMailboxAccessToken())
+                        .build();
+                Response response = client.newCall(request).execute();
+                String resStr = response.body().string();
+                JSONObject jsonObject = JSON.parseObject(resStr);
+                System.out.println("返回体resStr对象:" + jsonObject);
+                //获取邮件总数
+                int total = jsonObject.getInteger("total");
+                log.info("当前搜索到邮件总数total:" + total);
+                //下一页光标 $表示已经到达邮件列表尾部
+                nextCursor = jsonObject.getString("nextCursor");
+                //邮件列表
+                messages = jsonObject.getJSONArray("messages");
+            }catch (Exception e){
+                log.error("搜索邮件异常:{}", e);
+            }
+            if (messages.size() > 0){
+                for (int i = 0; i < messages.size(); i++) {
+                    //邮件收件人数组信息
+                    JSONArray toRecipients = messages.getJSONObject(i).getJSONArray("toRecipients");
+                    JSONObject jsonObject1 = toRecipients.getJSONObject(0);
+                    String email = jsonObject1.getString("email");
+                    System.out.println("收件人email:" + email);
+                    String name = jsonObject1.getString("name");
+                    System.out.println("收件人name:" + name);
+                    //邮件id
+                    String emailId = messages.getJSONObject(i).getString("id");
+                    System.out.println("邮件id:" + emailId);
+                    /*根据邮箱地址获取收件人userId*/
+                    //查询表单实例数据
+                    Map mapAll = new HashMap();
+                    //邮箱地址
+                    mapAll.put("textField_mob3qfjb", email);
+                    String mapAllStr = JSON.toJSONString(mapAll);
+                    System.out.println("封装查询的字段参数:" + mapAllStr);
+                    JSONObject queryYidaForm = YiDaFormDataV2.getQueryYidaForm("FORM-E489F03D805D4E169BA6A8755B4E72E6TN6N", mapAllStr, accessTokenService.getAccessToken());
+                    JSONArray data = queryYidaForm.getJSONArray("data");
+                    String emailUserId = null;
+                    if (data.isEmpty()) {
+                        System.out.println("根据邮箱地址查询【用户信息】返回数据为空!");
+                    } else {
+                        JSONObject formData = data.getJSONObject(0).getJSONObject("formData");
+                        //成员userId
+                        emailUserId = formData.getJSONArray("employeeField_mm1g5703_id").getString(0);
+                        System.out.println("成员userId===>" + emailUserId);
+                    }
+
+                    /*根据邮件id获取邮件中的附件列表*/
+                    log.info("开始获取邮件中的附件列表--->");
+                    JSONArray fileArray = AlibabaMailBox.MailAttachments(emailAddress, emailId, accessTokenService.getAlibabaMailboxAccessToken());
+                    if (fileArray.size() > 0){
+                        for (int j = 0; j < fileArray.size(); j++) {
+                            JSONObject attachment = fileArray.getJSONObject(j);
+                            float fileSize = attachment.getFloat("size");
+                            System.out.println("文件大小: " + fileSize);
+                            String fileName = attachment.getString("name");
+                            System.out.println("文件名称: " + fileName);
+                            String fileId = attachment.getString("id");
+                            System.out.println("文件id: " + fileId);
+
+                            // 判断是否为 PDF(忽略大小写)
+                            boolean isPdf = fileName != null && fileName.toLowerCase().endsWith(".pdf");
+                            // 判断是否包含指定内容的文件
+                            boolean hasInvoiceKeyword = fileName.contains("行程");
+                            // 或者用 contains(但 endWith 更准确)
+                            // boolean isPdf = fileName != null && fileName.toLowerCase().contains(".pdf");
+                            System.out.println("文件名: " + fileName + " → 是PDF? " + isPdf + " → 文件名包含[行程]? " + hasInvoiceKeyword);
+                            //NOTE 判断邮件中的附件是否为pdf
+                            if (isPdf && !hasInvoiceKeyword){
+                                log.info("该附件是pdf,开始处理文件,文件名:{},文件id:{}", fileName, fileId);
+                                /*根据文件id获取文件流进行下载转存文件*/
+                                Map mapEnd = AlibabaMailBox.downLoadMailFile("pdf", emailAddress, emailId, fileId, fileName, accessTokenService.getAlibabaMailboxAccessToken());
+                                System.out.println("mapEnd===>" + mapEnd);
+                                String fileNewUrl = (String) mapEnd.get("fileNewUrl");
+                                System.out.println("服务器访问全地址:" + fileNewUrl);
+                                if (fileNewUrl != null){
+                                    log.info("文件转存地址不为空,开始进行发票识别===");
+                                    /*调用发票识别*/
+                                    try {
+                                        JSONObject jsonObject = TencentInvoice.doRecognizeGeneralInvoice(fileNewUrl);
+                                        log.info("发票识别结果:" + jsonObject);
+                                        JSONArray jsonArray = jsonObject.getJSONArray("MixedInvoiceItems");
+                                        if (jsonArray != null && jsonArray.size() > 0) {
+                                            log.info("发票识别成功!");
+                                            JSONObject jsonObject11 = jsonArray.getJSONObject(0);
+                                            int Type = jsonObject11.getInteger("Type");
+                                            System.out.println("发票类型编号:" + Type);//示例:16
+                                            String SubType = jsonObject11.getString("SubType");
+                                            System.out.println("发票类型:" + SubType);//示例:VatElectronicInvoiceFull
+                                            String SubTypeDescription = jsonObject11.getString("SubTypeDescription");
+                                            System.out.println("-->发票类型描述:" + SubTypeDescription);//示例:电子发票(普通发票)
+                                            String TypeDescription = jsonObject11.getString("TypeDescription");
+                                            System.out.println("-->票种类型描述:" + TypeDescription);//示例:全电发票
+                                            //发票号码
+                                            String invoiceNumber = null;
+                                            String invoiceDateStr = null;
+                                            //开票日期
+                                            long invoiceDate = 0;
+                                            //发票代码
+                                            String invoiceCode = null;
+                                            //校验码
+                                            String checkCode = null;
+                                            //购买方名称
+                                            String buyerName = null;
+                                            //购买方纳税人识别号
+                                            String buyerTaxpayerId = null;
+                                            //销售方名称
+                                            String sellerName = null;
+                                            //销售方纳税人识别号
+                                            String sellerTaxpayerId = null;
+                                            //含税总额
+                                            float totalAmount = 0;
+                                            //税额
+                                            float taxAmount = 0;
+                                            //不含税总额
+                                            float amountWithoutTax = 0;
+                                            //发票备注
+                                            String invoiceRemark = null;
+                                            //税率
+                                            float taxRate = 0;
+                                            if (SubType.equals("VatElectronicInvoiceFull") || SubType.equals("VatElectronicSpecialInvoiceFull")){//NOTE 电子发票(普通发票)、电子发票(专用发票)
+                                                JSONObject singleInvoiceInfos = jsonObject11.getJSONObject("SingleInvoiceInfos");
+                                                JSONObject VatElectronicInvoiceFull = singleInvoiceInfos.getJSONObject(SubType);
+
+                                                if (SubType.equals("VatElectronicSpecialInvoiceFull")){
+                                                    taxRate = 6;
+                                                }
+                                                System.out.println("发票税率:" + taxRate);
+                                                //发票号码
+                                                invoiceNumber = VatElectronicInvoiceFull.getString("Number");
+                                                System.out.println("发票号码:" + invoiceNumber);
+                                                //开票日期
+                                                invoiceDateStr = VatElectronicInvoiceFull.getString("Date");
+                                                System.out.println("开票日期:" + invoiceDateStr);
+                                                // 定义格式化器
+                                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+                                                // 解析字符串为 LocalDate
+                                                LocalDate localDate = LocalDate.parse(invoiceDateStr, formatter);
+                                                // 转换为毫秒级时间戳(基于系统默认时区)
+                                                invoiceDate = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+                                                System.out.println("开票日期时间戳: " + invoiceDate);
+                                                //发票代码(该类型发票不存在该字段)
+                                                //校验码(该类型发票不存在该字段)
+                                                //购买方名称
+                                                buyerName = VatElectronicInvoiceFull.getString("Buyer");
+                                                System.out.println("购买方名称:" + buyerName);
+                                                //购买方纳税人识别号
+                                                buyerTaxpayerId = VatElectronicInvoiceFull.getString("BuyerTaxID");
+                                                System.out.println("购买方纳税人识别号:" + buyerTaxpayerId);
+                                                //销售方名称
+                                                sellerName = VatElectronicInvoiceFull.getString("Seller");
+                                                System.out.println("销售方名称:" + sellerName);
+                                                //销售方纳税人识别号
+                                                sellerTaxpayerId = VatElectronicInvoiceFull.getString("SellerTaxID");
+                                                System.out.println("销售方纳税人识别号:" + sellerTaxpayerId);
+                                                //含税总额(价税合计)
+                                                totalAmount = VatElectronicInvoiceFull.getFloat("Total");
+                                                System.out.println("含税总额(价税合计):" + totalAmount);
+                                                //税额
+                                                taxAmount = VatElectronicInvoiceFull.getFloat("Tax");
+                                                System.out.println("税额:" + taxAmount);
+                                                //不含税总额(税前金额)
+                                                amountWithoutTax = VatElectronicInvoiceFull.getFloat("PretaxAmount");
+                                                System.out.println("不含税总额(税前金额):" + amountWithoutTax);
+                                                //发票备注
+                                                invoiceRemark = VatElectronicInvoiceFull.getString("Remark");
+                                                System.out.println("发票备注:" + invoiceRemark);
+                                            }
+                                            if (SubType.equals("ElectronicTrainTicketFull")){//NOTE 电子发票(火车票)
+                                                JSONObject singleInvoiceInfos = jsonObject11.getJSONObject("SingleInvoiceInfos");
+                                                JSONObject VatElectronicInvoiceFull = singleInvoiceInfos.getJSONObject(SubType);
+
+                                                //税率
+                                                taxRate = 9;
+                                                System.out.println("发票税率:" + taxRate);
+                                                //发票号码
+                                                invoiceNumber = VatElectronicInvoiceFull.getString("Number");
+                                                System.out.println("发票号码:" + invoiceNumber);
+                                                //开票日期
+                                                invoiceDateStr = VatElectronicInvoiceFull.getString("Date");
+                                                System.out.println("开票日期:" + invoiceDateStr);
+                                                // 定义格式化器
+                                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+                                                // 解析字符串为 LocalDate
+                                                LocalDate localDate = LocalDate.parse(invoiceDateStr, formatter);
+                                                // 转换为毫秒级时间戳(基于系统默认时区)
+                                                invoiceDate = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+                                                System.out.println("开票日期时间戳: " + invoiceDate);
+                                                //发票代码(该类型发票不存在该字段)
+                                                //校验码(该类型发票不存在该字段)
+                                                //购买方名称
+                                                buyerName = VatElectronicInvoiceFull.getString("Buyer");
+                                                System.out.println("购买方名称:" + buyerName);
+                                                //购买方纳税人识别号
+                                                buyerTaxpayerId = VatElectronicInvoiceFull.getString("BuyerTaxID");
+                                                System.out.println("购买方纳税人识别号:" + buyerTaxpayerId);
+                                                //销售方名称
+                                                sellerName = VatElectronicInvoiceFull.getString("Seller") != null ? VatElectronicInvoiceFull.getString("Seller") : null;
+                                                System.out.println("销售方名称:" + sellerName);
+                                                //销售方纳税人识别号
+                                                sellerTaxpayerId = VatElectronicInvoiceFull.getString("SellerTaxID") != null ? VatElectronicInvoiceFull.getString("SellerTaxID") : null;
+                                                System.out.println("销售方纳税人识别号:" + sellerTaxpayerId);
+                                                //含税总额(票价)
+                                                totalAmount = VatElectronicInvoiceFull.getFloat("Fare");
+                                                System.out.println("含税总额(票价):" + totalAmount);
+                                                //税额
+                                                taxAmount = VatElectronicInvoiceFull.getFloat("Tax");
+                                                System.out.println("税额:" + taxAmount);
+                                                //不含税总额(税前金额)
+                                                amountWithoutTax = VatElectronicInvoiceFull.getFloat("PretaxAmount") != null ? VatElectronicInvoiceFull.getFloat("PretaxAmount") : 0;
+                                                System.out.println("不含税总额(税前金额):" + amountWithoutTax);
+                                                //发票备注
+                                                invoiceRemark = VatElectronicInvoiceFull.getString("Remark");
+                                                System.out.println("发票备注:" + invoiceRemark);
+                                            }
+                                            if (SubType.equals("VatElectronicCommonInvoice") || SubType.equals("VatElectronicSpecialInvoice")){//NOTE 增值税电子普通发票、增值税电子专用发票
+                                                JSONObject singleInvoiceInfos = jsonObject11.getJSONObject("SingleInvoiceInfos");
+                                                JSONObject VatElectronicInvoiceFull = singleInvoiceInfos.getJSONObject(SubType);
+
+                                                if (SubType.equals("VatElectronicSpecialInvoice")){
+                                                    taxRate = 6;
+                                                }
+                                                System.out.println("发票税率:" + taxRate);
+                                                //发票号码
+                                                invoiceNumber = VatElectronicInvoiceFull.getString("Number");
+                                                System.out.println("发票号码:" + invoiceNumber);
+                                                //开票日期
+                                                invoiceDateStr = VatElectronicInvoiceFull.getString("Date");
+                                                System.out.println("开票日期:" + invoiceDateStr);
+                                                // 定义格式化器
+                                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+                                                // 解析字符串为 LocalDate
+                                                LocalDate localDate = LocalDate.parse(invoiceDateStr, formatter);
+                                                // 转换为毫秒级时间戳(基于系统默认时区)
+                                                invoiceDate = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+                                                System.out.println("开票日期时间戳: " + invoiceDate);
+                                                //发票代码
+                                                invoiceCode = VatElectronicInvoiceFull.getString("Code");
+                                                System.out.println("发票代码: " + invoiceCode);
+                                                //校验码
+                                                checkCode = VatElectronicInvoiceFull.getString("CheckCode");
+                                                System.out.println("校验码: " + checkCode);
+                                                //购买方名称
+                                                buyerName = VatElectronicInvoiceFull.getString("Buyer");
+                                                System.out.println("购买方名称:" + buyerName);
+                                                //购买方纳税人识别号
+                                                buyerTaxpayerId = VatElectronicInvoiceFull.getString("BuyerTaxID");
+                                                System.out.println("购买方纳税人识别号:" + buyerTaxpayerId);
+                                                //销售方名称
+                                                sellerName = VatElectronicInvoiceFull.getString("Seller");
+                                                System.out.println("销售方名称:" + sellerName);
+                                                //销售方纳税人识别号
+                                                sellerTaxpayerId = VatElectronicInvoiceFull.getString("SellerTaxID");
+                                                System.out.println("销售方纳税人识别号:" + sellerTaxpayerId);
+                                                //含税总额(价税合计)
+                                                totalAmount = VatElectronicInvoiceFull.getFloat("Total");
+                                                System.out.println("含税总额(价税合计):" + totalAmount);
+                                                //税额
+                                                taxAmount = VatElectronicInvoiceFull.getFloat("Tax");
+                                                System.out.println("税额:" + taxAmount);
+                                                //不含税总额(税前金额)
+                                                amountWithoutTax = VatElectronicInvoiceFull.getFloat("PretaxAmount");
+                                                System.out.println("不含税总额(税前金额):" + amountWithoutTax);
+                                                //发票备注
+                                                invoiceRemark = VatElectronicInvoiceFull.getString("Remark");
+                                                System.out.println("发票备注:" + invoiceRemark);
+                                            }
+                                            if (aliBtripInvoiceNumbers.contains(normalizeInvoiceNumber(invoiceNumber))) {
+                                                log.info("发票号码 {} 已存在于阿里商旅明细中,跳过当前附件", invoiceNumber);
+                                                continue;
+                                            }
+                                            Map formMap = new HashMap<>();
+                                            //基础信息
+                                            if (emailUserId == null){
+                                                emailUserId = "326829332";
+                                            }
+                                            formMap.put("employeeField_mm1ntqs3", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员,默认使用默认的userId【外部系统开发对接userId:326829332】
+                                            formMap.put("selectField_mne83ozt", "发票识别完成");//发票识别状态:记录票夹完成、发票识别完成
+                                            formMap.put("selectField_mmya15ss", "等待验真");//验真结果:等待验真、验真成功、验真失败
+                                            formMap.put("textField_mne83p0o", email);//收件人邮箱地址
+                                            formMap.put("textField_mne83ozz", "发票识别完成,等待验真!");//发票识别验真返回信息
+                                            //发票信息
+                                            Map fileMap = new HashMap();
+                                            fileMap.put("previewUrl",fileNewUrl);
+                                            fileMap.put("size",fileSize);
+                                            fileMap.put("name",fileName);
+                                            fileMap.put("downloadUrl",fileNewUrl);
+                                            fileMap.put("url",fileNewUrl);
+                                            List fileList = new ArrayList();
+                                            fileList.add(fileMap);
+                                            formMap.put("attachmentField_mm1ntqs4",fileList);//发票文件
+                                            formMap.put("textField_mne83p0p", fileName);//发票文件名称
+                                            //发票信息-识别解析内容
+                                            formMap.put("textField_mne83p0w", SubTypeDescription);//发票类型
+                                            formMap.put("textField_mm1ntqtb", invoiceNumber);//发票号码
+                                            formMap.put("dateField_mm1ntqtc", invoiceDate);//开票日期
+                                            formMap.put("textField_mne83p0j", TypeDescription);//票种类型
+                                            formMap.put("textField_mne83p06", invoiceCode);//发票代码
+                                            formMap.put("textField_mne83p0a", checkCode);//校验码
+                                            formMap.put("textField_mm1ntqth", buyerName);//购买方名称
+                                            formMap.put("textField_mm1ntqti", buyerTaxpayerId);//购买方纳税人识别号
+                                            formMap.put("textField_mm1ntqtv", sellerName);//销售方名称
+                                            formMap.put("textField_mm1ntqtw", sellerTaxpayerId);//销售方纳税人识别号
+                                            formMap.put("numberField_mm1ntqua", totalAmount);//发票含税总额
+                                            formMap.put("numberField_mm1ntqub", taxAmount);//发票税额
+                                            formMap.put("numberField_mne83p09", amountWithoutTax);//发票不含税总额
+                                            formMap.put("textareaField_mm1ntquo", invoiceRemark);//发票备注
+                                            formMap.put("numberField_mqoo3y0c", taxRate);//发票税率
+                                            //系统信息
+                                            formMap.put("textField_mne83p05", fileNewUrl);//文件转存url地址
+                                            formMap.put("radioField_mm1ntqup", "未报销");//报销状态
+                                            formMap.put("radioField_mm1ntquc", "邮箱同步");//发票来源
+                                            formMap.put("textField_moibz1e1", invoiceDateStr);//开票日期
+
+                                            String formDataJson = JSON.toJSONString(formMap);
+                                            System.out.println("mapJson:" + formDataJson);
+
+                                            Map searchMap = new HashMap<>();
+                                            searchMap.put("key", "textField_mm1ntqtb");
+                                            searchMap.put("value", invoiceNumber);
+                                            searchMap.put("type", "TEXT");
+                                            searchMap.put("operator", "eq");
+                                            searchMap.put("componentName", "TextField");
+                                            List searchList = new ArrayList();
+                                            searchList.add(searchMap);
+                                            String searchCondition = JSON.toJSONString(searchList);
+                                            System.out.println("查询条件searchCondition:" + searchCondition);
+                                            /*新增或更新到宜搭*/
+                                            log.info("开始新增或更新到宜搭「票夹」表单--->");
+                                            YiDaFormData.CreateOrUpdateFormData(
+                                                    "FORM-2D49DB5769F542B8B0D836B4BE5DB20BETRV",
+                                                    searchCondition,
+                                                    formDataJson,
+                                                    accessTokenService.getAccessToken());
+                                        }else {
+                                            log.info("发票识别失败!");//STOP 识别失败的不记录票夹
+//                                            Map formMap = new HashMap<>();
+//                                            //基础信息
+//                                            if (emailUserId == null){
+//                                                emailUserId = "326829332";
+//                                            }
+//                                            formMap.put("employeeField_mm1ntqs3", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员,默认使用默认的userId【外部系统开发对接userId:326829332】
+//                                            formMap.put("selectField_mne83ozt", "记录票夹完成");//发票识别状态:记录票夹完成、发票识别完成
+//                                            formMap.put("selectField_mmya15ss", "等待验真");//验真结果:等待验真、验真成功、验真失败
+//                                            formMap.put("textField_mne83p0o", email);//收件人邮箱地址
+//                                            formMap.put("textField_mne83ozz", "发票识别失败!");//发票识别验真返回信息
+//                                            //发票信息
+//                                            Map fileMap = new HashMap();
+//                                            fileMap.put("previewUrl",fileNewUrl);
+//                                            fileMap.put("size",fileSize);
+//                                            fileMap.put("name",fileName);
+//                                            fileMap.put("downloadUrl",fileNewUrl);
+//                                            fileMap.put("url",fileNewUrl);
+//                                            List fileList = new ArrayList();
+//                                            fileList.add(fileMap);
+//                                            formMap.put("attachmentField_mm1ntqs4",fileList);//发票文件
+//                                            formMap.put("textField_mne83p0p", fileName);//发票文件名称
+//                                            //系统信息
+//                                            formMap.put("textField_mne83p05", fileNewUrl);//文件转存url地址
+//                                            formMap.put("radioField_mm1ntqup", "未报销");//报销状态
+//                                            formMap.put("radioField_mm1ntquc", "邮箱同步");//发票来源
+//
+//                                            String formDataJson = JSON.toJSONString(formMap);
+//                                            System.out.println("mapJson:" + formDataJson);
+//
+//                                            Map searchMap = new HashMap<>();
+//                                            searchMap.put("key", "textField_mne83p0p");
+//                                            searchMap.put("value", fileName);
+//                                            searchMap.put("type", "TEXT");
+//                                            searchMap.put("operator", "eq");
+//                                            searchMap.put("componentName", "TextField");
+//                                            List searchList = new ArrayList();
+//                                            searchList.add(searchMap);
+//                                            String searchCondition = JSON.toJSONString(searchList);
+//                                            System.out.println("查询条件searchCondition:" + searchCondition);
+//                                            /*新增或更新到宜搭*/
+//                                            log.info("开始新增或更新到宜搭「票夹」表单--->");
+//                                            YiDaFormData.CreateOrUpdateFormData(
+//                                                    "FORM-2D49DB5769F542B8B0D836B4BE5DB20BETRV",
+//                                                    searchCondition,
+//                                                    formDataJson,
+//                                                    accessTokenService.getAccessToken());
+                                        }
+                                    }catch (TencentCloudSDKException e){
+                                        log.error("发票识别异常:" + e);
+                                    }
+                                }
+                            }else {
+                                log.info("--->【3】该文件不是pdf文件,且该文件是行程单,不进行识别和写入宜搭!");
+                            }
+
+                            // 判断是否为 压缩包(忽略大小写)
+                            boolean isPackage = fileName != null && (fileName.toLowerCase().endsWith(".zip") || fileName.toLowerCase().endsWith(".rar"));
+                            System.out.println("文件名: " + fileName + " → 是压缩包? " + isPackage);
+                            //NOTE 判断邮件中的附件是否为压缩包
+//                            if (isPackage && !hasInvoiceKeyword) {
+//                                log.info("该附件是压缩包,开始解压处理文件===");
+//                                if (fileName != null && fileName.toLowerCase().endsWith(".zip")){
+//                                    log.info("该附件是zip格式,开始处理文件,文件名:{},文件id:{}", fileName, fileId);
+//                                    /*根据文件id获取文件流进行下载转存文件*/
+//                                    Map mapEnd = AlibabaMailBox.downLoadMailFile("zip", emailAddress, emailId, fileId, fileName, accessTokenService.getAlibabaMailboxAccessToken());
+//                                    String fileNewUrl = (String) mapEnd.get("fileNewUrl");
+//                                    if (fileNewUrl != null){
+//                                        /*调用发票识别*/
+//                                        //TODO
+//
+//                                        Map formMap = new HashMap<>();
+//                                        //基础信息
+//                                        if (emailUserId == null){
+//                                            emailUserId = "326829332";
+//                                        }
+//                                        formMap.put("employeeField_mm1ntqs3", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员,默认使用默认的userId【外部系统开发对接userId:326829332】
+//                                        formMap.put("selectField_mne83ozt", "记录票夹完成");//发票识别状态:记录票夹完成、发票识别完成
+//                                        formMap.put("selectField_mmya15ss", "等待验真");//验真结果:等待验真、验真成功、验真失败
+//                                        formMap.put("textField_mne83p0o", email);//收件人邮箱地址
+//                                        formMap.put("textField_mne83ozz", null);//发票识别验真返回信息
+//                                        //发票信息
+//                                        Map fileMap = new HashMap();
+//                                        fileMap.put("previewUrl",fileNewUrl);
+//                                        fileMap.put("size",fileSize);
+//                                        fileMap.put("name",fileName);
+//                                        fileMap.put("downloadUrl",fileNewUrl);
+//                                        fileMap.put("url",fileNewUrl);
+//                                        List fileList = new ArrayList();
+//                                        fileList.add(fileMap);
+//                                        formMap.put("attachmentField_mm1ntqs4",fileList);//发票文件
+//                                        formMap.put("textField_mne83p0p", fileName);//发票文件名称
+//                                        formMap.put("textField_mne83p05", fileNewUrl);//文件转存url地址
+//
+//                                        String formDataJson = JSON.toJSONString(formMap);
+//                                        System.out.println("mapJson:" + formDataJson);
+//
+//                                        Map searchMap = new HashMap<>();
+//                                        searchMap.put("key", "textField_mne83p0p");
+//                                        searchMap.put("value", fileName);
+//                                        searchMap.put("type", "TEXT");
+//                                        searchMap.put("operator", "eq");
+//                                        searchMap.put("componentName", "TextField");
+//                                        List searchList = new ArrayList();
+//                                        searchList.add(searchMap);
+//                                        String searchCondition = JSON.toJSONString(searchList);
+//                                        System.out.println("查询条件searchCondition:" + searchCondition);
+//                                        /*新增或更新到宜搭*/
+//                                        log.info("开始新增或更新到宜搭「票夹」表单--->");
+//                                        YiDaFormData.CreateOrUpdateFormData(
+//                                                "FORM-2D49DB5769F542B8B0D836B4BE5DB20BETRV",
+//                                                searchCondition,
+//                                                formDataJson,
+//                                                accessTokenService.getAccessToken());
+//                                    }
+//                                }
+//                                if (fileName != null && fileName.toLowerCase().endsWith(".rar")){
+//                                    log.info("该附件是rar格式,开始处理文件,文件名:{},文件id:{}", fileName, fileId);
+//
+//                                    /*根据文件id获取文件流进行下载转存文件*/
+//                                    Map mapEnd = AlibabaMailBox.downLoadMailFile("rar", emailAddress, emailId, fileId, fileName, accessTokenService.getAlibabaMailboxAccessToken());
+//                                    String fileNewUrl = (String) mapEnd.get("fileNewUrl");
+//                                    if (fileNewUrl != null){
+//                                        /*调用发票识别*/
+//                                        //TODO
+//
+//                                        Map formMap = new HashMap<>();
+//                                        //基础信息
+//                                        if (emailUserId == null){
+//                                            emailUserId = "326829332";
+//                                        }
+//                                        formMap.put("employeeField_mm1ntqs3", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员,默认使用默认的userId【外部系统开发对接userId:326829332】
+//                                        formMap.put("selectField_mne83ozt", "记录票夹完成");//发票识别状态:记录票夹完成、发票识别完成
+//                                        formMap.put("selectField_mmya15ss", "等待验真");//验真结果:等待验真、验真成功、验真失败
+//                                        formMap.put("textField_mne83p0o", email);//收件人邮箱地址
+//                                        formMap.put("textField_mne83ozz", null);//发票识别验真返回信息
+//                                        //发票信息
+//                                        Map fileMap = new HashMap();
+//                                        fileMap.put("previewUrl",fileNewUrl);
+//                                        fileMap.put("size",fileSize);
+//                                        fileMap.put("name",fileName);
+//                                        fileMap.put("downloadUrl",fileNewUrl);
+//                                        fileMap.put("url",fileNewUrl);
+//                                        List fileList = new ArrayList();
+//                                        fileList.add(fileMap);
+//                                        formMap.put("attachmentField_mm1ntqs4",fileList);//发票文件
+//                                        formMap.put("textField_mne83p0p", fileName);//发票文件名称
+//                                        formMap.put("textField_mne83p05", fileNewUrl);//文件转存url地址
+//
+//                                        String formDataJson = JSON.toJSONString(formMap);
+//                                        System.out.println("mapJson:" + formDataJson);
+//
+//                                        Map searchMap = new HashMap<>();
+//                                        searchMap.put("key", "textField_mne83p0p");
+//                                        searchMap.put("value", fileName);
+//                                        searchMap.put("type", "TEXT");
+//                                        searchMap.put("operator", "eq");
+//                                        searchMap.put("componentName", "TextField");
+//                                        List searchList = new ArrayList();
+//                                        searchList.add(searchMap);
+//                                        String searchCondition = JSON.toJSONString(searchList);
+//                                        System.out.println("查询条件searchCondition:" + searchCondition);
+//                                        /*新增或更新到宜搭*/
+//                                        log.info("开始新增或更新到宜搭「票夹」表单--->");
+//                                        YiDaFormData.CreateOrUpdateFormData(
+//                                                "FORM-2D49DB5769F542B8B0D836B4BE5DB20BETRV",
+//                                                searchCondition,
+//                                                formDataJson,
+//                                                accessTokenService.getAccessToken());
+//                                    }
+//                                }
+//                            }else {
+//                                log.info("--->【3】该文件不是压缩包文件!");
+//                            }
+
+                        }
+                    }else {
+                        log.info("--->【2】当前邮件未获取到发票附件列表!");
+                    }
+                }
+            }else {
+                log.info("--->【1】当前邮箱未获取到邮件列表!");
+            }
+
+            cursor = nextCursor;
+            log.info("下一页光标nextCursor:" + cursor);
+        }while (!cursor.equals("$"));
+
+        log.info("--->结束循环搜索邮件<---");
+        return R.ok().message("解析邮箱发票结束");
+    }
+
+    private Set<String> queryAliBtripInvoiceNumbers(String inputDate) {
+        Set<String> invoiceNumbers = new HashSet<>();
+        try {
+            JSONArray trainTickets = aliBtripTrainTicketService.queryByInvoiceDate(
+                    inputDate + " 00:00:00", inputDate + " 23:59:59");
+            for (int i = 0; i < trainTickets.size(); i++) {
+                JSONObject trainTicket = trainTickets.getJSONObject(i);
+                addInvoiceNumber(invoiceNumbers, trainTicket.getString("electronic_ticket_no"));
+            }
+            log.info("阿里商旅火车票凭证明细查询完成,共 {} 条", trainTickets.size());
+        } catch (Exception e) {
+            log.error("查询阿里商旅火车票凭证明细失败,不执行该来源的发票去重", e);
+        }
+
+        try {
+            JSONArray vatInvoices = aliBtripVatInvoiceService.queryByBillDate(inputDate);
+            for (int i = 0; i < vatInvoices.size(); i++) {
+                addInvoiceNumber(invoiceNumbers, vatInvoices.getJSONObject(i).getString("invoice_no"));
+            }
+            log.info("阿里商旅增值税发票明细查询完成,共 {} 条", vatInvoices.size());
+        } catch (Exception e) {
+            log.error("查询阿里商旅增值税发票明细失败,不执行该来源的发票去重", e);
+        }
+        return invoiceNumbers;
+    }
+
+    private static void addInvoiceNumber(Set<String> invoiceNumbers, String invoiceNumber) {
+        String normalized = normalizeInvoiceNumber(invoiceNumber);
+        if (normalized != null) {
+            invoiceNumbers.add(normalized);
+        }
+    }
+
+    private static String normalizeInvoiceNumber(String invoiceNumber) {
+        if (invoiceNumber == null) {
+            return null;
+        }
+        String normalized = invoiceNumber.replaceAll("\\s+", "").toUpperCase(Locale.ROOT);
+        return normalized.isEmpty() ? null : normalized;
+    }
+}

+ 384 - 0
src/main/java/com/muzhi/lemeng/service/impl/DingTalkServiceImpl.java

@@ -0,0 +1,384 @@
+package com.muzhi.lemeng.service.impl;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormDataV2;
+import com.muzhi.lemeng.common.thirdparty.EicFormData;
+import com.muzhi.lemeng.service.AccessTokenService;
+import com.muzhi.lemeng.service.DingTalkService;
+import com.muzhi.lemeng.common.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.*;
+import java.util.regex.Pattern;
+
+
+@Service
+@Slf4j
+public class DingTalkServiceImpl implements DingTalkService {
+
+    @Autowired
+    private AccessTokenService accessTokenService;
+
+    /**
+     * 提供EIC回写宜搭报销付款状态接口
+     */
+    @Override
+    public R updateState(JSONObject body) {
+        log.info("开始接收EIC传递参数......");
+        log.info("接收EIC传递json请求参数:" + body);
+        log.info("开始解析请求参数......");
+
+        String state = null;
+        String serialNumber = null;
+        String message = null;
+
+        try {
+            //付款状态
+            state = body.getString("state");
+            log.info("付款状态:" + state);
+            //宜搭流水号
+            serialNumber = body.getString("serialNumber");
+            log.info("宜搭流水号:" + serialNumber);
+        }catch (Exception e){
+            e.printStackTrace();
+            log.info("传参值异常!");
+        }
+
+        try {
+            message = body.getString("message");
+            log.info("付款状态信息:" + message);
+        }catch (Exception e){
+            log.info("付款状态信息异常!");
+        }
+
+        if (serialNumber == null || serialNumber.equals("")){
+            return R.error().message("请求参数必填信息有误【宜搭流水号】");
+        }else {
+            if (state == null || state.equals("")){
+                return R.error().message("请求参数必填信息有误【付款状态】");
+            }else {
+                /*更新报销档案状态*/
+                String slid = YiDaFormDataV2.searchAndUpdateFormData("FORM-FAF8DD2B7B2C4D31AF0FE33B4B3CC357K1ZV", serialNumber, state, message, accessTokenService.getAccessToken());
+                if (slid == null){
+                    return R.error().message("未匹配到当前【宜搭流水号】的报销档案");
+                }else {
+                    return R.ok().message("宜搭流水号【" + serialNumber + "】回写宜搭报销档案状态成功!宜搭报销档案实例id:" + slid);
+                }
+            }
+        }
+    }
+
+    //正则表达判断日期文本格式
+    private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}年(0?[1-9]|1[0-2])月(0?[1-9]|[12]\\d|3[01])日$");
+    public static boolean isValidDateFormat(String dateStr) {
+        return DATE_PATTERN.matcher(dateStr).matches();
+    }
+
+    // 定义源格式:年月日(如 "2023年10月15日")
+    private static final DateTimeFormatter SOURCE_FORMATTER =
+            DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+
+    // 定义目标格式:年-月-日(如 "2023-10-15")
+    private static final DateTimeFormatter TARGET_FORMATTER =
+            DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    /**
+     * 转换日期格式(自动验证日期有效性)
+     * @param dateStr 源日期字符串(如 "2023年10月15日")
+     * @return 转换后的字符串(如 "2023-10-15"),无效日期返回 null
+     */
+    public static String convertToStandardFormat(String dateStr) {
+        try {
+            // 1. 解析源格式字符串 → 转为 LocalDate 对象(自动验证日期有效性)
+            LocalDate date = LocalDate.parse(dateStr, SOURCE_FORMATTER);
+            // 2. 按目标格式重新格式化
+            return date.format(TARGET_FORMATTER);
+        } catch (DateTimeParseException e) {
+            // 捕获无效日期(如 "2023年02月30日")或格式错误
+            return null;
+        }
+    }
+
+    /**
+     * 获取宜搭报销单信息推送创建EIC报销单
+     */
+    @Override
+    public R createExpenseAccount(String shiliId) {
+        log.info("开始接收宜搭传递参数......");
+        log.info("接收宜搭传递实例id参数:" + shiliId);
+
+        /*根据实例id查询宜搭表单数据*/
+        JSONObject yidaFormData = YiDaFormDataV2.getYidaFormData(shiliId, accessTokenService.getAccessToken());
+        log.info("开始解析宜搭传递参数......");
+        Map mapAll = new HashMap();
+        if (yidaFormData == null){
+            System.out.println("未获取到宜搭表单数据【报销流程】表单数据!");
+            return R.error().message("未获取到宜搭表单数据【报销流程】表单数据!");
+        }else {
+            //获取表单数据
+            JSONObject baoxiaoData = yidaFormData.getJSONObject("formData");
+            System.out.println("获取宜搭表单数据【报销流程】表单object对象,数据内容{},-->" + baoxiaoData);
+
+            //EIC用户手机号
+            String eicPhone = baoxiaoData.getString("textField_mm1pqouh");
+            System.out.println("EIC用户手机号:" + eicPhone);
+            //EIC部门代码
+            String eicDepCode = baoxiaoData.getString("textField_mm1pqous");
+            System.out.println("EIC部门代码:" + eicDepCode);
+            //EIC付款单位账套编号
+            String eicPayComponyCode = baoxiaoData.getString("textField_mp54pq8q");
+            System.out.println("EIC付款单位账套编号:" + eicPayComponyCode);
+
+            //报销单据主题
+            String baoxiaodanTitle = baoxiaoData.getString("textField_mm1pqout");
+            System.out.println("报销单据主题:" + baoxiaodanTitle);
+            //结算方式
+            String jiesuanType = baoxiaoData.getString("radioField_mm1pqouy");
+            System.out.println("结算方式:" + jiesuanType);
+
+            //账户名称
+            String zhanghuName = baoxiaoData.getString("textField_mm1pqov4") != null ? baoxiaoData.getString("textField_mm1pqov4") : null;
+            System.out.println("账户名称:" + zhanghuName);
+            //账户号
+            String zhanghuNum = baoxiaoData.getString("textField_mm1pqov5") != null ? baoxiaoData.getString("textField_mm1pqov5") : null;
+            System.out.println("账户号:" + zhanghuNum);
+            //开户银行
+            String kaihuBank = baoxiaoData.getString("textField_mm1pqov7") != null ? baoxiaoData.getString("textField_mm1pqov7") : null;
+            System.out.println("开户银行:" + kaihuBank);
+
+            //结算日期
+            String jiesuanDate = baoxiaoData.getString("textField_mob4a7a2");
+            System.out.println("结算日期:" + jiesuanDate);
+            //币种
+            String bizhong = baoxiaoData.getString("radioField_mm5nl3c3");
+            System.out.println("币种:" + bizhong);
+            //宜搭报销单流水号
+            String yidaSerialNumber = baoxiaoData.getString("serialNumberField_mmsrxlwh");
+            System.out.println("宜搭报销单流水号:" + yidaSerialNumber);
+
+            //报销科目数组明细
+            List subjectArr = new ArrayList();
+            //发票文件数组
+            List invoiceArr = new ArrayList();
+            /*查询宜搭表单费用明细组件数据*/
+            JSONArray yidaFYMX = YiDaFormDataV2.getYidaMX("FORM-483ADD77352E4B5BBB10A2568BAC3CCDF79W", shiliId, "tableField_mm5nl3c8", accessTokenService.getAccessToken());
+            JSONArray yidaFPMX = YiDaFormDataV2.getYidaMX("FORM-483ADD77352E4B5BBB10A2568BAC3CCDF79W", shiliId, "tableField_mm5nl3cl", accessTokenService.getAccessToken());
+            if (yidaFYMX != null){
+                log.info("开始循环获取宜搭明细组件数据......");
+                for (int i = 0; i < yidaFYMX.size(); i++) {
+                    int size = i + 1;
+                    log.info("循环费用明细第:" + size + "次");
+                    //EIC用户手机号
+                    String userPhone = yidaFYMX.getJSONObject(i).getString("textField_mm5nl3cb");
+                    System.out.println("EIC用户手机号:" + userPhone);
+                    //EIC部门代码
+                    String depId = yidaFYMX.getJSONObject(i).getString("textField_mm5nl3ce");
+                    System.out.println("EIC部门代码:" + depId);
+                    //金额
+                    float jine = yidaFYMX.getJSONObject(i).getFloat("numberField_mm5nl3cf");
+                    System.out.println("金额:" + jine);
+                    //税额
+                    float shuie = yidaFYMX.getJSONObject(i).getFloat("numberField_mm5nl3cw");
+                    System.out.println("税额:" + shuie);
+                    //费用科目编号
+                    String kemuId = yidaFYMX.getJSONObject(i).getString("textField_mv0mndcs");
+                    System.out.println("费用科目编号:" + kemuId);
+                    //费用科目名称
+                    String kemuName = yidaFYMX.getJSONObject(i).getString("selectField_mv0mndcq");
+                    System.out.println("费用科目名称:" + kemuName);
+
+                    List listFP = new ArrayList();
+                    for (int j = 0; j < yidaFPMX.size(); j++) {
+                        int size1 = j + 1;
+                        log.info("循环发票明细第:" + size1 + "次");
+                        //发票科目编号
+                        String fapiaokemuId = yidaFPMX.getJSONObject(j).getString("textField_mqhp5sxm");
+                        System.out.println("发票科目编号:" + fapiaokemuId);
+                        //EIC发票类型
+                        String EICfapiaoleixing = yidaFPMX.getJSONObject(j).getString("textField_mqhp5sxo") != null ? yidaFPMX.getJSONObject(i).getString("textField_mqhp5sxo") : null;
+                        System.out.println("EIC发票类型:" + EICfapiaoleixing);
+                        if (EICfapiaoleixing == null){
+                            EICfapiaoleixing = "普票-电子";
+                        }
+                        //发票文件转存url地址
+                        String fapiaoURL = yidaFPMX.getJSONObject(j).getString("textField_mqolyr8n");
+                        System.out.println("发票文件转存url地址:" + fapiaoURL);
+                        //发票文件名称
+                        String fapiaowenjianmingcheng = yidaFPMX.getJSONObject(j).getString("textField_mqolyr8l");
+                        System.out.println("发票文件名称:" + fapiaowenjianmingcheng);
+                        //发票名称扩展后缀
+                        // 1. 查找最后一个 '.' 的位置
+                        int lastDotIndex = fapiaowenjianmingcheng.lastIndexOf('.');
+                        // 2. 截取包含 '.' 及之后的所有字符
+                        String extensionWithDot = (lastDotIndex != -1)
+                                ? fapiaowenjianmingcheng.substring(lastDotIndex)
+                                : ""; // 无扩展名时返回空
+                        //开票日期
+                        String kaipiaoriqi = yidaFPMX.getJSONObject(j).getString("textField_mqolyr8m");
+                        System.out.println("开票日期:" + kaipiaoriqi);
+                        if (kaipiaoriqi != null || !kaipiaoriqi.equals("")){
+                            if (isValidDateFormat(kaipiaoriqi)){//开票日期格式属于年月日,需要进行转换成x-x-x个格式,因为推送EIC发票明细中的开票日期格式有要求
+                                kaipiaoriqi = convertToStandardFormat(kaipiaoriqi);
+                            }
+                        }
+                        //发票号码
+                        String fapiaohaoma = yidaFPMX.getJSONObject(j).getString("textField_mmya35v1");
+                        System.out.println("发票号码:" + fapiaohaoma);
+                        //发票金额
+                        float fapiaojine = yidaFPMX.getJSONObject(j).getFloat("numberField_mm5nl3cp");
+                        System.out.println("发票金额:" + fapiaojine);
+                        //发票税额
+                        float fapiaoshuie = yidaFPMX.getJSONObject(j).getFloat("numberField_mm5nl3cq") != null ? yidaFPMX.getJSONObject(i).getFloat("numberField_mm5nl3cq") : 0;
+                        System.out.println("发票税额:" + fapiaoshuie);
+                        //发票税率
+                        float fapiaoshuilv = yidaFPMX.getJSONObject(j).getFloat("numberField_mqhp5sxj") != null ? yidaFPMX.getJSONObject(i).getFloat("numberField_mqhp5sxj") : 0;
+                        System.out.println("发票税率:" + fapiaoshuilv);
+                        String targetSubstring = "普票";
+                        boolean contains = EICfapiaoleixing.contains(targetSubstring);
+                        if (fapiaoshuilv <= 0){
+                            if (contains){
+                                fapiaoshuilv = 0;
+                                fapiaoshuie = 0;
+                            }else {
+                                fapiaoshuilv = 6;
+                            }
+                        }
+                        if (fapiaokemuId.equals(kemuId)){
+                            Map mapFP = new HashMap();
+                            mapFP.put("type", EICfapiaoleixing);
+                            mapFP.put("date2", kaipiaoriqi);
+                            mapFP.put("no", fapiaohaoma);
+                            mapFP.put("amount2", fapiaojine);
+                            mapFP.put("taxAmount2", fapiaoshuie);
+                            mapFP.put("tax", fapiaoshuilv);
+                            mapFP.put("subjectCode2", fapiaokemuId);
+                            listFP.add(mapFP);
+
+                            Map mapFPflie = new HashMap();
+                            mapFPflie.put("fileUrl", fapiaoURL);
+                            mapFPflie.put("fileName", fapiaowenjianmingcheng);
+                            mapFPflie.put("extension", extensionWithDot);
+                            invoiceArr.add(mapFPflie);
+                        }
+                    }
+
+                    /**封装报销明细*/
+                    Map mapMX = new HashMap();
+                    mapMX.put("bearer", userPhone);
+                    mapMX.put("bearerDep", depId);
+                    mapMX.put("amount", jine);
+                    mapMX.put("taxAmount", shuie);
+                    mapMX.put("subjectCode", kemuId);
+                    mapMX.put("subjectName", kemuName);
+                    mapMX.put("invoices", listFP);
+                    subjectArr.add(mapMX);
+                }
+            }
+
+            //NOTE 二期新增开发内容,报销流程增加无票或其他情况的佐证材料附件上传,对接EIC。
+            //票据情况
+            String piaojuqingkuang = baoxiaoData.getString("radioField_mqt882aa");
+            System.out.println("票据情况:" + piaojuqingkuang);
+
+            if (!piaojuqingkuang.equals("电子发票")){
+                // 获取附件字段,它当前是一个 JSON 数组字符串
+                String attachmentJson = null;
+                try {
+                    attachmentJson = baoxiaoData.getString("attachmentField_muqqasj3");
+                }catch (Exception e){
+                    e.printStackTrace();
+                }
+
+                if (attachmentJson == null || attachmentJson.trim().isEmpty()){
+                    log.info("佐证附件原始json为空!");
+                }else {
+                    // 再次解析为数组
+                    JSONArray attachments = JSON.parseArray(attachmentJson);
+                    for ( int i = 0 ; i < attachments.size(); i++) {
+                        JSONObject attachment = attachments.getJSONObject(i);
+                        String zuozhengwenjianmingcheng = attachment.getString( "name" );
+                        String zuozhengURL = attachment.getString( "downloadUrl" );
+
+                        //发票名称扩展后缀
+                        // 1. 查找最后一个 '.' 的位置
+                        int lastDotIndex = zuozhengwenjianmingcheng.lastIndexOf('.');
+                        // 2. 截取包含 '.' 及之后的所有字符
+                        String extensionWithDot = (lastDotIndex != -1)
+                                ? zuozhengwenjianmingcheng.substring(lastDotIndex)
+                                : ""; // 无扩展名时返回空
+
+                        System.out.println( "文件名:" + zuozhengwenjianmingcheng);
+                        System.out.println( "文件名后缀:" + extensionWithDot);
+                        System.out.println( "下载地址:" + zuozhengURL);
+                        System.out.println( "----------------" );
+
+                        /*获取宜搭附件免登授权地址*/
+                        String fileUrlDownloadToken = YiDaFormDataV2.getYidaTemporaryFileURL(zuozhengURL, accessTokenService.getAccessToken());
+                        System.out.println("宜搭附件免登授权地址:" + fileUrlDownloadToken);
+
+                        Map mapZZflie = new HashMap();
+                        mapZZflie.put("fileUrl", fileUrlDownloadToken);
+                        mapZZflie.put("fileName", zuozhengwenjianmingcheng);
+                        mapZZflie.put("extension", extensionWithDot);
+                        invoiceArr.add(mapZZflie);
+                    }
+                }
+            }
+
+            //NOTE 封装推送参数
+            Map map1 = new HashMap();
+            //EIC用户手机号
+            map1.put("handler", eicPhone);
+            //EIC部门代码
+            map1.put("handlerDep", eicDepCode);
+            //EIC付款单位账套编号
+            map1.put("payer", eicPayComponyCode);
+            //报销单据主题
+            map1.put("title", baoxiaodanTitle);
+            //结算方式(1现金 2银行(2.1账户名称2.2账户号2.3开户银行)3暂不支付;举例:结算方式为银行,填写“银行”)
+            map1.put("way", jiesuanType);
+            //账户名称(结算方式是银行,必填)
+            map1.put("name", zhanghuName);
+            //账户号(结算方式是银行,必填)
+            map1.put("cardNum", zhanghuNum);
+            //开户银行(结算方式是银行,必填)
+            map1.put("bank", kaihuBank);
+            //结算日期(yyyy-MM-dd)
+            map1.put("date", jiesuanDate);
+            //币种(默认人民币;币种:人民币、美元、欧元、美金、日元、港币。(必须按照汉字规则,比如币种是美元,填写“美元”))
+            map1.put("currency", bizhong);
+            //宜搭报销单流水号
+            map1.put("serialNumber", yidaSerialNumber);
+            //报销科目数组明细
+            map1.put("subjectArr", subjectArr);
+            //发票文件数组
+            map1.put("attachment", invoiceArr);
+
+            List list1 = new ArrayList();
+            list1.add(map1);
+            mapAll.put("data", list1);
+            String mapAllStr = JSON.toJSONString(mapAll);
+            System.out.println("封装查询的字段参数:" + mapAllStr);
+
+            /*调用EIC接口创建报销单*/
+            JSONObject eidFormData = EicFormData.createEidFormData(mapAllStr, accessTokenService.getEicAccessToken());
+            if (eidFormData != null){
+                float code = eidFormData.getFloat("code");
+                if (code == 0){
+                    return R.ok().message("创建EIC报销单成功!").data(eidFormData);
+                }else {
+                    return R.error().message("创建EIC报销单失败!").data(eidFormData);
+                }
+            }else {
+                return R.error().message("创建EIC报销单失败!").data(eidFormData);
+            }
+        }
+    }
+}

+ 570 - 0
src/main/java/com/muzhi/lemeng/service/impl/InvoiceServiceImpl.java

@@ -0,0 +1,570 @@
+package com.muzhi.lemeng.service.impl;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.lemeng.common.R;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormData;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormDataV2;
+import com.muzhi.lemeng.common.tencent.TencentInvoice;
+import com.muzhi.lemeng.service.AccessTokenService;
+import com.muzhi.lemeng.service.AliBtripTrainTicketService;
+import com.muzhi.lemeng.service.AliBtripVatInvoiceService;
+import com.muzhi.lemeng.service.InvoiceService;
+import com.tencentcloudapi.common.exception.TencentCloudSDKException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+import java.io.*;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+
+@Service
+@Slf4j
+public class InvoiceServiceImpl implements InvoiceService {
+
+    @Autowired
+    private AccessTokenService accessTokenService;
+
+    @Autowired
+    private AliBtripTrainTicketService aliBtripTrainTicketService;
+
+    @Autowired
+    private AliBtripVatInvoiceService aliBtripVatInvoiceService;
+
+    /**
+     * 从指定URL下载文件到本地路径
+     */
+    public static File downloadFile(String urlPath, String downloadDir, String fileName, String destinationDir) {
+        File file = null;
+        try {
+
+            URL url = new URL(urlPath);
+
+            URLConnection urlConnection = url.openConnection();
+
+            HttpURLConnection httpURLConnection = (HttpURLConnection) urlConnection;// http的连接类
+
+            //String contentType = httpURLConnection.getContentType();//请求类型,可用来过滤请求,
+
+            httpURLConnection.setConnectTimeout(1000*5);//设置超时
+
+            httpURLConnection.setRequestMethod("GET");//设置请求方式,默认是GET
+
+            httpURLConnection.setRequestProperty("Charset", "UTF-8");// 设置字符编码
+
+
+            httpURLConnection.connect();// 打开连接
+
+            BufferedInputStream bin = new BufferedInputStream(httpURLConnection.getInputStream());
+
+            //TODO 修改成动态文件名称:当前审批单的文件名称
+//            String fileName = "ceshi.pdf";// 指定文件名称
+
+            String path = destinationDir + File.separatorChar + downloadDir + File.separatorChar + fileName;// 指定存放位置,此处使用的是全局变量的文件名称
+            file = new File(path);
+            // 校验文件夹目录是否存在,不存在就创建一个目录
+            if (!file.getParentFile().exists()) {
+                file.getParentFile().mkdirs();
+            }
+
+            OutputStream out = new FileOutputStream(file);
+            int size = 0;
+
+            byte[] b = new byte[2048];
+            //把输入流的文件读取到字节数据b中,然后输出到指定目录的文件
+            while ((size = bin.read(b)) != -1) {
+                out.write(b, 0, size);
+            }
+            // 关闭资源
+            bin.close();
+            out.close();
+            System.out.println("文件下载成功!");
+        } catch (MalformedURLException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        } catch (IOException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+            System.out.println("文件下载失败!");
+        } finally {
+            return file;
+        }
+
+    }
+
+    /**
+     * 通用票据识别(高级)提取发票信息
+     */
+    @Override
+    public R getInvoiceOCR(JSONObject obj) throws TencentCloudSDKException{
+        /**通过宜搭附件url地址获取上传的发票文件url明细*/
+        log.info("宜搭传参--->" + obj);
+        log.info("开始解析参数......");
+        JSONArray jsonArrayUrl = obj.getJSONArray("urlList");
+
+        List list = new ArrayList<>();
+        Map<String, Set<String>> aliBtripInvoiceNumberCache = new HashMap<>();
+        JSONArray jsonArray_item = new JSONArray();
+        for (int x = 0; x < jsonArrayUrl.size(); x++) {
+            JSONObject jsonObjectUrl = jsonArrayUrl.getJSONObject(x);
+
+            String fileUrlDownload = jsonObjectUrl.getString("fileUrlDownload");
+            log.info("宜搭附件地址:" + fileUrlDownload);
+            String fileDownloadName = jsonObjectUrl.getString("fileDownloadName");
+            log.info("宜搭附件名称:" + fileDownloadName);
+            float fileSize = jsonObjectUrl.getFloat("fileSize");
+            log.info("宜搭附件大小:" + fileSize);
+
+            /**通过宜搭附件url地址获取上传的发票下载至本地服务器*/
+//        fileUrlDownload = "https://xyqvbp.aliwork.com/inst/preview?appType=APP_XV6ZGK1162TI1Z1FE17D&fileName=APP_XV6ZGK1162TI1Z1FE17D_MTU3MzExMjA5XzA3QjY2T0QxSE0wMUozSkhLNzlSSzY5NEpNRlEyUkdZUlFMSU02NA%24%24.xml&fileSize=3965&downloadUrl=APP_XV6ZGK1162TI1Z1FE17D_MTU3MzExMjA5XzA3QjY2T0QxSE0wMUozSkhLNzlSSzY5NEpNRlEyUkdZUlFMSU02NA$$.xml";
+//        fileDownloadName = "dzfp_25317000002107213624_20250902105324.xml";
+
+            /**获取宜搭附件免登授权地址*/
+            String fileUrlDownloadToken = YiDaFormDataV2.getYidaTemporaryFileURL(fileUrlDownload, accessTokenService.getAccessToken());
+            System.out.println("宜搭附件免登授权地址:" + fileUrlDownloadToken);
+
+            try {
+                String destinationDir = "invoice"; // Linux 路径
+
+                //获取当前日期,用于创建文件保存目录中增加当前日期的文件夹作区分
+                SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");//设置日期格式 yyyy-MM-dd HH:mm:ss
+                String format = df.format(new Date());
+                System.out.println(format);// new Date()为获取当前系统时间
+
+                /**通过宜搭授权免登地址下载文件至服务器*/
+                downloadFile(fileUrlDownloadToken, format, fileDownloadName, destinationDir);
+
+                String savePath = destinationDir + File.separatorChar + format + File.separatorChar + fileDownloadName; // Linux
+                // String savePath = "D:\\invoices\\downloaded_invoice.xml"; // Windows
+
+                /**【该下载附件方法也可以用,比较简洁】通过宜搭授权免登地址下载文件至服务器*/
+//                downloadXmlInvoice(fileUrlDownloadToken, savePath);
+
+                String yunpuHost = "https://poc.cloudpure.cn/lemeng/invoice/";//YUNPU临时服务器
+                String lemengHost = "http://112.124.41.243/lemeng/invoice/";//乐檬正式服务器
+
+                String fileNewUrl = lemengHost + format + File.separator + fileDownloadName;
+                System.out.println("服务器访问全地址:" + fileNewUrl);
+
+                //【Linux下面的路径】
+//                filePath = destinationDir + File.separatorChar + format + File.separator + fileDownloadName;
+//                System.out.println("服务器直接访问进行发票解析的地址:" + filePath);
+
+                //开票日期格式转换
+                DateTimeFormatter sourceFormatter = DateTimeFormatter.ofPattern( "yyyy年MM月dd日" );
+                DateTimeFormatter targetFormatter = DateTimeFormatter.ofPattern( "yyyy-MM-dd" );
+
+                /*调用发票识别接口*/
+                try {
+                    JSONObject jsonObject = TencentInvoice.doRecognizeGeneralInvoice(fileNewUrl);
+                    log.info("发票识别结果:" + jsonObject);
+                    JSONArray jsonArray = jsonObject.getJSONArray("MixedInvoiceItems");
+                    if (jsonArray != null && jsonArray.size() > 0) {
+                        log.info("发票识别成功!");
+                        JSONObject jsonObject11 = jsonArray.getJSONObject(0);
+                        int Type = jsonObject11.getInteger("Type");
+                        System.out.println("发票类型编号:" + Type);//示例:16
+                        String SubType = jsonObject11.getString("SubType");
+                        System.out.println("发票类型:" + SubType);//示例:VatElectronicInvoiceFull
+                        String SubTypeDescription = jsonObject11.getString("SubTypeDescription");
+                        System.out.println("-->发票类型描述:" + SubTypeDescription);//示例:电子发票(普通发票)
+                        String TypeDescription = jsonObject11.getString("TypeDescription");
+                        System.out.println("-->票种类型描述:" + TypeDescription);//示例:全电发票
+                        //发票号码
+                        String invoiceNumber = null;
+                        String invoiceDateStr = null;
+                        String stringKPRQ = null;
+                        //开票日期
+                        long invoiceDate = 0;
+                        //发票代码
+                        String invoiceCode = null;
+                        //校验码
+                        String checkCode = null;
+                        //购买方名称
+                        String buyerName = null;
+                        //购买方纳税人识别号
+                        String buyerTaxpayerId = null;
+                        //销售方名称
+                        String sellerName = null;
+                        //销售方纳税人识别号
+                        String sellerTaxpayerId = null;
+                        //含税总额
+                        float totalAmount = 0;
+                        //税额
+                        float taxAmount = 0;
+                        //不含税总额
+                        float amountWithoutTax = 0;
+                        //发票备注
+                        String invoiceRemark = null;
+                        //税率
+                        float taxRate = 0;
+
+                        String state = "等待验真";
+                        String message = null;
+                        if (SubType.equals("VatElectronicInvoiceFull") || SubType.equals("VatElectronicSpecialInvoiceFull")){//NOTE 电子发票(普通发票)、电子发票(专用发票)
+                            JSONObject singleInvoiceInfos = jsonObject11.getJSONObject("SingleInvoiceInfos");
+                            JSONObject VatElectronicInvoiceFull = singleInvoiceInfos.getJSONObject(SubType);
+
+                            if (SubType.equals("VatElectronicSpecialInvoiceFull")){
+                                taxRate = 6;
+                            }
+                            System.out.println("发票税率:" + taxRate);
+                            //发票号码
+                            invoiceNumber = VatElectronicInvoiceFull.getString("Number");
+                            System.out.println("发票号码:" + invoiceNumber);
+                            //开票日期
+                            invoiceDateStr = VatElectronicInvoiceFull.getString("Date");
+                            System.out.println("开票日期:" + invoiceDateStr);
+                            //将开票日期"xxxx年xx月xx日"转换成"xxxx-xx-xx"格式
+                            LocalDate date = LocalDate.parse(invoiceDateStr, sourceFormatter);
+                            stringKPRQ = date.format(targetFormatter);
+                            // 定义格式化器
+                            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+                            // 解析字符串为 LocalDate
+                            LocalDate localDate = LocalDate.parse(invoiceDateStr, formatter);
+                            // 转换为毫秒级时间戳(基于系统默认时区)
+                            invoiceDate = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+                            System.out.println("开票日期时间戳: " + invoiceDate);
+                            //发票代码(该类型发票不存在该字段)
+                            //校验码(该类型发票不存在该字段)
+                            //购买方名称
+                            buyerName = VatElectronicInvoiceFull.getString("Buyer");
+                            System.out.println("购买方名称:" + buyerName);
+                            //购买方纳税人识别号
+                            buyerTaxpayerId = VatElectronicInvoiceFull.getString("BuyerTaxID");
+                            System.out.println("购买方纳税人识别号:" + buyerTaxpayerId);
+                            //销售方名称
+                            sellerName = VatElectronicInvoiceFull.getString("Seller");
+                            System.out.println("销售方名称:" + sellerName);
+                            //销售方纳税人识别号
+                            sellerTaxpayerId = VatElectronicInvoiceFull.getString("SellerTaxID");
+                            System.out.println("销售方纳税人识别号:" + sellerTaxpayerId);
+                            //含税总额(价税合计)
+                            totalAmount = VatElectronicInvoiceFull.getFloat("Total");
+                            System.out.println("含税总额(价税合计):" + totalAmount);
+                            //税额
+                            taxAmount = VatElectronicInvoiceFull.getFloat("Tax");
+                            System.out.println("税额:" + taxAmount);
+                            //不含税总额(税前金额)
+                            amountWithoutTax = VatElectronicInvoiceFull.getFloat("PretaxAmount");
+                            System.out.println("不含税总额(税前金额):" + amountWithoutTax);
+                            //发票备注
+                            invoiceRemark = VatElectronicInvoiceFull.getString("Remark");
+                            System.out.println("发票备注:" + invoiceRemark);
+                        }
+                        if (SubType.equals("ElectronicTrainTicketFull")){//NOTE 电子发票(火车票)
+                            JSONObject singleInvoiceInfos = jsonObject11.getJSONObject("SingleInvoiceInfos");
+                            JSONObject VatElectronicInvoiceFull = singleInvoiceInfos.getJSONObject(SubType);
+                            //税率
+                            taxRate = 9;
+                            System.out.println("发票税率:" + taxRate);
+                            //发票号码
+                            invoiceNumber = VatElectronicInvoiceFull.getString("Number");
+                            System.out.println("发票号码:" + invoiceNumber);
+                            //开票日期
+                            invoiceDateStr = VatElectronicInvoiceFull.getString("Date");
+                            System.out.println("开票日期:" + invoiceDateStr);
+                            //将开票日期"xxxx年xx月xx日"转换成"xxxx-xx-xx"格式
+                            LocalDate date = LocalDate.parse(invoiceDateStr, sourceFormatter);
+                            stringKPRQ = date.format(targetFormatter);
+                            // 定义格式化器
+                            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+                            // 解析字符串为 LocalDate
+                            LocalDate localDate = LocalDate.parse(invoiceDateStr, formatter);
+                            // 转换为毫秒级时间戳(基于系统默认时区)
+                            invoiceDate = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+                            System.out.println("开票日期时间戳: " + invoiceDate);
+                            //发票代码(该类型发票不存在该字段)
+                            //校验码(该类型发票不存在该字段)
+                            //购买方名称
+                            buyerName = VatElectronicInvoiceFull.getString("Buyer");
+                            System.out.println("购买方名称:" + buyerName);
+                            //购买方纳税人识别号
+                            buyerTaxpayerId = VatElectronicInvoiceFull.getString("BuyerTaxID");
+                            System.out.println("购买方纳税人识别号:" + buyerTaxpayerId);
+                            //销售方名称
+                            sellerName = VatElectronicInvoiceFull.getString("Seller") != null ? VatElectronicInvoiceFull.getString("Seller") : null;
+                            System.out.println("销售方名称:" + sellerName);
+                            //销售方纳税人识别号
+                            sellerTaxpayerId = VatElectronicInvoiceFull.getString("SellerTaxID") != null ? VatElectronicInvoiceFull.getString("SellerTaxID") : null;
+                            System.out.println("销售方纳税人识别号:" + sellerTaxpayerId);
+
+                            //含税总额(票价)
+                            totalAmount = VatElectronicInvoiceFull.getFloat("Fare");
+                            System.out.println("含税总额(票价):" + totalAmount);
+                            BigDecimal totalAmountBigDecimal = new BigDecimal(totalAmount);
+
+                            //税额
+                            taxAmount = VatElectronicInvoiceFull.getFloat("Tax") != null ? VatElectronicInvoiceFull.getFloat("Tax") : 0;
+                            System.out.println("税额:" + taxAmount);
+                            //REMARK 针对火车票票面没有不含税金额的情况,通过指定公式计算:火车票含税总额/1.09*0.09
+                            if (taxAmount == 0){
+                                taxAmount = totalAmountBigDecimal.divide(new BigDecimal("1.09"), 2, RoundingMode.HALF_UP).multiply(new BigDecimal("0.09")).floatValue();
+                                System.out.println("「税额」公式计算:火车票含税总额/1.09*0.09:" + taxAmount);
+                            }
+                            //不含税总额(税前金额)
+                            amountWithoutTax = VatElectronicInvoiceFull.getFloat("PretaxAmount") != null ? VatElectronicInvoiceFull.getFloat("PretaxAmount") : 0;
+                            System.out.println("不含税总额(税前金额):" + amountWithoutTax);
+                            if (amountWithoutTax == 0){
+                                //不含税总额=含税总额-税额
+                                amountWithoutTax = totalAmountBigDecimal.subtract(new BigDecimal(taxAmount)).floatValue();
+                                System.out.println("「不含税总额」公式计算:含税总额-税额:" + amountWithoutTax);
+                            }
+
+                            //发票备注
+                            invoiceRemark = VatElectronicInvoiceFull.getString("Remark");
+                            System.out.println("发票备注:" + invoiceRemark);
+                        }
+                        if (SubType.equals("VatElectronicCommonInvoice") || SubType.equals("VatElectronicSpecialInvoice")){//NOTE 增值税电子普通发票、增值税电子专用发票
+                            JSONObject singleInvoiceInfos = jsonObject11.getJSONObject("SingleInvoiceInfos");
+                            JSONObject VatElectronicInvoiceFull = singleInvoiceInfos.getJSONObject(SubType);
+
+                            if (SubType.equals("VatElectronicSpecialInvoice")){
+                                taxRate = 6;
+                            }
+                            System.out.println("发票税率:" + taxRate);
+                            //发票号码
+                            invoiceNumber = VatElectronicInvoiceFull.getString("Number");
+                            System.out.println("发票号码:" + invoiceNumber);
+                            //开票日期
+                            invoiceDateStr = VatElectronicInvoiceFull.getString("Date");
+                            System.out.println("开票日期:" + invoiceDateStr);
+                            //将开票日期"xxxx年xx月xx日"转换成"xxxx-xx-xx"格式
+                            LocalDate date = LocalDate.parse(invoiceDateStr, sourceFormatter);
+                            stringKPRQ = date.format(targetFormatter);
+                            // 定义格式化器
+                            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
+                            // 解析字符串为 LocalDate
+                            LocalDate localDate = LocalDate.parse(invoiceDateStr, formatter);
+                            // 转换为毫秒级时间戳(基于系统默认时区)
+                            invoiceDate = localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+                            System.out.println("开票日期时间戳: " + invoiceDate);
+                            //发票代码
+                            invoiceCode = VatElectronicInvoiceFull.getString("Code");
+                            System.out.println("发票代码: " + invoiceCode);
+                            //校验码
+                            checkCode = VatElectronicInvoiceFull.getString("CheckCode");
+                            System.out.println("校验码: " + checkCode);
+                            //购买方名称
+                            buyerName = VatElectronicInvoiceFull.getString("Buyer");
+                            System.out.println("购买方名称:" + buyerName);
+                            //购买方纳税人识别号
+                            buyerTaxpayerId = VatElectronicInvoiceFull.getString("BuyerTaxID");
+                            System.out.println("购买方纳税人识别号:" + buyerTaxpayerId);
+                            //销售方名称
+                            sellerName = VatElectronicInvoiceFull.getString("Seller");
+                            System.out.println("销售方名称:" + sellerName);
+                            //销售方纳税人识别号
+                            sellerTaxpayerId = VatElectronicInvoiceFull.getString("SellerTaxID");
+                            System.out.println("销售方纳税人识别号:" + sellerTaxpayerId);
+                            //含税总额(价税合计)
+                            totalAmount = VatElectronicInvoiceFull.getFloat("Total");
+                            System.out.println("含税总额(价税合计):" + totalAmount);
+                            //税额
+                            taxAmount = VatElectronicInvoiceFull.getFloat("Tax");
+                            System.out.println("税额:" + taxAmount);
+                            //不含税总额(税前金额)
+                            amountWithoutTax = VatElectronicInvoiceFull.getFloat("PretaxAmount");
+                            System.out.println("不含税总额(税前金额):" + amountWithoutTax);
+                            //发票备注
+                            invoiceRemark = VatElectronicInvoiceFull.getString("Remark");
+                            System.out.println("发票备注:" + invoiceRemark);
+                        }
+
+                        boolean existsInAliBtrip = existsInAliBtrip(
+                                stringKPRQ, invoiceNumber, aliBtripInvoiceNumberCache);
+                        if (!existsInAliBtrip) {
+                            /*发票验真*/
+                            JSONObject jsonObjectYZ = TencentInvoice.doVatInvoiceVerifyNew(TypeDescription, invoiceCode, invoiceNumber, stringKPRQ, String.valueOf(totalAmount), checkCode, String.valueOf(amountWithoutTax), buyerName, buyerTaxpayerId);
+                            state = jsonObjectYZ.getString("state");//验真结果
+                            message = jsonObjectYZ.getString("message");//返回信息
+                        }
+
+                        //逐条封装识别出来的各个发票字段信息
+                        Map formMap = new HashMap<>();
+                        //基础信息
+                        formMap.put("fpsbzt", existsInAliBtrip ? "已存在阿里商旅" : "发票识别完成");//发票识别状态:记录票夹完成、发票识别完成、已存在阿里商旅
+                        formMap.put("yzjg", state);//验真结果:等待验真、验真成功、验真失败
+                        //发票信息
+                        Map fileMap = new HashMap();
+                        fileMap.put("previewUrl",fileNewUrl);
+                        fileMap.put("size",fileSize);
+                        fileMap.put("name",fileDownloadName);
+                        fileMap.put("downloadUrl",fileNewUrl);
+                        fileMap.put("url",fileNewUrl);
+                        List fileList = new ArrayList();
+                        fileList.add(fileMap);
+                        formMap.put("fpyj",fileList);//发票原件
+                        formMap.put("fpyjmc", fileDownloadName);//发票原件名称
+                        //发票信息-识别解析内容
+                        formMap.put("fplx", SubTypeDescription);//发票类型
+                        formMap.put("fphm", invoiceNumber);//发票号码
+                        formMap.put("kprq", invoiceDate);//开票日期
+                        formMap.put("kprqwb", invoiceDateStr);//开票日期文本
+                        formMap.put("pzlx", TypeDescription);//票种类型
+                        formMap.put("fpdm", invoiceCode);//发票代码
+                        formMap.put("jym", checkCode);//校验码
+                        formMap.put("gmfmc", buyerName);//购买方名称
+                        formMap.put("gmfnsrsbh", buyerTaxpayerId);//购买方纳税人识别号
+                        formMap.put("xsfmc", sellerName);//销售方名称
+                        formMap.put("xsfnsrsbh", sellerTaxpayerId);//销售方纳税人识别号
+                        formMap.put("fphsze", totalAmount);//发票含税总额
+                        formMap.put("fpse", taxAmount);//发票税额
+                        formMap.put("fpbhsze", amountWithoutTax);//发票不含税总额
+                        formMap.put("fpbz", invoiceRemark);//发票备注
+                        formMap.put("fpsl", taxRate);//发票税率
+                        //系统信息
+                        formMap.put("wjzcurldz", fileNewUrl);//文件转存url地址
+                        formMap.put("jkfhxx", message);//接口返回信息
+
+                        list.add(formMap);
+                        if (existsInAliBtrip) {
+                            log.info("发票号码 {} 已存在于阿里商旅明细中,跳过当前附件后续处理", invoiceNumber);
+                            continue;
+                        }
+                    }
+                }catch (TencentCloudSDKException e){
+                    log.info("发票识别失败:" + e.getMessage());
+                }
+            } catch (Exception e) {
+                System.err.println("下载失败: " + e.getMessage());
+                e.printStackTrace();
+            }
+        }
+        //转成jsonArray
+        String jsonString = JSON.toJSONString(list);
+        jsonArray_item = JSON.parseArray(jsonString);
+
+        if (jsonArray_item != null && jsonArray_item.size() > 0){
+            log.info("发票解析成功!" + jsonArray_item);
+            return R.ok().message("").data("items", jsonArray_item);
+        }else {
+            log.info("发票解析失败!");
+            return R.error().message("发票解析失败!");
+        }
+    }
+
+    private boolean existsInAliBtrip(String invoiceDate, String invoiceNumber,
+                                     Map<String, Set<String>> invoiceNumberCache) {
+        String normalizedInvoiceNumber = normalizeInvoiceNumber(invoiceNumber);
+        if (invoiceDate == null || normalizedInvoiceNumber == null) {
+            return false;
+        }
+        Set<String> invoiceNumbers = invoiceNumberCache.get(invoiceDate);
+        if (invoiceNumbers == null) {
+            invoiceNumbers = queryAliBtripInvoiceNumbers(invoiceDate);
+            invoiceNumberCache.put(invoiceDate, invoiceNumbers);
+        }
+        return invoiceNumbers.contains(normalizedInvoiceNumber);
+    }
+
+    private Set<String> queryAliBtripInvoiceNumbers(String invoiceDate) {
+        Set<String> invoiceNumbers = new HashSet<>();
+        try {
+            JSONArray trainTickets = aliBtripTrainTicketService.queryByInvoiceDate(
+                    invoiceDate + " 00:00:00", invoiceDate + " 23:59:59");
+            for (int i = 0; i < trainTickets.size(); i++) {
+                addInvoiceNumber(invoiceNumbers,
+                        trainTickets.getJSONObject(i).getString("electronic_ticket_no"));
+            }
+            log.info("按开票日期 {} 查询阿里商旅火车票凭证明细完成,共 {} 条",
+                    invoiceDate, trainTickets.size());
+        } catch (Exception e) {
+            log.error("按开票日期 {} 查询阿里商旅火车票凭证明细失败,不执行该来源的发票去重",
+                    invoiceDate, e);
+        }
+
+        try {
+            JSONArray vatInvoices = aliBtripVatInvoiceService.queryByBillDate(invoiceDate);
+            for (int i = 0; i < vatInvoices.size(); i++) {
+                addInvoiceNumber(invoiceNumbers,
+                        vatInvoices.getJSONObject(i).getString("invoice_no"));
+            }
+            log.info("按开票日期 {} 查询阿里商旅增值税发票明细完成,共 {} 条",
+                    invoiceDate, vatInvoices.size());
+        } catch (Exception e) {
+            log.error("按开票日期 {} 查询阿里商旅增值税发票明细失败,不执行该来源的发票去重",
+                    invoiceDate, e);
+        }
+        return invoiceNumbers;
+    }
+
+    private static void addInvoiceNumber(Set<String> invoiceNumbers, String invoiceNumber) {
+        String normalized = normalizeInvoiceNumber(invoiceNumber);
+        if (normalized != null) {
+            invoiceNumbers.add(normalized);
+        }
+    }
+
+    private static String normalizeInvoiceNumber(String invoiceNumber) {
+        if (invoiceNumber == null) {
+            return null;
+        }
+        String normalized = invoiceNumber.replaceAll("\\s+", "").toUpperCase(Locale.ROOT);
+        return normalized.isEmpty() ? null : normalized;
+    }
+
+    /**
+     * 增值税发票核验(新版)获取发票验真结果
+     */
+    @Override
+    public JSONObject doInvoiceVerification(JSONObject obj) throws TencentCloudSDKException{
+        log.info("传参解析中......");
+        String invoiceKind = obj.getString("invoiceKind");
+        String invoiceCode = obj.getString("invoiceCode");
+        String invoiceNo = obj.getString("invoiceNo");
+        String invoiceDate = obj.getString("invoiceDate");
+        String amount = obj.getString("amount");
+        String checkCode = obj.getString("checkCode");
+        String excludingTax = obj.getString("excludingTax");
+        String formInstId = obj.getString("formInstId");
+
+        String gmfName = obj.getString("gmfName");
+        String gmfNo = obj.getString("gmfNo");
+
+        log.info("接收传递票种类型:" + invoiceKind);
+        log.info("接收传递发票代码:" + invoiceCode);
+        log.info("接收传递发票号码:" + invoiceNo);
+        log.info("接收传递开票日期:" + invoiceDate);
+        log.info("接收传递含税金额:" + amount);
+        log.info("接收传递校验码后6位:" + checkCode);
+        log.info("接收传递不含税金额:" + excludingTax);
+        log.info("接收传递宜搭当前数据实例ID:" + formInstId);
+
+        log.info("接收传递购买方名称:" + gmfName);
+        log.info("接收传递购买方纳税人识别号:" + gmfNo);
+
+        JSONObject jsonObject = TencentInvoice.doVatInvoiceVerifyNew(invoiceKind, invoiceCode, invoiceNo, invoiceDate, amount, checkCode, excludingTax, gmfName, gmfNo);
+        /*更新宜搭表单数据*/
+        Map formMap = new HashMap<>();
+        formMap.put("selectField_mmya15ss", jsonObject.getString("state"));//验真结果
+        formMap.put("textField_mne83ozz", jsonObject.getString("message"));//返回信息
+        String formDataJson = JSON.toJSONString(formMap);
+        System.out.println("mapJson:" + formDataJson);
+        YiDaFormDataV2.updateFormData(formInstId, formDataJson, accessTokenService.getAccessToken());
+
+        return jsonObject;
+    }
+}

+ 25 - 8
main/resources/application.properties

@@ -1,8 +1,31 @@
-server.port=8107
-server.servlet.context-path=/huanshi
+server.port=8102
+server.servlet.context-path=/api/lemeng
 
 server.tomcat.uri-encoding=UTF-8
 
+#YIDA
+myapp.yida.appType=APP_YPF7S88D9O139KRVM83J
+myapp.yida.appSecret=3Z966P91XA1FTYVQ7UTVLDTGJ4613UJUR7ONLT7C
+
+#ALIYUN
+myapp.aliyun.accessKeyId=LTAI5tRXcYFKYuBV3EriBAKm
+myapp.aliyun.accessKeySecret=dWPdO22hTwkH4UoK734J53FLwwvBsY
+
+#TENGXUN
+myapp.tengxun.secretId=AKIDh1HRbZIrDKBnAIBrKZdS1MSJvHPJzRf7
+myapp.tengxun.secretKey=MUN1QmvpUzFG1ZtyrVu52kCQQCk5CWbR
+myapp.tengxun.region=ap-shanghai
+
+#EIC
+#?????http://182.92.202.148:8081/wbjk/api/reimbursement
+#NOTE ?????http://47.111.239.150:8088/wbjk/api/reimbursement
+myapp.eic.eicUrlAddress=http://47.111.239.150:8088/wbjk/api/reimbursement
+
+#?????????
+ribbon:
+    ReadTimeoutMillis: 5000
+    ConnectTimeoutMillis: 5000
+
 ## 服务名
 #spring.application.name=simaner-yd
 ## 环境设置:dev、test、prod(开发环境、测试环境、生产环境)
@@ -15,12 +38,6 @@ server.tomcat.uri-encoding=UTF-8
 #spring.datasource.username=root
 #spring.datasource.password=123456
 
-# 云璞【47.98.130.40服务器】测试数据库
-#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
-#spring.datasource.url=jdbc:mysql://47.97.181.40:3306/dingtalk?serverTimezone=GMT%2B8
-#spring.datasource.username=root
-#spring.datasource.password=cp-root@2022++
-
 #定时同步
 
 #配置日志,当前为默认的控制台输出,也可以用log4j

+ 6 - 0
src/main/resources/application.yml

@@ -0,0 +1,6 @@
+myapp:
+  alibtrip:
+    baseUrl: https://btripopen.alibtrip.com/api
+    corpId: ${ALIBTRIP_CORP_ID:open12v0460q5bvc7v16c26866334a0cfeca}
+    appKey: ${ALIBTRIP_APP_KEY:v0460q541c00}
+    appSecret: ${ALIBTRIP_APP_SECRET:djA0NjBxNTQxYzAwO0xNUC4wcHAiMEZYQ1g2SG9VeC4}

+ 770 - 0
src/test/java/com/muzhi/lemeng/APITest/Test1.java

@@ -0,0 +1,770 @@
+package com.muzhi.lemeng.APITest;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.dingtalkyida_1_0.models.GetOpenUrlResponse;
+import com.aliyun.dingtalkyida_1_0.models.SearchFormDatasResponse;
+import com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataResponse;
+import com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDResponse;
+import com.aliyun.ocr_api20210707.Client;
+import com.aliyun.ocr_api20210707.models.RecognizeAllTextRequest;
+import com.aliyun.ocr_api20210707.models.RecognizeAllTextResponse;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teaopenapi.models.Config;
+import com.aliyun.teautil.models.RuntimeOptions;
+import com.dingtalk.api.DefaultDingTalkClient;
+import com.dingtalk.api.request.OapiGettokenRequest;
+import com.dingtalk.api.response.OapiGettokenResponse;
+import com.muzhi.lemeng.common.alibaba.AliyunInvoice;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormData;
+import com.muzhi.lemeng.common.dingtalkyida.YiDaFormDataV2;
+import com.muzhi.lemeng.service.AccessTokenService;
+import com.taobao.api.ApiException;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@SpringBootTest
+@Slf4j
+public class Test1 {
+
+    @Autowired
+    private AccessTokenService accessTokenService;
+
+    public static com.aliyun.dingtalkworkflow_1_0.Client createClient() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkworkflow_1_0.Client(config);
+    }
+
+    public static com.aliyun.dingtalkyida_2_0.Client createClient2() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkyida_2_0.Client(config);
+    }
+
+    public static com.aliyun.dingtalkyida_1_0.Client createClient3() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
+        config.protocol = "https";
+        config.regionId = "central";
+        return new com.aliyun.dingtalkyida_1_0.Client(config);
+    }
+
+    @Test
+    public void test1(){
+        JSONObject jsonObject2 = null;
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClient2();
+            com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataHeaders createOrUpdateFormDataHeaders = new com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataHeaders();
+            createOrUpdateFormDataHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataRequest createOrUpdateFormDataRequest = new com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataRequest()
+                    .setSystemToken("JB966E91RQZUKPPFFNOKBB86G3GH38VSN42AMGV")
+                    .setFormUuid("FORM-BD31E2C07C7448DD931F686A5D881E96DQV8")
+                    .setUserId("yida_pub_account")
+                    .setSearchCondition("[{\"componentName\":\"TextField\",\"type\":\"TEXT\",\"value\":\"BC2000029\",\"key\":\"textField_maw7b9k0\",\"operator\":\"eq\"}]")
+                    .setAppType("APP_FJSLK9BJ20K1NL3G2PUW")
+                    .setFormDataJson("{\"selectField_lqesy4t1\":\"D\",\"textField_maw7b9k0\":\"BC2000029\",\"textField_lqesy4sy\":\"KETTLER Alu-Rad GmbH\",\"textField_mh8j17hy\":\"袁铷玥\"}");
+
+            CreateOrUpdateFormDataResponse orUpdateFormDataWithOptions = client.createOrUpdateFormDataWithOptions(createOrUpdateFormDataRequest, createOrUpdateFormDataHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(orUpdateFormDataWithOptions.getBody());
+            jsonObject2 = JSON.parseObject(s);
+            System.out.println("【创建或更新】表单object对象:" + jsonObject2);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+    }
+
+    @Test
+    public void test2(){
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClient2();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders getFormDataByIDHeaders = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders();
+            getFormDataByIDHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest getFormDataByIDRequest = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest()
+                    .setSystemToken("JB966E91RQZUKPPFFNOKBB86G3GH38VSN42AMGV")
+                    .setUserId("yida_pub_account")
+                    .setAppType("APP_FJSLK9BJ20K1NL3G2PUW");
+
+            GetFormDataByIDResponse formDataByIDWithOptions = client.getFormDataByIDWithOptions("FINST-R8E66381FS30I83TKR2W4DO14O8V35TY6QBHM5NH", getFormDataByIDRequest, getFormDataByIDHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(formDataByIDWithOptions.getBody());
+            JSONObject jsonObject = JSON.parseObject(s);
+            System.out.println("【查询】表单object对象:" + jsonObject);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        }
+    }
+
+    @Test
+    public void test3(){
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClient2();
+            com.aliyun.dingtalkyida_2_0.models.UpdateSubTableHeaders updateSubTableHeaders = new com.aliyun.dingtalkyida_2_0.models.UpdateSubTableHeaders();
+            updateSubTableHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_2_0.models.UpdateSubTableRequest updateSubTableRequest = new com.aliyun.dingtalkyida_2_0.models.UpdateSubTableRequest()
+                    .setSystemToken("JB966E91RQZUKPPFFNOKBB86G3GH38VSN42AMGV")
+                    .setFormInstanceId("FINST-R8E66381FS30I83TKR2W4DO14O8V35TY6QBHM5NH")
+                    .setUserId("yida_pub_account")
+                    .setTableFieldIds("tableField_lqesy4t4")
+                    .setUpdateFormDataJson("{     \"tableField_lqesy4t4\": [         {             \"textField_makozait\": \"0519-68198299\",             \"textField_lqesy4t7\": \"\",             \"textField_lqesy4t6\": \"\"         }     ] }")
+                    .setAppType("APP_FJSLK9BJ20K1NL3G2PUW");
+
+            client.updateSubTableWithOptions(updateSubTableRequest, updateSubTableHeaders, new com.aliyun.teautil.models.RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        }
+    }
+
+    @Test
+    public void test4(){
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient3();
+            com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders getOpenUrlHeaders = new com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders();
+            getOpenUrlHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest getOpenUrlRequest = new com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest()
+                    .setSystemToken("333")
+                    .setFileUrl("555")
+                    .setUserId("444");
+
+            client.getOpenUrlWithOptions("222", getOpenUrlRequest, getOpenUrlHeaders, new com.aliyun.teautil.models.RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+    }
+
+    @Test
+    public void test5(){
+
+        String url = "https://www.aliwork.com/ossFileHandle?appType=APP_XV6ZGK1162TI1Z1FE17D&fileName=APP_XV6ZGK1162TI1Z1FE17D_MTU3MzExMjA5XzMyRDY2Q0ExNEY2MUtLT0RIMDRIWTNDM0pLRjIySVhLOUtSSU1NNA$$.xml&instId=&type=download";
+
+        String fileUrl = "";
+        JSONObject jsonObject = null;
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient3();
+            com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders getOpenUrlHeaders = new com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders();
+            getOpenUrlHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest getOpenUrlRequest = new com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest()
+                    .setSystemToken("USC66V61YG8ZMBM5E8JQFBZQ6W0G3V1L6J0GMXE")
+                    .setFileUrl(url)//宜搭附件url
+                    .setUserId("yida_pub_account")
+                    .setTimeout(86400000L);//最大值: 86400000 , 即24小时。不填则默认60000,即1分钟。填0或者负数无效。
+
+            GetOpenUrlResponse appA1U3R89IHBD21BJZVX06 = client.getOpenUrlWithOptions("APP_XV6ZGK1162TI1Z1FE17D", getOpenUrlRequest, getOpenUrlHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(appA1U3R89IHBD21BJZVX06.getBody());
+            jsonObject = JSON.parseObject(s);
+            System.out.println("获取宜搭附件免登地址:" + jsonObject);
+
+            fileUrl = jsonObject.getString("result");
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        System.out.println("获取宜搭附件免登地址:" + fileUrl);
+    }
+
+    @Test
+    public void test6(){
+        String instanceId = "FINST-L3D66Z8197X1EX7NHEMMCDBF7JJV26UGWBZJMOWH";
+        JSONObject jsonObject = null;
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClient2();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders getFormDataByIDHeaders = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders();
+            getFormDataByIDHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest getFormDataByIDRequest = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest()
+                    .setSystemToken("USC66V61YG8ZMBM5E8JQFBZQ6W0G3V1L6J0GMXE")
+                    .setUserId("yida_pub_account")
+                    .setAppType("APP_XV6ZGK1162TI1Z1FE17D");
+            GetFormDataByIDResponse formDataByIDWithOptions = client.getFormDataByIDWithOptions(instanceId, getFormDataByIDRequest, getFormDataByIDHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(formDataByIDWithOptions.getBody());
+            jsonObject = JSON.parseObject(s);
+//            System.out.println("宜搭实例id查询【工时填报】接口object对象:" + jsonObject);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+        JSONObject formData = jsonObject.getJSONObject("formData");
+        System.out.println("宜搭实例id查询【工时填报】接口formData对象:" + formData);
+        if (formData != null){
+            //填报单号
+            String tianbaodanhao = formData.getString("serialNumberField_lmiltutx");
+            System.out.println("填报单号:" + tianbaodanhao);
+            //填报日期
+            long tianbaoriqi = formData.getLong("dateField_mgt1sluh");
+            System.out.println("填报日期:" + tianbaoriqi);
+            //填报总工时
+            float tianbaozonggongshi = formData.getFloat("numberField_llunq7e2") != null ? formData.getFloat("numberField_llunq7e2") : 0;
+            System.out.println("填报总工时:" + tianbaozonggongshi);
+            //填报人员userid
+            String tianbaorenyuanUserId = formData.getJSONArray("employeeField_mhiv3pjk_id").getString(0);
+            System.out.println("填报人员userid:" + tianbaorenyuanUserId);
+            //当日已填报总工时
+            float dangriyitianbaozonggongshi = formData.getFloat("numberField_mhiun888") != null ? formData.getFloat("numberField_mhiun888") : 0;
+            System.out.println("填报人员userid:" + dangriyitianbaozonggongshi);
+            //工时填报明细
+            JSONArray jsonArray = formData.getJSONArray("tableField_mhbs93eh");
+            for (int i = 0; i < jsonArray.size(); i++) {
+                JSONObject jsonObject1 = jsonArray.getJSONObject(i);
+                //工时填报明细-总工时
+                float mx_zonggongshi = jsonObject1.getFloat("numberField_mhbs93e3") != null ? jsonObject1.getFloat("numberField_mhbs93e3") : 0;
+                System.out.println("工时填报明细-总工时:" + mx_zonggongshi);
+                //工时填报明细-已填报工时
+                float mx_yitianbaogongshi = jsonObject1.getFloat("numberField_mhbs93e4") != null ? jsonObject1.getFloat("numberField_mhbs93e4") : 0;
+                System.out.println("工时填报明细-已填报工时:" + mx_yitianbaogongshi);
+                //工时填报明细-填报总工时
+                float mx_tianbaozonggongshi = jsonObject1.getFloat("numberField_mhbs93e5") != null ? jsonObject1.getFloat("numberField_mhbs93e5") : 0;
+                System.out.println("工时填报明细-填报总工时:" + mx_tianbaozonggongshi);
+
+                //工时填报明细-类型
+                String mx_leixing = jsonObject1.getString("radioField_mhbs93e6");
+                System.out.println("工时填报明细-类型:" + mx_leixing);
+                //工时填报明细-负责人userId
+                String mx_fuzerenUserId = jsonObject1.getJSONArray("employeeField_mhbs93e9_id").getString(0);
+                System.out.println("工时填报明细-负责人userId:" + mx_fuzerenUserId);
+                //工时填报明细-编号
+                String mx_bianhao = jsonObject1.getString("textField_mhbs93ea");
+                System.out.println("工时填报明细-编号:" + mx_bianhao);
+                //工时填报明细-客户名称
+                String mx_kehumingcheng = jsonObject1.getString("textField_mhbs93eb");
+                System.out.println("工时填报明细-客户名称:" + mx_kehumingcheng);
+                //工时填报明细-工时
+                float mx_gongshi = jsonObject1.getFloat("numberField_mhbs93ec") != null ? jsonObject1.getFloat("numberField_mhbs93ec") : 0;
+                System.out.println("工时填报明细-工时:" + mx_gongshi);
+                //工时填报明细-工作内容
+                String mx_gongzuoneirong = jsonObject1.getString("textareaField_mhbs93ed");
+                System.out.println("工时填报明细-工作内容:" + mx_gongzuoneirong);
+                //工时填报明细-备注
+                String mx_beizhu = jsonObject1.getString("textareaField_mhbs93ee");
+                System.out.println("工时填报明细-备注:" + mx_beizhu);
+            }
+        }
+    }
+
+    //TODO 指定代收邮箱地址
+    public static String emailAddress = "postmaster@fnsx.xyz";
+
+    /*阿里邮箱列出邮件的全部附件*/
+    @Test
+    public void test000() {
+        String emailId = "DzzzzzzNqZC";//邮件id
+        JSONArray jsonArray = new JSONArray();
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("text/plain");
+            Request request = new Request.Builder()
+                    .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/"+ emailId +"/attachments")
+                    .method("GET", null)
+                    .addHeader("Authorization", "Bearer " + accessTokenService.getAlibabaMailboxAccessToken())
+                    .build();
+            Response response = client.newCall(request).execute();
+            String resStr = response.body().string();
+            JSONObject jsonObject = JSONObject.parseObject(resStr);
+            jsonArray = jsonObject.getJSONArray("attachments") != null ? jsonObject.getJSONArray("attachments") : new JSONArray();
+            System.out.println("列出邮件的发票附件列表[]:" + jsonArray);
+        }catch (Exception e){
+            log.error("该邮件的发票附件获取异常:{}", e);
+        }
+    }
+
+    /*阿里邮箱文件下载*/
+    @Test
+    public void test7() {
+
+        String emailId = "DzzzzzzNqZC";//邮件id
+        String emailFileId = "8319daf3-7388-49f9-87fc-2bd8818179f5";//邮件附件id
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("text/plain");
+            Request request = new Request.Builder()
+                    .url("https://alimail-cn.aliyuncs.com/v2/users/"+ emailAddress +"/messages/"+ emailId +"/attachments/"+ emailFileId +"/$value")
+                    .method("GET", null)
+                    .addHeader("Authorization", "Bearer " + accessTokenService.getAlibabaMailboxAccessToken())
+                    .build();
+            try (Response response = client.newCall(request).execute()) {
+                if (!response.isSuccessful()) {
+                    System.err.println("下载失败: " + response.code() + " - " + response.message());
+                    // 打印错误 body(可能是 JSON 错误信息)
+                    if (response.body() != null) {
+                        System.err.println("Error body: " + response.body().string());
+                    }
+                    return;
+                }
+
+                String destinationDir = "invoice"; // Linux 路径
+
+                //获取当前日期,用于创建文件保存目录中增加当前日期的文件夹作区分
+                SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");//设置日期格式 yyyy-MM-dd HH:mm:ss
+                String format = df.format(new Date());
+                System.out.println(format);// new Date()为获取当前系统时间
+
+                String fileName = "测试发票-001.pdf";
+
+                String path = destinationDir + File.separatorChar + format + File.separatorChar + fileName;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                // 确保目录存在
+                Path dir = Paths.get(path);
+                Files.createDirectories(dir);
+
+                // 构建完整文件路径
+                Path filePath = dir.resolve("测试发票-001.pdf");
+
+                // 从响应流直接写入文件(高效,不加载全内存)
+                try (ResponseBody body = response.body()) {
+                    if (body == null) {
+                        throw new IOException("响应体为空");
+                    }
+                    Files.copy(body.byteStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
+                }
+                System.out.println("✅ 附件已保存至: " + filePath.toAbsolutePath());
+            } catch (IOException e) {
+                System.err.println("下载异常: " + e.getMessage());
+                e.printStackTrace();
+            }
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void test8() {
+
+        String email = "test@qq.com";
+        String fileName = "测试发票-001.pdf";
+        float fileSize = 1024;
+        String fileNewUrl = "https://poc.cloudpure.cn/changguchuan/invoice/2026-02-05/25317000003383776933-pdf.pdf";
+
+        Map formMap = new HashMap<>();
+        formMap.put("textField_ml9154m5", fileName);//发票文件名称
+        formMap.put("textField_ml7hkag3", email);//收件人邮箱地址
+        formMap.put("textField_ml9154ma", fileNewUrl);//文件转存url地址
+
+        Map fileMap = new HashMap();
+        fileMap.put("previewUrl",fileNewUrl);
+        fileMap.put("size",fileSize);
+        fileMap.put("name",fileName);
+        fileMap.put("downloadUrl",fileNewUrl);
+        fileMap.put("url",fileNewUrl);
+        List fileList = new ArrayList();
+        fileList.add(fileMap);
+        formMap.put("attachmentField_ml9154mb",fileList);
+
+        String formDataJson = JSON.toJSONString(formMap);
+        System.out.println("mapJson:" + formDataJson);
+
+        Map searchMap = new HashMap<>();
+        searchMap.put("key", "textField_ml9154m5");
+        searchMap.put("value", fileName);
+        searchMap.put("type", "TEXT");
+        searchMap.put("operator", "eq");
+        searchMap.put("componentName", "TextField");
+        List searchList = new ArrayList();
+        searchList.add(searchMap);
+        String searchCondition = JSON.toJSONString(searchList);
+        System.out.println("查询条件searchCondition:" + searchCondition);
+        /*新增或更新到宜搭*/
+        log.info("开始新增或更新到宜搭「票夹」表单--->");
+        YiDaFormData.CreateOrUpdateFormData(
+                "FORM-036D3F7CB4CC44E7AE7846A789C3DA65EOJN",
+                searchCondition,
+                formDataJson,
+                accessTokenService.getAccessToken());
+    }
+
+    /**钉钉应用*/
+    //APPKEY
+    private static String APPKEY = "dingzupxqpzcczaaddcw";
+    //APPSECRET
+    private static String APPSECRET = "EpND2Oj4adk2Od9ZwAszSYGC8WL_OWYPq1Uj7b5jR1DI6rCsWTdxzvqTHM4H3Dz6";
+    //AgenthId:
+    //corpId:
+
+    public static String DingtalkAccessToken = null;
+    @Test
+    public void test9() {
+
+        DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
+        OapiGettokenRequest request = new OapiGettokenRequest();
+        request.setAppkey(APPKEY);
+        request.setAppsecret(APPSECRET);
+        request.setHttpMethod("GET");
+        try {
+            OapiGettokenResponse response = client.execute(request);
+            DingtalkAccessToken = response.getAccessToken();
+            System.out.println("乐檬钉钉AccessToken:" + DingtalkAccessToken);
+        } catch (ApiException e) {
+            e.printStackTrace();
+        }
+    }
+
+//    /**
+//     * <b>description</b> :
+//     * <p>使用凭据初始化账号Client</p>
+//     * @return Client
+//     *
+//     * @throws Exception
+//     */
+//    public static com.aliyun.ocr_api20210707.Client createClientOCR() throws Exception {
+//        // 工程代码建议使用更安全的无AK方式,凭据配置方式请参见:https://help.aliyun.com/document_detail/378657.html。
+//        com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();
+//        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+//                .setCredential(credential);
+//        // Endpoint 请参考 https://api.aliyun.com/product/ocr-api
+//        config.endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
+//        return new com.aliyun.ocr_api20210707.Client(config);
+//    }
+//
+//    //OCR识别
+//    @Test
+//    public void test10() {
+//        try {
+//            com.aliyun.ocr_api20210707.Client client = createClientOCR();
+//            com.aliyun.ocr_api20210707.models.RecognizeAllTextRequest recognizeAllTextRequest = new com.aliyun.ocr_api20210707.models.RecognizeAllTextRequest()
+//                    .setType("Advanced")
+//                    .setUrl("https://poc.cloudpure.cn/changguchuan/invoice/2026-02-05/26329216804000097816.pdf");
+//            com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
+//            com.aliyun.ocr_api20210707.models.RecognizeAllTextResponse resp = client.recognizeAllTextWithOptions(recognizeAllTextRequest, runtime);
+//            System.out.println(new com.google.gson.Gson().toJson(resp));
+//        } catch (TeaException error) {
+//            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
+//            // 错误 message
+//            System.out.println(error.getMessage());
+//            // 诊断地址
+//            System.out.println(error.getData().get("Recommend"));
+//        } catch (Exception _error) {
+//            TeaException error = new TeaException(_error.getMessage(), _error);
+//            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
+//            // 错误 message
+//            System.out.println(error.getMessage());
+//            // 诊断地址
+//            System.out.println(error.getData().get("Recommend"));
+//        }
+//    }
+
+    /**
+     * 创建 OCR 客户端
+     * @param accessKeyId 阿里云 AccessKey ID
+     * @param accessKeySecret 阿里云 AccessKey Secret
+     * @return Client
+     * @throws Exception
+     */
+    public static Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
+        Config config = new Config()
+                // 必填,您的 AccessKey ID
+                .setAccessKeyId(accessKeyId)
+                // 必填,您的 AccessKey Secret
+                .setAccessKeySecret(accessKeySecret);
+        // Endpoint (地域节点),OCR 统一识别通常使用 cn-shanghai
+        config.endpoint = "ocr-api.cn-hangzhou.aliyuncs.com";
+        return new Client(config);
+    }
+
+    @Test
+    public void test11() {
+        // 1. 替换为你的阿里云密钥
+        String accessKeyId = "LTAI5tRXcYFKYuBV3EriBAKm";
+        String accessKeySecret = "dWPdO22hTwkH4UoK734J53FLwwvBsY";
+
+        try {
+            // 2. 初始化客户端
+            Client client = createClient(accessKeyId, accessKeySecret);
+
+            // 3. 构建请求对象
+            RecognizeAllTextRequest request = new RecognizeAllTextRequest();
+
+            // 设置请求参数
+            // 方式一:传图片 URL (推荐,简单)
+            request.setUrl("https://poc.cloudpure.cn/changguchuan/invoice/2026-02-05/26329216804000097816.pdf");
+
+            // 方式二:传本地图片二进制流 (需要自行读取文件)
+            // byte[] imageBytes = Files.readAllBytes(Paths.get("path/to/your/image.jpg"));
+            // request.setBody(TeaUtil.toReadableByteChannel(imageBytes));
+
+            // 必填参数:指定图片类型
+            // Advanced = 通用文字识别高精版
+            request.setType("Advanced");
+
+            // 可选参数:是否返回坐标
+            // request.setOutputCoordinate("points");
+
+            // 4. 设置运行时选项(如超时)
+            RuntimeOptions runtime = new RuntimeOptions();
+            runtime.readTimeout = 10000; // 10秒读超时
+            runtime.connectTimeout = 5000; // 5秒连接超时
+
+            // 5. 发起调用
+            // RecognizeAllText 是 API 方法名
+            RecognizeAllTextResponse response = client.recognizeAllTextWithOptions(request, runtime);
+
+            // 6. 处理结果
+            if (response.getBody() != null && response.getBody().getData() != null) {
+                System.out.println("识别成功!");
+                System.out.println("图片宽度: " + response.getBody().getData().getWidth());
+                System.out.println("图片高度: " + response.getBody().getData().getHeight());
+                System.out.println("识别内容: " + response.getBody().getData().getContent());
+
+                // 详细结果在 SubImages 或 BlockInfo 中
+                // 具体结构请参考文档中的 JSON 结构
+            } else {
+                System.out.println("识别失败,无数据返回");
+            }
+
+        } catch (TeaException e) {
+            // 阿里云 SDK 异常
+            System.err.println("ErrorCode: " + e.getCode());
+            System.err.println("Message: " + e.getMessage());
+            System.err.println("Data: " + e.getData());
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void test12() {
+        // 指定图片 URL (支持公网可访问的图片链接)
+        // 请替换为你的实际发票图片 URL
+        String imageUrl = "https://poc.cloudpure.cn/changguchuan/invoice/2026-02-05/26329216804000097816.pdf";
+
+        // 1. 执行 OCR 识别
+        System.out.println("第一步:开始 OCR 识别...");
+        try {
+            RecognizeAllTextResponse recognizeResponse = AliyunInvoice.performOcrRecognition(imageUrl);
+            if (recognizeResponse.getBody() == null ||
+                    recognizeResponse.getBody().getData() == null ||
+                    recognizeResponse.getBody().getData().getSubImages() == null) {
+                System.out.println("OCR识别失败或未检测到发票信息");
+            }else {
+                // 2. 解析 OCR 结果
+
+            }
+        }catch (Exception e){
+            System.out.println("识别失败");
+        }
+    }
+
+    @Test
+    public void test13(){
+        String accessToken = null;
+
+        String grant_type = "client_credentials";//固定值
+        String client_id = "tlHiqXxwX6jVFEKs";//应用id
+        String client_secret = "BI9O3IvEBJVYuKC55hEuxkwkpHTdheh8PUSipwBreHkEWq0xDc0rOUQ9pM9lktuE";//应用密钥
+        String bodyStr = "grant_type="+ grant_type +"&client_id="+ client_id +"&client_secret=" + client_secret;
+        try {
+            OkHttpClient client = new OkHttpClient().newBuilder()
+                    .build();
+            MediaType mediaType = MediaType.parse("application/x-www-form-urlencoded");
+            RequestBody body = RequestBody.create(mediaType, bodyStr);
+            Request request = new Request.Builder()
+                    .url("https://alimail-cn.aliyuncs.com/oauth2/v2.0/token")
+                    .method("POST", body)
+                    .addHeader("Content-Type", "application/x-www-form-urlencoded")
+                    .build();
+            Response response = client.newCall(request).execute();
+            String resStr = response.body().string();
+            JSONObject jsonObject = JSON.parseObject(resStr);
+            accessToken = jsonObject.getString("access_token");
+            System.out.println("阿里企业邮箱的access_token:" + accessToken);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void test14(){
+
+        String email = "zhuxiaojian@cnlemon.net";
+
+        //查询表单实例数据
+        Map mapAll = new HashMap();
+        //邮箱地址
+        mapAll.put("textField_mob3qfjb", email);
+        String mapAllStr = JSON.toJSONString(mapAll);
+        System.out.println("封装查询的字段参数:" + mapAllStr);
+
+        JSONObject jsonObject = new JSONObject();
+
+        int size = 100;
+        int page = 1;
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient3();
+            com.aliyun.dingtalkyida_1_0.models.SearchFormDatasHeaders searchFormDatasHeaders = new com.aliyun.dingtalkyida_1_0.models.SearchFormDatasHeaders();
+            searchFormDatasHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_1_0.models.SearchFormDatasRequest searchFormDatasRequest = new com.aliyun.dingtalkyida_1_0.models.SearchFormDatasRequest()
+                    .setSystemToken("3Z966P91XA1FTYVQ7UTVLDTGJ4613UJUR7ONLT7C")
+
+                    .setSearchFieldJson(mapAllStr)
+
+                    .setCurrentPage(page)
+                    .setPageSize(size)
+                    .setFormUuid("FORM-E489F03D805D4E169BA6A8755B4E72E6TN6N")
+                    .setUserId("yida_pub_account")
+                    .setAppType("APP_YPF7S88D9O139KRVM83J");
+
+            SearchFormDatasResponse searchFormDatasResponse = client.searchFormDatasWithOptions(searchFormDatasRequest, searchFormDatasHeaders, new RuntimeOptions());
+//            System.out.println(JSON.toJSONString(searchFormDatasResponse.getBody()));
+            String s = JSON.toJSONString(searchFormDatasResponse.getBody());
+            jsonObject = JSON.parseObject(s);
+            System.out.println("宜搭数据高级条件查询到接口object对象:" + jsonObject);
+            JSONArray data = jsonObject.getJSONArray("data");
+            if (data.isEmpty()) {
+                System.out.println("根据邮箱地址查询【用户信息】返回数据为空!");
+            } else {
+                JSONObject formData = data.getJSONObject(0).getJSONObject("formData");
+                //成员userId
+                String emailUserId = formData.getJSONArray("employeeField_mm1g5703_id").getString(0);
+                System.out.println("成员userId===>" + emailUserId);
+            }
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+    }
+
+    private static String appType;
+    private static String appSecret;
+
+    // 创建一个非静态的 Setter 方法,加上 @Value
+    // Spring 启动时会调用这个方法,将配置文件值传入
+    @Value("${myapp.yida.appType}")
+    public void setAppType(String value) {
+        Test1.appType = value; // 赋值给静态变量
+    }
+    @Value("${myapp.yida.appSecret}")
+    public void setAppSecret(String value) {
+        Test1.appSecret = value; // 赋值给静态变量
+    }
+
+    @Test
+    public void test15(){
+
+        String shiliID = "FINST-YOH665D1Z7B7A7Z1KVM0RBNP94O12UL8BHFRM9ME";
+        String formDataJson = "{\"selectField_mmya15ss\":\"验真成功\",\"textField_mne83ozz\":\"✅发票验真成功!执行时间:2026-07-20 11:45:27\"}";
+
+        try {
+            /**
+             * 更新表单数据「按照数据版本」
+             * 注:非表单字段调整不开启
+             * */
+
+            com.aliyun.dingtalkyida_1_0.Client client = createClient3();
+            com.aliyun.dingtalkyida_1_0.models.UpdateFormDataHeaders updateFormDataHeaders = new com.aliyun.dingtalkyida_1_0.models.UpdateFormDataHeaders();
+            updateFormDataHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_1_0.models.UpdateFormDataRequest updateFormDataRequest = new com.aliyun.dingtalkyida_1_0.models.UpdateFormDataRequest()
+                    .setSystemToken(appSecret)
+                    .setFormInstanceId(shiliID)
+                    .setUserId("yida_pub_account")
+                    .setUseLatestVersion(true)
+                    .setAppType(appType)
+                    .setUpdateFormDataJson(formDataJson);
+
+            client.updateFormDataWithOptions(updateFormDataRequest, updateFormDataHeaders, new RuntimeOptions());
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+        }
+    }
+
+    @Test
+    public void test16(){
+
+        String shiliID = "21ba66c6-0257-4530-b35c-87e596826baf";
+
+        JSONObject jsonObject = null;
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClient2();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders getFormDataByIDHeaders = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDHeaders();
+            getFormDataByIDHeaders.xAcsDingtalkAccessToken = accessTokenService.getAccessToken();
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest getFormDataByIDRequest = new com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDRequest()
+                    .setSystemToken(appSecret)
+                    .setUserId("yida_pub_account")
+                    .setAppType(appType);
+            com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDResponse formDataByIDWithOptions = client.getFormDataByIDWithOptions(shiliID, getFormDataByIDRequest, getFormDataByIDHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(formDataByIDWithOptions.getBody());
+            jsonObject = JSON.parseObject(s);
+//            System.out.println("获取宜搭表单数据,表单object对象:" + jsonObject);
+        } catch (TeaException err) {
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        } catch (Exception _err) {
+            TeaException err = new TeaException(_err.getMessage(), _err);
+            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
+                // err 中含有 code 和 message 属性,可帮助开发定位问题
+            }
+
+        }
+        System.out.println("获取宜搭表单数据,表单object对象:" + jsonObject);
+    }
+}

+ 1 - 1
test/java/com/muzhi/huanshi/TestApplicationTests.java

@@ -1,4 +1,4 @@
-package com.muzhi.huanshi;
+package com.muzhi.lemeng;
 
 import org.junit.jupiter.api.Test;
 import org.springframework.boot.test.context.SpringBootTest;

+ 159 - 0
src/test/java/com/muzhi/lemeng/service/AliBtripInvoiceServiceTest.java

@@ -0,0 +1,159 @@
+package com.muzhi.lemeng.service;
+
+import com.alibaba.fastjson.JSONArray;
+import com.muzhi.lemeng.service.impl.AliBtripTrainTicketServiceImpl;
+import com.muzhi.lemeng.service.impl.AliBtripVatInvoiceServiceImpl;
+import com.muzhi.lemeng.service.impl.InvoiceServiceImpl;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class AliBtripInvoiceServiceTest {
+
+    private HttpServer server;
+    private String baseUrl;
+
+    @BeforeEach
+    void setUp() throws IOException {
+        server = HttpServer.create(new InetSocketAddress(0), 0);
+        server.start();
+        baseUrl = "http://127.0.0.1:" + server.getAddress().getPort() + "/api";
+    }
+
+    @AfterEach
+    void tearDown() {
+        server.stop(0);
+    }
+
+    @Test
+    void trainTicketQueryLoadsEveryPageWithInvoiceDateRange() {
+        AtomicInteger requestCount = new AtomicInteger();
+        server.createContext("/api/scan/v1/train-ticket", exchange -> {
+            Map<String, String> query = parseQuery(exchange);
+            assertEquals("2026-10-11 00:00:00", query.get("invoice_date_start"));
+            assertEquals("2026-10-17 23:59:59", query.get("invoice_date_end"));
+            assertEquals("100", query.get("page_size"));
+            assertEquals("test-app-key", query.get("app_key"));
+            assertEquals("test-token", query.get("corp_token"));
+            int pageNo = Integer.parseInt(query.get("page_no"));
+            requestCount.incrementAndGet();
+            String items = pageNo == 1
+                    ? "[{\"electronic_ticket_no\":\"T1\"},{\"electronic_ticket_no\":\"T2\"}]"
+                    : "[{\"electronic_ticket_no\":\"T3\"}]";
+            writeJson(exchange, page(items, pageNo, 3, 2));
+        });
+
+        AliBtripTrainTicketServiceImpl service = new AliBtripTrainTicketServiceImpl();
+        configure(service);
+        JSONArray result = service.queryByInvoiceDate("2026-10-11 00:00:00", "2026-10-17 23:59:59");
+
+        assertEquals(2, requestCount.get());
+        assertEquals(3, result.size());
+        assertEquals("T3", result.getJSONObject(2).getString("electronic_ticket_no"));
+    }
+
+    @Test
+    void vatInvoiceQueryLoadsEveryPageWithBillDate() {
+        AtomicInteger requestCount = new AtomicInteger();
+        server.createContext("/api/scan/v1/vat-invoice", exchange -> {
+            Map<String, String> query = parseQuery(exchange);
+            assertEquals("2026-10-01", query.get("bill_date"));
+            assertEquals("100", query.get("page_size"));
+            int pageNo = Integer.parseInt(query.get("page_no"));
+            requestCount.incrementAndGet();
+            String items = pageNo == 1
+                    ? "[{\"invoice_no\":\"V1\"},{\"invoice_no\":\"V2\"}]"
+                    : "[{\"invoice_no\":\"V3\"}]";
+            writeJson(exchange, page(items, pageNo, 3, 2));
+        });
+
+        AliBtripVatInvoiceServiceImpl service = new AliBtripVatInvoiceServiceImpl();
+        configure(service);
+        JSONArray result = service.queryByBillDate("2026-10-01");
+
+        assertEquals(2, requestCount.get());
+        assertEquals(3, result.size());
+        assertEquals("V3", result.getJSONObject(2).getString("invoice_no"));
+    }
+
+    @Test
+    void invoiceOcrLookupUsesInvoiceDateAndCachesBothSources() {
+        AtomicInteger trainRequestCount = new AtomicInteger();
+        AtomicInteger vatRequestCount = new AtomicInteger();
+        InvoiceServiceImpl service = new InvoiceServiceImpl();
+        ReflectionTestUtils.setField(service, "aliBtripTrainTicketService",
+                (AliBtripTrainTicketService) (start, end) -> {
+                    assertEquals("2026-10-01 00:00:00", start);
+                    assertEquals("2026-10-01 23:59:59", end);
+                    trainRequestCount.incrementAndGet();
+                    return JSONArray.parseArray("[{\"electronic_ticket_no\":\" ab 12 \"}]");
+                });
+        ReflectionTestUtils.setField(service, "aliBtripVatInvoiceService",
+                (AliBtripVatInvoiceService) billDate -> {
+                    assertEquals("2026-10-01", billDate);
+                    vatRequestCount.incrementAndGet();
+                    return JSONArray.parseArray("[{\"invoice_no\":\"vat-34\"}]");
+                });
+
+        Map<String, Set<String>> cache = new HashMap<>();
+        Boolean trainExists = ReflectionTestUtils.invokeMethod(
+                service, "existsInAliBtrip", "2026-10-01", "AB12", cache);
+        Boolean vatExists = ReflectionTestUtils.invokeMethod(
+                service, "existsInAliBtrip", "2026-10-01", "VAT-34", cache);
+
+        assertEquals(Boolean.TRUE, trainExists);
+        assertEquals(Boolean.TRUE, vatExists);
+        assertEquals(1, trainRequestCount.get());
+        assertEquals(1, vatRequestCount.get());
+    }
+
+    private void configure(Object service) {
+        ReflectionTestUtils.setField(service, "corpTokenService", (AliBtripCorpTokenService) () -> "test-token");
+        ReflectionTestUtils.setField(service, "baseUrl", baseUrl);
+        ReflectionTestUtils.setField(service, "appKey", "test-app-key");
+    }
+
+    private static String page(String items, int pageNo, int totalSize, int totalPage) {
+        return "{\"success\":true,\"code\":\"SUCCESS\",\"module\":{"
+                + "\"items\":" + items + ","
+                + "\"total_size\":" + totalSize + ","
+                + "\"page_no\":" + pageNo + ","
+                + "\"page_size\":100,"
+                + "\"total_page\":" + totalPage + "}}";
+    }
+
+    private static Map<String, String> parseQuery(HttpExchange exchange) throws IOException {
+        Map<String, String> values = new HashMap<>();
+        String rawQuery = exchange.getRequestURI().getRawQuery();
+        for (String pair : rawQuery.split("&")) {
+            String[] parts = pair.split("=", 2);
+            values.put(URLDecoder.decode(parts[0], "UTF-8"),
+                    URLDecoder.decode(parts.length > 1 ? parts[1] : "", "UTF-8"));
+        }
+        return values;
+    }
+
+    private static void writeJson(HttpExchange exchange, String json) throws IOException {
+        byte[] response = json.getBytes(StandardCharsets.UTF_8);
+        exchange.getResponseHeaders().add("Content-Type", "application/json; charset=UTF-8");
+        exchange.sendResponseHeaders(200, response.length);
+        try (OutputStream output = exchange.getResponseBody()) {
+            output.write(response);
+        }
+    }
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 3321
test/java/com/muzhi/huanshi/APITest/Test1.java