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); }