admin/src/Oms.Application/Services/Customers/CustomerService.cs
zzh 07fc7fffe7 feat: resolve createBy/updateBy to RBAC user display names
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>
2026-06-29 19:07:17 +08:00

522 lines
19 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.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);
}