소스 검색

fix(汇安融): 下单/收款补 subject —— 上游不认规范里的 title,任何单都建不出来

线上实测(qcsj88888.com 开放接口 → 上游):bizContent 带非空 title,上游仍回
「详细参数校验异常,交易主题(subject)不能为空!」。换成纯 ASCII 的 title(HAR-TEST-ITEM)
复测,报文一字不变 ⇒ 不是编码问题,是字段名对不上:规范 docx 全篇只写 title
(subject 出现 0 次),线上上游只认 subject。docx 与线上不是同一版,以线上为准。

处理:两个都发,而不是把 title 改名成 subject。依据是上游成功反序列化了带 title 的
报文 —— 它报的是校验失败,不是解析失败 ⇒ 多余字段被容忍。这样「上游只认 subject」
与「上游两个都认」两种情况下都成立。

实现成 title 的只读别名(getSubject() 挂 @JsonProperty("subject")),而不是另立一个
字段再在两个 builder 里各赋一次值 —— 那样两个值有机会不同步,且漏一处不会红。

影响面:修前这个通道 100% 建不出单(门店、金额、trade_type 都轮不到上游校验)。

断言走 HarClient.toBizContent 本体(生产那支是裸 new ObjectMapper()),
不用容器里那个 SNAKE_CASE 的 @Primary mapper —— 那不是同一支量具,钉不住出网形态。

两向读数:加断言、未改生产码 ⇒ 2 红,失败信息里正是线上那份
{"title":"…","merchant_code":"M001"}(logs/_red-subject.log);
改后全量 846 绿、0 失败(logs/_har-subject-full.log,改前基线 840)。
alphaH 3 주 전
부모
커밋
37539b1845

+ 14 - 0
java/src/main/java/com/payment/platform/module/payment/har/client/dto/HarPayRequest.java

@@ -13,6 +13,20 @@ public class HarPayRequest {
 
     private String title;
 
+    /**
+     * 交易主题 —— 与 {@link #title} 同值,出网字段名是 {@code subject}。
+     *
+     * <p>规范 docx 里这个字段只叫 {@code title},但线上上游只认 {@code subject}:
+     * 2026-09-24 实测带非空 {@code title} 仍被回「交易主题(subject)不能为空」。
+     * 两个都发(上游容忍多余字段,见 {@code HarClientBizContentTest} 类注释)。
+     *
+     * <p>写成只读别名而非另立一个字段,是为了不让「两处赋值」有机会不同步。
+     */
+    @JsonProperty("subject")
+    public String getSubject() {
+        return title;
+    }
+
     /** 用户付款码 */
     @JsonProperty("auth_code")
     private String authCode;

+ 14 - 0
java/src/main/java/com/payment/platform/module/payment/har/client/dto/HarPrecreateRequest.java

@@ -13,6 +13,20 @@ public class HarPrecreateRequest {
 
     private String title;
 
+    /**
+     * 交易主题 —— 与 {@link #title} 同值,出网字段名是 {@code subject}。
+     *
+     * <p>规范 docx 里这个字段只叫 {@code title},但线上上游只认 {@code subject}:
+     * 2026-09-24 实测带非空 {@code title} 仍被回「交易主题(subject)不能为空」。
+     * 两个都发(上游容忍多余字段,见 {@code HarClientBizContentTest} 类注释)。
+     *
+     * <p>写成只读别名而非另立一个字段,是为了不让「两处赋值」有机会不同步。
+     */
+    @JsonProperty("subject")
+    public String getSubject() {
+        return title;
+    }
+
     /** 分期时必填,3/6/12/24 */
     @JsonProperty("stage_num")
     private String stageNum;

+ 89 - 0
java/src/test/java/com/payment/platform/module/payment/har/client/HarClientBizContentTest.java

@@ -0,0 +1,89 @@
+package com.payment.platform.module.payment.har.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.payment.platform.module.payment.har.client.dto.HarPayRequest;
+import com.payment.platform.module.payment.har.client.dto.HarPrecreateRequest;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 钉住下单/收款 bizContent 里的<b>交易主题字段名</b>:必须带 {@code subject}。
+ *
+ * <p>为什么值得一个测试:规范 docx(《支付宝支付接口接入规范260915》)里这个字段叫
+ * {@code title}(商品名称),全篇 0 次出现 {@code subject}。但<b>线上上游不认 {@code title}</b> ——
+ * 2026-09-24 对 {@code https://qcsj88888.com/api/v1/payment/openapi/har/order/create} 实测,
+ * 发出的 bizContent 里 {@code title} 非空,上游仍回:
+ *
+ * <pre>下单失败: 详细参数校验异常,交易主题(subject)不能为空!</pre>
+ *
+ * <p>换成纯 ASCII 的 title({@code HAR-TEST-ITEM})复测,报文一字不变 ⇒ 不是编码问题,
+ * 是<b>字段名对不上</b>。结论:docx 与线上上游不是同一版,以线上为准。
+ *
+ * <p>为什么两个都发而不是把 {@code title} 改名成 {@code subject}:上游<b>成功反序列化</b>了
+ * 带 {@code title} 的报文(否则报的会是解析失败,而不是校验失败),说明多余字段被容忍;
+ * 那么「两个都发」在「上游只认 subject」和「上游两个都认」两种情况下都成立。
+ *
+ * <p>断言走 {@link HarClient#toBizContent(Object)} 本体,而不是另起一个 ObjectMapper:
+ * 生产这支用的是裸 {@code new ObjectMapper()}({@link HarClient} 里的 MAPPER),
+ * 与容器里那个 SNAKE_CASE 的 {@code @Primary} mapper 不是同一支 —— 拿后者当量具,
+ * 测的是量具不是被测物。
+ */
+class HarClientBizContentTest {
+
+    /** 与 {@link HarClient#toBizContent(Object)} 同一支量具,仅用于把结果读回来断言。 */
+    private static final ObjectMapper OM = new ObjectMapper();
+
+    private static final String TITLE = "联调测试商品";
+
+    private JsonNode bizContentOf(Object req) throws Exception {
+        return OM.readTree(HarClient.toBizContent(req));
+    }
+
+    /**
+     * 断言 {@code subject} 存在、且跟着 {@code title} 走。
+     *
+     * <p>用两个不同的值各测一遍,是为了把「照抄一个常量」这种假修复也照出来 ——
+     * 只测一个值时,写死成那个值同样能绿。
+     */
+    private void assertSubjectTracksTitle(Object req, java.util.function.Consumer<String> setTitle) throws Exception {
+        setTitle.accept(TITLE);
+        JsonNode n = bizContentOf(req);
+        assertSubject(n, TITLE);
+
+        setTitle.accept("另一个标题");
+        assertSubject(bizContentOf(req), "另一个标题");
+    }
+
+    private void assertSubject(JsonNode n, String expected) {
+        // 正对照:先证明这串 JSON 真的序列化出了字段。否则「序列化出空对象」时,
+        // 下面那条也只会报 subject 缺失,把方向指到错误的地方。
+        assertTrue(n.has("merchant_code"), "正对照:bizContent 应有 merchant_code,实际=" + n);
+
+        JsonNode subject = n.get("subject");
+        assertNotNull(subject,
+                "bizContent 缺 subject —— 上游会回「交易主题(subject)不能为空」,bizContent=" + n);
+        assertEquals(expected, subject.asText());
+
+        // 规范 docx 写的是 title,一并保留(取舍见类注释)。
+        assertEquals(expected, n.path("title").asText(null), "title 应仍在(docx 契约)");
+    }
+
+    @Test
+    void precreate_bizContent_carriesSubject() throws Exception {
+        HarPrecreateRequest req = new HarPrecreateRequest();
+        req.setMerchantCode("M001");
+        assertSubjectTracksTitle(req, req::setTitle);
+    }
+
+    @Test
+    void pay_bizContent_carriesSubject() throws Exception {
+        HarPayRequest req = new HarPayRequest();
+        req.setMerchantCode("M001");
+        req.setAuthCode("280812345678901234");
+        assertSubjectTracksTitle(req, req::setTitle);
+    }
+}