已完成,学生端考勤记录现在按课程展示。
每门课程会独立显示: 课程名称、代码、教学班和任课教师。 课程总体出勤率。 出勤、迟到、缺勤、请假、免修次数。 按日期排列的历次点名记录。 缺勤或迟到记录仍可单独申诉。
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user