package com.talentcard.server.business.merchant;
import com.alibaba.fastjson.JSON;
import com.talentcard.server.business.DatabaseRows;
import com.talentcard.server.business.payment.huifu.HuifuMerchantRealnameCommand;
import com.talentcard.server.business.payment.huifu.HuifuMerchantRealnameGateway;
import com.talentcard.server.business.payment.huifu.HuifuOnboardingProgressGateway;
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.common.BusinessException;
import com.talentcard.server.common.ErrorCode;
import com.talentcard.server.common.SensitiveDataCrypto;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
/**
* 微信实名认证的发起与状态推进(法人路径)。
*
*
实名认证是「微信支付权限」的前置:业务开通成功之后才做,而不做完实名与授权就收不了款。
* 平台原先只能查询和展示实名状态({@code v2.merchant.busi.realname.query}),没有任何发起能力,
* 于是进度永远停在 {@code APPLYMENT_STATE_NOAPPLY}、二维码永远不出现。本服务补上发起这一环。
*
*
授权不是平台能代替的动作。申请单审核通过后,需要本人扫描通道回传的小程序码完成授权;
* 本服务只负责发起、推进状态与判断是否真的完成({@code APPLYMENT_STATE_PASSED} **且**
* {@code authorize_stat=1}),不会把本地状态改写成已授权。
*/
@Service
public class MerchantRealnameService {
private static final Logger log = LoggerFactory.getLogger(MerchantRealnameService.class);
/** 受理类回执:与业务开通一致,90000000=审核中属于已受理。 */
private static final List ACCEPTED_CODES = List.of("00000000","00000100","90000000","10000","0");
/** 未决状态:有这些状态就不要重新提交,先查询。 */
private static final List PENDING_STATES = List.of("processing","approved");
private final JdbcTemplate jdbc;
private final SensitiveDataCrypto sensitiveDataCrypto;
private final HuifuMerchantRealnameGateway realnameGateway;
private final HuifuOnboardingProgressGateway progressGateway;
private final MerchantOnboardingProgressService progressService;
private final HuifuPictureUploadGateway pictureUploadGateway;
private final HuifuPaymentProperties huifuPaymentProperties;
public MerchantRealnameService(JdbcTemplate jdbc, SensitiveDataCrypto sensitiveDataCrypto,
HuifuMerchantRealnameGateway realnameGateway, HuifuOnboardingProgressGateway progressGateway,
MerchantOnboardingProgressService progressService, HuifuPictureUploadGateway pictureUploadGateway,
HuifuPaymentProperties huifuPaymentProperties) {
this.jdbc = jdbc;
this.sensitiveDataCrypto = sensitiveDataCrypto;
this.realnameGateway = realnameGateway;
this.progressGateway = progressGateway;
this.progressService = progressService;
this.pictureUploadGateway = pictureUploadGateway;
this.huifuPaymentProperties = huifuPaymentProperties;
}
/**
* 发起(或推进)指定申请的微信实名认证。
*
* @param retry 只对「通道已明确驳回」的重发起有意义:给新申请单号重来。未决申请无论传什么
* 都不会重复提交——官方明确驳回后才允许更换业务申请编号重新提交。
*/
public Map submit(Long applicationId, boolean retry) {
Map application = application(applicationId);
if (!progressService.supports(application)) throw new BusinessException(ErrorCode.CONFLICT, "该进件申请暂不支持分阶段开通");
String huifuId = clean(application.get("providerHuifuId"));
if (huifuId.isBlank()) throw new BusinessException(ErrorCode.CONFLICT, "商户还没有汇付商户号,无法发起实名认证");
String status = clean(application.get("realnameStatus"));
if (PENDING_STATES.contains(status)) {
// 未决申请不重复提交:已有申请单或结果未知时先查询通道。
return progressService.refresh(applicationId, false);
}
if ("failed".equals(status) && !retry) {
throw new BusinessException(ErrorCode.CONFLICT, "上次实名申请未通过,请按驳回原因补齐资料后重新发起");
}
requireWechatBusinessOpen(application);
HuifuMerchantRealnameCommand command = command(application, huifuId);
String requestNo = requestNo(clean(application.get("applicationNo")));
jdbc.update("update merchant_onboarding_application set realname_status='processing', realname_request_no=?, realname_apply_no=null, realname_response_code=null, realname_response_message=null where id=?", requestNo, applicationId);
try {
HuifuMerchantRealnameGateway.Result result = realnameGateway.submit(requestNo, huifuId, command);
if (!result.accepted()) {
jdbc.update("update merchant_onboarding_application set realname_status='failed', realname_apply_no=nullif(?,''), realname_response_code=?, realname_response_message=? where id=?", result.applymentId(), result.code(), result.message(), applicationId);
} else {
jdbc.update("update merchant_onboarding_application set realname_status='processing', realname_apply_no=nullif(?,''), realname_response_code=?, realname_response_message=? where id=?", result.applymentId(), result.code(), result.message(), applicationId);
}
} catch (HuifuPaymentGatewayException exception) {
// 结果未知时保持 processing:不回退成可重提交,否则可能给同一商户报两份实名申请。
String state = exception.isOutcomeUnknown() ? "processing" : "failed";
log.warn("Huifu merchant realname outcome {}: applicationId={}, requestNo={}", state, applicationId, requestNo, exception);
jdbc.update("update merchant_onboarding_application set realname_status=?, realname_response_message=? where id=?", state, exception.getMessage() == null ? "" : truncate(exception.getMessage()), applicationId);
}
return progressService.refresh(applicationId, true);
}
/**
* 发起实名要求商户已成功入驻微信。以分阶段进度里「微信支付」阶段的证据为准,
* 而不是只看本地某个标志位——本地标志位与通道脱节时会把没开通的商户放过去。
*/
private void requireWechatBusinessOpen(Map application) {
Map view = progressService.view(application);
Object stages = view.get("stages");
if (stages instanceof List> list) {
for (Object item : list) {
if (item instanceof Map,?> stage && "wechat".equals(clean(stage.get("key"))) && "confirmed".equals(clean(stage.get("status")))) return;
}
}
throw new BusinessException(ErrorCode.CONFLICT, "微信支付业务开通尚未确认生效,请先刷新进度确认开通结果,再发起实名认证");
}
/** 法人资料。缺项要一次说清,不能提交后等通道驳回——驳回要换业务申请编号重来。 */
private HuifuMerchantRealnameCommand command(Map application, String huifuId) {
String legalName = clean(application.get("subjectName"));
String legalMobile = clean(application.get("legalMobile"));
String legalIdNumber = decrypt(application.get("subjectIdNoEncrypted"));
String legalAddress = clean(application.get("legalCertificateAddress"));
boolean enterprise = "enterprise".equalsIgnoreCase(clean(application.get("subjectType")));
Map store = enterprise ? Map.of() : store(application);
List missing = new ArrayList<>();
if (legalName.isBlank()) missing.add("法人姓名");
if (!legalMobile.matches("1\\d{10}")) missing.add("法人手机号(必须是法人本人的号码,不能用联系人手机号顶替)");
if (legalIdNumber.isBlank()) missing.add("法人身份证号");
if (enterprise && legalAddress.isBlank()) missing.add("法人证件居住地址");
if (!enterprise) requireMicroStoreMaterial(application, store, missing);
if (!missing.isEmpty()) throw new BusinessException(ErrorCode.BAD_REQUEST, "发起实名还缺:" + String.join(";", missing));
return new HuifuMerchantRealnameCommand(legalName, legalMobile, legalIdNumber, legalAddress,
legalIsBeneficiary(huifuId, legalName), enterprise ? null : microStore(application, store));
}
/**
* 小微(个人)主体在实名路径上另收一组门店材料,缺项先报出来。
*
* 官网把这一组都标成 N,但每一条都注明「小微商户填写」;而实名被驳回要换业务申请编号重来,
* 所以缺材料宁可在这里拦住,也不提交一份注定被驳回的申请。
*/
private void requireMicroStoreMaterial(Map application, Map store, List missing) {
if (clean(application.get("storeHeaderUrl")).isBlank()) missing.add("门店门头照");
if (clean(application.get("storeIndoorUrl")).isBlank()) missing.add("店内环境照");
if (clean(store.get("storeName")).isBlank()) missing.add("门店名称");
if (clean(application.get("legalIdFrontUrl")).isBlank() || clean(application.get("legalIdBackUrl")).isBlank()) missing.add("身份证正反面照片");
if (storeAddress(application, store).isBlank()) missing.add("门店地址");
}
/**
* 小微(个人)主体的门店材料,图片一律现传现取 fileId。
*
* {@code store_address_code} 用的是进件时选的经营市编码:官网示例 {@code 360400} 就是市编码,
* 但字段名写的是「门店省市编码」、长度 16,是否要「省+市」拼接尚无官方说明
* (见交付记录里的待确认项),这里按示例的市编码提交。
*/
private HuifuMerchantRealnameCommand.MicroStore microStore(Map application, Map store) {
return new HuifuMerchantRealnameCommand.MicroStore(
huifuPaymentProperties.getMerchantMicroBusinessType().trim(),
clean(store.get("storeName")),
pictureUploadGateway.uploadUrl(clean(application.get("storeHeaderUrl")), "F22"),
pictureUploadGateway.uploadUrl(clean(application.get("storeIndoorUrl")), "F24"),
clean(application.get("registeredCityCode")),
storeAddress(application, store),
pictureUploadGateway.uploadUrl(clean(application.get("legalIdFrontUrl")), "F40"),
pictureUploadGateway.uploadUrl(clean(application.get("legalIdBackUrl")), "F41"));
}
/**
* 门店地址取商家档案上的门店地址;档案没填时退回进件时的地址。
*
* 个人主体的 {@code registered_address} 存的是证件地址(进件表单没有单独收经营详细地址),
* 拿它当门店地址并不准确,但比留空更可能过审;商户在商家档案里补上门店地址后就会以档案为准。
*/
private String storeAddress(Map application, Map store) {
String address = clean(store.get("address"));
return address.isBlank() ? clean(application.get("registeredAddress")) : address;
}
private Map store(Map application) {
Object storeId = application.get("storeId");
if (storeId == null) return Map.of();
List