feat: 仓库地址排序备注与用户注册错误提示
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
仓库地址增加排序/备注字段与迁移;用户创建时对邮箱冲突等给出明确中文提示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c3d21dfdbb
commit
6852135774
2
db.sql
2
db.sql
@ -1329,6 +1329,8 @@ CREATE TABLE `t_warehouse_address` (
|
||||
`Recipient` VARCHAR(64) NULL COMMENT '收件人',
|
||||
`ContactPhone` VARCHAR(32) NULL COMMENT '联系电话',
|
||||
`ShippingAddress` TEXT COMMENT '收货地址',
|
||||
`Sort` INT NULL DEFAULT NULL COMMENT '展示排序,越小越靠前',
|
||||
`Remark` VARCHAR(512) NULL DEFAULT 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 '创建人',
|
||||
|
||||
@ -624,3 +624,44 @@ ALTER TABLE `t_customer_export_qualification`
|
||||
MODIFY COLUMN `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
MODIFY COLUMN `UpdateUserId` INT NULL COMMENT '更新人',
|
||||
MODIFY COLUMN `UpdateTime` DATETIME NULL COMMENT '更新时间';
|
||||
|
||||
-- =============================================================================
|
||||
-- migrate_warehouse_address_sort_remark.sql
|
||||
-- 仓库地å<C2B0>€ï¼šå±•示排åº<C3A5>(选填ï¼? 备注(选填ï¼?
|
||||
-- =============================================================================
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_warehouse_address'
|
||||
AND COLUMN_NAME = 'Sort'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_warehouse_address`
|
||||
ADD COLUMN `Sort` INT NULL DEFAULT NULL COMMENT ''展示排åº<EFBFBD>,越å°<EFBFBD>è¶Šé<EFBFBD> å‰<EFBFBD>'' AFTER `ShippingAddress`',
|
||||
'SELECT ''Sort already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_warehouse_address'
|
||||
AND COLUMN_NAME = 'Remark'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_warehouse_address`
|
||||
ADD COLUMN `Remark` VARCHAR(512) NULL DEFAULT NULL COMMENT ''备注'' AFTER `Sort`',
|
||||
'SELECT ''Remark already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
|
||||
39
sql/migrate_warehouse_address_sort_remark.sql
Normal file
39
sql/migrate_warehouse_address_sort_remark.sql
Normal file
@ -0,0 +1,39 @@
|
||||
-- =============================================================================
|
||||
-- migrate_warehouse_address_sort_remark.sql
|
||||
-- 仓库地址:展示排序(选填)+ 备注(选填)
|
||||
-- =============================================================================
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_warehouse_address'
|
||||
AND COLUMN_NAME = 'Sort'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_warehouse_address`
|
||||
ADD COLUMN `Sort` INT NULL DEFAULT NULL COMMENT ''展示排序,越小越靠前'' AFTER `ShippingAddress`',
|
||||
'SELECT ''Sort already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_warehouse_address'
|
||||
AND COLUMN_NAME = 'Remark'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_warehouse_address`
|
||||
ADD COLUMN `Remark` VARCHAR(512) NULL DEFAULT NULL COMMENT ''备注'' AFTER `Sort`',
|
||||
'SELECT ''Remark already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -37,4 +37,15 @@ public class CreateWarehouseInput
|
||||
/// 发货地址。
|
||||
/// </summary>
|
||||
public string? ShippingAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示排序(越小越靠前;可不填)。
|
||||
/// </summary>
|
||||
public int? Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注(可不填)。
|
||||
/// </summary>
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
@ -43,4 +43,15 @@ public class UpdateWarehouseInput
|
||||
/// 发货地址。
|
||||
/// </summary>
|
||||
public string? ShippingAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示排序(越小越靠前;可不填)。
|
||||
/// </summary>
|
||||
public int? Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注(可不填)。
|
||||
/// </summary>
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
@ -35,6 +35,16 @@ public class WarehouseOutput
|
||||
/// </summary>
|
||||
public string? ShippingAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示排序(越小越靠前;为空则排在有排序值的记录之后)。
|
||||
/// </summary>
|
||||
public int? Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
|
||||
@ -40,6 +40,16 @@ public class WarehousePaginatedOutput
|
||||
/// </summary>
|
||||
public string? ShippingAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示排序(越小越靠前;为空则排在有排序值的记录之后)。
|
||||
/// </summary>
|
||||
public int? Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
|
||||
@ -5,11 +5,13 @@ using Fake.Rbac.Application.Dtos.Role;
|
||||
using Fake.Rbac.Application.Dtos.User;
|
||||
using Fake.Rbac.Application.Services;
|
||||
using Fake.Rbac.Domain.Managers;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Fake.ObjectStorage;
|
||||
using Oms.Application.Dtos.Rbac;
|
||||
using Oms.Application.Rbac;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Enums;
|
||||
@ -27,6 +29,7 @@ public class OmsUserService(
|
||||
AccountManager accountManager,
|
||||
UserProfileRepository profileRepository,
|
||||
IEmployeeRepository employeeRepository,
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
IObjectStorage fileStorage) : ApplicationService
|
||||
{
|
||||
/// <summary>
|
||||
@ -94,34 +97,78 @@ public class OmsUserService(
|
||||
[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 = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
|
||||
OrganizationId = input.OrganizationId,
|
||||
RoleIds = input.RoleIds,
|
||||
}, cancellationToken);
|
||||
var account = input.Account?.Trim() ?? string.Empty;
|
||||
OmsRbacRegistrationGuard.ValidateAdminUserFields(
|
||||
input.Name,
|
||||
account,
|
||||
input.Password,
|
||||
input.Email,
|
||||
input.Phone,
|
||||
input.WechatId,
|
||||
requirePassword: true);
|
||||
|
||||
await profileRepository.SaveAsync(
|
||||
user.Id,
|
||||
new UserProfileData
|
||||
{
|
||||
Phone = input.Phone,
|
||||
EmployeeNo = employeeNo,
|
||||
WechatId = input.WechatId,
|
||||
WechatQrCode = input.WechatQrCode,
|
||||
WecomKfUrl = input.WecomKfUrl,
|
||||
},
|
||||
var employeeNo = await ResolveEmployeeNoForCreateAsync(input.EmployeeNo, cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsureLoginIdentityAvailableAsync(
|
||||
rbacDbContext,
|
||||
account,
|
||||
input.Email,
|
||||
forAdminUserCreate: true,
|
||||
excludeUserId: null,
|
||||
cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsurePhoneAvailableAsync(
|
||||
rbacDbContext,
|
||||
input.Phone,
|
||||
excludeUserId: null,
|
||||
cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsureWechatIdAvailableAsync(
|
||||
rbacDbContext,
|
||||
input.WechatId,
|
||||
excludeUserId: null,
|
||||
cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsureOrganizationExistsAsync(
|
||||
rbacDbContext,
|
||||
input.OrganizationId,
|
||||
cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsureRolesExistAsync(
|
||||
rbacDbContext,
|
||||
input.RoleIds,
|
||||
cancellationToken);
|
||||
|
||||
await EnsureEmployeeProfileAsync(user.Id, input, cancellationToken);
|
||||
try
|
||||
{
|
||||
var user = await userService.CreateAsync(new UserCreateDto
|
||||
{
|
||||
Name = input.Name!.Trim(),
|
||||
Account = account,
|
||||
Password = input.Password,
|
||||
// 禁止 Email='':uk_user_email 会把空串当成占用
|
||||
Email = string.IsNullOrWhiteSpace(input.Email) ? null : input.Email.Trim(),
|
||||
Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
|
||||
OrganizationId = input.OrganizationId,
|
||||
RoleIds = input.RoleIds,
|
||||
}, cancellationToken);
|
||||
|
||||
var profile = await profileRepository.GetAsync(user.Id, cancellationToken);
|
||||
return MapUserDetail(user, profile);
|
||||
await profileRepository.SaveAsync(
|
||||
user.Id,
|
||||
new UserProfileData
|
||||
{
|
||||
Phone = input.Phone,
|
||||
EmployeeNo = employeeNo,
|
||||
WechatId = input.WechatId,
|
||||
WechatQrCode = input.WechatQrCode,
|
||||
WecomKfUrl = input.WecomKfUrl,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
await EnsureEmployeeProfileAsync(user.Id, input, cancellationToken);
|
||||
|
||||
var profile = await profileRepository.GetAsync(user.Id, cancellationToken);
|
||||
return MapUserDetail(user, profile);
|
||||
}
|
||||
catch (Exception ex) when (ex is not BusinessException)
|
||||
{
|
||||
throw OmsRbacRegistrationGuard.ToSaveBusinessException(ex, forAdminUserCreate: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -133,30 +180,75 @@ public class OmsUserService(
|
||||
[FromBody] OmsUserUpdateInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var user = await userService.UpdateAsync(
|
||||
id,
|
||||
new UserUpdateDto
|
||||
{
|
||||
Name = input.Name,
|
||||
Email = input.Email,
|
||||
Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
|
||||
OrganizationId = input.OrganizationId,
|
||||
},
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
throw new BusinessException("用户 Id 无效");
|
||||
}
|
||||
|
||||
// 更新不改登录账号/密码
|
||||
OmsRbacRegistrationGuard.ValidateAdminUserFields(
|
||||
input.Name,
|
||||
account: "placeholder",
|
||||
password: null,
|
||||
input.Email,
|
||||
input.Phone,
|
||||
input.WechatId,
|
||||
requirePassword: false);
|
||||
|
||||
var email = string.IsNullOrWhiteSpace(input.Email) ? null : input.Email.Trim();
|
||||
await OmsRbacRegistrationGuard.EnsureEmailAvailableAsync(
|
||||
rbacDbContext,
|
||||
email,
|
||||
excludeUserId: id,
|
||||
forAdminUserCreate: true,
|
||||
cancellationToken);
|
||||
|
||||
await profileRepository.UpdateContactAsync(
|
||||
id,
|
||||
new UserProfileData
|
||||
{
|
||||
Phone = input.Phone,
|
||||
WechatId = input.WechatId,
|
||||
WechatQrCode = input.WechatQrCode,
|
||||
WecomKfUrl = input.WecomKfUrl,
|
||||
},
|
||||
await OmsRbacRegistrationGuard.EnsurePhoneAvailableAsync(
|
||||
rbacDbContext,
|
||||
input.Phone,
|
||||
excludeUserId: id,
|
||||
cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsureWechatIdAvailableAsync(
|
||||
rbacDbContext,
|
||||
input.WechatId,
|
||||
excludeUserId: id,
|
||||
cancellationToken);
|
||||
await OmsRbacRegistrationGuard.EnsureOrganizationExistsAsync(
|
||||
rbacDbContext,
|
||||
input.OrganizationId,
|
||||
cancellationToken);
|
||||
|
||||
var profile = await profileRepository.GetAsync(id, cancellationToken);
|
||||
return MapUserDetail(user, profile);
|
||||
try
|
||||
{
|
||||
var user = await userService.UpdateAsync(
|
||||
id,
|
||||
new UserUpdateDto
|
||||
{
|
||||
Name = input.Name!.Trim(),
|
||||
Email = email,
|
||||
Avatar = OmsObjectStorageExtensions.ToStorageKey(input.Avatar),
|
||||
OrganizationId = input.OrganizationId,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
await profileRepository.UpdateContactAsync(
|
||||
id,
|
||||
new UserProfileData
|
||||
{
|
||||
Phone = input.Phone,
|
||||
WechatId = input.WechatId,
|
||||
WechatQrCode = input.WechatQrCode,
|
||||
WecomKfUrl = input.WecomKfUrl,
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
var profile = await profileRepository.GetAsync(id, cancellationToken);
|
||||
return MapUserDetail(user, profile);
|
||||
}
|
||||
catch (Exception ex) when (ex is not BusinessException)
|
||||
{
|
||||
throw OmsRbacRegistrationGuard.ToSaveBusinessException(ex, forAdminUserCreate: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -259,12 +351,12 @@ public class OmsUserService(
|
||||
|
||||
if (!UserProfileRepository.IsValidEmployeeNo(employeeNo))
|
||||
{
|
||||
throw new BusinessException("工号格式无效,应为 IDE + 至少 3 位数字,如 IDE001");
|
||||
throw new BusinessException($"工号「{employeeNo}」格式无效,应为 IDE + 至少 3 位数字,如 IDE001");
|
||||
}
|
||||
|
||||
if (await profileRepository.IsEmployeeNoTakenAsync(employeeNo, null, cancellationToken))
|
||||
{
|
||||
throw new BusinessException($"工号已存在:{employeeNo}");
|
||||
throw new BusinessException($"工号「{employeeNo}」已存在,请更换工号后再创建");
|
||||
}
|
||||
|
||||
return employeeNo;
|
||||
|
||||
@ -33,13 +33,16 @@ public class WarehouseService : ApplicationService
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
|
||||
var entities = await WarehouseAddressRepository.GetPagedListAsync(
|
||||
// 有 Sort 的按升序靠前;无 Sort 的靠后;同序再按 Id
|
||||
var (entities, totalCount) = await RepositoryPageHelper.GetPageAsync(
|
||||
WarehouseAddressRepository,
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
new Dictionary<string, bool> { [nameof(WarehouseAddress.UpdateTime)] = false, [nameof(WarehouseAddress.Id)] = false });
|
||||
q => q.OrderBy(x => x.Sort == null)
|
||||
.ThenBy(x => x.Sort)
|
||||
.ThenBy(x => x.Id));
|
||||
|
||||
var totalCount = await WarehouseAddressRepository.CountAsync(predicate);
|
||||
var items = entities.Select(entity =>
|
||||
{
|
||||
var output = ObjectMapper.Map<WarehouseAddress, WarehousePaginatedOutput>(entity);
|
||||
@ -97,6 +100,8 @@ public class WarehouseService : ApplicationService
|
||||
deleted.Recipient = input.Recipient;
|
||||
deleted.ContactPhone = input.ContactPhone;
|
||||
deleted.ShippingAddress = input.ShippingAddress;
|
||||
deleted.Sort = input.Sort;
|
||||
deleted.Remark = NormalizeRemark(input.Remark);
|
||||
await WarehouseAddressRepository.UpdateAsync(deleted);
|
||||
return;
|
||||
}
|
||||
@ -108,6 +113,8 @@ public class WarehouseService : ApplicationService
|
||||
Recipient = input.Recipient,
|
||||
ContactPhone = input.ContactPhone,
|
||||
ShippingAddress = input.ShippingAddress,
|
||||
Sort = input.Sort,
|
||||
Remark = NormalizeRemark(input.Remark),
|
||||
};
|
||||
|
||||
await WarehouseAddressRepository.InsertAsync(entity);
|
||||
@ -141,6 +148,8 @@ public class WarehouseService : ApplicationService
|
||||
entity.Recipient = input.Recipient;
|
||||
entity.ContactPhone = input.ContactPhone;
|
||||
entity.ShippingAddress = input.ShippingAddress;
|
||||
entity.Sort = input.Sort;
|
||||
entity.Remark = NormalizeRemark(input.Remark);
|
||||
|
||||
await WarehouseAddressRepository.UpdateAsync(entity);
|
||||
}
|
||||
@ -180,4 +189,10 @@ public class WarehouseService : ApplicationService
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static string? NormalizeRemark(string? remark)
|
||||
{
|
||||
var value = remark?.Trim();
|
||||
return string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using System.Net.Mail;
|
||||
using System.Text.RegularExpressions;
|
||||
using Fake.Application;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -5,32 +7,276 @@ using Microsoft.EntityFrameworkCore;
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 微信/客户注册前校验 RBAC 登录账号,避免 user 表已存在但 t_customer 未创建时 EF 抛出英文 DbUpdateException。
|
||||
/// 微信/客户注册、后台建用户前校验 RBAC 登录身份,避免 EF 抛出英文 DbUpdateException。
|
||||
/// </summary>
|
||||
public static class OmsRbacRegistrationGuard
|
||||
{
|
||||
private static readonly Regex PhonePattern = new(@"^1\d{10}$", RegexOptions.Compiled);
|
||||
|
||||
public static async Task EnsureLoginIdentityAvailableAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string account,
|
||||
string? email,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureLoginIdentityAvailableAsync(
|
||||
rbacDbContext,
|
||||
account,
|
||||
email,
|
||||
forAdminUserCreate: false,
|
||||
excludeUserId: null,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验登录账号/邮箱是否可用。
|
||||
/// <paramref name="forAdminUserCreate"/> 为 true 时提示面向后台建员工用户(会说明可能已被客户账号占用)。
|
||||
/// </summary>
|
||||
public static async Task EnsureLoginIdentityAvailableAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string account,
|
||||
string? email,
|
||||
bool forAdminUserCreate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureLoginIdentityAvailableAsync(
|
||||
rbacDbContext,
|
||||
account,
|
||||
email,
|
||||
forAdminUserCreate,
|
||||
excludeUserId: null,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task EnsureLoginIdentityAvailableAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string account,
|
||||
string? email,
|
||||
bool forAdminUserCreate,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedAccount = account.Trim();
|
||||
var normalizedEmail = string.IsNullOrWhiteSpace(email) ? null : email.Trim();
|
||||
|
||||
if (await ExistsActiveUserByAccountAsync(rbacDbContext, normalizedAccount, cancellationToken))
|
||||
if (await ExistsUserByAccountAsync(rbacDbContext, normalizedAccount, excludeUserId, cancellationToken))
|
||||
{
|
||||
throw new BusinessException("登录账号已存在,请直接绑定或使用其他账号");
|
||||
throw new BusinessException(
|
||||
forAdminUserCreate
|
||||
? $"登录账号「{normalizedAccount}」已存在。客户门户注册也会占用同一登录账号,请更换账号后再创建员工用户"
|
||||
: "登录账号已存在,请直接绑定或使用其他账号");
|
||||
}
|
||||
|
||||
if (normalizedEmail != null
|
||||
&& await ExistsActiveUserByEmailAsync(rbacDbContext, normalizedEmail, cancellationToken))
|
||||
&& await ExistsUserByEmailAsync(rbacDbContext, normalizedEmail, excludeUserId, cancellationToken))
|
||||
{
|
||||
throw new BusinessException("邮箱已被占用,请更换邮箱或直接绑定已有账号");
|
||||
throw new BusinessException(
|
||||
forAdminUserCreate
|
||||
? $"邮箱「{normalizedEmail}」已被占用。客户门户注册也会占用同一邮箱,请更换邮箱后再创建员工用户"
|
||||
: "邮箱已被占用,请更换邮箱或直接绑定已有账号");
|
||||
}
|
||||
}
|
||||
|
||||
public static BusinessException ToSaveBusinessException(Exception ex)
|
||||
/// <summary>
|
||||
/// 后台创建/更新员工用户时的字段格式与必填校验(不含唯一性)。
|
||||
/// </summary>
|
||||
public static void ValidateAdminUserFields(
|
||||
string? name,
|
||||
string? account,
|
||||
string? password,
|
||||
string? email,
|
||||
string? phone,
|
||||
string? wechatId,
|
||||
bool requirePassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new BusinessException("姓名不能为空");
|
||||
}
|
||||
|
||||
if (name.Trim().Length > 64)
|
||||
{
|
||||
throw new BusinessException($"姓名过长(当前 {name.Trim().Length} 字,最多 64 字)");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(account))
|
||||
{
|
||||
throw new BusinessException("登录账号不能为空");
|
||||
}
|
||||
|
||||
if (account.Trim().Length > 64)
|
||||
{
|
||||
throw new BusinessException($"登录账号过长(当前 {account.Trim().Length} 字,最多 64 字)");
|
||||
}
|
||||
|
||||
if (requirePassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
throw new BusinessException("密码不能为空");
|
||||
}
|
||||
|
||||
if (password.Length < 6)
|
||||
{
|
||||
throw new BusinessException("密码长度不能少于6位");
|
||||
}
|
||||
|
||||
if (password.Length > 128)
|
||||
{
|
||||
throw new BusinessException("密码过长,请控制在 128 位以内");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
var trimmedEmail = email.Trim();
|
||||
if (trimmedEmail.Length > 128)
|
||||
{
|
||||
throw new BusinessException($"邮箱过长(当前 {trimmedEmail.Length} 字,最多 128 字)");
|
||||
}
|
||||
|
||||
if (!IsValidEmail(trimmedEmail))
|
||||
{
|
||||
throw new BusinessException($"邮箱格式不正确:{trimmedEmail}");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(phone))
|
||||
{
|
||||
var trimmedPhone = phone.Trim();
|
||||
if (!PhonePattern.IsMatch(trimmedPhone))
|
||||
{
|
||||
throw new BusinessException($"手机号格式不正确:{trimmedPhone}(应为 11 位中国大陆手机号)");
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(wechatId) && wechatId.Trim().Length > 64)
|
||||
{
|
||||
throw new BusinessException($"微信号过长(当前 {wechatId.Trim().Length} 字,最多 64 字)");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnsureOrganizationExistsAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
Guid? organizationId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (organizationId is null || organizationId == Guid.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hit = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"""
|
||||
SELECT 1 AS Value
|
||||
FROM `organization`
|
||||
WHERE `Id` = {organizationId.Value.ToString()} AND `IsDeleted` = 0
|
||||
LIMIT 1
|
||||
""")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (!hit.HasValue)
|
||||
{
|
||||
throw new BusinessException($"所属组织不存在或已删除({organizationId}),请重新选择组织");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnsureRolesExistAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
IEnumerable<Guid>? roleIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (roleIds == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ids = roleIds.Where(x => x != Guid.Empty).Distinct().ToList();
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var roleId in ids)
|
||||
{
|
||||
var hit = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"""
|
||||
SELECT 1 AS Value
|
||||
FROM `role`
|
||||
WHERE `Id` = {roleId.ToString()} AND `IsDeleted` = 0
|
||||
LIMIT 1
|
||||
""")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (!hit.HasValue)
|
||||
{
|
||||
throw new BusinessException($"角色不存在或已删除({roleId}),请重新选择角色");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnsureEmailAvailableAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string? email,
|
||||
Guid? excludeUserId,
|
||||
bool forAdminUserCreate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedEmail = email.Trim();
|
||||
if (await ExistsUserByEmailAsync(rbacDbContext, normalizedEmail, excludeUserId, cancellationToken))
|
||||
{
|
||||
throw new BusinessException(
|
||||
forAdminUserCreate
|
||||
? $"邮箱「{normalizedEmail}」已被占用。客户门户注册也会占用同一邮箱,请更换邮箱后再保存"
|
||||
: "邮箱已被占用,请更换邮箱或直接绑定已有账号");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnsurePhoneAvailableAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string? phone,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(phone))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = phone.Trim();
|
||||
if (await ExistsUserByPhoneAsync(rbacDbContext, normalized, excludeUserId, cancellationToken))
|
||||
{
|
||||
throw new BusinessException(
|
||||
$"手机号「{normalized}」已被占用。客户门户注册也会占用同一手机号,请更换手机号后再保存");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task EnsureWechatIdAvailableAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string? wechatId,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(wechatId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalized = wechatId.Trim();
|
||||
if (await ExistsUserByWechatIdAsync(rbacDbContext, normalized, excludeUserId, cancellationToken))
|
||||
{
|
||||
throw new BusinessException($"微信号「{normalized}」已被占用,请更换后再保存");
|
||||
}
|
||||
}
|
||||
|
||||
public static BusinessException ToSaveBusinessException(Exception ex, bool forAdminUserCreate = false)
|
||||
{
|
||||
var message = ExtractDeepMessage(ex);
|
||||
if (ContainsDuplicateKey(message))
|
||||
@ -39,19 +285,41 @@ public static class OmsRbacRegistrationGuard
|
||||
|| message.Contains("uk_user_account", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("IX_user_Account", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("登录账号已存在,请直接绑定或使用其他账号");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate
|
||||
? "登录账号已存在。客户门户注册也会占用同一登录账号,请更换账号后再创建员工用户"
|
||||
: "登录账号已存在,请直接绑定或使用其他账号");
|
||||
}
|
||||
|
||||
if (message.Contains("Email", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("uk_user_email", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("邮箱已被占用,请更换邮箱或直接绑定已有账号");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate
|
||||
? "邮箱已被占用。客户门户注册也会占用同一邮箱,请更换邮箱后再创建员工用户"
|
||||
: "邮箱已被占用,请更换邮箱或直接绑定已有账号");
|
||||
}
|
||||
|
||||
if (message.Contains("Phone", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("uk_user_phone", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("uk_cus_phone", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("手机号已存在");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate
|
||||
? "手机号已被占用。客户门户注册也会占用同一手机号,请更换手机号后再保存"
|
||||
: "手机号已存在");
|
||||
}
|
||||
|
||||
if (message.Contains("EmployeeNo", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("uk_user_employee", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("工号已被占用,请更换工号后再保存");
|
||||
}
|
||||
|
||||
if (message.Contains("WechatId", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("uk_user_wechat", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("微信号已被占用,请更换后再保存");
|
||||
}
|
||||
|
||||
if (message.Contains("uk_cus_email", StringComparison.OrdinalIgnoreCase))
|
||||
@ -66,41 +334,125 @@ public static class OmsRbacRegistrationGuard
|
||||
return new BusinessException("当前微信已绑定账号");
|
||||
}
|
||||
|
||||
return new BusinessException("账号信息已存在,请直接绑定或更换手机号/邮箱");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate
|
||||
? "账号信息与已有用户冲突(登录账号/邮箱/手机号等唯一字段),请检查后更换"
|
||||
: "账号信息已存在,请直接绑定或更换手机号/邮箱");
|
||||
}
|
||||
|
||||
if (message.Contains("Data too long", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("1406", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (message.Contains("Avatar", StringComparison.OrdinalIgnoreCase))
|
||||
var field = ResolveTooLongField(message, forAdminUserCreate);
|
||||
if (field != null)
|
||||
{
|
||||
return new BusinessException("微信头像地址过长,请稍后重试或联系客服");
|
||||
return new BusinessException(field);
|
||||
}
|
||||
|
||||
if (message.Contains("Nickname", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("CustomerName", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("Name", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("客户名称或微信昵称过长,请修改后重试");
|
||||
}
|
||||
|
||||
return new BusinessException("注册信息字段过长,请缩短客户名称后重试");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate
|
||||
? "保存失败:某个字段内容过长,请缩短后重试"
|
||||
: "注册信息字段过长,请缩短客户名称后重试");
|
||||
}
|
||||
|
||||
if (message.Contains("foreign key constraint fails", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("1452", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("客户角色未正确初始化,请联系管理员");
|
||||
if (message.Contains("organization", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("Organization", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("所属组织不存在或已删除,请重新选择组织");
|
||||
}
|
||||
|
||||
if (message.Contains("role", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("Role", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("角色不存在或已删除,请重新选择角色");
|
||||
}
|
||||
|
||||
return new BusinessException("关联数据无效(组织/角色),请检查后重试");
|
||||
}
|
||||
|
||||
if (message.Contains("Cannot insert duplicate key", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("2627", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new BusinessException("账号信息已存在,请直接绑定或更换手机号/邮箱");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate
|
||||
? "账号信息与已有用户冲突(登录账号/邮箱/手机号等唯一字段),请检查后更换"
|
||||
: "账号信息已存在,请直接绑定或更换手机号/邮箱");
|
||||
}
|
||||
|
||||
var reason = ExtractActionableReason(ex);
|
||||
return new BusinessException($"注册失败:{reason}");
|
||||
return new BusinessException(
|
||||
forAdminUserCreate ? $"保存用户失败:{reason}" : $"注册失败:{reason}");
|
||||
}
|
||||
|
||||
private static string? ResolveTooLongField(string message, bool forAdminUserCreate)
|
||||
{
|
||||
if (message.Contains("Avatar", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return forAdminUserCreate ? "头像地址过长,请更换后重试" : "微信头像地址过长,请稍后重试或联系客服";
|
||||
}
|
||||
|
||||
if (message.Contains("Account", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "登录账号过长,请缩短后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("Email", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "邮箱过长,请缩短后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("Phone", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "手机号过长,请检查后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("EmployeeNo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "工号过长,请检查后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("WechatId", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "微信号过长,请缩短后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("WechatQrCode", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("WecomKfUrl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "微信二维码或客服链接过长,请缩短后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("Password", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("PasswordHash", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "密码相关字段过长,请缩短密码后重试";
|
||||
}
|
||||
|
||||
if (message.Contains("Nickname", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("CustomerName", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains("`Name`", StringComparison.OrdinalIgnoreCase)
|
||||
|| message.Contains(" column 'Name'", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return forAdminUserCreate ? "姓名过长,请缩短后重试" : "客户名称或微信昵称过长,请修改后重试";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsValidEmail(string email)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addr = new MailAddress(email);
|
||||
return string.Equals(addr.Address, email, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractActionableReason(Exception ex)
|
||||
@ -130,29 +482,93 @@ public static class OmsRbacRegistrationGuard
|
||||
return reason.Length <= 160 ? reason : reason[..160];
|
||||
}
|
||||
|
||||
private static async Task<bool> ExistsActiveUserByAccountAsync(
|
||||
private static async Task<bool> ExistsUserByAccountAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string account,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 唯一索引不按 IsDeleted 区分,软删除账号仍会触发 Duplicate entry。
|
||||
var hit = await rbacDbContext.Database
|
||||
if (excludeUserId is Guid id && id != Guid.Empty)
|
||||
{
|
||||
var hit = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `Account` = {account} AND `Id` <> {id.ToString()} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return hit.HasValue;
|
||||
}
|
||||
|
||||
var any = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `Account` = {account} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return hit.HasValue;
|
||||
return any.HasValue;
|
||||
}
|
||||
|
||||
private static async Task<bool> ExistsActiveUserByEmailAsync(
|
||||
private static async Task<bool> ExistsUserByEmailAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string email,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var hit = await rbacDbContext.Database
|
||||
if (excludeUserId is Guid id && id != Guid.Empty)
|
||||
{
|
||||
var hit = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `Email` = {email} AND `Id` <> {id.ToString()} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return hit.HasValue;
|
||||
}
|
||||
|
||||
var any = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `Email` = {email} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return hit.HasValue;
|
||||
return any.HasValue;
|
||||
}
|
||||
|
||||
private static async Task<bool> ExistsUserByPhoneAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string phone,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (excludeUserId is Guid id && id != Guid.Empty)
|
||||
{
|
||||
var hit = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `Phone` = {phone} AND `Id` <> {id.ToString()} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return hit.HasValue;
|
||||
}
|
||||
|
||||
var any = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `Phone` = {phone} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return any.HasValue;
|
||||
}
|
||||
|
||||
private static async Task<bool> ExistsUserByWechatIdAsync(
|
||||
FakeRbacDbContext rbacDbContext,
|
||||
string wechatId,
|
||||
Guid? excludeUserId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (excludeUserId is Guid id && id != Guid.Empty)
|
||||
{
|
||||
var hit = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `WechatId` = {wechatId} AND `Id` <> {id.ToString()} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return hit.HasValue;
|
||||
}
|
||||
|
||||
var any = await rbacDbContext.Database
|
||||
.SqlQuery<int?>(
|
||||
$"SELECT 1 AS Value FROM `user` WHERE `WechatId` = {wechatId} LIMIT 1")
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
return any.HasValue;
|
||||
}
|
||||
|
||||
private static string ExtractDeepMessage(Exception ex)
|
||||
|
||||
@ -15,4 +15,16 @@ public class WarehouseAddress : OmsAuditedAggregateRoot
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
public string? ShippingAddress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 展示排序(越小越靠前;为空则排在有排序值的记录之后)。
|
||||
/// </summary>
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public int? Sort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注(选填,展示用)。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 512, IsNullable = true)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user