From 3818ce7876b336aa864206e7f2ff03eb9c4e0617 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Thu, 9 Jul 2026 11:23:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20RBAC=E7=94=A8=E6=88=B7=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E3=80=81=E4=B8=93=E5=B1=9E=E9=A1=BE=E9=97=AE=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E5=8F=8A=E4=BB=93=E5=BA=93=E5=AD=97=E6=AE=B5=E6=B8=85?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 接管用户 API 支持电话/工号/微信号/微信二维码,新增客户专属顾问查询,并移除仓库客服电话与微信相关字段。 Co-authored-by: Cursor --- db.sql | 3 - sql/migrate_rbac_user_profile.sql | 10 + ..._warehouse_address_drop_service_fields.sql | 6 + .../Customers/CustomerServiceAdvisorOutput.cs | 22 ++ .../Dtos/Rbac/InternalUserListDtos.cs | 6 + .../Dtos/Rbac/OmsUserProfileDtos.cs | 45 ++++ .../Dtos/System/CreateWarehouseInput.cs | 17 -- .../Dtos/System/UpdateWarehouseInput.cs | 17 -- .../Dtos/System/WarehouseOutput.cs | 15 -- .../Dtos/System/WarehousePaginatedOutput.cs | 15 -- .../OmsApplicationAutoMapperProfile.cs | 8 +- .../Rbac/CustomerServiceAdvisorResolver.cs | 86 +++++++ .../Rbac/InternalUserRepository.cs | 12 + .../Rbac/OmsUserProfileMapper.cs | 37 +++ .../Rbac/UserProfileRepository.cs | 226 ++++++++++++++++++ .../Services/Customers/CustomerService.cs | 26 ++ .../Services/Rbac/OmsUserService.cs | 192 +++++++++++++++ .../Services/System/WarehouseService.cs | 6 - .../Entities/WarehouseAddress.cs | 7 - .../WarehouseApiIntegrationTests.cs | 27 +-- src/SimpleAdmin.Api/SimpleAdminApiModule.cs | 4 +- 21 files changed, 679 insertions(+), 108 deletions(-) create mode 100644 sql/migrate_rbac_user_profile.sql create mode 100644 sql/migrate_t_warehouse_address_drop_service_fields.sql create mode 100644 src/Oms.Application/Dtos/Customers/CustomerServiceAdvisorOutput.cs create mode 100644 src/Oms.Application/Dtos/Rbac/OmsUserProfileDtos.cs create mode 100644 src/Oms.Application/Rbac/CustomerServiceAdvisorResolver.cs create mode 100644 src/Oms.Application/Rbac/OmsUserProfileMapper.cs create mode 100644 src/Oms.Application/Rbac/UserProfileRepository.cs create mode 100644 src/Oms.Application/Services/Rbac/OmsUserService.cs diff --git a/db.sql b/db.sql index 088135b..c158ea8 100644 --- a/db.sql +++ b/db.sql @@ -1125,9 +1125,6 @@ CREATE TABLE `t_warehouse_address` ( `Recipient` VARCHAR(64) NULL COMMENT '收件人', `ContactPhone` VARCHAR(32) NULL COMMENT '联系电话', `ShippingAddress` TEXT COMMENT '收货地址', - `ServicePhone` VARCHAR(32) NULL COMMENT '客服电话', - `ServiceWechat` VARCHAR(64) NULL COMMENT '客服微信', - `ServiceWechatFiles` JSON NULL COMMENT '微信二维码图片', `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', diff --git a/sql/migrate_rbac_user_profile.sql b/sql/migrate_rbac_user_profile.sql new file mode 100644 index 0000000..881843d --- /dev/null +++ b/sql/migrate_rbac_user_profile.sql @@ -0,0 +1,10 @@ +-- RBAC 用户扩展:电话、工号、微信号、微信二维码 +-- 执行前请确认当前库为 RBAC 库(含 `user` 表) + +ALTER TABLE `user` + ADD COLUMN `Phone` VARCHAR(32) NULL COMMENT '电话' AFTER `Avatar`, + ADD COLUMN `EmployeeNo` VARCHAR(16) NULL COMMENT '工号(NE+4位)' AFTER `Phone`, + ADD COLUMN `WechatId` VARCHAR(64) NULL COMMENT '微信号' AFTER `EmployeeNo`, + ADD COLUMN `WechatQrCode` LONGTEXT NULL COMMENT '微信二维码' AFTER `WechatId`; + +CREATE UNIQUE INDEX `IX_user_EmployeeNo` ON `user` (`EmployeeNo`); diff --git a/sql/migrate_t_warehouse_address_drop_service_fields.sql b/sql/migrate_t_warehouse_address_drop_service_fields.sql new file mode 100644 index 0000000..78d4d81 --- /dev/null +++ b/sql/migrate_t_warehouse_address_drop_service_fields.sql @@ -0,0 +1,6 @@ +-- 仓库地址表移除客服电话、客服微信及二维码字段 + +ALTER TABLE `t_warehouse_address` + DROP COLUMN `ServicePhone`, + DROP COLUMN `ServiceWechat`, + DROP COLUMN `ServiceWechatFiles`; diff --git a/src/Oms.Application/Dtos/Customers/CustomerServiceAdvisorOutput.cs b/src/Oms.Application/Dtos/Customers/CustomerServiceAdvisorOutput.cs new file mode 100644 index 0000000..e3a95fc --- /dev/null +++ b/src/Oms.Application/Dtos/Customers/CustomerServiceAdvisorOutput.cs @@ -0,0 +1,22 @@ +namespace Oms.Application.Dtos.Customers; + +/// +/// 当前客户所属客服(专属顾问)展示信息。 +/// +public class CustomerServiceAdvisorOutput +{ + /// + /// 顾问姓名。 + /// + public string? Name { get; set; } + + /// + /// 微信号。 + /// + public string? WechatId { get; set; } + + /// + /// 微信二维码(URL 或 Base64)。 + /// + public string? WechatQrCode { get; set; } +} diff --git a/src/Oms.Application/Dtos/Rbac/InternalUserListDtos.cs b/src/Oms.Application/Dtos/Rbac/InternalUserListDtos.cs index 2daa056..55fd380 100644 --- a/src/Oms.Application/Dtos/Rbac/InternalUserListDtos.cs +++ b/src/Oms.Application/Dtos/Rbac/InternalUserListDtos.cs @@ -14,6 +14,12 @@ public class InternalUserListItemDto public string? Avatar { get; set; } + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + public string? OrganizationId { get; set; } public string? OrganizationName { get; set; } diff --git a/src/Oms.Application/Dtos/Rbac/OmsUserProfileDtos.cs b/src/Oms.Application/Dtos/Rbac/OmsUserProfileDtos.cs new file mode 100644 index 0000000..9ea1153 --- /dev/null +++ b/src/Oms.Application/Dtos/Rbac/OmsUserProfileDtos.cs @@ -0,0 +1,45 @@ +using Fake.Rbac.Application.Dtos.User; + +namespace Oms.Application.Dtos.Rbac; + +public class UserProfileData +{ + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } +} + +public class OmsUserCreateInput : UserCreateDto +{ + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } +} + +public class OmsUserUpdateInput : UserUpdateDto +{ + public string? Phone { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } +} + +public class OmsUserDetailDto : UserDto +{ + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } +} diff --git a/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs b/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs index 31100c7..50c4a90 100644 --- a/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs +++ b/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs @@ -37,21 +37,4 @@ public class CreateWarehouseInput /// 发货地址。 /// public string? ShippingAddress { get; set; } - - /// - /// 客服手机号。 - /// - [StringLength(32)] - public string? ServicePhone { get; set; } - - /// - /// 客服微信号。 - /// - [StringLength(64)] - public string? ServiceWechat { get; set; } - - /// - /// 客服微信附件集合。 - /// - public List ServiceWechatFiles { get; set; } = []; } diff --git a/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs b/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs index 261e6e2..ea8b6af 100644 --- a/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs +++ b/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs @@ -43,21 +43,4 @@ public class UpdateWarehouseInput /// 发货地址。 /// public string? ShippingAddress { get; set; } - - /// - /// 客服手机号。 - /// - [StringLength(32)] - public string? ServicePhone { get; set; } - - /// - /// 客服微信号。 - /// - [StringLength(64)] - public string? ServiceWechat { get; set; } - - /// - /// 客服微信附件集合。 - /// - public List ServiceWechatFiles { get; set; } = []; } diff --git a/src/Oms.Application/Dtos/System/WarehouseOutput.cs b/src/Oms.Application/Dtos/System/WarehouseOutput.cs index 17a3337..95bf877 100644 --- a/src/Oms.Application/Dtos/System/WarehouseOutput.cs +++ b/src/Oms.Application/Dtos/System/WarehouseOutput.cs @@ -35,21 +35,6 @@ public class WarehouseOutput /// public string? ShippingAddress { get; set; } - /// - /// 客服手机号。 - /// - public string? ServicePhone { get; set; } - - /// - /// 客服微信号。 - /// - public string? ServiceWechat { get; set; } - - /// - /// 客服微信附件集合。 - /// - public List ServiceWechatFiles { get; set; } = []; - /// /// 创建时间。 /// diff --git a/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs b/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs index cea608a..a6b792e 100644 --- a/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs +++ b/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs @@ -40,21 +40,6 @@ public class WarehousePaginatedOutput /// public string? ShippingAddress { get; set; } - /// - /// 客服手机号。 - /// - public string? ServicePhone { get; set; } - - /// - /// 客服微信号。 - /// - public string? ServiceWechat { get; set; } - - /// - /// 客服微信附件集合。 - /// - public List ServiceWechatFiles { get; set; } = []; - /// /// 创建时间。 /// diff --git a/src/Oms.Application/Profiles/OmsApplicationAutoMapperProfile.cs b/src/Oms.Application/Profiles/OmsApplicationAutoMapperProfile.cs index c234007..17f06a0 100644 --- a/src/Oms.Application/Profiles/OmsApplicationAutoMapperProfile.cs +++ b/src/Oms.Application/Profiles/OmsApplicationAutoMapperProfile.cs @@ -28,12 +28,8 @@ public class OmsApplicationAutoMapperProfile : Profile CreateMap(); CreateMap(); CreateMap(); - CreateMap() - .ForMember(dest => dest.ServiceWechatFiles, - opt => opt.MapFrom(src => JsonStringArrayHelper.Deserialize(src.ServiceWechatFiles))); - CreateMap() - .ForMember(dest => dest.ServiceWechatFiles, - opt => opt.MapFrom(src => JsonStringArrayHelper.Deserialize(src.ServiceWechatFiles))); + CreateMap(); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); diff --git a/src/Oms.Application/Rbac/CustomerServiceAdvisorResolver.cs b/src/Oms.Application/Rbac/CustomerServiceAdvisorResolver.cs new file mode 100644 index 0000000..7d11d34 --- /dev/null +++ b/src/Oms.Application/Rbac/CustomerServiceAdvisorResolver.cs @@ -0,0 +1,86 @@ +using Fake.DependencyInjection; +using Fake.Rbac.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Oms.Application.Dtos.Customers; + +namespace Oms.Application.Rbac; + +public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScopedDependency +{ + public async Task ResolveAsync( + string? customerServiceKey, + CancellationToken cancellationToken = default) + { + var key = customerServiceKey?.Trim(); + if (string.IsNullOrWhiteSpace(key)) + { + return new CustomerServiceAdvisorOutput(); + } + + var row = await FindByUserIdAsync(key, cancellationToken) + ?? await FindByNameOrAccountAsync(key, cancellationToken); + + if (row == null) + { + return new CustomerServiceAdvisorOutput { Name = key }; + } + + return new CustomerServiceAdvisorOutput + { + Name = row.Name, + WechatId = row.WechatId, + WechatQrCode = row.WechatQrCode, + }; + } + + private async Task FindByUserIdAsync(string key, CancellationToken cancellationToken) + { + if (!int.TryParse(key, out var userId) || userId <= 0) + { + return null; + } + + const string sql = """ + SELECT + u.`Name` AS `Name`, + u.`WechatId` AS `WechatId`, + u.`WechatQrCode` AS `WechatQrCode` + FROM `user` u + WHERE u.`IsDeleted` = 0 + AND u.`userid` = {0} + LIMIT 1 + """; + + return await dbContext.Database + .SqlQueryRaw(sql, userId) + .FirstOrDefaultAsync(cancellationToken); + } + + private async Task FindByNameOrAccountAsync(string key, CancellationToken cancellationToken) + { + const string sql = """ + SELECT + u.`Name` AS `Name`, + u.`WechatId` AS `WechatId`, + u.`WechatQrCode` AS `WechatQrCode` + FROM `user` u + WHERE u.`IsDeleted` = 0 + AND (u.`Name` = {0} OR u.`Account` = {0}) + ORDER BY u.`CreateTime` ASC + LIMIT 1 + """; + + return await dbContext.Database + .SqlQueryRaw(sql, key) + .FirstOrDefaultAsync(cancellationToken); + } + + private sealed class AdvisorRow + { + public string? Name { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } + } +} diff --git a/src/Oms.Application/Rbac/InternalUserRepository.cs b/src/Oms.Application/Rbac/InternalUserRepository.cs index c12bcea..2c48839 100644 --- a/src/Oms.Application/Rbac/InternalUserRepository.cs +++ b/src/Oms.Application/Rbac/InternalUserRepository.cs @@ -61,6 +61,9 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend u.`Account` AS `Account`, u.`Email` AS `Email`, u.`Avatar` AS `Avatar`, + u.`Phone` AS `Phone`, + u.`EmployeeNo` AS `EmployeeNo`, + u.`WechatId` AS `WechatId`, u.`OrganizationId` AS `OrganizationId`, o.`Name` AS `OrganizationName`, u.`CreateTime` AS `CreatedAt` @@ -87,6 +90,9 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend Account = row.Account, Email = row.Email, Avatar = row.Avatar, + Phone = row.Phone, + EmployeeNo = row.EmployeeNo, + WechatId = row.WechatId, OrganizationId = row.OrganizationId?.ToString(), OrganizationName = row.OrganizationName, CreatedAt = row.CreatedAt, @@ -143,6 +149,12 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend public string? Avatar { get; set; } + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + public Guid? OrganizationId { get; set; } public string? OrganizationName { get; set; } diff --git a/src/Oms.Application/Rbac/OmsUserProfileMapper.cs b/src/Oms.Application/Rbac/OmsUserProfileMapper.cs new file mode 100644 index 0000000..84d7761 --- /dev/null +++ b/src/Oms.Application/Rbac/OmsUserProfileMapper.cs @@ -0,0 +1,37 @@ +using Fake.Rbac.Application.Dtos.User; +using Oms.Application.Dtos.Rbac; + +namespace Oms.Application.Rbac; + +internal static class OmsUserProfileMapper +{ + public static OmsUserDetailDto ToDetailDto(UserDto user, UserProfileData? profile) + { + var detail = new OmsUserDetailDto + { + Id = user.Id, + CreatedAt = user.CreatedAt, + CreatedBy = user.CreatedBy, + UpdatedAt = user.UpdatedAt, + UpdatedBy = user.UpdatedBy, + Name = user.Name, + Account = user.Account, + Email = user.Email, + Avatar = user.Avatar, + OrganizationId = user.OrganizationId, + OrganizationName = user.OrganizationName, + Roles = user.Roles, + }; + + if (profile == null) + { + return detail; + } + + detail.Phone = profile.Phone; + detail.EmployeeNo = profile.EmployeeNo; + detail.WechatId = profile.WechatId; + detail.WechatQrCode = profile.WechatQrCode; + return detail; + } +} diff --git a/src/Oms.Application/Rbac/UserProfileRepository.cs b/src/Oms.Application/Rbac/UserProfileRepository.cs new file mode 100644 index 0000000..af0b26a --- /dev/null +++ b/src/Oms.Application/Rbac/UserProfileRepository.cs @@ -0,0 +1,226 @@ +using System.Text.RegularExpressions; +using Fake.DependencyInjection; +using Fake.Rbac.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Oms.Application.Dtos.Rbac; + +namespace Oms.Application.Rbac; + +public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDependency +{ + private static readonly Regex EmployeeNoPattern = new(@"^NE\d{4}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + public async Task GetAsync(Guid userId, CancellationToken cancellationToken = default) + { + const string sql = """ + SELECT + u.`Phone` AS `Phone`, + u.`EmployeeNo` AS `EmployeeNo`, + u.`WechatId` AS `WechatId`, + u.`WechatQrCode` AS `WechatQrCode` + FROM `user` u + WHERE u.`Id` = {0} AND u.`IsDeleted` = 0 + LIMIT 1 + """; + + var row = await dbContext.Database + .SqlQueryRaw(sql, userId.ToString()) + .FirstOrDefaultAsync(cancellationToken); + + if (row == null) + { + return null; + } + + return new UserProfileData + { + Phone = row.Phone, + EmployeeNo = row.EmployeeNo, + WechatId = row.WechatId, + WechatQrCode = row.WechatQrCode, + }; + } + + public async Task> GetManyAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken = default) + { + if (userIds.Count == 0) + { + return []; + } + + var inClause = string.Join(",", userIds.Select(x => $"'{x}'")); + var sql = $""" + SELECT + u.`Id` AS `UserId`, + u.`Phone` AS `Phone`, + u.`EmployeeNo` AS `EmployeeNo`, + u.`WechatId` AS `WechatId`, + u.`WechatQrCode` AS `WechatQrCode` + FROM `user` u + WHERE u.`Id` IN ({inClause}) AND u.`IsDeleted` = 0 + """; + + var rows = await dbContext.Database.SqlQueryRaw(sql).ToListAsync(cancellationToken); + return rows.ToDictionary( + x => x.UserId, + x => new UserProfileData + { + Phone = x.Phone, + EmployeeNo = x.EmployeeNo, + WechatId = x.WechatId, + WechatQrCode = x.WechatQrCode, + }); + } + + public async Task SaveAsync(Guid userId, UserProfileData profile, CancellationToken cancellationToken = default) + { + const string sql = """ + UPDATE `user` + SET + `Phone` = {1}, + `EmployeeNo` = {2}, + `WechatId` = {3}, + `WechatQrCode` = {4} + WHERE `Id` = {0} AND `IsDeleted` = 0 + """; + + await dbContext.Database.ExecuteSqlRawAsync( + sql, + [ + userId.ToString(), + NormalizeOptional(profile.Phone), + NormalizeOptional(profile.EmployeeNo), + NormalizeOptional(profile.WechatId), + NormalizeOptional(profile.WechatQrCode), + ], + cancellationToken); + } + + public async Task UpdateContactAsync( + Guid userId, + UserProfileData profile, + CancellationToken cancellationToken = default) + { + const string sql = """ + UPDATE `user` + SET + `Phone` = {1}, + `WechatId` = {2}, + `WechatQrCode` = {3} + WHERE `Id` = {0} AND `IsDeleted` = 0 + """; + + await dbContext.Database.ExecuteSqlRawAsync( + sql, + [ + userId.ToString(), + NormalizeOptional(profile.Phone), + NormalizeOptional(profile.WechatId), + NormalizeOptional(profile.WechatQrCode), + ], + cancellationToken); + } + + public async Task IsEmployeeNoTakenAsync( + string employeeNo, + Guid? excludeUserId, + CancellationToken cancellationToken = default) + { + var normalized = employeeNo.Trim().ToUpperInvariant(); + var sql = excludeUserId.HasValue + ? """ + SELECT COUNT(*) AS `Value` + FROM `user` u + WHERE u.`IsDeleted` = 0 + AND u.`EmployeeNo` = {0} + AND u.`Id` <> {1} + """ + : """ + SELECT COUNT(*) AS `Value` + FROM `user` u + WHERE u.`IsDeleted` = 0 + AND u.`EmployeeNo` = {0} + """; + + var count = excludeUserId.HasValue + ? await dbContext.Database.SqlQueryRaw(sql, normalized, excludeUserId.Value.ToString()) + .FirstAsync(cancellationToken) + : await dbContext.Database.SqlQueryRaw(sql, normalized).FirstAsync(cancellationToken); + + return count > 0; + } + + public async Task GenerateNextEmployeeNoAsync(CancellationToken cancellationToken = default) + { + const string sql = """ + SELECT u.`EmployeeNo` AS `EmployeeNo` + FROM `user` u + WHERE u.`IsDeleted` = 0 + AND u.`EmployeeNo` IS NOT NULL + AND u.`EmployeeNo` <> '' + """; + + var rows = await dbContext.Database.SqlQueryRaw(sql).ToListAsync(cancellationToken); + var max = 0; + foreach (var row in rows) + { + var value = row.EmployeeNo?.Trim() ?? string.Empty; + var match = EmployeeNoPattern.Match(value); + if (!match.Success) + { + continue; + } + + if (int.TryParse(value[2..], out var number)) + { + max = Math.Max(max, number); + } + } + + var next = max + 1; + return next > 9999 ? $"NE{next}" : $"NE{next:D4}"; + } + + public static bool IsValidEmployeeNo(string employeeNo) => EmployeeNoPattern.IsMatch(employeeNo.Trim()); + + private static string? NormalizeOptional(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + return value.Trim(); + } + + private sealed class UserProfileRow + { + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } + } + + private sealed class UserProfileListRow + { + public Guid UserId { get; set; } + + public string? Phone { get; set; } + + public string? EmployeeNo { get; set; } + + public string? WechatId { get; set; } + + public string? WechatQrCode { get; set; } + } + + private sealed class EmployeeNoRow + { + public string? EmployeeNo { get; set; } + } +} diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs index 4ca749a..c723477 100644 --- a/src/Oms.Application/Services/Customers/CustomerService.cs +++ b/src/Oms.Application/Services/Customers/CustomerService.cs @@ -9,6 +9,7 @@ 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; @@ -35,6 +36,9 @@ public class CustomerService : ApplicationService private OmsRbacUserDisplayNameResolver UserDisplayNameResolver => LazyServiceProvider.GetRequiredService(); + private CustomerServiceAdvisorResolver ServiceAdvisorResolver => + LazyServiceProvider.GetRequiredService(); + /// /// 查询客户列表。 /// @@ -90,6 +94,28 @@ public class CustomerService : ApplicationService 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); + } + /// /// 新增客户。 /// diff --git a/src/Oms.Application/Services/Rbac/OmsUserService.cs b/src/Oms.Application/Services/Rbac/OmsUserService.cs new file mode 100644 index 0000000..f7751eb --- /dev/null +++ b/src/Oms.Application/Services/Rbac/OmsUserService.cs @@ -0,0 +1,192 @@ +using Fake.Application; +using Fake.Rbac.Application.Dtos.Common; +using Fake.Rbac.Application.Dtos.Role; +using Fake.Rbac.Application.Dtos.User; +using Fake.Rbac.Application.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oms.Application.Dtos.Rbac; +using Oms.Application.Rbac; + +namespace Oms.Application.Services.Rbac; + +/// +/// 接管 RBAC 用户接口,扩展电话、工号、微信号、微信二维码字段。 +/// +[Authorize] +[ApiExplorerSettings(GroupName = "RBAC-User")] +public class OmsUserService(UserService userService, UserProfileRepository profileRepository) : ApplicationService +{ + [HttpGet("/rbac/user/get")] + public async Task GetAsync([FromQuery] Guid id, CancellationToken cancellationToken = default) + { + var user = await userService.GetAsync(id, cancellationToken); + var profile = await profileRepository.GetAsync(id, cancellationToken); + return OmsUserProfileMapper.ToDetailDto(user, profile); + } + + [HttpGet("/rbac/user/get-list")] + public async Task> GetListAsync( + [FromQuery] UserPagedRequestDto input, + CancellationToken cancellationToken = default) + { + var result = await userService.GetListAsync(input, cancellationToken); + if (result.Items.Count == 0) + { + return new PagedResultDto + { + Total = result.Total, + Items = [], + }; + } + + var userIds = result.Items + .Select(x => x.Id) + .Where(x => x != Guid.Empty) + .Distinct() + .ToList(); + var profileMap = await profileRepository.GetManyAsync(userIds, cancellationToken); + return new PagedResultDto + { + Total = result.Total, + Items = result.Items + .Select(user => + { + profileMap.TryGetValue(user.Id, out var profile); + return OmsUserProfileMapper.ToDetailDto(user, profile); + }) + .ToList(), + }; + } + + [HttpGet("/rbac/user/get-users-by-role")] + public Task> GetUsersByRoleAsync( + [FromQuery] Guid roleId, + CancellationToken cancellationToken = default) => + userService.GetUsersByRoleAsync(roleId, cancellationToken); + + [HttpPost("/rbac/user/create")] + public async Task CreateAsync( + [FromBody] OmsUserCreateInput input, + CancellationToken cancellationToken = default) + { + var employeeNo = await ResolveEmployeeNoForCreateAsync(input.EmployeeNo, cancellationToken); + var user = await userService.CreateAsync(new UserCreateDto + { + Name = input.Name, + Account = input.Account, + Password = input.Password, + Email = input.Email, + Avatar = input.Avatar, + OrganizationId = input.OrganizationId, + RoleIds = input.RoleIds, + }, cancellationToken); + + await profileRepository.SaveAsync( + user.Id, + new UserProfileData + { + Phone = input.Phone, + EmployeeNo = employeeNo, + WechatId = input.WechatId, + WechatQrCode = input.WechatQrCode, + }, + cancellationToken); + + var profile = await profileRepository.GetAsync(user.Id, cancellationToken); + return OmsUserProfileMapper.ToDetailDto(user, profile); + } + + [HttpPut("/rbac/user/update")] + public async Task UpdateAsync( + [FromQuery] Guid id, + [FromBody] OmsUserUpdateInput input, + CancellationToken cancellationToken = default) + { + var user = await userService.UpdateAsync( + id, + new UserUpdateDto + { + Name = input.Name, + Email = input.Email, + Avatar = input.Avatar, + OrganizationId = input.OrganizationId, + }, + cancellationToken); + + await profileRepository.UpdateContactAsync( + id, + new UserProfileData + { + Phone = input.Phone, + WechatId = input.WechatId, + WechatQrCode = input.WechatQrCode, + }, + cancellationToken); + + var profile = await profileRepository.GetAsync(id, cancellationToken); + return OmsUserProfileMapper.ToDetailDto(user, profile); + } + + [HttpPut("/rbac/user/update-password")] + public Task UpdatePasswordAsync( + [FromQuery] Guid id, + [FromBody] UpdatePasswordDto input, + CancellationToken cancellationToken = default) => + userService.UpdatePasswordAsync(id, input, cancellationToken); + + [HttpPut("/rbac/user/update-avatar")] + public Task UpdateAvatarAsync( + [FromQuery] Guid id, + [FromQuery] string avatarUrl, + CancellationToken cancellationToken = default) => + userService.UpdateAvatarAsync(id, avatarUrl, cancellationToken); + + [HttpDelete("/rbac/user/delete")] + public Task DeleteAsync([FromQuery] Guid id, CancellationToken cancellationToken = default) => + userService.DeleteAsync(id, cancellationToken); + + [HttpDelete("/rbac/user/delete-batch")] + public Task DeleteBatchAsync([FromQuery] List ids, CancellationToken cancellationToken = default) => + userService.DeleteBatchAsync(ids, cancellationToken); + + [HttpPost("/rbac/user/assign-roles")] + public Task AssignRolesAsync( + [FromQuery] Guid userId, + [FromBody] List roleIds, + CancellationToken cancellationToken = default) => + userService.AssignRolesAsync(userId, roleIds, cancellationToken); + + [HttpGet("/rbac/user/get-user-roles")] + public Task> GetUserRolesAsync( + [FromQuery] Guid userId, + CancellationToken cancellationToken = default) => + userService.GetUserRolesAsync(userId, cancellationToken); + + [HttpGet("/rbac/user/get-user-permissions")] + public Task> GetUserPermissionsAsync( + [FromQuery] Guid userId, + CancellationToken cancellationToken = default) => + userService.GetUserPermissionsAsync(userId, cancellationToken); + + private async Task ResolveEmployeeNoForCreateAsync( + string? requestedEmployeeNo, + CancellationToken cancellationToken) + { + var employeeNo = string.IsNullOrWhiteSpace(requestedEmployeeNo) + ? await profileRepository.GenerateNextEmployeeNoAsync(cancellationToken) + : requestedEmployeeNo.Trim().ToUpperInvariant(); + + if (!UserProfileRepository.IsValidEmployeeNo(employeeNo)) + { + throw new BusinessException("工号格式无效,应为 NE + 4 位数字"); + } + + if (await profileRepository.IsEmployeeNoTakenAsync(employeeNo, null, cancellationToken)) + { + throw new BusinessException($"工号已存在:{employeeNo}"); + } + + return employeeNo; + } +} diff --git a/src/Oms.Application/Services/System/WarehouseService.cs b/src/Oms.Application/Services/System/WarehouseService.cs index 0ce5d66..2e27f6c 100644 --- a/src/Oms.Application/Services/System/WarehouseService.cs +++ b/src/Oms.Application/Services/System/WarehouseService.cs @@ -94,9 +94,6 @@ public class WarehouseService : ApplicationService Recipient = input.Recipient, ContactPhone = input.ContactPhone, ShippingAddress = input.ShippingAddress, - ServicePhone = input.ServicePhone, - ServiceWechat = input.ServiceWechat, - ServiceWechatFiles = JsonStringArrayHelper.Serialize(input.ServiceWechatFiles) }; await WarehouseAddressRepository.InsertAsync(entity); @@ -122,9 +119,6 @@ public class WarehouseService : ApplicationService entity.Recipient = input.Recipient; entity.ContactPhone = input.ContactPhone; entity.ShippingAddress = input.ShippingAddress; - entity.ServicePhone = input.ServicePhone; - entity.ServiceWechat = input.ServiceWechat; - entity.ServiceWechatFiles = JsonStringArrayHelper.Serialize(input.ServiceWechatFiles); await WarehouseAddressRepository.UpdateAsync(entity); } diff --git a/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs b/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs index 22f06d7..c9d981e 100644 --- a/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs +++ b/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs @@ -15,11 +15,4 @@ public class WarehouseAddress : OmsAuditedAggregateRoot public string? ContactPhone { get; set; } public string? ShippingAddress { get; set; } - - public string? ServicePhone { get; set; } - - public string? ServiceWechat { get; set; } - - [SugarColumn(ColumnDataType = "json", IsNullable = true)] - public string? ServiceWechatFiles { get; set; } } diff --git a/src/SimpleAdmin.Api.IntegrationTests/WarehouseApiIntegrationTests.cs b/src/SimpleAdmin.Api.IntegrationTests/WarehouseApiIntegrationTests.cs index 3f53a85..09d4e78 100644 --- a/src/SimpleAdmin.Api.IntegrationTests/WarehouseApiIntegrationTests.cs +++ b/src/SimpleAdmin.Api.IntegrationTests/WarehouseApiIntegrationTests.cs @@ -34,9 +34,6 @@ public class WarehouseApiIntegrationTests Recipient = "Alice", ContactPhone = "13800138000", ShippingAddress = "London", - ServicePhone = "020-123456", - ServiceWechat = "oms-warehouse", - ServiceWechatFiles = "[\"wx-1.png\",\"wx-2.png\"]", CreateTime = new DateTime(2026, 4, 29, 9, 0, 0), UpdateTime = new DateTime(2026, 4, 29, 10, 0, 0) } @@ -61,11 +58,11 @@ public class WarehouseApiIntegrationTests payload!.Total.Should().Be(1); payload.Items.Should().ContainSingle(); payload.Items[0].WarehouseTypeName.Should().Be("中转仓"); - payload.Items[0].ServiceWechatFiles.Should().ContainInOrder("wx-1.png", "wx-2.png"); + payload.Items[0].ShippingAddress.Should().Be("London"); } [Fact] - public async Task GetWarehouseDetail_ShouldReturnMappedAuditAndFileFields() + public async Task GetWarehouseDetail_ShouldReturnMappedAuditFields() { var repository = new Mock(MockBehavior.Strict); repository @@ -80,9 +77,6 @@ public class WarehouseApiIntegrationTests Recipient = "Bob", ContactPhone = "13900139000", ShippingAddress = "Shenzhen", - ServicePhone = "0755-123456", - ServiceWechat = "main-hub", - ServiceWechatFiles = "[\"main.png\"]", CreateTime = new DateTime(2026, 4, 20, 9, 30, 0), CreateUserId = 66, UpdateTime = new DateTime(2026, 4, 21, 14, 0, 0), @@ -102,11 +96,11 @@ public class WarehouseApiIntegrationTests payload.Should().NotBeNull(); payload!.CreateBy.Should().Be("66"); payload.UpdateBy.Should().Be("88"); - payload.ServiceWechatFiles.Should().ContainSingle().Which.Should().Be("main.png"); + payload.ShippingAddress.Should().Be("Shenzhen"); } [Fact] - public async Task CreateWarehouse_ShouldSerializeFilesBeforePersisting() + public async Task CreateWarehouse_ShouldPersistEntity() { WarehouseAddress? insertedEntity = null; var repository = new Mock(MockBehavior.Strict); @@ -131,16 +125,13 @@ public class WarehouseApiIntegrationTests Recipient = "Cindy", ContactPhone = "13700137000", ShippingAddress = "Guangzhou", - ServicePhone = "020-888888", - ServiceWechat = "return-hub", - ServiceWechatFiles = ["qr-a.png", "qr-b.png"] }); response.IsSuccessStatusCode.Should().BeTrue(); insertedEntity.Should().NotBeNull(); insertedEntity!.WarehouseName.Should().Be("New Warehouse"); insertedEntity.WarehouseType.Should().Be(WarehouseTypeCodes.Return); - insertedEntity.ServiceWechatFiles.Should().Be("[\"qr-a.png\",\"qr-b.png\"]"); + insertedEntity.ShippingAddress.Should().Be("Guangzhou"); } [Fact] @@ -155,9 +146,6 @@ public class WarehouseApiIntegrationTests Recipient = "Old", ContactPhone = "13600136000", ShippingAddress = "Old Address", - ServicePhone = "010-111111", - ServiceWechat = "old-wechat", - ServiceWechatFiles = "[\"old.png\"]" }; var repository = new Mock(MockBehavior.Strict); @@ -188,9 +176,6 @@ public class WarehouseApiIntegrationTests Recipient = "Daisy", ContactPhone = "13500135000", ShippingAddress = "New Address", - ServicePhone = "0755-999999", - ServiceWechat = "new-wechat", - ServiceWechatFiles = ["new-a.png", "new-b.png"] }); response.IsSuccessStatusCode.Should().BeTrue(); @@ -198,6 +183,6 @@ public class WarehouseApiIntegrationTests updatedEntity!.WarehouseName.Should().Be("Updated Warehouse"); updatedEntity.WarehouseType.Should().Be(WarehouseTypeCodes.Main); updatedEntity.Recipient.Should().Be("Daisy"); - updatedEntity.ServiceWechatFiles.Should().Be("[\"new-a.png\",\"new-b.png\"]"); + updatedEntity.ShippingAddress.Should().Be("New Address"); } } diff --git a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs index 61ca28e..c5d1a67 100644 --- a/src/SimpleAdmin.Api/SimpleAdminApiModule.cs +++ b/src/SimpleAdmin.Api/SimpleAdminApiModule.cs @@ -54,7 +54,9 @@ public class SimpleAdminApiModule : FakeModule options.ApplicationServices2Controller(settings => { settings.RootPath = "rbac"; - settings.TypePredicate = type => type != typeof(Fake.Rbac.Application.Services.AuthService); + settings.TypePredicate = type => + type != typeof(Fake.Rbac.Application.Services.AuthService) && + type != typeof(Fake.Rbac.Application.Services.UserService); }); });