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;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace TakeoutSaaS.Infrastructure.Identity.Options;
@@ -9,22 +7,52 @@ namespace TakeoutSaaS.Infrastructure.Identity.Options;
/// </summary>
public sealed class AdminSeedOptions
{
/// <summary>
/// 初始用户列表。
/// </summary>
public List<SeedUserOptions> Users { get; set; } = new();
}
/// <summary>
/// 种子用户配置:用于初始化管理后台账号。
/// </summary>
public sealed class SeedUserOptions
{
/// <summary>
/// 登录账号。
/// </summary>
[Required]
public string Account { get; set; } = string.Empty;
/// <summary>
/// 登录密码(明文,将在初始化时进行哈希处理)。
/// </summary>
[Required]
public string Password { get; set; } = string.Empty;
/// <summary>
/// 展示名称。
/// </summary>
[Required]
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// 所属租户 ID。
/// </summary>
public Guid TenantId { get; set; }
/// <summary>
/// 所属商户 ID平台管理员为空
/// </summary>
public Guid? MerchantId { get; set; }
/// <summary>
/// 角色集合。
/// </summary>
public string[] Roles { get; set; } = Array.Empty<string>();
/// <summary>
/// 权限集合。
/// </summary>
public string[] Permissions { get; set; } = Array.Empty<string>();
}

View File

@@ -3,23 +3,38 @@ using System.ComponentModel.DataAnnotations;
namespace TakeoutSaaS.Infrastructure.Identity.Options;
/// <summary>
/// JWT 配置。
/// JWT 配置选项
/// </summary>
public sealed class JwtOptions
{
/// <summary>
/// 令牌颁发者Issuer
/// </summary>
[Required]
public string Issuer { get; set; } = string.Empty;
/// <summary>
/// 令牌受众Audience
/// </summary>
[Required]
public string Audience { get; set; } = string.Empty;
/// <summary>
/// JWT 签名密钥(至少 32 个字符)。
/// </summary>
[Required]
[MinLength(32)]
public string Secret { get; set; } = string.Empty;
/// <summary>
/// 访问令牌过期时间分钟范围5-1440。
/// </summary>
[Range(5, 1440)]
public int AccessTokenExpirationMinutes { get; set; } = 60;
/// <summary>
/// 刷新令牌过期时间分钟范围60-2016014天
/// </summary>
[Range(60, 1440 * 14)]
public int RefreshTokenExpirationMinutes { get; set; } = 60 * 24 * 7;
}

View File

@@ -3,13 +3,19 @@ using System.ComponentModel.DataAnnotations;
namespace TakeoutSaaS.Infrastructure.Identity.Options;
/// <summary>
/// 登录限流配置。
/// 登录限流配置选项
/// </summary>
public sealed class LoginRateLimitOptions
{
/// <summary>
/// 时间窗口范围1-3600。
/// </summary>
[Range(1, 3600)]
public int WindowSeconds { get; set; } = 60;
/// <summary>
/// 时间窗口内允许的最大尝试次数范围1-100。
/// </summary>
[Range(1, 100)]
public int MaxAttempts { get; set; } = 5;
}

View File

@@ -1,9 +1,12 @@
namespace TakeoutSaaS.Infrastructure.Identity.Options;
/// <summary>
/// 刷新令牌存储配置。
/// 刷新令牌存储配置选项
/// </summary>
public sealed class RefreshTokenStoreOptions
{
/// <summary>
/// Redis 键前缀,用于存储刷新令牌。
/// </summary>
public string Prefix { get; set; } = "identity:refresh:";
}

View File

@@ -3,13 +3,19 @@ using System.ComponentModel.DataAnnotations;
namespace TakeoutSaaS.Infrastructure.Identity.Options;
/// <summary>
/// 微信小程序配置。
/// 微信小程序配置选项
/// </summary>
public sealed class WeChatMiniOptions
{
/// <summary>
/// 微信小程序 AppId。
/// </summary>
[Required]
public string AppId { get; set; } = string.Empty;
/// <summary>
/// 微信小程序 AppSecret。
/// </summary>
[Required]
public string Secret { get; set; } = string.Empty;
}

View File

@@ -1,14 +1,9 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Infrastructure.Identity.Options;
using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser;
@@ -17,29 +12,20 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// <summary>
/// 后台账号初始化种子任务
/// </summary>
public sealed class IdentityDataSeeder : IHostedService
public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger) : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<IdentityDataSeeder> _logger;
public IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<AdminSeedOptions>>().Value;
var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher<DomainIdentityUser>>();
await context.Database.MigrateAsync(cancellationToken);
if (options.Users == null || options.Users.Count == 0)
if (options.Users is null or { Count: 0 })
{
_logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化");
logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化");
return;
}
@@ -64,7 +50,7 @@ public sealed class IdentityDataSeeder : IHostedService
};
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
context.IdentityUsers.Add(user);
_logger.LogInformation("已创建后台账号 {Account}", user.Account);
logger.LogInformation("已创建后台账号 {Account}", user.Account);
}
else
{
@@ -74,7 +60,7 @@ public sealed class IdentityDataSeeder : IHostedService
user.Roles = roles;
user.Permissions = permissions;
user.PasswordHash = passwordHasher.HashPassword(user, userOptions.Password);
_logger.LogInformation("已更新后台账号 {Account}", user.Account);
logger.LogInformation("已更新后台账号 {Account}", user.Account);
}
}
@@ -85,10 +71,9 @@ public sealed class IdentityDataSeeder : IHostedService
private static string[] NormalizeValues(string[]? values)
=> values == null
? Array.Empty<string>()
: values
? []
: [.. values
.Where(v => !string.IsNullOrWhiteSpace(v))
.Select(v => v.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
.Distinct(StringComparer.OrdinalIgnoreCase)];
}

View File

@@ -1,4 +1,3 @@
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
@@ -11,12 +10,8 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// <summary>
/// 身份认证 DbContext。
/// </summary>
public sealed class IdentityDbContext : DbContext
public sealed class IdentityDbContext(DbContextOptions<IdentityDbContext> options) : DbContext(options)
{
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
: base(options)
{
}
public DbSet<IdentityUser> IdentityUsers => Set<IdentityUser>();
public DbSet<MiniUser> MiniUsers => Set<MiniUser>();
@@ -37,11 +32,11 @@ public sealed class IdentityDbContext : DbContext
builder.Property(x => x.Avatar).HasMaxLength(256);
var converter = new ValueConverter<string[], string>(
v => string.Join(',', v ?? Array.Empty<string>()),
v => string.Join(',', v),
v => string.IsNullOrWhiteSpace(v) ? Array.Empty<string>() : v.Split(',', StringSplitOptions.RemoveEmptyEntries));
var comparer = new ValueComparer<string[]>(
(l, r) => l!.SequenceEqual(r!),
(l, r) => (l == null && r == null) || (l != null && r != null && Enumerable.SequenceEqual(l, r)),
v => v.Aggregate(0, (current, item) => HashCode.Combine(current, item.GetHashCode())),
v => v.ToArray());

View File

@@ -1,10 +1,6 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using TakeoutSaaS.Application.Identity.Abstractions;
@@ -16,29 +12,33 @@ namespace TakeoutSaaS.Infrastructure.Identity.Services;
/// <summary>
/// JWT 令牌生成器。
/// </summary>
public sealed class JwtTokenService : IJwtTokenService
public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions<JwtOptions> options) : IJwtTokenService
{
private readonly JwtSecurityTokenHandler _tokenHandler = new();
private readonly IRefreshTokenStore _refreshTokenStore;
private readonly JwtOptions _options;
public JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptions<JwtOptions> options)
{
_refreshTokenStore = refreshTokenStore;
_options = options.Value;
}
private readonly JwtOptions _options = options.Value;
/// <summary>
/// 创建访问令牌和刷新令牌对。
/// </summary>
/// <param name="profile">用户档案</param>
/// <param name="isNewUser">是否为新用户(首次登录)</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>令牌响应</returns>
public async Task<TokenResponse> CreateTokensAsync(CurrentUserProfile profile, bool isNewUser = false, CancellationToken cancellationToken = default)
{
var now = DateTime.UtcNow;
var accessExpires = now.AddMinutes(_options.AccessTokenExpirationMinutes);
var refreshExpires = now.AddMinutes(_options.RefreshTokenExpirationMinutes);
// 1. 构建 JWT Claims包含用户 ID、账号、租户 ID、商户 ID、角色、权限等
var claims = BuildClaims(profile);
// 2. 创建签名凭据(使用 HMAC SHA256 算法)
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Secret)),
SecurityAlgorithms.HmacSha256);
// 3. 创建 JWT 安全令牌
var jwt = new JwtSecurityToken(
issuer: _options.Issuer,
audience: _options.Audience,
@@ -47,8 +47,11 @@ public sealed class JwtTokenService : IJwtTokenService
expires: accessExpires,
signingCredentials: signingCredentials);
// 4. 序列化 JWT 为字符串
var accessToken = _tokenHandler.WriteToken(jwt);
var refreshDescriptor = await _refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken);
// 5. 生成刷新令牌并存储到 Redis
var refreshDescriptor = await refreshTokenStore.IssueAsync(profile.UserId, refreshExpires, cancellationToken);
return new TokenResponse
{
@@ -61,6 +64,11 @@ public sealed class JwtTokenService : IJwtTokenService
};
}
/// <summary>
/// 构建 JWT Claims将用户档案转换为 Claims 集合。
/// </summary>
/// <param name="profile">用户档案</param>
/// <returns>Claims 集合</returns>
private static IEnumerable<Claim> BuildClaims(CurrentUserProfile profile)
{
var userId = profile.UserId.ToString();