Ver código fonte

Merge remote-tracking branch 'origin/master'

lfx 3 dias atrás
pai
commit
68c23cdbdd

+ 15 - 2
README.md

@@ -236,6 +236,19 @@ POST /api/yida/minglei/employee-roster/getUserRoster
 Content-Type: application/json
 ```
 
+响应体的 `result` 包含花名册基础信息和银行信息:
+
+```json
+{
+  "employeeNo": "2601010001",
+  "username": "张三",
+  "bank_card_no": "6222000000000000",
+  "bank_name": "示例银行"
+}
+```
+
+`bank_card_no` 为银行卡号,`bank_name` 为开户行名称。
+
 ### 4.5 外部 T100 接口转发
 
 ```http
@@ -316,7 +329,7 @@ Content-Type: application/json
 | `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K` | `MlWorkOrderCraftPriceFormPushHandler` | ML-工单工艺工价审定 | `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K` | `TPROC--WG966BA1VWF8Q1K4OGF8H9KW47RR3ZREH0YSM2` |
 | `axmt500` | `ApproveOrderFormPushHandler` | 客户订单审批 | `FORM-8454FA17732746058A2D90B1C559CC19G5SR` | `TPROC--9VD66QA18AJ8317KK3NXM54W1HIT30C7N28TM7` |
 | `axmt140` | `CustomerReleaseFormPushHandler` | 客户放行审批 | `FORM-3BA9FE18494F4FDD85FE6ABF7DE741BAE30C` | `TPROC--6TG66381N8G8Y7L9GBEEY7QVXTUY2LBBGZ7TMU` |
-| `cint301` | `MiscReceiptIssueFormPushHandler` | 杂收杂发审批 | `FORM-4B08ADABCDAA45788C043729A855E03ED76G` | `TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0` |
+| `aint302` | `MiscReceiptIssueFormPushHandler` | 杂收杂发审批 | `FORM-4B08ADABCDAA45788C043729A855E03ED76G` | `TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0` |
 
 ### 5.2 `mainData` 规则
 
@@ -349,7 +362,7 @@ Content-Type: application/json
 | `FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K` | `detailData` |
 | `axmt500` | `detailData1`、`detailData2`,兼容历史 `detailData` |
 | `axmt140` | 优先 `detailData1`,缺失时回退到 `detailData` |
-| `cint301` | `detailData1` |
+| `aint302` | `detailData1` |
 
 明细对象中可以直接传入宜搭 `fieldId`。服务端会保留包含 `Field_` 的字段值,用于补充默认字段映射。
 

+ 55 - 0
pom.xml

@@ -78,6 +78,61 @@
             <artifactId>bcprov-jdk15on</artifactId>
             <version>1.70</version>
         </dependency>
+        <!--
+          Local maven repo only has a stub java-mcli parent POM, so mjava:0.0.3
+          brings almost no transitive deps. Explicitly restore the libs present in
+          the production fat jar built by teammates (~60MB).
+        -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.6.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>2.2.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.querydsl</groupId>
+            <artifactId>querydsl-jpa</artifactId>
+            <version>5.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.ehcache</groupId>
+            <artifactId>ehcache</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.auth0</groupId>
+            <artifactId>java-jwt</artifactId>
+            <version>3.4.0</version>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib</artifactId>
+            <version>3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.6.15</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>

+ 56 - 5
src/main/java/com/malk/minglei/controller/MingleiPortController.java

@@ -1,7 +1,11 @@
 package com.malk.minglei.controller;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.dto.MingleiPortRequest;
+import com.malk.minglei.dto.SupplierPriceApprovalProcessRequest;
 import com.malk.minglei.service.port.MingleiPortService;
+import com.malk.minglei.service.port.SupplierPriceApprovalProcessService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
@@ -15,10 +19,13 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.client.RestClientException;
 
-import javax.validation.Valid;
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * 提供调用明磊外部单据接口的本地触发入口。
@@ -30,27 +37,60 @@ public class MingleiPortController {
     private static final Logger LOGGER = LoggerFactory.getLogger(MingleiPortController.class);
 
     private final MingleiPortService mingleiPortService;
+    private final SupplierPriceApprovalProcessService supplierPriceApprovalProcessService;
+    private final ObjectMapper objectMapper;
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
 
     /**
      * 创建明磊外部单据接口控制器。
      *
      * @param mingleiPortService 明磊外部单据接口服务
+     * @param supplierPriceApprovalProcessService 宜搭实例转 T100 单据服务
+     * @param objectMapper JSON 解析器
      */
-    public MingleiPortController(MingleiPortService mingleiPortService) {
+    public MingleiPortController(MingleiPortService mingleiPortService,
+                                 SupplierPriceApprovalProcessService supplierPriceApprovalProcessService,
+                                 ObjectMapper objectMapper) {
         this.mingleiPortService = mingleiPortService;
+        this.supplierPriceApprovalProcessService = supplierPriceApprovalProcessService;
+        this.objectMapper = objectMapper;
     }
 
     /**
-     * 接收本系统的供应商价格审批单请求,并调用外部 T100 接口。
+     * 接收两类请求。带 formId 时按宜搭实例读取表单并推送 T100;否则按已组装好的 head、detail 转发。
      *
-     * @param request 仅包含 head 和 detail 的供应商价格审批单请求体
+     * @param body 宜搭实例标识,或 T100 单据 head、detail
      * @return 外部 T100 接口响应内容
      */
     @PostMapping("/supplier-price-approvals")
-    public ResponseEntity<Map<String, Object>> submitSupplierPriceApproval(@Valid @RequestBody MingleiPortRequest request) {
+    public ResponseEntity<Map<String, Object>> submitSupplierPriceApproval(@RequestBody JsonNode body) {
+        if (body != null && body.has("formId")) {
+            return ResponseEntity.ok(submitYidaInstance(body));
+        }
+        MingleiPortRequest request = objectMapper.convertValue(body, MingleiPortRequest.class);
+        Set<ConstraintViolation<MingleiPortRequest>> violations = validator.validate(request);
+        if (!violations.isEmpty()) {
+            return ResponseEntity.badRequest().body(Collections.<String, Object>singletonMap("message", "Request validation failed"));
+        }
         return ResponseEntity.ok(mingleiPortService.submitSupplierPriceApproval(request));
     }
 
+    private Map<String, Object> submitYidaInstance(JsonNode body) {
+        SupplierPriceApprovalProcessRequest request = objectMapper.convertValue(body, SupplierPriceApprovalProcessRequest.class);
+        if (isBlank(request.getFormId()) || isBlank(request.getFormInstanceId())) {
+            throw new IllegalArgumentException("formId and formInstanceId must not be blank");
+        }
+        if (request.requiresProcessInstanceId() && isBlank(request.getProcessInstanceId())) {
+            throw new IllegalArgumentException("processInstanceId must not be blank");
+        }
+        return supplierPriceApprovalProcessService.submitByInstanceIds(
+                request.getFormId(), request.getFormInstanceId(), request.getProcessInstanceId());
+    }
+
+    private static boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
     /**
      * 返回本地请求体校验或 JSON 解析失败的信息,不发起外部调用。
      *
@@ -62,6 +102,17 @@ public class MingleiPortController {
         return ResponseEntity.badRequest().body(Collections.singletonMap("message", "Request validation failed"));
     }
 
+    /**
+     * 返回宜搭实例参数不完整或表单不受支持的错误信息。
+     *
+     * @param exception 参数异常
+     * @return 参数错误响应
+     */
+    @ExceptionHandler(IllegalArgumentException.class)
+    public ResponseEntity<Map<String, String>> handleInvalidArgument(IllegalArgumentException exception) {
+        return ResponseEntity.badRequest().body(Collections.singletonMap("message", exception.getMessage()));
+    }
+
     /**
      * 将外部服务的连接、超时或响应异常转换为本地网关错误。
      *

+ 12 - 6
src/main/java/com/malk/minglei/dto/EmployeeRosterInfo.java

@@ -17,7 +17,9 @@ public class EmployeeRosterInfo {
     private String jobGradeId;
     private String department;
     private String firstDepartment;
+    @JsonProperty("bank_card_no")
     private String bankCardNo;
+    @JsonProperty("bank_name")
     private String bankName;
     private String bankBranchCode;
 
@@ -103,15 +105,16 @@ public class EmployeeRosterInfo {
 
     /**
      * 获取银行卡号。
+     *
      * @return 银行卡号
      */
-    @JsonProperty("bank_card_no")
     public String getBankCardNo() {
         return bankCardNo;
     }
 
     /**
      * 设置银行卡号。
+     *
      * @param bankCardNo 银行卡号
      */
     public void setBankCardNo(String bankCardNo) {
@@ -119,17 +122,18 @@ public class EmployeeRosterInfo {
     }
 
     /**
-     * 获取开户行。
-     * @return 开户行
+     * 获取开户行名称。
+     *
+     * @return 开户行名称
      */
-    @JsonProperty("bank_name")
     public String getBankName() {
         return bankName;
     }
 
     /**
-     * 设置开户行。
-     * @param bankName 开户行
+     * 设置开户行名称。
+     *
+     * @param bankName 开户行名称
      */
     public void setBankName(String bankName) {
         this.bankName = bankName;
@@ -137,6 +141,7 @@ public class EmployeeRosterInfo {
 
     /**
      * 获取支行联行号。
+     *
      * @return 支行联行号
      */
     @JsonProperty("bank_branch_code")
@@ -146,6 +151,7 @@ public class EmployeeRosterInfo {
 
     /**
      * 设置支行联行号。
+     *
      * @param bankBranchCode 支行联行号
      */
     public void setBankBranchCode(String bankBranchCode) {

+ 1 - 0
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRepository.java

@@ -160,6 +160,7 @@ public class EmployeeRosterRepository {
         info.setJobGradeId(resultSet.getString("job_grade_id"));
         info.setDepartment(resultSet.getString("department"));
         info.setFirstDepartment(resultSet.getString("first_department"));
+        // 银行信息统一在末尾映射,避免与同事合并后重复赋值
         info.setBankCardNo(resultSet.getString("bank_card_no"));
         info.setBankName(resultSet.getString("bank_name"));
         info.setBankBranchCode(resultSet.getString("bank_branch_code"));

+ 28 - 10
src/main/java/com/malk/minglei/service/impl/port/MingleiPortServiceImpl.java

@@ -34,7 +34,8 @@ public class MingleiPortServiceImpl implements MingleiPortService {
     private static final String HOST_ACCOUNT = "tiptop";
     private static final String HOST_TIMESTAMP = "20260807164700004";
     private static final String SERVICE_PRODUCT = "T100";
-    private static final String SERVICE_NAME = "ProcurementApprovalPrice";
+    static final String SUPPLIER_PRICE_APPROVAL_SERVICE_NAME = "ProcurementApprovalPrice";
+    static final String DINGTALK_TO_T100_SERVICE_NAME = "DingTalk To T100";
 
     private final RestTemplate restTemplate;
     private final MingleiPortProperties properties;
@@ -58,11 +59,11 @@ public class MingleiPortServiceImpl implements MingleiPortService {
         Map<String, Object> parameter = new LinkedHashMap<String, Object>();
         parameter.put("head", request.getHead());
         parameter.put("detail", request.getDetail());
-        return submitDocument(parameter, null);
+        return submitDocument(parameter, null, SUPPLIER_PRICE_APPROVAL_SERVICE_NAME);
     }
 
     /**
-     * 提交除供应商核价单外的通用 T100 单据。
+     * 提交除供应商核价单外的通用 T100 单据,服务名沿用供应商价格审批单。
      *
      * @param parameter T100 标准报文中的 parameter 内容
      * @param dataKeyType T100 datakey.type 单据类型
@@ -70,25 +71,40 @@ public class MingleiPortServiceImpl implements MingleiPortService {
      */
     @Override
     public Map<String, Object> submitT100Document(Map<String, Object> parameter, String dataKeyType) {
+        return submitT100Document(parameter, dataKeyType, SUPPLIER_PRICE_APPROVAL_SERVICE_NAME);
+    }
+
+    /**
+     * 提交指定服务名的 T100 单据。包材材质核价、包材料号清单和供应商暂停/退出使用 DingTalk To T100。
+     *
+     * @param parameter T100 标准报文中的 parameter 内容
+     * @param dataKeyType T100 datakey.type 单据类型
+     * @param serviceName T100 service.name
+     * @return 外部接口原始响应体
+     */
+    @Override
+    public Map<String, Object> submitT100Document(Map<String, Object> parameter, String dataKeyType, String serviceName) {
         if (parameter == null) {
             throw new IllegalArgumentException("T100 parameter must not be null");
         }
         require(dataKeyType, "T100 datakey.type must not be blank");
-        return submitDocument(parameter, dataKeyType);
+        require(serviceName, "T100 service.name must not be blank");
+        return submitDocument(parameter, dataKeyType, serviceName);
     }
 
-    private Map<String, Object> submitDocument(Map<String, Object> parameter, String dataKeyType) {
+    private Map<String, Object> submitDocument(Map<String, Object> parameter, String dataKeyType, String serviceName) {
         HttpHeaders headers = new HttpHeaders();
         headers.setContentType(MediaType.APPLICATION_JSON);
         headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
 
         validateConfiguration();
-        LOGGER.info("Submitting T100 document dataKeyType={} parameterKeys={}", dataKeyType, parameter.keySet());
+        LOGGER.info("Submitting T100 document dataKeyType={} serviceName={} parameterKeys={}",
+                dataKeyType, serviceName, parameter.keySet());
 
         ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
                 properties.getSupplierPriceApprovalUrl(),
                 HttpMethod.POST,
-                new HttpEntity<Map<String, Object>>(createExternalRequest(parameter, dataKeyType), headers),
+                new HttpEntity<Map<String, Object>>(createExternalRequest(parameter, dataKeyType, serviceName), headers),
                 new ParameterizedTypeReference<Map<String, Object>>() {
                 });
         return response.getBody() == null ? Collections.<String, Object>emptyMap() : response.getBody();
@@ -97,10 +113,12 @@ public class MingleiPortServiceImpl implements MingleiPortService {
     /**
      * 使用固定的外层协议字段组装外部接口请求,仅透传调用方提供的 head 和 detail。
      *
-     * @param request 本地精简请求体
+     * @param parameter T100 标准报文中的 parameter 内容
+     * @param dataKeyType T100 datakey.type 单据类型
+     * @param serviceName T100 service.name
      * @return 外部接口完整请求体
      */
-    private Map<String, Object> createExternalRequest(Map<String, Object> parameter, String dataKeyType) {
+    private Map<String, Object> createExternalRequest(Map<String, Object> parameter, String dataKeyType, String serviceName) {
         Map<String, Object> host = new LinkedHashMap<String, Object>();
         host.put("prod", HOST_PRODUCT);
         host.put("ip", HOST_IP);
@@ -110,7 +128,7 @@ public class MingleiPortServiceImpl implements MingleiPortService {
 
         Map<String, Object> service = new LinkedHashMap<String, Object>();
         service.put("prod", SERVICE_PRODUCT);
-        service.put("name", SERVICE_NAME);
+        service.put("name", serviceName);
         service.put("ip", HOST_IP);
         service.put("id", erpT100Properties.getRequest().getServiceId());
 

+ 18 - 5
src/main/java/com/malk/minglei/service/impl/port/SupplierPriceApprovalProcessServiceImpl.java

@@ -213,10 +213,11 @@ public class SupplierPriceApprovalProcessServiceImpl implements SupplierPriceApp
         }
 
         Map<String, Object> parameter = toAdditionalT100Parameter(requestedFormId, components);
-        LOGGER.info("Submitting T100 document from formId={} formInstanceId={} processInstanceId={} dataKeyType={}",
-                requestedFormId, requestedFormInstanceId, processInstanceId, dataKeyType);
+        String serviceName = resolveServiceName(requestedFormId);
+        LOGGER.info("Submitting T100 document from formId={} formInstanceId={} processInstanceId={} dataKeyType={} serviceName={}",
+                requestedFormId, requestedFormInstanceId, processInstanceId, dataKeyType, serviceName);
         try {
-            return mingleiPortService.submitT100Document(parameter, dataKeyType);
+            return mingleiPortService.submitT100Document(parameter, dataKeyType, serviceName);
         } catch (RestClientException exception) {
             logExternalFailure("T100_DOCUMENT_PUSH", requestedFormId, requestedFormInstanceId, exception);
             throw exception;
@@ -501,14 +502,14 @@ public class SupplierPriceApprovalProcessServiceImpl implements SupplierPriceApp
     }
 
     /**
-     * T100 使用数字标记供应商状态,避免将宜搭选项文本直接写入状态字段。
+     * T100 变更类型:正常供应商为 1,停用为 2。
      */
     private String toT100SupplierStatus(String status) {
         if ("正常供应商".equals(status)) {
             return "1";
         }
         if ("停用".equals(status)) {
-            return "0";
+            return "2";
         }
         throw new IllegalArgumentException("Unsupported supplier status: " + status);
     }
@@ -831,6 +832,18 @@ public class SupplierPriceApprovalProcessServiceImpl implements SupplierPriceApp
         return null;
     }
 
+    /**
+     * 包材材质核价、包材料号清单和供应商暂停/退出走 DingTalk To T100,其余单据仍走供应商价格审批服务名。
+     */
+    private String resolveServiceName(String formId) {
+        if (PACKAGING_MATERIAL_PRICE_APPROVAL_FORM_ID.equals(formId)
+                || PACKAGING_MATERIAL_ITEM_LIST_FORM_ID.equals(formId)
+                || SUPPLIER_SUSPEND_EXIT_FORM_ID.equals(formId)) {
+            return MingleiPortServiceImpl.DINGTALK_TO_T100_SERVICE_NAME;
+        }
+        return MingleiPortServiceImpl.SUPPLIER_PRICE_APPROVAL_SERVICE_NAME;
+    }
+
     private Map<?, ?> requireMap(Object value, String message) {
         Map<?, ?> map = asMap(value);
         if (map == null) {

+ 3 - 1
src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandler.java

@@ -30,7 +30,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(MiscReceiptIssueFormPushHandler.class);
     private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
-    public static final String TYPE = "cint301";
+    public static final String TYPE = "aint302";
     private static final String FORM_NAME = "miscReceiptIssue";
     private static final String FORM_UUID = "FORM-4B08ADABCDAA45788C043729A855E03ED76G";
     private static final String PROCESS_CODE = "TPROC--MQD66371C1N84FPSJZO4W79IN4BW2AE7I08TM0";
@@ -48,6 +48,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
     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";
+    private static final String DOC_NO_FIELD_ID = "textField_mvc41oom";
 
     private static final String DETAIL_TABLE_FIELD_ID = "tableField_msh186sv";
     private static final String DETAIL_SEQ_FIELD_ID = "textField_mt7zi4g1";
@@ -163,6 +164,7 @@ public class MiscReceiptIssueFormPushHandler implements YidaFormPushHandler {
         putIfNotBlank(formData, REASON_CODE_FIELD_ID, value(main, "inba007"));
         putIfNotBlank(formData, REASON_DESC_FIELD_ID, value(main, "oocql004"));
         putIfNotBlank(formData, REMARK_FIELD_ID, value(main, "inba008"));
+        putIfNotBlank(formData, DOC_NO_FIELD_ID, docNo);
 
         List<Map<String, Object>> rows = toDetailRows(detailRows);
         if (!rows.isEmpty()) {

+ 10 - 0
src/main/java/com/malk/minglei/service/port/MingleiPortService.java

@@ -25,4 +25,14 @@ public interface MingleiPortService {
      * @return 外部接口原始响应体
      */
     Map<String, Object> submitT100Document(Map<String, Object> parameter, String dataKeyType);
+
+    /**
+     * 将指定类型的 T100 单据参数提交至外部接口,并使用调用方指定的服务名。
+     *
+     * @param parameter T100 标准报文中的 parameter 内容
+     * @param dataKeyType T100 datakey.type 单据类型
+     * @param serviceName T100 service.name
+     * @return 外部接口原始响应体
+     */
+    Map<String, Object> submitT100Document(Map<String, Object> parameter, String dataKeyType, String serviceName);
 }

+ 2 - 0
src/main/resources/application-prod.yml

@@ -1,6 +1,8 @@
 minglei:
   port:
     supplier-price-approval-url: ${MINGLEI_SUPPLIER_PRICE_APPROVAL_URL:http://10.0.2.60/wstopprd/ws/r/awsp920}
+  reply-erp:
+    url: ${MINGLEI_REPLY_ERP_URL:http://10.0.2.60/wstopprd/ws/r/awsp920}
   erp-t100:
     request:
       service-id: topprd

+ 2 - 0
src/main/resources/application-test.yml

@@ -1,6 +1,8 @@
 minglei:
   port:
     supplier-price-approval-url: ${MINGLEI_SUPPLIER_PRICE_APPROVAL_URL:http://10.0.2.60/wtoptst/ws/r/awsp920}
+  reply-erp:
+    url: ${MINGLEI_REPLY_ERP_URL:http://10.0.2.60/wtoptst/ws/r/awsp920}
   erp-t100:
     request:
       service-id: toptst

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

@@ -13,6 +13,7 @@ spring:
   application:
     name: mjava-minglei
   profiles:
+#    active: ${SPRING_PROFILES_ACTIVE:test}
     active: ${SPRING_PROFILES_ACTIVE:prod}
     group:
       dev: test
@@ -42,7 +43,10 @@ minglei:
     enterprise-private-key: ${MINGLEI_BANKING_ENTERPRISE_PRIVATE_KEY:MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgffvSW7BkE9MSvfXIdEyAtSrecDzkECtwoaymGZjyQGugCgYIKoEcz1UBgi2hRANCAATjayFlLuOhGVerrwFTitrTbXUBd6URu2/PBJ4yMoGuwJ7vLliHkhSkp9xXnTpxF7T/MZUH3irBMALoRHXLaUFq}
     connect-timeout-millis: ${MINGLEI_BANKING_CONNECT_TIMEOUT_MILLIS:10000}
     read-timeout-millis: ${MINGLEI_BANKING_READ_TIMEOUT_MILLIS:30000}
+  port:
     supplier-price-approval-key: ${MINGLEI_SUPPLIER_PRICE_APPROVAL_KEY:f5458f5c0f9022db743a7c0710145903}
+  reply-erp:
+    key: ${MINGLEI_REPLY_ERP_KEY:f5458f5c0f9022db743a7c0710145903}
   erp-t100:
     request:
       service-id: toptst

+ 28 - 2
src/test/java/com/malk/minglei/controller/MingleiPortControllerTest.java

@@ -1,7 +1,9 @@
 package com.malk.minglei.controller;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.dto.MingleiPortRequest;
 import com.malk.minglei.service.port.MingleiPortService;
+import com.malk.minglei.service.port.SupplierPriceApprovalProcessService;
 import org.junit.jupiter.api.Test;
 import org.springframework.http.MediaType;
 import org.springframework.test.web.servlet.MockMvc;
@@ -10,6 +12,8 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 import java.util.Collections;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -27,7 +31,7 @@ class MingleiPortControllerTest {
         MingleiPortService service = mock(MingleiPortService.class);
         given(service.submitSupplierPriceApproval(any(MingleiPortRequest.class)))
                 .willReturn(Collections.<String, Object>singletonMap("srvcode", "000"));
-        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new MingleiPortController(service)).build();
+        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller(service, mock(SupplierPriceApprovalProcessService.class))).build();
 
         mockMvc.perform(post("/api/minglei/ports/supplier-price-approvals")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -41,7 +45,7 @@ class MingleiPortControllerTest {
     @Test
     void rejectsRequestWithoutRequiredHeadField() throws Exception {
         MingleiPortService service = mock(MingleiPortService.class);
-        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new MingleiPortController(service)).build();
+        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller(service, mock(SupplierPriceApprovalProcessService.class))).build();
 
         mockMvc.perform(post("/api/minglei/ports/supplier-price-approvals")
                         .contentType(MediaType.APPLICATION_JSON)
@@ -50,6 +54,28 @@ class MingleiPortControllerTest {
                 .andExpect(jsonPath("$.message").value("Request validation failed"));
     }
 
+    @Test
+    void submitsPackagingMaterialPriceApprovalFromYidaInstance() throws Exception {
+        MingleiPortService portService = mock(MingleiPortService.class);
+        SupplierPriceApprovalProcessService processService = mock(SupplierPriceApprovalProcessService.class);
+        given(processService.submitByInstanceIds(eq("FORM-69A12600F87349D3B653BF86E419224DLIX9"),
+                eq("form-instance-1"), isNull())).willReturn(Collections.<String, Object>singletonMap("srvcode", "000"));
+        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(controller(portService, processService)).build();
+
+        mockMvc.perform(post("/api/minglei/ports/supplier-price-approvals")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("{\"formId\":\"FORM-69A12600F87349D3B653BF86E419224DLIX9\",\"formInstanceId\":\"form-instance-1\"}"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.srvcode").value("000"));
+
+        verify(processService).submitByInstanceIds("FORM-69A12600F87349D3B653BF86E419224DLIX9", "form-instance-1", null);
+    }
+
+    private MingleiPortController controller(MingleiPortService portService,
+                                             SupplierPriceApprovalProcessService processService) {
+        return new MingleiPortController(portService, processService, new ObjectMapper());
+    }
+
     private String validRequestJson() {
         return "{\"head\":{\"pmdidocno\":\"CH3\",\"pmdi001\":\"N\",\"pmdi034\":\"605188\","
                 + "\"pmdidocdt\":\"2026-08-06\",\"pmdi004\":\"517022\",\"pmdi015\":\"2026-08-06\","

+ 28 - 0
src/test/java/com/malk/minglei/service/impl/employee/EmployeeRosterRepositoryTest.java

@@ -20,6 +20,7 @@ import java.time.LocalDate;
 import java.util.Collections;
 import java.util.Map;
 
+import com.malk.minglei.dto.EmployeeRosterInfo;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -116,6 +117,33 @@ class EmployeeRosterRepositoryTest {
         assertTrue(sqlCaptor.getValue().contains("bank_branch_code"));
     }
 
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void mapsBankCardNumberAndBankNameForRosterQuery() throws Exception {
+        JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+        ObjectProvider<JdbcTemplate> jdbcTemplateProvider = mock(ObjectProvider.class);
+        given(jdbcTemplateProvider.getIfAvailable()).willReturn(jdbcTemplate);
+        given(jdbcTemplate.query(any(String.class), any(PreparedStatementSetter.class), any(RowMapper.class)))
+                .willReturn(Collections.emptyList());
+        EmployeeRosterRepository repository = new EmployeeRosterRepository(
+                jdbcTemplateProvider,
+                new EmployeeRosterProperties());
+
+        repository.findByEmployeeNo("1001");
+
+        ArgumentCaptor<RowMapper> rowMapperCaptor = ArgumentCaptor.forClass(RowMapper.class);
+        verify(jdbcTemplate).query(any(String.class), any(PreparedStatementSetter.class), rowMapperCaptor.capture());
+        ResultSet resultSet = mock(ResultSet.class);
+        given(resultSet.getInt("id")).willReturn(1);
+        given(resultSet.getString("bank_card_no")).willReturn("6222000000000000");
+        given(resultSet.getString("bank_name")).willReturn("示例银行");
+
+        EmployeeRosterInfo result = (EmployeeRosterInfo) rowMapperCaptor.getValue().mapRow(resultSet, 0);
+
+        assertEquals("6222000000000000", result.getBankCardNo());
+        assertEquals("示例银行", result.getBankName());
+    }
+
     @Test
     @SuppressWarnings({"rawtypes", "unchecked"})
     void findByUsernameFiltersResignedEmployees() {

+ 16 - 0
src/test/java/com/malk/minglei/service/impl/port/MingleiPortServiceImplTest.java

@@ -10,6 +10,8 @@ import org.springframework.http.MediaType;
 import org.springframework.test.web.client.MockRestServiceServer;
 import org.springframework.web.client.RestTemplate;
 
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -50,6 +52,20 @@ class MingleiPortServiceImplTest {
         server.verify();
     }
 
+    @Test
+    void submitT100DocumentUsesDingTalkServiceName() {
+        server.expect(requestTo("http://external.example/wtoptst/ws/r/awsp920"))
+                .andExpect(method(org.springframework.http.HttpMethod.POST))
+                .andExpect(content().json("{\"service\":{\"name\":\"DingTalk To T100\"},\"datakey\":{\"type\":\"priceSheets\"}}", false))
+                .andRespond(withSuccess("{}", MediaType.APPLICATION_JSON));
+
+        Map<String, Object> parameter = new LinkedHashMap<String, Object>();
+        parameter.put("head", Collections.singletonMap("pmaaucent", "86"));
+        mingleiPortService.submitT100Document(parameter, "priceSheets", "DingTalk To T100");
+
+        server.verify();
+    }
+
     private MingleiPortRequest readRequest() throws Exception {
         return objectMapper.readValue(localRequestJson(), MingleiPortRequest.class);
     }

+ 44 - 4
src/test/java/com/malk/minglei/service/impl/port/SupplierPriceApprovalProcessServiceImplTest.java

@@ -57,12 +57,14 @@ class SupplierPriceApprovalProcessServiceImplTest {
         given(yidaFormInstanceClient.getFormInstance(PACKAGING_MATERIAL_ITEM_LIST_FORM_ID, "process-instance-1"))
                 .willReturn(processInstance());
         given(mingleiPortService.submitT100Document(org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
-                eq("partNumberListAudit"))).willReturn(Collections.<String, Object>singletonMap("srvcode", "100"));
+                eq("partNumberListAudit"), eq("DingTalk To T100")))
+                .willReturn(Collections.<String, Object>singletonMap("srvcode", "100"));
 
         service.submitByProcessInstanceId(PACKAGING_MATERIAL_ITEM_LIST_FORM_ID, "process-instance-1");
 
         ArgumentCaptor<Map<String, Object>> parameterCaptor = ArgumentCaptor.forClass(Map.class);
-        verify(mingleiPortService).submitT100Document(parameterCaptor.capture(), eq("partNumberListAudit"));
+        verify(mingleiPortService).submitT100Document(parameterCaptor.capture(), eq("partNumberListAudit"),
+                eq("DingTalk To T100"));
         Map<?, ?> head = (Map<?, ?>) parameterCaptor.getValue().get("head");
         List<?> details = (List<?>) head.get("detail");
         Map<?, ?> detail = (Map<?, ?>) details.get(0);
@@ -192,17 +194,55 @@ class SupplierPriceApprovalProcessServiceImplTest {
         instance.put("formData", formData);
         given(yidaFormInstanceClient.getFormInstance(formId, "form-instance-001")).willReturn(instance);
         given(mingleiPortService.submitT100Document(org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
-                eq("suspensionExit"))).willReturn(Collections.<String, Object>singletonMap("srvcode", "000"));
+                eq("suspensionExit"), eq("DingTalk To T100")))
+                .willReturn(Collections.<String, Object>singletonMap("srvcode", "000"));
 
         service.submitByInstanceIds(formId, "form-instance-001", null);
 
         ArgumentCaptor<Map<String, Object>> parameterCaptor = ArgumentCaptor.forClass(Map.class);
-        verify(mingleiPortService).submitT100Document(parameterCaptor.capture(), eq("suspensionExit"));
+        verify(mingleiPortService).submitT100Document(parameterCaptor.capture(), eq("suspensionExit"),
+                eq("DingTalk To T100"));
         Map<?, ?> head = (Map<?, ?>) parameterCaptor.getValue().get("head");
+        Map<?, ?> detail = (Map<?, ?>) parameterCaptor.getValue().get("detail");
         assertEquals("{supplier-code", head.get("pmaa001"));
+        assertEquals("1", head.get("pmaa083"));
+        assertEquals("1", detail.get("pmab074"));
         verifyNoInteractions(yidaProcessOpenApiClient);
     }
 
+    @Test
+    void mapsSuspendedSupplierStatusToTwo() {
+        String formId = "FORM-0A05D30C6E0745EC9F4B74D0E038EE73O7B6";
+        YidaFormInstanceClient yidaFormInstanceClient = mock(YidaFormInstanceClient.class);
+        MingleiPortService mingleiPortService = mock(MingleiPortService.class);
+        SupplierPriceApprovalProcessServiceImpl service = new SupplierPriceApprovalProcessServiceImpl(
+                yidaFormInstanceClient, mock(YidaProcessOpenApiClient.class), applications(), mingleiPortService,
+                mock(EmployeeRosterService.class), new ObjectMapper());
+
+        Map<String, Object> formData = new LinkedHashMap<String, Object>();
+        formData.put("selectField_mt82p09c", "停用");
+        formData.put("textField_mvgepmw0", "SUP-001");
+        formData.put("textField_mvewj97z", "明磊");
+        formData.put("textField_mvewj980", "ML");
+        Map<String, Object> instance = new LinkedHashMap<String, Object>();
+        instance.put("formUuid", formId);
+        instance.put("formData", formData);
+        given(yidaFormInstanceClient.getFormInstance(formId, "form-instance-002")).willReturn(instance);
+        given(mingleiPortService.submitT100Document(org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
+                eq("suspensionExit"), eq("DingTalk To T100")))
+                .willReturn(Collections.<String, Object>singletonMap("srvcode", "000"));
+
+        service.submitByInstanceIds(formId, "form-instance-002", null);
+
+        ArgumentCaptor<Map<String, Object>> parameterCaptor = ArgumentCaptor.forClass(Map.class);
+        verify(mingleiPortService).submitT100Document(parameterCaptor.capture(), eq("suspensionExit"),
+                eq("DingTalk To T100"));
+        Map<?, ?> head = (Map<?, ?>) parameterCaptor.getValue().get("head");
+        Map<?, ?> detail = (Map<?, ?>) parameterCaptor.getValue().get("detail");
+        assertEquals("2", head.get("pmaa083"));
+        assertEquals("2", detail.get("pmab074"));
+    }
+
     /**
      * T100 实际直返 payload 时,执行状态码为 0 也必须触发宜搭单号回写。
      */

+ 1 - 0
src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandlerTest.java

@@ -110,6 +110,7 @@ class MiscReceiptIssueFormPushHandlerTest {
         assertEquals("RSN-001", formData.getString("textField_mt80nq6t"));
         assertEquals("reason-desc-main", formData.getString("textField_mt80nq6v"));
         assertEquals("remark-001", formData.getString("textField_mt7zi4fz"));
+        assertEquals("MS-001", formData.getString("textField_mvc41oom"));
 
         JSONArray detailRows = formData.getJSONArray("tableField_msh186sv");
         assertEquals(1, detailRows.size());