已按“实验成绩独立管理”完成修改。

每个实验项目独立建成绩单,支持评分项、参与状态、补做次数、安全违规、报告地址和教师评语。
流程为:录入 → 学院审核 → 教务发布;学生只能查看已发布成绩。
自主预约实验支持同步有效预约名单,已评分记录不会被误删。
课程成绩可将已发布实验成绩汇总为快照导入指定分项;导入后锁定,不能手工篡改。
实验项目成绩不完整时,对应课程分项保持空白,不按 0 分处理。
一级“实验管理”菜单下增加“项目与场次”“实验成绩”;学生端增加“实验安排”“实验成绩”。
已生成 MySQL 正式迁移及 SQLite 开发迁移,尚未操作生产数据库。
界面采用“实验项目 → 独立评分 → 审核发布”工作台设计。
This commit is contained in:
2026-07-28 22:21:18 +08:00 Unverified
parent 51c3b81e27
commit 8f56548b54
17 changed files with 9239 additions and 14 deletions
@@ -72,6 +72,8 @@ public sealed class DevelopmentSqliteMigrator(
"20260728_38_exam_publish_jobs";
private const string ExperimentManagementMigration =
"20260728_39_experiment_management";
private const string ExperimentGradeManagementMigration =
"20260728_40_experiment_grade_management";
public async Task MigrateAsync(CancellationToken cancellationToken = default)
{
@@ -543,6 +545,32 @@ public sealed class DevelopmentSqliteMigrator(
ExperimentManagementMigration,
experimentProjectsExist ? [] : ExperimentManagementStatements,
cancellationToken);
var gradeItemSourceExists = await db.Database
.SqlQueryRaw<int>(
"""
SELECT COUNT(*) AS "Value"
FROM pragma_table_info('GradeItems')
WHERE name = 'SourceType'
""")
.AnyAsync(value => value > 0, cancellationToken);
var experimentGradeSheetsExist = await db.Database
.SqlQueryRaw<int>(
"""
SELECT COUNT(*) AS "Value"
FROM sqlite_master
WHERE type = 'table' AND name = 'ExperimentGradeSheets'
""")
.AnyAsync(value => value > 0, cancellationToken);
await ApplyMigrationAsync(
ExperimentGradeManagementMigration,
(gradeItemSourceExists
? []
: ExperimentGradeItemSourceStatements)
.Concat(experimentGradeSheetsExist
? []
: ExperimentGradeManagementStatements),
cancellationToken);
}
private async Task ApplyMigrationAsync(
@@ -2511,4 +2539,133 @@ public sealed class DevelopmentSqliteMigrator(
);
"""
];
private static readonly string[] ExperimentGradeItemSourceStatements =
[
"""
ALTER TABLE "GradeItems"
ADD "SourceType" INTEGER NOT NULL DEFAULT 1;
""",
"""
ALTER TABLE "GradeItems"
ADD "SourceSnapshotAt" TEXT NULL;
"""
];
private static readonly string[] ExperimentGradeManagementStatements =
[
"""
CREATE TABLE "ExperimentGradeSheets" (
"Id" TEXT NOT NULL
CONSTRAINT "PK_ExperimentGradeSheets" PRIMARY KEY,
"ExperimentProjectId" TEXT NOT NULL,
"ContributionWeight" TEXT NOT NULL,
"PassScore" TEXT NOT NULL,
"Status" INTEGER NOT NULL,
"ReviewComment" TEXT NULL,
"SubmittedAt" TEXT NULL,
"ReviewedAt" TEXT NULL,
"PublishedAt" TEXT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
CONSTRAINT "FK_ExperimentGradeSheets_ExperimentProjects_ExperimentProjectId"
FOREIGN KEY ("ExperimentProjectId")
REFERENCES "ExperimentProjects" ("Id")
ON DELETE CASCADE
);
""",
"""
CREATE UNIQUE INDEX "IX_ExperimentGradeSheets_ExperimentProjectId"
ON "ExperimentGradeSheets" ("ExperimentProjectId");
""",
"""
CREATE TABLE "ExperimentGradeItems" (
"Id" TEXT NOT NULL
CONSTRAINT "PK_ExperimentGradeItems" PRIMARY KEY,
"ExperimentGradeSheetId" TEXT NOT NULL,
"Name" TEXT NOT NULL,
"Kind" INTEGER NOT NULL,
"Weight" TEXT NOT NULL,
"SortOrder" INTEGER NOT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
CONSTRAINT "FK_ExperimentGradeItems_ExperimentGradeSheets_ExperimentGradeSheetId"
FOREIGN KEY ("ExperimentGradeSheetId")
REFERENCES "ExperimentGradeSheets" ("Id")
ON DELETE CASCADE
);
""",
"""
CREATE INDEX "IX_ExperimentGradeItems_ExperimentGradeSheetId_SortOrder"
ON "ExperimentGradeItems" ("ExperimentGradeSheetId", "SortOrder");
""",
"""
CREATE TABLE "ExperimentGradeRecords" (
"Id" TEXT NOT NULL
CONSTRAINT "PK_ExperimentGradeRecords" PRIMARY KEY,
"ExperimentGradeSheetId" TEXT NOT NULL,
"StudentId" TEXT NOT NULL,
"ExperimentSessionId" TEXT NULL,
"ParticipationStatus" INTEGER NOT NULL,
"TotalScore" TEXT NULL,
"IsPassed" INTEGER NULL,
"SafetyViolation" INTEGER NOT NULL,
"AttemptNumber" INTEGER NOT NULL,
"SubmissionReference" TEXT NULL,
"SubmittedAt" TEXT NULL,
"IsLate" INTEGER NOT NULL,
"TeacherComment" TEXT NULL,
"CreatedAt" TEXT NOT NULL,
"UpdatedAt" TEXT NOT NULL,
CONSTRAINT "FK_ExperimentGradeRecords_ExperimentGradeSheets_ExperimentGradeSheetId"
FOREIGN KEY ("ExperimentGradeSheetId")
REFERENCES "ExperimentGradeSheets" ("Id")
ON DELETE CASCADE,
CONSTRAINT "FK_ExperimentGradeRecords_Students_StudentId"
FOREIGN KEY ("StudentId") REFERENCES "Students" ("Id")
ON DELETE RESTRICT,
CONSTRAINT "FK_ExperimentGradeRecords_ExperimentSessions_ExperimentSessionId"
FOREIGN KEY ("ExperimentSessionId")
REFERENCES "ExperimentSessions" ("Id")
ON DELETE RESTRICT
);
""",
"""
CREATE UNIQUE INDEX "IX_ExperimentGradeRecords_ExperimentGradeSheetId_StudentId"
ON "ExperimentGradeRecords" ("ExperimentGradeSheetId", "StudentId");
""",
"""
CREATE INDEX "IX_ExperimentGradeRecords_StudentId"
ON "ExperimentGradeRecords" ("StudentId");
""",
"""
CREATE INDEX "IX_ExperimentGradeRecords_ExperimentSessionId"
ON "ExperimentGradeRecords" ("ExperimentSessionId");
""",
"""
CREATE TABLE "ExperimentGradeItemScores" (
"ExperimentGradeRecordId" TEXT NOT NULL,
"ExperimentGradeItemId" TEXT NOT NULL,
"Score" TEXT NULL,
"Comment" TEXT NULL,
CONSTRAINT "PK_ExperimentGradeItemScores"
PRIMARY KEY (
"ExperimentGradeRecordId",
"ExperimentGradeItemId"
),
CONSTRAINT "FK_ExperimentGradeItemScores_ExperimentGradeRecords_ExperimentGradeRecordId"
FOREIGN KEY ("ExperimentGradeRecordId")
REFERENCES "ExperimentGradeRecords" ("Id")
ON DELETE CASCADE,
CONSTRAINT "FK_ExperimentGradeItemScores_ExperimentGradeItems_ExperimentGradeItemId"
FOREIGN KEY ("ExperimentGradeItemId")
REFERENCES "ExperimentGradeItems" ("Id")
ON DELETE CASCADE
);
""",
"""
CREATE INDEX "IX_ExperimentGradeItemScores_ExperimentGradeItemId"
ON "ExperimentGradeItemScores" ("ExperimentGradeItemId");
"""
];
}