feat: 初始化后台管理模块与基础业务框架

1.  新增依赖注入生命周期标记接口、基础仓储与管理员仓储实现
2.  新增管理员认证与用户服务接口,补充认证相关DTO
3.  重构实体审计字段命名,统一Created/UpdatedAt规范
4.  新增大量业务实体类与API版本枚举配置
5.  集成Autofac依赖注入、JWT自动刷新与跨域配置
6.  替换原有微信小程序与旧认证服务为后台管理系统架构
7.  完善Swagger文档配置与项目基础部署配置
This commit is contained in:
glz
2026-06-01 17:59:23 +08:00
parent ab53492cc4
commit 831f8ba7f5
51 changed files with 2417 additions and 596 deletions

View File

@ -44,28 +44,51 @@ public static class JwtHelper
}
/// <summary>
/// 验证JWT令牌
/// 获取Token过期时间
/// </summary>
/// <param name="token">JWT令牌</param>
/// <param name="settings">JWT配置</param>
/// <returns>ClaimsPrincipal对象</returns>
public static ClaimsPrincipal ValidateToken(string token, JwtSettings settings)
/// <returns>过期时间</returns>
public static DateTime? GetTokenExpiry(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(settings.SecretKey!);
var validationParameters = new TokenValidationParameters
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
ValidateIssuer = true,
ValidIssuer = settings.Issuer,
ValidateAudience = true,
ValidAudience = settings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
return jwtToken.ValidTo;
}
return null;
}
return tokenHandler.ValidateToken(token, validationParameters, out _);
/// <summary>
/// 从Token中获取用户ID
/// </summary>
/// <param name="token">JWT令牌</param>
/// <returns>用户ID</returns>
public static long? GetUserIdFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
var userIdClaim = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier);
if (long.TryParse(userIdClaim?.Value, out long userId))
{
return userId;
}
}
return null;
}
/// <summary>
/// 从Token中获取用户名
/// </summary>
/// <param name="token">JWT令牌</param>
/// <returns>用户名</returns>
public static string GetUserNameFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is JwtSecurityToken jwtToken)
{
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value ?? string.Empty;
}
return string.Empty;
}
}

View File

@ -0,0 +1,67 @@
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
using QYZH.InteractiveMagazine.Models.Settings;
using RabbitMQ.Client;
using StackExchange.Redis;
namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
{
public static class AutofacExtension
{
/// <summary>
/// 根据程序集名称获取程序集
/// </summary>
/// <param name="builder">Web应用程序构建器</param>
public static void UseAutofac(this WebApplicationBuilder builder)
{
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>((c, containerBuilder) =>
{
var friendlyName = AppDomain.CurrentDomain.FriendlyName;
var source = friendlyName.Split('.');
var assemblyNames = string.Join(".", source.Take(source.Length - 1));
containerBuilder.RegisterModule(new AutofacModuleRegister(assemblyNames));
InitializeRedis(c.Configuration);
InitializeRabbitMQ(c.Configuration, containerBuilder);
});
}
private static void InitializeRedis(IConfiguration configuration)
{
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>();
if (redisSettings != null && !string.IsNullOrWhiteSpace(redisSettings.ConnectionString))
{
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString);
RedisHelper.Connection = multiplexer;
RedisHelper.SetKeyPrefix(redisSettings.InstanceName ?? string.Empty);
}
}
private static void InitializeRabbitMQ(IConfiguration configuration, ContainerBuilder containerBuilder)
{
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>();
if (rabbitMQSettings != null && !string.IsNullOrWhiteSpace(rabbitMQSettings.HostName))
{
var factory = new ConnectionFactory
{
HostName = rabbitMQSettings.HostName,
Port = rabbitMQSettings.Port,
UserName = rabbitMQSettings.UserName ?? string.Empty,
Password = rabbitMQSettings.Password ?? string.Empty,
VirtualHost = rabbitMQSettings.VirtualHost ?? string.Empty
};
var connection = factory.CreateConnectionAsync().GetAwaiter().GetResult();
containerBuilder.RegisterInstance(connection).As<IConnection>().SingleInstance();
containerBuilder.RegisterType<RabbitMQPublisher>().InstancePerDependency();
}
}
}
}

View File

@ -0,0 +1,42 @@
using Autofac;
using System.Reflection;
namespace QYZH.InteractiveMagazine.Infrastructure.Autofacs
{
public class AutofacModuleRegister : Autofac.Module
{
private readonly string _assemblyName;
public AutofacModuleRegister(string assemblyNames)
{
_assemblyName = assemblyNames;
}
/// <summary>
/// 加在程序集
/// </summary>
/// <param name="builder"></param>
protected override void Load(ContainerBuilder builder)
{
//注册Repository(只注册接口,遵循依赖倒置原则)
builder.RegisterAssemblyTypes(GetAssemblyByName($"{_assemblyName}.Repository"))
.Where(t => t.Name.EndsWith("Repository") && !t.IsAbstract)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
//注册Service
builder.RegisterAssemblyTypes(GetAssemblyByName($"{_assemblyName}.Service"))
.Where(t => t.Name.EndsWith("Service") && !t.IsAbstract)
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
}
/// <summary>
/// 根据程序集名称获取程序集
/// </summary>
/// <param name="AssemblyName">程序集名称</param>
public static Assembly GetAssemblyByName(string AssemblyName)
{
return Assembly.Load(AssemblyName);
}
}
}

View File

@ -2,12 +2,8 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
using QYZH.InteractiveMagazine.Infrastructure.Middleware;
using QYZH.InteractiveMagazine.Models.Settings;
using RabbitMQ.Client;
using StackExchange.Redis;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
@ -25,11 +21,10 @@ public static class DependencyInjectionExtensions
public static void AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
AddJwtAuthentication(services, configuration);
AddRedisCache(services, configuration);
AddRabbitMQ(services, configuration);
services.AddTransient<GlobalExceptionMiddleware>();
services.AddTransient<OperationLogMiddleware>();
}
/// <summary>
@ -59,52 +54,4 @@ public static class DependencyInjectionExtensions
};
});
}
/// <summary>
/// 注册Redis缓存
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
private static void AddRedisCache(IServiceCollection services, IConfiguration configuration)
{
var redisSettings = configuration.GetSection("RedisSettings").Get<RedisSettings>()!;
services.AddSingleton(redisSettings);
services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var multiplexer = ConnectionMultiplexer.Connect(redisSettings.ConnectionString!);
RedisHelper.Connection = multiplexer;
RedisHelper.SetKeyPrefix(redisSettings.InstanceName!);
return multiplexer;
});
}
/// <summary>
/// 注册RabbitMQ
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
private static void AddRabbitMQ(IServiceCollection services, IConfiguration configuration)
{
var rabbitMQSettings = configuration.GetSection("RabbitMQSettings").Get<RabbitMQSettings>()!;
services.AddSingleton(rabbitMQSettings);
services.AddSingleton<IConnection>(sp =>
{
var factory = new ConnectionFactory
{
HostName = rabbitMQSettings.HostName!,
Port = rabbitMQSettings.Port,
UserName = rabbitMQSettings.UserName!,
Password = rabbitMQSettings.Password!,
VirtualHost = rabbitMQSettings.VirtualHost!
};
return factory.CreateConnectionAsync().GetAwaiter().GetResult();
});
services.AddTransient<RabbitMQPublisher>();
}
}

View File

@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware
{
/// <summary>
/// 跨域扩展
/// </summary>
public static class CorsExtension
{
/// <summary>
/// 跨域配置
/// </summary>
/// <param name="services"></param>
/// <param name="configuration"></param>
public static void AddCorsRegister(this WebApplicationBuilder builder)
{
bool isCorsAll = builder.Configuration["corsUrls"] == "*";
builder.Services.AddCors(options =>
{
options.AddPolicy("defaultCors", policy =>
{
if (isCorsAll)
{
policy.SetIsOriginAllowed(origin => true);
}
else
{
var corsUrls = builder.Configuration.GetSection("corsUrls").Get<string[]>();
policy.WithOrigins(corsUrls ?? Array.Empty<string>());
}
policy.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials()
.WithExposedHeaders("X-New-Token");
});
});
}
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using QYZH.InteractiveMagazine.Infrastructure.Cache;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
public class JwtAutoRefreshMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
private const string TokenKeyPrefix = "InteractiveMagazine:AdminAuth:Token";
private const int RefreshThresholdMinutes = 10;
public JwtAutoRefreshMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer "))
{
var token = authHeader.Substring("Bearer ".Length).Trim();
await TryAutoRefreshTokenAsync(context, token);
}
await _next(context);
}
private async Task TryAutoRefreshTokenAsync(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
if (tokenHandler.ReadToken(token) is not JwtSecurityToken jwtToken)
{
return;
}
var expiryTime = jwtToken.ValidTo;
var remainingTime = expiryTime - DateTime.Now;
if (remainingTime <= TimeSpan.FromMinutes(RefreshThresholdMinutes) && remainingTime > TimeSpan.Zero)
{
var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
var userName = jwtToken.Claims.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Name)?.Value;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(userName))
{
return;
}
var jwtSettings = _configuration.GetSection("JwtSettings").Get<JwtSettings>();
if (jwtSettings == null)
{
return;
}
var newToken = QYZH.InteractiveMagazine.Infrastructure.Auth.JwtHelper.GenerateToken(
long.Parse(userId), userName, jwtSettings);
await RedisHelper.StringSetAsync(
$"{TokenKeyPrefix}:{userId}",
newToken,
TimeSpan.FromMinutes(jwtSettings.ExpiryMinutes));
context.Response.Headers["X-New-Token"] = newToken;
}
}
catch
{
// 忽略自动刷新异常,由后续认证中间件处理
}
}
}

View File

@ -1,11 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.IService\QYZH.InteractiveMagazine.IService.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="9.1.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="11.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />