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