| | |
| | | |
| | | 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.transaction.annotation.Transactional; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.time.LocalDateTime; |
| | | |
| | | /** 将到期且未分账的订单收益幂等计入导游余额。 */ |
| | |
| | | TourOrder order = orderMapper.selectForUpdate(orderId); |
| | | if (!canSettle(order, settledAt)) return false; |
| | | |
| | | BigDecimal earning = order.getGuideEarningAmount(); |
| | | BigDecimal earning = effectiveEarning(order); |
| | | if (earning.signum() > 0) { |
| | | balanceService.credit(order.getGuideId(), earning); |
| | | 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; |
| | | boolean debited; |
| | | if (order.getCommissionTeamId() != null) { |
| | | debited = balanceService.debitTeamForRefund(order.getCommissionTeamId(), reversal); |
| | | } else if (order.getGuideId() != null) { |
| | | debited = balanceService.debitForRefund(order.getGuideId(), reversal); |
| | | } else { |
| | | return; |
| | | } |
| | | if (!debited) { |
| | | throw new BusinessException(500, "退款后导游收益冲正失败"); |
| | | } |
| | | } |
| | | |
| | | private boolean canSettle(TourOrder order, LocalDateTime now) { |
| | |
| | | && order.getGuideId() != null |
| | | && order.getGuideEarningAmount() != null |
| | | && order.getGuideEarningAmount().signum() >= 0 |
| | | && !RefundStatus.REFUNDED.matches(order.getRefundStatus()); |
| | | && canSettleRefund(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); |
| | | } |
| | | |
| | | private boolean canSettleRefund(String refundStatus) { |
| | | return refundStatus == null |
| | | || refundStatus.trim().isEmpty() |
| | | || RefundStatus.NONE.matches(refundStatus) |
| | | || RefundStatus.PARTIAL.matches(refundStatus) |
| | | || RefundStatus.REFUND_FAILED.matches(refundStatus); |
| | | } |
| | | } |