feat: 添加学生提交系统功能
Some checks failed
TechAct / explore-gitea-actions (push) Failing after 30s

- 添加学生提交管理服务 (StudentSubmissionService, StudentSubmissionDetailService)
- 新增学生提交相关控制器 (StudentSubmissionController, StudentSubmissionDetailController)
- 添加学生提交数据传输对象 (StudentSubmissionDetailDto, StudentSubmissionSummaryDto)
- 新增学生提交相关页面组件 (StudentExamView, ExamDetailView, StudentCard等)
- 添加学生提交信息卡片组件 (SubmissionInfoCard, TeacherSubmissionInfoCard)
- 更新数据库迁移文件以支持提交系统
This commit is contained in:
SpecialX
2025-09-09 15:42:31 +08:00
parent 6a65281850
commit 439c8a2421
47 changed files with 5486 additions and 119 deletions

7
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": []
}

View File

@@ -48,6 +48,8 @@ namespace Entities.Contracts
[Column("score")] [Column("score")]
public float? Score { get; set; } public float? Score { get; set; }
public bool BCorrect { get; set; }
[Column("deleted")] [Column("deleted")]
public bool IsDeleted { get; set; } public bool IsDeleted { get; set; }

View File

@@ -34,7 +34,7 @@ namespace Entities.Contracts
public DateTime SubmissionTime { get; set; } public DateTime SubmissionTime { get; set; }
[Column("overall_grade")] [Column("overall_grade")]
public float? OverallGrade { get; set; } public float OverallGrade { get; set; } = 0;
[Column("overall_feedback")] [Column("overall_feedback")]
public string? OverallFeedback { get; set; } public string? OverallFeedback { get; set; }

View File

@@ -13,8 +13,9 @@ namespace Entities.DTO
public string? DisplayName { get; set; } public string? DisplayName { get; set; }
public UInt32 ErrorQuestionNum { get; set; } public UInt32 ErrorQuestionNum { get; set; }
public Dictionary<QuestionType, UInt32> ErrorQuestionTypes { get; set; } = new Dictionary<QuestionType, UInt32>(); public Dictionary<string, UInt32> ErrorQuestionTypes { get; set; } = new Dictionary<string, UInt32>();
public Dictionary<SubjectAreaEnum, UInt32> SubjectAreaErrorQuestionDis { get; set; } = new Dictionary<SubjectAreaEnum, UInt32>(); public Dictionary<SubjectAreaEnum, UInt32> SubjectAreaErrorQuestionDis { get; set; } = new Dictionary<SubjectAreaEnum, UInt32>();
public Dictionary<byte, UInt32> LessonErrorDis { get; set; } = new Dictionary<byte, UInt32>(); public Dictionary<byte, UInt32> LessonErrorDis { get; set; } = new Dictionary<byte, UInt32>();
public float Score { get; set; }
} }
} }

View File

@@ -0,0 +1,41 @@
using Entities.Contracts;
using System;
using System.Collections.Generic;
namespace Entities.DTO
{
public class StudentSubmissionDetailDto
{
// 基本信息
public Guid Id { get; set; }
public Guid AssignmentId { get; set; }
public Guid StudentId { get; set; }
public DateTime SubmissionTime { get; set; }
public float OverallGrade { get; set; }
public string OverallFeedback { get; set; } = string.Empty;
public SubmissionStatus Status { get; set; }
// Assignment信息
public AssignmentDto Assignment { get; set; } = new AssignmentDto();
// 错误分析
public Dictionary<string, int> ErrorTypeDistribution { get; set; } = new Dictionary<string, int>();
public Dictionary<string, float> ErrorTypeScoreDistribution { get; set; } = new Dictionary<string, float>();
// 成绩统计
public int TotalRank { get; set; }
public List<float> AllScores { get; set; } = new List<float>();
public float AverageScore { get; set; }
public float ClassAverageScore { get; set; }
// 课文分布
public Dictionary<string, int> LessonErrorDistribution { get; set; } = new Dictionary<string, int>();
public Dictionary<string, int> KeyPointErrorDistribution { get; set; } = new Dictionary<string, int>();
// 基础统计
public int TotalQuestions { get; set; }
public int CorrectCount { get; set; }
public int ErrorCount { get; set; }
public float AccuracyRate { get; set; }
}
}

View File

@@ -0,0 +1,22 @@
using System;
namespace Entities.DTO
{
public class StudentSubmissionSummaryDto
{
public Guid Id { get; set; }
public string AssignmentName { get; set; }
public int ErrorCount { get; set; }
public DateTime CreatedDate { get; set; }
public float Score { get; set; }
public int TotalQuestions { get; set; }
public string StudentName { get; set; }
public string Status { get; set; }
}
public class StudentSubmissionSummaryResponseDto
{
public List<StudentSubmissionSummaryDto> Submissions { get; set; }
public int TotalCount { get; set; }
}
}

View File

@@ -24,13 +24,9 @@
<MudNavMenu Bordered="true" Dense="true" Rounded="true" Color="Color.Error" Margin="Margin.Dense"> <MudNavMenu Bordered="true" Dense="true" Rounded="true" Color="Color.Error" Margin="Margin.Dense">
<ApplicationMainIconCard></ApplicationMainIconCard> <ApplicationMainIconCard></ApplicationMainIconCard>
<MudDivider Class="my-2" /> <MudDivider Class="my-2" />
<MudNavLink Href="/">Dashboard</MudNavLink> <MudNavLink Href="/">Home</MudNavLink>
<MudNavLink Href="/exam">Exam</MudNavLink> <MudNavLink Href="/exam">Exam</MudNavLink>
<MudNavLink Href="/exam">Billing</MudNavLink> <MudNavLink Href="/students">Students</MudNavLink>
<MudNavGroup Title="Settings" Expanded="true">
<MudNavLink Href="/users">Users</MudNavLink>
<MudNavLink Href="/security">Security</MudNavLink>
</MudNavGroup>
<MudSpacer /> <MudSpacer />
<MudNavLink Class="align-content-end" Href="/about">About</MudNavLink> <MudNavLink Class="align-content-end" Href="/about">About</MudNavLink>
@@ -38,7 +34,7 @@
<MudSpacer /> <MudSpacer />
<MudNavMenu Class="align-content-end " Bordered="true" Dense="true" Rounded="true" Margin="Margin.Dense"> <MudNavMenu Class="align-content-end " Bordered="true" Dense="true" Rounded="true" Margin="Margin.Dense">
<TechHelper.Client.Pages.Global.LoginInOut.LoginInOut></TechHelper.Client.Pages.Global.LoginInOut.LoginInOut> <TechHelper.Client.Pages.Global.LoginInOut.LoginInOut></TechHelper.Client.Pages.Global.LoginInOut.LoginInOut>
<MudNavLink Class="align-content-end" Href="/about">Setting</MudNavLink> <MudNavLink Class="align-content-end" Href="/Account/Manage">Setting</MudNavLink>
</MudNavMenu> </MudNavMenu>
</MudPaper> </MudPaper>
</MudDrawerHeader> </MudDrawerHeader>
@@ -46,7 +42,7 @@
<MudMainContent Style="background: #f5f6fb"> <MudMainContent Style="background: #f5f6fb">
<SnackErrorBoundary @ref="errorBoundary"> <SnackErrorBoundary @ref="errorBoundary">
<MudPaper Height="calc(100vh - 64px)" Style="background-color:transparent" Class="overflow-hidden px-1 py-2" Elevation="0"> <MudPaper Height="calc(100vh - 64px)" Style="background-color:transparent" Class="overflow-hidden px-1 py-2" Elevation="0">
<MudPaper Style="background-color:#eeeeeeef" Elevation="3" Class="d-flex w-100 h-100 overflow-hidden pa-2 rounded-xl"> <MudPaper Style="background-color:transparent" Elevation="0" Class="d-flex w-100 h-100 overflow-hidden pa-2 rounded-xl">
@Body @Body
</MudPaper> </MudPaper>
</MudPaper> </MudPaper>

View File

@@ -2,7 +2,7 @@
<MudGrid Class="h-100"> <MudGrid Class="h-100">
<MudItem xs="12" sm="2" Class="h-100 pa-1 mt-1"> <MudItem sm="2" Class="h-100 pa-1 mt-1">
<MudStack Class="h-100"> <MudStack Class="h-100">
<MudText Style="color:white"> BETA版本 </MudText> <MudText Style="color:white"> BETA版本 </MudText>
<MudText Style="color:white" Typo="Typo.h3"><b> 75 </b></MudText> <MudText Style="color:white" Typo="Typo.h3"><b> 75 </b></MudText>
@@ -44,7 +44,7 @@
</MudItem> </MudItem>
<MudItem xs="12" sm="9"> <MudItem sm="9">
<MudPaper Style="background-color:transparent" Class="w-100 mt-n3" Height="100%" Elevation="0"> <MudPaper Style="background-color:transparent" Class="w-100 mt-n3" Height="100%" Elevation="0">
<MudChart ChartType="ChartType.Line" Class="pt-0" ChartSeries="@Series" XAxisLabels="@XAxisLabels" CanHideSeries <MudChart ChartType="ChartType.Line" Class="pt-0" ChartSeries="@Series" XAxisLabels="@XAxisLabels" CanHideSeries
Height="150px" Width="100%" AxisChartOptions="_axisChartOptions" ChartOptions="options"> Height="150px" Width="100%" AxisChartOptions="_axisChartOptions" ChartOptions="options">
@@ -72,12 +72,6 @@
</MudGrid> </MudGrid>
</MudPaper> </MudPaper>
@* <MudChip Size="Size.Small" Text="pink" Variant="Variant.Text" Color="Color.Secondary">成绩趋势</MudChip>
<MudChip Size="Size.Small" Text="blue" Variant="Variant.Text" Color="Color.Info">分值区间</MudChip>
<MudChip Size="Size.Small" Text="green" Variant="Variant.Text" Color="Color.Success">Success</MudChip>
<MudChip Size="Size.Small" Text="orange" Variant="Variant.Text" Color="Color.Warning">Warning</MudChip>
<MudChip Size="Size.Small" Text="red" Variant="Variant.Text" Color="Color.Error">Error</MudChip>
<MudChip Size="Size.Small" Text="black" Variant="Variant.Text" Color="Color.Dark">Dark</MudChip> *@
@code { @code {
public double[] data = { 25, 77, 28, 5 }; public double[] data = { 25, 77, 28, 5 };

View File

@@ -0,0 +1,191 @@
@using Entities.DTO
@using TechHelper.Client.Services
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5" Elevation="5" Height="170px" Style="background-color:#6bc6be">
<MudGrid Class="h-100">
<MudItem sm="2" Class="h-100 pa-1 mt-1">
<MudStack Class="h-100">
<MudText Style="color:white"> BETA版本 </MudText>
<MudText Style="color:white" Typo="Typo.h3"><b> @StudentSubmissionDetail.AverageScore </b></MudText>
<MudPaper Elevation=0 Class="h-100 w-100" Style="background-color:transparent">
<MudStack Class="h-100" Row=true>
<MudPaper Elevation=0 Height="100%" Width="100%" Style="background-color:transparent">
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
总数:
<span style="color: #fefefe;"> @StudentSubmissionDetail.TotalQuestions </span>
</MudText>
</MudPaper>
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
总分:
<span style="color: #fefefe;"> 150 </span>
</MudText>
</MudPaper>
</MudPaper>
<MudPaper Elevation=0 Height="100%" Width="100%" Style="background-color:transparent">
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
排名:
<span style="color: #fefefe;"> @StudentSubmissionDetail.TotalRank </span>
</MudText>
</MudPaper>
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
平均:
<span style="color: #fefefe;"> @StudentSubmissionDetail.ClassAverageScore </span>
</MudText>
</MudPaper>
</MudPaper>
</MudStack>
</MudPaper>
</MudStack>
</MudItem>
<MudItem sm="9">
<MudPaper Style="background-color:transparent" Class="w-100 mt-n3" Height="100%" Elevation="0">
<MudChart ChartType="ChartType.Line" Class="pt-0" ChartSeries="@Series" XAxisLabels="@XAxisLabels" CanHideSeries
Height="150px" Width="100%" AxisChartOptions="_axisChartOptions" ChartOptions="options">
<CustomGraphics>
<style>
.heavy {
font: normal 12px helvetica;
fill: rgb(255,255,255);
letter-spacing: 2px;
}
</style>
<text x="60" y="15" class="heavy"> 成绩的整体分布情况 </text>
</CustomGraphics>
</MudChart>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="1">
<MudChipSet T="string" SelectedValuesChanged="HandleSelectedValuesChanged" SelectedValues="@_selected" SelectionMode="SelectionMode.MultiSelection" CheckMark="true">
<MudChip Size="Size.Small" Text="类型错误数量分布" Variant="Variant.Text" Color="Color.Default">类型分布</MudChip>
<MudChip Size="Size.Small" Text="类型错误成绩分布" Variant="Variant.Text" Color="Color.Primary">课时分布</MudChip>
</MudChipSet>
</MudItem>
</MudGrid>
</MudPaper>
@* <MudChip Size="Size.Small" Text="pink" Variant="Variant.Text" Color="Color.Secondary">成绩趋势</MudChip>
<MudChip Size="Size.Small" Text="blue" Variant="Variant.Text" Color="Color.Info">分值区间</MudChip>
<MudChip Size="Size.Small" Text="green" Variant="Variant.Text" Color="Color.Success">Success</MudChip>
<MudChip Size="Size.Small" Text="orange" Variant="Variant.Text" Color="Color.Warning">Warning</MudChip>
<MudChip Size="Size.Small" Text="red" Variant="Variant.Text" Color="Color.Error">Error</MudChip>
<MudChip Size="Size.Small" Text="black" Variant="Variant.Text" Color="Color.Dark">Dark</MudChip> *@
@code {
private AxisChartOptions _axisChartOptions = new AxisChartOptions
{
};
private ChartOptions options = new ChartOptions
{
InterpolationOption = InterpolationOption.NaturalSpline,
YAxisFormat = "c2",
ShowLegend = false,
YAxisLines = false,
XAxisLines = false,
XAxisLabelPosition = XAxisLabelPosition.None,
YAxisLabelPosition = YAxisLabelPosition.None,
YAxisTicks = 100,
ShowLabels = false,
ShowLegendLabels = false
};
public List<ChartSeries> Series = new List<ChartSeries>()
{
new ChartSeries() { Name = "类型错误数量分布", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
};
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
Random random = new Random();
protected override void OnInitialized()
{
options.InterpolationOption = InterpolationOption.NaturalSpline;
options.YAxisFormat = "c2";
options.ShowLegend = false;
options.YAxisLines = false;
options.XAxisLines = false;
options.XAxisLabelPosition = XAxisLabelPosition.None;
options.YAxisLabelPosition = YAxisLabelPosition.None;
options.ShowLabels = false;
options.ShowLegendLabels = false;
options.LineStrokeWidth = 1;
_axisChartOptions.MatchBoundsToSize = true;
Series[0].LineDisplayType = LineDisplayType.Area;
}
[Parameter]
public Guid SubmissionID { get; set; } = Guid.Empty;
private StudentSubmissionDetailDto StudentSubmissionDetail { get; set; } = new StudentSubmissionDetailDto();
private IReadOnlyCollection<string> _selected;
#pragma warning restore 1998
#nullable restore
#line (82, 8) - (143, 1) "D:\AllWX\AllC\TechHelper\TechHelper.Client\Pages\Common\Exam\SubmissionInfoCard.razor"
[Inject]
public IStudentSubmissionDetailService StudentSubmissionDetailService { get; set; }
[Inject]
public ISnackbar Snackbar { get; set; }
protected override async Task OnInitializedAsync()
{
if (SubmissionID != Guid.Empty)
{
StudentSubmissionDetailDto result;
try
{
result = await StudentSubmissionDetailService.GetSubmissionDetailAsync(SubmissionID);
if (result != null)
{
StudentSubmissionDetail = result;
XAxisLabels = result.ErrorTypeDistribution.Keys.ToArray();
Series.Clear();
Series.Add(new ChartSeries
{
Name = "类型错误数量分布",
Data = result.ErrorTypeDistribution.Values.Select(d => (double)d).ToArray()
});
Series.Add(new ChartSeries
{
Name = "类型错误成绩分布",
Data = result.ErrorTypeScoreDistribution.Values.Select(d => (double)d).ToArray()
});
}
}
catch (Exception ex)
{
Snackbar.Add($"获取提交错误, 请重试, {ex.Message}", Severity.Warning);
}
}
}
private void HandleSelectedValuesChanged(IReadOnlyCollection<string> selected)
{
Series.ForEach(x => x.Visible = false);
foreach (var item in selected)
{
var sv = Series.FirstOrDefault(predicate: x => x.Name == item);
if (sv != null)
{
sv.Visible = true;
}
}
}
}

View File

@@ -0,0 +1,143 @@
<MudPaper Class="rounded-xl w-100 px-10 ma-3 pt-5" Elevation="5" Height="170px" Style="background-color:#6bc6be">
<MudGrid Class="h-100">
<MudItem xs="12" sm="2" Class="h-100 pa-1 mt-1">
<MudStack Class="h-100">
<MudText Style="color:white"> BETA版本 </MudText>
<MudText Style="color:white" Typo="Typo.h3"><b> 75 </b></MudText>
<MudPaper Elevation=0 Class="h-100 w-100" Style="background-color:transparent">
<MudStack Class="h-100" Row=true>
<MudPaper Elevation=0 Height="100%" Width="100%" Style="background-color:transparent">
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
总数:
<span style="color: #fefefe;">15</span>
</MudText>
</MudPaper>
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
总分:
<span style="color: #fefefe;">15</span>
</MudText>
</MudPaper>
</MudPaper>
<MudPaper Elevation=0 Height="100%" Width="100%" Style="background-color:transparent">
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
中位:
<span style="color: #fefefe;">15</span>
</MudText>
</MudPaper>
<MudPaper Elevation=0 Height="50%" Style="background-color:transparent">
<MudText Style="color:#9ed5f7" Typo="Typo.body2">
方差:
<span style="color: #fefefe;">15</span>
</MudText>
</MudPaper>
</MudPaper>
</MudStack>
</MudPaper>
</MudStack>
</MudItem>
<MudItem xs="12" sm="9">
<MudPaper Style="background-color:transparent" Class="w-100 mt-n3" Height="100%" Elevation="0">
<MudChart ChartType="ChartType.Line" Class="pt-0" ChartSeries="@Series" XAxisLabels="@XAxisLabels" CanHideSeries
Height="150px" Width="100%" AxisChartOptions="_axisChartOptions" ChartOptions="options">
<CustomGraphics>
<style>
.heavy {
font: normal 12px helvetica;
fill: rgb(255,255,255);
letter-spacing: 2px;
}
</style>
<text x="60" y="15" class="heavy"> 成绩的整体分布情况 </text>
</CustomGraphics>
</MudChart>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="1">
<MudChipSet T="string" SelectedValuesChanged="HandleSelectedValuesChanged" SelectedValues="@_selected" SelectionMode="SelectionMode.MultiSelection" CheckMark="true">
<MudChip Size="Size.Small" Text="类型错误数量分布" Variant="Variant.Text" Color="Color.Default">类型分布</MudChip>
<MudChip Size="Size.Small" Text="类型错误成绩分布" Variant="Variant.Text" Color="Color.Primary">课时分布</MudChip>
</MudChipSet>
</MudItem>
</MudGrid>
</MudPaper>
@* <MudChip Size="Size.Small" Text="pink" Variant="Variant.Text" Color="Color.Secondary">成绩趋势</MudChip>
<MudChip Size="Size.Small" Text="blue" Variant="Variant.Text" Color="Color.Info">分值区间</MudChip>
<MudChip Size="Size.Small" Text="green" Variant="Variant.Text" Color="Color.Success">Success</MudChip>
<MudChip Size="Size.Small" Text="orange" Variant="Variant.Text" Color="Color.Warning">Warning</MudChip>
<MudChip Size="Size.Small" Text="red" Variant="Variant.Text" Color="Color.Error">Error</MudChip>
<MudChip Size="Size.Small" Text="black" Variant="Variant.Text" Color="Color.Dark">Dark</MudChip> *@
@code {
public double[] data = { 25, 77, 28, 5 };
public string[] labels = { "Oil", "Coal", "Gas", "Biomass" };
private AxisChartOptions _axisChartOptions = new AxisChartOptions
{
};
private ChartOptions options = new ChartOptions
{
InterpolationOption = InterpolationOption.NaturalSpline,
YAxisFormat = "c2",
ShowLegend = false,
YAxisLines = false,
XAxisLines = false,
XAxisLabelPosition = XAxisLabelPosition.None,
YAxisLabelPosition = YAxisLabelPosition.None,
YAxisTicks = 100,
ShowLabels = false,
ShowLegendLabels = false
};
public List<ChartSeries> Series = new List<ChartSeries>()
{
new ChartSeries() { Name = "类型错误数量分布", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
new ChartSeries() { Name = "类型错误成绩分布", Data = new double[] { 55, 21, 45, 11, 45, 23, 11, 56, 13 } },
};
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
Random random = new Random();
protected override void OnInitialized()
{
options.InterpolationOption = InterpolationOption.NaturalSpline;
options.YAxisFormat = "c2";
options.ShowLegend = false;
options.YAxisLines = false;
options.XAxisLines = false;
options.XAxisLabelPosition = XAxisLabelPosition.None;
options.YAxisLabelPosition = YAxisLabelPosition.None;
options.ShowLabels = false;
options.ShowLegendLabels = false;
options.LineStrokeWidth = 1;
_axisChartOptions.MatchBoundsToSize = true;
Series[0].LineDisplayType = LineDisplayType.Area;
}
private IReadOnlyCollection<string> _selected;
private void HandleSelectedValuesChanged(IReadOnlyCollection<string> selected)
{
Series.ForEach(x => x.Visible = false);
foreach(var item in selected)
{
var sv = Series.FirstOrDefault(predicate: x => x.Name == item);
if(sv != null)
{
sv.Visible = true;
}
}
}
}

View File

@@ -0,0 +1,7 @@
<MudPaper>
<MudText> ExamName </MudText>
<MudText> 已经指派人数 </MudText>
<MudText> 总人数 </MudText>
<MudText> 平均S </MudText>
<MudText> 指派 </MudText>
</MudPaper>

View File

@@ -21,8 +21,6 @@ else
<MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%"> <MudPaper Class="d-flex flex-wrap flex-grow-0 gap-4" Height="100%" Width="100%">
@foreach (var item in examDtos) @foreach (var item in examDtos)
{ {
@* <ExamPreview AssignmentDto="item" Width="256px" Height="256px"> </ExamPreview> *@
<AssignmentInfoCard></AssignmentInfoCard>
} }
</MudPaper> </MudPaper>

View File

@@ -1,7 +1,34 @@
@page "/exam" @page "/exam"
@using TechHelper.Client.Pages.Student.BaseInfoCard
@inject NavigationManager NavigationManager
<AuthorizeView Roles="Teacher">
<Authorized>
<MudPaper Class="rounded-xl ma-2 px-2 overflow-auto w-100 h-100">
<StudentSubmissionPreviewTableCard />
</MudPaper>
</Authorized>
</AuthorizeView>
<MudText>HELLO WORLD</MudText> <AuthorizeView Roles="Student">
<Authorized>
<MudPaper Class="rounded-xl ma-2 px-2 overflow-auto w-100 h-100">
<StudentSubmissionPreviewTableCard />
</MudPaper>
</Authorized>
</AuthorizeView>
@code { @code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
protected override void OnParametersSet()
{
if (authenticationStateTask is null)
{
NavigationManager.Refresh(forceReload: true);
}
}
} }

View File

@@ -0,0 +1,4 @@

@code {
}

View File

@@ -7,7 +7,6 @@
<TechHelper.Client.Pages.Student.HomePage /> <TechHelper.Client.Pages.Student.HomePage />
</Authorized> </Authorized>
</AuthorizeView> </AuthorizeView>
@code { @code {
[CascadingParameter] [CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; } private Task<AuthenticationState> authenticationStateTask { get; set; }

View File

@@ -1,39 +1,97 @@
<MudPaper Class="ma-2 pa-2 rounded-xl d-flex flex-column flex-grow-1 overflow-auto" MaxHeight="100%"> @using TechHelper.Client.Services
@inject IStudentSubmissionService StudentSubmissionService
<MudPaper Class="ma-2 pa-2 rounded-xl d-flex flex-column flex-grow-1 overflow-auto" MaxHeight="100%">
<StudentSubmissionPreviewCard /> <StudentSubmissionPreviewCard />
@foreach (var submission in _studentSubmissions) @if (_isLoading)
{ {
<StudentSubmissionPreviewCard StudentSubmission="@submission" /> <div class="d-flex justify-content-center align-items-center" style="height: 200px;">
<MudProgressCircular Color="Color.Primary" Size="Size.Large" />
</div>
}
else if (_studentSubmissions == null || _studentSubmissions.Count == 0)
{
<div class="d-flex justify-content-center align-items-center" style="height: 200px;">
<MudText TextColor="Color.TextSecondary" Align="Align.Center">暂无提交记录</MudText>
</div>
}
else
{
@foreach (var submission in _studentSubmissions)
{
<StudentSubmissionPreviewCard StudentSubmission="@submission" />
}
} }
</MudPaper> </MudPaper>
@code { @code {
// 假设的学生提交数据模型 // 学生提交数据模型
public class StudentSubmission public class StudentSubmission
{ {
public string StudentName { get; set; } public string StudentName { get; set; }
public int TotalProblems { get; set; } public int TotalProblems { get; set; }
public int ErrorCount { get; set; } public int ErrorCount { get; set; }
public DateTime CreatedDate { get; set; }
public float Score { get; set; }
public string AssignmentName { get; set; }
public string Status { get; set; }
public TimeSpan TimeSpent { get; set; } public TimeSpan TimeSpent { get; set; }
public int Score { get; set; }
} }
// 模拟数据列表 // 学生提交列表
private List<StudentSubmission> _studentSubmissions = new(); private List<StudentSubmission> _studentSubmissions = new();
private bool _isLoading = true;
protected override void OnInitialized() protected override async Task OnInitializedAsync()
{ {
// 模拟获取或初始化数据实际应用中可能来自数据库或API await LoadStudentSubmissions();
_studentSubmissions = new List<StudentSubmission>
{
new() { StudentName = "张三", TotalProblems = 10, ErrorCount = 2, TimeSpent = TimeSpan.FromMinutes(25), Score = 80 },
new() { StudentName = "李四", TotalProblems = 10, ErrorCount = 1, TimeSpent = TimeSpan.FromMinutes(20), Score = 90 },
new() { StudentName = "王五", TotalProblems = 10, ErrorCount = 5, TimeSpent = TimeSpan.FromMinutes(30), Score = 50 },
new() { StudentName = "赵六", TotalProblems = 10, ErrorCount = 3, TimeSpent = TimeSpan.FromMinutes(28), Score = 70 },
new() { StudentName = "钱七", TotalProblems = 10, ErrorCount = 0, TimeSpent = TimeSpan.FromMinutes(18), Score = 100 }
// ... 可以添加更多模拟数据
};
} }
}
private async Task LoadStudentSubmissions()
{
try
{
_isLoading = true;
StateHasChanged();
var result = await StudentSubmissionService.GetMySubmissionsAsync();
if (result.Status && result.Result != null)
{
// 从服务器获取的数据映射到我们的模型
var submissions = result.Result as List<Entities.DTO.StudentSubmissionSummaryDto>;
if (submissions != null)
{
_studentSubmissions = submissions.Select(submission => new StudentSubmission
{
AssignmentName = submission.AssignmentName,
CreatedDate = submission.CreatedDate,
ErrorCount = submission.ErrorCount,
Score = submission.Score,
StudentName = submission.StudentName,
Status = submission.Status,
TotalProblems = submission.TotalQuestions,
TimeSpent = TimeSpan.FromMinutes(30) // 默认值,实际应用中可以从服务器获取
}).ToList();
}
}
else
{
// 如果API调用失败使用空列表
_studentSubmissions = new List<StudentSubmission>();
}
}
catch (Exception ex)
{
// 处理异常,可以记录日志
_studentSubmissions = new List<StudentSubmission>();
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
}

View File

@@ -2,10 +2,10 @@
@using TechHelper.Client.Pages.Student.BaseInfoCard; @using TechHelper.Client.Pages.Student.BaseInfoCard;
@using TechHelper.Client.Pages.Common; @using TechHelper.Client.Pages.Common;
<MudPaper Class="w-100 h-100 d-flex flex-row"> <MudPaper Class="w-100 h-100 d-flex flex-row" Style="background-color: transparent" Elevation="0">
<MudPaper Class="flex-grow-1 mx-2 d-flex flex-column"> <MudPaper Class="flex-grow-1 mx-2 d-flex flex-column" Style="background-color:transparent" Elevation="0">
<AssignmentInfoCard></AssignmentInfoCard> <SubmissionInfoCard ></SubmissionInfoCard>
<MudPaper Class="d-flex flex-row"> <MudPaper Class="d-flex flex-row" Style="background-color:transparent" Elevation="0">
<NotifyCard></NotifyCard> <NotifyCard></NotifyCard>
<HomeworkCard></HomeworkCard> <HomeworkCard></HomeworkCard>
<NotifyCard></NotifyCard> <NotifyCard></NotifyCard>
@@ -13,7 +13,7 @@
</MudPaper> </MudPaper>
<StudentSubmissionPreviewTableCard></StudentSubmissionPreviewTableCard> <StudentSubmissionPreviewTableCard></StudentSubmissionPreviewTableCard>
</MudPaper> </MudPaper>
<MudPaper Width="300px" Class="mx-2 align-content-center d-flex flex-column flex-grow-1"> <MudPaper Width="300px" Class="mx-2 align-content-center d-flex flex-column flex-grow-1" Style="background-color: transparent" Elevation="0">
<HeadIconCard></HeadIconCard> <HeadIconCard></HeadIconCard>
<TotalErrorQuestionType></TotalErrorQuestionType> <TotalErrorQuestionType></TotalErrorQuestionType>
@@ -22,42 +22,4 @@
@code { @code {
public double[] data = { 25, 77, 28, 5 };
public string[] labels = { "Oil", "Coal", "Gas", "Biomass" };
private AxisChartOptions _axisChartOptions = new AxisChartOptions();
private ChartOptions options = new ChartOptions();
public List<ChartSeries> Series = new List<ChartSeries>()
{
new ChartSeries() { Name = "Series 1", Data = new double[] { 90, 79, 72, 69, 62, 62, 55, 65, 70 } },
new ChartSeries() { Name = "Series 2", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
};
public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
Random random = new Random();
protected override void OnInitialized()
{
options.InterpolationOption = InterpolationOption.NaturalSpline;
options.YAxisFormat = "c2";
_axisChartOptions.MatchBoundsToSize = true;
}
public void RandomizeData()
{
foreach (var series in Series)
{
for (int i = 0; i < series.Data.Length - 1; i++)
{
series.Data[i] = random.NextDouble() * 100 + 10;
}
}
StateHasChanged();
}
void OnClickMenu(InterpolationOption interpolationOption)
{
options.InterpolationOption = interpolationOption;
StateHasChanged();
}
} }

View File

@@ -0,0 +1,3 @@
<MudPaper>
<MudText> EXAM NAME</MudText>
</MudPaper>

View File

@@ -0,0 +1,7 @@
@using Entities.DTO
<h3>StudentCard</h3>
@code {
[Parameter]
public StudentDto StudentDto{ get; set; }
}

View File

@@ -6,7 +6,9 @@
@foreach(var cs in ClassStudents) @foreach(var cs in ClassStudents)
{ {
<MudText> @cs.DisplayName </MudText> <StudentCard StudentDto="@cs">
</StudentCard>
} }

View File

@@ -39,6 +39,8 @@ builder.Services.AddLocalStorageServices();
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>(); builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
builder.Services.AddScoped<IExamService, ExamService>(); builder.Services.AddScoped<IExamService, ExamService>();
builder.Services.AddScoped<IStudentSubmissionService, StudentSubmissionService>();
builder.Services.AddScoped<IStudentSubmissionDetailService, StudentSubmissionDetailService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>(); builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>(); builder.Services.AddScoped<IRefreshTokenService, RefreshTokenService>();
builder.Services.AddScoped<IClassServices, ClasssServices>(); builder.Services.AddScoped<IClassServices, ClasssServices>();

View File

@@ -54,6 +54,11 @@ namespace TechHelper.Client.Services
} }
} }
public StudentDto GetStudents(byte Class)
{
throw new NotImplementedException();
}
public async Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto) public async Task<ResponseDto> UserRegister(UserRegistrationToClassDto userRegistrationToClassDto)
{ {
try try

View File

@@ -10,5 +10,6 @@ namespace TechHelper.Client.Services
public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass); public Task<ResponseDto> CreateClass(UserRegistrationToClassDto userClass);
public Task<ApiResponse> GetClassStudents(); public Task<ApiResponse> GetClassStudents();
public Task<ApiResponse> GetGradeClasses(byte grade); public Task<ApiResponse> GetGradeClasses(byte grade);
public StudentDto GetStudents(byte Class);
} }
} }

View File

@@ -0,0 +1,14 @@
using Entities.DTO;
namespace TechHelper.Client.Services
{
public interface IStudentSubmissionDetailService
{
/// <summary>
/// 获取学生提交的详细信息
/// </summary>
/// <param name="submissionId">提交ID</param>
/// <returns>学生提交详细信息</returns>
Task<StudentSubmissionDetailDto> GetSubmissionDetailAsync(Guid submissionId);
}
}

View File

@@ -0,0 +1,22 @@
using Entities.DTO;
using TechHelper.Services;
namespace TechHelper.Client.Services
{
public interface IStudentSubmissionService
{
/// <summary>
/// 获取当前学生的所有提交摘要
/// </summary>
/// <returns>学生提交摘要列表</returns>
Task<ApiResponse> GetMySubmissionsAsync();
/// <summary>
/// 获取当前学生的提交摘要(分页)
/// </summary>
/// <param name="pageNumber">页码默认为1</param>
/// <param name="pageSize">每页数量默认为10</param>
/// <returns>分页的学生提交摘要列表</returns>
Task<ApiResponse> GetMySubmissionsPagedAsync(int pageNumber = 1, int pageSize = 10);
}
}

View File

@@ -0,0 +1,26 @@
using Entities.DTO;
using TechHelper.Client.HttpRepository;
using System.Net.Http.Json;
namespace TechHelper.Client.Services
{
public class StudentSubmissionDetailService : IStudentSubmissionDetailService
{
private readonly HttpClient _httpClient;
public StudentSubmissionDetailService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<StudentSubmissionDetailDto> GetSubmissionDetailAsync(Guid submissionId)
{
var response = await _httpClient.GetAsync($"api/student-submission-detail/{submissionId}");
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadFromJsonAsync<StudentSubmissionDetailDto>();
}
throw new HttpRequestException($"获取学生提交详细信息失败: {response.StatusCode}");
}
}
}

View File

@@ -0,0 +1,75 @@
using Entities.DTO;
using TechHelper.Services;
using System.Net.Http.Json;
using Newtonsoft.Json;
namespace TechHelper.Client.Services
{
public class StudentSubmissionService : IStudentSubmissionService
{
private readonly HttpClient _client;
public StudentSubmissionService(HttpClient client)
{
_client = client;
}
/// <summary>
/// 获取当前学生的所有提交摘要
/// </summary>
/// <returns>学生提交摘要列表</returns>
public async Task<ApiResponse> GetMySubmissionsAsync()
{
try
{
var response = await _client.GetAsync("student-submission/my-submissions");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var submissions = JsonConvert.DeserializeObject<List<StudentSubmissionSummaryDto>>(content);
return ApiResponse.Success(result: submissions);
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
return ApiResponse.Error(message: $"获取学生提交信息失败: {response.StatusCode} - {errorContent}");
}
}
catch (Exception ex)
{
return ApiResponse.Error(message: $"内部错误: {ex.Message}");
}
}
/// <summary>
/// 获取当前学生的提交摘要(分页)
/// </summary>
/// <param name="pageNumber">页码默认为1</param>
/// <param name="pageSize">每页数量默认为10</param>
/// <returns>分页的学生提交摘要列表</returns>
public async Task<ApiResponse> GetMySubmissionsPagedAsync(int pageNumber = 1, int pageSize = 10)
{
try
{
var response = await _client.GetAsync($"student-submission/my-submissions-paged?pageNumber={pageNumber}&pageSize={pageSize}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<StudentSubmissionSummaryResponseDto>(content);
return ApiResponse.Success(result: result);
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
return ApiResponse.Error(message: $"获取学生提交信息失败: {response.StatusCode} - {errorContent}");
}
}
catch (Exception ex)
{
return ApiResponse.Error(message: $"内部错误: {ex.Message}");
}
}
}
}

View File

@@ -1,16 +1,44 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@layout AccountLayout @layout AccountLayout
@inject NavigationManager NavigationManager
<AuthorizeView Roles="Teacher">
<Authorized>
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Height="100%">
<MudPaper Width="200px">
<MudDivider Class="flex-grow-0" />
<ExamNavMenu />
</MudPaper>
<MudPaper Class="d-flex flex-grow-1 overflow-auto">
@Body
</MudPaper>
</MudPaper>
</Authorized>
</AuthorizeView>
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Height="100%"> <AuthorizeView Roles="Student">
<Authorized>
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Height="100%">
<MudPaper Width="200px">
<MudDivider Class="flex-grow-0" />
<ExamNavMenu />
</MudPaper>
<MudPaper Class="d-flex h-100 w-100 flex-grow-1 overflow-auto">
@Body
</MudPaper>
</MudPaper>
</Authorized>
</AuthorizeView>
<MudPaper Width="200px"> @code {
<MudDivider Class="flex-grow-0" /> [CascadingParameter]
<ExamNavMenu /> private Task<AuthenticationState> authenticationStateTask { get; set; }
</MudPaper> protected override void OnParametersSet()
{
<MudPaper Class="flex-grow-1 overflow-auto"> if (authenticationStateTask is null)
@Body {
</MudPaper> NavigationManager.Refresh(forceReload: true);
</MudPaper> }
}
}

View File

@@ -1,22 +1,39 @@
@using Microsoft.AspNetCore.Identity @using Microsoft.AspNetCore.Identity
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="exam/create" Match="NavLinkMatch.All">创建</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="exam/manage">管理</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/ChangePassword">Password</NavLink>
</li>
@* <li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/TwoFactorAuthentication">Two-factor authentication</NavLink>
</li> *@
</ul>
<AuthorizeView Roles="Teacher">
<Authorized>
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="exam/create" Match="NavLinkMatch.All">创建</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="exam/manage">管理</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/ChangePassword">Password</NavLink>
</li>
</ul>
</Authorized>
</AuthorizeView>
<AuthorizeView Roles="Student">
<Authorized>
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<NavLink class="nav-link" href="exam/studentHomework" Match="NavLinkMatch.All">Homework</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link" href="exam/manage">管理</NavLink>
</li>
</ul>
</Authorized>
</AuthorizeView>
@code { @code {
private bool hasExternalLogins; private bool hasExternalLogins;
} }

View File

@@ -9,3 +9,17 @@
@Body @Body
</MudStack> </MudStack>
</MudPaper> </MudPaper>
@code {
[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }
protected override void OnParametersSet()
{
if (authenticationStateTask is null)
{
// NavigationManager.Refresh(forceReload: true);
}
}
}

View File

@@ -9,11 +9,8 @@
<NavLink class="nav-link" href="Account/Manage/Class">Class</NavLink> <NavLink class="nav-link" href="Account/Manage/Class">Class</NavLink>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/ChangePassword">Password</NavLink> <NavLink class="nav-link" href="Account/Manage/ChangePassword">重设密码</NavLink>
</li> </li>
@* <li class="nav-item">
<NavLink class="nav-link" href="Account/Manage/TwoFactorAuthentication">Two-factor authentication</NavLink>
</li> *@
</ul> </ul>
@code { @code {

View File

@@ -58,6 +58,17 @@ namespace TechHelper.Context
CreateMap<SubmissionDetailDto, SubmissionDetail>().ReverseMap(); CreateMap<SubmissionDetailDto, SubmissionDetail>().ReverseMap();
// Student Submission Detail
CreateMap<Submission, StudentSubmissionDetailDto>()
.ForMember(dest => dest.AssignmentId, opt => opt.MapFrom(src => src.AssignmentId))
.ForMember(dest => dest.StudentId, opt => opt.MapFrom(src => src.StudentId))
.ForMember(dest => dest.SubmissionTime, opt => opt.MapFrom(src => src.SubmissionTime))
.ForMember(dest => dest.OverallGrade, opt => opt.MapFrom(src => src.OverallGrade))
.ForMember(dest => dest.OverallFeedback, opt => opt.MapFrom(src => src.OverallFeedback))
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Status));
CreateMap<Assignment, AssignmentDto>().ReverseMap();
CreateMap<SubjectTypeMetadataDto, Global>() CreateMap<SubjectTypeMetadataDto, Global>()
.ForMember(dest => dest.Info, opt => opt.MapFrom(src => JsonConvert.SerializeObject(src.Data))); .ForMember(dest => dest.Info, opt => opt.MapFrom(src => JsonConvert.SerializeObject(src.Data)));

View File

@@ -39,6 +39,7 @@ namespace TechHelper.Context.Configuration
builder.Property(s => s.OverallGrade) builder.Property(s => s.OverallGrade)
.HasColumnName("overall_grade") .HasColumnName("overall_grade")
.IsRequired()
.HasPrecision(5, 2); // 应用精度设置 .HasPrecision(5, 2); // 应用精度设置
// OverallFeedback // OverallFeedback

View File

@@ -0,0 +1,127 @@
using Entities.Contracts;
using Entities.DTO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using TechHelper.Server.Services;
using System.Security.Claims;
namespace TechHelper.Server.Controllers
{
[Route("api/student-submission")]
[ApiController]
[Authorize]
public class StudentSubmissionController : ControllerBase
{
private readonly IStudentSubmissionService _studentSubmissionService;
private readonly UserManager<User> _userManager;
public StudentSubmissionController(
IStudentSubmissionService studentSubmissionService,
UserManager<User> userManager)
{
_studentSubmissionService = studentSubmissionService;
_userManager = userManager;
}
/// <summary>
/// 获取当前学生的所有提交摘要
/// </summary>
/// <returns>学生提交摘要列表</returns>
[HttpGet("my-submissions")]
public async Task<IActionResult> GetMySubmissions()
{
var user = await _userManager.FindByEmailAsync(User.Identity.Name);
if (user == null)
return NotFound("未找到用户信息");
var result = await _studentSubmissionService.GetStudentSubmissionsAsync(user.Id);
if (result.Status)
{
return Ok(result.Result);
}
else
{
return BadRequest(result.Message);
}
}
/// <summary>
/// 获取当前学生的提交摘要(分页)
/// </summary>
/// <param name="pageNumber">页码默认为1</param>
/// <param name="pageSize">每页数量默认为10</param>
/// <returns>分页的学生提交摘要列表</returns>
[HttpGet("my-submissions-paged")]
public async Task<IActionResult> GetMySubmissionsPaged(int pageNumber = 1, int pageSize = 10)
{
if (pageNumber < 1) pageNumber = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100; // 限制最大页面大小
var user = await _userManager.FindByEmailAsync(User.Identity.Name);
if (user == null)
return NotFound("未找到用户信息");
var result = await _studentSubmissionService.GetStudentSubmissionsPagedAsync(user.Id, pageNumber, pageSize);
if (result.Status)
{
return Ok(result.Result);
}
else
{
return BadRequest(result.Message);
}
}
/// <summary>
/// 获取指定学生的提交摘要(仅教师可使用)
/// </summary>
/// <param name="studentId">学生ID</param>
/// <returns>学生提交摘要列表</returns>
[HttpGet("student/{studentId:guid}")]
[Authorize(Roles = "Teacher")]
public async Task<IActionResult> GetStudentSubmissions(Guid studentId)
{
var result = await _studentSubmissionService.GetStudentSubmissionsAsync(studentId);
if (result.Status)
{
return Ok(result.Result);
}
else
{
return BadRequest(result.Message);
}
}
/// <summary>
/// 获取指定学生的提交摘要(分页,仅教师可使用)
/// </summary>
/// <param name="studentId">学生ID</param>
/// <param name="pageNumber">页码默认为1</param>
/// <param name="pageSize">每页数量默认为10</param>
/// <returns>分页的学生提交摘要列表</returns>
[HttpGet("student/{studentId:guid}/paged")]
[Authorize(Roles = "Teacher")]
public async Task<IActionResult> GetStudentSubmissionsPaged(Guid studentId, int pageNumber = 1, int pageSize = 10)
{
if (pageNumber < 1) pageNumber = 1;
if (pageSize < 1) pageSize = 10;
if (pageSize > 100) pageSize = 100; // 限制最大页面大小
var result = await _studentSubmissionService.GetStudentSubmissionsPagedAsync(studentId, pageNumber, pageSize);
if (result.Status)
{
return Ok(result.Result);
}
else
{
return BadRequest(result.Message);
}
}
}
}

View File

@@ -0,0 +1,81 @@
using Entities.Contracts;
using Entities.DTO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using TechHelper.Server.Services;
using TechHelper.Context;
using TechHelper.Repository;
using SharedDATA.Api;
using System.Security.Claims;
namespace TechHelper.Server.Controllers
{
[Route("api/student-submission-detail")]
[ApiController]
[Authorize]
public class StudentSubmissionDetailController : ControllerBase
{
private readonly IStudentSubmissionDetailService _studentSubmissionDetailService;
private readonly UserManager<User> _userManager;
private readonly IUnitOfWork _unitOfWork;
public StudentSubmissionDetailController(
IStudentSubmissionDetailService studentSubmissionDetailService,
UserManager<User> userManager,
IUnitOfWork unitOfWork)
{
_studentSubmissionDetailService = studentSubmissionDetailService;
_userManager = userManager;
_unitOfWork = unitOfWork;
}
/// <summary>
/// 获取学生提交的详细信息
/// </summary>
/// <param name="submissionId">提交ID</param>
/// <returns>学生提交详细信息</returns>
[HttpGet("{submissionId:guid}")]
public async Task<IActionResult> GetSubmissionDetail(Guid submissionId)
{
try
{
// 验证用户权限 - 只有学生本人或教师可以查看
var user = await _userManager.FindByEmailAsync(User.Identity.Name);
if (user == null)
{
return NotFound("未找到用户信息");
}
var submission = await _unitOfWork.GetRepository<Submission>()
.GetFirstOrDefaultAsync(predicate: s => s.Id == submissionId);
if (submission == null)
{
return NotFound("未找到指定的提交记录");
}
// 检查权限:学生只能查看自己的提交,教师可以查看所有提交
if (user.Id != submission.StudentId && !User.IsInRole("Teacher"))
{
return Forbid("您没有权限查看此提交记录");
}
var result = await _studentSubmissionDetailService.GetSubmissionDetailAsync(submissionId);
if (result.Status)
{
return Ok(result.Result);
}
else
{
return BadRequest(result.Message);
}
}
catch (Exception ex)
{
return StatusCode(500, $"获取学生提交详细信息失败: {ex.Message}");
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,97 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace TechHelper.Server.Migrations
{
/// <inheritdoc />
public partial class submission_up_2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("2670f35a-df0c-4071-8879-80eb99d138a1"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("8c6c5e8e-ef00-444c-9c7c-cba5cd6f7043"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("9eda9d90-0cd2-4fbe-b07e-f90bd01f32db"));
migrationBuilder.AlterColumn<float>(
name: "overall_grade",
table: "submissions",
type: "float",
precision: 5,
scale: 2,
nullable: false,
defaultValue: 0f,
oldClrType: typeof(float),
oldType: "float",
oldPrecision: 5,
oldScale: 2,
oldNullable: true);
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ new Guid("264e4290-9d15-478d-8c49-8d0935e5a6e1"), null, "Administrator", "ADMINISTRATOR" },
{ new Guid("73cafcee-3e99-43ae-86c5-c01a1cbc6124"), null, "Teacher", "TEACHER" },
{ new Guid("f06927ff-4bba-4ab6-8f0a-e45a765c2fcc"), null, "Student", "STUDENT" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("264e4290-9d15-478d-8c49-8d0935e5a6e1"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("73cafcee-3e99-43ae-86c5-c01a1cbc6124"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("f06927ff-4bba-4ab6-8f0a-e45a765c2fcc"));
migrationBuilder.AlterColumn<float>(
name: "overall_grade",
table: "submissions",
type: "float",
precision: 5,
scale: 2,
nullable: true,
oldClrType: typeof(float),
oldType: "float",
oldPrecision: 5,
oldScale: 2);
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ new Guid("2670f35a-df0c-4071-8879-80eb99d138a1"), null, "Teacher", "TEACHER" },
{ new Guid("8c6c5e8e-ef00-444c-9c7c-cba5cd6f7043"), null, "Student", "STUDENT" },
{ new Guid("9eda9d90-0cd2-4fbe-b07e-f90bd01f32db"), null, "Administrator", "ADMINISTRATOR" }
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace TechHelper.Server.Migrations
{
/// <inheritdoc />
public partial class submission_up_3 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("264e4290-9d15-478d-8c49-8d0935e5a6e1"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("73cafcee-3e99-43ae-86c5-c01a1cbc6124"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("f06927ff-4bba-4ab6-8f0a-e45a765c2fcc"));
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ new Guid("388fdb1d-8cd5-4e8f-b49c-06dbee60527b"), null, "Administrator", "ADMINISTRATOR" },
{ new Guid("ba4054d5-2f8a-4c7f-bd56-0fc864720c7d"), null, "Teacher", "TEACHER" },
{ new Guid("c758a0d2-faea-4cf1-aa14-d162f3d0a1e9"), null, "Student", "STUDENT" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("388fdb1d-8cd5-4e8f-b49c-06dbee60527b"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("ba4054d5-2f8a-4c7f-bd56-0fc864720c7d"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("c758a0d2-faea-4cf1-aa14-d162f3d0a1e9"));
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ new Guid("264e4290-9d15-478d-8c49-8d0935e5a6e1"), null, "Administrator", "ADMINISTRATOR" },
{ new Guid("73cafcee-3e99-43ae-86c5-c01a1cbc6124"), null, "Teacher", "TEACHER" },
{ new Guid("f06927ff-4bba-4ab6-8f0a-e45a765c2fcc"), null, "Student", "STUDENT" }
});
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace TechHelper.Server.Migrations
{
/// <inheritdoc />
public partial class tee : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("388fdb1d-8cd5-4e8f-b49c-06dbee60527b"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("ba4054d5-2f8a-4c7f-bd56-0fc864720c7d"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("c758a0d2-faea-4cf1-aa14-d162f3d0a1e9"));
migrationBuilder.AddColumn<bool>(
name: "BCorrect",
table: "assignment_questions",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ new Guid("d480cdca-7de2-4abe-8129-73bbaa6c1b32"), null, "Student", "STUDENT" },
{ new Guid("d7bcfb37-3f1c-467b-a3f0-b2339a8a990d"), null, "Teacher", "TEACHER" },
{ new Guid("f4a6788a-04d8-499c-9e64-73dfba97ca6b"), null, "Administrator", "ADMINISTRATOR" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("d480cdca-7de2-4abe-8129-73bbaa6c1b32"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("d7bcfb37-3f1c-467b-a3f0-b2339a8a990d"));
migrationBuilder.DeleteData(
table: "AspNetRoles",
keyColumn: "Id",
keyValue: new Guid("f4a6788a-04d8-499c-9e64-73dfba97ca6b"));
migrationBuilder.DropColumn(
name: "BCorrect",
table: "assignment_questions");
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
values: new object[,]
{
{ new Guid("388fdb1d-8cd5-4e8f-b49c-06dbee60527b"), null, "Administrator", "ADMINISTRATOR" },
{ new Guid("ba4054d5-2f8a-4c7f-bd56-0fc864720c7d"), null, "Teacher", "TEACHER" },
{ new Guid("c758a0d2-faea-4cf1-aa14-d162f3d0a1e9"), null, "Student", "STUDENT" }
});
}
}
}

View File

@@ -175,6 +175,9 @@ namespace TechHelper.Server.Migrations
b.Property<Guid?>("AssignmentId") b.Property<Guid?>("AssignmentId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<bool>("BCorrect")
.HasColumnType("tinyint(1)");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)") .HasColumnType("datetime(6)")
.HasColumnName("created_at"); .HasColumnName("created_at");
@@ -558,7 +561,7 @@ namespace TechHelper.Server.Migrations
.HasColumnType("longtext") .HasColumnType("longtext")
.HasColumnName("overall_feedback"); .HasColumnName("overall_feedback");
b.Property<float?>("OverallGrade") b.Property<float>("OverallGrade")
.HasPrecision(5, 2) .HasPrecision(5, 2)
.HasColumnType("float") .HasColumnType("float")
.HasColumnName("overall_grade"); .HasColumnName("overall_grade");
@@ -800,19 +803,19 @@ namespace TechHelper.Server.Migrations
b.HasData( b.HasData(
new new
{ {
Id = new Guid("8c6c5e8e-ef00-444c-9c7c-cba5cd6f7043"), Id = new Guid("d480cdca-7de2-4abe-8129-73bbaa6c1b32"),
Name = "Student", Name = "Student",
NormalizedName = "STUDENT" NormalizedName = "STUDENT"
}, },
new new
{ {
Id = new Guid("2670f35a-df0c-4071-8879-80eb99d138a1"), Id = new Guid("d7bcfb37-3f1c-467b-a3f0-b2339a8a990d"),
Name = "Teacher", Name = "Teacher",
NormalizedName = "TEACHER" NormalizedName = "TEACHER"
}, },
new new
{ {
Id = new Guid("9eda9d90-0cd2-4fbe-b07e-f90bd01f32db"), Id = new Guid("f4a6788a-04d8-499c-9e64-73dfba97ca6b"),
Name = "Administrator", Name = "Administrator",
NormalizedName = "ADMINISTRATOR" NormalizedName = "ADMINISTRATOR"
}); });

View File

@@ -35,8 +35,10 @@ builder.Services.AddDbContext<ApplicationContext>(options =>
.AddCustomRepository<Question, QuestionRepository>() .AddCustomRepository<Question, QuestionRepository>()
.AddCustomRepository<QuestionContext, QuestionContextRepository>() .AddCustomRepository<QuestionContext, QuestionContextRepository>()
.AddCustomRepository<Submission, SubmissionRepository>() .AddCustomRepository<Submission, SubmissionRepository>()
.AddCustomRepository<User, UserRepository>()
.AddCustomRepository<Global, GlobalRepository>(); .AddCustomRepository<Global, GlobalRepository>();
builder.Services.AddAutoMapper(typeof(AutoMapperProFile).Assembly); builder.Services.AddAutoMapper(typeof(AutoMapperProFile).Assembly);
// 3. 配置服务 (IOptions) // 3. 配置服务 (IOptions)
@@ -90,6 +92,8 @@ builder.Services.AddScoped<IClassService, ClassService>();
builder.Services.AddScoped<IExamService, ExamService>(); builder.Services.AddScoped<IExamService, ExamService>();
builder.Services.AddScoped<IUserSerivces, UserServices>(); builder.Services.AddScoped<IUserSerivces, UserServices>();
builder.Services.AddScoped<ISubmissionServices, SubmissionServices>(); builder.Services.AddScoped<ISubmissionServices, SubmissionServices>();
builder.Services.AddScoped<IStudentSubmissionService, StudentSubmissionService>();
builder.Services.AddScoped<IStudentSubmissionDetailService, StudentSubmissionDetailService>();
builder.Services.AddScoped<IExamRepository, ExamRepository>(); builder.Services.AddScoped<IExamRepository, ExamRepository>();
builder.Services.AddScoped<INoteService, NoteService>(); builder.Services.AddScoped<INoteService, NoteService>();

View File

@@ -0,0 +1,15 @@
using Entities.DTO;
using TechHelper.Services;
namespace TechHelper.Server.Services
{
public interface IStudentSubmissionDetailService
{
/// <summary>
/// 获取学生提交的详细信息
/// </summary>
/// <param name="submissionId">提交ID</param>
/// <returns>学生提交详细信息</returns>
Task<ApiResponse> GetSubmissionDetailAsync(Guid submissionId);
}
}

View File

@@ -0,0 +1,24 @@
using Entities.DTO;
using TechHelper.Services;
namespace TechHelper.Server.Services
{
public interface IStudentSubmissionService : IBaseService<StudentSubmissionSummaryDto, Guid>
{
/// <summary>
/// 获取学生提交的作业摘要列表
/// </summary>
/// <param name="studentId">学生ID</param>
/// <returns>学生提交摘要列表</returns>
Task<ApiResponse> GetStudentSubmissionsAsync(Guid studentId);
/// <summary>
/// 获取学生提交的作业摘要列表(分页)
/// </summary>
/// <param name="studentId">学生ID</param>
/// <param name="pageNumber">页码</param>
/// <param name="pageSize">每页数量</param>
/// <returns>分页的学生提交摘要列表</returns>
Task<ApiResponse> GetStudentSubmissionsPagedAsync(Guid studentId, int pageNumber = 1, int pageSize = 10);
}
}

View File

@@ -0,0 +1,144 @@
using AutoMapper;
using AutoMapper.Internal.Mappers;
using Entities.Contracts;
using Entities.DTO;
using Microsoft.EntityFrameworkCore;
using SharedDATA.Api;
using TechHelper.Context;
using TechHelper.Repository;
using TechHelper.Services;
namespace TechHelper.Server.Services
{
public class StudentSubmissionDetailService : IStudentSubmissionDetailService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IExamService examService;
private readonly IMapper _mapper;
public StudentSubmissionDetailService(
IUnitOfWork unitOfWork,
IExamService examService,
IMapper mapper)
{
_unitOfWork = unitOfWork;
this.examService = examService;
_mapper = mapper;
}
public async Task<ApiResponse> GetSubmissionDetailAsync(Guid submissionId)
{
try
{
// 获取submission基本信息
var submission = await _unitOfWork.GetRepository<Submission>()
.GetAll(s => s.Id == submissionId)
.Include(s => s.Assignment)
.ThenInclude(a => a.Creator)
.FirstOrDefaultAsync();
if (submission == null)
{
return ApiResponse.Error("未找到指定的提交记录");
}
var assignment = await examService.GetAsync(submission.AssignmentId);
if (assignment == null)
{
return ApiResponse.Error("未找到指定的作业");
}
// 获取所有提交详情
var submissionDetails = await _unitOfWork.GetRepository<SubmissionDetail>()
.GetAll(sd => sd.SubmissionId == submissionId)
.Include(sd => sd.AssignmentQuestion)
.ThenInclude(aq => aq.Question)
.ThenInclude(q => q.Lesson)
.ThenInclude(q => q.KeyPoints)
.ToListAsync();
// 获取同作业的所有提交用于排名和成绩分布
var allSubmissions = await _unitOfWork.GetRepository<Submission>()
.GetAll(s => s.AssignmentId == submission.AssignmentId)
.ToListAsync();
// 映射基本信息
var result = _mapper.Map<StudentSubmissionDetailDto>(submission);
result.Assignment = assignment.Result as AssignmentDto ?? new AssignmentDto();
var errorQuestion = submissionDetails
.Where(sd => sd.IsCorrect == false && sd.AssignmentQuestion?.StructType == AssignmentStructType.Question && sd.AssignmentQuestion?.Question != null)
.ToList();
// 计算基础统计
result.TotalQuestions = submissionDetails.Select(x => x.AssignmentQuestion.StructType == AssignmentStructType.Question && x.AssignmentQuestion?.Question != null).Count();
result.ErrorCount = errorQuestion.Count;
result.CorrectCount = result.TotalQuestions - result.ErrorCount;
result.AccuracyRate = result.TotalQuestions > 0 ?
(float)result.CorrectCount / result.TotalQuestions : 0;
// 计算错误类型分布 - 只获取题目类型的错误
result.ErrorTypeDistribution = errorQuestion
.GroupBy(sd => sd.AssignmentQuestion.Question.Type.ToString())
.ToDictionary(g => g.Key, g => g.Count()); ;
// 计算错误类型成绩分布 - 只获取题目类型的错误
result.ErrorTypeScoreDistribution = errorQuestion
.GroupBy(sd => sd.AssignmentQuestion.Question.Type.ToString())
.ToDictionary(g => g.Key, g => g.Sum(sd => sd.PointsAwarded ?? 0));
// 计算成绩排名
var orderedSubmissions = allSubmissions
.OrderByDescending(s => s.OverallGrade)
.ToList();
result.TotalRank = orderedSubmissions.FindIndex(s => s.Id == submissionId) + 1;
SetBCorrect(result.Assignment, errorQuestion);
// 计算成绩分布
result.AllScores = allSubmissions.Select(s => s.OverallGrade).ToList();
result.AverageScore = submission.OverallGrade;
result.ClassAverageScore = allSubmissions.Average(s => s.OverallGrade);
// 计算课文错误分布
result.LessonErrorDistribution = errorQuestion
.Where(eq => eq.AssignmentQuestion.Question.Lesson != null)
.GroupBy(sd => sd.AssignmentQuestion.Question.Lesson.Title)
.ToDictionary(g => g.Key, g => g.Count());
// 计算关键点错误分布
result.KeyPointErrorDistribution = errorQuestion
.Where(eq => eq.AssignmentQuestion.Question.Lesson != null)
.GroupBy(sd => sd.AssignmentQuestion.Question.KeyPoint.Key)
.ToDictionary(g => g.Key, g => g.Count());
return ApiResponse.Success(result: result);
}
catch (Exception ex)
{
return ApiResponse.Error($"获取学生提交详细信息失败: {ex.Message}");
}
}
public void SetBCorrect(AssignmentDto assignment, List<SubmissionDetail> submissionDetails)
{
SetBCorrect(assignment.ExamStruct, submissionDetails);
}
public void SetBCorrect(AssignmentQuestionDto assignmentQuestion, List<SubmissionDetail> submissionDetails)
{
var sd = submissionDetails.FirstOrDefault(x => x.AssignmentQuestionId == assignmentQuestion.Id);
if (sd != null)
assignmentQuestion.BCorrect = sd.AssignmentQuestion.BCorrect;
else
assignmentQuestion.BCorrect = false;
assignmentQuestion.ChildrenAssignmentQuestion?.ForEach(
cq => SetBCorrect(cq, submissionDetails));
}
//Task<ApiResponse> IStudentSubmissionDetailService.GetSubmissionDetailAsync(Guid submissionId)
//{
// throw new NotImplementedException();
//}
}
}

View File

@@ -0,0 +1,142 @@
using AutoMapper;
using Entities.Contracts;
using Entities.DTO;
using Microsoft.EntityFrameworkCore;
using SharedDATA.Api;
using TechHelper.Context;
using TechHelper.Repository;
using TechHelper.Server.Repositories;
using TechHelper.Services;
namespace TechHelper.Server.Services
{
public class StudentSubmissionService : IStudentSubmissionService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMapper _mapper;
private readonly IRepository<Submission> _submissionRepository;
private readonly IRepository<Assignment> _assignmentRepository;
private readonly IRepository<User> _userRepository;
public StudentSubmissionService(
IUnitOfWork unitOfWork,
IMapper mapper,
IRepository<Submission> submissionRepository,
IRepository<Assignment> assignmentRepository,
IRepository<User> userRepository)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
_submissionRepository = submissionRepository;
_assignmentRepository = assignmentRepository;
_userRepository = userRepository;
}
public async Task<ApiResponse> GetStudentSubmissionsAsync(Guid studentId)
{
try
{
var submissions = await _submissionRepository
.GetAll(s => s.StudentId == studentId)
.Include(s => s.Assignment)
.ThenInclude(a => a.Creator)
.OrderByDescending(s => s.SubmissionTime)
.ToListAsync();
var result = new List<StudentSubmissionSummaryDto>();
foreach (var submission in submissions)
{
var summary = new StudentSubmissionSummaryDto
{
Id = submission.Id,
AssignmentName = submission.Assignment?.Title ?? "未知作业",
ErrorCount = await CalculateErrorCountAsync(submission.Id),
CreatedDate = submission.SubmissionTime,
Score = (int)submission.OverallGrade,
TotalQuestions = submission.Assignment?.TotalQuestions ?? 0,
StudentName = submission.Assignment?.Creator?.UserName ?? "未知老师",
Status = submission.Status.ToString()
};
result.Add(summary);
}
return ApiResponse.Success(result: result);
}
catch (Exception ex)
{
return ApiResponse.Error($"获取学生提交信息失败: {ex.Message}");
}
}
public async Task<ApiResponse> GetStudentSubmissionsPagedAsync(Guid studentId, int pageNumber = 1, int pageSize = 10)
{
try
{
var totalCount = await _submissionRepository
.GetAll(s => s.StudentId == studentId)
.CountAsync();
var submissions = await _submissionRepository
.GetAll(s => s.StudentId == studentId)
.Include(s => s.Assignment)
.ThenInclude(a => a.Creator)
.OrderByDescending(s => s.SubmissionTime)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var result = new List<StudentSubmissionSummaryDto>();
foreach (var submission in submissions)
{
var summary = new StudentSubmissionSummaryDto
{
Id = submission.Id,
AssignmentName = submission.Assignment?.Title ?? "未知作业",
ErrorCount = await CalculateErrorCountAsync(submission.Id),
CreatedDate = submission.SubmissionTime,
Score = submission.OverallGrade,
TotalQuestions = submission.Assignment?.TotalQuestions ?? 0,
StudentName = submission.Assignment?.Creator?.UserName ?? "未知老师",
Status = submission.Status.ToString()
};
result.Add(summary);
}
var response = new StudentSubmissionSummaryResponseDto
{
Submissions = result,
TotalCount = totalCount
};
return ApiResponse.Success(result: response);
}
catch (Exception ex)
{
return ApiResponse.Error($"获取学生提交信息失败: {ex.Message}");
}
}
private async Task<int> CalculateErrorCountAsync(Guid submissionId)
{
var submissionDetails = await _unitOfWork.GetRepository<SubmissionDetail>()
.GetAll(sd => sd.SubmissionId == submissionId)
.ToListAsync();
return submissionDetails.Select(x => !x.IsCorrect).Count();
}
// 以下方法是IBaseService接口的实现可以根据需要实现
public Task<ApiResponse> GetAllAsync() => throw new NotImplementedException();
public Task<ApiResponse> GetAsync(Guid id) => throw new NotImplementedException();
public Task<ApiResponse> AddAsync(StudentSubmissionSummaryDto model) => throw new NotImplementedException();
public Task<ApiResponse> UpdateAsync(StudentSubmissionSummaryDto model) => throw new NotImplementedException();
public Task<ApiResponse> DeleteAsync(Guid id) => throw new NotImplementedException();
public Task<ApiResponse> GetAllAsync(QueryParameter query)
{
throw new NotImplementedException();
}
}
}