feat: RBAC用户扩展、专属顾问接口及仓库字段清理

接管用户 API 支持电话/工号/微信号/微信二维码,新增客户专属顾问查询,并移除仓库客服电话与微信相关字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-09 11:23:54 +08:00
parent d8752959e7
commit 3818ce7876
21 changed files with 679 additions and 108 deletions

3
db.sql
View File

@ -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 '创建人',

View File

@ -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`);

View File

@ -0,0 +1,6 @@
-- 仓库地址表移除客服电话、客服微信及二维码字段
ALTER TABLE `t_warehouse_address`
DROP COLUMN `ServicePhone`,
DROP COLUMN `ServiceWechat`,
DROP COLUMN `ServiceWechatFiles`;

View File

@ -0,0 +1,22 @@
namespace Oms.Application.Dtos.Customers;
/// <summary>
/// 当前客户所属客服(专属顾问)展示信息。
/// </summary>
public class CustomerServiceAdvisorOutput
{
/// <summary>
/// 顾问姓名。
/// </summary>
public string? Name { get; set; }
/// <summary>
/// 微信号。
/// </summary>
public string? WechatId { get; set; }
/// <summary>
/// 微信二维码(URL 或 Base64)。
/// </summary>
public string? WechatQrCode { get; set; }
}

View File

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

View File

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

View File

@ -37,21 +37,4 @@ public class CreateWarehouseInput
/// 发货地址。
/// </summary>
public string? ShippingAddress { get; set; }
/// <summary>
/// 客服手机号。
/// </summary>
[StringLength(32)]
public string? ServicePhone { get; set; }
/// <summary>
/// 客服微信号。
/// </summary>
[StringLength(64)]
public string? ServiceWechat { get; set; }
/// <summary>
/// 客服微信附件集合。
/// </summary>
public List<string> ServiceWechatFiles { get; set; } = [];
}

View File

@ -43,21 +43,4 @@ public class UpdateWarehouseInput
/// 发货地址。
/// </summary>
public string? ShippingAddress { get; set; }
/// <summary>
/// 客服手机号。
/// </summary>
[StringLength(32)]
public string? ServicePhone { get; set; }
/// <summary>
/// 客服微信号。
/// </summary>
[StringLength(64)]
public string? ServiceWechat { get; set; }
/// <summary>
/// 客服微信附件集合。
/// </summary>
public List<string> ServiceWechatFiles { get; set; } = [];
}

View File

@ -35,21 +35,6 @@ public class WarehouseOutput
/// </summary>
public string? ShippingAddress { get; set; }
/// <summary>
/// 客服手机号。
/// </summary>
public string? ServicePhone { get; set; }
/// <summary>
/// 客服微信号。
/// </summary>
public string? ServiceWechat { get; set; }
/// <summary>
/// 客服微信附件集合。
/// </summary>
public List<string> ServiceWechatFiles { get; set; } = [];
/// <summary>
/// 创建时间。
/// </summary>

View File

@ -40,21 +40,6 @@ public class WarehousePaginatedOutput
/// </summary>
public string? ShippingAddress { get; set; }
/// <summary>
/// 客服手机号。
/// </summary>
public string? ServicePhone { get; set; }
/// <summary>
/// 客服微信号。
/// </summary>
public string? ServiceWechat { get; set; }
/// <summary>
/// 客服微信附件集合。
/// </summary>
public List<string> ServiceWechatFiles { get; set; } = [];
/// <summary>
/// 创建时间。
/// </summary>

View File

@ -28,12 +28,8 @@ public class OmsApplicationAutoMapperProfile : Profile
CreateMap<Customer, CustomerOutput>();
CreateMap<RechargeRecord, RechargeRecordOutput>();
CreateMap<RechargeRecord, RechargeRecordPaginatedOutput>();
CreateMap<WarehouseAddress, WarehousePaginatedOutput>()
.ForMember(dest => dest.ServiceWechatFiles,
opt => opt.MapFrom(src => JsonStringArrayHelper.Deserialize(src.ServiceWechatFiles)));
CreateMap<WarehouseAddress, WarehouseOutput>()
.ForMember(dest => dest.ServiceWechatFiles,
opt => opt.MapFrom(src => JsonStringArrayHelper.Deserialize(src.ServiceWechatFiles)));
CreateMap<WarehouseAddress, WarehousePaginatedOutput>();
CreateMap<WarehouseAddress, WarehouseOutput>();
CreateMap<InboundRecord, InboundRecordOutput>();
CreateMap<OutboundRecord, OutboundRecordOutput>();
CreateMap<LabelPrintRecord, LabelPrintRecordOutput>();

View File

@ -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<CustomerServiceAdvisorOutput> 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<AdvisorRow?> 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<AdvisorRow>(sql, userId)
.FirstOrDefaultAsync(cancellationToken);
}
private async Task<AdvisorRow?> 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<AdvisorRow>(sql, key)
.FirstOrDefaultAsync(cancellationToken);
}
private sealed class AdvisorRow
{
public string? Name { get; set; }
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
}
}

View File

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

View File

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

View File

@ -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<UserProfileData?> 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<UserProfileRow>(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<Dictionary<Guid, UserProfileData>> GetManyAsync(
IReadOnlyCollection<Guid> 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<UserProfileListRow>(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<bool> 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<long>(sql, normalized, excludeUserId.Value.ToString())
.FirstAsync(cancellationToken)
: await dbContext.Database.SqlQueryRaw<long>(sql, normalized).FirstAsync(cancellationToken);
return count > 0;
}
public async Task<string> 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<EmployeeNoRow>(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; }
}
}

View File

@ -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<OmsRbacUserDisplayNameResolver>();
private CustomerServiceAdvisorResolver ServiceAdvisorResolver =>
LazyServiceProvider.GetRequiredService<CustomerServiceAdvisorResolver>();
/// <summary>
/// 查询客户列表。
/// </summary>
@ -90,6 +94,28 @@ public class CustomerService : ApplicationService
return output;
}
/// <summary>
/// 查询当前登录客户所属客服(专属顾问)信息。
/// </summary>
/// <returns>专属顾问姓名、微信号、微信二维码。</returns>
[HttpGet("/customer/service-advisor")]
public async Task<CustomerServiceAdvisorOutput> GetServiceAdvisorAsync(CancellationToken cancellationToken = default)
{
var userId = CurrentUser.GetId<int>();
if (userId <= 0)
{
return new CustomerServiceAdvisorOutput();
}
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken);
if (customer == null || string.IsNullOrWhiteSpace(customer.CustomerService))
{
return new CustomerServiceAdvisorOutput();
}
return await ServiceAdvisorResolver.ResolveAsync(customer.CustomerService, cancellationToken);
}
/// <summary>
/// 新增客户。
/// </summary>

View File

@ -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;
/// <summary>
/// 接管 RBAC 用户接口,扩展电话、工号、微信号、微信二维码字段。
/// </summary>
[Authorize]
[ApiExplorerSettings(GroupName = "RBAC-User")]
public class OmsUserService(UserService userService, UserProfileRepository profileRepository) : ApplicationService
{
[HttpGet("/rbac/user/get")]
public async Task<OmsUserDetailDto> 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<PagedResultDto<OmsUserDetailDto>> GetListAsync(
[FromQuery] UserPagedRequestDto input,
CancellationToken cancellationToken = default)
{
var result = await userService.GetListAsync(input, cancellationToken);
if (result.Items.Count == 0)
{
return new PagedResultDto<OmsUserDetailDto>
{
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<OmsUserDetailDto>
{
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<List<UserSimpleDto>> GetUsersByRoleAsync(
[FromQuery] Guid roleId,
CancellationToken cancellationToken = default) =>
userService.GetUsersByRoleAsync(roleId, cancellationToken);
[HttpPost("/rbac/user/create")]
public async Task<OmsUserDetailDto> 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<OmsUserDetailDto> 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<Guid> ids, CancellationToken cancellationToken = default) =>
userService.DeleteBatchAsync(ids, cancellationToken);
[HttpPost("/rbac/user/assign-roles")]
public Task AssignRolesAsync(
[FromQuery] Guid userId,
[FromBody] List<Guid> roleIds,
CancellationToken cancellationToken = default) =>
userService.AssignRolesAsync(userId, roleIds, cancellationToken);
[HttpGet("/rbac/user/get-user-roles")]
public Task<List<RoleDto>> GetUserRolesAsync(
[FromQuery] Guid userId,
CancellationToken cancellationToken = default) =>
userService.GetUserRolesAsync(userId, cancellationToken);
[HttpGet("/rbac/user/get-user-permissions")]
public Task<List<string>> GetUserPermissionsAsync(
[FromQuery] Guid userId,
CancellationToken cancellationToken = default) =>
userService.GetUserPermissionsAsync(userId, cancellationToken);
private async Task<string> 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;
}
}

View File

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

View File

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

View File

@ -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<IWarehouseAddressRepository>(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<IWarehouseAddressRepository>(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<IWarehouseAddressRepository>(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");
}
}

View File

@ -54,7 +54,9 @@ public class SimpleAdminApiModule : FakeModule
options.ApplicationServices2Controller<FakeRbacApplicationModule>(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);
});
});