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',
|
||||
`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 '已用额度',
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
|
||||
@ -311,6 +311,8 @@ public partial class OrderService
|
||||
{
|
||||
throw new BusinessException("客户不存在或状态不可用");
|
||||
}
|
||||
|
||||
CustomerContactHelper.EnsureContactComplete(customer);
|
||||
}
|
||||
|
||||
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingAsync(SalesProduct product, DateTime bindTime)
|
||||
|
||||
@ -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)
|
||||
{
|
||||
|
||||
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))
|
||||
{
|
||||
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)
|
||||
|
||||
@ -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; }
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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("邮箱已存在");
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user