Эх сурвалжийг харах

docs(进件): area_code 真实形态是 Integer - 订正依据 + 补钉数字形态

alphaH 3 долоо хоног өмнө
parent
commit
3d1ab60d51

+ 10 - 10
java/src/main/java/com/payment/platform/module/payment/leshouying/dto/LsyAreaItemDTO.java

@@ -12,17 +12,17 @@ public class LsyAreaItemDTO {
     private String areaName;
 
     /**
-     * 区划编码(上游的 {@code area_code})。
+     * 区划编码(上游的 {@code area_code})。**字段保持 {@code String}。**
      *
-     * <p>★ <b>必须保持 {@code String},不得改成数字型、也不得在映射时解析成数字。</b>
-     * page 196236568 的返回示例里 {@code area_code} 出现的是 {@code 100A}…{@code 100H}
-     * 这一类值,其中 {@code 100G} / {@code 100H} **不是合法十六进制**(连十六进制都不是,
-     * 更不是十进制)⇒ 任何 {@code Integer.parseInt} / {@code Long.parseLong} /
-     * {@code Long} 型字段都会在三级联动的第一屏抛 {@code NumberFormatException}。
-     *
-     * <p>它还会被原样回传给上游(下一级联动的 {@code parent_code}),所以任何「顺手转成数字」
-     * 的处理都同时改变出站报文。钉住这条的用例:
-     * {@code LsyOnboardingServiceTest#areaList_areaCode_isString_notNumber}。
+     * <p>★ 依据是**实测**而非推测:2026-09-22 真机普查 483 条,{@code area_code} 运行期类型
+     * **全是 {@code Integer}(483/483)、非数字形态 0 条** —— 活服务发的是**数字**;
+     * 而文档 page 196236568 的示例写的是 {@code 100A}…{@code 100H}(**文档示例 ≠ 活服务**)。
+     * 两种形态各有出处 ⇒ 谁都不是唯一形态:{@code String} 是唯一对两边都稳定的承接 ——
+     * 服务侧 {@code asText} 把数字走 {@code String.valueOf} 成文本;写成 `(String)` 强转
+     * 会在活服务上 ClassCastException。它还要以文本回传上游(下一级联动的 {@code parent_code},
+     * 真机实测填上级的 {@code area_code} 才走得通),而签名原文本来就过 {@code String.valueOf}。
+     * 钉住两种形态的用例:{@code areaList_areaCode_isString_notNumber}(文档形态)与
+     * {@code areaList_areaCode_integerShape_isStringified}(活服务形态)。
      */
     private String areaCode;
 }

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

@@ -90,9 +90,9 @@ public class LsyOnboardingService {
      * 取值转字符串:Java null 与 Hutool 的 {@link JSONNull} 都归成 Java null
      * (为什么必须处理后者,见 {@link #toAreaItems});其余按 {@code String.valueOf} 带回。
      *
-     * <p>只做「文本形态」这一件事,**不做数字解析** —— {@code area_code} 的真实形态不是纯数字
-     * (见 {@link LsyAreaItemDTO#getAreaCode()})。DTO 两个字段都是 {@code String},
-     * 上游若哪天把某个值发成裸数字/布尔,这里得到的是它的文本形态而不是类型转换异常。
+     * <p>【必须容忍数字】{@code area_code} 的**活服务形态就是数字**(2026-09-22 真机普查
+     * 483/483 全是 {@code Integer}、非数字 0 条;文档示例里的 {@code 100G} 只是文档形态),
+     * 所以这里给出的是文本形态而不是类型转换异常(依据见 {@link LsyAreaItemDTO#getAreaCode()})。
      */
     private static String asText(Object value) {
         if (value == null || value instanceof JSONNull) {

+ 30 - 3
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyOnboardingServiceTest.java

@@ -128,10 +128,13 @@ class LsyOnboardingServiceTest {
     }
 
     /**
-     * ★ 这条钉上游数据的**真实形状**:page 196236568 的返回示例里 {@code area_code}
+     * ★ 这条钉的是**文档示例**的形状:page 196236568 的返回示例里 {@code area_code}
      * 出现 {@code 100A}…{@code 100H},其中 {@code 100G} / {@code 100H} **不是合法十六进制**
-     * (连十六进制都不是,更不是十进制)。任何 {@code Integer.parseInt} / {@code Long.parseLong} /
-     * 数字型 DTO 字段都会在这里炸 —— 而炸的是三级联动的第一屏。
+     * (连十六进制都不是,更不是十进制)⇒ 若哪天真发字符串,数字型字段/解析必炸。
+     *
+     * <p>注意 **{@code 100G} 在真机 483 条里一次都没出现过**(2026-09-22 普查)——
+     * 文档形态与活服务形态各一条用例,缺一条就有一种形态零覆盖:见下一条
+     * {@link #areaList_areaCode_integerShape_isStringified}。
      */
     @Test
     void areaList_areaCode_isString_notNumber() {
@@ -146,6 +149,30 @@ class LsyOnboardingServiceTest {
         assertEquals("100G", code);
     }
 
+    /**
+     * ★ 这条钉的是**活服务的真实形态**:2026-09-22 真机普查 483 条,{@code area_code}
+     * 的运行期类型**全是 {@code Integer}(483/483)、非数字形态 0 条**。桩回数字 {@code 1}
+     * ⇒ 服务必须取回**字符串** {@code "1"}。
+     *
+     * <p>为什么非有这条不可:上一条({@code "100G"})覆盖的是**文档示例**的形态,而真机上
+     * 会发生的数字形态此前**零覆盖** —— 把 {@code asText} 改成 {@code (String) m.get("area_code")}
+     * 能让上一条与其余用例全绿,却在活服务上抛 {@code ClassCastException}(M-1 变异读数)。
+     * 夹具与真机取证同形:{@code level=2} + {@code parent_code="1"}(真机实测这一组才返回条目)。
+     */
+    @Test
+    void areaList_areaCode_integerShape_isStringified() {
+        stubUpstream(ok(",\"area_list\":[{\"area_name\":\"市辖区\",\"area_code\":1}]"));
+
+        List<LsyAreaItemDTO> areas = service.areaList("2", "1");
+
+        assertEquals(1, areas.size());
+        Object code = areas.get(0).getAreaCode();
+        assertInstanceOf(String.class, code,
+                "活服务发的是数字,服务必须把它取成字符串而不是强转");
+        assertEquals("1", code);
+        assertEquals("市辖区", areas.get(0).getAreaName());
+    }
+
     /** 契约明写的三种「防御性读取」输入之一:上游没返回 area_list 这个键。 */
     @Test
     void areaList_blankList_returnsEmpty() {