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

每门课程会独立显示:
课程名称、代码、教学班和任课教师。
课程总体出勤率。
出勤、迟到、缺勤、请假、免修次数。
按日期排列的历次点名记录。
缺勤或迟到记录仍可单独申诉。
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
@@ -561,8 +561,12 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
builder.Entity<AttendanceSheet>(entity =>
{
entity.Property(x => x.Name).HasMaxLength(120);
entity.Property(x => x.CheckInToken).HasMaxLength(64);
entity.Property(x => x.TargetLatitude).HasPrecision(10, 7);
entity.Property(x => x.TargetLongitude).HasPrecision(10, 7);
entity.Property(x => x.Notes).HasMaxLength(500);
entity.HasIndex(x => new { x.TeachingTaskId, x.AttendanceDate });
entity.HasIndex(x => x.CheckInToken).IsUnique();
entity.HasOne(x => x.TeachingTask)
.WithMany()
.HasForeignKey(x => x.TeachingTaskId)
@@ -575,6 +579,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
entity.Property(x => x.Notes).HasMaxLength(300);
entity.Property(x => x.AppealReason).HasMaxLength(500);
entity.Property(x => x.AppealReviewComment).HasMaxLength(300);
entity.Property(x => x.CheckInLatitude).HasPrecision(10, 7);
entity.Property(x => x.CheckInLongitude).HasPrecision(10, 7);
entity.HasIndex(x => x.AppealStatus);
entity.HasOne(x => x.AttendanceSheet)
.WithMany(x => x.Records)
@@ -40,6 +40,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260725_22_course_adjustments";
private const string AttendanceAppealMigration =
"20260725_23_attendance_appeal";
private const string AttendanceCheckInMigration =
"20260726_26_attendance_check_in";
private const string ApprovalTablesMigration =
"20260725_24_approval_tables";
private const string AcademicWarningsMigration =
@@ -275,6 +277,19 @@ public sealed class DevelopmentSqliteMigrator(
attendanceAppealExists ? [] : AttendanceAppealStatements,
cancellationToken);
var attendanceCheckInExists = await db.Database
.SqlQueryRaw<int>(
"""
SELECT COUNT(*) AS "Value"
FROM pragma_table_info('AttendanceSheets')
WHERE name = 'CheckInMethod'
""")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
AttendanceCheckInMigration,
attendanceCheckInExists ? [] : AttendanceCheckInStatements,
cancellationToken);
var approvalTablesExist = await db.Database
.SqlQueryRaw<int>("SELECT COUNT(*) AS \"Value\" FROM sqlite_master WHERE type = 'table' AND name = 'CourseExemptions'")
.AnyAsync(value => value > 0, cancellationToken);
@@ -1623,6 +1638,24 @@ public sealed class DevelopmentSqliteMigrator(
"""CREATE INDEX IF NOT EXISTS "IX_AttendanceRecords_AppealStatus" ON "AttendanceRecords" ("AppealStatus");"""
];
private static readonly string[] AttendanceCheckInStatements =
[
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "CheckInMethod" INTEGER NOT NULL DEFAULT 1;""",
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "CheckInToken" TEXT NULL;""",
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "CheckInStartsAt" TEXT NULL;""",
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "CheckInEndsAt" TEXT NULL;""",
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "TargetLatitude" TEXT NULL;""",
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "TargetLongitude" TEXT NULL;""",
"""ALTER TABLE "AttendanceSheets" ADD COLUMN "LocationRadiusMeters" INTEGER NULL;""",
"""CREATE UNIQUE INDEX IF NOT EXISTS "IX_AttendanceSheets_CheckInToken" ON "AttendanceSheets" ("CheckInToken");""",
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "CheckInAt" TEXT NULL;""",
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "CheckedInMethod" INTEGER NULL;""",
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "CheckInLatitude" TEXT NULL;""",
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "CheckInLongitude" TEXT NULL;""",
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "CheckInAccuracyMeters" REAL NULL;""",
"""ALTER TABLE "AttendanceRecords" ADD COLUMN "CheckInDistanceMeters" REAL NULL;"""
];
private static readonly string[] ApprovalTableStatements =
[
"""CREATE TABLE "CourseExemptions" ("Id" TEXT NOT NULL CONSTRAINT "PK_CourseExemptions" PRIMARY KEY, "StudentId" TEXT NOT NULL, "TeachingTaskId" TEXT NOT NULL, "Reason" TEXT NOT NULL, "Status" INTEGER NOT NULL, "ReviewComment" TEXT NULL, "SubmittedAt" TEXT NOT NULL, "ReviewedAt" TEXT NULL, "ReviewedByUserId" TEXT NULL, "CreatedAt" TEXT NOT NULL, "UpdatedAt" TEXT NOT NULL, CONSTRAINT "FK_CourseExemptions_Students" FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id") ON DELETE RESTRICT, CONSTRAINT "FK_CourseExemptions_TeachingTasks" FOREIGN KEY ("TeachingTaskId") REFERENCES "TeachingTasks" ("Id") ON DELETE RESTRICT);""",
@@ -0,0 +1,131 @@
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql;
[DbContext(typeof(AppDbContext))]
[Migration("20260726022634_AttendanceCheckIn")]
public partial class AttendanceCheckIn : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "CheckInMethod",
table: "AttendanceSheets",
type: "int",
nullable: false,
defaultValue: 1);
migrationBuilder.AddColumn<string>(
name: "CheckInToken",
table: "AttendanceSheets",
type: "varchar(64)",
maxLength: 64,
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<DateTime>(
name: "CheckInStartsAt",
table: "AttendanceSheets",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "CheckInEndsAt",
table: "AttendanceSheets",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "TargetLatitude",
table: "AttendanceSheets",
type: "decimal(10,7)",
precision: 10,
scale: 7,
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "TargetLongitude",
table: "AttendanceSheets",
type: "decimal(10,7)",
precision: 10,
scale: 7,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "LocationRadiusMeters",
table: "AttendanceSheets",
type: "int",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "CheckInAt",
table: "AttendanceRecords",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "CheckedInMethod",
table: "AttendanceRecords",
type: "int",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "CheckInLatitude",
table: "AttendanceRecords",
type: "decimal(10,7)",
precision: 10,
scale: 7,
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "CheckInLongitude",
table: "AttendanceRecords",
type: "decimal(10,7)",
precision: 10,
scale: 7,
nullable: true);
migrationBuilder.AddColumn<double>(
name: "CheckInAccuracyMeters",
table: "AttendanceRecords",
type: "double",
nullable: true);
migrationBuilder.AddColumn<double>(
name: "CheckInDistanceMeters",
table: "AttendanceRecords",
type: "double",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_AttendanceSheets_CheckInToken",
table: "AttendanceSheets",
column: "CheckInToken",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_AttendanceSheets_CheckInToken",
table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "CheckInMethod", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "CheckInToken", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "CheckInStartsAt", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "CheckInEndsAt", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "TargetLatitude", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "TargetLongitude", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "LocationRadiusMeters", table: "AttendanceSheets");
migrationBuilder.DropColumn(name: "CheckInAt", table: "AttendanceRecords");
migrationBuilder.DropColumn(name: "CheckedInMethod", table: "AttendanceRecords");
migrationBuilder.DropColumn(name: "CheckInLatitude", table: "AttendanceRecords");
migrationBuilder.DropColumn(name: "CheckInLongitude", table: "AttendanceRecords");
migrationBuilder.DropColumn(name: "CheckInAccuracyMeters", table: "AttendanceRecords");
migrationBuilder.DropColumn(name: "CheckInDistanceMeters", table: "AttendanceRecords");
}
}
@@ -154,6 +154,26 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime?>("AppealSubmittedAt")
.HasColumnType("datetime(6)");
b.Property<double?>("CheckInAccuracyMeters")
.HasColumnType("double");
b.Property<DateTime?>("CheckInAt")
.HasColumnType("datetime(6)");
b.Property<double?>("CheckInDistanceMeters")
.HasColumnType("double");
b.Property<decimal?>("CheckInLatitude")
.HasPrecision(10, 7)
.HasColumnType("decimal(10,7)");
b.Property<decimal?>("CheckInLongitude")
.HasPrecision(10, 7)
.HasColumnType("decimal(10,7)");
b.Property<int?>("CheckedInMethod")
.HasColumnType("int");
b.Property<string>("Notes")
.HasMaxLength(300)
.HasColumnType("varchar(300)");
@@ -179,9 +199,25 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime>("AttendanceDate")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("CheckInEndsAt")
.HasColumnType("datetime(6)");
b.Property<int>("CheckInMethod")
.HasColumnType("int");
b.Property<DateTime?>("CheckInStartsAt")
.HasColumnType("datetime(6)");
b.Property<string>("CheckInToken")
.HasMaxLength(64)
.HasColumnType("varchar(64)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<int?>("LocationRadiusMeters")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(120)
@@ -197,6 +233,14 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<DateTime?>("SubmittedAt")
.HasColumnType("datetime(6)");
b.Property<decimal?>("TargetLatitude")
.HasPrecision(10, 7)
.HasColumnType("decimal(10,7)");
b.Property<decimal?>("TargetLongitude")
.HasPrecision(10, 7)
.HasColumnType("decimal(10,7)");
b.Property<Guid>("TeachingTaskId")
.HasColumnType("char(36)");
@@ -205,6 +249,9 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.HasKey("Id");
b.HasIndex("CheckInToken")
.IsUnique();
b.HasIndex("TeachingTaskId", "AttendanceDate");
b.ToTable("AttendanceSheets");