diff --git a/src/Oms.Application/Dtos/Agents/AgentDtos.cs b/src/Oms.Application/Dtos/Agents/AgentDtos.cs
index 8781ac0..23ad7d3 100644
--- a/src/Oms.Application/Dtos/Agents/AgentDtos.cs
+++ b/src/Oms.Application/Dtos/Agents/AgentDtos.cs
@@ -308,6 +308,12 @@ public class AgentWorkplaceStatsOutput
/// 派送成功运单数(全量状态桶)。
public int DeliverySuccess { get; set; }
+ /// 预计返佣笔数(未锁定)。
+ public int EstimatedCommissionCount { get; set; }
+
+ /// 预计返佣金额(未锁定,不计入应打款)。
+ public decimal EstimatedCommissionAmount { get; set; }
+
/// 待结算返佣笔数。
public int PendingCommissionCount { get; set; }
diff --git a/src/Oms.Application/Messaging/Agents/AgentCommissionJobActionCodes.cs b/src/Oms.Application/Messaging/Agents/AgentCommissionJobActionCodes.cs
new file mode 100644
index 0000000..ca11e1d
--- /dev/null
+++ b/src/Oms.Application/Messaging/Agents/AgentCommissionJobActionCodes.cs
@@ -0,0 +1,14 @@
+namespace Oms.Application.Messaging.Agents;
+
+/// 代理返佣异步任务动作。
+public static class AgentCommissionJobActionCodes
+{
+ /// 下单/计费后写或刷新预计返佣。
+ public const string Estimate = "estimate";
+
+ /// 派送终态锁定为待结算。
+ public const string Finalize = "finalize";
+
+ /// 删单/全额退作废。
+ public const string Void = "void";
+}
diff --git a/src/Oms.Application/Messaging/Agents/AgentCommissionJobEventHandler.cs b/src/Oms.Application/Messaging/Agents/AgentCommissionJobEventHandler.cs
new file mode 100644
index 0000000..8a5866c
--- /dev/null
+++ b/src/Oms.Application/Messaging/Agents/AgentCommissionJobEventHandler.cs
@@ -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;
+
+/// 消费代理返佣异步任务。
+public sealed class AgentCommissionJobEventHandler(
+ IServiceScopeFactory scopeFactory,
+ IUnitOfWorkManager unitOfWorkManager,
+ ILogger logger)
+ : IEventHandler, 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();
+
+ 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);
+ }
+}
diff --git a/src/Oms.Application/Messaging/Agents/AgentCommissionJobIntegrationEvent.cs b/src/Oms.Application/Messaging/Agents/AgentCommissionJobIntegrationEvent.cs
new file mode 100644
index 0000000..a5b168d
--- /dev/null
+++ b/src/Oms.Application/Messaging/Agents/AgentCommissionJobIntegrationEvent.cs
@@ -0,0 +1,21 @@
+using Fake.EventBus.Distributed;
+
+namespace Oms.Application.Messaging.Agents;
+
+///
+/// 代理返佣异步任务(预计 / 锁定 / 作废),走 Fake.EventBus Outbox。
+///
+[Serializable]
+public class AgentCommissionJobIntegrationEvent : IntegrationEvent
+{
+ public int OrderId { get; set; }
+
+ /// 。
+ public string Action { get; set; } = AgentCommissionJobActionCodes.Estimate;
+
+ /// finalize 时的触发运单状态。
+ public string? TriggerOrderStatus { get; set; }
+
+ /// void 时的原因备注。
+ public string? Reason { get; set; }
+}
diff --git a/src/Oms.Application/Messaging/TrackChange/TrackChangeOrderStatusApplier.cs b/src/Oms.Application/Messaging/TrackChange/TrackChangeOrderStatusApplier.cs
index 4a31055..9a973e6 100644
--- a/src/Oms.Application/Messaging/TrackChange/TrackChangeOrderStatusApplier.cs
+++ b/src/Oms.Application/Messaging/TrackChange/TrackChangeOrderStatusApplier.cs
@@ -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 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;
diff --git a/src/Oms.Application/Services/Agents/AgentAdminService.cs b/src/Oms.Application/Services/Agents/AgentAdminService.cs
index 4b1b670..f354267 100644
--- a/src/Oms.Application/Services/Agents/AgentAdminService.cs
+++ b/src/Oms.Application/Services/Agents/AgentAdminService.cs
@@ -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;
}
diff --git a/src/Oms.Application/Services/Agents/AgentPortalService.cs b/src/Oms.Application/Services/Agents/AgentPortalService.cs
index f863acf..de09879 100644
--- a/src/Oms.Application/Services/Agents/AgentPortalService.cs
+++ b/src/Oms.Application/Services/Agents/AgentPortalService.cs
@@ -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") ?? "",
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index e7bfc51..68bd43e 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -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);
}
+ ///
+ /// 代理返佣异步投递:有事务 Outbox 则发事件;否则同步兜底(无 UoW 场景)。
+ ///
+ 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();
+ 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();
+ 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]);
diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs
index b42546b..2a4e99c 100644
--- a/src/Oms.Application/Services/Orders/OrderService.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.cs
@@ -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();
- await agentCommission.TryAccrueOnDeliveryTerminalAsync(order.Id, triggerStatus);
- }
+ private Task ApplyAgentCommissionOnDeliveryTerminalAsync(Order order, string triggerStatus) =>
+ EnqueueAgentCommissionJobAsync(
+ order.Id,
+ Messaging.Agents.AgentCommissionJobActionCodes.Finalize,
+ triggerOrderStatus: triggerStatus);
///
/// 订单退款:全额退且券未过期则退回(新人首单券除外);部分退款不退券。
@@ -376,19 +376,27 @@ public partial class OrderService : ApplicationService
var commissionManager = LazyServiceProvider.GetRequiredService();
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();
- 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();
+ 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: "订单删除作废返佣");
}
///
diff --git a/src/Oms.Application/Shared/AgentCommissionManager.cs b/src/Oms.Application/Shared/AgentCommissionManager.cs
index 6684b59..d330e67 100644
--- a/src/Oms.Application/Shared/AgentCommissionManager.cs
+++ b/src/Oms.Application/Shared/AgentCommissionManager.cs
@@ -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;
///
-/// 渠道代理返佣:派送成功/失败按代理规则写明细(幂等,不入余额)。
+/// 渠道代理返佣:下单写预计 → 派送终态锁定待结算 → 取消/退款作废;不入余额。
///
public class AgentCommissionManager : IScopedDependency
{
@@ -32,6 +34,27 @@ public class AgentCommissionManager : IScopedDependency
_salesProductRepository = salesProductRepository;
}
+ /// 下单后写入/刷新预计返佣(仅 estimated;已锁定/已打款/已作废不改)。
+ 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);
+ }
+
+ /// 预报计费完成后刷新预计金额(仍为 estimated)。
+ public Task TryRefreshEstimateAfterBillingAsync(
+ int orderId,
+ CancellationToken cancellationToken = default) =>
+ TryEstimateOnOrderCreatedAsync(orderId, cancellationToken);
+
+ /// 派送成功/失败:重算锁定为 pending。
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);
+ }
+
+ /// 订单删除 / 全额退款:预计或待结算作废;已打款不动。
+ 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 fees,
- Domain.OrdersAggregate.Entities.Order order)
+ List 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)
{
diff --git a/src/Oms.Domain/AgentsAggregate/Consts/AgentCommissionStatusCodes.cs b/src/Oms.Domain/AgentsAggregate/Consts/AgentCommissionStatusCodes.cs
index 0639032..407ed2c 100644
--- a/src/Oms.Domain/AgentsAggregate/Consts/AgentCommissionStatusCodes.cs
+++ b/src/Oms.Domain/AgentsAggregate/Consts/AgentCommissionStatusCodes.cs
@@ -1,8 +1,31 @@
namespace Oms.Domain.AgentsAggregate.Consts;
-/// 代理返佣明细状态(首版)。
+/// 代理返佣明细结算状态。
public static class AgentCommissionStatusCodes
{
+ /// 下单后预计返佣(金额可随计费刷新,不计入应打款)。
+ public const string Estimated = "estimated";
+
+ /// 派送终态锁定,待运营线下打款。
public const string Pending = "pending";
+
+ /// 已打款。
public const string Paid = "paid";
+
+ /// 订单取消/全额退等作废。
+ 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
+ };
}
diff --git a/src/Oms.Domain/AgentsAggregate/Entities/AgentCommissionDetail.cs b/src/Oms.Domain/AgentsAggregate/Entities/AgentCommissionDetail.cs
index dba8aec..1e8a65d 100644
--- a/src/Oms.Domain/AgentsAggregate/Entities/AgentCommissionDetail.cs
+++ b/src/Oms.Domain/AgentsAggregate/Entities/AgentCommissionDetail.cs
@@ -24,9 +24,9 @@ public class AgentCommissionDetail : OmsAuditedAggregateRoot
public decimal CommissionAmount { get; set; }
- /// pending / paid。
+ /// estimated / pending / paid / void。
[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;