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<TourOrder> query = ledgerQuery(refund, keyword, paymentMethod, startDate, endDate);
|
Page<TourOrder> result = orderMapper.selectPage(new Page<TourOrder>(page, size), query);
|
PlatformSettingsResponse settings = settingsService.getSettings();
|
Map<Long, GuideUser> guides = loadGuides(result.getRecords());
|
Map<Long, String> teams = loadTeams(result.getRecords(), guides.values());
|
List<FinanceLedgerResponse> 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<FinanceLedgerResponse>(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<TourOrder> orders = orderMapper.selectList(refundOrderQuery(keyword, paymentMethod));
|
PlatformSettingsResponse settings = settingsService.getSettings();
|
Map<Long, GuideUser> guides = loadGuides(orders);
|
Map<Long, String> teams = loadTeams(orders, guides.values());
|
Map<Long, List<TourOrderRefund>> refundRecords = loadRefundRecords(orders);
|
List<FinanceLedgerResponse> records = new ArrayList<FinanceLedgerResponse>();
|
for (TourOrder order : orders) {
|
List<TourOrderRefund> 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<FinanceLedgerResponse>(records.subList(from, to), records.size(),
|
normalizedPage, normalizedSize),
|
totalAmount);
|
}
|
|
public PageResponse<WithdrawalResponse> withdrawals(long page, long size, String keyword, String status,
|
LocalDate startDate, LocalDate endDate) {
|
LambdaQueryWrapper<GuideWithdrawal> query = new LambdaQueryWrapper<GuideWithdrawal>();
|
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<GuideWithdrawal> result = withdrawalMapper.selectPage(new Page<GuideWithdrawal>(page, size), query);
|
return new PageResponse<WithdrawalResponse>(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<TourOrder> ledgerQuery(boolean refund, String keyword, String paymentMethod,
|
LocalDate startDate, LocalDate endDate) {
|
LambdaQueryWrapper<TourOrder> query = new LambdaQueryWrapper<TourOrder>();
|
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<TourOrder> refundOrderQuery(String keyword, String paymentMethod) {
|
LambdaQueryWrapper<TourOrder> query = new LambdaQueryWrapper<TourOrder>();
|
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<Long, List<TourOrderRefund>> loadRefundRecords(List<TourOrder> orders) {
|
if (refundMapper == null || orders == null || orders.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
Set<Long> orderIds = orders.stream()
|
.map(TourOrder::getId)
|
.filter(id -> id != null)
|
.collect(Collectors.toSet());
|
if (orderIds.isEmpty()) {
|
return Collections.emptyMap();
|
}
|
List<TourOrderRefund> 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<Long, String> 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<Long, GuideUser> loadGuides(List<TourOrder> orders) {
|
Set<Long> guideIds = orders.stream().map(TourOrder::getGuideId)
|
.filter(id -> id != null).collect(Collectors.toSet());
|
if (guideIds.isEmpty()) return new HashMap<Long, GuideUser>();
|
return guideUserMapper.selectBatchIds(guideIds).stream()
|
.collect(Collectors.toMap(GuideUser::getId, guide -> guide));
|
}
|
|
private Map<Long, String> loadTeams(List<TourOrder> orders, java.util.Collection<GuideUser> guides) {
|
Set<Long> 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<Long, String>();
|
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;
|
}
|
}
|