瀏覽代碼

fix(汇安融): 查询响应按上游**实际的**字段名收 —— 规范第四次被证伪

用户的第二份实测报错素材:那一行「查询成功但手续费/渠道成本仍为空」的 WARN 把查询
响应 detail 打了出来(订单 HAR1301886673700786176 退款后那次轮询)。这是**第一份真实的
查询响应报文**,也是「实收金额/交易时间/支付宝交易号一直是空」的根因 —— 查询这条腿
用的还是规范里的名字,而上游发的是另一套:

  platform_trade_no ⇒ alipay_order_no
  trade_time        ⇒ trade_date
  refund_time       ⇒ refund_date
  rcv_amount        ⇒ actual_money

修法是 @JsonAlias 并列,不是替换:别名与 @JsonProperty 同时存在时主名优先,两套名字
都发(或规范哪天被改回来)都能收。没有任何读数说过规范那套会被撤掉,删掉它们就是
又一次拿推理当读数。

规范名**保留**还有第二个用处:applyQueryDetail 那边一格没动,它消费的仍是规范名。

其余字段**没加**别名,因为这份报文里找不到证据:
- 手续费 / 渠道成本在 detail 里**根本没有对应字段** —— 未映射的金额只有 har_money=0 /
  request_money=null / merchant_money=null,而 0 与 null 在「这笔 0.01 的单没收费」与
  「这压根不是那个字段」两种解释下读数相同 ⇒ 分辨不了,不猜。(那两格为什么空,
  由此从「字段名对不上」改写为「上游不送」,但这要部署后再确认一次。)
- price:上游的 total_amount 在唯一一份报文里与本地 price 相等,分辨不了,而本地 price
  来自我方下单,猜错反而会把它弄坏。
- create_time:没有候选字段。

测试侧:HarResponseParserTest 新增一条用例,把那份 detail **原文照抄**(一个字没改)跑
一遍。它的价值不在于这几行断言,而在于这类缺陷在本类里**此前完全不可见** ——
原有的 toBean_mapsDetail 用的是规范里的名字,所以「上游根本不发这些名字」这件事
永远测不出来。四格逐个变异验证:各自摘掉别名 ⇒ 对应断言红。

部署后:点一次「同步」即可把 HAR1301886673700786176 那四格补上(轮询不会回头,
状态 2 是终态)。
alphaH 2 周之前
父節點
當前提交
ba978ea31f

+ 29 - 0
java/src/main/java/com/payment/platform/module/payment/har/client/dto/HarQueryDetail.java

@@ -1,5 +1,6 @@
 package com.payment.platform.module.payment.har.client.dto;
 
+import com.fasterxml.jackson.annotation.JsonAlias;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
@@ -10,6 +11,26 @@ import lombok.Data;
  * ignoreUnknown 保证上游新增字段不会导致解析失败。
  * 金额字段一律用 String 接收:上游给的是 "12.25" 这样的字符串,
  * 用 BigDecimal 接收在字段为空串时会抛异常,而空串在上游是有可能出现。
+ *
+ * <p><b>★ 2026-09-28:四格加了 {@link JsonAlias},因为上游发的名字与规范不同。</b>
+ * 依据是第一份**真实的**查询响应 detail(订单 HAR1301886673700786176 退款后那次轮询,
+ * 原文见 {@code HarResponseParserTest#toBean_realQueryDetail_mapsTheNamesUpstreamActuallySends}):
+ * <pre>
+ *   platform_trade_no ⇒ alipay_order_no   (实为支付宝交易号)
+ *   trade_time        ⇒ trade_date
+ *   refund_time       ⇒ refund_date
+ *   rcv_amount        ⇒ actual_money
+ * </pre>
+ * 这是这份 docx 规范**第四次**被证伪。别名与 {@code @JsonProperty} 同时存在时
+ * <b>主名优先</b>,所以两套名字都发(或规范哪天被改回来)都能收 ——
+ * 没有任何读数说过规范那套会被撤掉,**删掉它们就是又一次拿推理当读数**。
+ *
+ * <p>其余字段**没有**加别名,因为这份真实报文里找不到证据:
+ * 手续费 / 渠道成本在 detail 里**根本没有对应字段**(未映射的金额只有
+ * {@code har_money} / {@code request_money} / {@code merchant_money},值分别是 0 / null / null,
+ * 而 0 与 null 在「这笔 0.01 的单没收费」与「这压根不是那个字段」两种解释下读数相同);
+ * {@code price} 同理 —— 上游的 {@code total_amount} 在唯一一份报文里与 price 相等,
+ * 分辨不了,而本地 price 来自我方下单,写错反而会把它弄坏。
  */
 @Data
 @JsonIgnoreProperties(ignoreUnknown = true)
@@ -41,18 +62,26 @@ public class HarQueryDetail {
 
     private String percent;
 
+    /** 规范叫 trade_time,上游**实际**发的是 trade_date(2026-09-28 实测) */
     @JsonProperty("trade_time")
+    @JsonAlias({"trade_date", "tradeDate"})
     private String tradeTime;
 
+    /** 规范叫 refund_time,上游**实际**发的是 refund_date(2026-09-28 实测) */
     @JsonProperty("refund_time")
+    @JsonAlias({"refund_date", "refundDate"})
     private String refundTime;
 
+    /** 规范叫 platform_trade_no,上游**实际**发的是 alipay_order_no(2026-09-28 实测) */
     @JsonProperty("platform_trade_no")
+    @JsonAlias({"alipay_order_no", "alipayOrderNo"})
     private String platformTradeNo;
 
     private String title;
 
+    /** 规范叫 rcv_amount,上游**实际**发的是 actual_money(2026-09-28 实测) */
     @JsonProperty("rcv_amount")
+    @JsonAlias("actual_money")
     private String rcvAmount;
 
     @JsonProperty("fee_amount")

+ 40 - 0
java/src/test/java/com/payment/platform/module/payment/har/client/HarResponseParserTest.java

@@ -82,6 +82,46 @@ class HarResponseParserTest {
         assertEquals("12.20", bean.getRcvAmount());
     }
 
+    /**
+     * 2026-09-28 第一份**真实的**查询响应 detail —— 原文照抄,一个字没改
+     * (出处:{@code HarOrderService} 那行「查询成功但手续费/渠道成本仍为空」的 WARN,
+     *  它把 detail 原文打了出来,订单 HAR1301886673700786176 退款后那次轮询)。
+     *
+     * 为什么这条用例必须存在:上面 `toBean_mapsDetail` 用的是**规范里的**字段名,
+     * 所以「上游根本不发这些名字」这个缺陷在本类里**完全不可见** —— 它活到今天的原因
+     * 就是这个。规范名 ⇒ 上游实际名(四格):
+     *   platform_trade_no ⇒ alipay_order_no
+     *   trade_time        ⇒ trade_date
+     *   refund_time       ⇒ refund_date
+     *   rcv_amount        ⇒ actual_money
+     * 这是这份 docx 规范**第四次**被证伪(前三次:title、reason、通知里那两个名字)。
+     *
+     * 顺带一条负读数,别当发现用:这份 detail 里**没有**任何名字像「手续费 / 渠道成本」
+     * 的字段(未映射的金额只有 har_money=0 / request_money=null / merchant_money=null)。
+     * 0 与 null 在「这笔 0.01 的单没收费」与「这压根不是手续费字段」两种解释下**读数相同**
+     * ⇒ 分辨不了,所以那两格**不猜**、不映射。
+     */
+    @Test
+    void toBean_realQueryDetail_mapsTheNamesUpstreamActuallySends() {
+        String detail = """
+                {"alipay_order_no":"2026092823001417741424011978","order_no":"1156891538702077952","merchant_code":"142357140143751171","total_amount":0.01,"actual_money":0.01,"trade_date":"2026-09-28 09:54:51","buyer_logon_id":"475***@qq.com","buyer_user_id":"2088122583917741","status":2,"refund_date":"2026-09-28 10:23:54","reason":"商户申请退款","store_id":"785632","oper_name":null,"shop_name":null,"operator_id":"","stage_num":"","percent":"","subject":"测试0928","extra_params":"{}","refund_able":false,"biz":"har_ali_un_stage","har_money":0,"trade_type":"alipay_un_stage","phone_no":"0","shop_code":"0","request_money":null,"merchant_money":null,"app_apply":false,"related_orders":null,"third_order_no":"HAR1301886673700786176"}
+                """;
+
+        HarQueryDetail bean = HarResponseParser.toBean(detail, HarQueryDetail.class);
+
+        assertEquals("2026092823001417741424011978", bean.getPlatformTradeNo(),
+                "支付宝交易号在查询响应里叫 alipay_order_no");
+        assertEquals("2026-09-28 09:54:51", bean.getTradeTime(),
+                "交易时间在查询响应里叫 trade_date");
+        assertEquals("2026-09-28 10:23:54", bean.getRefundTime(),
+                "退款时间在查询响应里叫 refund_date");
+        assertEquals("0.01", bean.getRcvAmount(),
+                "实收金额在查询响应里叫 actual_money");
+        // 上游单号与状态仍是规范里那两个名字(这两格规范是对的)
+        assertEquals("1156891538702077952", bean.getOrderNo());
+        assertEquals(2, bean.getStatus());
+    }
+
     /** 空串在上游确实会出现,用 String 接收才不会炸 */
     @Test
     void toBean_blankAmounts_doNotThrow() {