admin/src/Oms.Application/Services/Agents/AgentAdminService.cs
zzh 1d987cde6e
All checks were successful
Build and Deploy / deploy (push) Successful in 1m13s
feat: 新增渠道代理注册归因与返佣明细
独立 Agent 主体与 loginType=2,注册绑定/下单快照,派送成功失败计提明细,不含提现入账。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 17:18:07 +08:00

415 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Linq.Expressions;
using Fake.Application;
using Fake.Domain.Exceptions;
using Fake.Rbac.Application.Dtos.Role;
using Fake.Rbac.Application.Dtos.User;
using Fake.Rbac.Application.Services;
using Fake.Rbac.Infrastructure;
using Fake.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Oms.Application.Dtos.Agents;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.AgentsAggregate.Consts;
using Oms.Domain.AgentsAggregate.Entities;
using Oms.Domain.AgentsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.Shared.Enums;
namespace Oms.Application.Services.Agents;
/// <summary>
/// 渠道代理管理端 API(开户、规则、改绑、返佣明细)。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Agent")]
public class AgentAdminService : ApplicationService
{
public const string AgentRoleCode = "Agent";
private IAgentRepository AgentRepository => LazyServiceProvider.GetRequiredService<IAgentRepository>();
private IAgentCommissionDetailRepository DetailRepository =>
LazyServiceProvider.GetRequiredService<IAgentCommissionDetailRepository>();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
private UserService UserService => LazyServiceProvider.GetRequiredService<UserService>();
private RoleService RoleService => LazyServiceProvider.GetRequiredService<RoleService>();
private FakeRbacDbContext RbacDbContext => LazyServiceProvider.GetRequiredService<FakeRbacDbContext>();
/// <summary>分页查询代理。</summary>
[HttpPost("/agent/admin/list")]
public async Task<OmsPageResult<AgentOutput>> GetListAsync(AgentListInput input)
{
var predicate = BuildAgentListPredicate(input);
var page = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await AgentRepository.GetPagedListAsync(
predicate,
page,
pageSize,
new Dictionary<string, bool> { [nameof(Agent.CreateTime)] = false, [nameof(Agent.Id)] = false });
var total = await AgentRepository.CountAsync(predicate);
return new OmsPageResult<AgentOutput>
{
Items = entities.Select(MapAgent).ToList(),
Total = total
};
}
/// <summary>创建代理(同步 RBAC 用户 + Agent 角色)。</summary>
[HttpPost("/agent/admin/create")]
public async Task CreateAsync(AgentCreateInput input)
{
var code = input.AgentCode.Trim();
if (await AgentRepository.ExistsAgentCodeAsync(code))
{
throw new BusinessException("代理编号已存在");
}
EnsureBaseType(input.BaseType);
var roleId = await GetAgentRoleIdOrThrowAsync();
var user = await UserService.CreateAsync(new UserCreateDto
{
Name = input.AgentName.Trim(),
Account = code,
Password = input.Password,
Email = string.IsNullOrWhiteSpace(input.Email)
? $"{code}@agent.local"
: input.Email.Trim(),
RoleIds = []
});
try
{
await UserService.AssignRolesAsync(user.Id, [roleId]);
var userId = await GetRbacUserMapIdOrThrowAsync(user.Id);
await AgentRepository.InsertAsync(new Agent
{
UserId = userId,
AgentCode = code,
AgentName = input.AgentName.Trim(),
Phone = NormalizeOptional(input.Phone),
Email = NormalizeOptional(input.Email),
Password = OmsPasswordHelper.ToMd5(input.Password),
Status = EnableStatus.Enabled,
BaseType = input.BaseType.Trim().ToLowerInvariant(),
CommissionRate = input.CommissionRate,
SalesProductIds = NormalizeOptional(input.SalesProductIds),
Remark = NormalizeOptional(input.Remark)
});
}
catch
{
try
{
await UserService.DeleteAsync(user.Id);
}
catch
{
// ignore rollback failure
}
throw;
}
}
/// <summary>更新代理资料与返佣规则。</summary>
[HttpPost("/agent/admin/update")]
public async Task UpdateAsync(AgentUpdateInput input)
{
var entity = await AgentRepository.FirstOrDefaultAsync(x => x.Id == input.Id)
?? throw new BusinessException("代理不存在");
EnsureBaseType(input.BaseType);
entity.AgentName = input.AgentName.Trim();
entity.Phone = NormalizeOptional(input.Phone);
entity.Email = NormalizeOptional(input.Email);
entity.Status = input.Status == EnableStatus.None ? EnableStatus.Enabled : input.Status;
entity.BaseType = input.BaseType.Trim().ToLowerInvariant();
entity.CommissionRate = input.CommissionRate;
entity.SalesProductIds = NormalizeOptional(input.SalesProductIds);
entity.Remark = NormalizeOptional(input.Remark);
if (!string.IsNullOrWhiteSpace(input.Password))
{
entity.Password = OmsPasswordHelper.ToMd5(input.Password);
}
await AgentRepository.UpdateAsync(entity);
}
/// <summary>改绑主客户所属代理(仅影响之后新单)。</summary>
[HttpPost("/agent/admin/rebind")]
public async Task RebindCustomerAsync(AgentRebindCustomerInput input)
{
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == input.CustomerId)
?? throw new BusinessException("客户不存在");
if (customer.ParentCustomerId.HasValue)
{
throw new BusinessException("请改绑主客户");
}
if (input.AgentId.HasValue && input.AgentId.Value > 0)
{
var agent = await AgentRepository.FirstOrDefaultAsync(x => x.Id == input.AgentId.Value)
?? throw new BusinessException("代理不存在");
if (agent.Status != EnableStatus.Enabled)
{
throw new BusinessException("代理已停用");
}
customer.AgentId = agent.Id;
customer.AgentBindTime = DateTime.Now;
}
else
{
customer.AgentId = null;
customer.AgentBindTime = null;
}
await CustomerRepository.UpdateAsync(customer);
}
/// <summary>分页查询返佣明细。</summary>
[HttpPost("/agent/admin/commission/list")]
public async Task<OmsPageResult<AgentCommissionDetailOutput>> GetCommissionListAsync(AgentCommissionListInput input)
{
var predicate = BuildCommissionPredicate(input);
var page = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await DetailRepository.GetPagedListAsync(
predicate,
page,
pageSize,
new Dictionary<string, bool>
{
[nameof(AgentCommissionDetail.CreateTime)] = false,
[nameof(AgentCommissionDetail.Id)] = false
});
var total = await DetailRepository.CountAsync(predicate);
return new OmsPageResult<AgentCommissionDetailOutput>
{
Items = await MapCommissionOutputsAsync(entities),
Total = total
};
}
/// <summary>标记返佣明细为已打款。</summary>
[HttpPost("/agent/admin/commission/mark-paid")]
public async Task MarkCommissionPaidAsync(AgentCommissionMarkPaidInput input)
{
if (input.Ids == null || input.Ids.Count == 0)
{
throw new BusinessException("请选择明细");
}
var ids = input.Ids.Distinct().ToList();
var list = await DetailRepository.GetListAsync(x => ids.Contains(x.Id));
var now = DateTime.Now;
var remark = NormalizeOptional(input.Remark);
foreach (var item in list)
{
if (string.Equals(item.Status, AgentCommissionStatusCodes.Paid, StringComparison.OrdinalIgnoreCase))
{
continue;
}
item.Status = AgentCommissionStatusCodes.Paid;
item.PaidTime = now;
if (!string.IsNullOrWhiteSpace(remark))
{
item.Remark = remark;
}
await DetailRepository.UpdateAsync(item);
}
}
private static Expression<Func<Agent, bool>> BuildAgentListPredicate(AgentListInput input)
{
var keyword = input.Keyword?.Trim();
var hasStatus = input.Status.HasValue && input.Status.Value != EnableStatus.None;
if (hasStatus && !string.IsNullOrWhiteSpace(keyword))
{
var status = input.Status!.Value;
return x => x.Status == status
&& (x.AgentCode.Contains(keyword!)
|| x.AgentName.Contains(keyword!)
|| (x.Phone != null && x.Phone.Contains(keyword!)));
}
if (hasStatus)
{
var status = input.Status!.Value;
return x => x.Status == status;
}
if (!string.IsNullOrWhiteSpace(keyword))
{
return x => x.AgentCode.Contains(keyword!)
|| x.AgentName.Contains(keyword!)
|| (x.Phone != null && x.Phone.Contains(keyword!));
}
return x => true;
}
private static Expression<Func<AgentCommissionDetail, bool>> BuildCommissionPredicate(AgentCommissionListInput input)
{
var status = input.Status?.Trim();
if (input.AgentId is > 0 && input.OrderId is > 0 && !string.IsNullOrWhiteSpace(status))
{
var agentId = input.AgentId.Value;
var orderId = input.OrderId.Value;
return x => x.AgentId == agentId && x.OrderId == orderId && x.Status == status;
}
if (input.AgentId is > 0 && input.OrderId is > 0)
{
var agentId = input.AgentId.Value;
var orderId = input.OrderId.Value;
return x => x.AgentId == agentId && x.OrderId == orderId;
}
if (input.AgentId is > 0 && !string.IsNullOrWhiteSpace(status))
{
var agentId = input.AgentId.Value;
return x => x.AgentId == agentId && x.Status == status;
}
if (input.OrderId is > 0 && !string.IsNullOrWhiteSpace(status))
{
var orderId = input.OrderId.Value;
return x => x.OrderId == orderId && x.Status == status;
}
if (input.AgentId is > 0)
{
var agentId = input.AgentId.Value;
return x => x.AgentId == agentId;
}
if (input.OrderId is > 0)
{
var orderId = input.OrderId.Value;
return x => x.OrderId == orderId;
}
if (!string.IsNullOrWhiteSpace(status))
{
return x => x.Status == status;
}
return x => true;
}
private async Task<List<AgentCommissionDetailOutput>> MapCommissionOutputsAsync(
List<AgentCommissionDetail> entities)
{
if (entities.Count == 0)
{
return [];
}
var agentIds = entities.Select(x => x.AgentId).Distinct().ToList();
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToList();
var orderIds = entities.Select(x => x.OrderId).Distinct().ToList();
var agents = await AgentRepository.GetListAsync(x => agentIds.Contains(x.Id));
var customers = await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
var agentMap = agents.ToDictionary(x => x.Id);
var customerMap = customers.ToDictionary(x => x.Id);
var orderMap = orders.ToDictionary(x => x.Id);
return entities.Select(x =>
{
agentMap.TryGetValue(x.AgentId, out var agent);
customerMap.TryGetValue(x.CustomerId, out var customer);
orderMap.TryGetValue(x.OrderId, out var order);
return new AgentCommissionDetailOutput
{
Id = x.Id,
AgentId = x.AgentId,
AgentCode = agent?.AgentCode,
AgentName = agent?.AgentName,
OrderId = x.OrderId,
ServiceNo = order?.ServiceNo,
CustomerId = x.CustomerId,
CustomerCode = customer?.CustomerCode,
CustomerName = customer?.CustomerName,
SalesProductId = x.SalesProductId,
BaseType = x.BaseType,
BaseAmount = x.BaseAmount,
CommissionRate = x.CommissionRate,
CommissionAmount = x.CommissionAmount,
Status = x.Status,
TriggerOrderStatus = x.TriggerOrderStatus,
PaidTime = x.PaidTime,
Remark = x.Remark,
CreateTime = x.CreateTime
};
}).ToList();
}
private static AgentOutput MapAgent(Agent x) => new()
{
Id = x.Id,
AgentCode = x.AgentCode,
AgentName = x.AgentName,
Phone = x.Phone,
Email = x.Email,
Status = x.Status,
BaseType = x.BaseType,
CommissionRate = x.CommissionRate,
SalesProductIds = x.SalesProductIds,
Remark = x.Remark,
CreateTime = x.CreateTime
};
private static void EnsureBaseType(string? baseType)
{
if (!AgentCommissionBaseTypes.IsSupported(baseType))
{
throw new BusinessException("基数类型仅支持 freight(运费)或 receivable(应收)");
}
}
private static string? NormalizeOptional(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private async Task<Guid> GetAgentRoleIdOrThrowAsync()
{
var roles = await RoleService.GetAllRolesAsync();
var role = roles.FirstOrDefault(x =>
string.Equals(x.Code, AgentRoleCode, StringComparison.OrdinalIgnoreCase));
if (role == null)
{
throw new BusinessException("未找到 Agent 角色,请先执行种子或手动创建");
}
return role.Id;
}
private async Task<int> GetRbacUserMapIdOrThrowAsync(Guid userId)
{
var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}";
var mapId = await RbacDbContext.Database.SqlQueryRaw<int?>(sql, userId).FirstOrDefaultAsync();
if (!mapId.HasValue || mapId.Value <= 0)
{
throw new BusinessException("RBAC 用户映射失败");
}
return mapId.Value;
}
}