package com.tourguide.modules.finance.service; 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.FinanceLedgerPageResponse; import com.tourguide.modules.finance.dto.FinanceLedgerResponse; import com.tourguide.modules.finance.dto.WithdrawalProcessRequest; import com.tourguide.modules.finance.dto.WithdrawalResponse; import com.tourguide.modules.finance.entity.GuideWithdrawal; import com.tourguide.modules.finance.mapper.GuideWithdrawalMapper; import com.tourguide.modules.operation.dto.PlatformSettingsResponse; import com.tourguide.modules.operation.service.PlatformSettingsService; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.entity.TourOrderRefund; import com.tourguide.modules.order.mapper.TourOrderMapper; import com.tourguide.modules.order.mapper.TourOrderRefundMapper; import com.tourguide.modules.user.entity.GuideTeam; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideTeamMapper; import com.tourguide.modules.user.mapper.GuideUserMapper; import org.springframework.beans.factory.annotation.Autowired; 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.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Service public class AdminFinanceService { private final TourOrderMapper orderMapper; private final GuideWithdrawalMapper withdrawalMapper; private final GuideUserMapper guideUserMapper; private final GuideTeamMapper guideTeamMapper; private final PlatformSettingsService settingsService; private final GuideWithdrawalService guideWithdrawalService; private final TourOrderRefundMapper refundMapper; public AdminFinanceService(TourOrderMapper orderMapper, GuideWithdrawalMapper withdrawalMapper, GuideUserMapper guideUserMapper, GuideTeamMapper guideTeamMapper, PlatformSettingsService settingsService, GuideWithdrawalService guideWithdrawalService) { this(orderMapper, withdrawalMapper, guideUserMapper, guideTeamMapper, settingsService, guideWithdrawalService, null); } @Autowired public AdminFinanceService(TourOrderMapper orderMapper, GuideWithdrawalMapper withdrawalMapper, GuideUserMapper guideUserMapper, GuideTeamMapper guideTeamMapper, PlatformSettingsService settingsService, GuideWithdrawalService guideWithdrawalService, TourOrderRefundMapper refundMapper) { this.orderMapper = orderMapper; this.withdrawalMapper = withdrawalMapper; this.guideUserMapper = guideUserMapper; this.guideTeamMapper = guideTeamMapper; this.settingsService = settingsService; this.guideWithdrawalService = guideWithdrawalService; this.refundMapper = refundMapper; } public FinanceLedgerPageResponse ledger(long page, long size, String direction, String keyword, String paymentMethod, LocalDate startDate, LocalDate endDate) { boolean refund = "REFUND".equals(direction); if (refund) { return refundLedger(page, size, keyword, paymentMethod, startDate, endDate); } LambdaQueryWrapper query = ledgerQuery(refund, keyword, paymentMethod, startDate, endDate); Page result = orderMapper.selectPage(new Page(page, size), query); PlatformSettingsResponse settings = settingsService.getSettings(); Map guides = loadGuides(result.getRecords()); Map teams = loadTeams(result.getRecords(), guides.values()); List records = result.getRecords().stream() .map(order -> toLedger(order, false, settings, guides.get(order.getGuideId()), teams, null)) .collect(Collectors.toList()); BigDecimal totalAmount = orderMapper.selectList( ledgerQuery(refund, keyword, paymentMethod, startDate, endDate)).stream() .map(this::paymentAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); return new FinanceLedgerPageResponse( new PageResponse(records, result.getTotal(), result.getCurrent(), result.getSize()), totalAmount.setScale(2, RoundingMode.HALF_UP)); } private FinanceLedgerPageResponse refundLedger(long page, long size, String keyword, String paymentMethod, LocalDate startDate, LocalDate endDate) { List orders = orderMapper.selectList(refundOrderQuery(keyword, paymentMethod)); PlatformSettingsResponse settings = settingsService.getSettings(); Map guides = loadGuides(orders); Map teams = loadTeams(orders, guides.values()); Map> refundRecords = loadRefundRecords(orders); List records = new ArrayList(); for (TourOrder order : orders) { List orderRefunds = refundRecords.get(order.getId()); if (orderRefunds == null || orderRefunds.isEmpty()) { if (hasLegacyRefund(order) && inRefundDateRange(order.getRefundTime(), startDate, endDate)) { records.add(toLedger(order, true, settings, guides.get(order.getGuideId()), teams, null)); } continue; } for (TourOrderRefund refund : orderRefunds) { LocalDateTime refundTime = firstPresent(refund.getRefundTime(), refund.getCreatedAt()); if (inRefundDateRange(refundTime, startDate, endDate)) { records.add(toLedger(order, true, settings, guides.get(order.getGuideId()), teams, refund)); } } } records.sort((left, right) -> { LocalDateTime leftTime = left.getFlowTime(); LocalDateTime rightTime = right.getFlowTime(); if (leftTime == null && rightTime != null) return 1; if (leftTime != null && rightTime == null) return -1; if (leftTime != null && rightTime != null) { int timeCompare = rightTime.compareTo(leftTime); if (timeCompare != 0) return timeCompare; } Long leftId = left.getId(); Long rightId = right.getId(); if (leftId == null && rightId != null) return 1; if (leftId != null && rightId == null) return -1; return leftId == null || rightId == null ? 0 : rightId.compareTo(leftId); }); BigDecimal totalAmount = records.stream() .map(FinanceLedgerResponse::getFlowAmount) .map(this::safeAmount) .reduce(BigDecimal.ZERO, BigDecimal::add) .setScale(2, RoundingMode.HALF_UP); long normalizedPage = Math.max(1, page); long normalizedSize = Math.max(1, size); long fromLong = (normalizedPage - 1) * normalizedSize; int from = fromLong >= records.size() ? records.size() : (int) fromLong; long toLong = Math.min((long) records.size(), fromLong + normalizedSize); int to = (int) Math.max(from, toLong); return new FinanceLedgerPageResponse( new PageResponse(records.subList(from, to), records.size(), normalizedPage, normalizedSize), totalAmount); } public PageResponse withdrawals(long page, long size, String keyword, String status, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapper query = new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { String value = keyword.trim(); query.and(wrapper -> wrapper.like(GuideWithdrawal::getWithdrawalNo, value) .or().like(GuideWithdrawal::getGuideName, value) .or().like(GuideWithdrawal::getGuideMobile, value)); } query.eq(StringUtils.hasText(status), GuideWithdrawal::getStatus, status) .ge(startDate != null, GuideWithdrawal::getAppliedAt, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, GuideWithdrawal::getAppliedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay()) .orderByDesc(GuideWithdrawal::getId); Page result = withdrawalMapper.selectPage(new Page(page, size), query); return new PageResponse(result.getRecords().stream() .map(WithdrawalResponse::new).collect(Collectors.toList()), result.getTotal(), result.getCurrent(), result.getSize()); } @Transactional public WithdrawalResponse processWithdrawal(Long id, WithdrawalProcessRequest request, String processor) { return guideWithdrawalService.process(id, request.getStatus(), request.getAuditRemark(), processor); } private LambdaQueryWrapper ledgerQuery(boolean refund, String keyword, String paymentMethod, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapper query = new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { String value = keyword.trim(); query.and(wrapper -> wrapper.like(TourOrder::getOrderNo, value) .or().like(TourOrder::getPaymentOrderNo, value) .or().like(TourOrder::getRefundOrderNo, value)); } if (refund) { query.isNotNull(TourOrder::getRefundTime) .ge(startDate != null, TourOrder::getRefundTime, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, TourOrder::getRefundTime, endDate == null ? null : endDate.plusDays(1).atStartOfDay()) .orderByDesc(TourOrder::getRefundTime); } else { query.isNotNull(TourOrder::getPaymentTime) .ge(startDate != null, TourOrder::getPaymentTime, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, TourOrder::getPaymentTime, endDate == null ? null : endDate.plusDays(1).atStartOfDay()) .orderByDesc(TourOrder::getPaymentTime); } query.eq(StringUtils.hasText(paymentMethod), TourOrder::getPaymentMethod, paymentMethod); return query; } private LambdaQueryWrapper refundOrderQuery(String keyword, String paymentMethod) { LambdaQueryWrapper query = new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { String value = keyword.trim(); query.and(wrapper -> wrapper.like(TourOrder::getOrderNo, value) .or().like(TourOrder::getPaymentOrderNo, value) .or().like(TourOrder::getRefundOrderNo, value)); } query.eq(StringUtils.hasText(paymentMethod), TourOrder::getPaymentMethod, paymentMethod); return query; } private Map> loadRefundRecords(List orders) { if (refundMapper == null || orders == null || orders.isEmpty()) { return Collections.emptyMap(); } Set orderIds = orders.stream() .map(TourOrder::getId) .filter(id -> id != null) .collect(Collectors.toSet()); if (orderIds.isEmpty()) { return Collections.emptyMap(); } List records = refundMapper.selectByOrderIds(orderIds); if (records == null || records.isEmpty()) { return Collections.emptyMap(); } return records.stream().collect(Collectors.groupingBy(TourOrderRefund::getOrderId)); } private boolean hasLegacyRefund(TourOrder order) { return order.getRefundAmount() != null || StringUtils.hasText(order.getRefundOrderNo()); } private boolean inRefundDateRange(LocalDateTime value, LocalDate startDate, LocalDate endDate) { if (value == null) return startDate == null && endDate == null; return (startDate == null || !value.isBefore(startDate.atStartOfDay())) && (endDate == null || value.isBefore(endDate.plusDays(1).atStartOfDay())); } private FinanceLedgerResponse toLedger(TourOrder order, boolean refund, PlatformSettingsResponse settings, GuideUser guide, Map teams, TourOrderRefund refundRecord) { BigDecimal amount = refund ? safeAmount(refundRecord == null ? order.getRefundAmount() : refundRecord.getRefundAmount()) : paymentAmount(order); BigDecimal orderAmount = paymentAmount(order); BigDecimal guideIncome; BigDecimal platformIncome; if (order.getGuideEarningAmount() != null) { guideIncome = order.getGuideEarningAmount().setScale(2, RoundingMode.HALF_UP); platformIncome = orderAmount.subtract(guideIncome).setScale(2, RoundingMode.HALF_UP); } else { BigDecimal commissionRate = settings.getCommissionRate().movePointLeft(2); platformIncome = orderAmount.multiply(commissionRate).setScale(2, RoundingMode.HALF_UP); guideIncome = orderAmount.subtract(platformIncome).setScale(2, RoundingMode.HALF_UP); } if (refund) { BigDecimal refundRatio = orderAmount.signum() == 0 ? BigDecimal.ZERO : amount.divide(orderAmount, 10, RoundingMode.HALF_UP); platformIncome = platformIncome.multiply(refundRatio).setScale(2, RoundingMode.HALF_UP).negate(); guideIncome = guideIncome.multiply(refundRatio).setScale(2, RoundingMode.HALF_UP).negate(); amount = amount.negate(); } LocalDateTime settledAt = order.getSettledAt() == null ? order.getSettlementDueAt() : order.getSettledAt(); Long teamId = order.getCommissionTeamId() != null ? order.getCommissionTeamId() : guide == null ? null : guide.getTeamId(); String teamName = teamId == null ? "个人导游" : teams.get(teamId); String flowNo = refund ? refundRecord == null ? order.getRefundOrderNo() : refundRecord.getRefundOrderNo() : "IN" + order.getOrderNo(); String channelFlowNo = refund ? StringUtils.hasText(refundRecord == null ? null : refundRecord.getRefundOrderNo()) ? refundRecord.getRefundOrderNo() : order.getRefundOrderNo() : order.getPaymentOrderNo(); String flowMethod = refund ? StringUtils.hasText(refundRecord == null ? null : refundRecord.getRefundMethod()) ? refundRecord.getRefundMethod() : order.getRefundMethod() : order.getPaymentMethod(); LocalDateTime flowTime = refund ? refundRecord == null ? order.getRefundTime() : firstPresent(refundRecord.getRefundTime(), refundRecord.getCreatedAt()) : order.getPaymentTime(); Long flowId = refund && refundRecord != null && refundRecord.getId() != null ? refundRecord.getId() : order.getId(); return new FinanceLedgerResponse(flowId, flowNo, order.getOrderNo(), paymentAmount(order), platformIncome, guideIncome, guideName(order, guide), guideMobile(order, guide), teamName, channelFlowNo, flowMethod, amount, flowTime, settledAt); } private LocalDateTime firstPresent(LocalDateTime first, LocalDateTime second) { return first == null ? second : first; } private String guideName(TourOrder order, GuideUser guide) { return StringUtils.hasText(order.getGuideName()) ? order.getGuideName() : guide == null ? null : StringUtils.hasText(guide.getRealName()) ? guide.getRealName() : guide.getUsername(); } private String guideMobile(TourOrder order, GuideUser guide) { return StringUtils.hasText(order.getGuideMobile()) ? order.getGuideMobile() : guide == null ? null : guide.getMobile(); } private Map loadGuides(List orders) { Set guideIds = orders.stream().map(TourOrder::getGuideId) .filter(id -> id != null).collect(Collectors.toSet()); if (guideIds.isEmpty()) return new HashMap(); return guideUserMapper.selectBatchIds(guideIds).stream() .collect(Collectors.toMap(GuideUser::getId, guide -> guide)); } private Map loadTeams(List orders, java.util.Collection guides) { Set teamIds = guides.stream().map(GuideUser::getTeamId) .filter(id -> id != null).collect(Collectors.toSet()); orders.stream().map(TourOrder::getCommissionTeamId) .filter(id -> id != null).forEach(teamIds::add); if (teamIds.isEmpty()) return new HashMap(); return guideTeamMapper.selectBatchIds(teamIds).stream() .collect(Collectors.toMap(GuideTeam::getId, GuideTeam::getTeamName)); } private BigDecimal paymentAmount(TourOrder order) { return safeAmount(order.getPaymentAmount() == null ? order.getAmount() : order.getPaymentAmount()); } private BigDecimal safeAmount(BigDecimal amount) { return amount == null ? BigDecimal.ZERO : amount; } }