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