|
@@ -201,13 +201,21 @@ public class HarOrderService {
|
|
|
// 抢占:条件更新,守卫写在 WHERE 里(返回值不是守卫 —— 见乐收赢模块同款纪律)。
|
|
// 抢占:条件更新,守卫写在 WHERE 里(返回值不是守卫 —— 见乐收赢模块同款纪律)。
|
|
|
// 走到这里与真正落库之间没有上游往返,但仍可能有回调/轮询并发改这一行,
|
|
// 走到这里与真正落库之间没有上游往返,但仍可能有回调/轮询并发改这一行,
|
|
|
// 所以照旧只允许从「1 支付成功」改起。
|
|
// 所以照旧只允许从「1 支付成功」改起。
|
|
|
|
|
+ // 退款原因先定下来:同一句话既要发给上游,也要落本地这一行。
|
|
|
|
|
+ // 上游对空 reason 是**整笔直接拒**(2026-09-24 实测),默认值由我们补;
|
|
|
|
|
+ // 调用方填了就按他填的走 —— 这一格会进商户的账务记录,不该被默认值盖掉。
|
|
|
|
|
+ String reason = StrUtil.isBlank(dto.getReason()) ? DEFAULT_REFUND_REASON : dto.getReason();
|
|
|
|
|
+
|
|
|
order.setTradeStatus(HarOrderStatus.REFUNDING);
|
|
order.setTradeStatus(HarOrderStatus.REFUNDING);
|
|
|
order.setQueryCount(0);
|
|
order.setQueryCount(0);
|
|
|
reschedulePoll(order); // 「1 已支付」是终态、next_query_time 已为 null,不重排就没人去收敛
|
|
reschedulePoll(order); // 「1 已支付」是终态、next_query_time 已为 null,不重排就没人去收敛
|
|
|
|
|
+ order.setRefundReason(reason);
|
|
|
HarOrderEntity patch = new HarOrderEntity();
|
|
HarOrderEntity patch = new HarOrderEntity();
|
|
|
patch.setTradeStatus(order.getTradeStatus());
|
|
patch.setTradeStatus(order.getTradeStatus());
|
|
|
patch.setQueryCount(order.getQueryCount());
|
|
patch.setQueryCount(order.getQueryCount());
|
|
|
patch.setNextQueryTime(order.getNextQueryTime());
|
|
patch.setNextQueryTime(order.getNextQueryTime());
|
|
|
|
|
+ // 与 trade_status 同一笔条件更新:抢占没命中就直接抛了,不会留下「原因存了但退款没发」
|
|
|
|
|
+ patch.setRefundReason(reason);
|
|
|
// update(entity, wrapper) 不经过 AutoFillMetaObjectHandler,updated_time 只能自己写
|
|
// update(entity, wrapper) 不经过 AutoFillMetaObjectHandler,updated_time 只能自己写
|
|
|
patch.setUpdatedTime(OffsetDateTime.now());
|
|
patch.setUpdatedTime(OffsetDateTime.now());
|
|
|
UpdateWrapper<HarOrderEntity> guard = new UpdateWrapper<>();
|
|
UpdateWrapper<HarOrderEntity> guard = new UpdateWrapper<>();
|
|
@@ -228,10 +236,9 @@ public class HarOrderService {
|
|
|
} else {
|
|
} else {
|
|
|
req.setThirdOrderNo(order.getOrderNo());
|
|
req.setThirdOrderNo(order.getOrderNo());
|
|
|
}
|
|
}
|
|
|
- // 上游要求 reason 非空(规范没写,2026-09-24 实测)。调用方留空时补默认值 ——
|
|
|
|
|
- // 让每个接我们 API 的人去猜一个必填值,等于把这格变成会随机失败的坑。
|
|
|
|
|
- // 调用方填了就按他填的走:这一格会进商户的账务记录,不该被默认值盖掉。
|
|
|
|
|
- req.setReason(StrUtil.isBlank(dto.getReason()) ? DEFAULT_REFUND_REASON : dto.getReason());
|
|
|
|
|
|
|
+ // 与抢占那一笔落库的是**同一个串**,不是在两处各算一次:
|
|
|
|
|
+ // 分开算就有漂移的余地,而漂移的那天没人查得出来「上游收到的」和「我们记的」哪句是真的。
|
|
|
|
|
+ req.setReason(reason);
|
|
|
|
|
|
|
|
HarResponse response;
|
|
HarResponse response;
|
|
|
try {
|
|
try {
|
|
@@ -258,14 +265,17 @@ public class HarOrderService {
|
|
|
/**
|
|
/**
|
|
|
* 回退退款抢占(上游明确拒绝时)。
|
|
* 回退退款抢占(上游明确拒绝时)。
|
|
|
*
|
|
*
|
|
|
- * 用 {@code update(null, wrapper)} 而不是实体:{@code next_query_time} 要写回 **null**
|
|
|
|
|
- * 才能停掉轮询,而 MyBatis-Plus 默认的 NOT_NULL 策略会把实体里的 null 字段整个跳过 ——
|
|
|
|
|
- * 那样这笔订单会被轮询永远捞起。代价是 updated_time 也得显式写(不经过实体就没有自动填充)。
|
|
|
|
|
|
|
+ * 用 {@code update(null, wrapper)} 而不是实体:{@code next_query_time} 与
|
|
|
|
|
+ * {@code refund_reason} 都要写回 **null** 才能表达「这笔退款没有发生」,
|
|
|
|
|
+ * 而 MyBatis-Plus 默认的 NOT_NULL 策略会把实体里的 null 字段整个跳过 ——
|
|
|
|
|
+ * 那样这笔订单会被轮询永远捞起、页面上还会留着一句从没发生过的退款原因。
|
|
|
|
|
+ * 代价是 updated_time 也得显式写(不经过实体就没有自动填充)。
|
|
|
*/
|
|
*/
|
|
|
private void rollbackRefundPreempt(HarOrderEntity order) {
|
|
private void rollbackRefundPreempt(HarOrderEntity order) {
|
|
|
UpdateWrapper<HarOrderEntity> w = new UpdateWrapper<>();
|
|
UpdateWrapper<HarOrderEntity> w = new UpdateWrapper<>();
|
|
|
w.set("trade_status", HarOrderStatus.PAID)
|
|
w.set("trade_status", HarOrderStatus.PAID)
|
|
|
.set("next_query_time", null)
|
|
.set("next_query_time", null)
|
|
|
|
|
+ .set("refund_reason", null)
|
|
|
.set("updated_time", OffsetDateTime.now())
|
|
.set("updated_time", OffsetDateTime.now())
|
|
|
.eq("id", order.getId())
|
|
.eq("id", order.getId())
|
|
|
.eq("trade_status", HarOrderStatus.REFUNDING);
|
|
.eq("trade_status", HarOrderStatus.REFUNDING);
|
|
@@ -275,6 +285,7 @@ public class HarOrderService {
|
|
|
}
|
|
}
|
|
|
order.setTradeStatus(HarOrderStatus.PAID);
|
|
order.setTradeStatus(HarOrderStatus.PAID);
|
|
|
order.setNextQueryTime(null);
|
|
order.setNextQueryTime(null);
|
|
|
|
|
+ order.setRefundReason(null);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -434,6 +445,20 @@ public class HarOrderService {
|
|
|
HarQueryDetail detail = HarResponseParser.toBean(response.detailRaw(), HarQueryDetail.class);
|
|
HarQueryDetail detail = HarResponseParser.toBean(response.detailRaw(), HarQueryDetail.class);
|
|
|
if (detail != null) {
|
|
if (detail != null) {
|
|
|
applyQueryDetail(order, detail);
|
|
applyQueryDetail(order, detail);
|
|
|
|
|
+ // 三个金额只有这一条路径能落库 —— 通知的 15 个字段里没有金额。
|
|
|
|
|
+ // 所以「上游送了就要落库」这句话在这里是可验证的:已经付掉/退掉的单,
|
|
|
|
|
+ // 三个金额却全空,只可能是「上游没送」或「字段名与我们对不上」。
|
|
|
|
|
+ // 后者不是假想:这份规范已被证伪过两次(只认 subject 不认 title;
|
|
|
|
|
+ // reason 全篇没写却必填)。detail 原文只在这里出现一次,联调期先留着 ——
|
|
|
|
|
+ // 它每次都报,说明上游压根不送,那时该改的是这句话,不是再加日志。
|
|
|
|
|
+ if ((HarOrderStatus.PAID.equals(order.getTradeStatus())
|
|
|
|
|
+ || HarOrderStatus.REFUNDED.equals(order.getTradeStatus()))
|
|
|
|
|
+ && order.getRcvAmount() == null
|
|
|
|
|
+ && order.getFeeAmount() == null
|
|
|
|
|
+ && order.getChannelAmount() == null) {
|
|
|
|
|
+ log.warn("[汇安融订单] 查询成功但三个金额全空: order_no={}, status={}, detail={}",
|
|
|
|
|
+ order.getOrderNo(), order.getTradeStatus(), response.detailRaw());
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
order.setNextQueryTime(OffsetDateTime.now()
|
|
order.setNextQueryTime(OffsetDateTime.now()
|
|
|
.plusSeconds(nextDelaySeconds(order.getQueryCount(), false)));
|
|
.plusSeconds(nextDelaySeconds(order.getQueryCount(), false)));
|