添加项目文件。

This commit is contained in:
glz
2026-06-01 13:42:40 +08:00
parent 435474c5fe
commit bba985f937
56 changed files with 3758 additions and 0 deletions

View File

@ -0,0 +1,71 @@
using Microsoft.IdentityModel.Tokens;
using QYZH.InteractiveMagazine.Models.Settings;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.Auth;
/// <summary>
/// JWT工具类
/// </summary>
public static class JwtHelper
{
/// <summary>
/// 生成JWT令牌
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="userName">用户名</param>
/// <param name="settings">JWT配置</param>
/// <returns>JWT令牌字符串</returns>
public static string GenerateToken(long userId, string userName, JwtSettings settings)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim(JwtRegisteredClaimNames.Name, userName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim(ClaimTypes.Name, userName)
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(settings.SecretKey!));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: settings.Issuer,
audience: settings.Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(settings.ExpiryMinutes),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>
/// 验证JWT令牌
/// </summary>
/// <param name="token">JWT令牌</param>
/// <param name="settings">JWT配置</param>
/// <returns>ClaimsPrincipal对象</returns>
public static ClaimsPrincipal ValidateToken(string token, JwtSettings settings)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.UTF8.GetBytes(settings.SecretKey!);
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = settings.Issuer,
ValidateAudience = true,
ValidAudience = settings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
return tokenHandler.ValidateToken(token, validationParameters, out _);
}
}

View File

@ -0,0 +1,119 @@
using StackExchange.Redis;
namespace QYZH.InteractiveMagazine.Infrastructure.Cache;
/// <summary>
/// Redis操作封装
/// </summary>
public static class RedisHelper
{
private static string? _keyPrefix;
/// <summary>
/// Redis连接实例
/// </summary>
public static IConnectionMultiplexer? Connection { get; set; }
/// <summary>
/// 设置键前缀
/// </summary>
/// <param name="prefix">前缀字符串</param>
public static void SetKeyPrefix(string prefix)
{
_keyPrefix = prefix;
}
/// <summary>
/// 获取Redis数据库实例
/// </summary>
/// <param name="db">数据库索引</param>
/// <returns>IDatabase实例</returns>
public static IDatabase GetDatabase(int db = -1)
{
if (Connection == null || !Connection.IsConnected)
{
throw new InvalidOperationException("Redis连接未初始化或已断开");
}
return Connection.GetDatabase(db);
}
/// <summary>
/// 设置字符串值
/// </summary>
/// <param name="key">键</param>
/// <param name="value">值</param>
/// <param name="expiry">过期时间</param>
/// <returns>是否成功</returns>
public static async Task<bool> StringSetAsync(string key, string value, TimeSpan? expiry = null)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
if (expiry.HasValue)
{
return await db.StringSetAsync(prefixedKey, value, (Expiration)expiry.Value);
}
return await db.StringSetAsync(prefixedKey, value);
}
/// <summary>
/// 获取字符串值
/// </summary>
/// <param name="key">键</param>
/// <returns>值</returns>
public static async Task<string?> StringGetAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.StringGetAsync(prefixedKey);
}
/// <summary>
/// 删除键
/// </summary>
/// <param name="key">键</param>
/// <returns>是否成功</returns>
public static async Task<bool> KeyDeleteAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyDeleteAsync(prefixedKey);
}
/// <summary>
/// 检查键是否存在
/// </summary>
/// <param name="key">键</param>
/// <returns>是否存在</returns>
public static async Task<bool> KeyExistsAsync(string key)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyExistsAsync(prefixedKey);
}
/// <summary>
/// 设置键的过期时间
/// </summary>
/// <param name="key">键</param>
/// <param name="expiry">过期时间</param>
/// <returns>是否成功</returns>
public static async Task<bool> KeyExpireAsync(string key, TimeSpan expiry)
{
var db = GetDatabase();
var prefixedKey = GetPrefixedKey(key);
return await db.KeyExpireAsync(prefixedKey, expiry);
}
/// <summary>
/// 获取带前缀的键
/// </summary>
/// <param name="key">原始键</param>
/// <returns>带前缀的键</returns>
private static string GetPrefixedKey(string key)
{
return string.IsNullOrEmpty(_keyPrefix) ? key : $"{_keyPrefix}:{key}";
}
}

View File

@ -0,0 +1,110 @@
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;
/// <summary>
/// 统一服务注册扩展
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>
/// 注册基础设施服务
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
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>
/// 配置JWT认证
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="configuration">配置</param>
private static void AddJwtAuthentication(IServiceCollection services, IConfiguration configuration)
{
var jwtSettings = configuration.GetSection("JwtSettings").Get<JwtSettings>()!;
services.AddSingleton(jwtSettings);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtSettings.Issuer,
ValidateAudience = true,
ValidAudience = jwtSettings.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey!)),
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
}
/// <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,72 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
/// <summary>
/// RabbitMQ消息消费者基类
/// </summary>
public abstract class RabbitMQConsumer : IDisposable
{
private readonly IConnection _connection;
private readonly ILogger<RabbitMQConsumer> _logger;
private IChannel? _channel;
private AsyncEventingBasicConsumer? _consumer;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connection">RabbitMQ连接</param>
/// <param name="logger">日志记录器</param>
protected RabbitMQConsumer(IConnection connection, ILogger<RabbitMQConsumer> logger)
{
_connection = connection;
_logger = logger;
}
/// <summary>
/// 启动消费
/// </summary>
/// <param name="queueName">队列名称</param>
/// <param name="handleMessage">消息处理委托</param>
public async Task StartConsume(string queueName, Func<string, Task> handleMessage)
{
_channel = await _connection.CreateChannelAsync();
_consumer = new AsyncEventingBasicConsumer(_channel);
_consumer.ReceivedAsync += async (model, ea) =>
{
try
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
await handleMessage(message);
await _channel.BasicAckAsync(ea.DeliveryTag, false);
_logger.LogInformation("消息消费成功 | 队列: {QueueName} | 消息: {Message}", queueName, message);
}
catch (Exception ex)
{
_logger.LogError(ex, "消息消费失败 | 队列: {QueueName}", queueName);
await _channel.BasicNackAsync(ea.DeliveryTag, false, true);
}
};
await _channel.BasicConsumeAsync(queue: queueName, autoAck: false, consumer: _consumer);
_logger.LogInformation("开始消费消息 | 队列: {QueueName}", queueName);
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_channel?.DisposeAsync().GetAwaiter().GetResult();
GC.SuppressFinalize(this);
}
}

View File

@ -0,0 +1,47 @@
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using System.Text;
namespace QYZH.InteractiveMagazine.Infrastructure.MessageQueue;
/// <summary>
/// RabbitMQ消息发布器
/// </summary>
public class RabbitMQPublisher
{
private readonly IConnection _connection;
private readonly ILogger<RabbitMQPublisher> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="connection">RabbitMQ连接</param>
/// <param name="logger">日志记录器</param>
public RabbitMQPublisher(IConnection connection, ILogger<RabbitMQPublisher> logger)
{
_connection = connection;
_logger = logger;
}
/// <summary>
/// 发布消息
/// </summary>
/// <param name="exchange">交换机名称</param>
/// <param name="routingKey">路由键</param>
/// <param name="message">消息内容</param>
public async Task PublishMessage(string exchange, string routingKey, string message)
{
await using var channel = await _connection.CreateChannelAsync();
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(
exchange: exchange,
routingKey: routingKey,
body: body
);
_logger.LogInformation("消息已发布 | 交换机: {Exchange} | 路由键: {RoutingKey} | 消息: {Message}",
exchange, routingKey, message);
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using QYZH.InteractiveMagazine.Models.Common;
using QYZH.InteractiveMagazine.Models.Dto;
using System.Text.Json;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// 全局异常中间件
/// </summary>
public class GlobalExceptionMiddleware : IMiddleware
{
private readonly ILogger<GlobalExceptionMiddleware> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="logger">日志记录器</param>
public GlobalExceptionMiddleware(ILogger<GlobalExceptionMiddleware> logger)
{
_logger = logger;
}
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="context">HTTP上下文</param>
/// <param name="next">下一个中间件委托</param>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (BusinessException ex)
{
_logger.LogWarning(ex, "业务异常:{Message}", ex.Message);
await HandleBusinessExceptionAsync(context, ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "系统异常:{Message}", ex.Message);
await HandleSystemExceptionAsync(context, ex);
}
}
/// <summary>
/// 处理业务异常
/// </summary>
/// <param name="context">HTTP上下文</param>
/// <param name="ex">业务异常</param>
private static async Task HandleBusinessExceptionAsync(HttpContext context, BusinessException ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status400BadRequest;
var response = BaseResponse<object>.Fail(ex.Message, ex.Code);
var json = JsonSerializer.Serialize(response);
await context.Response.WriteAsync(json);
}
/// <summary>
/// 处理系统异常
/// </summary>
/// <param name="context">HTTP上下文</param>
/// <param name="ex">系统异常</param>
private static async Task HandleSystemExceptionAsync(HttpContext context, Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
var response = BaseResponse<object>.Fail("系统内部错误,请稍后重试");
var json = JsonSerializer.Serialize(response);
await context.Response.WriteAsync(json);
}
}

View File

@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
namespace QYZH.InteractiveMagazine.Infrastructure.Middleware;
/// <summary>
/// 操作日志中间件
/// </summary>
public class OperationLogMiddleware : IMiddleware
{
private readonly ILogger<OperationLogMiddleware> _logger;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="logger">日志记录器</param>
public OperationLogMiddleware(ILogger<OperationLogMiddleware> logger)
{
_logger = logger;
}
/// <summary>
/// 执行中间件
/// </summary>
/// <param name="context">HTTP上下文</param>
/// <param name="next">下一个中间件委托</param>
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var stopwatch = Stopwatch.StartNew();
var requestMethod = context.Request.Method;
var requestUrl = context.Request.Path.ToString();
try
{
await next(context);
}
finally
{
stopwatch.Stop();
var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
_logger.LogInformation(
"请求完成 | {Method} {Url} | 状态码: {StatusCode} | 耗时: {ElapsedMilliseconds}ms",
requestMethod,
requestUrl,
context.Response.StatusCode,
elapsedMilliseconds
);
}
}
}

View File

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\QYZH.InteractiveMagazine.Common\QYZH.InteractiveMagazine.Common.csproj" />
<ProjectReference Include="..\QYZH.InteractiveMagazine.Models\QYZH.InteractiveMagazine.Models.csproj" />
</ItemGroup>
<ItemGroup>
<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" />
<PackageReference Include="SqlSugar" Version="5.1.4.207" />
<PackageReference Include="StackExchange.Redis" Version="2.13.17" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.18.0" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>