From 68c6a5802964fc8f5b577feb39a44d733ddc9038 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 14 九月 2026 13:30:42 +0800
Subject: [PATCH] refactor(payment): 退役平台自动提现,改由斗拱自动结算并新增结算记录页

---
 talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java |  128 ++++++++++++++++++++++++++----------------
 1 files changed, 79 insertions(+), 49 deletions(-)

diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
index 419da1f..ab925f1 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java
@@ -1,16 +1,14 @@
 package com.talentcard.server.business.admin;
 
-import java.time.LocalDate;
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import com.alibaba.fastjson.JSON;
 import com.talentcard.server.business.DatabaseRows;
 import com.talentcard.server.business.PageResult;
-import com.talentcard.server.business.merchant.AutoWithdrawalAggregationService;
-import com.talentcard.server.business.merchant.AutoWithdrawalResultService;
-import com.talentcard.server.business.merchant.AutoWithdrawalScheduler;
+import com.talentcard.server.business.payment.huifu.HuifuSettlementRecordGateway;
 import com.talentcard.server.common.BusinessException;
 import com.talentcard.server.common.ErrorCode;
 import com.talentcard.server.common.SensitiveDataCrypto;
@@ -22,35 +20,95 @@
 /**
  * 后台结算管理。
  *
- * <p>后台只能查看进件、银行卡和提现记录,不能修改提现状态:提现记录的状态只由斗拱的异步通知或主动
- * 查询收敛(见 {@code AutoWithdrawalResultService})。进件和银行卡的审核入口在这一版已经下线,
- * 后台保留查询与能力快照刷新。
+ * <p>出款由斗拱按结算周期自动完成(核销确认后的钱进商户基本户,次日自动结算到结算卡),
+ * 平台不再产生提现记录。后台保留进件、银行卡查询、收款能力快照刷新,以及向通道查询
+ * 真实结算记录用于监控每天结了多少到银行卡。
  */
 @Service
 public class MerchantSettlementAdminService {
     private final JdbcTemplate jdbc;
     private final SensitiveDataCrypto sensitiveDataCrypto;
     private final MerchantPaymentReadinessService readinessService;
-    private final AutoWithdrawalScheduler autoWithdrawalScheduler;
-    public MerchantSettlementAdminService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService, AutoWithdrawalScheduler autoWithdrawalScheduler) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = readinessService; this.autoWithdrawalScheduler = autoWithdrawalScheduler; }
+    private final HuifuSettlementRecordGateway settlementRecordGateway;
+    public MerchantSettlementAdminService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService, HuifuSettlementRecordGateway settlementRecordGateway) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = readinessService; this.settlementRecordGateway = settlementRecordGateway; }
 
     /**
-     * 按业务日补跑一次日汇总并立刻提交、收敛结果。
+     * 结算记录:钱按结算周期自动划到结算卡之后,「每天结了多少」只能问通道。
      *
-     * <p>定时任务汇总的是「前一自然日」,当天核销的订单要等到第二天凌晨才会出账;排查和验收都等不起,
-     * 所以这里提供一次显式补跑。它不接受「不传日期」:隐式取当天容易误刷历史,日期必须由调用方给定。
+     * <p>按门店取该商户当前生效的斗拱子商户号,逐商户查询结算记录后合并成一条时间倒序的列表。
+     * 通道查询失败只影响这一条商户,不会让整页失败——监控页最重要的是「看得到」。
      */
-    public Map<String, Object> runAutoWithdrawalAggregation(LocalDate businessDate) {
-        if (businessDate == null) throw new BusinessException(ErrorCode.BAD_REQUEST, "请指定业务日 businessDate(yyyyMMdd)");
-        AutoWithdrawalAggregationService.Result result = autoWithdrawalScheduler.runForBusinessDate(businessDate);
+    public Map<String, Object> settlements(String beginDate, String endDate, int size) {
+        List<Map<String, Object>> merchants = rows("""
+                select a.merchant_id, a.provider_huifu_id, m.merchant_name, s.store_name
+                from merchant_onboarding_application a
+                join merchant m on m.id = a.merchant_id
+                join merchant_store s on s.id = a.store_id
+                where a.status = 'approved' and a.provider_huifu_id is not null and a.provider_huifu_id <> ''
+                  and a.id = (select max(newest.id) from merchant_onboarding_application newest
+                              where newest.merchant_id = a.merchant_id)
+                order by a.merchant_id asc
+                """);
+        List<Map<String, Object>> records = new ArrayList<>();
+        List<String> failures = new ArrayList<>();
+        int limit = Math.min(Math.max(size, 1), 100);
+        for (Map<String, Object> merchant : merchants) {
+            String huifuId = String.valueOf(merchant.get("providerHuifuId"));
+            try {
+                HuifuSettlementRecordGateway.SettlementRecords response =
+                        settlementRecordGateway.query(huifuId, beginDate, endDate, limit);
+                if (!response.succeeded()) {
+                    failures.add(String.valueOf(merchant.get("merchantName")) + ":" + response.message());
+                    continue;
+                }
+                records.addAll(decode(response.rawRecords(), merchant));
+            } catch (RuntimeException exception) {
+                failures.add(String.valueOf(merchant.get("merchantName")) + ":查询失败");
+            }
+        }
+        records.sort((left, right) -> String.valueOf(right.get("settleDate"))
+                .compareTo(String.valueOf(left.get("settleDate"))));
         Map<String, Object> view = new LinkedHashMap<>();
-        view.put("businessDate", result.businessDate());
-        view.put("merchantsAggregated", result.merchantsAggregated());
-        view.put("merchantsSkipped", result.merchantsSkipped());
-        view.put("merchantsFailed", result.merchantsFailed());
-        view.put("ordersAggregated", result.ordersAggregated());
-        view.put("amountCents", result.amountCents());
+        view.put("beginDate", beginDate);
+        view.put("endDate", endDate);
+        view.put("records", records.size() > limit ? records.subList(0, limit) : records);
+        view.put("total", records.size());
+        view.put("failures", failures);
         return view;
+    }
+
+    /** 把通道返回的结算记录 JSON 转成后台要展示的字段;只读展示,不做金额换算以外的加工。 */
+    private List<Map<String, Object>> decode(String rawRecords, Map<String, Object> merchant) {
+        List<Map<String, Object>> decoded = new ArrayList<>();
+        if (rawRecords == null || rawRecords.isBlank()) return decoded;
+        List<Map<String, Object>> parsed;
+        try {
+            parsed = JSON.parseObject(rawRecords, new com.alibaba.fastjson.TypeReference<>() { });
+        } catch (RuntimeException exception) {
+            return decoded;
+        }
+        if (parsed == null) return decoded;
+        for (Map<String, Object> record : parsed) {
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("merchantId", merchant.get("merchantId"));
+            row.put("merchantName", merchant.get("merchantName"));
+            row.put("storeName", merchant.get("storeName"));
+            row.put("providerTransactionNo", record.get("trans_id"));
+            row.put("settleDate", record.get("trans_date"));
+            row.put("settleAmountCents", cents(record.get("trans_amt")));
+            row.put("feeAmountCents", cents(record.get("fee_amt")));
+            row.put("settleCycle", record.get("settle_cycle"));
+            row.put("status", record.get("trans_stat"));
+            row.put("cardMasked", record.get("card_no"));
+            row.put("cardHolder", record.get("card_name"));
+            decoded.add(row);
+        }
+        return decoded;
+    }
+
+    private Long cents(Object yuan) {
+        if (yuan == null) return null;
+        return com.talentcard.server.business.payment.huifu.HuifuPayload.amountInCent(String.valueOf(yuan));
     }
 
     @Transactional(readOnly = true)
@@ -79,34 +137,6 @@
         });
         return result;
     }
-    /**
-     * 提现记录只读:状态由斗拱的通知或查询收敛,后台不能人工审核或改写。列表只展示三种对外状态,
-     * 因此筛选值也按「处理中 / 已到账 / 提现失败」翻译成库里的原始状态。
-     */
-    @Transactional(readOnly = true)
-    public PageResult<Map<String, Object>> withdrawals(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) {
-        String available = "greatest(0, coalesce((select sum(o.pay_amount) from benefit_order o where o.merchant_id = w.merchant_id and (o.store_id is null or o.store_id = w.store_id) and o.order_status = 1 and o.verify_status = 1 and o.refund_status = 0 and o.withdrawal_status = 0 and o.deleted = 0), 0) - coalesce((select sum(w2.amount_cents) from merchant_withdrawal w2 where w2.merchant_id = w.merchant_id and w2.store_id = w.store_id and w2.status in ('pending', 'processing', 'paid')), 0))";
-        String select = "select w.*, c.bank_name, c.card_masked, m.merchant_name, m.merchant_type, m.contact_mobile, s.store_name, " + available + " as available_amount_cents from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id";
-        String count = "select count(*) from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id join merchant m on m.id = w.merchant_id join merchant_store s on s.id = w.store_id";
-        PageResult<Map<String, Object>> result = page(select, count, "w", page, size, merchantName, contactMobile,
-                merchantType, null, startDate, endDate, withdrawalStatusCondition(status));
-        result.records().forEach(row -> row.put("status",
-                AutoWithdrawalResultService.displayStatus(row.get("status") == null ? "" : String.valueOf(row.get("status")))));
-        return result;
-    }
-
-    /**
-     * 对外三种状态到库里原始状态的映射条件,由 {@link AutoWithdrawalResultService#rawStatuses} 推导。
-     * 没有筛选时不加条件;筛选取值不认识是「筛不出记录」,不是「忽略筛选」。
-     */
-    private String withdrawalStatusCondition(String status) {
-        if (status == null || status.isBlank()) return "";
-        List<String> rawStatuses = AutoWithdrawalResultService.rawStatuses(status);
-        if (rawStatuses.isEmpty()) return " and 1 = 0";
-        return " and w.status in (" + rawStatuses.stream().map(value -> "'" + value + "'")
-                .collect(java.util.stream.Collectors.joining(", ")) + ")";
-    }
-
     /** Provider-confirmed settlement and encashment capability of one approved sub-merchant. */
     public Map<String, Object> paymentCapability(Long applicationId) { return readinessService.capability(applicationId); }
 

--
Gitblit v1.7.1