admin/src/Oms.Application/Services/Customers/CustomerService.cs
zzh 9be7fcd143
All checks were successful
Build and Deploy / deploy (push) Successful in 1m11s
fix: 客户公开注册接口允许匿名访问,避免注册页 401
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-30 17:33:04 +08:00

953 lines
35 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 Fake.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Oms.Application.Dtos.Customers;
using Oms.Application.Dtos.Shared;
using Oms.Application.Rbac;
using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Enums;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.AgentsAggregate.Repositories;
using Oms.Domain.MarketingAggregate.Repositories;
using Oms.Domain.Shared.Enums;
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 IMemberLevelRepository MemberLevelRepository =>
LazyServiceProvider.GetRequiredService<IMemberLevelRepository>();
private ICustomerReferralCodeRepository ReferralCodeRepository =>
LazyServiceProvider.GetRequiredService<ICustomerReferralCodeRepository>();
private IAgentRepository AgentRepository =>
LazyServiceProvider.GetRequiredService<IAgentRepository>();
private MarketingCouponManager CouponManager =>
LazyServiceProvider.GetRequiredService<MarketingCouponManager>();
private UserService UserService => LazyServiceProvider.GetRequiredService<UserService>();
private RoleService RoleService => LazyServiceProvider.GetRequiredService<RoleService>();
private FakeRbacDbContext RbacDbContext => LazyServiceProvider.GetRequiredService<FakeRbacDbContext>();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
private CustomerServiceAdvisorResolver ServiceAdvisorResolver =>
LazyServiceProvider.GetRequiredService<CustomerServiceAdvisorResolver>();
/// <summary>
/// 查询客户列表。
/// </summary>
/// <param name="input">入参:客户分页查询条件。</param>
/// <returns>客户分页结果,包含列表数据和总记录数。</returns>
[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,
BuildCustomerSortDictionary(input));
var totalCount = await CustomerRepository.CountAsync(predicate);
var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(
entities.SelectMany(x => new[] { x.CreateUserId, x.UpdateUserId }));
var memberLevelNameMap = await ResolveMemberLevelNameMapAsync(
entities.Where(x => x.MemberLevelId.HasValue).Select(x => x.MemberLevelId!.Value));
var agentMap = await ResolveAgentDisplayMapAsync(
entities.Where(x => x.AgentId.HasValue).Select(x => x.AgentId!.Value));
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);
if (entity.MemberLevelId.HasValue
&& memberLevelNameMap.TryGetValue(entity.MemberLevelId.Value, out var levelName))
{
output.MemberLevelName = levelName;
}
FillAgentDisplay(output, entity.AgentId, agentMap);
return output;
}).ToList();
return new OmsPageResult<CustomerPaginatedOutput>
{
Items = items,
Total = totalCount
};
}
/// <summary>
/// 查询客户详情。
/// </summary>
/// <param name="id">入参:目标主键。</param>
/// <returns>客户详情。</returns>
[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);
if (entity.MemberLevelId.HasValue)
{
var level = await MemberLevelRepository.FirstOrDefaultAsync(x => x.Id == entity.MemberLevelId.Value);
output.MemberLevelName = level?.LevelName;
}
if (entity.AgentId.HasValue)
{
var agentMap = await ResolveAgentDisplayMapAsync([entity.AgentId.Value]);
FillAgentDisplay(output, entity.AgentId, agentMap);
}
return output;
}
/// <summary>
/// 查询当前登录客户所属顾问信息。
/// </summary>
/// <returns>专属顾问姓名、微信号、微信二维码。</returns>
[HttpGet("/customer/service-advisor")]
public async Task<CustomerServiceAdvisorOutput> GetServiceAdvisorAsync(CancellationToken cancellationToken = default)
{
var userId = CurrentUser.GetId<int>();
if (userId <= 0)
{
return new CustomerServiceAdvisorOutput();
}
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken);
if (customer == null || string.IsNullOrWhiteSpace(customer.CustomerService))
{
return new CustomerServiceAdvisorOutput();
}
return await ServiceAdvisorResolver.ResolveAsync(customer.CustomerService, cancellationToken);
}
/// <summary>
/// 当前登录客户联系方式完整度(下单/揽收前校验)。
/// </summary>
[HttpGet("/customer/profile-completeness")]
public async Task<CustomerProfileCompletenessOutput> GetProfileCompletenessAsync(
CancellationToken cancellationToken = default)
{
var customer = await GetCurrentCustomerOrThrowAsync(cancellationToken);
var phoneFilled = CustomerContactHelper.HasPhone(customer);
var emailFilled = CustomerContactHelper.HasEmail(customer);
return new CustomerProfileCompletenessOutput
{
PhoneFilled = phoneFilled,
EmailFilled = emailFilled,
NeedContact = !phoneFilled || !emailFilled,
CustomerCode = customer.CustomerCode,
Phone = customer.Phone,
Email = customer.Email
};
}
/// <summary>
/// 补全手机号与邮箱(下单前)。
/// </summary>
[HttpPost("/customer/complete-contact")]
public async Task CompleteContactAsync(
[FromBody] CompleteCustomerContactInput input,
CancellationToken cancellationToken = default)
{
var customer = await GetCurrentCustomerOrThrowAsync(cancellationToken);
var phone = WeChatRegistrationFieldNormalizer.TruncateRequired(
NormalizeRequiredText(input.Phone, "手机号"),
WeChatRegistrationFieldNormalizer.PhoneMaxLength);
var email = WeChatRegistrationFieldNormalizer.TruncateRequired(
NormalizeRequiredText(input.Email, "邮箱"),
WeChatRegistrationFieldNormalizer.EmailMaxLength);
await FreeSoftDeletedCustomerIdentityIfNeededAsync(
customer.CustomerCode,
phone,
email,
excludeId: customer.Id);
var duplicatedPhone = await CustomerRepository.GetByPhoneAsync(phone, cancellationToken);
if (duplicatedPhone != null && duplicatedPhone.Id != customer.Id)
{
throw new BusinessException("手机号已存在");
}
var duplicatedEmail = await CustomerRepository.GetByEmailAsync(email, cancellationToken);
if (duplicatedEmail != null && duplicatedEmail.Id != customer.Id)
{
throw new BusinessException("邮箱已存在");
}
customer.Phone = phone;
customer.Email = email;
await CustomerRepository.UpdateAsync(customer);
// 同步 RBAC 登录邮箱,覆盖注册时的 @noemail.local 占位
try
{
var syncEmailSql =
$"UPDATE `{FakeTables.User}` SET `Email` = {{0}} WHERE `userid` = {{1}} AND `IsDeleted` = 0";
await RbacDbContext.Database.ExecuteSqlRawAsync(syncEmailSql, email, customer.UserId);
}
catch
{
// 客户资料已更新;RBAC 同步失败不阻断补全
}
}
/// <summary>
/// 新增客户(公开注册页可匿名调用;管理端也可带 token 代开户)。
/// </summary>
/// <param name="input">入参:客户创建参数。</param>
/// <returns>无返回体,仅完成新增客户。</returns>
[AllowAnonymous]
[HttpPost("/customer/create")]
public async Task CreateAsync(CreateCustomerInput input)
{
var normalized = await NormalizeCreateCustomerInputAsync(input, "客户编号", "客户名称");
await FreeSoftDeletedCustomerIdentityIfNeededAsync(
normalized.CustomerCode,
normalized.Phone,
normalized.Email);
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);
entity.MemberLevelId = await ResolveMemberLevelIdForCreateAsync(input.MemberLevelId);
entity.PriceTierKey = MarketingCustomerHelper.ResolveNewCustomerPriceTierKey(input.PriceTierKey);
await AgentBinder.BindAgentOnCreateAsync(entity, input.AgentCode, AgentRepository);
// 有代理编号时不走 C2C 邀请,避免双发佣金
if (!entity.AgentId.HasValue)
{
await MarketingReferralBinder.BindReferrerOnCreateAsync(
entity,
input.InviteCode,
ReferralCodeRepository,
CustomerRepository,
MemberLevelRepository,
LazyServiceProvider.GetRequiredService<IReferralCommissionRuleRepository>());
}
await StaffAdvisorBinder.BindAdvisorOnCreateAsync(
entity,
input.AdvisorCode,
RbacDbContext);
await CustomerRepository.InsertAsync(entity);
try
{
await CouponManager.GrantNewcomerCouponsAsync(entity.Id);
if (entity.ReferrerCustomerId.HasValue)
{
await CouponManager.GrantInviteeCouponAsync(entity.Id);
}
}
catch
{
// 新人券发放失败不回滚注册
}
}
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 FreeSoftDeletedCustomerIdentityIfNeededAsync(
normalized.CustomerCode,
normalized.Phone,
normalized.Email);
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);
// 子客户跟随主客户价区与等级展示,计价走主客户
entity.MemberLevelId = mainCustomer.MemberLevelId
?? await ResolveMemberLevelIdForCreateAsync(input.MemberLevelId);
entity.PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(
mainCustomer.PriceTierKey ?? input.PriceTierKey);
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>
[HttpPost("/customer/update")]
public async Task UpdateAsync(UpdateCustomerInput input)
{
var entity = await GetCustomerOrThrowAsync(input.Id);
// 未传字段保留原值,避免局部更新(如只改名称)把编号/余额/状态冲成空或默认值
var customerCode = string.IsNullOrWhiteSpace(input.CustomerCode)
? entity.CustomerCode
: NormalizeRequiredText(input.CustomerCode, "客户编号");
var customerName = string.IsNullOrWhiteSpace(input.CustomerName)
? entity.CustomerName
: NormalizeRequiredText(input.CustomerName, "客户名称");
var phone = string.IsNullOrWhiteSpace(input.Phone)
? entity.Phone
: NormalizeRequiredText(input.Phone, "手机号");
var email = input.Email != null ? NormalizeOptionalText(input.Email) : entity.Email;
await EnsureCustomerIdentityUniqueForUpdateAsync(entity.Id, customerCode, phone, email);
entity.CustomerCode = customerCode;
entity.CustomerName = customerName;
entity.Phone = phone;
entity.Email = email;
if (input.WechatAvatar != null)
{
entity.WechatAvatar = NormalizeOptionalText(input.WechatAvatar);
}
if (input.WechatNickname != null)
{
entity.WechatNickname = NormalizeOptionalText(input.WechatNickname);
}
if (input.Balance.HasValue)
{
entity.Balance = input.Balance.Value;
}
if (input.TotalQuota.HasValue)
{
entity.TotalQuota = input.TotalQuota.Value;
}
if (input.UsedQuota.HasValue)
{
entity.UsedQuota = input.UsedQuota.Value;
}
if (input.Status.HasValue && input.Status.Value != CustomerStatus.None)
{
entity.Status = input.Status.Value;
}
if (input.Remark != null)
{
entity.Remark = NormalizeOptionalText(input.Remark);
}
if (input.Salesman != null)
{
entity.Salesman = NormalizeOptionalText(input.Salesman);
}
if (input.CustomerService != null)
{
entity.CustomerService = NormalizeOptionalText(input.CustomerService);
}
if (input.AgentCode != null)
{
await AgentBinder.ApplyAgentBindingAsync(entity, input.AgentCode, AgentRepository);
}
if (input.CustomerGroup.HasValue)
{
entity.CustomerGroup = input.CustomerGroup;
}
if (input.MemberLevelId.HasValue)
{
entity.MemberLevelId = await ResolveMemberLevelIdForSaveAsync(input.MemberLevelId);
}
if (input.PriceTierKey != null)
{
entity.PriceTierKey = MarketingCustomerHelper.NormalizePriceTierKeyOrDefault(input.PriceTierKey);
}
await CustomerRepository.UpdateAsync(entity);
}
/// <summary>
/// 删除客户。
/// </summary>
/// <param name="input">入参:目标主键。</param>
/// <returns>无返回体,仅完成删除客户。</returns>
[HttpPost("/customer/delete")]
public async Task DeleteAsync(IdInput input)
{
var entity = await GetCustomerOrThrowAsync(input.Id);
if (!entity.ParentCustomerId.HasValue
&& await CustomerRepository.AnyAsync(x => x.ParentCustomerId == entity.Id))
{
throw new BusinessException("存在子客户,请先删除子客户");
}
await CustomerRepository.DeleteAsync(entity);
await DeleteLinkedRbacUserAsync(entity);
}
private async Task DeleteLinkedRbacUserAsync(Customer entity)
{
var userGuid = await GetRbacUserGuidByMapIdAsync(entity.UserId);
if (userGuid.HasValue && userGuid.Value != Guid.Empty)
{
await UserService.DeleteAsync(userGuid.Value);
}
if (entity.ParentCustomerId.HasValue)
{
await TryDeleteSubCustomerRoleAsync(entity.UserId);
}
}
private async Task<Guid?> GetRbacUserGuidByMapIdAsync(int mapId)
{
var sql = $"select `Id` as `Value` from `{FakeTables.User}` where `userid` = {{0}} and `IsDeleted` = 0";
return await RbacDbContext.Database.SqlQueryRaw<Guid?>(sql, mapId)
.FirstOrDefaultAsync();
}
private async Task TryDeleteSubCustomerRoleAsync(int userId)
{
try
{
var roles = await RoleService.GetAllRolesAsync();
var role = roles.FirstOrDefault(x =>
string.Equals(x.Code, $"CustomerSub_{userId}", StringComparison.OrdinalIgnoreCase));
if (role != null)
{
await RoleService.DeleteAsync(role.Id);
}
}
catch
{
// 子客户角色清理失败不阻断客户删除
}
}
/// <summary>
/// 重置客户密码。
/// </summary>
/// <param name="input">入参:客户密码重置参数。</param>
/// <returns>无返回体,仅完成重置客户密码。</returns>
[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();
var hasAgentFilter = input.AgentId is > 0;
var agentId = input.AgentId.GetValueOrDefault();
return customer =>
(string.IsNullOrWhiteSpace(keyword) ||
customer.CustomerCode.Contains(keyword) ||
customer.CustomerName.Contains(keyword) ||
(customer.Phone ?? string.Empty).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) &&
(!hasAgentFilter || customer.AgentId == agentId);
}
private async Task<Dictionary<int, (string Code, string Name)>> ResolveAgentDisplayMapAsync(
IEnumerable<int> agentIds)
{
var ids = agentIds.Distinct().ToList();
if (ids.Count == 0)
{
return new Dictionary<int, (string Code, string Name)>();
}
var agents = await AgentRepository.GetListAsync(x => ids.Contains(x.Id));
return agents.ToDictionary(x => x.Id, x => (x.AgentCode, x.AgentName));
}
private static void FillAgentDisplay(
CustomerPaginatedOutput output,
int? agentId,
IReadOnlyDictionary<int, (string Code, string Name)> agentMap)
{
output.AgentId = agentId;
if (agentId.HasValue && agentMap.TryGetValue(agentId.Value, out var agent))
{
output.AgentCode = agent.Code;
output.AgentName = agent.Name;
}
}
private static void FillAgentDisplay(
CustomerOutput output,
int? agentId,
IReadOnlyDictionary<int, (string Code, string Name)> agentMap)
{
output.AgentId = agentId;
if (agentId.HasValue && agentMap.TryGetValue(agentId.Value, out var agent))
{
output.AgentCode = agent.Code;
output.AgentName = agent.Name;
}
}
private static Dictionary<string, bool> BuildCustomerSortDictionary(CustomerPaginatedInput input)
{
var field = input.SortField?.Trim();
var asc = string.Equals(input.SortOrder, "asc", StringComparison.OrdinalIgnoreCase);
// Dictionary value: true=asc, false=desc(与仓库约定一致)
if (string.Equals(field, "createTime", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary<string, bool> { [nameof(Customer.CreateTime)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "customerCode", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary<string, bool> { [nameof(Customer.CustomerCode)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "customerName", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary<string, bool> { [nameof(Customer.CustomerName)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "balance", StringComparison.OrdinalIgnoreCase)
|| string.Equals(field, "accountBalance", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary<string, bool> { [nameof(Customer.Balance)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "updateTime", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary<string, bool> { [nameof(Customer.UpdateTime)] = asc, [nameof(Customer.Id)] = false };
}
return new Dictionary<string, bool> { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false };
}
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 sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}";
var userId = await RbacDbContext.Database.SqlQueryRaw<int?>(sql, 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> GetCurrentCustomerOrThrowAsync(CancellationToken cancellationToken = default)
{
var userId = CurrentUser.GetId<int>();
if (userId <= 0)
{
throw new BusinessException("客户未登录");
}
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken);
if (customer == null)
{
throw new BusinessException("客户不存在");
}
return customer;
}
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("邮箱已存在");
}
}
/// <summary>
/// 客户创建会新建 RBAC 用户,不适合直接恢复软删行;释放软删占用的唯一键即可。
/// </summary>
private async Task FreeSoftDeletedCustomerIdentityIfNeededAsync(
string customerCode,
string phone,
string? email,
int? excludeId = null)
{
var byCode = await CustomerRepository.GetByCustomerCodeIncludingDeletedAsync(customerCode);
if (byCode != null && byCode.IsDeleted && byCode.Id != excludeId)
{
byCode.CustomerCode = $"{customerCode}__deleted_{byCode.Id}";
await CustomerRepository.UpdateAsync(byCode);
}
var byPhone = await CustomerRepository.GetByPhoneIncludingDeletedAsync(phone);
if (byPhone != null && byPhone.IsDeleted && byPhone.Id != excludeId)
{
byPhone.Phone = $"{phone}__deleted_{byPhone.Id}";
await CustomerRepository.UpdateAsync(byPhone);
}
if (string.IsNullOrWhiteSpace(email))
{
return;
}
var byEmail = await CustomerRepository.GetByEmailIncludingDeletedAsync(email);
if (byEmail != null && byEmail.IsDeleted && byEmail.Id != excludeId)
{
byEmail.Email = $"{email}__deleted_{byEmail.Id}";
await CustomerRepository.UpdateAsync(byEmail);
}
}
private async Task EnsureCustomerIdentityUniqueForUpdateAsync(int id, string customerCode, string phone, string? email)
{
await FreeSoftDeletedCustomerIdentityIfNeededAsync(customerCode, phone, email, excludeId: id);
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,
MemberLevelId = input.MemberLevelId,
PriceTierKey = input.PriceTierKey
};
}
private async Task<Dictionary<int, string>> ResolveMemberLevelNameMapAsync(IEnumerable<int> memberLevelIds)
{
var ids = memberLevelIds.Distinct().ToList();
if (ids.Count == 0)
{
return new Dictionary<int, string>();
}
var levels = await MemberLevelRepository.GetListAsync(x => ids.Contains(x.Id));
return levels.ToDictionary(x => x.Id, x => x.LevelName);
}
/// <summary>创建时:未指定则默认「普通会员」等级。</summary>
private async Task<int?> ResolveMemberLevelIdForCreateAsync(int? memberLevelId)
{
if (memberLevelId.HasValue)
{
return await ResolveMemberLevelIdForSaveAsync(memberLevelId);
}
return await MarketingCustomerHelper.ResolveDefaultMemberLevelIdAsync(MemberLevelRepository);
}
private async Task<int?> ResolveMemberLevelIdForSaveAsync(int? memberLevelId)
{
if (!memberLevelId.HasValue)
{
return null;
}
var level = await MemberLevelRepository.FirstOrDefaultAsync(x => x.Id == memberLevelId.Value);
if (level == null)
{
throw new BusinessException("会员等级不存在");
}
if (level.Status != EnableStatus.Enabled)
{
throw new BusinessException("会员等级已禁用,无法分配");
}
return level.Id;
}
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);
}