chore: 优化代码注释

This commit is contained in:
2025-11-23 09:52:54 +08:00
parent 1169e1f220
commit ccadacaa9d
33 changed files with 457 additions and 221 deletions

View File

@@ -1,5 +1,3 @@
using System;
namespace TakeoutSaaS.Application.Identity.Contracts;
/// <summary>
@@ -7,12 +5,43 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
/// </summary>
public sealed class CurrentUserProfile
{
/// <summary>
/// 用户 ID。
/// </summary>
public Guid UserId { get; init; }
/// <summary>
/// 登录账号。
/// </summary>
public string Account { get; init; } = string.Empty;
/// <summary>
/// 展示名称。
/// </summary>
public string DisplayName { get; init; } = string.Empty;
/// <summary>
/// 所属租户 ID。
/// </summary>
public Guid TenantId { get; init; }
/// <summary>
/// 所属商户 ID平台管理员为空
/// </summary>
public Guid? MerchantId { get; init; }
/// <summary>
/// 角色集合。
/// </summary>
public string[] Roles { get; init; } = Array.Empty<string>();
/// <summary>
/// 权限集合。
/// </summary>
public string[] Permissions { get; init; } = Array.Empty<string>();
/// <summary>
/// 头像地址(可选)。
/// </summary>
public string? Avatar { get; init; }
}

View File

@@ -1,5 +1,3 @@
using System;
namespace TakeoutSaaS.Application.Identity.Contracts;
/// <summary>
@@ -7,10 +5,33 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
/// </summary>
public class TokenResponse
{
/// <summary>
/// 访问令牌JWT
/// </summary>
public string AccessToken { get; init; } = string.Empty;
/// <summary>
/// 访问令牌过期时间UTC
/// </summary>
public DateTime AccessTokenExpiresAt { get; init; }
/// <summary>
/// 刷新令牌。
/// </summary>
public string RefreshToken { get; init; } = string.Empty;
/// <summary>
/// 刷新令牌过期时间UTC
/// </summary>
public DateTime RefreshTokenExpiresAt { get; init; }
/// <summary>
/// 当前用户档案(可选,首次登录时可能为空)。
/// </summary>
public CurrentUserProfile? User { get; init; }
/// <summary>
/// 是否为新用户(首次登录)。
/// </summary>
public bool IsNewUser { get; init; }
}

View File

@@ -1,11 +1,13 @@
using System;
namespace TakeoutSaaS.Application.Identity.Models;
/// <summary>
/// 刷新令牌描述。
/// 刷新令牌描述:存储刷新令牌的元数据信息
/// </summary>
public sealed record class RefreshTokenDescriptor(
/// <param name="Token">刷新令牌值</param>
/// <param name="UserId">关联的用户 ID</param>
/// <param name="ExpiresAt">过期时间UTC</param>
/// <param name="Revoked">是否已撤销</param>
public sealed record RefreshTokenDescriptor(
string Token,
Guid UserId,
DateTime ExpiresAt,

View File

@@ -1,6 +1,3 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
@@ -14,59 +11,75 @@ namespace TakeoutSaaS.Application.Identity.Services;
/// <summary>
/// 管理后台认证服务实现。
/// </summary>
public sealed class AdminAuthService : IAdminAuthService
public sealed class AdminAuthService(
IIdentityUserRepository userRepository,
IPasswordHasher<IdentityUser> passwordHasher,
IJwtTokenService jwtTokenService,
IRefreshTokenStore refreshTokenStore) : IAdminAuthService
{
private readonly IIdentityUserRepository _userRepository;
private readonly IPasswordHasher<IdentityUser> _passwordHasher;
private readonly IJwtTokenService _jwtTokenService;
private readonly IRefreshTokenStore _refreshTokenStore;
public AdminAuthService(
IIdentityUserRepository userRepository,
IPasswordHasher<IdentityUser> passwordHasher,
IJwtTokenService jwtTokenService,
IRefreshTokenStore refreshTokenStore)
{
_userRepository = userRepository;
_passwordHasher = passwordHasher;
_jwtTokenService = jwtTokenService;
_refreshTokenStore = refreshTokenStore;
}
/// <summary>
/// 管理后台登录:验证账号密码并生成令牌。
/// </summary>
/// <param name="request">登录请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>令牌响应</returns>
/// <exception cref="BusinessException">账号或密码错误时抛出</exception>
public async Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
{
var user = await _userRepository.FindByAccountAsync(request.Account, cancellationToken)
// 1. 根据账号查找用户
var user = await userRepository.FindByAccountAsync(request.Account, cancellationToken)
?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
// 2. 验证密码(使用 ASP.NET Core Identity 的密码哈希器)
var result = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, request.Password);
if (result == PasswordVerificationResult.Failed)
{
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
}
// 3. 构建用户档案并生成令牌
var profile = BuildProfile(user);
return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
}
/// <summary>
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
/// </summary>
/// <param name="request">刷新令牌请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>新的令牌响应</returns>
/// <exception cref="BusinessException">刷新令牌无效、已过期或用户不存在时抛出</exception>
public async Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default)
{
var descriptor = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
// 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销)
var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked)
{
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
}
var user = await _userRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
// 2. 根据用户 ID 查找用户
var user = await userRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
await _refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
// 3. 撤销旧刷新令牌(防止重复使用)
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
// 4. 生成新的令牌对
var profile = BuildProfile(user);
return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
}
/// <summary>
/// 获取用户档案。
/// </summary>
/// <param name="userId">用户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户档案</returns>
/// <exception cref="BusinessException">用户不存在时抛出</exception>
public async Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
{
var user = await _userRepository.FindByIdAsync(userId, cancellationToken)
var user = await userRepository.FindByIdAsync(userId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
return BuildProfile(user);

View File

@@ -1,7 +1,4 @@
using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
@@ -16,90 +13,117 @@ namespace TakeoutSaaS.Application.Identity.Services;
/// <summary>
/// 小程序认证服务实现。
/// </summary>
public sealed class MiniAuthService : IMiniAuthService
public sealed class MiniAuthService(
IWeChatAuthService weChatAuthService,
IMiniUserRepository miniUserRepository,
IJwtTokenService jwtTokenService,
IRefreshTokenStore refreshTokenStore,
ILoginRateLimiter rateLimiter,
IHttpContextAccessor httpContextAccessor,
ITenantProvider tenantProvider) : IMiniAuthService
{
private readonly IWeChatAuthService _weChatAuthService;
private readonly IMiniUserRepository _miniUserRepository;
private readonly IJwtTokenService _jwtTokenService;
private readonly IRefreshTokenStore _refreshTokenStore;
private readonly ILoginRateLimiter _rateLimiter;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ITenantProvider _tenantProvider;
public MiniAuthService(
IWeChatAuthService weChatAuthService,
IMiniUserRepository miniUserRepository,
IJwtTokenService jwtTokenService,
IRefreshTokenStore refreshTokenStore,
ILoginRateLimiter rateLimiter,
IHttpContextAccessor httpContextAccessor,
ITenantProvider tenantProvider)
{
_weChatAuthService = weChatAuthService;
_miniUserRepository = miniUserRepository;
_jwtTokenService = jwtTokenService;
_refreshTokenStore = refreshTokenStore;
_rateLimiter = rateLimiter;
_httpContextAccessor = httpContextAccessor;
_tenantProvider = tenantProvider;
}
/// <summary>
/// 微信小程序登录:通过微信 code 获取用户信息并生成令牌。
/// </summary>
/// <param name="request">微信登录请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>令牌响应</returns>
/// <exception cref="BusinessException">获取微信用户信息失败、缺少租户标识时抛出</exception>
public async Task<TokenResponse> LoginWithWeChatAsync(WeChatLoginRequest request, CancellationToken cancellationToken = default)
{
// 1. 限流检查(基于 IP 地址)
var throttleKey = BuildThrottleKey();
await _rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken);
await rateLimiter.EnsureAllowedAsync(throttleKey, cancellationToken);
var session = await _weChatAuthService.Code2SessionAsync(request.Code, cancellationToken);
// 2. 通过微信 code 获取 sessionOpenId、UnionId、SessionKey
var session = await weChatAuthService.Code2SessionAsync(request.Code, cancellationToken);
if (string.IsNullOrWhiteSpace(session.OpenId))
{
throw new BusinessException(ErrorCodes.Unauthorized, "获取微信用户信息失败");
}
var tenantId = _tenantProvider.GetCurrentTenantId();
// 3. 获取当前租户 ID多租户支持
var tenantId = tenantProvider.GetCurrentTenantId();
if (tenantId == Guid.Empty)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识");
}
// 4. 获取或创建小程序用户(如果 OpenId 已存在则返回现有用户,否则创建新用户)
var (user, isNew) = await GetOrBindMiniUserAsync(session.OpenId, session.UnionId, request.Nickname, request.Avatar, tenantId, cancellationToken);
await _rateLimiter.ResetAsync(throttleKey, cancellationToken);
// 5. 登录成功后重置限流计数
await rateLimiter.ResetAsync(throttleKey, cancellationToken);
// 6. 构建用户档案并生成令牌
var profile = BuildProfile(user);
return await _jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken);
return await jwtTokenService.CreateTokensAsync(profile, isNew, cancellationToken);
}
/// <summary>
/// 刷新访问令牌:使用刷新令牌获取新的访问令牌和刷新令牌。
/// </summary>
/// <param name="request">刷新令牌请求</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>新的令牌响应</returns>
/// <exception cref="BusinessException">刷新令牌无效、已过期或用户不存在时抛出</exception>
public async Task<TokenResponse> RefreshTokenAsync(RefreshTokenRequest request, CancellationToken cancellationToken = default)
{
var descriptor = await _refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
// 1. 验证刷新令牌(检查是否存在、是否过期、是否已撤销)
var descriptor = await refreshTokenStore.GetAsync(request.RefreshToken, cancellationToken);
if (descriptor == null || descriptor.ExpiresAt <= DateTime.UtcNow || descriptor.Revoked)
{
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
}
var user = await _miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
// 2. 根据用户 ID 查找用户
var user = await miniUserRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
await _refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
// 3. 撤销旧刷新令牌(防止重复使用)
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
// 4. 生成新的令牌对
var profile = BuildProfile(user);
return await _jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
return await jwtTokenService.CreateTokensAsync(profile, false, cancellationToken);
}
/// <summary>
/// 获取用户档案。
/// </summary>
/// <param name="userId">用户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户档案</returns>
/// <exception cref="BusinessException">用户不存在时抛出</exception>
public async Task<CurrentUserProfile> GetProfileAsync(Guid userId, CancellationToken cancellationToken = default)
{
var user = await _miniUserRepository.FindByIdAsync(userId, cancellationToken)
var user = await miniUserRepository.FindByIdAsync(userId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
return BuildProfile(user);
}
/// <summary>
/// 获取或绑定小程序用户:如果 OpenId 已存在则返回现有用户,否则创建新用户。
/// </summary>
/// <param name="openId">微信 OpenId</param>
/// <param name="unionId">微信 UnionId可选</param>
/// <param name="nickname">昵称</param>
/// <param name="avatar">头像地址(可选)</param>
/// <param name="tenantId">租户 ID</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>用户实体和是否为新用户的元组</returns>
private async Task<(MiniUser user, bool isNew)> GetOrBindMiniUserAsync(string openId, string? unionId, string? nickname, string? avatar, Guid tenantId, CancellationToken cancellationToken)
{
var existing = await _miniUserRepository.FindByOpenIdAsync(openId, cancellationToken);
// 检查用户是否已存在
var existing = await miniUserRepository.FindByOpenIdAsync(openId, cancellationToken);
if (existing != null)
{
return (existing, false);
}
var created = await _miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken);
// 创建新用户
var created = await miniUserRepository.CreateOrUpdateAsync(openId, unionId, nickname, avatar, tenantId, cancellationToken);
return (created, true);
}
@@ -118,7 +142,7 @@ public sealed class MiniAuthService : IMiniAuthService
private string BuildThrottleKey()
{
var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback;
var ip = httpContextAccessor.HttpContext?.Connection.RemoteIpAddress ?? IPAddress.Loopback;
return $"mini-login:{ip}";
}
}