| | |
| | | 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; |
| | |
| | | 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; |
| | | |
| | |
| | | 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)); |
| | |
| | | |
| | | @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()); |
| | |
| | | |
| | | @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); |
| | |
| | | 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); |
| | |
| | | + 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(); |
| | |
| | | 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 包装继续使用顶层字段。 |
| | | } |
| | | } |
| | | } |
| | | } |