feat: simplify WeChat register and require contact before order

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-27 13:07:29 +08:00
parent 5003cacd79
commit 69d234c705
11 changed files with 196 additions and 22 deletions

2
db.sql
View File

@ -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 '已用额度',

View File

@ -0,0 +1,5 @@
-- 微信注册简化:客户手机号改为可空(空值请用 NULL,勿存空字符串)
-- 执行环境:生产库先备份;与 docs/微信注册简化与联系方式补全方案.md 一致
ALTER TABLE `t_customer`
MODIFY COLUMN `Phone` VARCHAR(16) NULL COMMENT '手机号,用于联系;可空,唯一(NULL 可多条)';

View File

@ -0,0 +1,37 @@
using System.ComponentModel.DataAnnotations;
namespace Oms.Application.Dtos.Customers;
/// <summary>
/// 客户联系方式完整度。
/// </summary>
public class CustomerProfileCompletenessOutput
{
public bool PhoneFilled { get; set; }
public bool EmailFilled { get; set; }
/// <summary>缺手机或邮箱时为 true。</summary>
public bool NeedContact { get; set; }
public string CustomerCode { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Email { get; set; }
}
/// <summary>
/// 补全手机号与邮箱(下单前)。
/// </summary>
public class CompleteCustomerContactInput
{
[Required]
[StringLength(16)]
public string Phone { get; set; } = string.Empty;
[Required]
[EmailAddress]
[StringLength(64)]
public string Email { get; set; } = string.Empty;
}

View File

@ -141,6 +141,60 @@ public class CustomerService : ApplicationService
return await ServiceAdvisorResolver.ResolveAsync(customer.CustomerService, cancellationToken);
}
/// <summary>
/// 当前登录客户联系方式完整度(下单/揽收前校验)。
/// </summary>
[HttpGet("/customer/profile-completeness")]
public async Task<CustomerProfileCompletenessOutput> 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
};
}
/// <summary>
/// 补全手机号与邮箱(下单前)。
/// </summary>
[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);
}
/// <summary>
/// 新增客户。
/// </summary>
@ -389,6 +443,23 @@ public class CustomerService : ApplicationService
return role.Id;
}
private async Task<Customer> GetCurrentCustomerOrThrowAsync(CancellationToken cancellationToken = default)
{
var userId = CurrentUser.GetId<int>();
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<Customer> GetCurrentMainCustomerOrThrowAsync()
{
var userId = CurrentUser.GetId<int>();

View File

@ -311,6 +311,8 @@ public partial class OrderService
{
throw new BusinessException("客户不存在或状态不可用");
}
CustomerContactHelper.EnsureContactComplete(customer);
}
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)

View File

@ -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)
{

View File

@ -0,0 +1,35 @@
using Fake.Application;
using Oms.Domain.CustomersAggregate.Entities;
namespace Oms.Application.Shared;
/// <summary>
/// 客户联系方式完整度(下单/揽收前必填手机+邮箱)。
/// </summary>
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;
}
}

View File

@ -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)

View File

@ -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; }

View File

@ -65,12 +65,14 @@ public class WeChatCustomerRegisterInput
[Required]
public string CustomerName { get; set; } = string.Empty;
[Required]
public string Phone { get; set; } = string.Empty;
/// <summary>可选;下单前再补全。</summary>
[StringLength(16)]
public string? Phone { get; set; }
[Required]
/// <summary>可选;下单前再补全。</summary>
[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]

View File

@ -76,10 +76,10 @@ public class WeChatCustomerService(
}
/// <summary>
/// 微信授权注册客户。
/// 微信授权注册客户(手机/邮箱可选,下单前补全)。
/// </summary>
[HttpPost("/wechat/customer/register")]
public async Task RegisterAsync(
public async Task<WeChatCustomerRegisterOutput> 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("邮箱已存在");
}