feat: RBAC用户扩展、专属顾问接口及仓库字段清理
接管用户 API 支持电话/工号/微信号/微信二维码,新增客户专属顾问查询,并移除仓库客服电话与微信相关字段。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d8752959e7
commit
3818ce7876
3
db.sql
3
db.sql
@ -1125,9 +1125,6 @@ CREATE TABLE `t_warehouse_address` (
|
|||||||
`Recipient` VARCHAR(64) NULL COMMENT '收件人',
|
`Recipient` VARCHAR(64) NULL COMMENT '收件人',
|
||||||
`ContactPhone` VARCHAR(32) NULL COMMENT '联系电话',
|
`ContactPhone` VARCHAR(32) NULL COMMENT '联系电话',
|
||||||
`ShippingAddress` TEXT 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-是',
|
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
|||||||
10
sql/migrate_rbac_user_profile.sql
Normal file
10
sql/migrate_rbac_user_profile.sql
Normal 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`);
|
||||||
6
sql/migrate_t_warehouse_address_drop_service_fields.sql
Normal file
6
sql/migrate_t_warehouse_address_drop_service_fields.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
-- 仓库地址表移除客服电话、客服微信及二维码字段
|
||||||
|
|
||||||
|
ALTER TABLE `t_warehouse_address`
|
||||||
|
DROP COLUMN `ServicePhone`,
|
||||||
|
DROP COLUMN `ServiceWechat`,
|
||||||
|
DROP COLUMN `ServiceWechatFiles`;
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -14,6 +14,12 @@ public class InternalUserListItemDto
|
|||||||
|
|
||||||
public string? Avatar { get; set; }
|
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? OrganizationId { get; set; }
|
||||||
|
|
||||||
public string? OrganizationName { get; set; }
|
public string? OrganizationName { get; set; }
|
||||||
|
|||||||
45
src/Oms.Application/Dtos/Rbac/OmsUserProfileDtos.cs
Normal file
45
src/Oms.Application/Dtos/Rbac/OmsUserProfileDtos.cs
Normal 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; }
|
||||||
|
}
|
||||||
@ -37,21 +37,4 @@ public class CreateWarehouseInput
|
|||||||
/// 发货地址。
|
/// 发货地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ShippingAddress { get; set; }
|
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; } = [];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,21 +43,4 @@ public class UpdateWarehouseInput
|
|||||||
/// 发货地址。
|
/// 发货地址。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ShippingAddress { get; set; }
|
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; } = [];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,21 +35,6 @@ public class WarehouseOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ShippingAddress { get; set; }
|
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>
|
||||||
/// 创建时间。
|
/// 创建时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -40,21 +40,6 @@ public class WarehousePaginatedOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ShippingAddress { get; set; }
|
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>
|
||||||
/// 创建时间。
|
/// 创建时间。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -28,12 +28,8 @@ public class OmsApplicationAutoMapperProfile : Profile
|
|||||||
CreateMap<Customer, CustomerOutput>();
|
CreateMap<Customer, CustomerOutput>();
|
||||||
CreateMap<RechargeRecord, RechargeRecordOutput>();
|
CreateMap<RechargeRecord, RechargeRecordOutput>();
|
||||||
CreateMap<RechargeRecord, RechargeRecordPaginatedOutput>();
|
CreateMap<RechargeRecord, RechargeRecordPaginatedOutput>();
|
||||||
CreateMap<WarehouseAddress, WarehousePaginatedOutput>()
|
CreateMap<WarehouseAddress, WarehousePaginatedOutput>();
|
||||||
.ForMember(dest => dest.ServiceWechatFiles,
|
CreateMap<WarehouseAddress, WarehouseOutput>();
|
||||||
opt => opt.MapFrom(src => JsonStringArrayHelper.Deserialize(src.ServiceWechatFiles)));
|
|
||||||
CreateMap<WarehouseAddress, WarehouseOutput>()
|
|
||||||
.ForMember(dest => dest.ServiceWechatFiles,
|
|
||||||
opt => opt.MapFrom(src => JsonStringArrayHelper.Deserialize(src.ServiceWechatFiles)));
|
|
||||||
CreateMap<InboundRecord, InboundRecordOutput>();
|
CreateMap<InboundRecord, InboundRecordOutput>();
|
||||||
CreateMap<OutboundRecord, OutboundRecordOutput>();
|
CreateMap<OutboundRecord, OutboundRecordOutput>();
|
||||||
CreateMap<LabelPrintRecord, LabelPrintRecordOutput>();
|
CreateMap<LabelPrintRecord, LabelPrintRecordOutput>();
|
||||||
|
|||||||
86
src/Oms.Application/Rbac/CustomerServiceAdvisorResolver.cs
Normal file
86
src/Oms.Application/Rbac/CustomerServiceAdvisorResolver.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,6 +61,9 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
|||||||
u.`Account` AS `Account`,
|
u.`Account` AS `Account`,
|
||||||
u.`Email` AS `Email`,
|
u.`Email` AS `Email`,
|
||||||
u.`Avatar` AS `Avatar`,
|
u.`Avatar` AS `Avatar`,
|
||||||
|
u.`Phone` AS `Phone`,
|
||||||
|
u.`EmployeeNo` AS `EmployeeNo`,
|
||||||
|
u.`WechatId` AS `WechatId`,
|
||||||
u.`OrganizationId` AS `OrganizationId`,
|
u.`OrganizationId` AS `OrganizationId`,
|
||||||
o.`Name` AS `OrganizationName`,
|
o.`Name` AS `OrganizationName`,
|
||||||
u.`CreateTime` AS `CreatedAt`
|
u.`CreateTime` AS `CreatedAt`
|
||||||
@ -87,6 +90,9 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
|||||||
Account = row.Account,
|
Account = row.Account,
|
||||||
Email = row.Email,
|
Email = row.Email,
|
||||||
Avatar = row.Avatar,
|
Avatar = row.Avatar,
|
||||||
|
Phone = row.Phone,
|
||||||
|
EmployeeNo = row.EmployeeNo,
|
||||||
|
WechatId = row.WechatId,
|
||||||
OrganizationId = row.OrganizationId?.ToString(),
|
OrganizationId = row.OrganizationId?.ToString(),
|
||||||
OrganizationName = row.OrganizationName,
|
OrganizationName = row.OrganizationName,
|
||||||
CreatedAt = row.CreatedAt,
|
CreatedAt = row.CreatedAt,
|
||||||
@ -143,6 +149,12 @@ public class InternalUserRepository(FakeRbacDbContext dbContext) : IScopedDepend
|
|||||||
|
|
||||||
public string? Avatar { get; set; }
|
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 Guid? OrganizationId { get; set; }
|
||||||
|
|
||||||
public string? OrganizationName { get; set; }
|
public string? OrganizationName { get; set; }
|
||||||
|
|||||||
37
src/Oms.Application/Rbac/OmsUserProfileMapper.cs
Normal file
37
src/Oms.Application/Rbac/OmsUserProfileMapper.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
226
src/Oms.Application/Rbac/UserProfileRepository.cs
Normal file
226
src/Oms.Application/Rbac/UserProfileRepository.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Oms.Application.Dtos.Customers;
|
using Oms.Application.Dtos.Customers;
|
||||||
using Oms.Application.Dtos.Shared;
|
using Oms.Application.Dtos.Shared;
|
||||||
|
using Oms.Application.Rbac;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.CustomersAggregate.Entities;
|
using Oms.Domain.CustomersAggregate.Entities;
|
||||||
using Oms.Domain.CustomersAggregate.Enums;
|
using Oms.Domain.CustomersAggregate.Enums;
|
||||||
@ -35,6 +36,9 @@ public class CustomerService : ApplicationService
|
|||||||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||||
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
|
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
|
||||||
|
|
||||||
|
private CustomerServiceAdvisorResolver ServiceAdvisorResolver =>
|
||||||
|
LazyServiceProvider.GetRequiredService<CustomerServiceAdvisorResolver>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询客户列表。
|
/// 查询客户列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -90,6 +94,28 @@ public class CustomerService : ApplicationService
|
|||||||
return output;
|
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>
|
||||||
/// 新增客户。
|
/// 新增客户。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
192
src/Oms.Application/Services/Rbac/OmsUserService.cs
Normal file
192
src/Oms.Application/Services/Rbac/OmsUserService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -94,9 +94,6 @@ public class WarehouseService : ApplicationService
|
|||||||
Recipient = input.Recipient,
|
Recipient = input.Recipient,
|
||||||
ContactPhone = input.ContactPhone,
|
ContactPhone = input.ContactPhone,
|
||||||
ShippingAddress = input.ShippingAddress,
|
ShippingAddress = input.ShippingAddress,
|
||||||
ServicePhone = input.ServicePhone,
|
|
||||||
ServiceWechat = input.ServiceWechat,
|
|
||||||
ServiceWechatFiles = JsonStringArrayHelper.Serialize(input.ServiceWechatFiles)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await WarehouseAddressRepository.InsertAsync(entity);
|
await WarehouseAddressRepository.InsertAsync(entity);
|
||||||
@ -122,9 +119,6 @@ public class WarehouseService : ApplicationService
|
|||||||
entity.Recipient = input.Recipient;
|
entity.Recipient = input.Recipient;
|
||||||
entity.ContactPhone = input.ContactPhone;
|
entity.ContactPhone = input.ContactPhone;
|
||||||
entity.ShippingAddress = input.ShippingAddress;
|
entity.ShippingAddress = input.ShippingAddress;
|
||||||
entity.ServicePhone = input.ServicePhone;
|
|
||||||
entity.ServiceWechat = input.ServiceWechat;
|
|
||||||
entity.ServiceWechatFiles = JsonStringArrayHelper.Serialize(input.ServiceWechatFiles);
|
|
||||||
|
|
||||||
await WarehouseAddressRepository.UpdateAsync(entity);
|
await WarehouseAddressRepository.UpdateAsync(entity);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,11 +15,4 @@ public class WarehouseAddress : OmsAuditedAggregateRoot
|
|||||||
public string? ContactPhone { get; set; }
|
public string? ContactPhone { get; set; }
|
||||||
|
|
||||||
public string? ShippingAddress { 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; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,9 +34,6 @@ public class WarehouseApiIntegrationTests
|
|||||||
Recipient = "Alice",
|
Recipient = "Alice",
|
||||||
ContactPhone = "13800138000",
|
ContactPhone = "13800138000",
|
||||||
ShippingAddress = "London",
|
ShippingAddress = "London",
|
||||||
ServicePhone = "020-123456",
|
|
||||||
ServiceWechat = "oms-warehouse",
|
|
||||||
ServiceWechatFiles = "[\"wx-1.png\",\"wx-2.png\"]",
|
|
||||||
CreateTime = new DateTime(2026, 4, 29, 9, 0, 0),
|
CreateTime = new DateTime(2026, 4, 29, 9, 0, 0),
|
||||||
UpdateTime = new DateTime(2026, 4, 29, 10, 0, 0)
|
UpdateTime = new DateTime(2026, 4, 29, 10, 0, 0)
|
||||||
}
|
}
|
||||||
@ -61,11 +58,11 @@ public class WarehouseApiIntegrationTests
|
|||||||
payload!.Total.Should().Be(1);
|
payload!.Total.Should().Be(1);
|
||||||
payload.Items.Should().ContainSingle();
|
payload.Items.Should().ContainSingle();
|
||||||
payload.Items[0].WarehouseTypeName.Should().Be("中转仓");
|
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]
|
[Fact]
|
||||||
public async Task GetWarehouseDetail_ShouldReturnMappedAuditAndFileFields()
|
public async Task GetWarehouseDetail_ShouldReturnMappedAuditFields()
|
||||||
{
|
{
|
||||||
var repository = new Mock<IWarehouseAddressRepository>(MockBehavior.Strict);
|
var repository = new Mock<IWarehouseAddressRepository>(MockBehavior.Strict);
|
||||||
repository
|
repository
|
||||||
@ -80,9 +77,6 @@ public class WarehouseApiIntegrationTests
|
|||||||
Recipient = "Bob",
|
Recipient = "Bob",
|
||||||
ContactPhone = "13900139000",
|
ContactPhone = "13900139000",
|
||||||
ShippingAddress = "Shenzhen",
|
ShippingAddress = "Shenzhen",
|
||||||
ServicePhone = "0755-123456",
|
|
||||||
ServiceWechat = "main-hub",
|
|
||||||
ServiceWechatFiles = "[\"main.png\"]",
|
|
||||||
CreateTime = new DateTime(2026, 4, 20, 9, 30, 0),
|
CreateTime = new DateTime(2026, 4, 20, 9, 30, 0),
|
||||||
CreateUserId = 66,
|
CreateUserId = 66,
|
||||||
UpdateTime = new DateTime(2026, 4, 21, 14, 0, 0),
|
UpdateTime = new DateTime(2026, 4, 21, 14, 0, 0),
|
||||||
@ -102,11 +96,11 @@ public class WarehouseApiIntegrationTests
|
|||||||
payload.Should().NotBeNull();
|
payload.Should().NotBeNull();
|
||||||
payload!.CreateBy.Should().Be("66");
|
payload!.CreateBy.Should().Be("66");
|
||||||
payload.UpdateBy.Should().Be("88");
|
payload.UpdateBy.Should().Be("88");
|
||||||
payload.ServiceWechatFiles.Should().ContainSingle().Which.Should().Be("main.png");
|
payload.ShippingAddress.Should().Be("Shenzhen");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CreateWarehouse_ShouldSerializeFilesBeforePersisting()
|
public async Task CreateWarehouse_ShouldPersistEntity()
|
||||||
{
|
{
|
||||||
WarehouseAddress? insertedEntity = null;
|
WarehouseAddress? insertedEntity = null;
|
||||||
var repository = new Mock<IWarehouseAddressRepository>(MockBehavior.Strict);
|
var repository = new Mock<IWarehouseAddressRepository>(MockBehavior.Strict);
|
||||||
@ -131,16 +125,13 @@ public class WarehouseApiIntegrationTests
|
|||||||
Recipient = "Cindy",
|
Recipient = "Cindy",
|
||||||
ContactPhone = "13700137000",
|
ContactPhone = "13700137000",
|
||||||
ShippingAddress = "Guangzhou",
|
ShippingAddress = "Guangzhou",
|
||||||
ServicePhone = "020-888888",
|
|
||||||
ServiceWechat = "return-hub",
|
|
||||||
ServiceWechatFiles = ["qr-a.png", "qr-b.png"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
response.IsSuccessStatusCode.Should().BeTrue();
|
response.IsSuccessStatusCode.Should().BeTrue();
|
||||||
insertedEntity.Should().NotBeNull();
|
insertedEntity.Should().NotBeNull();
|
||||||
insertedEntity!.WarehouseName.Should().Be("New Warehouse");
|
insertedEntity!.WarehouseName.Should().Be("New Warehouse");
|
||||||
insertedEntity.WarehouseType.Should().Be(WarehouseTypeCodes.Return);
|
insertedEntity.WarehouseType.Should().Be(WarehouseTypeCodes.Return);
|
||||||
insertedEntity.ServiceWechatFiles.Should().Be("[\"qr-a.png\",\"qr-b.png\"]");
|
insertedEntity.ShippingAddress.Should().Be("Guangzhou");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -155,9 +146,6 @@ public class WarehouseApiIntegrationTests
|
|||||||
Recipient = "Old",
|
Recipient = "Old",
|
||||||
ContactPhone = "13600136000",
|
ContactPhone = "13600136000",
|
||||||
ShippingAddress = "Old Address",
|
ShippingAddress = "Old Address",
|
||||||
ServicePhone = "010-111111",
|
|
||||||
ServiceWechat = "old-wechat",
|
|
||||||
ServiceWechatFiles = "[\"old.png\"]"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var repository = new Mock<IWarehouseAddressRepository>(MockBehavior.Strict);
|
var repository = new Mock<IWarehouseAddressRepository>(MockBehavior.Strict);
|
||||||
@ -188,9 +176,6 @@ public class WarehouseApiIntegrationTests
|
|||||||
Recipient = "Daisy",
|
Recipient = "Daisy",
|
||||||
ContactPhone = "13500135000",
|
ContactPhone = "13500135000",
|
||||||
ShippingAddress = "New Address",
|
ShippingAddress = "New Address",
|
||||||
ServicePhone = "0755-999999",
|
|
||||||
ServiceWechat = "new-wechat",
|
|
||||||
ServiceWechatFiles = ["new-a.png", "new-b.png"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
response.IsSuccessStatusCode.Should().BeTrue();
|
response.IsSuccessStatusCode.Should().BeTrue();
|
||||||
@ -198,6 +183,6 @@ public class WarehouseApiIntegrationTests
|
|||||||
updatedEntity!.WarehouseName.Should().Be("Updated Warehouse");
|
updatedEntity!.WarehouseName.Should().Be("Updated Warehouse");
|
||||||
updatedEntity.WarehouseType.Should().Be(WarehouseTypeCodes.Main);
|
updatedEntity.WarehouseType.Should().Be(WarehouseTypeCodes.Main);
|
||||||
updatedEntity.Recipient.Should().Be("Daisy");
|
updatedEntity.Recipient.Should().Be("Daisy");
|
||||||
updatedEntity.ServiceWechatFiles.Should().Be("[\"new-a.png\",\"new-b.png\"]");
|
updatedEntity.ShippingAddress.Should().Be("New Address");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,9 @@ public class SimpleAdminApiModule : FakeModule
|
|||||||
options.ApplicationServices2Controller<FakeRbacApplicationModule>(settings =>
|
options.ApplicationServices2Controller<FakeRbacApplicationModule>(settings =>
|
||||||
{
|
{
|
||||||
settings.RootPath = "rbac";
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user