| | |
| | | List<Map<String, Object>> repeated = rows("select * from merchant_onboarding_application where merchant_id = ? and idempotency_key = ? limit 1", principal.merchantId(), key.trim()); |
| | | if (!repeated.isEmpty()) return onboarding(repeated.get(0)); |
| | | List<Map<String, Object>> active = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? and initial_bank_card_id is not null and status in ('pending','approved') order by id desc limit 1", principal.merchantId(), principal.storeId()); |
| | | if (!active.isEmpty() && onboardingProgressService.supports(active.get(0))) return onboarding(active.get(0)); |
| | | if (!active.isEmpty() && onboardingProgressService.supports(active.get(0))) { |
| | | supplyMissingStorePhotos(active.get(0), request); |
| | | return onboarding(active.get(0)); |
| | | } |
| | | if (!active.isEmpty() && !retryable(active.get(0))) return onboarding(active.get(0)); |
| | | boolean enterprise = "enterprise".equals(request.subjectType()); |
| | | if (enterprise && (request.creditCode() == null || !request.creditCode().trim().matches("[0-9A-Z]{18}"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的18位统一社会信用代码"); |
| | |
| | | normalizeRegisteredCapital(request.companyType(), request.registeredCapital()); |
| | | } else { |
| | | if (blank(request.registeredDistrictCode()) || blank(request.registeredAddress()) || blank(request.legalIdFrontUrl()) || blank(request.legalIdBackUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请补全个人商户进件资料"); |
| | | // 汇付对个人(小微)商户的微信业务开通与微信实名认证都要门店照片(门头照 F22 + 内景照 F24)。 |
| | | // 放在提交时校验:这两张图是拿到微信收款能力的硬条件,缺了也能把进件提上去, |
| | | // 但后面微信开通和实名一定被通道驳回,那时商户得把整份进件重走一遍。 |
| | | if (blank(request.storeHeaderUrl()) || blank(request.storeIndoorUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传店铺照片与店内环境照,微信支付开通与实名认证都需要"); |
| | | } |
| | | boolean privateSettlement = !enterprise || "个体工商户".equals(clean(request.companyType())); |
| | | if (privateSettlement && blank(request.settlementCardFrontUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传结算银行卡卡号面"); |
| | |
| | | String no = BusinessIdGenerator.next("MO"); |
| | | String certificateValidityType = certificateValidityType(request.subjectCertificateValidityType()); |
| | | String certificateBeginDate = certificateBeginDate(request.subjectCertificateBeginDate()); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, license_validity_type, license_begin_date, license_end_date, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, subject_cert_end_date, registered_region, registered_province_code, registered_city_code, registered_district_code, registered_address, merchant_name, merchant_short_name, company_type, business_license_url, established_date, registered_capital, legal_id_front_url, legal_id_back_url, legal_certificate_address, legal_mobile, administrator_email, contact_name, contact_mobile, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, certificateEndDate(request.subjectCertificateEndDate()), clean(request.registeredRegion()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), clean(request.merchantName()), clean(request.merchantShortName()), clean(request.companyType()), clean(request.businessLicenseUrl()), clean(request.establishedDate()), clean(request.registeredCapital()), clean(request.legalIdFrontUrl()), clean(request.legalIdBackUrl()), clean(request.legalCertificateAddress()), clean(request.legalMobile()), clean(request.administratorEmail()), request.contactName().trim(), request.contactMobile().trim(), key.trim()); |
| | | jdbc.update("insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name, credit_code, license_validity_type, license_begin_date, license_end_date, subject_id_no_masked, subject_id_no_encrypted, subject_cert_validity_type, subject_cert_begin_date, subject_cert_end_date, registered_region, registered_province_code, registered_city_code, registered_district_code, registered_address, merchant_name, merchant_short_name, company_type, business_license_url, established_date, registered_capital, legal_id_front_url, legal_id_back_url, legal_certificate_address, legal_mobile, administrator_email, contact_name, contact_mobile, store_header_url, store_indoor_url, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), no, request.subjectType(), request.subjectName().trim(), clean(request.creditCode()), certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()), maskId(request.subjectIdNumber()), sensitiveDataCrypto.encrypt(clean(request.subjectIdNumber()).toUpperCase()), certificateValidityType, certificateBeginDate, certificateEndDate(request.subjectCertificateEndDate()), clean(request.registeredRegion()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), clean(request.merchantName()), clean(request.merchantShortName()), clean(request.companyType()), clean(request.businessLicenseUrl()), clean(request.establishedDate()), clean(request.registeredCapital()), clean(request.legalIdFrontUrl()), clean(request.legalIdBackUrl()), clean(request.legalCertificateAddress()), clean(request.legalMobile()), clean(request.administratorEmail()), request.contactName().trim(), request.contactMobile().trim(), clean(request.storeHeaderUrl()), clean(request.storeIndoorUrl()), key.trim()); |
| | | String cardNumber = request.bankCardNumber().trim(); |
| | | String cardKey = key.trim() + ":initial"; |
| | | if (cardKey.length() > 128) cardKey = cardKey.substring(0, 128); |
| | |
| | | if ("individual".equals(request.subjectType())) submitIndividualOnboarding(principal, no, bankCardId, request, branch); |
| | | else submitEnterpriseOnboarding(principal, no, bankCardId, request, branch, privateSettlement); |
| | | return onboarding(rows("select * from merchant_onboarding_application where application_no = ?", no).get(0)); |
| | | } |
| | | |
| | | /** |
| | | * 存量个人进件缺门店照片时,允许商户在重新提交里补上。 |
| | | * |
| | | * <p>门店照片是个人(小微)主体开通微信支付与发起微信实名的硬材料,而分阶段模型下重复提交 |
| | | * **不会新建进件**(也就不会在通道上多出一家商户),商户端因此没有别的补图入口——真实开发库里 |
| | | * 已经有拿到汇付商户号的个人进件,就是这个形态。 |
| | | * |
| | | * <p>只补空着的那两项,已有值不覆盖;进件、银行卡与通道申请一律不动。 |
| | | */ |
| | | private void supplyMissingStorePhotos(Map<String, Object> application, MerchantOnboardingRequest request) { |
| | | if (request == null || !"individual".equalsIgnoreCase(clean(application.get("subjectType")))) return; |
| | | if (clean(application.get("storeHeaderUrl")).isBlank() && !blank(request.storeHeaderUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_header_url=? where id=?", request.storeHeaderUrl().trim(), application.get("id")); |
| | | } |
| | | if (clean(application.get("storeIndoorUrl")).isBlank() && !blank(request.storeIndoorUrl())) { |
| | | jdbc.update("update merchant_onboarding_application set store_indoor_url=? where id=?", request.storeIndoorUrl().trim(), application.get("id")); |
| | | } |
| | | } |
| | | |
| | | private void submitEnterpriseOnboarding(MerchantPrincipal principal, String applicationNo, Long bankCardId, MerchantOnboardingRequest request, Map<String, Object> branch, boolean privateSettlement) { |
| | |
| | | String legalFrontPictureId = pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F40"); |
| | | String legalBackPictureId = pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F41"); |
| | | String settlementCardPictureId = pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13"); |
| | | // 店铺照片:个人(小微)商户进件的必填项,三种经营类型都要传,文件类型固定 F22。 |
| | | String storeHeaderPictureId = pictureUploadGateway.uploadUrl(request.storeHeaderUrl(), "F22"); |
| | | HuifuIndividualOnboardingGateway.Result result = individualOnboardingGateway.create(new HuifuIndividualOnboardingCommand( |
| | | applicationNo, request.subjectName().trim(), request.subjectIdNumber().trim().toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()), certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactMobile().trim(), clean(request.registeredAddress()), request.administratorEmail().trim(), |
| | | mcc(principal.merchantId()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), legalFrontPictureId, legalBackPictureId, |
| | | request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId)); |
| | | mcc(principal.merchantId(), "individual", request.companyType()), clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), legalFrontPictureId, legalBackPictureId, |
| | | request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), settlementCardPictureId, storeHeaderPictureId)); |
| | | applyOnboardingResult(applicationNo, bankCardId, result.accepted(), result.huifuId(), result.applyNo(), result.tokenNo(), result.code(), result.message()); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu submission outcome is pending confirmation", applicationNo); |
| | |
| | | String huifuId = clean(application.get("providerHuifuId")); |
| | | if (huifuId.isBlank() || "rejected".equals(clean(application.get("status")))) return; |
| | | |
| | | String wechatStatus = clean(application.get("wechatConfigStatus")); |
| | | // 本地状态可能与通道脱节(通道侧已经配好、本地还是 not_started)。微信商户配置是**添加**接口、 |
| | | // 成功后不支持修改删除(每种参数最多 5 组),配置已在通道生效时再提一次会留下重复配置。 |
| | | if (needsProvisioningStep(wechatStatus) && wechatConfigEffective(huifuId)) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_message=? where id=?", "通道已有配置,按已配置收口", application.get("id")); |
| | | wechatStatus = "approved"; |
| | | } |
| | | if (wechatStatus.isBlank() || "not_started".equals(wechatStatus) || "failed".equals(wechatStatus)) { |
| | | String requestNo = clean(application.get("wechatConfigRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "WC"); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', wechat_config_status='processing', wechat_config_request_no=?, reject_reason=null, reviewed_at=null where id=?", requestNo, application.get("id")); |
| | | try { |
| | | HuifuMerchantWechatConfigGateway.Result result = merchantWechatConfigGateway.configure(requestNo, huifuId); |
| | | if (!result.accepted()) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='failed', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=? where id=?", result.code(), result.message(), "微信商户配置失败:" + providerReason(result.code(), result.message()), application.get("id")); |
| | | return; |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_code=?, wechat_config_response_message=? where id=?", result.code(), result.message(), application.get("id")); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | String status = exception.isOutcomeUnknown() ? "processing" : "failed"; |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status=?, reject_reason=? where id=?", status, exception.isOutcomeUnknown() ? "微信商户配置结果待确认" : "微信商户配置请求失败", application.get("id")); |
| | | return; |
| | | } catch (BusinessException exception) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='failed', reject_reason=? where id=?", "微信商户配置失败:" + exception.getMessage(), application.get("id")); |
| | | return; |
| | | } |
| | | } else if (!"approved".equals(wechatStatus)) { |
| | | return; |
| | | } |
| | | advanceBusinessOpen(applicationNo, application, huifuId); |
| | | // 微信小程序配置必须排在业务开通**之后**,而且两步互不阻断:微信子商户号是业务开通创建的, |
| | | // 先做配置会在通道侧报「根据渠道号或微信子商户号未查到入驻记录」(2026-09-16 新商户实测, |
| | | // 当时配置失败还把业务开通整个跳过了,于是微信支付、人才端绑定、实名全部连坐)。 |
| | | advanceWechatConfig(applicationNo, fresh(application), huifuId); |
| | | completePostOnboardingProvisioning(application.get("id")); |
| | | } |
| | | |
| | | application = rows("select * from merchant_onboarding_application where id=? limit 1", application.get("id")).get(0); |
| | | private Map<String, Object> fresh(Map<String, Object> application) { |
| | | return rows("select * from merchant_onboarding_application where id=? limit 1", application.get("id")).get(0); |
| | | } |
| | | |
| | | /** |
| | | * 商户业务开通。排在微信小程序配置之前:这一步受理时通道就为三个线上场景创建了微信子商户号, |
| | | * 微信小程序配置要挂在这个子商户号下面。 |
| | | */ |
| | | private void advanceBusinessOpen(String applicationNo, Map<String, Object> application, String huifuId) { |
| | | String businessStatus = clean(application.get("businessOpenStatus")); |
| | | // 同理:通道侧已经开通微信业务时不再重新提交,否则会为同一商户开出第二份业务开通申请。 |
| | | if (needsProvisioningStep(businessStatus) && wechatBusinessEffective(huifuId)) { |
| | | // 通道侧已经开通微信业务时不再重新提交,否则会为同一商户开出第二份业务开通申请。 |
| | | // 同时把本地状态收口成 approved——**包括 processing**:业务开通回的是 90000000(审核中), |
| | | // 没有回调时(本地开发收不到异步通知)没人把它推成 approved,而 approved 才是能力快照刷新 |
| | | // 与「整单完成」的入口,于是商户明明已经开通却一直停在审核中。 |
| | | if (!"approved".equals(businessStatus) && wechatBusinessEffective(huifuId)) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='approved', business_open_response_message=? where id=?", "通道已开通微信业务,按已开通收口", application.get("id")); |
| | | businessStatus = "approved"; |
| | | } |
| | | if (businessStatus.isBlank() || "not_started".equals(businessStatus) || "failed".equals(businessStatus)) { |
| | | String requestNo = clean(application.get("businessOpenRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "BO"); |
| | | // 上一次是**明确失败**时必须换新号重试(通道按流水号去重);其余情况沿用已存的号, |
| | | // 结果未知时换号会给同一商户开出第二份业务开通申请。 |
| | | String requestNo = "failed".equals(businessStatus) ? provisioningRequestNo(applicationNo, "BO", true) : clean(application.get("businessOpenRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "BO", false); |
| | | String shortName = clean(application.get("merchantShortName")); |
| | | if (shortName.isBlank()) shortName = clean(application.get("merchantName")); |
| | | if (shortName.isBlank()) shortName = clean(application.get("subjectName")); |
| | | Long merchantId = ((Number) application.get("merchantId")).longValue(); |
| | | String subjectType = clean(application.get("subjectType")); |
| | | boolean individual = "individual".equalsIgnoreCase(subjectType); |
| | | // mcc 对「个体工商户、小微商户」是必填(官方原文),企业才可以用 fee_rule_id 代替。 |
| | | // 个体工商户走的是企业进件接口,所以不是 individual,但同样要带 mcc。 |
| | | // 注意:个人主体的 company_type 存的是进件时选的经营行业,不会等于「个体工商户」。 |
| | | boolean mccRequired = individual || "个体工商户".equals(clean(application.get("companyType"))); |
| | | // 个人(小微)商户的门店照片必须随业务开通一起交上去。缺图就直接收口成失败、 |
| | | // 不发这次请求:发出去也只会被通道驳回,还会在通道侧留下一条缺材料的申请。 |
| | | // 商户在进件页补上照片重新提交即可(见 supplyMissingStorePhotos),下次刷新会带材料重发。 |
| | | String storeHeaderFileId = ""; |
| | | String storeIndoorFileId = ""; |
| | | if (individual) { |
| | | String storeHeaderUrl = clean(application.get("storeHeaderUrl")); |
| | | String storeIndoorUrl = clean(application.get("storeIndoorUrl")); |
| | | if (storeHeaderUrl.isBlank() || storeIndoorUrl.isBlank()) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "商户业务开通失败:个人商户开通微信支付需要店铺照片与店内环境照,请在进件页补上这两张图后重新提交", application.get("id")); |
| | | return; |
| | | } |
| | | try { |
| | | storeHeaderFileId = pictureUploadGateway.uploadUrl(storeHeaderUrl, "F22"); |
| | | storeIndoorFileId = pictureUploadGateway.uploadUrl(storeIndoorUrl, "F24"); |
| | | } catch (BusinessException | HuifuPaymentGatewayException exception) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "门店照片上传失败:" + exception.getMessage(), application.get("id")); |
| | | return; |
| | | } |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set status='pending', business_open_status='processing', business_open_request_no=?, reject_reason=null, reviewed_at=null where id=?", requestNo, application.get("id")); |
| | | try { |
| | | HuifuMerchantBusinessOpenGateway.Result result = merchantBusinessOpenGateway.open(requestNo, huifuId, shortName, mcc(merchantId), "individual".equalsIgnoreCase(clean(application.get("subjectType")))); |
| | | HuifuMerchantBusinessOpenGateway.Result result = merchantBusinessOpenGateway.open(requestNo, huifuId, shortName, mcc(merchantId, subjectType, clean(application.get("companyType"))), individual, mccRequired, storeHeaderFileId, storeIndoorFileId); |
| | | if (!result.accepted()) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', business_open_apply_no=nullif(?,''), business_open_response_code=?, business_open_response_message=?, reject_reason=? where id=?", clean(result.applyNo()), result.code(), result.message(), "商户业务开通失败:" + providerReason(result.code(), result.message()), application.get("id")); |
| | | return; |
| | |
| | | } catch (BusinessException exception) { |
| | | jdbc.update("update merchant_onboarding_application set business_open_status='failed', reject_reason=? where id=?", "商户业务开通失败:" + exception.getMessage(), application.get("id")); |
| | | } |
| | | } else if ("approved".equals(businessStatus)) { |
| | | completePostOnboardingProvisioning(application.get("id")); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 微信小程序支付配置(人才端 AppID 绑定)。排在业务开通之后,见 {@link #advanceBusinessOpen}。 |
| | | * |
| | | * <p>这里不再因为本步失败就跳过后续步骤——两步各自记状态、各自可重试。原先配置一失败就 |
| | | * {@code return},业务开通永远轮不到,而通道侧的失败原因只会指向配置这一步,看不出是顺序问题。 |
| | | */ |
| | | private void advanceWechatConfig(String applicationNo, Map<String, Object> application, String huifuId) { |
| | | String wechatStatus = clean(application.get("wechatConfigStatus")); |
| | | // 本地状态可能与通道脱节(通道侧已经配好、本地还是 not_started)。微信商户配置是**添加**接口、 |
| | | // 成功后不支持修改删除(每种参数最多 5 组),配置已在通道生效时再提一次会留下重复配置。 |
| | | if (needsProvisioningStep(wechatStatus) && wechatConfigEffective(huifuId)) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_message=? where id=?", "通道已有配置,按已配置收口", application.get("id")); |
| | | return; |
| | | } |
| | | if (!needsProvisioningStep(wechatStatus)) return; |
| | | // 业务开通被**受理**(90000000 审核中)不等于微信子商户号已经建好:微信入驻是异步的,受理那一刻 |
| | | // 通道侧查不到子商户号,此时发配置必然被驳回「根据渠道号或微信子商户号未查到入驻记录」, |
| | | // 还会把 wechat_config_status 记成 failed,让人以为是配置本身有问题。 |
| | | // 所以闸门用通道证据而不是本地状态:`qry_wx_conf_list` 里真的出现线上场景才算开通。 |
| | | // 代价是要多刷一次(入驻完成后再来一次就会带上配置),换来的是不发注定被拒的请求。 |
| | | if (!wechatBusinessEffective(huifuId)) return; |
| | | // 同理:上一次是明确失败时必须换新号,否则通道侧会把它当成重复请求。 |
| | | String requestNo = "failed".equals(wechatStatus) ? provisioningRequestNo(applicationNo, "WC", true) : clean(application.get("wechatConfigRequestNo")); |
| | | if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "WC", false); |
| | | jdbc.update("update merchant_onboarding_application set status='pending', wechat_config_status='processing', wechat_config_request_no=?, reject_reason=null, reviewed_at=null where id=?", requestNo, application.get("id")); |
| | | try { |
| | | HuifuMerchantWechatConfigGateway.Result result = merchantWechatConfigGateway.configure(requestNo, huifuId); |
| | | if (!result.accepted()) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='failed', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=? where id=?", result.code(), result.message(), "微信商户配置失败:" + providerReason(result.code(), result.message()), application.get("id")); |
| | | return; |
| | | } |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_code=?, wechat_config_response_message=? where id=?", result.code(), result.message(), application.get("id")); |
| | | } catch (HuifuPaymentGatewayException exception) { |
| | | String status = exception.isOutcomeUnknown() ? "processing" : "failed"; |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status=?, reject_reason=? where id=?", status, exception.isOutcomeUnknown() ? "微信商户配置结果待确认" : "微信商户配置请求失败", application.get("id")); |
| | | } catch (BusinessException exception) { |
| | | jdbc.update("update merchant_onboarding_application set wechat_config_status='failed', reject_reason=? where id=?", "微信商户配置失败:" + exception.getMessage(), application.get("id")); |
| | | } |
| | | } |
| | | |
| | |
| | | }; |
| | | } |
| | | |
| | | private String provisioningRequestNo(String applicationNo, String suffix) { String value = applicationNo + suffix; return value.length() <= 32 ? value : value.substring(0, 32); } |
| | | /** |
| | | * 通道请求流水号。 |
| | | * |
| | | * <p>{@code retry} 为真时在尾部追加随机后缀——**明确失败后的重试必须换新号**:通道按流水号 |
| | | * 去重,被驳回的申请沿用旧号会被当成重复请求。进件与实名走的是同一条规则 |
| | | * (见 {@code MerchantRealnameService.requestNo})。受理结果未知时不换号,避免为同一次提交 |
| | | * 开出第二份申请。 |
| | | * |
| | | * <p>随机后缀必须保住:{@code applicationNo} 长度不保证,先按剩余宽度裁前缀再接后缀, |
| | | * 否则超长截断会把随机位一起切掉,两次重试又变成同一个号。 |
| | | */ |
| | | private String provisioningRequestNo(String applicationNo, String suffix, boolean retry) { |
| | | if (!retry) { |
| | | String value = applicationNo + suffix; |
| | | return value.length() <= 32 ? value : value.substring(0, 32); |
| | | } |
| | | String random = String.format("%05d", new java.util.Random().nextInt(100_000)); |
| | | String head = applicationNo + suffix + "R"; |
| | | int room = 32 - random.length(); |
| | | return (head.length() <= room ? head : head.substring(0, room)) + random; |
| | | } |
| | | private void requireKey(String key) { if (key == null || key.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "缺少幂等键"); } |
| | | private void store(MerchantPrincipal principal) { if (rows("select id from merchant_store where id = ? and merchant_id = ? and status = 1 and deleted = 0", principal.storeId(), principal.merchantId()).isEmpty()) throw new BusinessException(ErrorCode.FORBIDDEN, "当前门店不可操作"); } |
| | | private void refreshRecoveryAfterCommit(Long id) { |
| | |
| | | default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "公司类型无效"); |
| | | }; |
| | | } |
| | | /** |
| | | * 进件主体所属行业的 MCC。 |
| | | * |
| | | * <p>个人(小微)主体在进件表单第一步单独选过「行业类型」(餐饮服务/酒店服务/景区服务…), |
| | | * 它跟着企业主体的公司类型一起落在 {@code company_type} 列上。原先个人进件的 MCC 只认商家档案上的 |
| | | * 经营类型,等于把商户自己在进件时选的行业**静默丢掉**:档案没配好就一律落 7399(其它), |
| | | * 通道侧的微信行业类目和费率跟着一起错。所以个人主体优先用进件时选的行业, |
| | | * 行业识别不出来(选了「其它」)再退回商家档案。 |
| | | * |
| | | * <p>{@code mcc} 是进件的必填项;业务开通只在没有 {@code fee_rule_id} 时才用它兜底。 |
| | | */ |
| | | private String mcc(Long merchantId, String subjectType, String industryType) { |
| | | if ("individual".equalsIgnoreCase(clean(subjectType))) { |
| | | String industry = mccForIndustry(industryType); |
| | | if (!industry.isBlank()) return industry; |
| | | } |
| | | return mcc(merchantId); |
| | | } |
| | | |
| | | /** |
| | | * 经营行业选中的 MCC。取值为静态形式,便于直接断言这条规则本身 |
| | | * (见 {@code MerchantOnboardingIndustryMccTests});返回空串表示这个行业识别不出来,交给商家档案兜底。 |
| | | */ |
| | | static String mccForIndustry(String industryType) { |
| | | String industry = MerchantTypeCatalog.canonicalCode(cleanStatus(industryType)); |
| | | return "other".equals(industry) ? "" : MCC_BY_MERCHANT_TYPE.getOrDefault(industry, "7399"); |
| | | } |
| | | |
| | | private String mcc(Long merchantId) { |
| | | List<Map<String, Object>> merchants = rows("select merchant_type_code, merchant_type from merchant where id=? and deleted=0 limit 1", merchantId); |
| | | if (merchants.isEmpty()) throw new BusinessException(ErrorCode.NOT_FOUND, "商家不存在"); |