ht преди 6 дни
родител
ревизия
e5e7a12eb3
променени са 37 файла, в които са добавени 573 реда и са изтрити 1177 реда
  1. 3 2
      mjava-jinlun/pom.xml
  2. 3 1
      mjava-jinlun/src/main/java/com/malk/jinlun/entity/CustomerModel.java
  3. 0 1
      mjava-jinlun/src/main/java/com/malk/jinlun/service/impl/CpClientImpl.java
  4. 6 5
      mjava-jinlun/src/main/java/com/malk/jinlun/service/impl/JinlunServiceImpl.java
  5. 9 3
      mjava-jinlun/src/main/java/com/malk/jinlun/service/impl/JinlunTaskServiceImpl.java
  6. 2 1
      mjava-jinlun/src/main/resources/application-dev.yml
  7. 2 1
      mjava-jinlun/src/main/resources/application-prod.yml
  8. 18 0
      mjava-jinlun/src/test/java/com/malk/jinlun/DdTest.java
  9. 171 75
      mjava-minglei/ERP-YIDA-PUSH-API.md
  10. 127 41
      mjava-minglei/README.md
  11. 10 0
      mjava-minglei/pom.xml
  12. 2 0
      mjava-minglei/src/main/java/com/malk/minglei/MjavaMingleiApplication.java
  13. 14 4
      mjava-minglei/src/main/java/com/malk/minglei/config/MjavaYidaConfiguration.java
  14. 1 1
      mjava-minglei/src/main/java/com/malk/minglei/controller/ErpYidaPushController.java
  15. 30 2
      mjava-minglei/src/main/java/com/malk/minglei/controller/GlobalExceptionHandler.java
  16. 1 1
      mjava-minglei/src/main/java/com/malk/minglei/controller/MingleiPortController.java
  17. 2 2
      mjava-minglei/src/main/java/com/malk/minglei/controller/YidaProcessController.java
  18. 42 2
      mjava-minglei/src/main/java/com/malk/minglei/dto/ErpYidaPushMainData.java
  19. 45 0
      mjava-minglei/src/main/java/com/malk/minglei/dto/ErpYidaPushRequest.java
  20. 9 0
      mjava-minglei/src/main/java/com/malk/minglei/dto/YidaOperationParam.java
  21. 0 18
      mjava-minglei/src/main/java/com/malk/minglei/service/ErpYidaPushService.java
  22. 0 35
      mjava-minglei/src/main/java/com/malk/minglei/service/MingleiDocumentService.java
  23. 0 19
      mjava-minglei/src/main/java/com/malk/minglei/service/MingleiPortService.java
  24. 0 25
      mjava-minglei/src/main/java/com/malk/minglei/service/YidaProcessHandler.java
  25. 0 14
      mjava-minglei/src/main/java/com/malk/minglei/service/YidaProcessService.java
  26. 0 89
      mjava-minglei/src/main/java/com/malk/minglei/service/impl/BomYidaProcessHandler.java
  27. 0 56
      mjava-minglei/src/main/java/com/malk/minglei/service/impl/ErpYidaPushServiceImpl.java
  28. 0 357
      mjava-minglei/src/main/java/com/malk/minglei/service/impl/MingleiDocumentServiceImpl.java
  29. 0 132
      mjava-minglei/src/main/java/com/malk/minglei/service/impl/MingleiPortServiceImpl.java
  30. 0 54
      mjava-minglei/src/main/java/com/malk/minglei/service/impl/YidaProcessServiceImpl.java
  31. 8 0
      mjava-minglei/src/main/resources/application.yml
  32. 66 4
      mjava-minglei/src/test/java/com/malk/minglei/controller/ErpYidaPushControllerTest.java
  33. 2 2
      mjava-minglei/src/test/java/com/malk/minglei/controller/YidaProcessControllerTest.java
  34. 0 38
      mjava-minglei/src/test/java/com/malk/minglei/service/impl/ErpYidaPushServiceImplTest.java
  35. 0 72
      mjava-minglei/src/test/java/com/malk/minglei/service/impl/MingleiDocumentServiceImplTest.java
  36. 0 74
      mjava-minglei/src/test/java/com/malk/minglei/service/impl/MingleiPortServiceImplTest.java
  37. 0 46
      mjava-minglei/src/test/java/com/malk/minglei/service/impl/YidaProcessServiceImplTest.java

+ 3 - 2
mjava-jinlun/pom.xml

@@ -18,6 +18,7 @@
         <maven.compiler.source>8</maven.compiler.source>
         <maven.compiler.target>8</maven.compiler.target>
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <skipTests>true</skipTests>
     </properties>
 
     <dependencies>
@@ -73,7 +74,7 @@
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <configuration>
-                    <skipTests>true</skipTests>
+                    <skipTests>${skipTests}</skipTests>
                 </configuration>
             </plugin>
             <plugin>
@@ -97,4 +98,4 @@
         </plugins>
     </build>
 
-</project>
+</project>

+ 3 - 1
mjava-jinlun/src/main/java/com/malk/jinlun/entity/CustomerModel.java

@@ -27,7 +27,9 @@ public class CustomerModel extends Model{
 
     private FNumber FSALDEPTID;//销售部门
 
-    private FNumber FSELLER;//销售员
+    private FNumber FSELLER;//商务信息销售员
+
+    private FNumber F_VMKV_Base;//基础信息销售员
 
     private FSTAFFNUMBER F_VMKV_Base1;//跟单员
 

+ 0 - 1
mjava-jinlun/src/main/java/com/malk/jinlun/service/impl/CpClientImpl.java

@@ -151,7 +151,6 @@ public class CpClientImpl implements CpClient {
         body.put("size",size);
         body.put("showTotal",true);
 
-        System.out.println("cloudpivotConfig"+cloudpivotConfig);
         if (Strings.isNullOrEmpty(userId)){
             body.put("userId",cloudpivotConfig.getOperatorUserId());
         }else {

+ 6 - 5
mjava-jinlun/src/main/java/com/malk/jinlun/service/impl/JinlunServiceImpl.java

@@ -83,7 +83,8 @@ public class JinlunServiceImpl implements JinlunService {
         String FDescription = UtilMap.getString(customerData,"LongText1758198621716");//备注
 
         //商务信息
-        FNumber FSELLER = new FNumber(UtilMap.getString(customerData,"ShortText1759997501370"));//销售员
+        FNumber FSELLER = new FNumber(UtilMap.getString(customerData,"ShortText1759997501370"));//商务信息销售员
+        FNumber F_VMKV_Base = new FNumber(UtilMap.getString(customerData,"ShortText1759997501370"));//基础信息销售员
         FSTAFFNUMBER F_VMKV_Base1 = new FSTAFFNUMBER(UtilMap.getString(customerData,"Text1762059270203"));//跟单员
         FNumber FRECCONDITIONID = new FNumber(UtilMap.getString(customerData,"Dropdown1758199217950_key"));//收款条件
         FNumber FTRADINGCURRID = new FNumber(UtilMap.getString(customerData,"Dropdown1758199184443_key"));//结算币别
@@ -128,6 +129,7 @@ public class JinlunServiceImpl implements JinlunService {
         customerModel.setFCustTypeId(FCustTypeId);
         customerModel.setFDescription(FDescription);
         customerModel.setFSELLER(FSELLER);
+        customerModel.setF_VMKV_Base(F_VMKV_Base);
         customerModel.setF_VMKV_Base1(F_VMKV_Base1);
         customerModel.setFRECCONDITIONID(FRECCONDITIONID);
         customerModel.setFTRADINGCURRID(FTRADINGCURRID);
@@ -197,13 +199,12 @@ public class JinlunServiceImpl implements JinlunService {
                 try {
                     resultJson3 = client.audit(formId,JSONObject.toJSONString(audit));
                 }catch (Exception e){
-//                    throw new RuntimeException(e);
-                    e.printStackTrace();
-                    log.error(e.getMessage());
+                    throw new RuntimeException(e);
                 }
 
+                log.info("客户审核返回结果:{}", resultJson3);
                 RepoRet repoRet3 = gson.fromJson(resultJson3, RepoRet.class);
-//                isTrue(repoRet3);
+                isTrue(repoRet3);
             }
         }
 

Файловите разлики са ограничени, защото са твърде много
+ 9 - 3
mjava-jinlun/src/main/java/com/malk/jinlun/service/impl/JinlunTaskServiceImpl.java


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

@@ -48,7 +48,8 @@ kingdee:
 # 第三方系统登录授权的账套ID
   X-KDApi-AcctID: 66816d8a115ffc
   # 第三方系统登录授权的用户
-  X-KDApi-UserName: Administrator
+  #X-KDApi-UserName: Administrator
+  X-KDApi-UserName: 陈莹姣
   # 第三方系统登录授权的应用ID
   X-KDApi-AppID: 295561_7e1I0bHHVqGaSZxJ7Z4pVxxs7L5bxMMI
   # 第三方系统登录授权的应用密钥

+ 2 - 1
mjava-jinlun/src/main/resources/application-prod.yml

@@ -47,7 +47,8 @@ kingdee:
   # 第三方系统登录授权的账套ID
   X-KDApi-AcctID: 66816d8a115ffc
   # 第三方系统登录授权的用户
-  X-KDApi-UserName: Administrator
+  #X-KDApi-UserName: Administrator
+  X-KDApi-UserName: 陈莹姣
   # 第三方系统登录授权的应用ID
   X-KDApi-AppID: 295561_7e1I0bHHVqGaSZxJ7Z4pVxxs7L5bxMMI
   # 第三方系统登录授权的应用密钥

+ 18 - 0
mjava-jinlun/src/test/java/com/malk/jinlun/DdTest.java

@@ -53,6 +53,24 @@ public class DdTest {
         jinlunTaskService.syncMaterial();
     }
 
+    @Test
+    public void testK3ReadOnlyBillQuery() throws Exception {
+        K3CloudApi client = new K3CloudApi(initIden());
+        BillQuery billQuery = new BillQuery();
+        billQuery.setFormId("BD_MATERIAL");
+        billQuery.setFieldKeys("FNumber");
+        billQuery.setStartRow(0);
+        billQuery.setLimit(1);
+
+        String response = client.billQuery(JSONObject.toJSONString(billQuery));
+        if (!response.trim().startsWith("[")) {
+            throw new AssertionError("K3 read-only query failed. Check the WebAPI session, third-party authorization, and material query permission.");
+        }
+
+        List<Map> records = JSONObject.parseArray(response, Map.class);
+        log.info("K3 read-only query succeeded, returned {} record(s).", records.size());
+    }
+
     @Test
     public void test4() {
         jinlunTaskService.syncSaleOut();

+ 171 - 75
mjava-minglei/ERP-YIDA-PUSH-API.md

@@ -2,7 +2,7 @@
 
 ## 1. 接口说明
 
-ERP(T100)向明磊业务集成服务推送单据数据。当前接口仅负责接收和记录请求数据,暂不调用宜搭、不发起宜搭流程、不保存数据库
+ERP(T100)向明磊业务集成服务推送单据数据。当前接口按 `type` 分发到对应的宜搭处理器,支持发起宜搭流程
 
 ## 2. 基本信息
 
@@ -13,10 +13,11 @@ ERP(T100)向明磊业务集成服务推送单据数据。当前接口仅负
 | 请求地址 | `http://10.0.0.250/api/erp/yida/push` |
 | 请求格式 | `application/json` |
 | 返回格式 | `application/json` |
-| 当前处理方式 | 接收入参、打印日志、返回成功 |
-| 当前支持类型 | `type` 非空即可,暂不限制类型白名单 |
+| 当前处理方式 | 接收入参、打印日志、按 `type` 路由到宜搭流程处理器 |
+| 当前支持类型 | `cpmp402`、`ml_work_order_craft_price` |
 | `cpmp402` 对应单据 | 包材核价单通知 |
-| `cpmp402` 目标宜搭应用 | `APP_RJJL69QUIZVQSV7YC8TL` |
+| `ml_work_order_craft_price` 对应单据 | ML-工单工艺工价审定 |
+| 目标宜搭应用 | `APP_RJJL69QUIZVQSV7YC8TL` |
 
 ## 3. 请求头
 
@@ -32,24 +33,26 @@ Content-Type: application/json
 {
   "type": "cpmp402",
   "mainData": {
-    "rq": "2026-09-07",
-    "bm": "采购部",
-    "tjr": "ERP_USER_001",
-    "gs": "明磊锂能有限公司",
-    "t100id": "T100_USER_001",
-    "ent": "CN",
-    "t100bm": "PURCHASE"
+    "rq": "2026-09-08",
+    "bm": "ML07000000",
+    "tjr": "3075",
+    "gs": "ML",
+    "t100id": "ML",
+    "ent": 87,
+    "t100bm": "ML07000000"
   },
-  "detailData": [
-    {
-      "hjdh": "HJ20260907001",
-      "gysbh": "SUP001",
-      "gysmc": "供应商名称",
-      "t100id": "T100_SUP001",
-      "ent": "CN",
-      "yy": "zh_CN"
-    }
-  ]
+  "detailData": {
+    "detailData": [
+      {
+        "hjdh": "MLCH326090800001",
+        "gysbh": "113009",
+        "gysmc": "南京鼎泰五金工具有限公司",
+        "t100id": "ML",
+        "ent": "87",
+        "yy": "zh_CN"
+      }
+    ]
+  }
 }
 ```
 
@@ -57,9 +60,9 @@ Content-Type: application/json
 
 | 字段 | 类型 | 必填 | 说明 |
 | --- | --- | --- | --- |
-| `type` | `String` | 是 | 单据类型标识。`cpmp402` 表示包材核价单通知,对应宜搭应用 `APP_RJJL69QUIZVQSV7YC8TL`。当前只校验非空,不限制具体值。 |
+| `type` | `String` | 是 | 单据类型路由键。`cpmp402` 表示包材核价单流程;`ml_work_order_craft_price` 表示 `ML-工单工艺工价审定` 流程。 |
 | `mainData` | `Object` | 是 | 固定主数据对象。 |
-| `detailData` | `Array/Object` | 是 | 明细数据节点。必须传入,不校验具体 JSON 类型,可以是数组或对象。 |
+| `detailData` | `Object` | 是 | 明细数据外壳。必须是对象,且内部必须包含 `detailData` 数组。 |
 
 ### 4.3 `mainData` 字段
 
@@ -73,11 +76,11 @@ Content-Type: application/json
 | `ent` | `String` | 否 | 地区(代码)。                 |
 | `t100bm` | `String` | 否 | T100 部门。                |
 
-`mainData` 中除 `tjr` 外的字段可以传空字符串、空值或不填写。建议 ERP 端按约定字段完整传输,便于后续接入宜搭时直接映射
+`mainData` 中除 `tjr` 外的字段可以传空字符串、空值或不填写。服务端会统一处理 `mainData`:`tjr` 按员工花名册的工号查询钉钉 `userId`,并作为宜搭流程发起人;`rq`、`bm`、`gs`、`t100id`、`ent`、`t100bm` 会作为通用默认元数据补入内部明细对象,供后续所有走通用接口的 `type` 复用。若工号不存在或没有钉钉 ID,接口返回 HTTP 400
 
 ### 4.4 `detailData` 明细字段
 
-`detailData` 的格式不做校验,允许根据 `type` 对应的单据类型传入数组、对象或其他 JSON 结构。对于当前 ERP 推送格式,也支持在 `detailData` 外层再包一层 `detailData` 对象
+`detailData` 的外层结构固定为对象,且必须包含 `detailData` 数组。数组内对象字段根据 `type` 对应的单据类型定义;后续所有走通用接口的类型都必须遵守该结构
 
 以 `cpmp402` 包材核价单通知为例,推荐字段如下:
 
@@ -106,7 +109,7 @@ HTTP 状态码:`200 OK`
 }
 ```
 
-该响应由基座 `com.malk.server.common.McR` 的 `McR.success()` 生成,当前不返回 ERP 推送的业务数据或处理结果
+该响应由基座 `com.malk.server.common.McR` 的 `McR.success()` 生成;宜搭流程实例号会写入服务日志
 
 ## 6. 参数校验失败
 
@@ -115,7 +118,8 @@ HTTP 状态码:`200 OK`
 - `type` 缺失或为空白字符串。
 - `mainData` 缺失或为 `null`。
 - `mainData.tjr` 缺失或为空白字符串。
-- `detailData` 缺失或为 `null`。
+- `mainData.tjr` 对应工号在员工花名册中不存在,或该员工没有钉钉 `userId`。
+- `detailData` 缺失、为 `null`、不是对象,或不包含数组字段 `detailData.detailData`。
 - 请求 JSON 格式错误。
 
 HTTP 状态码:`400 Bad Request`
@@ -131,15 +135,17 @@ HTTP 状态码:`400 Bad Request`
 }
 ```
 
-`detailData` 可以为空数组:
+`detailData.detailData` 可以为空数组:
 
 ```json
 {
   "type": "cpmp402",
   "mainData": {
-    "tjr": "ERP_USER_001"
+    "tjr": "3075"
   },
-  "detailData": []
+  "detailData": {
+    "detailData": []
+  }
 }
 ```
 
@@ -172,7 +178,22 @@ HTTP 状态码:`400 Bad Request`
 }
 ```
 
-## 7. 日志说明
+## 7. 宜搭调用失败
+
+当底层宜搭或钉钉接口返回错误时,HTTP 状态码为 `502 Bad Gateway`,响应会返回上游错误码和错误信息,便于定位字段格式、权限或配置问题:
+
+```json
+{
+  "code": "DINGTALK_YIDA_ERROR",
+  "message": "上游字段校验失败"
+}
+```
+
+如果响应仍为 `YIDA_CALL_FAILED`,说明不是宜搭业务错误码,而是网络、JSON 序列化或其他未分类异常;此时需要查看应用错误日志中的异常堆栈。
+
+`ml_work_order_craft_price` 会复用通用 `mainData` 处理结果:`mainData.tjr` 会按工号查询员工花名册,取 `dingtalk_id` 作为流程发起人并写入申请人字段 `employeeField_msh186rr`;`mainData.rq` 作为默认申请日期;`mainData.gs` 写入申请公司字段 `textField_mu1yk6l5`;`mainData.bm` 部门 ID 直接以数组写入申请部门字段 `departmentSelectField_msh186ry`。
+
+## 8. 日志说明
 
 请求成功进入 Service 后,会记录以下两类 `INFO` 日志:
 
@@ -188,34 +209,36 @@ ERP Yida push request: {"type":"cpmp402","mainData":{"tjr":"ERP_USER_001"},"deta
 ERP Yida push response: {"success":true,"code":"200","message":"SUCCESS"}
 ```
 
-当前日志记录的是完整业务请求内容。生产环境如请求中包含密码、令牌或其他敏感信息,应在 ERP 端避免传输无关敏感字段;后续如接入敏感字段,需要增加日志脱敏策略。
+当前日志记录的是完整业务请求内容,以及宜搭返回的流程实例号。生产环境如请求中包含密码、令牌或其他敏感信息,应在 ERP 端避免传输无关敏感字段;后续如接入敏感字段,需要增加日志脱敏策略。
 
-## 8. 调用示例
+## 9. 调用示例
 
-### 8.1 PowerShell
+### 9.1 PowerShell
 
 ```powershell
 $body = @{
     type = "cpmp402"
     mainData = @{
-        rq = "2026-09-07"
-        bm = "采购部"
-        tjr = "ERP_USER_001"
-        gs = "明磊锂能有限公司"
-        t100id = "T100_USER_001"
-        ent = "CN"
-        t100bm = "PURCHASE"
+        rq = "2026-09-08"
+        bm = "ML07000000"
+        tjr = "3075"
+        gs = "ML"
+        t100id = "ML"
+        ent = 87
+        t100bm = "ML07000000"
+    }
+    detailData = @{
+        detailData = @(
+            @{
+                hjdh = "MLCH326090800001"
+                gysbh = "113009"
+                gysmc = "南京鼎泰五金工具有限公司"
+                t100id = "ML"
+                ent = "87"
+                yy = "zh_CN"
+            }
+        )
     }
-    detailData = @(
-        @{
-            hjdh = "HJ20260907001"
-            gysbh = "SUP001"
-            gysmc = "供应商名称"
-            t100id = "T100_SUP001"
-            ent = "CN"
-            yy = "zh_CN"
-        }
-    )
 } | ConvertTo-Json -Depth 10
 
 Invoke-RestMethod `
@@ -225,7 +248,7 @@ Invoke-RestMethod `
     -Body $body
 ```
 
-### 8.2 curl
+### 9.2 curl
 
 ```bash
 curl -X POST "http://10.0.0.250/api/erp/yida/push" \
@@ -233,38 +256,111 @@ curl -X POST "http://10.0.0.250/api/erp/yida/push" \
   -d '{
     "type": "cpmp402",
     "mainData": {
-      "rq": "2026-09-07",
-      "bm": "采购部",
-      "tjr": "ERP_USER_001",
-      "gs": "明磊锂能有限公司",
-      "t100id": "T100_USER_001",
-      "ent": "CN",
-      "t100bm": "PURCHASE"
+      "rq": "2026-09-08",
+      "bm": "ML07000000",
+      "tjr": "3075",
+      "gs": "ML",
+      "t100id": "ML",
+      "ent": 87,
+      "t100bm": "ML07000000"
     },
+    "detailData": {
+      "detailData": [
+        {
+          "hjdh": "MLCH326090800001",
+          "gysbh": "113009",
+          "gysmc": "南京鼎泰五金工具有限公司",
+          "t100id": "ML",
+          "ent": "87",
+          "yy": "zh_CN"
+        }
+      ]
+    }
+  }'
+```
+
+### 9.3 `ml_work_order_craft_price` 流程发起示例
+
+```json
+{
+  "type": "ml_work_order_craft_price",
+  "mainData": {
+    "rq": "2026-09-08",
+    "tjr": "3075"
+  },
+  "detailData": {
     "detailData": [
       {
-        "hjdh": "HJ20260907001",
-        "gysbh": "SUP001",
-        "gysmc": "供应商名称",
-        "t100id": "T100_SUP001",
-        "ent": "CN",
-        "yy": "zh_CN"
+        "标题": "工单工艺工价审定-GD-001",
+        "工单号": "GD-001",
+        "生产令": "MO-001",
+        "生产料号": "ITEM-001",
+        "品名": "电池包",
+        "规格": "24V",
+        "组装|包装": "组装",
+        "固定工价": "10.5",
+        "总单位工价": "17.5",
+        "明细表1": [
+          {
+            "工序编号": "GX-001",
+            "名称": "装配",
+            "数量": "2",
+            "工价": "3.5",
+            "合计": "7.0"
+          }
+        ],
+        "明细表2": [
+          {
+            "项次": "10",
+            "料号": "MAT-001",
+            "品名": "电池盒",
+            "规格": "A1",
+            "用量": "1.2",
+            "t100id": "ML",
+            "车间线长": "leader-001",
+            "企业编号": "87"
+          }
+        ]
       }
     ]
-  }'
+  }
+}
+```
+
+该类型发起宜搭流程,流程表单名 `ML-工单工艺工价审定`,表单 UUID `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K`,流程编码 `TPROC--WG966BA1VWF8Q1K4OGF8H9KW47RR3ZREH0YSM2`。字段 ID 已通过 OpenYida schema 获取,服务端支持中文字段名、常见英文/拼音字段名,也支持调用方在 `detailData.detailData[]` 的明细对象中直接传入宜搭 `fieldId` 覆盖默认映射。
+
+申请人、申请公司和申请部门对应字段固定为:
+
+- 申请人:`employeeField_msh186rr`,固定由 `mainData.tjr` 工号查询员工花名册后取 `dingtalk_id` 写入。
+- 申请公司:`textField_mu1yk6l5`,优先取 `mainData.gs`;`mainData.gs` 为空时可用明细中的 `申请公司`、`applyCompany`、`company` 兜底。
+- 申请部门:`departmentSelectField_msh186ry`,优先取 `mainData.bm` 部门 ID 直接写入;`mainData.bm` 为空时可用明细中的 `申请部门`、`applyDepartment`、`department`、`deptId` 兜底。
+
+如果 `mainData.gs` 或 `mainData.bm` 为空,需要在明细中兜底申请公司和申请部门,请传申请公司文本值与钉钉部门 ID:
+
+```json
+{
+  "detailData": {
+    "detailData": [
+      {
+        "申请公司": "浙江明磊锂能源科技股份有限公司",
+        "申请部门": ["123456"]
+      }
+    ]
+  }
+}
 ```
 
-## 9. 单据类型映射
+## 10. 单据类型映射
 
 | `type` | 单据名称 | 目标宜搭应用 | 当前状态 |
 | --- | --- | --- | --- |
-| `cpmp402` | 包材核价单通知 | `APP_RJJL69QUIZVQSV7YC8TL` | 仅接收并记录 ERP 推送数据,暂不调用宜搭 |
+| `cpmp402` | 包材核价单 | `APP_RJJL69QUIZVQSV7YC8TL` | 已按 `type` 路由到宜搭流程处理器 |
+| `ml_work_order_craft_price` | ML-工单工艺工价审定 | `APP_RJJL69QUIZVQSV7YC8TL` | 已按 `type` 路由到宜搭流程处理器 |
 
-## 10. 当前处理边界
+## 11. 当前处理边界
 
-- 当前不调用宜搭接口。
-- 当前不发起宜搭流程。
-- 当前不根据 `type` 分发具体单据处理器。
-- 当前不校验 `cpmp402` 以外的单据类型。
-- 当前不持久化 ERP 推送数据。
-- 后续接入宜搭时,`cpmp402` 应路由至宜搭应用 `APP_RJJL69QUIZVQSV7YC8TL` 下的包材核价单通知表单,并根据 `type` 增加单据类型处理器和字段映射逻辑。
+- 当前支持 `cpmp402` 和 `ml_work_order_craft_price`。
+- 当前 `type` 作为路由键,不作为表单 ID。
+- 当前发起人 `originatorUserId` 由 `mainData.tjr` 工号通过员工花名册解析。
+- 当前包材核价单表单 ID 和流程编码由代码中的 `Cpmp402YidaProcessHandler` 维护。
+- 当前 `ML-工单工艺工价审定` 是流程表单,必须走流程发起接口;表单 ID 和流程编码由代码中的 `MlWorkOrderCraftPriceFormPushHandler` 维护。

Файловите разлики са ограничени, защото са твърде много
+ 127 - 41
mjava-minglei/README.md


+ 10 - 0
mjava-minglei/pom.xml

@@ -52,6 +52,16 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-validation</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <version>8.0.33</version>
+            <scope>runtime</scope>
+        </dependency>
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>

+ 2 - 0
mjava-minglei/src/main/java/com/malk/minglei/MjavaMingleiApplication.java

@@ -2,7 +2,9 @@ package com.malk.minglei;
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
 
+@EnableScheduling
 @SpringBootApplication
 public class MjavaMingleiApplication {
 

+ 14 - 4
mjava-minglei/src/main/java/com/malk/minglei/config/MjavaYidaConfiguration.java

@@ -3,10 +3,13 @@ package com.malk.minglei.config;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.service.aliwork.YDClient;
+import com.malk.service.aliwork.YDClient_Process;
 import com.malk.service.aliwork.impl.YDClientImpl;
+import com.malk.service.aliwork.impl.YDClient_ProcessImpl;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.impl.DDImplClient;
 import com.malk.service.dingtalk.impl.DDImplClient_Contacts;
+import com.malk.service.dingtalk.impl.DDImplClient_Personnel;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -16,7 +19,7 @@ import java.lang.reflect.Field;
 
 @Configuration
 @EnableConfigurationProperties({DDConf.class, YidaApplicationsProperties.class})
-@Import({DDImplClient.class, DDImplClient_Contacts.class})
+@Import({DDImplClient.class, DDImplClient_Contacts.class, DDImplClient_Personnel.class})
 public class MjavaYidaConfiguration {
 
     @Bean("coordinationOfficeYidaClient")
@@ -29,6 +32,13 @@ public class MjavaYidaConfiguration {
         return createYidaClient(dingTalkClient, applications.getMasterData());
     }
 
+    @Bean("coordinationOfficeYidaProcessClient")
+    public YDClient_Process coordinationOfficeYidaProcessClient(DDClient dingTalkClient) {
+        YDClient_ProcessImpl yidaProcessClient = new YDClient_ProcessImpl();
+        inject(yidaProcessClient, "ddClient", dingTalkClient);
+        return yidaProcessClient;
+    }
+
     /**
      * 为 malk 自动扫描到的默认客户端提供协调办公应用配置。
      */
@@ -53,11 +63,11 @@ public class MjavaYidaConfiguration {
         return yidaConfiguration;
     }
 
-    private void inject(YDClientImpl yidaClient, String fieldName, Object value) {
+    private void inject(Object target, String fieldName, Object value) {
         try {
-            Field field = YDClientImpl.class.getDeclaredField(fieldName);
+            Field field = target.getClass().getDeclaredField(fieldName);
             field.setAccessible(true);
-            field.set(yidaClient, value);
+            field.set(target, value);
         } catch (ReflectiveOperationException exception) {
             throw new IllegalStateException("Unable to initialize malk Yida client", exception);
         }

+ 1 - 1
mjava-minglei/src/main/java/com/malk/minglei/controller/ErpYidaPushController.java

@@ -1,7 +1,7 @@
 package com.malk.minglei.controller;
 
 import com.malk.minglei.dto.ErpYidaPushRequest;
-import com.malk.minglei.service.ErpYidaPushService;
+import com.malk.minglei.service.yida.ErpYidaPushService;
 import com.malk.server.common.McR;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;

+ 30 - 2
mjava-minglei/src/main/java/com/malk/minglei/controller/GlobalExceptionHandler.java

@@ -1,11 +1,14 @@
 package com.malk.minglei.controller;
 
+import com.malk.server.common.McException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -22,6 +25,27 @@ public class GlobalExceptionHandler {
                 .body(Collections.singletonMap("message", exception.getMessage()));
     }
 
+    @ExceptionHandler(IllegalArgumentException.class)
+    public ResponseEntity<Map<String, String>> handleInvalidArgument(IllegalArgumentException exception) {
+        return ResponseEntity.badRequest()
+                .body(Collections.singletonMap("message", defaultText(exception.getMessage(), "Request parameter is invalid")));
+    }
+
+    @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
+    public ResponseEntity<Map<String, String>> handleBadRequest(Exception exception) {
+        return ResponseEntity.badRequest().body(Collections.singletonMap("message", "Request validation failed"));
+    }
+
+    @ExceptionHandler(McException.class)
+    public ResponseEntity<Map<String, String>> handleYidaClient(McException exception) {
+        LOGGER.error("Yida client request failed code={} source={}", exception.getCode(), exception.getSource(), exception);
+
+        Map<String, String> response = new LinkedHashMap<String, String>();
+        response.put("code", defaultText(exception.getCode(), "YIDA_CALL_FAILED"));
+        response.put("message", defaultText(exception.getMessage(), "Yida client request failed"));
+        return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(response);
+    }
+
     @ExceptionHandler(Exception.class)
     public ResponseEntity<Map<String, String>> handleUnexpected(Exception exception) {
         LOGGER.error("Yida client request failed", exception);
@@ -31,4 +55,8 @@ public class GlobalExceptionHandler {
         response.put("message", "Yida client request failed. Check the application log for details.");
         return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(response);
     }
+
+    private String defaultText(String value, String fallback) {
+        return value == null || value.trim().isEmpty() ? fallback : value.trim();
+    }
 }

+ 1 - 1
mjava-minglei/src/main/java/com/malk/minglei/controller/MingleiPortController.java

@@ -1,7 +1,7 @@
 package com.malk.minglei.controller;
 
 import com.malk.minglei.dto.MingleiPortRequest;
-import com.malk.minglei.service.MingleiPortService;
+import com.malk.minglei.service.port.MingleiPortService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;

+ 2 - 2
mjava-minglei/src/main/java/com/malk/minglei/controller/YidaProcessController.java

@@ -5,8 +5,8 @@ import com.malk.minglei.dto.DepartmentUserQueryRequest;
 import com.malk.minglei.dto.DepartmentUserIdsResponse;
 import com.malk.minglei.dto.ApiResponse;
 import com.malk.minglei.dto.YidaProcessStartRequest;
-import com.malk.minglei.service.MingleiDocumentService;
-import com.malk.minglei.service.YidaProcessService;
+import com.malk.minglei.service.yida.MingleiDocumentService;
+import com.malk.minglei.service.yida.YidaProcessService;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.validation.annotation.Validated;

+ 42 - 2
mjava-minglei/src/main/java/com/malk/minglei/dto/ErpYidaPushMainData.java

@@ -1,23 +1,53 @@
 package com.malk.minglei.dto;
 
-import javax.validation.constraints.NotBlank;
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
 
 /**
  * ERP 推送宜搭时的固定主数据。
  */
 public class ErpYidaPushMainData {
 
+    /**
+     * 日期。
+     */
     private String rq;
+
+    /**
+     * 部门:钉钉部门编号。
+     */
     private String bm;
 
-    @NotBlank
+    /**
+     * 提交人:工号。服务端根据该工号查询钉钉人员 ID。
+     */
     private String tjr;
 
+    /**
+     * 公司:代码。
+     */
     private String gs;
+
+    /**
+     * T100 用户或业务对象标识。
+     */
     private String t100id;
+
+    /**
+     * 地区代码。
+     */
     private String ent;
+
+    /**
+     * T100 部门。
+     */
     private String t100bm;
 
+    private final Map<String, Object> extraFields = new LinkedHashMap<String, Object>();
+
     public String getRq() {
         return rq;
     }
@@ -73,4 +103,14 @@ public class ErpYidaPushMainData {
     public void setT100bm(String t100bm) {
         this.t100bm = t100bm;
     }
+
+    @JsonAnyGetter
+    public Map<String, Object> getExtraFields() {
+        return extraFields;
+    }
+
+    @JsonAnySetter
+    public void setExtraField(String key, Object value) {
+        extraFields.put(key, value);
+    }
 }

+ 45 - 0
mjava-minglei/src/main/java/com/malk/minglei/dto/ErpYidaPushRequest.java

@@ -1,5 +1,7 @@
 package com.malk.minglei.dto;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
 import javax.validation.Valid;
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.NotNull;
@@ -12,6 +14,12 @@ public class ErpYidaPushRequest {
     @NotBlank
     private String type;
 
+    private String bm;
+
+    private String gs;
+
+    private String tjr;
+
     @Valid
     @NotNull
     private ErpYidaPushMainData mainData;
@@ -19,6 +27,9 @@ public class ErpYidaPushRequest {
     @NotNull
     private Object detailData;
 
+    @JsonIgnore
+    private String originatorUserId;
+
     public String getType() {
         return type;
     }
@@ -27,6 +38,30 @@ public class ErpYidaPushRequest {
         this.type = type;
     }
 
+    public String getBm() {
+        return bm;
+    }
+
+    public void setBm(String bm) {
+        this.bm = bm;
+    }
+
+    public String getGs() {
+        return gs;
+    }
+
+    public void setGs(String gs) {
+        this.gs = gs;
+    }
+
+    public String getTjr() {
+        return tjr;
+    }
+
+    public void setTjr(String tjr) {
+        this.tjr = tjr;
+    }
+
     public ErpYidaPushMainData getMainData() {
         return mainData;
     }
@@ -42,4 +77,14 @@ public class ErpYidaPushRequest {
     public void setDetailData(Object detailData) {
         this.detailData = detailData;
     }
+
+    @JsonIgnore
+    public String getOriginatorUserId() {
+        return originatorUserId;
+    }
+
+    @JsonIgnore
+    public void setOriginatorUserId(String originatorUserId) {
+        this.originatorUserId = originatorUserId;
+    }
 }

+ 9 - 0
mjava-minglei/src/main/java/com/malk/minglei/dto/YidaOperationParam.java

@@ -4,8 +4,17 @@ import com.malk.server.aliwork.YDParam;
 
 public class YidaOperationParam extends YDParam {
 
+    private String title;
     private String dataTitle;
 
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
     public String getDataTitle() {
         return dataTitle;
     }

+ 0 - 18
mjava-minglei/src/main/java/com/malk/minglei/service/ErpYidaPushService.java

@@ -1,18 +0,0 @@
-package com.malk.minglei.service;
-
-import com.malk.minglei.dto.ErpYidaPushRequest;
-import com.malk.server.common.McR;
-
-/**
- * ERP 推送宜搭数据接收服务。
- */
-public interface ErpYidaPushService {
-
-    /**
-     * 记录 ERP 推送数据,当前阶段不调用宜搭。
-     *
-     * @param request ERP 推送请求
-     * @return 固定成功响应
-     */
-    McR push(ErpYidaPushRequest request);
-}

+ 0 - 35
mjava-minglei/src/main/java/com/malk/minglei/service/MingleiDocumentService.java

@@ -1,35 +0,0 @@
-package com.malk.minglei.service;
-
-import com.malk.minglei.dto.MingleiDocumentQueryRequest;
-
-import java.util.List;
-import java.util.Map;
-
-public interface MingleiDocumentService {
-
-    /**
-     * 分页查询明蕾普通表单的单据实例。
-     *
-     * @param page 页码,从 1 开始
-     * @param size 每页记录数
-     * @return 包含 records、totalCount 和 pageNumber 的分页查询结果
-     */
-    Map<String, Object> queryDocuments(int page, int size);
-
-    /**
-     * 按部门编号、用户编号或部门名称的优先级查询明磊普通表单单据。
-     *
-     * @param request 包含 user_id、dept_id、dept_name 的查询参数
-     * @return 仅包含各级部门主管和部门分管领导用户 ID 的单层结果
-     */
-    Map<String, Object> queryDocuments(MingleiDocumentQueryRequest request);
-
-    /**
-     * 按查询范围获取指定部门中的钉钉用户 ID。
-     *
-     * @param departmentId 钉钉部门 ID
-     * @param type 查询范围,0 表示包含子部门,1 表示仅当前部门
-     * @return 去重后的钉钉 userid 列表
-     */
-    List<String> listDepartmentUserIds(long departmentId, int type);
-}

+ 0 - 19
mjava-minglei/src/main/java/com/malk/minglei/service/MingleiPortService.java

@@ -1,19 +0,0 @@
-package com.malk.minglei.service;
-
-import com.malk.minglei.dto.MingleiPortRequest;
-
-import java.util.Map;
-
-/**
- * 定义调用明磊外部单据接口的业务能力。
- */
-public interface MingleiPortService {
-
-    /**
-     * 将供应商价格审批单(核价)转发到外部 T100 接口,并返回外部响应内容。
-     *
-     * @param request 仅包含 head 和 detail 的本地请求体
-     * @return 外部接口原始响应体
-     */
-    Map<String, Object> submitSupplierPriceApproval(MingleiPortRequest request);
-}

+ 0 - 25
mjava-minglei/src/main/java/com/malk/minglei/service/YidaProcessHandler.java

@@ -1,25 +0,0 @@
-package com.malk.minglei.service;
-
-import com.malk.minglei.dto.YidaProcessStartRequest;
-
-/**
- * 定义单个宜搭流程表单的处理能力。
- * 每个表单类型由一个独立实现类维护表单标识、流程编码和业务处理逻辑。
- */
-public interface YidaProcessHandler {
-
-    /**
-     * 返回当前处理器支持的统一接口 type 值。
-     *
-     * @return 表单类型标识
-     */
-    String getType();
-
-    /**
-     * 根据请求参数发起当前表单对应的宜搭流程。
-     *
-     * @param request 统一流程发起请求
-     * @return 宜搭返回的流程实例 ID
-     */
-    String startProcess(YidaProcessStartRequest request);
-}

+ 0 - 14
mjava-minglei/src/main/java/com/malk/minglei/service/YidaProcessService.java

@@ -1,14 +0,0 @@
-package com.malk.minglei.service;
-
-import com.malk.minglei.dto.YidaProcessStartRequest;
-
-public interface YidaProcessService {
-
-    /**
-     * 使用请求中的 type、发起人、标题和表单字段创建对应的宜搭流程实例。
-     *
-     * @param request 流程发起请求
-     * @return 宜搭返回的流程实例 ID
-     */
-    String startProcess(YidaProcessStartRequest request);
-}

+ 0 - 89
mjava-minglei/src/main/java/com/malk/minglei/service/impl/BomYidaProcessHandler.java

@@ -1,89 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.alibaba.fastjson.JSON;
-import com.malk.minglei.config.YidaApplicationsProperties;
-import com.malk.minglei.dto.YidaOperationParam;
-import com.malk.minglei.dto.YidaProcessStartRequest;
-import com.malk.minglei.service.YidaProcessHandler;
-import com.malk.server.aliwork.YDConf;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.service.aliwork.YDClient;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-/**
- * 处理 BOM 表单的宜搭流程发起请求。
- */
-@Service
-public class BomYidaProcessHandler implements YidaProcessHandler {
-
-    private static final String FORM_UUID = "FORM-04001902BF9C40E6859102569FE139179LWH";
-    private static final String PROCESS_CODE = "TPROC--NNC669C1FTC8TKCLO79LYBRKZB2U3MT3QNTSM0";
-
-    /** 统一接口使用目标表单 ID 作为流程类型。 */
-    public static final String TYPE = FORM_UUID;
-
-    private final YDClient yidaClient;
-    private final DDConf dingTalkConfiguration;
-    private final YidaApplicationsProperties.ApplicationCredentials coordinationOfficeConfiguration;
-
-    public BomYidaProcessHandler(@Qualifier("coordinationOfficeYidaClient") YDClient yidaClient,
-                                 DDConf dingTalkConfiguration,
-                                 YidaApplicationsProperties applications) {
-        this.yidaClient = yidaClient;
-        this.dingTalkConfiguration = dingTalkConfiguration;
-        this.coordinationOfficeConfiguration = applications.getCoordinationOffice();
-    }
-
-    /**
-     * 返回 BOM 表单 ID,作为统一接口的流程类型。
-     *
-     * @return BOM 表单 ID
-     */
-    @Override
-    public String getType() {
-        return TYPE;
-    }
-
-    /**
-     * 组装 BOM 表单参数,并通过 malk 宜搭客户端发起流程。
-     *
-     * @param request 统一流程发起请求
-     * @return 宜搭返回的流程实例 ID
-     */
-    @Override
-    public String startProcess(YidaProcessStartRequest request) {
-        validateConfiguration();
-
-        YidaOperationParam parameter = new YidaOperationParam();
-        parameter.setAppType(coordinationOfficeConfiguration.getAppType());
-        parameter.setSystemToken(coordinationOfficeConfiguration.getSystemToken());
-        parameter.setUserId(request.getUserId());
-        parameter.setFormUuid(FORM_UUID);
-        parameter.setProcessCode(PROCESS_CODE);
-        parameter.setFormDataJson(JSON.toJSONString(request.getFields()));
-        parameter.setDataTitle(request.getDataTitle());
-
-        Object result = yidaClient.operateData(parameter, YDConf.FORM_OPERATION.start);
-        if (result == null) {
-            throw new IllegalStateException("Yida process start returned an empty result");
-        }
-        return String.valueOf(result);
-    }
-
-    /**
-     * 校验 BOM 表单流程调用所需的应用级鉴权配置是否完整。
-     */
-    private void validateConfiguration() {
-        require(dingTalkConfiguration.getAppKey(), "DINGTALK_APP_KEY is not configured");
-        require(dingTalkConfiguration.getAppSecret(), "DINGTALK_APP_SECRET is not configured");
-        require(coordinationOfficeConfiguration.getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
-        require(coordinationOfficeConfiguration.getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
-    }
-
-    private void require(String value, String message) {
-        if (value == null || value.trim().isEmpty()) {
-            throw new IllegalStateException(message);
-        }
-    }
-}

+ 0 - 56
mjava-minglei/src/main/java/com/malk/minglei/service/impl/ErpYidaPushServiceImpl.java

@@ -1,56 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.malk.minglei.dto.ErpYidaPushRequest;
-import com.malk.minglei.service.ErpYidaPushService;
-import com.malk.server.common.McR;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Service;
-
-/**
- * ERP 推送宜搭数据接收服务实现。
- */
-@Service
-public class ErpYidaPushServiceImpl implements ErpYidaPushService {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(ErpYidaPushServiceImpl.class);
-
-    private final Logger logger;
-    private final ObjectMapper objectMapper;
-
-    /**
-     * 使用应用日志记录器创建服务。
-     */
-    public ErpYidaPushServiceImpl() {
-        this(LOGGER);
-    }
-
-    ErpYidaPushServiceImpl(Logger logger) {
-        this.logger = logger;
-        this.objectMapper = new ObjectMapper();
-    }
-
-    /**
-     * 打印 ERP 原始业务入参,当前不调用宜搭。
-     *
-     * @param request ERP 推送请求
-     * @return 固定成功响应
-     */
-    @Override
-    public McR push(ErpYidaPushRequest request) {
-        try {
-            logger.info("ERP Yida push request: {}", objectMapper.writeValueAsString(request));
-        } catch (JsonProcessingException exception) {
-            logger.warn("ERP Yida push request could not be serialized", exception);
-        }
-        McR response = McR.success();
-        try {
-            logger.info("ERP Yida push response: {}", objectMapper.writeValueAsString(response));
-        } catch (JsonProcessingException exception) {
-            logger.warn("ERP Yida push response could not be serialized", exception);
-        }
-        return response;
-    }
-}

+ 0 - 357
mjava-minglei/src/main/java/com/malk/minglei/service/impl/MingleiDocumentServiceImpl.java

@@ -1,357 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.malk.minglei.config.YidaApplicationsProperties;
-import com.malk.minglei.dto.MingleiDocumentQueryRequest;
-import com.malk.minglei.service.MingleiDocumentService;
-import com.malk.server.aliwork.YDConf;
-import com.malk.server.aliwork.YDParam;
-import com.malk.server.dingtalk.DDR_New;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.service.aliwork.YDClient;
-import com.malk.service.dingtalk.DDClient;
-import com.malk.service.dingtalk.DDClient_Contacts;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-@Service
-public class MingleiDocumentServiceImpl implements MingleiDocumentService {
-
-    private static final String MINGLEI_DOCUMENT_FORM_UUID = "FORM-8F527F696C5B4AF7AB7A53979527FBA23USO";
-    private static final String DEPARTMENT_ID_FIELD_ID = "textField_msztwhs2";
-    private static final String DEPARTMENT_NAME_FIELD_ID = "textField_mszr6tuz";
-    private static final String FIRST_LEVEL_DEPARTMENT_MANAGER_FIELD_ID = "employeeField_mszdqrl8";
-    private static final String SECOND_LEVEL_DEPARTMENT_MANAGER_FIELD_ID = "employeeField_mszdqrl9";
-    private static final String THIRD_LEVEL_DEPARTMENT_MANAGER_FIELD_ID = "employeeField_mszdqrla";
-    private static final String FOURTH_LEVEL_DEPARTMENT_MANAGER_FIELD_ID = "employeeField_mszdqrlb";
-    private static final String DEPARTMENT_LEADER_FIELD_ID = "employeeField_mszdqrl3";
-    private static final String[] DEPARTMENT_MANAGER_FIELD_IDS = {
-            FIRST_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
-            SECOND_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
-            THIRD_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
-            FOURTH_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
-            DEPARTMENT_LEADER_FIELD_ID
-    };
-    private static final int PRECISE_QUERY_PAGE_SIZE = 100;
-
-    private final YDClient yidaClient;
-    private final DDClient dingTalkClient;
-    private final DDClient_Contacts dingTalkContactsClient;
-    private final DDConf dingTalkConfiguration;
-    private final YidaApplicationsProperties.ApplicationCredentials masterDataConfiguration;
-    private final ObjectMapper objectMapper;
-
-    public MingleiDocumentServiceImpl(@Qualifier("masterDataYidaClient") YDClient yidaClient,
-                                      DDClient dingTalkClient,
-                                      DDClient_Contacts dingTalkContactsClient,
-                                      DDConf dingTalkConfiguration,
-                                      YidaApplicationsProperties applications,
-                                      ObjectMapper objectMapper) {
-        this.yidaClient = yidaClient;
-        this.dingTalkClient = dingTalkClient;
-        this.dingTalkContactsClient = dingTalkContactsClient;
-        this.dingTalkConfiguration = dingTalkConfiguration;
-        this.masterDataConfiguration = applications.getMasterData();
-        this.objectMapper = objectMapper;
-    }
-
-    /**
-     * 使用明磊表单 UUID 组装查询条件,并通过 malk 的普通表单查询接口返回分页单据。
-     *
-     * @param page 页码,从 1 开始
-     * @param size 每页记录数
-     * @return 包含记录列表、总数和页码的分页查询结果
-     */
-    @Override
-    public Map<String, Object> queryDocuments(int page, int size) {
-        return executeQuery(null, null, page, size);
-    }
-
-    /**
-     * 根据请求参数的优先级生成精准筛选条件:部门编号、用户所属部门、部门名称。
-     *
-     * @param request 包含 user_id、dept_id、dept_name 的查询参数
-     * @return 仅包含各级部门主管和部门分管领导用户 ID 的单层结果
-     */
-    @Override
-    public Map<String, Object> queryDocuments(MingleiDocumentQueryRequest request) {
-        if (request == null) {
-            throw new IllegalArgumentException("Query request must not be null");
-        }
-
-        String departmentId = trimToNull(request.getDeptId());
-        if (departmentId != null) {
-            return extractDepartmentManagers(executeQuery(DEPARTMENT_ID_FIELD_ID, departmentId, 1, PRECISE_QUERY_PAGE_SIZE));
-        }
-
-        String userId = trimToNull(request.getUserId());
-        if (userId != null) {
-            return extractDepartmentManagers(executeQuery(DEPARTMENT_ID_FIELD_ID, resolveDepartmentId(userId), 1, PRECISE_QUERY_PAGE_SIZE));
-        }
-
-        String departmentName = trimToNull(request.getDeptName());
-        if (departmentName != null) {
-            return extractDepartmentManagers(executeQuery(DEPARTMENT_NAME_FIELD_ID, departmentName, 1, PRECISE_QUERY_PAGE_SIZE));
-        }
-
-        throw new IllegalArgumentException("One of dept_id, user_id or dept_name must be provided");
-    }
-
-    /**
-     * 按查询范围读取部门成员 ID,并按成员首次出现顺序去重。
-     *
-     * @param departmentId 钉钉部门 ID
-     * @param type 查询范围,0 表示包含子部门,1 表示仅当前部门
-     * @return 去重后的钉钉 userid 列表
-     */
-    @Override
-    public List<String> listDepartmentUserIds(long departmentId, int type) {
-        String accessToken = dingTalkClient.getAccessToken();
-        require(accessToken, "Unable to obtain DingTalk access token");
-
-        Set<Long> departmentIds = new LinkedHashSet<Long>();
-        departmentIds.add(departmentId);
-
-        // 仅 type 为 0 时查询子部门,type 为 1 时保留当前部门。
-        if (type == 0) {
-            List<Long> childDepartmentIds = dingTalkContactsClient.getDepartmentId_all(accessToken, false, departmentId);
-            if (childDepartmentIds != null) {
-                departmentIds.addAll(childDepartmentIds);
-            }
-        }
-
-        Set<String> userIds = new LinkedHashSet<String>();
-        for (Long currentDepartmentId : departmentIds) {
-            if (currentDepartmentId == null) {
-                continue;
-            }
-            List<String> departmentUserIds = dingTalkContactsClient.listDepartmentUserId(accessToken, currentDepartmentId.longValue());
-            if (departmentUserIds == null) {
-                continue;
-            }
-            for (String userId : departmentUserIds) {
-                String normalizedUserId = trimToNull(userId);
-                if (normalizedUserId != null) {
-                    userIds.add(normalizedUserId);
-                }
-            }
-        }
-        return new ArrayList<String>(userIds);
-    }
-
-    private Map<String, Object> executeQuery(String fieldId, String fieldValue, int page, int size) {
-        validateConfiguration();
-
-        YDParam parameter = new YDParam();
-        parameter.setAppType(masterDataConfiguration.getAppType());
-        parameter.setSystemToken(masterDataConfiguration.getSystemToken());
-        parameter.setFormUuid(MINGLEI_DOCUMENT_FORM_UUID);
-        parameter.setCurrentPage(page);
-        parameter.setPageSize(size);
-        if (fieldId != null) {
-            parameter.setSearchFieldJson(createExactSearchFieldJson(fieldId, fieldValue));
-        }
-
-        DDR_New response = yidaClient.queryData(parameter, YDConf.FORM_QUERY.retrieve_search_form);
-        if (response == null) {
-            throw new IllegalStateException("Yida form query returned an empty response");
-        }
-        response.assertSuccess();
-
-        Map<String, Object> result = new LinkedHashMap<String, Object>();
-        Object records = response.getRecords();
-        if (records == null) {
-            records = response.getResult();
-        }
-        if (records == null) {
-            records = response.getData();
-        }
-        result.put("records", records);
-        result.put("totalCount", response.getTotalCount());
-        result.put("pageNumber", response.getPageNumber());
-        return result;
-    }
-
-    /**
-     * 从精准查询命中的首条单据提取五个主管字段对应的用户 ID,并直接返回到顶层。
-     *
-     * @param queryResult 宜搭列表查询结果
-     * @return 仅包含一级至四级部门主管和部门分管领导的单层结果
-     */
-    private Map<String, Object> extractDepartmentManagers(Map<String, Object> queryResult) {
-        Map formData = firstFormData(queryResult.get("records"));
-        if (formData == null) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, Object> result = new LinkedHashMap<String, Object>();
-        for (String fieldId : DEPARTMENT_MANAGER_FIELD_IDS) {
-            result.put(fieldId, firstEmployeeUserId(formData.get(fieldId + "_id")));
-        }
-        return result;
-    }
-
-    private Map firstFormData(Object records) {
-        if (!(records instanceof Iterable)) {
-            return null;
-        }
-        for (Object record : (Iterable<?>) records) {
-            if (!(record instanceof Map)) {
-                return null;
-            }
-            Object formData = ((Map) record).get("formData");
-            return formData instanceof Map ? (Map) formData : null;
-        }
-        return null;
-    }
-
-    private Object firstValue(Object values) {
-        if (values instanceof Iterable) {
-            for (Object value : (Iterable<?>) values) {
-                return value;
-            }
-            return null;
-        }
-        return values;
-    }
-
-    /**
-     * 从宜搭成员字段对应的 ID 数组中提取首个钉钉 userid。
-     *
-     * @param employeeIds 宜搭成员字段对应的 ID 数组
-     * @return 钉钉 userid;字段未填写或无法识别时为 null
-     */
-    private String firstEmployeeUserId(Object employeeIds) {
-        Object employee = firstValue(employeeIds);
-        if (employee == null) {
-            return null;
-        }
-        if (!(employee instanceof Map)) {
-            return toStringValue(employee);
-        }
-
-        Map employeeInfo = (Map) employee;
-        String userId = toStringValue(employeeInfo.get("userid"));
-        if (userId == null) {
-            userId = toStringValue(employeeInfo.get("userId"));
-        }
-        if (userId == null) {
-            userId = toStringValue(employeeInfo.get("user_id"));
-        }
-        return userId == null ? toStringValue(employeeInfo.get("id")) : userId;
-    }
-
-    /**
-     * 调用钉钉通讯录接口读取员工所属部门,并取返回列表中的首个部门编号作为查询条件。
-     *
-     * @param userId 钉钉用户编号
-     * @return 员工所属的部门编号
-     */
-    private String resolveDepartmentId(String userId) {
-        Map userInfo = dingTalkContactsClient.getUserInfoById(dingTalkClient.getAccessToken(), userId);
-        if (userInfo == null) {
-            throw new IllegalArgumentException("No DingTalk user information was found for user_id");
-        }
-
-        String mainDepartmentId = toStringValue(userInfo.get("main_dept_id"));
-        if (mainDepartmentId != null) {
-            return mainDepartmentId;
-        }
-
-        String departmentId = firstDepartmentId(userInfo.get("department"));
-        if (departmentId == null) {
-            departmentId = firstDepartmentId(userInfo.get("dept_id_list"));
-        }
-        if (departmentId == null) {
-            throw new IllegalArgumentException("No department was found for user_id");
-        }
-        return departmentId;
-    }
-
-    /**
-     * 按宜搭普通文本字段的等值查询格式生成 searchFieldJson。
-     *
-     * @param fieldId 宜搭字段 ID
-     * @param value 需要精确匹配的字段值
-     * @return 宜搭搜索条件 JSON
-     */
-    private String createExactSearchFieldJson(String fieldId, String value) {
-        Map<String, String> condition = new LinkedHashMap<String, String>();
-        condition.put("key", fieldId);
-        condition.put("value", value);
-        condition.put("type", "TEXT");
-        condition.put("operator", "eq");
-        condition.put("componentName", "TextField");
-        try {
-            return objectMapper.writeValueAsString(Collections.singletonList(condition));
-        } catch (JsonProcessingException exception) {
-            throw new IllegalStateException("Unable to create Yida search condition", exception);
-        }
-    }
-
-    private String firstDepartmentId(Object departmentIds) {
-        if (departmentIds instanceof Iterable) {
-            for (Object departmentId : (Iterable<?>) departmentIds) {
-                String value = toStringValue(departmentId);
-                if (value != null) {
-                    return value;
-                }
-            }
-            return null;
-        }
-        if (departmentIds != null && departmentIds.getClass().isArray()) {
-            Object[] values = (Object[]) departmentIds;
-            for (Object departmentId : values) {
-                String value = toStringValue(departmentId);
-                if (value != null) {
-                    return value;
-                }
-            }
-            return null;
-        }
-        return toStringValue(departmentIds);
-    }
-
-    private String toStringValue(Object value) {
-        return value == null ? null : trimToNull(String.valueOf(value));
-    }
-
-    private String trimToNull(String value) {
-        if (value == null) {
-            return null;
-        }
-        String trimmedValue = value.trim();
-        return trimmedValue.isEmpty() ? null : trimmedValue;
-    }
-
-    /**
-     * 校验调用宜搭所需的应用级鉴权配置是否完整。
-     */
-    private void validateConfiguration() {
-        require(dingTalkConfiguration.getAppKey(), "DINGTALK_APP_KEY is not configured");
-        require(dingTalkConfiguration.getAppSecret(), "DINGTALK_APP_SECRET is not configured");
-        require(masterDataConfiguration.getAppType(), "YIDA_MASTER_DATA_APP_TYPE is not configured");
-        require(masterDataConfiguration.getSystemToken(), "YIDA_MASTER_DATA_SYSTEM_TOKEN is not configured");
-    }
-
-    /**
-     * 校验单个配置值非空,避免以不完整配置调用远端接口。
-     *
-     * @param value 待校验配置值
-     * @param message 配置缺失时抛出的错误信息
-     */
-    private void require(String value, String message) {
-        if (value == null || value.trim().isEmpty()) {
-            throw new IllegalStateException(message);
-        }
-    }
-}

+ 0 - 132
mjava-minglei/src/main/java/com/malk/minglei/service/impl/MingleiPortServiceImpl.java

@@ -1,132 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.malk.minglei.config.MingleiPortProperties;
-import com.malk.minglei.dto.MingleiPortRequest;
-import com.malk.minglei.service.MingleiPortService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
-import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * 使用 HTTP 客户端调用供应商价格审批单(核价)外部接口。
- */
-@Service
-public class MingleiPortServiceImpl implements MingleiPortService {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(MingleiPortServiceImpl.class);
-    private static final String TYPE = "sync";
-    private static final String HOST_PRODUCT = "APP";
-    private static final String HOST_IP = "10.0.2.60";
-    private static final String HOST_LANGUAGE = "zh_CN";
-    private static final String HOST_ACCOUNT = "tiptop";
-    private static final String HOST_TIMESTAMP = "20260807164700004";
-    private static final String SERVICE_PRODUCT = "T100";
-    private static final String SERVICE_NAME = "ProcurementApprovalPrice";
-    private static final String SERVICE_ID = "toptst";
-    private static final String ENTERPRISE_ID = "87";
-    private static final String COMPANY_ID = "ML";
-
-    private final RestTemplate restTemplate;
-    private final MingleiPortProperties properties;
-
-    public MingleiPortServiceImpl(@Qualifier("mingleiPortRestTemplate") RestTemplate restTemplate,
-                                  MingleiPortProperties properties) {
-        this.restTemplate = restTemplate;
-        this.properties = properties;
-    }
-
-    /**
-     * 将供应商价格审批单 JSON 按原有字段结构发送到外部 T100 接口。
-     *
-     * @param request 仅包含 head 和 detail 的本地请求体
-     * @return 外部接口原始响应体;外部响应无请求体时返回空 Map
-     */
-    @Override
-    public Map<String, Object> submitSupplierPriceApproval(MingleiPortRequest request) {
-        HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(MediaType.APPLICATION_JSON);
-        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-
-        validateConfiguration();
-        LOGGER.info("Submitting supplier price approval documentNo={} detailCount={}",
-                new Object[]{request.getDocumentNumber(), request.getDetailCount()});
-
-        ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
-                properties.getSupplierPriceApprovalUrl(),
-                HttpMethod.POST,
-                new HttpEntity<Map<String, Object>>(createExternalRequest(request), headers),
-                new ParameterizedTypeReference<Map<String, Object>>() {
-                });
-        return response.getBody() == null ? Collections.<String, Object>emptyMap() : response.getBody();
-    }
-
-    /**
-     * 使用固定的外层协议字段组装外部接口请求,仅透传调用方提供的 head 和 detail。
-     *
-     * @param request 本地精简请求体
-     * @return 外部接口完整请求体
-     */
-    private Map<String, Object> createExternalRequest(MingleiPortRequest request) {
-        Map<String, Object> host = new LinkedHashMap<String, Object>();
-        host.put("prod", HOST_PRODUCT);
-        host.put("ip", HOST_IP);
-        host.put("lang", HOST_LANGUAGE);
-        host.put("acct", HOST_ACCOUNT);
-        host.put("timestamp", HOST_TIMESTAMP);
-
-        Map<String, Object> service = new LinkedHashMap<String, Object>();
-        service.put("prod", SERVICE_PRODUCT);
-        service.put("name", SERVICE_NAME);
-        service.put("ip", HOST_IP);
-        service.put("id", SERVICE_ID);
-
-        Map<String, Object> dataKey = new LinkedHashMap<String, Object>();
-        dataKey.put("EntId", ENTERPRISE_ID);
-        dataKey.put("CompanyId", COMPANY_ID);
-
-        Map<String, Object> parameter = new LinkedHashMap<String, Object>();
-        parameter.put("head", request.getHead());
-        parameter.put("detail", request.getDetail());
-
-        Map<String, Object> standardData = new LinkedHashMap<String, Object>();
-        standardData.put("parameter", parameter);
-
-        Map<String, Object> payload = new LinkedHashMap<String, Object>();
-        payload.put("std_data", standardData);
-
-        Map<String, Object> externalRequest = new LinkedHashMap<String, Object>();
-        externalRequest.put("key", properties.getSupplierPriceApprovalKey());
-        externalRequest.put("type", TYPE);
-        externalRequest.put("host", host);
-        externalRequest.put("service", service);
-        externalRequest.put("datakey", dataKey);
-        externalRequest.put("payload", payload);
-        return externalRequest;
-    }
-
-    /**
-     * 校验调用外部接口需要的地址和密钥配置是否完整。
-     */
-    private void validateConfiguration() {
-        require(properties.getSupplierPriceApprovalUrl(), "MINGLEI_SUPPLIER_PRICE_APPROVAL_URL is not configured");
-        require(properties.getSupplierPriceApprovalKey(), "MINGLEI_SUPPLIER_PRICE_APPROVAL_KEY is not configured");
-    }
-
-    private void require(String value, String message) {
-        if (value == null || value.trim().isEmpty()) {
-            throw new IllegalStateException(message);
-        }
-    }
-}

+ 0 - 54
mjava-minglei/src/main/java/com/malk/minglei/service/impl/YidaProcessServiceImpl.java

@@ -1,54 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.malk.minglei.dto.YidaProcessStartRequest;
-import com.malk.minglei.service.YidaProcessHandler;
-import com.malk.minglei.service.YidaProcessService;
-import org.springframework.stereotype.Service;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-
-/**
- * 按请求 type 将统一流程接口分发到对应表单处理器。
- */
-@Service
-public class YidaProcessServiceImpl implements YidaProcessService {
-
-    private final Map<String, YidaProcessHandler> handlers;
-
-    public YidaProcessServiceImpl(List<YidaProcessHandler> handlers) {
-        Map<String, YidaProcessHandler> handlerMap = new LinkedHashMap<String, YidaProcessHandler>();
-        for (YidaProcessHandler handler : handlers) {
-            String type = normalizeType(handler.getType());
-            if (handlerMap.put(type, handler) != null) {
-                throw new IllegalStateException("Duplicate Yida process handler type: " + type);
-            }
-        }
-        this.handlers = Collections.unmodifiableMap(handlerMap);
-    }
-
-    /**
-     * 按请求 type 选择对应表单处理器并发起流程实例。
-     *
-     * @param request 统一流程发起请求
-     * @return 宜搭返回的流程实例 ID
-     */
-    @Override
-    public String startProcess(YidaProcessStartRequest request) {
-        YidaProcessHandler handler = handlers.get(normalizeType(request.getType()));
-        if (handler == null) {
-            throw new IllegalArgumentException("Unsupported Yida process type: " + request.getType());
-        }
-        return handler.startProcess(request);
-    }
-
-    private String normalizeType(String type) {
-        if (type == null || type.trim().isEmpty()) {
-            throw new IllegalArgumentException("Yida process type must not be blank");
-        }
-        return type.trim().toUpperCase(Locale.ROOT);
-    }
-}

+ 8 - 0
mjava-minglei/src/main/resources/application.yml

@@ -16,6 +16,7 @@ spring:
     active: ${SPRING_PROFILES_ACTIVE:dev}
 
 dingtalk:
+  agent-id: ${DINGTALK_AGENT_ID:4858845168}
   app-key: ${DINGTALK_APP_KEY:dingpdriyshfwyzxhe9y}
   app-secret: ${DINGTALK_APP_SECRET:z1YxVWLauwNdns9cyIJEg58Lz0S2XLdxMmbOkk-xISmeHoKdmusyt2xB4N8vpIFj}
 
@@ -31,6 +32,13 @@ minglei:
       app-type: ${YIDA_MASTER_DATA_APP_TYPE:APP_KI0AOTH7OLFS2UAUHZXI}
       system-token: ${YIDA_MASTER_DATA_SYSTEM_TOKEN:0E966191H5C8CRUCHOOOL4UMHY7Y2A2K8QTSMDY7}
 
+  employee-roster:
+    datasource:
+      url: ${MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_URL:jdbc:mysql://127.0.0.1:3306/yida?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false}
+      username: ${MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_USERNAME:root}
+      password: ${MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_PASSWORD:Minglei_2026}
+      driver-class-name: ${MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
+
 logging:
   file:
     path: ${LOGGING_FILE_PATH:logs}

+ 66 - 4
mjava-minglei/src/test/java/com/malk/minglei/controller/ErpYidaPushControllerTest.java

@@ -1,7 +1,11 @@
 package com.malk.minglei.controller;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.malk.minglei.service.impl.ErpYidaPushServiceImpl;
+import com.malk.minglei.dto.EmployeeRosterInfo;
+import com.malk.minglei.service.employee.EmployeeRosterService;
+import com.malk.minglei.service.impl.yida.ErpYidaPushServiceImpl;
+import com.malk.minglei.service.yida.ErpYidaPushService;
+import com.malk.server.common.McException;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.springframework.http.MediaType;
 import org.springframework.test.web.servlet.MockMvc;
@@ -10,13 +14,28 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 class ErpYidaPushControllerTest {
 
+    private final EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
+
     private final MockMvc mockMvc = MockMvcBuilders
-            .standaloneSetup(new ErpYidaPushController(new ErpYidaPushServiceImpl()))
+            .standaloneSetup(new ErpYidaPushController(new ErpYidaPushServiceImpl(
+                    request -> "process-instance-1",
+                    java.util.Collections.emptyList(),
+                    employeeRosterService)))
+            .setControllerAdvice(new GlobalExceptionHandler())
             .build();
 
+    @BeforeEach
+    void setUp() {
+        EmployeeRosterInfo employee = new EmployeeRosterInfo();
+        employee.setDingtalkId("dingtalk-user-001");
+        when(employeeRosterService.findByEmployeeNo("user-001")).thenReturn(employee);
+    }
+
     @Test
     void acceptsObjectDetailDataAndReturnsMcRSuccess() throws Exception {
         String request = "{\"type\":\"cpmp402\",\"mainData\":{\"tjr\":\"user-001\"},"
@@ -29,13 +48,56 @@ class ErpYidaPushControllerTest {
                 .andExpect(jsonPath("$.code").value(200));
     }
 
+    @Test
+    void acceptsTopLevelSubmitterAndReturnsMcRSuccess() throws Exception {
+        String request = "{\"type\":\"cpmp402\",\"bm\":\"123456\",\"gs\":\"ML\",\"tjr\":\"user-001\",\"mainData\":{},"
+                + "\"detailData\":{\"detailData\":[{\"hjdh\":\"HJ-001\",\"customField\":123}]}}";
+
+        mockMvc.perform(post("/api/erp/yida/push")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(request))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200));
+    }
+
     @Test
     void rejectsRequestWithoutMainDataTjr() throws Exception {
-        String request = "{\"type\":\"cpmp402\",\"mainData\":{},\"detailData\":[]}";
+        String request = "{\"type\":\"cpmp402\",\"mainData\":{},\"detailData\":{\"detailData\":[]}}";
 
         mockMvc.perform(post("/api/erp/yida/push")
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(request))
                 .andExpect(status().isBadRequest());
     }
+
+    @Test
+    void rejectsArrayDetailDataWithoutWrapper() throws Exception {
+        String request = "{\"type\":\"cpmp402\",\"mainData\":{\"tjr\":\"user-001\"},\"detailData\":[]}";
+
+        mockMvc.perform(post("/api/erp/yida/push")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(request))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.message").value("ERP Yida push detailData must be an object containing detailData array"));
+    }
+
+    @Test
+    void returnsYidaClientErrorDetails() throws Exception {
+        ErpYidaPushService service = request -> {
+            throw new McException("DINGTALK_YIDA_ERROR", "上游字段校验失败");
+        };
+        MockMvc errorMockMvc = MockMvcBuilders
+                .standaloneSetup(new ErpYidaPushController(service))
+                .setControllerAdvice(new GlobalExceptionHandler())
+                .build();
+
+        String request = "{\"type\":\"ml_work_order_craft_price\",\"mainData\":{\"tjr\":\"user-001\"},\"detailData\":{\"detailData\":[]}}";
+
+        errorMockMvc.perform(post("/api/erp/yida/push")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(request))
+                .andExpect(status().isBadGateway())
+                .andExpect(jsonPath("$.code").value("DINGTALK_YIDA_ERROR"))
+                .andExpect(jsonPath("$.message").value("上游字段校验失败"));
+    }
 }

+ 2 - 2
mjava-minglei/src/test/java/com/malk/minglei/controller/YidaProcessControllerTest.java

@@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.dto.ApiResponse;
 import com.malk.minglei.dto.YidaProcessStartRequest;
-import com.malk.minglei.service.MingleiDocumentService;
-import com.malk.minglei.service.YidaProcessService;
+import com.malk.minglei.service.yida.MingleiDocumentService;
+import com.malk.minglei.service.yida.YidaProcessService;
 import org.junit.jupiter.api.Test;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;

+ 0 - 38
mjava-minglei/src/test/java/com/malk/minglei/service/impl/ErpYidaPushServiceImplTest.java

@@ -1,38 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.malk.minglei.dto.ErpYidaPushRequest;
-import com.malk.minglei.dto.ErpYidaPushMainData;
-import com.malk.server.common.McR;
-import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-
-import java.util.Collections;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-class ErpYidaPushServiceImplTest {
-
-    @Test
-    void logsRequestAndReturnsSuccessWithoutCallingYida() {
-        Logger logger = mock(Logger.class);
-        ErpYidaPushServiceImpl service = new ErpYidaPushServiceImpl(logger);
-        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
-        mainData.setTjr("user-001");
-        ErpYidaPushRequest request = new ErpYidaPushRequest();
-        request.setType("cpmp402");
-        request.setMainData(mainData);
-        request.setDetailData(Collections.<Map<String, Object>>singletonList(
-                Collections.<String, Object>singletonMap("hjdh", "HJ-001")));
-
-        McR result = service.push(request);
-
-        assertNotNull(result);
-        verify(logger).info(org.mockito.ArgumentMatchers.startsWith("ERP Yida push request: {}"),
-                org.mockito.ArgumentMatchers.anyString());
-        verify(logger).info(org.mockito.ArgumentMatchers.startsWith("ERP Yida push response: {}"),
-                org.mockito.ArgumentMatchers.anyString());
-    }
-}

+ 0 - 72
mjava-minglei/src/test/java/com/malk/minglei/service/impl/MingleiDocumentServiceImplTest.java

@@ -1,72 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.malk.minglei.config.YidaApplicationsProperties;
-import com.malk.server.dingtalk.DDConf;
-import com.malk.service.aliwork.YDClient;
-import com.malk.service.dingtalk.DDClient;
-import com.malk.service.dingtalk.DDClient_Contacts;
-import org.junit.jupiter.api.Test;
-
-import java.util.Arrays;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-
-class MingleiDocumentServiceImplTest {
-
-    /**
-     * 验证 type 为 0 时会合并当前部门与子部门成员,并保持去重顺序。
-     */
-    @Test
-    void listDepartmentUserIdsIncludesChildDepartmentsAndRemovesDuplicates() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(contactsClient.getDepartmentId_all("access-token", false, 10L)).willReturn(Arrays.asList(11L, 12L));
-        given(contactsClient.listDepartmentUserId("access-token", 10L)).willReturn(Arrays.asList("user-1", "user-shared", " "));
-        given(contactsClient.listDepartmentUserId("access-token", 11L)).willReturn(Arrays.asList("user-shared", "user-2"));
-        given(contactsClient.listDepartmentUserId("access-token", 12L)).willReturn(Arrays.asList("user-3"));
-
-        MingleiDocumentServiceImpl service = new MingleiDocumentServiceImpl(
-                mock(YDClient.class),
-                dingTalkClient,
-                contactsClient,
-                mock(DDConf.class),
-                new YidaApplicationsProperties(),
-                new ObjectMapper());
-
-        List<String> userIds = service.listDepartmentUserIds(10L, 0);
-
-        assertEquals(Arrays.asList("user-1", "user-shared", "user-2", "user-3"), userIds);
-        verify(contactsClient).getDepartmentId_all("access-token", false, 10L);
-    }
-
-    /**
-     * 验证 type 为 1 时仅查询当前部门,且不访问子部门列表。
-     */
-    @Test
-    void listDepartmentUserIdsDoesNotIncludeChildDepartmentsWhenTypeIsOne() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(contactsClient.listDepartmentUserId("access-token", 10L)).willReturn(Arrays.asList("user-1", "user-2"));
-
-        MingleiDocumentServiceImpl service = new MingleiDocumentServiceImpl(
-                mock(YDClient.class),
-                dingTalkClient,
-                contactsClient,
-                mock(DDConf.class),
-                new YidaApplicationsProperties(),
-                new ObjectMapper());
-
-        List<String> userIds = service.listDepartmentUserIds(10L, 1);
-
-        assertEquals(Arrays.asList("user-1", "user-2"), userIds);
-        verify(contactsClient, never()).getDepartmentId_all("access-token", false, 10L);
-    }
-}

+ 0 - 74
mjava-minglei/src/test/java/com/malk/minglei/service/impl/MingleiPortServiceImplTest.java

@@ -1,74 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.malk.minglei.config.MingleiPortProperties;
-import com.malk.minglei.dto.MingleiPortRequest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.springframework.http.MediaType;
-import org.springframework.test.web.client.MockRestServiceServer;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
-import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
-
-class MingleiPortServiceImplTest {
-
-    private final ObjectMapper objectMapper = new ObjectMapper();
-    private MockRestServiceServer server;
-    private MingleiPortServiceImpl mingleiPortService;
-
-    @BeforeEach
-    void setUp() {
-        RestTemplate restTemplate = new RestTemplate();
-        server = MockRestServiceServer.createServer(restTemplate);
-
-        MingleiPortProperties properties = new MingleiPortProperties();
-        properties.setSupplierPriceApprovalUrl("http://external.example/wtoptst/ws/r/awsp920");
-        properties.setSupplierPriceApprovalKey("key-1");
-        mingleiPortService = new MingleiPortServiceImpl(restTemplate, properties);
-    }
-
-    @Test
-    void submitSupplierPriceApprovalForwardsDocumentAndReturnsExternalResponse() throws Exception {
-        server.expect(requestTo("http://external.example/wtoptst/ws/r/awsp920"))
-                .andExpect(method(org.springframework.http.HttpMethod.POST))
-                .andExpect(header("Content-Type", MediaType.APPLICATION_JSON_VALUE))
-                .andExpect(content().json(externalRequestJson()))
-                .andRespond(withSuccess("{\"srvver\":\"1.0\",\"srvcode\":\"100\",\"datakey\":null,\"payload\":{\"std_data\":{\"execution\":{\"code\":\"0\",\"sql_code\":null,\"description\":\"SUCCESS\"},\"parameter\":{\"pmdidocno\":\"CH3\",\"tran_time\":\"2026-08-25 10:00:00\"}}}}", MediaType.APPLICATION_JSON));
-
-        Map<String, Object> response = mingleiPortService.submitSupplierPriceApproval(readRequest());
-
-        assertEquals("100", response.get("srvcode"));
-        server.verify();
-    }
-
-    private MingleiPortRequest readRequest() throws Exception {
-        return objectMapper.readValue(localRequestJson(), MingleiPortRequest.class);
-    }
-
-    private String localRequestJson() {
-        return "{"
-                + "\"head\":{\"pmdidocno\":\"CH3\",\"pmdi001\":\"N\",\"pmdi034\":\"605188\",\"pmdidocdt\":\"2026-08-06\",\"pmdi004\":\"517022\",\"pmdi015\":\"2026-08-06\",\"pmdi016\":\"2027-06-01\",\"pmdi003\":\"A043\",\"pmdi002\":\"210721017\",\"pmdiud001\":\"N\",\"pmdistus\":\"I\"},"
-                + "\"detail\":[{\"pmdj011\":\"0.5000\",\"pmdj002\":\"524000059517022000000\",\"pmdj030\":\"\"}]"
-                + "}";
-    }
-
-    private String externalRequestJson() {
-        return "{"
-                + "\"key\":\"key-1\",\"type\":\"sync\","
-                + "\"host\":{\"prod\":\"APP\",\"ip\":\"10.0.2.60\",\"lang\":\"zh_CN\",\"acct\":\"tiptop\",\"timestamp\":\"20260807164700004\"},"
-                + "\"service\":{\"prod\":\"T100\",\"name\":\"ProcurementApprovalPrice\",\"ip\":\"10.0.2.60\",\"id\":\"toptst\"},"
-                + "\"datakey\":{\"EntId\":\"87\",\"CompanyId\":\"ML\"},"
-                + "\"payload\":{\"std_data\":{\"parameter\":{"
-                + "\"head\":{\"pmdidocno\":\"CH3\",\"pmdi001\":\"N\",\"pmdi034\":\"605188\",\"pmdidocdt\":\"2026-08-06\",\"pmdi004\":\"517022\",\"pmdi015\":\"2026-08-06\",\"pmdi016\":\"2027-06-01\",\"pmdi003\":\"A043\",\"pmdi002\":\"210721017\",\"pmdiud001\":\"N\",\"pmdistus\":\"I\"},"
-                + "\"detail\":[{\"pmdj011\":\"0.5000\",\"pmdj002\":\"524000059517022000000\",\"pmdj030\":\"\"}]"
-                + "}}}}";
-    }
-}

+ 0 - 46
mjava-minglei/src/test/java/com/malk/minglei/service/impl/YidaProcessServiceImplTest.java

@@ -1,46 +0,0 @@
-package com.malk.minglei.service.impl;
-
-import com.malk.minglei.dto.YidaProcessStartRequest;
-import com.malk.minglei.service.YidaProcessHandler;
-import org.junit.jupiter.api.Test;
-
-import java.util.Collections;
-import java.util.Locale;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.ArgumentMatchers.same;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-
-class YidaProcessServiceImplTest {
-
-    @Test
-    void startProcessDispatchesRequestToMatchingHandler() {
-        YidaProcessHandler bomHandler = mock(YidaProcessHandler.class);
-        given(bomHandler.getType()).willReturn(BomYidaProcessHandler.TYPE);
-
-        YidaProcessStartRequest request = new YidaProcessStartRequest();
-        request.setType(" " + BomYidaProcessHandler.TYPE.toLowerCase(Locale.ROOT) + " ");
-        given(bomHandler.startProcess(same(request))).willReturn("process-instance-1");
-
-        YidaProcessServiceImpl service = new YidaProcessServiceImpl(Collections.singletonList(bomHandler));
-
-        assertEquals("process-instance-1", service.startProcess(request));
-        verify(bomHandler).startProcess(request);
-    }
-
-    @Test
-    void startProcessRejectsUnsupportedType() {
-        YidaProcessHandler bomHandler = mock(YidaProcessHandler.class);
-        given(bomHandler.getType()).willReturn(BomYidaProcessHandler.TYPE);
-
-        YidaProcessStartRequest request = new YidaProcessStartRequest();
-        request.setType("PURCHASE");
-        YidaProcessServiceImpl service = new YidaProcessServiceImpl(Collections.singletonList(bomHandler));
-
-        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> service.startProcess(request));
-        assertEquals("Unsupported Yida process type: PURCHASE", exception.getMessage());
-    }
-}