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; /// /// 渠道代理管理端 API(开户、规则、改绑、返佣明细)。 /// [ApiExplorerSettings(GroupName = "OMS-Agent")] public class AgentAdminService : ApplicationService { public const string AgentRoleCode = "Agent"; private IAgentRepository AgentRepository => LazyServiceProvider.GetRequiredService(); private IAgentCommissionDetailRepository DetailRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService(); private UserService UserService => LazyServiceProvider.GetRequiredService(); private RoleService RoleService => LazyServiceProvider.GetRequiredService(); private FakeRbacDbContext RbacDbContext => LazyServiceProvider.GetRequiredService(); /// 分页查询代理。 [HttpPost("/agent/admin/list")] public async Task> 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 { [nameof(Agent.CreateTime)] = false, [nameof(Agent.Id)] = false }); var total = await AgentRepository.CountAsync(predicate); return new OmsPageResult { Items = entities.Select(MapAgent).ToList(), Total = total }; } /// 创建代理(同步 RBAC 用户 + Agent 角色)。 [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; } } /// 更新代理资料与返佣规则。 [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); } /// 改绑主客户所属代理(仅影响之后新单)。 [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); } /// 分页查询返佣明细。 [HttpPost("/agent/admin/commission/list")] public async Task> 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 { [nameof(AgentCommissionDetail.CreateTime)] = false, [nameof(AgentCommissionDetail.Id)] = false }); var total = await DetailRepository.CountAsync(predicate); return new OmsPageResult { Items = await MapCommissionOutputsAsync(entities), Total = total }; } /// 标记返佣明细为已打款。 [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> 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> 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> MapCommissionOutputsAsync( List 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 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 GetRbacUserMapIdOrThrowAsync(Guid userId) { var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}"; var mapId = await RbacDbContext.Database.SqlQueryRaw(sql, userId).FirstOrDefaultAsync(); if (!mapId.HasValue || mapId.Value <= 0) { throw new BusinessException("RBAC 用户映射失败"); } return mapId.Value; } }