主要变更:
新增统一缓存封装:[AppCache.cs (line 12)](/E:/jiaowu/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs:12) 接入 HybridCache 和可选 Redis:[Program.cs (line 153)](/E:/jiaowu/src/Jiaowu.Api/Program.cs:153) 缓存学生激活选项、基础数据、公开课表和课表选项。 个人课表、选课容量、成绩、权限、通知和任务状态保持实时查询。 基础数据、课程、教师、教学任务、作息、考试和课表发布后自动失效相关缓存。 新增 /health/cache,Redis 故障不影响 /health/ready。 Compose 增加 256MB、allkeys-lfu、无持久化的 redis:8.8-alpine 服务;该镜像标签已由 Docker 官方镜像仓库核对。 更新 [.env.example (line 1)](/E:/jiaowu/.env.example:1) 和 [README.md (line 190)](/E:/jiaowu/README.md:190) 部署说明。
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Text.Json.Serialization;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Configuration;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Caching;
|
||||
using Jiaowu.Api.Infrastructure.Exams;
|
||||
using Jiaowu.Api.Infrastructure.Middleware;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
@@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using System.Threading.RateLimiting;
|
||||
@@ -61,6 +63,9 @@ if (seedDemoData && builder.Environment.IsDevelopment())
|
||||
var databaseOptions = builder.Configuration
|
||||
.GetSection(DatabaseOptions.SectionName)
|
||||
.Get<DatabaseOptions>() ?? new DatabaseOptions();
|
||||
var cacheOptions = builder.Configuration
|
||||
.GetSection(AppCacheOptions.SectionName)
|
||||
.Get<AppCacheOptions>() ?? new AppCacheOptions();
|
||||
|
||||
if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase) &&
|
||||
!builder.Environment.IsDevelopment())
|
||||
@@ -82,7 +87,24 @@ if (databaseOptions.CommandTimeoutSeconds is < 5 or > 300)
|
||||
"Database:CommandTimeoutSeconds 必须在 5 到 300 秒之间。");
|
||||
}
|
||||
|
||||
if (cacheOptions.ReferenceExpirationMinutes is < 1 or > 1440 ||
|
||||
cacheOptions.TimetableExpirationMinutes is < 1 or > 1440 ||
|
||||
cacheOptions.ReferenceLocalExpirationSeconds is < 1 or > 3600 ||
|
||||
cacheOptions.TimetableLocalExpirationSeconds is < 1 or > 3600 ||
|
||||
cacheOptions.MaximumPayloadKilobytes is < 64 or > 16384 ||
|
||||
cacheOptions.ReferenceLocalExpirationSeconds >
|
||||
cacheOptions.ReferenceExpirationMinutes * 60 ||
|
||||
cacheOptions.TimetableLocalExpirationSeconds >
|
||||
cacheOptions.TimetableExpirationMinutes * 60 ||
|
||||
string.IsNullOrWhiteSpace(cacheOptions.KeyPrefix) ||
|
||||
cacheOptions.KeyPrefix.Length > 100)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cache 缓存时间或 MaximumPayloadKilobytes 超出允许范围。");
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton(databaseOptions);
|
||||
builder.Services.AddSingleton(cacheOptions);
|
||||
builder.Services.AddDbContextPool<AppDbContext>(options =>
|
||||
{
|
||||
if (databaseOptions.Provider.Equals("SQLite", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -125,6 +147,19 @@ builder.Services.AddDbContextPool<AppDbContext>(options =>
|
||||
});
|
||||
});
|
||||
|
||||
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
|
||||
if (cacheOptions.Enabled && !string.IsNullOrWhiteSpace(redisConnectionString))
|
||||
{
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
options.Configuration = redisConnectionString);
|
||||
}
|
||||
builder.Services.AddHybridCache(options =>
|
||||
{
|
||||
options.MaximumKeyLength = 512;
|
||||
options.MaximumPayloadBytes = cacheOptions.MaximumPayloadKilobytes * 1024;
|
||||
});
|
||||
builder.Services.AddSingleton<IAppCache, HybridAppCache>();
|
||||
|
||||
builder.Services
|
||||
.AddIdentityCore<ApplicationUser>(options =>
|
||||
{
|
||||
@@ -320,6 +355,7 @@ app.MapGet("/health/live", () => Results.Ok(new { Status = "healthy" }))
|
||||
.AllowAnonymous();
|
||||
app.MapGet("/health", CheckDatabaseHealthAsync).AllowAnonymous();
|
||||
app.MapGet("/health/ready", CheckDatabaseHealthAsync).AllowAnonymous();
|
||||
app.MapGet("/health/cache", CheckCacheHealthAsync).AllowAnonymous();
|
||||
app.MapFallback(async context =>
|
||||
{
|
||||
if (context.Request.Path.StartsWithSegments("/api") ||
|
||||
@@ -383,4 +419,27 @@ static async Task<IResult> CheckDatabaseHealthAsync(
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<IResult> CheckCacheHealthAsync(
|
||||
IServiceProvider services,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var distributedCache = services.GetService<IDistributedCache>();
|
||||
if (distributedCache is null)
|
||||
return Results.Ok(new { Status = "disabled", Backend = "memory" });
|
||||
|
||||
try
|
||||
{
|
||||
await distributedCache.GetAsync(
|
||||
"jiaowu:health:probe",
|
||||
cancellationToken);
|
||||
return Results.Ok(new { Status = "healthy", Backend = "redis" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Results.Json(
|
||||
new { Status = "unhealthy", Backend = "redis" },
|
||||
statusCode: StatusCodes.Status503ServiceUnavailable);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class Program;
|
||||
|
||||
Reference in New Issue
Block a user