zhangkan преди 1 месец
родител
ревизия
29a5758d98
променени са 39 файла, в които са добавени 4772 реда и са изтрити 6544 реда
  1. 15 0
      .gitignore
  2. 0 0
      README.md
  3. 102 0
      SEAL_DEPLOYMENT.md
  4. 0 43
      main/java/com/muzhi/huanshi/controller/AccessTokenController.java
  5. 0 43
      main/java/com/muzhi/huanshi/controller/DingOaController.java
  6. 0 14
      main/java/com/muzhi/huanshi/service/AccessTokenService.java
  7. 0 14
      main/java/com/muzhi/huanshi/service/DingOaService.java
  8. 0 86
      main/java/com/muzhi/huanshi/service/impl/AccessTokenServiceImpl.java
  9. 0 2921
      main/java/com/muzhi/huanshi/service/impl/DingOaServiceImpl.java
  10. 414 0
      pom.xml
  11. 4 4
      main/java/com/muzhi/huanshi/HuanshiApplication.java
  12. 95 77
      main/java/com/muzhi/huanshi/common/R.java
  13. 12 12
      main/java/com/muzhi/huanshi/common/ResultCode.java
  14. 289 0
      src/main/java/com/muzhi/changguchuan/common/alibaba/AlibabaMailBox.java
  15. 39 0
      src/main/java/com/muzhi/changguchuan/common/dingtalkwebhook/DingtalkWebhook.java
  16. 71 0
      src/main/java/com/muzhi/changguchuan/common/dingtalkyida/YiDaFormData.java
  17. 348 0
      src/main/java/com/muzhi/changguchuan/common/dingtalkyida/YiDaFormDataV2.java
  18. 40 0
      src/main/java/com/muzhi/changguchuan/common/time/BeijingToUtcConverter.java
  19. 122 0
      src/main/java/com/muzhi/changguchuan/common/utilityclass/PackageFlie.java
  20. 39 0
      src/main/java/com/muzhi/changguchuan/controller/AccessTokenController.java
  21. 32 0
      src/main/java/com/muzhi/changguchuan/controller/AlibabaMailBoxController.java
  22. 67 0
      src/main/java/com/muzhi/changguchuan/controller/DingTalkController.java
  23. 40 0
      src/main/java/com/muzhi/changguchuan/entity/InvoiceItem.java
  24. 198 0
      src/main/java/com/muzhi/changguchuan/seal/config/SealProperties.java
  25. 119 0
      src/main/java/com/muzhi/changguchuan/seal/controller/SealController.java
  26. 75 0
      src/main/java/com/muzhi/changguchuan/seal/model/SealApprovalRequest.java
  27. 53 0
      src/main/java/com/muzhi/changguchuan/seal/model/SealResult.java
  28. 15 0
      src/main/java/com/muzhi/changguchuan/seal/service/SealService.java
  29. 757 0
      src/main/java/com/muzhi/changguchuan/seal/service/impl/SealServiceImpl.java
  30. 14 0
      src/main/java/com/muzhi/changguchuan/service/AccessTokenService.java
  31. 13 0
      src/main/java/com/muzhi/changguchuan/service/AlibabaMailBoxService.java
  32. 23 0
      src/main/java/com/muzhi/changguchuan/service/DingTalkService.java
  33. 86 0
      src/main/java/com/muzhi/changguchuan/service/impl/AccessTokenServiceImpl.java
  34. 335 0
      src/main/java/com/muzhi/changguchuan/service/impl/AlibabaMailBoxServiceImpl.java
  35. 706 0
      src/main/java/com/muzhi/changguchuan/service/impl/DingTalkServiceImpl.java
  36. 11 8
      main/resources/application.properties
  37. 637 0
      src/test/java/com/muzhi/changguchuan/APITest/Test1.java
  38. 1 1
      test/java/com/muzhi/huanshi/TestApplicationTests.java
  39. 0 3321
      test/java/com/muzhi/huanshi/APITest/Test1.java

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+# Maven build output
+target/
+
+# IDE and local tooling
+.idea/
+*.iml
+.cache/
+
+# OS metadata
+.DS_Store
+
+# Runtime files
+*.log
+*.pid
+logs/

+ 0 - 0
README.md


+ 102 - 0
SEAL_DEPLOYMENT.md

@@ -0,0 +1,102 @@
+# 审批印章模块部署说明
+
+新增模块的接口地址为:
+
+```text
+POST /api/changguchuan/seal/approval-complete
+GET  /api/changguchuan/seal/health
+```
+
+请求体最少需要传入当前宜搭流程实例 ID:
+
+```json
+{
+  "processInstanceId": "${当前流程实例ID}",
+  "approvalTime": "2026-09-21"
+}
+```
+
+## Windows 配置
+
+默认配置已经对应当前宜搭应用、底图表单和业务表单:
+
+- 应用:`APP_RP2SE56YJWMECIWYN4BR`
+- 底图表单:`FORM-97B11108942B4DEA86E7207834FA76392Y23`
+- 底图实例:`FINST-F9D667D180C9S8YBMFJKL57SI9WL3EY87CBUM8UU`
+- 底图字段:`imageField_mubc5ugk`
+- 业务表单:`FORM-64A9D5065C234383AB2ACF7AB4F20975DW6M`
+- 回写字段:`imageField_mubc0xwk`
+
+服务会把图片写入运行目录下的 `sealFile/yyyy/MM/dd/`,并返回类似:
+
+```text
+http://139.196.11.100:8101/sealFile/2026/09/21/seal-FINST-xxx-uuid.png
+```
+
+目标应用 `APP_RP2SE56YJWMECIWYN4BR` 必须配置它自己的宜搭 systemToken(应用密钥):
+
+```text
+SEAL_YIDA_SYSTEM_TOKEN=目标应用的 systemToken
+```
+
+现有 `myapp.yida.appSecret` 属于旧应用时不能直接复用,否则宜搭会返回 HTTP 400。钉钉 AccessToken 仍由现有 `AccessTokenService` 获取。
+
+钉钉 AccessToken 和宜搭 systemToken 是两种不同凭证:本服务会把钉钉 AccessToken 放到
+`x-acs-dingtalk-access-token` 请求头,把目标宜搭应用密钥放到 `systemToken` 参数。
+实例读取和表单回写使用宜搭 v2.0 接口,对应接口路径为 `/v2.0/yida/...`;不要再用旧的
+v1.0 `GetInstanceById` 调用替代它。普通底图表单读取使用原始 HTTP 解析,因为当前 v2.0
+SDK 的 `GetFormDataByIDResponse` 未映射 `formData` 字段。
+审批流程实例读取使用 `/v2.0/yida/processes/instancesInfos/{instanceId}`;印章底图是普通
+表单实例,使用 `/v2.0/yida/forms/instances/{formInstId}` 读取。两者不能混用。
+如果日志出现 `code=innerError`、`statusCode=500`、`没有权限`,而旧宜搭应用可以正常读取,
+说明当前钉钉应用尚未被目标宜搭应用授权。需要在宜搭目标应用的开放接口/应用授权设置中,
+授权当前 `AccessTokenService` 使用的钉钉应用,并开通表单实例读取和更新权限;这不是 Java
+参数顺序问题。服务日志会输出脱敏参数和宜搭 `requestId`,便于在宜搭后台继续定位。
+
+服务会优先调用宜搭官方 `GetOpenUrl` 免登文件接口,将底图地址换成临时 OSS URL 后下载,不依赖浏览器 Cookie。若目标应用权限或接口版本不支持免登地址,可把当前组织 Cookie 以环境变量 `SEAL_YIDA_COOKIE` 注入服务;也可以设置 `SEAL_TEMPLATE_IMAGE_FILE` 指向服务器上的底图 PNG 作为最终回退。不要把 Cookie 写进代码或提交到仓库。
+
+可选的 Spring 环境变量:
+
+```text
+SEAL_YIDA_COOKIE=...
+SEAL_TEMPLATE_IMAGE_FILE=C:\\app\\sealFile\\template\\seal-template.png
+SEAL_CONNECTOR_TOKEN=一段随机字符串
+SEAL_PUBLIC_BASE_URL=http://139.196.11.100:8101/sealFile
+```
+
+如果设置了 `SEAL_CONNECTOR_TOKEN`,宜搭连接器需要增加同名请求头 `X-Seal-Connector-Token`。
+
+## Nginx
+
+将下面的 `alias` 指向 Java 程序运行目录下的 `sealFile`;Nginx 对外监听 8101,Java 服务监听 8102:
+
+```nginx
+location /sealFile/ {
+    alias C:/app/changguchuan/sealFile/;
+    try_files $uri =404;
+    add_header Cache-Control "public, max-age=31536000";
+}
+
+location /api/changguchuan/ {
+    proxy_pass http://127.0.0.1:8102/api/changguchuan/;
+    proxy_set_header Host $host;
+    proxy_set_header X-Real-IP $remote_addr;
+}
+```
+
+## 宜搭连接器和自动化
+
+`.cache/openyida/changguchuan/connector/seal-operations.json` 是连接器动作定义。连接器域名使用 `139.196.11.100:8101`,基础路径填写 `/api/changguchuan/`,动作路径填写 `seal/approval-complete`,最终请求地址为 `/api/changguchuan/seal/approval-complete`,请求体映射:
+
+- `processInstanceId`:当前审批流程实例 ID(必填)
+- `approvalTime`:审批通过时间(可选)
+
+在“禀议书”流程自动化中选择“审批通过”作为触发条件,再调用该连接器动作即可。接口会读取底图、叠加 `yyyy-MM-dd` 日期、保存图片,并把 URL 写入 `imageField_mubc0xwk`。
+
+当前已创建并发布自动化逻辑流:
+
+- 逻辑流名称:`审批通过生成印章`
+- `processCode`:`LPROC-PZA668C1YOF95MEJMNH236O34G1W2FTSMBCUMQ`
+- 触发事件:`processFinish`
+- 审批动作:`agree`
+- 连接器参数:`processInstanceId` 映射为系统变量 `__masterdata_form_inst_id`

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

Файловите разлики са ограничени, защото са твърде много
+ 0 - 2921
main/java/com/muzhi/huanshi/service/impl/DingOaServiceImpl.java


+ 414 - 0
pom.xml

@@ -0,0 +1,414 @@
+<?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.changguchuan</groupId>
+    <artifactId>springboot</artifactId>
+<!--    <packaging>jar</packaging>-->
+    <version>changguchuan</version>
+    <name>changguchuan</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>-->
+
+        <!--压缩包相关-->
+        <!-- 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.changguchuan;
 
 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 ChangguchuanApplication {
 
     public static void main(String[] args){
-        SpringApplication.run(HuanshiApplication.class, args);
+        SpringApplication.run(ChangguchuanApplication.class, args);
     }
 
 }

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

@@ -1,77 +1,95 @@
-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.changguchuan.common;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+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>();
+
+    @ApiModelProperty(value = "返回数据")
+    private List<String> dataList = new ArrayList<>();
+
+    @ApiModelProperty(value = "返回数据")
+    private String dataString;
+
+    //把构造方法私有
+    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;
+    }
+
+    public R dataList(List<String> list){
+        this.setDataList(list);
+        return this;
+    }
+
+    public R dataString(String string){
+        this.setDataString(string);
+        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.changguchuan.common;
+
+/**
+ * @Author ZhangKan
+ * @Data 11:15
+ */
+public interface ResultCode {
+
+    public static Integer SUCCESS = 200;//成功
+
+    public static Integer ERROR = 201;//失败
+}

+ 289 - 0
src/main/java/com/muzhi/changguchuan/common/alibaba/AlibabaMailBox.java

@@ -0,0 +1,289 @@
+package com.muzhi.changguchuan.common.alibaba;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.changguchuan.common.utilityclass.PackageFlie;
+import com.muzhi.changguchuan.service.AccessTokenService;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+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;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+@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);
+
+        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());
+
+                    fileNewUrl = "https://poc.cloudpure.cn/changguchuan/invoice/" + 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/changguchuan/invoice/" + format;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                    // 确保目录存在
+                    Path dir = Paths.get(path);
+
+                    Files.createDirectories(dir);
+
+                    try (InputStream inputStream = body.byteStream()) {
+                        // 构建完整文件路径
+                        Path filePath = dir.resolve("");
+                        fileName = PackageFlie.extractPdfFromZipStream(inputStream, filePath);
+
+                        fileNewUrl = "https://poc.cloudpure.cn/changguchuan/invoice/" + 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/changguchuan/invoice/" + format;// 指定存放位置,此处使用的是全局变量的文件名称
+
+                    // 确保目录存在
+                    Path dir = Paths.get(path);
+
+                    Files.createDirectories(dir);
+
+                    try (InputStream inputStream = body.byteStream()) {
+                        // 构建完整文件路径
+                        Path filePath = dir.resolve("");
+                        fileName = PackageFlie.extractPdfFromRarStream(inputStream, filePath);
+
+                        fileNewUrl = "https://poc.cloudpure.cn/changguchuan/invoice/" + 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;
+        }
+    }
+}

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

@@ -0,0 +1,39 @@
+package com.muzhi.changguchuan.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;
+    }
+
+}

+ 71 - 0
src/main/java/com/muzhi/changguchuan/common/dingtalkyida/YiDaFormData.java

@@ -0,0 +1,71 @@
+package com.muzhi.changguchuan.common.dingtalkyida;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+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 com.muzhi.changguchuan.service.AccessTokenService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+public class YiDaFormData {
+
+    //宜搭接口调用
+    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 appType         应用编码
+     * @param systemToken     应用密钥
+     * @param formUuid        表单编码
+     * @param searchCondition 查询条件
+     * @param formDataJson    表单字段
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static JSONObject CreateOrUpdateFormData(String appType, String systemToken, 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(systemToken)
+                    //表单编码
+                    .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;
+    }
+}

+ 348 - 0
src/main/java/com/muzhi/changguchuan/common/dingtalkyida/YiDaFormDataV2.java

@@ -0,0 +1,348 @@
+package com.muzhi.changguchuan.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);
+    }
+
+    /**
+     * 查询和更新表单实例
+     * 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();
+            SearchFormDatasHeaders searchFormDatasHeaders = new SearchFormDatasHeaders();
+            searchFormDatasHeaders.xAcsDingtalkAccessToken = accessToken;
+            SearchFormDatasRequest searchFormDatasRequest = new 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();
+                UpdateFormDataHeaders updateFormDataHeaders = new UpdateFormDataHeaders();
+                updateFormDataHeaders.xAcsDingtalkAccessToken = accessToken;
+                UpdateFormDataRequest updateFormDataRequest = new 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 formDataJson    更新表单数据
+     * @param accessToken     钉钉accessToken
+     * @return jsonObject{}
+     */
+    public static void updateFormData(String shiliID, String formDataJson, String accessToken) {
+        try {
+            /**
+             * 更新表单数据「按照数据版本」
+             * 注:非表单字段调整不开启
+             * */
+
+            com.aliyun.dingtalkyida_1_0.Client client = createClient();
+            UpdateFormDataHeaders updateFormDataHeaders = new UpdateFormDataHeaders();
+            updateFormDataHeaders.xAcsDingtalkAccessToken = accessToken;
+            UpdateFormDataRequest updateFormDataRequest = new 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_1_0.Client client = createClient();
+            GetInstanceByIdHeaders getInstanceByIdHeaders = new GetInstanceByIdHeaders();
+            getInstanceByIdHeaders.xAcsDingtalkAccessToken = accessToken;
+            GetInstanceByIdRequest getInstanceByIdRequest = new GetInstanceByIdRequest()
+                    .setSystemToken(appSecret)
+                    .setUserId("yida_pub_account")
+                    .setAppType(appType);
+
+            GetInstanceByIdResponse instanceByIdWithOptions = client.getInstanceByIdWithOptions(shiliID, getInstanceByIdRequest, getInstanceByIdHeaders, new RuntimeOptions());
+            String s = JSON.toJSONString(instanceByIdWithOptions.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();
+                ListTableDataByFormInstanceIdTableIdHeaders listTableDataByFormInstanceIdTableIdHeaders = new ListTableDataByFormInstanceIdTableIdHeaders();
+                listTableDataByFormInstanceIdTableIdHeaders.xAcsDingtalkAccessToken = accessToken;
+                ListTableDataByFormInstanceIdTableIdRequest listTableDataByFormInstanceIdTableIdRequest = new 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();
+            SearchFormDatasHeaders searchFormDatasHeaders = new SearchFormDatasHeaders();
+            searchFormDatasHeaders.xAcsDingtalkAccessToken = accessToken;
+            SearchFormDatasRequest searchFormDatasRequest = new 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;
+    }
+}

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

@@ -0,0 +1,40 @@
+package com.muzhi.changguchuan.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/changguchuan/common/utilityclass/PackageFlie.java

@@ -0,0 +1,122 @@
+package com.muzhi.changguchuan.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();
+    }
+}

+ 39 - 0
src/main/java/com/muzhi/changguchuan/controller/AccessTokenController.java

@@ -0,0 +1,39 @@
+package com.muzhi.changguchuan.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.changguchuan.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;
+    }
+}

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

@@ -0,0 +1,32 @@
+package com.muzhi.changguchuan.controller;
+
+import com.muzhi.changguchuan.common.R;
+import com.muzhi.changguchuan.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("/dingservice")
+@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;
+    }
+}

+ 67 - 0
src/main/java/com/muzhi/changguchuan/controller/DingTalkController.java

@@ -0,0 +1,67 @@
+package com.muzhi.changguchuan.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.changguchuan.service.DingTalkService;
+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.*;
+
+import com.muzhi.changguchuan.common.R;
+
+
+@Api(description = "钉钉")
+@RestController
+@RequestMapping("/dingservice")
+@CrossOrigin//解决跨域问题
+public class DingTalkController {
+
+    @Autowired
+    private DingTalkService dingTalkService;
+
+    /**同步组织架构员工信息到宜搭*/
+    @ApiOperation(value = "同步组织架构员工信息到宜搭")
+    @PostMapping("tongbuRenyuan")
+    @Scheduled(cron = "0 0 21 * * ?")//每天21:00触发(也就是晚上21点00分触发)
+//    @Scheduled(cron = "0 0/30 8-20 * * ?")//每天8点~20点,每30分钟执行一次
+    public R tongbuRenyuan(){
+
+        dingTalkService.tongbuRenyuan();
+        return R.ok();
+    }
+
+    /**更新花名册信息*/
+    @ApiOperation(value = "更新花名册信息")
+    @PostMapping("updateHuamingce")
+    public R updateHuamingce(String userId,String bankName,String bankNum){
+
+        dingTalkService.updateHuamingce(userId, bankName, bankNum);
+        return R.ok();
+    }
+
+    /**----------【连接器】----------*/
+    /**创建宜搭出差申请表*/
+    @ApiOperation(value = "创建宜搭出差申请表")
+    @PostMapping("createYidaChuchai")
+    @ResponseBody
+    public R createYidaChuchai(String processInstanceId){
+
+        R updateYidaKaoqin = dingTalkService.createYidaChuchai(processInstanceId);
+        System.out.println(updateYidaKaoqin);
+
+        return updateYidaKaoqin;
+    }
+
+    /**执行动态审批流程*/
+    @ApiOperation(value = "执行动态审批流程")
+    @PostMapping("executeDongtaiShenpi")
+    @ResponseBody
+    public R executeDongtaiShenpi(String processInstanceId,int node,String chuchairenyuanUserId){
+
+        R executeDongtaiShenpi = dingTalkService.executeDongtaiShenpi(processInstanceId, node, chuchairenyuanUserId);
+        System.out.println(executeDongtaiShenpi);
+
+        return executeDongtaiShenpi;
+    }
+}

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

@@ -0,0 +1,40 @@
+package com.muzhi.changguchuan.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
+        );
+    }
+}

+ 198 - 0
src/main/java/com/muzhi/changguchuan/seal/config/SealProperties.java

@@ -0,0 +1,198 @@
+package com.muzhi.changguchuan.seal.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 印章服务配置。所有配置均有默认值,部署时可通过 Spring 环境变量或 JVM 参数覆盖。
+ */
+@Component
+public class SealProperties {
+
+    /** 目标宜搭应用编码,用于调用读取实例和更新表单数据接口。 */
+    @Value("${seal.yida.app-type:APP_RP2SE56YJWMECIWYN4BR}")
+    private String appType;
+
+    /** 原项目旧宜搭应用密钥,仅供其他旧业务使用,印章模块不再复用。 */
+    @Value("${myapp.yida.appSecret:}")
+    private String appSecret;
+
+    /**
+     * 目标宜搭应用自己的 systemToken(接入秘钥)。
+     *
+     * <p>这里不能回退到 {@code myapp.yida.appSecret},因为旧应用与印章目标应用的
+     * systemToken 不同,否则宜搭会返回“接入秘钥不匹配”。</p>
+     */
+    @Value("${seal.yida.system-token:${seal.yida.systemToken:${SEAL_YIDA_SYSTEM_TOKEN:GOD667C1UOZ35EF4MTYCB7ZYCPC5237XPKSMMLLQ}}}")
+    private String systemToken;
+
+    /** 宜搭 OpenAPI 调用身份;默认使用官方服务账号,也可配置为目标应用内的管理员 userId。 */
+    @Value("${seal.yida.user-id:${SEAL_YIDA_USER_ID:yida_pub_account}}")
+    private String yidaUserId;
+
+    /** 宜搭组织访问域名。 */
+    @Value("${seal.yida.base-url:https://bbe8ux.aliwork.com}")
+    private String yidaBaseUrl;
+
+    /** 宜搭 OpenAPI 域名;底图普通表单读取使用 v2.0 原始 HTTP 接口。 */
+    @Value("${seal.yida.api-base-url:https://api.dingtalk.com}")
+    private String yidaApiBaseUrl;
+
+    /** 印章底图模板所在表单的编码。 */
+    @Value("${seal.template.form-uuid:FORM-97B11108942B4DEA86E7207834FA76392Y23}")
+    private String templateFormUuid;
+
+    /** 印章底图模板的固定表单实例 ID。 */
+    @Value("${seal.template.instance-id:FINST-F9D667D180C9S8YBMFJKL57SI9WL3EY87CBUM8UU}")
+    private String templateInstanceId;
+
+    /** 底图表单中保存图片的字段编码。 */
+    @Value("${seal.template.image-field:imageField_mubc5ugk}")
+    private String templateImageField;
+
+    /** 审批流程业务表单编码。 */
+    @Value("${seal.target.form-uuid:FORM-64A9D5065C234383AB2ACF7AB4F20975DW6M}")
+    private String targetFormUuid;
+
+    /** 审批流程表单中用于回写印章图片的字段编码。 */
+    @Value("${seal.target.image-field:imageField_mubc0xwk}")
+    private String targetImageField;
+
+    /** 生成文件的根目录;相对路径会解析到 Java 程序运行目录下。 */
+    @Value("${seal.storage-root:sealFile}")
+    private String storageRoot;
+
+    /** 图片对外访问的 URL 前缀,通常对应 Nginx 的静态文件目录。 */
+    @Value("${seal.public-base-url:http://139.196.11.100:8101/sealFile}")
+    private String publicBaseUrl;
+
+    /** 可选的本地底图文件,宜搭下载失败时作为回退方案。 */
+    @Value("${seal.template-image-file:}")
+    private String templateImageFile;
+
+    /** 可选的宜搭 Cookie,用于下载免登地址失败时访问原始图片地址。 */
+    @Value("${seal.yida-cookie:}")
+    private String yidaCookie;
+
+    /** 可选的连接器请求头鉴权值。 */
+    @Value("${seal.connector-token:}")
+    private String connectorToken;
+
+    /** 日期计算使用的时区。 */
+    @Value("${seal.time-zone:Asia/Shanghai}")
+    private String timeZone;
+
+    /** 下载底图时的连接和读取超时时间,单位为毫秒。 */
+    @Value("${seal.http-timeout-ms:30000}")
+    private int httpTimeoutMs;
+
+    /** 返回目标宜搭应用编码。 */
+    public String getAppType() {
+        return appType;
+    }
+
+    /** 返回旧项目宜搭应用密钥。 */
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    /** 返回目标宜搭应用 systemToken。 */
+    public String getSystemToken() {
+        return systemToken;
+    }
+
+    /** 返回宜搭 OpenAPI 调用身份。 */
+    public String getYidaUserId() {
+        return yidaUserId;
+    }
+
+    /** 返回目标宜搭 systemToken 是否已配置,不暴露密钥内容。 */
+    public boolean hasSystemToken() {
+        return systemToken != null && !systemToken.trim().isEmpty();
+    }
+
+    /**
+     * 返回 systemToken 的脱敏指纹,便于排查服务器是否加载了正确配置。
+     * 该值不是密钥本身,不可用于调用宜搭接口。
+     */
+    public String getSystemTokenFingerprint() {
+        if (!hasSystemToken()) {
+            return "NOT_CONFIGURED";
+        }
+        String value = systemToken.trim();
+        if (value.length() <= 8) {
+            return "****";
+        }
+        return value.substring(0, 4) + "****" + value.substring(value.length() - 4);
+    }
+
+    /** 返回宜搭组织访问域名。 */
+    public String getYidaBaseUrl() {
+        return yidaBaseUrl;
+    }
+
+    /** 返回宜搭 OpenAPI 域名。 */
+    public String getYidaApiBaseUrl() {
+        return yidaApiBaseUrl;
+    }
+
+    /** 返回底图模板表单编码。 */
+    public String getTemplateFormUuid() {
+        return templateFormUuid;
+    }
+
+    /** 返回底图模板实例 ID。 */
+    public String getTemplateInstanceId() {
+        return templateInstanceId;
+    }
+
+    /** 返回底图图片字段编码。 */
+    public String getTemplateImageField() {
+        return templateImageField;
+    }
+
+    /** 返回审批业务表单编码。 */
+    public String getTargetFormUuid() {
+        return targetFormUuid;
+    }
+
+    /** 返回审批业务表单的印章图片字段编码。 */
+    public String getTargetImageField() {
+        return targetImageField;
+    }
+
+    /** 返回本地文件存储根目录。 */
+    public String getStorageRoot() {
+        return storageRoot;
+    }
+
+    /** 返回图片公网 URL 前缀。 */
+    public String getPublicBaseUrl() {
+        return publicBaseUrl;
+    }
+
+    /** 返回本地底图回退文件路径。 */
+    public String getTemplateImageFile() {
+        return templateImageFile;
+    }
+
+    /** 返回宜搭 Cookie 回退配置。 */
+    public String getYidaCookie() {
+        return yidaCookie;
+    }
+
+    /** 返回连接器鉴权令牌。 */
+    public String getConnectorToken() {
+        return connectorToken;
+    }
+
+    /** 返回日期计算时区。 */
+    public String getTimeZone() {
+        return timeZone;
+    }
+
+    /** 返回 HTTP 超时时间。 */
+    public int getHttpTimeoutMs() {
+        return httpTimeoutMs;
+    }
+}

+ 119 - 0
src/main/java/com/muzhi/changguchuan/seal/controller/SealController.java

@@ -0,0 +1,119 @@
+package com.muzhi.changguchuan.seal.controller;
+
+import com.muzhi.changguchuan.common.R;
+import com.muzhi.changguchuan.seal.config.SealProperties;
+import com.muzhi.changguchuan.seal.model.SealApprovalRequest;
+import com.muzhi.changguchuan.seal.model.SealResult;
+import com.muzhi.changguchuan.seal.service.SealService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+/**
+ * 印章生成 HTTP 接口。
+ *
+ * <p>项目配置了统一 context-path,因此实际访问地址为
+ * {@code /api/changguchuan/seal/...}。</p>
+ */
+@Api(description = "宜搭审批印章")
+@RestController
+@RequestMapping("/seal")
+@CrossOrigin
+@Slf4j
+public class SealController {
+
+    @Autowired
+    private SealService sealService;
+
+    @Autowired
+    private SealProperties properties;
+
+    /**
+     * 接收宜搭审批通过回调,生成带日期的印章图片并回写当前流程实例。
+     *
+     * <p>请求既支持 JSON body,也支持宜搭连接器常见的 query 参数;这样可以兼容
+     * 不同版本连接器对参数的传递方式。</p>
+     */
+    @ApiOperation(value = "审批通过后生成印章日期并回写宜搭")
+    @PostMapping("/approval-complete")
+    public ResponseEntity<R> approvalComplete(
+            @RequestBody(required = false) SealApprovalRequest body,
+            @RequestParam Map<String, String> params,
+            @RequestHeader(value = "X-Seal-Connector-Token", required = false) String connectorToken) {
+        log.info("[seal.http] 审批印章接口开始,bodyPresent={}, queryParamCount={}, connectorTokenPresent={}",
+                body != null, params == null ? 0 : params.size(), !StringUtils.isEmpty(connectorToken));
+        // 配置了连接器令牌时,先校验请求来源,避免接口被公网任意调用。
+        if (!StringUtils.isEmpty(properties.getConnectorToken())
+                && !properties.getConnectorToken().equals(connectorToken)) {
+            log.warn("[seal.http] 连接器鉴权失败,configuredTokenPresent={}, requestTokenPresent={}",
+                    !StringUtils.isEmpty(properties.getConnectorToken()), !StringUtils.isEmpty(connectorToken));
+            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(R.error().message("连接器鉴权失败"));
+        }
+
+        // 优先使用 JSON body;body 中没有的参数再从 query 参数中补齐。
+        SealApprovalRequest request = body == null ? new SealApprovalRequest() : body;
+        if (StringUtils.isEmpty(request.getProcessInstanceId())) {
+            request.setProcessInstanceId(first(params, "processInstanceId", "formInstanceId", "formInstId", "instanceId"));
+        }
+        if (StringUtils.isEmpty(request.getFormUuid())) {
+            request.setFormUuid(params.get("formUuid"));
+        }
+        if (StringUtils.isEmpty(request.getApprovalTime())) {
+            request.setApprovalTime(params.get("approvalTime"));
+        }
+        if (StringUtils.isEmpty(request.getTemplateImageUrl())) {
+            request.setTemplateImageUrl(params.get("templateImageUrl"));
+        }
+        try {
+            // 业务服务负责宜搭读取、图片处理、本地保存以及结果回写。
+            SealResult result = sealService.generateAndWrite(request);
+            log.info("[seal.http] 审批印章接口成功,instanceId={}, approvalDate={}, fileUrlPresent={}",
+                    result.getFormInstanceId(), result.getApprovalDate(), !StringUtils.isEmpty(result.getFileUrl()));
+            return ResponseEntity.ok(R.ok()
+                    .data("formInstanceId", result.getFormInstanceId())
+                    .data("formUuid", result.getFormUuid())
+                    .data("approvalDate", result.getApprovalDate())
+                    .data("filePath", result.getFilePath())
+                    .data("fileUrl", result.getFileUrl()));
+        } catch (Exception e) {
+            // 将服务层异常转换为连接器可识别的 HTTP 500 响应。
+            log.error("[seal.http] 审批印章接口失败,instanceId={}, error={}",
+                    request.getProcessInstanceId(), e.getMessage(), e);
+            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
+                    .body(R.error().message(e.getMessage() == null ? "生成印章失败" : e.getMessage()));
+        }
+    }
+
+    /** 返回简单健康状态,供 Nginx、云服务器和部署检查使用。 */
+    @ApiOperation(value = "印章服务健康检查")
+    @GetMapping("/health")
+    public R health() {
+        log.info("[seal.http] 健康检查通过");
+        return R.ok().message("seal service is running");
+    }
+
+    /** 按给定优先级从 query 参数中取得第一个非空值。 */
+    private String first(Map<String, String> params, String... names) {
+        for (String name : names) {
+            String value = params.get(name);
+            if (!StringUtils.isEmpty(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}

+ 75 - 0
src/main/java/com/muzhi/changguchuan/seal/model/SealApprovalRequest.java

@@ -0,0 +1,75 @@
+package com.muzhi.changguchuan.seal.model;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/** 宜搭连接器调用印章接口时的请求参数。 */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class SealApprovalRequest {
+
+    /** 当前审批流程实例 ID;兼容宜搭传入的多个同义字段名。 */
+    @JsonAlias({"formInstanceId", "formInstId", "processInstanceId", "instanceId"})
+    private String processInstanceId;
+
+    /** 可选的业务表单编码,为空时从宜搭实例或系统配置中取得。 */
+    private String formUuid;
+
+    /** 可选的宜搭应用编码,为空时使用系统配置。 */
+    private String appType;
+
+    /** 审批通过时间,可传毫秒时间戳、ISO 时间或 yyyy-MM-dd 日期。 */
+    private String approvalTime;
+
+    /** 可选的底图 URL;未传时读取固定的宜搭底图实例。 */
+    private String templateImageUrl;
+
+    /** 返回审批流程实例 ID。 */
+    public String getProcessInstanceId() {
+        return processInstanceId;
+    }
+
+    /** 设置审批流程实例 ID。 */
+    public void setProcessInstanceId(String processInstanceId) {
+        this.processInstanceId = processInstanceId;
+    }
+
+    /** 返回业务表单编码。 */
+    public String getFormUuid() {
+        return formUuid;
+    }
+
+    /** 设置业务表单编码。 */
+    public void setFormUuid(String formUuid) {
+        this.formUuid = formUuid;
+    }
+
+    /** 返回宜搭应用编码。 */
+    public String getAppType() {
+        return appType;
+    }
+
+    /** 设置宜搭应用编码。 */
+    public void setAppType(String appType) {
+        this.appType = appType;
+    }
+
+    /** 返回审批通过时间。 */
+    public String getApprovalTime() {
+        return approvalTime;
+    }
+
+    /** 设置审批通过时间。 */
+    public void setApprovalTime(String approvalTime) {
+        this.approvalTime = approvalTime;
+    }
+
+    /** 返回请求中指定的底图 URL。 */
+    public String getTemplateImageUrl() {
+        return templateImageUrl;
+    }
+
+    /** 设置请求中指定的底图 URL。 */
+    public void setTemplateImageUrl(String templateImageUrl) {
+        this.templateImageUrl = templateImageUrl;
+    }
+}

+ 53 - 0
src/main/java/com/muzhi/changguchuan/seal/model/SealResult.java

@@ -0,0 +1,53 @@
+package com.muzhi.changguchuan.seal.model;
+
+public class SealResult {
+
+    /** 已处理的宜搭流程实例 ID。 */
+    private String formInstanceId;
+    /** 已处理业务表单编码。 */
+    private String formUuid;
+    /** 服务器本地生成文件的绝对或规范化路径。 */
+    private String filePath;
+    /** 可通过 Nginx 访问的图片公网 URL。 */
+    private String fileUrl;
+    /** 绘制到印章上的审批日期文本。 */
+    private String approvalDate;
+
+    /** 提供 JSON 反序列化所需的无参构造方法。 */
+    public SealResult() {
+    }
+
+    /** 创建一次印章处理结果。 */
+    public SealResult(String formInstanceId, String formUuid, String filePath, String fileUrl, String approvalDate) {
+        this.formInstanceId = formInstanceId;
+        this.formUuid = formUuid;
+        this.filePath = filePath;
+        this.fileUrl = fileUrl;
+        this.approvalDate = approvalDate;
+    }
+
+    /** 返回流程实例 ID。 */
+    public String getFormInstanceId() {
+        return formInstanceId;
+    }
+
+    /** 返回业务表单编码。 */
+    public String getFormUuid() {
+        return formUuid;
+    }
+
+    /** 返回本地文件路径。 */
+    public String getFilePath() {
+        return filePath;
+    }
+
+    /** 返回公网图片 URL。 */
+    public String getFileUrl() {
+        return fileUrl;
+    }
+
+    /** 返回审批日期。 */
+    public String getApprovalDate() {
+        return approvalDate;
+    }
+}

+ 15 - 0
src/main/java/com/muzhi/changguchuan/seal/service/SealService.java

@@ -0,0 +1,15 @@
+package com.muzhi.changguchuan.seal.service;
+
+import com.muzhi.changguchuan.seal.model.SealApprovalRequest;
+import com.muzhi.changguchuan.seal.model.SealResult;
+
+public interface SealService {
+
+    /**
+     * 生成审批印章图片,并将生成图片的 URL 回写到宜搭业务表单。
+     *
+     * @param request 宜搭流程实例及可选的日期、底图参数
+     * @return 图片路径、图片 URL 和审批日期等处理结果
+     */
+    SealResult generateAndWrite(SealApprovalRequest request);
+}

+ 757 - 0
src/main/java/com/muzhi/changguchuan/seal/service/impl/SealServiceImpl.java

@@ -0,0 +1,757 @@
+package com.muzhi.changguchuan.seal.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyun.dingtalkyida_2_0.models.GetInstanceByIdHeaders;
+import com.aliyun.dingtalkyida_2_0.models.GetInstanceByIdRequest;
+import com.aliyun.dingtalkyida_2_0.models.GetInstanceByIdResponse;
+import com.aliyun.dingtalkyida_1_0.models.GetOpenUrlHeaders;
+import com.aliyun.dingtalkyida_1_0.models.GetOpenUrlRequest;
+import com.aliyun.dingtalkyida_1_0.models.GetOpenUrlResponse;
+import com.aliyun.dingtalkyida_2_0.models.UpdateFormDataHeaders;
+import com.aliyun.dingtalkyida_2_0.models.UpdateFormDataRequest;
+import com.aliyun.dingtalkyida_2_0.models.UpdateFormDataResponse;
+import com.aliyun.tea.TeaException;
+import com.muzhi.changguchuan.seal.config.SealProperties;
+import com.muzhi.changguchuan.seal.model.SealApprovalRequest;
+import com.muzhi.changguchuan.seal.model.SealResult;
+import com.muzhi.changguchuan.seal.service.SealService;
+import com.muzhi.changguchuan.service.AccessTokenService;
+import com.aliyun.teautil.models.RuntimeOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.imageio.ImageIO;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 印章生成业务实现。
+ *
+ * <p>处理链路为:读取宜搭实例 -> 获取底图 -> 在底图空白区域绘制审批日期 ->
+ * 按日期保存到 sealFile -> 将公网 URL 回写到审批表单。</p>
+ */
+@Service
+@Slf4j
+public class SealServiceImpl implements SealService {
+
+    /** 文件夹按年/月/日分层,便于长期运行时管理文件。 */
+    private static final DateTimeFormatter DIRECTORY_DATE = DateTimeFormatter.ofPattern("yyyy/MM/dd");
+    /** 图片中显示的审批日期格式。 */
+    private static final DateTimeFormatter FILE_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+    @Autowired
+    private AccessTokenService accessTokenService;
+
+    @Autowired
+    private SealProperties properties;
+
+    /**
+     * 执行一次完整的印章生成和宜搭回写流程。
+     *
+     * <p>如果当前表单已经回写过本服务生成的公网 URL,则直接返回已有结果,
+     * 避免自动化重复触发时产生多张图片。</p>
+     */
+    @Override
+    public SealResult generateAndWrite(SealApprovalRequest request) {
+        log.info("[seal] 开始处理审批印章请求,requestPresent={}", request != null);
+        if (request == null) {
+            request = new SealApprovalRequest();
+        }
+        String instanceId = firstNonBlank(request.getProcessInstanceId(), null);
+        if (instanceId == null) {
+            log.warn("[seal] 请求缺少流程实例 ID");
+            throw new IllegalArgumentException("缺少宜搭流程实例 ID(processInstanceId/formInstanceId)");
+        }
+
+        // 请求参数优先,未传应用编码时使用部署配置中的目标应用。
+        // 印章服务只允许访问配置的目标宜搭应用,避免连接器误传其他应用编码导致权限错误。
+        String appType = properties.getAppType();
+        if (!isBlank(request.getAppType()) && !appType.equals(request.getAppType())) {
+            log.warn("[seal.config] 忽略请求中的其他宜搭应用编码,requestAppType={}, configuredAppType={}",
+                    request.getAppType(), appType);
+        }
+        log.info("[seal] 请求参数已解析,appType={}, instanceId={}, approvalTime={}, templateUrlPresent={}, systemToken={}",
+                appType, instanceId, request.getApprovalTime(), !isBlank(request.getTemplateImageUrl()),
+                properties.getSystemTokenFingerprint());
+        requiredSystemToken();
+        String accessToken = required(accessTokenService.getAccessToken(), "无法获取钉钉 AccessToken");
+        log.info("[seal] 已获取钉钉 AccessToken,tokenPresent={}, tokenLength={}",
+                !isBlank(accessToken), accessToken == null ? 0 : accessToken.length());
+
+        // 先读取业务实例,以获得表单编码和当前图片字段的已有值。
+        String targetFormUuid = firstNonBlank(request.getFormUuid(), properties.getTargetFormUuid());
+        JSONObject targetContent = getInstance(appType, instanceId, targetFormUuid, accessToken);
+        String formUuid = firstNonBlank(request.getFormUuid(), targetContent.getString("formUuid"), properties.getTargetFormUuid());
+        log.info("[seal] 业务实例读取完成,instanceId={}, formUuid={}, dataKeys={}",
+                instanceId, formUuid, targetContent == null ? 0 : targetContent.size());
+
+        String approvalDate = resolveApprovalDate(request.getApprovalTime(), properties.getTimeZone());
+        String existingUrl = findExistingUrl(targetContent, properties.getTargetImageField());
+        if (existingUrl != null && existingUrl.startsWith(trimTrailingSlash(properties.getPublicBaseUrl()))) {
+            // 已存在本服务生成的文件时执行幂等返回,不重复下载、绘图和回写。
+            log.info("[seal] 检测到已生成的印章 URL,跳过重复处理,instanceId={}, fileUrl={}", instanceId, existingUrl);
+            return new SealResult(instanceId, formUuid, null, existingUrl, approvalDate);
+        }
+
+        // 获取底图、叠加日期、保存文件,最后把文件 URL 写回宜搭。
+        byte[] templateBytes = loadTemplateBytes(request.getTemplateImageUrl(), appType, accessToken);
+        log.info("[seal] 印章底图读取完成,bytes={}", templateBytes.length);
+        byte[] stampedBytes = renderSeal(templateBytes, approvalDate);
+        log.info("[seal] 日期绘制完成,approvalDate={}, outputBytes={}", approvalDate, stampedBytes.length);
+        StoredFile storedFile = store(stampedBytes, instanceId, approvalDate);
+        log.info("[seal] 印章文件保存完成,path={}, url={}", storedFile.path, storedFile.url);
+        updateTarget(appType, formUuid, instanceId, storedFile, accessToken);
+        log.info("[seal] 印章 URL 回写宜搭完成,instanceId={}, fieldId={}", instanceId, properties.getTargetImageField());
+        return new SealResult(instanceId, formUuid, storedFile.path.toString(), storedFile.url, approvalDate);
+    }
+
+    /** 通过宜搭 GetInstanceById 接口读取流程实例详情和表单数据。 */
+    private JSONObject getInstance(String appType, String instanceId, String formUuid, String accessToken) {
+        log.info("[seal.yida] GetInstanceById 开始,appType={}, instanceId={}, baseUrl={}, userId={}, systemToken={}",
+                appType, instanceId, properties.getYidaBaseUrl(), properties.getYidaUserId(), properties.getSystemTokenFingerprint());
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClientV2();
+            GetInstanceByIdHeaders headers = new GetInstanceByIdHeaders();
+            headers.xAcsDingtalkAccessToken = accessToken;
+            GetInstanceByIdRequest request = new GetInstanceByIdRequest()
+                    .setSystemToken(requiredSystemToken())
+                    .setUserId(properties.getYidaUserId())
+                    .setAppType(appType);
+            if (!isBlank(formUuid)) {
+                request.setFormUuid(formUuid);
+            }
+            GetInstanceByIdResponse response = client.getInstanceByIdWithOptions(instanceId, request, headers, new RuntimeOptions());
+            log.info("[seal.yida] GetInstanceById 返回,statusCode={}, bodyPresent={}",
+                    response == null ? null : response.getStatusCode(), response != null && response.getBody() != null);
+            JSONObject body = JSON.parseObject(JSON.toJSONString(response.getBody()));
+            JSONObject content = body == null ? null : body.getJSONObject("content");
+            // SDK 返回模型直接使用 data/formUuid,HTTP 原始接口则包在 content 中,统一成 content 结构。
+            JSONObject result = content == null ? body : content;
+            log.info("[seal.yida] GetInstanceById 解析完成,formUuid={}, dataPresent={}",
+                    result == null ? null : result.getString("formUuid"),
+                    result != null && (result.getJSONObject("data") != null || result.getJSONObject("formData") != null));
+            return result;
+        } catch (TeaException e) {
+            log.error("[seal.yida] GetInstanceById 权限/接口调用失败,appType={}, instanceId={}, userId={}, statusCode={}, code={}, requestId={}, error={}",
+                    appType, instanceId, properties.getYidaUserId(), e.statusCode, e.code,
+                    requestId(e), summarize(e), e);
+            if (isPermissionError(e)) {
+                throw new IllegalStateException("读取宜搭实例失败:钉钉 AccessToken 已获取,但当前钉钉应用没有目标宜搭应用 "
+                        + appType + " 的访问权限。请在宜搭目标应用授权当前钉钉应用,并开通表单实例读取权限;"
+                        + "requestId=" + requestId(e), e);
+            }
+            throw new IllegalStateException("读取宜搭实例失败:" + summarize(e), e);
+        } catch (Exception e) {
+            log.error("[seal.yida] GetInstanceById 失败,appType={}, instanceId={}, systemToken={}, error={}",
+                    appType, instanceId, properties.getSystemTokenFingerprint(), summarize(e), e);
+            throw new IllegalStateException("读取宜搭实例失败:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 获取印章底图字节。
+     *
+     * <p>优先使用请求指定的 URL;否则读取固定宜搭模板实例。下载时先调用官方免登接口,
+     * 失败后再尝试原始 URL、本地模板文件等回退方案。</p>
+     */
+    private byte[] loadTemplateBytes(String requestUrl, String appType, String accessToken) {
+        String url = requestUrl;
+        log.info("[seal] 开始读取印章底图,requestUrlPresent={}, templateInstanceId={}, imageField={}",
+                !isBlank(requestUrl), properties.getTemplateInstanceId(), properties.getTemplateImageField());
+        if (isBlank(url)) {
+            // 连接器没有传底图时,从固定的宜搭底图模板实例中解析图片字段。
+            JSONObject template = getFormDataById(appType, properties.getTemplateInstanceId(),
+                    properties.getTemplateFormUuid(), accessToken);
+            url = extractImageUrl(required(template, "读取印章底图模板实例失败"), properties.getTemplateImageField());
+            log.info("[seal] 已从宜搭底图实例解析图片地址,urlPresent={}, url={}", !isBlank(url), safeUrl(url));
+        }
+        if (!isBlank(url)) {
+            try {
+                // 将宜搭文件地址换成短期免登地址后再下载,避免依赖浏览器 Cookie。
+                String openUrl = getOpenUrl(appType, url, accessToken);
+                String downloadUrl = firstNonBlank(openUrl, url);
+                log.info("[seal] 底图免登地址处理完成,openUrlPresent={}, downloadUrl={}", !isBlank(openUrl), safeUrl(downloadUrl));
+                return download(downloadUrl);
+            } catch (RuntimeException e) {
+                log.warn("下载宜搭印章底图失败,将尝试本地模板回退:{}", e.getMessage());
+            }
+        }
+        if (!isBlank(properties.getTemplateImageFile())) {
+            try {
+                // 服务器无法访问宜搭时,允许使用预置在本地的 PNG 底图。
+                byte[] bytes = Files.readAllBytes(Paths.get(properties.getTemplateImageFile()));
+                log.info("[seal] 已使用本地底图回退文件,file={}, bytes={}", properties.getTemplateImageFile(), bytes.length);
+                return bytes;
+            } catch (IOException e) {
+                throw new IllegalStateException("读取本地印章底图失败:" + properties.getTemplateImageFile(), e);
+            }
+        }
+        throw new IllegalStateException("无法获取印章底图。请配置 seal.yida-cookie、seal.template-image-file,或在连接器请求中传 templateImageUrl");
+    }
+
+    /**
+     * 通过宜搭 v2.0 普通表单实例原始 HTTP 接口读取印章底图记录。
+     * 当前 SDK 的 GetFormDataByIDResponse 在 HTTP 200 时无法映射 formData,
+     * 因此这里严格复刻可用的 curl 请求并直接解析 JSON。
+     */
+    private JSONObject getFormDataById(String appType, String instanceId, String formUuid, String accessToken) {
+        log.info("[seal.yida] GetFormDataByID 开始,appType={}, formUuid={}, instanceId={}, userId={}, systemToken={}",
+                appType, formUuid, instanceId, properties.getYidaUserId(), properties.getSystemTokenFingerprint());
+        HttpURLConnection connection = null;
+        try {
+            String endpoint = trimTrailingSlash(properties.getYidaApiBaseUrl())
+                    + "/v2.0/yida/forms/instances/" + encode(instanceId)
+                    + "?appType=" + encode(appType)
+                    + "&systemToken=" + encode(requiredSystemToken())
+                    + "&userId=" + encode(properties.getYidaUserId())
+                    + "&formUuid=" + encode(formUuid);
+            connection = (HttpURLConnection) new URL(endpoint).openConnection();
+            connection.setRequestMethod("GET");
+            connection.setConnectTimeout(properties.getHttpTimeoutMs());
+            connection.setReadTimeout(properties.getHttpTimeoutMs());
+            connection.setRequestProperty("Content-Type", "application/json");
+            connection.setRequestProperty("x-acs-dingtalk-access-token", accessToken);
+            int status = connection.getResponseCode();
+            String responseText = readResponse(connection, status);
+            log.info("[seal.yida] GetFormDataByID 返回,statusCode={}, bodyPresent={}, bodyLength={}",
+                    status, !isBlank(responseText), responseText == null ? 0 : responseText.length());
+            if (status < 200 || status >= 300) {
+                throw new IllegalStateException("宜搭底图接口 HTTP " + status + ":" + responseText);
+            }
+            JSONObject body = JSON.parseObject(responseText);
+            log.info("[seal.yida] GetFormDataByID 解析完成,formInstId={}, formDataPresent={}",
+                    body == null ? null : body.getString("formInstId"), body != null && body.getJSONObject("formData") != null);
+            return body;
+        } catch (TeaException e) {
+            log.error("[seal.yida] GetFormDataByID 失败,appType={}, formUuid={}, instanceId={}, statusCode={}, code={}, requestId={}, error={}",
+                    appType, formUuid, instanceId, e.statusCode, e.code, requestId(e), summarize(e), e);
+            throw new IllegalStateException("读取宜搭印章底图实例失败:" + summarize(e), e);
+        } catch (Exception e) {
+            log.error("[seal.yida] GetFormDataByID 失败,appType={}, formUuid={}, instanceId={}, error={}",
+                    appType, formUuid, instanceId, summarize(e), e);
+            throw new IllegalStateException("读取宜搭印章底图实例失败:" + e.getMessage(), e);
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /** 读取宜搭 HTTP 响应正文;错误响应也保留正文用于定位 code/message/requestId。 */
+    private String readResponse(HttpURLConnection connection, int status) throws IOException {
+        InputStream stream = status >= 200 && status < 300 ? connection.getInputStream() : connection.getErrorStream();
+        if (stream == null) {
+            return "";
+        }
+        try (InputStream input = stream; ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[4096];
+            int count;
+            while ((count = input.read(buffer)) >= 0) {
+                output.write(buffer, 0, count);
+            }
+            return new String(output.toByteArray(), java.nio.charset.StandardCharsets.UTF_8);
+        }
+    }
+
+    /** 对 OpenAPI 查询参数进行 UTF-8 编码,避免实例 ID 或 userId 中的特殊字符破坏请求。 */
+    private String encode(String value) throws IOException {
+        return URLEncoder.encode(value == null ? "" : value, "UTF-8");
+    }
+
+    /** 通过宜搭官方免登文件接口获取 OSS 临时地址,避免服务端依赖浏览器 Cookie。 */
+    private String getOpenUrl(String appType, String fileUrl, String accessToken) {
+        log.info("[seal.yida] GetOpenUrl 开始,appType={}, fileUrl={}, systemToken={}",
+                appType, safeUrl(fileUrl), properties.getSystemTokenFingerprint());
+        try {
+            com.aliyun.dingtalkyida_1_0.Client client = createClient();
+            GetOpenUrlHeaders headers = new GetOpenUrlHeaders();
+            headers.xAcsDingtalkAccessToken = accessToken;
+            GetOpenUrlRequest request = new GetOpenUrlRequest()
+                    .setSystemToken(requiredSystemToken())
+                    .setFileUrl(fileUrl)
+                    .setUserId(properties.getYidaUserId())
+                    .setTimeout(86400000L);
+            GetOpenUrlResponse response = client.getOpenUrlWithOptions(appType, request, headers, new RuntimeOptions());
+            String result = response == null || response.getBody() == null ? null : response.getBody().getResult();
+            log.info("[seal.yida] GetOpenUrl 返回,statusCode={}, resultPresent={}",
+                    response == null ? null : response.getStatusCode(), !isBlank(result));
+            return result;
+        } catch (Exception e) {
+            log.warn("[seal.yida] GetOpenUrl 失败,将尝试原始地址,fileUrl={}, error={}", safeUrl(fileUrl), summarize(e));
+            return null;
+        }
+    }
+
+    /** 从宜搭图片字段的数组值中提取第一张图片的可下载地址。 */
+    private String extractImageUrl(JSONObject content, String fieldId) {
+        JSONObject formData = getFieldContainer(content);
+        if (formData == null) {
+            return null;
+        }
+        Object raw = formData.get(fieldId);
+        if (raw == null) {
+            return null;
+        }
+        JSONArray images;
+        if (raw instanceof String) {
+            images = JSON.parseArray((String) raw);
+        } else if (raw instanceof JSONArray) {
+            images = (JSONArray) raw;
+        } else {
+            images = JSON.parseArray(JSON.toJSONString(raw));
+        }
+        if (images == null || images.isEmpty()) {
+            return null;
+        }
+        JSONObject image = images.getJSONObject(0);
+        String imageUrl = firstNonBlank(image.getString("downloadUrl"), image.getString("url"), image.getString("previewUrl"));
+        return toAbsoluteUrl(imageUrl);
+    }
+
+    /** 使用 HTTP 下载底图字节,并在需要时附带宜搭 Cookie。 */
+    private byte[] download(String imageUrl) {
+        HttpURLConnection connection = null;
+        log.info("[seal.http] 图片下载开始,url={}", safeUrl(imageUrl));
+        try {
+            URL url = new URL(toAbsoluteUrl(imageUrl));
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(properties.getHttpTimeoutMs());
+            connection.setReadTimeout(properties.getHttpTimeoutMs());
+            connection.setInstanceFollowRedirects(true);
+            connection.setRequestProperty("User-Agent", "changguchuan-seal/1.0");
+            if (!isBlank(properties.getYidaCookie())) {
+                // 仅在免登地址不可用时使用可选 Cookie 配置。
+                connection.setRequestProperty("Cookie", properties.getYidaCookie());
+            }
+            int status = connection.getResponseCode();
+            log.info("[seal.http] 图片下载响应,statusCode={}, contentType={}, contentLength={}",
+                    status, connection.getContentType(), connection.getContentLengthLong());
+            if (status < 200 || status >= 300) {
+                throw new IllegalStateException("图片下载 HTTP " + status);
+            }
+            try (InputStream input = connection.getInputStream(); ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+                byte[] buffer = new byte[8192];
+                int count;
+                while ((count = input.read(buffer)) >= 0) {
+                    output.write(buffer, 0, count);
+                }
+                byte[] bytes = output.toByteArray();
+                log.info("[seal.http] 图片下载完成,bytes={}", bytes.length);
+                return bytes;
+            }
+        } catch (IOException e) {
+            log.error("[seal.http] 图片下载失败,url={}, error={}", safeUrl(imageUrl), summarize(e), e);
+            throw new IllegalStateException("图片下载失败:" + e.getMessage(), e);
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 在底图中心空白区域绘制审批日期,并统一输出为 PNG 字节。
+     */
+    private byte[] renderSeal(byte[] sourceBytes, String dateText) {
+        try {
+            BufferedImage source = ImageIO.read(new ByteArrayInputStream(sourceBytes));
+            if (source == null) {
+                throw new IllegalStateException("印章底图不是有效图片");
+            }
+            log.info("[seal.image] 开始绘制日期,width={}, height={}, dateText={}",
+                    source.getWidth(), source.getHeight(), dateText);
+            int imageType = source.getColorModel().hasAlpha() ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
+            BufferedImage result = new BufferedImage(source.getWidth(), source.getHeight(), imageType);
+            Graphics2D graphics = result.createGraphics();
+            graphics.drawImage(source, 0, 0, null);
+            // 开启抗锯齿和高质量渲染,使日期文字边缘与印章底图更自然。
+            graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+            graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+
+            Color ink = detectInkColor(source);
+            int centerY = findBlankCenter(source);
+            // 字体大小根据图片尺寸计算,避免固定字号在不同分辨率下过大或过小。
+            int fontSize = Math.max(12, Math.min(source.getWidth(), source.getHeight()) / 10);
+            Font font = new Font("宋体", Font.BOLD, fontSize);
+            if (font.getFamily(Locale.CHINA).equalsIgnoreCase("Dialog")) {
+                font = new Font(Font.SERIF, Font.BOLD, fontSize);
+            }
+            graphics.setFont(font);
+            graphics.setColor(ink);
+            FontMetrics metrics = graphics.getFontMetrics();
+            int x = (source.getWidth() - metrics.stringWidth(dateText)) / 2;
+            // drawString 的 y 是文字基线;用 ascent-descent 计算,才能让实际字形包围盒的中心
+            // 对齐印章上下横线之间的空白中心。旧公式把 descent 也向上扣除,导致日期整体偏上。
+            int baselineY = centerY + (metrics.getAscent() - metrics.getDescent()) / 2;
+            log.info("[seal.image] 日期文字定位,centerY={}, baselineY={}, x={}, ascent={}, descent={}",
+                    centerY, baselineY, x, metrics.getAscent(), metrics.getDescent());
+            graphics.drawString(dateText, x, baselineY);
+            graphics.dispose();
+
+            ByteArrayOutputStream output = new ByteArrayOutputStream();
+            ImageIO.write(result, "png", output);
+            byte[] bytes = output.toByteArray();
+            log.info("[seal.image] 日期绘制输出 PNG,bytes={}", bytes.length);
+            return bytes;
+        } catch (IOException e) {
+            log.error("[seal.image] 图片绘制失败,dateText={}, error={}", dateText, summarize(e), e);
+            throw new IllegalStateException("生成印章图片失败", e);
+        }
+    }
+
+    /** 从底图中统计红色印章笔画,得到与底图接近的文字颜色。 */
+    private Color detectInkColor(BufferedImage image) {
+        long red = 0;
+        long green = 0;
+        long blue = 0;
+        long count = 0;
+        for (int y = 0; y < image.getHeight(); y++) {
+            for (int x = 0; x < image.getWidth(); x++) {
+                Color color = new Color(image.getRGB(x, y), true);
+                if (color.getRed() > color.getGreen() + 40 && color.getRed() > color.getBlue() + 40) {
+                    red += color.getRed();
+                    green += color.getGreen();
+                    blue += color.getBlue();
+                    count++;
+                }
+            }
+        }
+        return count == 0 ? Color.RED : new Color((int) (red / count), (int) (green / count), (int) (blue / count));
+    }
+
+    /** 根据红色笔画所在行估算印章中间空白区域的纵向中心。 */
+    private int findBlankCenter(BufferedImage image) {
+        List<Integer> lineRows = new ArrayList<>();
+        for (int y = 0; y < image.getHeight(); y++) {
+            int redPixels = 0;
+            for (int x = 0; x < image.getWidth(); x++) {
+                Color color = new Color(image.getRGB(x, y), true);
+                if (color.getRed() > color.getGreen() + 40 && color.getRed() > color.getBlue() + 40) {
+                    redPixels++;
+                }
+            }
+            if (redPixels > image.getWidth() * 0.35) {
+                lineRows.add(y);
+            }
+        }
+        if (lineRows.size() >= 2) {
+            return (lineRows.get(0) + lineRows.get(lineRows.size() - 1)) / 2;
+        }
+        return image.getHeight() / 2;
+    }
+
+    /** 将生成图片保存到 sealFile/yyyy/MM/dd,并生成对应的公网访问 URL。 */
+    private StoredFile store(byte[] imageBytes, String instanceId, String approvalDate) {
+        log.info("[seal.file] 开始保存印章文件,instanceId={}, approvalDate={}, bytes={}, storageRoot={}",
+                instanceId, approvalDate, imageBytes.length, properties.getStorageRoot());
+        LocalDate today = LocalDate.now(ZoneId.of(properties.getTimeZone()));
+        Path root = Paths.get(properties.getStorageRoot());
+        if (!root.isAbsolute()) {
+            root = Paths.get(System.getProperty("user.dir")).resolve(root);
+        }
+        Path directory = root.resolve(today.format(DIRECTORY_DATE));
+        // 清理实例 ID 中的特殊字符,防止实例值影响文件路径。
+        String safeId = instanceId.replaceAll("[^a-zA-Z0-9._-]", "_");
+        String fileName = "seal-" + safeId + "-" + UUID.randomUUID().toString().replace("-", "") + ".png";
+        Path file = directory.resolve(fileName).normalize();
+        if (!file.startsWith(root.normalize())) {
+            throw new IllegalStateException("生成的图片路径不合法");
+        }
+        try {
+            // CREATE_NEW 保证同一路径不会覆盖已有文件;文件名中的 UUID 进一步避免冲突。
+            Files.createDirectories(directory);
+            Files.write(file, imageBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
+        } catch (IOException e) {
+            throw new IllegalStateException("保存印章图片失败:" + file, e);
+        }
+        String relative = today.format(DIRECTORY_DATE) + "/" + fileName;
+        String url = trimTrailingSlash(properties.getPublicBaseUrl()) + "/" + relative;
+        log.info("[seal.file] 文件保存成功,path={}, publicUrl={}", file, url);
+        return new StoredFile(file, url, imageBytes.length, fileName);
+    }
+
+    /** 使用宜搭 UpdateFormData 接口,把公网图片地址写入图片上传字段。 */
+    private void updateTarget(String appType, String formUuid, String instanceId, StoredFile file, String accessToken) {
+        log.info("[seal.yida] UpdateFormData 开始,appType={}, formUuid={}, instanceId={}, fieldId={}, fileUrl={}",
+                appType, formUuid, instanceId, properties.getTargetImageField(), file.url);
+        // 图片字段使用宜搭兼容的数组对象结构,保留名称、大小和各类访问地址。
+        Map<String, Object> image = new LinkedHashMap<>();
+        image.put("name", file.name);
+        image.put("size", file.size);
+        image.put("fileUuid", file.url);
+        image.put("url", file.url);
+        image.put("downloadUrl", file.url);
+        image.put("previewUrl", file.url);
+        List<Map<String, Object>> images = new ArrayList<>();
+        images.add(image);
+        Map<String, Object> formData = new HashMap<>();
+        formData.put(properties.getTargetImageField(), images);
+        try {
+            com.aliyun.dingtalkyida_2_0.Client client = createClientV2();
+            UpdateFormDataHeaders headers = new UpdateFormDataHeaders();
+            headers.xAcsDingtalkAccessToken = accessToken;
+            UpdateFormDataRequest request = new UpdateFormDataRequest()
+                    .setSystemToken(requiredSystemToken())
+                    .setFormInstanceId(instanceId)
+                    .setFormUuid(formUuid)
+                    .setUserId(properties.getYidaUserId())
+                    .setUseLatestVersion(true)
+                    .setAppType(appType)
+                    .setUpdateFormDataJson(JSON.toJSONString(formData));
+            UpdateFormDataResponse response = client.updateFormDataWithOptions(request, headers, new RuntimeOptions());
+            // 当前 SDK 的 UpdateFormDataResponse 只有状态码,不提供 body 字段。
+            log.info("[seal.yida] UpdateFormData 返回,statusCode={}, responsePresent={}",
+                    response == null ? null : response.getStatusCode(), response != null);
+            if (response != null && response.getStatusCode() != null && response.getStatusCode() >= 400) {
+                throw new IllegalStateException("宜搭回写失败,HTTP 状态码:" + response.getStatusCode());
+            }
+        } catch (Exception e) {
+            log.error("[seal.yida] UpdateFormData 失败,instanceId={}, fieldId={}, error={}",
+                    instanceId, properties.getTargetImageField(), summarize(e), e);
+            throw new IllegalStateException("回写宜搭印章图片失败:" + e.getMessage(), e);
+        }
+    }
+
+    /** 读取业务表单中已有的印章 URL,用于幂等判断。 */
+    private String findExistingUrl(JSONObject content, String fieldId) {
+        JSONObject formData = getFieldContainer(content);
+        String value = formData == null ? null : formData.getString(fieldId);
+        if (isBlank(value)) {
+            return null;
+        }
+        try {
+            JSONArray array = JSON.parseArray(value);
+            if (array != null && !array.isEmpty()) {
+                return firstNonBlank(array.getJSONObject(0).getString("url"), array.getJSONObject(0).getString("downloadUrl"));
+            }
+        } catch (Exception ignored) {
+            // 兼容历史数据中直接保存 URL 字符串的情况。
+        }
+        return value.startsWith("http") ? value : null;
+    }
+
+    /** 兼容宜搭返回的 formData 和 SDK data 两种数据容器。 */
+    private JSONObject getFieldContainer(JSONObject content) {
+        if (content == null) {
+            return null;
+        }
+        JSONObject formData = content.getJSONObject("formData");
+        return formData == null ? content.getJSONObject("data") : formData;
+    }
+
+    /** 将毫秒时间戳、带时区时间或日期字符串统一转换为 yyyy-MM-dd。 */
+    private String resolveApprovalDate(String approvalTime, String zone) {
+        log.info("[seal] 开始解析审批日期,approvalTimePresent={}, zone={}", !isBlank(approvalTime), zone);
+        ZoneId zoneId = ZoneId.of(zone);
+        if (isBlank(approvalTime)) {
+            String result = LocalDate.now(zoneId).format(FILE_DATE);
+            log.info("[seal] 审批时间为空,使用服务器日期 {}", result);
+            return result;
+        }
+        try {
+            String result = Instant.ofEpochMilli(Long.parseLong(approvalTime)).atZone(zoneId).toLocalDate().format(FILE_DATE);
+            log.info("[seal] 按毫秒时间戳解析审批日期 {}", result);
+            return result;
+        } catch (NumberFormatException ignored) {
+            // 不是纯数字时继续尝试 ISO 格式时间。
+        }
+        try {
+            String result = OffsetDateTime.parse(approvalTime).atZoneSameInstant(zoneId).toLocalDate().format(FILE_DATE);
+            log.info("[seal] 按 OffsetDateTime 解析审批日期 {}", result);
+            return result;
+        } catch (DateTimeParseException ignored) {
+            try {
+                String result = ZonedDateTime.parse(approvalTime).withZoneSameInstant(zoneId).toLocalDate().format(FILE_DATE);
+                log.info("[seal] 按 ZonedDateTime 解析审批日期 {}", result);
+                return result;
+            } catch (DateTimeParseException ignoredAgain) {
+                // 最后兼容带时间后缀但前十位为 yyyy-MM-dd 的字符串。
+                String result = LocalDate.parse(approvalTime.substring(0, Math.min(10, approvalTime.length()))).format(FILE_DATE);
+                log.info("[seal] 按日期前缀解析审批日期 {}", result);
+                return result;
+            }
+        }
+    }
+
+    /** 将宜搭返回的相对文件地址转换成绝对 URL。 */
+    private String toAbsoluteUrl(String value) {
+        if (isBlank(value) || value.startsWith("http://") || value.startsWith("https://")) {
+            return value;
+        }
+        return URI.create(trimTrailingSlash(properties.getYidaBaseUrl()) + "/").resolve(value).toString();
+    }
+
+    /** 创建宜搭 Java SDK 客户端,统一设置 HTTPS 和区域参数。 */
+    private com.aliyun.dingtalkyida_1_0.Client createClient() throws Exception {
+        log.debug("[seal.yida] 创建宜搭 SDK Client,protocol=https, regionId=central");
+        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);
+    }
+
+    /** 创建宜搭 v2.0 SDK 客户端;实例读取和表单回写与官方 v2.0 接口保持一致。 */
+    private com.aliyun.dingtalkyida_2_0.Client createClientV2() throws Exception {
+        log.debug("[seal.yida] 创建宜搭 v2.0 SDK Client,protocol=https, regionId=central");
+        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);
+    }
+
+    /** 从 JSON 对象中取得必需的子对象;不存在时抛出带业务含义的异常。 */
+    private JSONObject requiredObject(JSONObject object, String key, String message) {
+        JSONObject value = object == null ? null : object.getJSONObject(key);
+        if (value == null) {
+            throw new IllegalStateException(message);
+        }
+        return value;
+    }
+
+    /** 校验 JSON 对象不为空。 */
+    private JSONObject required(JSONObject object, String message) {
+        if (object == null) {
+            throw new IllegalStateException(message);
+        }
+        return object;
+    }
+
+    /**
+     * 取得目标应用专用 systemToken;禁止回退到旧应用密钥,避免产生误导性的 400 错误。
+     */
+    private String requiredSystemToken() {
+        if (!properties.hasSystemToken()) {
+            log.error("[seal.config] 未配置目标宜搭 systemToken,appType={}, expectedProperty=SEAL_YIDA_SYSTEM_TOKEN",
+                    properties.getAppType());
+            throw new IllegalStateException("缺少目标宜搭应用 systemToken。请在服务器配置目标应用 APP_RP2SE56YJWMECIWYN4BR 的 SEAL_YIDA_SYSTEM_TOKEN(或 seal.yida.system-token)");
+        }
+        return properties.getSystemToken().trim();
+    }
+
+    /** 校验字符串配置或参数不为空。 */
+    private String required(String value, String message) {
+        if (isBlank(value)) {
+            throw new IllegalStateException(message);
+        }
+        return value;
+    }
+
+    /** 将异常压缩为日志可读的一行,同时保留错误类型和原始消息。 */
+    private String summarize(Throwable exception) {
+        if (exception == null) {
+            return "unknown";
+        }
+        if (exception instanceof TeaException) {
+            TeaException teaException = (TeaException) exception;
+            return "TeaException{statusCode=" + teaException.statusCode
+                    + ",code=" + teaException.code
+                    + ",message=" + (teaException.message == null ? "" : teaException.message.replaceAll("\\s+", " "))
+                    + ",requestId=" + requestId(teaException) + "}";
+        }
+        String message = exception.getMessage();
+        return exception.getClass().getSimpleName() + ":" + (message == null ? "" : message.replaceAll("\\s+", " "));
+    }
+
+    /** 从宜搭 SDK 异常中提取 requestId,方便在宜搭后台定位同一条请求。 */
+    private String requestId(TeaException exception) {
+        if (exception == null || exception.data == null) {
+            return "UNKNOWN";
+        }
+        Object value = exception.data.get("requestId");
+        if (value == null) {
+            value = exception.data.get("requestid");
+        }
+        return value == null ? "UNKNOWN" : String.valueOf(value);
+    }
+
+    /** 判断宜搭返回的异常是否属于权限不足,而不是参数或网络故障。 */
+    private boolean isPermissionError(TeaException exception) {
+        String text = summarize(exception).toLowerCase(Locale.ROOT);
+        return text.contains("permission") || text.contains("没有权限") || text.contains("无权限")
+                || text.contains("accessdenied") || text.contains("forbidden");
+    }
+
+    /** 只记录 URL 的协议、主机和路径,避免把签名查询参数写入日志。 */
+    private String safeUrl(String value) {
+        if (isBlank(value)) {
+            return "EMPTY";
+        }
+        try {
+            URI uri = URI.create(value);
+            String path = uri.getPath() == null ? "" : uri.getPath();
+            return (uri.getScheme() == null ? "" : uri.getScheme() + "://")
+                    + (uri.getHost() == null ? "" : uri.getHost()) + path;
+        } catch (Exception ignored) {
+            return "[invalid-url]";
+        }
+    }
+
+    /** 返回参数列表中第一个非空字符串。 */
+    private String firstNonBlank(String... values) {
+        for (String value : values) {
+            if (!isBlank(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    /** 判断字符串是否为 null、空串或只包含空白字符。 */
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    /** 去除 URL 末尾多余的斜杠,避免拼接出双斜杠。 */
+    private String trimTrailingSlash(String value) {
+        return value == null ? "" : value.replaceAll("/+$", "");
+    }
+
+    /** 保存本地路径、访问 URL、文件大小和文件名的内部结果对象。 */
+    private static class StoredFile {
+        private final Path path;
+        private final String url;
+        private final int size;
+        private final String name;
+
+        /** 创建一次已保存文件的描述对象。 */
+        private StoredFile(Path path, String url, int size, String name) {
+            this.path = path;
+            this.url = url;
+            this.size = size;
+            this.name = name;
+        }
+    }
+}

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

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

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

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

+ 23 - 0
src/main/java/com/muzhi/changguchuan/service/DingTalkService.java

@@ -0,0 +1,23 @@
+package com.muzhi.changguchuan.service;
+
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.changguchuan.common.R;
+
+/**
+ * @Author ZhangKan
+ * @Data 15:34
+ */
+public interface DingTalkService {
+
+    /**同步组织架构员工信息到宜搭*/
+    R tongbuRenyuan();
+
+    /**更新花名册信息*/
+    R updateHuamingce(String userId,String bankName,String bankNum);
+
+    /**创建宜搭出差申请表*/
+    R createYidaChuchai(String processInstanceId);
+
+    /**执行动态审批流程*/
+    R executeDongtaiShenpi(String processInstanceId,int node,String chuchairenyuanUserId);
+}

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

@@ -0,0 +1,86 @@
+package com.muzhi.changguchuan.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+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.changguchuan.service.AccessTokenService;
+import com.taobao.api.ApiException;
+import okhttp3.*;
+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 = "ding2z1ek7uf8vfcaoyn";
+    //APPSECRET
+    private static String APPSECRET = "jQsvGbOwLfqdAVY1Cyv21hgpdhL9Q604av5LIQsuCcb6_w-3LEBG2rcii6JaKThQ";
+    //AgenthId:4239732655
+    //corpId:ding9a214ef08856629aa1320dcb25e91351
+
+    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 = "Jmqabhqc82KUmRtq";//应用id
+        String client_secret = "2U4taCFhVAxVv5aRmBM5L3PFbF0N4iNbpxhlmFteEcy0eK3ROOuadpqJSeK9NBDt";//应用密钥
+        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;
+    }
+}

+ 335 - 0
src/main/java/com/muzhi/changguchuan/service/impl/AlibabaMailBoxServiceImpl.java

@@ -0,0 +1,335 @@
+package com.muzhi.changguchuan.service.impl;
+
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.muzhi.changguchuan.common.R;
+import com.muzhi.changguchuan.common.alibaba.AlibabaMailBox;
+import com.muzhi.changguchuan.common.dingtalkyida.YiDaFormData;
+import com.muzhi.changguchuan.common.dingtalkyida.YiDaFormDataV2;
+import com.muzhi.changguchuan.common.time.BeijingToUtcConverter;
+import com.muzhi.changguchuan.service.AccessTokenService;
+import com.muzhi.changguchuan.service.AlibabaMailBoxService;
+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.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+@Service
+@Component
+@Slf4j
+public class AlibabaMailBoxServiceImpl implements AlibabaMailBoxService {
+
+    @Autowired
+    private AccessTokenService accessTokenService;
+
+    /**
+     * 搜索邮件,并解析发票信息
+     * NOTE 邮件标题匹配各类发票邮件标识:【发票、电子发票、电子票】
+     */
+    @Override
+    public R queryMessages() {
+
+        //TODO 指定代收邮箱地址
+        String emailAddress = "invoice_receiver@hasegawa-sh.com";//长谷川正式
+
+
+        /*搜索邮件*/
+        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);//REMARK 正式使用
+        String inputDate = "2026-05-29"; //TODO 指定日期
+        System.out.println("获取邮箱邮件的日期: " + 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];//邮件发送时间
+        //构造查询条件
+        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_moclf2te", email);
+                    String mapAllStr = JSON.toJSONString(mapAll);
+                    System.out.println("封装查询的字段参数:" + mapAllStr);
+                    JSONObject queryYidaForm = YiDaFormDataV2.getQueryYidaForm("FORM-B4771E671F5D453EA7350DFDC5D98646THS7", 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_moclf2t8_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");
+                            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);
+                            //NOTE 判断邮件中的附件是否为pdf
+                            if (isPdf){
+                                log.info("该附件是pdf,开始处理文件,文件名:{},文件id:{}", fileName, fileId);
+                                /*根据文件id获取文件流进行下载转存文件*/
+                                Map mapEnd = AlibabaMailBox.downLoadMailFile("pdf", emailAddress, emailId, fileId, fileName, accessTokenService.getAlibabaMailboxAccessToken());
+                                String fileNewUrl = (String) mapEnd.get("fileNewUrl");
+                                if (fileNewUrl != null){
+                                    Map formMap = new HashMap<>();
+                                    formMap.put("employeeField_ml7hkag2", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员
+                                    formMap.put("selectField_ml9154me", "提取完成");
+                                    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(
+                                            "APP_FS41ZGRW4AXJRZZA0S57",
+                                            "1MA664C1CXW2PZE4IYK0KALA2YAR2QG0IH7LMXB2",
+                                            "FORM-036D3F7CB4CC44E7AE7846A789C3DA65EOJN",
+                                            searchCondition,
+                                            formDataJson,
+                                            accessTokenService.getAccessToken());
+                                }
+                            }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) {
+                                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){
+                                        fileName = (String) mapEnd.get("fileName");
+
+                                        Map formMap = new HashMap<>();
+                                        formMap.put("employeeField_ml7hkag2", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员
+                                        formMap.put("selectField_ml9154me", "提取完成");
+                                        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(
+                                                "APP_FS41ZGRW4AXJRZZA0S57",
+                                                "1MA664C1CXW2PZE4IYK0KALA2YAR2QG0IH7LMXB2",
+                                                "FORM-036D3F7CB4CC44E7AE7846A789C3DA65EOJN",
+                                                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){
+                                        fileName = (String) mapEnd.get("fileName");
+
+                                        Map formMap = new HashMap<>();
+                                        formMap.put("employeeField_ml7hkag2", emailUserId);//NOTE 发票所属人userId,如果根据email地址没有匹配到宜搭用户信息表上的人员
+                                        formMap.put("selectField_ml9154me", "提取完成");
+                                        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(
+                                                "APP_FS41ZGRW4AXJRZZA0S57",
+                                                "1MA664C1CXW2PZE4IYK0KALA2YAR2QG0IH7LMXB2",
+                                                "FORM-036D3F7CB4CC44E7AE7846A789C3DA65EOJN",
+                                                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("解析邮箱发票结束");
+    }
+}

Файловите разлики са ограничени, защото са твърде много
+ 706 - 0
src/main/java/com/muzhi/changguchuan/service/impl/DingTalkServiceImpl.java


+ 11 - 8
main/resources/application.properties

@@ -1,8 +1,17 @@
-server.port=8107
-server.servlet.context-path=/huanshi
+server.port=8102
+server.servlet.context-path=/api/changguchuan
 
 server.tomcat.uri-encoding=UTF-8
 
+#YIDA
+myapp.yida.appType=APP_FS41ZGRW4AXJRZZA0S57
+myapp.yida.appSecret=1MA664C1CXW2PZE4IYK0KALA2YAR2QG0IH7LMXB2
+
+#?????????
+ribbon:
+    ReadTimeoutMillis: 5000
+    ConnectTimeoutMillis: 5000
+
 ## 服务名
 #spring.application.name=simaner-yd
 ## 环境设置:dev、test、prod(开发环境、测试环境、生产环境)
@@ -15,12 +24,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

+ 637 - 0
src/test/java/com/muzhi/changguchuan/APITest/Test1.java

@@ -0,0 +1,637 @@
+package com.muzhi.changguchuan.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_2_0.models.CreateOrUpdateFormDataResponse;
+import com.aliyun.dingtalkyida_2_0.models.GetFormDataByIDResponse;
+import com.aliyun.tea.TeaException;
+import com.aliyun.teautil.models.RuntimeOptions;
+import com.muzhi.changguchuan.common.dingtalkwebhook.DingtalkWebhook;
+import com.muzhi.changguchuan.common.dingtalkyida.YiDaFormData;
+import com.muzhi.changguchuan.service.AccessTokenService;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+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.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@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(
+                "APP_FS41ZGRW4AXJRZZA0S57",
+                "1MA664C1CXW2PZE4IYK0KALA2YAR2QG0IH7LMXB2",
+                "FORM-036D3F7CB4CC44E7AE7846A789C3DA65EOJN",
+                searchCondition,
+                formDataJson,
+                accessTokenService.getAccessToken());
+    }
+
+    /**
+     * 简单的XML标签内容提取方法
+     */
+    private static String extractResult(String xml, String startTag, String endTag) {
+        int startIndex = xml.indexOf(startTag);
+        int endIndex = xml.indexOf(endTag);
+        if (startIndex != -1 && endIndex != -1) {
+            return xml.substring(startIndex + startTag.length(), endIndex);
+        }
+        return "";
+    }
+
+    @Test
+    public void test9() {
+        /*创建-供应商档案*/
+        /**
+         * CACC_ID  U8账套号@必填
+         * CVENCODE 供应商编码@必填
+         * CVENNAME 供应商名称@必填
+         * CVENABBNAME  供应商简称@必填
+         * CVCCODE  供应商分类编号@必填
+         * CVENREGCODE  税号
+         * */
+
+        // 接口地址
+//        String wsUrl = "http://172.18.0.25:10007/webService.asmx";
+        //最新接口地址
+        String wsUrl = "https://socket.svr.hasegawa-sh.com:10007/webService.asmx";
+        // SOAPAction
+        String soapAction = "http://tempuri.org/U8_VendorSave";
+        // 需要传入的JSON业务数据(替换为实际值)
+        String jsonData = "{\n" +
+                "    \"CACC_ID\": \"998\",\n" +
+                "    \"CVENCODE\": \"test002\",\n" +
+                "    \"CVENNAME\": \"更新测试-钉钉测试供应商test002\",\n" +
+                "    \"CVENABBNAME\": \"钉测002\",\n" +
+                "    \"CVCCODE\": \"99\",\n" +
+                "    \"CVENREGCODE\": null\n" +
+                "  }";
+
+        // 拼接SOAP请求报文
+        String soapXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
+                "<soap:Body>" +
+                "<U8_VendorSave xmlns=\"http://tempuri.org/\">" +
+                "<JSon>" + jsonData + "</JSon>" +
+                "</U8_VendorSave>" +
+                "</soap:Body>" +
+                "</soap:Envelope>";
+
+        try {
+            URL url = new URL(wsUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+            // 设置请求头
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
+            conn.setRequestProperty("SOAPAction", soapAction);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+
+            // 发送请求
+            OutputStream os = conn.getOutputStream();
+            os.write(soapXml.getBytes("UTF-8"));
+            os.flush();
+            os.close();
+
+            // 获取响应
+            int responseCode = conn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
+                StringBuilder response = new StringBuilder();
+                String line;
+                while ((line = br.readLine()) != null) {
+                    response.append(line);
+                }
+                br.close();
+
+                // 解析返回的XML获取结果
+                String responseBody = response.toString();
+                String result = extractResult(responseBody, "<U8_VendorSaveResult>", "</U8_VendorSaveResult>");
+                System.out.println("调用成功,返回结果: " + result);
+            } else {
+                System.out.println("调用失败,HTTP状态码: " + responseCode);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void test10() {
+        /*创建-凭证*/
+        /**
+         * CACC_ID  U8账套号@必填
+         * CVENCODE 供应商编码@必填
+         * CVENNAME 供应商名称@必填
+         * CVENABBNAME  供应商简称@必填
+         * CVCCODE  供应商分类编号@必填
+         * CVENREGCODE  税号
+         * */
+
+        // 接口地址
+//        String wsUrl = "http://172.18.0.25:10007/webService.asmx";
+        //最新接口地址
+        String wsUrl = "https://socket.svr.hasegawa-sh.com:10007/webService.asmx";
+        // SOAPAction
+        String soapAction = "http://tempuri.org/U8_GLAccvouch";
+        // 需要传入的JSON业务数据(替换为实际值)
+        String jsonData = "{\n" +
+                "\"CVOUCHCODE\": \"202607010001\",\n" +
+                "\"DDATE\": \"2026-07-01\",\n" +
+                "\"CACC_ID\": \"998\",\n" +
+                "\"CSIGN\": \"银付\",\n" +
+                "\"CMAKER\": \"demo\",\n" +
+                "\"DetailList\": [{\n" +
+                "\"CCODE\": \"1001\",\n" +
+                "\"CCUSCODE\": \"XXXXX\",\n" +
+                "\"CVENCODE\": \"XXXX\",\n" +
+                "\"CDEPCODE\": \"XXXX\",\n" +
+                "\"CITEMCODE\": \"XXXX\",\n" +
+                "\"CDIGEST\": \"员工报销餐补1\",\n" +
+                "\"CEXCHNAME\": \"人民币\",\n" +
+                "\"NFLAT\": \"1\",\n" +
+                "\"MD\": \"100\",\n" +
+                "\"MC\": \"0\",\n" +
+                "\"MD_F\": \"0\",\n" +
+                "\"MC_D\": \"0\",\n" +
+                "\"CPERSONCODE\": \"XXXXX\",\n" +
+                "\"CCASHITEMList\": [{\n" +
+                "\"CCASHITEMCODE\": \"XXXX\",\n" +
+                "\"MD\": \"100\",\n" +
+                "\"MC\": \"0\"\n" +
+                "}]\n" +
+                "7\n" +
+                "}, {\n" +
+                "\"CCODE\": \"1001\",\n" +
+                "\"CCUSCODE\": \"XXXXX\",\n" +
+                "\"CVENCODE\": \"XXXX\",\n" +
+                "\"CDEPCODE\": \"XXXX\",\n" +
+                "\"CITEMCODE\": \"XXXX\",\n" +
+                "\"CDIGEST\": \"员工报销餐补2\",\n" +
+                "\"CEXCHNAME\": \"人民币\",\n" +
+                "\"NFLAT\": \"1\",\n" +
+                "\"MD\": \"0\",\n" +
+                "\"MC\": \"100\",\n" +
+                "\"MD_F\": \"0\",\n" +
+                "\"MC_D\": \"0\",\n" +
+                "\"CPERSONCODE\": \"XXXXX\",\n" +
+                "\"CCASHITEMList\": [{\n" +
+                "\"CCASHITEMCODE\": \"XXXX\",\n" +
+                "\"MD\": \"0\",\n" +
+                "\"MC\": \"100\"\n" +
+                "}]\n" +
+                "}]\n" +
+                "}";
+
+        // 拼接SOAP请求报文
+        String soapXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
+                "<soap:Body>" +
+                "<U8_GLAccvouch xmlns=\"http://tempuri.org/\">" +
+                "<JSon>" + jsonData + "</JSon>" +
+                "</U8_GLAccvouch>" +
+                "</soap:Body>" +
+                "</soap:Envelope>";
+
+        try {
+            URL url = new URL(wsUrl);
+            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+
+            // 设置请求头
+            conn.setRequestMethod("POST");
+            conn.setRequestProperty("Content-Type", "text/xml; charset=utf-8");
+            conn.setRequestProperty("SOAPAction", soapAction);
+            conn.setDoOutput(true);
+            conn.setDoInput(true);
+
+            // 发送请求
+            OutputStream os = conn.getOutputStream();
+            os.write(soapXml.getBytes("UTF-8"));
+            os.flush();
+            os.close();
+
+            // 获取响应
+            int responseCode = conn.getResponseCode();
+            if (responseCode == HttpURLConnection.HTTP_OK) {
+                BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
+                StringBuilder response = new StringBuilder();
+                String line;
+                while ((line = br.readLine()) != null) {
+                    response.append(line);
+                }
+                br.close();
+
+                // 解析返回的XML获取结果
+                String responseBody = response.toString();
+                String result = extractResult(responseBody, "<U8_GLAccvouchResult>", "</U8_GLAccvouchResult>");
+                System.out.println("调用成功,返回结果: " + result);
+            } else {
+                System.out.println("调用失败,HTTP状态码: " + responseCode);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void test11(){
+
+    }
+}

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

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

Файловите разлики са ограничени, защото са твърде много
+ 0 - 3321
test/java/com/muzhi/huanshi/APITest/Test1.java