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;
///
/// 客户 API 服务。
///
[ApiExplorerSettings(GroupName = "OMS-Customer")]
public class CustomerService : ApplicationService
{
private const string CustomerRoleCode = "Customer";
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService();
private IMemberLevelRepository MemberLevelRepository =>
LazyServiceProvider.GetRequiredService();
private ICustomerReferralCodeRepository ReferralCodeRepository =>
LazyServiceProvider.GetRequiredService();
private IAgentRepository AgentRepository =>
LazyServiceProvider.GetRequiredService();
private MarketingCouponManager CouponManager =>
LazyServiceProvider.GetRequiredService();
private UserService UserService => LazyServiceProvider.GetRequiredService();
private RoleService RoleService => LazyServiceProvider.GetRequiredService();
private FakeRbacDbContext RbacDbContext => LazyServiceProvider.GetRequiredService();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
LazyServiceProvider.GetRequiredService();
private CustomerServiceAdvisorResolver ServiceAdvisorResolver =>
LazyServiceProvider.GetRequiredService();
///
/// 查询客户列表。
///
/// 入参:客户分页查询条件。
/// 客户分页结果,包含列表数据和总记录数。
[HttpGet("/customer/list")]
public async Task> 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(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
{
Items = items,
Total = totalCount
};
}
///
/// 查询客户详情。
///
/// 入参:目标主键。
/// 客户详情。
[HttpGet("/customer/get")]
public async Task GetByIdAsync([FromQuery] int id)
{
var entity = await GetCustomerOrThrowAsync(id);
var output = ObjectMapper.Map(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;
}
///
/// 查询当前登录客户所属顾问信息。
///
/// 专属顾问姓名、微信号、微信二维码。
[HttpGet("/customer/service-advisor")]
public async Task GetServiceAdvisorAsync(CancellationToken cancellationToken = default)
{
var userId = CurrentUser.GetId();
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);
}
///
/// 当前登录客户联系方式完整度(下单/揽收前校验)。
///
[HttpGet("/customer/profile-completeness")]
public async Task 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
};
}
///
/// 补全手机号与邮箱(下单前)。
///
[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 同步失败不阻断补全
}
}
///
/// 新增客户(公开注册页可匿名调用;管理端也可带 token 代开户)。
///
/// 入参:客户创建参数。
/// 无返回体,仅完成新增客户。
[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());
}
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;
}
}
///
/// 创建子客户。
///
/// 入参:子客户基础资料、登录密码和授权权限编码列表。
/// 无返回体,仅完成子客户创建和RBAC用户、角色绑定。
[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;
}
}
///
/// 更新客户。
///
/// 入参:客户更新参数。
/// 无返回体,仅完成更新客户。
[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);
}
///
/// 删除客户。
///
/// 入参:目标主键。
/// 无返回体,仅完成删除客户。
[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 GetRbacUserGuidByMapIdAsync(int mapId)
{
var sql = $"select `Id` as `Value` from `{FakeTables.User}` where `userid` = {{0}} and `IsDeleted` = 0";
return await RbacDbContext.Database.SqlQueryRaw(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
{
// 子客户角色清理失败不阻断客户删除
}
}
///
/// 重置客户密码。
///
/// 入参:客户密码重置参数。
/// 无返回体,仅完成重置客户密码。
[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> 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> ResolveAgentDisplayMapAsync(
IEnumerable agentIds)
{
var ids = agentIds.Distinct().ToList();
if (ids.Count == 0)
{
return new Dictionary();
}
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 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 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 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 { [nameof(Customer.CreateTime)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "customerCode", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary { [nameof(Customer.CustomerCode)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "customerName", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary { [nameof(Customer.CustomerName)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "balance", StringComparison.OrdinalIgnoreCase)
|| string.Equals(field, "accountBalance", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary { [nameof(Customer.Balance)] = asc, [nameof(Customer.Id)] = false };
}
if (string.Equals(field, "updateTime", StringComparison.OrdinalIgnoreCase))
{
return new Dictionary { [nameof(Customer.UpdateTime)] = asc, [nameof(Customer.Id)] = false };
}
return new Dictionary { [nameof(Customer.UpdateTime)] = false, [nameof(Customer.Id)] = false };
}
private async Task GetCustomerOrThrowAsync(int id)
{
var entity = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
throw new BusinessException("客户不存在");
}
return entity;
}
private async Task GetRbacUserMapIdOrThrowAsync(Guid id)
{
var sql = $"select `userid` as `Value` from `{FakeTables.User}` where `Id` = {{0}}";
var userId = await RbacDbContext.Database.SqlQueryRaw(sql, id)
.FirstOrDefaultAsync();
if (!userId.HasValue || userId.Value <= 0)
{
throw new BusinessException("用户编号创建失败");
}
return userId.Value;
}
private async Task 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 GetCurrentCustomerOrThrowAsync(CancellationToken cancellationToken = default)
{
var userId = CurrentUser.GetId();
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 GetCurrentMainCustomerOrThrowAsync()
{
var userId = CurrentUser.GetId();
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("邮箱已存在");
}
}
///
/// 客户创建会新建 RBAC 用户,不适合直接恢复软删行;释放软删占用的唯一键即可。
///
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 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> ResolveMemberLevelNameMapAsync(IEnumerable memberLevelIds)
{
var ids = memberLevelIds.Distinct().ToList();
if (ids.Count == 0)
{
return new Dictionary();
}
var levels = await MemberLevelRepository.GetListAsync(x => ids.Contains(x.Id));
return levels.ToDictionary(x => x.Id, x => x.LevelName);
}
/// 创建时:未指定则默认「普通会员」等级。
private async Task ResolveMemberLevelIdForCreateAsync(int? memberLevelId)
{
if (memberLevelId.HasValue)
{
return await ResolveMemberLevelIdForSaveAsync(memberLevelId);
}
return await MarketingCustomerHelper.ResolveDefaultMemberLevelIdAsync(MemberLevelRepository);
}
private async Task 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 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> NormalizeAndValidateSubCustomerPermissionsAsync(IEnumerable? 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 CreateSubCustomerRoleAsync(int userId, string customerName, List 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);
}