|
@@ -9,6 +9,7 @@ import com.payment.platform.module.payment.har.mapper.HarOrderMapper;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.OffsetDateTime;
|
|
import java.time.OffsetDateTime;
|
|
|
import java.time.ZoneId;
|
|
import java.time.ZoneId;
|
|
@@ -82,8 +83,10 @@ public class HarNotifyService {
|
|
|
return SIGN_FAILED;
|
|
return SIGN_FAILED;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // third_order_no 是我方订单号;上游自己的单号在 order_no 字段
|
|
|
|
|
- String orderNo = params.get("third_order_no");
|
|
|
|
|
|
|
+ // third_order_no 是我方订单号;上游自己的单号在 order_no 字段。
|
|
|
|
|
+ // 两个名字都收:上游实际上对同一个字段发 camelCase + snake_case **两份**
|
|
|
|
|
+ // (值一致,2026-09-28 实测),这里两份都认,只来一份时也不至于定位不到订单。
|
|
|
|
|
+ String orderNo = pick(params, "third_order_no", "thirdOrderNo");
|
|
|
if (StrUtil.isBlank(orderNo)) {
|
|
if (StrUtil.isBlank(orderNo)) {
|
|
|
log.error("[汇安融通知] 缺少 third_order_no,无法定位订单: {}", params);
|
|
log.error("[汇安融通知] 缺少 third_order_no,无法定位订单: {}", params);
|
|
|
return SUCCESS;
|
|
return SUCCESS;
|
|
@@ -108,28 +111,42 @@ public class HarNotifyService {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
order.setTradeStatus(newStatus);
|
|
order.setTradeStatus(newStatus);
|
|
|
- if (StrUtil.isNotBlank(params.get("order_no"))) {
|
|
|
|
|
- order.setUpstreamOrderNo(params.get("order_no"));
|
|
|
|
|
|
|
+ String upstreamOrderNo = pick(params, "order_no", "orderNo");
|
|
|
|
|
+ if (upstreamOrderNo != null) {
|
|
|
|
|
+ order.setUpstreamOrderNo(upstreamOrderNo);
|
|
|
}
|
|
}
|
|
|
- if (StrUtil.isNotBlank(params.get("platform_trade_no"))) {
|
|
|
|
|
- order.setPlatformTradeNo(params.get("platform_trade_no"));
|
|
|
|
|
|
|
+ // 规范里叫 platform_trade_no,上游实际叫 alipay_order_no / alipayOrderNo
|
|
|
|
|
+ // —— 2026-09-28 实测(规范第三次被证伪:前两次是 subject、reason)。
|
|
|
|
|
+ String platformTradeNo = pick(params, "platform_trade_no", "alipay_order_no", "alipayOrderNo");
|
|
|
|
|
+ if (platformTradeNo != null) {
|
|
|
|
|
+ order.setPlatformTradeNo(platformTradeNo);
|
|
|
}
|
|
}
|
|
|
- order.setTradeTime(parseTime(params.get("trade_time")));
|
|
|
|
|
- order.setRefundTime(parseTime(params.get("refund_time")));
|
|
|
|
|
- OffsetDateTime upstreamCreateTime = parseTime(params.get("create_time"));
|
|
|
|
|
|
|
+ // 规范里叫 trade_time,上游实际叫 trade_date / tradeDate
|
|
|
|
|
+ OffsetDateTime tradeTime = parseTime(pick(params, "trade_time", "trade_date", "tradeDate"));
|
|
|
|
|
+ order.setTradeTime(tradeTime);
|
|
|
|
|
+ // 退款通知的字段名**至今没见过** —— 只认规范里那个 refund_time,真实叫什么
|
|
|
|
|
+ // 等退款通知自己报上来(下面那行 WARN 正是在等它)。不按想象补名字。
|
|
|
|
|
+ OffsetDateTime refundTime = parseTime(pick(params, "refund_time"));
|
|
|
|
|
+ order.setRefundTime(refundTime);
|
|
|
|
|
+ OffsetDateTime upstreamCreateTime = parseTime(pick(params, "create_time"));
|
|
|
if (upstreamCreateTime != null) {
|
|
if (upstreamCreateTime != null) {
|
|
|
order.setUpstreamCreateTime(upstreamCreateTime);
|
|
order.setUpstreamCreateTime(upstreamCreateTime);
|
|
|
}
|
|
}
|
|
|
- // 付款成功/退款成功的通知里,platform_trade_no 与 trade_time 按规范应当在场。
|
|
|
|
|
- // 不在场就说明字段名与我们对不上 —— 这份规范已被证伪过两次(只认 subject 不认
|
|
|
|
|
- // title;reason 全篇没写却必填)。通知的重试窗口只有约 25 秒,错过就再也看不到
|
|
|
|
|
- // 原文,所以异常这件事必须在收到的那一刻记下来。
|
|
|
|
|
- // 每次都报 ⇒ 上游就不送,那时该改的是这句话,不是再加日志。
|
|
|
|
|
- if ((HarOrderStatus.PAID.equals(newStatus) || HarOrderStatus.REFUNDED.equals(newStatus))
|
|
|
|
|
- && (StrUtil.isBlank(params.get("platform_trade_no"))
|
|
|
|
|
- || StrUtil.isBlank(params.get("trade_time")))) {
|
|
|
|
|
- log.warn("[汇安融通知] 支付/退款成功的通知里缺 platform_trade_no 或 trade_time: "
|
|
|
|
|
- + "order_no={}, status={}, 全部参数={}",
|
|
|
|
|
|
|
+ // 实收金额:上游叫 actual_money,而且**只有这一份**,没有 camelCase 孪生。
|
|
|
|
|
+ // 它到底是不是「实收」:这一次的 0.01 单里它与 total_amount 相等,分辨不出来 ——
|
|
|
|
|
+ // 取它是因为另一个金额字段(total_amount)已经在了,两个同义字段不会并排发。
|
|
|
|
|
+ String actualMoney = pick(params, "actual_money");
|
|
|
|
|
+ if (actualMoney != null) {
|
|
|
|
|
+ order.setRcvAmount(parseAmount(actualMoney));
|
|
|
|
|
+ }
|
|
|
|
|
+ // 联调期诊断:上面几格填不上,就是字段名又变了。**整份参数**打出来 ——
|
|
|
|
|
+ // 通知重试窗口只有约 25 秒,错过就再也看不到原文。
|
|
|
|
|
+ // 每次都报 ⇒ 上游压根不送,那时该改的是这句话,不是再加日志。
|
|
|
|
|
+ boolean paidOrRefunded = HarOrderStatus.PAID.equals(newStatus)
|
|
|
|
|
+ || HarOrderStatus.REFUNDED.equals(newStatus);
|
|
|
|
|
+ boolean refundHasNoTime = HarOrderStatus.REFUNDED.equals(newStatus) && refundTime == null;
|
|
|
|
|
+ if ((paidOrRefunded && (platformTradeNo == null || tradeTime == null)) || refundHasNoTime) {
|
|
|
|
|
+ log.warn("[汇安融通知] 通知里缺关键字段: order_no={}, status={}, 全部参数={}",
|
|
|
orderNo, newStatus, new TreeMap<>(params));
|
|
orderNo, newStatus, new TreeMap<>(params));
|
|
|
}
|
|
}
|
|
|
orderService.reschedulePoll(order);
|
|
orderService.reschedulePoll(order);
|
|
@@ -140,6 +157,36 @@ public class HarNotifyService {
|
|
|
return SUCCESS;
|
|
return SUCCESS;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按次序取第一个非空值。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 存在的理由:上游对同一个字段发 camelCase + snake_case **两份**(2026-09-28 实测,
|
|
|
|
|
+ * 值一致),而规范里写的名字有时候两份都不是(platform_trade_no / trade_time)。
|
|
|
|
|
+ * 这里收的每一个名字都是**实测见过或规范写过的**,没有一个是照语义推出来的 ——
|
|
|
|
|
+ * 这份规范已经被证伪三次,按它推名字就是再造一次同样的缺陷。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static String pick(Map<String, String> params, String... keys) {
|
|
|
|
|
+ for (String key : keys) {
|
|
|
|
|
+ if (StrUtil.isNotBlank(params.get(key))) {
|
|
|
|
|
+ return params.get(key);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 空串不能被解析成 0,否则对账口径失真 —— 同 HarOrderService#parseAmount */
|
|
|
|
|
+ private BigDecimal parseAmount(String value) {
|
|
|
|
|
+ if (StrUtil.isBlank(value)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new BigDecimal(value.trim());
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ log.warn("[汇安融通知] 金额解析失败: {}", value);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private OffsetDateTime parseTime(String value) {
|
|
private OffsetDateTime parseTime(String value) {
|
|
|
if (StrUtil.isBlank(value)) {
|
|
if (StrUtil.isBlank(value)) {
|
|
|
return null;
|
|
return null;
|