admin/src/SimpleAdmin.Api/WeChat/WeChatCustomerService.cs
zzh f972a28d79
All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
feat: 落地滚动会员、券发放与邀请分成奖励
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 17:40:24 +08:00

379 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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