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

每个实验项目独立建成绩单,支持评分项、参与状态、补做次数、安全违规、报告地址和教师评语。
流程为:录入 → 学院审核 → 教务发布;学生只能查看已发布成绩。
自主预约实验支持同步有效预约名单,已评分记录不会被误删。
课程成绩可将已发布实验成绩汇总为快照导入指定分项;导入后锁定,不能手工篡改。
实验项目成绩不完整时,对应课程分项保持空白,不按 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
@@ -0,0 +1,88 @@
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
namespace Jiaowu.Api.Infrastructure.Grades;
public static class ExperimentGradeAggregationService
{
public static async Task<ExperimentGradeAggregateResult> CalculateAsync(
AppDbContext db,
Guid teachingTaskId,
IReadOnlyCollection<Guid> studentIds,
CancellationToken cancellationToken)
{
var sheets = await db.ExperimentGradeSheets.AsNoTracking()
.Where(x =>
x.Status ==
Domain.Academic.ExperimentGradeSheetStatus.Published &&
x.ExperimentProject!.TeachingTaskId == teachingTaskId)
.OrderBy(x => x.ExperimentProject!.Code)
.Select(x => new PublishedExperimentSheet(
x.Id,
x.ExperimentProject!.Code,
x.ExperimentProject.Name,
x.ContributionWeight,
x.Records.Select(record => new PublishedExperimentScore(
record.StudentId,
record.TotalScore)).ToList()))
.AsSplitQuery()
.ToListAsync(cancellationToken);
var scores = new Dictionary<Guid, decimal?>();
foreach (var studentId in studentIds.Distinct())
{
decimal weightedTotal = 0;
decimal totalWeight = 0;
var complete = sheets.Count > 0;
foreach (var sheet in sheets)
{
var score = sheet.Scores.FirstOrDefault(x =>
x.StudentId == studentId);
if (score?.TotalScore is not decimal totalScore)
{
complete = false;
break;
}
weightedTotal += totalScore * sheet.ContributionWeight;
totalWeight += sheet.ContributionWeight;
}
scores[studentId] = complete && totalWeight > 0
? Math.Round(
weightedTotal / totalWeight,
1,
MidpointRounding.AwayFromZero)
: null;
}
return new ExperimentGradeAggregateResult(
sheets.Count,
sheets.Select(x => new ExperimentGradeAggregateProject(
x.Id,
x.Code,
x.Name,
x.ContributionWeight)).ToList(),
scores);
}
private sealed record PublishedExperimentSheet(
Guid Id,
string Code,
string Name,
decimal ContributionWeight,
List<PublishedExperimentScore> Scores);
private sealed record PublishedExperimentScore(
Guid StudentId,
decimal? TotalScore);
}
public sealed record ExperimentGradeAggregateResult(
int PublishedProjectCount,
IReadOnlyList<ExperimentGradeAggregateProject> Projects,
IReadOnlyDictionary<Guid, decimal?> Scores);
public sealed record ExperimentGradeAggregateProject(
Guid Id,
string Code,
string Name,
decimal ContributionWeight);
@@ -0,0 +1,45 @@
using Jiaowu.Api.Domain.Academic;
namespace Jiaowu.Api.Infrastructure.Grades;
public static class ExperimentGradeCalculator
{
public static bool AreWeightsValid(
decimal contributionWeight,
decimal passScore,
IEnumerable<ExperimentGradeItem> items)
{
var itemList = items.ToList();
return contributionWeight is > 0 and <= 100 &&
passScore is >= 0 and <= 100 &&
itemList.Count > 0 &&
itemList.All(item => item.Weight is > 0 and <= 100) &&
itemList.Sum(item => item.Weight) == 100;
}
public static decimal? CalculateTotal(
ExperimentGradeRecord record,
IReadOnlyCollection<ExperimentGradeItem> items)
{
if (record.SafetyViolation ||
record.ParticipationStatus == ExperimentParticipationStatus.Absent)
return 0;
if (record.ParticipationStatus is
ExperimentParticipationStatus.Pending or
ExperimentParticipationStatus.Excused or
ExperimentParticipationStatus.Exempt)
return null;
var scores = record.ItemScores.ToDictionary(
x => x.ExperimentGradeItemId);
decimal total = 0;
foreach (var item in items)
{
if (!scores.TryGetValue(item.Id, out var score) ||
!score.Score.HasValue)
return null;
total += score.Score.Value * item.Weight / 100;
}
return Math.Round(total, 1, MidpointRounding.AwayFromZero);
}
}
@@ -47,6 +47,14 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
public DbSet<ExperimentProject> ExperimentProjects => Set<ExperimentProject>();
public DbSet<ExperimentSession> ExperimentSessions => Set<ExperimentSession>();
public DbSet<ExperimentBooking> ExperimentBookings => Set<ExperimentBooking>();
public DbSet<ExperimentGradeSheet> ExperimentGradeSheets =>
Set<ExperimentGradeSheet>();
public DbSet<ExperimentGradeItem> ExperimentGradeItems =>
Set<ExperimentGradeItem>();
public DbSet<ExperimentGradeRecord> ExperimentGradeRecords =>
Set<ExperimentGradeRecord>();
public DbSet<ExperimentGradeItemScore> ExperimentGradeItemScores =>
Set<ExperimentGradeItemScore>();
public DbSet<CourseSelectionRound> CourseSelectionRounds =>
Set<CourseSelectionRound>();
public DbSet<CourseSelectionRoundGrade> CourseSelectionRoundGrades =>
@@ -608,6 +616,75 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<ExperimentGradeSheet>(entity =>
{
entity.Property(x => x.ContributionWeight).HasPrecision(5, 1);
entity.Property(x => x.PassScore).HasPrecision(5, 1);
entity.Property(x => x.ReviewComment).HasMaxLength(500);
entity.HasIndex(x => x.ExperimentProjectId).IsUnique();
entity.HasOne(x => x.ExperimentProject)
.WithOne(x => x.GradeSheet)
.HasForeignKey<ExperimentGradeSheet>(
x => x.ExperimentProjectId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<ExperimentGradeItem>(entity =>
{
entity.Property(x => x.Name).HasMaxLength(60);
entity.Property(x => x.Weight).HasPrecision(5, 1);
entity.HasIndex(x => new
{
x.ExperimentGradeSheetId,
x.SortOrder
});
entity.HasOne(x => x.ExperimentGradeSheet)
.WithMany(x => x.Items)
.HasForeignKey(x => x.ExperimentGradeSheetId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<ExperimentGradeRecord>(entity =>
{
entity.Property(x => x.TotalScore).HasPrecision(5, 1);
entity.Property(x => x.SubmissionReference).HasMaxLength(500);
entity.Property(x => x.TeacherComment).HasMaxLength(500);
entity.HasIndex(x => new
{
x.ExperimentGradeSheetId,
x.StudentId
}).IsUnique();
entity.HasOne(x => x.ExperimentGradeSheet)
.WithMany(x => x.Records)
.HasForeignKey(x => x.ExperimentGradeSheetId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.Student).WithMany()
.HasForeignKey(x => x.StudentId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasOne(x => x.ExperimentSession).WithMany()
.HasForeignKey(x => x.ExperimentSessionId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<ExperimentGradeItemScore>(entity =>
{
entity.HasKey(x => new
{
x.ExperimentGradeRecordId,
x.ExperimentGradeItemId
});
entity.Property(x => x.Score).HasPrecision(5, 1);
entity.Property(x => x.Comment).HasMaxLength(300);
entity.HasOne(x => x.ExperimentGradeRecord)
.WithMany(x => x.ItemScores)
.HasForeignKey(x => x.ExperimentGradeRecordId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(x => x.ExperimentGradeItem)
.WithMany(x => x.Scores)
.HasForeignKey(x => x.ExperimentGradeItemId)
.OnDelete(DeleteBehavior.Cascade);
});
builder.Entity<CourseSelectionRound>(entity =>
{
entity.Property(x => x.Name).HasMaxLength(120);
@@ -687,6 +764,8 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
{
entity.Property(x => x.Name).HasMaxLength(60);
entity.Property(x => x.Weight).HasPrecision(5, 1);
entity.Property(x => x.SourceType)
.HasDefaultValue(GradeItemSourceType.Manual);
entity.HasIndex(x => new { x.GradeSheetId, x.SortOrder });
entity.HasOne(x => x.GradeSheet)
.WithMany(x => x.Items)
@@ -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");
"""
];
}
@@ -0,0 +1,208 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
{
/// <inheritdoc />
public partial class ExperimentGradeManagement : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "SourceSnapshotAt",
table: "GradeItems",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SourceType",
table: "GradeItems",
type: "int",
nullable: false,
defaultValue: 1);
migrationBuilder.CreateTable(
name: "ExperimentGradeSheets",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
ExperimentProjectId = table.Column<Guid>(type: "char(36)", nullable: false),
ContributionWeight = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
PassScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
Status = table.Column<int>(type: "int", nullable: false),
ReviewComment = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
SubmittedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
ReviewedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
PublishedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ExperimentGradeSheets", x => x.Id);
table.ForeignKey(
name: "FK_ExperimentGradeSheets_ExperimentProjects_ExperimentProjectId",
column: x => x.ExperimentProjectId,
principalTable: "ExperimentProjects",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "ExperimentGradeItems",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
ExperimentGradeSheetId = table.Column<Guid>(type: "char(36)", nullable: false),
Name = table.Column<string>(type: "varchar(60)", maxLength: 60, nullable: false),
Kind = table.Column<int>(type: "int", nullable: false),
Weight = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: false),
SortOrder = table.Column<int>(type: "int", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ExperimentGradeItems", x => x.Id);
table.ForeignKey(
name: "FK_ExperimentGradeItems_ExperimentGradeSheets_ExperimentGradeSh~",
column: x => x.ExperimentGradeSheetId,
principalTable: "ExperimentGradeSheets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "ExperimentGradeRecords",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false),
ExperimentGradeSheetId = table.Column<Guid>(type: "char(36)", nullable: false),
StudentId = table.Column<Guid>(type: "char(36)", nullable: false),
ExperimentSessionId = table.Column<Guid>(type: "char(36)", nullable: true),
ParticipationStatus = table.Column<int>(type: "int", nullable: false),
TotalScore = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: true),
IsPassed = table.Column<bool>(type: "tinyint(1)", nullable: true),
SafetyViolation = table.Column<bool>(type: "tinyint(1)", nullable: false),
AttemptNumber = table.Column<int>(type: "int", nullable: false),
SubmissionReference = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
SubmittedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
IsLate = table.Column<bool>(type: "tinyint(1)", nullable: false),
TeacherComment = table.Column<string>(type: "varchar(500)", maxLength: 500, nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ExperimentGradeRecords", x => x.Id);
table.ForeignKey(
name: "FK_ExperimentGradeRecords_ExperimentGradeSheets_ExperimentGrade~",
column: x => x.ExperimentGradeSheetId,
principalTable: "ExperimentGradeSheets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ExperimentGradeRecords_ExperimentSessions_ExperimentSessionId",
column: x => x.ExperimentSessionId,
principalTable: "ExperimentSessions",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_ExperimentGradeRecords_Students_StudentId",
column: x => x.StudentId,
principalTable: "Students",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateTable(
name: "ExperimentGradeItemScores",
columns: table => new
{
ExperimentGradeRecordId = table.Column<Guid>(type: "char(36)", nullable: false),
ExperimentGradeItemId = table.Column<Guid>(type: "char(36)", nullable: false),
Score = table.Column<decimal>(type: "decimal(5,1)", precision: 5, scale: 1, nullable: true),
Comment = table.Column<string>(type: "varchar(300)", maxLength: 300, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ExperimentGradeItemScores", x => new { x.ExperimentGradeRecordId, x.ExperimentGradeItemId });
table.ForeignKey(
name: "FK_ExperimentGradeItemScores_ExperimentGradeItems_ExperimentGra~",
column: x => x.ExperimentGradeItemId,
principalTable: "ExperimentGradeItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ExperimentGradeItemScores_ExperimentGradeRecords_ExperimentG~",
column: x => x.ExperimentGradeRecordId,
principalTable: "ExperimentGradeRecords",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySQL:Charset", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_ExperimentGradeItems_ExperimentGradeSheetId_SortOrder",
table: "ExperimentGradeItems",
columns: new[] { "ExperimentGradeSheetId", "SortOrder" });
migrationBuilder.CreateIndex(
name: "IX_ExperimentGradeItemScores_ExperimentGradeItemId",
table: "ExperimentGradeItemScores",
column: "ExperimentGradeItemId");
migrationBuilder.CreateIndex(
name: "IX_ExperimentGradeRecords_ExperimentGradeSheetId_StudentId",
table: "ExperimentGradeRecords",
columns: new[] { "ExperimentGradeSheetId", "StudentId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_ExperimentGradeRecords_ExperimentSessionId",
table: "ExperimentGradeRecords",
column: "ExperimentSessionId");
migrationBuilder.CreateIndex(
name: "IX_ExperimentGradeRecords_StudentId",
table: "ExperimentGradeRecords",
column: "StudentId");
migrationBuilder.CreateIndex(
name: "IX_ExperimentGradeSheets_ExperimentProjectId",
table: "ExperimentGradeSheets",
column: "ExperimentProjectId",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ExperimentGradeItemScores");
migrationBuilder.DropTable(
name: "ExperimentGradeItems");
migrationBuilder.DropTable(
name: "ExperimentGradeRecords");
migrationBuilder.DropTable(
name: "ExperimentGradeSheets");
migrationBuilder.DropColumn(
name: "SourceSnapshotAt",
table: "GradeItems");
migrationBuilder.DropColumn(
name: "SourceType",
table: "GradeItems");
}
}
}
@@ -2043,6 +2043,176 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.ToTable("ExperimentBookings");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("ExperimentGradeSheetId")
.HasColumnType("char(36)");
b.Property<int>("Kind")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(60)
.HasColumnType("varchar(60)");
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<decimal>("Weight")
.HasPrecision(5, 1)
.HasColumnType("decimal(5,1)");
b.HasKey("Id");
b.HasIndex("ExperimentGradeSheetId", "SortOrder");
b.ToTable("ExperimentGradeItems");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b =>
{
b.Property<Guid>("ExperimentGradeRecordId")
.HasColumnType("char(36)");
b.Property<Guid>("ExperimentGradeItemId")
.HasColumnType("char(36)");
b.Property<string>("Comment")
.HasMaxLength(300)
.HasColumnType("varchar(300)");
b.Property<decimal?>("Score")
.HasPrecision(5, 1)
.HasColumnType("decimal(5,1)");
b.HasKey("ExperimentGradeRecordId", "ExperimentGradeItemId");
b.HasIndex("ExperimentGradeItemId");
b.ToTable("ExperimentGradeItemScores");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<int>("AttemptNumber")
.HasColumnType("int");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("ExperimentGradeSheetId")
.HasColumnType("char(36)");
b.Property<Guid?>("ExperimentSessionId")
.HasColumnType("char(36)");
b.Property<bool>("IsLate")
.HasColumnType("tinyint(1)");
b.Property<bool?>("IsPassed")
.HasColumnType("tinyint(1)");
b.Property<int>("ParticipationStatus")
.HasColumnType("int");
b.Property<bool>("SafetyViolation")
.HasColumnType("tinyint(1)");
b.Property<Guid>("StudentId")
.HasColumnType("char(36)");
b.Property<string>("SubmissionReference")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime?>("SubmittedAt")
.HasColumnType("datetime(6)");
b.Property<string>("TeacherComment")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<decimal?>("TotalScore")
.HasPrecision(5, 1)
.HasColumnType("decimal(5,1)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ExperimentSessionId");
b.HasIndex("StudentId");
b.HasIndex("ExperimentGradeSheetId", "StudentId")
.IsUnique();
b.ToTable("ExperimentGradeRecords");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<decimal>("ContributionWeight")
.HasPrecision(5, 1)
.HasColumnType("decimal(5,1)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("ExperimentProjectId")
.HasColumnType("char(36)");
b.Property<decimal>("PassScore")
.HasPrecision(5, 1)
.HasColumnType("decimal(5,1)");
b.Property<DateTime?>("PublishedAt")
.HasColumnType("datetime(6)");
b.Property<string>("ReviewComment")
.HasMaxLength(500)
.HasColumnType("varchar(500)");
b.Property<DateTime?>("ReviewedAt")
.HasColumnType("datetime(6)");
b.Property<int>("Status")
.HasColumnType("int");
b.Property<DateTime?>("SubmittedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("ExperimentProjectId")
.IsUnique();
b.ToTable("ExperimentGradeSheets");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b =>
{
b.Property<Guid>("Id")
@@ -2176,6 +2346,14 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Property<int>("SortOrder")
.HasColumnType("int");
b.Property<DateTime?>("SourceSnapshotAt")
.HasColumnType("datetime(6)");
b.Property<int>("SourceType")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
@@ -4812,6 +4990,73 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("Student");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet")
.WithMany("Items")
.HasForeignKey("ExperimentGradeSheetId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExperimentGradeSheet");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItemScore", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", "ExperimentGradeItem")
.WithMany("Scores")
.HasForeignKey("ExperimentGradeItemId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", "ExperimentGradeRecord")
.WithMany("ItemScores")
.HasForeignKey("ExperimentGradeRecordId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExperimentGradeItem");
b.Navigation("ExperimentGradeRecord");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentGradeSheet")
.WithMany("Records")
.HasForeignKey("ExperimentGradeSheetId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentSession", "ExperimentSession")
.WithMany()
.HasForeignKey("ExperimentSessionId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("Jiaowu.Api.Domain.Academic.Student", "Student")
.WithMany()
.HasForeignKey("StudentId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("ExperimentGradeSheet");
b.Navigation("ExperimentSession");
b.Navigation("Student");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.ExperimentProject", "ExperimentProject")
.WithOne("GradeSheet")
.HasForeignKey("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", "ExperimentProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ExperimentProject");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b =>
{
b.HasOne("Jiaowu.Api.Domain.Academic.TeachingTask", "TeachingTask")
@@ -5550,10 +5795,29 @@ namespace Jiaowu.Api.Infrastructure.Persistence.Migrations.MySql
b.Navigation("SeatAssignments");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeItem", b =>
{
b.Navigation("Scores");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeRecord", b =>
{
b.Navigation("ItemScores");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentGradeSheet", b =>
{
b.Navigation("Items");
b.Navigation("Records");
});
modelBuilder.Entity("Jiaowu.Api.Domain.Academic.ExperimentProject", b =>
{
b.Navigation("Bookings");
b.Navigation("GradeSheet");
b.Navigation("Sessions");
});