Quellcode durchsuchen

接口开发10.30

ht vor 6 Tagen
Ursprung
Commit
0005c8062f
35 geänderte Dateien mit 271 neuen und 2954 gelöschten Zeilen
  1. 63 41
      ERP-YIDA-PUSH-API.md
  2. 92 875
      README.md
  3. 0 22
      pom.xml
  4. 0 15
      src/main/java/com/malk/minglei/config/EmployeeRosterConfiguration.java
  5. 2 8
      src/main/java/com/malk/minglei/config/HttpRequestResponseLoggingFilter.java
  6. 0 10
      src/main/java/com/malk/minglei/config/YidaApplicationsProperties.java
  7. 0 16
      src/main/java/com/malk/minglei/controller/EmployeeRosterController.java
  8. 2 37
      src/main/java/com/malk/minglei/controller/GlobalExceptionHandler.java
  9. 11 27
      src/main/java/com/malk/minglei/controller/MingleiPortController.java
  10. 5 30
      src/main/java/com/malk/minglei/controller/YidaProcessController.java
  11. 0 10
      src/main/java/com/malk/minglei/dto/DepartmentUserQueryRequest.java
  12. 0 50
      src/main/java/com/malk/minglei/dto/EmployeeRosterInfo.java
  13. 0 28
      src/main/java/com/malk/minglei/service/employee/EmployeeRosterService.java
  14. 0 57
      src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRecord.java
  15. 6 108
      src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRepository.java
  16. 1 594
      src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImpl.java
  17. 21 94
      src/main/java/com/malk/minglei/service/impl/port/MingleiPortServiceImpl.java
  18. 3 179
      src/main/java/com/malk/minglei/service/impl/yida/DingTalkYidaProcessOpenApiClient.java
  19. 2 7
      src/main/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImpl.java
  20. 3 94
      src/main/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImpl.java
  21. 0 2
      src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandler.java
  22. 31 48
      src/main/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandler.java
  23. 0 9
      src/main/java/com/malk/minglei/service/port/MingleiPortService.java
  24. 0 8
      src/main/java/com/malk/minglei/service/yida/MingleiDocumentService.java
  25. 0 8
      src/main/java/com/malk/minglei/service/yida/YidaProcessOpenApiClient.java
  26. 0 31
      src/main/resources/application.yml
  27. 0 6
      src/main/resources/db/employee_roster.sql
  28. 0 41
      src/test/java/com/malk/minglei/controller/EmployeeRosterControllerTest.java
  29. 1 58
      src/test/java/com/malk/minglei/controller/YidaProcessControllerTest.java
  30. 8 317
      src/test/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImplTest.java
  31. 14 37
      src/test/java/com/malk/minglei/service/impl/port/MingleiPortServiceImplTest.java
  32. 0 32
      src/test/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImplTest.java
  33. 0 35
      src/test/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImplTest.java
  34. 0 1
      src/test/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandlerTest.java
  35. 6 19
      src/test/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandlerTest.java

Datei-Diff unterdrückt, da er zu groß ist
+ 63 - 41
ERP-YIDA-PUSH-API.md


Datei-Diff unterdrückt, da er zu groß ist
+ 92 - 875
README.md


+ 0 - 22
pom.xml

@@ -62,33 +62,11 @@
             <version>8.0.33</version>
             <scope>runtime</scope>
         </dependency>
-        <dependency>
-            <groupId>com.oracle.database.jdbc</groupId>
-            <artifactId>ojdbc8</artifactId>
-            <version>19.3.0.0</version>
-            <scope>runtime</scope>
-        </dependency>
         <dependency>
             <groupId>com.alibaba</groupId>
             <artifactId>fastjson</artifactId>
             <version>1.2.83</version>
         </dependency>
-        <!-- mjava UtilToken / 钉钉 token 缓存依赖;未打入包会导致 NoClassDefFoundError: CacheUtil -->
-        <dependency>
-            <groupId>cn.hutool</groupId>
-            <artifactId>hutool-all</artifactId>
-            <version>5.8.25</version>
-        </dependency>
-        <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk15on</artifactId>
-            <version>1.70</version>
-        </dependency>
-        <!-- mjava UtilToken.buildKey 依赖;未打入包会导致 NoClassDefFoundError: StringUtils -->
-        <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-lang3</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>

+ 0 - 15
src/main/java/com/malk/minglei/config/EmployeeRosterConfiguration.java

@@ -7,9 +7,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.jdbc.datasource.DataSourceTransactionManager;
 import org.springframework.jdbc.datasource.DriverManagerDataSource;
-import org.springframework.transaction.PlatformTransactionManager;
 
 import javax.sql.DataSource;
 
@@ -56,19 +54,6 @@ public class EmployeeRosterConfiguration {
         return new JdbcTemplate(dataSource);
     }
 
-    /**
-     * 为工号日序列预留和花名册写入提供同一数据源事务,避免并发创建时生成重复工号。
-     *
-     * @param dataSource 员工花名册数据源
-     * @return 员工花名册事务管理器
-     */
-    @Bean("employeeRosterTransactionManager")
-    @ConditionalOnBean(name = "employeeRosterDataSource")
-    public PlatformTransactionManager employeeRosterTransactionManager(
-            @Qualifier("employeeRosterDataSource") DataSource dataSource) {
-        return new DataSourceTransactionManager(dataSource);
-    }
-
     private boolean hasText(String value) {
         return value != null && !value.trim().isEmpty();
     }

+ 2 - 8
src/main/java/com/malk/minglei/config/HttpRequestResponseLoggingFilter.java

@@ -24,13 +24,12 @@ public class HttpRequestResponseLoggingFilter extends OncePerRequestFilter {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(HttpRequestResponseLoggingFilter.class);
 
-    private static final String DICTIONARY_API_PATH_PREFIX = "/api/dictionary/";
     private static final int MAX_PAYLOAD_LENGTH = 10 * 1024;
     private static final Pattern SENSITIVE_JSON_FIELD = Pattern.compile(
-            "(\\\"(?:password|secret|token|authorization|accessToken|systemToken|key|idCardNo|id_card_no)\\\"\\s*:\\s*\\\")[^\\\"]*(\\\")",
+            "(\\\"(?:password|secret|token|authorization|accessToken|systemToken|key)\\\"\\s*:\\s*\\\")[^\\\"]*(\\\")",
             Pattern.CASE_INSENSITIVE);
     private static final Pattern SENSITIVE_QUERY_PARAMETER = Pattern.compile(
-            "((?:password|secret|token|authorization|accessToken|systemToken|key|idCardNo|id_card_no)=)[^&]*",
+            "((?:password|secret|token|authorization|accessToken|systemToken|key)=)[^&]*",
             Pattern.CASE_INSENSITIVE);
 
     /**
@@ -41,11 +40,6 @@ public class HttpRequestResponseLoggingFilter extends OncePerRequestFilter {
      */
     @Override
     protected boolean shouldNotFilter(HttpServletRequest request) {
-        // 数据字典可能返回大量记录,暂时关闭该接口的请求和响应日志输出。
-        if (request.getRequestURI().startsWith(DICTIONARY_API_PATH_PREFIX)) {
-            return true;
-        }
-
         String contentType = request.getContentType();
         return contentType != null && (contentType.startsWith("multipart/")
                 || contentType.startsWith("application/octet-stream"));

+ 0 - 10
src/main/java/com/malk/minglei/config/YidaApplicationsProperties.java

@@ -34,7 +34,6 @@ public class YidaApplicationsProperties {
 
         private String appType;
         private String systemToken;
-        private String processInstanceQueryUserId = "41603502201288023";
 
         public String getAppType() {
             return appType;
@@ -51,14 +50,5 @@ public class YidaApplicationsProperties {
         public void setSystemToken(String systemToken) {
             this.systemToken = systemToken;
         }
-
-        public String getProcessInstanceQueryUserId() {
-            return processInstanceQueryUserId;
-        }
-
-        public void setProcessInstanceQueryUserId(String processInstanceQueryUserId) {
-            this.processInstanceQueryUserId = processInstanceQueryUserId;
-        }
-
     }
 }

+ 0 - 16
src/main/java/com/malk/minglei/controller/EmployeeRosterController.java

@@ -1,14 +1,11 @@
 package com.malk.minglei.controller;
 
 import com.malk.minglei.dto.ApiResponse;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateRequest;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateResult;
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterQueryRequest;
 import com.malk.minglei.dto.EmployeeRosterSyncResult;
 import com.malk.minglei.service.employee.EmployeeRosterService;
 import org.springframework.http.ResponseEntity;
-import org.springframework.http.HttpStatus;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
@@ -39,19 +36,6 @@ public class EmployeeRosterController {
         this.employeeRosterService = employeeRosterService;
     }
 
-    /**
-     * 创建钉钉自建企业账号。
-     *
-     * @param request 已完成字段校验的账号创建请求
-     * @return 钉钉返回的用户 ID 和实际提交的部门 ID 列表
-     */
-    @PostMapping("/dingtalk-dedicated-accounts")
-    public ResponseEntity<ApiResponse<DingTalkDedicatedAccountCreateResult>> createDingTalkDedicatedAccount(
-            @Valid @RequestBody DingTalkDedicatedAccountCreateRequest request) {
-        return ResponseEntity.status(HttpStatus.CREATED)
-                .body(ApiResponse.success(employeeRosterService.createDingTalkDedicatedAccount(request)));
-    }
-
     /**
      * 按指定部门同步钉钉员工花名册。
      *

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

@@ -10,8 +10,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.RestControllerAdvice;
 
-import java.io.PrintWriter;
-import java.io.StringWriter;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -83,47 +81,14 @@ public class GlobalExceptionHandler {
      */
     @ExceptionHandler(Exception.class)
     public ResponseEntity<Map<String, String>> handleUnexpected(Exception exception) {
-        Throwable rootCause = rootCause(exception);
-        String exceptionMessage = sanitizeExceptionMessage(exception.getMessage());
-        String rootCauseMessage = sanitizeExceptionMessage(rootCause.getMessage());
-        LOGGER.error("Yida client request failed exceptionType={} exceptionMessage={} rootCauseType={} rootCauseMessage={} stackTrace={}",
-                exception.getClass().getName(), exceptionMessage, rootCause.getClass().getName(), rootCauseMessage,
-                sanitizeStackTrace(exception));
+        LOGGER.error("Yida client request failed", exception);
 
         Map<String, String> response = new LinkedHashMap<String, String>();
         response.put("code", "YIDA_CALL_FAILED");
-        response.put("message", "Yida client request failed: " + exception.getClass().getSimpleName()
-                + ": " + defaultText(exceptionMessage, "no message"));
-        response.put("exceptionType", exception.getClass().getName());
-        response.put("rootCause", rootCause.getClass().getName() + ": "
-                + defaultText(rootCauseMessage, "no message"));
+        response.put("message", "Yida client request failed. Check the application log for details.");
         return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(response);
     }
 
-    private Throwable rootCause(Throwable exception) {
-        Throwable current = exception;
-        while (current.getCause() != null && current.getCause() != current) {
-            current = current.getCause();
-        }
-        return current;
-    }
-
-    private String sanitizeExceptionMessage(String message) {
-        if (message == null) {
-            return null;
-        }
-        String sanitized = message
-                .replaceAll("(?i)(systemToken=)[^&\\s]+", "$1******")
-                .replaceAll("(?i)(x-acs-dingtalk-access-token[:=])[^\\s,]+", "$1******");
-        return sanitized.length() <= 2048 ? sanitized : sanitized.substring(0, 2048) + "...<truncated>";
-    }
-
-    private String sanitizeStackTrace(Throwable exception) {
-        StringWriter stackTrace = new StringWriter();
-        exception.printStackTrace(new PrintWriter(stackTrace));
-        return sanitizeExceptionMessage(stackTrace.toString());
-    }
-
     private String defaultText(String value, String fallback) {
         return value == null || value.trim().isEmpty() ? fallback : value.trim();
     }

+ 11 - 27
src/main/java/com/malk/minglei/controller/MingleiPortController.java

@@ -1,13 +1,12 @@
 package com.malk.minglei.controller;
 
-import com.malk.minglei.dto.SupplierPriceApprovalProcessRequest;
-import com.malk.minglei.service.port.SupplierPriceApprovalProcessService;
+import com.malk.minglei.dto.MingleiPortRequest;
+import com.malk.minglei.service.port.MingleiPortService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.validation.FieldError;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -15,7 +14,6 @@ import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.client.RestClientException;
-import org.springframework.util.StringUtils;
 
 import javax.validation.Valid;
 import java.util.Collections;
@@ -31,33 +29,26 @@ public class MingleiPortController {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(MingleiPortController.class);
 
-    private final SupplierPriceApprovalProcessService supplierPriceApprovalProcessService;
+    private final MingleiPortService mingleiPortService;
 
     /**
      * 创建明磊外部单据接口控制器。
      *
-     * @param supplierPriceApprovalProcessService 核价流程读取与 T100 推送服务
+     * @param mingleiPortService 明磊外部单据接口服务
      */
-    public MingleiPortController(SupplierPriceApprovalProcessService supplierPriceApprovalProcessService) {
-        this.supplierPriceApprovalProcessService = supplierPriceApprovalProcessService;
+    public MingleiPortController(MingleiPortService mingleiPortService) {
+        this.mingleiPortService = mingleiPortService;
     }
 
     /**
-     * 接收宜搭表单和流程实例 ID,按表单字段映射读取数据并调用外部 T100 接口。
+     * 接收本系统的供应商价格审批单请求,并调用外部 T100 接口。
      *
-     * @param request 包含表单 ID 和流程实例 ID 的请求体
+     * @param request 仅包含 head 和 detail 的供应商价格审批单请求体
      * @return 外部 T100 接口响应内容
      */
     @PostMapping("/supplier-price-approvals")
-    public ResponseEntity<Map<String, Object>> submitSupplierPriceApproval(
-            @Valid @RequestBody SupplierPriceApprovalProcessRequest request) {
-        if (request.requiresProcessInstanceId() && !StringUtils.hasText(request.getProcessInstanceId())) {
-            return ResponseEntity.badRequest().body(Collections.<String, Object>singletonMap("message",
-                    "Request validation failed: processInstanceId must not be blank for supplier price approval forms"));
-        }
-        return ResponseEntity.ok(supplierPriceApprovalProcessService
-                .submitByInstanceIds(request.getFormId(), request.getFormInstanceId(),
-                        request.getProcessInstanceId()));
+    public ResponseEntity<Map<String, Object>> submitSupplierPriceApproval(@Valid @RequestBody MingleiPortRequest request) {
+        return ResponseEntity.ok(mingleiPortService.submitSupplierPriceApproval(request));
     }
 
     /**
@@ -68,14 +59,7 @@ public class MingleiPortController {
      */
     @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
     public ResponseEntity<Map<String, String>> handleInvalidRequest(Exception exception) {
-        if (exception instanceof MethodArgumentNotValidException) {
-            FieldError fieldError = ((MethodArgumentNotValidException) exception).getBindingResult().getFieldError();
-            if (fieldError != null) {
-                return ResponseEntity.badRequest().body(Collections.singletonMap("message",
-                        "Request validation failed: " + fieldError.getField() + " " + fieldError.getDefaultMessage()));
-            }
-        }
-        return ResponseEntity.badRequest().body(Collections.singletonMap("message", "Request body is invalid"));
+        return ResponseEntity.badRequest().body(Collections.singletonMap("message", "Request validation failed"));
     }
 
     /**

+ 5 - 30
src/main/java/com/malk/minglei/controller/YidaProcessController.java

@@ -6,7 +6,6 @@ import com.malk.minglei.dto.DepartmentUserIdsResponse;
 import com.malk.minglei.dto.ApiResponse;
 import com.malk.minglei.dto.YidaProcessStartRequest;
 import com.malk.minglei.service.yida.MingleiDocumentService;
-import com.malk.minglei.service.yida.YidaProcessInstanceClient;
 import com.malk.minglei.service.yida.YidaProcessService;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
@@ -14,7 +13,6 @@ import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.MethodArgumentNotValidException;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -24,15 +22,12 @@ import org.springframework.web.bind.annotation.RestController;
 import javax.validation.constraints.Max;
 import javax.validation.constraints.Min;
 import javax.validation.Valid;
-import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 /**
- * 宜搭流程发起、实例查询、单据查询和部门成员查询接口。
+ * 宜搭流程发起、单据查询和部门成员查询接口。
  */
 @Validated
 @RestController
@@ -41,20 +36,16 @@ public class YidaProcessController {
 
     private final YidaProcessService yidaProcessService;
     private final MingleiDocumentService mingleiDocumentService;
-    private final YidaProcessInstanceClient yidaProcessInstanceClient;
+
     /**
      * 创建宜搭业务接口控制器。
      *
      * @param yidaProcessService 宜搭流程服务
      * @param mingleiDocumentService 明磊单据查询服务
-     * @param yidaProcessInstanceClient 宜搭流程实例查询客户端
      */
-    public YidaProcessController(YidaProcessService yidaProcessService,
-                                 MingleiDocumentService mingleiDocumentService,
-                                 YidaProcessInstanceClient yidaProcessInstanceClient) {
+    public YidaProcessController(YidaProcessService yidaProcessService, MingleiDocumentService mingleiDocumentService) {
         this.yidaProcessService = yidaProcessService;
         this.mingleiDocumentService = mingleiDocumentService;
-        this.yidaProcessInstanceClient = yidaProcessInstanceClient;
     }
 
 
@@ -71,18 +62,6 @@ public class YidaProcessController {
                 .body(ApiResponse.success(Collections.singletonMap("processInstanceId", processInstanceId)));
     }
 
-    /**
-     * 按宜搭流程实例 ID 查询实例原始数据,仅用于排查流程数据和字段映射问题,不会触发 ERP 推送。
-     *
-     * @param processInstanceId 宜搭流程实例 ID
-     * @return 钉钉宜搭接口返回的流程实例数据
-     */
-    @GetMapping("/process-instances/{processInstanceId}")
-    public ResponseEntity<Map<String, Object>> getProcessInstance(
-            @PathVariable String processInstanceId) {
-        return ResponseEntity.ok(yidaProcessInstanceClient.getProcessInstance(processInstanceId));
-    }
-
     /**
      * 返回流程类型不支持等本地请求参数错误。
      *
@@ -126,12 +105,8 @@ public class YidaProcessController {
      */
     @PostMapping("/minglei/departments/users")
     public ResponseEntity<DepartmentUserIdsResponse> listDepartmentUserIds(@Valid @RequestBody DepartmentUserQueryRequest request) {
-        Set<String> userIds = new LinkedHashSet<String>(
-                mingleiDocumentService.listDepartmentUserIds(request.getDepartmentIds(), request.getType()));
-        if (request.getUserIds() != null && !request.getUserIds().isEmpty()) {
-            userIds.addAll(request.getUserIds());
-        }
-        return ResponseEntity.ok(DepartmentUserIdsResponse.success(new ArrayList<String>(userIds)));
+        List<String> userIds = mingleiDocumentService.listDepartmentUserIds(request.getDepartmentIds(), request.getType());
+        return ResponseEntity.ok(DepartmentUserIdsResponse.success(userIds));
     }
 
     /**

+ 0 - 10
src/main/java/com/malk/minglei/dto/DepartmentUserQueryRequest.java

@@ -25,8 +25,6 @@ public class DepartmentUserQueryRequest {
     @Max(value = 1, message = "type must be 0 or 1")
     private Integer type;
 
-    private List<String> userIds;
-
     public List<Long> getDepartmentIds() {
         List<Long> departmentIds = new ArrayList<Long>();
         for (String value : departmentId.split(",")) {
@@ -56,12 +54,4 @@ public class DepartmentUserQueryRequest {
     public void setType(Integer type) {
         this.type = type;
     }
-
-    public List<String> getUserIds() {
-        return userIds;
-    }
-
-    public void setUserIds(List<String> userIds) {
-        this.userIds = userIds;
-    }
 }

+ 0 - 50
src/main/java/com/malk/minglei/dto/EmployeeRosterInfo.java

@@ -1,7 +1,5 @@
 package com.malk.minglei.dto;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-
 /**
  * 员工花名册查询结果。
  */
@@ -17,11 +15,6 @@ public class EmployeeRosterInfo {
     private String jobGradeId;
     private String department;
     private String firstDepartment;
-    private String hireDate;
-    private String positionName;
-    private String bankCardNo;
-    private String bankName;
-    private String bankBranchCode;
 
     public Integer getId() {
         return id;
@@ -102,47 +95,4 @@ public class EmployeeRosterInfo {
     public void setFirstDepartment(String firstDepartment) {
         this.firstDepartment = firstDepartment;
     }
-
-    public String getHireDate() {
-        return hireDate;
-    }
-
-    public void setHireDate(String hireDate) {
-        this.hireDate = hireDate;
-    }
-
-    public String getPositionName() {
-        return positionName;
-    }
-
-    public void setPositionName(String positionName) {
-        this.positionName = positionName;
-    }
-
-    @JsonProperty("bank_card_no")
-    public String getBankCardNo() {
-        return bankCardNo;
-    }
-
-    public void setBankCardNo(String bankCardNo) {
-        this.bankCardNo = bankCardNo;
-    }
-
-    @JsonProperty("bank_name")
-    public String getBankName() {
-        return bankName;
-    }
-
-    public void setBankName(String bankName) {
-        this.bankName = bankName;
-    }
-
-    @JsonProperty("bank_branch_code")
-    public String getBankBranchCode() {
-        return bankBranchCode;
-    }
-
-    public void setBankBranchCode(String bankBranchCode) {
-        this.bankBranchCode = bankBranchCode;
-    }
 }

+ 0 - 28
src/main/java/com/malk/minglei/service/employee/EmployeeRosterService.java

@@ -2,24 +2,12 @@ package com.malk.minglei.service.employee;
 
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterSyncResult;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateRequest;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateResult;
-
-import java.util.List;
 
 /**
  * 员工花名册同步和查询服务。
  */
 public interface EmployeeRosterService {
 
-    /**
-     * 创建钉钉自建企业账号。
-     *
-     * @param request 账号创建请求
-     * @return 钉钉创建结果
-     */
-    DingTalkDedicatedAccountCreateResult createDingTalkDedicatedAccount(DingTalkDedicatedAccountCreateRequest request);
-
     /**
      * 按指定部门和查询范围同步员工花名册。
      *
@@ -51,20 +39,4 @@ public interface EmployeeRosterService {
      * @return 员工花名册信息,不存在时返回 null
      */
     EmployeeRosterInfo findByEmployeeNo(String employeeNo);
-
-    /**
-     * 根据员工姓名查询在职员工花名册。
-     *
-     * @param username 员工姓名
-     * @return 员工花名册信息,不存在时返回 null
-     */
-    EmployeeRosterInfo findByUsername(String username);
-
-    /**
-     * 将钉钉部门 ID、部门名称或层级部门路径解析为钉钉部门 ID。
-     *
-     * @param departmentReferences 部门引用列表,支持数字 ID、名称和以 - 或 / 分隔的层级路径
-     * @return 去重后的钉钉部门 ID 列表
-     */
-    List<Long> resolveDingTalkDepartmentIds(List<Object> departmentReferences);
 }

+ 0 - 57
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRecord.java

@@ -14,15 +14,6 @@ public class EmployeeRosterRecord {
     private String jobGradeId;
     private String department;
     private String firstDepartment;
-    private String hireDate;
-    private String positionName;
-    private String bankCardNo;
-    private String bankName;
-    private String bankBranchCode;
-    /**
-     * 数据库人员状态:1 表示在职,0 表示离职。
-     */
-    private int isResigned = 1;
 
     public String getDingtalkId() {
         return dingtalkId;
@@ -95,52 +86,4 @@ public class EmployeeRosterRecord {
     public void setFirstDepartment(String firstDepartment) {
         this.firstDepartment = firstDepartment;
     }
-
-    public String getHireDate() {
-        return hireDate;
-    }
-
-    public void setHireDate(String hireDate) {
-        this.hireDate = hireDate;
-    }
-
-    public String getPositionName() {
-        return positionName;
-    }
-
-    public void setPositionName(String positionName) {
-        this.positionName = positionName;
-    }
-
-    public String getBankCardNo() {
-        return bankCardNo;
-    }
-
-    public void setBankCardNo(String bankCardNo) {
-        this.bankCardNo = bankCardNo;
-    }
-
-    public String getBankName() {
-        return bankName;
-    }
-
-    public void setBankName(String bankName) {
-        this.bankName = bankName;
-    }
-
-    public String getBankBranchCode() {
-        return bankBranchCode;
-    }
-
-    public void setBankBranchCode(String bankBranchCode) {
-        this.bankBranchCode = bankBranchCode;
-    }
-
-    public int getIsResigned() {
-        return isResigned;
-    }
-
-    public void setIsResigned(int isResigned) {
-        this.isResigned = isResigned;
-    }
 }

+ 6 - 108
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRepository.java

@@ -10,11 +10,7 @@ import org.springframework.stereotype.Repository;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 
 /**
  * 员工花名册本地数据库访问组件。
@@ -22,9 +18,6 @@ import java.util.Map;
 @Repository
 public class EmployeeRosterRepository {
 
-    private static final DateTimeFormatter EMPLOYEE_NO_DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd");
-    private static final int MAX_DAILY_EMPLOYEE_SEQUENCE = 9999;
-
     private final ObjectProvider<JdbcTemplate> jdbcTemplateProvider;
     private final EmployeeRosterProperties properties;
 
@@ -59,26 +52,6 @@ public class EmployeeRosterRepository {
         return records.size();
     }
 
-    /**
-     * 从员工花名册中锁定当日工号范围并分配下一位工号。工号记录与创建账号操作处于同一事务,
-     * 避免依赖额外流水表,并确保并发请求不会读取到相同的当日最大工号。
-     *
-     * @param date 工号所属日期
-     * @return 格式为 YYMMDD 加四位流水号的员工工号
-     */
-    public String reserveNextEmployeeNo(LocalDate date) {
-        if (date == null) {
-            throw new IllegalArgumentException("date must not be null");
-        }
-        JdbcTemplate jdbcTemplate = requireJdbcTemplate();
-        String datePrefix = EMPLOYEE_NO_DATE_FORMAT.format(date);
-        Integer sequence = resolveNextDailySequence(jdbcTemplate, datePrefix);
-        if (sequence.intValue() < 1 || sequence.intValue() > MAX_DAILY_EMPLOYEE_SEQUENCE) {
-            throw new IllegalStateException("Daily employee number sequence is exhausted for " + datePrefix);
-        }
-        return datePrefix + String.format("%04d", sequence);
-    }
-
     /**
      * 根据钉钉用户 ID 查询员工花名册。
      *
@@ -99,73 +72,15 @@ public class EmployeeRosterRepository {
         return findOne("employee_no", employeeNo);
     }
 
-    /**
-     * 根据员工姓名查询在职员工的工号。
-     *
-     * <p>供应商价格审批仅需工号写入 T100,避免依赖花名册中与该场景无关的扩展列。
-     *
-     * @param username 员工姓名
-     * @return 员工花名册信息,不存在时返回 null
-     */
-    public EmployeeRosterInfo findByUsername(String username) {
-        JdbcTemplate jdbcTemplate = requireJdbcTemplate();
-        String sql = "SELECT employee_no FROM " + resolveTableName()
-                + " WHERE username = ? AND is_resigned = 1 LIMIT 1";
-        List<EmployeeRosterInfo> rows = jdbcTemplate.query(sql, preparedStatement -> preparedStatement.setString(1, username),
-                (resultSet, rowNum) -> {
-                    EmployeeRosterInfo info = new EmployeeRosterInfo();
-                    info.setEmployeeNo(resultSet.getString("employee_no"));
-                    return info;
-                });
-        return rows.isEmpty() ? null : rows.get(0);
-    }
-
     private EmployeeRosterInfo findOne(String columnName, String value) {
         JdbcTemplate jdbcTemplate = requireJdbcTemplate();
-        String sql = "SELECT id, dingtalk_id, employee_no, username, job_title, job_title_id, job_grade, job_grade_id, department, first_department, hire_date, position_name, bank_card_no, bank_name, bank_branch_code " +
-                "FROM " + resolveTableName() + " WHERE " + columnName + " = ? AND is_resigned = 1 LIMIT 1";
+        String sql = "SELECT id, dingtalk_id, employee_no, username, job_title, job_title_id, job_grade, job_grade_id, department, first_department " +
+                "FROM " + resolveTableName() + " WHERE " + columnName + " = ? LIMIT 1";
         List<EmployeeRosterInfo> rows = jdbcTemplate.query(sql, preparedStatement -> preparedStatement.setString(1, value),
                 (resultSet, rowNum) -> mapRow(resultSet));
         return rows.isEmpty() ? null : rows.get(0);
     }
 
-    private Integer resolveNextDailySequence(JdbcTemplate jdbcTemplate, String datePrefix) {
-        String maximumEmployeeNo = jdbcTemplate.query(
-                "SELECT employee_no FROM " + resolveTableName() +
-                        " WHERE employee_no >= ? AND employee_no < ? AND employee_no REGEXP ?" +
-                        " ORDER BY employee_no DESC LIMIT 1 FOR UPDATE",
-                preparedStatement -> {
-                    preparedStatement.setString(1, datePrefix);
-                    preparedStatement.setString(2, datePrefix + ":");
-                    preparedStatement.setString(3, "^" + datePrefix + "[0-9]{4}$");
-                },
-                resultSet -> resultSet.next() ? resultSet.getString("employee_no") : null);
-        if (maximumEmployeeNo == null) {
-            return 1;
-        }
-        int lastSequence = Integer.parseInt(maximumEmployeeNo.substring(datePrefix.length()));
-        return lastSequence + 1;
-    }
-
-    /**
-     * 查询所有在职重名人员及其人数,用于同步后通知人事核对。
-     *
-     * @return 姓名到同名在职人数的映射,按姓名升序排列
-     */
-    public Map<String, Integer> findActiveDuplicateUsernameCounts() {
-        JdbcTemplate jdbcTemplate = requireJdbcTemplate();
-        String sql = "SELECT username, COUNT(*) AS duplicate_count FROM " + resolveTableName() +
-                " WHERE is_resigned = 1 AND username IS NOT NULL AND TRIM(username) <> '' " +
-                "GROUP BY username HAVING COUNT(*) > 1 ORDER BY username";
-        return jdbcTemplate.query(sql, resultSet -> {
-            Map<String, Integer> duplicateCounts = new LinkedHashMap<String, Integer>();
-            while (resultSet.next()) {
-                duplicateCounts.put(resultSet.getString("username"), Integer.valueOf(resultSet.getInt("duplicate_count")));
-            }
-            return duplicateCounts;
-        });
-    }
-
     private EmployeeRosterInfo mapRow(ResultSet resultSet) throws SQLException {
         EmployeeRosterInfo info = new EmployeeRosterInfo();
         info.setId(Integer.valueOf(resultSet.getInt("id")));
@@ -178,11 +93,6 @@ public class EmployeeRosterRepository {
         info.setJobGradeId(resultSet.getString("job_grade_id"));
         info.setDepartment(resultSet.getString("department"));
         info.setFirstDepartment(resultSet.getString("first_department"));
-        info.setHireDate(resultSet.getString("hire_date"));
-        info.setPositionName(resultSet.getString("position_name"));
-        info.setBankCardNo(resultSet.getString("bank_card_no"));
-        info.setBankName(resultSet.getString("bank_name"));
-        info.setBankBranchCode(resultSet.getString("bank_branch_code"));
         return info;
     }
 
@@ -196,13 +106,7 @@ public class EmployeeRosterRepository {
         statement.setString(index++, record.getJobGrade());
         statement.setString(index++, record.getJobGradeId());
         statement.setString(index++, record.getDepartment());
-        statement.setString(index++, record.getFirstDepartment());
-        statement.setString(index++, record.getHireDate());
-        statement.setString(index++, record.getPositionName());
-        statement.setString(index++, record.getBankCardNo());
-        statement.setString(index++, record.getBankName());
-        statement.setString(index++, record.getBankBranchCode());
-        statement.setInt(index, record.getIsResigned());
+        statement.setString(index, record.getFirstDepartment());
     }
 
     private JdbcTemplate requireJdbcTemplate() {
@@ -215,8 +119,8 @@ public class EmployeeRosterRepository {
 
     private String createMergeSql(String tableName) {
         return "INSERT INTO " + tableName + " " +
-                "(dingtalk_id, employee_no, username, job_title, job_title_id, job_grade, job_grade_id, department, first_department, hire_date, position_name, bank_card_no, bank_name, bank_branch_code, is_resigned) " +
-                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
+                "(dingtalk_id, employee_no, username, job_title, job_title_id, job_grade, job_grade_id, department, first_department) " +
+                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) " +
                 "ON DUPLICATE KEY UPDATE " +
                 "dingtalk_id = VALUES(dingtalk_id), " +
                 "employee_no = VALUES(employee_no), " +
@@ -226,13 +130,7 @@ public class EmployeeRosterRepository {
                 "job_grade = VALUES(job_grade), " +
                 "job_grade_id = VALUES(job_grade_id), " +
                 "department = VALUES(department), " +
-                "first_department = VALUES(first_department), " +
-                "hire_date = VALUES(hire_date), " +
-                "position_name = VALUES(position_name), " +
-                "bank_card_no = VALUES(bank_card_no), " +
-                "bank_name = VALUES(bank_name), " +
-                "bank_branch_code = VALUES(bank_branch_code), " +
-                "is_resigned = VALUES(is_resigned)";
+                "first_department = VALUES(first_department)";
     }
 
     private String resolveTableName() {

+ 1 - 594
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImpl.java

@@ -1,20 +1,14 @@
 package com.malk.minglei.service.impl.employee;
 
-import com.alibaba.fastjson.JSON;
 import com.malk.minglei.config.EmployeeRosterProperties;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateRequest;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateResult;
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterSyncResult;
-import com.malk.minglei.service.approval.DingTalkApprovalClient;
 import com.malk.minglei.service.employee.DingTalkEmployeeRosterClient;
 import com.malk.minglei.service.employee.EmployeeRosterService;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Contacts;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -23,11 +17,6 @@ import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.time.LocalDate;
-import java.time.ZoneId;
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
 
 /**
  * 员工花名册同步与查询服务实现。
@@ -40,8 +29,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
     private static final long ROOT_DEPARTMENT_ID = 1L;
     private static final int DEFAULT_USER_PAGE_SIZE = 100;
     private static final int DEFAULT_ROSTER_BATCH_SIZE = 20;
-    private static final ZoneId EMPLOYEE_NO_ZONE_ID = ZoneId.of("Asia/Shanghai");
-    private static final String DEDICATED_ACCOUNT_PASSWORD_PREFIX = "ML";
 
     private final DDClient dingTalkClient;
     private final DDClient_Contacts contactsClient;
@@ -49,8 +36,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
     private final DingTalkEmployeeRosterClient rosterClient;
     private final EmployeeRosterRepository repository;
     private final EmployeeRosterProperties properties;
-    private final EmployeeRosterDuplicateNameNotifier duplicateNameNotifier;
-    private final DingTalkApprovalClient approvalClient;
 
     /**
      * 创建员工花名册服务实现。
@@ -67,330 +52,13 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
                                      DDConf dingTalkConfiguration,
                                      DingTalkEmployeeRosterClient rosterClient,
                                      EmployeeRosterRepository repository,
-                                     EmployeeRosterProperties properties,
-                                     EmployeeRosterDuplicateNameNotifier duplicateNameNotifier) {
-        this(dingTalkClient, contactsClient, dingTalkConfiguration, rosterClient, repository, properties,
-                duplicateNameNotifier, null);
-    }
-
-    /**
-     * 创建员工花名册服务实现。
-     *
-     * @param dingTalkClient 钉钉基础客户端
-     * @param contactsClient 钉钉通讯录客户端
-     * @param dingTalkConfiguration 钉钉应用配置
-     * @param rosterClient 钉钉花名册客户端
-     * @param repository 员工花名册本地仓储
-     * @param properties 员工花名册同步配置
-     * @param duplicateNameNotifier 同名员工通知组件
-     * @param approvalClient 钉钉 OA 审批实例客户端
-     */
-    @Autowired
-    public EmployeeRosterServiceImpl(DDClient dingTalkClient,
-                                     DDClient_Contacts contactsClient,
-                                     DDConf dingTalkConfiguration,
-                                     DingTalkEmployeeRosterClient rosterClient,
-                                     EmployeeRosterRepository repository,
-                                     EmployeeRosterProperties properties,
-                                     EmployeeRosterDuplicateNameNotifier duplicateNameNotifier,
-                                     DingTalkApprovalClient approvalClient) {
+                                     EmployeeRosterProperties properties) {
         this.dingTalkClient = dingTalkClient;
         this.contactsClient = contactsClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.rosterClient = rosterClient;
         this.repository = repository;
         this.properties = properties;
-        this.duplicateNameNotifier = duplicateNameNotifier;
-        this.approvalClient = approvalClient;
-    }
-
-    /**
-     * 调用钉钉通讯录创建自建企业账号。钉钉 OA 部门选择字段允许包含对象或嵌套 value,
-     * 此处只提取部门 ID 并去重,避免将展示名称传入钉钉接口。
-     *
-     * @param request 账号创建请求
-     * @return 钉钉创建结果
-     */
-    @Override
-    @Transactional(transactionManager = "employeeRosterTransactionManager")
-    public DingTalkDedicatedAccountCreateResult createDingTalkDedicatedAccount(DingTalkDedicatedAccountCreateRequest request) {
-        if (request == null) {
-            throw new IllegalArgumentException("request must not be null");
-        }
-
-        String accessToken = dingTalkClient.getAccessToken();
-        require(accessToken, "Unable to obtain DingTalk access token");
-        DedicatedAccountData account = queryDedicatedAccountData(accessToken,
-                requiredText(request.getOaProcessInstanceId(), "oaProcessInstanceId must not be blank"));
-
-        List<Long> departmentIds = resolveDepartmentIds(accessToken, account.departmentList);
-        if (departmentIds.isEmpty()) {
-            throw new IllegalArgumentException("departmentList must contain at least one DingTalk department name or ID");
-        }
-
-        String employeeNo = optionalText(account.employeeNo);
-        if (employeeNo == null) {
-            employeeNo = repository.reserveNextEmployeeNo(LocalDate.now(EMPLOYEE_NO_ZONE_ID));
-        }
-        String loginId = employeeNo;
-        String initialPassword = DEDICATED_ACCOUNT_PASSWORD_PREFIX + employeeNo;
-
-        Map<String, Object> extraFields = new LinkedHashMap<String, Object>();
-        extraFields.put("job_number", employeeNo);
-        // 钉钉 title 是职位字段;未配置职位时兼容沿用原职称映射。
-        putIfNotBlank(extraFields, "title", firstNotBlank(account.position, account.jobTitle));
-        String gender = normalizeGender(account.gender);
-        if (gender != null) {
-            extraFields.put("gender", gender);
-        }
-        if (account.expectedArrivalDate != null) {
-            extraFields.put("hired_date", Long.valueOf(account.expectedArrivalDate
-                    .atStartOfDay(EMPLOYEE_NO_ZONE_ID).toInstant().toEpochMilli()));
-        }
-        Map<String, Object> extension = new LinkedHashMap<String, Object>();
-        putIfNotBlank(extension, "positionName", account.positionName);
-        putIfNotBlank(extension, "jobTitle", account.jobTitle);
-        if (account.age != null) {
-            extension.put("age", account.age);
-        }
-        putIfNotBlank(extension, "jobGrade", account.jobGrade);
-        putIfNotBlank(extension, "idCardNo", account.idCardNo);
-        if (!extension.isEmpty()) {
-            extraFields.put("extension", JSON.toJSONString(extension));
-        }
-        Map response = contactsClient.createUser_dingTalk(
-                accessToken,
-                loginId,
-                initialPassword,
-                account.name,
-                departmentIds,
-                extraFields);
-        if (response == null) {
-            throw new IllegalStateException("DingTalk dedicated account creation response is empty");
-        }
-        String userId = extractUserId(response);
-        require(userId, "DingTalk dedicated account creation response does not contain userid");
-
-        EmployeeRosterRecord record = new EmployeeRosterRecord();
-        record.setDingtalkId(userId);
-        record.setEmployeeNo(employeeNo);
-        record.setUsername(account.name);
-        record.setPositionName(optionalText(account.positionName));
-        record.setHireDate(account.expectedArrivalDate == null ? null : account.expectedArrivalDate.toString());
-        record.setJobTitle(optionalText(account.jobTitle));
-        record.setJobGrade(optionalText(account.jobGrade));
-        record.setIsResigned(1);
-        if (repository.upsertAll(Collections.singletonList(record)) != 1) {
-            throw new IllegalStateException("Failed to persist the newly created employee roster record");
-        }
-        return new DingTalkDedicatedAccountCreateResult(userId, employeeNo, departmentIds);
-    }
-
-    /**
-     * 查询 OA 审批实例并将人员表单控件转换为创建账号所需的数据。
-     * 表单字段名称允许使用当前人事表单的中文名称或常见别名,以兼容控件 bizAlias 的调整。
-     *
-     * @param accessToken 钉钉访问令牌
-     * @param processInstanceId 钉钉 OA 审批实例 ID
-     * @return 已完成字段校验的账号创建数据
-     */
-    private DedicatedAccountData queryDedicatedAccountData(String accessToken, String processInstanceId) {
-        if (approvalClient == null) {
-            throw new IllegalStateException("DingTalk approval client is not configured");
-        }
-        Map<String, Object> detail = approvalClient.getProcessInstanceDetail(accessToken, processInstanceId);
-        if (detail == null || Boolean.FALSE.equals(detail.get("success"))) {
-            throw new IllegalArgumentException("DingTalk OA approval instance query was not successful");
-        }
-        Object resultValue = detail.get("result");
-        if (!(resultValue instanceof Map)) {
-            throw new IllegalArgumentException("OA approval instance does not contain result data");
-        }
-        Map result = (Map) resultValue;
-        List<Map> components = formComponents(firstNotNull(result.get("formComponentValues"),
-                result.get("form_component_values")));
-        if (components.isEmpty()) {
-            throw new IllegalArgumentException("OA approval instance does not contain formComponentValues");
-        }
-
-        DedicatedAccountData account = new DedicatedAccountData();
-        account.name = requiredComponentText(components, "姓名", "员工姓名", "name");
-        Object departmentValue = requiredComponentValue(components, "部门", "所属部门", "入职部门", "申请部门", "department");
-        account.departmentList = Collections.<Object>singletonList(departmentValue);
-        account.employeeNo = componentText(components, "工号", "员工工号", "员工编号", "jobNumber", "employeeNo");
-        account.positionName = componentText(components, "岗位", "职务", "positionName");
-        account.position = componentText(components, "职位", "position", "positionTitle");
-        account.gender = componentText(components, "性别", "gender");
-        account.age = componentAge(components);
-        account.expectedArrivalDate = componentExpectedArrivalDate(components);
-        account.jobTitle = componentText(components, "职称", "jobTitle");
-        account.jobGrade = componentText(components, "职等", "职级", "jobGrade");
-        account.idCardNo = componentText(components, "身份证号", "身份证号码", "idCardNo");
-        return account;
-    }
-
-    private List<Map> formComponents(Object value) {
-        Object structuredValue = parseJsonValue(value);
-        if (!(structuredValue instanceof Iterable)) {
-            return Collections.emptyList();
-        }
-        List<Map> components = new ArrayList<Map>();
-        for (Object item : (Iterable) structuredValue) {
-            if (item instanceof Map) {
-                components.add((Map) item);
-            }
-        }
-        return components;
-    }
-
-    private String requiredComponentText(List<Map> components, String... aliases) {
-        String value = componentText(components, aliases);
-        if (value == null) {
-            throw new IllegalArgumentException("OA approval instance does not contain required field: " + aliases[0]);
-        }
-        return value;
-    }
-
-    private Object requiredComponentValue(List<Map> components, String... aliases) {
-        Map component = findComponent(components, aliases);
-        if (component == null) {
-            throw new IllegalArgumentException("OA approval instance does not contain required field: " + aliases[0]);
-        }
-        Object value = parseJsonValue(component.get("extValue"));
-        if (isMeaningfulValue(value)) {
-            return value;
-        }
-        value = parseJsonValue(component.get("value"));
-        if (!isMeaningfulValue(value)) {
-            throw new IllegalArgumentException("OA approval instance field must not be blank: " + aliases[0]);
-        }
-        return value;
-    }
-
-    private String componentText(List<Map> components, String... aliases) {
-        Map component = findComponent(components, aliases);
-        if (component == null) {
-            return null;
-        }
-        String value = toDisplayText(parseJsonValue(component.get("value")));
-        return value == null ? toDisplayText(parseJsonValue(component.get("extValue"))) : value;
-    }
-
-    private Integer componentAge(List<Map> components) {
-        String ageText = componentText(components, "年龄", "age");
-        if (ageText == null) {
-            return null;
-        }
-        try {
-            int age = Integer.parseInt(ageText);
-            if (age < 0 || age > 150) {
-                throw new IllegalArgumentException("OA approval field age must be between 0 and 150");
-            }
-            return Integer.valueOf(age);
-        } catch (NumberFormatException exception) {
-            throw new IllegalArgumentException("OA approval field age must be an integer");
-        }
-    }
-
-    private LocalDate componentExpectedArrivalDate(List<Map> components) {
-        String dateText = componentText(components, "预计到岗日期", "预计入职日期", "到岗日期", "expectedArrivalDate");
-        if (dateText == null) {
-            return null;
-        }
-        try {
-            if (dateText.matches("[0-9]{10,13}")) {
-                long timestamp = Long.parseLong(dateText);
-                if (dateText.length() == 10) {
-                    timestamp *= 1000L;
-                }
-                return Instant.ofEpochMilli(timestamp).atZone(EMPLOYEE_NO_ZONE_ID).toLocalDate();
-            }
-            return LocalDate.parse(dateText.length() >= 10 ? dateText.substring(0, 10) : dateText,
-                    DateTimeFormatter.ISO_LOCAL_DATE);
-        } catch (DateTimeParseException | NumberFormatException exception) {
-            throw new IllegalArgumentException("OA approval field 预计到岗日期 must use yyyy-MM-dd or a Unix timestamp");
-        }
-    }
-
-    private Map findComponent(List<Map> components, String... aliases) {
-        for (String alias : aliases) {
-            String normalizedAlias = normalizeKey(alias);
-            for (Map component : components) {
-                String name = firstText(component, "name", "label", "title", "componentName", "bizAlias", "biz_alias");
-                if (normalizeKey(name).equals(normalizedAlias)) {
-                    return component;
-                }
-            }
-        }
-        for (Map component : components) {
-            String name = firstText(component, "name", "label", "title", "componentName", "bizAlias", "biz_alias");
-            String normalizedName = normalizeKey(name);
-            for (String alias : aliases) {
-                if (!normalizedName.isEmpty() && normalizedName.contains(normalizeKey(alias))) {
-                    return component;
-                }
-            }
-        }
-        return null;
-    }
-
-    private Object parseJsonValue(Object value) {
-        if (!(value instanceof String)) {
-            return value;
-        }
-        String text = optionalText((String) value);
-        if (text == null || (!text.startsWith("{") && !text.startsWith("["))) {
-            return text;
-        }
-        try {
-            return JSON.parse(text);
-        } catch (Exception ignored) {
-            return text;
-        }
-    }
-
-    private boolean isMeaningfulValue(Object value) {
-        if (value instanceof Iterable) {
-            return ((Iterable) value).iterator().hasNext();
-        }
-        if (value instanceof Map) {
-            return !((Map) value).isEmpty();
-        }
-        return toDisplayText(value) != null;
-    }
-
-    /**
-     * 复用自建企业账号创建时的部门解析规则,将部门名称或路径转换为钉钉部门 ID。
-     *
-     * @param departmentReferences 部门引用列表
-     * @return 去重后的钉钉部门 ID 列表
-     */
-    @Override
-    public List<Long> resolveDingTalkDepartmentIds(List<Object> departmentReferences) {
-        String accessToken = dingTalkClient.getAccessToken();
-        require(accessToken, "Unable to obtain DingTalk access token");
-        return resolveDepartmentIds(accessToken, departmentReferences);
-    }
-
-    private String normalizeGender(String gender) {
-        String value = optionalText(gender);
-        if (value == null) {
-            return null;
-        }
-        if ("1".equals(value) || "男".equals(value) || "male".equalsIgnoreCase(value)) {
-            return "1";
-        }
-        if ("2".equals(value) || "女".equals(value) || "female".equalsIgnoreCase(value)) {
-            return "2";
-        }
-        throw new IllegalArgumentException("gender must be 1, 2, 男, 女, male or female");
-    }
-
-    private void putIfNotBlank(Map<String, Object> fields, String fieldName, String value) {
-        String text = optionalText(value);
-        if (text != null) {
-            fields.put(fieldName, text);
-        }
     }
 
     /**
@@ -433,7 +101,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         }
 
         int syncedCount = repository.upsertAll(records);
-        duplicateNameNotifier.notifyDuplicateUsernames(repository.findActiveDuplicateUsernameCounts());
         return new EmployeeRosterSyncResult(departmentId, type == INCLUDE_CHILD_DEPARTMENTS, records.size(), fieldCodes.size(), syncedCount);
     }
 
@@ -486,20 +153,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         return repository.findByEmployeeNo(employeeNo.trim());
     }
 
-    /**
-     * 根据员工姓名查询本地员工花名册记录。
-     *
-     * @param username 员工姓名
-     * @return 员工花名册信息,不存在时返回 null
-     */
-    @Override
-    public EmployeeRosterInfo findByUsername(String username) {
-        if (username == null || username.trim().isEmpty()) {
-            throw new IllegalArgumentException("username must not be blank");
-        }
-        return repository.findByUsername(username.trim());
-    }
-
     private List<Map> listUsers(String accessToken, long departmentId, int type) {
         Set<Long> departmentIds = new LinkedHashSet<Long>();
         departmentIds.add(departmentId);
@@ -519,192 +172,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         return new ArrayList<Map>(usersByUserId.values());
     }
 
-    /**
-     * 将 OA 传入的部门名称或部门 ID 转换为钉钉创建用户接口所需的部门 ID。
-     *
-     * <p>部门名称不能直接传给钉钉创建用户接口。名称按层级路径逐段查询直属子部门,
-     * 避免为一次账号创建遍历整个部门树;直接传入部门 ID 的历史调用方式保持兼容。</p>
-     *
-     * @param accessToken 钉钉应用访问令牌
-     * @param departmentList OA 部门选择结果,支持名称、ID 或包含名称/ID 的对象
-     * @return 去重后的钉钉部门 ID 列表
-     */
-    private List<Long> resolveDepartmentIds(String accessToken, List<Object> departmentList) {
-        Set<Long> departmentIds = new LinkedHashSet<Long>();
-        Set<String> departmentNames = new LinkedHashSet<String>();
-        if (departmentList != null) {
-            for (Object department : departmentList) {
-                collectDepartmentReferences(department, departmentIds, departmentNames);
-            }
-        }
-
-        if (departmentNames.isEmpty()) {
-            return new ArrayList<Long>(departmentIds);
-        }
-
-        for (String departmentName : departmentNames) {
-            departmentIds.add(resolveDepartmentIdByPath(accessToken, departmentName));
-        }
-        return new ArrayList<Long>(departmentIds);
-    }
-
-    /**
-     * 从根部门开始按路径段查询直属子部门,得到目标部门 ID。
-     *
-     * @param accessToken 钉钉应用访问令牌
-     * @param departmentPath 以 - 或 / 分隔的部门路径
-     * @return 路径末端的钉钉部门 ID
-     */
-    private Long resolveDepartmentIdByPath(String accessToken, String departmentPath) {
-        long parentDepartmentId = ROOT_DEPARTMENT_ID;
-        for (String departmentName : splitDepartmentPath(departmentPath)) {
-            Set<Long> matchedDepartmentIds = new LinkedHashSet<Long>();
-            List<Map> childDepartments = contactsClient.listSubDepartmentDetail(accessToken, parentDepartmentId);
-            if (childDepartments != null) {
-                for (Map childDepartment : childDepartments) {
-                    String childDepartmentName = firstText(childDepartment, "name", "dept_name", "deptName");
-                    if (!departmentName.equals(normalizeDepartmentName(childDepartmentName))) {
-                        continue;
-                    }
-                    Long childDepartmentId = toPositiveLong(firstNotNull(childDepartment.get("dept_id"),
-                            childDepartment.get("deptId"), childDepartment.get("id")));
-                    if (childDepartmentId != null) {
-                        matchedDepartmentIds.add(childDepartmentId);
-                    }
-                }
-            }
-            if (matchedDepartmentIds.isEmpty()) {
-                throw new IllegalArgumentException("departmentList contains an unknown DingTalk department path: " + departmentPath);
-            }
-            if (matchedDepartmentIds.size() > 1) {
-                throw new IllegalArgumentException("departmentList matches multiple DingTalk departments for name: " + departmentName);
-            }
-            parentDepartmentId = matchedDepartmentIds.iterator().next().longValue();
-        }
-        return Long.valueOf(parentDepartmentId);
-    }
-
-    private List<String> splitDepartmentPath(String departmentPath) {
-        List<String> departmentNames = new ArrayList<String>();
-        for (String segment : departmentPath.split("[-/]")) {
-            String departmentName = normalizeDepartmentName(segment);
-            if (departmentName == null) {
-                throw new IllegalArgumentException("departmentList contains an invalid DingTalk department path: " + departmentPath);
-            }
-            departmentNames.add(departmentName);
-        }
-        return departmentNames;
-    }
-
-    private void collectDepartmentReferences(Object value, Set<Long> departmentIds, Set<String> departmentNames) {
-        if (value instanceof Iterable) {
-            for (Object item : (Iterable<?>) value) {
-                collectDepartmentReferences(item, departmentIds, departmentNames);
-            }
-            return;
-        }
-        if (value instanceof Map) {
-            Map department = (Map) value;
-            if (collectDepartmentIdsFromObject(department, departmentIds)) {
-                return;
-            }
-            String departmentName = firstText(department, "name", "deptName", "dept_name", "departmentName",
-                    "department_name", "label", "text");
-            if (departmentName != null) {
-                departmentNames.add(departmentName);
-                return;
-            }
-            collectDepartmentReferences(department.get("value"), departmentIds, departmentNames);
-            collectDepartmentReferences(department.get("departments"), departmentIds, departmentNames);
-            collectDepartmentReferences(department.get("departmentList"), departmentIds, departmentNames);
-            collectDepartmentReferences(department.get("deptIdList"), departmentIds, departmentNames);
-            return;
-        }
-        if (value instanceof Number) {
-            addDepartmentId(((Number) value).longValue(), departmentIds);
-            return;
-        }
-        if (value instanceof String) {
-            String text = ((String) value).trim();
-            if (text.isEmpty()) {
-                return;
-            }
-            for (String departmentReference : text.split(",")) {
-                String reference = optionalText(departmentReference);
-                if (reference == null) {
-                    continue;
-                }
-                if (isNumericDepartmentId(reference)) {
-                    addDepartmentId(Long.parseLong(reference), departmentIds);
-                } else {
-                    departmentNames.add(reference);
-                }
-            }
-        }
-    }
-
-    private boolean collectDepartmentIdsFromObject(Object value, Set<Long> departmentIds) {
-        int departmentIdCount = departmentIds.size();
-        if (value instanceof Map) {
-            Map department = (Map) value;
-            collectDepartmentIdsFromObject(department.get("id"), departmentIds);
-            collectDepartmentIdsFromObject(department.get("deptId"), departmentIds);
-            collectDepartmentIdsFromObject(department.get("dept_id"), departmentIds);
-            collectDepartmentIdsFromObject(department.get("departmentId"), departmentIds);
-            collectDepartmentIdsFromObject(department.get("department_id"), departmentIds);
-            collectDepartmentIdsFromObject(department.get("value"), departmentIds);
-            return departmentIds.size() > departmentIdCount;
-        }
-        if (value instanceof Iterable) {
-            for (Object item : (Iterable<?>) value) {
-                collectDepartmentIdsFromObject(item, departmentIds);
-            }
-            return departmentIds.size() > departmentIdCount;
-        }
-        if (value instanceof Number) {
-            addDepartmentId(((Number) value).longValue(), departmentIds);
-        } else if (value instanceof String) {
-            String text = optionalText((String) value);
-            if (text != null) {
-                for (String departmentId : text.split(",")) {
-                    String candidate = optionalText(departmentId);
-                    if (candidate != null && isNumericDepartmentId(candidate)) {
-                        addDepartmentId(Long.parseLong(candidate), departmentIds);
-                    }
-                }
-            }
-        }
-        return departmentIds.size() > departmentIdCount;
-    }
-
-    private String normalizeDepartmentName(String departmentName) {
-        return optionalText(departmentName);
-    }
-
-    private boolean isNumericDepartmentId(String value) {
-        return value.matches("-?[0-9]+");
-    }
-
-    private Long toPositiveLong(Object value) {
-        if (value instanceof Number) {
-            long number = ((Number) value).longValue();
-            return number > 0 ? Long.valueOf(number) : null;
-        }
-        String text = toStringValue(value);
-        if (text == null || !isNumericDepartmentId(text)) {
-            return null;
-        }
-        long number = Long.parseLong(text);
-        return number > 0 ? Long.valueOf(number) : null;
-    }
-
-    private void addDepartmentId(long departmentId, Set<Long> departmentIds) {
-        if (departmentId < 1) {
-            throw new IllegalArgumentException("departmentList contains an invalid DingTalk department ID");
-        }
-        departmentIds.add(Long.valueOf(departmentId));
-    }
-
     private void appendDepartmentUsers(String accessToken, long departmentId, Map<String, Map> usersByUserId) {
         long cursor = 0L;
         int pageSize = normalizePageSize(properties.getUserPageSize());
@@ -776,36 +243,9 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         record.setJobGradeId(findRosterValue(rosterValues, "职等", "ad410990-cee4-4e19-b70f-189a8ab87527", "job_grade"));
         record.setDepartment(findRosterValue(rosterValues, "部门", "所在部门", "部门名称", "department"));
         record.setFirstDepartment(findRosterValue(rosterValues, "一级部门", "一级部门名称", "first_department"));
-        record.setHireDate(findRosterValue(rosterValues, "\u5165\u804c\u65f6\u95f4", "hire_date"));
-        record.setPositionName(findRosterLabel(rosterValues, "\u804c\u4f4d", "sys00-position", "position_name"));
-        record.setBankCardNo(findRosterValue(rosterValues, "\u94f6\u884c\u5361\u53f7", "bank_card_no"));
-        record.setBankName(findRosterValue(rosterValues, "\u5f00\u6237\u884c", "bank_name"));
-        record.setBankBranchCode(findRosterValue(rosterValues, "\u652f\u884c\u8054\u884c\u53f7", "bank_branch_code"));
-        record.setIsResigned(isActive(user) ? 1 : 0);
         return record;
     }
 
-    /**
-     * 钉钉通讯录明确返回非激活状态时,按离职处理;缺失状态字段时保持原有在职兼容行为。
-     *
-     * @param user 钉钉通讯录用户详情
-     * @return 是否在职
-     */
-    private boolean isActive(Map user) {
-        Object active = firstNotNull(user.get("active"), user.get("is_active"), user.get("isActive"));
-        if (active == null) {
-            return true;
-        }
-        if (active instanceof Boolean) {
-            return ((Boolean) active).booleanValue();
-        }
-        if (active instanceof Number) {
-            return ((Number) active).intValue() != 0;
-        }
-        String value = String.valueOf(active).trim();
-        return !"false".equalsIgnoreCase(value) && !"0".equals(value);
-    }
-
     private List<Map> extractUserList(Map result) {
         if (result == null) {
             return Collections.emptyList();
@@ -1106,20 +546,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         }
     }
 
-    private String requiredText(String value, String message) {
-        if (value == null || value.trim().isEmpty()) {
-            throw new IllegalArgumentException(message);
-        }
-        return value.trim();
-    }
-
-    private String optionalText(String value) {
-        if (value == null || value.trim().isEmpty()) {
-            return null;
-        }
-        return value.trim();
-    }
-
     private static class RosterFieldValue {
 
         private String label;
@@ -1145,23 +571,4 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
             return label == null && value == null;
         }
     }
-
-    /**
-     * OA 审批实例解析出的账号创建资料,仅在当前服务内使用,避免把未经审批的数据暴露为接口入参。
-     */
-    private static class DedicatedAccountData {
-
-        private String name;
-        private List<Object> departmentList;
-        private String employeeNo;
-        private String positionName;
-        private String position;
-        private String gender;
-        private Integer age;
-        private LocalDate expectedArrivalDate;
-        private String jobTitle;
-        private String jobGrade;
-        private String idCardNo;
-    }
-
 }

+ 21 - 94
src/main/java/com/malk/minglei/service/impl/port/MingleiPortServiceImpl.java

@@ -1,7 +1,5 @@
 package com.malk.minglei.service.impl.port;
 
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.config.MingleiPortProperties;
 import com.malk.minglei.dto.MingleiPortRequest;
 import com.malk.minglei.service.port.MingleiPortService;
@@ -16,19 +14,13 @@ import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 import org.springframework.web.client.RestTemplate;
-import org.springframework.web.client.RestClientException;
-import org.springframework.web.client.RestClientResponseException;
 
-import java.nio.charset.StandardCharsets;
-import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
 /**
- * 使用 HTTP 客户端调用明磊外部 T100 单据接口。
+ * 使用 HTTP 客户端调用供应商价格审批单(核价)外部接口。
  */
 @Service
 public class MingleiPortServiceImpl implements MingleiPortService {
@@ -39,25 +31,20 @@ public class MingleiPortServiceImpl implements MingleiPortService {
     private static final String HOST_IP = "10.0.2.60";
     private static final String HOST_LANGUAGE = "zh_CN";
     private static final String HOST_ACCOUNT = "tiptop";
+    private static final String HOST_TIMESTAMP = "20260807164700004";
     private static final String SERVICE_PRODUCT = "T100";
     private static final String SERVICE_NAME = "ProcurementApprovalPrice";
-    private static final String T100_DOCUMENT_SERVICE_NAME = "DingTalk To T100";
     private static final String SERVICE_ID = "toptst";
     private static final String ENTERPRISE_ID = "87";
     private static final String COMPANY_ID = "ML";
-    private static final ZoneId T100_ZONE = ZoneId.of("Asia/Shanghai");
-    private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");
 
     private final RestTemplate restTemplate;
     private final MingleiPortProperties properties;
-    private final ObjectMapper objectMapper;
 
     public MingleiPortServiceImpl(@Qualifier("mingleiPortRestTemplate") RestTemplate restTemplate,
-                                  MingleiPortProperties properties,
-                                  ObjectMapper objectMapper) {
+                                  MingleiPortProperties properties) {
         this.restTemplate = restTemplate;
         this.properties = properties;
-        this.objectMapper = objectMapper;
     }
 
     /**
@@ -68,96 +55,50 @@ public class MingleiPortServiceImpl implements MingleiPortService {
      */
     @Override
     public Map<String, Object> submitSupplierPriceApproval(MingleiPortRequest request) {
-        if (request == null) {
-            throw new IllegalArgumentException("request must not be null");
-        }
-        LOGGER.info("Submitting supplier price approval documentNo={} detailCount={}",
-                new Object[]{request.getDocumentNumber(), request.getDetailCount()});
-
-        Map<String, Object> parameter = new LinkedHashMap<String, Object>();
-        parameter.put("head", request.getHead());
-        parameter.put("detail", request.getDetail());
-        return submit(parameter, null, SERVICE_NAME);
-    }
-
-    /**
-     * 使用已有的 T100 协议外层转发非核价单据,调用方只负责组装 parameter 节点。
-     *
-     * @param parameter T100 payload.std_data.parameter 节点
-     * @param dataKeyType T100 datakey.type 业务类型
-     * @return 外部接口原始响应内容
-     */
-    @Override
-    public Map<String, Object> submitT100Document(Map<String, Object> parameter, String dataKeyType) {
-        if (parameter == null || parameter.isEmpty()) {
-            throw new IllegalArgumentException("parameter must not be empty");
-        }
-        if (dataKeyType == null || dataKeyType.trim().isEmpty()) {
-            throw new IllegalArgumentException("dataKeyType must not be blank");
-        }
-        LOGGER.info("Submitting T100 document dataKeyType={}", dataKeyType);
-        return submit(parameter, dataKeyType.trim(), T100_DOCUMENT_SERVICE_NAME);
-    }
-
-    private Map<String, Object> submit(Map<String, Object> parameter, String dataKeyType, String serviceName) {
         HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
+        headers.setContentType(MediaType.APPLICATION_JSON);
         headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
 
         validateConfiguration();
+        LOGGER.info("Submitting supplier price approval documentNo={} detailCount={}",
+                new Object[]{request.getDocumentNumber(), request.getDetailCount()});
 
-        ResponseEntity<Map<String, Object>> response;
-        try {
-            Map<String, Object> externalRequest = createExternalRequest(parameter, dataKeyType, serviceName);
-            LOGGER.info("T100 push request endpoint={} requestBody={}",
-                    properties.getSupplierPriceApprovalUrl(), toLogJson(externalRequest));
-            response = restTemplate.exchange(
-                    properties.getSupplierPriceApprovalUrl(),
-                    HttpMethod.POST,
-                    new HttpEntity<Map<String, Object>>(externalRequest, headers),
-                    new ParameterizedTypeReference<Map<String, Object>>() {
-                    });
-        } catch (RestClientResponseException exception) {
-            LOGGER.error("T100 document request failed stage=T100_DOCUMENT_PUSH status={} responseBodyLength={}",
-                    exception.getRawStatusCode(), exception.getResponseBodyAsByteArray().length);
-            throw exception;
-        } catch (RestClientException exception) {
-            LOGGER.error("T100 document request failed stage=T100_DOCUMENT_PUSH exceptionType={}",
-                    exception.getClass().getSimpleName());
-            throw exception;
-        }
+        ResponseEntity<Map<String, Object>> response = restTemplate.exchange(
+                properties.getSupplierPriceApprovalUrl(),
+                HttpMethod.POST,
+                new HttpEntity<Map<String, Object>>(createExternalRequest(request), headers),
+                new ParameterizedTypeReference<Map<String, Object>>() {
+                });
         return response.getBody() == null ? Collections.<String, Object>emptyMap() : response.getBody();
     }
 
     /**
-     * 使用固定的外层协议字段组装外部接口请求,并透传调用方提供的 parameter 节点。
+     * 使用固定的外层协议字段组装外部接口请求,仅透传调用方提供的 head 和 detail。
      *
-     * @param parameter T100 payload.std_data.parameter 节点
-     * @param dataKeyType T100 datakey.type 业务类型;核价单为空
+     * @param request 本地精简请求体
      * @return 外部接口完整请求体
      */
-    private Map<String, Object> createExternalRequest(Map<String, Object> parameter, String dataKeyType,
-                                                       String serviceName) {
+    private Map<String, Object> createExternalRequest(MingleiPortRequest request) {
         Map<String, Object> host = new LinkedHashMap<String, Object>();
         host.put("prod", HOST_PRODUCT);
         host.put("ip", HOST_IP);
         host.put("lang", HOST_LANGUAGE);
         host.put("acct", HOST_ACCOUNT);
-        // T100 会校验时间戳,不能依赖部署服务器的 JVM 默认时区。
-        host.put("timestamp", LocalDateTime.now(T100_ZONE).format(TIMESTAMP_FORMATTER));
+        host.put("timestamp", HOST_TIMESTAMP);
 
         Map<String, Object> service = new LinkedHashMap<String, Object>();
         service.put("prod", SERVICE_PRODUCT);
-        service.put("name", serviceName);
+        service.put("name", SERVICE_NAME);
         service.put("ip", HOST_IP);
         service.put("id", SERVICE_ID);
 
         Map<String, Object> dataKey = new LinkedHashMap<String, Object>();
         dataKey.put("EntId", ENTERPRISE_ID);
         dataKey.put("CompanyId", COMPANY_ID);
-        if (dataKeyType != null) {
-            dataKey.put("type", dataKeyType);
-        }
+
+        Map<String, Object> parameter = new LinkedHashMap<String, Object>();
+        parameter.put("head", request.getHead());
+        parameter.put("detail", request.getDetail());
 
         Map<String, Object> standardData = new LinkedHashMap<String, Object>();
         standardData.put("parameter", parameter);
@@ -175,20 +116,6 @@ public class MingleiPortServiceImpl implements MingleiPortService {
         return externalRequest;
     }
 
-    /**
-     * 输出 T100 实际报文以便排查字段映射,同时避免将接口密钥写入日志。
-     */
-    private String toLogJson(Map<String, Object> externalRequest) {
-        Map<String, Object> logRequest = new LinkedHashMap<String, Object>(externalRequest);
-        logRequest.put("key", "******");
-        try {
-            return objectMapper.writeValueAsString(logRequest);
-        } catch (JsonProcessingException exception) {
-            LOGGER.warn("Failed to serialize T100 push request for logging", exception);
-            return logRequest.toString();
-        }
-    }
-
     /**
      * 校验调用外部接口需要的地址和密钥配置是否完整。
      */

+ 3 - 179
src/main/java/com/malk/minglei/service/impl/yida/DingTalkYidaProcessOpenApiClient.java

@@ -3,25 +3,9 @@ package com.malk.minglei.service.impl.yida;
 import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
 import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.dingtalk.DDClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
-import org.springframework.util.StringUtils;
-import org.springframework.web.client.HttpStatusCodeException;
-import org.springframework.web.client.RestTemplate;
 
-import java.nio.charset.StandardCharsets;
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.Set;
 
 /**
  * 使用钉钉 OpenAPI 发起宜搭流程。
@@ -29,26 +13,18 @@ import java.util.Set;
 @Service
 public class DingTalkYidaProcessOpenApiClient implements YidaProcessOpenApiClient {
 
-    private static final Logger LOGGER = LoggerFactory.getLogger(DingTalkYidaProcessOpenApiClient.class);
     private static final String YIDA_OPEN_API_BASE_URL = "https://api.dingtalk.com/v1.0/yida";
-    private static final String YIDA_FORM_DATA_API_BASE_URL = "https://api.dingtalk.com/v2.0/yida";
     private static final String START_PROCESS_ENDPOINT = "/processes/instances/start";
-    private static final String UPDATE_FORM_DATA_ENDPOINT = "/forms/instances";
-    private static final String ACCESS_TOKEN_HEADER = "x-acs-dingtalk-access-token";
-    private static final String YIDA_PUBLIC_ACCOUNT = "yida_pub_account";
+
     private final DDClient dingTalkClient;
-    private final RestTemplate dingTalkRestTemplate;
 
     /**
      * 创建宜搭流程 OpenAPI 客户端。
      *
-     * @param dingTalkClient 钉钉应用客户端
-     * @param dingTalkRestTemplate 钉钉开放平台 HTTP 客户端
+     * @param dingTalkClient 钉钉开放平台客户端
      */
-    public DingTalkYidaProcessOpenApiClient(DDClient dingTalkClient,
-                                            @Qualifier("dingTalkRestTemplate") RestTemplate dingTalkRestTemplate) {
+    public DingTalkYidaProcessOpenApiClient(DDClient dingTalkClient) {
         this.dingTalkClient = dingTalkClient;
-        this.dingTalkRestTemplate = dingTalkRestTemplate;
     }
 
     /**
@@ -61,156 +37,4 @@ public class DingTalkYidaProcessOpenApiClient implements YidaProcessOpenApiClien
     public DDR_New startProcess(Map<String, Object> body) {
         return DDR_New.doPost(YIDA_OPEN_API_BASE_URL + START_PROCESS_ENDPOINT, dingTalkClient.initTokenHeader(), null, body);
     }
-
-    /**
-     * 使用钉钉开放平台 UpdateFormData v2 接口回写流程表单数据。
-     *
-     * @param body 宜搭流程实例更新请求体
-     * @return 钉钉 OpenAPI 原始响应
-     */
-    @Override
-    public DDR_New updateProcessInstance(Map<String, Object> body) {
-        Map<String, Object> requestBody = toUpdateFormDataRequest(body);
-        String endpoint = YIDA_FORM_DATA_API_BASE_URL + UPDATE_FORM_DATA_ENDPOINT;
-        LOGGER.info("Yida form instance update request endpoint={} body={}", endpoint, maskSensitive(requestBody));
-        try {
-            ResponseEntity<Map<String, Object>> httpResponse = dingTalkRestTemplate.exchange(
-                    endpoint,
-                    HttpMethod.PUT,
-                    new HttpEntity<Map<String, Object>>(requestBody, createJsonHeaders()),
-                    new ParameterizedTypeReference<Map<String, Object>>() {
-                    });
-            DDR_New response = toDingTalkResponse(httpResponse.getBody(), httpResponse.getStatusCodeValue());
-            LOGGER.info("Yida form instance update response endpoint={} status={} responseKeys={} success={} code={} message={} formInstanceId={} processInstanceId={}",
-                    endpoint,
-                    httpResponse.getStatusCodeValue(), responseKeys(httpResponse.getBody()),
-                    response.isSuccess(), response.getCode(), response.getMessage(),
-                    requestBody.get("formInstanceId"), body == null ? null : body.get("processInstanceId"));
-            return response;
-        } catch (HttpStatusCodeException exception) {
-            String responseBody = sanitizeResponseBody(exception.getResponseBodyAsString(),
-                    textValue(requestBody.get("systemToken")));
-            LOGGER.error("Yida form instance update rejected endpoint={} status={} formInstanceId={} processInstanceId={} "
-                            + "responseBodyLength={} responseBody={}",
-                    endpoint, exception.getRawStatusCode(), requestBody.get("formInstanceId"),
-                    body == null ? null : body.get("processInstanceId"), exception.getResponseBodyAsByteArray().length,
-                    responseBody);
-            return failedResponse(String.valueOf(exception.getRawStatusCode()),
-                    "Yida form instance update request was rejected: " + responseBody);
-        } catch (RuntimeException exception) {
-            LOGGER.error("Yida form instance update failed endpoint={} formInstanceId={} processInstanceId={} exceptionType={} message={}",
-                    endpoint, requestBody.get("formInstanceId"), body == null ? null : body.get("processInstanceId"),
-                    exception.getClass().getSimpleName(), exception.getMessage(), exception);
-            return failedResponse("YIDA_FORM_INSTANCE_UPDATE_FAILED",
-                    "Yida form instance update request failed");
-        }
-    }
-
-    private Map<String, Object> toUpdateFormDataRequest(Map<String, Object> body) {
-        Map<String, Object> requestBody = new LinkedHashMap<String, Object>();
-        requestBody.put("appType", requiredText(body, "appType"));
-        requestBody.put("systemToken", requiredText(body, "systemToken"));
-        requestBody.put("userId", YIDA_PUBLIC_ACCOUNT);
-        requestBody.put("language", "zh_CN");
-        requestBody.put("formInstanceId", requiredText(body, "formInstanceId"));
-        requestBody.put("useLatestVersion", false);
-        requestBody.put("useAlias", false);
-        requestBody.put("formUuid", requiredText(body, "formUuid"));
-        requestBody.put("updateFormDataJson", requiredText(body, "updateFormDataJson"));
-        return requestBody;
-    }
-
-    private HttpHeaders createJsonHeaders() {
-        String accessToken = dingTalkClient.getAccessToken();
-        if (!StringUtils.hasText(accessToken)) {
-            throw new IllegalStateException("Unable to obtain DingTalk access token");
-        }
-        HttpHeaders headers = new HttpHeaders();
-        headers.setContentType(new MediaType(MediaType.APPLICATION_JSON, StandardCharsets.UTF_8));
-        headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-        headers.set(ACCESS_TOKEN_HEADER, accessToken.trim());
-        return headers;
-    }
-
-    private DDR_New toDingTalkResponse(Map<String, Object> responseBody, int statusCode) {
-        DDR_New response = new DDR_New();
-        Map<String, Object> safeResponseBody = responseBody == null
-                ? Collections.<String, Object>emptyMap()
-                : responseBody;
-        response.setCode(textValue(firstPresent(safeResponseBody, "code", "errcode")));
-        response.setMessage(textValue(firstPresent(safeResponseBody, "message", "errmsg", "errMsg")));
-        response.setResult(safeResponseBody.get("result"));
-        response.setData(safeResponseBody.get("data"));
-        Object success = safeResponseBody.get("success");
-        if (success != null) {
-            response.setSuccess(Boolean.TRUE.equals(success) || "true".equalsIgnoreCase(String.valueOf(success)));
-        } else {
-            String code = response.getCode();
-            response.setSuccess(statusCode >= 200 && statusCode < 300
-                    && (code == null || "0".equals(code) || "200".equals(code)));
-        }
-        if (!response.isSuccess() && !StringUtils.hasText(response.getMessage())) {
-            response.setMessage("Yida form instance update did not return a successful result");
-        }
-        return response;
-    }
-
-    private Set<String> responseKeys(Map<String, Object> responseBody) {
-        return responseBody == null ? Collections.<String>emptySet() : responseBody.keySet();
-    }
-
-    private DDR_New failedResponse(String code, String message) {
-        DDR_New response = new DDR_New();
-        response.setSuccess(false);
-        response.setCode(code);
-        response.setMessage(message);
-        return response;
-    }
-
-    private Object firstPresent(Map<String, Object> source, String... fieldNames) {
-        for (String fieldName : fieldNames) {
-            Object value = source.get(fieldName);
-            if (value != null) {
-                return value;
-            }
-        }
-        return null;
-    }
-
-    private String textValue(Object value) {
-        return value == null ? null : String.valueOf(value);
-    }
-
-    private String requiredText(Map<String, Object> body, String fieldName) {
-        if (body == null || body.get(fieldName) == null || String.valueOf(body.get(fieldName)).trim().isEmpty()) {
-            throw new IllegalArgumentException("Yida process update request field " + fieldName + " must not be blank");
-        }
-        return String.valueOf(body.get(fieldName)).trim();
-    }
-
-    /**
-     * 日志保留请求结构用于排查,但不输出宜搭 System Token。
-     */
-    private Map<String, Object> maskSensitive(Map<String, Object> body) {
-        if (body == null) {
-            return null;
-        }
-        Map<String, Object> masked = new LinkedHashMap<String, Object>(body);
-        if (masked.containsKey("systemToken")) {
-            masked.put("systemToken", "******");
-        }
-        return masked;
-    }
-
-    private String sanitizeResponseBody(String responseBody, String systemToken) {
-        if (!StringUtils.hasText(responseBody)) {
-            return "<empty>";
-        }
-        String sanitized = responseBody;
-        if (StringUtils.hasText(systemToken)) {
-            sanitized = sanitized.replace(systemToken, "******");
-        }
-        sanitized = sanitized.replaceAll("(?i)(x-acs-dingtalk-access-token[:=])[^\\s,]+", "$1******");
-        return sanitized.length() <= 4096 ? sanitized : sanitized.substring(0, 4096) + "...<truncated>";
-    }
 }

+ 2 - 7
src/main/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImpl.java

@@ -75,7 +75,7 @@ public class ErpYidaPushServiceImpl implements ErpYidaPushService {
      */
     @Override
     public McR push(ErpYidaPushRequest request) {
-        validateDetailDataShape(request);
+        validateDetailDataShape(request.getDetailData());
         applyTopLevelCommonFields(request);
         YidaFormPushHandler formPushHandler = formPushHandlers.get(normalizeType(request.getType()));
         String originatorUserId = resolveOriginatorUserId(request, formPushHandler != null);
@@ -103,15 +103,10 @@ public class ErpYidaPushServiceImpl implements ErpYidaPushService {
         return response;
     }
 
-    private void validateDetailDataShape(ErpYidaPushRequest request) {
-        Object detailData = request.getDetailData();
+    private void validateDetailDataShape(Object detailData) {
         if (!(detailData instanceof Map)) {
             throw new IllegalArgumentException("ERP Yida push detailData must be an object containing detailData array");
         }
-        if (normalizeType(Apmt100FormPushHandler.TYPE).equals(normalizeType(request.getType()))
-                && ((Map<?, ?>) detailData).isEmpty()) {
-            return;
-        }
         if (!hasDetailDataRows((Map<?, ?>) detailData)) {
             throw new IllegalArgumentException("ERP Yida push detailData.detailData must be an array");
         }

+ 3 - 94
src/main/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImpl.java

@@ -37,21 +37,11 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
     private static final String THIRD_LEVEL_DEPARTMENT_MANAGER_FIELD_ID = "employeeField_mszdqrla";
     private static final String FOURTH_LEVEL_DEPARTMENT_MANAGER_FIELD_ID = "employeeField_mszdqrlb";
     private static final String DEPARTMENT_LEADER_FIELD_ID = "employeeField_mszdqrl3";
-    /**
-     * 审批矩阵表单中"部门"选择字段 ID,其存储值的 {@code _id} 即钉钉部门 ID。
-     * 审批矩阵按此字段精确匹配转入部门,替代原先的"部门编号"文本字段。
-     */
-    private static final String DEPARTMENT_FIELD_ID = "departmentSelectField_mszdqrks";
-    /**
-     * 审批矩阵表单中"部门负责人"成员字段 ID(宜搭线上表单新增字段)。
-     */
-    private static final String DEPARTMENT_HEAD_FIELD_ID = "employeeField_mualkf6a";
     private static final String[] DEPARTMENT_MANAGER_FIELD_IDS = {
             FIRST_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
             SECOND_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
             THIRD_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
             FOURTH_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
-            DEPARTMENT_HEAD_FIELD_ID,
             DEPARTMENT_LEADER_FIELD_ID
     };
     private static final int PRECISE_QUERY_PAGE_SIZE = 100;
@@ -129,27 +119,6 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         throw new IllegalArgumentException("One of dept_id, user_id or dept_name must be provided");
     }
 
-    /**
-     * 按"部门"选择字段中内嵌的部门 ID 精确查询审批矩阵,返回各级别负责人 userid。
-     *
-     * <p>与 {@link #queryDocuments(MingleiDocumentQueryRequest)} 的区别在于:本方法按部门选择字段
-     * {@code departmentSelectField_mszdqrks}(其 {@code _id} 为钉钉部门 ID)匹配,而非按"部门编号"文本字段;
-     * 并且返回的结果额外包含"部门负责人"字段,以语义化 key 暴露,解耦调用方与具体字段 ID。
-     *
-     * @param departmentId 钉钉部门 ID,与审批矩阵"部门"字段的 {@code _id} 精确匹配
-     * @return 仅包含一级至四级部门负责人、部门负责人和部门分管领导的单层结果
-     */
-    @Override
-    public Map<String, Object> queryDepartmentManagersByDepartmentId(String departmentId) {
-        String normalizedDepartmentId = trimToNull(departmentId);
-        if (normalizedDepartmentId == null) {
-            throw new IllegalArgumentException("departmentId must not be blank");
-        }
-
-        Map<String, Object> queryResult = executeQuery(DEPARTMENT_FIELD_ID, normalizedDepartmentId, 1, PRECISE_QUERY_PAGE_SIZE, true);
-        return extractApprovalTransferManagers(queryResult);
-    }
-
     /**
      * 按查询范围读取部门成员 ID,并按成员首次出现顺序去重。
      *
@@ -211,10 +180,6 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
     }
 
     private Map<String, Object> executeQuery(String fieldId, String fieldValue, int page, int size) {
-        return executeQuery(fieldId, fieldValue, page, size, false);
-    }
-
-    private Map<String, Object> executeQuery(String fieldId, String fieldValue, int page, int size, boolean departmentField) {
         validateConfiguration();
 
         YDParam parameter = new YDParam();
@@ -224,9 +189,7 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         parameter.setCurrentPage(page);
         parameter.setPageSize(size);
         if (fieldId != null) {
-            parameter.setSearchFieldJson(departmentField
-                    ? createDepartmentSearchFieldJson(fieldId, fieldValue)
-                    : createExactSearchFieldJson(fieldId, fieldValue));
+            parameter.setSearchFieldJson(createExactSearchFieldJson(fieldId, fieldValue));
         }
 
         DDR_New response = yidaClient.queryData(parameter, YDConf.FORM_QUERY.retrieve_search_form);
@@ -256,48 +219,18 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
      * @return 仅包含一级至四级部门主管和部门分管领导的单层结果
      */
     private Map<String, Object> extractDepartmentManagers(Map<String, Object> queryResult) {
-        // 固定返回字段结构,调用方无需再因审批矩阵未配置对应部门而额外判断字段是否存在。
-        Map<String, Object> result = new LinkedHashMap<String, Object>();
-        for (String fieldId : DEPARTMENT_MANAGER_FIELD_IDS) {
-            result.put(fieldId, null);
-        }
-
         Map formData = firstFormData(queryResult.get("records"));
         if (formData == null) {
-            return result;
+            return Collections.emptyMap();
         }
 
+        Map<String, Object> result = new LinkedHashMap<String, Object>();
         for (String fieldId : DEPARTMENT_MANAGER_FIELD_IDS) {
             result.put(fieldId, firstEmployeeUserId(formData.get(fieldId + "_id")));
         }
         return result;
     }
 
-    /**
-     * 从精准查询命中的首条单据提取审批转入所需的各级别负责人,并以语义化 key 暴露。
-     *
-     * <p>相较 {@link #extractDepartmentManagers(Map)},本方法额外提取"部门负责人"字段,且不直接返回
-     * 宜搭字段 ID,避免调用方硬编码字段 ID 而产生漂移。
-     *
-     * @param queryResult 宜搭列表查询结果
-     * @return 包含一级至四级部门负责人、部门负责人和部门分管领导的语义化结果
-     */
-    private Map<String, Object> extractApprovalTransferManagers(Map<String, Object> queryResult) {
-        Map formData = firstFormData(queryResult.get("records"));
-        if (formData == null) {
-            return Collections.emptyMap();
-        }
-
-        Map<String, Object> result = new LinkedHashMap<String, Object>();
-        result.put("firstLevelManagerUserId", firstEmployeeUserId(formData.get(FIRST_LEVEL_DEPARTMENT_MANAGER_FIELD_ID + "_id")));
-        result.put("secondLevelManagerUserId", firstEmployeeUserId(formData.get(SECOND_LEVEL_DEPARTMENT_MANAGER_FIELD_ID + "_id")));
-        result.put("thirdLevelManagerUserId", firstEmployeeUserId(formData.get(THIRD_LEVEL_DEPARTMENT_MANAGER_FIELD_ID + "_id")));
-        result.put("fourthLevelManagerUserId", firstEmployeeUserId(formData.get(FOURTH_LEVEL_DEPARTMENT_MANAGER_FIELD_ID + "_id")));
-        result.put("departmentHeadUserId", firstEmployeeUserId(formData.get(DEPARTMENT_HEAD_FIELD_ID + "_id")));
-        result.put("departmentLeaderUserId", firstEmployeeUserId(formData.get(DEPARTMENT_LEADER_FIELD_ID + "_id")));
-        return result;
-    }
-
     private Map firstFormData(Object records) {
         if (!(records instanceof Iterable)) {
             return null;
@@ -396,30 +329,6 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         }
     }
 
-    /**
-     * 按宜搭部门选择字段(DepartmentSelectField)的等值查询格式生成 searchFieldJson。
-     *
-     * <p>部门字段存储为 {@code [{"_id":"部门ID","label":"部门名"}]},需以部门 ID 作为 value,
-     * 并使用 {@code DepartmentSelectField} 组件类型;沿用报表字段配置约定 {@code dataType=STRING}。
-     *
-     * @param fieldId 宜搭部门字段 ID
-     * @param value 需要精确匹配的部门 ID
-     * @return 宜搭搜索条件 JSON
-     */
-    private String createDepartmentSearchFieldJson(String fieldId, String value) {
-        Map<String, String> condition = new LinkedHashMap<String, String>();
-        condition.put("key", fieldId);
-        condition.put("value", value);
-        condition.put("type", "STRING");
-        condition.put("operator", "eq");
-        condition.put("componentName", "DepartmentSelectField");
-        try {
-            return objectMapper.writeValueAsString(Collections.singletonList(condition));
-        } catch (JsonProcessingException exception) {
-            throw new IllegalStateException("Unable to create Yida department search condition", exception);
-        }
-    }
-
     private String firstDepartmentId(Object departmentIds) {
         if (departmentIds instanceof Iterable) {
             for (Object departmentId : (Iterable<?>) departmentIds) {

+ 0 - 2
src/main/java/com/malk/minglei/service/impl/yida/MiscReceiptIssueFormPushHandler.java

@@ -48,7 +48,6 @@ 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 DOCUMENT_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";
@@ -164,7 +163,6 @@ 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, DOCUMENT_NO_FIELD_ID, docNo);
 
         List<Map<String, Object>> rows = toDetailRows(detailRows);
         if (!rows.isEmpty()) {

+ 31 - 48
src/main/java/com/malk/minglei/service/impl/yida/MlWorkOrderCraftPriceFormPushHandler.java

@@ -28,12 +28,11 @@ import java.util.Map;
 @Service
 public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler {
 
-    public static final String TYPE = "csfp340";
-
     private static final Logger LOGGER = LoggerFactory.getLogger(MlWorkOrderCraftPriceFormPushHandler.class);
     private static final String START_PROCESS_ENDPOINT = "/v1.0/yida/processes/instances/start";
     private static final String FORM_NAME = "ML-工单工艺工价审定";
     private static final String FORM_UUID = "FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K";
+    public static final String TYPE = FORM_UUID;
     private static final String PROCESS_CODE = "TPROC--WG966BA1VWF8Q1K4OGF8H9KW47RR3ZREH0YSM2";
     private static final String DEFAULT_ORIGINATOR_USER_ID = "41603502201288023";
 
@@ -149,41 +148,35 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
 
     private Map<String, Object> toYidaFormData(ErpYidaPushRequest request, String operatorUserId) {
         ErpYidaPushMainData mainData = request.getMainData();
-        Map<String, Object> mainFields = toMainDataMap(mainData);
         Map<String, Object> detail = toDetailRecord(request.getDetailData());
-        Object workOrderNo = firstNonBlank(mainFields.get("sfaadocno"),
-                value(detail, "工单号", "workOrderNo", "gdh"));
-        String title = isBlank(workOrderNo) ? null : FORM_NAME + "-" + toText(workOrderNo);
 
         Map<String, Object> formData = new LinkedHashMap<String, Object>();
-        putIfNotBlank(formData, TITLE_FIELD_ID, firstNonBlank(title, value(detail, "标题", "title", "dataTitle")));
+        putIfNotBlank(formData, TITLE_FIELD_ID, firstNonBlank(value(detail, "标题", "title", "dataTitle"), value(detail, "工单号", "workOrderNo", "gdh")));
         putIfNotBlank(formData, APPLICANT_FIELD_ID, toStringList(operatorUserId, null));
-        putIfNotBlank(formData, COMPANY_CODE_FIELD_ID, firstNonBlank(mainFields.get("gs"), value(detail, "申请公司", "applyCompany", "company", "gs", "companyCode")));
-        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(firstNonBlank(mainFields.get("bm"), value(detail, "申请部门", "applyDepartment", "department", "deptId")), null));
-        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(value(detail, "申请日期", "applyDate", "date", "rq"), mainFields.get("rq"))));
+        putIfNotBlank(formData, COMPANY_CODE_FIELD_ID, firstNonBlank(mainData == null ? null : mainData.getGs(), value(detail, "申请公司", "applyCompany", "company", "gs", "companyCode")));
+        putIfNotBlank(formData, DEPARTMENT_FIELD_ID, toStringList(firstNonBlank(mainData == null ? null : mainData.getBm(), value(detail, "申请部门", "applyDepartment", "department", "deptId")), null));
+        putIfNotBlank(formData, APPLY_DATE_FIELD_ID, toDateMillis(firstNonBlank(value(detail, "申请日期", "applyDate", "date", "rq"), mainData == null ? null : mainData.getRq())));
         putIfNotBlank(formData, APPLICANT_POSITION_FIELD_ID, value(detail, "申请人职位", "applicantPosition", "position"));
         putIfNotBlank(formData, MOBILE_FIELD_ID, value(detail, "手机号", "mobile", "phone"));
-        putIfNotBlank(formData, FORM_TITLE_FIELD_ID, firstNonBlank(title, value(detail, "表单标题", "formTitle"), value(detail, "标题", "title", "dataTitle")));
-        putIfNotBlank(formData, WORK_ORDER_NO_FIELD_ID, firstNonBlank(mainFields.get("sfaadocno"), value(detail, "工单号", "workOrderNo", "gdh", "workOrderCode")));
-        putIfNotBlank(formData, PRODUCTION_ORDER_FIELD_ID, firstNonBlank(mainFields.get("sfaaud009"), value(detail, "生产令", "productionOrder", "scl", "productionNo")));
-        putIfNotBlank(formData, PRODUCTION_MATERIAL_FIELD_ID, firstNonBlank(mainFields.get("sfaa010"), value(detail, "生产料号", "productionMaterial", "productionItemCode", "sclh")));
-        putIfNotBlank(formData, ITEM_NAME_FIELD_ID, firstNonBlank(mainFields.get("imaal003"), value(detail, "品名", "itemName", "pm", "productName")));
-        putIfNotBlank(formData, SPECIFICATION_FIELD_ID, firstNonBlank(mainFields.get("imaal004"), value(detail, "规格", "specification", "spec", "gg")));
-        putIfNotBlank(formData, ASSEMBLY_PACKAGING_FIELD_ID, firstNonBlank(mainFields.get("zzbz"), value(detail, "组装|包装", "assemblyPackaging", "zzbz", "assemblyOrPackaging")));
+        putIfNotBlank(formData, FORM_TITLE_FIELD_ID, firstNonBlank(value(detail, "表单标题", "formTitle"), value(detail, "标题", "title", "dataTitle"), value(detail, "工单号", "workOrderNo", "gdh")));
+        putIfNotBlank(formData, WORK_ORDER_NO_FIELD_ID, value(detail, "工单号", "workOrderNo", "gdh", "workOrderCode"));
+        putIfNotBlank(formData, PRODUCTION_ORDER_FIELD_ID, value(detail, "生产令", "productionOrder", "scl", "productionNo"));
+        putIfNotBlank(formData, PRODUCTION_MATERIAL_FIELD_ID, value(detail, "生产料号", "productionMaterial", "productionItemCode", "sclh"));
+        putIfNotBlank(formData, ITEM_NAME_FIELD_ID, value(detail, "品名", "itemName", "pm", "productName"));
+        putIfNotBlank(formData, SPECIFICATION_FIELD_ID, value(detail, "规格", "specification", "spec", "gg"));
+        putIfNotBlank(formData, ASSEMBLY_PACKAGING_FIELD_ID, value(detail, "组装|包装", "assemblyPackaging", "zzbz", "assemblyOrPackaging"));
         putStructuredAssociation(formData, PACKAGING_METHOD_FIELD_ID, value(detail, "包装方式", "packagingMethod"));
-        putIfNotBlank(formData, FIXED_PRICE_FIELD_ID, toDecimal(firstNonBlank(mainFields.get("cjzg"), value(detail, "固定工价", "fixedPrice", "cjzg", "gdgj"))));
-        putIfNotBlank(formData, TOTAL_UNIT_PRICE_FIELD_ID, toDecimal(firstNonBlank(mainFields.get("gdgj"), value(detail, "总单位工价", "totalUnitPrice", "gdgj", "zdwgj"))));
-        putIfNotBlank(formData, MATERIAL_T100_ID_FIELD_ID, mainFields.get("t100id"));
-        putIfNotBlank(formData, MATERIAL_ENTERPRISE_NO_FIELD_ID, mainFields.get("sfaaent"));
+        putIfNotBlank(formData, FIXED_PRICE_FIELD_ID, toDecimal(value(detail, "固定工价", "fixedPrice", "gdgj")));
+        putIfNotBlank(formData, TOTAL_UNIT_PRICE_FIELD_ID, toDecimal(value(detail, "总单位工价", "totalUnitPrice", "zdwgj")));
 
         List<Map<String, Object>> processRows = toProcessRows(firstNonNull(
-                detail.get("明细表1"), detail.get("processData"), detail.get("processList"), detail.get("gongxuData"), detail.get("detailData")));
+                detail.get("明细表1"), detail.get("processData"), detail.get("processList"), detail.get("gongxuData"), detail.get("detailData1"), detail.get("detailData")));
         if (!processRows.isEmpty()) {
             formData.put(PROCESS_TABLE_FIELD_ID, processRows);
         }
 
         List<Map<String, Object>> materialRows = toMaterialRows(firstNonNull(
-                detail.get("明细表2"), detail.get("materialData"), detail.get("materialList"), detail.get("bomData"), detail.get("bomList"), detail.get("detailData1"), detail.get("detailData2")));
+                detail.get("明细表2"), detail.get("materialData"), detail.get("materialList"), detail.get("bomData"), detail.get("bomList"), detail.get("detailData2")));
         if (!materialRows.isEmpty()) {
             formData.put(MATERIAL_TABLE_FIELD_ID, materialRows);
         }
@@ -221,10 +214,10 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
                 continue;
             }
             Map<String, Object> row = new LinkedHashMap<String, Object>();
-            putIfNotBlank(row, MATERIAL_SEQ_FIELD_ID, value(source, "项次", "sfbaseq", "seq", "xc"));
-            putIfNotBlank(row, MATERIAL_CODE_FIELD_ID, value(source, "料号", "sfba006", "materialCode", "lh"));
-            putIfNotBlank(row, MATERIAL_NAME_FIELD_ID, value(source, "品名", "imaal003", "materialName", "pm", "name"));
-            putIfNotBlank(row, MATERIAL_SPECIFICATION_FIELD_ID, value(source, "规格", "imaal004", "specification", "spec", "gg"));
+            putIfNotBlank(row, MATERIAL_SEQ_FIELD_ID, value(source, "项次", "seq", "xc"));
+            putIfNotBlank(row, MATERIAL_CODE_FIELD_ID, value(source, "料号", "materialCode", "lh"));
+            putIfNotBlank(row, MATERIAL_NAME_FIELD_ID, value(source, "品名", "materialName", "pm", "name"));
+            putIfNotBlank(row, MATERIAL_SPECIFICATION_FIELD_ID, value(source, "规格", "specification", "spec", "gg"));
             putIfNotBlank(row, MATERIAL_USAGE_FIELD_ID, value(source, "用量", "usage", "yl"));
             putIfNotBlank(row, MATERIAL_T100_ID_FIELD_ID, value(source, "t100id", "t100Id"));
             putIfNotBlank(row, MATERIAL_LINE_LEADER_FIELD_ID, value(source, "车间线长", "lineLeader", "cjxz"));
@@ -274,17 +267,22 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
         return null;
     }
 
-    private Object firstNonNull(Object first, Object... others) {
+    private Object firstNonNull(Object first, Object second, Object third, Object fourth, Object fifth, Object sixth) {
         if (first != null) {
             return first;
         }
-        for (Object other : others) {
-            if (other != null) {
-                return other;
-            }
+        if (second != null) {
+            return second;
         }
-        return null;
+        if (third != null) {
+            return third;
+        }
+        if (fourth != null) {
+            return fourth;
+        }
+        return fifth == null ? sixth : fifth;
     }
+
     private List<?> toList(Object value) {
         if (value instanceof List) {
             return (List<?>) value;
@@ -387,21 +385,6 @@ public class MlWorkOrderCraftPriceFormPushHandler implements YidaFormPushHandler
         return detail;
     }
 
-    private Map<String, Object> toMainDataMap(ErpYidaPushMainData mainData) {
-        if (mainData == null) {
-            return Collections.emptyMap();
-        }
-        Map<String, Object> result = new LinkedHashMap<String, Object>();
-        putIfNotBlank(result, "rq", mainData.getRq());
-        putIfNotBlank(result, "bm", mainData.getBm());
-        putIfNotBlank(result, "tjr", mainData.getTjr());
-        putIfNotBlank(result, "gs", mainData.getGs());
-        putIfNotBlank(result, "t100id", mainData.getT100id());
-        putIfNotBlank(result, "ent", mainData.getEnt());
-        putIfNotBlank(result, "t100bm", mainData.getT100bm());
-        result.putAll(mainData.getExtraFields());
-        return result;
-    }
     private boolean isBlank(Object value) {
         if (value == null) {
             return true;

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

@@ -16,13 +16,4 @@ public interface MingleiPortService {
      * @return 外部接口原始响应体
      */
     Map<String, Object> submitSupplierPriceApproval(MingleiPortRequest request);
-
-    /**
-     * 将不同业务单据的 T100 parameter 节点按统一协议外层转发。
-     *
-     * @param parameter T100 payload.std_data.parameter 节点
-     * @param dataKeyType T100 datakey.type 业务类型
-     * @return 外部接口原始响应内容
-     */
-    Map<String, Object> submitT100Document(Map<String, Object> parameter, String dataKeyType);
 }

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

@@ -27,14 +27,6 @@ public interface MingleiDocumentService {
      */
     Map<String, Object> queryDocuments(MingleiDocumentQueryRequest request);
 
-    /**
-     * 按"部门"选择字段中内嵌的部门 ID 精确查询审批矩阵,返回各级别负责人 userid。
-     *
-     * @param departmentId 钉钉部门 ID,与审批矩阵"部门"字段的 _id 精确匹配
-     * @return 包含一级至四级部门负责人、部门负责人和部门分管领导的语义化结果
-     */
-    Map<String, Object> queryDepartmentManagersByDepartmentId(String departmentId);
-
     /**
      * 按查询范围获取指定部门中的钉钉用户 ID。
      *

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

@@ -16,12 +16,4 @@ public interface YidaProcessOpenApiClient {
      * @return 钉钉 OpenAPI 原始响应
      */
     DDR_New startProcess(Map<String, Object> body);
-
-    /**
-     * 按流程实例 ID 更新宜搭流程表单字段。
-     *
-     * @param body 包含表单实例 ID、流程实例 ID 与 updateFormDataJson 的更新请求体
-     * @return 钉钉 OpenAPI 原始响应
-     */
-    DDR_New updateProcessInstance(Map<String, Object> body);
 }

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

@@ -1,10 +1,5 @@
 server:
   port: ${SERVER_PORT:80}
-  servlet:
-    encoding:
-      charset: UTF-8
-      enabled: true
-      force: true
   tomcat:
     accesslog:
       enabled: true
@@ -26,25 +21,6 @@ dingtalk:
   app-secret: ${DINGTALK_APP_SECRET:z1YxVWLauwNdns9cyIJEg58Lz0S2XLdxMmbOkk-xISmeHoKdmusyt2xB4N8vpIFj}
 
 minglei:
-  banking:
-    # 正式环境:支付、支付状态、流水、余额、支行查询和电子回单下载均使用该地址。
-    base-url: ${MINGLEI_BANKING_BASE_URL:https://open.nexbankapi.com}
-    # 支付状态补偿任务:复用 employee-roster 数据源,表结构见 sql/banking_payment_tracking.sql。
-    pending-payment:
-      table-name: ${MINGLEI_BANKING_PENDING_PAYMENT_TABLE_NAME:banking_payment_tracking}
-      schedule-fixed-delay-millis: ${MINGLEI_BANKING_PENDING_PAYMENT_SCHEDULE_FIXED_DELAY_MILLIS:300000}
-    payment-encryption-enabled: ${MINGLEI_BANKING_PAYMENT_ENCRYPTION_ENABLED:true}
-    app-id: ${MINGLEI_BANKING_APP_ID:895565822f9544658035b3b14238d5b7}
-    app-secret: ${MINGLEI_BANKING_APP_SECRET:qb8cX3n1zwmJH5qDRQpBuRqE5imrJesLEkWJK5xTk0QY}
-    customer-code: ${MINGLEI_BANKING_CUSTOMER_CODE:CUST384D21B943304769}
-    intra-bank-name: ${MINGLEI_BANKING_INTRA_BANK_NAME:交通银行}
-    receipt-file-storage-path: ${MINGLEI_BANKING_RECEIPT_FILE_STORAGE_PATH:/minglei/pay_file}
-    public-base-url: ${MINGLEI_BANKING_PUBLIC_BASE_URL:http://www.minglei.online:55580}
-    # 支付国密模式使用 16 进制 SM2 公钥/私钥;请通过环境变量注入,禁止提交真实密钥。
-    xencio-public-key: ${MINGLEI_BANKING_XENCIO_PUBLIC_KEY:MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEcPG7quzrHQlrIuAV3WhMSvjvOZSyocmwTTAQkwazxMv5+KTbzFn29ZObRu1La90OOU3Q7HSsqrhkYK80VV/9Vg==}
-    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-url: ${MINGLEI_SUPPLIER_PRICE_APPROVAL_URL:http://10.0.2.60/wtoptst/ws/r/awsp920}
     supplier-price-approval-key: ${MINGLEI_SUPPLIER_PRICE_APPROVAL_KEY:f5458f5c0f9022db743a7c0710145903}
@@ -63,13 +39,6 @@ minglei:
       password: ${MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_PASSWORD:Minglei_2026}
       driver-class-name: ${MINGLEI_EMPLOYEE_ROSTER_DATASOURCE_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
 
-  dictionary:
-    datasource:
-      url: ${MINGLEI_DICTIONARY_DATASOURCE_URL:jdbc:oracle:thin:@10.0.2.61:1521:topprd}
-      username: ${MINGLEI_DICTIONARY_DATASOURCE_USERNAME:DS_QRY}
-      password: ${MINGLEI_DICTIONARY_DATASOURCE_PASSWORD:dsqry}
-      driver-class-name: ${MINGLEI_DICTIONARY_DATASOURCE_DRIVER_CLASS_NAME:oracle.jdbc.OracleDriver}
-
 logging:
   file:
     path: ${LOGGING_FILE_PATH:logs}

+ 0 - 6
src/main/resources/db/employee_roster.sql

@@ -9,12 +9,6 @@ CREATE TABLE employee_roster (
   job_grade_id VARCHAR(64) DEFAULT NULL COMMENT '职等ID',
   department VARCHAR(128) DEFAULT NULL COMMENT '部门',
   first_department VARCHAR(128) DEFAULT NULL COMMENT '一级部门',
-  hire_date DATE DEFAULT NULL COMMENT '入职时间',
-  position_name VARCHAR(128) DEFAULT NULL COMMENT '职位',
-  bank_card_no VARCHAR(64) DEFAULT NULL COMMENT '银行卡号',
-  bank_name VARCHAR(128) DEFAULT NULL COMMENT '开户行',
-  bank_branch_code VARCHAR(64) DEFAULT NULL COMMENT '支行联行号',
-  is_resigned TINYINT NOT NULL DEFAULT 1 COMMENT '人员状态:1在职,0离职',
   PRIMARY KEY (id),
   UNIQUE KEY uk_dingtalk_id (dingtalk_id),
   UNIQUE KEY uk_employee_no (employee_no),

+ 0 - 41
src/test/java/com/malk/minglei/controller/EmployeeRosterControllerTest.java

@@ -3,8 +3,6 @@ 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.DingTalkDedicatedAccountCreateRequest;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateResult;
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterQueryRequest;
 import com.malk.minglei.service.employee.EmployeeRosterService;
@@ -13,8 +11,6 @@ import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 
 import java.util.Map;
-import java.util.Arrays;
-import java.util.Collections;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.BDDMockito.given;
@@ -22,33 +18,6 @@ import static org.mockito.Mockito.mock;
 
 class EmployeeRosterControllerTest {
 
-    @Test
-    void createDingTalkDedicatedAccountReturnsCreatedEmployeeNo() throws Exception {
-        EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
-        DingTalkDedicatedAccountCreateRequest request = new DingTalkDedicatedAccountCreateRequest();
-        request.setOaProcessInstanceId("oa-instance-1");
-        request.setName("张三");
-        request.setEmployeeNo("1001");
-        request.setDepartmentList(Collections.<Object>singletonList(1L));
-        DingTalkDedicatedAccountCreateResult result = new DingTalkDedicatedAccountCreateResult(
-                "ding-user-1", "1001", Arrays.asList(1L, 2L));
-        given(employeeRosterService.createDingTalkDedicatedAccount(request)).willReturn(result);
-
-        EmployeeRosterController controller = new EmployeeRosterController(employeeRosterService);
-        ResponseEntity<ApiResponse<DingTalkDedicatedAccountCreateResult>> response =
-                controller.createDingTalkDedicatedAccount(request);
-        ObjectMapper objectMapper = new ObjectMapper();
-        Map<String, Object> body = objectMapper.readValue(
-                objectMapper.writeValueAsString(response.getBody()),
-                new TypeReference<Map<String, Object>>() {
-                });
-        Map<?, ?> responseResult = (Map<?, ?>) body.get("result");
-
-        assertEquals(HttpStatus.CREATED, response.getStatusCode());
-        assertEquals(result, response.getBody().getResult());
-        assertEquals("1001", responseResult.get("employeeNo"));
-    }
-
     @Test
     void getUserRosterReturnsRosterInfoFromDatabase() throws Exception {
         EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
@@ -63,11 +32,6 @@ class EmployeeRosterControllerTest {
         info.setJobGradeId("10010");
         info.setDepartment("钉钉顾问");
         info.setFirstDepartment("钉钉顾问");
-        info.setHireDate("2024-01-15");
-        info.setPositionName("研发工程师");
-        info.setBankCardNo("6222020000000001");
-        info.setBankName("交通银行");
-        info.setBankBranchCode("301290000001");
         given(employeeRosterService.findByUserId("41603502201288023")).willReturn(info);
 
         EmployeeRosterController controller = new EmployeeRosterController(employeeRosterService);
@@ -90,10 +54,5 @@ class EmployeeRosterControllerTest {
         assertEquals("10040", result.get("jobTitleId"));
         assertEquals("10", result.get("jobGrade"));
         assertEquals("10010", result.get("jobGradeId"));
-        assertEquals("2024-01-15", result.get("hireDate"));
-        assertEquals("研发工程师", result.get("positionName"));
-        assertEquals("6222020000000001", result.get("bank_card_no"));
-        assertEquals("交通银行", result.get("bank_name"));
-        assertEquals("301290000001", result.get("bank_branch_code"));
     }
 }

+ 1 - 58
src/test/java/com/malk/minglei/controller/YidaProcessControllerTest.java

@@ -3,25 +3,19 @@ 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.DepartmentUserIdsResponse;
-import com.malk.minglei.dto.DepartmentUserQueryRequest;
 import com.malk.minglei.dto.YidaProcessStartRequest;
 import com.malk.minglei.service.yida.MingleiDocumentService;
-import com.malk.minglei.service.yida.YidaProcessInstanceClient;
 import com.malk.minglei.service.yida.YidaProcessService;
 import org.junit.jupiter.api.Test;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 
 /**
  * 验证宜搭流程接口的 HTTP 状态和统一响应体结构。
@@ -35,8 +29,7 @@ class YidaProcessControllerTest {
     void startReturnsProcessInstanceIdInUnifiedResponse() throws Exception {
         YidaProcessService yidaProcessService = mock(YidaProcessService.class);
         given(yidaProcessService.startProcess(any(YidaProcessStartRequest.class))).willReturn("process-instance-1");
-        YidaProcessController controller = new YidaProcessController(yidaProcessService, mock(MingleiDocumentService.class),
-                mock(YidaProcessInstanceClient.class));
+        YidaProcessController controller = new YidaProcessController(yidaProcessService, mock(MingleiDocumentService.class));
 
         ResponseEntity<ApiResponse<Map<String, String>>> response = controller.start(new YidaProcessStartRequest());
         ObjectMapper objectMapper = new ObjectMapper();
@@ -50,54 +43,4 @@ class YidaProcessControllerTest {
         assertEquals(true, body.get("isSuccess"));
         assertEquals("process-instance-1", ((Map<?, ?>) body.get("result")).get("processInstanceId"));
     }
-
-    @Test
-    void listDepartmentUserIdsLeavesResultUnchangedWhenUserIdsIsEmpty() {
-        MingleiDocumentService mingleiDocumentService = mock(MingleiDocumentService.class);
-        given(mingleiDocumentService.listDepartmentUserIds(Arrays.asList(10L), 0))
-                .willReturn(Arrays.asList("department-user-1", "department-user-2"));
-        DepartmentUserQueryRequest request = new DepartmentUserQueryRequest();
-        request.setDepartmentId("10");
-        request.setType(Integer.valueOf(0));
-        request.setUserIds(Collections.<String>emptyList());
-
-        ResponseEntity<DepartmentUserIdsResponse> response = new YidaProcessController(
-                mock(YidaProcessService.class), mingleiDocumentService, mock(YidaProcessInstanceClient.class))
-                .listDepartmentUserIds(request);
-
-        assertEquals(Arrays.asList("department-user-1", "department-user-2"), response.getBody().getResult());
-    }
-
-    @Test
-    void listDepartmentUserIdsAppendsOnlyDistinctSpecifiedUsersAfterDepartmentUsers() {
-        MingleiDocumentService mingleiDocumentService = mock(MingleiDocumentService.class);
-        given(mingleiDocumentService.listDepartmentUserIds(Arrays.asList(10L), 0))
-                .willReturn(Arrays.asList("department-user-1", "department-user-2"));
-        DepartmentUserQueryRequest request = new DepartmentUserQueryRequest();
-        request.setDepartmentId("10");
-        request.setType(Integer.valueOf(0));
-        request.setUserIds(Arrays.asList("specified-user-1", "department-user-1", "specified-user-1"));
-
-        ResponseEntity<DepartmentUserIdsResponse> response = new YidaProcessController(
-                mock(YidaProcessService.class), mingleiDocumentService, mock(YidaProcessInstanceClient.class))
-                .listDepartmentUserIds(request);
-
-        assertEquals(Arrays.asList("department-user-1", "department-user-2", "specified-user-1"),
-                response.getBody().getResult());
-    }
-
-    @Test
-    void getProcessInstanceReturnsYidaInstanceDataWithoutStartingErpPush() {
-        YidaProcessInstanceClient yidaProcessInstanceClient = mock(YidaProcessInstanceClient.class);
-        Map<String, Object> expected = Collections.<String, Object>singletonMap("formUuid", "FORM-001");
-        given(yidaProcessInstanceClient.getProcessInstance("process-instance-1")).willReturn(expected);
-        YidaProcessController controller = new YidaProcessController(mock(YidaProcessService.class),
-                mock(MingleiDocumentService.class), yidaProcessInstanceClient);
-
-        ResponseEntity<Map<String, Object>> response = controller.getProcessInstance("process-instance-1");
-
-        assertEquals(HttpStatus.OK, response.getStatusCode());
-        assertEquals(expected, response.getBody());
-        verify(yidaProcessInstanceClient).getProcessInstance("process-instance-1");
-    }
 }

+ 8 - 317
src/test/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImplTest.java

@@ -1,12 +1,8 @@
 package com.malk.minglei.service.impl.employee;
 
 import com.malk.minglei.config.EmployeeRosterProperties;
-import com.alibaba.fastjson.JSON;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateRequest;
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateResult;
 import com.malk.minglei.dto.EmployeeRosterInfo;
 import com.malk.minglei.dto.EmployeeRosterSyncResult;
-import com.malk.minglei.service.approval.DingTalkApprovalClient;
 import com.malk.minglei.service.employee.DingTalkEmployeeRosterClient;
 import com.malk.server.dingtalk.DDConf;
 import com.malk.service.dingtalk.DDClient;
@@ -19,231 +15,19 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import java.time.LocalDate;
-import java.time.ZoneId;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.ArgumentMatchers.anyMap;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 class EmployeeRosterServiceImplTest {
 
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void createDingTalkDedicatedAccountUsesProvidedEmployeeNoForLoginCredentials() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        DingTalkApprovalClient approvalClient = mock(DingTalkApprovalClient.class);
-        EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(approvalClient.getProcessInstanceDetail("access-token", "oa-instance-1"))
-                .willReturn(dedicatedAccountApproval("张三", "1001", mapOf("id", "1"), "研发工程师", "男",
-                        "28", "2026-11-01", "高级工程师", "P6", "TEST-ID-001", "软件工程师"));
-        given(contactsClient.createUser_dingTalk(eq("access-token"), eq("1001"), eq("ML1001"), eq("张三"),
-                anyList(), anyMap())).willReturn(mapOf("userid", "ding-user-1"));
-        given(repository.upsertAll(anyList())).willReturn(1);
-
-        DingTalkDedicatedAccountCreateRequest request = new DingTalkDedicatedAccountCreateRequest();
-        request.setOaProcessInstanceId("oa-instance-1");
-        request.setName("张三");
-        request.setEmployeeNo("1001");
-        request.setPositionName("研发工程师");
-        request.setGender("男");
-        request.setAge(Integer.valueOf(28));
-        request.setExpectedArrivalDate(LocalDate.of(2026, 11, 1));
-        request.setJobTitle("高级工程师");
-        request.setJobGrade("P6");
-        request.setIdCardNo("TEST-ID-001");
-        request.setDepartmentList(Arrays.<Object>asList(
-                mapOf("id", "1"),
-                mapOf("value", mapOf("dept_id", 2L)),
-                "1,3"));
-
-        EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
-                dingTalkClient,
-                contactsClient,
-                new DDConf(),
-                mock(DingTalkEmployeeRosterClient.class),
-                repository,
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class),
-                approvalClient);
-
-        DingTalkDedicatedAccountCreateResult result = service.createDingTalkDedicatedAccount(request);
-
-        ArgumentCaptor<List> departmentIdsCaptor = ArgumentCaptor.forClass(List.class);
-        ArgumentCaptor<Map> extraFieldsCaptor = ArgumentCaptor.forClass(Map.class);
-        ArgumentCaptor<List> rosterRecordsCaptor = ArgumentCaptor.forClass(List.class);
-        verify(contactsClient).createUser_dingTalk(eq("access-token"), eq("1001"), eq("ML1001"), eq("张三"),
-                departmentIdsCaptor.capture(), extraFieldsCaptor.capture());
-        verify(repository, never()).reserveNextEmployeeNo(any(java.time.LocalDate.class));
-        verify(repository).upsertAll(rosterRecordsCaptor.capture());
-        assertEquals(Collections.singletonList(1L), departmentIdsCaptor.getValue());
-        assertEquals("1001", extraFieldsCaptor.getValue().get("job_number"));
-        assertEquals("软件工程师", extraFieldsCaptor.getValue().get("title"));
-        assertEquals("1", extraFieldsCaptor.getValue().get("gender"));
-        assertEquals(Long.valueOf(LocalDate.of(2026, 11, 1).atStartOfDay(ZoneId.of("Asia/Shanghai"))
-                .toInstant().toEpochMilli()), extraFieldsCaptor.getValue().get("hired_date"));
-        Map<String, Object> extension = JSON.parseObject((String) extraFieldsCaptor.getValue().get("extension"));
-        assertEquals("研发工程师", extension.get("positionName"));
-        assertEquals("高级工程师", extension.get("jobTitle"));
-        assertEquals(Integer.valueOf(28), extension.get("age"));
-        assertEquals("P6", extension.get("jobGrade"));
-        assertEquals("TEST-ID-001", extension.get("idCardNo"));
-        EmployeeRosterRecord savedRecord = (EmployeeRosterRecord) rosterRecordsCaptor.getValue().get(0);
-        assertEquals("ding-user-1", savedRecord.getDingtalkId());
-        assertEquals("1001", savedRecord.getEmployeeNo());
-        assertEquals("张三", savedRecord.getUsername());
-        assertEquals("研发工程师", savedRecord.getPositionName());
-        assertEquals("2026-11-01", savedRecord.getHireDate());
-        assertEquals("高级工程师", savedRecord.getJobTitle());
-        assertEquals("P6", savedRecord.getJobGrade());
-        assertEquals("ding-user-1", result.getUserId());
-        assertEquals("1001", result.getEmployeeNo());
-        assertEquals(Collections.singletonList(1L), result.getDepartmentIds());
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void createDingTalkDedicatedAccountGeneratesEmployeeNoWhenItIsNotProvided() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        DingTalkApprovalClient approvalClient = mock(DingTalkApprovalClient.class);
-        EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(approvalClient.getProcessInstanceDetail("access-token", "oa-instance-2"))
-                .willReturn(dedicatedAccountApproval("李四", null, mapOf("id", "1"), null, null,
-                        null, null, null, null, null, null));
-        given(repository.reserveNextEmployeeNo(any(java.time.LocalDate.class))).willReturn("2610260001");
-        given(repository.upsertAll(anyList())).willReturn(1);
-        given(contactsClient.createUser_dingTalk(eq("access-token"), eq("2610260001"), eq("ML2610260001"), eq("李四"),
-                anyList(), anyMap())).willReturn(mapOf("userid", "ding-user-2"));
-
-        DingTalkDedicatedAccountCreateRequest request = new DingTalkDedicatedAccountCreateRequest();
-        request.setOaProcessInstanceId("oa-instance-2");
-        request.setName("李四");
-        request.setDepartmentList(Collections.<Object>singletonList(1L));
-
-        EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
-                dingTalkClient,
-                contactsClient,
-                new DDConf(),
-                mock(DingTalkEmployeeRosterClient.class),
-                repository,
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class),
-                approvalClient);
-
-        DingTalkDedicatedAccountCreateResult result = service.createDingTalkDedicatedAccount(request);
-
-        verify(repository).reserveNextEmployeeNo(any(java.time.LocalDate.class));
-        verify(contactsClient).createUser_dingTalk(eq("access-token"), eq("2610260001"), eq("ML2610260001"), eq("李四"),
-                anyList(), anyMap());
-        assertEquals("2610260001", result.getEmployeeNo());
-    }
-
-    @Test
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    void createDingTalkDedicatedAccountResolvesDepartmentNameToDepartmentId() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        DingTalkApprovalClient approvalClient = mock(DingTalkApprovalClient.class);
-        EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(approvalClient.getProcessInstanceDetail("access-token", "oa-instance-3"))
-                .willReturn(dedicatedAccountApproval("王五", "1002", "集团运营中心-人力资源部", null, null,
-                        null, null, null, null, null, null));
-        given(contactsClient.listSubDepartmentDetail("access-token", 1L)).willReturn(
-                Collections.singletonList(department(10L, "集团运营中心", 1L)));
-        given(contactsClient.listSubDepartmentDetail("access-token", 10L)).willReturn(
-                Collections.singletonList(department(20L, "人力资源部", 10L)));
-        given(contactsClient.createUser_dingTalk(eq("access-token"), eq("1002"), eq("ML1002"), eq("王五"),
-                anyList(), anyMap())).willReturn(mapOf("userid", "ding-user-3"));
-        given(repository.upsertAll(anyList())).willReturn(1);
-
-        DingTalkDedicatedAccountCreateRequest request = new DingTalkDedicatedAccountCreateRequest();
-        request.setOaProcessInstanceId("oa-instance-3");
-        request.setName("王五");
-        request.setEmployeeNo("1002");
-        request.setDepartmentList(Collections.<Object>singletonList("集团运营中心-人力资源部"));
-
-        EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
-                dingTalkClient,
-                contactsClient,
-                new DDConf(),
-                mock(DingTalkEmployeeRosterClient.class),
-                repository,
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class),
-                approvalClient);
-
-        service.createDingTalkDedicatedAccount(request);
-
-        ArgumentCaptor<List> departmentIdsCaptor = ArgumentCaptor.forClass(List.class);
-        verify(contactsClient).listSubDepartmentDetail("access-token", 1L);
-        verify(contactsClient).listSubDepartmentDetail("access-token", 10L);
-        verify(contactsClient, never()).getDepartmentDetail_all("access-token", true);
-        verify(contactsClient).createUser_dingTalk(eq("access-token"), eq("1002"), eq("ML1002"), eq("王五"),
-                departmentIdsCaptor.capture(), anyMap());
-        assertEquals(Collections.singletonList(20L), departmentIdsCaptor.getValue());
-    }
-
-    @Test
-    void createDingTalkDedicatedAccountRejectsUnknownDepartmentName() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(contactsClient.listSubDepartmentDetail("access-token", 1L)).willReturn(Collections.<Map>emptyList());
-
-        EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
-                dingTalkClient,
-                contactsClient,
-                new DDConf(),
-                mock(DingTalkEmployeeRosterClient.class),
-                mock(EmployeeRosterRepository.class),
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class));
-
-        IllegalArgumentException exception = org.junit.jupiter.api.Assertions.assertThrows(
-                IllegalArgumentException.class,
-                () -> service.resolveDingTalkDepartmentIds(Collections.<Object>singletonList("不存在的部门")));
-
-        assertEquals("departmentList contains an unknown DingTalk department path: 不存在的部门", exception.getMessage());
-    }
-
-    @Test
-    void createDingTalkDedicatedAccountRejectsAmbiguousDepartmentName() {
-        DDClient dingTalkClient = mock(DDClient.class);
-        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
-        given(dingTalkClient.getAccessToken()).willReturn("access-token");
-        given(contactsClient.listSubDepartmentDetail("access-token", 1L)).willReturn(Arrays.asList(
-                department(10L, "研发部", null),
-                department(20L, "研发部", null)));
-
-        EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
-                dingTalkClient,
-                contactsClient,
-                new DDConf(),
-                mock(DingTalkEmployeeRosterClient.class),
-                mock(EmployeeRosterRepository.class),
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class));
-
-        IllegalArgumentException exception = org.junit.jupiter.api.Assertions.assertThrows(
-                IllegalArgumentException.class,
-                () -> service.resolveDingTalkDepartmentIds(Collections.<Object>singletonList("研发部")));
-
-        assertEquals("departmentList matches multiple DingTalk departments for name: 研发部", exception.getMessage());
-    }
-
     @Test
     void findByEmployeeNoDelegatesToRepository() {
         EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
@@ -257,33 +41,12 @@ class EmployeeRosterServiceImplTest {
                 null,
                 null,
                 repository,
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class));
+                new EmployeeRosterProperties());
 
         assertSame(employee, service.findByEmployeeNo(" 1001 "));
         verify(repository).findByEmployeeNo("1001");
     }
 
-    @Test
-    void findByUsernameDelegatesToRepository() {
-        EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
-        EmployeeRosterInfo employee = new EmployeeRosterInfo();
-        employee.setEmployeeNo("1001");
-        given(repository.findByUsername("张三")).willReturn(employee);
-
-        EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
-                null,
-                null,
-                null,
-                null,
-                repository,
-                new EmployeeRosterProperties(),
-                mock(EmployeeRosterDuplicateNameNotifier.class));
-
-        assertSame(employee, service.findByUsername(" 张三 "));
-        verify(repository).findByUsername("张三");
-    }
-
     @Test
     @SuppressWarnings({"rawtypes", "unchecked"})
     void syncCollectsDepartmentUsersAndUpsertsRosterRecords() {
@@ -291,7 +54,6 @@ class EmployeeRosterServiceImplTest {
         DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
         DingTalkEmployeeRosterClient rosterClient = mock(DingTalkEmployeeRosterClient.class);
         EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
-        EmployeeRosterDuplicateNameNotifier duplicateNameNotifier = mock(EmployeeRosterDuplicateNameNotifier.class);
         DDConf dingTalkConfiguration = new DDConf();
         dingTalkConfiguration.setAgentId(123456L);
 
@@ -308,22 +70,14 @@ class EmployeeRosterServiceImplTest {
                         field("sys00-jobTitle", "职称"),
                         field("sys00-jobGrade", "职等"),
                         field("sys00-department", "部门"),
-                        field("sys00-firstDepartment", "一级部门"),
-                        field("sys00-hireDate", "入职时间"),
-                        field("sys00-position", "职位"),
-                        field("bank-card-no", "银行卡号"),
-                        field("bank-name", "开户行"),
-                        field("bank-branch-code", "支行联行号")))));
+                        field("sys00-firstDepartment", "一级部门")))));
         given(rosterClient.listEmployeeRoster(eq("access-token"), anyList(), eq(123456L), eq(Arrays.asList(
-                "sys00-jobTitle", "sys00-jobGrade", "sys00-department", "sys00-firstDepartment",
-                "sys00-hireDate", "sys00-position", "bank-card-no", "bank-name", "bank-branch-code"))))
+                "sys00-jobTitle", "sys00-jobGrade", "sys00-department", "sys00-firstDepartment"))))
                 .willReturn(Arrays.asList(
                         roster("user-1", "高级工程师", "P6", "技术部", "研发中心"),
                         roster("user-2", "工程师", "P5", "技术部", "研发中心"),
                         roster("user-3", "专员", "P4", "行政部", "职能中心")));
         given(repository.upsertAll(anyList())).willReturn(3);
-        Map<String, Integer> duplicateUsernameCounts = Collections.singletonMap("Alice", Integer.valueOf(2));
-        given(repository.findActiveDuplicateUsernameCounts()).willReturn(duplicateUsernameCounts);
 
         EmployeeRosterServiceImpl service = new EmployeeRosterServiceImpl(
                 dingTalkClient,
@@ -331,15 +85,14 @@ class EmployeeRosterServiceImplTest {
                 dingTalkConfiguration,
                 rosterClient,
                 repository,
-                new EmployeeRosterProperties(),
-                duplicateNameNotifier);
+                new EmployeeRosterProperties());
 
         EmployeeRosterSyncResult result = service.sync(1L, 0);
 
         assertEquals(1L, result.getDepartmentId());
         assertEquals(true, result.isIncludeSubDepartments());
         assertEquals(3, result.getUserCount());
-        assertEquals(9, result.getFieldCount());
+        assertEquals(4, result.getFieldCount());
         assertEquals(3, result.getSyncedCount());
 
         ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
@@ -354,13 +107,6 @@ class EmployeeRosterServiceImplTest {
         assertEquals("10010", records.get(0).getJobGradeId());
         assertEquals("技术部", records.get(0).getDepartment());
         assertEquals("研发中心", records.get(0).getFirstDepartment());
-        assertEquals("2024-01-15", records.get(0).getHireDate());
-        assertEquals("研发工程师", records.get(0).getPositionName());
-        assertEquals("6222020000000001", records.get(0).getBankCardNo());
-        assertEquals("交通银行", records.get(0).getBankName());
-        assertEquals("301290000001", records.get(0).getBankBranchCode());
-        assertEquals(1, records.get(0).getIsResigned());
-        verify(duplicateNameNotifier).notifyDuplicateUsernames(duplicateUsernameCounts);
     }
 
     @Test
@@ -370,13 +116,12 @@ class EmployeeRosterServiceImplTest {
         DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
         DingTalkEmployeeRosterClient rosterClient = mock(DingTalkEmployeeRosterClient.class);
         EmployeeRosterRepository repository = mock(EmployeeRosterRepository.class);
-        EmployeeRosterDuplicateNameNotifier duplicateNameNotifier = mock(EmployeeRosterDuplicateNameNotifier.class);
         DDConf dingTalkConfiguration = new DDConf();
         dingTalkConfiguration.setAgentId(123456L);
 
         given(dingTalkClient.getAccessToken()).willReturn("access-token");
         given(contactsClient.listDeptUserDetail_v2(eq("access-token"), eq(1L), anyMap())).willReturn(mapOf(
-                "list", Collections.singletonList(resignedUser("user-1", "Alice", "1001", "Fallback Title")),
+                "list", Collections.singletonList(userWithTitle("user-1", "Alice", "1001", "Fallback Title")),
                 "has_more", false));
         given(rosterClient.listRosterFieldGroups("access-token", 123456L)).willReturn(Collections.singletonList(mapOf(
                 "field_list", Arrays.asList(
@@ -395,8 +140,7 @@ class EmployeeRosterServiceImplTest {
                 dingTalkConfiguration,
                 rosterClient,
                 repository,
-                new EmployeeRosterProperties(),
-                duplicateNameNotifier);
+                new EmployeeRosterProperties());
 
         service.sync(1L, 1);
 
@@ -407,7 +151,6 @@ class EmployeeRosterServiceImplTest {
         assertNull(records.get(0).getJobTitleId());
         assertNull(records.get(0).getJobGrade());
         assertNull(records.get(0).getJobGradeId());
-        assertEquals(0, records.get(0).getIsResigned());
     }
 
     private static Map<String, Object> user(String userId, String name, String jobNumber) {
@@ -425,12 +168,6 @@ class EmployeeRosterServiceImplTest {
         return user;
     }
 
-    private static Map<String, Object> resignedUser(String userId, String name, String jobNumber, String title) {
-        Map<String, Object> user = userWithTitle(userId, name, jobNumber, title);
-        user.put("active", false);
-        return user;
-    }
-
     private static Map<String, Object> mapOf(String key, Object value) {
         Map<String, Object> map = new LinkedHashMap<String, Object>();
         map.put(key, value);
@@ -443,47 +180,6 @@ class EmployeeRosterServiceImplTest {
         return map;
     }
 
-    private static Map<String, Object> dedicatedAccountApproval(String name, String employeeNo, Object department,
-                                                                 String positionName, String gender, String age,
-                                                                 String expectedArrivalDate, String jobTitle,
-                                                                 String jobGrade, String idCardNo, String position) {
-        List<Map<String, Object>> components = Arrays.asList(
-                approvalComponent("姓名", name),
-                approvalComponent("部门", null, department),
-                approvalComponent("工号", employeeNo),
-                approvalComponent("岗位", positionName),
-                approvalComponent("职位", position),
-                approvalComponent("性别", gender),
-                approvalComponent("年龄", age),
-                approvalComponent("预计到岗日期", expectedArrivalDate),
-                approvalComponent("职称", jobTitle),
-                approvalComponent("职等", jobGrade),
-                approvalComponent("身份证号", idCardNo));
-        return mapOf("success", Boolean.TRUE, "result", mapOf("formComponentValues", components));
-    }
-
-    private static Map<String, Object> approvalComponent(String name, Object value) {
-        return approvalComponent(name, value, null);
-    }
-
-    private static Map<String, Object> approvalComponent(String name, Object value, Object extValue) {
-        Map<String, Object> component = new LinkedHashMap<String, Object>();
-        component.put("name", name);
-        component.put("value", value);
-        component.put("extValue", extValue);
-        return component;
-    }
-
-    private static Map department(Long id, String name, Long parentId) {
-        Map<String, Object> department = new LinkedHashMap<String, Object>();
-        department.put("dept_id", id);
-        department.put("name", name);
-        if (parentId != null) {
-            department.put("parent_id", parentId);
-        }
-        return department;
-    }
-
     private static Map<String, Object> field(String fieldCode, String fieldName) {
         Map<String, Object> field = new LinkedHashMap<String, Object>();
         field.put("field_code", fieldCode);
@@ -496,12 +192,7 @@ class EmployeeRosterServiceImplTest {
                 rosterField("sys00-jobTitle", jobTitle, "10040"),
                 rosterField("sys00-jobGrade", jobGrade, "10010"),
                 rosterField("sys00-department", department),
-                rosterField("sys00-firstDepartment", firstDepartment),
-                rosterField("sys00-hireDate", "2024-01-15"),
-                rosterField("sys00-position", "研发工程师", "position-001"),
-                rosterField("bank-card-no", "6222020000000001"),
-                rosterField("bank-name", "交通银行"),
-                rosterField("bank-branch-code", "301290000001")));
+                rosterField("sys00-firstDepartment", firstDepartment)));
     }
 
     private static Map<String, Object> rosterField(String fieldCode, String value) {

+ 14 - 37
src/test/java/com/malk/minglei/service/impl/port/MingleiPortServiceImplTest.java

@@ -10,13 +10,10 @@ import org.springframework.test.web.client.MockRestServiceServer;
 import org.springframework.web.client.RestTemplate;
 
 import java.util.Map;
-import java.util.LinkedHashMap;
 
-import static org.hamcrest.Matchers.matchesPattern;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.springframework.test.web.client.match.MockRestRequestMatchers.content;
 import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
-import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
 import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
 import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
 import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@@ -35,23 +32,15 @@ class MingleiPortServiceImplTest {
         MingleiPortProperties properties = new MingleiPortProperties();
         properties.setSupplierPriceApprovalUrl("http://external.example/wtoptst/ws/r/awsp920");
         properties.setSupplierPriceApprovalKey("key-1");
-        mingleiPortService = new MingleiPortServiceImpl(restTemplate, properties, objectMapper);
+        mingleiPortService = new MingleiPortServiceImpl(restTemplate, properties);
     }
 
     @Test
     void submitSupplierPriceApprovalForwardsDocumentAndReturnsExternalResponse() throws Exception {
         server.expect(requestTo("http://external.example/wtoptst/ws/r/awsp920"))
                 .andExpect(method(org.springframework.http.HttpMethod.POST))
-                .andExpect(header("Content-Type", MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"))
-                .andExpect(jsonPath("$.key").value("key-1"))
-                .andExpect(jsonPath("$.type").value("sync"))
-                .andExpect(jsonPath("$.host.timestamp").value(matchesPattern("\\d{17}")))
-                .andExpect(jsonPath("$.service.name").value("ProcurementApprovalPrice"))
-                .andExpect(jsonPath("$.datakey.EntId").value("87"))
-                .andExpect(jsonPath("$.datakey.CompanyId").value("ML"))
-                .andExpect(jsonPath("$.payload.std_data.parameter.head.pmdidocno").value("CH3"))
-                .andExpect(jsonPath("$.payload.std_data.parameter.detail[0].pmdj011").value("0.5000"))
-                .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
+                .andExpect(header("Content-Type", MediaType.APPLICATION_JSON_VALUE))
+                .andExpect(content().json(externalRequestJson()))
                 .andRespond(withSuccess("{\"srvver\":\"1.0\",\"srvcode\":\"100\",\"datakey\":null,\"payload\":{\"std_data\":{\"execution\":{\"code\":\"0\",\"sql_code\":null,\"description\":\"SUCCESS\"},\"parameter\":{\"pmdidocno\":\"CH3\",\"tran_time\":\"2026-08-25 10:00:00\"}}}}", MediaType.APPLICATION_JSON));
 
         Map<String, Object> response = mingleiPortService.submitSupplierPriceApproval(readRequest());
@@ -60,29 +49,6 @@ class MingleiPortServiceImplTest {
         server.verify();
     }
 
-    @Test
-    void submitT100DocumentForwardsCustomParameterAndDataKeyType() {
-        server.expect(requestTo("http://external.example/wtoptst/ws/r/awsp920"))
-                .andExpect(method(org.springframework.http.HttpMethod.POST))
-                .andExpect(jsonPath("$.service.name").value("DingTalk To T100"))
-                .andExpect(jsonPath("$.datakey.type").value("供应商暂停/退出流程"))
-                .andExpect(jsonPath("$.payload.std_data.parameter.head.pmaa001").value("SUP-001"))
-                .andExpect(jsonPath("$.payload.std_data.parameter.detail.pmab001").value("SUP-001"))
-                .andRespond(withSuccess("{\"srvcode\":\"100\"}", MediaType.APPLICATION_JSON));
-        Map<String, Object> head = new LinkedHashMap<String, Object>();
-        head.put("pmaa001", "SUP-001");
-        Map<String, Object> detail = new LinkedHashMap<String, Object>();
-        detail.put("pmab001", "SUP-001");
-        Map<String, Object> parameter = new LinkedHashMap<String, Object>();
-        parameter.put("head", head);
-        parameter.put("detail", detail);
-
-        Map<String, Object> response = mingleiPortService.submitT100Document(parameter, "供应商暂停/退出流程");
-
-        assertEquals("100", response.get("srvcode"));
-        server.verify();
-    }
-
     private MingleiPortRequest readRequest() throws Exception {
         return objectMapper.readValue(localRequestJson(), MingleiPortRequest.class);
     }
@@ -94,4 +60,15 @@ class MingleiPortServiceImplTest {
                 + "}";
     }
 
+    private String externalRequestJson() {
+        return "{"
+                + "\"key\":\"key-1\",\"type\":\"sync\","
+                + "\"host\":{\"prod\":\"APP\",\"ip\":\"10.0.2.60\",\"lang\":\"zh_CN\",\"acct\":\"tiptop\",\"timestamp\":\"20260807164700004\"},"
+                + "\"service\":{\"prod\":\"T100\",\"name\":\"ProcurementApprovalPrice\",\"ip\":\"10.0.2.60\",\"id\":\"toptst\"},"
+                + "\"datakey\":{\"EntId\":\"87\",\"CompanyId\":\"ML\"},"
+                + "\"payload\":{\"std_data\":{\"parameter\":{"
+                + "\"head\":{\"pmdidocno\":\"CH3\",\"pmdi001\":\"N\",\"pmdi034\":\"605188\",\"pmdidocdt\":\"2026-08-06\",\"pmdi004\":\"517022\",\"pmdi015\":\"2026-08-06\",\"pmdi016\":\"2027-06-01\",\"pmdi003\":\"A043\",\"pmdi002\":\"210721017\",\"pmdiud001\":\"N\",\"pmdistus\":\"I\"},"
+                + "\"detail\":[{\"pmdj011\":\"0.5000\",\"pmdj002\":\"524000059517022000000\",\"pmdj030\":\"\"}]"
+                + "}}}}";
+    }
 }

+ 0 - 32
src/test/java/com/malk/minglei/service/impl/yida/ErpYidaPushServiceImplTest.java

@@ -197,38 +197,6 @@ class ErpYidaPushServiceImplTest {
         verify(yidaProcessService, never()).startProcess(any(YidaProcessStartRequest.class));
     }
 
-    @Test
-    void acceptsEmptyDetailDataForSupplierImportFormPushHandler() {
-        Logger logger = mock(Logger.class);
-        YidaProcessService yidaProcessService = mock(YidaProcessService.class);
-        EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
-        YidaFormPushHandler formPushHandler = mock(YidaFormPushHandler.class);
-        when(formPushHandler.getTypes()).thenReturn(Collections.singletonList(Apmt100FormPushHandler.TYPE));
-        when(employeeRosterService.findByEmployeeNo("user-001")).thenReturn(employee("dingtalk-user-001"));
-
-        ErpYidaPushRequest request = new ErpYidaPushRequest();
-        request.setType(Apmt100FormPushHandler.TYPE);
-        ErpYidaPushMainData mainData = new ErpYidaPushMainData();
-        mainData.setTjr("user-001");
-        request.setMainData(mainData);
-        request.setDetailData(Collections.emptyMap());
-        when(formPushHandler.push(request)).thenReturn("form-instance-1");
-
-        ErpYidaPushServiceImpl service = new ErpYidaPushServiceImpl(
-                logger,
-                yidaProcessService,
-                Collections.singletonList(formPushHandler),
-                employeeRosterService);
-
-        McR result = service.push(request);
-
-        assertNotNull(result);
-        assertEquals(Collections.emptyMap(), request.getDetailData());
-        assertEquals("dingtalk-user-001", request.getOriginatorUserId());
-        verify(formPushHandler).push(request);
-        verify(yidaProcessService, never()).startProcess(any(YidaProcessStartRequest.class));
-    }
-
     @Test
     void rejectsMainDataTjrWhenEmployeeNoCannotBeResolved() {
         Logger logger = mock(Logger.class);

+ 0 - 35
src/test/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImplTest.java

@@ -8,15 +8,10 @@ import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Contacts;
 import org.junit.jupiter.api.Test;
 
-import java.lang.reflect.Method;
 import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -24,36 +19,6 @@ import static org.mockito.Mockito.verify;
 
 class MingleiDocumentServiceImplTest {
 
-    /**
-     * 验证未命中审批矩阵时仍返回完整负责人字段,调用方可直接读取字段值。
-     */
-    @Test
-    @SuppressWarnings("unchecked")
-    void extractDepartmentManagersReturnsNullFieldsWhenMatrixHasNoMatch() throws Exception {
-        MingleiDocumentServiceImpl service = new MingleiDocumentServiceImpl(
-                mock(YDClient.class),
-                mock(DDClient.class),
-                mock(DDClient_Contacts.class),
-                mock(DDConf.class),
-                new YidaApplicationsProperties(),
-                new ObjectMapper());
-        Map<String, Object> queryResult = new LinkedHashMap<String, Object>();
-        queryResult.put("records", Collections.emptyList());
-
-        Method method = MingleiDocumentServiceImpl.class
-                .getDeclaredMethod("extractDepartmentManagers", Map.class);
-        method.setAccessible(true);
-        Map<String, Object> result = (Map<String, Object>) method.invoke(service, queryResult);
-
-        assertEquals(6, result.size());
-        assertNull(result.get("employeeField_mszdqrl8"));
-        assertNull(result.get("employeeField_mszdqrl9"));
-        assertNull(result.get("employeeField_mszdqrla"));
-        assertNull(result.get("employeeField_mszdqrlb"));
-        assertNull(result.get("employeeField_mualkf6a"));
-        assertNull(result.get("employeeField_mszdqrl3"));
-    }
-
     /**
      * 验证 type 为 0 时会合并当前部门与子部门成员,并保持去重顺序。
      */

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

@@ -110,7 +110,6 @@ 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());

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

@@ -44,17 +44,7 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         ErpYidaPushMainData mainData = new ErpYidaPushMainData();
         mainData.setRq("1799337600000");
         mainData.setGs("浙江明磊锂能源科技股份有限公司");
-        mainData.setT100id("ML");
         mainData.setBm("123456");
-        mainData.setExtraField("sfaadocno", "GD-001");
-        mainData.setExtraField("sfaaud009", "MO-001");
-        mainData.setExtraField("sfaa010", "ITEM-001");
-        mainData.setExtraField("imaal003", "电池包");
-        mainData.setExtraField("imaal004", "24V");
-        mainData.setExtraField("zzbz", "组装");
-        mainData.setExtraField("cjzg", "10.5");
-        mainData.setExtraField("gdgj", "17.5");
-        mainData.setExtraField("sfaaent", "87");
 
         Map<String, Object> processRow = new LinkedHashMap<String, Object>();
         processRow.put("工序编号", "GX-001");
@@ -87,11 +77,10 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         detailRow.put("固定工价", "10.5");
         detailRow.put("总单位工价", "17.5");
         detailRow.put("明细表1", Collections.singletonList(processRow));
-
+        detailRow.put("明细表2", Collections.singletonList(materialRow));
 
         Map<String, Object> detailData = new LinkedHashMap<String, Object>();
         detailData.put("detailData", Collections.singletonList(detailRow));
-        detailData.put("detailData1", Collections.singletonList(materialRow));
 
         ErpYidaPushRequest request = new ErpYidaPushRequest();
         request.setType(MlWorkOrderCraftPriceFormPushHandler.TYPE);
@@ -101,7 +90,7 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
 
         MlWorkOrderCraftPriceFormPushHandler handler = new MlWorkOrderCraftPriceFormPushHandler(yidaClient, ddConf, applications);
 
-        assertEquals(Collections.singletonList("csfp340"), handler.getTypes());
+        assertEquals(Collections.singletonList("FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K"), handler.getTypes());
         assertEquals("process-instance-1", handler.push(request));
 
         ArgumentCaptor<Map> bodyCaptor = ArgumentCaptor.forClass(Map.class);
@@ -114,12 +103,12 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         assertEquals("FORM-4403B45A7F184CB79088FCDAFD2FD925LZ3K", body.get("formUuid"));
         assertEquals(true, body.get("executeExpression"));
         assertEquals(false, body.get("noExecuteExpression"));
-        assertEquals("ML-工单工艺工价审定-GD-001", body.get("title"));
-        assertEquals("ML-工单工艺工价审定-GD-001", body.get("dataTitle"));
+        assertEquals("工单工艺工价审定-GD-001", body.get("title"));
+        assertEquals("工单工艺工价审定-GD-001", body.get("dataTitle"));
 
         JSONObject formData = JSONObject.parseObject(String.valueOf(body.get("formDataJson")));
-        assertEquals("ML-工单工艺工价审定-GD-001", formData.getString("textField_msh27unu"));
-        assertEquals("ML-工单工艺工价审定-GD-001", formData.getString("textField_mspcpz92"));
+        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"));
@@ -154,8 +143,6 @@ class MlWorkOrderCraftPriceFormPushHandlerTest {
         assertEquals("ML", savedMaterialRow.getString("textField_msy0zj6b"));
         assertEquals("leader-001", savedMaterialRow.getString("textField_msy0zj6c"));
         assertEquals("87", savedMaterialRow.getString("textField_msy0zj6h"));
-        assertEquals("ML", formData.getString("textField_msy0zj6b"));
-        assertEquals("87", formData.getString("textField_msy0zj6h"));
     }
 
     @Test