仪表盘和 6 类统计摘要接入 HybridCache(内存 + Redis)。

默认 Redis 缓存 3 分钟、本地缓存 30 秒,可通过环境变量调整。
缓存键包含数据权限范围、有效学院和全部筛选条件,避免跨学院串数据。
Excel 导出保持实时查询,不使用摘要缓存。
无需数据库迁移。
This commit is contained in:
biss committed 2026-07-26 15:13:22 +08:00
1 parent 77dfa8145c
commit 631c81821d
10 files changed
+597 -26

No files matched your search

+2
View File
@@ -15,6 +15,8 @@ Cache__ReferenceExpirationMinutes=30
Cache__ReferenceLocalExpirationSeconds=120 Cache__ReferenceLocalExpirationSeconds=120
Cache__TimetableExpirationMinutes=10 Cache__TimetableExpirationMinutes=10
Cache__TimetableLocalExpirationSeconds=30 Cache__TimetableLocalExpirationSeconds=30
Cache__AnalyticsExpirationMinutes=3
Cache__AnalyticsLocalExpirationSeconds=30
Cache__MaximumPayloadKilobytes=2048 Cache__MaximumPayloadKilobytes=2048
Jwt__Issuer=Jiaowu.Api Jwt__Issuer=Jiaowu.Api
+7 -2
View File
@@ -225,8 +225,9 @@ SQLite 只用于本地开发:新库通过 `EnsureCreated` 建立,已有开
### 查询缓存与 Redis ### 查询缓存与 Redis
应用使用 HybridCache 统一管理进程内一级缓存和可选 Redis 二级缓存。目前缓存范围为 应用使用 HybridCache 统一管理进程内一级缓存和可选 Redis 二级缓存。目前缓存范围为
学生激活/基础数据选项以及匿名可访问的已发布课表;选课容量、成绩、考勤、审批、通知 学生激活/基础数据选项、匿名可访问的已发布课表、仪表盘以及统计分析摘要。统计缓存键
未读数、权限和后台任务状态仍直接以 MySQL 为准。 包含有效数据范围、学院和规范化筛选条件,避免跨学院复用;统计 Excel 导出仍实时查询。
选课容量、成绩写入、考勤、审批、通知未读数、权限和后台任务状态仍直接以 MySQL 为准。
不配置 `ConnectionStrings__Redis` 时,开发和单机部署仍使用进程内缓存,不要求安装 不配置 `ConnectionStrings__Redis` 时,开发和单机部署仍使用进程内缓存,不要求安装
Redis。生产环境使用 Redis 时,通过环境变量配置连接串,例如: Redis。生产环境使用 Redis 时,通过环境变量配置连接串,例如:
@@ -235,6 +236,10 @@ Redis。生产环境使用 Redis 时,通过环境变量配置连接串,例
ConnectionStrings__Redis=redis.internal:6380,user=jiaowu,password=REPLACE_ME,ssl=true,abortConnect=false ConnectionStrings__Redis=redis.internal:6380,user=jiaowu,password=REPLACE_ME,ssl=true,abortConnect=false
``` ```
仪表盘和统计摘要默认在 Redis 中缓存 3 分钟、进程内缓存 30 秒,可分别通过
`Cache__AnalyticsExpirationMinutes` 和 `Cache__AnalyticsLocalExpirationSeconds`
调整。该类汇总采用短 TTL 控制数据新鲜度,不要求每个业务写入点同步清理缓存。
Redis 只作为可丢弃的查询缓存。连接失败时应用回源数据库,普通启动和 Redis 只作为可丢弃的查询缓存。连接失败时应用回源数据库,普通启动和
`/health/ready` 不依赖 Redis;可以单独检查 `/health/cache`。缓存键自动包含运行环境, `/health/ready` 不依赖 Redis;可以单独检查 `/health/cache`。缓存键自动包含运行环境,
同一 Redis 可以安全承载 Development、Staging 和 Production,但生产环境仍建议使用 同一 Redis 可以安全承载 Development、Staging 和 Production,但生产环境仍建议使用
@@ -1,18 +1,35 @@
using System.Text.Json;
using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence; using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
namespace Jiaowu.Api.Controllers; namespace Jiaowu.Api.Controllers;
[ApiController] [ApiController]
[Authorize] [Authorize]
[Route("api/dashboard")] [Route("api/dashboard")]
public sealed class DashboardController(AppDbContext db) : ControllerBase public sealed class DashboardController(
AppDbContext db,
IAppCache appCache,
IOptions<JsonOptions> jsonOptions) : ControllerBase
{ {
[HttpGet] [HttpGet]
public async Task<ActionResult<object>> Get(CancellationToken cancellationToken) public async Task<ActionResult<object>> Get(CancellationToken cancellationToken)
{
var response = await appCache.GetOrCreateAsync(
AppCacheKeys.Dashboard,
LoadAsync,
AppCacheProfile.Analytics,
[AppCacheTags.Analytics],
cancellationToken);
return response;
}
private async Task<JsonElement> LoadAsync(CancellationToken cancellationToken)
{ {
var currentTerm = await db.AcademicTerms var currentTerm = await db.AcademicTerms
.AsNoTracking() .AsNoTracking()
@@ -20,7 +37,8 @@ public sealed class DashboardController(AppDbContext db) : ControllerBase
.Select(x => new { x.Id, x.Name, x.StartDate, x.EndDate }) .Select(x => new { x.Id, x.Name, x.StartDate, x.EndDate })
.FirstOrDefaultAsync(cancellationToken); .FirstOrDefaultAsync(cancellationToken);
return new return JsonSerializer.SerializeToElement(
new
{ {
CurrentTerm = currentTerm, CurrentTerm = currentTerm,
Counts = new Counts = new
@@ -78,6 +96,7 @@ public sealed class DashboardController(AppDbContext db) : ControllerBase
cancellationToken), cancellationToken),
Users = await db.Users.CountAsync(cancellationToken) Users = await db.Users.CountAsync(cancellationToken)
} }
}; },
jsonOptions.Value.JsonSerializerOptions);
} }
} }
Loaded 3 of 10 files, more files were not shown because too many files have changed in this diff. Show more