diff --git a/db.sql b/db.sql
index c3e4e3f..3704730 100644
--- a/db.sql
+++ b/db.sql
@@ -908,7 +908,7 @@ CREATE TABLE `t_customer` (
`WechatAvatar` VARCHAR(255) NULL COMMENT '微信头像URL',
`WechatNickname` VARCHAR(64) NULL COMMENT '微信昵称',
`Email` VARCHAR(64) NULL COMMENT '邮箱,用于登录/找回密码',
- `Phone` VARCHAR(16) NOT NULL COMMENT '手机号,用于登录/验证',
+ `Phone` VARCHAR(16) NULL COMMENT '手机号,用于联系;可空,唯一(NULL 可多条)',
`Balance` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',
`TotalQuota` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '总额度',
`UsedQuota` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '已用额度',
diff --git a/sql/20260727_customer_phone_nullable.sql b/sql/20260727_customer_phone_nullable.sql
new file mode 100644
index 0000000..1788f35
--- /dev/null
+++ b/sql/20260727_customer_phone_nullable.sql
@@ -0,0 +1,5 @@
+-- 微信注册简化:客户手机号改为可空(空值请用 NULL,勿存空字符串)
+-- 执行环境:生产库先备份;与 docs/微信注册简化与联系方式补全方案.md 一致
+
+ALTER TABLE `t_customer`
+ MODIFY COLUMN `Phone` VARCHAR(16) NULL COMMENT '手机号,用于联系;可空,唯一(NULL 可多条)';
diff --git a/src/Oms.Application/Dtos/Customers/CustomerContactDtos.cs b/src/Oms.Application/Dtos/Customers/CustomerContactDtos.cs
new file mode 100644
index 0000000..8e24fb6
--- /dev/null
+++ b/src/Oms.Application/Dtos/Customers/CustomerContactDtos.cs
@@ -0,0 +1,37 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Oms.Application.Dtos.Customers;
+
+///
+/// 客户联系方式完整度。
+///
+public class CustomerProfileCompletenessOutput
+{
+ public bool PhoneFilled { get; set; }
+
+ public bool EmailFilled { get; set; }
+
+ /// 缺手机或邮箱时为 true。
+ public bool NeedContact { get; set; }
+
+ public string CustomerCode { get; set; } = string.Empty;
+
+ public string? Phone { get; set; }
+
+ public string? Email { get; set; }
+}
+
+///
+/// 补全手机号与邮箱(下单前)。
+///
+public class CompleteCustomerContactInput
+{
+ [Required]
+ [StringLength(16)]
+ public string Phone { get; set; } = string.Empty;
+
+ [Required]
+ [EmailAddress]
+ [StringLength(64)]
+ public string Email { get; set; } = string.Empty;
+}
diff --git a/src/Oms.Application/Services/Customers/CustomerService.cs b/src/Oms.Application/Services/Customers/CustomerService.cs
index f9172a4..8772f4f 100644
--- a/src/Oms.Application/Services/Customers/CustomerService.cs
+++ b/src/Oms.Application/Services/Customers/CustomerService.cs
@@ -141,6 +141,60 @@ public class CustomerService : ApplicationService
return await ServiceAdvisorResolver.ResolveAsync(customer.CustomerService, cancellationToken);
}
+ ///
+ /// 当前登录客户联系方式完整度(下单/揽收前校验)。
+ ///
+ [HttpGet("/customer/profile-completeness")]
+ public async Task GetProfileCompletenessAsync(
+ CancellationToken cancellationToken = default)
+ {
+ var customer = await GetCurrentCustomerOrThrowAsync(cancellationToken);
+ var phoneFilled = CustomerContactHelper.HasPhone(customer);
+ var emailFilled = CustomerContactHelper.HasEmail(customer);
+ return new CustomerProfileCompletenessOutput
+ {
+ PhoneFilled = phoneFilled,
+ EmailFilled = emailFilled,
+ NeedContact = !phoneFilled || !emailFilled,
+ CustomerCode = customer.CustomerCode,
+ Phone = customer.Phone,
+ Email = customer.Email
+ };
+ }
+
+ ///
+ /// 补全手机号与邮箱(下单前)。
+ ///
+ [HttpPost("/customer/complete-contact")]
+ public async Task CompleteContactAsync(
+ [FromBody] CompleteCustomerContactInput input,
+ CancellationToken cancellationToken = default)
+ {
+ var customer = await GetCurrentCustomerOrThrowAsync(cancellationToken);
+ var phone = WeChatRegistrationFieldNormalizer.TruncateRequired(
+ NormalizeRequiredText(input.Phone, "手机号"),
+ WeChatRegistrationFieldNormalizer.PhoneMaxLength);
+ var email = WeChatRegistrationFieldNormalizer.TruncateRequired(
+ NormalizeRequiredText(input.Email, "邮箱"),
+ WeChatRegistrationFieldNormalizer.EmailMaxLength);
+
+ var duplicatedPhone = await CustomerRepository.GetByPhoneAsync(phone, cancellationToken);
+ if (duplicatedPhone != null && duplicatedPhone.Id != customer.Id)
+ {
+ throw new BusinessException("手机号已存在");
+ }
+
+ var duplicatedEmail = await CustomerRepository.GetByEmailAsync(email, cancellationToken);
+ if (duplicatedEmail != null && duplicatedEmail.Id != customer.Id)
+ {
+ throw new BusinessException("邮箱已存在");
+ }
+
+ customer.Phone = phone;
+ customer.Email = email;
+ await CustomerRepository.UpdateAsync(customer);
+ }
+
///
/// 新增客户。
///
@@ -389,6 +443,23 @@ public class CustomerService : ApplicationService
return role.Id;
}
+ private async Task GetCurrentCustomerOrThrowAsync(CancellationToken cancellationToken = default)
+ {
+ var userId = CurrentUser.GetId();
+ if (userId <= 0)
+ {
+ throw new BusinessException("客户未登录");
+ }
+
+ var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken);
+ if (customer == null)
+ {
+ throw new BusinessException("客户不存在");
+ }
+
+ return customer;
+ }
+
private async Task GetCurrentMainCustomerOrThrowAsync()
{
var userId = CurrentUser.GetId();
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index 0e87d98..734a773 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -311,6 +311,8 @@ public partial class OrderService
{
throw new BusinessException("客户不存在或状态不可用");
}
+
+ CustomerContactHelper.EnsureContactComplete(customer);
}
private async Task ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)
diff --git a/src/Oms.Application/Services/Orders/OrderService.Pickup.cs b/src/Oms.Application/Services/Orders/OrderService.Pickup.cs
index e31fe88..e8158d4 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Pickup.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Pickup.cs
@@ -49,6 +49,9 @@ public partial class OrderService
CustomerRepository,
CurrentUser,
input.CustomerId);
+ var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId);
+ CustomerContactHelper.EnsureContactComplete(customer);
+
var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId);
if (senderAddress == null)
{
diff --git a/src/Oms.Application/Shared/CustomerContactHelper.cs b/src/Oms.Application/Shared/CustomerContactHelper.cs
new file mode 100644
index 0000000..e5f355c
--- /dev/null
+++ b/src/Oms.Application/Shared/CustomerContactHelper.cs
@@ -0,0 +1,35 @@
+using Fake.Application;
+using Oms.Domain.CustomersAggregate.Entities;
+
+namespace Oms.Application.Shared;
+
+///
+/// 客户联系方式完整度(下单/揽收前必填手机+邮箱)。
+///
+public static class CustomerContactHelper
+{
+ public const string ContactRequiredMessage = "请先完善手机号和邮箱后再下单";
+
+ public static bool HasPhone(Customer? customer) =>
+ !string.IsNullOrWhiteSpace(customer?.Phone);
+
+ public static bool HasEmail(Customer? customer) =>
+ !string.IsNullOrWhiteSpace(customer?.Email);
+
+ public static bool NeedsContact(Customer? customer) =>
+ customer == null || !HasPhone(customer) || !HasEmail(customer);
+
+ public static void EnsureContactComplete(Customer? customer)
+ {
+ if (NeedsContact(customer))
+ {
+ throw new BusinessException(ContactRequiredMessage);
+ }
+ }
+
+ public static string? NormalizeOptionalContact(string? value)
+ {
+ var trimmed = value?.Trim();
+ return string.IsNullOrWhiteSpace(trimmed) ? null : trimmed;
+ }
+}
diff --git a/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs b/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs
index 258c710..e726af9 100644
--- a/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs
+++ b/src/Oms.Application/Shared/OmsRbacRegistrationGuard.cs
@@ -20,7 +20,7 @@ public static class OmsRbacRegistrationGuard
if (await ExistsActiveUserByAccountAsync(rbacDbContext, normalizedAccount, cancellationToken))
{
- throw new BusinessException("登录账号已存在,请直接绑定或使用其他手机号");
+ throw new BusinessException("登录账号已存在,请直接绑定或使用其他账号");
}
if (normalizedEmail != null
@@ -39,7 +39,7 @@ public static class OmsRbacRegistrationGuard
|| message.Contains("uk_user_account", StringComparison.OrdinalIgnoreCase)
|| message.Contains("IX_user_Account", StringComparison.OrdinalIgnoreCase))
{
- return new BusinessException("登录账号已存在,请直接绑定或使用其他手机号");
+ return new BusinessException("登录账号已存在,请直接绑定或使用其他账号");
}
if (message.Contains("Email", StringComparison.OrdinalIgnoreCase)
diff --git a/src/Oms.Domain/CustomersAggregate/Entities/Customer.cs b/src/Oms.Domain/CustomersAggregate/Entities/Customer.cs
index 830a41a..b97281f 100644
--- a/src/Oms.Domain/CustomersAggregate/Entities/Customer.cs
+++ b/src/Oms.Domain/CustomersAggregate/Entities/Customer.cs
@@ -21,7 +21,7 @@ public class Customer : OmsAuditedAggregateRoot
public string? Email { get; set; }
- public string Phone { get; set; } = string.Empty;
+ public string? Phone { get; set; }
public decimal Balance { get; set; }
diff --git a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
index 2415f6e..d22a6d7 100644
--- a/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
+++ b/src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs
@@ -65,12 +65,14 @@ public class WeChatCustomerRegisterInput
[Required]
public string CustomerName { get; set; } = string.Empty;
- [Required]
- public string Phone { get; set; } = string.Empty;
+ /// 可选;下单前再补全。
+ [StringLength(16)]
+ public string? Phone { get; set; }
- [Required]
+ /// 可选;下单前再补全。
[EmailAddress]
- public string Email { get; set; } = string.Empty;
+ [StringLength(64)]
+ public string? Email { get; set; }
[Required]
[MinLength(6)]
@@ -85,6 +87,11 @@ public class WeChatCustomerRegisterInput
public WeChatProfileInput Wechat { get; set; } = new();
}
+public class WeChatCustomerRegisterOutput
+{
+ public string CustomerCode { get; set; } = string.Empty;
+}
+
public class WeChatCustomerBindInput
{
[Required]
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs b/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs
index 32e3613..e7eb8b2 100644
--- a/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs
+++ b/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs
@@ -76,10 +76,10 @@ public class WeChatCustomerService(
}
///
- /// 微信授权注册客户。
+ /// 微信授权注册客户(手机/邮箱可选,下单前补全)。
///
[HttpPost("/wechat/customer/register")]
- public async Task RegisterAsync(
+ public async Task RegisterAsync(
[FromBody] WeChatCustomerRegisterInput input,
CancellationToken cancellationToken = default)
{
@@ -88,11 +88,11 @@ public class WeChatCustomerService(
var customerName = WeChatRegistrationFieldNormalizer.TruncateRequired(
NormalizeRequired(input.CustomerName, "客户名称"),
WeChatRegistrationFieldNormalizer.CustomerNameMaxLength);
- var phone = WeChatRegistrationFieldNormalizer.TruncateRequired(
- NormalizeRequired(input.Phone, "手机号"),
+ var phone = WeChatRegistrationFieldNormalizer.TruncateOptional(
+ CustomerContactHelper.NormalizeOptionalContact(input.Phone),
WeChatRegistrationFieldNormalizer.PhoneMaxLength);
- var email = WeChatRegistrationFieldNormalizer.TruncateRequired(
- NormalizeRequired(input.Email, "邮箱"),
+ var email = WeChatRegistrationFieldNormalizer.TruncateOptional(
+ CustomerContactHelper.NormalizeOptionalContact(input.Email),
WeChatRegistrationFieldNormalizer.EmailMaxLength);
var password = NormalizeRequired(input.Password, "密码");
var wechatOpenid = WeChatRegistrationFieldNormalizer.TruncateRequired(
@@ -115,7 +115,11 @@ public class WeChatCustomerService(
var customerCode = await CustomerCodeGenerator.GenerateNextAsync(customerRepository, cancellationToken);
await EnsureCustomerIdentityUniqueAsync(customerCode, phone, email, cancellationToken);
- await OmsRbacRegistrationGuard.EnsureLoginIdentityAvailableAsync(rbacDbContext, phone, email, cancellationToken);
+ await OmsRbacRegistrationGuard.EnsureLoginIdentityAvailableAsync(
+ rbacDbContext,
+ customerCode,
+ email,
+ cancellationToken);
var customerRoleId = await GetCustomerRoleIdOrThrowAsync(cancellationToken);
UserDto? user = null;
@@ -124,9 +128,9 @@ public class WeChatCustomerService(
user = await userService.CreateAsync(new UserCreateDto
{
Name = customerName,
- Account = phone,
+ Account = customerCode,
Password = password,
- Email = email,
+ Email = email ?? string.Empty,
RoleIds = [customerRoleId]
});
@@ -164,6 +168,8 @@ public class WeChatCustomerService(
{
logger.LogWarning(grantEx, "新人券发放失败: customerId={CustomerId}", entity.Id);
}
+
+ return new WeChatCustomerRegisterOutput { CustomerCode = customerCode };
}
catch (Exception ex)
{
@@ -172,7 +178,13 @@ public class WeChatCustomerService(
await TryRollbackRbacUserAsync(user.Id);
}
- logger.LogWarning(ex, "微信客户注册失败: phone={Phone}, email={Email}, openid={Openid}", phone, email, wechatOpenid);
+ logger.LogWarning(
+ ex,
+ "微信客户注册失败: customerCode={CustomerCode}, phone={Phone}, email={Email}, openid={Openid}",
+ customerCode,
+ phone,
+ email,
+ wechatOpenid);
throw ex is BusinessException ? ex : OmsRbacRegistrationGuard.ToSaveBusinessException(ex);
}
}
@@ -241,8 +253,8 @@ public class WeChatCustomerService(
private async Task EnsureCustomerIdentityUniqueAsync(
string customerCode,
- string phone,
- string email,
+ string? phone,
+ string? email,
CancellationToken cancellationToken)
{
if (await customerRepository.GetByCustomerCodeAsync(customerCode, cancellationToken) != null)
@@ -250,12 +262,14 @@ public class WeChatCustomerService(
throw new BusinessException("客户编号已存在");
}
- if (await customerRepository.ExistsPhoneAnyAsync(phone, cancellationToken))
+ if (!string.IsNullOrWhiteSpace(phone)
+ && await customerRepository.ExistsPhoneAnyAsync(phone, cancellationToken))
{
throw new BusinessException("手机号已存在");
}
- if (await customerRepository.ExistsEmailAnyAsync(email, cancellationToken))
+ if (!string.IsNullOrWhiteSpace(email)
+ && await customerRepository.ExistsEmailAnyAsync(email, cancellationToken))
{
throw new BusinessException("邮箱已存在");
}