已完成,学生端考勤记录现在按课程展示。

每门课程会独立显示:
课程名称、代码、教学班和任课教师。
课程总体出勤率。
出勤、迟到、缺勤、请假、免修次数。
按日期排列的历次点名记录。
缺勤或迟到记录仍可单独申诉。
This commit is contained in:
2026-07-26 10:49:06 +08:00 Unverified
parent 72297aa23f
commit 3b73292d93
12 changed files with 2172 additions and 54 deletions
@@ -38,10 +38,19 @@ public sealed class AttendanceControllerTests
MajorId = major.Id,
Grade = 2026
};
var studentUserId = Guid.NewGuid();
var studentUser = new ApplicationUser
{
Id = studentUserId,
UserName = "202601001",
NormalizedUserName = "202601001",
DisplayName = "周同学"
};
var firstStudent = new Student
{
StudentNumber = "202601001",
Name = "周同学",
UserId = studentUserId,
AdministrativeClassId = administrativeClass.Id,
EnrollmentYear = 2026,
EnrollmentDate = new DateOnly(2026, 9, 1)
@@ -85,6 +94,7 @@ public sealed class AttendanceControllerTests
Status = TeachingTaskStatus.Published
};
db.AddRange(
studentUser,
college,
major,
administrativeClass,
@@ -181,6 +191,111 @@ public sealed class AttendanceControllerTests
Assert.Equal(
3,
workbook.Worksheet("历次点名趋势").LastRowUsed()!.RowNumber());
var now = DateTime.UtcNow;
var qrRecord = new AttendanceRecord
{
StudentId = firstStudent.Id,
Status = AttendanceStatus.Absent
};
var qrSheet = new AttendanceSheet
{
TeachingTaskId = task.Id,
Name = "课堂扫码签到",
AttendanceDate = now,
Status = AttendanceSheetStatus.Draft,
CheckInMethod = AttendanceCheckInMethod.QrCode,
CheckInToken = "TEST-QR-TOKEN",
CheckInStartsAt = now.AddMinutes(-1),
CheckInEndsAt = now.AddMinutes(10),
Records = [qrRecord]
};
db.AttendanceSheets.Add(qrSheet);
await db.SaveChangesAsync();
var studentController = new AttendanceController(
db,
new StudentDataScope(studentUserId));
var myRecordsResult = await studentController.GetMyRecords(
null,
CancellationToken.None);
var myRecordsOk = Assert.IsType<OkObjectResult>(myRecordsResult);
var myRecords = Assert
.IsAssignableFrom<System.Collections.IEnumerable>(myRecordsOk.Value)
.Cast<object>()
.ToList();
Assert.Equal(2, myRecords.Count);
Assert.All(myRecords, item =>
Assert.Equal(
task.Id,
item.GetType().GetProperty("TeachingTaskId")!.GetValue(item)));
var infoResult = await studentController.GetCheckInInfo(
qrSheet.CheckInToken,
CancellationToken.None);
Assert.IsType<OkObjectResult>(infoResult);
var qrCheckInResult = await studentController.CheckIn(
new AttendanceCheckInRequest(
null,
qrSheet.CheckInToken,
null,
null,
null),
CancellationToken.None);
Assert.IsType<OkObjectResult>(qrCheckInResult);
Assert.Equal(AttendanceStatus.Present, qrRecord.Status);
Assert.Equal(AttendanceCheckInMethod.QrCode, qrRecord.CheckedInMethod);
Assert.NotNull(qrRecord.CheckInAt);
Assert.Null(qrRecord.CheckInLatitude);
var locationRecord = new AttendanceRecord
{
StudentId = firstStudent.Id,
Status = AttendanceStatus.Absent
};
var locationSheet = new AttendanceSheet
{
TeachingTaskId = task.Id,
Name = "课堂定位签到",
AttendanceDate = now,
Status = AttendanceSheetStatus.Draft,
CheckInMethod = AttendanceCheckInMethod.Location,
CheckInStartsAt = now.AddMinutes(-1),
CheckInEndsAt = now.AddMinutes(10),
TargetLatitude = 39.9m,
TargetLongitude = 116.4m,
LocationRadiusMeters = 100,
Records = [locationRecord]
};
db.AttendanceSheets.Add(locationSheet);
await db.SaveChangesAsync();
var outsideResult = await studentController.CheckIn(
new AttendanceCheckInRequest(
locationSheet.Id,
null,
39.91m,
116.4m,
8),
CancellationToken.None);
Assert.IsType<ConflictObjectResult>(outsideResult);
Assert.Null(locationRecord.CheckInAt);
var nearbyResult = await studentController.CheckIn(
new AttendanceCheckInRequest(
locationSheet.Id,
null,
39.9001m,
116.4m,
8),
CancellationToken.None);
Assert.IsType<OkObjectResult>(nearbyResult);
Assert.Equal(AttendanceStatus.Present, locationRecord.Status);
Assert.Equal(
AttendanceCheckInMethod.Location,
locationRecord.CheckedInMethod);
Assert.InRange(locationRecord.CheckInDistanceMeters!.Value, 1, 100);
}
private sealed class AllDataScope : ICurrentUserDataScope
@@ -192,4 +307,14 @@ public sealed class AttendanceControllerTests
DataScope.All,
new HashSet<string>([SystemRoles.SuperAdmin]));
}
private sealed class StudentDataScope(Guid userId) : ICurrentUserDataScope
{
public CurrentUserScope Current { get; } = new(
userId,
"测试学生",
null,
DataScope.Self,
new HashSet<string>([SystemRoles.Student]));
}
}
@@ -8,7 +8,7 @@ namespace Jiaowu.Api.Tests;
public sealed class MySqlMigrationTests
{
private const string LatestMigration =
"20260725120917_ProductionSchemaCompletion";
"20260726022634_AttendanceCheckIn";
[Fact]
public void Production_migration_is_discoverable_and_generates_mysql_sql()
@@ -25,6 +25,8 @@ public sealed class MySqlMigrationTests
Assert.Contains("CREATE TABLE `GradeItems`", script);
Assert.Contains("CREATE TABLE `EvaluationSetups`", script);
Assert.Contains("CREATE TABLE `WarningRules`", script);
Assert.Contains("ADD `CheckInMethod` int NOT NULL DEFAULT 1", script);
Assert.Contains("CREATE UNIQUE INDEX `IX_AttendanceSheets_CheckInToken`", script);
Assert.Contains("SET `ExamDate` = DATE(`StartsAt`)", script);
Assert.Contains("DEFAULT 1", script);
Assert.DoesNotContain("0001-01-01", script);