已按“实验成绩独立管理”完成修改。
每个实验项目独立建成绩单,支持评分项、参与状态、补做次数、安全违规、报告地址和教师评语。 流程为:录入 → 学院审核 → 教务发布;学生只能查看已发布成绩。 自主预约实验支持同步有效预约名单,已评分记录不会被误删。 课程成绩可将已发布实验成绩汇总为快照导入指定分项;导入后锁定,不能手工篡改。 实验项目成绩不完整时,对应课程分项保持空白,不按 0 分处理。 一级“实验管理”菜单下增加“项目与场次”“实验成绩”;学生端增加“实验安排”“实验成绩”。 已生成 MySQL 正式迁移及 SQLite 开发迁移,尚未操作生产数据库。 界面采用“实验项目 → 独立评分 → 审核发布”工作台设计。
This commit is contained in:
@@ -0,0 +1,945 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jiaowu.Api.Domain.Academic;
|
||||
using Jiaowu.Api.Domain.Identity;
|
||||
using Jiaowu.Api.Infrastructure.Auth;
|
||||
using Jiaowu.Api.Infrastructure.Grades;
|
||||
using Jiaowu.Api.Infrastructure.Persistence;
|
||||
using Jiaowu.Api.Infrastructure.Teaching;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jiaowu.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/experiment-grades")]
|
||||
public sealed class ExperimentGradesController(
|
||||
AppDbContext db,
|
||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
||||
{
|
||||
private const string Managers =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
SystemRoles.AcademicAdmin + "," +
|
||||
SystemRoles.CollegeAdmin + "," +
|
||||
SystemRoles.Teacher;
|
||||
|
||||
private const string Reviewers =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
SystemRoles.AcademicAdmin + "," +
|
||||
SystemRoles.CollegeAdmin;
|
||||
|
||||
private const string Publishers =
|
||||
SystemRoles.SuperAdmin + "," +
|
||||
SystemRoles.AcademicAdmin;
|
||||
|
||||
[HttpGet("management")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> GetManagement(
|
||||
Guid? academicTermId,
|
||||
ExperimentGradeSheetStatus? status,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var source = ScopedProjects().AsNoTracking()
|
||||
.Where(x =>
|
||||
x.Status == ExperimentProjectStatus.Published ||
|
||||
x.Status == ExperimentProjectStatus.Closed);
|
||||
if (academicTermId.HasValue)
|
||||
source = source.Where(x =>
|
||||
x.TeachingTask!.AcademicTermId == academicTermId);
|
||||
if (status.HasValue)
|
||||
source = source.Where(x =>
|
||||
x.GradeSheet != null &&
|
||||
x.GradeSheet.Status == status.Value);
|
||||
|
||||
return Ok(await source
|
||||
.OrderByDescending(x => x.TeachingTask!.AcademicTerm!.StartDate)
|
||||
.ThenBy(x => x.TeachingTask!.TaskNumber)
|
||||
.ThenBy(x => x.Code)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.Code,
|
||||
x.Name,
|
||||
x.ArrangementMode,
|
||||
ProjectStatus = x.Status,
|
||||
x.TeachingTaskId,
|
||||
x.TeachingTask!.TaskNumber,
|
||||
TaskName = x.TeachingTask.Name,
|
||||
AcademicTermId = x.TeachingTask.AcademicTermId,
|
||||
TermName = x.TeachingTask.AcademicTerm!.Name,
|
||||
CourseCode = x.TeachingTask.Course!.Code,
|
||||
CourseName = x.TeachingTask.Course.Name,
|
||||
CollegeName = x.TeachingTask.Course.College!.Name,
|
||||
TeacherNames = x.TeachingTask.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
Sheet = x.GradeSheet == null
|
||||
? null
|
||||
: new
|
||||
{
|
||||
x.GradeSheet.Id,
|
||||
x.GradeSheet.Status,
|
||||
x.GradeSheet.ContributionWeight,
|
||||
x.GradeSheet.PassScore,
|
||||
ItemCount = x.GradeSheet.Items.Count,
|
||||
StudentCount = x.GradeSheet.Records.Count,
|
||||
CompletedCount = x.GradeSheet.Records.Count(record =>
|
||||
record.ParticipationStatus !=
|
||||
ExperimentParticipationStatus.Pending),
|
||||
ScoredCount = x.GradeSheet.Records.Count(record =>
|
||||
record.TotalScore != null),
|
||||
PassedCount = x.GradeSheet.Records.Count(record =>
|
||||
record.IsPassed == true),
|
||||
x.GradeSheet.SubmittedAt,
|
||||
x.GradeSheet.ReviewedAt,
|
||||
x.GradeSheet.PublishedAt
|
||||
}
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("mine")]
|
||||
[Authorize(Roles = SystemRoles.Student)]
|
||||
public async Task<ActionResult> GetMine(CancellationToken cancellationToken)
|
||||
{
|
||||
var student = await CurrentStudentAsync(cancellationToken);
|
||||
if (student is null)
|
||||
return ConflictProblem("当前账号未关联有效学生档案。");
|
||||
|
||||
return Ok(await db.ExperimentGradeRecords.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.StudentId == student.Id &&
|
||||
x.ExperimentGradeSheet!.Status ==
|
||||
ExperimentGradeSheetStatus.Published)
|
||||
.OrderByDescending(x =>
|
||||
x.ExperimentGradeSheet!.PublishedAt)
|
||||
.ThenBy(x =>
|
||||
x.ExperimentGradeSheet!.ExperimentProject!.Code)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.ExperimentGradeSheetId,
|
||||
ProjectId = x.ExperimentGradeSheet!.ExperimentProjectId,
|
||||
ProjectCode =
|
||||
x.ExperimentGradeSheet.ExperimentProject!.Code,
|
||||
ProjectName =
|
||||
x.ExperimentGradeSheet.ExperimentProject.Name,
|
||||
x.ExperimentGradeSheet.ExperimentProject.ArrangementMode,
|
||||
CourseCode = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask!.Course!.Code,
|
||||
CourseName = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask.Course.Name,
|
||||
TermName = x.ExperimentGradeSheet.ExperimentProject
|
||||
.TeachingTask.AcademicTerm!.Name,
|
||||
x.ExperimentGradeSheet.PassScore,
|
||||
x.ParticipationStatus,
|
||||
x.TotalScore,
|
||||
x.IsPassed,
|
||||
x.SafetyViolation,
|
||||
x.AttemptNumber,
|
||||
x.SubmissionReference,
|
||||
x.SubmittedAt,
|
||||
x.IsLate,
|
||||
x.TeacherComment,
|
||||
Items = x.ItemScores
|
||||
.OrderBy(score =>
|
||||
score.ExperimentGradeItem!.SortOrder)
|
||||
.Select(score => new
|
||||
{
|
||||
score.ExperimentGradeItemId,
|
||||
score.ExperimentGradeItem!.Name,
|
||||
score.ExperimentGradeItem.Kind,
|
||||
score.ExperimentGradeItem.Weight,
|
||||
score.Score,
|
||||
score.Comment
|
||||
}),
|
||||
x.ExperimentGradeSheet.PublishedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("sheets")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> CreateSheet(
|
||||
ExperimentGradeSheetRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var items = BuildItems(request.Items);
|
||||
if (!ExperimentGradeCalculator.AreWeightsValid(
|
||||
request.ContributionWeight,
|
||||
request.PassScore,
|
||||
items))
|
||||
return ValidationProblem(
|
||||
"实验评分项至少设置一项、权重合计 100%,项目贡献权重应大于 0。");
|
||||
|
||||
var project = await ScopedProjects()
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.FirstOrDefaultAsync(x =>
|
||||
x.Id == request.ExperimentProjectId,
|
||||
cancellationToken);
|
||||
if (project is null) return NotFound();
|
||||
if (project.Status is not (
|
||||
ExperimentProjectStatus.Published or
|
||||
ExperimentProjectStatus.Closed))
|
||||
return ConflictProblem("只有已发布或已关闭实验项目可以建立成绩单。");
|
||||
if (!CanInitialize(project.TeachingTask!)) return Forbid();
|
||||
if (await db.ExperimentGradeSheets.AnyAsync(x =>
|
||||
x.ExperimentProjectId == project.Id,
|
||||
cancellationToken))
|
||||
return ConflictProblem("该实验项目已经建立成绩单。");
|
||||
|
||||
var participants = await LoadParticipantsAsync(
|
||||
project,
|
||||
cancellationToken);
|
||||
if (participants.Count == 0)
|
||||
return ConflictProblem("实验项目当前没有可评分学生。");
|
||||
|
||||
var sheet = new ExperimentGradeSheet
|
||||
{
|
||||
ExperimentProjectId = project.Id,
|
||||
ContributionWeight = request.ContributionWeight,
|
||||
PassScore = request.PassScore,
|
||||
Items = items,
|
||||
Records = participants.Select(participant =>
|
||||
new ExperimentGradeRecord
|
||||
{
|
||||
StudentId = participant.StudentId,
|
||||
ExperimentSessionId = participant.SessionId,
|
||||
ItemScores = items.Select(item =>
|
||||
new ExperimentGradeItemScore
|
||||
{
|
||||
ExperimentGradeItemId = item.Id
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
db.ExperimentGradeSheets.Add(sheet);
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Created(string.Empty, new { sheet.Id });
|
||||
}
|
||||
|
||||
[HttpGet("sheets/{id:guid}")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> GetSheet(
|
||||
Guid id,
|
||||
int recordPage = 1,
|
||||
int recordPageSize = 50,
|
||||
string? studentKeyword = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
recordPage = Math.Max(1, recordPage);
|
||||
recordPageSize = Math.Clamp(recordPageSize, 10, 100);
|
||||
studentKeyword = string.IsNullOrWhiteSpace(studentKeyword)
|
||||
? null
|
||||
: studentKeyword.Trim();
|
||||
|
||||
var sheet = await ScopedSheets().AsNoTracking()
|
||||
.Where(x => x.Id == id)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.ExperimentProjectId,
|
||||
ProjectCode = x.ExperimentProject!.Code,
|
||||
ProjectName = x.ExperimentProject.Name,
|
||||
x.ExperimentProject.ArrangementMode,
|
||||
x.ExperimentProject.TeachingTaskId,
|
||||
x.ExperimentProject.TeachingTask!.TaskNumber,
|
||||
TaskName = x.ExperimentProject.TeachingTask.Name,
|
||||
AcademicTermId =
|
||||
x.ExperimentProject.TeachingTask.AcademicTermId,
|
||||
TermName = x.ExperimentProject.TeachingTask
|
||||
.AcademicTerm!.Name,
|
||||
CourseCollegeId = x.ExperimentProject.TeachingTask
|
||||
.Course!.CollegeId,
|
||||
CourseCollegeName = x.ExperimentProject.TeachingTask
|
||||
.Course.College!.Name,
|
||||
CourseCode =
|
||||
x.ExperimentProject.TeachingTask.Course.Code,
|
||||
CourseName =
|
||||
x.ExperimentProject.TeachingTask.Course.Name,
|
||||
TeacherNames = x.ExperimentProject.TeachingTask.Teachers
|
||||
.OrderByDescending(item => item.IsPrimary)
|
||||
.Select(item => item.Teacher!.Name),
|
||||
x.ContributionWeight,
|
||||
x.PassScore,
|
||||
Items = x.Items.OrderBy(item => item.SortOrder)
|
||||
.Select(item => new
|
||||
{
|
||||
item.Id,
|
||||
item.Name,
|
||||
item.Kind,
|
||||
item.Weight
|
||||
}),
|
||||
x.Status,
|
||||
x.ReviewComment,
|
||||
x.SubmittedAt,
|
||||
x.ReviewedAt,
|
||||
x.PublishedAt,
|
||||
x.CreatedAt,
|
||||
x.UpdatedAt
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
|
||||
var recordsSource = db.ExperimentGradeRecords.AsNoTracking()
|
||||
.Where(x => x.ExperimentGradeSheetId == id);
|
||||
if (studentKeyword is not null)
|
||||
recordsSource = recordsSource.Where(x =>
|
||||
x.Student!.StudentNumber.Contains(studentKeyword) ||
|
||||
x.Student.Name.Contains(studentKeyword) ||
|
||||
x.Student.AdministrativeClass!.Name.Contains(studentKeyword));
|
||||
var recordTotal = await recordsSource.CountAsync(cancellationToken);
|
||||
var records = await recordsSource
|
||||
.OrderBy(x => x.Student!.StudentNumber)
|
||||
.Skip((recordPage - 1) * recordPageSize)
|
||||
.Take(recordPageSize)
|
||||
.Select(x => new
|
||||
{
|
||||
x.Id,
|
||||
x.StudentId,
|
||||
x.Student!.StudentNumber,
|
||||
x.Student.Name,
|
||||
ClassName = x.Student.AdministrativeClass!.Name,
|
||||
x.ExperimentSessionId,
|
||||
Session = x.ExperimentSession == null
|
||||
? null
|
||||
: new
|
||||
{
|
||||
x.ExperimentSession.SessionDate,
|
||||
x.ExperimentSession.StartPeriod,
|
||||
x.ExperimentSession.PeriodCount,
|
||||
ClassroomName =
|
||||
x.ExperimentSession.Classroom!.Name
|
||||
},
|
||||
x.ParticipationStatus,
|
||||
x.TotalScore,
|
||||
x.IsPassed,
|
||||
x.SafetyViolation,
|
||||
x.AttemptNumber,
|
||||
x.SubmissionReference,
|
||||
x.SubmittedAt,
|
||||
x.IsLate,
|
||||
x.TeacherComment,
|
||||
ItemScores = x.ItemScores
|
||||
.OrderBy(score =>
|
||||
score.ExperimentGradeItem!.SortOrder)
|
||||
.Select(score => new
|
||||
{
|
||||
score.ExperimentGradeItemId,
|
||||
score.ExperimentGradeItem!.Name,
|
||||
score.Score,
|
||||
score.Comment
|
||||
}),
|
||||
x.UpdatedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var task = await db.TeachingTasks.AsNoTracking()
|
||||
.Include(x => x.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.SingleAsync(
|
||||
x => x.Id == sheet.TeachingTaskId,
|
||||
cancellationToken);
|
||||
var canReview = CanReviewCollege(sheet.CourseCollegeId) &&
|
||||
sheet.Status ==
|
||||
ExperimentGradeSheetStatus.Submitted;
|
||||
return Ok(new
|
||||
{
|
||||
Sheet = new
|
||||
{
|
||||
sheet.Id,
|
||||
sheet.ExperimentProjectId,
|
||||
sheet.ProjectCode,
|
||||
sheet.ProjectName,
|
||||
sheet.ArrangementMode,
|
||||
sheet.TeachingTaskId,
|
||||
sheet.TaskNumber,
|
||||
sheet.TaskName,
|
||||
sheet.AcademicTermId,
|
||||
sheet.TermName,
|
||||
sheet.CourseCollegeName,
|
||||
sheet.CourseCode,
|
||||
sheet.CourseName,
|
||||
sheet.TeacherNames,
|
||||
sheet.ContributionWeight,
|
||||
sheet.PassScore,
|
||||
sheet.Items,
|
||||
sheet.Status,
|
||||
sheet.ReviewComment,
|
||||
sheet.SubmittedAt,
|
||||
sheet.ReviewedAt,
|
||||
sheet.PublishedAt,
|
||||
RecordTotal = recordTotal,
|
||||
RecordPage = recordPage,
|
||||
RecordPageSize = recordPageSize,
|
||||
Records = records,
|
||||
sheet.CreatedAt,
|
||||
sheet.UpdatedAt
|
||||
},
|
||||
CanEdit = CanEdit(task) &&
|
||||
sheet.Status is
|
||||
ExperimentGradeSheetStatus.Draft or
|
||||
ExperimentGradeSheetStatus.Returned,
|
||||
CanReview = canReview,
|
||||
CanPublish = IsPublisher() &&
|
||||
sheet.Status == ExperimentGradeSheetStatus.Approved
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("sheets/{id:guid}/settings")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> UpdateSettings(
|
||||
Guid id,
|
||||
ExperimentGradeSettingsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var replacementItems = BuildItems(request.Items);
|
||||
if (!ExperimentGradeCalculator.AreWeightsValid(
|
||||
request.ContributionWeight,
|
||||
request.PassScore,
|
||||
replacementItems))
|
||||
return ValidationProblem(
|
||||
"实验评分项至少设置一项、权重合计 100%,项目贡献权重应大于 0。");
|
||||
|
||||
var sheet = await ScopedSheets()
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Records)
|
||||
.ThenInclude(x => x.ItemScores)
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid();
|
||||
if (sheet.Status is not (
|
||||
ExperimentGradeSheetStatus.Draft or
|
||||
ExperimentGradeSheetStatus.Returned))
|
||||
return ConflictProblem("当前状态不能修改实验评分方案。");
|
||||
|
||||
sheet.ContributionWeight = request.ContributionWeight;
|
||||
sheet.PassScore = request.PassScore;
|
||||
db.ExperimentGradeItems.RemoveRange(sheet.Items);
|
||||
foreach (var record in sheet.Records)
|
||||
{
|
||||
record.ItemScores.Clear();
|
||||
record.TotalScore = null;
|
||||
record.IsPassed = null;
|
||||
}
|
||||
sheet.Items = replacementItems;
|
||||
foreach (var record in sheet.Records)
|
||||
foreach (var item in replacementItems)
|
||||
record.ItemScores.Add(new ExperimentGradeItemScore
|
||||
{
|
||||
ExperimentGradeItemId = item.Id
|
||||
});
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("sheets/{id:guid}/records")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> UpdateRecords(
|
||||
Guid id,
|
||||
ExperimentGradeRecordsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await ScopedSheets()
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Records)
|
||||
.ThenInclude(x => x.ItemScores)
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid();
|
||||
if (sheet.Status is not (
|
||||
ExperimentGradeSheetStatus.Draft or
|
||||
ExperimentGradeSheetStatus.Returned))
|
||||
return ConflictProblem("成绩单提交后不能继续修改。");
|
||||
|
||||
var records = sheet.Records.ToDictionary(x => x.Id);
|
||||
if (request.Records.Select(x => x.Id).Distinct().Count() !=
|
||||
request.Records.Count ||
|
||||
request.Records.Any(x => !records.ContainsKey(x.Id)))
|
||||
return ValidationProblem("包含无效或重复的实验成绩记录。");
|
||||
var itemIds = sheet.Items.Select(x => x.Id).ToHashSet();
|
||||
foreach (var item in request.Records)
|
||||
{
|
||||
if (item.AttemptNumber is < 1 or > 20)
|
||||
return ValidationProblem("实验尝试次数应在 1—20 次之间。");
|
||||
if (item.ItemScores.Select(x => x.ExperimentGradeItemId)
|
||||
.Distinct().Count() != item.ItemScores.Count ||
|
||||
item.ItemScores.Any(x =>
|
||||
!itemIds.Contains(x.ExperimentGradeItemId) ||
|
||||
!ValidScore(x.Score)))
|
||||
return ValidationProblem("包含无效、重复或超出 0—100 分的评分项。");
|
||||
|
||||
var record = records[item.Id];
|
||||
record.ParticipationStatus = item.ParticipationStatus;
|
||||
record.SafetyViolation = item.SafetyViolation;
|
||||
record.AttemptNumber = item.AttemptNumber;
|
||||
record.SubmissionReference =
|
||||
Normalize(item.SubmissionReference);
|
||||
record.SubmittedAt = item.SubmittedAt;
|
||||
record.IsLate = item.IsLate;
|
||||
record.TeacherComment = Normalize(item.TeacherComment);
|
||||
var scores = record.ItemScores.ToDictionary(
|
||||
x => x.ExperimentGradeItemId);
|
||||
foreach (var score in item.ItemScores)
|
||||
{
|
||||
var target = scores[score.ExperimentGradeItemId];
|
||||
target.Score = score.Score;
|
||||
target.Comment = Normalize(score.Comment);
|
||||
}
|
||||
Recalculate(sheet, record);
|
||||
}
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/sync-participants")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> SyncParticipants(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await ScopedSheets()
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Records)
|
||||
.ThenInclude(x => x.ItemScores)
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid();
|
||||
if (sheet.Status is not (
|
||||
ExperimentGradeSheetStatus.Draft or
|
||||
ExperimentGradeSheetStatus.Returned))
|
||||
return ConflictProblem("成绩单提交后不能同步参与学生。");
|
||||
|
||||
var expected = await LoadParticipantsAsync(
|
||||
sheet.ExperimentProject,
|
||||
cancellationToken);
|
||||
var expectedByStudent = expected.ToDictionary(x => x.StudentId);
|
||||
var existingByStudent = sheet.Records.ToDictionary(x => x.StudentId);
|
||||
var added = 0;
|
||||
var removed = 0;
|
||||
var retained = 0;
|
||||
var recordsToRemove = new List<ExperimentGradeRecord>();
|
||||
var recordsToAdd = new List<ExperimentGradeRecord>();
|
||||
|
||||
foreach (var participant in expected)
|
||||
{
|
||||
if (existingByStudent.TryGetValue(
|
||||
participant.StudentId,
|
||||
out var record))
|
||||
{
|
||||
if (record.ParticipationStatus ==
|
||||
ExperimentParticipationStatus.Pending &&
|
||||
record.ItemScores.All(x => !x.Score.HasValue))
|
||||
record.ExperimentSessionId = participant.SessionId;
|
||||
retained++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var newRecord = new ExperimentGradeRecord
|
||||
{
|
||||
ExperimentGradeSheetId = sheet.Id,
|
||||
StudentId = participant.StudentId,
|
||||
ExperimentSessionId = participant.SessionId
|
||||
};
|
||||
db.ExperimentGradeRecords.Add(newRecord);
|
||||
recordsToAdd.Add(newRecord);
|
||||
added++;
|
||||
}
|
||||
|
||||
foreach (var record in sheet.Records
|
||||
.Where(x => !expectedByStudent.ContainsKey(x.StudentId))
|
||||
.ToList())
|
||||
{
|
||||
if (record.ParticipationStatus !=
|
||||
ExperimentParticipationStatus.Pending ||
|
||||
record.ItemScores.Any(x => x.Score.HasValue))
|
||||
{
|
||||
retained++;
|
||||
continue;
|
||||
}
|
||||
|
||||
db.ExperimentGradeItemScores.RemoveRange(record.ItemScores);
|
||||
recordsToRemove.Add(record);
|
||||
removed++;
|
||||
}
|
||||
|
||||
if (recordsToRemove.Count > 0 || recordsToAdd.Count > 0)
|
||||
{
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
foreach (var record in recordsToRemove)
|
||||
sheet.Records.Remove(record);
|
||||
db.ExperimentGradeRecords.RemoveRange(recordsToRemove);
|
||||
}
|
||||
foreach (var record in recordsToAdd)
|
||||
foreach (var item in sheet.Items)
|
||||
record.ItemScores.Add(new ExperimentGradeItemScore
|
||||
{
|
||||
ExperimentGradeItemId = item.Id
|
||||
});
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new
|
||||
{
|
||||
Added = added,
|
||||
Removed = removed,
|
||||
Retained = retained,
|
||||
Total = await db.ExperimentGradeRecords.CountAsync(
|
||||
x => x.ExperimentGradeSheetId == sheet.Id,
|
||||
cancellationToken)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/submit")]
|
||||
[Authorize(Roles = Managers)]
|
||||
public async Task<ActionResult> Submit(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
if (!CanEdit(sheet.ExperimentProject!.TeachingTask!)) return Forbid();
|
||||
if (sheet.Status is not (
|
||||
ExperimentGradeSheetStatus.Draft or
|
||||
ExperimentGradeSheetStatus.Returned))
|
||||
return ConflictProblem("只有草稿或已退回实验成绩单可以提交。");
|
||||
if (sheet.Records.Count == 0)
|
||||
return ConflictProblem("实验成绩单没有学生记录。");
|
||||
|
||||
foreach (var record in sheet.Records) Recalculate(sheet, record);
|
||||
if (sheet.Records.Any(x =>
|
||||
x.ParticipationStatus ==
|
||||
ExperimentParticipationStatus.Pending))
|
||||
return ConflictProblem("仍有学生未登记实验参与状态。");
|
||||
if (sheet.Records.Any(x =>
|
||||
x.ParticipationStatus is
|
||||
ExperimentParticipationStatus.Completed or
|
||||
ExperimentParticipationStatus.Makeup &&
|
||||
!x.TotalScore.HasValue))
|
||||
return ConflictProblem("仍有已完成或补做学生缺少必填评分项。");
|
||||
|
||||
sheet.Status = ExperimentGradeSheetStatus.Submitted;
|
||||
sheet.SubmittedAt = DateTime.UtcNow;
|
||||
sheet.ReviewComment = null;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await NotificationService.SendToRoleAsync(
|
||||
db,
|
||||
SystemRoles.CollegeAdmin,
|
||||
"实验成绩待审核",
|
||||
$"“{sheet.ExperimentProject!.Name}”实验成绩已提交,请及时审核。",
|
||||
sheet.ExperimentProject.TeachingTask!.Course!.CollegeId,
|
||||
"/experiment-grades",
|
||||
cancellationToken,
|
||||
NotificationCategory.Grade);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/approve")]
|
||||
[Authorize(Roles = Reviewers)]
|
||||
public async Task<ActionResult> Approve(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
var collegeId = sheet.ExperimentProject!.TeachingTask!.Course!.CollegeId;
|
||||
if (!CanReviewCollege(collegeId)) return Forbid();
|
||||
if (sheet.Status != ExperimentGradeSheetStatus.Submitted)
|
||||
return ConflictProblem("只有待审核实验成绩单可以通过审核。");
|
||||
|
||||
sheet.Status = ExperimentGradeSheetStatus.Approved;
|
||||
sheet.ReviewedAt = DateTime.UtcNow;
|
||||
sheet.ReviewComment = null;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await NotifyTeachersAsync(
|
||||
sheet.ExperimentProject.TeachingTaskId,
|
||||
"实验成绩审核通过",
|
||||
$"“{sheet.ExperimentProject.Name}”实验成绩已通过学院审核。",
|
||||
cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/return")]
|
||||
[Authorize(Roles = Reviewers)]
|
||||
public async Task<ActionResult> Return(
|
||||
Guid id,
|
||||
ExperimentGradeReviewRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
var collegeId = sheet.ExperimentProject!.TeachingTask!.Course!.CollegeId;
|
||||
if (!CanReviewCollege(collegeId)) return Forbid();
|
||||
if (sheet.Status != ExperimentGradeSheetStatus.Submitted)
|
||||
return ConflictProblem("只有待审核实验成绩单可以退回。");
|
||||
if (string.IsNullOrWhiteSpace(request.Comment))
|
||||
return ValidationProblem("退回时必须填写修改意见。");
|
||||
|
||||
sheet.Status = ExperimentGradeSheetStatus.Returned;
|
||||
sheet.ReviewedAt = DateTime.UtcNow;
|
||||
sheet.ReviewComment = request.Comment.Trim();
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
await NotifyTeachersAsync(
|
||||
sheet.ExperimentProject.TeachingTaskId,
|
||||
"实验成绩被退回",
|
||||
$"“{sheet.ExperimentProject.Name}”实验成绩被退回:{sheet.ReviewComment}",
|
||||
cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/publish")]
|
||||
[Authorize(Roles = Publishers)]
|
||||
public async Task<ActionResult> Publish(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await LoadForWorkflowAsync(id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
if (!IsPublisher()) return Forbid();
|
||||
if (sheet.Status != ExperimentGradeSheetStatus.Approved)
|
||||
return ConflictProblem("只有审核通过的实验成绩单可以发布。");
|
||||
|
||||
sheet.Status = ExperimentGradeSheetStatus.Published;
|
||||
sheet.PublishedAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
var userIds = await db.ExperimentGradeRecords
|
||||
.Where(x => x.ExperimentGradeSheetId == sheet.Id)
|
||||
.Select(x => x.Student!.UserId)
|
||||
.Where(x => x != null)
|
||||
.Select(x => x!.Value)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
await NotificationService.SendToUserIdsAsync(
|
||||
db,
|
||||
userIds,
|
||||
"实验成绩已发布",
|
||||
$"“{sheet.ExperimentProject!.Name}”实验成绩已发布。",
|
||||
"/experiment-grades",
|
||||
cancellationToken,
|
||||
NotificationCategory.Grade);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private async Task<ExperimentGradeSheet?> LoadForWorkflowAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await ScopedSheets().AnyAsync(
|
||||
x => x.Id == id,
|
||||
cancellationToken))
|
||||
return null;
|
||||
return await db.ExperimentGradeSheets
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Records)
|
||||
.ThenInclude(x => x.ItemScores)
|
||||
.Include(x => x.ExperimentProject)
|
||||
.ThenInclude(x => x!.TeachingTask)
|
||||
.ThenInclude(x => x!.Course)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<List<ExperimentParticipantSeed>> LoadParticipantsAsync(
|
||||
ExperimentProject project,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (project.ArrangementMode ==
|
||||
ExperimentArrangementMode.Centralized)
|
||||
{
|
||||
return await TeachingTaskRosterQuery
|
||||
.ForTask(db, project.TeachingTaskId)
|
||||
.AsNoTracking()
|
||||
.OrderBy(x => x.StudentNumber)
|
||||
.Select(x => new ExperimentParticipantSeed(x.Id, null))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return await db.ExperimentBookings.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.ExperimentProjectId == project.Id &&
|
||||
x.Status == ExperimentBookingStatus.Booked &&
|
||||
x.Student!.Status == StudentStatus.Active)
|
||||
.OrderBy(x => x.Student!.StudentNumber)
|
||||
.Select(x => new ExperimentParticipantSeed(
|
||||
x.StudentId,
|
||||
x.ExperimentSessionId))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static List<ExperimentGradeItem> BuildItems(
|
||||
IReadOnlyCollection<ExperimentGradeItemRequest>? requests) =>
|
||||
requests?.Select((item, index) => new ExperimentGradeItem
|
||||
{
|
||||
Name = item.Name.Trim(),
|
||||
Kind = item.Kind,
|
||||
Weight = item.Weight,
|
||||
SortOrder = index
|
||||
}).ToList() ?? [];
|
||||
|
||||
private static void Recalculate(
|
||||
ExperimentGradeSheet sheet,
|
||||
ExperimentGradeRecord record)
|
||||
{
|
||||
record.TotalScore = ExperimentGradeCalculator.CalculateTotal(
|
||||
record,
|
||||
sheet.Items.ToList());
|
||||
record.IsPassed = record.TotalScore.HasValue
|
||||
? record.TotalScore.Value >= sheet.PassScore &&
|
||||
!record.SafetyViolation
|
||||
: null;
|
||||
}
|
||||
|
||||
private IQueryable<TeachingTask> AccessibleTeachingTasks()
|
||||
{
|
||||
var source = db.TeachingTasks.AsQueryable();
|
||||
var scope = currentUserDataScope.Current;
|
||||
if (scope.Scope == DataScope.All) return source;
|
||||
if (scope.Scope == DataScope.College)
|
||||
return source.Where(x =>
|
||||
x.Course!.CollegeId == scope.RestrictedCollegeId);
|
||||
if (scope.IsInRole(SystemRoles.Teacher))
|
||||
return source.Where(x => x.Teachers.Any(item =>
|
||||
item.Teacher!.UserId == scope.UserId));
|
||||
return source.Where(_ => false);
|
||||
}
|
||||
|
||||
private IQueryable<ExperimentProject> ScopedProjects()
|
||||
{
|
||||
var taskIds = AccessibleTeachingTasks().Select(x => x.Id);
|
||||
return db.ExperimentProjects.Where(x =>
|
||||
taskIds.Contains(x.TeachingTaskId));
|
||||
}
|
||||
|
||||
private IQueryable<ExperimentGradeSheet> ScopedSheets()
|
||||
{
|
||||
var projectIds = ScopedProjects().Select(x => x.Id);
|
||||
return db.ExperimentGradeSheets.Where(x =>
|
||||
projectIds.Contains(x.ExperimentProjectId));
|
||||
}
|
||||
|
||||
private bool CanInitialize(TeachingTask task) =>
|
||||
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
|
||||
currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin) ||
|
||||
currentUserDataScope.Current.IsInRole(SystemRoles.CollegeAdmin) ||
|
||||
IsAssignedTeacher(task);
|
||||
|
||||
private bool CanEdit(TeachingTask task) =>
|
||||
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
|
||||
IsAssignedTeacher(task);
|
||||
|
||||
private bool IsAssignedTeacher(TeachingTask task) =>
|
||||
task.Teachers.Any(x =>
|
||||
x.Teacher?.UserId == currentUserDataScope.Current.UserId);
|
||||
|
||||
private bool CanReviewCollege(Guid collegeId)
|
||||
{
|
||||
var scope = currentUserDataScope.Current;
|
||||
if (!scope.IsInRole(SystemRoles.SuperAdmin) &&
|
||||
!scope.IsInRole(SystemRoles.AcademicAdmin) &&
|
||||
!scope.IsInRole(SystemRoles.CollegeAdmin))
|
||||
return false;
|
||||
return scope.Scope == DataScope.All ||
|
||||
scope.RestrictedCollegeId == collegeId;
|
||||
}
|
||||
|
||||
private bool IsPublisher() =>
|
||||
currentUserDataScope.Current.IsInRole(SystemRoles.SuperAdmin) ||
|
||||
currentUserDataScope.Current.IsInRole(SystemRoles.AcademicAdmin);
|
||||
|
||||
private Task<Student?> CurrentStudentAsync(
|
||||
CancellationToken cancellationToken) =>
|
||||
db.Students.FirstOrDefaultAsync(x =>
|
||||
x.UserId == currentUserDataScope.Current.UserId &&
|
||||
x.Status == StudentStatus.Active,
|
||||
cancellationToken);
|
||||
|
||||
private async Task NotifyTeachersAsync(
|
||||
Guid teachingTaskId,
|
||||
string title,
|
||||
string content,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userIds = await db.TeachingTaskTeachers
|
||||
.Where(x => x.TeachingTaskId == teachingTaskId)
|
||||
.Select(x => x.Teacher!.UserId)
|
||||
.Where(x => x != null)
|
||||
.Select(x => x!.Value)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
await NotificationService.SendToUserIdsAsync(
|
||||
db,
|
||||
userIds,
|
||||
title,
|
||||
content,
|
||||
"/experiment-grades",
|
||||
cancellationToken,
|
||||
NotificationCategory.Grade);
|
||||
}
|
||||
|
||||
private static bool ValidScore(decimal? score) =>
|
||||
!score.HasValue || score.Value is >= 0 and <= 100;
|
||||
|
||||
private static string? Normalize(string? value) =>
|
||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
|
||||
private ActionResult ConflictProblem(string detail) =>
|
||||
Conflict(new ProblemDetails
|
||||
{
|
||||
Title = "无法完成实验成绩操作",
|
||||
Detail = detail,
|
||||
Status = StatusCodes.Status409Conflict
|
||||
});
|
||||
|
||||
private sealed record ExperimentParticipantSeed(
|
||||
Guid StudentId,
|
||||
Guid? SessionId);
|
||||
}
|
||||
|
||||
public sealed record ExperimentGradeSheetRequest(
|
||||
Guid ExperimentProjectId,
|
||||
[Range(typeof(decimal), "0.1", "100")] decimal ContributionWeight,
|
||||
[Range(typeof(decimal), "0", "100")] decimal PassScore,
|
||||
IReadOnlyCollection<ExperimentGradeItemRequest> Items);
|
||||
|
||||
public sealed record ExperimentGradeSettingsRequest(
|
||||
[Range(typeof(decimal), "0.1", "100")] decimal ContributionWeight,
|
||||
[Range(typeof(decimal), "0", "100")] decimal PassScore,
|
||||
IReadOnlyCollection<ExperimentGradeItemRequest> Items);
|
||||
|
||||
public sealed record ExperimentGradeItemRequest(
|
||||
[Required, MaxLength(60)] string Name,
|
||||
ExperimentGradeItemKind Kind,
|
||||
[Range(typeof(decimal), "0.1", "100")] decimal Weight);
|
||||
|
||||
public sealed record ExperimentGradeRecordsRequest(
|
||||
IReadOnlyCollection<ExperimentGradeRecordRequest> Records);
|
||||
|
||||
public sealed record ExperimentGradeRecordRequest(
|
||||
Guid Id,
|
||||
ExperimentParticipationStatus ParticipationStatus,
|
||||
bool SafetyViolation,
|
||||
[Range(1, 20)] int AttemptNumber,
|
||||
[MaxLength(500)] string? SubmissionReference,
|
||||
DateTime? SubmittedAt,
|
||||
bool IsLate,
|
||||
[MaxLength(500)] string? TeacherComment,
|
||||
IReadOnlyCollection<ExperimentGradeItemScoreRequest> ItemScores);
|
||||
|
||||
public sealed record ExperimentGradeItemScoreRequest(
|
||||
Guid ExperimentGradeItemId,
|
||||
decimal? Score,
|
||||
[MaxLength(300)] string? Comment);
|
||||
|
||||
public sealed record ExperimentGradeReviewRequest(
|
||||
[MaxLength(500)] string? Comment);
|
||||
@@ -101,7 +101,9 @@ public sealed class GradesController(
|
||||
{
|
||||
item.Id,
|
||||
item.Name,
|
||||
item.Weight
|
||||
item.Weight,
|
||||
item.SourceType,
|
||||
item.SourceSnapshotAt
|
||||
}),
|
||||
StudentCount = sheet.Records.Count,
|
||||
CompletedCount = sheet.Records.Count(record =>
|
||||
@@ -220,7 +222,9 @@ public sealed class GradesController(
|
||||
{
|
||||
item.Id,
|
||||
item.Name,
|
||||
item.Weight
|
||||
item.Weight,
|
||||
item.SourceType,
|
||||
item.SourceSnapshotAt
|
||||
}),
|
||||
x.Status,
|
||||
x.ReviewComment,
|
||||
@@ -408,7 +412,7 @@ public sealed class GradesController(
|
||||
request.Records.Any(x => !records.ContainsKey(x.Id)))
|
||||
return ValidationProblem("包含无效或重复的成绩记录。");
|
||||
|
||||
var itemIds = sheet.Items.Select(item => item.Id).ToHashSet();
|
||||
var itemsById = sheet.Items.ToDictionary(item => item.Id);
|
||||
foreach (var item in request.Records)
|
||||
{
|
||||
if (!ValidScore(item.RegularScore) ||
|
||||
@@ -428,8 +432,19 @@ public sealed class GradesController(
|
||||
{
|
||||
if (!ValidScore(scoreEntry.Score))
|
||||
return ValidationProblem("分项成绩必须在 0—100 分之间。");
|
||||
if (scoreMap.TryGetValue(scoreEntry.GradeItemId, out var existing))
|
||||
existing.Score = scoreEntry.Score;
|
||||
if (!itemsById.TryGetValue(
|
||||
scoreEntry.GradeItemId,
|
||||
out var gradeItem) ||
|
||||
!scoreMap.TryGetValue(
|
||||
scoreEntry.GradeItemId,
|
||||
out var existing))
|
||||
continue;
|
||||
if (gradeItem.SourceType ==
|
||||
GradeItemSourceType.ExperimentSummary &&
|
||||
existing.Score != scoreEntry.Score)
|
||||
return ConflictProblem(
|
||||
$"“{gradeItem.Name}”来自实验成绩快照,不能手工修改;请重新导入实验成绩。");
|
||||
existing.Score = scoreEntry.Score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,6 +453,66 @@ public sealed class GradesController(
|
||||
return await SaveAsync(id, false, cancellationToken);
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/import-experiment-scores")]
|
||||
[Authorize(Roles = SheetUsers)]
|
||||
public async Task<ActionResult> ImportExperimentScores(
|
||||
Guid id,
|
||||
ImportExperimentScoresRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sheet = await AccessibleSheets()
|
||||
.Include(x => x.Items)
|
||||
.Include(x => x.Records)
|
||||
.ThenInclude(x => x.ItemScores)
|
||||
.Include(x => x.TeachingTask)
|
||||
.ThenInclude(x => x!.Teachers)
|
||||
.ThenInclude(x => x.Teacher)
|
||||
.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
|
||||
if (sheet is null) return NotFound();
|
||||
if (!CanEditScores(sheet.TeachingTask!)) return Forbid();
|
||||
if (sheet.Status is not (
|
||||
GradeSheetStatus.Draft or
|
||||
GradeSheetStatus.Returned))
|
||||
return ConflictProblem("成绩单提交后不能导入实验成绩。");
|
||||
|
||||
var targetItem = sheet.Items.FirstOrDefault(x =>
|
||||
x.Id == request.GradeItemId);
|
||||
if (targetItem is null)
|
||||
return ValidationProblem("请选择当前成绩单中的有效分项。");
|
||||
|
||||
var aggregate = await ExperimentGradeAggregationService.CalculateAsync(
|
||||
db,
|
||||
sheet.TeachingTaskId,
|
||||
sheet.Records.Select(x => x.StudentId).ToArray(),
|
||||
cancellationToken);
|
||||
if (aggregate.PublishedProjectCount == 0)
|
||||
return ConflictProblem(
|
||||
"该教学任务尚无已发布的实验项目成绩,无法导入。");
|
||||
|
||||
var imported = 0;
|
||||
var missing = 0;
|
||||
foreach (var record in sheet.Records)
|
||||
{
|
||||
var targetScore = record.ItemScores.First(x =>
|
||||
x.GradeItemId == targetItem.Id);
|
||||
targetScore.Score = aggregate.Scores[record.StudentId];
|
||||
if (targetScore.Score.HasValue) imported++;
|
||||
else missing++;
|
||||
Recalculate(sheet, record);
|
||||
}
|
||||
targetItem.SourceType = GradeItemSourceType.ExperimentSummary;
|
||||
targetItem.SourceSnapshotAt = DateTime.UtcNow;
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
return Ok(new
|
||||
{
|
||||
aggregate.PublishedProjectCount,
|
||||
aggregate.Projects,
|
||||
ImportedCount = imported,
|
||||
MissingCount = missing,
|
||||
targetItem.SourceSnapshotAt
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("sheets/{id:guid}/submit")]
|
||||
[Authorize(Roles = SheetUsers)]
|
||||
public async Task<ActionResult> Submit(Guid id, CancellationToken cancellationToken)
|
||||
@@ -998,5 +1073,7 @@ public sealed record GradeItemScoreRequest(
|
||||
Guid GradeItemId,
|
||||
decimal? Score);
|
||||
|
||||
public sealed record ImportExperimentScoresRequest(Guid GradeItemId);
|
||||
|
||||
public sealed record GradeReviewRequest(
|
||||
[MaxLength(500)] string? Comment);
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class ExperimentProject : EntityBase
|
||||
public DateTime? ClosedAt { get; set; }
|
||||
public ICollection<ExperimentSession> Sessions { get; set; } = [];
|
||||
public ICollection<ExperimentBooking> Bookings { get; set; } = [];
|
||||
public ExperimentGradeSheet? GradeSheet { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ExperimentSession : EntityBase
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using Jiaowu.Api.Domain.Common;
|
||||
|
||||
namespace Jiaowu.Api.Domain.Academic;
|
||||
|
||||
public sealed class ExperimentGradeSheet : EntityBase
|
||||
{
|
||||
public Guid ExperimentProjectId { get; set; }
|
||||
public ExperimentProject? ExperimentProject { get; set; }
|
||||
public decimal ContributionWeight { get; set; } = 1;
|
||||
public decimal PassScore { get; set; } = 60;
|
||||
public ExperimentGradeSheetStatus Status { get; set; } =
|
||||
ExperimentGradeSheetStatus.Draft;
|
||||
public string? ReviewComment { get; set; }
|
||||
public DateTime? SubmittedAt { get; set; }
|
||||
public DateTime? ReviewedAt { get; set; }
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public ICollection<ExperimentGradeItem> Items { get; set; } = [];
|
||||
public ICollection<ExperimentGradeRecord> Records { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ExperimentGradeItem : EntityBase
|
||||
{
|
||||
public Guid ExperimentGradeSheetId { get; set; }
|
||||
public ExperimentGradeSheet? ExperimentGradeSheet { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public ExperimentGradeItemKind Kind { get; set; } =
|
||||
ExperimentGradeItemKind.Other;
|
||||
public decimal Weight { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public ICollection<ExperimentGradeItemScore> Scores { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ExperimentGradeRecord : EntityBase
|
||||
{
|
||||
public Guid ExperimentGradeSheetId { get; set; }
|
||||
public ExperimentGradeSheet? ExperimentGradeSheet { get; set; }
|
||||
public Guid StudentId { get; set; }
|
||||
public Student? Student { get; set; }
|
||||
public Guid? ExperimentSessionId { get; set; }
|
||||
public ExperimentSession? ExperimentSession { get; set; }
|
||||
public ExperimentParticipationStatus ParticipationStatus { get; set; } =
|
||||
ExperimentParticipationStatus.Pending;
|
||||
public decimal? TotalScore { get; set; }
|
||||
public bool? IsPassed { get; set; }
|
||||
public bool SafetyViolation { get; set; }
|
||||
public int AttemptNumber { get; set; } = 1;
|
||||
public string? SubmissionReference { get; set; }
|
||||
public DateTime? SubmittedAt { get; set; }
|
||||
public bool IsLate { get; set; }
|
||||
public string? TeacherComment { get; set; }
|
||||
public ICollection<ExperimentGradeItemScore> ItemScores { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class ExperimentGradeItemScore
|
||||
{
|
||||
public Guid ExperimentGradeRecordId { get; set; }
|
||||
public ExperimentGradeRecord? ExperimentGradeRecord { get; set; }
|
||||
public Guid ExperimentGradeItemId { get; set; }
|
||||
public ExperimentGradeItem? ExperimentGradeItem { get; set; }
|
||||
public decimal? Score { get; set; }
|
||||
public string? Comment { get; set; }
|
||||
}
|
||||
|
||||
public enum ExperimentGradeSheetStatus
|
||||
{
|
||||
Draft = 1,
|
||||
Submitted = 2,
|
||||
Approved = 3,
|
||||
Published = 4,
|
||||
Returned = 5
|
||||
}
|
||||
|
||||
public enum ExperimentGradeItemKind
|
||||
{
|
||||
Operation = 1,
|
||||
Report = 2,
|
||||
Result = 3,
|
||||
Defense = 4,
|
||||
Attendance = 5,
|
||||
Safety = 6,
|
||||
Other = 7
|
||||
}
|
||||
|
||||
public enum ExperimentParticipationStatus
|
||||
{
|
||||
Pending = 1,
|
||||
Completed = 2,
|
||||
Absent = 3,
|
||||
Excused = 4,
|
||||
Makeup = 5,
|
||||
Exempt = 6
|
||||
}
|
||||
@@ -24,6 +24,9 @@ public sealed class GradeItem : EntityBase
|
||||
public required string Name { get; set; }
|
||||
public decimal Weight { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public GradeItemSourceType SourceType { get; set; } =
|
||||
GradeItemSourceType.Manual;
|
||||
public DateTime? SourceSnapshotAt { get; set; }
|
||||
public ICollection<GradeItemScore> Scores { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -68,3 +71,9 @@ public enum GradeExamStatus
|
||||
Exempt = 4,
|
||||
Makeup = 5
|
||||
}
|
||||
|
||||
public enum GradeItemSourceType
|
||||
{
|
||||
Manual = 1,
|
||||
ExperimentSummary = 2
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
"""
|
||||
];
|
||||
}
|
||||
|
||||
+5905
File diff suppressed because it is too large
Load Diff
+208
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
+264
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user