“教务总览”改成可实际工作的分级工作台:
校级管理员查看全校数据,学院管理员仅查看本学院数据。 待办按当前审批阶段统计,涵盖授课资格、成绩、调停课、学籍异动、教室借用等。 增加学期进度、教学任务发布、课表覆盖、成绩发布状态。 快捷入口根据管理员角色自动调整。 完善未配置学期、无待办、加载失败等状态。 适配桌面和 390px 移动端,无横向溢出。
This commit is contained in:
@@ -1,11 +1,10 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Jiaowu.Api.Domain.Academic;
|
using Jiaowu.Api.Domain.Academic;
|
||||||
using Jiaowu.Api.Infrastructure.Caching;
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
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;
|
||||||
|
|
||||||
@@ -14,89 +13,301 @@ namespace Jiaowu.Api.Controllers;
|
|||||||
[Route("api/dashboard")]
|
[Route("api/dashboard")]
|
||||||
public sealed class DashboardController(
|
public sealed class DashboardController(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
IAppCache appCache,
|
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||||
IOptions<JsonOptions> jsonOptions) : ControllerBase
|
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult<object>> Get(CancellationToken cancellationToken)
|
public async Task<ActionResult<DashboardResponse>> Get(
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var response = await appCache.GetOrCreateAsync(
|
var scope = currentUserDataScope.Current;
|
||||||
AppCacheKeys.Dashboard,
|
Guid? restrictedCollegeId = scope.Scope == DataScope.All
|
||||||
LoadAsync,
|
? null
|
||||||
AppCacheProfile.Analytics,
|
: scope.CollegeId ?? Guid.Empty;
|
||||||
[AppCacheTags.Analytics],
|
var collegeName = restrictedCollegeId.HasValue &&
|
||||||
cancellationToken);
|
restrictedCollegeId.Value != Guid.Empty
|
||||||
return response;
|
? await db.Colleges.AsNoTracking()
|
||||||
}
|
.Where(x => x.Id == restrictedCollegeId.Value)
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken)
|
||||||
|
: null;
|
||||||
|
|
||||||
private async Task<JsonElement> LoadAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var currentTerm = await db.AcademicTerms
|
var currentTerm = await db.AcademicTerms
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.IsCurrent)
|
.Where(x => x.IsCurrent)
|
||||||
.Select(x => new { x.Id, x.Name, x.StartDate, x.EndDate })
|
.Select(x => new DashboardTerm(
|
||||||
|
x.Id,
|
||||||
|
x.Name,
|
||||||
|
x.StartDate,
|
||||||
|
x.EndDate))
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
var currentTermId = currentTerm?.Id;
|
||||||
|
|
||||||
return JsonSerializer.SerializeToElement(
|
var students = db.Students.AsNoTracking()
|
||||||
new
|
.Where(x =>
|
||||||
{
|
!restrictedCollegeId.HasValue ||
|
||||||
CurrentTerm = currentTerm,
|
x.AdministrativeClass!.Major!.CollegeId ==
|
||||||
Counts = new
|
restrictedCollegeId.Value);
|
||||||
{
|
var teachers = db.Teachers.AsNoTracking()
|
||||||
Campuses = await db.Campuses.CountAsync(cancellationToken),
|
.Where(x =>
|
||||||
Colleges = await db.Colleges.CountAsync(cancellationToken),
|
!restrictedCollegeId.HasValue ||
|
||||||
Majors = await db.Majors.CountAsync(cancellationToken),
|
x.CollegeId == restrictedCollegeId.Value);
|
||||||
Classes = await db.AdministrativeClasses.CountAsync(cancellationToken),
|
var courses = db.Courses.AsNoTracking()
|
||||||
Classrooms = await db.Classrooms.CountAsync(cancellationToken),
|
.Where(x =>
|
||||||
Teachers = await db.Teachers.CountAsync(cancellationToken),
|
!restrictedCollegeId.HasValue ||
|
||||||
Students = await db.Students.CountAsync(cancellationToken),
|
x.CollegeId == restrictedCollegeId.Value);
|
||||||
Courses = await db.Courses.CountAsync(cancellationToken),
|
var teachingTasks = db.TeachingTasks.AsNoTracking()
|
||||||
CurriculumPlans = await db.CurriculumPlans.CountAsync(cancellationToken),
|
.Where(x =>
|
||||||
TeachingTasks = await db.TeachingTasks.CountAsync(cancellationToken),
|
currentTermId.HasValue &&
|
||||||
SchedulePlans = await db.SchedulePlans.CountAsync(cancellationToken),
|
x.AcademicTermId == currentTermId.Value &&
|
||||||
CourseSelectionRounds = await db.CourseSelectionRounds
|
(!restrictedCollegeId.HasValue ||
|
||||||
.CountAsync(cancellationToken),
|
x.Course!.CollegeId == restrictedCollegeId.Value));
|
||||||
CourseSelectionOfferings = await db.CourseSelectionOfferings
|
var gradeSheets = db.GradeSheets.AsNoTracking()
|
||||||
.CountAsync(cancellationToken),
|
.Where(x =>
|
||||||
CourseEnrollments = await db.CourseEnrollments
|
currentTermId.HasValue &&
|
||||||
.CountAsync(
|
x.TeachingTask!.AcademicTermId == currentTermId.Value &&
|
||||||
x => x.Status == CourseEnrollmentStatus.Enrolled,
|
(!restrictedCollegeId.HasValue ||
|
||||||
cancellationToken),
|
x.TeachingTask.Course!.CollegeId ==
|
||||||
GradeSheets = await db.GradeSheets.CountAsync(cancellationToken),
|
restrictedCollegeId.Value));
|
||||||
PublishedGradeSheets = await db.GradeSheets.CountAsync(
|
var enrollments = db.CourseEnrollments.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
currentTermId.HasValue &&
|
||||||
|
x.Status == CourseEnrollmentStatus.Enrolled &&
|
||||||
|
x.CourseSelectionOffering!.CourseSelectionRound!
|
||||||
|
.AcademicTermId == currentTermId.Value &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.CourseSelectionOffering.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value));
|
||||||
|
|
||||||
|
var taskCount = await teachingTasks.CountAsync(cancellationToken);
|
||||||
|
var publishedTaskCount = await teachingTasks.CountAsync(
|
||||||
|
x => x.Status == TeachingTaskStatus.Published,
|
||||||
|
cancellationToken);
|
||||||
|
var scheduledTaskCount = currentTermId.HasValue
|
||||||
|
? await db.ScheduleEntries.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.SchedulePlan!.AcademicTermId == currentTermId.Value &&
|
||||||
|
x.SchedulePlan.Status == SchedulePlanStatus.Published &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value))
|
||||||
|
.Select(x => x.TeachingTaskId)
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(cancellationToken)
|
||||||
|
: 0;
|
||||||
|
var gradeSheetCount = await gradeSheets.CountAsync(cancellationToken);
|
||||||
|
var publishedGradeSheetCount = await gradeSheets.CountAsync(
|
||||||
x => x.Status == GradeSheetStatus.Published,
|
x => x.Status == GradeSheetStatus.Published,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var counts = new DashboardCounts(
|
||||||
|
await students.CountAsync(
|
||||||
|
x => x.Status == StudentStatus.Active,
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
GradeRecords = await db.GradeRecords.CountAsync(cancellationToken),
|
await teachers.CountAsync(
|
||||||
ExamPlans = await db.ExamPlans.CountAsync(cancellationToken),
|
x => x.Status == TeacherStatus.Active,
|
||||||
ExamSessions = await db.ExamSessions.CountAsync(cancellationToken),
|
|
||||||
StudentStatusChanges = await db.StudentStatusChanges
|
|
||||||
.CountAsync(cancellationToken),
|
|
||||||
PendingStudentStatusChanges = await db.StudentStatusChanges.CountAsync(
|
|
||||||
x => x.State == StudentStatusChangeState.Submitted ||
|
|
||||||
x.State == StudentStatusChangeState.CounselorApproved ||
|
|
||||||
x.State == StudentStatusChangeState.CollegeApproved,
|
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
GraduationAuditBatches = await db.GraduationAuditBatches
|
await courses.CountAsync(
|
||||||
.CountAsync(cancellationToken),
|
x => x.IsEnabled,
|
||||||
PublishedGraduationAuditBatches = await db.GraduationAuditBatches
|
|
||||||
.CountAsync(
|
|
||||||
x => x.Status == GraduationAuditBatchStatus.Published,
|
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
DegreeAwardBatches = await db.DegreeAwardBatches
|
taskCount,
|
||||||
.CountAsync(cancellationToken),
|
publishedTaskCount,
|
||||||
PublishedDegreeAwardBatches = await db.DegreeAwardBatches
|
scheduledTaskCount,
|
||||||
.CountAsync(
|
await enrollments.CountAsync(cancellationToken),
|
||||||
x => x.Status == DegreeAwardBatchStatus.Published,
|
gradeSheetCount,
|
||||||
|
publishedGradeSheetCount,
|
||||||
|
await gradeSheets.CountAsync(
|
||||||
|
x => x.Status == GradeSheetStatus.Submitted,
|
||||||
cancellationToken),
|
cancellationToken),
|
||||||
GraduationClearanceBatches = await db.GraduationClearanceBatches
|
currentTermId.HasValue
|
||||||
.CountAsync(cancellationToken),
|
? await db.CourseSelectionRounds.AsNoTracking().CountAsync(
|
||||||
OpenGraduationClearanceBatches = await db.GraduationClearanceBatches
|
x => x.AcademicTermId == currentTermId.Value &&
|
||||||
.CountAsync(
|
x.Status == CourseSelectionRoundStatus.Open,
|
||||||
x => x.Status == GraduationClearanceBatchStatus.Open,
|
cancellationToken)
|
||||||
cancellationToken),
|
: 0);
|
||||||
Users = await db.Users.CountAsync(cancellationToken)
|
|
||||||
|
var pending = await LoadPendingAsync(
|
||||||
|
scope,
|
||||||
|
restrictedCollegeId,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return Ok(new DashboardResponse(
|
||||||
|
BuildAudience(scope, collegeName),
|
||||||
|
currentTerm,
|
||||||
|
counts,
|
||||||
|
pending,
|
||||||
|
DateTime.UtcNow));
|
||||||
}
|
}
|
||||||
},
|
|
||||||
jsonOptions.Value.JsonSerializerOptions);
|
private async Task<DashboardPending> LoadPendingAsync(
|
||||||
|
CurrentUserScope scope,
|
||||||
|
Guid? restrictedCollegeId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var isSchoolManager =
|
||||||
|
scope.IsInRole(SystemRoles.SuperAdmin) ||
|
||||||
|
scope.IsInRole(SystemRoles.AcademicAdmin);
|
||||||
|
var isCollegeManager =
|
||||||
|
!isSchoolManager && scope.IsInRole(SystemRoles.CollegeAdmin);
|
||||||
|
if (!isSchoolManager && !isCollegeManager)
|
||||||
|
return new DashboardPending(0, 0, 0, 0, 0, 0, 0);
|
||||||
|
|
||||||
|
var teacherApplications = db.TeacherCourseApplications.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Status == TeacherCourseApplicationStatus.Pending &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.Teacher!.CollegeId == restrictedCollegeId.Value));
|
||||||
|
var gradeSheets = db.GradeSheets.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Status == GradeSheetStatus.Submitted &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value));
|
||||||
|
var courseAdjustments = db.CourseAdjustments.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Status == CourseAdjustmentStatus.Submitted &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value));
|
||||||
|
var studentStatusChanges = db.StudentStatusChanges.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.State == (isCollegeManager
|
||||||
|
? StudentStatusChangeState.CounselorApproved
|
||||||
|
: StudentStatusChangeState.CollegeApproved) &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.Student!.AdministrativeClass!.Major!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value));
|
||||||
|
var gradeModifications = db.GradeModifications.AsNoTracking()
|
||||||
|
.Where(x =>
|
||||||
|
x.Status == (isCollegeManager
|
||||||
|
? GradeModificationStatus.TeacherSubmitted
|
||||||
|
: GradeModificationStatus.CollegeApproved) &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.GradeRecord!.GradeSheet!.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value));
|
||||||
|
|
||||||
|
var generalApprovals =
|
||||||
|
await db.CourseExemptions.AsNoTracking().CountAsync(
|
||||||
|
x => x.Status == ApprovalStatus.Submitted &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value),
|
||||||
|
cancellationToken) +
|
||||||
|
await db.DeferredExams.AsNoTracking().CountAsync(
|
||||||
|
x => x.Status == ApprovalStatus.Submitted &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.TeachingTask!.Course!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value),
|
||||||
|
cancellationToken) +
|
||||||
|
await db.CourseSubstitutions.AsNoTracking().CountAsync(
|
||||||
|
x => x.Status == ApprovalStatus.Submitted &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.Student!.AdministrativeClass!.Major!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value),
|
||||||
|
cancellationToken) +
|
||||||
|
await db.AttendanceRecords.AsNoTracking().CountAsync(
|
||||||
|
x => x.AppealStatus == AttendanceAppealStatus.Pending &&
|
||||||
|
(!restrictedCollegeId.HasValue ||
|
||||||
|
x.Student!.AdministrativeClass!.Major!.CollegeId ==
|
||||||
|
restrictedCollegeId.Value),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var classroomReservations = isCollegeManager &&
|
||||||
|
restrictedCollegeId.HasValue
|
||||||
|
? await db.ClassroomReservations.AsNoTracking().CountAsync(
|
||||||
|
x => x.Status == ClassroomReservationStatus.Submitted &&
|
||||||
|
x.ApplicantCollegeId == restrictedCollegeId.Value,
|
||||||
|
cancellationToken)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return new DashboardPending(
|
||||||
|
await teacherApplications.CountAsync(cancellationToken),
|
||||||
|
await gradeSheets.CountAsync(cancellationToken),
|
||||||
|
await courseAdjustments.CountAsync(cancellationToken),
|
||||||
|
await studentStatusChanges.CountAsync(cancellationToken),
|
||||||
|
await gradeModifications.CountAsync(cancellationToken),
|
||||||
|
classroomReservations,
|
||||||
|
generalApprovals);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DashboardAudience BuildAudience(
|
||||||
|
CurrentUserScope scope,
|
||||||
|
string? collegeName)
|
||||||
|
{
|
||||||
|
if (scope.IsInRole(SystemRoles.SuperAdmin))
|
||||||
|
return new DashboardAudience(
|
||||||
|
"System",
|
||||||
|
"全域教务工作台",
|
||||||
|
"全校",
|
||||||
|
"统筹基础数据、教学运行与系统治理");
|
||||||
|
if (scope.IsInRole(SystemRoles.AcademicAdmin))
|
||||||
|
return new DashboardAudience(
|
||||||
|
"School",
|
||||||
|
"校级教务工作台",
|
||||||
|
"全校",
|
||||||
|
"聚焦跨学院教学运行与校级审核");
|
||||||
|
if (scope.IsInRole(SystemRoles.CollegeAdmin))
|
||||||
|
return new DashboardAudience(
|
||||||
|
"College",
|
||||||
|
"学院教务工作台",
|
||||||
|
collegeName ?? "本学院",
|
||||||
|
"聚焦本学院教学准备、过程审核与成绩归档");
|
||||||
|
if (scope.IsInRole(SystemRoles.Leader))
|
||||||
|
return new DashboardAudience(
|
||||||
|
"Leadership",
|
||||||
|
"教学运行观察台",
|
||||||
|
"全校",
|
||||||
|
"查看全校教学运行与质量数据");
|
||||||
|
if (scope.IsInRole(SystemRoles.Counselor))
|
||||||
|
return new DashboardAudience(
|
||||||
|
"Counselor",
|
||||||
|
"班级工作台",
|
||||||
|
collegeName ?? "所辖班级",
|
||||||
|
"处理学生过程管理与学业支持");
|
||||||
|
return new DashboardAudience(
|
||||||
|
"Teaching",
|
||||||
|
"教学工作台",
|
||||||
|
collegeName ?? "个人教学",
|
||||||
|
"查看课程运行并进入日常教学工作");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record DashboardResponse(
|
||||||
|
DashboardAudience Audience,
|
||||||
|
DashboardTerm? CurrentTerm,
|
||||||
|
DashboardCounts Counts,
|
||||||
|
DashboardPending Pending,
|
||||||
|
DateTime GeneratedAt);
|
||||||
|
|
||||||
|
public sealed record DashboardAudience(
|
||||||
|
string Level,
|
||||||
|
string Title,
|
||||||
|
string ScopeName,
|
||||||
|
string Description);
|
||||||
|
|
||||||
|
public sealed record DashboardTerm(
|
||||||
|
Guid Id,
|
||||||
|
string Name,
|
||||||
|
DateOnly StartDate,
|
||||||
|
DateOnly EndDate);
|
||||||
|
|
||||||
|
public sealed record DashboardCounts(
|
||||||
|
int Students,
|
||||||
|
int Teachers,
|
||||||
|
int Courses,
|
||||||
|
int TeachingTasks,
|
||||||
|
int PublishedTeachingTasks,
|
||||||
|
int ScheduledTeachingTasks,
|
||||||
|
int CourseEnrollments,
|
||||||
|
int GradeSheets,
|
||||||
|
int PublishedGradeSheets,
|
||||||
|
int SubmittedGradeSheets,
|
||||||
|
int OpenCourseSelectionRounds);
|
||||||
|
|
||||||
|
public sealed record DashboardPending(
|
||||||
|
int TeacherApplications,
|
||||||
|
int GradeSheets,
|
||||||
|
int CourseAdjustments,
|
||||||
|
int StudentStatusChanges,
|
||||||
|
int GradeModifications,
|
||||||
|
int ClassroomReservations,
|
||||||
|
int GeneralApprovals);
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
using Jiaowu.Api.Controllers;
|
||||||
|
using Jiaowu.Api.Domain.Academic;
|
||||||
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Tests;
|
||||||
|
|
||||||
|
public sealed class DashboardControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task College_dashboard_is_scoped_and_only_counts_actionable_stage()
|
||||||
|
{
|
||||||
|
await using var database = await DashboardDatabase.CreateAsync();
|
||||||
|
var controller = new DashboardController(
|
||||||
|
database.Db,
|
||||||
|
new TestDataScope(
|
||||||
|
database.FirstCollegeId,
|
||||||
|
DataScope.College,
|
||||||
|
SystemRoles.CollegeAdmin));
|
||||||
|
|
||||||
|
var result = await controller.Get(CancellationToken.None);
|
||||||
|
|
||||||
|
var response = Assert.IsType<DashboardResponse>(
|
||||||
|
Assert.IsType<OkObjectResult>(result.Result).Value);
|
||||||
|
Assert.Equal("College", response.Audience.Level);
|
||||||
|
Assert.Equal("第一学院", response.Audience.ScopeName);
|
||||||
|
Assert.Equal(1, response.Counts.Students);
|
||||||
|
Assert.Equal(1, response.Counts.Teachers);
|
||||||
|
Assert.Equal(1, response.Counts.TeachingTasks);
|
||||||
|
Assert.Equal(1, response.Pending.TeacherApplications);
|
||||||
|
Assert.Equal(1, response.Pending.StudentStatusChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task School_dashboard_uses_school_stage_and_all_colleges()
|
||||||
|
{
|
||||||
|
await using var database = await DashboardDatabase.CreateAsync();
|
||||||
|
var controller = new DashboardController(
|
||||||
|
database.Db,
|
||||||
|
new TestDataScope(
|
||||||
|
null,
|
||||||
|
DataScope.All,
|
||||||
|
SystemRoles.AcademicAdmin));
|
||||||
|
|
||||||
|
var result = await controller.Get(CancellationToken.None);
|
||||||
|
|
||||||
|
var response = Assert.IsType<DashboardResponse>(
|
||||||
|
Assert.IsType<OkObjectResult>(result.Result).Value);
|
||||||
|
Assert.Equal("School", response.Audience.Level);
|
||||||
|
Assert.Equal(2, response.Counts.Students);
|
||||||
|
Assert.Equal(2, response.Counts.Teachers);
|
||||||
|
Assert.Equal(2, response.Counts.TeachingTasks);
|
||||||
|
Assert.Equal(2, response.Pending.TeacherApplications);
|
||||||
|
Assert.Equal(1, response.Pending.StudentStatusChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class DashboardDatabase : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly SqliteConnection connection;
|
||||||
|
|
||||||
|
private DashboardDatabase(
|
||||||
|
SqliteConnection connection,
|
||||||
|
AppDbContext db,
|
||||||
|
Guid firstCollegeId)
|
||||||
|
{
|
||||||
|
this.connection = connection;
|
||||||
|
Db = db;
|
||||||
|
FirstCollegeId = firstCollegeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppDbContext Db { get; }
|
||||||
|
public Guid FirstCollegeId { get; }
|
||||||
|
|
||||||
|
public static async Task<DashboardDatabase> CreateAsync()
|
||||||
|
{
|
||||||
|
var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||||
|
.UseSqlite(connection)
|
||||||
|
.Options;
|
||||||
|
var db = new AppDbContext(options);
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
|
||||||
|
var term = new AcademicTerm
|
||||||
|
{
|
||||||
|
Code = "2026-1",
|
||||||
|
Name = "2026—2027 学年第一学期",
|
||||||
|
AcademicYear = "2026-2027",
|
||||||
|
Season = TermSeason.Autumn,
|
||||||
|
StartDate = new DateOnly(2026, 9, 1),
|
||||||
|
EndDate = new DateOnly(2027, 1, 15),
|
||||||
|
IsCurrent = true
|
||||||
|
};
|
||||||
|
var firstCollege = new College
|
||||||
|
{
|
||||||
|
Code = "C01",
|
||||||
|
Name = "第一学院"
|
||||||
|
};
|
||||||
|
var secondCollege = new College
|
||||||
|
{
|
||||||
|
Code = "C02",
|
||||||
|
Name = "第二学院"
|
||||||
|
};
|
||||||
|
var firstMajor = CreateMajor("M01", firstCollege);
|
||||||
|
var secondMajor = CreateMajor("M02", secondCollege);
|
||||||
|
var firstClass = CreateClass("CL01", firstMajor);
|
||||||
|
var secondClass = CreateClass("CL02", secondMajor);
|
||||||
|
var firstStudent = CreateStudent("S01", firstClass);
|
||||||
|
var secondStudent = CreateStudent("S02", secondClass);
|
||||||
|
var firstTeacher = CreateTeacher("T01", firstCollege);
|
||||||
|
var secondTeacher = CreateTeacher("T02", secondCollege);
|
||||||
|
var firstCourse = CreateCourse("COURSE01", firstCollege);
|
||||||
|
var secondCourse = CreateCourse("COURSE02", secondCollege);
|
||||||
|
var firstTask = CreateTask("TASK01", term, firstCourse);
|
||||||
|
var secondTask = CreateTask("TASK02", term, secondCourse);
|
||||||
|
|
||||||
|
db.AddRange(
|
||||||
|
term,
|
||||||
|
firstCollege,
|
||||||
|
secondCollege,
|
||||||
|
firstMajor,
|
||||||
|
secondMajor,
|
||||||
|
firstClass,
|
||||||
|
secondClass,
|
||||||
|
firstStudent,
|
||||||
|
secondStudent,
|
||||||
|
firstTeacher,
|
||||||
|
secondTeacher,
|
||||||
|
firstCourse,
|
||||||
|
secondCourse,
|
||||||
|
firstTask,
|
||||||
|
secondTask,
|
||||||
|
new TeacherCourseApplication
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
Teacher = firstTeacher,
|
||||||
|
Course = firstCourse,
|
||||||
|
Status = TeacherCourseApplicationStatus.Pending
|
||||||
|
},
|
||||||
|
new TeacherCourseApplication
|
||||||
|
{
|
||||||
|
AcademicTerm = term,
|
||||||
|
Teacher = secondTeacher,
|
||||||
|
Course = secondCourse,
|
||||||
|
Status = TeacherCourseApplicationStatus.Pending
|
||||||
|
},
|
||||||
|
CreateStatusChange(
|
||||||
|
firstStudent,
|
||||||
|
StudentStatusChangeState.CounselorApproved),
|
||||||
|
CreateStatusChange(
|
||||||
|
firstStudent,
|
||||||
|
StudentStatusChangeState.CollegeApproved),
|
||||||
|
CreateStatusChange(
|
||||||
|
secondStudent,
|
||||||
|
StudentStatusChangeState.Submitted));
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return new DashboardDatabase(connection, db, firstCollege.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await Db.DisposeAsync();
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Major CreateMajor(string code, College college) => new()
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Name = $"专业 {code}",
|
||||||
|
College = college,
|
||||||
|
DegreeType = "本科"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static AdministrativeClass CreateClass(
|
||||||
|
string code,
|
||||||
|
Major major) => new()
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Name = $"班级 {code}",
|
||||||
|
Major = major,
|
||||||
|
Grade = 2026
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Student CreateStudent(
|
||||||
|
string number,
|
||||||
|
AdministrativeClass administrativeClass) => new()
|
||||||
|
{
|
||||||
|
StudentNumber = number,
|
||||||
|
Name = $"学生 {number}",
|
||||||
|
AdministrativeClass = administrativeClass,
|
||||||
|
EnrollmentYear = 2026,
|
||||||
|
EnrollmentDate = new DateOnly(2026, 9, 1),
|
||||||
|
Status = StudentStatus.Active
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Teacher CreateTeacher(
|
||||||
|
string number,
|
||||||
|
College college) => new()
|
||||||
|
{
|
||||||
|
TeacherNumber = number,
|
||||||
|
Name = $"教师 {number}",
|
||||||
|
College = college,
|
||||||
|
Status = TeacherStatus.Active
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Course CreateCourse(string code, College college) => new()
|
||||||
|
{
|
||||||
|
Code = code,
|
||||||
|
Name = $"课程 {code}",
|
||||||
|
College = college,
|
||||||
|
Nature = CourseNature.MajorRequired,
|
||||||
|
Credits = 2,
|
||||||
|
TotalHours = 32,
|
||||||
|
LectureHours = 32,
|
||||||
|
AssessmentMethod = AssessmentMethod.Examination
|
||||||
|
};
|
||||||
|
|
||||||
|
private static TeachingTask CreateTask(
|
||||||
|
string number,
|
||||||
|
AcademicTerm term,
|
||||||
|
Course course) => new()
|
||||||
|
{
|
||||||
|
TaskNumber = number,
|
||||||
|
Name = $"教学班 {number}",
|
||||||
|
AcademicTerm = term,
|
||||||
|
Course = course,
|
||||||
|
Capacity = 40,
|
||||||
|
Status = TeachingTaskStatus.Published
|
||||||
|
};
|
||||||
|
|
||||||
|
private static StudentStatusChange CreateStatusChange(
|
||||||
|
Student student,
|
||||||
|
StudentStatusChangeState state) => new()
|
||||||
|
{
|
||||||
|
Student = student,
|
||||||
|
Type = StudentStatusChangeType.Suspension,
|
||||||
|
OriginalStatus = StudentStatus.Active,
|
||||||
|
TargetStatus = StudentStatus.Suspended,
|
||||||
|
Reason = "测试学籍异动流程",
|
||||||
|
State = state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDataScope(
|
||||||
|
Guid? collegeId,
|
||||||
|
DataScope dataScope,
|
||||||
|
string role) : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"测试管理员",
|
||||||
|
collegeId,
|
||||||
|
dataScope,
|
||||||
|
new HashSet<string> { role });
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -38,6 +38,7 @@ declare module 'vue' {
|
|||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
|
ElResult: typeof import('element-plus/es')['ElResult']
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||||
|
|||||||
+1224
-129
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user