package com.tourguide.modules.finance.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.tourguide.common.exception.BusinessException; import com.tourguide.common.huifu.biz.DouGongBasicDataBiz; import com.tourguide.modules.finance.dto.GuideBankBranchResponse; import com.tourguide.modules.finance.dto.GuideBankCardRequest; import com.tourguide.modules.finance.dto.GuideBankCardResponse; import com.tourguide.modules.finance.dto.GuideSettlementOnboardingRequest; import com.tourguide.modules.finance.dto.GuideSettlementStatusResponse; import com.tourguide.modules.finance.entity.GuideWithdrawalAccount; import com.tourguide.modules.finance.entity.GuideBankBranch; import com.tourguide.modules.finance.mapper.GuideBankBranchMapper; import com.tourguide.modules.finance.mapper.GuideWithdrawalAccountMapper; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.service.GuideUserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; /** 导游端进件、审核状态和当前结算卡能力。 */ @Service public class GuideSettlementService { static final int GUIDE_USER_TYPE = 2; static final int HUIFU_TYPE = 1; static final int ACTIVE_STATUS = 1; private static final int AUDIT_PENDING = 1; private static final int AUDIT_APPROVED = 2; private static final int AUDIT_REJECTED = 3; private static final String SUCCESS_CODE = "00000000"; private final GuideWithdrawalAccountMapper accountMapper; private final GuideUserService guideUserService; private final DouGongBasicDataBiz douGongBasicDataBiz; private final GuideBankBranchMapper bankBranchMapper; public GuideSettlementService(GuideWithdrawalAccountMapper accountMapper, GuideUserService guideUserService, DouGongBasicDataBiz douGongBasicDataBiz, GuideBankBranchMapper bankBranchMapper) { this.accountMapper = accountMapper; this.guideUserService = guideUserService; this.douGongBasicDataBiz = douGongBasicDataBiz; this.bankBranchMapper = bankBranchMapper; } public GuideSettlementStatusResponse status(Long guideId) { guideUserService.findEnabledById(guideId); GuideWithdrawalAccount account = findCurrent(guideId); if (account == null) return new GuideSettlementStatusResponse("unsubmitted", null, null, null); refreshAuditState(account); return toStatus(account); } @Transactional(noRollbackFor = BusinessException.class) public GuideSettlementStatusResponse submit(Long guideId, GuideSettlementOnboardingRequest request) { GuideUser guide = guideUserService.findEnabledById(guideId); validateOnboarding(request); GuideWithdrawalAccount account = findCurrent(guideId); if (account != null && "approved".equals(statusValue(account))) { throw new BusinessException(409, "当前导游已完成进件"); } if (account != null && "pending".equals(statusValue(account))) { throw new BusinessException(409, "进件资料正在审核中,请勿重复提交"); } if (account != null && StringUtils.hasText(account.getHuifuId()) && StringUtils.hasText(account.getSubjectType()) && !account.getSubjectType().equals(request.getSubjectType())) { throw new BusinessException(409, "已提交的进件主体类型不可变更,请按原类型重新提交"); } if (account == null) account = new GuideWithdrawalAccount(); applyOnboarding(account, guideId, request, guide); account.setApplicationStatus("processing"); account.setIsAudit(AUDIT_PENDING); account.setFailureMsgsReason(null); account.setOutRequestNo(randomRequestNo()); account.setCreateTime(account.getCreateTime() == null ? LocalDateTime.now() : account.getCreateTime()); save(account); try { boolean enterprise = "enterprise".equals(account.getSubjectType()); Map result = enterprise ? submitEnterprise(account) : submitIndividual(account); requireSuccess(result, "斗拱进件提交失败"); applyRemoteIds(account, result); if (enterprise) { applyRemoteAuditState(account, result, false); } else { applyIndividualSuccess(account); } save(account); return toStatus(account); } catch (BusinessException exception) { markRejected(account, exception.getMessage()); throw exception; } catch (Exception exception) { markRejected(account, exception.getMessage()); throw new BusinessException(502, "斗拱进件服务暂不可用,请稍后重试"); } } public List bankCards(Long guideId) { GuideWithdrawalAccount account = requireApproved(guideId); if (!StringUtils.hasText(account.getBankCardCode())) return Collections.emptyList(); refreshBankCardState(account); return Collections.singletonList(new GuideBankCardResponse(account)); } @Transactional public GuideBankCardResponse bindBankCard(Long guideId, GuideBankCardRequest request) { guideUserService.findEnabledById(guideId); GuideWithdrawalAccount account = accountMapper.selectCurrentForUpdate(toTableUserId(guideId)); requireApproved(account); GuideBankBranch branch = findActiveBranch(request.getBranchCode(), request.getCityCode()); account.setBankName(branch.getBankName()); account.setBankCardCode(trim(request.getCardNumber())); account.setBankBranchName(branch.getBranchName()); account.setBankBranchCode(branch.getBranchCode()); account.setBankCardProvinceCode(trim(request.getProvinceCode())); account.setBankCardCityCode(branch.getCityCode()); account.setOpenAccountName(account.getCertificateName()); account.setBankCardStatus("local"); account.setBankCardReason(null); account.setTokenNo(null); account.setCusApplicationId(null); save(account); return new GuideBankCardResponse(account); } @Transactional(noRollbackFor = BusinessException.class) public GuideBankCardResponse openBankCardBusiness(Long guideId, Integer cardId) { guideUserService.findEnabledById(guideId); GuideWithdrawalAccount account = accountMapper.selectCurrentForUpdate(toTableUserId(guideId)); requireApproved(account); requireCurrentBankCard(account, cardId); if (StringUtils.hasText(account.getTokenNo())) { account.setBankCardStatus("approved"); account.setBankCardReason(null); save(account); return new GuideBankCardResponse(account); } if ("pending".equals(account.getBankCardStatus()) && StringUtils.hasText(account.getCusApplicationId())) { refreshBankCardState(account); return new GuideBankCardResponse(account); } account.setBankCardStatus("pending"); account.setBankCardReason(null); account.setCusApplicationId(null); save(account); try { Map result = douGongBasicDataBiz.userBusinessOpen(account); requireSuccess(result, "银行卡业务入驻失败"); account.setTokenNo(first(result, "token_no", "card_token_no")); account.setCusApplicationId(first(result, "apply_no", "application_no", "cus_application_id")); if (!StringUtils.hasText(account.getTokenNo()) && !StringUtils.hasText(account.getCusApplicationId())) { throw new BusinessException(502, "银行卡业务入驻响应缺少取现卡序列号"); } account.setBankCardStatus(StringUtils.hasText(account.getTokenNo()) ? "approved" : "pending"); save(account); return new GuideBankCardResponse(account); } catch (BusinessException exception) { account.setBankCardStatus("rejected"); account.setBankCardReason(exception.getMessage()); save(account); throw exception; } catch (Exception exception) { account.setBankCardStatus("rejected"); account.setBankCardReason(exception.getMessage()); save(account); throw new BusinessException(502, "银行卡业务入驻服务暂不可用,请稍后重试"); } } private void requireCurrentBankCard(GuideWithdrawalAccount account, Integer cardId) { if (cardId == null || !cardId.equals(account.getId()) || !StringUtils.hasText(account.getBankCardCode())) { throw new BusinessException(404, "银行卡不存在,请重新选择"); } } private GuideBankBranch findActiveBranch(String branchCode, String cityCode) { GuideBankBranch branch = bankBranchMapper.selectOne(new LambdaQueryWrapper() .eq(GuideBankBranch::getBranchCode, trim(branchCode)) .eq(GuideBankBranch::getCityCode, trim(cityCode)) .eq(GuideBankBranch::getStatus, ACTIVE_STATUS) .last("LIMIT 1")); if (branch == null) { throw new BusinessException(400, "开户行不存在或已停用,请重新选择"); } return branch; } public List branches(String cityCode, String keyword) { String query = trim(keyword); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(GuideBankBranch::getStatus, 1) .eq(StringUtils.hasText(cityCode), GuideBankBranch::getCityCode, StringUtils.hasText(cityCode) ? cityCode.trim() : null) .and(StringUtils.hasText(query), item -> item.like(GuideBankBranch::getBankName, query) .or().like(GuideBankBranch::getBranchName, query)) .orderByAsc(GuideBankBranch::getBankName) .orderByAsc(GuideBankBranch::getBranchName) .last("LIMIT 100"); return bankBranchMapper.selectList(wrapper).stream() .map(GuideBankBranchResponse::new) .collect(Collectors.toList()); } public GuideWithdrawalAccount requireApproved(Long guideId) { guideUserService.findEnabledById(guideId); return requireApproved(findCurrent(guideId)); } public GuideWithdrawalAccount requireApprovedCard(Long guideId, Integer cardId) { GuideWithdrawalAccount account = requireApproved(guideId); return requireApprovedCard(account, cardId); } /** 提现创建事务内锁定当前进件账户,串行校验余额和结算卡。 */ public GuideWithdrawalAccount requireApprovedCardForUpdate(Long guideId, Integer cardId) { guideUserService.findEnabledById(guideId); GuideWithdrawalAccount account = accountMapper.selectCurrentForUpdate(toTableUserId(guideId)); requireApproved(account); return requireApprovedCard(account, cardId); } private GuideWithdrawalAccount requireApprovedCard(GuideWithdrawalAccount account, Integer cardId) { if (!account.getId().equals(cardId) || !StringUtils.hasText(account.getTokenNo()) || "rejected".equals(account.getBankCardStatus())) { throw new BusinessException(409, "请选择审核通过的银行卡"); } return account; } public GuideWithdrawalAccount findCurrent(Long guideId) { return accountMapper.selectOne(new LambdaQueryWrapper() .eq(GuideWithdrawalAccount::getUserId, toTableUserId(guideId)) .eq(GuideWithdrawalAccount::getUserType, GUIDE_USER_TYPE) .eq(GuideWithdrawalAccount::getType, HUIFU_TYPE) .eq(GuideWithdrawalAccount::getStatus, ACTIVE_STATUS) .orderByDesc(GuideWithdrawalAccount::getId) .last("LIMIT 1")); } private Map submitEnterprise(GuideWithdrawalAccount account) throws Exception { return StringUtils.hasText(account.getHuifuId()) ? douGongBasicDataBiz.userBasicDataEntModify(account) : douGongBasicDataBiz.userBasicDataEnt(account); } private Map submitIndividual(GuideWithdrawalAccount account) throws Exception { return StringUtils.hasText(account.getHuifuId()) ? douGongBasicDataBiz.userBasicDataIndvModify(account) : douGongBasicDataBiz.userBasicDataIndv(account); } private void refreshAuditState(GuideWithdrawalAccount account) { if (!"pending".equals(statusValue(account)) || !StringUtils.hasText(account.getHuifuId())) return; // 个人基础进件是同步接口:成功响应只有 huifu_id,不会返回申请单号,也无需查询审核状态。 if (!"enterprise".equals(account.getSubjectType())) { applyIndividualSuccess(account); save(account); return; } if (!StringUtils.hasText(account.getCusApplicationId())) return; try { Map result = douGongBasicDataBiz.userApplyQuery(account, account.getCusApplicationId()); requireSuccess(result, "进件审核状态查询失败"); applyRemoteAuditState(account, result, true); save(account); } catch (BusinessException exception) { throw exception; } catch (Exception exception) { throw new BusinessException(502, "进件审核状态查询失败,请稍后重试"); } } private void refreshBankCardState(GuideWithdrawalAccount account) { if (!"pending".equals(account.getBankCardStatus()) || !StringUtils.hasText(account.getHuifuId()) || !StringUtils.hasText(account.getCusApplicationId())) return; try { Map result = douGongBasicDataBiz.userApplyQuery(account, account.getCusApplicationId()); requireSuccess(result, "银行卡审核状态查询失败"); String remoteStatus = remoteStatus(result); String tokenNo = first(result, "token_no", "card_token_no"); if (StringUtils.hasText(tokenNo)) account.setTokenNo(tokenNo); if (isApproved(remoteStatus) || StringUtils.hasText(account.getTokenNo())) { account.setBankCardStatus("approved"); account.setBankCardReason(null); } else if (isRejected(remoteStatus)) { account.setBankCardStatus("rejected"); account.setBankCardReason(remoteReason(result)); } save(account); } catch (Exception ignored) { // 备注:银行卡列表仍返回本地最后状态,避免第三方短暂不可用导致页面完全不可用。 } } private void applyRemoteAuditState(GuideWithdrawalAccount account, Map result, boolean keepPendingWhenUnknown) { String remoteStatus = remoteStatus(result); if (isApproved(remoteStatus)) { account.setIsAudit(AUDIT_APPROVED); account.setApplicationStatus("succeeded"); account.setSucceededAt(account.getSucceededAt() == null ? LocalDateTime.now() : account.getSucceededAt()); account.setFailureMsgsReason(null); } else if (isRejected(remoteStatus)) { account.setIsAudit(AUDIT_REJECTED); account.setApplicationStatus("failed"); account.setFailureMsgsReason(remoteReason(result)); } else if (keepPendingWhenUnknown || !StringUtils.hasText(remoteStatus)) { account.setIsAudit(AUDIT_PENDING); account.setApplicationStatus("submitted"); } else { account.setIsAudit(AUDIT_PENDING); account.setApplicationStatus(remoteStatus); } } private void applyIndividualSuccess(GuideWithdrawalAccount account) { if (!StringUtils.hasText(account.getHuifuId())) { throw new BusinessException(502, "斗拱个人进件成功响应缺少汇付ID"); } account.setIsAudit(AUDIT_APPROVED); account.setApplicationStatus("succeeded"); account.setSucceededAt(account.getSucceededAt() == null ? LocalDateTime.now() : account.getSucceededAt()); account.setFailureMsgsReason(null); account.setCusApplicationId(null); } private GuideWithdrawalAccount requireApproved(GuideWithdrawalAccount account) { if (account == null) throw new BusinessException(409, "请先完成导游进件"); if (!"approved".equals(statusValue(account))) { throw new BusinessException(409, "进件审核通过后才能继续操作"); } if (!StringUtils.hasText(account.getHuifuId())) { throw new BusinessException(409, "进件账户信息不完整,请联系平台处理"); } return account; } private GuideSettlementStatusResponse toStatus(GuideWithdrawalAccount account) { return new GuideSettlementStatusResponse(statusValue(account), StringUtils.hasText(account.getSubjectType()) ? account.getSubjectType() : "individual", account.getFailureMsgsReason(), account.getCreateTime()); } private String statusValue(GuideWithdrawalAccount account) { if (account == null) return "unsubmitted"; if (Integer.valueOf(AUDIT_APPROVED).equals(account.getIsAudit())) return "approved"; if (Integer.valueOf(AUDIT_REJECTED).equals(account.getIsAudit())) return "rejected"; String value = account.getApplicationStatus(); if (isApproved(value) && StringUtils.hasText(account.getHuifuId())) return "approved"; if (isRejected(value)) return "rejected"; return "pending"; } private void validateOnboarding(GuideSettlementOnboardingRequest request) { boolean enterprise = "enterprise".equals(request.getSubjectType()); if (enterprise) { requireText(request.getEnterpriseName(), "企业名称不能为空"); if (!StringUtils.hasText(request.getCreditCode()) || !request.getCreditCode().trim().matches("^[0-9A-Z]{18}$")) { throw new BusinessException(400, "统一社会信用代码格式不正确"); } requireValidity(request.getLicenseValidityType(), request.getLicenseBeginDate(), request.getLicenseEndDate(), "营业执照"); requireText(request.getRegisteredProvinceCode(), "注册地址省编码不能为空"); requireText(request.getRegisteredCityCode(), "注册地址市编码不能为空"); requireText(request.getRegisteredDistrictCode(), "注册地址区县编码不能为空"); requireText(request.getRegisteredAddress(), "详细注册地址不能为空"); } requireValidity(request.getSubjectCertificateValidityType(), request.getSubjectCertificateBeginDate(), request.getSubjectCertificateEndDate(), "证件"); } private void requireValidity(String type, String begin, String end, String label) { if (!"fixed".equals(type) && !"long_term".equals(type)) { throw new BusinessException(400, label + "有效期类型不支持"); } requireText(begin, label + "有效期开始日期不能为空"); if ("fixed".equals(type)) requireText(end, label + "有效期结束日期不能为空"); } private void requireText(String value, String message) { if (!StringUtils.hasText(value)) throw new BusinessException(400, message); } private void applyOnboarding(GuideWithdrawalAccount account, Long guideId, GuideSettlementOnboardingRequest request, GuideUser guide) { account.setUserId(toTableUserId(guideId)); account.setUserType(GUIDE_USER_TYPE); account.setType(HUIFU_TYPE); account.setStatus(ACTIVE_STATUS); account.setSubjectType(request.getSubjectType()); account.setEnterpriseName(trim(request.getEnterpriseName())); account.setCreditCode(trim(request.getCreditCode())); account.setLicenseValidityType(trim(request.getLicenseValidityType())); account.setLicenseBeginDate(trim(request.getLicenseBeginDate())); account.setLicenseEndDate(trim(request.getLicenseEndDate())); account.setRegisteredProvinceCode(trim(request.getRegisteredProvinceCode())); account.setRegisteredCityCode(trim(request.getRegisteredCityCode())); account.setRegisteredDistrictCode(trim(request.getRegisteredDistrictCode())); account.setRegisteredAddress(trim(request.getRegisteredAddress())); account.setContactName(trim(request.getContactName())); account.setCertificateName(trim(request.getSubjectName())); account.setCertificateType(0); account.setCertificateCode(trim(request.getSubjectIdNumber()).toUpperCase()); account.setCertificateExpireTime(dateRange(request.getSubjectCertificateValidityType(), request.getSubjectCertificateBeginDate(), request.getSubjectCertificateEndDate())); account.setOpenAccountName(trim(request.getSubjectName())); account.setPhone(StringUtils.hasText(request.getContactMobile()) ? request.getContactMobile().trim() : guide.getMobile()); } private String dateRange(String type, String begin, String end) { return trim(begin) + "," + ("long_term".equals(type) ? "长期" : trim(end)); } private void applyRemoteIds(GuideWithdrawalAccount account, Map result) { String huifuId = first(result, "huifu_id", "user_huifu_id"); String applicationId = first(result, "apply_no", "application_no", "cus_application_id"); if (StringUtils.hasText(huifuId)) account.setHuifuId(huifuId); if (StringUtils.hasText(applicationId)) account.setCusApplicationId(applicationId); } private void requireSuccess(Map result, String fallback) { String code = first(result, "resp_code", "code"); if (SUCCESS_CODE.equals(code)) return; String message = first(result, "resp_desc", "message", "msg"); throw new BusinessException(502, StringUtils.hasText(message) ? message : fallback); } private String remoteStatus(Map result) { return first(result, "apply_status", "application_status", "audit_status", "status", "trans_stat"); } private String remoteReason(Map result) { String reason = first(result, "failure_reason", "fail_reason", "audit_remark", "resp_desc"); return StringUtils.hasText(reason) ? reason : "进件审核未通过"; } private boolean isApproved(String value) { if (!StringUtils.hasText(value)) return false; String normalized = value.trim().toLowerCase(); return Arrays.asList("2", "s", "success", "succeeded", "approved", "pass", "passed").contains(normalized); } private boolean isRejected(String value) { if (!StringUtils.hasText(value)) return false; String normalized = value.trim().toLowerCase(); return Arrays.asList("3", "f", "failed", "failure", "rejected", "reject").contains(normalized); } @SuppressWarnings("unchecked") private String first(Map result, String... keys) { if (result == null) return null; for (String key : keys) { Object value = result.get(key); if (value != null && StringUtils.hasText(String.valueOf(value))) return String.valueOf(value); } Object data = result.get("data"); if (data instanceof Map) return first((Map) data, keys); if (data instanceof String && StringUtils.hasText((String) data)) { try { Map nested = com.alibaba.fastjson.JSONObject.parseObject( (String) data, LinkedHashMap.class); return first(nested, keys); } catch (Exception ignored) {} } return null; } private void markRejected(GuideWithdrawalAccount account, String reason) { account.setIsAudit(AUDIT_REJECTED); account.setApplicationStatus("failed"); account.setFailureMsgsReason(StringUtils.hasText(reason) ? reason : "斗拱进件失败"); save(account); } private void save(GuideWithdrawalAccount account) { if (account.getId() == null) accountMapper.insert(account); else accountMapper.updateById(account); } private int toTableUserId(Long guideId) { try { return Math.toIntExact(guideId); } catch (ArithmeticException exception) { throw new BusinessException(400, "导游ID超出进件表支持范围"); } } private String randomRequestNo() { return UUID.randomUUID().toString().replace("-", ""); } private String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; } }