feat: simplify WeChat register and require contact before order
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5003cacd79
commit
69d234c705
2
db.sql
2
db.sql
@ -908,7 +908,7 @@ CREATE TABLE `t_customer` (
|
|||||||
`WechatAvatar` VARCHAR(255) NULL COMMENT '微信头像URL',
|
`WechatAvatar` VARCHAR(255) NULL COMMENT '微信头像URL',
|
||||||
`WechatNickname` VARCHAR(64) NULL COMMENT '微信昵称',
|
`WechatNickname` VARCHAR(64) NULL COMMENT '微信昵称',
|
||||||
`Email` 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 '账户余额',
|
`Balance` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额',
|
||||||
`TotalQuota` 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 '已用额度',
|
`UsedQuota` DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '已用额度',
|
||||||
|
|||||||
5
sql/20260727_customer_phone_nullable.sql
Normal file
5
sql/20260727_customer_phone_nullable.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- 微信注册简化:客户手机号改为可空(空值请用 NULL,勿存空字符串)
|
||||||
|
-- 执行环境:生产库先备份;与 docs/微信注册简化与联系方式补全方案.md 一致
|
||||||
|
|
||||||
|
ALTER TABLE `t_customer`
|
||||||
|
MODIFY COLUMN `Phone` VARCHAR(16) NULL COMMENT '手机号,用于联系;可空,唯一(NULL 可多条)';
|
||||||
37
src/Oms.Application/Dtos/Customers/CustomerContactDtos.cs
Normal file
37
src/Oms.Application/Dtos/Customers/CustomerContactDtos.cs
Normal 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;
|
||||||
|
}
|
||||||
@ -141,6 +141,60 @@ public class CustomerService : ApplicationService
|
|||||||
return await ServiceAdvisorResolver.ResolveAsync(customer.CustomerService, cancellationToken);
|
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>
|
||||||
/// 新增客户。
|
/// 新增客户。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -389,6 +443,23 @@ public class CustomerService : ApplicationService
|
|||||||
return role.Id;
|
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()
|
private async Task<Customer> GetCurrentMainCustomerOrThrowAsync()
|
||||||
{
|
{
|
||||||
var userId = CurrentUser.GetId<int>();
|
var userId = CurrentUser.GetId<int>();
|
||||||
|
|||||||
@ -311,6 +311,8 @@ public partial class OrderService
|
|||||||
{
|
{
|
||||||
throw new BusinessException("客户不存在或状态不可用");
|
throw new BusinessException("客户不存在或状态不可用");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CustomerContactHelper.EnsureContactComplete(customer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)
|
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)
|
||||||
|
|||||||
@ -49,6 +49,9 @@ public partial class OrderService
|
|||||||
CustomerRepository,
|
CustomerRepository,
|
||||||
CurrentUser,
|
CurrentUser,
|
||||||
input.CustomerId);
|
input.CustomerId);
|
||||||
|
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId);
|
||||||
|
CustomerContactHelper.EnsureContactComplete(customer);
|
||||||
|
|
||||||
var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId);
|
var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId);
|
||||||
if (senderAddress == null)
|
if (senderAddress == null)
|
||||||
{
|
{
|
||||||
|
|||||||
35
src/Oms.Application/Shared/CustomerContactHelper.cs
Normal file
35
src/Oms.Application/Shared/CustomerContactHelper.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ public static class OmsRbacRegistrationGuard
|
|||||||
|
|
||||||
if (await ExistsActiveUserByAccountAsync(rbacDbContext, normalizedAccount, cancellationToken))
|
if (await ExistsActiveUserByAccountAsync(rbacDbContext, normalizedAccount, cancellationToken))
|
||||||
{
|
{
|
||||||
throw new BusinessException("登录账号已存在,请直接绑定或使用其他手机号");
|
throw new BusinessException("登录账号已存在,请直接绑定或使用其他账号");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedEmail != null
|
if (normalizedEmail != null
|
||||||
@ -39,7 +39,7 @@ public static class OmsRbacRegistrationGuard
|
|||||||
|| message.Contains("uk_user_account", StringComparison.OrdinalIgnoreCase)
|
|| message.Contains("uk_user_account", StringComparison.OrdinalIgnoreCase)
|
||||||
|| message.Contains("IX_user_Account", StringComparison.OrdinalIgnoreCase))
|
|| message.Contains("IX_user_Account", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return new BusinessException("登录账号已存在,请直接绑定或使用其他手机号");
|
return new BusinessException("登录账号已存在,请直接绑定或使用其他账号");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.Contains("Email", StringComparison.OrdinalIgnoreCase)
|
if (message.Contains("Email", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public class Customer : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
|
||||||
public string Phone { get; set; } = string.Empty;
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
public decimal Balance { get; set; }
|
public decimal Balance { get; set; }
|
||||||
|
|
||||||
|
|||||||
@ -65,12 +65,14 @@ public class WeChatCustomerRegisterInput
|
|||||||
[Required]
|
[Required]
|
||||||
public string CustomerName { get; set; } = string.Empty;
|
public string CustomerName { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Required]
|
/// <summary>可选;下单前再补全。</summary>
|
||||||
public string Phone { get; set; } = string.Empty;
|
[StringLength(16)]
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
[Required]
|
/// <summary>可选;下单前再补全。</summary>
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
public string Email { get; set; } = string.Empty;
|
[StringLength(64)]
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[MinLength(6)]
|
[MinLength(6)]
|
||||||
@ -85,6 +87,11 @@ public class WeChatCustomerRegisterInput
|
|||||||
public WeChatProfileInput Wechat { get; set; } = new();
|
public WeChatProfileInput Wechat { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class WeChatCustomerRegisterOutput
|
||||||
|
{
|
||||||
|
public string CustomerCode { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public class WeChatCustomerBindInput
|
public class WeChatCustomerBindInput
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
|
|||||||
@ -76,10 +76,10 @@ public class WeChatCustomerService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信授权注册客户。
|
/// 微信授权注册客户(手机/邮箱可选,下单前补全)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("/wechat/customer/register")]
|
[HttpPost("/wechat/customer/register")]
|
||||||
public async Task RegisterAsync(
|
public async Task<WeChatCustomerRegisterOutput> RegisterAsync(
|
||||||
[FromBody] WeChatCustomerRegisterInput input,
|
[FromBody] WeChatCustomerRegisterInput input,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@ -88,11 +88,11 @@ public class WeChatCustomerService(
|
|||||||
var customerName = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
var customerName = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
||||||
NormalizeRequired(input.CustomerName, "客户名称"),
|
NormalizeRequired(input.CustomerName, "客户名称"),
|
||||||
WeChatRegistrationFieldNormalizer.CustomerNameMaxLength);
|
WeChatRegistrationFieldNormalizer.CustomerNameMaxLength);
|
||||||
var phone = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
var phone = WeChatRegistrationFieldNormalizer.TruncateOptional(
|
||||||
NormalizeRequired(input.Phone, "手机号"),
|
CustomerContactHelper.NormalizeOptionalContact(input.Phone),
|
||||||
WeChatRegistrationFieldNormalizer.PhoneMaxLength);
|
WeChatRegistrationFieldNormalizer.PhoneMaxLength);
|
||||||
var email = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
var email = WeChatRegistrationFieldNormalizer.TruncateOptional(
|
||||||
NormalizeRequired(input.Email, "邮箱"),
|
CustomerContactHelper.NormalizeOptionalContact(input.Email),
|
||||||
WeChatRegistrationFieldNormalizer.EmailMaxLength);
|
WeChatRegistrationFieldNormalizer.EmailMaxLength);
|
||||||
var password = NormalizeRequired(input.Password, "密码");
|
var password = NormalizeRequired(input.Password, "密码");
|
||||||
var wechatOpenid = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
var wechatOpenid = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
||||||
@ -115,7 +115,11 @@ public class WeChatCustomerService(
|
|||||||
|
|
||||||
var customerCode = await CustomerCodeGenerator.GenerateNextAsync(customerRepository, cancellationToken);
|
var customerCode = await CustomerCodeGenerator.GenerateNextAsync(customerRepository, cancellationToken);
|
||||||
await EnsureCustomerIdentityUniqueAsync(customerCode, phone, email, 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);
|
var customerRoleId = await GetCustomerRoleIdOrThrowAsync(cancellationToken);
|
||||||
UserDto? user = null;
|
UserDto? user = null;
|
||||||
@ -124,9 +128,9 @@ public class WeChatCustomerService(
|
|||||||
user = await userService.CreateAsync(new UserCreateDto
|
user = await userService.CreateAsync(new UserCreateDto
|
||||||
{
|
{
|
||||||
Name = customerName,
|
Name = customerName,
|
||||||
Account = phone,
|
Account = customerCode,
|
||||||
Password = password,
|
Password = password,
|
||||||
Email = email,
|
Email = email ?? string.Empty,
|
||||||
RoleIds = [customerRoleId]
|
RoleIds = [customerRoleId]
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -164,6 +168,8 @@ public class WeChatCustomerService(
|
|||||||
{
|
{
|
||||||
logger.LogWarning(grantEx, "新人券发放失败: customerId={CustomerId}", entity.Id);
|
logger.LogWarning(grantEx, "新人券发放失败: customerId={CustomerId}", entity.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new WeChatCustomerRegisterOutput { CustomerCode = customerCode };
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -172,7 +178,13 @@ public class WeChatCustomerService(
|
|||||||
await TryRollbackRbacUserAsync(user.Id);
|
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);
|
throw ex is BusinessException ? ex : OmsRbacRegistrationGuard.ToSaveBusinessException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,8 +253,8 @@ public class WeChatCustomerService(
|
|||||||
|
|
||||||
private async Task EnsureCustomerIdentityUniqueAsync(
|
private async Task EnsureCustomerIdentityUniqueAsync(
|
||||||
string customerCode,
|
string customerCode,
|
||||||
string phone,
|
string? phone,
|
||||||
string email,
|
string? email,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (await customerRepository.GetByCustomerCodeAsync(customerCode, cancellationToken) != null)
|
if (await customerRepository.GetByCustomerCodeAsync(customerCode, cancellationToken) != null)
|
||||||
@ -250,12 +262,14 @@ public class WeChatCustomerService(
|
|||||||
throw new BusinessException("客户编号已存在");
|
throw new BusinessException("客户编号已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await customerRepository.ExistsPhoneAnyAsync(phone, cancellationToken))
|
if (!string.IsNullOrWhiteSpace(phone)
|
||||||
|
&& await customerRepository.ExistsPhoneAnyAsync(phone, cancellationToken))
|
||||||
{
|
{
|
||||||
throw new BusinessException("手机号已存在");
|
throw new BusinessException("手机号已存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await customerRepository.ExistsEmailAnyAsync(email, cancellationToken))
|
if (!string.IsNullOrWhiteSpace(email)
|
||||||
|
&& await customerRepository.ExistsEmailAnyAsync(email, cancellationToken))
|
||||||
{
|
{
|
||||||
throw new BusinessException("邮箱已存在");
|
throw new BusinessException("邮箱已存在");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user