package com.tourguide.modules.finance.service; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tourguide.common.api.PageResponse; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.finance.dto.GuideWithdrawalCreateRequest; import com.tourguide.modules.finance.dto.GuideWithdrawalSummaryResponse; import com.tourguide.modules.finance.dto.WithdrawalResponse; import com.tourguide.modules.finance.entity.GuideWithdrawal; import com.tourguide.modules.finance.entity.GuideWithdrawalAccount; import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper; 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.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; /** 导游提现申请、审核发起和斗拱到账回调。 */ @Service public class GuideWithdrawalService { private static final DateTimeFormatter NO_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"); private static final String STATUS_PENDING = "PENDING"; private static final String STATUS_APPROVED = "APPROVED"; private static final String STATUS_REJECTED = "REJECTED"; private final GuideWithdrawalMapper withdrawalMapper; private final GuideSettlementService settlementService; private final GuideUserService guideUserService; private final GuideWithdrawableBalanceService balanceService; private final GuideWithdrawalStateService stateService; private final GuideWithdrawalPayoutService payoutService; public GuideWithdrawalService(GuideWithdrawalMapper withdrawalMapper, GuideSettlementService settlementService, GuideUserService guideUserService, GuideWithdrawableBalanceService balanceService, GuideWithdrawalStateService stateService, GuideWithdrawalPayoutService payoutService) { this.withdrawalMapper = withdrawalMapper; this.settlementService = settlementService; this.guideUserService = guideUserService; this.balanceService = balanceService; this.stateService = stateService; this.payoutService = payoutService; } public GuideWithdrawalSummaryResponse summary(Long guideId) { try { settlementService.requireApproved(guideId); return new GuideWithdrawalSummaryResponse(balanceService.getAvailableAmount(guideId)); } catch (BusinessException exception) { if (exception.getCode() == 409) { return new GuideWithdrawalSummaryResponse(money(BigDecimal.ZERO)); } throw exception; } } public PageResponse page(Long guideId, long page, long size) { guideUserService.findEnabledById(guideId); long safePage = Math.max(page, 1); long safeSize = Math.min(Math.max(size, 1), 100); Page result = withdrawalMapper.selectPage( new Page(safePage, safeSize), new LambdaQueryWrapper() .eq(GuideWithdrawal::getGuideId, guideId) .orderByDesc(GuideWithdrawal::getAppliedAt) .orderByDesc(GuideWithdrawal::getId)); return new PageResponse(result.getRecords().stream() .map(WithdrawalResponse::new).collect(Collectors.toList()), result.getTotal(), result.getCurrent(), result.getSize()); } @Transactional public WithdrawalResponse create(Long guideId, GuideWithdrawalCreateRequest request) { GuideUser guide = guideUserService.findEnabledById(guideId); GuideWithdrawalAccount account = settlementService.requireApprovedCardForUpdate( guideId, request.getBankCardId()); BigDecimal amount = money(request.getAmount()); if (amount.compareTo(BigDecimal.ZERO) <= 0) { throw new BusinessException(400, "提现金额必须大于0"); } if (!balanceService.debit(guideId, amount)) { throw new BusinessException(409, "提现金额不能超过当前可提金额"); } GuideWithdrawal withdrawal = new GuideWithdrawal(); withdrawal.setWithdrawalNo(generateWithdrawalNo()); withdrawal.setGuideId(guideId); withdrawal.setGuideName(StringUtils.hasText(guide.getRealName()) ? guide.getRealName() : guide.getUsername()); withdrawal.setGuideMobile(guide.getMobile()); withdrawal.setAmount(amount); withdrawal.setBankCardId(account.getId()); withdrawal.setBankName(account.getBankName()); withdrawal.setCardMasked(maskCard(account.getBankCardCode())); withdrawal.setStatus(STATUS_PENDING); withdrawal.setAppliedAt(LocalDateTime.now()); withdrawal.setCreatedAt(LocalDateTime.now()); withdrawal.setUpdatedAt(LocalDateTime.now()); withdrawalMapper.insert(withdrawal); return new WithdrawalResponse(withdrawal); } public WithdrawalResponse process(Long id, String status, String remark, String processor) { if (STATUS_REJECTED.equals(status)) { return stateService.reject(id, remark, processor); } if (!STATUS_APPROVED.equals(status)) throw new BusinessException(400, "提现处理状态不正确"); return payoutService.approve(id, remark, processor); } @Transactional public boolean handleCallback(JSONObject body) { String requestSequence = first(body, "req_seq_id", "org_req_seq_id"); if (!StringUtils.hasText(requestSequence)) { throw new BusinessException(400, "汇付提现回调缺少请求流水号"); } GuideWithdrawal withdrawal = withdrawalMapper.selectByPayoutSequenceForUpdate(requestSequence); if (withdrawal == null) throw new BusinessException(404, "提现申请不存在"); if (STATUS_APPROVED.equals(withdrawal.getStatus()) || STATUS_REJECTED.equals(withdrawal.getStatus())) { return false; } boolean processing = GuideWithdrawalStateService.PROCESSING.equals(withdrawal.getStatus()); boolean recoverableTransferred = GuideWithdrawalStateService.TRANSFERRED.equals(withdrawal.getStatus()) && requestSequence.equals(withdrawal.getCashReqSeqId()); if (!processing && !recoverableTransferred) { return false; } validateCallback(body, withdrawal); String transStatus = first(body, "trans_stat", "cash_stat", "status"); LocalDateTime now = LocalDateTime.now(); if (isFailure(body, transStatus)) { String message = "导游账户取现失败:" + callbackMessage(body); if (withdrawalMapper.resetCashForRetry(withdrawal.getId(), message, now, now) != 1) { throw new BusinessException(500, "提现回调状态更新失败"); } withdrawal.setStatus(GuideWithdrawalStateService.TRANSFERRED); withdrawal.setCashReqSeqId(null); withdrawal.setCashReqDate(null); withdrawal.setCashHfSeqId(null); withdrawal.setPayoutMessage(message); withdrawal.setProcessedAt(now); withdrawal.setUpdatedAt(now); return true; } String hfSeqId = first(body, "hf_seq_id", "trans_seq_id"); if (StringUtils.hasText(hfSeqId)) withdrawal.setCashHfSeqId(hfSeqId); if (isSuccess(transStatus)) { withdrawal.setStatus(STATUS_APPROVED); withdrawal.setPayoutMessage("取现到账成功"); withdrawal.setProcessedAt(now); } else { withdrawal.setStatus(GuideWithdrawalStateService.PROCESSING); withdrawal.setPayoutMessage("导游账户取现结果待确认:" + callbackMessage(body)); } withdrawal.setUpdatedAt(now); if (withdrawalMapper.updateById(withdrawal) != 1) { throw new BusinessException(500, "提现回调状态更新失败"); } return true; } private void validateCallback(JSONObject body, GuideWithdrawal withdrawal) { GuideWithdrawalAccount account = settlementService.findCurrent(withdrawal.getGuideId()); String callbackHuifuId = first(body, "huifu_id", "out_huifu_id"); if (account == null || !StringUtils.hasText(callbackHuifuId) || !callbackHuifuId.equals(account.getHuifuId())) { throw new BusinessException(400, "汇付提现回调账户不匹配"); } String amountText = first(body, "cash_amt", "trans_amt", "ord_amt"); if (StringUtils.hasText(amountText)) { try { if (withdrawal.getAmount().compareTo(new BigDecimal(amountText)) != 0) { throw new BusinessException(409, "汇付提现回调金额不匹配"); } } catch (NumberFormatException exception) { throw new BusinessException(400, "汇付提现回调金额格式错误"); } } } private String callbackMessage(JSONObject body) { String message = first(body, "resp_desc", "fail_reason", "failure_reason"); return StringUtils.hasText(message) ? message : "斗拱未返回终态说明"; } private String generateWithdrawalNo() { return "WD" + LocalDateTime.now().format(NO_TIME_FORMAT) + String.format("%04d", ThreadLocalRandom.current().nextInt(10000)); } private String maskCard(String cardNumber) { if (!StringUtils.hasText(cardNumber)) return ""; String value = cardNumber.trim(); return "****" + value.substring(Math.max(0, value.length() - 4)); } private BigDecimal money(BigDecimal value) { return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP); } private boolean isSuccess(String status) { return "S".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status) || "SUCCEEDED".equalsIgnoreCase(status); } private boolean isFailure(JSONObject body, String status) { if ("F".equalsIgnoreCase(status) || "FAIL".equalsIgnoreCase(status) || "FAILED".equalsIgnoreCase(status)) { return true; } String responseCode = first(body, "resp_code", "code"); return StringUtils.hasText(responseCode) && !"00000000".equals(responseCode) && !"00000100".equals(responseCode); } private String first(JSONObject body, String... keys) { if (body == null) return null; for (String key : keys) { String value = body.getString(key); if (StringUtils.hasText(value)) return value; } JSONObject data = body.getJSONObject("data"); return data == null ? null : first(data, keys); } }