Forráskód Böngészése

fix(进件): Task 5 评审 F1–F5(修复轮 2)—— 公司腿值断言补齐 + channel_type trim 钉住 + 残留清单与口径订正

F1 areaList_missingGbCode_leavesFieldNull:三种输入的 provinceCode 期望各由本用例
自己断言(前两格 null、第三格原值)—— 此前只断言 city/county,它自己声明的 ②③ 条
对 provinceCode 那一格是空的。

F2 anxinSignCompany_sendsAllElevenParams:9 个键的值全部断言(原只断言 4 个)。
前提实测:9 个夹具值两两不同(4 个 ANXIN_* 常量 + 5 个字面量)。
两向读数 —— 把 principal_cert_no 与 account_card_num 的 getter 互换:本用例红
(expected: <100000000000000003> but was: <6200000000000001>);
**同一变异体下退回修复前那 4 条断言 ⇒ 绿** ⇒ 缺口是真的,不是纸面推理。

F3 LsyOnboardingService.java:749:新增 channelType_paddedConfig_trimmedOnMccList
(加法路线,不动共享常量 CFG_CHANNEL_TYPE)。两向读数 —— 去掉 trimOrNull ⇒
本用例红(expected: <3> but was: <  3  >),而既有的 mccList_sendsChannelTypeFromConfig
仍绿(正是评审指出的那一格);改成行为等价的内联三元 ⇒ 仍绿。
生产代码本轮未改(该变异已还原,与 HEAD 逐字节一致)。

F4 LeshouyingSensitive 残留清单:加第 4 条「出站(单层)路径上,自由文本字段里的
11 位号码不被遮」,表头「下面三条」随之改成「前三条…第 4 条语境不同(出站只有一层)」。
写清它没被覆盖、为什么、且不是代码缺陷。

F5 同文件那句改名成本:两个半句都补口径 ——「引用」指编译进产物的代码
(新名全仓 6 处命中,其中 4 处是本文件 javadoc);测试里对新名零引用,
旧名 5 处命中无一处是代码(本文件 javadoc 2 / 测试 javadoc 1 / 历史计划文档 2)。

测试:定向 112 跑 0 红(LeshouyingSensitiveTest 29 / LsyOnboardingServiceTest 83);
全量 567 跑 0 红 BUILD SUCCESS。生产侧只动 javadoc(非注释改动行 0)。
alphaH 3 hete
szülő
commit
2f0cc4a9f8

+ 13 - 3
java/src/main/java/com/payment/platform/module/payment/leshouying/client/LeshouyingSensitive.java

@@ -66,11 +66,19 @@ import java.util.regex.Pattern;
  * 既可读成 ≥12 也可读成 >12,而代码是前者。
  *
  * 【具名的、已接受的残留(勿当成已覆盖)】值层对「键的写法」不变,对「值的再编码」不变。
- * 下面三条是**实测确认遮不住**的(每条都有对应的断言测试):
+ * 下面**前三条**是**实测确认遮不住**的(每条都有对应的断言测试);**第 4 条的语境不同**
+ * (出站路径只有一层),见条内说明:
  *   1. HTML 实体(&#49;&#51;…)—— `&` 在键层的排除集里,键层匹配到空值就停,
  *      所以标签会被重写成 `auth_code=***`,而实体编码的数字原样留在后面。
  *   2. 码内被插入分隔符(1345 7360 7383 5418)—— 连续数字被打断,值层接不住,只遮得住第一段。
  *   3. 键名本身也被编码掉的情形(如整包 base64)。
+ *   4. ★ **出站(单层)路径上,自由文本字段里的 11 位号码不被遮** —— 与上面三条**不同源**:
+ *      出站 trace 只有键层(`maskByKey`,见 `LeshouyingClient.traceOutbound`),值层不参与;
+ *      且 11 位本就低于值层的闭区间下界 12。`merchant_address` 这类字段(公司版必填、确实出站)
+ *      的值由**商户自己**书写 —— 里面写不写手机号不由我们决定,而键层只认键名,
+ *      这类字段的键名不含 PII 语义 ⇒ 键层**天生看不见**(不同于「知道是 PII 但有意豁免」)。
+ *      定性:**不是代码缺陷** —— 要修得加「按值形态认手机号」,那正是本文件拒绝的方向
+ *      (拒绝理由见上面的判据段)。**别把它写成已覆盖**。
  *
  * 【几条**看起来**像残留、实测其实被覆盖的(别误加进上面的清单)】
  *   - `auth_code=%31%33%34…`:**能遮**。键层的值尾巴 `[^"'&\s,}]*` 不含 `%`,故整段被消费。
@@ -97,8 +105,10 @@ public final class LeshouyingSensitive {
      * `contact_service`),判据 (b) 的**凭据** 2 个(付款码 `auth_code`、安心签验证码 `check_code`)。
      * 旧名字在说一件不再成立的事,而「PII 键管不管」正是读这个文件的人
      * 要问的第一个问题;留着一个失实的名字,等于用一行代码把答案说反。
-     * 改名成本实测为零:全仓引用只有本文件的声明处与 {@link #maskByKey} 里那一处,
-     * 测试文件里零引用。
+     * 改名成本实测为零:口径是**引用指「编译进产物的代码」** —— 按此口径只有本文件的声明处
+     * 与 {@link #maskByKey} 里那一处(新名全仓 6 处命中,另 4 处是本文件的 javadoc);
+     * **测试里对新名零引用**(旧名的 5 处命中没有一处是代码:本文件 javadoc 2、测试 javadoc 1、
+     * 历史计划文档 2 —— 计划文档是留痕,不是当前实现的引用)。
      *
      * <p>【★★ 一个键名收不收,判据是**两段式** —— 改名单前先读这段】
      * <blockquote>

+ 53 - 5
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingServiceTest.java

@@ -355,6 +355,10 @@ class LsyOnboardingServiceTest {
      * <p>★ 填 {@code ""} 比留 null 更坏:{@code address_province} 这类字段会带着空串
      * 进**签名原文**(本仓规则:null 不参与、**空串参与**),报文与签名一起变,
      * 而出错只在上游。
+     *
+     * <p>★ 三种输入的 {@code provinceCode} 期望**各由本用例自己断言**(前两格必须 Java null、
+     * 第三格必须原值):这一格此前只由 {@code areaList_carriesProvinceCode_atLevel1} 兜着,
+     * 本条对它自己声明的第 ②③ 条毫无断言(F1)。
      */
     @Test
     void areaList_missingGbCode_leavesFieldNull() {
@@ -363,12 +367,18 @@ class LsyOnboardingServiceTest {
                 ok(",\"area_list\":[{\"area_name\":\"某区\",\"area_code\":\"1\",\"province_code\":null}]"),
                 ok(",\"area_list\":[{\"area_name\":\"某区\",\"area_code\":\"3\",\"province_code\":\"110000\"}]"),
         };
-        for (String json : jsons) {
+        // 与 jsons 一一对应的 provinceCode 期望:前两格(缺键 / JSON null)必须是 Java null;
+        // 第三格是三种输入里唯一真正给了省码的 ⇒ 必须是原值,不是 "null" 也不是 ""。
+        String[] expectedProvince = {null, null, "110000"};
+        for (int i = 0; i < jsons.length; i++) {
+            String json = jsons[i];
             stubUpstream(json);
 
             List<LsyAreaItemDTO> areas = service.areaList("1", null);
 
             assertEquals(1, areas.size(), json);
+            assertEquals(expectedProvince[i], areas.get(0).getProvinceCode(),
+                    "province_code 的期望按输入而异(缺键 / JSON null ⇒ Java null;给了值 ⇒ 原值):" + json);
             assertNull(areas.get(0).getCountyCode(), "缺键 / JSON null ⇒ 必须是 Java null:" + json);
             assertNull(areas.get(0).getCityCode(), "缺键 / JSON null ⇒ 必须是 Java null:" + json);
         }
@@ -2714,6 +2724,33 @@ class LsyOnboardingServiceTest {
                 "出站参数必须恰好是服务拥有的 4 个键(inst_no / sign 归客户端补):" + sent);
     }
 
+    /**
+     * ★ {@code channel_type} 的空白处理与 {@code channel_id} **同口径:trim**
+     * (先例:{@link #channelId_paddedConfig_trimmedIdenticallyOnAllFourLegs})。
+     *
+     * <p>为什么单独一条:{@code mccList} 是唯一用 {@code channel_type} 的腿,而
+     * {@link #mccList_sendsChannelTypeFromConfig} 用的配置值 {@code "3"} 本来就无空白
+     * ⇒ 把生产代码里的 {@code trimOrNull} 去掉,它**照样绿**(实测:该变异下它绿、
+     * 本用例红)。两个字段同源(都取自配置、都进**签名原文**),而配置来自**环境变量**
+     * (用户手工注入),带首尾空白是现实风险 —— 症状是上游回一句签名错误,本侧看不出是空白。
+     *
+     * <p>★ 本用例**不碰共享常量** {@code CFG_CHANNEL_TYPE}:它在
+     * {@code withOnboardingCredentials()} 里是全体用例的公共输入,改它会波及全文件。
+     * 这里照夹具配齐后,只在本用例内覆盖这一个字段。
+     */
+    @Test
+    void channelType_paddedConfig_trimmedOnMccList() {
+        withOnboardingCredentials();
+        props.setChannelType("  " + CFG_CHANNEL_TYPE + "  ");
+        stubUpstream(MCC_LIST_EXAMPLE);
+
+        service.mccList("2", "420100", null);
+
+        Map<String, Object> sent = sentParams("/api/merchant/getTopMccByType");
+        assertEquals(CFG_CHANNEL_TYPE, sent.get("channel_type"),
+                "配置带首尾空白时 channel_type 必须发 trim 后的值(与 channel_id 四腿同口径):" + sent);
+    }
+
     /**
      * ★ 「1 级不填 {@code parent_id}」的判据是 **level**,不是「{@code parentId} 是不是 null」
      * —— 与 {@link #areaList_level1_omitsParentCode} 逐字同一条规则(该页参数表写
@@ -2864,10 +2901,15 @@ class LsyOnboardingServiceTest {
      * —— 差 2,同 {@link #anxinSignPerson_sendsAllSevenParams}({@code inst_no} / {@code sign} 归客户端补)。
      *
      * <p>比个人版多的正是四个键:{@code business_licence} / {@code merchant_address} /
-     * {@code legal_cert_no} / {@code legal_name}。这里**逐个断言值**、且四个值两两不同,
-     * 是为了让「公司版复用了个人版的组装、漏掉了多的那四个」与
-     * 「四个里取错了一个 getter」这两种实现都过不了集合相等这一关(漏键会红、
-     * 取错 getter 会让值对不上)。
+     * {@code legal_cert_no} / {@code legal_name}。这里**9 个键逐个断言值**,是为了让
+     * 「公司版复用了个人版的组装、漏掉了多的那四个」与「某一个键取错了 getter」这两种实现
+     * 都过不了(漏键会红、取错 getter 会让值对不上)。
+     *
+     * <p>★ <b>9 个夹具值实测两两不同</b>(4 个 {@code ANXIN_*} 常量值互不相同,5 个与键同名的
+     * 字面量也互不相同)—— 这是值断言有分辨力的**前提**:只要有两个键同值,
+     * 把这两个 getter 互换就照样绿,那几行断言等于摆设。改夹具时先重量这一点。
+     * 未断言的 5 个里 {@code principal_cert_no} 与 {@code legal_cert_no} 尤其要紧:
+     * 它们是**同一类 PII 的两个不同人**(结算人 / 法人),取错不会抛异常。
      */
     @Test
     void anxinSignCompany_sendsAllElevenParams() {
@@ -2881,6 +2923,12 @@ class LsyOnboardingServiceTest {
                         "merchant_address", "legal_cert_no", "legal_name"), sent.keySet(),
                 "出站参数必须恰好是服务拥有的 9 个键(inst_no / sign 归客户端补;"
                         + "用例名里的 11 是上游参数表的行数):" + sent);
+        // 下面 9 行覆盖全部 9 个键:5 个与个人腿共用的(取错 getter 只会静默发错人)+ 4 个公司腿独有的。
+        assertEquals(ANXIN_CONTACT_PHONE, sent.get("contact_phone"));
+        assertEquals(ANXIN_PRINCIPAL_CERT_NO, sent.get("principal_cert_no"));
+        assertEquals("account_card_name", sent.get("account_card_name"));
+        assertEquals(ANXIN_ACCOUNT_CARD_NUM, sent.get("account_card_num"));
+        assertEquals("merchant_name", sent.get("merchant_name"));
         assertEquals("business_licence", sent.get("business_licence"));
         assertEquals("merchant_address", sent.get("merchant_address"));
         assertEquals(ANXIN_LEGAL_CERT_NO, sent.get("legal_cert_no"));