From 85831da2a56cfb304df5eaee7021e491d39ee55d Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 14 九月 2026 12:29:36 +0800
Subject: [PATCH] fix(merchant): 提现失败后可重试,且不再丢掉通道流水号
---
talent-card-server/src/main/java/com/talentcard/server/business/admin/MerchantSettlementAdminService.java | 25 ++++++
talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java | 13 +++
talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalProperties.java | 16 ---
talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java | 7 +
talent-card-server/src/test/java/com/talentcard/server/business/merchant/AutoWithdrawalAggregationTests.java | 88 +++++++++++----------
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java | 11 ++
talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalScheduler.java | 22 +++++
talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java | 7 +
talent-card-server/src/main/java/com/talentcard/server/business/payment/PaymentReconciliationService.java | 4
9 files changed, 131 insertions(+), 62 deletions(-)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
index 6b7bfc1..bc63a2f 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/admin/AdminManagementController.java
@@ -35,6 +35,19 @@
@PostMapping("/merchant-settlement/onboardings/{id}/payment-capability/refresh") public ApiResponse<Map<String,Object>> refreshPaymentCapability(@PathVariable Long id) { return ApiResponse.success(settlementService.refreshPaymentCapability(id)); }
@GetMapping("/merchant-settlement/bank-cards") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> bankCards(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.bankCards(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); }
@GetMapping("/merchant-settlement/withdrawals") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> withdrawals(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String merchantName, @RequestParam(required = false) String contactMobile, @RequestParam(required = false) String merchantType, @RequestParam(required = false) String status, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ApiResponse.success(settlementService.withdrawals(page, size, merchantName, contactMobile, merchantType, status, startDate, endDate)); }
+ /**
+ * 按业务日补跑一次自动提现汇总(运维排查与验收用)。日期格式 yyyyMMdd,必须显式传。
+ */
+ @PostMapping("/merchant-settlement/auto-withdrawal/run") public ApiResponse<Map<String,Object>> runAutoWithdrawalAggregation(@RequestParam String businessDate) { return ApiResponse.success(settlementService.runAutoWithdrawalAggregation(parseBusinessDate(businessDate))); }
+
+ private static java.time.LocalDate parseBusinessDate(String businessDate) {
+ if (businessDate == null || businessDate.isBlank()) throw new com.talentcard.server.common.BusinessException(com.talentcard.server.common.ErrorCode.BAD_REQUEST, "请指定业务日 businessDate(yyyyMMdd)");
+ try {
+ return java.time.LocalDate.parse(businessDate.trim(), java.time.format.DateTimeFormatter.BASIC_ISO_DATE);
+ } catch (java.time.format.DateTimeParseException exception) {
+ throw new com.talentcard.server.common.BusinessException(com.talentcard.server.common.ErrorCode.BAD_REQUEST, "业务日格式应为 yyyyMMdd");
+ }
+ }
@GetMapping("/payment-reconciliation/issues") public ApiResponse<com.talentcard.server.business.PageResult<Map<String,Object>>> reconciliationIssues(@RequestParam(defaultValue = "open") String status, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size) { return ApiResponse.success(reconciliationService.issues(status, page, size)); }
@PatchMapping("/payment-reconciliation/issues/{id}") public ApiResponse<Map<String,Object>> closeReconciliationIssue(@PathVariable Long id, @RequestBody Map<String,String> body) { return ApiResponse.success(reconciliationService.close(id, body.get("status"), body.get("operator"))); }
@PostMapping("/payment-reconciliation/run") public ApiResponse<Map<String,Object>> runPaymentReconciliation(@RequestParam(defaultValue = "5") int pendingAfterMinutes) { return ApiResponse.success(reconciliationService.run(pendingAfterMinutes)); }
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 126a829..419da1f 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,5 +1,6 @@
package com.talentcard.server.business.admin;
+import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -7,7 +8,9 @@
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.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import com.talentcard.server.common.SensitiveDataCrypto;
@@ -28,7 +31,27 @@
private final JdbcTemplate jdbc;
private final SensitiveDataCrypto sensitiveDataCrypto;
private final MerchantPaymentReadinessService readinessService;
- public MerchantSettlementAdminService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, MerchantPaymentReadinessService readinessService) { this.jdbc = jdbc; this.sensitiveDataCrypto = sensitiveDataCrypto; this.readinessService = 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; }
+
+ /**
+ * 按业务日补跑一次日汇总并立刻提交、收敛结果。
+ *
+ * <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);
+ 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());
+ return view;
+ }
@Transactional(readOnly = true)
public PageResult<Map<String, Object>> onboardings(int page, int size, String merchantName, String contactMobile, String merchantType, String status, String startDate, String endDate) {
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalProperties.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalProperties.java
index 5ca0f08..e4be58b 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalProperties.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalProperties.java
@@ -3,19 +3,13 @@
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
- * 每日自动提现汇总与结果处理的阈值和开关。金额都是整数分,和库里的金额列一致。
+ * 每日自动提现汇总与结果处理的开关。汇总按整数分计算,和库里的金额列一致。
*
- * <p>低于最低提现金额,或者扣掉手续费缓冲后低于最低提现金额,这一天的汇总就顺延到下一次任务,
- * 不生成提现记录(CONTEXT.md「可提现订单」)。
+ * <p>不设最低提现金额:斗拱主动取现没有最低金额,取现也不收手续费(手续费在支付时已经内扣),
+ * 所以只要当天有已核销订单就出账,金额小也照提(CONTEXT.md「可提现订单」)。
*/
@ConfigurationProperties(prefix = "app.merchant.auto-withdrawal")
public class AutoWithdrawalProperties {
-
- /** 最低提现金额(分)。斗拱主动取现的最小金额以目标产品为准,部署时按产品配置。 */
- private long minAmountCents = 1000;
-
- /** 手续费缓冲(分):可提金额减去缓冲后仍要达到最低金额,否则顺延。 */
- private long feeBufferCents = 0;
/** 任务是否启用。凌晨的定时任务按北京时间触发,本地开发可以关掉。 */
private boolean enabled = true;
@@ -41,10 +35,6 @@
/** 结果处理任务(补交待提交记录 + 查询处理中记录)是否启用。 */
private boolean resultCheckEnabled = true;
- public long getMinAmountCents() { return minAmountCents; }
- public void setMinAmountCents(long minAmountCents) { this.minAmountCents = minAmountCents; }
- public long getFeeBufferCents() { return feeBufferCents; }
- public void setFeeBufferCents(long feeBufferCents) { this.feeBufferCents = feeBufferCents; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getCron() { return cron; }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalScheduler.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalScheduler.java
index 469996b..2309b91 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalScheduler.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/AutoWithdrawalScheduler.java
@@ -1,5 +1,7 @@
package com.talentcard.server.business.merchant;
+import java.time.LocalDate;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
@@ -44,9 +46,27 @@
submitPending();
}
+ /**
+ * 按指定业务日补跑一次汇总,然后立刻提交并收敛结果。供后台运维排查和验收使用:
+ * 正常任务汇总的是「前一自然日」,无法用来验证当天核销的订单。
+ *
+ * <p>补跑只汇总入参指定的那一天,不会顺带处理别的日期;重复调用同一天由
+ * {@code uk_merchant_withdrawal_daily} 唯一键兜底,不会重复出账。
+ */
+ public AutoWithdrawalAggregationService.Result runForBusinessDate(LocalDate businessDate) {
+ AutoWithdrawalAggregationService.Result result = aggregation.run(businessDate);
+ log.info("Auto withdrawal manual aggregation {}", result);
+ submitPending();
+ reconcile();
+ return result;
+ }
+
@Scheduled(fixedDelayString = "${app.merchant.auto-withdrawal.result-check-fixed-delay-ms:300000}")
public void processResults() {
- if (!properties.isEnabled() || !properties.isResultCheckEnabled()) return;
+ // 结果收敛不受 enabled 控制:enabled 管的是「要不要自动汇总出账」,而这里做的是「把已经生成的
+ // 记录提交出去并收敛状态」。用后台补跑、或把 enabled 关掉只做手动汇总时,这一步仍然必须运行,
+ // 否则记录会永远停在 pending。
+ if (!properties.isResultCheckEnabled()) return;
submitPending();
reconcile();
}
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java
index a2a069d..5fe6d3b 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java
@@ -48,9 +48,11 @@
this.jdbc = jdbc;
this.capabilityGateway = capabilityGateway;
this.huifuPaymentProperties = huifuPaymentProperties;
- this.refreshTransaction = refreshTransaction;
+ // 注入的 TransactionTemplate 是容器里的单例,支付、退款和自动提现都共用它。这里绝不能改它的
+ // 传播行为,否则那些服务会跟着被改成 REQUIRES_NEW,外层事务被静默挂起。两个模板都自己新建。
// The snapshot must survive a failed provider query in the caller's transaction, so it runs
// in its own transaction instead of joining the caller's.
+ this.refreshTransaction = new TransactionTemplate(refreshTransaction.getTransactionManager());
this.refreshTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
// The provider is an HTTP call: never make payment or onboarding hold a database transaction
// (and its row locks) while waiting for it.
@@ -276,7 +278,7 @@
return new Application(
((Number) row.get("id")).longValue(),
((Number) row.get("merchantId")).longValue(),
- ((Number) row.get("storeId")).longValue(),
+ number(row.get("storeId")),
clean(row.get("status")),
clean(row.get("providerHuifuId")),
clean(row.get("merchantStat")),
@@ -286,6 +288,11 @@
);
}
+ /** 进件的 store_id 允许为空(历史数据):这里不能直接拆箱,否则 NullPointerException 会冒到调用方。 */
+ private static long number(Object value) {
+ return value instanceof Number number ? number.longValue() : 0L;
+ }
+
private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
private String clean(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/PaymentReconciliationService.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/PaymentReconciliationService.java
index 359812d..e9a6359 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/PaymentReconciliationService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/PaymentReconciliationService.java
@@ -145,7 +145,9 @@
String status = HuifuPayload.value(data, "trans_stat", "transStat");
String code = HuifuPayload.code(data);
String message = HuifuPayload.message(data);
- String providerNo = HuifuPayload.value(data, "hf_seq_id", "hfSeqId");
+ // 支付查询回执里通道全局流水号的字段是 org_hf_seq_id;hf_seq_id 只出现在异步通知里。
+ // 这里保留 hf_seq_id 兜底,避免通道换字段名后又被静默丢成 NULL。
+ String providerNo = HuifuPayload.value(data, "org_hf_seq_id", "orgHfSeqId", "hf_seq_id", "hfSeqId");
String huifuId = HuifuPayload.value(data, "huifu_id", "huifuId");
TransStatus classification = HuifuPayload.status(status);
boolean success = classification == TransStatus.SUCCESS && HuifuPayload.successCode(code);
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
index 66fdd4a..c1b57c3 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/portal/AppOrderPortalService.java
@@ -65,7 +65,7 @@
try {
jdbc.update("""
- insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, order_type,
+ insert into benefit_order(order_no, talent_id, card_id, card_level_snapshot, merchant_id, store_id, order_type,
order_amount, subsidy_amount, base_pay_amount, pay_amount,
coupon_id, coupon_amount, expires_at, price_version,
merchant_name_snapshot, merchant_address_snapshot, merchant_mobile_snapshot,
@@ -73,13 +73,16 @@
product_id, product_name_snapshot, product_cover_url_snapshot,
collect_onboarding_id, collect_store_id, collect_huifu_id,
order_status, verify_status)
- values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
orderNo,
talentId,
talentCard.get("id"),
talentCard.get("cardLevel"),
request.merchantId(),
+ // 订单的门店必须落库:日汇总按「商户 + 门店」出账(benefit_order.store_id),
+ // 这里为空的话这笔已核销订单永远汇总不到,也就永远不会生成提现记录。
+ MerchantPaymentReadinessService.CollectingMerchant.storeIdOf(collectingMerchant),
normalizeOrderType(product.get("productType")),
originalAmount,
subsidyAmount,
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/AutoWithdrawalAggregationTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/AutoWithdrawalAggregationTests.java
index 6c6323d..ccdfdbe 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/AutoWithdrawalAggregationTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/AutoWithdrawalAggregationTests.java
@@ -26,7 +26,7 @@
/**
* 每日自动提现汇总 seam:按北京时间统计前一个自然日;只汇总已核销、未退款且未进入其它提现记录的订单;
- * 同一商户生成一条日汇总记录并内部保存来源订单关联;最低金额和手续费缓冲不足时顺延;重复执行不重复出账。
+ * 同一商户生成一条日汇总记录并内部保存来源订单关联;不设最低金额(当天有已核销订单就出账);重复执行不重复出账。
*
* <p>任务按设计会扫描全部商户,而多个 Spring 测试共用同一个不清空的 schema,因此断言只看本用例自己创建的
* 商户与业务日;每个用例的业务日也互不相同。
@@ -148,48 +148,16 @@
}
@Test
- void anAmountBelowTheMinimumIsDeferredToTheNextRun() {
+ void aTinyAmountIsStillAggregatedBecauseThereIsNoMinimumAnyMore() {
LocalDate businessDate = someBusinessDate();
- Long small = verifiedOrder(businessDate, 500);
+ // 斗拱主动取现没有最低金额,取现也不收手续费,所以再小的金额也要出账,不再顺延。
+ Long tiny = verifiedOrder(businessDate, 1);
service.run(businessDate);
- assertThat(dailyWithdrawalOrNull(businessDate)).isNull();
- assertThat(lockedOrderIds()).isEmpty();
-
- // 第二天新增一笔订单,两天的金额一起顺延到下一次汇总。
- Long later = verifiedOrder(businessDate.plusDays(1), 1500);
- service.run(businessDate.plusDays(1));
-
- assertThat(dailyWithdrawal(businessDate.plusDays(1))).containsEntry("amount_cents", 2000L);
- assertThat(sourceOrderIds(businessDate.plusDays(1))).contains(small, later);
- assertThat(lockedOrderIds()).containsExactlyInAnyOrder(small, later);
- }
- @Test
- void anAmountThatWouldNotCoverTheFeeBufferIsDeferred() {
- AutoWithdrawalProperties properties = new AutoWithdrawalProperties();
- properties.setMinAmountCents(1000);
- properties.setFeeBufferCents(500);
- AutoWithdrawalAggregationService buffered = new AutoWithdrawalAggregationService(
- jdbc, new OrderFundBoundaryService(jdbc),
- new TransactionTemplate(new DataSourceTransactionManager(jdbc.getDataSource())),
- properties);
-
- LocalDate businessDate = someBusinessDate();
- Long exact = verifiedOrder(businessDate, 1200);
-
- buffered.run(businessDate);
-
- // 1200 分虽然过了最低金额,但扣掉 500 分缓冲后不足最低金额,仍然顺延。
- assertThat(dailyWithdrawalOrNull(businessDate)).isNull();
- assertThat(lockedOrderIds()).isEmpty();
-
- Long extra = verifiedOrder(businessDate, 300);
- buffered.run(businessDate);
-
- assertThat(dailyWithdrawal(businessDate)).containsEntry("amount_cents", 1500L);
- assertThat(sourceOrderIds(businessDate)).contains(exact, extra);
- assertThat(lockedOrderIds()).containsExactlyInAnyOrder(exact, extra);
+ assertThat(dailyWithdrawal(businessDate)).containsEntry("amount_cents", 1L);
+ assertThat(sourceOrderIds(businessDate)).containsExactly(tiny);
+ assertThat(lockedOrderIds()).containsExactly(tiny);
}
@Test
@@ -208,6 +176,41 @@
assertThat(sourceOrderIds(businessDate)).containsExactlyInAnyOrder(first, second);
assertThat(lockedOrderIds()).containsExactlyInAnyOrder(first, second);
assertThat(lockedOrderIds()).doesNotContain(late);
+ }
+
+ /**
+ * 通道当时说不可取(例如 DM 额度随账户可取资金浮动),钱到账后必须还能再出一次账。
+ *
+ * <p>失败时来源订单被放回可汇总范围,所以这一业务日再次汇总必须复用那条失败记录:状态退回
+ * {@code pending`、金额更新。若这里仍然按「已汇总」跳过,这笔钱就再没有任何出账路径。
+ */
+ @Test
+ void aFailedDailyWithdrawalIsRetriedOnceTheFundsBecomeAvailable() {
+ LocalDate businessDate = someBusinessDate();
+ Long first = verifiedOrder(businessDate, 2000);
+ service.run(businessDate);
+
+ // 通道拒绝这笔出账。失败时 AutoWithdrawalResultService.releaseSourceOrders 会同时做两件事:
+ // 给关联打 released_at、并解锁订单的 withdrawal_status,这里按同样效果模拟(本用例只覆盖汇总侧)。
+ Map<String, Object> withdrawal = dailyWithdrawal(businessDate);
+ String idempotencyKey = (String) withdrawal.get("idempotency_key");
+ long withdrawalId = ((Number) withdrawal.get("id")).longValue();
+ jdbc.update("update merchant_withdrawal set status = 'failed', failure_reason = ? where id = ?",
+ "DM取现额度不足", withdrawalId);
+ jdbc.update("update merchant_withdrawal_order set released_at = now() where withdrawal_id = ?", withdrawalId);
+ jdbc.update("update benefit_order set withdrawal_status = 0 where id = ?", first);
+
+ // 失败之后又核销了一笔,两笔都应当进入同一条重试记录。
+ Long second = verifiedOrder(businessDate, 3000);
+ service.run(businessDate);
+
+ assertThat(dailyWithdrawalCount(businessDate)).as("失败重试不会新建第二条日汇总记录").isEqualTo(1);
+ Map<String, Object> retried = dailyWithdrawal(businessDate);
+ assertThat(retried).containsEntry("status", "pending");
+ assertThat(retried).containsEntry("amount_cents", 5000L);
+ assertThat(retried).containsEntry("idempotency_key", idempotencyKey);
+ assertThat(sourceOrderIds(businessDate)).containsExactlyInAnyOrder(first, second);
+ assertThat(lockedOrderIds()).containsExactlyInAnyOrder(first, second);
}
@Test
@@ -279,10 +282,13 @@
service.run(businessDate);
- // 另一个门店只有 500 分,低于最低金额,它的订单不会被主门店的汇总带走。
+ // 两个门店各出一条日汇总:主门店只有主订单,另一个门店的 500 分单独出账(已无最低金额),
+ // 谁也不会把对方的订单带走。
+ assertThat(dailyWithdrawalCountForStore(businessDate, storeId)).isEqualTo(1);
+ assertThat(dailyWithdrawalCountForStore(businessDate, otherStoreId)).isEqualTo(1);
assertThat(dailyWithdrawal(businessDate)).containsEntry("amount_cents", 2000L).containsEntry("store_id", storeId);
assertThat(sourceOrderIds(businessDate)).containsExactly(mainOrder);
- assertThat(lockedOrderIds()).containsExactly(mainOrder).doesNotContain(otherOrder);
+ assertThat(lockedOrderIds()).containsExactlyInAnyOrder(mainOrder, otherOrder);
}
@Test
diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java
index 55cdce7..947c4b5 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java
@@ -292,9 +292,11 @@
void aProviderQuerySettlesThePaymentAndVerifiesStoreConsumption() {
String orderNo = createOrder("STORE_CONSUME");
String paymentNo = insertPendingPayment(orderNo, 6000L, huifuId);
+ // 支付查询回执里通道全局流水号的字段是 org_hf_seq_id(hf_seq_id 只出现在异步通知里)。
+ // 用真实字段名,才能保证这条断言真的守住落库行为。
when(paymentGateway.queryMiniProgramPayment(any())).thenReturn(providerResponse(Map.of(
"trans_stat", "S", "resp_code", "00000000", "trans_amt", "60.00",
- "huifu_id", huifuId, "hf_seq_id", "HF-QUERY-1")));
+ "huifu_id", huifuId, "org_hf_seq_id", "HF-QUERY-1")));
Map<String, Object> result = reconciliationService.reconcile(5);
@@ -306,6 +308,9 @@
.containsEntry("order_status", 1)
.containsEntry("verify_status", 1);
assertThat(jdbc.queryForObject("select status from payment_transaction where payment_no = ?", String.class, paymentNo)).isEqualTo("SUCCEEDED");
+ assertThat(jdbc.queryForObject("select provider_transaction_no from payment_transaction where payment_no = ?", String.class, paymentNo))
+ .as("查询落账必须把通道全局流水号写进 provider_transaction_no,否则退款和排查都失去定位依据")
+ .isEqualTo("HF-QUERY-1");
}
@Test
--
Gitblit v1.7.1