From 65b57875700bf42ebd17eafa01a265e5690ce271 Mon Sep 17 00:00:00 2001
From: shinnosuke <tantao@1hoper.com>
Date: 星期四, 10 九月 2026 09:17:56 +0800
Subject: [PATCH] 又又更新
---
talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java | 324 ++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 300 insertions(+), 24 deletions(-)
diff --git a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
index 2e37e77..e1aba29 100644
--- a/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
+++ b/talent-card-server/src/main/java/com/talentcard/server/business/merchant/MerchantSettlementService.java
@@ -2,6 +2,8 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
@@ -16,8 +18,11 @@
import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingGateway;
import com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingCommand;
import com.talentcard.server.business.payment.huifu.HuifuEnterpriseOnboardingGateway;
+import com.talentcard.server.business.payment.huifu.HuifuMerchantBusinessOpenGateway;
+import com.talentcard.server.business.payment.huifu.HuifuMerchantWechatConfigGateway;
import com.talentcard.server.business.payment.huifu.HuifuPaymentGatewayException;
import com.talentcard.server.business.payment.huifu.HuifuPaymentProperties;
+import com.talentcard.server.business.payment.huifu.HuifuPictureUploadGateway;
import com.talentcard.server.business.payment.huifu.HuifuUserBusinessOpenCommand;
import com.talentcard.server.business.payment.huifu.HuifuUserBusinessOpenGateway;
import com.talentcard.server.common.SensitiveDataCrypto;
@@ -29,28 +34,41 @@
@Service
public class MerchantSettlementService {
+ private static final Map<String, String> MCC_BY_MERCHANT_TYPE = Map.ofEntries(
+ Map.entry("food", "5812"), Map.entry("scenic", "7991"), Map.entry("hotel", "7011"), Map.entry("apartment", "7011"),
+ Map.entry("cinema", "7832"), Map.entry("fuel", "5541"), Map.entry("parking", "7523"), Map.entry("carWash", "7542"),
+ Map.entry("medical", "8099"), Map.entry("employment", "7361"), Map.entry("finance", "6012"), Map.entry("approval", "9399"),
+ Map.entry("children", "8351"), Map.entry("housing", "6513"), Map.entry("car", "5511"), Map.entry("appliance", "5722"),
+ Map.entry("digital", "5732"), Map.entry("culture", "5942"), Map.entry("telecom", "4814"), Map.entry("dental", "8021"),
+ Map.entry("shopping", "5311"), Map.entry("other", "7399"));
private final JdbcTemplate jdbc;
private final SensitiveDataCrypto sensitiveDataCrypto;
private final HuifuIndividualOnboardingGateway individualOnboardingGateway;
private final HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway;
+ private final HuifuMerchantWechatConfigGateway merchantWechatConfigGateway;
+ private final HuifuMerchantBusinessOpenGateway merchantBusinessOpenGateway;
private final HuifuMerchantWithdrawalService withdrawalService;
private final HuifuUserBusinessOpenGateway userBusinessOpenGateway;
private final HuifuPaymentProperties huifuPaymentProperties;
+ private final HuifuPictureUploadGateway pictureUploadGateway;
- public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway, HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway, HuifuMerchantWithdrawalService withdrawalService, HuifuUserBusinessOpenGateway userBusinessOpenGateway, HuifuPaymentProperties huifuPaymentProperties) {
+ public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway, HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway, HuifuMerchantWechatConfigGateway merchantWechatConfigGateway, HuifuMerchantBusinessOpenGateway merchantBusinessOpenGateway, HuifuMerchantWithdrawalService withdrawalService, HuifuUserBusinessOpenGateway userBusinessOpenGateway, HuifuPaymentProperties huifuPaymentProperties, HuifuPictureUploadGateway pictureUploadGateway) {
this.jdbc = jdbc;
this.sensitiveDataCrypto = sensitiveDataCrypto;
this.individualOnboardingGateway = individualOnboardingGateway;
this.enterpriseOnboardingGateway = enterpriseOnboardingGateway;
+ this.merchantWechatConfigGateway = merchantWechatConfigGateway;
+ this.merchantBusinessOpenGateway = merchantBusinessOpenGateway;
this.withdrawalService = withdrawalService;
this.userBusinessOpenGateway = userBusinessOpenGateway;
this.huifuPaymentProperties = huifuPaymentProperties;
+ this.pictureUploadGateway = pictureUploadGateway;
}
@Transactional(readOnly = true)
public Map<String, Object> onboardingStatus(MerchantPrincipal principal) {
store(principal);
- List<Map<String, Object>> records = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? order by id desc limit 1", principal.merchantId(), principal.storeId());
+ List<Map<String, Object>> records = rows("select * from merchant_onboarding_application where merchant_id = ? and store_id = ? and initial_bank_card_id is not null order by id desc limit 1", principal.merchantId(), principal.storeId());
return records.isEmpty() ? Map.of("status", "unsubmitted", "reason", "") : onboarding(records.get(0));
}
@@ -59,51 +77,111 @@
requireKey(key); store(principal);
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 status in ('pending','approved') order by id desc limit 1", principal.merchantId(), principal.storeId());
+ 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()) 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位统一社会信用代码");
- if (enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入法人正确的身份证号");
- if (enterprise && (request.subjectCertificateBeginDate() == null || request.subjectCertificateBeginDate().isBlank())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入营业执照成立日期");
- if (!enterprise && (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的身份证号");
+ if (enterprise && (request.creditCode() == null || !request.creditCode().trim().matches("[0-9A-Z]{18}"))) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的18位统一社会信用代码");
+ if (request.subjectIdNumber() == null || !request.subjectIdNumber().trim().matches("(?:\\d{15}|\\d{17}[0-9X])")) throw new BusinessException(ErrorCode.BAD_REQUEST, enterprise ? "请输入正确的法人身份证号" : "请输入正确的负责人身份证号");
requireCertificatePeriod(request.subjectCertificateValidityType(), request.subjectCertificateBeginDate(), request.subjectCertificateEndDate(), "Identity document");
+ if (blank(request.legalCertificateAddress())) throw new BusinessException(ErrorCode.BAD_REQUEST, enterprise ? "请输入法人证件地址" : "请输入负责人证件地址");
+ if (blank(request.administratorEmail()) || !request.administratorEmail().trim().matches("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入正确的联系人邮箱");
if (enterprise) {
requireCertificatePeriod(request.licenseValidityType(), request.licenseBeginDate(), request.licenseEndDate(), "Business licence");
if (blank(request.registeredProvinceCode()) || blank(request.registeredCityCode()) || blank(request.registeredDistrictCode())) throw new BusinessException(ErrorCode.BAD_REQUEST, "Enterprise registration address codes are required");
+ if (blank(request.merchantName()) || blank(request.businessLicenseUrl()) || blank(request.legalIdFrontUrl()) || blank(request.legalIdBackUrl()) || blank(request.establishedDate())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请补全企业进件资料");
+ 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, "请补全个人商户进件资料");
}
+ boolean privateSettlement = !enterprise || "个体工商户".equals(clean(request.companyType()));
+ if (privateSettlement && blank(request.settlementCardFrontUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传结算银行卡卡号面");
+ if (!privateSettlement && blank(request.registrationAccountUrl())) throw new BusinessException(ErrorCode.BAD_REQUEST, "请上传开户许可证");
+ List<Map<String, Object>> branchRows = rows("select clearing_code, branch_name, bank_name, head_bank_code from bank_branch_directory where clearing_code = ? limit 1", request.bankBranchCode().trim());
+ if (branchRows.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择真实开户行");
+ Map<String, Object> branch = branchRows.get(0);
+ if (clean(branch.get("headBankCode")).isBlank()) 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, 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()), request.contactName().trim(), request.contactMobile().trim(), key.trim());
- if ("individual".equals(request.subjectType())) submitIndividualOnboarding(no, request);
- else submitEnterpriseOnboarding(no, request);
+ 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, 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.administratorEmail()), request.contactName().trim(), request.contactMobile().trim(), key.trim());
+ String cardNumber = request.bankCardNumber().trim();
+ String cardKey = key.trim() + ":initial";
+ if (cardKey.length() > 128) cardKey = cardKey.substring(0, 128);
+ String cardHash = sha256(cardNumber);
+ List<Map<String, Object>> existingCards = rows("select * from merchant_bank_card where merchant_id = ? and card_hash = ? limit 1 for update", principal.merchantId(), cardHash);
+ Long bankCardId;
+ if (!existingCards.isEmpty()) {
+ Map<String, Object> existing = existingCards.get(0);
+ String existingStatus = clean(existing.get("status"));
+ Long existingCardId = ((Number) existing.get("id")).longValue();
+ List<Map<String, Object>> linkedActiveOnboardings = rows("select id from merchant_onboarding_application where initial_bank_card_id = ? and status in ('pending','approved') limit 1", existingCardId);
+ if ("pending".equals(existingStatus) || !linkedActiveOnboardings.isEmpty()) {
+ throw new BusinessException(ErrorCode.CONFLICT, "该银行卡已有审核中的进件或已通过的申请,请勿重复提交");
+ }
+ bankCardId = existingCardId;
+ jdbc.update("update merchant_bank_card set store_id=?, bank_name=?, branch_name=?, branch_code=?, city_code=?, card_tail=?, card_masked=?, card_no_encrypted=?, provider_request_no=?, idempotency_key=?, reject_reason=null, reviewed_at=null, provider_token_no=null, provider_apply_no=null, provider_response_code=null, provider_response_message=null, status='pending' where id=?", principal.storeId(), clean(branch.get("bankName")), clean(branch.get("branchName")), clean(branch.get("clearingCode")), request.bankCityCode().trim(), cardNumber.substring(cardNumber.length() - 4), "**** **** **** " + cardNumber.substring(cardNumber.length() - 4), sensitiveDataCrypto.encrypt(cardNumber), no, cardKey, bankCardId);
+ } else {
+ jdbc.update("insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, branch_code, city_code, card_tail, card_masked, card_hash, card_no_encrypted, provider_request_no, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), clean(branch.get("bankName")), clean(branch.get("branchName")), clean(branch.get("clearingCode")), request.bankCityCode().trim(), cardNumber.substring(cardNumber.length() - 4), "**** **** **** " + cardNumber.substring(cardNumber.length() - 4), cardHash, sensitiveDataCrypto.encrypt(cardNumber), no, cardKey);
+ bankCardId = ((Number) rows("select id from merchant_bank_card where merchant_id=? and idempotency_key=?", principal.merchantId(), cardKey).get(0).get("id")).longValue();
+ }
+ jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where application_no=?", bankCardId, no);
+ 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));
}
- private void submitEnterpriseOnboarding(String applicationNo, MerchantOnboardingRequest request) {
+ private void submitEnterpriseOnboarding(MerchantPrincipal principal, String applicationNo, Long bankCardId, MerchantOnboardingRequest request, Map<String, Object> branch, boolean privateSettlement) {
try {
+ String licensePictureId = pictureUploadGateway.uploadUrl(request.businessLicenseUrl(), "F07");
+ String legalFrontPictureId = pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F02");
+ String legalBackPictureId = pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F03");
+ String registrationAccountPictureId = privateSettlement ? "" : pictureUploadGateway.uploadUrl(request.registrationAccountUrl(), "F08");
+ String settlementCardPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13") : "";
+ String settlementCertFrontPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F55") : "";
+ String settlementCertBackPictureId = privateSettlement ? pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F56") : "";
+ String shortName = blank(request.merchantShortName()) ? request.merchantName().trim() : request.merchantShortName().trim();
HuifuEnterpriseOnboardingGateway.Result result = enterpriseOnboardingGateway.create(new HuifuEnterpriseOnboardingCommand(
- applicationNo, request.subjectName().trim(), request.creditCode().trim(),
+ applicationNo, clean(request.merchantName()), shortName, request.creditCode().trim(),
certificateValidityType(request.licenseValidityType()), certificateBeginDate(request.licenseBeginDate()), certificateEndDate(request.licenseEndDate()),
clean(request.registeredProvinceCode()), clean(request.registeredCityCode()), clean(request.registeredDistrictCode()), clean(request.registeredAddress()), request.subjectName().trim(),
clean(request.subjectIdNumber()).toUpperCase(), certificateValidityType(request.subjectCertificateValidityType()),
- certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), request.contactName().trim(), request.contactMobile().trim()));
- if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo);
- else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result.code(), result.message()), applicationNo);
- else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo);
+ certificateBeginDate(request.subjectCertificateBeginDate()), certificateEndDate(request.subjectCertificateEndDate()), clean(request.legalCertificateAddress()), request.contactName().trim(), request.contactMobile().trim(), request.administratorEmail().trim(),
+ entType(request.companyType()), mcc(principal.merchantId()), licensePictureId, legalFrontPictureId, legalBackPictureId,
+ privateSettlement ? "1" : "0", privateSettlement ? request.subjectName().trim() : request.merchantName().trim(), request.bankCardNumber().trim(), request.bankProvinceCode().trim(), request.bankCityCode().trim(), clean(branch.get("headBankCode")), clean(branch.get("clearingCode")), clean(branch.get("branchName")),
+ privateSettlement ? request.subjectIdNumber().trim().toUpperCase() : "", privateSettlement ? certificateValidityType(request.subjectCertificateValidityType()) : "", privateSettlement ? certificateBeginDate(request.subjectCertificateBeginDate()) : "", privateSettlement ? certificateEndDate(request.subjectCertificateEndDate()) : "", privateSettlement ? request.contactMobile().trim() : "",
+ registrationAccountPictureId, settlementCardPictureId, settlementCertFrontPictureId, settlementCertBackPictureId,
+ certificateBeginDate(request.establishedDate()), normalizeRegisteredCapital(request.companyType(), request.registeredCapital())));
+ 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);
}
}
- private void submitIndividualOnboarding(String applicationNo, MerchantOnboardingRequest request) {
+ private void submitIndividualOnboarding(MerchantPrincipal principal, String applicationNo, Long bankCardId, MerchantOnboardingRequest request, Map<String, Object> branch) {
try {
- 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())));
- if (result.accepted() && !result.huifuId().isBlank()) jdbc.update("update merchant_onboarding_application set status='approved', provider_huifu_id=?, reviewed_at=now(), reject_reason=null where application_no=? and status='pending'", result.huifuId(), applicationNo);
- else if (!result.accepted()) jdbc.update("update merchant_onboarding_application set status='rejected', reject_reason=?, reviewed_at=now() where application_no=? and status='pending'", providerReason(result), applicationNo);
- else jdbc.update("update merchant_onboarding_application set reject_reason=? where application_no=? and status='pending'", "Huifu accepted the request but did not return huifu_id", applicationNo);
+ String legalFrontPictureId = pictureUploadGateway.uploadUrl(request.legalIdFrontUrl(), "F40");
+ String legalBackPictureId = pictureUploadGateway.uploadUrl(request.legalIdBackUrl(), "F41");
+ String settlementCardPictureId = pictureUploadGateway.uploadUrl(request.settlementCardFrontUrl(), "F13");
+ 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));
+ 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);
+ }
+ }
+
+ private void applyOnboardingResult(String applicationNo, Long bankCardId, boolean accepted, String huifuId, String applyNo, String tokenNo, String code, String message) {
+ if (accepted) {
+ boolean completed = !blank(huifuId) && !blank(tokenNo);
+ jdbc.update("update merchant_onboarding_application set status='pending', provider_huifu_id=nullif(?,''), provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reviewed_at=null, reject_reason=null where application_no=?", clean(huifuId), clean(applyNo), clean(code), clean(message), applicationNo);
+ jdbc.update("update merchant_bank_card set status=?, provider_token_no=nullif(?,''), provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reviewed_at=?, reject_reason=null where id=?", completed ? "approved" : "pending", clean(tokenNo), clean(applyNo), clean(code), clean(message), completed ? LocalDateTime.now() : null, bankCardId);
+ if (completed) continuePostOnboardingProvisioning(applicationNo);
+ } else {
+ String reason = providerReason(code, message);
+ jdbc.update("update merchant_onboarding_application set status='rejected', provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where application_no=?", clean(applyNo), clean(code), clean(message), reason, applicationNo);
+ jdbc.update("update merchant_bank_card set status='rejected', provider_apply_no=nullif(?,''), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where id=?", clean(applyNo), clean(code), clean(message), reason, bankCardId);
}
}
@Transactional(readOnly = true)
@@ -205,7 +283,7 @@
private void requireApproved(MerchantPrincipal principal) { if (!"approved".equals(onboardingStatus(principal).get("status"))) throw new BusinessException(ErrorCode.CONFLICT, "商户进件尚未审核通过"); }
private void submitBankCardToHuifu(MerchantPrincipal principal, Map<String, Object> card, String cardNumber, Map<String, Object> branch) {
- List<Map<String, Object>> onboarding = rows("select * from merchant_onboarding_application where merchant_id=? and store_id=? and status='approved' and provider_huifu_id is not null order by id desc limit 1", principal.merchantId(), principal.storeId());
+ List<Map<String, Object>> onboarding = rows("select * from merchant_onboarding_application where merchant_id=? and store_id=? and initial_bank_card_id is not null and status='approved' and provider_huifu_id is not null order by id desc limit 1", principal.merchantId(), principal.storeId());
if (onboarding.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "Huifu merchant onboarding is not approved");
Map<String, Object> subject = onboarding.get(0);
String bankCode = clean(branch.get("headBankCode"));
@@ -249,6 +327,185 @@
else jdbc.update("update merchant_bank_card set provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=? where id=? and status='pending'", applyNo, code, message, cards.get(0).get("id"));
return true;
}
+ @Transactional
+ @SuppressWarnings("unchecked")
+ public boolean handleOnboardingCallback(Map<String, Object> data) {
+ Object nested = data.get("data");
+ if (nested instanceof String value && !value.isBlank()) {
+ try { data = com.alibaba.fastjson.JSON.parseObject(value, Map.class); } catch (Exception ignored) { }
+ } else if (nested instanceof Map<?, ?> value) data = (Map<String, Object>) value;
+ String requestNo = clean(data.get("req_seq_id"));
+ if (requestNo.isBlank()) return false;
+ List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where application_no=? limit 1 for update", requestNo);
+ if (applications.isEmpty()) return false;
+ Map<String, Object> application = applications.get(0);
+ String code = clean(data.get("sub_resp_code")); if (code.isBlank()) code = clean(data.get("resp_code"));
+ String message = clean(data.get("audit_desc")); if (message.isBlank()) message = clean(data.get("sub_resp_desc")); if (message.isBlank()) message = clean(data.get("resp_desc"));
+ String auditStatus = clean(data.get("audit_status"));
+ String huifuId = clean(data.get("huifu_id")); String applyNo = clean(data.get("apply_no")); String tokenNo = clean(data.get("token_no"));
+ Long bankCardId = application.get("initialBankCardId") instanceof Number number ? number.longValue() : null;
+ if ("Y".equalsIgnoreCase(auditStatus)) {
+ jdbc.update("update merchant_onboarding_application set status='pending', provider_huifu_id=coalesce(nullif(?,''),provider_huifu_id), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=null, reviewed_at=null where application_no=?", huifuId, applyNo, code, message, requestNo);
+ if (bankCardId != null) jdbc.update("update merchant_bank_card set status=case when coalesce(nullif(?,''),provider_token_no) is null then status else 'approved' end, provider_token_no=coalesce(nullif(?,''),provider_token_no), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=null where id=?", tokenNo, tokenNo, applyNo, code, message, bankCardId);
+ continuePostOnboardingProvisioning(requestNo);
+ } else if ("N".equalsIgnoreCase(auditStatus) || "F".equalsIgnoreCase(auditStatus) || !providerSuccess(code)) {
+ String reason = providerReason(code, message);
+ jdbc.update("update merchant_onboarding_application set status='rejected', provider_huifu_id=coalesce(nullif(?,''),provider_huifu_id), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where application_no=?", huifuId, applyNo, code, message, reason, requestNo);
+ if (bankCardId != null) jdbc.update("update merchant_bank_card set status='rejected', provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where id=?", applyNo, code, message, reason, bankCardId);
+ } else {
+ jdbc.update("update merchant_onboarding_application set provider_huifu_id=coalesce(nullif(?,''),provider_huifu_id), provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=? where application_no=?", huifuId, applyNo, code, message, requestNo);
+ }
+ return true;
+ }
+
+ @Transactional
+ public boolean handleWechatConfigCallback(Map<String, Object> callback) {
+ Map<String, Object> data = callbackData(callback);
+ String requestNo = clean(data.get("req_seq_id"));
+ if (requestNo.isBlank()) return false;
+ List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where wechat_config_request_no=? limit 1 for update", requestNo);
+ if (applications.isEmpty()) return false;
+ Map<String, Object> application = applications.get(0);
+ String code = responseCode(data);
+ String message = responseMessage(data);
+ String configStatus = wechatAppletConfigStatus(data.get("result_list"));
+ if (!providerSuccess(code) || "F".equalsIgnoreCase(configStatus) || "B".equalsIgnoreCase(configStatus)) {
+ String reason = providerReason(code, message);
+ jdbc.update("update merchant_onboarding_application set status='pending', wechat_config_status='failed', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=?, reviewed_at=null where id=?", code, message, "微信商户配置失败:" + reason, application.get("id"));
+ } else {
+ jdbc.update("update merchant_onboarding_application set wechat_config_status='approved', wechat_config_response_code=?, wechat_config_response_message=?, reject_reason=null where id=?", code, message, application.get("id"));
+ continuePostOnboardingProvisioning(clean(application.get("applicationNo")));
+ }
+ return true;
+ }
+
+ @Transactional
+ public boolean handleBusinessOpenCallback(Map<String, Object> callback) {
+ Map<String, Object> data = callbackData(callback);
+ String requestNo = clean(data.get("req_seq_id"));
+ if (requestNo.isBlank()) return false;
+ List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where business_open_request_no=? limit 1 for update", requestNo);
+ if (applications.isEmpty()) return false;
+ Map<String, Object> application = applications.get(0);
+ String code = responseCode(data);
+ String message = responseMessage(data);
+ String auditStatus = clean(data.get("audit_status"));
+ String businessStatus = wechatBusinessStatus(data);
+ String applyNo = clean(data.get("apply_no"));
+ boolean rejected = List.of("N", "F", "C").contains(auditStatus.toUpperCase()) || "F".equalsIgnoreCase(businessStatus) || !providerSuccess(code);
+ if (rejected) {
+ String reason = providerReason(code, message);
+ jdbc.update("update merchant_onboarding_application set status='pending', business_open_status='failed', business_open_apply_no=coalesce(nullif(?,''),business_open_apply_no), business_open_response_code=?, business_open_response_message=?, reject_reason=?, reviewed_at=null where id=?", applyNo, code, message, "商户业务开通失败:" + reason, application.get("id"));
+ } else if ("S".equalsIgnoreCase(businessStatus)) {
+ jdbc.update("update merchant_onboarding_application set business_open_status='approved', business_open_apply_no=coalesce(nullif(?,''),business_open_apply_no), business_open_response_code=?, business_open_response_message=?, reject_reason=null where id=?", applyNo, code, message, application.get("id"));
+ completePostOnboardingProvisioning(application.get("id"));
+ } else {
+ jdbc.update("update merchant_onboarding_application set business_open_status='processing', business_open_apply_no=coalesce(nullif(?,''),business_open_apply_no), business_open_response_code=?, business_open_response_message=? where id=?", applyNo, code, message, application.get("id"));
+ }
+ return true;
+ }
+
+ private void continuePostOnboardingProvisioning(String applicationNo) {
+ List<Map<String, Object>> applications = rows("select * from merchant_onboarding_application where application_no=? limit 1 for update", applicationNo);
+ if (applications.isEmpty()) return;
+ Map<String, Object> application = applications.get(0);
+ String huifuId = clean(application.get("providerHuifuId"));
+ if (huifuId.isBlank() || "rejected".equals(clean(application.get("status")))) return;
+
+ String wechatStatus = clean(application.get("wechatConfigStatus"));
+ 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;
+ }
+
+ application = rows("select * from merchant_onboarding_application where id=? limit 1", application.get("id")).get(0);
+ String businessStatus = clean(application.get("businessOpenStatus"));
+ if (businessStatus.isBlank() || "not_started".equals(businessStatus) || "failed".equals(businessStatus)) {
+ String requestNo = clean(application.get("businessOpenRequestNo"));
+ if (requestNo.isBlank()) requestNo = provisioningRequestNo(applicationNo, "BO");
+ 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();
+ 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));
+ 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;
+ }
+ jdbc.update("update merchant_onboarding_application set business_open_status='processing', business_open_apply_no=nullif(?,''), business_open_response_code=?, business_open_response_message=? where id=?", clean(result.applyNo()), result.code(), result.message(), application.get("id"));
+ } catch (HuifuPaymentGatewayException exception) {
+ String status = exception.isOutcomeUnknown() ? "processing" : "failed";
+ jdbc.update("update merchant_onboarding_application set business_open_status=?, reject_reason=? where id=?", status, exception.isOutcomeUnknown() ? "商户业务开通结果待确认" : "商户业务开通请求失败", application.get("id"));
+ } 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"));
+ }
+ }
+
+ private void completePostOnboardingProvisioning(Object applicationId) {
+ jdbc.update("update merchant_onboarding_application set status='approved', reject_reason=null, reviewed_at=now() where id=? and wechat_config_status='approved' and business_open_status='approved'", applicationId);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> callbackData(Map<String, Object> callback) {
+ Object nested = callback.get("data");
+ if (nested instanceof Map<?, ?> value) return (Map<String, Object>) value;
+ if (nested instanceof String value && !value.isBlank()) {
+ try { return com.alibaba.fastjson.JSON.parseObject(value, Map.class); } catch (Exception ignored) { }
+ }
+ return callback;
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<Map<String, Object>> callbackList(Object raw) {
+ if (raw instanceof List<?> values) return values.stream().filter(Map.class::isInstance).map(value -> (Map<String, Object>) value).toList();
+ if (raw instanceof String value && !value.isBlank()) {
+ try { return (List<Map<String, Object>>) (List<?>) com.alibaba.fastjson.JSON.parseArray(value, Map.class); } catch (Exception ignored) { }
+ }
+ return List.of();
+ }
+
+ private String wechatAppletConfigStatus(Object raw) {
+ for (Map<String, Object> result : callbackList(raw)) if ("02".equals(clean(result.get("config_type")))) return clean(result.get("config_stat"));
+ return "";
+ }
+
+ private String wechatBusinessStatus(Map<String, Object> data) {
+ if (!"W".equalsIgnoreCase(clean(data.get("notify_type")))) return "";
+ boolean found = false;
+ for (Map<String, Object> result : callbackList(data.get("reg_result_list"))) {
+ String status = clean(result.get("business_stat"));
+ if ("F".equalsIgnoreCase(status)) return "F";
+ if ("S".equalsIgnoreCase(status)) found = true;
+ }
+ return found ? "S" : "";
+ }
+
+ private String responseCode(Map<String, Object> data) { String code = clean(data.get("sub_resp_code")); return code.isBlank() ? clean(data.get("resp_code")) : code; }
+ private String responseMessage(Map<String, Object> data) { String message = clean(data.get("audit_desc")); if (message.isBlank()) message = clean(data.get("sub_resp_desc")); return message.isBlank() ? clean(data.get("resp_desc")) : message; }
+ private String provisioningRequestNo(String applicationNo, String suffix) { String value = applicationNo + suffix; return value.length() <= 32 ? value : value.substring(0, 32); }
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 Map<String, Object> onboarding(Map<String, Object> row) { return Map.of("id", String.valueOf(row.get("id")), "applicationNo", row.get("applicationNo"), "status", row.get("status"), "subjectType", clean(row.get("subjectType")), "reason", clean(row.get("rejectReason")), "submittedAt", String.valueOf(row.get("createdAt"))); }
@@ -270,9 +527,28 @@
if (!blank(endDate) && clean(beginDate).compareTo(clean(endDate)) > 0) throw new BusinessException(ErrorCode.BAD_REQUEST, label + " end date must not precede its begin date");
}
private boolean blank(String value) { return value == null || value.isBlank(); }
- private String providerReason(HuifuIndividualOnboardingGateway.Result result) { return providerReason(result.code(), result.message()); }
private String providerReason(String code, String message) { String value = clean(message); return value.isEmpty() ? "Huifu onboarding rejected: " + clean(code) : value; }
- private boolean providerSuccess(String code) { return "00000000".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
+ private boolean providerSuccess(String code) { return "00000000".equals(code) || "00000100".equals(code) || "10000".equals(code) || "0".equals(code) || "SUCCESS".equalsIgnoreCase(code); }
private String maskId(String id) { String value = clean(id); return value.length() < 8 ? "" : value.substring(0, 3) + "********" + value.substring(value.length() - 4); }
private String sha256(String value) { try { byte[] digest = MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(); for (byte valueByte : digest) result.append(String.format("%02x", valueByte)); return result.toString(); } catch (Exception exception) { throw new IllegalStateException("Unable to hash bank card", exception); } }
+ private String entType(String companyType) {
+ return switch (clean(companyType)) {
+ case "政府机构" -> "1"; case "国营企业" -> "2"; case "私营企业" -> "3"; case "外资企业" -> "4";
+ case "个体工商户" -> "5"; case "其它组织" -> "6"; case "事业单位" -> "7"; case "业主委员会" -> "9";
+ default -> throw new BusinessException(ErrorCode.BAD_REQUEST, "公司类型无效");
+ };
+ }
+ 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, "商家不存在");
+ String code = clean(merchants.get(0).get("merchantTypeCode"));
+ if (code.isBlank()) code = MerchantTypeCatalog.canonicalCode(clean(merchants.get(0).get("merchantType")));
+ return MCC_BY_MERCHANT_TYPE.getOrDefault(code, "7399");
+ }
+ private String normalizeRegisteredCapital(String companyType, String value) {
+ String text = clean(value); boolean required = List.of("国营企业", "私营企业", "外资企业", "其它组织", "事业单位").contains(clean(companyType));
+ if (text.isBlank()) { if (required) throw new BusinessException(ErrorCode.BAD_REQUEST, "请输入注册资本"); return ""; }
+ try { BigDecimal capital = new BigDecimal(text); if (capital.signum() < 0) throw new NumberFormatException(); return capital.setScale(2, RoundingMode.HALF_UP).toPlainString(); }
+ catch (NumberFormatException exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "注册资本请输入数字,单位为万元"); }
+ }
}
--
Gitblit v1.7.1