From a058f6109834e29435f5ce4aa19a43d67d07c060 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期五, 18 九月 2026 15:52:59 +0800
Subject: [PATCH] fix(onboarding): 结算银行卡卡号面从未落库,历史进件重提时补齐

---
 talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantExistingOnboardingTests.java |   89 ++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 88 insertions(+), 1 deletions(-)

diff --git a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantExistingOnboardingTests.java b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantExistingOnboardingTests.java
index e2561cf..20a67f8 100644
--- a/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantExistingOnboardingTests.java
+++ b/talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantExistingOnboardingTests.java
@@ -23,6 +23,9 @@
     // 否则测试会真的打到汇付。
     @MockitoBean com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway wechatConfigGateway;
     @MockitoBean com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway businessOpenGateway;
+    // 提交进件会真的上传图片、并向汇付递交申请,这两个也一并挡住——测试不该打网络。
+    @MockitoBean com.talentcard.server.business.payment.huifu.HuifuPictureUploadGateway pictureUploadGateway;
+    @MockitoBean com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway individualOnboardingGateway;
     @Autowired org.springframework.web.context.WebApplicationContext context;
     @Autowired com.talentcard.server.business.admin.auth.AdminAuthService adminAuth;
 
@@ -501,6 +504,45 @@
         assertThat(statusOf("store_indoor_url",applicationNo)).isEqualTo("https://cdn.example.com/store-indoor.jpg");
     }
 
+    /**
+     * 存量进件缺「结算银行卡卡号面」时,重新提交要能补上。
+     *
+     * <p>这一列过去从来没落过库(2026-09-18 工单),库里也没有别的地方能还原它;而分阶段模型下
+     * 重复提交不会新建进件,重提就是已提交进件唯一的补救入口,所以这条必须补得住。
+     */
+    @Test
+    void resubmittingSuppliesTheMissingSettlementCardFront() {
+        Map<String,Object> fixture = individualProvisioningFixture();
+        MerchantPrincipal principal = (MerchantPrincipal) fixture.get("principal");
+        String applicationNo = String.valueOf(fixture.get("applicationNo"));
+        assertThat(statusOf("settlement_card_front_url",applicationNo)).isNull();
+
+        service.submitOnboarding(principal, request(
+                "https://cdn.example.com/store-header.jpg",
+                "https://cdn.example.com/store-indoor.jpg",
+                "https://cdn.example.com/card-front.jpg"), "T02-card-"+System.nanoTime());
+
+        assertThat(statusOf("settlement_card_front_url",applicationNo)).isEqualTo("https://cdn.example.com/card-front.jpg");
+        // 没有重新进件,原汇付商户号与银行卡关系都保持。
+        assertThat(jdbc.queryForObject("select count(*) from merchant_onboarding_application where application_no=?",Integer.class,applicationNo)).isEqualTo(1);
+    }
+
+    /** 已经有值的卡号面不会被重新提交覆盖——存档的那张才是当时提交的。 */
+    @Test
+    void resubmittingDoesNotOverwriteAnExistingSettlementCardFront() {
+        Map<String,Object> fixture = individualProvisioningFixture();
+        MerchantPrincipal principal = (MerchantPrincipal) fixture.get("principal");
+        String applicationNo = String.valueOf(fixture.get("applicationNo"));
+        jdbc.update("update merchant_onboarding_application set settlement_card_front_url='https://cdn.example.com/original-card.jpg' where application_no=?", applicationNo);
+
+        service.submitOnboarding(principal, request(
+                "https://cdn.example.com/store-header.jpg",
+                "https://cdn.example.com/store-indoor.jpg",
+                "https://cdn.example.com/other-card.jpg"), "T02-card-"+System.nanoTime());
+
+        assertThat(statusOf("settlement_card_front_url",applicationNo)).isEqualTo("https://cdn.example.com/original-card.jpg");
+    }
+
     private Map<String,Object> individualProvisioningFixture() {
         String key="T02I-"+System.nanoTime();
         jdbc.update("insert into merchant(merchant_name,status,deleted) values (?,1,0)",key);
@@ -515,9 +557,54 @@
 
     /** 补齐门店照片这一步只读这两项,其余字段留空即可。 */
     private static MerchantOnboardingRequest request(String storeHeaderUrl,String storeIndoorUrl) {
+        return request(storeHeaderUrl, storeIndoorUrl, null);
+    }
+
+    private static MerchantOnboardingRequest request(String storeHeaderUrl,String storeIndoorUrl,String settlementCardFrontUrl) {
         return new MerchantOnboardingRequest("individual","Owner",null,null,null,null,null,null,null,null,
                 null,null,"510100",null,null,null,null,null,null,null,null,null,null,null,null,null,
-                "Owner","13700000000",null,null,"510100",null,null,null,storeHeaderUrl,storeIndoorUrl);
+                "Owner","13700000000",null,null,"510100",null,settlementCardFrontUrl,null,storeHeaderUrl,storeIndoorUrl);
+    }
+
+    /**
+     * 新提交的个人进件要把「结算银行卡卡号面」真的写进库。
+     *
+     * <p>在这之前那条 INSERT 的列清单里没有 {@code settlement_card_front_url}:商户端传了值、后端也
+     * 拿它上传了汇付图片(F13),但写库时被静默丢掉,于是后台详情里这一格永远是 {@code -}。
+     * 断言直接盯数据库那一列,不看接口返回值——返回值在修复前也是「成功」的。
+     */
+    @Test
+    void aNewIndividualSubmissionPersistsTheSettlementCardFront() {
+        String key = "T02C-" + System.nanoTime();
+        jdbc.update("insert into merchant(merchant_name,merchant_type,status,deleted) values (?, '餐饮服务', 1, 0)", key);
+        Long merchant = jdbc.queryForObject("select id from merchant where merchant_name=?", Long.class, key);
+        jdbc.update("insert into merchant_store(merchant_id,store_name,status,deleted) values (?,?,1,0)", merchant, key);
+        Long store = jdbc.queryForObject("select id from merchant_store where merchant_id=?", Long.class, merchant);
+        String clearingCode = String.format("%012d", Math.abs(System.nanoTime() % 1_000_000_000_000L));
+        jdbc.update("insert into bank_branch_directory(clearing_code,branch_name,bank_name,head_bank_code) values (?,?,?,?)",
+                clearingCode, "测试支行", "测试银行", "HB" + clearingCode);
+        when(pictureUploadGateway.uploadUrl(anyString(),anyString())).thenReturn("pic-id");
+        // 通道递交刻意回「驳回」:写库发生在递交**之前**,而驳回会让流程就此收口,
+        // 不再触发后面的开通链路——这条测试只关心那一列有没有落库,其余都是噪音。
+        when(individualOnboardingGateway.create(any())).thenReturn(new com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway.Result(false,"","","","00000001","参数非法"));
+
+        service.submitOnboarding(new MerchantPrincipal(1L,merchant,store,"owner"), individualRequest(clearingCode), "T02-card-"+System.nanoTime());
+
+        assertThat(jdbc.queryForObject("select settlement_card_front_url from merchant_onboarding_application where merchant_id=?",String.class,merchant))
+                .as("上传了结算卡卡号面就必须落库,否则后台详情只能显示 -")
+                .isEqualTo("https://cdn.example.com/card-front.jpg");
+    }
+
+    /** 个人进件的一次完整提交(除门店照片与结算卡卡号面外的必填项也都给上)。 */
+    private static MerchantOnboardingRequest individualRequest(String clearingCode) {
+        return new MerchantOnboardingRequest(
+                "individual","测试负责人",null,"511623200211245724","fixed","2022-02-14","2032-02-14",
+                null,null,null,"四川省 成都市 武侯区","510000","510100","510107","四川省邻水县丰禾镇9组108号",
+                null,null,"其它",null,null,null,
+                "https://cdn.example.com/id-front.jpg","https://cdn.example.com/id-back.jpg","四川省邻水县丰禾镇9组108号","13700000000","owner@example.com",
+                "测试负责人","13700000000","6222000012345678","510000","510100",clearingCode,
+                "https://cdn.example.com/card-front.jpg",null,
+                "https://cdn.example.com/store-header.jpg","https://cdn.example.com/store-indoor.jpg");
     }
 
     /** 建一张还停在 not_started 的进件,用来观察刷新会不会去推进开通步骤。 */

--
Gitblit v1.7.1