yeg
2026-09-16 4ef94185af0c3f00ae1dc933bb099bde26894df8
feat(merchant): 未完成微信实名与授权前不允许收款

收款就绪原来只看进件审核通过、结算卡 token、通道账户状态、自动结算与取现能力,
完全没看微信侧——一个微信支付权限没生效、甚至微信业务没开通的商户也会被判成可收款。
用户已明确:未完成实名前不允许商户收款。

- MerchantPaymentReadinessService 补上「微信支付业务开通」与「微信实名与授权」两项,
缺哪项报哪项,不再只回一句「暂时无法收款」。证据取自分阶段进度(由通道查询落库),
不靠请求参数推断——「请求里带了参数」不等于通道侧生效。
- 没有进度记录时两项都算缺:查不到通道证据就不能认为能收款。这与「未完成实名不允许
收款」一致,也意味着存量商户必须先刷新过进度才可能通过判定。
- 测试:四个走下单路径的测试类(PaymentSettlementTests、AppOrderPortalServiceTests、
PaymentFundConfirmationTests、MerchantVerificationServiceTests)的 fixture 里补上进度
记录——它们测的是支付结算与核销,商户建模成已办完微信开通与实名。收款就绪自己的测试
新增两项:实名未确认时拒绝、完全没有证据时拒绝。

验证:全量 216 项,仅 docs/agents/testing.md 所列 3 项基线失败,无新增。
6个文件已修改
66 ■■■■■ 已修改文件
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessServiceTests.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationServiceTests.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentFundConfirmationTests.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/test/java/com/talentcard/server/business/portal/AppOrderPortalServiceTests.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessService.java
@@ -159,6 +159,13 @@
        if (!isMerchantNormal(application.merchantStatus())) {
            return Readiness.blocked("斗拱子商户状态异常(" + application.merchantStatus() + "),暂时无法收款");
        }
        // 微信支付权限的前置:业务开通生效 + 实名与授权完成。只开通不实名,微信侧并没有把收款
        // 权限给到商户——实名认证的适用对象就是「需要开通微信支付权限的商户」。证据取自分阶段
        // 进度(由通道查询落库),不靠请求参数推断:「请求里带了参数」不等于通道侧生效。
        List<String> missingWechat = missingWechatCapabilities(application.id());
        if (!missingWechat.isEmpty()) {
            return Readiness.blocked("微信收款能力未就绪:" + String.join("、", missingWechat) + ",暂时无法收款");
        }
        return Readiness.allowed(new CollectingMerchant(application.id(), application.storeId(), application.huifuId()));
    }
@@ -266,6 +273,22 @@
        return huifuPaymentProperties.isMerchantCapabilityRequired();
    }
    /**
     * 分阶段进度里代表微信收款权限的两个阶段,缺哪个报哪个。
     *
     * <p>没有进度记录时两个都算缺——查不到证据就不能认为能收款。后续配置(延迟入账、自动结算、
     * 银行卡)另有各自的阶段与快照,这里只管微信侧这两项。
     */
    private List<String> missingWechatCapabilities(long applicationId) {
        List<Map<String, Object>> saved = rows("select stages_json from merchant_onboarding_progress where application_id=?", applicationId);
        if (saved.isEmpty()) return List.of("微信支付业务开通", "微信实名与授权");
        Map<String, Object> stages = com.alibaba.fastjson.JSON.parseObject(clean(saved.get(0).get("stagesJson")), Map.class);
        List<String> missing = new java.util.ArrayList<>();
        if (!"confirmed".equals(clean(stages.get("wechat")))) missing.add("微信支付业务开通");
        if (!"confirmed".equals(clean(stages.get("realname")))) missing.add("微信实名与授权");
        return missing;
    }
    private boolean isAutoSettleOff(String value) { return "0".equals(value) || "N".equalsIgnoreCase(value); }
    private boolean isCashAvailable(String value) { return "1".equals(value) || "Y".equalsIgnoreCase(value); }
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantPaymentReadinessServiceTests.java
@@ -71,6 +71,39 @@
                """, merchantId, storeId, "hash-readiness-" + suffix, "readiness-card-" + suffix, shortReference("T", suffix));
        bankCardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", bankCardId, applicationId);
        confirmWechatCapabilities();
    }
    /** 微信支付业务开通与实名/授权都有通道证据——收款能力的前置。 */
    private void confirmWechatCapabilities() {
        jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId);
    }
    /**
     * 微信实名未完成时不允许收款:只开通微信业务、不做实名,微信侧并没有把收款权限给到商户。
     * 缺哪一项要说清楚,不能只回一句「暂时无法收款」。
     */
    @Test
    void rejectsANewPaidOrderWhenWechatRealnameIsNotConfirmed() {
        confirmSnapshot("NORMAL", "0", "1");
        jdbc.update("update merchant_onboarding_progress set stages_json='{\"wechat\":\"confirmed\",\"realname\":\"missing\"}' where application_id=?", applicationId);
        assertThatThrownBy(() -> readinessService.requireCollectingMerchant(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("微信实名与授权")
                .hasMessageContaining("微信收款能力未就绪");
    }
    /** 没有进度记录时两项都算缺——查不到通道证据就不能认为能收款。 */
    @Test
    void rejectsANewPaidOrderWhenThereIsNoCapabilityEvidenceAtAll() {
        confirmSnapshot("NORMAL", "0", "1");
        jdbc.update("delete from merchant_onboarding_progress where application_id=?", applicationId);
        assertThatThrownBy(() -> readinessService.requireCollectingMerchant(merchantId, storeId))
                .isInstanceOf(BusinessException.class)
                .hasMessageContaining("微信支付业务开通")
                .hasMessageContaining("微信实名与授权");
    }
    @Test
talent-card-server/src/test/java/com/talentcard/server/business/merchant/MerchantVerificationServiceTests.java
@@ -83,6 +83,8 @@
                values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '8888', '**** **** **** 8888', ?, 'approved', ?, ?)
                """, merchantId, storeId, "hash-verification-" + suffix, "verification-card-" + suffix, "T" + reference);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id = (select max(id) from merchant_bank_card) where id = ?", applicationId);
        // 收款就绪要求微信支付业务开通与实名/授权都有通道证据,这里的商户建模成已办完这两步。
        jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId);
        jdbc.update("""
                insert into talent_user(openid, name, mobile, talent_level, status, deleted)
                values (?, ?, ?, ?, 1, 0)
talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentFundConfirmationTests.java
@@ -94,6 +94,8 @@
                """, merchantId, storeId, "hash-delay-" + suffix, "delay-card-" + suffix, "T" + suffix);
        Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id = ? where id = ?", cardId, onboardingId);
        // 收款就绪要求微信支付业务开通与实名/授权都有通道证据,这里的商户建模成已办完这两步。
        jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", onboardingId);
        jdbc.update("""
                insert into merchant_product(merchant_id, product_no, product_type, product_name, original_price,
                                             sale_price, stock, audit_status, status, deleted)
talent-card-server/src/test/java/com/talentcard/server/business/payment/PaymentSettlementTests.java
@@ -102,6 +102,9 @@
                """, merchantId, storeId, "hash-payment-" + suffix, "payment-card-" + suffix, shortReference("T", suffix));
        Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", cardId, onboardingId);
        // 收款就绪要求微信支付业务开通与实名/授权都有通道证据。这里的商户建模成已经办完这两步,
        // 否则下单会被「微信收款能力未就绪」挡住——那些是开通流程的测试,不是支付结算的测试。
        jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", onboardingId);
        jdbc.update("""
                insert into merchant_product(product_no, merchant_id, product_type, product_name, original_price, sale_price,
                                             subsidy_amount, stock, audit_status, status, deleted)
talent-card-server/src/test/java/com/talentcard/server/business/portal/AppOrderPortalServiceTests.java
@@ -69,6 +69,9 @@
                """, merchant, storeId, "hash-app-order-" + suffix, "app-order-card-" + suffix, shortReference("T", suffix));
        Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
        jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", cardId, applicationId);
        // 收款就绪要求微信支付业务开通与实名/授权都有通道证据。这里的商户建模成已经办完这两步,
        // 否则下单会被「微信收款能力未就绪」挡住——那些是开通流程的测试,不是下单的测试。
        jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId);
        return applicationId;
    }