using Fake.DependencyInjection; using Fake.Application; using Fake.Domain.Exceptions; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.MarketingAggregate.Entities; using Oms.Domain.MarketingAggregate.Enums; using Oms.Domain.MarketingAggregate.Repositories; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; using System.Text.Json; namespace Oms.Application.Shared; /// /// 引荐分成:派送成功按实付基础运费百分比计奖,T+N 工作日入运费余额。 /// public class ReferralCommissionManager : IScopedDependency { private readonly IReferralCommissionRuleRepository _ruleRepository; private readonly IReferralCommissionDetailRepository _detailRepository; private readonly ICustomerRepository _customerRepository; private readonly IMemberLevelRepository _memberLevelRepository; private readonly IOrderRepository _orderRepository; private readonly CustomerBalanceMutator _balanceMutator; public ReferralCommissionManager( IReferralCommissionRuleRepository ruleRepository, IReferralCommissionDetailRepository detailRepository, ICustomerRepository customerRepository, IMemberLevelRepository memberLevelRepository, IOrderRepository orderRepository, CustomerBalanceMutator balanceMutator) { _ruleRepository = ruleRepository; _detailRepository = detailRepository; _customerRepository = customerRepository; _memberLevelRepository = memberLevelRepository; _orderRepository = orderRepository; _balanceMutator = balanceMutator; } public async Task OnOrderDeliverySuccessAsync(int orderId, CancellationToken cancellationToken = default) { var order = await _orderRepository.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken) ?? throw new BusinessException("订单不存在"); if (!string.Equals(order.OrderStatus, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase)) { throw new BusinessException("订单未达到派送成功状态"); } if (order.IsRefunded > 0 || !order.CustomerId.HasValue) { return; } var sessionCustomer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value, cancellationToken); if (sessionCustomer == null) { return; } var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync( _customerRepository, sessionCustomer, cancellationToken); if (!mainCustomer.ReferrerCustomerId.HasValue || mainCustomer.ReferrerCustomerId.Value <= 0 || mainCustomer.ReferrerCustomerId.Value == mainCustomer.Id) { return; } var referrerId = mainCustomer.ReferrerCustomerId.Value; var existing = await _detailRepository.GetByOrderAndReferrerAsync(orderId, referrerId, cancellationToken); if (existing != null) { return; } var rule = await _ruleRepository.GetActiveRuleAsync(cancellationToken); if (rule == null) { return; } var referrer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == referrerId, cancellationToken); if (referrer == null) { return; } var referrerLevel = referrer.MemberLevelId.HasValue ? await _memberLevelRepository.FirstOrDefaultAsync(x => x.Id == referrer.MemberLevelId.Value, cancellationToken) : null; if (!MarketingInviteRewardCalculator.CanInvite(referrerLevel?.LevelCode, rule.MinInviteLevelCode)) { return; } var familyIds = await ResolveFamilyCustomerIdsAsync(mainCustomer.Id, cancellationToken); var completedOrders = await _orderRepository.GetListAsync( x => x.CustomerId != null && familyIds.Contains(x.CustomerId.Value) && x.OrderStatus == OrderStatusCodes.DeliverySuccess && x.IsRefunded == 0); var ordered = completedOrders .OrderBy(x => x.SignTime ?? x.PayTime ?? x.CreateTime) .ThenBy(x => x.Id) .ToList(); var orderIndex = ordered.FindIndex(x => x.Id == order.Id) + 1; if (orderIndex <= 0) { orderIndex = ordered.Count + 1; } var fees = await _orderRepository.GetFeesByOrderIdsAsync([order.Id]); var paidBaseFreight = MarketingFeeHelper.ResolvePaidBaseFreight( MarketingFeeHelper.SumReceivableBaseFreight(fees), order.LevelDiscountAmount, order.CouponDiscountAmount); var snapshot = ToRewardSnapshot(rule); var completeTime = order.SignTime ?? DateTime.Now; var decision = MarketingInviteRewardCalculator.Resolve( paidBaseFreight, orderIndex, mainCustomer.CreateTime, completeTime, snapshot); if (!decision.Eligible || decision.Amount <= 0) { return; } var settleDue = MarketingInviteRewardCalculator.AddBusinessDays(completeTime, snapshot.SettleBusinessDays); await _detailRepository.InsertAsync(new ReferralCommissionDetail { ReferrerCustomerId = referrerId, RefereeCustomerId = mainCustomer.Id, OrderId = order.Id, OrderNo = order.ServiceNo, BasePayableAmount = paidBaseFreight, CommissionAmount = decision.Amount, Status = CommissionStatus.Pending, EligibleTime = completeTime, SettleDueTime = settleDue, RewardOrderIndex = decision.OrderIndex, RuleSnapshotJson = JsonSerializer.Serialize(snapshot) }, cancellationToken); } public async Task SettlePendingAsync(int batchSize, CancellationToken cancellationToken = default) { var pendingList = await _detailRepository.GetPendingSettleAsync(DateTime.Now, batchSize, cancellationToken); foreach (var detail in pendingList) { if (!IsDue(detail, DateTime.Now)) { continue; } var referrer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == detail.ReferrerCustomerId, cancellationToken); if (referrer == null) { continue; } await _balanceMutator.CreditReferralRewardAsync( referrer, detail.CommissionAmount, detail.Id, detail.OrderNo, $"邀请奖励 {detail.OrderNo}", cancellationToken); detail.Status = CommissionStatus.Settled; detail.SettleTime = DateTime.Now; await _detailRepository.UpdateAsync(detail, cancellationToken); } } public async Task ClawbackOnOrderRefundAsync(Order order, CancellationToken cancellationToken = default) { var detail = await _detailRepository.GetByOrderIdAsync(order.Id, cancellationToken); if (detail == null || detail.Status == CommissionStatus.Cancelled) { return; } if (detail.Status == CommissionStatus.Pending) { detail.Status = CommissionStatus.Cancelled; detail.CancelReason = "订单退款扣回"; await _detailRepository.UpdateAsync(detail, cancellationToken); return; } if (detail.Status == CommissionStatus.Settled) { var referrer = await _customerRepository.FirstOrDefaultAsync(x => x.Id == detail.ReferrerCustomerId, cancellationToken); if (referrer != null) { await _balanceMutator.DebitReferralClawbackAsync( referrer, detail.CommissionAmount, detail.Id, detail.OrderNo, $"邀请奖励退款扣回 {detail.OrderNo}", cancellationToken); } detail.Status = CommissionStatus.Cancelled; detail.CancelReason = "订单退款扣回"; await _detailRepository.UpdateAsync(detail, cancellationToken); } } private async Task ResolveFamilyCustomerIdsAsync(int mainCustomerId, CancellationToken cancellationToken) { var children = await _customerRepository.GetListAsync(x => x.ParentCustomerId == mainCustomerId); return children.Select(x => x.Id).Append(mainCustomerId).ToArray(); } private static bool IsDue(ReferralCommissionDetail detail, DateTime now) { if (detail.SettleDueTime.HasValue) { return detail.SettleDueTime.Value <= now; } if (detail.EligibleTime.HasValue) { var freezeDays = 7; try { if (!string.IsNullOrWhiteSpace(detail.RuleSnapshotJson)) { using var doc = JsonDocument.Parse(detail.RuleSnapshotJson); if (doc.RootElement.TryGetProperty("SettleBusinessDays", out var days)) { freezeDays = days.GetInt32(); } else if (doc.RootElement.TryGetProperty("FreezeDays", out var oldDays)) { freezeDays = oldDays.GetInt32(); } } } catch { // ignore } return MarketingInviteRewardCalculator.AddBusinessDays(detail.EligibleTime.Value, freezeDays) <= now; } return false; } private static InviteRewardRuleSnapshot ToRewardSnapshot(ReferralCommissionRule rule) => new() { MinPaidBaseFreight = rule.MinPaidBaseFreight > 0 ? rule.MinPaidBaseFreight : 50M, FirstOrderRate = rule.FirstOrderRate > 0 ? rule.FirstOrderRate : 0.05M, FirstOrderCap = rule.FirstOrderCap > 0 ? rule.FirstOrderCap : 50M, FollowOrderRate = rule.FollowOrderRate > 0 ? rule.FollowOrderRate : 0.03M, FollowOrderCap = rule.FollowOrderCap > 0 ? rule.FollowOrderCap : 30M, FollowWindowDays = rule.FollowWindowDays > 0 ? rule.FollowWindowDays : 90, MaxRewardOrders = rule.MaxRewardOrders > 0 ? rule.MaxRewardOrders : 6, SettleBusinessDays = rule.SettleBusinessDays > 0 ? rule.SettleBusinessDays : rule.FreezeDays, MinInviteLevelCode = string.IsNullOrWhiteSpace(rule.MinInviteLevelCode) ? "V1" : rule.MinInviteLevelCode }; }