瀏覽代碼

feat: add AR notification and customer query APIs

lfx 1 周之前
父節點
當前提交
5de3a0e2de
共有 19 個文件被更改,包括 800 次插入4 次删除
  1. 41 0
      mjava-ts/src/main/java/com/malk/taisen/controller/ArNotifyController.java
  2. 57 0
      mjava-ts/src/main/java/com/malk/taisen/controller/CustomerInfoController.java
  3. 31 0
      mjava-ts/src/main/java/com/malk/taisen/dto/ArNotifyRecord.java
  4. 13 0
      mjava-ts/src/main/java/com/malk/taisen/dto/ArNotifyRequest.java
  5. 37 0
      mjava-ts/src/main/java/com/malk/taisen/dto/ArNotifyResponse.java
  6. 19 0
      mjava-ts/src/main/java/com/malk/taisen/service/ArNotifyService.java
  7. 288 0
      mjava-ts/src/main/java/com/malk/taisen/service/impl/ArNotifyServiceImpl.java
  8. 27 0
      mjava-ts/src/main/java/com/malk/taisen/service/sap/SapCustomerClient.java
  9. 27 0
      mjava-ts/src/main/java/com/malk/taisen/service/sap/SapCustomerService.java
  10. 61 0
      mjava-ts/src/main/java/com/malk/taisen/service/sap/impl/SapCustomerClientImpl.java
  11. 92 0
      mjava-ts/src/main/java/com/malk/taisen/service/sap/impl/SapCustomerServiceImpl.java
  12. 7 1
      mjava-ts/src/main/resources/application-dev.yml
  13. 7 1
      mjava-ts/src/main/resources/application-prod.yml
  14. 8 2
      mjava-ts/src/main/resources/application-test.yml
  15. 10 0
      mjava/src/main/java/com/malk/server/dingtalk/DDR.java
  16. 8 0
      mjava/src/main/java/com/malk/service/aliwork/YDClient.java
  17. 15 0
      mjava/src/main/java/com/malk/service/aliwork/impl/YDClientImpl.java
  18. 19 0
      mjava/src/main/java/com/malk/service/dingtalk/DDClient_NoticeResult.java
  19. 33 0
      mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_NoticeResult.java

+ 41 - 0
mjava-ts/src/main/java/com/malk/taisen/controller/ArNotifyController.java

@@ -0,0 +1,41 @@
+package com.malk.taisen.controller;
+
+import com.malk.taisen.dto.ArNotifyRecord;
+import com.malk.taisen.dto.ArNotifyRequest;
+import com.malk.taisen.dto.ArNotifyResponse;
+import com.malk.taisen.service.ArNotifyService;
+import com.malk.utils.UtilList;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * AR automatic posting notification API for SAP middleware.
+ */
+@RestController
+@RequestMapping("/ar-notify")
+public class ArNotifyController {
+
+    @Autowired
+    private ArNotifyService arNotifyService;
+
+    /**
+     * Receive AR automatic posting records and send work notifications.
+     *
+     * @param request batch of AR records
+     * @return only the final notification result
+     */
+    @PostMapping
+    public ArNotifyResponse notify(@RequestBody ArNotifyRequest request) {
+        if (request == null || UtilList.isEmpty(request.getItems())) {
+            return ArNotifyResponse.error("ITEM不能为空");
+        }
+        if (request.getItems().size() > 100) {
+            return ArNotifyResponse.error("ITEM单次最多100条");
+        }
+        return arNotifyService.notify(request.getItems()) ? ArNotifyResponse.success()
+                : ArNotifyResponse.error("通知失败");
+    }
+}

+ 57 - 0
mjava-ts/src/main/java/com/malk/taisen/controller/CustomerInfoController.java

@@ -0,0 +1,57 @@
+package com.malk.taisen.controller;
+
+import com.malk.taisen.service.sap.SapCustomerService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Customer information query API for the Yida customer query page.
+ */
+@RestController
+@RequestMapping("/customer-info")
+public class CustomerInfoController {
+
+    @Autowired
+    private SapCustomerService sapCustomerService;
+
+    /**
+     * Query customer balance.
+     *
+     * @param body CUSTOMER_NUMBER and optional CUSTOMER_NAME
+     * @return SAP balance response or mock response while SAP URL is unconfigured
+     */
+    @PostMapping("/balance")
+    public Map queryBalance(@RequestBody Map body) {
+        return sapCustomerService.queryBalance(customerNumber(body), stringValue(body.get("CUSTOMER_NAME")));
+    }
+
+    /**
+     * Query payment-unlocked sales orders.
+     *
+     * @param body CUSTOMER_NUMBER and optional CUSTOMER_NAME
+     * @return SAP unlocked order response or mock response while SAP URL is unconfigured
+     */
+    @PostMapping("/unlock-orders")
+    public Map queryUnlockOrders(@RequestBody Map body) {
+        return sapCustomerService.queryUnlockOrders(customerNumber(body), stringValue(body.get("CUSTOMER_NAME")));
+    }
+
+    private String customerNumber(Map body) {
+        String customerNumber = stringValue(body == null ? null : body.get("CUSTOMER_NUMBER"));
+        if (StringUtils.isBlank(customerNumber)) {
+            throw new IllegalArgumentException("CUSTOMER_NUMBER不能为空");
+        }
+        return customerNumber;
+    }
+
+    private String stringValue(Object value) {
+        return value == null ? "" : String.valueOf(value);
+    }
+}

+ 31 - 0
mjava-ts/src/main/java/com/malk/taisen/dto/ArNotifyRecord.java

@@ -0,0 +1,31 @@
+package com.malk.taisen.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class ArNotifyRecord {
+
+    @JsonProperty("COMPANY_CODE")
+    private String BUKRS;
+    @JsonProperty("CUSTOMER_NUMBER")
+    private String KUNNR;
+    @JsonProperty("CUSTOMER_NAME")
+    private String ZNAME;
+    @JsonProperty("DOCUMENT_NUMBER")
+    private String BELNR;
+    @JsonProperty("FISCAL_YEAR")
+    private String GJAHR;
+    @JsonProperty("POSTING_DATE")
+    private String BUDAT;
+    @JsonProperty("DOCUMENT_DATE")
+    private String BLDAT;
+    @JsonProperty("ENTER_DATE")
+    private String CPUDT;
+    @JsonProperty("AMOUNT")
+    private BigDecimal WRBTR;
+    @JsonProperty("CURRENCY")
+    private String WAERS;
+}

+ 13 - 0
mjava-ts/src/main/java/com/malk/taisen/dto/ArNotifyRequest.java

@@ -0,0 +1,13 @@
+package com.malk.taisen.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class ArNotifyRequest {
+
+    @JsonProperty("ITEM")
+    private List<ArNotifyRecord> items;
+}

+ 37 - 0
mjava-ts/src/main/java/com/malk/taisen/dto/ArNotifyResponse.java

@@ -0,0 +1,37 @@
+package com.malk.taisen.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * AR automatic posting notification response for SAP middleware.
+ */
+@Data
+@AllArgsConstructor
+public class ArNotifyResponse {
+
+    @JsonProperty("STATUS")
+    private String STATUS;
+    @JsonProperty("MESSAGE")
+    private String MESSAGE;
+
+    /**
+     * Build a successful response.
+     *
+     * @return success response
+     */
+    public static ArNotifyResponse success() {
+        return new ArNotifyResponse("S", "");
+    }
+
+    /**
+     * Build a failed response.
+     *
+     * @param message failure reason
+     * @return failed response
+     */
+    public static ArNotifyResponse error(String message) {
+        return new ArNotifyResponse("E", message);
+    }
+}

+ 19 - 0
mjava-ts/src/main/java/com/malk/taisen/service/ArNotifyService.java

@@ -0,0 +1,19 @@
+package com.malk.taisen.service;
+
+import com.malk.taisen.dto.ArNotifyRecord;
+
+import java.util.List;
+
+/**
+ * AR automatic posting notification orchestration service.
+ */
+public interface ArNotifyService {
+
+    /**
+     * Persist an AR batch and send one DingTalk work notification.
+     *
+     * @param records AR automatic posting records
+     * @return true only after DingTalk confirms delivery and Yida is updated
+     */
+    boolean notify(List<ArNotifyRecord> records);
+}

+ 288 - 0
mjava-ts/src/main/java/com/malk/taisen/service/impl/ArNotifyServiceImpl.java

@@ -0,0 +1,288 @@
+package com.malk.taisen.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
+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_Notice;
+import com.malk.service.dingtalk.DDClient_NoticeResult;
+import com.malk.taisen.dto.ArNotifyRecord;
+import com.malk.taisen.service.ArNotifyService;
+import com.malk.utils.UtilList;
+import com.malk.utils.UtilMap;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class ArNotifyServiceImpl implements ArNotifyService {
+
+    private static final String APP_TYPE = "APP_N9NPHVTQLPBPO8MR6WFG";
+    private static final String FORM_UUID = "FORM-A7AA1BE41C354D5C879925006D3A3F06K6ZY";
+    private static final int MAX_ATTEMPTS = 6;
+
+    @Autowired
+    private YDClient ydClient;
+    @Autowired
+    private DDClient ddClient;
+    @Autowired
+    private DDClient_Notice ddClientNotice;
+    @Autowired
+    private DDClient_NoticeResult ddClientNoticeResult;
+
+    @Value("${ar.notify.user-ids:}")
+    private String notifyUserIds;
+
+    /**
+     * Persist an AR batch and send one DingTalk work notification.
+     *
+     * @param records AR automatic posting records
+     * @return true only after DingTalk confirms delivery and Yida is updated
+     */
+    @Override
+    public boolean notify(List<ArNotifyRecord> records) {
+        try {
+            for (ArNotifyRecord record : records) {
+                validate(record);
+            }
+            final String instanceId = createRecord(records);
+            if (instanceId == null) {
+                return false;
+            }
+            List<String> recipients = recipients();
+            if (UtilList.isEmpty(recipients)) {
+                markFailure(instanceId, 0, "未配置AR_NOTIFY_USER_IDS");
+                return false;
+            }
+            return sendAndConfirm(records, instanceId, recipients);
+        } catch (Exception e) {
+            log.error("AR自动入账通知执行失败", e);
+            return false;
+        }
+    }
+
+    private String createRecord(List<ArNotifyRecord> records) {
+        Map formData = UtilMap.map("tableField_3ycv10yjn", detailRows(records));
+        formData.put("selectField_89a55tbg2", "处理中");
+        formData.put("numberField_89a56h485", 0);
+        Object result = retry("创建宜搭实例", new Action() {
+            @Override
+            public Object execute() {
+                return ydClient.createData(YDParam.builder().appType(APP_TYPE).formUuid(FORM_UUID)
+                        .formDataJson(JSON.toJSONString(formData)).build());
+            }
+        });
+        return instanceId(result);
+    }
+
+    private boolean sendAndConfirm(List<ArNotifyRecord> records, String instanceId, List<String> recipients) {
+        String lastError = "";
+        String taskId = null;
+        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
+            try {
+                if (StringUtils.isBlank(taskId)) {
+                    taskId = ddClientNotice.sendNotification(ddClient.getAccessToken(), recipients, null, false,
+                            notificationMessage(records, instanceId));
+                    if (StringUtils.isBlank(taskId)) {
+                        throw new IllegalStateException("钉钉未返回工作通知任务编号");
+                    }
+                    update(instanceId, UtilMap.map("textField_89a545m8z, numberField_89a56h485",
+                            taskId, attempt - 1));
+                }
+                Map result = ddClientNoticeResult.getSendResult(ddClient.getAccessToken(), taskId);
+                if (!isSent(result)) {
+                    throw new IllegalStateException("钉钉工作通知尚未发送成功: " + JSON.toJSONString(result));
+                }
+                Date now = new Date();
+                update(instanceId, UtilMap.map(
+                        "textField_89a41cak4, dateField_89a42ww2f, textField_89a53tgha, selectField_89a55tbg2, numberField_89a56h485, textareaField_89a575hku",
+                        "X", now.getTime(), new SimpleDateFormat("HHmmss").format(now), "成功", attempt - 1, "钉钉工作通知发送成功"));
+                return true;
+            } catch (Exception e) {
+                lastError = e.getMessage();
+                log.warn("AR自动入账通知第{}次尝试失败, instanceId={}, taskId={}, error={}",
+                        attempt, instanceId, taskId, lastError);
+                if (attempt < MAX_ATTEMPTS) {
+                    sleep(attempt);
+                }
+            }
+        }
+        log.error("AR自动入账通知重试耗尽, instanceId={}, error={}", instanceId, lastError);
+        markFailure(instanceId, MAX_ATTEMPTS - 1, lastError);
+        return false;
+    }
+
+    private void markFailure(final String instanceId, final int retryCount, final String message) {
+        retry("回写宜搭失败状态", new Action() {
+            @Override
+            public Object execute() {
+                update(instanceId, UtilMap.map("selectField_89a55tbg2, numberField_89a56h485, textareaField_89a575hku",
+                        "失败", retryCount, StringUtils.abbreviate(message, 1900)));
+                return Boolean.TRUE;
+            }
+        });
+    }
+
+    private void update(String instanceId, Map formData) {
+        ydClient.operateData(YDParam.builder().appType(APP_TYPE).formInstanceId(instanceId)
+                .updateFormDataJson(JSON.toJSONString(formData)).build(), YDConf.FORM_OPERATION.update);
+    }
+
+    private Object retry(String actionName, Action action) {
+        for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
+            try {
+                return action.execute();
+            } catch (Exception e) {
+                log.warn("{}第{}次尝试失败: {}", actionName, attempt, e.getMessage());
+                if (attempt < MAX_ATTEMPTS) {
+                    sleep(attempt);
+                }
+            }
+        }
+        log.error("{}重试耗尽", actionName);
+        return null;
+    }
+
+    private void sleep(int retryNumber) {
+        try {
+            Thread.sleep((1L << (retryNumber - 1)) * 1000L);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IllegalStateException("重试等待被中断", e);
+        }
+    }
+
+    private Map notificationMessage(List<ArNotifyRecord> records, String instanceId) {
+        String detailUrl = "https://www.aliwork.com/" + APP_TYPE + "/formDetail/" + FORM_UUID
+                + "?formInstId=" + instanceId + "&corpid=dinge61fe69900ea236b35c2f4657eb6378f";
+        ArNotifyRecord first = records.get(0);
+        String text = "本次自动入账记录数:" + records.size() + "\n公司代码:" + first.getBUKRS()
+                + "\n首条客户:" + first.getKUNNR() + " " + StringUtils.defaultString(first.getZNAME())
+                + "\n首条凭证:" + first.getBELNR() + "\n请点击查看明细。";
+        Map<String, Object> link = new HashMap<>();
+        link.put("picUrl", "@lALOACZwe2Rk");
+        link.put("title", "AR自动入账通知");
+        link.put("text", text);
+        link.put("messageUrl", detailUrl);
+        Map<String, Object> message = new HashMap<>();
+        message.put("msgtype", "link");
+        message.put("link", link);
+        return message;
+    }
+
+    private boolean isSent(Map result) {
+        if (result == null) {
+            return false;
+        }
+        return !hasFailure(result, "invalid_user_id_list")
+                && !hasFailure(result, "forbidden_list")
+                && !hasFailure(result, "failed_user_id_list")
+                && !hasFailure(result, "invalid_dept_id_list")
+                && !hasFailure(result, "forbidden_dept_id_list")
+                && !hasFailure(result, "failed_dept_id_list");
+    }
+
+    private boolean hasFailure(Map result, String key) {
+        Object value = result.get(key);
+        if (value == null) {
+            return false;
+        }
+        if (value instanceof Collection) {
+            return !((Collection) value).isEmpty();
+        }
+        if (value.getClass().isArray()) {
+            return java.lang.reflect.Array.getLength(value) > 0;
+        }
+        return StringUtils.isNotBlank(String.valueOf(value));
+    }
+
+    private String instanceId(Object result) {
+        if (result instanceof DDR_New) {
+            DDR_New response = (DDR_New) result;
+            if (StringUtils.isNotBlank(response.getFormInstId())) {
+                return response.getFormInstId();
+            }
+            if (StringUtils.isNotBlank(response.getInstanceId())) {
+                return response.getInstanceId();
+            }
+            result = response.getResult();
+        }
+        if (result instanceof String && StringUtils.isNotBlank((String) result)) {
+            return (String) result;
+        }
+        if (!(result instanceof Map)) {
+            log.error("宜搭创建响应未包含实例ID, response={}", JSON.toJSONString(result));
+            return null;
+        }
+        Map map = (Map) result;
+        Object id = map.get("formInstId");
+        if (id == null) {
+            id = map.get("formInstanceId");
+        }
+        if (id == null) {
+            id = map.get("instanceId");
+        }
+        return id == null ? null : String.valueOf(id);
+    }
+
+    private List<String> recipients() {
+        List<String> result = new ArrayList<>();
+        String[] userIds = StringUtils.split(StringUtils.defaultString(notifyUserIds), ',');
+        if (userIds == null) {
+            return result;
+        }
+        for (String userId : userIds) {
+            if (StringUtils.isNotBlank(userId)) {
+                result.add(userId.trim());
+            }
+        }
+        return result;
+    }
+
+    private List<Map> detailRows(List<ArNotifyRecord> records) {
+        List<Map> rows = new ArrayList<>();
+        for (ArNotifyRecord record : records) {
+            rows.add(UtilMap.map(
+                    "textField_3ycw2w508, textField_3ycw3wu79, textField_3ycw4j3ot, textField_3ycw539ws, textField_3ycw6yohj, dateField_3ycw7ql0n, dateField_3ycw8qqry, dateField_3ycw9wnsf, numberField_3ycwa25z7, textField_3ycwbocwu, textField_3ycwcntzu, dateField_3ycwdnk73, textField_3ycwes8k4, textField_3ycwfk3zf, textField_3ycwgmpib, textField_3ycwhz5dp",
+                    record.getBUKRS(), record.getKUNNR(), record.getZNAME(), record.getBELNR(), record.getGJAHR(),
+                    dateMillis(record.getBUDAT()), dateMillis(record.getBLDAT()), dateMillis(record.getCPUDT()), record.getWRBTR(),
+                    record.getWAERS(), "", null, "", "", "", ""));
+        }
+        return rows;
+    }
+
+    private long dateMillis(String date) {
+        try {
+            SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
+            formatter.setLenient(false);
+            return formatter.parse(date).getTime();
+        } catch (ParseException e) {
+            throw new IllegalArgumentException("日期必须为yyyyMMdd格式: " + date, e);
+        }
+    }
+
+    private void validate(ArNotifyRecord record) {
+        if (record == null || StringUtils.isAnyBlank(record.getBUKRS(), record.getKUNNR(), record.getBELNR(),
+                record.getGJAHR(), record.getBUDAT(), record.getBLDAT(), record.getCPUDT()) || record.getWRBTR() == null) {
+            throw new IllegalArgumentException("BUKRS、KUNNR、BELNR、GJAHR、BUDAT、BLDAT、CPUDT、WRBTR不能为空");
+        }
+    }
+
+    private interface Action {
+        Object execute();
+    }
+}

+ 27 - 0
mjava-ts/src/main/java/com/malk/taisen/service/sap/SapCustomerClient.java

@@ -0,0 +1,27 @@
+package com.malk.taisen.service.sap;
+
+import java.util.Map;
+
+/**
+ * SAP customer information atomic query client.
+ */
+public interface SapCustomerClient {
+
+    /**
+     * Query customer balance by customer number.
+     *
+     * @param customerNumber SAP customer number
+     * @return raw SAP response
+     * @apiNote SAP interface URL pending confirmation
+     */
+    Map queryBalance(String customerNumber);
+
+    /**
+     * Query customer payment-unlocked sales orders by customer number.
+     *
+     * @param customerNumber SAP customer number
+     * @return raw SAP response
+     * @apiNote SAP interface URL pending confirmation
+     */
+    Map queryUnlockOrders(String customerNumber);
+}

+ 27 - 0
mjava-ts/src/main/java/com/malk/taisen/service/sap/SapCustomerService.java

@@ -0,0 +1,27 @@
+package com.malk.taisen.service.sap;
+
+import java.util.Map;
+
+/**
+ * Customer information query service for the customer query page.
+ */
+public interface SapCustomerService {
+
+    /**
+     * Query balance, or return mock data until the SAP URL is configured.
+     *
+     * @param customerNumber SAP customer number
+     * @param customerName selected customer name
+     * @return customer balance response
+     */
+    Map queryBalance(String customerNumber, String customerName);
+
+    /**
+     * Query payment-unlocked orders, or return mock data until the SAP URL is configured.
+     *
+     * @param customerNumber SAP customer number
+     * @param customerName selected customer name
+     * @return unlocked sales order response
+     */
+    Map queryUnlockOrders(String customerNumber, String customerName);
+}

+ 61 - 0
mjava-ts/src/main/java/com/malk/taisen/service/sap/impl/SapCustomerClientImpl.java

@@ -0,0 +1,61 @@
+package com.malk.taisen.service.sap.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.malk.taisen.service.sap.SapCustomerClient;
+import com.malk.utils.UtilHttp;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * SAP customer information atomic HTTP client.
+ */
+@Component
+public class SapCustomerClientImpl implements SapCustomerClient {
+
+    @Value("${sap.customer-balance-url:}")
+    private String customerBalanceUrl;
+    @Value("${sap.customer-unlock-order-url:}")
+    private String customerUnlockOrderUrl;
+
+    /**
+     * Query customer balance.
+     *
+     * @param customerNumber SAP customer number
+     * @return raw SAP response
+     * @apiNote SAP interface URL pending confirmation
+     */
+    @Override
+    public Map queryBalance(String customerNumber) {
+        return query(customerBalanceUrl, customerNumber);
+    }
+
+    /**
+     * Query customer payment-unlocked sales orders.
+     *
+     * @param customerNumber SAP customer number
+     * @return raw SAP response
+     * @apiNote SAP interface URL pending confirmation
+     */
+    @Override
+    public Map queryUnlockOrders(String customerNumber) {
+        return query(customerUnlockOrderUrl, customerNumber);
+    }
+
+    private Map query(String url, String customerNumber) {
+        if (StringUtils.isBlank(url)) {
+            return null;
+        }
+        Map<String, Object> body = new HashMap<>();
+        body.put("CUSTOMER_NUMBER", customerNumber);
+        String response = UtilHttp.doPost_S(url, null, null, body);
+        Map result = JSON.parseObject(response, Map.class);
+        if (result == null) {
+            throw new IllegalStateException("SAP客户查询接口返回为空");
+        }
+        return result;
+    }
+}

+ 92 - 0
mjava-ts/src/main/java/com/malk/taisen/service/sap/impl/SapCustomerServiceImpl.java

@@ -0,0 +1,92 @@
+package com.malk.taisen.service.sap.impl;
+
+import com.malk.taisen.service.sap.SapCustomerClient;
+import com.malk.taisen.service.sap.SapCustomerService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Customer information query service with temporary SAP mock responses.
+ */
+@Service
+public class SapCustomerServiceImpl implements SapCustomerService {
+
+    @Autowired
+    private SapCustomerClient sapCustomerClient;
+
+    /**
+     * Query customer balance.
+     *
+     * @param customerNumber SAP customer number
+     * @param customerName selected customer name
+     * @return customer balance response
+     */
+    @Override
+    public Map queryBalance(String customerNumber, String customerName) {
+        Map result = sapCustomerClient.queryBalance(customerNumber);
+        return result == null ? mockBalance(customerNumber, customerName) : result;
+    }
+
+    /**
+     * Query customer payment-unlocked orders.
+     *
+     * @param customerNumber SAP customer number
+     * @param customerName selected customer name
+     * @return unlocked sales order response
+     */
+    @Override
+    public Map queryUnlockOrders(String customerNumber, String customerName) {
+        Map result = sapCustomerClient.queryUnlockOrders(customerNumber);
+        return result == null ? mockUnlockOrders(customerNumber, customerName) : result;
+    }
+
+    private Map mockBalance(String customerNumber, String customerName) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("CUSTOMER_NUMBER", customerNumber);
+        result.put("CUSTOMER_NAME", StringUtils.defaultString(customerName));
+        List<Map<String, String>> items = new ArrayList<>();
+        items.add(balanceItem("6500", "CNY", "8931791.38", "1093170.13"));
+        items.add(balanceItem("6521", "CNY", "303820.93", "2901733.10"));
+        result.put("ITEM", items);
+        return result;
+    }
+
+    private Map mockUnlockOrders(String customerNumber, String customerName) {
+        Map<String, Object> result = new HashMap<>();
+        List<Map<String, String>> items = new ArrayList<>();
+        items.add(unlockOrder(customerNumber, customerName, "450024450", "24900", "100800"));
+        items.add(unlockOrder(customerNumber, customerName, "450024473", "31900", "101234"));
+        result.put("ITEM", items);
+        return result;
+    }
+
+    private Map<String, String> balanceItem(String companyCode, String currency, String outstandingAmount, String totalRiskAmount) {
+        Map<String, String> item = new HashMap<>();
+        item.put("COMPANY_CODE", companyCode);
+        item.put("CURRENCY", currency);
+        item.put("OUTSTAND_AMT", outstandingAmount);
+        item.put("TOTAL_RISK_AMT", totalRiskAmount);
+        return item;
+    }
+
+    private Map<String, String> unlockOrder(String customerNumber, String customerName, String salesOrder, String salesAmount, String releaseTime) {
+        Map<String, String> item = new HashMap<>();
+        item.put("CUSTOMER_NUMBER", customerNumber);
+        item.put("CUSTOMER_NAME", StringUtils.defaultString(customerName));
+        item.put("COMPANY_CODE", "6500");
+        item.put("SALES_ORDER", salesOrder);
+        item.put("SALES_ORG", "6500");
+        item.put("SALES_AMOUNT", salesAmount);
+        item.put("CURRENCY", "CNY");
+        item.put("RELEASE_DATE", "20260701");
+        item.put("RELEASE_TIME", releaseTime);
+        item.put("RELEASE_STATUS", "S");
+        return item;
+    }
+}

+ 7 - 1
mjava-ts/src/main/resources/application-dev.yml

@@ -55,6 +55,8 @@ aliwork:
   systemToken: "UM6660D1PGF2O34KAVVKG8XZ756E3O06MZX5LW"
 
 sap:
+  customer-balance-url: ${SAP_CUSTOMER_BALANCE_URL:}
+  customer-unlock-order-url: ${SAP_CUSTOMER_UNLOCK_ORDER_URL:}
   QA_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/10-QA-team-fssc/tk_oa_to_sap_p2p?bearer_token=bBiGTA7PS0JJ6wKhQU8Vm0vLsIDNjLaN&interface_id="
   JT_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/10-QA-team-fssc/tk_oa_to_sap_r2r_generate_fi_document_with_copa?bearer_token=TVof6BjMdOvJreUmI9DzEepesFVwbfEc&interface_id=SAP020"
   #  JT_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/00-STG-team-fssc/tk_oa_to_sap_r2r_generate_fi_document_with_copa?bearer_token=TVof6BjMdOvJreUmI9DzEepesFVwbfEc&interface_id=SAP020"
@@ -62,4 +64,8 @@ sap:
 
 hangxing:
   TARGET_URL_Poc: "http://139.224.3.140:8091/stms/openapi/income/bookkeeping"
-  PROXY_URL:  "https://www.senhouse.cn/apits/bookkeeping"
+  PROXY_URL:  "https://www.senhouse.cn/apits/bookkeeping"
+
+ar:
+  notify:
+    user-ids: ${AR_NOTIFY_USER_IDS:}

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

@@ -56,6 +56,8 @@ aliwork:
   systemToken: "UM6660D1PGF2O34KAVVKG8XZ756E3O06MZX5LW"
 
 sap:
+  customer-balance-url: ${SAP_CUSTOMER_BALANCE_URL:}
+  customer-unlock-order-url: ${SAP_CUSTOMER_UNLOCK_ORDER_URL:}
   QA_sapUrl_Poc: "https://etl-prod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonProd/Ultraprojects/team-fssc/tk_oa_to_sap_p2p?bearer_token=bBiGTA7PS0JJ6wKhQU8Vm0vLsIDNjLaN&interface_id="
   JT_sapUrl_Poc: "https://etl-prod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonProd/Ultraprojects/team-fssc/tk_oa_to_sap_r2r_generate_fi_document_with_copa?bearer_token=TVof6BjMdOvJreUmI9DzEepesFVwbfEc&interface_id=SAP020"
 #  JT_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/00-STG-team-fssc/tk_oa_to_sap_r2r_generate_fi_document_with_copa?bearer_token=TVof6BjMdOvJreUmI9DzEepesFVwbfEc&interface_id=SAP020"
@@ -63,4 +65,8 @@ sap:
 
 hangxing:
   TARGET_URL_Poc: "http://106.15.251.153:8091/stms/openapi/ts/income/bookkeeping"
-  PROXY_URL:  "https://www.senhouse.cn/apitsprod/bookkeeping"
+  PROXY_URL:  "https://www.senhouse.cn/apitsprod/bookkeeping"
+
+ar:
+  notify:
+    user-ids: ${AR_NOTIFY_USER_IDS:}

+ 8 - 2
mjava-ts/src/main/resources/application-test.yml

@@ -55,11 +55,17 @@ aliwork:
   systemToken: "UM6660D1PGF2O34KAVVKG8XZ756E3O06MZX5LW"
 
 sap:
-  QA_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/10-QA-team-fssc/tk_oa_to_sap_p2p?bearer_token=bBiGTA7PS0JJ6wKhQU8Vm0vLsIDNjLaN&interface_id="
+  customer-balance-url: ${SAP_CUSTOMER_BALANCE_URL:}
+  customer-unlock-order-url: ${SAP_CUSTOMER_UNLOCK_ORDER_URL:}
+  QA_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/00-STG-team-fssc/tk_oa_to_sap_p2p?bearer_token=bBiGTA7PS0JJ6wKhQU8Vm0vLsIDNjLaN&interface_id="
   JT_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/10-QA-team-fssc/tk_oa_to_sap_r2r_generate_fi_document_with_copa?bearer_token=TVof6BjMdOvJreUmI9DzEepesFVwbfEc&interface_id=SAP020"
 #  JT_sapUrl_Poc: "https://etl-nonprod-tasks.tysondt.com:443/api/1/rest/feed/run/task/TysonNonProd/Ultra_PoC/00-STG-team-fssc/tk_oa_to_sap_r2r_generate_fi_document_with_copa?bearer_token=TVof6BjMdOvJreUmI9DzEepesFVwbfEc&interface_id=SAP020"
   TARGET_URL_Poc: "http://139.224.3.140:8091/stms/openapi/income/bookkeeping"
 
 hangxing:
   TARGET_URL_Poc: "http://139.224.3.140:8091/stms/openapi/income/bookkeeping"
-  PROXY_URL:  "https://www.senhouse.cn/apits/bookkeeping"
+  PROXY_URL:  "https://www.senhouse.cn/apits/bookkeeping"
+
+ar:
+  notify:
+    user-ids: ${AR_NOTIFY_USER_IDS:15362032421245878}

+ 10 - 0
mjava/src/main/java/com/malk/server/dingtalk/DDR.java

@@ -64,6 +64,16 @@ public class DDR<T> extends VenR {
      */
     private String task_id; // 可调用获取工作通知消息的发送结果查询结果
 
+    /**
+     * Work notification task status returned by getsendresult.
+     */
+    private Integer task_status;
+
+    /**
+     * Work notification recipient result returned by getsendresult.
+     */
+    private Map send_result;
+
     /**
      * 考勤打卡数据
      */

+ 8 - 0
mjava/src/main/java/com/malk/service/aliwork/YDClient.java

@@ -12,6 +12,14 @@ public interface YDClient {
      */
     Object operateData(YDParam param, YDConf.FORM_OPERATION type);
 
+    /**
+     * Create a form instance and retain the complete Yida response.
+     *
+     * @param param form instance creation parameters
+     * @return complete raw Yida response, including the instance identifier
+     */
+    DDR_New createData(YDParam param);
+
     /**
      * 查询数据
      */

+ 15 - 0
mjava/src/main/java/com/malk/service/aliwork/impl/YDClientImpl.java

@@ -92,6 +92,21 @@ public class YDClientImpl implements YDClient {
         return ddr_new.getResult();
     }
 
+    /**
+     * Create a form instance and retain the complete Yida response.
+     *
+     * @param ydParam form instance creation parameters
+     * @return complete raw Yida response, including the instance identifier
+     */
+    @Override
+    public DDR_New createData(YDParam ydParam) {
+        Map bodys = _initBodyParam(ydParam);
+        DDR_New response = (DDR_New) UtilHttp.doPost(getRequestUrl("/forms/instances"),
+                ddClient.initTokenHeader(), bodys, DDR_New.class);
+        response.assertSuccess();
+        return response;
+    }
+
     /**
      * 查询数据
      *

+ 19 - 0
mjava/src/main/java/com/malk/service/dingtalk/DDClient_NoticeResult.java

@@ -0,0 +1,19 @@
+package com.malk.service.dingtalk;
+
+import java.util.Map;
+
+/**
+ * DingTalk work-notification delivery-result atomic client.
+ */
+public interface DDClient_NoticeResult {
+
+    /**
+     * Query a work notification task delivery result.
+     *
+     * @apiNote https://open.dingtalk.com/document/orgapp/queries-the-results-of-sending-asynchronous-sent-work-notifications
+     * @param access_token DingTalk application access token
+     * @param task_id task identifier returned by asyncsend_v2
+     * @return raw DingTalk result payload
+     */
+    Map getSendResult(String access_token, String task_id);
+}

+ 33 - 0
mjava/src/main/java/com/malk/service/dingtalk/impl/DDImplClient_NoticeResult.java

@@ -0,0 +1,33 @@
+package com.malk.service.dingtalk.impl;
+
+import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDR;
+import com.malk.service.dingtalk.DDClient_NoticeResult;
+import com.malk.utils.UtilMap;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class DDImplClient_NoticeResult implements DDClient_NoticeResult {
+
+    @Autowired
+    private DDConf ddConf;
+
+    /**
+     * Query a work notification task delivery result.
+     *
+     * @param access_token DingTalk application access token
+     * @param task_id task identifier returned by asyncsend_v2
+     * @return raw DingTalk result payload
+     */
+    @Override
+    public Map getSendResult(String access_token, String task_id) {
+        DDR ddr = DDR.doPost("https://oapi.dingtalk.com/topapi/message/corpconversation/getsendresult",
+                null, DDConf.initTokenParams(access_token),
+                UtilMap.map("agent_id, task_id", ddConf.getAgentId(), task_id));
+        ddr.assertSuccess();
+        return ddr.getSend_result();
+    }
+}