新增“课程统计”模式,展示总体出勤率、状态分布、历次趋势和每位学生的出勤明细。
支持按姓名/学号、行政班及异常情况筛选,默认优先显示低出勤率学生。 新增课程统计 Excel 导出,包含课程概览、学生出勤明细、历次点名趋势三个工作表。 统计仅使用已提交点名;出勤和迟到计为到课,免修不计入应到次数。 修复了枚举兼容问题,已提交点名不会再错误显示为“草稿”,考勤状态中文标签恢复正常。 统计与导出接口均执行教师/数据范围权限校验。
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
using ClosedXML.Excel;
|
||||
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 AttendanceControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task TaskStatistics_UsesSubmittedSheetsAndExportsAllDetails()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
var options = new DbContextOptionsBuilder<AppDbContext>()
|
||||
.UseSqlite(connection)
|
||||
.Options;
|
||||
await using var db = new AppDbContext(options);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
|
||||
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||
var major = new Major
|
||||
{
|
||||
Code = "080901",
|
||||
Name = "计算机科学与技术",
|
||||
CollegeId = college.Id,
|
||||
DegreeType = "工学学士"
|
||||
};
|
||||
var administrativeClass = new AdministrativeClass
|
||||
{
|
||||
Code = "CS2026-01",
|
||||
Name = "计科 2026-1 班",
|
||||
MajorId = major.Id,
|
||||
Grade = 2026
|
||||
};
|
||||
var firstStudent = new Student
|
||||
{
|
||||
StudentNumber = "202601001",
|
||||
Name = "周同学",
|
||||
AdministrativeClassId = administrativeClass.Id,
|
||||
EnrollmentYear = 2026,
|
||||
EnrollmentDate = new DateOnly(2026, 9, 1)
|
||||
};
|
||||
var secondStudent = new Student
|
||||
{
|
||||
StudentNumber = "202601002",
|
||||
Name = "吴同学",
|
||||
AdministrativeClassId = administrativeClass.Id,
|
||||
EnrollmentYear = 2026,
|
||||
EnrollmentDate = new DateOnly(2026, 9, 1)
|
||||
};
|
||||
var course = new Course
|
||||
{
|
||||
Code = "CS101",
|
||||
Name = "程序设计基础",
|
||||
CollegeId = college.Id,
|
||||
Credits = 4,
|
||||
TotalHours = 64,
|
||||
LectureHours = 48,
|
||||
PracticeHours = 16,
|
||||
Nature = CourseNature.MajorRequired,
|
||||
AssessmentMethod = AssessmentMethod.Examination
|
||||
};
|
||||
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, 20)
|
||||
};
|
||||
var task = new TeachingTask
|
||||
{
|
||||
TaskNumber = "2026-1-CS101-01",
|
||||
Name = "程序设计基础教学班",
|
||||
AcademicTermId = term.Id,
|
||||
CourseId = course.Id,
|
||||
Capacity = 60,
|
||||
Status = TeachingTaskStatus.Published
|
||||
};
|
||||
db.AddRange(
|
||||
college,
|
||||
major,
|
||||
administrativeClass,
|
||||
firstStudent,
|
||||
secondStudent,
|
||||
course,
|
||||
term,
|
||||
task);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
db.AttendanceSheets.AddRange(
|
||||
new AttendanceSheet
|
||||
{
|
||||
TeachingTaskId = task.Id,
|
||||
Name = "第1周点名",
|
||||
AttendanceDate = new DateTime(2026, 9, 3),
|
||||
Status = AttendanceSheetStatus.Submitted,
|
||||
Records =
|
||||
[
|
||||
new AttendanceRecord
|
||||
{
|
||||
StudentId = firstStudent.Id,
|
||||
Status = AttendanceStatus.Present
|
||||
},
|
||||
new AttendanceRecord
|
||||
{
|
||||
StudentId = secondStudent.Id,
|
||||
Status = AttendanceStatus.Absent
|
||||
}
|
||||
]
|
||||
},
|
||||
new AttendanceSheet
|
||||
{
|
||||
TeachingTaskId = task.Id,
|
||||
Name = "第2周点名",
|
||||
AttendanceDate = new DateTime(2026, 9, 10),
|
||||
Status = AttendanceSheetStatus.Submitted,
|
||||
Records =
|
||||
[
|
||||
new AttendanceRecord
|
||||
{
|
||||
StudentId = firstStudent.Id,
|
||||
Status = AttendanceStatus.Late
|
||||
},
|
||||
new AttendanceRecord
|
||||
{
|
||||
StudentId = secondStudent.Id,
|
||||
Status = AttendanceStatus.Excused
|
||||
}
|
||||
]
|
||||
},
|
||||
new AttendanceSheet
|
||||
{
|
||||
TeachingTaskId = task.Id,
|
||||
Name = "未提交点名",
|
||||
AttendanceDate = new DateTime(2026, 9, 17),
|
||||
Status = AttendanceSheetStatus.Draft,
|
||||
Records =
|
||||
[
|
||||
new AttendanceRecord
|
||||
{
|
||||
StudentId = firstStudent.Id,
|
||||
Status = AttendanceStatus.Absent
|
||||
}
|
||||
]
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var controller = new AttendanceController(db, new AllDataScope());
|
||||
var result = await controller.GetTaskStatistics(
|
||||
task.Id,
|
||||
CancellationToken.None);
|
||||
|
||||
var ok = Assert.IsType<OkObjectResult>(result);
|
||||
var statistics = Assert.IsType<AttendanceCourseStatistics>(ok.Value);
|
||||
Assert.Equal(2, statistics.Summary.SubmittedSheetCount);
|
||||
Assert.Equal(4, statistics.Summary.RecordCount);
|
||||
Assert.Equal(3, statistics.Summary.RequiredCount);
|
||||
Assert.Equal(66.7m, statistics.Summary.OverallAttendanceRate);
|
||||
Assert.Equal(0, statistics.Summary.PerfectAttendanceCount);
|
||||
Assert.Equal(2, statistics.Students.Count);
|
||||
Assert.Equal(0m, statistics.Students[0].AttendanceRate);
|
||||
Assert.Equal(100m, statistics.Students[1].AttendanceRate);
|
||||
|
||||
var exportResult = await controller.ExportTaskStatistics(
|
||||
task.Id,
|
||||
CancellationToken.None);
|
||||
var file = Assert.IsType<FileContentResult>(exportResult);
|
||||
using var workbook = new XLWorkbook(new MemoryStream(file.FileContents));
|
||||
Assert.NotNull(workbook.Worksheet("课程概览"));
|
||||
Assert.Equal(
|
||||
3,
|
||||
workbook.Worksheet("学生出勤明细").LastRowUsed()!.RowNumber());
|
||||
Assert.Equal(
|
||||
3,
|
||||
workbook.Worksheet("历次点名趋势").LastRowUsed()!.RowNumber());
|
||||
}
|
||||
|
||||
private sealed class AllDataScope : ICurrentUserDataScope
|
||||
{
|
||||
public CurrentUserScope Current { get; } = new(
|
||||
Guid.NewGuid(),
|
||||
"测试管理员",
|
||||
null,
|
||||
DataScope.All,
|
||||
new HashSet<string>([SystemRoles.SuperAdmin]));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user