Bladeren bron

fix(乐收赢): 回调报文原文不再随响应体外发

管理端抽屉的「回调轨迹」原先把 raw_body 铺在一个 <pre> 里,于是页面上出现的是
上游自己的字段名(alipay_order_no / trade_date / actual_money …)和一份 JSON。
用户 2026-09-30 裁定:这些属于上游的信息,不该到使用者眼前。

只删前端那一行不够 —— 报文照样随响应体进浏览器(F12 一看就有),
且下一个人顺手补个折叠面板就又放出去了。堵在 @JsonIgnore 这一层,
这条通道结构上不存在;@JsonIgnore 同时关掉反序列化方向,而本实体没有任何
从 JSON 入站的路径(只读审计表),不存在被它挡住的合法输入。

原话仍然留在库里(pay_lsy_notify_log.raw_body,已脱敏、已截断),
只是取用方式从「页面点到」变成「有权限的运维查库」。

测试:LsyNotifyLogRawBodySerializationTest 2 条 —— 序列化后连 raw_body 这个键名
都不存在(不是「值为空」)、rawBody 为 null 时同样不出现(2 条绿)。
alphaH 2 weken geleden
bovenliggende
commit
0d6cdfd4f4

+ 22 - 0
java/src/main/java/com/payment/platform/module/payment/leshouying/entity/LsyNotifyLogEntity.java

@@ -1,6 +1,7 @@
 package com.payment.platform.module.payment.leshouying.entity;
 
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
 import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.payment.platform.common.base.PaymentTenantBaseEntity;
@@ -48,7 +49,28 @@ public class LsyNotifyLogEntity extends PaymentTenantBaseEntity {
      * §一「★★ 2026-09-22 裁定」。
      */
     private String refundNo;
+
+    /**
+     * 上游回调报文原文(已过 {@code LeshouyingSensitive.mask})。**永不外发**。
+     *
+     * <p>【为什么在实体上直接堵死,而不是「记得别渲染」】管理端抽屉的「回调轨迹」曾把它
+     * 原样铺在一个 {@code <pre>} 里 —— 于是页面上出现的是**上游自己的字段名**
+     * ({@code alipay_order_no} / {@code trade_date} / {@code actual_money} …)和一份 JSON。
+     * 用户 2026-09-30 裁定:这些属于上游的信息,不该到使用者眼前。
+     * 只删前端那一行是不够的 —— 报文照样随响应体进浏览器(F12 一看就有),
+     * 且下一个人「顺手补个折叠面板」就又放出去了。堵在序列化这一层,
+     * 这条通道**结构上**不存在。
+     *
+     * <p>【保留在库里,不删列】排障要看的原话仍在 {@code pay_lsy_notify_log.raw_body},
+     * 只是取用方式从「页面点到」变成「有权限的运维查库」。截断与脱敏规则见
+     * {@code LsyNotifyService#logNotify}。
+     *
+     * <p>@JsonIgnore 同时关掉反序列化方向 —— 本实体没有任何从 JSON 入站的路径(只读审计表),
+     * 不存在被它挡住的合法输入。
+     */
+    @JsonIgnore
     private String rawBody;
+
     private String verifyStatus;
     private String processStatus;
     private String errorMsg;

+ 97 - 0
java/src/test/java/com/payment/platform/module/payment/leshouying/entity/LsyNotifyLogRawBodySerializationTest.java

@@ -0,0 +1,97 @@
+package com.payment.platform.module.payment.leshouying.entity;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.junit.jupiter.api.Test;
+
+import java.util.TimeZone;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * 回调审计行的**上游原文不得随响应体出网**。
+ *
+ * <p>【要修的是什么】管理端订单抽屉的「回调轨迹」曾把 {@code raw_body} 原样铺在一个
+ * {@code <pre>} 里,于是页面上出现的是上游自己的字段名({@code alipay_order_no} /
+ * {@code trade_date} / {@code actual_money} …)和一份 JSON。用户 2026-09-30 裁定:
+ * 这些是上游的信息,不该到使用者眼前。
+ *
+ * <p>【为什么钉子下在实体上,而不是只删前端那一行】报文是随**响应体**进浏览器的 ——
+ * 前端不渲染不等于拿不到(F12 一看就有),且下一个人「顺手补个折叠面板」就又放出去了。
+ * 堵在序列化层,这条通道**结构上**不存在。所以本用例断言的是**真跑一遍 Jackson 出来的 JSON**,
+ * 不是「注解在场」这种代理指标。
+ *
+ * <p>【量具的诚实说明】线上那支 mapper 是 Spring 容器里由 {@code JacksonConfig} 建的那个 Bean。
+ * 本类构造的是一份按 {@code JacksonConfig:20-24} **逐行配的等价物**(SNAKE_CASE + JavaTimeModule
+ * + 关时间戳 + 东八区),**不是同一个实例**。这不影响本用例的结论:{@code @JsonIgnore} 是
+ * 注解级行为,与 mapper 的命名策略、时间模块都无关 —— 但它决定了「出网的键叫什么」
+ * ({@code rawBody} 还是 {@code raw_body}),而那正是要断言的东西,所以命名策略必须配成一样的。
+ */
+class LsyNotifyLogRawBodySerializationTest {
+
+    /**
+     * 一份**形态照抄真实报文**的原文:里面带上游的字段名与一个只在本用例出现的哨兵值。
+     * 哨兵是必需的 —— 只断言「键名 raw_body 不在」,会在「值被塞进别的键」时漏过去。
+     */
+    private static final String SENTINEL = "ZX9SENTINEL";
+    private static final String UPSTREAM_RAW_BODY =
+            "{\"return_code\":\"SUCCESS\",\"alipay_order_no\":\"2026093022001" + SENTINEL + "\","
+                    + "\"actual_money\":\"1.00\",\"trade_date\":\"2026-09-30 12:00:00\"}";
+
+    @Test
+    void rawBody_不出网_连键名都不存在() throws Exception {
+        LsyNotifyLogEntity log = new LsyNotifyLogEntity();
+        log.setRawBody(UPSTREAM_RAW_BODY);
+        log.setVerifyStatus("PASS");
+        log.setTradeType("1");
+
+        String json = mapper().writeValueAsString(log);
+        JsonNode node = mapper().readTree(json);
+
+        assertFalse(node.has("raw_body"),
+                "raw_body 这个键不该出现在响应体里(前端曾把它渲染成 <pre>)。实测 JSON=" + json);
+        assertFalse(json.contains(SENTINEL),
+                "原文里的**值**也不得从别的键漏出去 —— 只查键名会在字段改名后漏过去。实测 JSON=" + json);
+        assertFalse(json.contains("alipay_order_no"),
+                "上游的字段名本身就是不该给使用者看的信息(用户裁定的原话)。实测 JSON=" + json);
+        assertFalse(json.contains("actual_money"), "同上。实测 JSON=" + json);
+
+        // ★ 正对照:同一个对象的**其它**字段必须照常出网。
+        //   没有这一条,上面四个断言在「整个序列化坏掉 / 返回空对象」时也全绿 ——
+        //   那就成了一条永远通过、什么也没钉住的用例。
+        assertTrue(node.has("verify_status"),
+                "正对照:verify_status 必须照常出网,否则上面的「没有」是因为整个序列化没工作。实测 JSON=" + json);
+        assertEquals("PASS", node.get("verify_status").asText(),
+                "正对照必须是**真值**,不能只看键在不在。实测 JSON=" + json);
+        assertTrue(node.has("trade_type"), "正对照:trade_type 也要在。实测 JSON=" + json);
+    }
+
+    /**
+     * 值为 null 时同样不出网 —— 「有值才忽略」是两种不同的实现,
+     * 而 {@code @JsonIgnore} 是**无条件**的。这条钉住我们用的确实是后者
+     * (将来若有人换成 {@code @JsonInclude(NON_NULL)},raw_body 有值的那些行照样会漏出去)。
+     */
+    @Test
+    void rawBody_为null时也不出现() throws Exception {
+        LsyNotifyLogEntity log = new LsyNotifyLogEntity();
+        log.setVerifyStatus("FAIL");
+
+        JsonNode node = mapper().readTree(mapper().writeValueAsString(log));
+
+        assertFalse(node.has("raw_body"), "null 值同样不得出现 —— 忽略必须是无条件的。实测=" + node);
+        assertTrue(node.has("verify_status"), "正对照。实测=" + node);
+    }
+
+    /** 与 {@code JacksonConfig#objectMapper} 逐行等价的量具(不是容器里那一支实例,见类注释)。 */
+    private static ObjectMapper mapper() {
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+        mapper.registerModule(new JavaTimeModule());
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        mapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
+        return mapper;
+    }
+}