From 69d234c705dd5a32ec7d02617055f77ce35e8429 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Mon, 27 Jul 2026 13:07:29 +0800 Subject: [PATCH] feat: simplify WeChat register and require contact before order Co-authored-by: Cursor --- db.sql | 2 +- sql/20260727_customer_phone_nullable.sql | 5 ++ .../Dtos/Customers/CustomerContactDtos.cs | 37 ++++++++++ .../Services/Customers/CustomerService.cs | 71 +++++++++++++++++++ .../Orders/OrderService.ParcelBilling.cs | 2 + .../Services/Orders/OrderService.Pickup.cs | 3 + .../Shared/CustomerContactHelper.cs | 35 +++++++++ .../Shared/OmsRbacRegistrationGuard.cs | 4 +- .../CustomersAggregate/Entities/Customer.cs | 2 +- src/SimpleAdmin.Api/WeChat/Dtos/WeChatDtos.cs | 15 ++-- .../WeChat/WeChatCustomerService.cs | 42 +++++++---- 11 files changed, 196 insertions(+), 22 deletions(-) create mode 100644 sql/20260727_customer_phone_nullable.sql create mode 100644 src/Oms.Application/Dtos/Customers/CustomerContactDtos.cs create mode 100644 src/Oms.Application/Shared/CustomerContactHelper.cs 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("邮箱已存在"); }