Przeglądaj źródła

fix(进件): channel_id 的空白处理四条腿统一 trim(Task 4 修复轮 1 · b)

**行为改动。** 四条腿里创建/更新走 upstreamInteger(第一步就是 trim),提交入网/入网状态
按各自页的 String 声明原样发配置字符串 ⇒ 同一个配置值「两条腿修剪、两条腿不修剪」。

改前实测(本提交的用例红在这一点上,原文见 task-4-report.md 修复轮 1 节):
props.setChannelId("  1001  ") 之下,create/update 发 1001,而
commitMerchantInfo/reportQuery 发 "  1001  "。危害只在联调暴露 —— 配置来自环境变量
(用户手工注入),带首尾空白是现实风险,而 channel_id 进签名原文 ⇒ 症状是上游回一句签名错误。

改法:新增私有 stringChannelId()(只 trim、不做数字规范化),两条 String 腿共用它
(抄两份就会出现「某天只给其中一处加处理」,与 requireOnboardingCredentials 同一理由)。
★ **不碰类型轴**:创建/更新仍发 Integer(11)、提交入网/入网状态仍发 String ——
「按各页声明发」是既定口径,这次只统一空白这一个轴。新用例里两条 assertInstanceOf
就是钉这条的(少了它,将来「顺手统一」成四条腿都走 upstreamInteger 时用例仍绿)。

连带订正三处随行为改动而失真的句子(都在本提交内,否则提交后即刻是假话):
commitMerchantInfo 的 javadoc「原样发配置字符串,不过 upstreamInteger」、以及两条腿里
同义的两行行内注释 —— 现在写的是「不过 upstreamInteger,但仍过 stringChannelId trim」。

TDD:先写 channelId_paddedConfig_trimmedIdenticallyOnAllFourLegs 并看着它红
(失败信息逐字给出四腿的实际值:{create=1001, update=1001, commit="  1001  ", report="  1001  "}),
再实现。本类 66 → 67 绿,0 fail / 0 error / 0 skipped,BUILD SUCCESS。
alphaH 3 tygodni temu
rodzic
commit
7d54729933

+ 34 - 6
java/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingService.java

@@ -549,8 +549,10 @@ public class LsyOnboardingService {
      * (首段小写 + {@code L} 大写):键名进**签名原文**,改一个字母就换签名。
      * 同族先例:创建腿的 {@code fSettle_account_type}、上游自己拼错的 {@code picture_idcard_hander}。
      * <br>★ {@code channel_id} 在本页声明成光秃秃的 {@code String}(创建页 / 更新页是
-     * {@code Integer(11)})⇒ **原样发配置字符串,不过 {@code upstreamInteger}**。
-     * 同一物理值在四个页面上的声明类型不同,本类按**各页自己的声明**发(计划 §0.5 对同批字段的口径)。
+     * {@code Integer(11)})⇒ **不过 {@code upstreamInteger}**(它做的是数字规范化,本页按声明不需要),
+     * 但仍过 {@link #stringChannelId} **trim** —— 空白处理四条腿必须一致,理由见那个方法。
+     * 同一物理值在四个页面上的声明类型不同,本类按**各页自己的声明**发(计划 §0.5 对同批字段的口径);
+     * 统一的是**空白**这一个轴,**不是**类型。
      *
      * <p>★ 租户缺口(R-4)对本法同样成立:{@code dto.mchNo} 不受租户约束,三-B 的端点必须先证明
      * 它属于当前租户,见类 javadoc。
@@ -574,8 +576,9 @@ public class LsyOnboardingService {
 
         // ③ 组装:9 个键
         Map<String, Object> p = mchNoParams(dto.getMchNo());
-        // 本页把 channel_id 声明成 String ⇒ 原样发(与创建/更新页的 Integer(11) 不同,见 javadoc)
-        p.put("channel_id", props.getChannelId());
+        // 本页把 channel_id 声明成 String ⇒ 不做数字规范化(与创建/更新页的 Integer(11) 不同,
+        // 见 javadoc);但 trim 是与那两条腿共用的口径(见 stringChannelId)
+        p.put("channel_id", stringChannelId());
         p.put("mcht_aptitude", dto.getMchtAptitude());
         p.put("mcht_aptitude_note", dto.getMchtAptitudeNote());
         // ★ store_Loca / mcht_Landuse 的大小写是签名原文,不得规整
@@ -652,8 +655,9 @@ public class LsyOnboardingService {
 
         // ② mchNo 必填 + ③ 组装
         Map<String, Object> p = mchNoParams(mchNo);
-        // 本页把 channel_id 声明成 String ⇒ 原样发配置字符串(与创建/更新页的 Integer(11) 不同)
-        p.put("channel_id", props.getChannelId());
+        // 本页把 channel_id 声明成 String ⇒ 不做数字规范化(与创建/更新页的 Integer(11) 不同),
+        // 但 trim 与那两条腿同一口径(见 stringChannelId)
+        p.put("channel_id", stringChannelId());
 
         // ④ 上送 + 解析:只做 asText 一件事,不映射任何码表(见 javadoc 的两套冲突映射)
         return asText(client.postApi("/api/merchant/reportQuery", p).get("reviewed_status"));
@@ -687,6 +691,30 @@ public class LsyOnboardingService {
                 + " 项),明细见服务端日志");
     }
 
+    /**
+     * 进件配置里的 {@code String} 型 {@code channel_id} → 出站值:**只 trim**。
+     *
+     * <p>提交入网页(196236563)与入网状态页(196236564)把 {@code channel_id} 声明成
+     * {@code String},创建页 / 更新页声明 {@code Integer(11)} ⇒ 本方法**不做**
+     * {@link #upstreamInteger} 那套数字规范化(「按各页声明发」是既定口径),
+     * 只把首尾空白去掉。两条 {@code String} 腿共用这一个方法,理由同
+     * {@link #requireOnboardingCredentials}:抄两份就会出现「某天只给其中一处加处理」。
+     *
+     * <p>★ <b>为什么必须 trim</b>:另外两条腿走 {@code upstreamInteger},而它的第一步就是
+     * {@code trim()}。这两条若原样发,同一个配置值就会「两条腿修剪、两条腿不修剪」——
+     * 配置来自**环境变量**(联调时用户手工注入),带首尾空白是现实风险,而
+     * {@code channel_id} 进**签名原文** ⇒ 症状是上游回一句签名错误,本侧看不出是空白。
+     *
+     * <p>★ 空白值到不了这里:调用方先过 {@link #requireOnboardingCredentials},而
+     * {@code channel_id} 正是那五项之一 ⇒ 本方法只需要处理「非空白、但首尾带空白」这一态。
+     * 写法仍取 {@code null} 安全的形态(与 {@link #upstreamInteger} 第一行同款),
+     * 这样即便将来闸与组装的顺序被改动,也不会在这里 NPE。
+     */
+    private String stringChannelId() {
+        String channelId = props.getChannelId();
+        return channelId == null ? null : channelId.trim();
+    }
+
     /**
      * 增量语义的落点:**有值才进 Map**,空白({@code null} / 空串 / 全空格)一律**不 put**。
      *

+ 51 - 0
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingServiceTest.java

@@ -2442,6 +2442,57 @@ class LsyOnboardingServiceTest {
         verifyNoInteractions(client);
     }
 
+    // ---- 四条腿共用的口径:channel_id 的空白处理(修复轮 1 · (b))----
+
+    /**
+     * ★ {@code channel_id} 的**空白处理在四条腿上必须一致:统一 trim**。
+     *
+     * <p>为什么单独一条:四条腿里有两条走 {@code upstreamInteger}(它的第一步就是 {@code trim()}),
+     * 另两条按各自页声明的 {@code String} **原样**发配置字符串 ⇒ 同一个配置值,
+     * 两条腿修剪、两条腿不修剪。实测(改前):{@code props.setChannelId("  1001  ")} 之下,
+     * 创建/更新腿发 {@code 1001},而提交入网/入网状态腿发 {@code "  1001  "}。
+     * 危害只在联调暴露:配置来自**环境变量**(用户手工注入),带首尾空白是现实风险,
+     * 而 {@code channel_id} 进签名原文 ⇒ 签名对不上,症状是上游回一句签名错误、本侧看不出是空白。
+     *
+     * <p>★ <b>本用例同时钉住「类型差异保留」</b>:创建/更新两页声明 {@code Integer(11)}、
+     * 提交入网/入网状态两页声明 {@code String},「按各自页的类型发」是既定口径
+     * (计划 Task 4 §6-2 / 联调清单 C5)⇒ 统一的是**空白**这一个轴,**不是**类型。
+     * 少了这两条 {@code assertInstanceOf},下一个人「顺手统一」成四条腿都走
+     * {@code upstreamInteger} 时本用例仍绿 —— 那就把两页声明的是 {@code String} 这件事抹掉了。
+     */
+    @Test
+    void channelId_paddedConfig_trimmedIdenticallyOnAllFourLegs() {
+        withOnboardingCredentials();
+        // 环境变量注入的现实形态:干净值被首尾空白包住
+        props.setChannelId("  " + CFG_CHANNEL_ID + "  ");
+        // 四条腿共用一次桩:createMerchant 要读 merchant_no,其余三条不读应答
+        stubUpstream(MERCHANT_CREATE_OK);
+
+        service.createMerchant(merchantDto());
+        service.updateMerchant(updateDto());
+        service.commitMerchantInfo(commitDto());
+        service.reportQuery(MCH_NO);
+
+        Map<String, String> sentByLeg = new LinkedHashMap<>();
+        for (String path : new String[]{"/api/merchant/create", "/api/merchant/update"}) {
+            Object channelId = sentParams(path).get("channel_id");
+            assertInstanceOf(Integer.class, channelId,
+                    path + " 的 channel_id 按该页声明发 Integer(11),实际: " + channelId);
+            sentByLeg.put(path, String.valueOf(channelId));
+        }
+        for (String path : new String[]{"/api/merchant/commitMerchantInfo", "/api/merchant/reportQuery"}) {
+            Object channelId = sentParams(path).get("channel_id");
+            assertInstanceOf(String.class, channelId,
+                    path + " 的 channel_id 按该页声明发 String,实际: " + channelId);
+            sentByLeg.put(path, (String) channelId);
+        }
+
+        for (Map.Entry<String, String> leg : sentByLeg.entrySet()) {
+            assertEquals(CFG_CHANNEL_ID, leg.getValue(),
+                    "配置带首尾空白时四条腿必须发出同一个(trim 后的)channel_id,各腿实际: " + sentByLeg);
+        }
+    }
+
     /**
      * 把 {@link ListAppender} 挂到指定 logger 上、跑完 body、**无论成败都摘掉**
      * —— 与 {@code LsyNotifyServiceTest} / {@code LsyQrPayTest} 同款(本仓库按测试文件各自持有一份,