仪表盘和 6 类统计摘要接入 HybridCache(内存 + Redis)。
默认 Redis 缓存 3 分钟、本地缓存 30 秒,可通过环境变量调整。 缓存键包含数据权限范围、有效学院和全部筛选条件,避免跨学院串数据。 Excel 导出保持实时查询,不使用摘要缓存。 无需数据库迁移。
This commit is contained in:
1 parent
77dfa8145c
commit
631c81821d
10 files changed
+651
-80
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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,64 +37,66 @@ 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,
|
|
||||||
Counts = new
|
|
||||||
{
|
{
|
||||||
Campuses = await db.Campuses.CountAsync(cancellationToken),
|
CurrentTerm = currentTerm,
|
||||||
Colleges = await db.Colleges.CountAsync(cancellationToken),
|
Counts = new
|
||||||
Majors = await db.Majors.CountAsync(cancellationToken),
|
{
|
||||||
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
Campuses = await db.Campuses.CountAsync(cancellationToken),
|
||||||
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
Colleges = await db.Colleges.CountAsync(cancellationToken),
|
||||||
Teachers = await db.Teachers.CountAsync(cancellationToken),
|
Majors = await db.Majors.CountAsync(cancellationToken),
|
||||||
Students = await db.Students.CountAsync(cancellationToken),
|
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
||||||
Courses = await db.Courses.CountAsync(cancellationToken),
|
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
||||||
CurriculumPlans = await db.CurriculumPlans.CountAsync(cancellationToken),
|
Teachers = await db.Teachers.CountAsync(cancellationToken),
|
||||||
TeachingTasks = await db.TeachingTasks.CountAsync(cancellationToken),
|
Students = await db.Students.CountAsync(cancellationToken),
|
||||||
SchedulePlans = await db.SchedulePlans.CountAsync(cancellationToken),
|
Courses = await db.Courses.CountAsync(cancellationToken),
|
||||||
CourseSelectionRounds = await db.CourseSelectionRounds
|
CurriculumPlans = await db.CurriculumPlans.CountAsync(cancellationToken),
|
||||||
.CountAsync(cancellationToken),
|
TeachingTasks = await db.TeachingTasks.CountAsync(cancellationToken),
|
||||||
CourseSelectionOfferings = await db.CourseSelectionOfferings
|
SchedulePlans = await db.SchedulePlans.CountAsync(cancellationToken),
|
||||||
.CountAsync(cancellationToken),
|
CourseSelectionRounds = await db.CourseSelectionRounds
|
||||||
CourseEnrollments = await db.CourseEnrollments
|
.CountAsync(cancellationToken),
|
||||||
.CountAsync(
|
CourseSelectionOfferings = await db.CourseSelectionOfferings
|
||||||
x => x.Status == CourseEnrollmentStatus.Enrolled,
|
.CountAsync(cancellationToken),
|
||||||
|
CourseEnrollments = await db.CourseEnrollments
|
||||||
|
.CountAsync(
|
||||||
|
x => x.Status == CourseEnrollmentStatus.Enrolled,
|
||||||
|
cancellationToken),
|
||||||
|
GradeSheets = await db.GradeSheets.CountAsync(cancellationToken),
|
||||||
|
PublishedGradeSheets = await db.GradeSheets.CountAsync(
|
||||||
|
x => x.Status == GradeSheetStatus.Published,
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
GradeSheets = await db.GradeSheets.CountAsync(cancellationToken),
|
GradeRecords = await db.GradeRecords.CountAsync(cancellationToken),
|
||||||
PublishedGradeSheets = await db.GradeSheets.CountAsync(
|
ExamPlans = await db.ExamPlans.CountAsync(cancellationToken),
|
||||||
x => x.Status == GradeSheetStatus.Published,
|
ExamSessions = await db.ExamSessions.CountAsync(cancellationToken),
|
||||||
cancellationToken),
|
StudentStatusChanges = await db.StudentStatusChanges
|
||||||
GradeRecords = await db.GradeRecords.CountAsync(cancellationToken),
|
.CountAsync(cancellationToken),
|
||||||
ExamPlans = await db.ExamPlans.CountAsync(cancellationToken),
|
PendingStudentStatusChanges = await db.StudentStatusChanges.CountAsync(
|
||||||
ExamSessions = await db.ExamSessions.CountAsync(cancellationToken),
|
x => x.State == StudentStatusChangeState.Submitted ||
|
||||||
StudentStatusChanges = await db.StudentStatusChanges
|
x.State == StudentStatusChangeState.CounselorApproved ||
|
||||||
.CountAsync(cancellationToken),
|
x.State == StudentStatusChangeState.CollegeApproved,
|
||||||
PendingStudentStatusChanges = await db.StudentStatusChanges.CountAsync(
|
|
||||||
x => x.State == StudentStatusChangeState.Submitted ||
|
|
||||||
x.State == StudentStatusChangeState.CounselorApproved ||
|
|
||||||
x.State == StudentStatusChangeState.CollegeApproved,
|
|
||||||
cancellationToken),
|
|
||||||
GraduationAuditBatches = await db.GraduationAuditBatches
|
|
||||||
.CountAsync(cancellationToken),
|
|
||||||
PublishedGraduationAuditBatches = await db.GraduationAuditBatches
|
|
||||||
.CountAsync(
|
|
||||||
x => x.Status == GraduationAuditBatchStatus.Published,
|
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
DegreeAwardBatches = await db.DegreeAwardBatches
|
GraduationAuditBatches = await db.GraduationAuditBatches
|
||||||
.CountAsync(cancellationToken),
|
.CountAsync(cancellationToken),
|
||||||
PublishedDegreeAwardBatches = await db.DegreeAwardBatches
|
PublishedGraduationAuditBatches = await db.GraduationAuditBatches
|
||||||
.CountAsync(
|
.CountAsync(
|
||||||
x => x.Status == DegreeAwardBatchStatus.Published,
|
x => x.Status == GraduationAuditBatchStatus.Published,
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
GraduationClearanceBatches = await db.GraduationClearanceBatches
|
DegreeAwardBatches = await db.DegreeAwardBatches
|
||||||
.CountAsync(cancellationToken),
|
.CountAsync(cancellationToken),
|
||||||
OpenGraduationClearanceBatches = await db.GraduationClearanceBatches
|
PublishedDegreeAwardBatches = await db.DegreeAwardBatches
|
||||||
.CountAsync(
|
.CountAsync(
|
||||||
x => x.Status == GraduationClearanceBatchStatus.Open,
|
x => x.Status == DegreeAwardBatchStatus.Published,
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
Users = await db.Users.CountAsync(cancellationToken)
|
GraduationClearanceBatches = await db.GraduationClearanceBatches
|
||||||
}
|
.CountAsync(cancellationToken),
|
||||||
};
|
OpenGraduationClearanceBatches = await db.GraduationClearanceBatches
|
||||||
|
.CountAsync(
|
||||||
|
x => x.Status == GraduationClearanceBatchStatus.Open,
|
||||||
|
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
Reference in new issue
Block a user