Przeglądaj źródła

OM合同列表定时同步

fyz 1 miesiąc temu
rodzic
commit
3394857560

+ 123 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/common/YqbApiClient.java

@@ -0,0 +1,123 @@
+package com.malk.sdzl.common;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.utils.UtilHttp;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 有成-易企报开放接口 HTTP 客户端。
+ *
+ * <p>统一维护 base-url / appKey / appSecret、accessToken 的缓存与刷新,
+ * 以及各业务接口的 POST 调用与统一响应解析,供辅助选项、发票抬头等业务复用。</p>
+ */
+@Slf4j
+@Component
+public class YqbApiClient {
+
+    /** 获取 accessToken 接口。 */
+    private static final String TOKEN_PATH = "/auth/getAccessToken.rjson";
+    /** accessToken 有效期(分钟)。 */
+    private static final int TOKEN_EXPIRE_MINUTES = 120;
+    /** accessToken 提前刷新时间(分钟)。 */
+    private static final int TOKEN_REFRESH_AHEAD_MINUTES = 30;
+
+    private final Object tokenLock = new Object();
+    private volatile String accessToken;
+    private volatile LocalDateTime accessTokenExpireAt;
+
+    @Value("${yqb.base-url}")
+    private String baseUrl;
+    @Value("${yqb.app-key}")
+    private String appKey;
+    @Value("${yqb.app-secret}")
+    private String appSecret;
+
+    /**
+     * 调用易企报 POST 接口,自动拼接 appKey 与 accessToken。
+     *
+     * @param apiPath     接口路径,例如 /reimburse/corp/invoice/addOrUpdateInvoiceTitle.rjson
+     * @param requestBody 请求体,可为空
+     * @param apiName     接口中文名,仅用于异常提示
+     * @return 易企报统一响应,result 为 1xx 表示调用成功
+     */
+    public YqbApiResponse<Object> post(String apiPath, Map<String, Object> requestBody, String apiName) {
+        String requestUrl = trimRightSlash(baseUrl) + apiPath
+                + "?appKey=" + appKey + "&accessToken=" + getAccessToken();
+
+        Map<String, Object> headers = new HashMap<>();
+        headers.put("Content-Type", "application/json;charset=UTF-8");
+        String responseBody = UtilHttp.doPost(requestUrl, headers, null, requestBody);
+        YqbApiResponse<Object> response = JSON.parseObject(responseBody, YqbApiResponse.class);
+        if (response == null) {
+            throw new IllegalStateException("易企报" + apiName + "接口返回为空");
+        }
+        return response;
+    }
+
+    /**
+     * token 有效期 2 小时;剩余不足 30 分钟时重新获取。
+     * 双重检查与同步锁避免并发请求同时刷新 token。
+     */
+    private String getAccessToken() {
+        if (isTokenUsable()) {
+            return accessToken;
+        }
+        synchronized (tokenLock) {
+            if (isTokenUsable()) {
+                return accessToken;
+            }
+            return refreshAccessToken();
+        }
+    }
+
+    private boolean isTokenUsable() {
+        return accessToken != null
+                && accessTokenExpireAt != null
+                && LocalDateTime.now().plusMinutes(TOKEN_REFRESH_AHEAD_MINUTES).isBefore(accessTokenExpireAt);
+    }
+
+    private String refreshAccessToken() {
+        String requestUrl = trimRightSlash(baseUrl) + TOKEN_PATH;
+        Map<String, Object> headers = new HashMap<>();
+        headers.put("Content-Type", "application/json;charset=UTF-8");
+
+        Map<String, Object> requestBody = new HashMap<>();
+        requestBody.put("appKey", appKey);
+        requestBody.put("appSecret", appSecret);
+
+        String responseBody = UtilHttp.doPost(requestUrl, headers, null, requestBody);
+        JSONObject jsonObject = JSON.parseObject(responseBody);
+        Integer result = jsonObject == null ? null : jsonObject.getInteger("result");
+        String token = jsonObject == null || jsonObject.getJSONObject("data") == null
+                ? null : jsonObject.getJSONObject("data").getString("accessToken");
+
+        if (result == null || result < 100 || result >= 200 || token == null || token.trim().isEmpty()) {
+            String message = jsonObject == null ? responseBody : jsonObject.getString("message");
+            throw new IllegalStateException("获取易企报 accessToken 失败:" + message);
+        }
+
+        accessToken = token;
+        accessTokenExpireAt = LocalDateTime.now().plusMinutes(TOKEN_EXPIRE_MINUTES);
+        log.info("易企报 accessToken 已刷新,过期时间:{}", accessTokenExpireAt);
+        return accessToken;
+    }
+
+    private String trimRightSlash(String url) {
+        if (url == null || url.trim().isEmpty()) {
+            throw new IllegalArgumentException("yqb.base-url 未配置");
+        }
+        String result = url.trim();
+        while (result.endsWith("/")) {
+            result = result.substring(0, result.length() - 1);
+        }
+        return result;
+    }
+}

+ 2 - 2
mjava-sdzl/src/main/java/com/malk/sdzl/controller/DLController.java

@@ -187,9 +187,9 @@ public class DLController {
 
     /**
      * 将宜搭/钉钉 OM 表单审核结果回写到 AMS。
-     * POST /api/sdzl/ams/om/approval/callback
+     * POST /api/sdzl/ams/om/approval/callback(兼容旧入口 /api/sdzl/callback)
      */
-    @PostMapping("/callback")
+    @PostMapping({"/callback", "/ams/om/approval/callback"})
     public McR callback(@RequestBody AmsOmApprovalCallbackRequest request) {
         log.info("开始 AMS OM 审核结果回写,colId={}, oaCode={}, processInstanceId={}",
                 request == null ? null : request.getColId(),

+ 45 - 1
mjava-sdzl/src/main/java/com/malk/sdzl/controller/YqbOptionPushController.java

@@ -1,8 +1,11 @@
 package com.malk.sdzl.controller;
 
 import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.sdzl.entity.dto.YqbCategoryCreateRequest;
+import com.malk.sdzl.entity.dto.YqbInvoiceTitleRequest;
 import com.malk.sdzl.entity.dto.YqbOptionPushRequest;
 import com.malk.sdzl.entity.dto.YqbOptionPushResult;
+import com.malk.sdzl.service.YqbInvoiceTitleService;
 import com.malk.sdzl.service.YqbOptionPushService;
 import com.malk.server.common.McR;
 import lombok.extern.slf4j.Slf4j;
@@ -14,7 +17,7 @@ import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
 
-/** 易企报辅助选项推送接口。 */
+/** 易企报辅助选项推送、发票抬头接口。 */
 @Slf4j
 @RestController
 @RequestMapping("yqb/option")
@@ -23,6 +26,21 @@ public class YqbOptionPushController {
     @Autowired
     private YqbOptionPushService yqbOptionPushService;
 
+    @Autowired
+    private YqbInvoiceTitleService yqbInvoiceTitleService;
+
+    /**
+     * 新增辅助类别。
+     * POST /api/sdzl/yqb/option/category
+     */
+    @PostMapping("/category")
+    public McR createCategory(@RequestBody YqbCategoryCreateRequest request) {
+        log.info("开始新增易企报辅助类别,categoryCode={}",
+                request == null ? null : request.getCategoryCode());
+        YqbApiResponse<Object> result = yqbOptionPushService.createCategory(request);
+        return McR.success(result);
+    }
+
     /**
      * 单条推送。
      * POST /api/sdzl/yqb/option/push
@@ -47,4 +65,30 @@ public class YqbOptionPushController {
         YqbOptionPushResult result = yqbOptionPushService.pushByExcel(file);
         return McR.success(result);
     }
+
+    /**
+     * 新增/编辑发票抬头(公司抬头)。抬头 + 纳税人识别号相同视为同一抬头,做更新处理。
+     * POST /api/sdzl/yqb/option/invoiceTitle
+     *
+     * <p>请求体示例:</p>
+     * <pre>
+     * {
+     *   "invoiceTitle": "深圳某某科技有限公司",
+     *   "taxPayerNo": "91440300MA5XXXXXXX",
+     *   "bankName": "招商银行深圳分行",
+     *   "bankCardNum": "755900000000000",
+     *   "phone": "0755-88888888",
+     *   "address": "深圳市南山区某某大厦 1 栋 101",
+     *   "visible": 1
+     * }
+     * </pre>
+     */
+    @PostMapping("/invoiceTitle")
+    public McR addInvoiceTitle(@RequestBody YqbInvoiceTitleRequest request) {
+        log.info("开始新增易企报发票抬头,invoiceTitle={}, taxPayerNo={}",
+                request == null ? null : request.getInvoiceTitle(),
+                request == null ? null : request.getTaxPayerNo());
+        YqbApiResponse<Object> result = yqbInvoiceTitleService.addOrUpdateInvoiceTitle(request);
+        return McR.success(result);
+    }
 }

+ 55 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/AmsOmApprovalCallbackRequest.java

@@ -0,0 +1,55 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * AMS OM 表单审核通过后回写请求。
+ * 对应外部接口:POST /api/approval/om/save。
+ */
+@Data
+public class AmsOmApprovalCallbackRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 节点主键,必填。 */
+    private String colId;
+    /** OA 编号,必填。 */
+    private String oaCode;
+    /** 宜搭表单流程主键,必填。 */
+    private String processInstanceId;
+    /** 审核状态,必填,例如“审核通过”。 */
+    private String flowState;
+    /** 审核日期,必填,格式 yyyy-MM-dd。 */
+    private String flowDate;
+    /**
+     * 表单名称,必填。支持:OM收入会签单、OM合同作废终止申请单、OM收款单、OM开票申请单。
+     */
+    private String formName;
+    /** 钉钉表单转出的 JSON 字符串,必填。 */
+    private String formContentJson;
+    /** OM 业务主键,非必填。 */
+    private String omId;
+    /** 审批意见,非必填。 */
+    private String message;
+
+    /**
+     * 按接口字段名转换为请求体,保留可选字段的 null 值以兼容外部接口字段绑定。
+     */
+    public Map<String, Object> toRequestBody() {
+        Map<String, Object> body = new LinkedHashMap<>();
+        body.put("colId", colId);
+        body.put("oaCode", oaCode);
+        body.put("processInstanceId", processInstanceId);
+        body.put("flowState", flowState);
+        body.put("flowDate", flowDate);
+        body.put("formName", formName);
+        body.put("formContentJson", formContentJson);
+        body.put("omId", omId);
+        body.put("message", message);
+        return body;
+    }
+}

+ 26 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/AmsOmApprovalCallbackResponse.java

@@ -0,0 +1,26 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * AMS OM 审核回写接口响应。
+ * status 为 200 时表示回写成功。
+ */
+@Data
+public class AmsOmApprovalCallbackResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 错误信息;成功时通常为空字符串。 */
+    private String error;
+    /** 回写节点主键。 */
+    private String colId;
+    /** 返回状态,200 表示成功。 */
+    private Integer status;
+
+    public boolean isSuccess() {
+        return status != null && status == 200;
+    }
+}

+ 66 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/YqbCategoryCreateRequest.java

@@ -0,0 +1,66 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 易企报“新增辅助类别”接口请求实体。
+ */
+@Data
+public class YqbCategoryCreateRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 原辅助类别编码,可为空。 */
+    private String code;
+    /** 辅助类别编码,必填。 */
+    private String categoryCode;
+    /** 辅助核算类别名称,必填。 */
+    private String categoryName;
+    /** 可见范围配置,可为空。 */
+    private YqbOptionPushRequest.VisibleScope visibleScope;
+    /** 是否关联收款账户,默认 false。 */
+    private Boolean relationAccountSwitch;
+    /** 是否关联发票抬头,默认 false。 */
+    private Boolean relationTitleSwitch;
+    /** 是否关联辅助核算,默认 false。 */
+    private Boolean relationAuxiliarySwitch;
+    /** 关联的单个辅助类别编码,可为空。 */
+    private String relationAuxiliaryCategoryCode;
+    /** 关联的多个辅助类别编码,可为空。 */
+    private List<String> relationAuxiliaryCategoryCodeList;
+
+    public Map<String, Object> toRequestBody() {
+        Map<String, Object> body = new LinkedHashMap<>();
+        putIfNotBlank(body, "code", code);
+        putIfNotBlank(body, "categoryCode", categoryCode);
+        putIfNotBlank(body, "categoryName", categoryName);
+        if (visibleScope != null && visibleScope.hasValue()) {
+            body.put("visibleScope", visibleScope);
+        }
+        putIfNotNull(body, "relationAccountSwitch", relationAccountSwitch);
+        putIfNotNull(body, "relationTitleSwitch", relationTitleSwitch);
+        putIfNotNull(body, "relationAuxiliarySwitch", relationAuxiliarySwitch);
+        putIfNotBlank(body, "relationAuxiliaryCategoryCode", relationAuxiliaryCategoryCode);
+        if (relationAuxiliaryCategoryCodeList != null && !relationAuxiliaryCategoryCodeList.isEmpty()) {
+            body.put("relationAuxiliaryCategoryCodeList", relationAuxiliaryCategoryCodeList);
+        }
+        return body;
+    }
+
+    private void putIfNotBlank(Map<String, Object> body, String key, String value) {
+        if (value != null && !value.trim().isEmpty()) {
+            body.put(key, value.trim());
+        }
+    }
+
+    private void putIfNotNull(Map<String, Object> body, String key, Boolean value) {
+        if (value != null) {
+            body.put(key, value);
+        }
+    }
+}

+ 61 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/entity/dto/YqbInvoiceTitleRequest.java

@@ -0,0 +1,61 @@
+package com.malk.sdzl.entity.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 易企报“新增/编辑发票抬头(公司抬头)”接口请求实体。
+ *
+ * <p>对应接口:POST /reimburse/corp/invoice/addOrUpdateInvoiceTitle.rjson</p>
+ */
+@Data
+public class YqbInvoiceTitleRequest implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 可见范围:全部可见。 */
+    public static final int VISIBLE_ALL = 1;
+    /** 可见范围:部分可见。 */
+    public static final int VISIBLE_PART = 2;
+
+    /** 抬头,必填。 */
+    private String invoiceTitle;
+    /** 纳税人识别号,必填。 */
+    private String taxPayerNo;
+    /** 开户行名称,选填。 */
+    private String bankName;
+    /** 银行账号,选填。 */
+    private String bankCardNum;
+    /** 地址,选填。 */
+    private String address;
+    /** 固定电话,选填。 */
+    private String phone;
+    /** 可见范围:1-全部可见,2-部分可见;不传时默认全部可见。 */
+    private Integer visible;
+    /** 可见人员用户 id,多个以英文逗号分隔;visible=2 时必填。 */
+    private String visibleUsers;
+
+    public Map<String, Object> toRequestBody() {
+        Map<String, Object> body = new LinkedHashMap<>();
+        putIfNotBlank(body, "invoiceTitle", invoiceTitle);
+        putIfNotBlank(body, "taxPayerNo", taxPayerNo);
+        putIfNotBlank(body, "bankName", bankName);
+        putIfNotBlank(body, "bankCardNum", bankCardNum);
+        putIfNotBlank(body, "address", address);
+        putIfNotBlank(body, "phone", phone);
+        if (visible != null) {
+            body.put("visible", visible);
+        }
+        putIfNotBlank(body, "visibleUsers", visibleUsers);
+        return body;
+    }
+
+    private void putIfNotBlank(Map<String, Object> body, String key, String value) {
+        if (value != null && !value.trim().isEmpty()) {
+            body.put(key, value.trim());
+        }
+    }
+}

+ 71 - 39
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/CommissionServiceImpl.java

@@ -1584,10 +1584,13 @@ public class CommissionServiceImpl implements CommissionService {
         String supplierName = UtilMap.getString(data, "supplierName");
         String permitCode = UtilMap.getString(data, "permitCode");
         int pageSize = UtilMap.getInt(data, "pageSize");
-        if (pageSize == 0 || pageSize > 100){
-            pageSize = 100;
+        if (pageSize == 0 || pageSize > 50){
+            pageSize = 50;
         }
         int currentPage = UtilMap.getInt(data, "currentPage");
+        if (currentPage <= 0) {
+            currentPage = 1;
+        }
 
 //        if (supplierName.isEmpty() && permitCode.isEmpty()){
 //            return McR.errorParam("permitCode或supplierName必填其一");
@@ -1600,7 +1603,8 @@ public class CommissionServiceImpl implements CommissionService {
         if (!supplierName.isEmpty()){
             searchMap.put("textField_mpfb5ond",supplierName);
         }
-        List<Map> dataList = searchGetFormData(searchMap,currentPage,pageSize,"FORM-3648E003C1614C87AD5EC2709874F975TPYM");
+        DDR_New queryResult = queryFormPage(searchMap, currentPage, pageSize, "FORM-3648E003C1614C87AD5EC2709874F975TPYM");
+        List<Map> dataList = getFormDataList(queryResult);
 
         List<SupplierInfoEntity> supplierInfoEntities = new ArrayList<>();
         if (ObjectUtil.isNotNull(dataList) && dataList.size() > 0){
@@ -1638,7 +1642,12 @@ public class CommissionServiceImpl implements CommissionService {
             });
         }
 
-        return McR.success(supplierInfoEntities);
+        FormPageResponse<SupplierInfoEntity> result = new FormPageResponse<>();
+        result.setCurrentPage(currentPage);
+        result.setPageSize(pageSize);
+        result.setTotalCount(queryResult.getTotalCount());
+        result.setData(supplierInfoEntities);
+        return McR.success(result);
     }
 
     @Override
@@ -1647,10 +1656,13 @@ public class CommissionServiceImpl implements CommissionService {
         String customerName = UtilMap.getString(data, "customerName");
         String permitCode = UtilMap.getString(data, "permitCode");
         int pageSize = UtilMap.getInt(data, "pageSize");
-        if (pageSize == 0 || pageSize > 100){
-            pageSize = 100;
+        if (pageSize == 0 || pageSize > 50){
+            pageSize = 50;
         }
         int currentPage = UtilMap.getInt(data, "currentPage");
+        if (currentPage <= 0) {
+            currentPage = 1;
+        }
 
         Map<String, Object> searchMap = new HashMap<>();
 //        if (customerName.isEmpty() && permitCode.isEmpty()){
@@ -1669,12 +1681,13 @@ public class CommissionServiceImpl implements CommissionService {
 //                .formUuid("FORM-92FEC5C0F0774F89B668095F95B17424D6YG")
 //                .searchFieldJson(JSON.toJSONString(searchMap))
 //                .build());
-        List<Map> dataList = searchGetFormData(searchMap,currentPage,pageSize,"FORM-3648E003C1614C87AD5EC2709874F975TPYM");
+        DDR_New queryResult = queryFormPage(searchMap, currentPage, pageSize, "FORM-3648E003C1614C87AD5EC2709874F975TPYM");
+        List<Map> dataList = getFormDataList(queryResult);
 
-        List<CustomerInfoEntity> customerInfoEntities = new ArrayList<>();
+        List<SupplierInfoEntity> customerInfoEntities = new ArrayList<>();
         if (ObjectUtil.isNotNull(dataList) && dataList.size() > 0){
             dataList.forEach(e->{
-                CustomerInfoEntity customerInfoEntity = new CustomerInfoEntity();
+/*                CustomerInfoEntity customerInfoEntity = new CustomerInfoEntity();
                 customerInfoEntity.setAccountingEntity(UtilMap.getString(e,"textField_mpgn217o"));
                 customerInfoEntity.setOperator(UtilMap.getString(e,"employeeField_mpgn2182"));
                 customerInfoEntity.setJobNumber(UtilMap.getString(e,"textField_mpgom2h8"));
@@ -1697,13 +1710,45 @@ public class CommissionServiceImpl implements CommissionService {
                 customerInfoEntity.setLegalRepresentative(UtilMap.getString(e,"textField_mpgom2i6"));
                 customerInfoEntity.setDateOfEstablishment(UtilMap.getString(e,"dateField_mpgom2i8"));
                 customerInfoEntity.setBusinessTerm(UtilMap.getString(e,"cascadeDateField_mpgozj6i"));
-                customerInfoEntity.setScopeOfBusiness(UtilMap.getString(e,"textareaField_mpgom2ia"));
-
-                customerInfoEntities.add(customerInfoEntity);
+                customerInfoEntity.setScopeOfBusiness(UtilMap.getString(e,"textareaField_mpgom2ia"));*/
+                SupplierInfoEntity supplierInfoEntity = new SupplierInfoEntity();
+                supplierInfoEntity.setAccountingEntity(UtilMap.getString(e,"selectField_tafovni"));
+                supplierInfoEntity.setAccountingSubjectCode(UtilMap.getString(e,"textField_mpfb5on8"));
+                supplierInfoEntity.setNature(UtilMap.getString(e,"selectField_g9kc21s"));
+                supplierInfoEntity.setOperator(UtilMap.getString(e,"employeeField_mpcfgj2r"));
+                supplierInfoEntity.setFullName(UtilMap.getString(e,"textField_mpfb5ond"));
+                supplierInfoEntity.setCompanyWebsite(UtilMap.getString(e,"textField_mpdskkg2"));
+                supplierInfoEntity.setCompanyContactAddress(UtilMap.getString(e,"textField_mpdskkg3"));
+                supplierInfoEntity.setSalesContact(UtilMap.getString(e,"employeeField_mpfb5onf"));
+                supplierInfoEntity.setSalesContactInformation(UtilMap.getString(e,"textField_mpdskkg5"));
+                supplierInfoEntity.setAfterSalesContact(UtilMap.getString(e,"employeeField_mpfb5one"));
+                supplierInfoEntity.setAfterSalesInformation(UtilMap.getString(e,"textField_mpdskkg4"));
+                supplierInfoEntity.setFullNameOfBank(UtilMap.getString(e,"textField_mpfb5onx"));
+                supplierInfoEntity.setBankAccountNumber(UtilMap.getString(e,"textField_mpfb5ony"));
+                supplierInfoEntity.setLegalPersonCollectionBank(UtilMap.getString(e,"selectField_mpfb5oo1"));
+                supplierInfoEntity.setCityBankIsLocated(UtilMap.getString(e,"textField_mrlpiibu"));
+                supplierInfoEntity.setFinancialContact(UtilMap.getString(e,"employeeField_mpfb5oo2"));
+                supplierInfoEntity.setFinancialContactInformation(UtilMap.getString(e,"textField_mpfb5oo3"));
+                supplierInfoEntity.setCreditCodeIDCard(UtilMap.getString(e,"textField_mpfb5oo5"));
+                supplierInfoEntity.setType(UtilMap.getString(e,"selectField_mpfb5ooa"));
+                supplierInfoEntity.setResidence(UtilMap.getString(e,"textField_mpfb5ooh"));
+                supplierInfoEntity.setLegalRepresentative(UtilMap.getString(e,"textField_mpfb5ooi"));
+                supplierInfoEntity.setAmountOfRegisteredCapital(BigDecimal.valueOf(UtilMap.getDouble(e,"numberField_mpfb5ooj")));
+                supplierInfoEntity.setDateOfEstablishment(UtilMap.getString(e,"dateField_mpfb5ook"));
+                supplierInfoEntity.setBusinessTerm(UtilMap.getString(e,"cascadeDateField_mpfb5ool"));
+                supplierInfoEntity.setScopeOfBusiness(UtilMap.getString(e,"textareaField_mpfb5oom"));
+                supplierInfoEntity.setSupplierSerialNumber(UtilMap.getString(e,"textField_mrleu14h"));
+                supplierInfoEntity.setSupplierStatus(UtilMap.getString(e,"radioField_mrlf2d7w"));
+                customerInfoEntities.add(supplierInfoEntity);
             });
         }
 
-        return McR.success(customerInfoEntities);
+        FormPageResponse<SupplierInfoEntity> result = new FormPageResponse<>();
+        result.setCurrentPage(currentPage);
+        result.setPageSize(pageSize);
+        result.setTotalCount(queryResult.getTotalCount());
+        result.setData(customerInfoEntities);
+        return McR.success(result);
     }
 
     @Override
@@ -2137,26 +2182,10 @@ public class CommissionServiceImpl implements CommissionService {
             }
         }
 
-        YDParam ydParam = YDParam.builder()
-                .appType(appType)
-                .systemToken(systemToken)
-                .formUuid(formUuid)
-                .currentPage(currentPage)
-                .pageSize(pageSize)
-                .searchFieldJson(JSON.toJSONString(searchMap))
-                .build();
-        DDR_New queryResult = ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form);
-
-        List<Map> records = (List<Map>) queryResult.getData();
-        List<T> dataList = new ArrayList<>();
-        if (records != null) {
-            dataList = records.stream().map(item -> {
-                Map formData = (Map) item.get("formData");
-                Map result = formData == null ? new HashMap() : new HashMap(formData);
-                result.put("formInstanceId", item.get("formInstanceId"));
-                return converter.apply(result);
-            }).collect(Collectors.toList());
-        }
+        DDR_New queryResult = queryFormPage(searchMap, currentPage, pageSize, formUuid);
+        List<T> dataList = getFormDataList(queryResult).stream()
+                .map(converter)
+                .collect(Collectors.toList());
 
         FormPageResponse<T> result = new FormPageResponse<>();
         result.setCurrentPage(currentPage);
@@ -2185,7 +2214,7 @@ public class CommissionServiceImpl implements CommissionService {
         return entity;
     }
 
-    private List<Map> searchGetFormData(Map<String, Object> searchMap, int currentPage, int pageSize, String formUuid){
+    private DDR_New queryFormPage(Map<String, Object> searchMap, int currentPage, int pageSize, String formUuid) {
         YDParam ydParam = YDParam.builder()
                 .appType(appType)
                 .systemToken(systemToken)
@@ -2194,12 +2223,15 @@ public class CommissionServiceImpl implements CommissionService {
                 .pageSize(pageSize)
                 .searchFieldJson(JSON.toJSONString(searchMap))
                 .build();
-        List<Map> dataList = new ArrayList<>();
-        List<Map> records = (List<Map>) ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form).getData();
-        if (records != null) {
-            dataList.addAll(records);
+        return ydClient.queryData(ydParam, YDConf.FORM_QUERY.retrieve_search_form);
+    }
+
+    private List<Map> getFormDataList(DDR_New queryResult) {
+        List<Map> records = (List<Map>) queryResult.getData();
+        if (records == null) {
+            return new ArrayList<>();
         }
-        return dataList.stream().map(item -> {
+        return records.stream().map(item -> {
             Map formData = (Map) item.get("formData");
             Map result = formData == null ? new HashMap() : new HashMap(formData);
             result.put("instanceId", item.get("formInstanceId"));

+ 281 - 1
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/PayServiceImpl.java

@@ -18,6 +18,7 @@ import com.malk.sdzl.service.PayService;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
 import com.malk.server.common.McR;
+import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Personnel;
@@ -46,6 +47,7 @@ import java.time.LocalDateTime;
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -75,6 +77,9 @@ public class PayServiceImpl implements PayService {
     private YDClient ydClient;
 
     private ConcurrentHashMap<String, LocalDateTime> tokenStore = new ConcurrentHashMap<>();
+    private static final String CONTRACT_FORM_UUID = "FORM-AAC94DE308FA4B40BC149E29C036166CXB6Y";
+    private static final ZoneId SHANGHAI_ZONE = ZoneId.of("Asia/Shanghai");
+    private static final DateTimeFormatter CONTRACT_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     //出差类型枚举
     private static final Map<String, String> BUSINESS_TYPE = new HashMap<>();
 
@@ -1190,6 +1195,7 @@ public class PayServiceImpl implements PayService {
 
         ContractPageListResponse response = getContractList(bodyMap);
         if (ObjectUtil.isNotNull(response) && response.getStatus() == 200){
+
             int totalRows = response.getData().getRows();
             int maxPage =  (totalRows + pageSize - 1)/pageSize;
             for (int i = 1; i <= maxPage; i++) {
@@ -1217,10 +1223,17 @@ public class PayServiceImpl implements PayService {
                                         s.getAchievementAmount()));
                             });
                         }
+
+                        List<Map> searchCondition = new ArrayList<>();
+                        searchCondition.addAll(Arrays.asList(
+                                YDConf.searchCondition_TextFiled("textField_mt804b4o", e.getOmId(), "eq")
+                        ));
+
                         DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
                         ydClient.operateData(YDParam.builder()
                                 .appType(appType)
                                 .systemToken(systemToken)
+                                .searchCondition(JSON.toJSONString(searchCondition))
                                 .formUuid("FORM-AAC94DE308FA4B40BC149E29C036166CXB6Y")
                                 .formDataJson(JSON.toJSONString(UtilMap.map("textField_mt804b4o, " +
                                                 "textField_mt804b4p, " +
@@ -1257,7 +1270,7 @@ public class PayServiceImpl implements PayService {
                                                 .toEpochMilli(),
                                         e.getOurCompany()
                                 )))
-                                .build(), YDConf.FORM_OPERATION.create);
+                                .build(), YDConf.FORM_OPERATION.upsert_v2);
                     });
                 } catch (Exception e) {
                     log.info("处理合同失败:{}",bodyMap);
@@ -1277,6 +1290,132 @@ public class PayServiceImpl implements PayService {
 
     }
 
+    /**
+     * 分页获取合同,只同步 updateDate 为当天的合同。
+     * 宜搭通过 omId 精确匹配,使用 upsert_v2 更新已有记录并补建缺失记录。
+     */
+    @Override
+    public void syncTodayContracts() {
+        LocalDate today = LocalDate.now(SHANGHAI_ZONE);
+        int pageSize = 10;
+        Map<String, Object> bodyMap = new HashMap<>();
+        bodyMap.put("pageIndex", 1);
+        bodyMap.put("pageSize", pageSize);
+        bodyMap.put("beginDate", today.format(CONTRACT_DATE_FORMATTER));
+
+        ContractPageListResponse firstPage = getContractList(bodyMap);
+        if (firstPage == null || firstPage.getData() == null || firstPage.getData().getRows() == null) {
+            log.warn("同步当天合同失败:合同分页接口没有返回有效数据");
+            return;
+        }
+
+        int totalRows = firstPage.getData().getRows();
+        int maxPage = (totalRows + pageSize - 1) / pageSize;
+        int syncedCount = 0;
+        for (int pageIndex = 1; pageIndex <= maxPage; pageIndex++) {
+            try {
+                if (pageIndex > 1 && pageIndex % 10 == 0) {
+                    Thread.sleep(2000L);
+                }
+                ContractPageListResponse pageResponse;
+                if (pageIndex == 1) {
+                    pageResponse = firstPage;
+                } else {
+                    bodyMap.put("pageIndex", pageIndex);
+                    pageResponse = getContractList(bodyMap);
+                }
+                if (pageResponse == null || pageResponse.getData() == null
+                        || pageResponse.getData().getPageList() == null) {
+                    log.warn("同步当天合同跳过无效分页:pageIndex={}", pageIndex);
+                    continue;
+                }
+                for (ContractPageListResponse.ContractInfo contract : pageResponse.getData().getPageList()) {
+                    if (contract != null && today.toString().equals(contract.getUpdateDate())) {
+                        upsertContractToYida(contract);
+                        syncedCount++;
+                    }
+                }
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                log.error("同步当天合同被中断:pageIndex={}", pageIndex, e);
+                return;
+            } catch (Exception e) {
+                log.error("同步当天合同失败:pageIndex={}", pageIndex, e);
+            }
+        }
+        log.info("当天合同同步完成:date={}, totalRows={}, syncedCount={}", today, totalRows, syncedCount);
+    }
+
+    private void upsertContractToYida(ContractPageListResponse.ContractInfo contract) {
+        List<Map> details = new ArrayList<>();
+        if (contract.getEstimateExtendList() != null) {
+            for (EstimateExtendItem item : contract.getEstimateExtendList()) {
+                details.add(UtilMap.map("textField_mt804b50, " +
+                                "textField_mt804b52, " +
+                                "numberField_mt804b54, " +
+                                "numberField_mt804b55, " +
+                                "numberField_mt804b56",
+                        item.getProductName(),
+                        item.getSubProjectName(),
+                        item.getExecuteAmount(),
+                        item.getInvoicedAmount(),
+                        item.getAchievementAmount()));
+            }
+        }
+
+        List<Map> searchCondition = Collections.singletonList(
+                YDConf.searchCondition_TextFiled("textField_mt804b4o", contract.getOmId(), "eq"));
+        ydClient.operateData(YDParam.builder()
+                .appType(appType)
+                .systemToken(systemToken)
+                .searchCondition(JSON.toJSONString(searchCondition))
+                .formUuid(CONTRACT_FORM_UUID)
+                .formDataJson(JSON.toJSONString(UtilMap.map("textField_mt804b4o, " +
+                                "textField_mt804b4p, " +
+                                "textField_mt804b4q, " +
+                                "textField_mt804b4r, " +
+                                "textField_mt804b4s, " +
+                                "textField_mt804b4t, " +
+                                "textField_mt804b4u, " +
+                                "textField_mt804b4v, " +
+                                "textField_mt804b4w, " +
+                                "numberField_mt804b4x, " +
+                                "tableField_mt804b4y, " +
+                                "dateField_mt804b5c, " +
+                                "dateField_mt804b5d, " +
+                                "textField_mti0qoe1",
+                        contract.getOmId(),
+                        contract.getCreateDate(),
+                        contract.getClientName(),
+                        contract.getContractType(),
+                        contract.getContractNo(),
+                        contract.getContractName(),
+                        contract.getOppositeName(),
+                        contract.getClientGroupName(),
+                        contract.getUpdateDate(),
+                        contract.getContractAmount(),
+                        details,
+                        toYidaDate(contract.getCreateDate()),
+                        toYidaDate(contract.getUpdateDate()),
+                        contract.getOurCompany())))
+                .build(), YDConf.FORM_OPERATION.upsert_v2);
+    }
+
+    private Long toYidaDate(String value) {
+        if (value == null || value.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return LocalDate.parse(value, CONTRACT_DATE_FORMATTER)
+                    .atStartOfDay(SHANGHAI_ZONE)
+                    .toInstant()
+                    .toEpochMilli();
+        } catch (DateTimeParseException e) {
+            log.warn("合同日期格式异常:{}", value);
+            return null;
+        }
+    }
+
     @Override
     public void getContract(Map<String, Object> bodyMap) {
         try {
@@ -1559,6 +1698,8 @@ public class PayServiceImpl implements PayService {
 
     @Override
     public AmsOmApprovalCallbackResponse callback(AmsOmApprovalCallbackRequest request) {
+        request = enrichOmApprovalCallbackRequest(request);
+        validateOmApprovalCallbackRequest(request);
 
         String requestUrl = sdzlUrl + CALLBACK_PATH;
 
@@ -1569,6 +1710,9 @@ public class PayServiceImpl implements PayService {
         headers.put("Authorization", authorization);
 
         String responseBody = UtilHttp.doPost(requestUrl, headers, null, request.toRequestBody());
+        if (responseBody == null || responseBody.trim().isEmpty()) {
+            throw new IllegalStateException("AMS OM 审核回写接口返回为空");
+        }
         AmsOmApprovalCallbackResponse response = JSON.parseObject(responseBody,
                 AmsOmApprovalCallbackResponse.class);
         if (response == null) {
@@ -1581,6 +1725,142 @@ public class PayServiceImpl implements PayService {
         return response;
     }
 
+    /**
+     * 仅传流程主键时,从宜搭表单和审批记录补齐 AMS 回写参数。
+     */
+    private AmsOmApprovalCallbackRequest enrichOmApprovalCallbackRequest(
+            AmsOmApprovalCallbackRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("OM 审核回写请求不能为空");
+        }
+        if (request.getProcessInstanceId() == null
+                || request.getProcessInstanceId().trim().isEmpty()) {
+            return request;
+        }
+
+        DDR_New formResponse = ydClient.queryData(YDParam.builder()
+                        .appType(appType)
+                        .systemToken(systemToken)
+                        .formInstanceId(request.getProcessInstanceId())
+                        .build(), YDConf.FORM_QUERY.retrieve_id);
+        if (formResponse == null || formResponse.getFormData() == null
+                || formResponse.getFormData().isEmpty()) {
+            throw new IllegalStateException("宜搭未查询到流程表单数据,processInstanceId="
+                    + request.getProcessInstanceId());
+        }
+
+        Map formData = formResponse.getFormData();
+        if (isBlank(request.getFormContentJson())) {
+            request.setFormContentJson(JSON.toJSONString(formData));
+        }
+        if (isBlank(request.getOaCode())) {
+            request.setOaCode(firstString(formData,
+                    "oaCode", "oaCoding", "code",
+                    "textField_mpxojd0l", "textField_mpxnklyj"));
+        }
+        if (isBlank(request.getFormName())) {
+            request.setFormName(firstString(formData,
+                    "formName", "form_name", "formTitle", "title"));
+        }
+        if (isBlank(request.getOmId())) {
+            request.setOmId(firstString(formData,
+                    "omId", "omID", "om_id", "textField_mt804b4o"));
+        }
+
+        JSONObject latestRecord = findLatestApprovalRecord(request.getProcessInstanceId());
+        if (latestRecord != null) {
+            if (isBlank(request.getColId())) {
+                request.setColId(latestRecord.getString("taskId"));
+            }
+            if (isBlank(request.getFlowState())) {
+                request.setFlowState(latestRecord.getString("action"));
+            }
+            if (isBlank(request.getFlowDate())) {
+                request.setFlowDate(toShanghaiDate(latestRecord.getString("operateTimeGMT")));
+            }
+            if (isBlank(request.getMessage())) {
+                request.setMessage(latestRecord.getString("remark"));
+            }
+        }
+        return request;
+    }
+
+    private JSONObject findLatestApprovalRecord(String processInstanceId) {
+        DDR_New recordResponse = ydClient.queryData(YDParam.builder()
+                        .appType(appType)
+                        .systemToken(systemToken)
+                        .processInstanceId(processInstanceId)
+                        .build(), YDConf.FORM_QUERY.retrieve_approval_record);
+        if (recordResponse == null || recordResponse.getResult() == null) {
+            return null;
+        }
+
+        JSONArray records;
+        Object result = recordResponse.getResult();
+        if (result instanceof JSONArray) {
+            records = (JSONArray) result;
+        } else {
+            records = JSON.parseArray(JSON.toJSONString(result));
+        }
+        if (records == null || records.isEmpty()) {
+            return null;
+        }
+        return records.stream()
+                .map(item -> item instanceof JSONObject
+                        ? (JSONObject) item : JSON.parseObject(JSON.toJSONString(item)))
+                .filter(item -> item.getString("operateTimeGMT") != null)
+                .max(Comparator.comparing(item -> {
+                    OffsetDateTime time = safeParseIsoTime(item.getString("operateTimeGMT"));
+                    return time == null ? OffsetDateTime.MIN : time;
+                }))
+                .orElse(null);
+    }
+
+    private String firstString(Map formData, String... keys) {
+        for (String key : keys) {
+            Object value = formData.get(key);
+            if (value != null && !String.valueOf(value).trim().isEmpty()) {
+                return String.valueOf(value);
+            }
+        }
+        return null;
+    }
+
+    private String toShanghaiDate(String value) {
+        OffsetDateTime time = safeParseIsoTime(value);
+        return time == null ? null : time.atZoneSameInstant(SHANGHAI_ZONE)
+                .toLocalDate().format(CONTRACT_DATE_FORMATTER);
+    }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+
+    /**
+     * 校验 OM 审核回写接口文档中声明的必填字段。
+     */
+    private void validateOmApprovalCallbackRequest(AmsOmApprovalCallbackRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("OM 审核回写请求不能为空");
+        }
+        requireCallbackField(request.getColId(), "colId");
+        requireCallbackField(request.getOaCode(), "oaCode");
+        requireCallbackField(request.getProcessInstanceId(), "processInstanceId");
+        requireCallbackField(request.getFlowState(), "flowState");
+        requireCallbackField(request.getFlowDate(), "flowDate");
+        requireCallbackField(request.getFormName(), "formName");
+        requireCallbackField(request.getFormContentJson(), "formContentJson");
+        if ("OM收入会签单".equals(request.getFormName())) {
+            requireCallbackField(request.getOmId(), "omId");
+        }
+    }
+
+    private void requireCallbackField(String value, String fieldName) {
+        if (value == null || value.trim().isEmpty()) {
+            throw new IllegalArgumentException("OM 审核回写字段 " + fieldName + " 不能为空");
+        }
+    }
+
     /**
      * 密钥算法
      */

+ 64 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/YqbInvoiceTitleServiceImpl.java

@@ -0,0 +1,64 @@
+package com.malk.sdzl.service.Impl;
+
+import com.malk.sdzl.common.YqbApiClient;
+import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.sdzl.entity.dto.YqbInvoiceTitleRequest;
+import com.malk.sdzl.service.YqbInvoiceTitleService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/** 易企报发票抬头服务实现。 */
+@Slf4j
+@Service
+public class YqbInvoiceTitleServiceImpl implements YqbInvoiceTitleService {
+
+    /** 新增/编辑发票抬头(公司抬头)。 */
+    private static final String ADD_OR_UPDATE_TITLE_PATH = "/reimburse/corp/invoice/addOrUpdateInvoiceTitle.rjson";
+
+    @Autowired
+    private YqbApiClient yqbApiClient;
+
+    @Override
+    public YqbApiResponse<Object> addOrUpdateInvoiceTitle(YqbInvoiceTitleRequest request) {
+        validateAndNormalize(request);
+        return yqbApiClient.post(ADD_OR_UPDATE_TITLE_PATH, request.toRequestBody(), "新增发票抬头");
+    }
+
+    /**
+     * 参数校验与默认值处理。
+     *
+     * <p>未传 visible 时:填写了 visibleUsers 按“部分可见”处理,否则默认“全部可见”;
+     * 传 visible=2 时必须提供 visibleUsers。</p>
+     */
+    private void validateAndNormalize(YqbInvoiceTitleRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
+        }
+        if (isBlank(request.getInvoiceTitle())) {
+            throw new IllegalArgumentException("invoiceTitle(抬头)不能为空");
+        }
+        if (isBlank(request.getTaxPayerNo())) {
+            throw new IllegalArgumentException("taxPayerNo(纳税人识别号)不能为空");
+        }
+
+        Integer visible = request.getVisible();
+        if (visible == null) {
+            visible = isBlank(request.getVisibleUsers())
+                    ? YqbInvoiceTitleRequest.VISIBLE_ALL
+                    : YqbInvoiceTitleRequest.VISIBLE_PART;
+            request.setVisible(visible);
+        } else if (visible != YqbInvoiceTitleRequest.VISIBLE_ALL
+                && visible != YqbInvoiceTitleRequest.VISIBLE_PART) {
+            throw new IllegalArgumentException("visible 仅支持 1(全部可见)或 2(部分可见)");
+        }
+
+        if (visible == YqbInvoiceTitleRequest.VISIBLE_PART && isBlank(request.getVisibleUsers())) {
+            throw new IllegalArgumentException("可见范围为部分可见(visible=2)时,visibleUsers(可见人员)不能为空");
+        }
+    }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}

+ 19 - 87
mjava-sdzl/src/main/java/com/malk/sdzl/service/Impl/YqbOptionPushServiceImpl.java

@@ -1,63 +1,42 @@
 package com.malk.sdzl.service.Impl;
 
 import com.alibaba.excel.EasyExcel;
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
+import com.malk.sdzl.common.YqbApiClient;
 import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.sdzl.entity.dto.YqbCategoryCreateRequest;
 import com.malk.sdzl.entity.dto.YqbOptionExcelRow;
 import com.malk.sdzl.entity.dto.YqbOptionPushRequest;
 import com.malk.sdzl.entity.dto.YqbOptionPushResult;
 import com.malk.sdzl.service.YqbOptionPushService;
-import com.malk.utils.UtilHttp;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
-import java.time.LocalDateTime;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 /** 易企报辅助选项推送服务。 */
 @Slf4j
 @Service
 public class YqbOptionPushServiceImpl implements YqbOptionPushService {
 
-    private static final String TOKEN_PATH = "/auth/getAccessToken.rjson";
+    private static final String ADD_CATEGORY_PATH = "/reimburse/auxiliary/v2/category/add.rjson";
     private static final String ADD_OPTION_PATH = "/reimburse/auxiliary/v2/item/add.rjson";
-    private static final int TOKEN_EXPIRE_MINUTES = 120;
-    private static final int TOKEN_REFRESH_AHEAD_MINUTES = 30;
 
-    private final Object tokenLock = new Object();
-    private volatile String accessToken;
-    private volatile LocalDateTime accessTokenExpireAt;
+    @Autowired
+    private YqbApiClient yqbApiClient;
 
-    @Value("${yqb.base-url}")
-    private String baseUrl;
-    @Value("${yqb.app-key}")
-    private String appKey;
-    @Value("${yqb.app-secret}")
-    private String appSecret;
+    @Override
+    public YqbApiResponse<Object> createCategory(YqbCategoryCreateRequest request) {
+        validateCategoryRequest(request);
+        return yqbApiClient.post(ADD_CATEGORY_PATH, request.toRequestBody(), "新增辅助类别");
+    }
 
     @Override
     public YqbApiResponse<Object> pushOne(YqbOptionPushRequest request) {
         validateRequest(request);
-
-        String token = getAccessToken();
-        String requestUrl = trimRightSlash(baseUrl) + ADD_OPTION_PATH
-                + "?appKey=" + appKey + "&accessToken=" + token;
-
-        Map<String, Object> headers = new HashMap<>();
-        headers.put("Content-Type", "application/json;charset=UTF-8");
-
-        String responseBody = UtilHttp.doPost(requestUrl, headers, null, request.toRequestBody());
-        YqbApiResponse<Object> response = JSON.parseObject(responseBody, YqbApiResponse.class);
-        if (response == null) {
-            throw new IllegalStateException("易企报新增选项接口返回为空");
-        }
-        return response;
+        return yqbApiClient.post(ADD_OPTION_PATH, request.toRequestBody(), "新增选项");
     }
 
     @Override
@@ -112,52 +91,16 @@ public class YqbOptionPushServiceImpl implements YqbOptionPushService {
         return result;
     }
 
-    /**
-     * token 有效期 2 小时;剩余不足 30 分钟时重新获取。
-     * 双重检查与同步锁避免并发请求同时刷新 token。
-     */
-    private String getAccessToken() {
-        if (isTokenUsable()) {
-            return accessToken;
+    private void validateCategoryRequest(YqbCategoryCreateRequest request) {
+        if (request == null) {
+            throw new IllegalArgumentException("请求参数不能为空");
         }
-        synchronized (tokenLock) {
-            if (isTokenUsable()) {
-                return accessToken;
-            }
-            return refreshAccessToken();
+        if (isBlank(request.getCategoryCode())) {
+            throw new IllegalArgumentException("categoryCode不能为空");
         }
-    }
-
-    private boolean isTokenUsable() {
-        return accessToken != null
-                && accessTokenExpireAt != null
-                && LocalDateTime.now().plusMinutes(TOKEN_REFRESH_AHEAD_MINUTES).isBefore(accessTokenExpireAt);
-    }
-
-    private String refreshAccessToken() {
-        String requestUrl = trimRightSlash(baseUrl) + TOKEN_PATH;
-        Map<String, Object> headers = new HashMap<>();
-        headers.put("Content-Type", "application/json;charset=UTF-8");
-
-        Map<String, Object> requestBody = new HashMap<>();
-        requestBody.put("appKey", appKey);
-        requestBody.put("appSecret", appSecret);
-
-        String responseBody = UtilHttp.doPost(requestUrl, headers, null, requestBody);
-        JSONObject jsonObject = JSON.parseObject(responseBody);
-        Integer result = jsonObject == null ? null : jsonObject.getInteger("result");
-        String token = jsonObject == null || jsonObject.getJSONObject("data") == null
-                ? null : jsonObject.getJSONObject("data").getString("accessToken");
-
-        if (result == null || result < 100 || result >= 200 || token == null || token.trim().isEmpty()) {
-            String message = jsonObject == null ? responseBody : jsonObject.getString("message");
-            throw new IllegalStateException("获取易企报 accessToken 失败:" + message);
+        if (isBlank(request.getCategoryName())) {
+            throw new IllegalArgumentException("categoryName不能为空");
         }
-
-        accessToken = token;
-        accessTokenExpireAt = LocalDateTime.now().plusMinutes(TOKEN_EXPIRE_MINUTES);
-        log.info("易企报 accessToken 已刷新,过期时间:{}", accessTokenExpireAt);
-        return accessToken;
     }
 
     private void validateRequest(YqbOptionPushRequest request) {
@@ -178,15 +121,4 @@ public class YqbOptionPushServiceImpl implements YqbOptionPushService {
     private boolean isBlank(String value) {
         return value == null || value.trim().isEmpty();
     }
-
-    private String trimRightSlash(String url) {
-        if (url == null || url.trim().isEmpty()) {
-            throw new IllegalArgumentException("yqb.base-url 未配置");
-        }
-        String result = url.trim();
-        while (result.endsWith("/")) {
-            result = result.substring(0, result.length() - 1);
-        }
-        return result;
-    }
 }

+ 5 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/PayService.java

@@ -92,6 +92,11 @@ public interface PayService {
      */
     void getAllContract(Map<String, Object> bodyMap);
 
+    /**
+     * 同步今天更新的合同到宜搭。
+     */
+    void syncTodayContracts();
+
     void getContract(Map<String, Object> bodyMap);
 
     OmContractSignResp getRevenueCountersignContract(Map<String, Object> bodyMap);

+ 14 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/YqbInvoiceTitleService.java

@@ -0,0 +1,14 @@
+package com.malk.sdzl.service;
+
+import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.sdzl.entity.dto.YqbInvoiceTitleRequest;
+
+/** 易企报发票抬头服务。 */
+public interface YqbInvoiceTitleService {
+
+    /**
+     * 新增或编辑公司发票抬头。
+     * 抬头 + 纳税人识别号相同视为同一抬头,做更新处理。
+     */
+    YqbApiResponse<Object> addOrUpdateInvoiceTitle(YqbInvoiceTitleRequest request);
+}

+ 4 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/service/YqbOptionPushService.java

@@ -1,12 +1,16 @@
 package com.malk.sdzl.service;
 
 import com.malk.sdzl.entity.dto.YqbApiResponse;
+import com.malk.sdzl.entity.dto.YqbCategoryCreateRequest;
 import com.malk.sdzl.entity.dto.YqbOptionPushRequest;
 import com.malk.sdzl.entity.dto.YqbOptionPushResult;
 import org.springframework.web.multipart.MultipartFile;
 
 public interface YqbOptionPushService {
 
+    /** 新增一个辅助类别。 */
+    YqbApiResponse<Object> createCategory(YqbCategoryCreateRequest request);
+
     /** 推送一条辅助选项。 */
     YqbApiResponse<Object> pushOne(YqbOptionPushRequest request);
 

+ 32 - 0
mjava-sdzl/src/main/java/com/malk/sdzl/task/ContractScheduleTask.java

@@ -0,0 +1,32 @@
+package com.malk.sdzl.task;
+
+import com.malk.sdzl.service.PayService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+
+/**
+ * 合同数据定时同步任务。
+ */
+@Slf4j
+@Configuration
+@EnableScheduling
+@ConditionalOnProperty(name = "spel.scheduling", havingValue = "true")
+public class ContractScheduleTask {
+
+    @Autowired
+    private PayService payService;
+
+    /** 每 30 分钟(北京时间)同步当天更新的合同。 */
+    @Scheduled(cron = "0 0/30 * * * ?", zone = "Asia/Shanghai")
+    public void syncTodayContracts() {
+        try {
+            payService.syncTodayContracts();
+        } catch (Exception e) {
+            log.error("定时同步当天合同失败", e);
+        }
+    }
+}

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

@@ -6,7 +6,7 @@ server:
 
 # condition
 spel:
-  scheduling: false        # 定时任务是否执行
+  scheduling: true         # 定时任务是否执行
   multiSource: false       # 是否多数据源配置
 
 spring: