diff --git a/db.sql b/db.sql
index b2c744a..f55db57 100644
--- a/db.sql
+++ b/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 '创建人',
diff --git a/sql/admin1008.sql b/sql/admin1008.sql
index b516377..9c49440 100644
--- a/sql/admin1008.sql
+++ b/sql/admin1008.sql
@@ -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
+-- 仓库地址:展示排序(选填ï¼? 备注(选填ï¼?
+-- =============================================================================
+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;
+
diff --git a/sql/migrate_warehouse_address_sort_remark.sql b/sql/migrate_warehouse_address_sort_remark.sql
new file mode 100644
index 0000000..e4c047b
--- /dev/null
+++ b/sql/migrate_warehouse_address_sort_remark.sql
@@ -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;
diff --git a/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs b/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs
index 50c4a90..6e2c66b 100644
--- a/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs
+++ b/src/Oms.Application/Dtos/System/CreateWarehouseInput.cs
@@ -37,4 +37,15 @@ public class CreateWarehouseInput
/// 发货地址。
///
public string? ShippingAddress { get; set; }
+
+ ///
+ /// 展示排序(越小越靠前;可不填)。
+ ///
+ public int? Sort { get; set; }
+
+ ///
+ /// 备注(可不填)。
+ ///
+ [StringLength(512)]
+ public string? Remark { get; set; }
}
diff --git a/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs b/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs
index ea8b6af..77b89c5 100644
--- a/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs
+++ b/src/Oms.Application/Dtos/System/UpdateWarehouseInput.cs
@@ -43,4 +43,15 @@ public class UpdateWarehouseInput
/// 发货地址。
///
public string? ShippingAddress { get; set; }
+
+ ///
+ /// 展示排序(越小越靠前;可不填)。
+ ///
+ public int? Sort { get; set; }
+
+ ///
+ /// 备注(可不填)。
+ ///
+ [StringLength(512)]
+ public string? Remark { get; set; }
}
diff --git a/src/Oms.Application/Dtos/System/WarehouseOutput.cs b/src/Oms.Application/Dtos/System/WarehouseOutput.cs
index 95bf877..26314b6 100644
--- a/src/Oms.Application/Dtos/System/WarehouseOutput.cs
+++ b/src/Oms.Application/Dtos/System/WarehouseOutput.cs
@@ -35,6 +35,16 @@ public class WarehouseOutput
///
public string? ShippingAddress { get; set; }
+ ///
+ /// 展示排序(越小越靠前;为空则排在有排序值的记录之后)。
+ ///
+ public int? Sort { get; set; }
+
+ ///
+ /// 备注。
+ ///
+ public string? Remark { get; set; }
+
///
/// 创建时间。
///
diff --git a/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs b/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs
index a6b792e..1067733 100644
--- a/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs
+++ b/src/Oms.Application/Dtos/System/WarehousePaginatedOutput.cs
@@ -40,6 +40,16 @@ public class WarehousePaginatedOutput
///
public string? ShippingAddress { get; set; }
+ ///
+ /// 展示排序(越小越靠前;为空则排在有排序值的记录之后)。
+ ///
+ public int? Sort { get; set; }
+
+ ///
+ /// 备注。
+ ///
+ public string? Remark { get; set; }
+
///
/// 创建时间。
///
diff --git a/src/Oms.Application/Services/Rbac/OmsUserService.cs b/src/Oms.Application/Services/Rbac/OmsUserService.cs
index 8abb5fb..46a3439 100644
--- a/src/Oms.Application/Services/Rbac/OmsUserService.cs
+++ b/src/Oms.Application/Services/Rbac/OmsUserService.cs
@@ -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
{
///
@@ -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);
+ }
}
///
@@ -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);
+ }
}
///
@@ -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;
diff --git a/src/Oms.Application/Services/System/WarehouseService.cs b/src/Oms.Application/Services/System/WarehouseService.cs
index 8532769..64dc16f 100644
--- a/src/Oms.Application/Services/System/WarehouseService.cs
+++ b/src/Oms.Application/Services/System/WarehouseService.cs
@@ -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 { [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(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;
+ }
}
diff --git a/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs b/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs
index e726af9..6241a72 100644
--- a/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs
+++ b/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs
@@ -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;
///
-/// 微信/客户注册前校验 RBAC 登录账号,避免 user 表已存在但 t_customer 未创建时 EF 抛出英文 DbUpdateException。
+/// 微信/客户注册、后台建用户前校验 RBAC 登录身份,避免 EF 抛出英文 DbUpdateException。
///
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);
+ }
+
+ ///
+ /// 校验登录账号/邮箱是否可用。
+ /// 为 true 时提示面向后台建员工用户(会说明可能已被客户账号占用)。
+ ///
+ 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)
+ ///
+ /// 后台创建/更新员工用户时的字段格式与必填校验(不含唯一性)。
+ ///
+ 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(
+ $"""
+ 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? 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(
+ $"""
+ 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 ExistsActiveUserByAccountAsync(
+ private static async Task 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(
+ $"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(
$"SELECT 1 AS Value FROM `user` WHERE `Account` = {account} LIMIT 1")
.FirstOrDefaultAsync(cancellationToken);
- return hit.HasValue;
+ return any.HasValue;
}
- private static async Task ExistsActiveUserByEmailAsync(
+ private static async Task 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(
+ $"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(
$"SELECT 1 AS Value FROM `user` WHERE `Email` = {email} LIMIT 1")
.FirstOrDefaultAsync(cancellationToken);
- return hit.HasValue;
+ return any.HasValue;
+ }
+
+ private static async Task ExistsUserByPhoneAsync(
+ FakeRbacDbContext rbacDbContext,
+ string phone,
+ Guid? excludeUserId,
+ CancellationToken cancellationToken)
+ {
+ if (excludeUserId is Guid id && id != Guid.Empty)
+ {
+ var hit = await rbacDbContext.Database
+ .SqlQuery(
+ $"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(
+ $"SELECT 1 AS Value FROM `user` WHERE `Phone` = {phone} LIMIT 1")
+ .FirstOrDefaultAsync(cancellationToken);
+ return any.HasValue;
+ }
+
+ private static async Task ExistsUserByWechatIdAsync(
+ FakeRbacDbContext rbacDbContext,
+ string wechatId,
+ Guid? excludeUserId,
+ CancellationToken cancellationToken)
+ {
+ if (excludeUserId is Guid id && id != Guid.Empty)
+ {
+ var hit = await rbacDbContext.Database
+ .SqlQuery(
+ $"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(
+ $"SELECT 1 AS Value FROM `user` WHERE `WechatId` = {wechatId} LIMIT 1")
+ .FirstOrDefaultAsync(cancellationToken);
+ return any.HasValue;
}
private static string ExtractDeepMessage(Exception ex)
diff --git a/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs b/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs
index c9d981e..91d436d 100644
--- a/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs
+++ b/src/Oms.Domain/SystemAggregate/Entities/WarehouseAddress.cs
@@ -15,4 +15,16 @@ public class WarehouseAddress : OmsAuditedAggregateRoot
public string? ContactPhone { get; set; }
public string? ShippingAddress { get; set; }
+
+ ///
+ /// 展示排序(越小越靠前;为空则排在有排序值的记录之后)。
+ ///
+ [SugarColumn(IsNullable = true)]
+ public int? Sort { get; set; }
+
+ ///
+ /// 备注(选填,展示用)。
+ ///
+ [SugarColumn(Length = 512, IsNullable = true)]
+ public string? Remark { get; set; }
}