liujie
2026-09-14 a910819c1853081735356c7a2c5e05a2953602ab
backend/src/main/java/com/tourguide/modules/finance/service/GuideWithdrawalService.java
@@ -12,6 +12,9 @@
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.entity.GuideTeam;
import com.tourguide.modules.user.mapper.GuideTeamMapper;
import com.tourguide.modules.user.mapper.GuideUserMapper;
import com.tourguide.modules.user.service.GuideUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -21,6 +24,8 @@
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@@ -38,25 +43,35 @@
    private final GuideWithdrawableBalanceService balanceService;
    private final GuideWithdrawalStateService stateService;
    private final GuideWithdrawalPayoutService payoutService;
    private final GuideTeamMapper teamMapper;
    private final GuideUserMapper guideUserMapper;
    public GuideWithdrawalService(GuideWithdrawalMapper withdrawalMapper,
                                  GuideSettlementService settlementService,
                                  GuideUserService guideUserService,
                                  GuideWithdrawableBalanceService balanceService,
                                  GuideWithdrawalStateService stateService,
                                  GuideWithdrawalPayoutService payoutService) {
                                  GuideWithdrawalPayoutService payoutService,
                                  GuideTeamMapper teamMapper,
                                  GuideUserMapper guideUserMapper) {
        this.withdrawalMapper = withdrawalMapper;
        this.settlementService = settlementService;
        this.guideUserService = guideUserService;
        this.balanceService = balanceService;
        this.stateService = stateService;
        this.payoutService = payoutService;
        this.teamMapper = teamMapper;
        this.guideUserMapper = guideUserMapper;
    }
    public GuideWithdrawalSummaryResponse summary(Long guideId) {
        try {
            settlementService.requireApproved(guideId);
            return new GuideWithdrawalSummaryResponse(balanceService.getAvailableAmount(guideId));
            GuideUser guide = guideUserService.findEnabledById(guideId);
            GuideTeam collectionTeam = findCollectionTeam(guide);
            return new GuideWithdrawalSummaryResponse(collectionTeam == null
                    ? balanceService.getAvailableAmount(guideId)
                    : balanceService.getAvailableTeamAmount(collectionTeam.getId()));
        } catch (BusinessException exception) {
            if (exception.getCode() == 409) {
                return new GuideWithdrawalSummaryResponse(money(BigDecimal.ZERO));
@@ -82,19 +97,27 @@
    @Transactional
    public WithdrawalResponse create(Long guideId, GuideWithdrawalCreateRequest request) {
        GuideUser guide = guideUserService.findEnabledById(guideId);
        GuideUser guide = guideUserMapper.selectForUpdate(guideId);
        if (guide == null || !Integer.valueOf(1).equals(guide.getStatus())) {
            throw new BusinessException(401, "导游用户不存在或已停用");
        }
        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)) {
        GuideTeam collectionTeam = findCollectionTeam(guide);
        boolean debited = collectionTeam == null
                ? balanceService.debit(guideId, amount)
                : balanceService.debitTeam(collectionTeam.getId(), amount);
        if (!debited) {
            throw new BusinessException(409, "提现金额不能超过当前可提金额");
        }
        GuideWithdrawal withdrawal = new GuideWithdrawal();
        withdrawal.setWithdrawalNo(generateWithdrawalNo());
        withdrawal.setGuideId(guideId);
        withdrawal.setTeamId(collectionTeam == null ? null : collectionTeam.getId());
        withdrawal.setGuideName(StringUtils.hasText(guide.getRealName())
                ? guide.getRealName() : guide.getUsername());
        withdrawal.setGuideMobile(guide.getMobile());
@@ -120,23 +143,34 @@
    @Transactional
    public boolean handleCallback(JSONObject body) {
        String requestSequence = first(body, "req_seq_id", "org_req_seq_id");
        if (!StringUtils.hasText(requestSequence)) {
        List<String> requestSequences = callbackSequences(body);
        if (requestSequences.isEmpty()) {
            throw new BusinessException(400, "汇付提现回调缺少请求流水号");
        }
        GuideWithdrawal withdrawal = withdrawalMapper.selectByPayoutSequenceForUpdate(requestSequence);
        String requestSequence = null;
        GuideWithdrawal withdrawal = null;
        for (String candidate : requestSequences) {
            GuideWithdrawal matched = withdrawalMapper.selectByPayoutSequenceForUpdate(candidate);
            if (matched != null) {
                requestSequence = candidate;
                withdrawal = matched;
                break;
            }
        }
        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());
                && (requestSequence.equals(withdrawal.getCashReqSeqId())
                || requestSequence.equals(withdrawal.getCashHfSeqId()));
        if (!processing && !recoverableTransferred) {
            return false;
        }
        validateCallback(body, withdrawal);
        String transStatus = first(body, "trans_stat", "cash_stat", "status");
        String transStatus = first(body, "trans_stat", "cash_stat", "cash_status",
                "trans_status", "status");
        LocalDateTime now = LocalDateTime.now();
        if (isFailure(body, transStatus)) {
            String message = "导游账户取现失败:" + callbackMessage(body);
@@ -152,7 +186,7 @@
            withdrawal.setUpdatedAt(now);
            return true;
        }
        String hfSeqId = first(body, "hf_seq_id", "trans_seq_id");
        String hfSeqId = first(body, "hf_seq_id", "trans_seq_id", "cash_hf_seq_id");
        if (StringUtils.hasText(hfSeqId)) withdrawal.setCashHfSeqId(hfSeqId);
        if (isSuccess(transStatus)) {
            withdrawal.setStatus(STATUS_APPROVED);
@@ -198,6 +232,12 @@
                + String.format("%04d", ThreadLocalRandom.current().nextInt(10000));
    }
    private GuideTeam findCollectionTeam(GuideUser guide) {
        if (guide == null || guide.getTeamId() == null) return null;
        GuideTeam team = teamMapper.selectById(guide.getTeamId());
        return team != null && guide.getId().equals(team.getCollectionGuideId()) ? team : null;
    }
    private String maskCard(String cardNumber) {
        if (!StringUtils.hasText(cardNumber)) return "";
        String value = cardNumber.trim();
@@ -231,6 +271,46 @@
            if (StringUtils.hasText(value)) return value;
        }
        JSONObject data = body.getJSONObject("data");
        return data == null ? null : first(data, keys);
        if (data != null) return first(data, keys);
        String dataText = body.getString("data");
        if (StringUtils.hasText(dataText)) {
            try {
                JSONObject nested = JSONObject.parseObject(dataText);
                return nested == null ? null : first(nested, keys);
            } catch (RuntimeException ignored) {
                // 非 JSON 包装直接按字段缺失处理。
            }
        }
        return null;
    }
    private List<String> callbackSequences(JSONObject body) {
        List<String> sequences = new ArrayList<>();
        collectCallbackSequences(body, sequences);
        return sequences;
    }
    private void collectCallbackSequences(JSONObject body, List<String> sequences) {
        if (body == null) return;
        String[] keys = {"req_seq_id", "org_req_seq_id", "cash_req_seq_id", "withdrawal_no",
                "hf_seq_id", "trans_seq_id", "cash_hf_seq_id"};
        for (String key : keys) {
            String value = body.getString(key);
            if (StringUtils.hasText(value) && !sequences.contains(value)) sequences.add(value);
        }
        JSONObject data = body.getJSONObject("data");
        if (data != null) {
            collectCallbackSequences(data, sequences);
            return;
        }
        String dataText = body.getString("data");
        if (StringUtils.hasText(dataText)) {
            try {
                JSONObject nested = JSONObject.parseObject(dataText);
                collectCallbackSequences(nested, sequences);
            } catch (RuntimeException ignored) {
                // 非 JSON 包装继续使用顶层字段。
            }
        }
    }
}