From 20e19d8693d08ccb47e6533d8523fd44cd546a70 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 17 九月 2026 13:12:10 +0800
Subject: [PATCH] fix(merchant): 收款能力自查不再因缺列打成 500
---
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java | 42 ++++++++++++++++++++++++------------------
1 files changed, 24 insertions(+), 18 deletions(-)
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 3a2e7e1..b6dcd8a 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
@@ -108,12 +108,12 @@
Application application = approvedApplication(merchantId, storeId);
Readiness readiness = readiness(merchantId, storeId);
// 冷却剩余时间由服务端算好:客户端不必自己解析数据库时间戳字符串,也能在冷却结束时恢复按钮。
- long retryAfterSeconds = application == null ? 0L : capabilityCooldownSeconds(application.id());
+ long retryAfterSeconds = cooldownSeconds(application == null ? null : application.capabilitySyncUntil());
Map<String, Object> view = new LinkedHashMap<>();
view.put("ready", readiness.ready());
view.put("reason", readiness.reason());
- view.put("verifiedAt", application == null ? "" : application.capabilityVerifiedAt());
- view.put("nextVerifyAt", application == null ? "" : application.capabilitySyncUntil());
+ view.put("verifiedAt", clean(application == null ? null : application.capabilityVerifiedAt()));
+ view.put("nextVerifyAt", clean(application == null ? null : application.capabilitySyncUntil()));
view.put("verifyAvailable", application != null && retryAfterSeconds == 0L);
view.put("retryAfterSeconds", retryAfterSeconds);
return view;
@@ -153,15 +153,20 @@
}
/**
- * 冷却剩余秒数,0 表示现在就能核实。与写侧读的是同一列;{@code coalesce} 兜住「从没设过冷却」
- * 与「冷却已过期」两种情况——{@code timestampdiff} 遇上 NULL 会连 {@code greatest} 一起变 NULL。
+ * 冷却剩余秒数,0 表示现在就能核实。用申请行上已经读到的那个时间戳算,不单发一条 select:
+ * 开发库(flyway 关闭)的列由启动时 best-effort 补,见 {@code MerchantOnboardingSchemaInitializer};
+ * 点名 `capability_sync_until` 的查询在缺列时会整条报错、把商户页打成 500,而读已读到的行只会少一个字段。
+ *
+ * <p>时区显式取 Asia/Shanghai:库里的 datetime 由 `serverTimezone=Asia/Shanghai` 解释成上海墙钟值,
+ * 拿它跟"本机默认时区"的 now() 相减,在非上海时区的机器上会整体偏掉(本仓库其它地方同样显式钉这个时区)。
+ *
+ * <p>它只服务于按钮的倒计时:真正的闸门是 {@link #acquireVerifyWindow} 那条按数据库时钟判定的 UPDATE,
+ * 所以即便这里算早了,也只会让按钮早恢复、点了回来一个 {@code refreshed=false},不会多查通道。
*/
- private long capabilityCooldownSeconds(long applicationId) {
- List<Map<String, Object>> rows = rows("""
- select coalesce(greatest(0, timestampdiff(second, now(), capability_sync_until)), 0) as remainingSeconds
- from merchant_onboarding_application where id = ?
- """, applicationId);
- return rows.isEmpty() ? 0L : number(rows.get(0).get("remainingSeconds"));
+ private static long cooldownSeconds(java.time.LocalDateTime syncUntil) {
+ if (syncUntil == null) return 0L;
+ java.time.LocalDateTime now = java.time.LocalDateTime.now(java.time.ZoneId.of("Asia/Shanghai"));
+ return Math.max(0L, java.time.Duration.between(now, syncUntil).getSeconds());
}
/**
@@ -170,7 +175,7 @@
*/
public Map<String, Object> capability(Long applicationId) {
Application application = requireApprovedApplication(applicationId);
- if (application.capabilityVerifiedAt().isBlank()) {
+ if (clean(application.capabilityVerifiedAt()).isBlank()) {
queryProviderOutsideTransaction(application);
application = requireApprovedApplication(applicationId);
}
@@ -219,7 +224,7 @@
boolean unknown = application.autoSettleStatus().isBlank()
|| application.cashSwitchStatus().isBlank()
|| application.merchantStatus().isBlank()
- || application.capabilityVerifiedAt().isBlank();
+ || clean(application.capabilityVerifiedAt()).isBlank();
if (application.huifuId().isBlank()) return Readiness.blocked("商户进件缺少斗拱商户号,暂时无法收款");
if (unknown && !providerVerificationRequired()) return localOnly(application);
// The payment path never calls the provider itself: an unfilled snapshot is filled by the
@@ -325,7 +330,7 @@
view.put("autoSettleStatus", application.autoSettleStatus());
view.put("cashAvailable", isCashAvailable(application.cashSwitchStatus()));
view.put("cashSwitchStatus", application.cashSwitchStatus());
- view.put("verifiedAt", application.capabilityVerifiedAt());
+ view.put("verifiedAt", clean(application.capabilityVerifiedAt()));
return view;
}
@@ -377,8 +382,9 @@
clean(row.get("merchantStat")),
clean(row.get("autoSettleStatus")),
clean(row.get("cashSwitchStatus")),
- clean(row.get("capabilityVerifiedAt")),
- clean(row.get("capabilitySyncUntil"))
+ // DatabaseRows 已把 datetime 归一成 LocalDateTime;缺列时这里是 null,不是异常。
+ (java.time.LocalDateTime) row.get("capabilityVerifiedAt"),
+ (java.time.LocalDateTime) row.get("capabilitySyncUntil")
);
}
@@ -400,8 +406,8 @@
String merchantStatus,
String autoSettleStatus,
String cashSwitchStatus,
- String capabilityVerifiedAt,
- String capabilitySyncUntil
+ java.time.LocalDateTime capabilityVerifiedAt,
+ java.time.LocalDateTime capabilitySyncUntil
) {}
/**
--
Gitblit v1.7.1