All checks were successful
Build and Deploy / deploy (push) Successful in 1m13s
独立 Agent 主体与 loginType=2,注册绑定/下单快照,派送成功失败计提明细,不含提现入账。 Co-authored-by: Cursor <cursoragent@cursor.com>
415 lines
15 KiB
C#
415 lines
15 KiB
C#
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;
|
||
}
|
||
}
|