package com.tourguide.modules.finance.service; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.domain.RefundAmountCalculator; import com.tourguide.modules.order.domain.RefundStatus; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.mapper.TourOrderMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; /** 将到期且未分账的订单收益幂等计入导游余额。 */ @Service public class GuideBalanceSettlementService { private final TourOrderMapper orderMapper; private final GuideWithdrawableBalanceService balanceService; public GuideBalanceSettlementService(TourOrderMapper orderMapper, GuideWithdrawableBalanceService balanceService) { this.orderMapper = orderMapper; this.balanceService = balanceService; } @Transactional public boolean settle(Long orderId, LocalDateTime settledAt) { TourOrder order = orderMapper.selectForUpdate(orderId); if (!canSettle(order, settledAt)) return false; BigDecimal earning = effectiveEarning(order); if (earning.signum() > 0) { if (order.getCommissionTeamId() != null) { balanceService.creditTeam(order.getCommissionTeamId(), earning); } else { balanceService.credit(order.getGuideId(), earning); } } order.setSettledAt(settledAt); if (orderMapper.updateById(order) != 1) { throw new BusinessException(500, "订单分账状态更新失败"); } return true; } /** 退款成功后冲正已入账收益;按累计退款差额计算,保证部分退款幂等。 */ @Transactional public void reverseRefund(TourOrder order, BigDecimal previousRefundedAmount, BigDecimal currentRefundedAmount) { if (order == null || order.getSettledAt() == null || order.getGuideEarningAmount() == null) return; BigDecimal paidAmount = RefundAmountCalculator.paidAmount(order); if (paidAmount.signum() <= 0) return; BigDecimal previous = nonNegative(previousRefundedAmount); BigDecimal current = nonNegative(currentRefundedAmount).min(paidAmount); BigDecimal previousReversal = proratedEarning(order.getGuideEarningAmount(), previous, paidAmount); BigDecimal currentReversal = proratedEarning(order.getGuideEarningAmount(), current, paidAmount); BigDecimal reversal = currentReversal.subtract(previousReversal).setScale(2, RoundingMode.HALF_UP); if (reversal.signum() <= 0) return; if (order.getCommissionTeamId() != null) { balanceService.debitTeamForRefund(order.getCommissionTeamId(), reversal); } else if (order.getGuideId() != null) { balanceService.debitForRefund(order.getGuideId(), reversal); } } private boolean canSettle(TourOrder order, LocalDateTime now) { return order != null && OrderStatus.COMPLETED.matches(order.getStatus()) && order.getSettledAt() == null && order.getSettlementDueAt() != null && !order.getSettlementDueAt().isAfter(now) && order.getGuideId() != null && order.getGuideEarningAmount() != null && order.getGuideEarningAmount().signum() >= 0 && !RefundStatus.REFUNDED.matches(order.getRefundStatus()); } private BigDecimal effectiveEarning(TourOrder order) { BigDecimal earning = order.getGuideEarningAmount(); BigDecimal paidAmount = RefundAmountCalculator.paidAmount(order); if (paidAmount.signum() <= 0) return earning; BigDecimal refunded = RefundAmountCalculator.refundedAmount(order).min(paidAmount); return earning.multiply(paidAmount.subtract(refunded)) .divide(paidAmount, 2, RoundingMode.HALF_UP); } private BigDecimal proratedEarning(BigDecimal earning, BigDecimal refunded, BigDecimal paidAmount) { return earning.multiply(refunded).divide(paidAmount, 2, RoundingMode.HALF_UP); } private BigDecimal nonNegative(BigDecimal value) { return value == null ? BigDecimal.ZERO : value.max(BigDecimal.ZERO); } }