All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
Co-authored-by: Cursor <cursoragent@cursor.com>
379 lines
14 KiB
C#
379 lines
14 KiB
C#
using Fake.Application;
|
||
using Fake.Domain.Exceptions;
|
||
using Fake.Rbac.Application.Dtos.Role;
|
||
using Fake.Rbac.Application.Dtos.User;
|
||
using Fake.Rbac.Application.Services;
|
||
using Fake.Rbac.Infrastructure;
|
||
using Microsoft.AspNetCore.Authorization;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.Extensions.Logging;
|
||
using Oms.Application.Shared;
|
||
using Oms.Domain.CustomersAggregate.Entities;
|
||
using Oms.Domain.CustomersAggregate.Enums;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
using Oms.Domain.MarketingAggregate.Repositories;
|
||
using SimpleAdmin.Api.WeChat.Dtos;
|
||
using SimpleAdmin.Api.WeChat.Services;
|
||
|
||
namespace SimpleAdmin.Api.WeChat;
|
||
|
||
/// <summary>
|
||
/// 微信 OAuth 注册 / 绑定客户。
|
||
/// </summary>
|
||
[AllowAnonymous]
|
||
[ApiExplorerSettings(GroupName = "WeChat")]
|
||
public class WeChatCustomerService(
|
||
IWeChatMpApiClient weChatMpApiClient,
|
||
ICustomerRepository customerRepository,
|
||
ICustomerReferralCodeRepository referralCodeRepository,
|
||
IMemberLevelRepository memberLevelRepository,
|
||
MarketingCouponManager couponManager,
|
||
UserService userService,
|
||
RoleService roleService,
|
||
FakeRbacDbContext rbacDbContext,
|
||
ILogger<WeChatCustomerService> logger) : ApplicationService
|
||
{
|
||
private const string CustomerRoleCode = "Customer";
|
||
|
||
/// <summary>
|
||
/// OAuth code 换取微信用户信息。
|
||
/// </summary>
|
||
[HttpPost("/wechat/oauth/user-info")]
|
||
public async Task<WeChatOAuthUserInfoOutput> GetOAuthUserInfoAsync(
|
||
[FromBody] WeChatOAuthCodeInput input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(input.Code))
|
||
{
|
||
throw new BusinessException("缺少 code");
|
||
}
|
||
|
||
var token = await weChatMpApiClient.GetOAuthAccessTokenAsync(input.Code.Trim(), cancellationToken);
|
||
if (token == null)
|
||
{
|
||
throw new BusinessException("获取授权信息出现错误");
|
||
}
|
||
|
||
var userInfo = await weChatMpApiClient.GetOAuthUserInfoAsync(token.AccessToken, token.OpenId, cancellationToken);
|
||
if (userInfo == null)
|
||
{
|
||
throw new BusinessException("获取授权信息出现错误");
|
||
}
|
||
|
||
var existing = await customerRepository.GetByWechatOpenidAsync(userInfo.Openid, cancellationToken);
|
||
return new WeChatOAuthUserInfoOutput
|
||
{
|
||
Openid = userInfo.Openid,
|
||
Nickname = userInfo.Nickname,
|
||
Sex = userInfo.Sex,
|
||
Province = userInfo.Province,
|
||
City = userInfo.City,
|
||
Country = userInfo.Country,
|
||
Headimgurl = userInfo.Headimgurl,
|
||
Unionid = userInfo.Unionid,
|
||
AlreadyBound = existing != null
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 微信授权注册客户(手机/邮箱可选,下单前补全)。
|
||
/// </summary>
|
||
[HttpPost("/wechat/customer/register")]
|
||
public async Task<WeChatCustomerRegisterOutput> RegisterAsync(
|
||
[FromBody] WeChatCustomerRegisterInput input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
ValidateWechatProfile(input.Wechat);
|
||
|
||
var customerName = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
||
NormalizeRequired(input.CustomerName, "客户名称"),
|
||
WeChatRegistrationFieldNormalizer.CustomerNameMaxLength);
|
||
var phone = WeChatRegistrationFieldNormalizer.TruncateOptional(
|
||
CustomerContactHelper.NormalizeOptionalContact(input.Phone),
|
||
WeChatRegistrationFieldNormalizer.PhoneMaxLength);
|
||
var email = WeChatRegistrationFieldNormalizer.TruncateOptional(
|
||
CustomerContactHelper.NormalizeOptionalContact(input.Email),
|
||
WeChatRegistrationFieldNormalizer.EmailMaxLength);
|
||
var password = NormalizeRequired(input.Password, "密码");
|
||
var wechatOpenid = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
||
input.Wechat.Openid.Trim(),
|
||
WeChatRegistrationFieldNormalizer.OpenidMaxLength);
|
||
var wechatNickname = WeChatRegistrationFieldNormalizer.TruncateOptional(
|
||
input.Wechat.Nickname,
|
||
WeChatRegistrationFieldNormalizer.NicknameMaxLength);
|
||
var wechatAvatar = WeChatRegistrationFieldNormalizer.NormalizeCustomerWechatAvatar(input.Wechat.Avatar);
|
||
|
||
if (password.Length < 6)
|
||
{
|
||
throw new BusinessException("密码长度不能少于6位");
|
||
}
|
||
|
||
if (await customerRepository.ExistsWechatOpenidAnyAsync(wechatOpenid, cancellationToken))
|
||
{
|
||
throw new BusinessException("当前微信已绑定账号");
|
||
}
|
||
|
||
var customerCode = await CustomerCodeGenerator.GenerateNextAsync(customerRepository, cancellationToken);
|
||
await EnsureCustomerIdentityUniqueAsync(customerCode, phone, email, cancellationToken);
|
||
await OmsRbacRegistrationGuard.EnsureLoginIdentityAvailableAsync(
|
||
rbacDbContext,
|
||
customerCode,
|
||
email,
|
||
cancellationToken);
|
||
|
||
var customerRoleId = await GetCustomerRoleIdOrThrowAsync(cancellationToken);
|
||
UserDto? user = null;
|
||
try
|
||
{
|
||
user = await userService.CreateAsync(new UserCreateDto
|
||
{
|
||
Name = customerName,
|
||
Account = customerCode,
|
||
Password = password,
|
||
// 禁止 Email='':uk_user_email 会把第二条空邮箱当成占用
|
||
Email = CustomerContactHelper.ResolveRbacEmailOrPlaceholder(email, customerCode),
|
||
RoleIds = [customerRoleId]
|
||
});
|
||
|
||
var userId = await GetRbacUserMapIdOrThrowAsync(user.Id, cancellationToken);
|
||
var entity = new Customer
|
||
{
|
||
UserId = userId,
|
||
CustomerCode = customerCode,
|
||
CustomerName = customerName,
|
||
Phone = phone,
|
||
Email = email,
|
||
Password = OmsPasswordHelper.ToMd5(password),
|
||
WechatOpenid = wechatOpenid,
|
||
WechatNickname = wechatNickname,
|
||
WechatAvatar = wechatAvatar,
|
||
Status = CustomerStatus.Normal,
|
||
MemberLevelId = await MarketingCustomerHelper.ResolveDefaultMemberLevelIdAsync(
|
||
memberLevelRepository,
|
||
cancellationToken),
|
||
PriceTierKey = MarketingCustomerHelper.DefaultNewCustomerPriceTierKey
|
||
};
|
||
await MarketingReferralBinder.BindReferrerOnCreateAsync(
|
||
entity,
|
||
input.InviteCode,
|
||
referralCodeRepository,
|
||
customerRepository,
|
||
memberLevelRepository,
|
||
LazyServiceProvider.GetRequiredService<IReferralCommissionRuleRepository>(),
|
||
cancellationToken);
|
||
if (!string.IsNullOrWhiteSpace(input.InviteCode) && entity.ReferrerCustomerId.HasValue)
|
||
{
|
||
entity.ReferralChannel = "wechat";
|
||
}
|
||
await StaffAdvisorBinder.BindAdvisorOnCreateAsync(
|
||
entity,
|
||
input.AdvisorCode,
|
||
rbacDbContext,
|
||
cancellationToken);
|
||
await customerRepository.InsertAsync(entity);
|
||
|
||
try
|
||
{
|
||
await couponManager.GrantNewcomerCouponsAsync(entity.Id, cancellationToken);
|
||
if (entity.ReferrerCustomerId.HasValue)
|
||
{
|
||
await couponManager.GrantInviteeCouponAsync(entity.Id, cancellationToken);
|
||
}
|
||
}
|
||
catch (Exception grantEx)
|
||
{
|
||
logger.LogWarning(grantEx, "新人券发放失败: customerId={CustomerId}", entity.Id);
|
||
}
|
||
|
||
return new WeChatCustomerRegisterOutput { CustomerCode = customerCode };
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (user != null)
|
||
{
|
||
await TryRollbackRbacUserAsync(user.Id);
|
||
}
|
||
|
||
logger.LogWarning(
|
||
ex,
|
||
"微信客户注册失败: customerCode={CustomerCode}, phone={Phone}, email={Email}, openid={Openid}",
|
||
customerCode,
|
||
phone,
|
||
email,
|
||
wechatOpenid);
|
||
throw ex is BusinessException ? ex : OmsRbacRegistrationGuard.ToSaveBusinessException(ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 微信授权绑定已有客户。
|
||
/// </summary>
|
||
[HttpPost("/wechat/customer/bind")]
|
||
public async Task BindAsync(
|
||
[FromBody] WeChatCustomerBindInput input,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
ValidateWechatProfile(input.Wechat);
|
||
|
||
var account = NormalizeRequired(input.Account, "账号");
|
||
var password = NormalizeRequired(input.Password, "密码");
|
||
var wechatOpenid = WeChatRegistrationFieldNormalizer.TruncateRequired(
|
||
input.Wechat.Openid.Trim(),
|
||
WeChatRegistrationFieldNormalizer.OpenidMaxLength);
|
||
var wechatNickname = WeChatRegistrationFieldNormalizer.TruncateOptional(
|
||
input.Wechat.Nickname,
|
||
WeChatRegistrationFieldNormalizer.NicknameMaxLength);
|
||
var wechatAvatar = WeChatRegistrationFieldNormalizer.NormalizeCustomerWechatAvatar(input.Wechat.Avatar);
|
||
|
||
if (await customerRepository.ExistsWechatOpenidAnyAsync(wechatOpenid, cancellationToken))
|
||
{
|
||
throw new BusinessException("当前微信已绑定账号");
|
||
}
|
||
|
||
var customer = await FindCustomerByAccountAsync(account, cancellationToken);
|
||
if (customer == null)
|
||
{
|
||
throw new BusinessException("未查询到账号信息");
|
||
}
|
||
|
||
if (customer.ParentCustomerId.HasValue)
|
||
{
|
||
throw new BusinessException("子客户不支持绑定微信");
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(customer.WechatOpenid))
|
||
{
|
||
throw new BusinessException("当前账号已绑定微信");
|
||
}
|
||
|
||
ValidateMainCustomerStatus(customer);
|
||
|
||
if (!string.Equals(customer.Password, OmsPasswordHelper.ToMd5(password), StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
throw new BusinessException("账号或密码错误");
|
||
}
|
||
|
||
customer.WechatOpenid = wechatOpenid;
|
||
customer.WechatNickname = wechatNickname ?? customer.WechatNickname;
|
||
customer.WechatAvatar = wechatAvatar ?? customer.WechatAvatar;
|
||
await customerRepository.UpdateAsync(customer, cancellationToken);
|
||
}
|
||
|
||
private static void ValidateWechatProfile(WeChatProfileInput wechat)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(wechat.Openid))
|
||
{
|
||
throw new BusinessException("缺少 Openid 授权信息");
|
||
}
|
||
}
|
||
|
||
private async Task EnsureCustomerIdentityUniqueAsync(
|
||
string customerCode,
|
||
string? phone,
|
||
string? email,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
if (await customerRepository.GetByCustomerCodeAsync(customerCode, cancellationToken) != null)
|
||
{
|
||
throw new BusinessException("客户编号已存在");
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(phone)
|
||
&& await customerRepository.ExistsPhoneAnyAsync(phone, cancellationToken))
|
||
{
|
||
throw new BusinessException("手机号已存在");
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(email)
|
||
&& await customerRepository.ExistsEmailAnyAsync(email, cancellationToken))
|
||
{
|
||
throw new BusinessException("邮箱已存在");
|
||
}
|
||
}
|
||
|
||
private async Task<Customer?> FindCustomerByAccountAsync(string account, CancellationToken cancellationToken)
|
||
{
|
||
var customer = await customerRepository.GetByPhoneAsync(account, cancellationToken);
|
||
if (customer != null)
|
||
{
|
||
return customer;
|
||
}
|
||
|
||
customer = await customerRepository.GetByEmailAsync(account, cancellationToken);
|
||
if (customer != null)
|
||
{
|
||
return customer;
|
||
}
|
||
|
||
return await customerRepository.GetByCustomerCodeAsync(account, cancellationToken);
|
||
}
|
||
|
||
private async Task<Guid> GetCustomerRoleIdOrThrowAsync(CancellationToken cancellationToken)
|
||
{
|
||
var roles = await roleService.GetAllRolesAsync();
|
||
var role = roles.FirstOrDefault(x => string.Equals(x.Code, CustomerRoleCode, StringComparison.OrdinalIgnoreCase));
|
||
if (role == null)
|
||
{
|
||
throw new BusinessException("客户角色未初始化");
|
||
}
|
||
|
||
return role.Id;
|
||
}
|
||
|
||
private async Task<int> GetRbacUserMapIdOrThrowAsync(Guid id, CancellationToken cancellationToken)
|
||
{
|
||
var userId = await rbacDbContext.Database.SqlQuery<int?>(
|
||
$"select `userid` as `Value` from `user` where `Id` = {id}")
|
||
.FirstOrDefaultAsync(cancellationToken);
|
||
if (!userId.HasValue || userId.Value <= 0)
|
||
{
|
||
throw new BusinessException("用户编号创建失败");
|
||
}
|
||
|
||
return userId.Value;
|
||
}
|
||
|
||
private async Task TryRollbackRbacUserAsync(Guid userId)
|
||
{
|
||
try
|
||
{
|
||
await userService.DeleteAsync(userId);
|
||
}
|
||
catch
|
||
{
|
||
// ignore rollback failure
|
||
}
|
||
}
|
||
|
||
private static void ValidateMainCustomerStatus(Customer customer)
|
||
{
|
||
if (customer.Status == CustomerStatus.Disabled)
|
||
{
|
||
throw new BusinessException("客户已禁用");
|
||
}
|
||
|
||
if (customer.Status == CustomerStatus.Frozen)
|
||
{
|
||
throw new BusinessException("客户已冻结");
|
||
}
|
||
}
|
||
|
||
private static string NormalizeRequired(string? value, string fieldName)
|
||
{
|
||
var normalized = value?.Trim();
|
||
if (string.IsNullOrWhiteSpace(normalized))
|
||
{
|
||
throw new BusinessException($"{fieldName}不能为空");
|
||
}
|
||
|
||
return normalized;
|
||
}
|
||
|
||
private static string? NormalizeOptional(string? value)
|
||
{
|
||
var normalized = value?.Trim();
|
||
return string.IsNullOrWhiteSpace(normalized) ? null : normalized;
|
||
}
|
||
}
|