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;
///
/// 微信 OAuth 注册 / 绑定客户。
///
[AllowAnonymous]
[ApiExplorerSettings(GroupName = "WeChat")]
public class WeChatCustomerService(
IWeChatMpApiClient weChatMpApiClient,
ICustomerRepository customerRepository,
ICustomerReferralCodeRepository referralCodeRepository,
IMemberLevelRepository memberLevelRepository,
MarketingCouponManager couponManager,
UserService userService,
RoleService roleService,
FakeRbacDbContext rbacDbContext,
ILogger logger) : ApplicationService
{
private const string CustomerRoleCode = "Customer";
///
/// OAuth code 换取微信用户信息。
///
[HttpPost("/wechat/oauth/user-info")]
public async Task 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
};
}
///
/// 微信授权注册客户(手机/邮箱可选,下单前补全)。
///
[HttpPost("/wechat/customer/register")]
public async Task 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(),
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);
}
}
///
/// 微信授权绑定已有客户。
///
[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 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 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 GetRbacUserMapIdOrThrowAsync(Guid id, CancellationToken cancellationToken)
{
var userId = await rbacDbContext.Database.SqlQuery(
$"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;
}
}