package com.talentcard.server.business.merchant;
|
|
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;
|
import java.util.List;
|
import java.util.Map;
|
|
import com.talentcard.server.business.BusinessIdGenerator;
|
import com.talentcard.server.business.DatabaseRows;
|
import com.talentcard.server.business.PageResult;
|
import com.talentcard.server.business.merchant.auth.MerchantPrincipal;
|
import com.talentcard.server.business.payment.huifu.HuifuIndividualOnboardingCommand;
|
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;
|
import com.talentcard.server.common.BusinessException;
|
import com.talentcard.server.common.ErrorCode;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
@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 HuifuUserBusinessOpenGateway userBusinessOpenGateway;
|
private final HuifuPaymentProperties huifuPaymentProperties;
|
private final HuifuPictureUploadGateway pictureUploadGateway;
|
private final MerchantPaymentReadinessService paymentReadinessService;
|
|
public MerchantSettlementService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto, HuifuIndividualOnboardingGateway individualOnboardingGateway, HuifuEnterpriseOnboardingGateway enterpriseOnboardingGateway, HuifuMerchantWechatConfigGateway merchantWechatConfigGateway, HuifuMerchantBusinessOpenGateway merchantBusinessOpenGateway, HuifuUserBusinessOpenGateway userBusinessOpenGateway, HuifuPaymentProperties huifuPaymentProperties, HuifuPictureUploadGateway pictureUploadGateway, MerchantPaymentReadinessService paymentReadinessService) {
|
this.jdbc = jdbc;
|
this.sensitiveDataCrypto = sensitiveDataCrypto;
|
this.individualOnboardingGateway = individualOnboardingGateway;
|
this.enterpriseOnboardingGateway = enterpriseOnboardingGateway;
|
this.merchantWechatConfigGateway = merchantWechatConfigGateway;
|
this.merchantBusinessOpenGateway = merchantBusinessOpenGateway;
|
this.userBusinessOpenGateway = userBusinessOpenGateway;
|
this.huifuPaymentProperties = huifuPaymentProperties;
|
this.pictureUploadGateway = pictureUploadGateway;
|
this.paymentReadinessService = paymentReadinessService;
|
}
|
|
@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 = ? 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));
|
}
|
|
@Transactional
|
public Map<String, Object> submitOnboarding(MerchantPrincipal principal, MerchantOnboardingRequest request, String key) {
|
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 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 (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, 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(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, 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()), 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(MerchantPrincipal principal, String applicationNo, Long bankCardId, MerchantOnboardingRequest request, Map<String, Object> branch) {
|
try {
|
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)
|
public List<Map<String, Object>> bankCards(MerchantPrincipal principal) {
|
store(principal);
|
return rows("select * from merchant_bank_card where merchant_id = ? and store_id = ? order by id desc", principal.merchantId(), principal.storeId()).stream().map(this::bankCard).toList();
|
}
|
|
@Transactional
|
public Map<String, Object> addBankCard(MerchantPrincipal principal, MerchantBankCardRequest request, String key) {
|
requireKey(key); store(principal); requireApproved(principal);
|
List<Map<String, Object>> repeated = rows("select * from merchant_bank_card where merchant_id = ? and idempotency_key = ? limit 1", principal.merchantId(), key.trim());
|
if (!repeated.isEmpty()) return bankCard(repeated.get(0));
|
String number = request.cardNumber().trim(); String hash = sha256(number);
|
List<Map<String, Object>> branchRecords = rows("select clearing_code, branch_name, bank_name, head_bank_code from bank_branch_directory where clearing_code = ? limit 1", request.branchCode().trim());
|
if (branchRecords.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择真实开户行");
|
Map<String, Object> branch = branchRecords.get(0);
|
List<Map<String, Object>> active = rows("select id from merchant_bank_card where merchant_id = ? and card_hash = ? and status in ('pending', 'approved') order by id desc limit 1", principal.merchantId(), hash);
|
if (!active.isEmpty()) throw new BusinessException(ErrorCode.CONFLICT, "该银行卡已有审核中或已通过的申请,请勿重复添加");
|
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, idempotency_key) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", principal.merchantId(), principal.storeId(), clean(branch.get("bankName")), clean(branch.get("branchName")), clean(branch.get("clearingCode")), request.cityCode().trim(), number.substring(number.length() - 4), "**** **** **** " + number.substring(number.length() - 4), hash, sensitiveDataCrypto.encrypt(number), key.trim());
|
Map<String, Object> card = rows("select * from merchant_bank_card where merchant_id = ? and idempotency_key = ? limit 1", principal.merchantId(), key.trim()).get(0);
|
submitBankCardToHuifu(principal, card, number, branch);
|
return bankCard(rows("select * from merchant_bank_card where id = ?", card.get("id")).get(0));
|
}
|
|
@Transactional(readOnly = true)
|
public PageResult<Map<String, Object>> bankBranches(String cityCode, String cityName, String keyword, int page, int size) {
|
if (jdbc != null) return bankBranchesFromDirectory(cityCode, cityName, keyword, page, size);
|
// The payment provider's bank directory should replace this curated fallback during Huifu onboarding.
|
List<Map<String, Object>> all = List.of(
|
branch("CCB510400", "中国建设银行攀枝花东区支行营业部", "中国建设银行"), branch("CCB510401", "中国建设银行攀枝花分行营业部", "中国建设银行"),
|
branch("ICBC510400", "中国工商银行攀枝花东区支行", "中国工商银行"), branch("ABC510400", "中国农业银行攀枝花炳草岗支行", "中国农业银行"));
|
String query = clean(keyword).toLowerCase(); int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 50);
|
List<Map<String, Object>> matched = all.stream().filter(value -> query.isEmpty() || (value.get("branchName") + "" + value.get("bankName")).toLowerCase().contains(query)).toList();
|
int start = Math.min((safePage - 1) * safeSize, matched.size()); int end = Math.min(start + safeSize, matched.size());
|
return new PageResult<>(matched.subList(start, end), safePage, safeSize, matched.size());
|
}
|
|
private PageResult<Map<String, Object>> bankBranchesFromDirectory(String cityCode, String cityName, String keyword, int page, int size) {
|
int safePage = Math.max(page, 1);
|
int safeSize = Math.min(Math.max(size, 1), 50);
|
StringBuilder where = new StringBuilder(" where 1=1");
|
List<Object> params = new ArrayList<>();
|
String cityFilter = blank(cityName) ? clean(cityCode) : clean(cityName);
|
int citySeparator = cityFilter.lastIndexOf(' ');
|
if (citySeparator >= 0 && citySeparator + 1 < cityFilter.length()) cityFilter = cityFilter.substring(citySeparator + 1).trim();
|
cityFilter = cityKeyword(cityFilter);
|
if (!blank(cityFilter)) {
|
where.append(" and (branch_name like ? or bank_name like ?)");
|
String match = "%" + cityFilter + "%";
|
params.add(match);
|
params.add(match);
|
}
|
if (!blank(keyword)) {
|
where.append(" and (branch_name like ? or bank_name like ? or clearing_code = ?)");
|
String match = "%" + clean(keyword) + "%";
|
params.add(match);
|
params.add(match);
|
params.add(clean(keyword));
|
}
|
Long total = jdbc.queryForObject("select count(*) from bank_branch_directory" + where, Long.class, params.toArray());
|
List<Map<String, Object>> values = rows("select clearing_code as id, clearing_code as branch_code, branch_name, head_bank_code, bank_name from bank_branch_directory" + where + " order by branch_name limit ? offset ?", appendPaging(params, safeSize, (safePage - 1) * safeSize));
|
return new PageResult<>(values, safePage, safeSize, total == null ? 0 : total);
|
}
|
|
@Transactional(readOnly = true)
|
public Map<String, Object> withdrawalSummary(MerchantPrincipal principal) {
|
store(principal);
|
Long verified = jdbc.queryForObject("select coalesce(sum(pay_amount), 0) from benefit_order where merchant_id = ? and (store_id is null or store_id = ?) and order_status = 1 and verify_status = 1 and refund_status = 0 and withdrawal_status = 0 and deleted = 0", Long.class, principal.merchantId(), principal.storeId());
|
Long held = jdbc.queryForObject("select coalesce(sum(amount_cents), 0) from merchant_withdrawal where merchant_id = ? and store_id = ? and status in ('pending','processing','paid')", Long.class, principal.merchantId(), principal.storeId());
|
long available = Math.max(0, (verified == null ? 0 : verified) - (held == null ? 0 : held));
|
return Map.of("availableAmountCents", available, "frozenAmountCents", held == null ? 0 : held);
|
}
|
|
/**
|
* 商户端提现记录:只读,且只展示「处理中 / 已到账 / 提现失败」三种状态。自动提现由系统在每日凌晨
|
* 生成并提交给斗拱,商户不能发起提现申请、选择提现金额,也不能修改提现状态(CONTEXT.md「自动提现」)。
|
*/
|
@Transactional(readOnly = true)
|
public PageResult<Map<String, Object>> withdrawals(MerchantPrincipal principal, int page, int size) {
|
store(principal); int safePage = Math.max(page, 1); int safeSize = Math.min(Math.max(size, 1), 50);
|
List<Map<String, Object>> values = rows("select w.*, c.bank_name, c.card_masked from merchant_withdrawal w join merchant_bank_card c on c.id = w.bank_card_id where w.merchant_id = ? and w.store_id = ? order by w.id desc limit ? offset ?", principal.merchantId(), principal.storeId(), safeSize, (safePage - 1) * safeSize).stream().map(this::withdrawal).toList();
|
Long total = jdbc.queryForObject("select count(*) from merchant_withdrawal where merchant_id = ? and store_id = ?", Long.class, principal.merchantId(), principal.storeId());
|
return new PageResult<>(values, safePage, safeSize, total == null ? 0 : total);
|
}
|
|
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 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"));
|
if (bankCode.isBlank()) throw new BusinessException(ErrorCode.BAD_REQUEST, "The selected bank branch has no Huifu bank code");
|
boolean enterprise = "enterprise".equalsIgnoreCase(clean(subject.get("subjectType")));
|
String cardName = enterprise ? clean(branch.get("bankName")) : clean(subject.get("subjectName"));
|
if (!enterprise && cardName.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "进件姓名缺失,无法绑定个人银行卡");
|
String requestNo = BusinessIdGenerator.next("BC");
|
try {
|
HuifuUserBusinessOpenGateway.Result result = userBusinessOpenGateway.create(new HuifuUserBusinessOpenCommand(
|
requestNo, clean(subject.get("providerHuifuId")), huifuPaymentProperties.getMerchantId().trim(), enterprise ? "0" : "1", cardName, cardNumber,
|
provinceCode(clean(card.get("cityCode"))), clean(card.get("cityCode")), bankCode, clean(branch.get("clearingCode")), clean(branch.get("branchName")),
|
sensitiveDataCrypto.decrypt(clean(subject.get("subjectIdNoEncrypted"))), certificateValidityType(clean(subject.get("subjectCertValidityType"))), clean(subject.get("subjectCertBeginDate")), clean(subject.get("subjectCertEndDate")), clean(subject.get("contactMobile"))));
|
if (result.accepted() && !result.tokenNo().isBlank()) jdbc.update("update merchant_bank_card set status='approved', provider_token_no=?, provider_apply_no=?, provider_request_no=?, provider_response_code=?, provider_response_message=?, reviewed_at=now(), reject_reason=null where id=?", result.tokenNo(), result.applyNo(), requestNo, result.code(), result.message(), card.get("id"));
|
else if (result.accepted()) jdbc.update("update merchant_bank_card set status='pending', provider_apply_no=?, provider_request_no=?, provider_response_code=?, provider_response_message=?, reject_reason=null where id=?", result.applyNo(), requestNo, result.code(), result.message(), card.get("id"));
|
else jdbc.update("update merchant_bank_card set status='rejected', provider_request_no=?, provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where id=?", requestNo, result.code(), result.message(), providerReason(result.code(), result.message()), card.get("id"));
|
} catch (HuifuPaymentGatewayException exception) {
|
jdbc.update("update merchant_bank_card set provider_request_no=?, reject_reason=? where id=?", requestNo, "Huifu submission outcome is pending confirmation", card.get("id"));
|
}
|
}
|
|
/**
|
* Business-opening (card binding) callback. Only a card that is still pending may change state,
|
* so a late or duplicated notification for an already approved or rejected card is ignored.
|
*/
|
@Transactional
|
@SuppressWarnings("unchecked")
|
public boolean handleBankCardCallback(Map<String, Object> payload) {
|
Map<String, Object> data = payload;
|
Object nested = payload.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()) requestNo = clean(payload.get("req_seq_id"));
|
if (requestNo.isBlank()) return false;
|
List<Map<String, Object>> cards = rows("select * from merchant_bank_card where provider_request_no=? limit 1 for update", requestNo);
|
if (cards.isEmpty()) return false;
|
Map<String, Object> card = cards.get(0);
|
Object cardId = card.get("id");
|
if (!"pending".equals(clean(card.get("status")))) return true;
|
String tokenNo = clean(data.get("token_no"));
|
String code = clean(data.get("sub_resp_code")); if (code.isBlank()) code = clean(data.get("resp_code"));
|
String message = clean(data.get("sub_resp_desc")); if (message.isBlank()) message = clean(data.get("resp_desc"));
|
String applyNo = clean(data.get("apply_no"));
|
if (!tokenNo.isBlank() && providerSuccess(code)) jdbc.update("update merchant_bank_card set status='approved', provider_token_no=?, provider_apply_no=coalesce(nullif(?,''),provider_apply_no), provider_response_code=?, provider_response_message=?, reviewed_at=now(), reject_reason=null where id=? and status='pending'", tokenNo, applyNo, code, message, cardId);
|
else if (!providerSuccess(code)) jdbc.update("update merchant_bank_card set status='rejected', provider_response_code=?, provider_response_message=?, reject_reason=?, reviewed_at=now() where id=? and status='pending'", code, message, providerReason(code, message), cardId);
|
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, cardId);
|
return true;
|
}
|
|
/**
|
* Onboarding audit callback. The handler is keyed by the application number and only moves an
|
* application that is not already decided, so duplicated notifications are no-ops and a late
|
* notification cannot overwrite a decided application.
|
*/
|
@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);
|
if ("approved".equals(clean(application.get("status"))) || "rejected".equals(clean(application.get("status")))) return true;
|
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 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=? and status not in ('approved','rejected')", 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=? and status='pending'", 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) {
|
int completed = 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);
|
if (completed > 0 && applicationId instanceof Number number) {
|
// Approved sub-merchants need a confirmed settlement/encashment snapshot before they can collect money.
|
paymentReadinessService.refreshQuietly(number.longValue());
|
}
|
}
|
|
@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"))); }
|
private Map<String, Object> bankCard(Map<String, Object> row) { return Map.of("id", String.valueOf(row.get("id")), "bankName", row.get("bankName"), "branchName", row.get("branchName"), "cardMasked", row.get("cardMasked"), "status", row.get("status"), "reason", clean(row.get("rejectReason")), "createdAt", String.valueOf(row.get("createdAt"))); }
|
private Map<String, Object> withdrawal(Map<String, Object> row) { Map<String, Object> value = new LinkedHashMap<>(); value.put("id", String.valueOf(row.get("id"))); value.put("withdrawalNo", row.get("withdrawalNo")); value.put("amountCents", row.get("amountCents")); value.put("status", AutoWithdrawalResultService.displayStatus(row.get("status") == null ? "" : String.valueOf(row.get("status")))); value.put("reason", clean(row.get("failureReason"))); value.put("bankName", clean(row.get("bankName"))); value.put("cardMasked", clean(row.get("cardMasked"))); value.put("createdAt", String.valueOf(row.get("createdAt"))); value.put("reviewedAt", clean(row.get("reviewedAt"))); value.put("paidAt", clean(row.get("paidAt"))); return value; }
|
private Map<String, Object> branch(String id, String branchName, String bankName) { return Map.of("id", id, "branchName", branchName, "bankName", bankName); }
|
private Object[] appendPaging(List<Object> params, int size, int offset) { Object[] values = new Object[params.size() + 2]; for (int index = 0; index < params.size(); index++) values[index] = params.get(index); values[params.size()] = size; values[params.size() + 1] = offset; return values; }
|
private List<Map<String, Object>> rows(String sql, Object... args) { return DatabaseRows.normalize(jdbc.queryForList(sql, args)); }
|
private Long number(String value) { try { return Long.valueOf(value); } catch (Exception ignored) { throw new BusinessException(ErrorCode.BAD_REQUEST, "编号无效"); } }
|
private String clean(Object value) { return value == null ? "" : String.valueOf(value).trim(); }
|
private String cityKeyword(String value) { String text = clean(value); for (String suffix : List.of("自治州", "地区", "盟", "市", "州")) if (text.endsWith(suffix)) return text.substring(0, text.length() - suffix.length()); return text; }
|
private String certificateValidityType(String value) { return "long_term".equals(value) ? "1" : "0"; }
|
private String certificateBeginDate(String value) { return clean(value).replace("-", ""); }
|
private String certificateEndDate(String value) { return certificateBeginDate(value); }
|
private String provinceCode(String cityCode) { return cityCode != null && cityCode.matches("\\d{6}") ? cityCode.substring(0, 2) + "0000" : ""; }
|
private void requireCertificatePeriod(String validityType, String beginDate, String endDate, String label) {
|
if (blank(beginDate)) throw new BusinessException(ErrorCode.BAD_REQUEST, label + " begin date is required");
|
if (!"long_term".equals(validityType) && blank(endDate)) throw new BusinessException(ErrorCode.BAD_REQUEST, label + " end date is required when it is not long-term");
|
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(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) || "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, "注册资本请输入数字,单位为万元"); }
|
}
|
}
|