Add OmsRbacUserDisplayNameResolver and use it in customer, order, warehouse, and channel APIs so audit fields return names instead of mapIds. Co-authored-by: Cursor <cursoragent@cursor.com>
522 lines
19 KiB
C#
522 lines
19 KiB
C#
using System.Linq.Expressions;
|
||
using Fake.Application;
|
||
using Fake.Rbac.Application.Dtos.Role;
|
||
using Fake.Rbac.Application.Dtos.User;
|
||
using Fake.Rbac.Application.Services;
|
||
using Fake.Rbac.Infrastructure;
|
||
using Fake.Security.Claims;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Oms.Application.Dtos.Customers;
|
||
using Oms.Application.Dtos.Shared;
|
||
using Oms.Application.Shared;
|
||
using Oms.Domain.CustomersAggregate.Entities;
|
||
using Oms.Domain.CustomersAggregate.Enums;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Customers;
|
||
|
||
/// <summary>
|
||
/// 客户 API 服务。
|
||
/// </summary>
|
||
[ApiExplorerSettings(GroupName = "OMS-Customer")]
|
||
public class CustomerService : ApplicationService
|
||
{
|
||
private const string CustomerRoleCode = "Customer";
|
||
|
||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||
|
||
private UserService UserService => LazyServiceProvider.GetRequiredService<UserService>();
|
||
|
||
private RoleService RoleService => LazyServiceProvider.GetRequiredService<RoleService>();
|
||
|
||
private FakeRbacDbContext RbacDbContext => LazyServiceProvider.GetRequiredService<FakeRbacDbContext>();
|
||
|
||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
|
||
|
||
/// <summary>
|
||
/// 查询客户列表。
|
||
/// </summary>
|
||
/// <param name="input">入参:客户分页查询条件。</param>
|
||
/// <returns>客户分页结果,包含列表数据和总记录数。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 根据查询条件构造筛选规则。
|
||
/// 2. 查询目标数据并补齐关联展示字段。
|
||
/// 3. 按接口约定返回列表结果。
|
||
/// </remarks>
|
||
[HttpGet("/customer/list")]
|
||
public async Task<OmsPageResult<CustomerPaginatedOutput>> GetPaginatedListAsync([FromQuery] CustomerPaginatedInput input)
|
||
{
|
||
var predicate = BuildPaginatedPredicate(input);
|
||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||
|
||
var entities = await CustomerRepository.GetPagedListAsync(
|
||
predicate,
|
||
pageIndex,
|
||
pageSize,
|
||
new Dictionary<string, bool> { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false });
|
||
|
||
var totalCount = await CustomerRepository.CountAsync(predicate);
|
||
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
|
||
entities.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
|
||
var items = entities.Select(entity =>
|
||
{
|
||
var output = ObjectMapper.Map<Customer, CustomerPaginatedOutput>(entity);
|
||
output.CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, auditUserNameMap);
|
||
output.UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.UpdateUserId, auditUserNameMap);
|
||
output.UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime);
|
||
return output;
|
||
}).ToList();
|
||
|
||
return new OmsPageResult<CustomerPaginatedOutput>
|
||
{
|
||
Items = items,
|
||
Total = totalCount
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询客户详情。
|
||
/// </summary>
|
||
/// <param name="id">入参:目标主键。</param>
|
||
/// <returns>客户详情。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 根据标识查询目标对象。
|
||
/// 2. 目标不存在时抛出业务异常。
|
||
/// 3. 映射并返回详情结果。
|
||
/// </remarks>
|
||
[HttpGet("/customer/get")]
|
||
public async Task<CustomerOutput> GetByIdAsync([FromQuery] int id)
|
||
{
|
||
var entity = await GetCustomerOrThrowAsync(id);
|
||
var output = ObjectMapper.Map<Customer, CustomerOutput>(entity);
|
||
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
|
||
[entity.CreateUserId, entity.UpdateUserId]);
|
||
output.CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, auditUserNameMap);
|
||
output.UpdateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.UpdateUserId, auditUserNameMap);
|
||
output.UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime);
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 新增客户。
|
||
/// </summary>
|
||
/// <param name="input">入参:客户创建参数。</param>
|
||
/// <returns>无返回体,仅完成新增客户。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 校验入参与关联对象、唯一性等前置条件。
|
||
/// 2. 组装目标实体及相关明细数据。
|
||
/// 3. 写入审计字段并持久化结果。
|
||
/// </remarks>
|
||
[HttpPost("/customer/create")]
|
||
public async Task CreateAsync(CreateCustomerInput input)
|
||
{
|
||
var normalized = await NormalizeCreateCustomerInputAsync(input, "客户编号", "客户名称");
|
||
|
||
await EnsureCustomerIdentityUniqueAsync(normalized.CustomerCode, normalized.Phone, normalized.Email);
|
||
|
||
var customerRoleId = await GetCustomerRoleIdOrThrowAsync();
|
||
var user = await CreateRbacUserAsync(input, normalized);
|
||
|
||
try
|
||
{
|
||
await UserService.AssignRolesAsync(user.Id, [customerRoleId]);
|
||
var userId = await GetRbacUserMapIdOrThrowAsync(user.Id);
|
||
var entity = BuildCustomerEntity(input, normalized, null, userId);
|
||
await CustomerRepository.InsertAsync(entity);
|
||
}
|
||
catch
|
||
{
|
||
await TryRollbackRbacUserAsync(user.Id);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建子客户。
|
||
/// </summary>
|
||
/// <param name="input">入参:子客户基础资料、登录密码和授权权限编码列表。</param>
|
||
/// <returns>无返回体,仅完成子客户创建和RBAC用户、角色绑定。</returns>
|
||
[HttpPost("/customer/sub/create")]
|
||
public async Task CreateSubCustomerAsync([FromBody] CreateSubCustomerInput input)
|
||
{
|
||
var mainCustomer = await GetCurrentMainCustomerOrThrowAsync();
|
||
ValidateCustomerStatus(mainCustomer);
|
||
|
||
var normalized = await NormalizeCreateCustomerInputAsync(input, "子客户编号", "子客户名称");
|
||
var permissions = await NormalizeAndValidateSubCustomerPermissionsAsync(input.Permissions);
|
||
|
||
await EnsureCustomerIdentityUniqueAsync(normalized.CustomerCode, normalized.Phone, normalized.Email);
|
||
|
||
var user = await CreateRbacUserAsync(input, normalized);
|
||
|
||
Guid? roleId = null;
|
||
try
|
||
{
|
||
var userId = await GetRbacUserMapIdOrThrowAsync(user.Id);
|
||
roleId = await CreateSubCustomerRoleAsync(userId, normalized.CustomerName, permissions);
|
||
await UserService.AssignRolesAsync(user.Id, [roleId.Value]);
|
||
var entity = BuildCustomerEntity(input, normalized, mainCustomer.Id, userId);
|
||
await CustomerRepository.InsertAsync(entity);
|
||
}
|
||
catch
|
||
{
|
||
if (roleId.HasValue)
|
||
{
|
||
await TryRollbackRbacRoleAsync(roleId.Value);
|
||
}
|
||
|
||
await TryRollbackRbacUserAsync(user.Id);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新客户。
|
||
/// </summary>
|
||
/// <param name="input">入参:客户更新参数。</param>
|
||
/// <returns>无返回体,仅完成更新客户。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 查询并校验目标对象。
|
||
/// 2. 按入参更新主体数据和关联配置。
|
||
/// 3. 写入更新审计字段并保存结果。
|
||
/// </remarks>
|
||
[HttpPost("/customer/update")]
|
||
public async Task UpdateAsync(UpdateCustomerInput input)
|
||
{
|
||
var entity = await GetCustomerOrThrowAsync(input.Id);
|
||
var customerCode = NormalizeRequiredText(input.CustomerCode, "客户编号");
|
||
var customerName = NormalizeRequiredText(input.CustomerName, "客户名称");
|
||
var phone = NormalizeRequiredText(input.Phone, "手机号");
|
||
var email = NormalizeOptionalText(input.Email);
|
||
|
||
await EnsureCustomerIdentityUniqueForUpdateAsync(entity.Id, customerCode, phone, email);
|
||
|
||
entity.CustomerCode = customerCode;
|
||
entity.CustomerName = customerName;
|
||
entity.Phone = phone;
|
||
entity.Email = email;
|
||
entity.WechatAvatar = NormalizeOptionalText(input.WechatAvatar);
|
||
entity.WechatNickname = NormalizeOptionalText(input.WechatNickname);
|
||
entity.Balance = input.Balance;
|
||
entity.TotalQuota = input.TotalQuota;
|
||
entity.UsedQuota = input.UsedQuota;
|
||
entity.Status = input.Status;
|
||
entity.Remark = NormalizeOptionalText(input.Remark);
|
||
entity.Salesman = NormalizeOptionalText(input.Salesman);
|
||
entity.CustomerService = NormalizeOptionalText(input.CustomerService);
|
||
entity.CustomerGroup = input.CustomerGroup;
|
||
|
||
await CustomerRepository.UpdateAsync(entity);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除客户。
|
||
/// </summary>
|
||
/// <param name="input">入参:目标主键。</param>
|
||
/// <returns>无返回体,仅完成删除客户。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 查询并校验目标对象。
|
||
/// 2. 执行软删除或删除关联明细。
|
||
/// 3. 持久化删除结果。
|
||
/// </remarks>
|
||
[HttpPost("/customer/delete")]
|
||
public async Task DeleteAsync(IdInput input)
|
||
{
|
||
var entity = await GetCustomerOrThrowAsync(input.Id);
|
||
await CustomerRepository.DeleteAsync(entity);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重置客户密码。
|
||
/// </summary>
|
||
/// <param name="input">入参:客户密码重置参数。</param>
|
||
/// <returns>无返回体,仅完成重置客户密码。</returns>
|
||
/// <remarks>
|
||
/// 核心业务逻辑:
|
||
/// 1. 查询并校验目标对象及当前状态。
|
||
/// 2. 执行业务状态流转或敏感字段更新。
|
||
/// 3. 写入审计字段并保存结果。
|
||
/// </remarks>
|
||
[HttpPost("/customer/reset-password")]
|
||
public async Task ResetPasswordAsync(ResetCustomerPasswordInput input)
|
||
{
|
||
var entity = await GetCustomerOrThrowAsync(input.Id);
|
||
entity.Password = OmsPasswordHelper.ToMd5(input.NewPassword);
|
||
await CustomerRepository.UpdateAsync(entity);
|
||
}
|
||
|
||
private Expression<Func<Customer, bool>> BuildPaginatedPredicate(CustomerPaginatedInput input)
|
||
{
|
||
var keyword = input.Keyword?.Trim();
|
||
var salesman = input.Salesman?.Trim();
|
||
var customerService = input.CustomerService?.Trim();
|
||
var hasStatusFilter = input.Status.HasValue;
|
||
var statusValue = input.Status.GetValueOrDefault();
|
||
var hasCustomerGroupFilter = input.CustomerGroup.HasValue;
|
||
var customerGroupValue = input.CustomerGroup.GetValueOrDefault();
|
||
|
||
return customer =>
|
||
(string.IsNullOrWhiteSpace(keyword) ||
|
||
customer.CustomerCode.Contains(keyword) ||
|
||
customer.CustomerName.Contains(keyword) ||
|
||
customer.Phone.Contains(keyword) ||
|
||
(customer.Email ?? string.Empty).Contains(keyword)) &&
|
||
(!hasStatusFilter || customer.Status == statusValue) &&
|
||
(!hasCustomerGroupFilter || customer.CustomerGroup == customerGroupValue) &&
|
||
(string.IsNullOrWhiteSpace(salesman) || customer.Salesman == salesman) &&
|
||
(string.IsNullOrWhiteSpace(customerService) || customer.CustomerService == customerService);
|
||
}
|
||
|
||
private async Task<Customer> GetCustomerOrThrowAsync(int id)
|
||
{
|
||
var entity = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||
if (entity == null)
|
||
{
|
||
throw new BusinessException("客户不存在");
|
||
}
|
||
|
||
return entity;
|
||
}
|
||
|
||
private async Task<int> GetRbacUserMapIdOrThrowAsync(Guid id)
|
||
{
|
||
var userId = await RbacDbContext.Database.SqlQuery<int?>(
|
||
$"select `userid` as `Value` from `user` where `Id` = {id}")
|
||
.FirstOrDefaultAsync();
|
||
if (!userId.HasValue || userId.Value <= 0)
|
||
{
|
||
throw new BusinessException("用户编号创建失败");
|
||
}
|
||
|
||
return userId.Value;
|
||
}
|
||
|
||
private async Task<Guid> GetCustomerRoleIdOrThrowAsync()
|
||
{
|
||
var roles = await RoleService.GetAllRolesAsync();
|
||
var role = roles.FirstOrDefault(x => string.Equals(x.Code, CustomerRoleCode, StringComparison.OrdinalIgnoreCase));
|
||
if (role == null)
|
||
{
|
||
throw new BusinessException("客户角色未初始化");
|
||
}
|
||
|
||
return role.Id;
|
||
}
|
||
|
||
private async Task<Customer> GetCurrentMainCustomerOrThrowAsync()
|
||
{
|
||
var userId = CurrentUser.GetId<int>();
|
||
if (userId <= 0)
|
||
{
|
||
throw new BusinessException("客户未登录");
|
||
}
|
||
|
||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == userId && x.ParentCustomerId == null);
|
||
if (customer == null)
|
||
{
|
||
throw new BusinessException("仅主客户可以管理子客户");
|
||
}
|
||
|
||
return customer;
|
||
}
|
||
|
||
private async Task EnsureCustomerIdentityUniqueAsync(string customerCode, string phone, string email)
|
||
{
|
||
if (await CustomerRepository.GetByCustomerCodeAsync(customerCode) != null)
|
||
{
|
||
throw new BusinessException("客户编号已存在");
|
||
}
|
||
|
||
if (await CustomerRepository.GetByPhoneAsync(phone) != null)
|
||
{
|
||
throw new BusinessException("手机号已存在");
|
||
}
|
||
|
||
if (await CustomerRepository.GetByEmailAsync(email) != null)
|
||
{
|
||
throw new BusinessException("邮箱已存在");
|
||
}
|
||
}
|
||
|
||
private async Task EnsureCustomerIdentityUniqueForUpdateAsync(int id, string customerCode, string phone, string? email)
|
||
{
|
||
var duplicatedCode = await CustomerRepository.GetByCustomerCodeAsync(customerCode);
|
||
if (duplicatedCode != null && duplicatedCode.Id != id)
|
||
{
|
||
throw new BusinessException("客户编号已存在");
|
||
}
|
||
|
||
var duplicatedPhone = await CustomerRepository.GetByPhoneAsync(phone);
|
||
if (duplicatedPhone != null && duplicatedPhone.Id != id)
|
||
{
|
||
throw new BusinessException("手机号已存在");
|
||
}
|
||
|
||
if (email == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var duplicatedEmail = await CustomerRepository.GetByEmailAsync(email);
|
||
if (duplicatedEmail != null && duplicatedEmail.Id != id)
|
||
{
|
||
throw new BusinessException("邮箱已存在");
|
||
}
|
||
}
|
||
|
||
private async Task<UserDto> CreateRbacUserAsync(CreateCustomerInput input, NormalizedCustomerInput normalized)
|
||
{
|
||
return await UserService.CreateAsync(new UserCreateDto
|
||
{
|
||
Name = normalized.CustomerName,
|
||
Account = normalized.Phone,
|
||
Password = input.Password,
|
||
Email = normalized.Email,
|
||
Avatar = NormalizeOptionalText(input.WechatAvatar),
|
||
RoleIds = []
|
||
});
|
||
}
|
||
|
||
private static Customer BuildCustomerEntity(
|
||
CreateCustomerInput input,
|
||
NormalizedCustomerInput normalized,
|
||
int? parentCustomerId,
|
||
int userId)
|
||
{
|
||
return new Customer
|
||
{
|
||
ParentCustomerId = parentCustomerId,
|
||
UserId = userId,
|
||
CustomerCode = normalized.CustomerCode,
|
||
CustomerName = normalized.CustomerName,
|
||
WechatAvatar = NormalizeOptionalText(input.WechatAvatar),
|
||
WechatNickname = NormalizeOptionalText(input.WechatNickname),
|
||
Phone = normalized.Phone,
|
||
Email = normalized.Email,
|
||
Password = OmsPasswordHelper.ToMd5(input.Password),
|
||
Balance = input.Balance,
|
||
TotalQuota = input.TotalQuota,
|
||
UsedQuota = input.UsedQuota,
|
||
Status = input.Status,
|
||
Remark = NormalizeOptionalText(input.Remark),
|
||
Salesman = NormalizeOptionalText(input.Salesman),
|
||
CustomerService = NormalizeOptionalText(input.CustomerService),
|
||
CustomerGroup = input.CustomerGroup
|
||
};
|
||
}
|
||
|
||
private async Task<NormalizedCustomerInput> NormalizeCreateCustomerInputAsync(
|
||
CreateCustomerInput input,
|
||
string codeFieldName,
|
||
string nameFieldName)
|
||
{
|
||
var customerCode = string.IsNullOrWhiteSpace(input.CustomerCode)
|
||
? await CustomerCodeGenerator.GenerateNextAsync(CustomerRepository)
|
||
: NormalizeRequiredText(input.CustomerCode, codeFieldName);
|
||
|
||
return new NormalizedCustomerInput(
|
||
customerCode,
|
||
NormalizeRequiredText(input.CustomerName, nameFieldName),
|
||
NormalizeRequiredText(input.Phone, "手机号"),
|
||
NormalizeRequiredText(input.Email, "邮箱"));
|
||
}
|
||
|
||
private async Task<List<string>> NormalizeAndValidateSubCustomerPermissionsAsync(IEnumerable<string>? permissions)
|
||
{
|
||
var requestedPermissions = (permissions ?? [])
|
||
.Select(NormalizeOptionalText)
|
||
.Where(x => x != null)
|
||
.Select(x => x!)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
var currentUserId = CurrentUser.Id ?? throw new BusinessException("客户未登录");
|
||
var currentPermissions = await UserService.GetUserPermissionsAsync(currentUserId);
|
||
var currentPermissionSet = currentPermissions.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||
if (requestedPermissions.Any(x => !currentPermissionSet.Contains(x)))
|
||
{
|
||
throw new BusinessException("子客户权限不能超出当前客户权限");
|
||
}
|
||
|
||
return requestedPermissions;
|
||
}
|
||
|
||
private async Task<Guid> CreateSubCustomerRoleAsync(int userId, string customerName, List<string> permissions)
|
||
{
|
||
var role = await RoleService.CreateAsync(new RoleCreateDto
|
||
{
|
||
Code = $"CustomerSub_{userId}",
|
||
Name = $"子客户-{customerName}",
|
||
Permissions = permissions
|
||
});
|
||
|
||
return role.Id;
|
||
}
|
||
|
||
private async Task TryRollbackRbacUserAsync(Guid userId)
|
||
{
|
||
try
|
||
{
|
||
await UserService.DeleteAsync(userId);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
|
||
private async Task TryRollbackRbacRoleAsync(Guid roleId)
|
||
{
|
||
try
|
||
{
|
||
await RoleService.DeleteAsync(roleId);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
}
|
||
|
||
private static void ValidateCustomerStatus(Customer customer)
|
||
{
|
||
if (customer.Status == CustomerStatus.Disabled)
|
||
{
|
||
throw new BusinessException("客户已禁用");
|
||
}
|
||
|
||
if (customer.Status == CustomerStatus.Frozen)
|
||
{
|
||
throw new BusinessException("客户已冻结");
|
||
}
|
||
}
|
||
|
||
private static string NormalizeRequiredText(string? value, string fieldName)
|
||
{
|
||
var normalized = value?.Trim();
|
||
if (string.IsNullOrWhiteSpace(normalized))
|
||
{
|
||
throw new BusinessException($"{fieldName}不能为空");
|
||
}
|
||
|
||
return normalized;
|
||
}
|
||
|
||
private static string? NormalizeOptionalText(string? value)
|
||
{
|
||
var normalized = value?.Trim();
|
||
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
|
||
}
|
||
|
||
private sealed record NormalizedCustomerInput(
|
||
string CustomerCode,
|
||
string CustomerName,
|
||
string Phone,
|
||
string Email);
|
||
}
|