Преглед изворни кода

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/com/malk/minglei/service/yida/MingleiDocumentService.java
ht пре 1 дан
родитељ
комит
f0f17cda34
27 измењених фајлова са 991 додато и 94 уклоњено
  1. 1 1
      DATA-DICTIONARY-API.md
  2. 19 1
      src/main/java/com/malk/minglei/controller/EmployeeRosterController.java
  3. 36 0
      src/main/java/com/malk/minglei/dto/EmployeeDingTalkIdentity.java
  4. 23 0
      src/main/java/com/malk/minglei/dto/EmployeeIdentityQueryRequest.java
  5. 33 0
      src/main/java/com/malk/minglei/service/employee/EmployeeDingTalkIdentityService.java
  6. 1 1
      src/main/java/com/malk/minglei/service/impl/dictionary/DictionaryQueryServiceImpl.java
  7. 84 0
      src/main/java/com/malk/minglei/service/impl/employee/EmployeeDingTalkIdentityServiceImpl.java
  8. 36 4
      src/main/java/com/malk/minglei/service/impl/yida/ApproveOrderFormPushHandler.java
  9. 14 2
      src/main/java/com/malk/minglei/service/impl/yida/BomYidaProcessHandler.java
  10. 8 2
      src/main/java/com/malk/minglei/service/impl/yida/Cpmp402YidaProcessHandler.java
  11. 67 19
      src/main/java/com/malk/minglei/service/impl/yida/CustomerReleaseFormPushHandler.java
  12. 16 0
      src/main/java/com/malk/minglei/service/impl/yida/LocalFormDepartmentEnricher.java
  13. 13 0
      src/main/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImpl.java
  14. 192 18
      src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandler.java
  15. 15 0
      src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptKeeperLookup.java
  16. 45 0
      src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptKeeperOracleLookup.java
  17. 6 1
      src/main/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandler.java
  18. 8 0
      src/main/java/com/malk/minglei/service/yida/MingleiDocumentService.java
  19. 28 1
      src/test/java/com/malk/minglei/controller/EmployeeRosterControllerTest.java
  20. 1 1
      src/test/java/com/malk/minglei/service/impl/dictionary/DictionaryQueryServiceImplTest.java
  21. 64 0
      src/test/java/com/malk/minglei/service/impl/employee/EmployeeDingTalkIdentityServiceImplTest.java
  22. 29 13
      src/test/java/com/malk/minglei/service/impl/yida/ApproveOrderFormPushHandlerTest.java
  23. 11 2
      src/test/java/com/malk/minglei/service/impl/yida/Cpmp402YidaProcessHandlerTest.java
  24. 20 6
      src/test/java/com/malk/minglei/service/impl/yida/CustomerReleaseFormPushHandlerTest.java
  25. 158 16
      src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandlerTest.java
  26. 46 0
      src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptKeeperOracleLookupTest.java
  27. 17 6
      src/test/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandlerTest.java

+ 1 - 1
DATA-DICTIONARY-API.md

@@ -255,7 +255,7 @@ GET /api/dictionary/erp-outsourced-bom?formtable_main_158_dt1_wwcgdh=WWCG-001&fo
 - `formtable_main_158_dt1_wwcgdh`:采购单号,必填。
 - `formtable_main_158_dt1_cgxc`:采购项次,必填。
 - 固定查询公司 `86`、据点 `ML`。
-- 返回字段:`sfba006`、`imaal003`、`imaal004`、`sfba013`。
+- 返回字段:`sfba006`、`imaal003`、`imaal004`、`sfba013-sfba016`。
 
 ## 5. 兼容路径
 

+ 19 - 1
src/main/java/com/malk/minglei/controller/EmployeeRosterController.java

@@ -1,9 +1,12 @@
 package com.malk.minglei.controller;
 
 import com.malk.minglei.dto.ApiResponse;
+import com.malk.minglei.dto.EmployeeDingTalkIdentity;
+import com.malk.minglei.dto.EmployeeIdentityQueryRequest;
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterQueryRequest;
 import com.malk.minglei.dto.EmployeeRosterSyncResult;
+import com.malk.minglei.service.employee.EmployeeDingTalkIdentityService;
 import com.malk.minglei.service.employee.EmployeeRosterService;
 import org.springframework.http.ResponseEntity;
 import org.springframework.validation.annotation.Validated;
@@ -26,14 +29,18 @@ import javax.validation.constraints.Min;
 public class EmployeeRosterController {
 
     private final EmployeeRosterService employeeRosterService;
+    private final EmployeeDingTalkIdentityService employeeDingTalkIdentityService;
 
     /**
      * 创建员工花名册控制器。
      *
      * @param employeeRosterService 员工花名册服务
+     * @param employeeDingTalkIdentityService 工号到钉钉用户和部门的解析服务
      */
-    public EmployeeRosterController(EmployeeRosterService employeeRosterService) {
+    public EmployeeRosterController(EmployeeRosterService employeeRosterService,
+                                    EmployeeDingTalkIdentityService employeeDingTalkIdentityService) {
         this.employeeRosterService = employeeRosterService;
+        this.employeeDingTalkIdentityService = employeeDingTalkIdentityService;
     }
 
     /**
@@ -69,4 +76,15 @@ public class EmployeeRosterController {
     public ResponseEntity<ApiResponse<EmployeeRosterInfo>> getUserRoster(@Valid @RequestBody EmployeeRosterQueryRequest request) {
         return ResponseEntity.ok(ApiResponse.success(employeeRosterService.findByUserId(request.getUserID())));
     }
+
+    /**
+     * 按工号一次返回钉钉用户 ID 和通讯录主部门 ID。
+     *
+     * @param request 员工工号
+     * @return 钉钉用户和部门
+     */
+    @PostMapping("/resolve")
+    public ResponseEntity<ApiResponse<EmployeeDingTalkIdentity>> resolve(@Valid @RequestBody EmployeeIdentityQueryRequest request) {
+        return ResponseEntity.ok(ApiResponse.success(employeeDingTalkIdentityService.resolve(request.getEmployeeNo())));
+    }
 }

+ 36 - 0
src/main/java/com/malk/minglei/dto/EmployeeDingTalkIdentity.java

@@ -0,0 +1,36 @@
+package com.malk.minglei.dto;
+
+/**
+ * 工号对应的钉钉用户和主部门。
+ */
+public class EmployeeDingTalkIdentity {
+
+    private final String employeeNo;
+    private final String dingtalkUserId;
+    private final String departmentId;
+
+    /**
+     * 创建工号解析结果。
+     *
+     * @param employeeNo 员工工号
+     * @param dingtalkUserId 钉钉用户 ID
+     * @param departmentId 钉钉部门 ID
+     */
+    public EmployeeDingTalkIdentity(String employeeNo, String dingtalkUserId, String departmentId) {
+        this.employeeNo = employeeNo;
+        this.dingtalkUserId = dingtalkUserId;
+        this.departmentId = departmentId;
+    }
+
+    public String getEmployeeNo() {
+        return employeeNo;
+    }
+
+    public String getDingtalkUserId() {
+        return dingtalkUserId;
+    }
+
+    public String getDepartmentId() {
+        return departmentId;
+    }
+}

+ 23 - 0
src/main/java/com/malk/minglei/dto/EmployeeIdentityQueryRequest.java

@@ -0,0 +1,23 @@
+package com.malk.minglei.dto;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+
+import javax.validation.constraints.NotBlank;
+
+/**
+ * 按工号查询钉钉人员和部门的请求。
+ */
+public class EmployeeIdentityQueryRequest {
+
+    @NotBlank
+    @JsonAlias("工号")
+    private String employeeNo;
+
+    public String getEmployeeNo() {
+        return employeeNo;
+    }
+
+    public void setEmployeeNo(String employeeNo) {
+        this.employeeNo = employeeNo;
+    }
+}

+ 33 - 0
src/main/java/com/malk/minglei/service/employee/EmployeeDingTalkIdentityService.java

@@ -0,0 +1,33 @@
+package com.malk.minglei.service.employee;
+
+import com.malk.minglei.dto.EmployeeDingTalkIdentity;
+
+/**
+ * 把员工工号解析成钉钉用户和通讯录主部门。
+ */
+public interface EmployeeDingTalkIdentityService {
+
+    /**
+     * 按工号查询钉钉用户 ID。
+     *
+     * @param employeeNo 员工工号
+     * @return 钉钉用户 ID
+     */
+    String resolveDingtalkUserId(String employeeNo);
+
+    /**
+     * 按工号查询通讯录主部门 ID。
+     *
+     * @param employeeNo 员工工号
+     * @return 钉钉部门 ID
+     */
+    String resolveDepartmentId(String employeeNo);
+
+    /**
+     * 一次返回工号对应的钉钉用户 ID 和主部门 ID。
+     *
+     * @param employeeNo 员工工号
+     * @return 钉钉用户和部门
+     */
+    EmployeeDingTalkIdentity resolve(String employeeNo);
+}

+ 1 - 1
src/main/java/com/malk/minglei/service/impl/dictionary/DictionaryQueryServiceImpl.java

@@ -323,7 +323,7 @@ public class DictionaryQueryServiceImpl implements DictionaryQueryService {
     private static final String ERP_OUTSOURCED_BOM_DICTIONARY_CODE = "erp-outsourced-bom";
     private static final String OUTSOURCED_PURCHASE_ORDER_SEQUENCE_FILTER = "formtable_main_158_dt1_cgxc";
     private static final String ERP_OUTSOURCED_BOM_DATA_SQL =
-            "select sfba006, imaal003, imaal004, sfba013 " +
+            "select sfba006, imaal003, imaal004, sfba013-sfba016 " +
                     "from DSDATA.pmdp_t " +
                     "left join DSDATA.sfba_t on pmdpent = sfbaent and pmdpsite = sfbasite " +
                     "and pmdp003 = sfbadocno and sfba008 = '1' " +

+ 84 - 0
src/main/java/com/malk/minglei/service/impl/employee/EmployeeDingTalkIdentityServiceImpl.java

@@ -0,0 +1,84 @@
+package com.malk.minglei.service.impl.employee;
+
+import com.malk.minglei.dto.EmployeeDingTalkIdentity;
+import com.malk.minglei.dto.EmployeeRosterInfo;
+import com.malk.minglei.service.employee.EmployeeDingTalkIdentityService;
+import com.malk.minglei.service.employee.EmployeeRosterService;
+import com.malk.minglei.service.yida.MingleiDocumentService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 工号先查花名册得到钉钉用户,再按该用户查通讯录主部门。
+ */
+@Service
+public class EmployeeDingTalkIdentityServiceImpl implements EmployeeDingTalkIdentityService {
+
+    private final EmployeeRosterService employeeRosterService;
+    private final MingleiDocumentService mingleiDocumentService;
+
+    /**
+     * 创建工号解析服务。
+     *
+     * @param employeeRosterService 员工花名册
+     * @param mingleiDocumentService 通讯录部门查询
+     */
+    public EmployeeDingTalkIdentityServiceImpl(EmployeeRosterService employeeRosterService,
+                                               MingleiDocumentService mingleiDocumentService) {
+        this.employeeRosterService = employeeRosterService;
+        this.mingleiDocumentService = mingleiDocumentService;
+    }
+
+    @Override
+    public String resolveDingtalkUserId(String employeeNo) {
+        String requestedEmployeeNo = requireEmployeeNo(employeeNo);
+        EmployeeRosterInfo employee = employeeRosterService.findByEmployeeNo(requestedEmployeeNo);
+        if (employee == null) {
+            throw new IllegalArgumentException("employee not found: " + requestedEmployeeNo);
+        }
+        String dingtalkUserId = trimToNull(employee.getDingtalkId());
+        if (dingtalkUserId == null) {
+            throw new IllegalArgumentException("employee has no DingTalk userId: " + requestedEmployeeNo);
+        }
+        return dingtalkUserId;
+    }
+
+    @Override
+    public String resolveDepartmentId(String employeeNo) {
+        String requestedEmployeeNo = requireEmployeeNo(employeeNo);
+        String dingtalkUserId = resolveDingtalkUserId(requestedEmployeeNo);
+        try {
+            return mingleiDocumentService.resolveMainDepartmentId(dingtalkUserId);
+        } catch (IllegalArgumentException exception) {
+            throw new IllegalArgumentException("employee department not found: " + requestedEmployeeNo, exception);
+        }
+    }
+
+    @Override
+    public EmployeeDingTalkIdentity resolve(String employeeNo) {
+        String requestedEmployeeNo = requireEmployeeNo(employeeNo);
+        String dingtalkUserId = resolveDingtalkUserId(requestedEmployeeNo);
+        String departmentId;
+        try {
+            departmentId = mingleiDocumentService.resolveMainDepartmentId(dingtalkUserId);
+        } catch (IllegalArgumentException exception) {
+            throw new IllegalArgumentException("employee department not found: " + requestedEmployeeNo, exception);
+        }
+        return new EmployeeDingTalkIdentity(requestedEmployeeNo, dingtalkUserId, departmentId);
+    }
+
+    private String requireEmployeeNo(String employeeNo) {
+        String requestedEmployeeNo = trimToNull(employeeNo);
+        if (requestedEmployeeNo == null) {
+            throw new IllegalArgumentException("employeeNo must not be blank");
+        }
+        return requestedEmployeeNo;
+    }
+
+    private String trimToNull(String value) {
+        if (value == null) {
+            return null;
+        }
+        String trimmed = value.trim();
+        return trimmed.isEmpty() ? null : trimmed;
+    }
+}

+ 36 - 4
src/main/java/com/malk/minglei/service/impl/yida/ApproveOrderFormPushHandler.java

@@ -36,7 +36,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
     private static final Logger LOGGER = LoggerFactory.getLogger(ApproveOrderFormPushHandler.class);
     private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
     public static final String TYPE = "axmt500";
-    private static final String FORM_NAME = "approveOrder";
+    private static final String FORM_NAME = "订单审批流程";
     private static final String FORM_UUID = "FORM-8454FA17732746058A2D90B1C559CC19G5SR";
     private static final String PROCESS_CODE = "TPROC--9VD66QA18AJ8317KK3NXM54W1HIT30C7N28TM7";
     private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
@@ -94,6 +94,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
     private final YidaApplicationsProperties applications;
     private final EmployeeRosterService employeeRosterService;
     private final MingleiDocumentService mingleiDocumentService;
+    private final LocalFormDepartmentEnricher departmentEnricher;
 
     /**
      * 创建销售订单审批推送处理器。
@@ -103,17 +104,20 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
      * @param applications 宜搭应用配置
      * @param employeeRosterService 员工花名册,用于把业务人员工号换成钉钉用户 ID
      * @param mingleiDocumentService 部门配置查询,用于按业务人员取钉钉部门
+     * @param departmentEnricher 按发起人写入申请公司和申请部门
      */
     public ApproveOrderFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
                                        DDConf dingTalkConfiguration,
                                        YidaApplicationsProperties applications,
                                        EmployeeRosterService employeeRosterService,
-                                       MingleiDocumentService mingleiDocumentService) {
+                                       MingleiDocumentService mingleiDocumentService,
+                                       LocalFormDepartmentEnricher departmentEnricher) {
         this.yidaProcessOpenApiClient = yidaProcessOpenApiClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.applications = applications;
         this.employeeRosterService = employeeRosterService;
         this.mingleiDocumentService = mingleiDocumentService;
+        this.departmentEnricher = departmentEnricher;
     }
 
     /**
@@ -140,6 +144,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         String appType = requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
         String systemToken = requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
         Map<String, Object> formData = toYidaFormData(request, operatorUserId);
+        departmentEnricher.fill(FORM_UUID, operatorUserId, formData);
         String title = requireText(toText(formData.get(TITLE_FIELD_ID)), "Yida process title is not configured");
         Map<String, Object> body = new LinkedHashMap<String, Object>();
         body.put("appType", appType);
@@ -187,7 +192,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         List<Map<String, Object>> deliverySourceRows = toMapRows(deliverySource);
         List<Map<String, Object>> orderSourceRows = toMapRows(orderSource);
         String orderNo = toText(value(main, "xmdadocno"));
-        String title = toText(firstNonBlank(value(main, "title", "dataTitle"), orderNo == null ? null : "approveOrder-" + orderNo));
+        String title = orderNo == null ? null : FORM_NAME + "-" + orderNo;
 
         Map<String, Object> formData = new LinkedHashMap<String, Object>();
         putIfNotBlank(formData, TITLE_FIELD_ID, title);
@@ -204,7 +209,7 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         putIfNotBlank(formData, VERSION_FIELD_ID, toDecimal(value(main, "xmda001")));
         putIfNotBlank(formData, CUSTOMER_CODE_FIELD_ID, value(main, "xmda004"));
         putIfNotBlank(formData, CUSTOMER_SHORT_NAME_FIELD_ID, value(main, "pmaal004"));
-        putIfNotBlank(formData, ORDER_TYPE_FIELD_ID, value(main, "xmda005"));
+        putIfNotBlank(formData, ORDER_TYPE_FIELD_ID, orderNature(value(main, "xmda005")));
         putIfNotBlank(formData, PRICE_TAX_INCLUDED_FIELD_ID, value(main, "xmda013"));
         putIfNotBlank(formData, ORDER_AMOUNT_FIELD_ID, value(main, "xmda042"));
         putIfNotBlank(formData, REMARK_FIELD_ID, value(main, "xmda071"));
@@ -521,6 +526,33 @@ public class ApproveOrderFormPushHandler implements YidaFormPushHandler {
         return value;
     }
 
+    /**
+     * 订单性质 xmda005:1 一般订单,2 换货订单,3 签收订单,4 调拨订单,5 预先订单。
+     */
+    static String orderNature(Object value) {
+        String text = value == null ? null : String.valueOf(value).trim();
+        if (text == null || text.isEmpty()) {
+            return null;
+        }
+        String code = text.endsWith(".0") ? text.substring(0, text.length() - 2) : text;
+        if ("1".equals(code)) {
+            return "一般订单";
+        }
+        if ("2".equals(code)) {
+            return "换货订单";
+        }
+        if ("3".equals(code)) {
+            return "签收订单";
+        }
+        if ("4".equals(code)) {
+            return "调拨订单";
+        }
+        if ("5".equals(code)) {
+            return "预先订单";
+        }
+        return text;
+    }
+
     private String toText(Object value) {
         return isBlank(value) ? null : String.valueOf(value);
     }

+ 14 - 2
src/main/java/com/malk/minglei/service/impl/yida/BomYidaProcessHandler.java

@@ -11,6 +11,9 @@ import com.malk.service.aliwork.YDClient;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
+import java.util.LinkedHashMap;
+import java.util.Map;
+
 /**
  * 处理 BOM 表单的宜搭流程发起请求。
  */
@@ -26,6 +29,7 @@ public class BomYidaProcessHandler implements YidaProcessHandler {
     private final YDClient yidaClient;
     private final DDConf dingTalkConfiguration;
     private final YidaApplicationsProperties.ApplicationCredentials coordinationOfficeConfiguration;
+    private final LocalFormDepartmentEnricher departmentEnricher;
 
     /**
      * 创建 BOM 流程处理器。
@@ -33,13 +37,16 @@ public class BomYidaProcessHandler implements YidaProcessHandler {
      * @param yidaClient 协同办公宜搭客户端
      * @param dingTalkConfiguration 钉钉应用配置
      * @param applications 宜搭应用配置
+     * @param departmentEnricher 按发起人写入申请公司和申请部门
      */
     public BomYidaProcessHandler(@Qualifier("coordinationOfficeYidaClient") YDClient yidaClient,
                                  DDConf dingTalkConfiguration,
-                                 YidaApplicationsProperties applications) {
+                                 YidaApplicationsProperties applications,
+                                 LocalFormDepartmentEnricher departmentEnricher) {
         this.yidaClient = yidaClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.coordinationOfficeConfiguration = applications.getCoordinationOffice();
+        this.departmentEnricher = departmentEnricher;
     }
 
     /**
@@ -68,7 +75,12 @@ public class BomYidaProcessHandler implements YidaProcessHandler {
         parameter.setUserId(request.getUserId());
         parameter.setFormUuid(FORM_UUID);
         parameter.setProcessCode(PROCESS_CODE);
-        parameter.setFormDataJson(JSON.toJSONString(request.getFields()));
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
+        if (request.getFields() != null) {
+            formData.putAll(request.getFields());
+        }
+        departmentEnricher.fill(FORM_UUID, request.getUserId(), formData);
+        parameter.setFormDataJson(JSON.toJSONString(formData));
         parameter.setDataTitle(request.getDataTitle());
 
         Object result = yidaClient.operateData(parameter, YDConf.FORM_OPERATION.start);

+ 8 - 2
src/main/java/com/malk/minglei/service/impl/yida/Cpmp402YidaProcessHandler.java

@@ -51,6 +51,7 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
     private final YDClient yidaClient;
     private final DDConf dingTalkConfiguration;
     private final YidaApplicationsProperties applications;
+    private final LocalFormDepartmentEnricher departmentEnricher;
 
     /**
      * 创建包材核价单流程处理器。
@@ -58,13 +59,16 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
      * @param yidaClient 协同办公宜搭客户端
      * @param dingTalkConfiguration 钉钉应用配置
      * @param applications 宜搭应用配置
+     * @param departmentEnricher 按发起人写入申请公司和申请部门
      */
     public Cpmp402YidaProcessHandler(@Qualifier("coordinationOfficeYidaClient") YDClient yidaClient,
                                      DDConf dingTalkConfiguration,
-                                     YidaApplicationsProperties applications) {
+                                     YidaApplicationsProperties applications,
+                                     LocalFormDepartmentEnricher departmentEnricher) {
         this.yidaClient = yidaClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.applications = applications;
+        this.departmentEnricher = departmentEnricher;
     }
 
     /**
@@ -94,7 +98,9 @@ public class Cpmp402YidaProcessHandler implements YidaProcessHandler {
         parameter.setFormUuid(FORM_UUID);
         parameter.setProcessCode(PROCESS_CODE);
         String title = requireText(request.getDataTitle(), "Yida process dataTitle is not configured");
-        parameter.setFormDataJson(JSON.toJSONString(toYidaFormData(request, title)));
+        Map<String, Object> formData = toYidaFormData(request, title);
+        departmentEnricher.fill(FORM_UUID, request.getUserId(), formData);
+        parameter.setFormDataJson(JSON.toJSONString(formData));
         parameter.setExecuteExpression(true);
         parameter.setNoExecuteExpression(false);
         parameter.setTitle(title);

+ 67 - 19
src/main/java/com/malk/minglei/service/impl/yida/CustomerReleaseFormPushHandler.java

@@ -16,7 +16,10 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -32,20 +35,17 @@ public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
     private static final Logger LOGGER = LoggerFactory.getLogger(CustomerReleaseFormPushHandler.class);
     private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
     public static final String TYPE = "axmt140";
-    private static final String FORM_NAME = "customerRelease";
+    private static final String FORM_NAME = "客户放行";
     private static final String FORM_UUID = "FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C";
     private static final String PROCESS_CODE = "TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU";
     private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
     // 与客户放行申请表单中“财务中心”字段的默认值保持一致。
     private static final String DEFAULT_FINANCE_DEPARTMENT_ID = "962219827";
 
     private static final String TITLE_FIELD_ID = "textField_mtv1tgg1";
     private static final String APPLICANT_FIELD_ID = "employeeField_msh186rr";
-    private static final String COMPANY_FIELD_ID = "selectField_mtv7uxgd";
     private static final String DEPARTMENT_FIELD_ID = "departmentSelectField_msh186ry";
-    private static final String FIRST_LEVEL_MANAGER_FIELD_ID = "employeeField_mt6thsdh";
-    private static final String SECOND_LEVEL_MANAGER_FIELD_ID = "employeeField_mt6thsdi";
-    private static final String THIRD_LEVEL_MANAGER_FIELD_ID = "employeeField_mt6v6alx";
     private static final String FINANCE_DEPARTMENT_FIELD_ID = "textField_mte1kc0i";
     private static final String FINANCE_MANAGER_FIELD_ID = "employeeField_mte1kc0g";
     private static final String APPLY_DATE_FIELD_ID = "dateField_msh186rs";
@@ -69,6 +69,7 @@ public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
     private final DDConf dingTalkConfiguration;
     private final YidaApplicationsProperties applications;
     private final MingleiDocumentService mingleiDocumentService;
+    private final LocalFormDepartmentEnricher departmentEnricher;
 
     /**
      * 创建客户放行审批推送处理器。
@@ -77,15 +78,18 @@ public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
      * @param dingTalkConfiguration 钉钉应用配置
      * @param applications 宜搭应用配置
      * @param mingleiDocumentService 部门申请公司和负责人配置查询服务
+     * @param departmentEnricher 按发起人写入申请公司、申请部门和审批负责人
      */
     public CustomerReleaseFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
                                           DDConf dingTalkConfiguration,
                                           YidaApplicationsProperties applications,
-                                          MingleiDocumentService mingleiDocumentService) {
+                                          MingleiDocumentService mingleiDocumentService,
+                                          LocalFormDepartmentEnricher departmentEnricher) {
         this.yidaProcessOpenApiClient = yidaProcessOpenApiClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.applications = applications;
         this.mingleiDocumentService = mingleiDocumentService;
+        this.departmentEnricher = departmentEnricher;
     }
 
     /**
@@ -148,13 +152,14 @@ public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
         Map<String, Object> detailWrapper = objectToMap(request.getDetailData());
         List<Map<String, Object>> detailRows = toMapRows(firstNonBlank(detailWrapper.get("detailData1"), detailWrapper.get("detailData")));
         String docNo = toText(value(main, "xmfxdocno"));
-        String title = toText(firstNonBlank(value(main, "title", "dataTitle"), docNo == null ? null : "customerRelease-" + docNo));
+        String title = docNo == null ? null : FORM_NAME + "-" + docNo;
 
         Map<String, Object> formData = new LinkedHashMap<String, Object>();
         putIfNotBlank(formData, TITLE_FIELD_ID, title);
         putIfNotBlank(formData, APPLICANT_FIELD_ID, toStringList(operatorUserId, null));
         putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(value(main, "bm", "applyDepartment", "department", "deptId"), null));
-        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(value(main, "rq", "applyDate"), value(main, "xmfxdocdt"))));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateTimeMillis(mostPreciseDate(
+                value(main, "rq"), value(main, "applyDate"), value(main, "xmfxdocdt"))));
         putIfNotBlank(formData, FORM_TITLE_FIELD_ID, title);
         putIfNotBlank(formData, RELEASE_NO_FIELD_ID, value(main, "xmfxdocno"));
         putIfNotBlank(formData, RELEASE_DESC_FIELD_ID, value(main, "xmfx003"));
@@ -165,17 +170,7 @@ public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
             formData.put(DETAIL_TABLE_FIELD_ID, rows);
         }
         copyYidaFieldValues(main, formData);
-        Map<String, Object> departmentConfiguration = mingleiDocumentService.queryApplicantDepartmentConfiguration(operatorUserId);
-        // 公司和负责人以申请人的部门配置为准,避免 ERP 透传字段覆盖查询结果。
-        formData.remove(COMPANY_FIELD_ID);
-        formData.remove(FIRST_LEVEL_MANAGER_FIELD_ID);
-        formData.remove(SECOND_LEVEL_MANAGER_FIELD_ID);
-        formData.remove(THIRD_LEVEL_MANAGER_FIELD_ID);
-        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(departmentConfiguration.get("departmentId"), null));
-        putIfNotBlank(formData, COMPANY_FIELD_ID, departmentConfiguration.get("company"));
-        putIfNotBlank(formData, FIRST_LEVEL_MANAGER_FIELD_ID, toStringList(departmentConfiguration.get("firstLevelManagerUserIds"), null));
-        putIfNotBlank(formData, SECOND_LEVEL_MANAGER_FIELD_ID, toStringList(departmentConfiguration.get("secondLevelManagerUserIds"), null));
-        putIfNotBlank(formData, THIRD_LEVEL_MANAGER_FIELD_ID, toStringList(departmentConfiguration.get("thirdLevelManagerUserIds"), null));
+        departmentEnricher.fill(FORM_UUID, operatorUserId, formData);
         String financeDepartmentId = defaultText(toText(formData.get(FINANCE_DEPARTMENT_FIELD_ID)), DEFAULT_FINANCE_DEPARTMENT_ID);
         formData.put(FINANCE_DEPARTMENT_FIELD_ID, financeDepartmentId);
         Map<String, Object> financeManagers = mingleiDocumentService.queryDepartmentManagersByDepartmentId(financeDepartmentId);
@@ -271,6 +266,59 @@ public class CustomerReleaseFormPushHandler implements YidaFormPushHandler {
         return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
     }
 
+    private Object mostPreciseDate(Object first, Object second, Object third) {
+        Object selected = null;
+        for (Object candidate : new Object[]{first, second, third}) {
+            if (isBlank(candidate)) {
+                continue;
+            }
+            if (selected == null) {
+                selected = candidate;
+            }
+            String text = String.valueOf(candidate);
+            if (text.indexOf(':') >= 0 || text.indexOf('T') >= 0) {
+                return candidate;
+            }
+        }
+        return selected;
+    }
+
+    /**
+     * 带时分的文本保留到秒。只有日期时,补上当前的时分秒。
+     */
+    private Long toDateTimeMillis(Object value) {
+        if (isBlank(value)) {
+            return null;
+        }
+        String text = String.valueOf(value).trim().replace('/', '-');
+        if (text.matches("\\d+")) {
+            return Long.valueOf(text);
+        }
+        if (text.indexOf(':') >= 0 || text.indexOf('T') >= 0) {
+            String normalized = text.replace('T', ' ');
+            int zoneIndex = normalized.indexOf('+');
+            if (zoneIndex < 0 && normalized.lastIndexOf('-') > 10) {
+                zoneIndex = normalized.lastIndexOf('-');
+            }
+            if (zoneIndex > 10) {
+                normalized = normalized.substring(0, zoneIndex);
+            }
+            if (normalized.endsWith("Z")) {
+                normalized = normalized.substring(0, normalized.length() - 1);
+            }
+            if (normalized.length() > 19) {
+                normalized = normalized.substring(0, 19);
+            }
+            if (normalized.length() == 16) {
+                normalized = normalized + ":00";
+            }
+            return LocalDateTime.parse(normalized, DATE_TIME_FORMATTER)
+                    .atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+        }
+        LocalTime now = LocalTime.now(ZoneId.systemDefault());
+        return LocalDate.parse(text).atTime(now).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
     private Object value(Map<String, Object> source, String firstKey, String... otherKeys) {
         Object value = source.get(firstKey);
         if (!isBlank(value)) {

+ 16 - 0
src/main/java/com/malk/minglei/service/impl/yida/LocalFormDepartmentEnricher.java

@@ -64,6 +64,22 @@ public class LocalFormDepartmentEnricher {
                 fixedDepartmentId = "962219816";
                 fixedManagerField = "employeeField_mttrhcqs";
                 break;
+            case "FORM-8454FA17732746058A2D90B1C559CC19G5SR":
+                companyFields = new String[]{"selectField_mtv87n9f"};
+                break;
+            case "FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C":
+                companyFields = new String[]{"selectField_mtv7uxgd"};
+                applicantManagerFields = new String[]{"employeeField_mt6thsdh", "employeeField_mt6thsdi", "employeeField_mt6v6alx"};
+                break;
+            case "FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K":
+                companyFields = new String[]{"textField_mu1yk6l5", "selectField_mso1em48"};
+                break;
+            case "FORM-A3D4889187E54697A59590BE54F350E7AOO5":
+                companyFields = new String[]{"textField_mu1ys6rc", "selectField_mso1em48"};
+                break;
+            case "FORM-04001902BF9C40E6859102569FE139179LWH":
+                companyFields = new String[]{"selectField_mtv912k3"};
+                break;
             default:
                 throw new IllegalArgumentException("Unsupported department enrichment form: " + formUuid);
         }

+ 13 - 0
src/main/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImpl.java

@@ -216,6 +216,19 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         return new ApprovalMatrixSyncResult(records.size(), syncedCount);
     }
 
+    /**
+     * 按钉钉用户读取通讯录主部门。未设置主部门时取首个所属部门。
+     *
+     * @param userId 钉钉用户 ID
+     * @return 钉钉部门 ID
+     */
+    @Override
+    public String resolveMainDepartmentId(String userId) {
+        String requestedUserId = trimToNull(userId);
+        require(requestedUserId, "userId must not be blank");
+        return resolveDepartmentId(requestedUserId);
+    }
+
     /**
      * 按查询范围读取部门成员 ID,并按成员首次出现顺序去重。
      *

+ 192 - 18
src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandler.java

@@ -6,6 +6,7 @@ import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.ErpYidaPushMainData;
 import com.malk.minglei.dto.ErpYidaPushRequest;
 import com.malk.minglei.service.employee.EmployeeRosterService;
+import com.malk.minglei.service.yida.MingleiDocumentService;
 import com.malk.minglei.service.yida.YidaFormPushHandler;
 import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
 import com.malk.server.common.McException;
@@ -13,14 +14,19 @@ import com.malk.server.dingtalk.DDR_New;
 import com.malk.server.dingtalk.DDConf;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
 import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -38,6 +44,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
     private static final String FORM_UUID = "FORM-4B08ADABCDAA45788C043729A855E03ED76G";
     private static final String PROCESS_CODE = "TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0";
     private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
 
     private static final String TITLE_FIELD_ID = "textField_msh27unu";
     private static final String APPLICANT_FIELD_ID = "employeeField_msh186rr";
@@ -48,6 +55,8 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
     private static final String DATA_SOURCE_FIELD_ID = "textField_mt7zi4fy";
     private static final String APPLICANT_NO_FIELD_ID = "textField_mt7zi4g0";
     private static final String DEPARTMENT_NO_FIELD_ID = "textField_mt80nq6s";
+    private static final String APPLICANT_NAME_FIELD_ID = "employeeField_mw0jwshi";
+    private static final String APPLICANT_DEPARTMENT_FIELD_ID = "departmentSelectField_mw0ko5yp";
     private static final String REASON_CODE_FIELD_ID = "textField_mt80nq6t";
     private static final String REASON_DESC_FIELD_ID = "textField_mt80nq6v";
     private static final String REMARK_FIELD_ID = "textField_mt7zi4fz";
@@ -73,13 +82,16 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
     private static final String DETAIL_PROJECT_NO_FIELD_ID = "textField_mt6ur4w8";
     private static final String DETAIL_PROJECT_DESC_FIELD_ID = "textField_mt80nq7a";
     private static final String DETAIL_SOURCE_DOC_NO_FIELD_ID = "textField_mt80nq7b";
-    private static final String DETAIL_KEEPER_FIELD_ID = "employeeField_mt80nq7d";
+    private static final String DETAIL_KEEPER_NO_FIELD_ID = "textField_mw1ekhte";
+    private static final String DETAIL_KEEPER_OA_FIELD_ID = "employeeField_mt80nq7e";
 
     private final YidaProcessOpenApiClient yidaProcessOpenApiClient;
     private final DDConf dingTalkConfiguration;
     private final YidaApplicationsProperties applications;
     private final LocalFormDepartmentEnricher departmentEnricher;
     private final EmployeeRosterService employeeRosterService;
+    private final MingleiDocumentService mingleiDocumentService;
+    private final MiscReceiptKeeperLookup keeperLookup;
 
     /**
      * 创建杂收发料单推送处理器。
@@ -88,19 +100,77 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
      * @param dingTalkConfiguration 钉钉应用配置
      * @param applications 宜搭应用配置
      * @param departmentEnricher 部门公司和负责人填充组件
-     * @param employeeRosterService 员工花名册,用于把明细仓管员工号换成钉钉用户 ID
+     * @param employeeRosterService 员工花名册,用于把工号换成钉钉用户 ID
+     * @param mingleiDocumentService 通讯录部门查询,用于按申请人钉钉用户取所在部门
+     * @param keeperLookupProvider 按料号查询仓管员工号,字典库不可用时跳过仓管员
      */
     @Autowired
     public MiscReceiptIssueFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
                                            DDConf dingTalkConfiguration,
                                            YidaApplicationsProperties applications,
                                            LocalFormDepartmentEnricher departmentEnricher,
-                                           EmployeeRosterService employeeRosterService) {
+                                           EmployeeRosterService employeeRosterService,
+                                           MingleiDocumentService mingleiDocumentService,
+                                           ObjectProvider<MiscReceiptKeeperLookup> keeperLookupProvider) {
+        this(yidaProcessOpenApiClient, dingTalkConfiguration, applications, departmentEnricher,
+                employeeRosterService, mingleiDocumentService, keeperLookupProvider.getIfAvailable());
+    }
+
+    /**
+     * 创建可指定仓管员查询的杂收发料单推送处理器。
+     */
+    public MiscReceiptIssueFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
+                                           DDConf dingTalkConfiguration,
+                                           YidaApplicationsProperties applications,
+                                           LocalFormDepartmentEnricher departmentEnricher,
+                                           EmployeeRosterService employeeRosterService,
+                                           MingleiDocumentService mingleiDocumentService,
+                                           MiscReceiptKeeperLookup keeperLookup) {
         this.yidaProcessOpenApiClient = yidaProcessOpenApiClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.applications = applications;
         this.departmentEnricher = departmentEnricher;
         this.employeeRosterService = employeeRosterService;
+        this.mingleiDocumentService = mingleiDocumentService;
+        this.keeperLookup = keeperLookup;
+    }
+
+    /**
+     * 创建带花名册和通讯录、不查仓管员的杂收发料单推送处理器。
+     */
+    public MiscReceiptIssueFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
+                                           DDConf dingTalkConfiguration,
+                                           YidaApplicationsProperties applications,
+                                           LocalFormDepartmentEnricher departmentEnricher,
+                                           EmployeeRosterService employeeRosterService,
+                                           MingleiDocumentService mingleiDocumentService) {
+        this(yidaProcessOpenApiClient, dingTalkConfiguration, applications, departmentEnricher,
+                employeeRosterService, mingleiDocumentService, (MiscReceiptKeeperLookup) null);
+    }
+
+    /**
+     * 创建带花名册、不查通讯录部门的杂收发料单推送处理器。
+     */
+    public MiscReceiptIssueFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
+                                           DDConf dingTalkConfiguration,
+                                           YidaApplicationsProperties applications,
+                                           LocalFormDepartmentEnricher departmentEnricher,
+                                           EmployeeRosterService employeeRosterService) {
+        this(yidaProcessOpenApiClient, dingTalkConfiguration, applications, departmentEnricher,
+                employeeRosterService, (MingleiDocumentService) null, (MiscReceiptKeeperLookup) null);
+    }
+
+    /**
+     * 创建带花名册和仓管员查询、不查通讯录部门的杂收发料单推送处理器。
+     */
+    public MiscReceiptIssueFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
+                                           DDConf dingTalkConfiguration,
+                                           YidaApplicationsProperties applications,
+                                           LocalFormDepartmentEnricher departmentEnricher,
+                                           EmployeeRosterService employeeRosterService,
+                                           MiscReceiptKeeperLookup keeperLookup) {
+        this(yidaProcessOpenApiClient, dingTalkConfiguration, applications, departmentEnricher,
+                employeeRosterService, (MingleiDocumentService) null, keeperLookup);
     }
 
     /**
@@ -115,7 +185,8 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
                                            DDConf dingTalkConfiguration,
                                            YidaApplicationsProperties applications,
                                            LocalFormDepartmentEnricher departmentEnricher) {
-        this(yidaProcessOpenApiClient, dingTalkConfiguration, applications, departmentEnricher, null);
+        this(yidaProcessOpenApiClient, dingTalkConfiguration, applications, departmentEnricher,
+                (EmployeeRosterService) null, (MingleiDocumentService) null, (MiscReceiptKeeperLookup) null);
     }
 
     /**
@@ -152,8 +223,8 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
         body.put("processCode", PROCESS_CODE);
         body.put("formUuid", FORM_UUID);
         body.put("formDataJson", JSON.toJSONString(formData));
-        body.put("executeExpression", true);
-        body.put("noExecuteExpression", false);
+        body.put("executeExpression", Boolean.FALSE);
+        body.put("noExecuteExpression", Boolean.TRUE);
         body.put("title", title);
         body.put("dataTitle", title);
         LOGGER.info("Yida process start request endpoint={} formName={} body={}",
@@ -180,15 +251,16 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
         Map<String, Object> detailWrapper = objectToMap(request.getDetailData());
         List<Map<String, Object>> detailRows = toMapRows(detailWrapper.get("detailData1"));
         String docNo = toText(value(main, "inbadocno"));
-        String title = toText(firstNonBlank(value(main, "title", "dataTitle"), docNo == null ? null : "miscReceiptIssue-" + docNo));
         String reasonDesc = toText(value(main, "oocql004"));
+        String title = joinText(reasonDesc, docNo);
 
         Map<String, Object> formData = new LinkedHashMap<String, Object>();
-        putIfNotBlank(formData, TITLE_FIELD_ID, joinText(reasonDesc, docNo));
+        putIfNotBlank(formData, TITLE_FIELD_ID, title);
         putIfNotBlank(formData, APPLICANT_FIELD_ID, toStringList(operatorUserId, null));
         putIfNotBlank(formData, COMPANY_FIELD_ID, firstNonBlank(value(main, "gs", "applyCompany", "company"), value(main, "inbaent")));
         putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(value(main, "bm", "applyDepartment", "department", "deptId"), null));
-        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(value(main, "rq", "applyDate"), value(main, "inbadocdt"))));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateTimeMillis(mostPreciseDate(
+                value(main, "rq"), value(main, "applyDate"), value(main, "inbadocdt"))));
         putIfNotBlank(formData, FORM_TITLE_FIELD_ID, title);
         putIfNotBlank(formData, DATA_SOURCE_FIELD_ID, value(main, "inba006"));
         putIfNotBlank(formData, APPLICANT_NO_FIELD_ID, value(main, "inba003"));
@@ -204,9 +276,51 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
             formData.put(DETAIL_TABLE_FIELD_ID, rows);
         }
         copyYidaFieldValues(main, formData);
+        putApplicantIdentity(formData, value(main, "inba003"));
         return formData;
     }
 
+    /**
+     * 申请人员 ID 仍保留工号。申请人名称写入花名册对应的钉钉用户,申请人所在部门写入通讯录主部门。
+     */
+    private void putApplicantIdentity(Map<String, Object> formData, Object employeeNoValue) {
+        String employeeNo = toText(employeeNoValue);
+        if (employeeNo == null) {
+            return;
+        }
+        String dingtalkId = resolveApplicantUserId(employeeNo);
+        formData.put(APPLICANT_NAME_FIELD_ID, Collections.singletonList(dingtalkId));
+        formData.put(APPLICANT_DEPARTMENT_FIELD_ID,
+                Collections.singletonList(resolveApplicantDepartmentId(dingtalkId, employeeNo)));
+    }
+
+    private String resolveApplicantUserId(String employeeNo) {
+        if (employeeRosterService == null) {
+            throw new IllegalStateException("Employee roster service is not configured");
+        }
+        EmployeeRosterInfo employee = employeeRosterService.findByEmployeeNo(employeeNo);
+        if (employee == null) {
+            throw new IllegalArgumentException("misc receipt issue applicant not found: " + employeeNo);
+        }
+        String dingtalkId = toText(employee.getDingtalkId());
+        if (dingtalkId == null) {
+            throw new IllegalArgumentException("misc receipt issue applicant has no DingTalk userId: " + employeeNo);
+        }
+        return dingtalkId;
+    }
+
+    private String resolveApplicantDepartmentId(String dingtalkId, String employeeNo) {
+        if (mingleiDocumentService == null) {
+            throw new IllegalStateException("Department service is not configured");
+        }
+        Map<String, Object> configuration = mingleiDocumentService.queryApplicantDepartmentConfiguration(dingtalkId);
+        String departmentId = configuration == null ? null : toText(configuration.get("departmentId"));
+        if (departmentId == null) {
+            throw new IllegalArgumentException("misc receipt issue applicant department not found: " + employeeNo);
+        }
+        return departmentId;
+    }
+
     /**
      * 单据别取明细 inbbdocno 中 -ML 后的 3 个字符。单号本身以 ML 开头时同样取其后 3 位。
      */
@@ -238,6 +352,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
 
     private List<Map<String, Object>> toDetailRows(List<Map<String, Object>> sourceRows) {
         List<Map<String, Object>> rows = new ArrayList<Map<String, Object>>();
+        Map<String, String> keeperEmployeeNos = new HashMap<String, String>();
         for (Map<String, Object> source : sourceRows) {
             Map<String, Object> row = new LinkedHashMap<String, Object>();
             putIfNotBlank(row, DETAIL_SEQ_FIELD_ID, value(source, "inbbseq"));
@@ -259,7 +374,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
             putIfNotBlank(row, DETAIL_PROJECT_DESC_FIELD_ID, value(source, "pjbal003"));
             putIfNotBlank(row, DETAIL_SOURCE_DOC_NO_FIELD_ID, value(source, "inbb017"));
             copyYidaFieldValues(source, row);
-            putWarehouseKeeper(row, source);
+            putWarehouseKeeper(row, source, keeperEmployeeNos);
             if (!row.isEmpty()) {
                 rows.add(row);
             }
@@ -268,14 +383,31 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
     }
 
     /**
-     * 明细仓管员传入的是工号。按花名册换成钉钉用户 ID 后再写入员工组件。
+     * 仓管员工号按明细料号查询 imaf052。仓管员OA再用该工号换成钉钉用户,员工组件显示姓名。
      */
-    private void putWarehouseKeeper(Map<String, Object> row, Map<String, Object> source) {
-        String employeeNo = toText(firstItem(value(source, DETAIL_KEEPER_FIELD_ID, "仓管员", "仓管员工号")));
+    private void putWarehouseKeeper(Map<String, Object> row, Map<String, Object> source, Map<String, String> keeperEmployeeNos) {
+        String itemCode = toText(value(source, "inbb001"));
+        if (itemCode == null || keeperLookup == null) {
+            return;
+        }
+        String employeeNo = keeperEmployeeNo(itemCode, keeperEmployeeNos);
         if (employeeNo == null) {
-            row.remove(DETAIL_KEEPER_FIELD_ID);
             return;
         }
+        row.put(DETAIL_KEEPER_NO_FIELD_ID, employeeNo);
+        row.put(DETAIL_KEEPER_OA_FIELD_ID, Collections.singletonList(resolveKeeperUserId(employeeNo)));
+    }
+
+    private String keeperEmployeeNo(String itemCode, Map<String, String> keeperEmployeeNos) {
+        if (keeperEmployeeNos.containsKey(itemCode)) {
+            return keeperEmployeeNos.get(itemCode);
+        }
+        String employeeNo = toText(keeperLookup.findKeeperEmployeeNo(itemCode));
+        keeperEmployeeNos.put(itemCode, employeeNo);
+        return employeeNo;
+    }
+
+    private String resolveKeeperUserId(String employeeNo) {
         if (employeeRosterService == null) {
             throw new IllegalStateException("Employee roster service is not configured");
         }
@@ -287,7 +419,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
         if (dingtalkId == null) {
             throw new IllegalArgumentException("misc receipt issue warehouse keeper employee has no DingTalk userId: " + employeeNo);
         }
-        row.put(DETAIL_KEEPER_FIELD_ID, Collections.singletonList(dingtalkId));
+        return dingtalkId;
     }
 
     private List<Map<String, Object>> toMapRows(Object value) {
@@ -345,15 +477,57 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
         return new BigDecimal(String.valueOf(value).trim());
     }
 
-    private Long toDateMillis(Object value) {
+    private Object mostPreciseDate(Object first, Object second, Object third) {
+        Object selected = null;
+        for (Object candidate : new Object[]{first, second, third}) {
+            if (isBlank(candidate)) {
+                continue;
+            }
+            if (selected == null) {
+                selected = candidate;
+            }
+            String text = String.valueOf(candidate);
+            if (text.indexOf(':') >= 0 || text.indexOf('T') >= 0) {
+                return candidate;
+            }
+        }
+        return selected;
+    }
+
+    /**
+     * 带时分的文本保留到秒。只有日期时,补上当前的时分秒。
+     */
+    private Long toDateTimeMillis(Object value) {
         if (isBlank(value)) {
             return null;
         }
-        String text = String.valueOf(value).trim();
+        String text = String.valueOf(value).trim().replace('/', '-');
         if (text.matches("\\d+")) {
             return Long.valueOf(text);
         }
-        return LocalDate.parse(text).atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+        if (text.indexOf(':') >= 0 || text.indexOf('T') >= 0) {
+            String normalized = text.replace('T', ' ');
+            int zoneIndex = normalized.indexOf('+');
+            if (zoneIndex < 0 && normalized.lastIndexOf('-') > 10) {
+                zoneIndex = normalized.lastIndexOf('-');
+            }
+            if (zoneIndex > 10) {
+                normalized = normalized.substring(0, zoneIndex);
+            }
+            if (normalized.endsWith("Z")) {
+                normalized = normalized.substring(0, normalized.length() - 1);
+            }
+            if (normalized.length() > 19) {
+                normalized = normalized.substring(0, 19);
+            }
+            if (normalized.length() == 16) {
+                normalized = normalized + ":00";
+            }
+            return LocalDateTime.parse(normalized, DATE_TIME_FORMATTER)
+                    .atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+        }
+        LocalTime now = LocalTime.now(ZoneId.systemDefault());
+        return LocalDate.parse(text).atTime(now).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
     }
 
     private Object value(Map<String, Object> source, String firstKey, String... otherKeys) {

+ 15 - 0
src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptKeeperLookup.java

@@ -0,0 +1,15 @@
+package com.malk.minglei.service.impl.yida;
+
+/**
+ * 按杂收发明细料号查询 T100 仓管员工号。
+ */
+public interface MiscReceiptKeeperLookup {
+
+    /**
+     * 按料号读取料件默认仓管员工号。
+     *
+     * @param itemCode 明细料号 inbb001
+     * @return 仓管员工号,没有记录时返回 null
+     */
+    String findKeeperEmployeeNo(String itemCode);
+}

+ 45 - 0
src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptKeeperOracleLookup.java

@@ -0,0 +1,45 @@
+package com.malk.minglei.service.impl.yida;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 仓管员工号使用料件表查询。表名加 DSDATA,企业编号和据点按原 SQL 固定。
+ */
+@Service
+@ConditionalOnBean(name = "dictionaryNamedParameterJdbcTemplate")
+public class MiscReceiptKeeperOracleLookup implements MiscReceiptKeeperLookup {
+
+    private static final String KEEPER_EMPLOYEE_NO_SQL =
+            "select imaf052 from DSDATA.imaf_t where imafent = 86 and imafsite = 'ML' and imaf001 = :itemCode";
+
+    private final NamedParameterJdbcTemplate jdbcTemplate;
+
+    public MiscReceiptKeeperOracleLookup(
+            @Qualifier("dictionaryNamedParameterJdbcTemplate") NamedParameterJdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    @Override
+    public String findKeeperEmployeeNo(String itemCode) {
+        if (itemCode == null || itemCode.trim().isEmpty()) {
+            return null;
+        }
+        MapSqlParameterSource parameters = new MapSqlParameterSource("itemCode", itemCode.trim());
+        List<String> values = jdbcTemplate.query(KEEPER_EMPLOYEE_NO_SQL, parameters, (resultSet, rowNum) -> {
+            String value = resultSet.getString(1);
+            return value == null ? null : value.trim();
+        });
+        for (String value : values) {
+            if (value != null && !value.isEmpty()) {
+                return value;
+            }
+        }
+        return null;
+    }
+}

+ 6 - 1
src/main/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandler.java

@@ -74,6 +74,7 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
     private final YidaProcessOpenApiClient yidaProcessOpenApiClient;
     private final DDConf dingTalkConfiguration;
     private final YidaApplicationsProperties applications;
+    private final LocalFormDepartmentEnricher departmentEnricher;
 
     /**
      * 创建工单工艺工价审定推送处理器。
@@ -81,13 +82,16 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
      * @param yidaProcessOpenApiClient 宜搭流程 OpenAPI 客户端
      * @param dingTalkConfiguration 钉钉应用配置
      * @param applications 宜搭应用配置
+     * @param departmentEnricher 按发起人写入申请公司和申请部门
      */
     public MlWorkOrderCraftPriceFormPushHandler(YidaProcessOpenApiClient yidaProcessOpenApiClient,
                                                 DDConf dingTalkConfiguration,
-                                                YidaApplicationsProperties applications) {
+                                                YidaApplicationsProperties applications,
+                                                LocalFormDepartmentEnricher departmentEnricher) {
         this.yidaProcessOpenApiClient = yidaProcessOpenApiClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.applications = applications;
+        this.departmentEnricher = departmentEnricher;
     }
 
     /**
@@ -114,6 +118,7 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
         String appType = requireText(applications.getCoordinationOffice().getAppType(), "YIDA_COORDINATION_OFFICE_APP_TYPE is not configured");
         String systemToken = requireText(applications.getCoordinationOffice().getSystemToken(), "YIDA_COORDINATION_OFFICE_SYSTEM_TOKEN is not configured");
         Map<String, Object> formData = toYidaFormData(request, operatorUserId);
+        departmentEnricher.fill(FORM_UUID, operatorUserId, formData);
         String title = requireText(toText(formData.get(TITLE_FIELD_ID)), "Yida process title is not configured");
         if (isBlank(formData.get(FORM_TITLE_FIELD_ID))) {
             formData.put(FORM_TITLE_FIELD_ID, title);

+ 8 - 0
src/main/java/com/malk/minglei/service/yida/MingleiDocumentService.java

@@ -51,6 +51,14 @@ public interface MingleiDocumentService {
      */
     ApprovalMatrixSyncResult syncApprovalMatrix();
 
+    /**
+     * 按钉钉用户读取通讯录主部门。未设置主部门时取首个所属部门。
+     *
+     * @param userId 钉钉用户 ID
+     * @return 钉钉部门 ID
+     */
+    String resolveMainDepartmentId(String userId);
+
     /**
      * 按查询范围获取指定部门中的钉钉用户 ID。
      *

+ 28 - 1
src/test/java/com/malk/minglei/controller/EmployeeRosterControllerTest.java

@@ -3,8 +3,11 @@ package com.malk.minglei.controller;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.dto.ApiResponse;
+import com.malk.minglei.dto.EmployeeDingTalkIdentity;
+import com.malk.minglei.dto.EmployeeIdentityQueryRequest;
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterQueryRequest;
+import com.malk.minglei.service.employee.EmployeeDingTalkIdentityService;
 import com.malk.minglei.service.employee.EmployeeRosterService;
 import org.junit.jupiter.api.Test;
 import org.springframework.http.HttpStatus;
@@ -34,7 +37,8 @@ class EmployeeRosterControllerTest {
         info.setFirstDepartment("钉钉顾问");
         given(employeeRosterService.findByUserId("41603502201288023")).willReturn(info);
 
-        EmployeeRosterController controller = new EmployeeRosterController(employeeRosterService);
+        EmployeeRosterController controller = new EmployeeRosterController(employeeRosterService,
+                mock(EmployeeDingTalkIdentityService.class));
 
         EmployeeRosterQueryRequest request = new EmployeeRosterQueryRequest();
         request.setUserID("41603502201288023");
@@ -55,4 +59,27 @@ class EmployeeRosterControllerTest {
         assertEquals("10", result.get("jobGrade"));
         assertEquals("10010", result.get("jobGradeId"));
     }
+
+    @Test
+    void resolveReturnsDingTalkUserAndDepartment() throws Exception {
+        EmployeeDingTalkIdentityService identityService = mock(EmployeeDingTalkIdentityService.class);
+        given(identityService.resolve("241021001"))
+                .willReturn(new EmployeeDingTalkIdentity("241021001", "dingtalk-user-001", "dept-88"));
+        EmployeeRosterController controller = new EmployeeRosterController(mock(EmployeeRosterService.class), identityService);
+
+        EmployeeIdentityQueryRequest request = new EmployeeIdentityQueryRequest();
+        request.setEmployeeNo("241021001");
+        ResponseEntity<ApiResponse<EmployeeDingTalkIdentity>> response = controller.resolve(request);
+        ObjectMapper objectMapper = new ObjectMapper();
+        Map<String, Object> body = objectMapper.readValue(
+                objectMapper.writeValueAsString(response.getBody()),
+                new TypeReference<Map<String, Object>>() {
+                });
+        Map<?, ?> result = (Map<?, ?>) body.get("result");
+
+        assertEquals(HttpStatus.OK, response.getStatusCode());
+        assertEquals("241021001", result.get("employeeNo"));
+        assertEquals("dingtalk-user-001", result.get("dingtalkUserId"));
+        assertEquals("dept-88", result.get("departmentId"));
+    }
 }

+ 1 - 1
src/test/java/com/malk/minglei/service/impl/dictionary/DictionaryQueryServiceImplTest.java

@@ -785,7 +785,7 @@ class DictionaryQueryServiceImplTest {
         ArgumentCaptor<String> pageSql = ArgumentCaptor.forClass(String.class);
         ArgumentCaptor<SqlParameterSource> pageParameters = ArgumentCaptor.forClass(SqlParameterSource.class);
         verify(jdbcTemplate).queryForList(pageSql.capture(), pageParameters.capture());
-        assertEquals(true, pageSql.getValue().contains("select sfba006, imaal003, imaal004, sfba013"));
+        assertEquals(true, pageSql.getValue().contains("select sfba006, imaal003, imaal004, sfba013-sfba016"));
         assertEquals(Long.valueOf(0L), pageParameters.getValue().getValue("offset"));
         assertEquals(Long.valueOf(100L), pageParameters.getValue().getValue("endRow"));
         assertEquals(1, result.get("page"));

+ 64 - 0
src/test/java/com/malk/minglei/service/impl/employee/EmployeeDingTalkIdentityServiceImplTest.java

@@ -0,0 +1,64 @@
+package com.malk.minglei.service.impl.employee;
+
+import com.malk.minglei.dto.EmployeeDingTalkIdentity;
+import com.malk.minglei.dto.EmployeeRosterInfo;
+import com.malk.minglei.service.employee.EmployeeRosterService;
+import com.malk.minglei.service.yida.MingleiDocumentService;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class EmployeeDingTalkIdentityServiceImplTest {
+
+    @Test
+    void resolveMapsEmployeeNoToDingTalkUserAndDepartment() {
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        EmployeeRosterInfo employee = new EmployeeRosterInfo();
+        employee.setEmployeeNo("241021001");
+        employee.setDingtalkId("dingtalk-user-001");
+        when(rosterService.findByEmployeeNo("241021001")).thenReturn(employee);
+        when(documentService.resolveMainDepartmentId("dingtalk-user-001")).thenReturn("dept-88");
+
+        EmployeeDingTalkIdentity identity = new EmployeeDingTalkIdentityServiceImpl(rosterService, documentService)
+                .resolve(" 241021001 ");
+
+        assertEquals("241021001", identity.getEmployeeNo());
+        assertEquals("dingtalk-user-001", identity.getDingtalkUserId());
+        assertEquals("dept-88", identity.getDepartmentId());
+        verify(documentService).resolveMainDepartmentId("dingtalk-user-001");
+    }
+
+    @Test
+    void resolveDingtalkUserIdFailsWhenRosterMissesEmployee() {
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        when(rosterService.findByEmployeeNo("241021001")).thenReturn(null);
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new EmployeeDingTalkIdentityServiceImpl(rosterService, mock(MingleiDocumentService.class))
+                        .resolveDingtalkUserId("241021001"));
+
+        assertEquals("employee not found: 241021001", exception.getMessage());
+    }
+
+    @Test
+    void resolveDepartmentIdFailsWhenContactsHasNoDepartment() {
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        EmployeeRosterInfo employee = new EmployeeRosterInfo();
+        employee.setDingtalkId("dingtalk-user-001");
+        when(rosterService.findByEmployeeNo("241021001")).thenReturn(employee);
+        when(documentService.resolveMainDepartmentId("dingtalk-user-001"))
+                .thenThrow(new IllegalArgumentException("No department was found for user_id"));
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> new EmployeeDingTalkIdentityServiceImpl(rosterService, documentService)
+                        .resolveDepartmentId("241021001"));
+
+        assertEquals("employee department not found: 241021001", exception.getMessage());
+    }
+}

+ 29 - 13
src/test/java/com/malk/minglei/service/impl/yida/ApproveOrderFormPushHandlerTest.java

@@ -52,7 +52,7 @@ class ApproveOrderFormPushHandlerTest {
         mainData.setExtraField("xmda003", "dept-001");
         mainData.setExtraField("xmda004", "CUS-001");
         mainData.setExtraField("pmaal004", "customer-short-name");
-        mainData.setExtraField("xmda005", "normal-order");
+        mainData.setExtraField("xmda005", "1");
         mainData.setExtraField("xmda013", "Y");
         mainData.setExtraField("xmda042", "1234.56");
         mainData.setExtraField("xmda071", "remark-001");
@@ -108,11 +108,15 @@ class ApproveOrderFormPushHandlerTest {
         salesPerson.setDingtalkId("sales-dingtalk-001");
         when(roster.findByEmployeeNo("sales-user-001")).thenReturn(salesPerson);
         MingleiDocumentService documentService = mock(MingleiDocumentService.class);
-        Map<String, Object> departmentConfiguration = new LinkedHashMap<String, Object>();
-        departmentConfiguration.put("departmentId", "sales-dept-88");
-        when(documentService.queryApplicantDepartmentConfiguration("sales-dingtalk-001")).thenReturn(departmentConfiguration);
+        Map<String, Object> salesDepartment = new LinkedHashMap<String, Object>();
+        salesDepartment.put("departmentId", "sales-dept-88");
+        when(documentService.queryApplicantDepartmentConfiguration("sales-dingtalk-001")).thenReturn(salesDepartment);
+        Map<String, Object> applicantConfiguration = new LinkedHashMap<String, Object>();
+        applicantConfiguration.put("departmentId", "matrix-dept");
+        applicantConfiguration.put("company", "明磊");
+        when(documentService.queryApplicantDepartmentConfiguration("41603502201288023")).thenReturn(applicantConfiguration);
         ApproveOrderFormPushHandler handler = new ApproveOrderFormPushHandler(yidaClient, ddConf, applications,
-                roster, documentService);
+                roster, documentService, new LocalFormDepartmentEnricher(documentService));
 
         assertEquals("process-instance-1", handler.push(request));
 
@@ -124,15 +128,15 @@ class ApproveOrderFormPushHandlerTest {
         assertEquals("41603502201288023", body.get("userId"));
         assertEquals("TPROC--9VD66QA18AJ8317KK3NXM54W1HIT30C7N28TM7", body.get("processCode"));
         assertEquals("FORM-8454FA17732746058A2D90B1C559CC19G5SR", body.get("formUuid"));
-        assertEquals("approveOrder-SO-001", body.get("title"));
-        assertEquals("approveOrder-SO-001", body.get("dataTitle"));
+        assertEquals("订单审批流程-SO-001", body.get("title"));
+        assertEquals("订单审批流程-SO-001", body.get("dataTitle"));
 
         JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
-        assertEquals("approveOrder-SO-001", formData.getString("textField_msh27unu"));
-        assertEquals("approveOrder-SO-001", formData.getString("textField_mspcpz92"));
+        assertEquals("订单审批流程-SO-001", formData.getString("textField_msh27unu"));
+        assertEquals("订单审批流程-SO-001", formData.getString("textField_mspcpz92"));
         assertEquals("41603502201288023", formData.getJSONArray("employeeField_msh186rr").getString(0));
-        assertEquals("ML", formData.getString("selectField_mtv87n9f"));
-        assertEquals("dept-001", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
+        assertEquals("明磊", formData.getString("selectField_mtv87n9f"));
+        assertEquals("matrix-dept", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
         assertEquals(Long.valueOf(1789451130000L), formData.getLong("dateField_msh186rs"));
         assertEquals("正常", formData.getString("textField_mt7zi4fy"));
         assertEquals("87", formData.getString("textField_mt82p09i"));
@@ -143,7 +147,7 @@ class ApproveOrderFormPushHandlerTest {
         assertEquals("sales-dept-88", formData.getJSONArray("departmentSelectField_mt82p08u").getString(0));
         assertEquals("CUS-001", formData.getString("textField_mt80nq6v"));
         assertEquals("customer-short-name", formData.getString("textField_mt82p08x"));
-        assertEquals("normal-order", formData.getString("textField_mt82p091"));
+        assertEquals("一般订单", formData.getString("textField_mt82p091"));
         assertEquals("Y", formData.getString("textField_mt82p092"));
         assertEquals("1234.56", formData.getString("textField_mt82p094"));
         assertEquals("remark-001", formData.getString("textField_mt82p097"));
@@ -228,6 +232,15 @@ class ApproveOrderFormPushHandlerTest {
         assertEquals(Long.valueOf(1789401600000L), formData.getLong("dateField_msh186rs"));
     }
 
+    @Test
+    void mapsOrderNatureCodes() {
+        assertEquals("一般订单", ApproveOrderFormPushHandler.orderNature(1));
+        assertEquals("换货订单", ApproveOrderFormPushHandler.orderNature("2"));
+        assertEquals("签收订单", ApproveOrderFormPushHandler.orderNature("3.0"));
+        assertEquals("调拨订单", ApproveOrderFormPushHandler.orderNature(4));
+        assertEquals("预先订单", ApproveOrderFormPushHandler.orderNature("5"));
+    }
+
     private ApproveOrderFormPushHandler handler(YidaProcessOpenApiClient yidaClient,
                                                 EmployeeRosterService roster,
                                                 MingleiDocumentService documentService) {
@@ -237,7 +250,10 @@ class ApproveOrderFormPushHandlerTest {
         YidaApplicationsProperties applications = new YidaApplicationsProperties();
         applications.getCoordinationOffice().setAppType("APP_RJJL69QUIZVQSV7YC8TL");
         applications.getCoordinationOffice().setSystemToken("system-token");
-        return new ApproveOrderFormPushHandler(yidaClient, ddConf, applications, roster, documentService);
+        when(documentService.queryApplicantDepartmentConfiguration(org.mockito.ArgumentMatchers.anyString()))
+                .thenReturn(Collections.emptyMap());
+        return new ApproveOrderFormPushHandler(yidaClient, ddConf, applications, roster, documentService,
+                new LocalFormDepartmentEnricher(documentService));
     }
 
     private ErpYidaPushRequest requestWithSalesPerson(String employeeNo) {

+ 11 - 2
src/test/java/com/malk/minglei/service/impl/yida/Cpmp402YidaProcessHandlerTest.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.malk.minglei.config.YidaApplicationsProperties;
 import com.malk.minglei.dto.YidaOperationParam;
 import com.malk.minglei.dto.YidaProcessStartRequest;
+import com.malk.minglei.service.yida.MingleiDocumentService;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.service.aliwork.YDClient;
@@ -66,7 +67,13 @@ class Cpmp402YidaProcessHandlerTest {
         request.setDataTitle("package-price-title");
         request.setFields(fields);
 
-        Cpmp402YidaProcessHandler handler = new Cpmp402YidaProcessHandler(yidaClient, ddConf, applications);
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        Map<String, Object> applicantConfiguration = new LinkedHashMap<String, Object>();
+        applicantConfiguration.put("departmentId", "matrix-dept");
+        applicantConfiguration.put("company", "明磊");
+        when(documentService.queryApplicantDepartmentConfiguration("41603502201288023")).thenReturn(applicantConfiguration);
+        Cpmp402YidaProcessHandler handler = new Cpmp402YidaProcessHandler(yidaClient, ddConf, applications,
+                new LocalFormDepartmentEnricher(documentService));
 
         assertEquals("process-instance-1", handler.startProcess(request));
 
@@ -87,7 +94,9 @@ class Cpmp402YidaProcessHandlerTest {
         assertEquals("package-price-title", formData.getString("textField_msh27unu"));
         assertEquals("package-price-title", formData.getString("textField_mspcpz92"));
         assertEquals("41603502201288023", formData.getJSONArray("employeeField_msh186rr").getString(0));
-        assertEquals("ML", formData.getString("selectField_mso1em48"));
+        assertEquals("明磊", formData.getString("selectField_mso1em48"));
+        assertEquals("明磊", formData.getString("textField_mu1ys6rc"));
+        assertEquals("matrix-dept", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
         assertEquals(Long.valueOf(1799337600000L), formData.getLong("dateField_msh186rs"));
         assertEquals("明磊", formData.getString("selectField_msy2akpj"));
         assertEquals("ML", formData.getString("textField_msy2akpi"));

+ 20 - 6
src/test/java/com/malk/minglei/service/impl/yida/CustomerReleaseFormPushHandlerTest.java

@@ -12,11 +12,17 @@ import com.malk.server.dingtalk.DDConf;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -78,7 +84,8 @@ class CustomerReleaseFormPushHandlerTest {
         departmentConfiguration.put("departmentId", "dept-001");
         departmentConfiguration.put("company", "ML");
         when(documentService.queryApplicantDepartmentConfiguration("41603502201288023")).thenReturn(departmentConfiguration);
-        CustomerReleaseFormPushHandler handler = new CustomerReleaseFormPushHandler(yidaClient, ddConf, applications, documentService);
+        CustomerReleaseFormPushHandler handler = new CustomerReleaseFormPushHandler(yidaClient, ddConf, applications,
+                documentService, new LocalFormDepartmentEnricher(documentService));
 
         assertEquals(Collections.singletonList("axmt140"), handler.getTypes());
         assertEquals("process-instance-1", handler.push(request));
@@ -91,16 +98,16 @@ class CustomerReleaseFormPushHandlerTest {
         assertEquals("41603502201288023", body.get("userId"));
         assertEquals("TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU", body.get("processCode"));
         assertEquals("FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C", body.get("formUuid"));
-        assertEquals("customerRelease-CR-001", body.get("title"));
-        assertEquals("customerRelease-CR-001", body.get("dataTitle"));
+        assertEquals("客户放行-CR-001", body.get("title"));
+        assertEquals("客户放行-CR-001", body.get("dataTitle"));
 
         JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
-        assertEquals("customerRelease-CR-001", formData.getString("textField_mtv1tgg1"));
-        assertEquals("customerRelease-CR-001", formData.getString("textField_mspcpz92"));
+        assertEquals("客户放行-CR-001", formData.getString("textField_mtv1tgg1"));
+        assertEquals("客户放行-CR-001", formData.getString("textField_mspcpz92"));
         assertEquals("41603502201288023", formData.getJSONArray("employeeField_msh186rr").getString(0));
         assertEquals("ML", formData.getString("selectField_mtv7uxgd"));
         assertEquals("dept-001", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
-        assertEquals(Long.valueOf(1789401600000L), formData.getLong("dateField_msh186rs"));
+        assertDateUsesCurrentTime(formData.getLong("dateField_msh186rs"), "2026-09-15");
         assertEquals("CR-001", formData.getString("textField_mt7zi4fy"));
         assertEquals("release-desc", formData.getString("textField_mt7zi4g0"));
         assertEquals("cannot-deliver-reason", formData.getString("textField_mt7zi4fz"));
@@ -118,4 +125,11 @@ class CustomerReleaseFormPushHandlerTest {
         assertEquals("3000.5", row.getString("numberField_mt7zi4g7"));
         assertEquals("short-remark", row.getString("textField_mt6ur4w8"));
     }
+
+    private void assertDateUsesCurrentTime(Long millis, String day) {
+        LocalDateTime actual = Instant.ofEpochMilli(millis.longValue()).atZone(ZoneId.systemDefault()).toLocalDateTime();
+        assertEquals(LocalDate.parse(day), actual.toLocalDate());
+        long secondsApart = Math.abs(Duration.between(actual.toLocalTime(), LocalDateTime.now().toLocalTime()).getSeconds());
+        assertTrue(secondsApart < 5 || secondsApart > 86400 - 5);
+    }
 }

+ 158 - 16
src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandlerTest.java

@@ -14,6 +14,11 @@ import com.malk.server.dingtalk.DDConf;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -21,6 +26,7 @@ import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -87,9 +93,18 @@ class MiscReceiptIssueFormPushHandlerTest {
         request.setMainData(mainData);
         request.setDetailData(detailData);
 
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        EmployeeRosterInfo applicant = new EmployeeRosterInfo();
+        applicant.setEmployeeNo("applicant-001");
+        applicant.setDingtalkId("applicant-dingtalk-001");
+        when(rosterService.findByEmployeeNo("applicant-001")).thenReturn(applicant);
+
         MingleiDocumentService documentService = FormDepartmentTestSupport.configuredDocumentService("41603502201288023");
+        Map<String, Object> applicantDepartment = new LinkedHashMap<String, Object>();
+        applicantDepartment.put("departmentId", "applicant-dept-88");
+        when(documentService.queryApplicantDepartmentConfiguration("applicant-dingtalk-001")).thenReturn(applicantDepartment);
         MiscReceiptIssueFormPushHandler handler = new MiscReceiptIssueFormPushHandler(yidaClient, ddConf, applications,
-                new LocalFormDepartmentEnricher(documentService));
+                new LocalFormDepartmentEnricher(documentService), rosterService, documentService);
 
         assertEquals("process-instance-1", handler.push(request));
 
@@ -106,17 +121,23 @@ class MiscReceiptIssueFormPushHandlerTest {
 
         JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
         assertEquals("reason-desc-main-MS-001", formData.getString("textField_msh27unu"));
-        assertEquals("miscReceiptIssue-MS-001", formData.getString("textField_mspcpz92"));
+        assertEquals("reason-desc-main-MS-001", formData.getString("textField_mspcpz92"));
+        assertEquals(Boolean.FALSE, body.get("executeExpression"));
+        assertEquals(Boolean.TRUE, body.get("noExecuteExpression"));
         assertEquals("41603502201288023", formData.getJSONArray("employeeField_msh186rr").getString(0));
         assertEquals("Configured Company", formData.getString("selectField_mso1em48"));
         assertEquals(Collections.singletonList("configured-dept-001"), formData.getJSONArray("departmentSelectField_msh186ry"));
         assertEquals(Arrays.asList("applicant-first-001", "applicant-first-002"), formData.getJSONArray("employeeField_mtjp3e58"));
         assertEquals(Collections.singletonList("applicant-second-001"), formData.getJSONArray("employeeField_mtjp3e59"));
         assertEquals(Collections.singletonList("applicant-third-001"), formData.getJSONArray("employeeField_mvylqnek"));
-        assertEquals(Long.valueOf(1789401600000L), formData.getLong("dateField_msh186rs"));
+        assertDateUsesCurrentTime(formData.getLong("dateField_msh186rs"), "2026-09-15");
         assertEquals("ERP", formData.getString("textField_mt7zi4fy"));
         assertEquals("applicant-001", formData.getString("textField_mt7zi4g0"));
         assertEquals("dept-code-001", formData.getString("textField_mt80nq6s"));
+        assertEquals("applicant-dingtalk-001", formData.getJSONArray("employeeField_mw0jwshi").getString(0));
+        assertEquals(1, formData.getJSONArray("employeeField_mw0jwshi").size());
+        assertEquals("applicant-dept-88", formData.getJSONArray("departmentSelectField_mw0ko5yp").getString(0));
+        assertEquals(1, formData.getJSONArray("departmentSelectField_mw0ko5yp").size());
         assertEquals("RSN-001", formData.getString("textField_mt80nq6t"));
         assertEquals("reason-desc-main", formData.getString("textField_mt80nq6v"));
         assertEquals("remark-001", formData.getString("textField_mt7zi4fz"));
@@ -144,7 +165,90 @@ class MiscReceiptIssueFormPushHandlerTest {
         assertEquals("PROJ-001", row.getString("textField_mt6ur4w8"));
         assertEquals("project-desc", row.getString("textField_mt80nq7a"));
         assertEquals("SRC-001", row.getString("textField_mt80nq7b"));
-        assertEquals(null, row.get("employeeField_mt80nq7d"));
+        assertEquals(null, row.get("textField_mw1ekhte"));
+        assertEquals(null, row.get("employeeField_mt80nq7e"));
+    }
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void pushKeepsApplyDateTimeToSecondsAndUsesReasonDocNoTitle() {
+        YidaProcessOpenApiClient yidaClient = mock(YidaProcessOpenApiClient.class);
+        DDR_New response = new DDR_New();
+        response.setResult("process-instance-time");
+        when(yidaClient.startProcess(org.mockito.ArgumentMatchers.anyMap())).thenReturn(response);
+
+        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
+        mainData.setExtraField("inbadocno", "MLSQ1261102003");
+        mainData.setExtraField("oocql004", "杂收_客供品入库");
+        mainData.setExtraField("rq", "2026-09-15 13:45:30");
+        mainData.setExtraField("inbadocdt", "2026-09-15");
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(MiscReceiptIssueFormPushHandler.TYPE);
+        request.setMainData(mainData);
+
+        assertEquals("process-instance-time", handler(yidaClient, null).push(request));
+
+        ArgumentCaptor<Map> bodyCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(yidaClient).startProcess(bodyCaptor.capture());
+        Map body = bodyCaptor.getValue();
+        assertEquals("杂收_客供品入库-MLSQ1261102003", body.get("title"));
+        assertEquals("杂收_客供品入库-MLSQ1261102003", body.get("dataTitle"));
+        JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
+        assertEquals("杂收_客供品入库-MLSQ1261102003", formData.getString("textField_msh27unu"));
+        assertEquals("杂收_客供品入库-MLSQ1261102003", formData.getString("textField_mspcpz92"));
+        assertEquals(Long.valueOf(1789451130000L), formData.getLong("dateField_msh186rs"));
+    }
+
+    @Test
+    void pushFailsWhenApplicantEmployeeIsMissingFromRoster() {
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        when(rosterService.findByEmployeeNo("applicant-001")).thenReturn(null);
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        MiscReceiptIssueFormPushHandler handler = new MiscReceiptIssueFormPushHandler(
+                mock(YidaProcessOpenApiClient.class), dingTalkConfiguration(), applications(),
+                new LocalFormDepartmentEnricher(documentService), rosterService, documentService);
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> handler.push(requestWithApplicant("applicant-001")));
+
+        assertEquals("misc receipt issue applicant not found: applicant-001", exception.getMessage());
+    }
+
+    @Test
+    void pushFailsWhenApplicantHasNoDingTalkUserId() {
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        EmployeeRosterInfo applicant = new EmployeeRosterInfo();
+        applicant.setEmployeeNo("applicant-001");
+        when(rosterService.findByEmployeeNo("applicant-001")).thenReturn(applicant);
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        MiscReceiptIssueFormPushHandler handler = new MiscReceiptIssueFormPushHandler(
+                mock(YidaProcessOpenApiClient.class), dingTalkConfiguration(), applications(),
+                new LocalFormDepartmentEnricher(documentService), rosterService, documentService);
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> handler.push(requestWithApplicant("applicant-001")));
+
+        assertEquals("misc receipt issue applicant has no DingTalk userId: applicant-001", exception.getMessage());
+    }
+
+    @Test
+    void pushFailsWhenApplicantDepartmentIsMissing() {
+        EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
+        EmployeeRosterInfo applicant = new EmployeeRosterInfo();
+        applicant.setEmployeeNo("applicant-001");
+        applicant.setDingtalkId("applicant-dingtalk-001");
+        when(rosterService.findByEmployeeNo("applicant-001")).thenReturn(applicant);
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        when(documentService.queryApplicantDepartmentConfiguration("applicant-dingtalk-001"))
+                .thenReturn(Collections.<String, Object>emptyMap());
+        MiscReceiptIssueFormPushHandler handler = new MiscReceiptIssueFormPushHandler(
+                mock(YidaProcessOpenApiClient.class), dingTalkConfiguration(), applications(),
+                new LocalFormDepartmentEnricher(documentService), rosterService, documentService);
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> handler.push(requestWithApplicant("applicant-001")));
+
+        assertEquals("misc receipt issue applicant department not found: applicant-001", exception.getMessage());
     }
 
     @Test
@@ -161,50 +265,88 @@ class MiscReceiptIssueFormPushHandlerTest {
         keeper.setDingtalkId("keeper-dingtalk-id");
         when(rosterService.findByEmployeeNo("10086001")).thenReturn(keeper);
 
-        MiscReceiptIssueFormPushHandler handler = handler(yidaClient, rosterService);
-        ErpYidaPushRequest request = requestWithKeeper("10086001");
+        MiscReceiptKeeperLookup keeperLookup = mock(MiscReceiptKeeperLookup.class);
+        when(keeperLookup.findKeeperEmployeeNo("ITEM-001")).thenReturn("10086001");
+        MiscReceiptIssueFormPushHandler handler = handler(yidaClient, rosterService, keeperLookup);
 
-        assertEquals("process-instance-keeper", handler.push(request));
+        assertEquals("process-instance-keeper", handler.push(requestWithItem("ITEM-001")));
 
         ArgumentCaptor<Map> bodyCaptor = ArgumentCaptor.forClass(Map.class);
         verify(yidaClient).startProcess(bodyCaptor.capture());
         JSONObject formData = JSONObject.parseObject(String.valueOf(bodyCaptor.getValue().get("formDataJson")));
         JSONObject row = formData.getJSONArray("tableField_msh186sv").getJSONObject(0);
-        assertEquals("keeper-dingtalk-id", row.getJSONArray("employeeField_mt80nq7d").getString(0));
-        assertEquals(1, row.getJSONArray("employeeField_mt80nq7d").size());
+        assertEquals("10086001", row.getString("textField_mw1ekhte"));
+        assertEquals("keeper-dingtalk-id", row.getJSONArray("employeeField_mt80nq7e").getString(0));
+        assertEquals(1, row.getJSONArray("employeeField_mt80nq7e").size());
     }
 
     @Test
     void pushFailsWhenDetailKeeperEmployeeIsMissingFromRoster() {
         EmployeeRosterService rosterService = mock(EmployeeRosterService.class);
         when(rosterService.findByEmployeeNo("10086001")).thenReturn(null);
-        MiscReceiptIssueFormPushHandler handler = handler(mock(YidaProcessOpenApiClient.class), rosterService);
+        MiscReceiptKeeperLookup keeperLookup = mock(MiscReceiptKeeperLookup.class);
+        when(keeperLookup.findKeeperEmployeeNo("ITEM-001")).thenReturn("10086001");
+        MiscReceiptIssueFormPushHandler handler = handler(mock(YidaProcessOpenApiClient.class), rosterService, keeperLookup);
 
         IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
-                () -> handler.push(requestWithKeeper("10086001")));
+                () -> handler.push(requestWithItem("ITEM-001")));
 
         assertEquals("misc receipt issue warehouse keeper employee not found: 10086001", exception.getMessage());
     }
 
-    private MiscReceiptIssueFormPushHandler handler(YidaProcessOpenApiClient yidaClient, EmployeeRosterService rosterService) {
+    private void assertDateUsesCurrentTime(Long millis, String day) {
+        LocalDateTime actual = Instant.ofEpochMilli(millis.longValue()).atZone(ZoneId.systemDefault()).toLocalDateTime();
+        assertEquals(LocalDate.parse(day), actual.toLocalDate());
+        long secondsApart = Math.abs(Duration.between(actual.toLocalTime(), LocalDateTime.now().toLocalTime()).getSeconds());
+        assertTrue(secondsApart < 5 || secondsApart > 86400 - 5);
+    }
+
+    private DDConf dingTalkConfiguration() {
         DDConf ddConf = new DDConf();
         ddConf.setAppKey("app-key");
         ddConf.setAppSecret("app-secret");
+        return ddConf;
+    }
+
+    private YidaApplicationsProperties applications() {
         YidaApplicationsProperties applications = new YidaApplicationsProperties();
         applications.getCoordinationOffice().setAppType("APP_RJJL69QUIZVQSV7YC8TL");
         applications.getCoordinationOffice().setSystemToken("system-token");
+        return applications;
+    }
+
+    private ErpYidaPushRequest requestWithApplicant(String employeeNo) {
+        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
+        mainData.setExtraField("inbadocno", "MS-001");
+        mainData.setExtraField("oocql004", "reason-desc-main");
+        mainData.setExtraField("inba003", employeeNo);
+        ErpYidaPushRequest request = new ErpYidaPushRequest();
+        request.setType(MiscReceiptIssueFormPushHandler.TYPE);
+        request.setMainData(mainData);
+        return request;
+    }
+
+    private MiscReceiptIssueFormPushHandler handler(YidaProcessOpenApiClient yidaClient, EmployeeRosterService rosterService) {
+        return handler(yidaClient, rosterService, null);
+    }
+
+    private MiscReceiptIssueFormPushHandler handler(YidaProcessOpenApiClient yidaClient, EmployeeRosterService rosterService,
+                                                    MiscReceiptKeeperLookup keeperLookup) {
+        DDConf ddConf = new DDConf();
+        ddConf.setAppKey("app-key");
+        ddConf.setAppSecret("app-secret");
+        YidaApplicationsProperties applications = applications();
         MingleiDocumentService documentService = FormDepartmentTestSupport.configuredDocumentService("41603502201288023");
         return new MiscReceiptIssueFormPushHandler(yidaClient, ddConf, applications,
-                new LocalFormDepartmentEnricher(documentService), rosterService);
+                new LocalFormDepartmentEnricher(documentService), rosterService, documentService, keeperLookup);
     }
 
-    private ErpYidaPushRequest requestWithKeeper(String employeeNo) {
+    private ErpYidaPushRequest requestWithItem(String itemCode) {
         ErpYidaPushMainData mainData = new ErpYidaPushMainData();
         mainData.setExtraField("inbadocno", "MS-001");
         mainData.setExtraField("oocql004", "reason-desc-main");
         Map<String, Object> detailRow = new LinkedHashMap<String, Object>();
-        detailRow.put("inbb001", "ITEM-001");
-        detailRow.put("employeeField_mt80nq7d", employeeNo);
+        detailRow.put("inbb001", itemCode);
         Map<String, Object> detailData = new LinkedHashMap<String, Object>();
         detailData.put("detailData1", Collections.singletonList(detailRow));
         ErpYidaPushRequest request = new ErpYidaPushRequest();

+ 46 - 0
src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptKeeperOracleLookupTest.java

@@ -0,0 +1,46 @@
+package com.malk.minglei.service.impl.yida;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.namedparam.SqlParameterSource;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MiscReceiptKeeperOracleLookupTest {
+
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void findKeeperEmployeeNoUsesItemCodeOnHardcodedCompanySite() {
+        NamedParameterJdbcTemplate jdbcTemplate = mock(NamedParameterJdbcTemplate.class);
+        when(jdbcTemplate.query(anyString(), any(SqlParameterSource.class), any(RowMapper.class)))
+                .thenReturn(Collections.singletonList("10086001"));
+
+        assertEquals("10086001", new MiscReceiptKeeperOracleLookup(jdbcTemplate).findKeeperEmployeeNo(" ITEM-001 "));
+
+        org.mockito.ArgumentCaptor<String> sql = org.mockito.ArgumentCaptor.forClass(String.class);
+        org.mockito.ArgumentCaptor<SqlParameterSource> parameters = org.mockito.ArgumentCaptor.forClass(SqlParameterSource.class);
+        verify(jdbcTemplate).query(sql.capture(), parameters.capture(), any(RowMapper.class));
+        assertEquals("select imaf052 from DSDATA.imaf_t where imafent = 86 and imafsite = 'ML' and imaf001 = :itemCode",
+                sql.getValue());
+        assertEquals("ITEM-001", ((MapSqlParameterSource) parameters.getValue()).getValue("itemCode"));
+    }
+
+    @Test
+    void findKeeperEmployeeNoReturnsNullWhenItemHasNoKeeper() {
+        NamedParameterJdbcTemplate jdbcTemplate = mock(NamedParameterJdbcTemplate.class);
+        when(jdbcTemplate.query(anyString(), any(SqlParameterSource.class), any(RowMapper.class)))
+                .thenReturn(Collections.emptyList());
+
+        assertNull(new MiscReceiptKeeperOracleLookup(jdbcTemplate).findKeeperEmployeeNo("ITEM-404"));
+    }
+}

+ 17 - 6
src/test/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandlerTest.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.malk.minglei.config.YidaApplicationsProperties;
 import com.malk.minglei.dto.ErpYidaPushMainData;
 import com.malk.minglei.dto.ErpYidaPushRequest;
+import com.malk.minglei.service.yida.MingleiDocumentService;
 import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
 import com.malk.server.common.McException;
 import com.malk.server.dingtalk.DDR_New;
@@ -17,7 +18,6 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -88,7 +88,8 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         request.setDetailData(detailData);
         request.setOriginatorUserId("dingtalk-user-001");
 
-        MlWorkOrderCraftPriceFormPushHandler handler = new MlWorkOrderCraftPriceFormPushHandler(yidaClient, ddConf, applications);
+        MlWorkOrderCraftPriceFormPushHandler handler = new MlWorkOrderCraftPriceFormPushHandler(yidaClient, ddConf, applications,
+                applicantEnricher("dingtalk-user-001", "明磊", "matrix-dept"));
 
         assertEquals(Collections.singletonList("FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K"), handler.getTypes());
         assertEquals("process-instance-1", handler.push(request));
@@ -110,9 +111,9 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         assertEquals("工单工艺工价审定-GD-001", formData.getString("textField_msh27unu"));
         assertEquals("工单工艺工价审定-GD-001", formData.getString("textField_mspcpz92"));
         assertEquals("dingtalk-user-001", formData.getJSONArray("employeeField_msh186rr").getString(0));
-        assertFalse(formData.containsKey("selectField_mso1em48"));
-        assertEquals(mainData.getGs(), formData.getString("textField_mu1yk6l5"));
-        assertEquals("123456", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
+        assertEquals("明磊", formData.getString("selectField_mso1em48"));
+        assertEquals("明磊", formData.getString("textField_mu1yk6l5"));
+        assertEquals("matrix-dept", formData.getJSONArray("departmentSelectField_msh186ry").getString(0));
         assertEquals(Long.valueOf(1799337600000L), formData.getLong("dateField_msh186rs"));
         assertEquals("GD-001", formData.getString("textField_msy0zj5i"));
         assertEquals("MO-001", formData.getString("textField_msy0zj5j"));
@@ -174,11 +175,21 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         request.setDetailData(detailData);
         request.setOriginatorUserId("dingtalk-user-001");
 
-        MlWorkOrderCraftPriceFormPushHandler handler = new MlWorkOrderCraftPriceFormPushHandler(yidaClient, ddConf, applications);
+        MlWorkOrderCraftPriceFormPushHandler handler = new MlWorkOrderCraftPriceFormPushHandler(yidaClient, ddConf, applications,
+                applicantEnricher("dingtalk-user-001", "明磊", "matrix-dept"));
 
         McException exception = assertThrows(McException.class, () -> handler.push(request));
 
         assertEquals("DINGTALK_YIDA_ERROR", exception.getCode());
         assertEquals("宜搭流程发起失败: invalid select option", exception.getMessage());
     }
+
+    private LocalFormDepartmentEnricher applicantEnricher(String userId, String company, String departmentId) {
+        MingleiDocumentService documentService = mock(MingleiDocumentService.class);
+        Map<String, Object> configuration = new LinkedHashMap<String, Object>();
+        configuration.put("company", company);
+        configuration.put("departmentId", departmentId);
+        when(documentService.queryApplicantDepartmentConfiguration(userId)).thenReturn(configuration);
+        return new LocalFormDepartmentEnricher(documentService);
+    }
 }