From 7a5303f5245e618422f32267c758dadb704487bb Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 29 九月 2026 16:45:48 +0800
Subject: [PATCH] feat(huifu): 进件补采线下经营的三张门店材料(门头照/内景照/收银台)
---
talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java | 71 ++++++++++++++++++++++++++---------
1 files changed, 53 insertions(+), 18 deletions(-)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
index b167230..46cb603 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/payment/huifu/HuifuIndividualOnboardingGateway.java
@@ -25,6 +25,31 @@
public Result create(HuifuIndividualOnboardingCommand command) {
ensureConfigured();
+ Map<String, Object> request = buildRequest(command);
+ try {
+ synchronized (SDK_LOCK) {
+ initializeSdk();
+ Map<String, Object> response = BasePayClient.request(request, "v2.merchant.basicdata.indv", false);
+ log.info("Huifu individual merchant onboarding response: applicationNo={}, upperHuifuId={}, response={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), JSON.toJSONString(response));
+ return Result.from(response);
+ }
+ } catch (BusinessException exception) {
+ throw exception;
+ } catch (Exception exception) {
+ log.error("Huifu individual merchant onboarding request error: applicationNo={}, upperHuifuId={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), exception);
+ throw new HuifuPaymentGatewayException("Huifu individual merchant onboarding request failed", outcomeUnknown(exception), exception);
+ }
+ }
+
+ /**
+ * 组装个人进件请求体,与发送解耦,便于直接断言商户归属
+ * (见 {@code HuifuOnboardingMerchantLevelTests})。
+ */
+ static Map<String, Object> buildRequestForTest(HuifuPaymentProperties properties, HuifuIndividualOnboardingCommand command) {
+ return new HuifuIndividualOnboardingGateway(properties).buildRequest(command);
+ }
+
+ private Map<String, Object> buildRequest(HuifuIndividualOnboardingCommand command) {
Map<String, Object> card = new LinkedHashMap<>();
card.put("card_type", "1");
card.put("card_name", command.name());
@@ -36,7 +61,8 @@
card.put("cert_no", command.idCardNo());
card.put("cert_validity_type", command.certificateValidityType());
card.put("cert_begin_date", command.certificateBeginDate());
- if (!blank(command.certificateEndDate())) card.put("cert_end_date", command.certificateEndDate());
+ // 长期有效(cert_validity_type=1)时截止日期留空——官方写明「当 cert_validity_type=1 时为空」。
+ if (!"1".equals(command.certificateValidityType()) && !blank(command.certificateEndDate())) card.put("cert_end_date", command.certificateEndDate());
card.put("mp", command.mobileNo());
card.put("is_settle_default", "Y");
@@ -52,15 +78,18 @@
request.put("reg_name", command.name());
request.put("short_name", command.name());
request.put("mcc", command.mcc());
- request.put("scene_type", "ONLINE");
+ // 进件场景:线下 OFFLINE(线上 ONLINE)。线下属「有实体场所」口径,经营详细地址随之变成必填,
+ // 所以 detail_addr 取商户填的经营地址,不再用证件地址顶替(见 legal_addr 与 detail_addr 的分离)。
+ request.put("scene_type", properties.getMerchantOnboardingSceneType());
request.put("prov_id", command.provinceCode());
request.put("area_id", command.cityCode());
request.put("district_id", command.districtCode());
- request.put("detail_addr", command.address());
+ request.put("detail_addr", command.businessAddress());
request.put("legal_cert_no", command.idCardNo());
request.put("legal_cert_validity_type", command.certificateValidityType());
request.put("legal_cert_begin_date", command.certificateBeginDate());
- putIfPresent(request, "legal_cert_end_date", command.certificateEndDate());
+ // 证件「长期有效」(validity_type=1)时不填截止日期——官方口径已确认:长期有效无需填写。
+ if (!"1".equals(command.certificateValidityType())) putIfPresent(request, "legal_cert_end_date", command.certificateEndDate());
request.put("legal_addr", command.address());
request.put("legal_cert_front_pic", command.legalFrontPictureId());
request.put("legal_cert_back_pic", command.legalBackPictureId());
@@ -70,21 +99,21 @@
request.put("login_name", loginName(command.applicationNo()));
request.put("card_info", JSON.toJSONString(card));
request.put("settle_card_front_pic", command.settlementCardFrontPictureId());
+ // 店铺照片(F22):个人(小微)商户进件的必填项,三种经营类型都要传,只是图片内容要求不同——
+ // 线上商品/服务交易传店铺首页截图、流动经营/便民服务传经营现场照、门店场所传实体门头照。
+ // 这里只负责把商户交的图带上,内容按经营类型由商户端提示。
+ request.put("store_header_pic", command.storeHeaderPictureId());
+ // 线下经营的门店材料是一组三张:门头照(F22)、内景照(F24)、收银台(F105)。通道的
+ // 「商户材料校验服务」在进件这一步按 F-code 逐张核对,缺一张就整笔驳回——报错原文
+ // 「缺失图片:F22-店铺门头照图片;F24-店铺内景图片;F105-店铺收银台」(2026-09-29 实测)。
+ // 字段名 store_indoor_pic / store_cashier_desk_pic 取自通道自己的报错原文。
+ putIfPresent(request, "store_indoor_pic", command.storeIndoorPictureId());
+ putIfPresent(request, "store_cashier_desk_pic", command.storeCashierPictureId());
request.put("cash_config", JSON.toJSONString(List.of(cash)));
+ // 与商户主体一致:进件即打开自动结算,核销确认后的钱由斗拱按结算周期自动划到结算卡。
+ request.put("settle_config", JSON.toJSONString(HuifuEnterpriseOnboardingGateway.settleConfig()));
putIfPresent(request, "async_return_url", properties.getMerchantOnboardingNotifyUrl());
- try {
- synchronized (SDK_LOCK) {
- initializeSdk();
- Map<String, Object> response = BasePayClient.request(request, "v2.merchant.basicdata.indv", false);
- log.info("Huifu individual merchant onboarding response: applicationNo={}, upperHuifuId={}, response={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), JSON.toJSONString(response));
- return Result.from(response);
- }
- } catch (BusinessException exception) {
- throw exception;
- } catch (Exception exception) {
- log.error("Huifu individual merchant onboarding request error: applicationNo={}, upperHuifuId={}", command.applicationNo(), properties.getOnboardingUpperHuifuId(), exception);
- throw new HuifuPaymentGatewayException("Huifu individual merchant onboarding request failed", outcomeUnknown(exception), exception);
- }
+ return request;
}
private void ensureConfigured() {
@@ -116,7 +145,13 @@
String code = value(data, "sub_resp_code", "resp_code", "code");
return new Result(success(code), value(data, "huifu_id", "huifuId"), value(data, "apply_no", "applyNo"), value(data, "token_no", "tokenNo"), code, value(data, "sub_resp_desc", "resp_desc", "message"));
}
- private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
+ /**
+ * Merchant onboarding return codes: {@code 00000000} means processed, {@code 90000000} means
+ * the application was accepted and is under provider review. Treating the review status as a
+ * failure would reject an application the provider actually accepted, so both are accepted
+ * here; an accepted application without a merchant number or card token stays pending.
+ */
+ private static boolean success(String code) { return "00000000".equals(code) || "00000100".equals(code) || "90000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
private static String value(Map<String, Object> data, String... keys) { for (String key : keys) if (data.get(key) != null) return String.valueOf(data.get(key)).trim(); return ""; }
}
}
--
Gitblit v1.7.1