20261030 #18
@ -308,6 +308,12 @@ public class AgentWorkplaceStatsOutput
|
||||
/// <summary>派送成功运单数(全量状态桶)。</summary>
|
||||
public int DeliverySuccess { get; set; }
|
||||
|
||||
/// <summary>预计返佣笔数(未锁定)。</summary>
|
||||
public int EstimatedCommissionCount { get; set; }
|
||||
|
||||
/// <summary>预计返佣金额(未锁定,不计入应打款)。</summary>
|
||||
public decimal EstimatedCommissionAmount { get; set; }
|
||||
|
||||
/// <summary>待结算返佣笔数。</summary>
|
||||
public int PendingCommissionCount { get; set; }
|
||||
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
namespace Oms.Application.Messaging.Agents;
|
||||
|
||||
/// <summary>代理返佣异步任务动作。</summary>
|
||||
public static class AgentCommissionJobActionCodes
|
||||
{
|
||||
/// <summary>下单/计费后写或刷新预计返佣。</summary>
|
||||
public const string Estimate = "estimate";
|
||||
|
||||
/// <summary>派送终态锁定为待结算。</summary>
|
||||
public const string Finalize = "finalize";
|
||||
|
||||
/// <summary>删单/全额退作废。</summary>
|
||||
public const string Void = "void";
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Shared;
|
||||
|
||||
namespace Oms.Application.Messaging.Agents;
|
||||
|
||||
/// <summary>消费代理返佣异步任务。</summary>
|
||||
public sealed class AgentCommissionJobEventHandler(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<AgentCommissionJobEventHandler> logger)
|
||||
: IEventHandler<AgentCommissionJobIntegrationEvent>, ITransientDependency
|
||||
{
|
||||
public async Task HandleAsync(
|
||||
AgentCommissionJobIntegrationEvent @event,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (@event.OrderId <= 0 || string.IsNullOrWhiteSpace(@event.Action))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"AgentCommission job skipped invalid payload OrderId={OrderId} Action={Action}",
|
||||
@event.OrderId,
|
||||
@event.Action);
|
||||
return;
|
||||
}
|
||||
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var manager = scope.ServiceProvider.GetRequiredService<AgentCommissionManager>();
|
||||
|
||||
try
|
||||
{
|
||||
var action = @event.Action.Trim();
|
||||
if (string.Equals(action, AgentCommissionJobActionCodes.Estimate, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await manager.TryEstimateOnOrderCreatedAsync(@event.OrderId, cancellationToken);
|
||||
}
|
||||
else if (string.Equals(action, AgentCommissionJobActionCodes.Finalize, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await manager.TryAccrueOnDeliveryTerminalAsync(
|
||||
@event.OrderId,
|
||||
@event.TriggerOrderStatus ?? string.Empty,
|
||||
cancellationToken);
|
||||
}
|
||||
else if (string.Equals(action, AgentCommissionJobActionCodes.Void, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await manager.TryVoidOnOrderCancelledAsync(
|
||||
@event.OrderId,
|
||||
@event.Reason ?? "异步作废返佣",
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning(
|
||||
"AgentCommission job unknown Action={Action} OrderId={OrderId}",
|
||||
@event.Action,
|
||||
@event.OrderId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 失败 Ack,避免打满队列;明细可后续补偿
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"AgentCommission job failed OrderId={OrderId} Action={Action}",
|
||||
@event.OrderId,
|
||||
@event.Action);
|
||||
}
|
||||
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
using Fake.EventBus.Distributed;
|
||||
|
||||
namespace Oms.Application.Messaging.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// 代理返佣异步任务(预计 / 锁定 / 作废),走 Fake.EventBus Outbox。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AgentCommissionJobIntegrationEvent : IntegrationEvent
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary><see cref="AgentCommissionJobActionCodes"/>。</summary>
|
||||
public string Action { get; set; } = AgentCommissionJobActionCodes.Estimate;
|
||||
|
||||
/// <summary>finalize 时的触发运单状态。</summary>
|
||||
public string? TriggerOrderStatus { get; set; }
|
||||
|
||||
/// <summary>void 时的原因备注。</summary>
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus.Distributed;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Messaging.Agents;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
@ -16,7 +18,7 @@ public sealed class TrackChangeOrderStatusApplier(
|
||||
IOrderRepository orderRepository,
|
||||
OrderOperationLogWriter operationLogWriter,
|
||||
ReferralCommissionManager referralCommissionManager,
|
||||
AgentCommissionManager agentCommissionManager,
|
||||
IDistributedEventBus eventBus,
|
||||
MarketingCouponManager marketingCouponManager,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<TrackChangeOrderStatusApplier> logger) : ITrackChangeOrderStatusApplier, IScopedDependency
|
||||
@ -133,17 +135,21 @@ public sealed class TrackChangeOrderStatusApplier(
|
||||
// 重新加载,避免营销侧依赖最新字段
|
||||
order = await orderRepository.GetByServiceNoAsync(orderNo, cancellationToken) ?? order;
|
||||
await marketingCouponManager.OnFirstDeliverySuccessAsync(order, cancellationToken);
|
||||
await agentCommissionManager.TryAccrueOnDeliveryTerminalAsync(
|
||||
order.Id,
|
||||
OrderStatusCodes.DeliverySuccess,
|
||||
cancellationToken);
|
||||
await eventBus.PublishAsync(new AgentCommissionJobIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
Action = AgentCommissionJobActionCodes.Finalize,
|
||||
TriggerOrderStatus = OrderStatusCodes.DeliverySuccess
|
||||
});
|
||||
}
|
||||
else if (string.Equals(targetStatus, OrderStatusCodes.DeliveryFailed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await agentCommissionManager.TryAccrueOnDeliveryTerminalAsync(
|
||||
order.Id,
|
||||
OrderStatusCodes.DeliveryFailed,
|
||||
cancellationToken);
|
||||
await eventBus.PublishAsync(new AgentCommissionJobIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
Action = AgentCommissionJobActionCodes.Finalize,
|
||||
TriggerOrderStatus = OrderStatusCodes.DeliveryFailed
|
||||
});
|
||||
}
|
||||
|
||||
order = await orderRepository.GetByServiceNoAsync(orderNo, cancellationToken) ?? order;
|
||||
|
||||
@ -254,7 +254,8 @@ public class AgentAdminService : ApplicationService
|
||||
var remark = NormalizeOptional(input.Remark);
|
||||
foreach (var item in list)
|
||||
{
|
||||
if (string.Equals(item.Status, AgentCommissionStatusCodes.Paid, StringComparison.OrdinalIgnoreCase))
|
||||
// 仅「待结算」可标记打款;预计/作废/已打款跳过
|
||||
if (!string.Equals(item.Status, AgentCommissionStatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -202,6 +202,8 @@ public class AgentPortalService : ApplicationService
|
||||
&& x.AgentBindTime >= weekStart
|
||||
&& x.AgentBindTime < tomorrow);
|
||||
|
||||
var estimatedDetails = await DetailRepository.GetListAsync(x =>
|
||||
x.AgentId == agent.Id && x.Status == AgentCommissionStatusCodes.Estimated);
|
||||
var pendingDetails = await DetailRepository.GetListAsync(x =>
|
||||
x.AgentId == agent.Id && x.Status == AgentCommissionStatusCodes.Pending);
|
||||
var paidDetails = await DetailRepository.GetListAsync(x =>
|
||||
@ -218,6 +220,8 @@ public class AgentPortalService : ApplicationService
|
||||
Problematic = GetCount(OrderStatusCodes.Problematic),
|
||||
DeliverySuccess = GetCount(OrderStatusCodes.DeliverySuccess),
|
||||
DeliveryFailed = GetCount(OrderStatusCodes.DeliveryFailed),
|
||||
EstimatedCommissionCount = estimatedDetails.Count,
|
||||
EstimatedCommissionAmount = estimatedDetails.Sum(x => x.CommissionAmount),
|
||||
PendingCommissionCount = pendingDetails.Count,
|
||||
PendingCommissionAmount = pendingDetails.Sum(x => x.CommissionAmount),
|
||||
PaidCommissionAmount = paidDetails.Sum(x => x.CommissionAmount),
|
||||
@ -315,9 +319,7 @@ public class AgentPortalService : ApplicationService
|
||||
基数金额 = x.BaseAmount,
|
||||
返佣比例 = x.CommissionRate,
|
||||
返佣金额 = x.CommissionAmount,
|
||||
状态 = string.Equals(x.Status, AgentCommissionStatusCodes.Paid, StringComparison.OrdinalIgnoreCase)
|
||||
? "已打款"
|
||||
: "待结算",
|
||||
状态 = AgentCommissionStatusCodes.DisplayName(x.Status),
|
||||
触发状态 = x.TriggerOrderStatus,
|
||||
计提时间 = x.CreateTime.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
打款时间 = x.PaidTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? "",
|
||||
|
||||
@ -186,6 +186,9 @@ public partial class OrderService
|
||||
createContext);
|
||||
order.AgentId = orderAgentId;
|
||||
order = await OrderRepository.InsertAsync(order);
|
||||
await EnqueueAgentCommissionJobAsync(
|
||||
order.Id,
|
||||
Messaging.Agents.AgentCommissionJobActionCodes.Estimate);
|
||||
|
||||
var weights = BuildInitialWeightRows(order.Id, input);
|
||||
await SaveParcelChildrenAsync(order, input, weights, country);
|
||||
@ -2955,6 +2958,9 @@ public partial class OrderService
|
||||
order.PredeclareFailReason = string.Empty;
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
await EnqueueAgentCommissionJobAsync(
|
||||
order.Id,
|
||||
Messaging.Agents.AgentCommissionJobActionCodes.Estimate);
|
||||
|
||||
// 轨迹订单依赖排货后的渠道/ApiCode,只有排货成功才创建。
|
||||
// 消费端没有 HttpContext,须用创单时捕获的 Authorization,否则轨迹网关返回 401。
|
||||
@ -3522,6 +3528,55 @@ public partial class OrderService
|
||||
agentRepository);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 代理返佣异步投递:有事务 Outbox 则发事件;否则同步兜底(无 UoW 场景)。
|
||||
/// </summary>
|
||||
private async Task EnqueueAgentCommissionJobAsync(
|
||||
int orderId,
|
||||
string action,
|
||||
string? triggerOrderStatus = null,
|
||||
string? reason = null)
|
||||
{
|
||||
if (orderId <= 0 || string.IsNullOrWhiteSpace(action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var uowManager = LazyServiceProvider.GetRequiredService<IUnitOfWorkManager>();
|
||||
if (OmsOutboxUnitOfWork.IsAmbientTransactional(uowManager))
|
||||
{
|
||||
await EventBus.PublishAsync(new Messaging.Agents.AgentCommissionJobIntegrationEvent
|
||||
{
|
||||
OrderId = orderId,
|
||||
Action = action.Trim(),
|
||||
TriggerOrderStatus = triggerOrderStatus,
|
||||
Reason = reason
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mgr = LazyServiceProvider.GetRequiredService<AgentCommissionManager>();
|
||||
if (string.Equals(action, Messaging.Agents.AgentCommissionJobActionCodes.Estimate, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await mgr.TryEstimateOnOrderCreatedAsync(orderId);
|
||||
}
|
||||
else if (string.Equals(action, Messaging.Agents.AgentCommissionJobActionCodes.Finalize, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await mgr.TryAccrueOnDeliveryTerminalAsync(orderId, triggerOrderStatus ?? string.Empty);
|
||||
}
|
||||
else if (string.Equals(action, Messaging.Agents.AgentCommissionJobActionCodes.Void, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await mgr.TryVoidOnOrderCancelledAsync(orderId, reason ?? "同步作废返佣");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 返佣失败不阻断主业务
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyMarketingSnapshotOnCreateAsync(Order order, int? couponInstanceId)
|
||||
{
|
||||
var fees = await OrderRepository.GetFeesByOrderIdsAsync([order.Id]);
|
||||
|
||||
@ -349,11 +349,11 @@ public partial class OrderService : ApplicationService
|
||||
await ApplyAgentCommissionOnDeliveryTerminalAsync(order, OrderStatusCodes.DeliverySuccess);
|
||||
}
|
||||
|
||||
private async Task ApplyAgentCommissionOnDeliveryTerminalAsync(Order order, string triggerStatus)
|
||||
{
|
||||
var agentCommission = LazyServiceProvider.GetRequiredService<AgentCommissionManager>();
|
||||
await agentCommission.TryAccrueOnDeliveryTerminalAsync(order.Id, triggerStatus);
|
||||
}
|
||||
private Task ApplyAgentCommissionOnDeliveryTerminalAsync(Order order, string triggerStatus) =>
|
||||
EnqueueAgentCommissionJobAsync(
|
||||
order.Id,
|
||||
Messaging.Agents.AgentCommissionJobActionCodes.Finalize,
|
||||
triggerOrderStatus: triggerStatus);
|
||||
|
||||
/// <summary>
|
||||
/// 订单退款:全额退且券未过期则退回(新人首单券除外);部分退款不退券。
|
||||
@ -376,19 +376,27 @@ public partial class OrderService : ApplicationService
|
||||
var commissionManager = LazyServiceProvider.GetRequiredService<ReferralCommissionManager>();
|
||||
await commissionManager.ClawbackOnOrderRefundAsync(order);
|
||||
|
||||
if (input.IsFullRefund && order.CustomerId.HasValue)
|
||||
if (input.IsFullRefund)
|
||||
{
|
||||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value);
|
||||
if (customer != null)
|
||||
await EnqueueAgentCommissionJobAsync(
|
||||
order.Id,
|
||||
Messaging.Agents.AgentCommissionJobActionCodes.Void,
|
||||
reason: "订单全额退款作废返佣");
|
||||
|
||||
if (order.CustomerId.HasValue)
|
||||
{
|
||||
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(
|
||||
CustomerRepository, customer);
|
||||
var pointsManager = LazyServiceProvider.GetRequiredService<MarketingPointsManager>();
|
||||
await pointsManager.ClawbackOnOrderAsync(
|
||||
mainCustomer.Id,
|
||||
order.Id,
|
||||
order.ServiceNo,
|
||||
"订单退款扣回积分");
|
||||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value);
|
||||
if (customer != null)
|
||||
{
|
||||
var mainCustomer = await MarketingCustomerHelper.ResolveMarketingMainCustomerAsync(
|
||||
CustomerRepository, customer);
|
||||
var pointsManager = LazyServiceProvider.GetRequiredService<MarketingPointsManager>();
|
||||
await pointsManager.ClawbackOnOrderAsync(
|
||||
mainCustomer.Id,
|
||||
order.Id,
|
||||
order.ServiceNo,
|
||||
"订单退款扣回积分");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -477,6 +485,10 @@ public partial class OrderService : ApplicationService
|
||||
}
|
||||
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
await EnqueueAgentCommissionJobAsync(
|
||||
order.Id,
|
||||
Messaging.Agents.AgentCommissionJobActionCodes.Void,
|
||||
reason: "订单删除作废返佣");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -4,14 +4,16 @@ using Oms.Domain.AgentsAggregate.Entities;
|
||||
using Oms.Domain.AgentsAggregate.Repositories;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
using Oms.Domain.Shared.Enums;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 渠道代理返佣:派送成功/失败按代理规则写明细(幂等,不入余额)。
|
||||
/// 渠道代理返佣:下单写预计 → 派送终态锁定待结算 → 取消/退款作废;不入余额。
|
||||
/// </summary>
|
||||
public class AgentCommissionManager : IScopedDependency
|
||||
{
|
||||
@ -32,6 +34,27 @@ public class AgentCommissionManager : IScopedDependency
|
||||
_salesProductRepository = salesProductRepository;
|
||||
}
|
||||
|
||||
/// <summary>下单后写入/刷新预计返佣(仅 estimated;已锁定/已打款/已作废不改)。</summary>
|
||||
public async Task TryEstimateOnOrderCreatedAsync(
|
||||
int orderId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var order = await _orderRepository.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await UpsertEstimatedAsync(order, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>预报计费完成后刷新预计金额(仍为 estimated)。</summary>
|
||||
public Task TryRefreshEstimateAfterBillingAsync(
|
||||
int orderId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
TryEstimateOnOrderCreatedAsync(orderId, cancellationToken);
|
||||
|
||||
/// <summary>派送成功/失败:重算锁定为 pending。</summary>
|
||||
public async Task TryAccrueOnDeliveryTerminalAsync(
|
||||
int orderId,
|
||||
string triggerOrderStatus,
|
||||
@ -42,12 +65,6 @@ public class AgentCommissionManager : IScopedDependency
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = await _detailRepository.GetByOrderIdAsync(orderId, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var order = await _orderRepository.FirstOrDefaultAsync(x => x.Id == orderId, cancellationToken);
|
||||
if (order == null || !order.AgentId.HasValue || order.AgentId.Value <= 0 || !order.CustomerId.HasValue)
|
||||
{
|
||||
@ -59,44 +76,50 @@ public class AgentCommissionManager : IScopedDependency
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = await _detailRepository.GetByOrderIdAsync(orderId, cancellationToken);
|
||||
if (existing != null
|
||||
&& (string.Equals(existing.Status, AgentCommissionStatusCodes.Paid, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(existing.Status, AgentCommissionStatusCodes.Pending, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// pending 已锁定;paid 不回写
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing != null
|
||||
&& string.Equals(existing.Status, AgentCommissionStatusCodes.Void, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var agent = await _agentRepository.FirstOrDefaultAsync(x => x.Id == order.AgentId.Value, cancellationToken);
|
||||
if (agent == null || agent.Status != Domain.Shared.Enums.EnableStatus.Enabled)
|
||||
if (agent == null || agent.Status != EnableStatus.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AgentCommissionBaseTypes.IsSupported(agent.BaseType))
|
||||
var built = await TryBuildAmountsAsync(agent, order, cancellationToken);
|
||||
if (built == null)
|
||||
{
|
||||
// 终态仍算不出金额:若已有预计行则作废,避免脏数据进应结
|
||||
if (existing != null
|
||||
&& string.Equals(existing.Status, AgentCommissionStatusCodes.Estimated, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
existing.Status = AgentCommissionStatusCodes.Void;
|
||||
existing.TriggerOrderStatus = triggerOrderStatus.Trim();
|
||||
existing.Remark = AppendRemark(existing.Remark, "终态基数为0,自动作废");
|
||||
await _detailRepository.UpdateAsync(existing, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (agent.CommissionRate <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int? salesProductId = null;
|
||||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
var product = await _salesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
salesProductId = product?.Id;
|
||||
}
|
||||
|
||||
if (!IsProductAllowed(agent.SalesProductIds, salesProductId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fees = await _orderRepository.GetFeesByOrderIdsAsync([order.Id]);
|
||||
var baseAmount = ResolveBaseAmount(agent.BaseType, fees, order);
|
||||
if (baseAmount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var amount = decimal.Round(baseAmount * agent.CommissionRate, 4, MidpointRounding.AwayFromZero);
|
||||
if (amount <= 0)
|
||||
if (existing != null)
|
||||
{
|
||||
ApplyAmounts(existing, agent, built.Value.SalesProductId, built.Value.BaseAmount, built.Value.Amount);
|
||||
existing.Status = AgentCommissionStatusCodes.Pending;
|
||||
existing.TriggerOrderStatus = triggerOrderStatus.Trim();
|
||||
existing.Remark = AppendRemark(existing.Remark, "派送终态锁定");
|
||||
await _detailRepository.UpdateAsync(existing, cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -104,6 +127,132 @@ public class AgentCommissionManager : IScopedDependency
|
||||
existing = await _detailRepository.GetByOrderIdAsync(orderId, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
if (string.Equals(existing.Status, AgentCommissionStatusCodes.Estimated, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ApplyAmounts(existing, agent, built.Value.SalesProductId, built.Value.BaseAmount, built.Value.Amount);
|
||||
existing.Status = AgentCommissionStatusCodes.Pending;
|
||||
existing.TriggerOrderStatus = triggerOrderStatus.Trim();
|
||||
await _detailRepository.UpdateAsync(existing, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await _detailRepository.InsertAsync(new AgentCommissionDetail
|
||||
{
|
||||
AgentId = agent.Id,
|
||||
OrderId = order.Id,
|
||||
CustomerId = order.CustomerId.Value,
|
||||
SalesProductId = built.Value.SalesProductId,
|
||||
BaseType = agent.BaseType.Trim().ToLowerInvariant(),
|
||||
BaseAmount = built.Value.BaseAmount,
|
||||
CommissionRate = agent.CommissionRate,
|
||||
CommissionAmount = built.Value.Amount,
|
||||
Status = AgentCommissionStatusCodes.Pending,
|
||||
TriggerOrderStatus = triggerOrderStatus.Trim()
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>订单删除 / 全额退款:预计或待结算作废;已打款不动。</summary>
|
||||
public async Task TryVoidOnOrderCancelledAsync(
|
||||
int orderId,
|
||||
string reason,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await _detailRepository.GetByOrderIdAsync(orderId, cancellationToken);
|
||||
if (existing == null || !AgentCommissionStatusCodes.IsOpen(existing.Status))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
existing.Status = AgentCommissionStatusCodes.Void;
|
||||
existing.Remark = AppendRemark(existing.Remark, reason);
|
||||
await _detailRepository.UpdateAsync(existing, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task UpsertEstimatedAsync(Order order, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!order.AgentId.HasValue || order.AgentId.Value <= 0 || !order.CustomerId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (order.IsRefunded > 0
|
||||
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var existing = await _detailRepository.GetByOrderIdAsync(order.Id, cancellationToken);
|
||||
if (existing != null
|
||||
&& !string.Equals(existing.Status, AgentCommissionStatusCodes.Estimated, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var agent = await _agentRepository.FirstOrDefaultAsync(x => x.Id == order.AgentId.Value, cancellationToken);
|
||||
if (agent == null || agent.Status != EnableStatus.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var built = await TryBuildAmountsAsync(agent, order, cancellationToken);
|
||||
int? salesProductId;
|
||||
decimal baseAmount;
|
||||
decimal amount;
|
||||
if (built == null)
|
||||
{
|
||||
// 计费未出前也落一条预计(金额 0),方便代理看到「已关联」
|
||||
if (agent.CommissionRate <= 0 || !AgentCommissionBaseTypes.IsSupported(agent.BaseType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int? productId = null;
|
||||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
var product = await _salesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
productId = product?.Id;
|
||||
}
|
||||
|
||||
if (!IsProductAllowed(agent.SalesProductIds, productId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
salesProductId = productId;
|
||||
baseAmount = 0M;
|
||||
amount = 0M;
|
||||
}
|
||||
else
|
||||
{
|
||||
salesProductId = built.Value.SalesProductId;
|
||||
baseAmount = built.Value.BaseAmount;
|
||||
amount = built.Value.Amount;
|
||||
}
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
ApplyAmounts(existing, agent, salesProductId, baseAmount, amount);
|
||||
existing.Status = AgentCommissionStatusCodes.Estimated;
|
||||
if (string.IsNullOrWhiteSpace(existing.TriggerOrderStatus))
|
||||
{
|
||||
existing.TriggerOrderStatus = order.OrderStatus?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
await _detailRepository.UpdateAsync(existing, cancellationToken: cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
existing = await _detailRepository.GetByOrderIdAsync(order.Id, cancellationToken);
|
||||
if (existing != null)
|
||||
{
|
||||
if (string.Equals(existing.Status, AgentCommissionStatusCodes.Estimated, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
ApplyAmounts(existing, agent, salesProductId, baseAmount, amount);
|
||||
await _detailRepository.UpdateAsync(existing, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@ -117,19 +266,92 @@ public class AgentCommissionManager : IScopedDependency
|
||||
BaseAmount = baseAmount,
|
||||
CommissionRate = agent.CommissionRate,
|
||||
CommissionAmount = amount,
|
||||
Status = AgentCommissionStatusCodes.Pending,
|
||||
TriggerOrderStatus = triggerOrderStatus.Trim()
|
||||
Status = AgentCommissionStatusCodes.Estimated,
|
||||
TriggerOrderStatus = order.OrderStatus?.Trim() ?? string.Empty
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<(int? SalesProductId, decimal BaseAmount, decimal Amount)?> TryBuildAmountsAsync(
|
||||
Agent agent,
|
||||
Order order,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!AgentCommissionBaseTypes.IsSupported(agent.BaseType) || agent.CommissionRate <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int? salesProductId = null;
|
||||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
var product = await _salesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
salesProductId = product?.Id;
|
||||
}
|
||||
|
||||
if (!IsProductAllowed(agent.SalesProductIds, salesProductId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var fees = await _orderRepository.GetFeesByOrderIdsAsync([order.Id]);
|
||||
var baseAmount = ResolveBaseAmount(agent.BaseType, fees, order);
|
||||
if (baseAmount <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var amount = decimal.Round(baseAmount * agent.CommissionRate, 4, MidpointRounding.AwayFromZero);
|
||||
if (amount <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (salesProductId, baseAmount, amount);
|
||||
}
|
||||
|
||||
private static void ApplyAmounts(
|
||||
AgentCommissionDetail detail,
|
||||
Agent agent,
|
||||
int? salesProductId,
|
||||
decimal baseAmount,
|
||||
decimal amount)
|
||||
{
|
||||
detail.SalesProductId = salesProductId;
|
||||
detail.BaseType = agent.BaseType.Trim().ToLowerInvariant();
|
||||
detail.BaseAmount = baseAmount;
|
||||
detail.CommissionRate = agent.CommissionRate;
|
||||
detail.CommissionAmount = amount;
|
||||
}
|
||||
|
||||
private static string? AppendRemark(string? existing, string note)
|
||||
{
|
||||
var n = note?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(n))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(existing))
|
||||
{
|
||||
return n;
|
||||
}
|
||||
|
||||
if (existing.Contains(n, StringComparison.Ordinal))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
return $"{existing}; {n}";
|
||||
}
|
||||
|
||||
private static bool IsTerminalTrigger(string? status) =>
|
||||
string.Equals(status, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, OrderStatusCodes.DeliveryFailed, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static decimal ResolveBaseAmount(
|
||||
string baseType,
|
||||
List<Domain.OrdersAggregate.Entities.OrderFee> fees,
|
||||
Domain.OrdersAggregate.Entities.Order order)
|
||||
List<OrderFee> fees,
|
||||
Order order)
|
||||
{
|
||||
if (string.Equals(baseType, AgentCommissionBaseTypes.Freight, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@ -139,7 +361,6 @@ public class AgentCommissionManager : IScopedDependency
|
||||
order.CouponDiscountAmount);
|
||||
}
|
||||
|
||||
// receivable:折前应收小计(排除营销抵扣行)
|
||||
var sum = MarketingFeeHelper.SumReceivable(fees);
|
||||
if (sum > 0)
|
||||
{
|
||||
|
||||
@ -1,8 +1,31 @@
|
||||
namespace Oms.Domain.AgentsAggregate.Consts;
|
||||
|
||||
/// <summary>代理返佣明细状态(首版)。</summary>
|
||||
/// <summary>代理返佣明细结算状态。</summary>
|
||||
public static class AgentCommissionStatusCodes
|
||||
{
|
||||
/// <summary>下单后预计返佣(金额可随计费刷新,不计入应打款)。</summary>
|
||||
public const string Estimated = "estimated";
|
||||
|
||||
/// <summary>派送终态锁定,待运营线下打款。</summary>
|
||||
public const string Pending = "pending";
|
||||
|
||||
/// <summary>已打款。</summary>
|
||||
public const string Paid = "paid";
|
||||
|
||||
/// <summary>订单取消/全额退等作废。</summary>
|
||||
public const string Void = "void";
|
||||
|
||||
public static bool IsOpen(string? status) =>
|
||||
string.Equals(status, Estimated, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, Pending, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static string DisplayName(string? status) =>
|
||||
status?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
Estimated => "预计返佣",
|
||||
Pending => "待结算",
|
||||
Paid => "已打款",
|
||||
Void => "已作废",
|
||||
_ => string.IsNullOrWhiteSpace(status) ? "--" : status
|
||||
};
|
||||
}
|
||||
|
||||
@ -24,9 +24,9 @@ public class AgentCommissionDetail : OmsAuditedAggregateRoot
|
||||
|
||||
public decimal CommissionAmount { get; set; }
|
||||
|
||||
/// <summary>pending / paid。</summary>
|
||||
/// <summary>estimated / pending / paid / void。</summary>
|
||||
[SugarColumn(Length = 16)]
|
||||
public string Status { get; set; } = "pending";
|
||||
public string Status { get; set; } = "estimated";
|
||||
|
||||
[SugarColumn(Length = 32)]
|
||||
public string TriggerOrderStatus { get; set; } = string.Empty;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user