重构试卷相关内容
This commit is contained in:
@@ -16,10 +16,12 @@ namespace Entities.Contracts
|
|||||||
[Column("id")]
|
[Column("id")]
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
[Required]
|
|
||||||
[Column("question_id")]
|
[Column("question_id")]
|
||||||
[ForeignKey("Question")]
|
public Guid? QuestionId { get; set; } // 设为可空
|
||||||
public Guid QuestionId { get; set; }
|
|
||||||
|
// 当 IsGroup 为 true 时,此为 QuestionGroup 的外键
|
||||||
|
[Column("question_group_id")] // 新增一个外键列
|
||||||
|
public Guid? QuestionGroupId { get; set; } // 设为可空
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("group_id")]
|
[Column("group_id")]
|
||||||
@@ -38,11 +40,16 @@ namespace Entities.Contracts
|
|||||||
[Column("score")]
|
[Column("score")]
|
||||||
public float? Score { get; set; }
|
public float? Score { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Column("bgroup")]
|
||||||
|
public bool IsGroup { get; set; }
|
||||||
|
|
||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public Question Question { get; set; }
|
public Question Question { get; set; }
|
||||||
|
public QuestionGroup QuestionGroup { get; set; }
|
||||||
public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
|
public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
|
||||||
public AssignmentGroup AssignmentGroup { get; set; }
|
public AssignmentGroup AssignmentGroup { get; set; }
|
||||||
|
|
||||||
|
@@ -29,6 +29,9 @@ namespace Entities.Contracts
|
|||||||
[MaxLength(65535)]
|
[MaxLength(65535)]
|
||||||
public string CorrectAnswer { get; set; }
|
public string CorrectAnswer { get; set; }
|
||||||
|
|
||||||
|
[Column("question_group_id")]
|
||||||
|
public Guid? QuestionGroupId { get; set; }
|
||||||
|
|
||||||
[Column("difficulty_level")]
|
[Column("difficulty_level")]
|
||||||
[MaxLength(10)]
|
[MaxLength(10)]
|
||||||
public DifficultyLevel DifficultyLevel { get; set; }
|
public DifficultyLevel DifficultyLevel { get; set; }
|
||||||
@@ -36,6 +39,9 @@ namespace Entities.Contracts
|
|||||||
[Column("subject_area")]
|
[Column("subject_area")]
|
||||||
public SubjectAreaEnum SubjectArea { get; set; }
|
public SubjectAreaEnum SubjectArea { get; set; }
|
||||||
|
|
||||||
|
[Column("options")]
|
||||||
|
public string? Options { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("created_by")]
|
[Column("created_by")]
|
||||||
[ForeignKey("Creator")]
|
[ForeignKey("Creator")]
|
||||||
@@ -55,6 +61,7 @@ namespace Entities.Contracts
|
|||||||
|
|
||||||
// Navigation Properties
|
// Navigation Properties
|
||||||
public User Creator { get; set; }
|
public User Creator { get; set; }
|
||||||
|
public QuestionGroup QuestionGroup { get; set; }
|
||||||
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
|
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
|
||||||
|
|
||||||
public Question()
|
public Question()
|
||||||
|
79
Entities/Contracts/QuestionGroup.cs
Normal file
79
Entities/Contracts/QuestionGroup.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.Contracts
|
||||||
|
{
|
||||||
|
[Table("question_groups")]
|
||||||
|
public class QuestionGroup
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[Column("id")]
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
[Column("title")]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Column("description")]
|
||||||
|
[MaxLength(65535)]
|
||||||
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Column("type")]
|
||||||
|
[MaxLength(50)]
|
||||||
|
public string Type { get; set; }
|
||||||
|
|
||||||
|
[Column("difficulty_level")]
|
||||||
|
[MaxLength(10)]
|
||||||
|
public DifficultyLevel DifficultyLevel { get; set; }
|
||||||
|
|
||||||
|
[Column("subject_area")]
|
||||||
|
public SubjectAreaEnum SubjectArea { get; set; }
|
||||||
|
|
||||||
|
[Column("total_questions")]
|
||||||
|
public int TotalQuestions { get; set; } = 0;
|
||||||
|
|
||||||
|
[Column("parent_question_group")]
|
||||||
|
public Guid? ParentQG { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[Column("created_by")]
|
||||||
|
[ForeignKey("Creator")]
|
||||||
|
public Guid CreatedBy { get; set; }
|
||||||
|
|
||||||
|
[Column("created_at")]
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[Column("updated_at")]
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
[Column("deleted")]
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
|
[Column("valid_group")]
|
||||||
|
public bool ValidGroup { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public User Creator { get; set; }
|
||||||
|
public QuestionGroup ParentQuestionGroup { get; set; }
|
||||||
|
public ICollection<QuestionGroup> ChildQuestionGroups { get; set; }
|
||||||
|
public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
|
||||||
|
public ICollection<Question> Questions { get; set; }
|
||||||
|
|
||||||
|
public QuestionGroup()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid();
|
||||||
|
Questions = new HashSet<Question>();
|
||||||
|
CreatedAt = DateTime.UtcNow;
|
||||||
|
UpdatedAt = DateTime.UtcNow;
|
||||||
|
IsDeleted = false;
|
||||||
|
ValidGroup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -52,7 +52,6 @@ namespace Entities.Contracts
|
|||||||
[Column("deleted")]
|
[Column("deleted")]
|
||||||
public bool IsDeleted { get; set; }
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
// Navigation Properties
|
|
||||||
public Submission Submission { get; set; }
|
public Submission Submission { get; set; }
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
public AssignmentQuestion AssignmentQuestion { get; set; }
|
public AssignmentQuestion AssignmentQuestion { get; set; }
|
||||||
|
@@ -27,7 +27,6 @@ namespace Entities.DTO
|
|||||||
|
|
||||||
public string? Descript { get; set; }
|
public string? Descript { get; set; }
|
||||||
public List<SubQuestionDto> SubQuestions { get; set; } = new List<SubQuestionDto>();
|
public List<SubQuestionDto> SubQuestions { get; set; } = new List<SubQuestionDto>();
|
||||||
|
|
||||||
public List<QuestionGroupDto> SubQuestionGroups { get; set; } = new List<QuestionGroupDto>();
|
public List<QuestionGroupDto> SubQuestionGroups { get; set; } = new List<QuestionGroupDto>();
|
||||||
public bool ValidQuestionGroup { get; set; } = false;
|
public bool ValidQuestionGroup { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
@@ -15,49 +15,44 @@ namespace TechHelper.Client.Exam
|
|||||||
dto.SubjectArea = examPaper.SubjectArea;
|
dto.SubjectArea = examPaper.SubjectArea;
|
||||||
dto.QuestionGroups.Title = examPaper.AssignmentTitle;
|
dto.QuestionGroups.Title = examPaper.AssignmentTitle;
|
||||||
dto.QuestionGroups.Descript = examPaper.Description;
|
dto.QuestionGroups.Descript = examPaper.Description;
|
||||||
// 处理顶级 QuestionGroups
|
|
||||||
foreach (var qg in examPaper.QuestionGroups)
|
foreach (var qg in examPaper.QuestionGroups)
|
||||||
{
|
{
|
||||||
var qgd = new QuestionGroupDto();
|
var qgd = new QuestionGroupDto();
|
||||||
// 顶级 QuestionGroup,其父组当然无效 (false),所以 isParentGroupValidChain 为 false
|
|
||||||
ParseMajorQuestionGroup(qg, qgd, false);
|
ParseMajorQuestionGroup(qg, qgd, false);
|
||||||
dto.QuestionGroups.SubQuestionGroups.Add(qgd);
|
dto.QuestionGroups.SubQuestionGroups.Add(qgd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 TopLevelQuestions
|
|
||||||
foreach (var question in examPaper.TopLevelQuestions)
|
foreach (var question in examPaper.TopLevelQuestions)
|
||||||
{
|
{
|
||||||
// 对于 TopLevelQuestions,它们没有父组,所以 isParentGroupValidChain 初始为 false
|
|
||||||
// 如果顶级 Question 包含子问题,则将其视为一个 QuestionGroupDto
|
|
||||||
if (question.SubQuestions != null && question.SubQuestions.Any())
|
if (question.SubQuestions != null && question.SubQuestions.Any())
|
||||||
{
|
{
|
||||||
var qgDto = new QuestionGroupDto
|
var qgDto = new QuestionGroupDto
|
||||||
{
|
{
|
||||||
Title = question.Stem,
|
Title = question.Stem,
|
||||||
Score = (int)question.Score,
|
Score = (int)question.Score,
|
||||||
Descript = "", // 顶级 Question 默认无描述
|
Descript = "",
|
||||||
};
|
};
|
||||||
// 判断当前组是否有效:如果有描述,则为有效组
|
|
||||||
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
|
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
|
||||||
|
|
||||||
// 传递给子项的 isParentGroupValidChain 状态:如果当前组有效,则传递 true;否则继承父级状态 (此处为 false)
|
|
||||||
ParseQuestionWithSubQuestions(question, qgDto, qgDto.ValidQuestionGroup);
|
ParseQuestionWithSubQuestions(question, qgDto, qgDto.ValidQuestionGroup);
|
||||||
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
|
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
|
||||||
}
|
}
|
||||||
else // 如果顶级 Question 没有子问题,则它本身就是一个独立的 SubQuestionDto,放在一个容器 QuestionGroupDto 中
|
else
|
||||||
{
|
{
|
||||||
var qgDto = new QuestionGroupDto
|
var qgDto = new QuestionGroupDto
|
||||||
{
|
{
|
||||||
Title = question.Stem,
|
Title = question.Stem,
|
||||||
Score = (int)question.Score,
|
Score = (int)question.Score,
|
||||||
Descript = "", // 独立题目的容器组通常无描述
|
Descript = "",
|
||||||
};
|
};
|
||||||
// 独立题目的容器组,如果没有描述,则不是“有效组”
|
|
||||||
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
|
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
|
||||||
|
|
||||||
var subQuestionDto = new SubQuestionDto();
|
var subQuestionDto = new SubQuestionDto();
|
||||||
// 此时,qgDto.ValidQuestionGroup 为 false,所以传入 true,表示题目是有效的
|
|
||||||
// 因为其父组链 (此处为自身) 不是有效组
|
|
||||||
ParseSingleQuestion(question, subQuestionDto, !qgDto.ValidQuestionGroup);
|
ParseSingleQuestion(question, subQuestionDto, !qgDto.ValidQuestionGroup);
|
||||||
qgDto.SubQuestions.Add(subQuestionDto);
|
qgDto.SubQuestions.Add(subQuestionDto);
|
||||||
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
|
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
|
||||||
@@ -67,28 +62,25 @@ namespace TechHelper.Client.Exam
|
|||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 MajorQuestionGroup 及其子项
|
|
||||||
// isParentGroupValidChain 参数表示从顶层到当前组的任一父组是否已经是“有效组”
|
|
||||||
private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain)
|
private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain)
|
||||||
{
|
{
|
||||||
qgd.Title = qg.Title;
|
qgd.Title = qg.Title;
|
||||||
qgd.Score = (int)qg.Score;
|
qgd.Score = (int)qg.Score;
|
||||||
qgd.Descript = qg.Descript;
|
qgd.Descript = qg.Descript;
|
||||||
|
|
||||||
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
|
|
||||||
qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qg.Descript) && !isParentGroupValidChain;
|
qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qg.Descript) && !isParentGroupValidChain;
|
||||||
|
|
||||||
// 更新传递给子项的 isParentGroupValidChain 状态:
|
|
||||||
// 如果当前组是有效组 (即 qgd.ValidQuestionGroup 为 true),那么子项的父级链就包含了有效组
|
|
||||||
// 否则,子项的父级链有效性继承自其父级 (isParentGroupValidChain)
|
|
||||||
bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain;
|
bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain;
|
||||||
|
|
||||||
// 处理子 QuestionGroup
|
|
||||||
if (qg.SubQuestionGroups != null)
|
if (qg.SubQuestionGroups != null)
|
||||||
{
|
{
|
||||||
qg.SubQuestionGroups.ForEach(sqg =>
|
qg.SubQuestionGroups.ForEach(sqg =>
|
||||||
{
|
{
|
||||||
var sqgd = new QuestionGroupDto();
|
var sqgd = new QuestionGroupDto();
|
||||||
|
sqgd.Index = (byte)qg.SubQuestionGroups.IndexOf(sqg);
|
||||||
ParseMajorQuestionGroup(sqg, sqgd, nextIsParentGroupValidChain);
|
ParseMajorQuestionGroup(sqg, sqgd, nextIsParentGroupValidChain);
|
||||||
qgd.SubQuestionGroups.Add(sqgd);
|
qgd.SubQuestionGroups.Add(sqgd);
|
||||||
});
|
});
|
||||||
@@ -105,6 +97,7 @@ namespace TechHelper.Client.Exam
|
|||||||
var subQgd = new QuestionGroupDto
|
var subQgd = new QuestionGroupDto
|
||||||
{
|
{
|
||||||
Title = sq.Stem,
|
Title = sq.Stem,
|
||||||
|
Index = (byte)qg.SubQuestions.IndexOf(sq),
|
||||||
Score = (int)sq.Score,
|
Score = (int)sq.Score,
|
||||||
Descript = "" // 默认为空
|
Descript = "" // 默认为空
|
||||||
};
|
};
|
||||||
@@ -119,6 +112,7 @@ namespace TechHelper.Client.Exam
|
|||||||
var subQd = new SubQuestionDto();
|
var subQd = new SubQuestionDto();
|
||||||
// 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效
|
// 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效
|
||||||
ParseSingleQuestion(sq, subQd, !nextIsParentGroupValidChain);
|
ParseSingleQuestion(sq, subQd, !nextIsParentGroupValidChain);
|
||||||
|
subQd.Index = (byte)qg.SubQuestions.IndexOf(sq);
|
||||||
qgd.SubQuestions.Add(subQd);
|
qgd.SubQuestions.Add(subQd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,6 +185,26 @@ namespace TechHelper.Client.Exam
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void SeqIndex(this ExamDto dto)
|
||||||
|
{
|
||||||
|
dto.QuestionGroups.SeqQGroupIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void SeqQGroupIndex(this QuestionGroupDto dto)
|
||||||
|
{
|
||||||
|
dto.SubQuestions?.ForEach(sq =>
|
||||||
|
{
|
||||||
|
sq.Index = (byte)dto.SubQuestions.IndexOf(sq);
|
||||||
|
});
|
||||||
|
|
||||||
|
dto.SubQuestionGroups?.ForEach(sqg =>
|
||||||
|
{
|
||||||
|
sqg.Index = (byte)dto.SubQuestionGroups.IndexOf(sqg);
|
||||||
|
sqg.SeqQGroupIndex();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public static string SerializeExamDto(this ExamDto dto)
|
public static string SerializeExamDto(this ExamDto dto)
|
||||||
{
|
{
|
||||||
|
@@ -101,6 +101,7 @@
|
|||||||
Snackbar.Add("试卷解析成功。", Severity.Success);
|
Snackbar.Add("试卷解析成功。", Severity.Success);
|
||||||
Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success);
|
Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success);
|
||||||
ExamContent = _parsedExam.ConvertToExamDTO();
|
ExamContent = _parsedExam.ConvertToExamDTO();
|
||||||
|
ExamContent.SeqIndex();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Identity;
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Entities.Contracts;
|
using Entities.Contracts;
|
||||||
|
using TechHelper.Server.Context.Configuration;
|
||||||
|
|
||||||
namespace TechHelper.Context
|
namespace TechHelper.Context
|
||||||
{
|
{
|
||||||
@@ -35,6 +36,7 @@ namespace TechHelper.Context
|
|||||||
builder.ApplyConfiguration(new ClassTeacherConfiguration());
|
builder.ApplyConfiguration(new ClassTeacherConfiguration());
|
||||||
builder.ApplyConfiguration(new QuestionConfiguration());
|
builder.ApplyConfiguration(new QuestionConfiguration());
|
||||||
builder.ApplyConfiguration(new SubmissionConfiguration());
|
builder.ApplyConfiguration(new SubmissionConfiguration());
|
||||||
|
builder.ApplyConfiguration(new QuestionGroupConfiguration());
|
||||||
builder.ApplyConfiguration(new SubmissionDetailConfiguration());
|
builder.ApplyConfiguration(new SubmissionDetailConfiguration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -59,8 +59,31 @@ namespace TechHelper.Context
|
|||||||
.ForMember(dest => dest.AssignmentTitle, opt => opt.MapFrom(src => src.Title))
|
.ForMember(dest => dest.AssignmentTitle, opt => opt.MapFrom(src => src.Title))
|
||||||
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Description))
|
||||||
.ForMember(dest => dest.AssignmentId, opt => opt.MapFrom(src => src.Id))
|
.ForMember(dest => dest.AssignmentId, opt => opt.MapFrom(src => src.Id))
|
||||||
|
.ForMember(dest => dest.QuestionGroups, opt => opt.MapFrom(src =>
|
||||||
|
src.AssignmentGroups.FirstOrDefault(ag => ag.ParentGroup == null)))
|
||||||
.ForMember(dest => dest.SubjectArea, opt => opt.MapFrom(src => src.SubjectArea.ToString()));
|
.ForMember(dest => dest.SubjectArea, opt => opt.MapFrom(src => src.SubjectArea.ToString()));
|
||||||
|
|
||||||
|
CreateMap<AssignmentGroup, QuestionGroupDto>()
|
||||||
|
.ForMember(dest => dest.SubQuestionGroups, opt => opt.MapFrom(src => src.ChildAssignmentGroups))
|
||||||
|
.ForMember(dest => dest.SubQuestions, opt => opt.MapFrom(src => src.AssignmentQuestions));
|
||||||
|
|
||||||
|
CreateMap<AssignmentQuestion, SubQuestionDto>()
|
||||||
|
.ForMember(dest => dest.Stem, opt => opt.MapFrom(src => src.Question.QuestionText))
|
||||||
|
.ForMember(dest => dest.SampleAnswer, opt => opt.MapFrom(src => src.Question.CorrectAnswer))
|
||||||
|
.ForMember(dest => dest.QuestionType, opt => opt.MapFrom(src => src.Question.QuestionType.ToString()))
|
||||||
|
.ForMember(dest => dest.DifficultyLevel, opt => opt.MapFrom(src => src.Question.DifficultyLevel.ToString()));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CreateMap<QuestionGroupDto, AssignmentGroup>()
|
||||||
|
.ForMember(dest => dest.ChildAssignmentGroups, opt => opt.MapFrom(src => src.SubQuestionGroups))
|
||||||
|
.ForMember(dest => dest.AssignmentQuestions, opt => opt.MapFrom(src => src.SubQuestions));
|
||||||
|
|
||||||
|
CreateMap<SubQuestionDto, AssignmentQuestion>()
|
||||||
|
.ForMember(dest => dest.Question, opt => opt.MapFrom(src => src)); // 映射到嵌套的 Question 对象
|
||||||
|
|
||||||
|
|
||||||
|
CreateMap<Assignment, ExamDto>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -22,8 +22,12 @@ namespace TechHelper.Context.Configuration
|
|||||||
|
|
||||||
// 配置 QuestionId 列 (已修正拼写)
|
// 配置 QuestionId 列 (已修正拼写)
|
||||||
builder.Property(aq => aq.QuestionId)
|
builder.Property(aq => aq.QuestionId)
|
||||||
.HasColumnName("question_id")
|
.HasColumnName("question_id");
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
|
builder.Property(aq => aq.QuestionGroupId)
|
||||||
|
.HasColumnName("question_group_id");
|
||||||
|
|
||||||
|
|
||||||
// 配置 QuestionNumber 列
|
// 配置 QuestionNumber 列
|
||||||
builder.Property(aq => aq.QuestionNumber)
|
builder.Property(aq => aq.QuestionNumber)
|
||||||
@@ -44,6 +48,10 @@ namespace TechHelper.Context.Configuration
|
|||||||
.HasColumnName("group_id")
|
.HasColumnName("group_id")
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
|
|
||||||
|
builder.Property(aq => aq.IsGroup)
|
||||||
|
.HasColumnName("is_group") // 修正为一致的列名
|
||||||
|
.IsRequired(); // IsGroup 应该是必需的
|
||||||
// 配置 IsDeleted 列
|
// 配置 IsDeleted 列
|
||||||
builder.Property(aq => aq.IsDeleted)
|
builder.Property(aq => aq.IsDeleted)
|
||||||
.HasColumnName("deleted")
|
.HasColumnName("deleted")
|
||||||
@@ -61,6 +69,12 @@ namespace TechHelper.Context.Configuration
|
|||||||
.HasForeignKey(aq => aq.QuestionId) // 外键是 AssignmentQuestion.QuestionId
|
.HasForeignKey(aq => aq.QuestionId) // 外键是 AssignmentQuestion.QuestionId
|
||||||
.OnDelete(DeleteBehavior.Cascade); // 当 Question 被删除时,相关的 AssignmentQuestion 也级联删除。
|
.OnDelete(DeleteBehavior.Cascade); // 当 Question 被删除时,相关的 AssignmentQuestion 也级联删除。
|
||||||
|
|
||||||
|
builder.HasOne(aq => aq.QuestionGroup)
|
||||||
|
.WithMany(qg => qg.AssignmentQuestions)
|
||||||
|
.HasForeignKey(aq => aq.QuestionGroupId)
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// 配置 AssignmentQuestion 到 AssignmentGroup 的关系 (多对一)
|
// 配置 AssignmentQuestion 到 AssignmentGroup 的关系 (多对一)
|
||||||
// 一个 AssignmentQuestion 属于一个 AssignmentGroup。
|
// 一个 AssignmentQuestion 属于一个 AssignmentGroup。
|
||||||
|
@@ -14,6 +14,8 @@ namespace TechHelper.Context.Configuration
|
|||||||
// 2. 设置主键
|
// 2. 设置主键
|
||||||
builder.HasKey(q => q.Id);
|
builder.HasKey(q => q.Id);
|
||||||
|
|
||||||
|
builder.HasIndex(q => q.QuestionText);
|
||||||
|
|
||||||
// 3. 配置列名、必需性、长度及其他属性
|
// 3. 配置列名、必需性、长度及其他属性
|
||||||
|
|
||||||
// Id
|
// Id
|
||||||
@@ -21,6 +23,11 @@ namespace TechHelper.Context.Configuration
|
|||||||
.HasColumnName("id");
|
.HasColumnName("id");
|
||||||
// 对于 Guid 类型的主键,EF Core 默认由应用程序生成值,无需 ValueGeneratedOnAdd()
|
// 对于 Guid 类型的主键,EF Core 默认由应用程序生成值,无需 ValueGeneratedOnAdd()
|
||||||
|
|
||||||
|
builder.Property(q => q.QuestionGroupId)
|
||||||
|
.HasColumnName("question_group_id")
|
||||||
|
.IsRequired(false); // 可为空,因为题目不一定属于某个题组
|
||||||
|
|
||||||
|
|
||||||
// QuestionText
|
// QuestionText
|
||||||
builder.Property(q => q.QuestionText)
|
builder.Property(q => q.QuestionText)
|
||||||
.HasColumnName("question_text")
|
.HasColumnName("question_text")
|
||||||
@@ -97,6 +104,13 @@ namespace TechHelper.Context.Configuration
|
|||||||
builder.HasMany(q => q.AssignmentQuestions) // 当前 Question 有多个 AssignmentQuestion
|
builder.HasMany(q => q.AssignmentQuestions) // 当前 Question 有多个 AssignmentQuestion
|
||||||
.WithOne(aq => aq.Question); // 每一个 AssignmentQuestion 都有一个 Question
|
.WithOne(aq => aq.Question); // 每一个 AssignmentQuestion 都有一个 Question
|
||||||
// .HasForeignKey(aq => aq.QuestionId); // 外键的配置应在 `AssignmentQuestionConfiguration` 中进行
|
// .HasForeignKey(aq => aq.QuestionId); // 外键的配置应在 `AssignmentQuestionConfiguration` 中进行
|
||||||
|
|
||||||
|
builder.HasOne(q => q.QuestionGroup) // Question 实体中的 QuestionGroup 导航属性
|
||||||
|
.WithMany(qg => qg.Questions) // QuestionGroup 实体中的 Questions 集合
|
||||||
|
.HasForeignKey(q => q.QuestionGroupId) // Question 实体中的 QuestionGroupId 外键
|
||||||
|
.IsRequired(false) // QuestionGroupId 在 Question 实体中是可空的
|
||||||
|
.OnDelete(DeleteBehavior.SetNull); // 如果 QuestionGroup 被删除,关联的 Question 的外键设置为 NULL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,111 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Entities.Contracts;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Context.Configuration
|
||||||
|
{
|
||||||
|
public class QuestionGroupConfiguration : IEntityTypeConfiguration<QuestionGroup>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<QuestionGroup> builder)
|
||||||
|
{
|
||||||
|
// 1. 设置表名
|
||||||
|
builder.ToTable("question_groups");
|
||||||
|
|
||||||
|
// 2. 设置主键
|
||||||
|
builder.HasKey(qg => qg.Id);
|
||||||
|
|
||||||
|
// 3. 配置列属性
|
||||||
|
|
||||||
|
// Title 标题
|
||||||
|
builder.Property(qg => qg.Title)
|
||||||
|
.HasColumnName("title")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.IsRequired(false); // 允许为空
|
||||||
|
|
||||||
|
// Description 描述内容 (Required)
|
||||||
|
builder.Property(qg => qg.Description)
|
||||||
|
.HasColumnName("description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext"); // 对应 MySQL 的 TEXT 或 LONGTEXT
|
||||||
|
|
||||||
|
// Type 类型 (例如: "ReadingComprehension", "DiagramAnalysis")
|
||||||
|
builder.Property(qg => qg.Type)
|
||||||
|
.HasColumnName("type")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.IsRequired(false); // 允许为空
|
||||||
|
|
||||||
|
// DifficultyLevel 难度级别 (枚举映射为字符串)
|
||||||
|
builder.Property(qg => qg.DifficultyLevel)
|
||||||
|
.HasColumnName("difficulty_level")
|
||||||
|
.HasConversion<string>() // 将枚举转换为字符串存储
|
||||||
|
.HasMaxLength(10);
|
||||||
|
|
||||||
|
// SubjectArea 科目领域 (枚举映射为字符串)
|
||||||
|
builder.Property(qg => qg.SubjectArea)
|
||||||
|
.HasColumnName("subject_area")
|
||||||
|
.HasConversion<string>(); // 将枚举转换为字符串存储
|
||||||
|
|
||||||
|
// TotalQuestions 包含题目总数
|
||||||
|
builder.Property(qg => qg.TotalQuestions)
|
||||||
|
.HasColumnName("total_questions")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// ParentQG 父题组 ID (外键,自引用关系)
|
||||||
|
builder.Property(qg => qg.ParentQG)
|
||||||
|
.HasColumnName("parent_question_group") // 使用你定义的列名
|
||||||
|
.IsRequired(false); // 可为空,因为根题组没有父级
|
||||||
|
|
||||||
|
// CreatedBy 创建者 ID (外键)
|
||||||
|
builder.Property(qg => qg.CreatedBy)
|
||||||
|
.HasColumnName("created_by")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// CreatedAt 创建时间
|
||||||
|
builder.Property(qg => qg.CreatedAt)
|
||||||
|
.HasColumnName("created_at")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// UpdatedAt 更新时间
|
||||||
|
builder.Property(qg => qg.UpdatedAt)
|
||||||
|
.HasColumnName("updated_at")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// IsDeleted 是否删除 (软删除)
|
||||||
|
builder.Property(qg => qg.IsDeleted)
|
||||||
|
.HasColumnName("deleted")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// ValidGroup 是否有效
|
||||||
|
builder.Property(qg => qg.ValidGroup)
|
||||||
|
.HasColumnName("valid_group")
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
// 4. 配置关系
|
||||||
|
|
||||||
|
// 与 User 的关系 (创建者)
|
||||||
|
builder.HasOne(qg => qg.Creator)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(qg => qg.CreatedBy)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict); // 阻止删除关联的 User
|
||||||
|
|
||||||
|
// 与 Question 的关系 (一对多)
|
||||||
|
// 一个 QuestionGroup 可以包含多个 Question
|
||||||
|
builder.HasMany(qg => qg.Questions)
|
||||||
|
.WithOne(q => q.QuestionGroup) // Question 实体中的 QuestionGroup 导航属性
|
||||||
|
.HasForeignKey(q => q.QuestionGroupId) // Question 实体中的 QuestionGroupId 外键
|
||||||
|
.IsRequired(false) // QuestionGroupId 在 Question 实体中是可空的
|
||||||
|
.OnDelete(DeleteBehavior.SetNull); // 如果 QuestionGroup 被删除,关联的 Question 的外键设置为 NULL
|
||||||
|
|
||||||
|
// 与自身的自引用关系 (父子题组)
|
||||||
|
// 一个 QuestionGroup 可以有多个 ChildQuestionGroups
|
||||||
|
builder.HasMany(qg => qg.ChildQuestionGroups)
|
||||||
|
.WithOne(childQG => childQG.ParentQuestionGroup) // 子 QuestionGroup 实体中的 ParentQuestionGroup 导航属性
|
||||||
|
.HasForeignKey(childQG => childQG.ParentQG) // 子 QuestionGroup 实体中的 ParentQG 外键
|
||||||
|
.IsRequired(false) // ParentQG 是可空的,因为根题组没有父级
|
||||||
|
.OnDelete(DeleteBehavior.Restrict); // 或者 SetNull, Cascade。Restrict 更安全,避免意外删除整个分支。
|
||||||
|
// 如果选择 SetNull,删除父组时子组的 ParentQG 会变为 NULL,它们就成了新的根组。
|
||||||
|
// 如果选择 Cascade,删除父组会递归删除所有子组。根据业务逻辑选择。
|
||||||
|
// 这里我选择了 Restrict 作为默认安全选项。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
TechHelper.Server/Repository/ExamRepository.cs
Normal file
84
TechHelper.Server/Repository/ExamRepository.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedDATA.Api;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Repository
|
||||||
|
{
|
||||||
|
public class ExamRepository : IExamRepository
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IRepository<Assignment> _assignmentRepo;
|
||||||
|
private readonly IRepository<AssignmentGroup> _assignmentGroupRepo;
|
||||||
|
|
||||||
|
public ExamRepository(IUnitOfWork unitOfWork)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_assignmentRepo = _unitOfWork.GetRepository<Assignment>();
|
||||||
|
_assignmentGroupRepo = _unitOfWork.GetRepository<AssignmentGroup>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Assignment?> GetFullExamByIdAsync(Guid assignmentId)
|
||||||
|
{
|
||||||
|
|
||||||
|
var assignment = await _assignmentRepo.GetFirstOrDefaultAsync(
|
||||||
|
predicate: a => a.Id == assignmentId && !a.IsDeleted,
|
||||||
|
include: source => source
|
||||||
|
.Include
|
||||||
|
(a => a.AssignmentGroups.Where(ag => ag.ParentGroup == null && !ag.IsDeleted)) // 加载根题组
|
||||||
|
.ThenInclude(ag => ag.ChildAssignmentGroups.Where(cag => !cag.IsDeleted)) // 加载子题组
|
||||||
|
.ThenInclude(cag => cag.AssignmentQuestions.Where(aq => !aq.IsDeleted)) // 加载子题组的题目
|
||||||
|
.ThenInclude(aq => aq.Question)
|
||||||
|
.Include(a => a.AssignmentGroups.Where(ag => ag.ParentGroup == null && !ag.IsDeleted)) // 再次从根开始,加载题组下的题目
|
||||||
|
.ThenInclude(ag => ag.AssignmentQuestions.Where(aq => !aq.IsDeleted))
|
||||||
|
.ThenInclude(aq => aq.Question)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assignment?.AssignmentGroups != null)
|
||||||
|
{
|
||||||
|
foreach (var rootGroup in assignment.AssignmentGroups)
|
||||||
|
{
|
||||||
|
await LoadSubGroupsRecursive(rootGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task LoadSubGroupsRecursive(AssignmentGroup group)
|
||||||
|
{
|
||||||
|
// EF Core 已经加载了下一层,我们需要确保更深层次的加载
|
||||||
|
var groupWithChildren = await _assignmentGroupRepo.GetFirstOrDefaultAsync(
|
||||||
|
predicate: g => g.Id == group.Id,
|
||||||
|
include: source => source
|
||||||
|
.Include(g => g.ChildAssignmentGroups.Where(cg => !cg.IsDeleted))
|
||||||
|
.ThenInclude(cg => cg.AssignmentQuestions.Where(aq => !aq.IsDeleted))
|
||||||
|
.ThenInclude(aq => aq.Question)
|
||||||
|
.Include(g => g.AssignmentQuestions.Where(aq => !aq.IsDeleted))
|
||||||
|
.ThenInclude(aq => aq.Question)
|
||||||
|
);
|
||||||
|
|
||||||
|
group.ChildAssignmentGroups = groupWithChildren.ChildAssignmentGroups;
|
||||||
|
group.AssignmentQuestions = groupWithChildren.AssignmentQuestions;
|
||||||
|
|
||||||
|
if (group.ChildAssignmentGroups != null)
|
||||||
|
{
|
||||||
|
foreach (var child in group.ChildAssignmentGroups)
|
||||||
|
{
|
||||||
|
await LoadSubGroupsRecursive(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Assignment>> GetExamPreviewsByUserAsync(Guid userId)
|
||||||
|
{
|
||||||
|
return await _assignmentRepo.GetAllAsync(
|
||||||
|
predicate: a => a.CreatedBy == userId && !a.IsDeleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(Assignment assignment)
|
||||||
|
{
|
||||||
|
await _assignmentRepo.InsertAsync(assignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
TechHelper.Server/Repository/IExamRepository.cs
Normal file
27
TechHelper.Server/Repository/IExamRepository.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Repository
|
||||||
|
{
|
||||||
|
public interface IExamRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据ID异步获取一个完整的试卷实体,包括所有子题组和题目。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assignmentId">试卷ID</param>
|
||||||
|
/// <returns>完整的 Assignment 实体,如果找不到则返回 null。</returns>
|
||||||
|
Task<Assignment?> GetFullExamByIdAsync(Guid assignmentId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定用户创建的所有试卷的预览信息。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">用户ID</param>
|
||||||
|
/// <returns>Assignment 实体集合。</returns>
|
||||||
|
Task<IEnumerable<Assignment>> GetExamPreviewsByUserAsync(Guid userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 向数据库添加一个新的试卷。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assignment">要添加的试卷实体。</param>
|
||||||
|
Task AddAsync(Assignment assignment);
|
||||||
|
}
|
||||||
|
}
|
@@ -351,8 +351,16 @@ namespace TechHelper.Server.Services
|
|||||||
Guid assignmentId,
|
Guid assignmentId,
|
||||||
Guid? parentAssignmentGroupId,
|
Guid? parentAssignmentGroupId,
|
||||||
Guid createdById)
|
Guid createdById)
|
||||||
|
{
|
||||||
|
if (qgDto.ValidQuestionGroup)
|
||||||
|
{
|
||||||
|
await SaveQuestionGroup(qgDto);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
byte groupNumber = 1;
|
byte groupNumber = 1;
|
||||||
|
|
||||||
|
|
||||||
var newAssignmentGroup = new AssignmentGroup
|
var newAssignmentGroup = new AssignmentGroup
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(), // 后端生成 GUID
|
Id = Guid.NewGuid(), // 后端生成 GUID
|
||||||
@@ -396,6 +404,8 @@ namespace TechHelper.Server.Services
|
|||||||
questionNumber++;
|
questionNumber++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 递归处理子题组
|
// 递归处理子题组
|
||||||
// 这里需要遍历 SubQuestionGroups,并对每个子组进行递归调用
|
// 这里需要遍历 SubQuestionGroups,并对每个子组进行递归调用
|
||||||
foreach (var subQgDto in qgDto.SubQuestionGroups.OrderBy(s => s.Index))
|
foreach (var subQgDto in qgDto.SubQuestionGroups.OrderBy(s => s.Index))
|
||||||
@@ -408,6 +418,12 @@ namespace TechHelper.Server.Services
|
|||||||
createdById);
|
createdById);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveQuestionGroup(QuestionGroupDto qgDto)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ApiResponse> GetAllExamPreview(Guid user)
|
public async Task<ApiResponse> GetAllExamPreview(Guid user)
|
||||||
{
|
{
|
||||||
|
115
TechHelper.Server/Services/ExamService2.cs
Normal file
115
TechHelper.Server/Services/ExamService2.cs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
|
using SharedDATA.Api;
|
||||||
|
using TechHelper.Server.Repository;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
|
||||||
|
public class ExamService2 : IExamService2
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IExamRepository _examRepository;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
|
||||||
|
public ExamService2(IUnitOfWork unitOfWork, IExamRepository examRepository, IMapper mapper)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_examRepository = examRepository;
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Guid> CreateExamAsync(ExamDto examDto, Guid creatorId)
|
||||||
|
{
|
||||||
|
if (examDto.QuestionGroups == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("试卷必须包含一个根题组。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 AutoMapper 将 DTO 映射到实体
|
||||||
|
var assignment = _mapper.Map<Assignment>(examDto);
|
||||||
|
|
||||||
|
// 设置后端生成的属性
|
||||||
|
assignment.Id = Guid.NewGuid();
|
||||||
|
assignment.CreatedBy = creatorId;
|
||||||
|
assignment.CreatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// 递归设置所有子实体的ID和关联关系
|
||||||
|
SetEntityIdsAndRelations(assignment.AssignmentGroups.First(), assignment.Id, creatorId);
|
||||||
|
|
||||||
|
await _examRepository.AddAsync(assignment);
|
||||||
|
await _unitOfWork.SaveChangesAsync();
|
||||||
|
|
||||||
|
return assignment.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetEntityIdsAndRelations(AssignmentGroup group, Guid? assignmentId, Guid creatorId)
|
||||||
|
{
|
||||||
|
group.Id = Guid.NewGuid();
|
||||||
|
group.AssignmentId = assignmentId;
|
||||||
|
|
||||||
|
foreach (var aq in group.AssignmentQuestions)
|
||||||
|
{
|
||||||
|
aq.Id = Guid.NewGuid();
|
||||||
|
aq.AssignmentGroupId = group.Id;
|
||||||
|
aq.Question.Id = Guid.NewGuid();
|
||||||
|
aq.Question.CreatedBy = creatorId;
|
||||||
|
aq.CreatedAt = DateTime.UtcNow;
|
||||||
|
// ... 其他默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var childGroup in group.ChildAssignmentGroups)
|
||||||
|
{
|
||||||
|
// 子题组的 AssignmentId 为 null,通过 ParentGroup 关联
|
||||||
|
SetEntityIdsAndRelations(childGroup, null, creatorId);
|
||||||
|
childGroup.ParentGroup = group.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ExamDto> GetExamByIdAsync(Guid id)
|
||||||
|
{
|
||||||
|
var assignment = await _examRepository.GetFullExamByIdAsync(id);
|
||||||
|
if (assignment == null)
|
||||||
|
{
|
||||||
|
|
||||||
|
throw new InvalidOperationException("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _mapper.Map<ExamDto>(assignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ExamDto>> GetAllExamPreviewsAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var assignments = await _examRepository.GetExamPreviewsByUserAsync(userId);
|
||||||
|
return _mapper.Map<IEnumerable<ExamDto>>(assignments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> GetAllAsync(QueryParameter query)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> GetAsync(Guid id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> AddAsync(ExamDto model)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> UpdateAsync(ExamDto model)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
TechHelper.Server/Services/IAssignmentGroupService.cs
Normal file
9
TechHelper.Server/Services/IAssignmentGroupService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public interface IAssignmentGroupService : IBaseService<AssignmentGroup, Guid>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
using Entities.DTO;
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
using TechHelper.Services;
|
using TechHelper.Services;
|
||||||
|
|
||||||
namespace TechHelper.Server.Services
|
namespace TechHelper.Server.Services
|
||||||
@@ -6,5 +7,6 @@ namespace TechHelper.Server.Services
|
|||||||
public interface IExamService : IBaseService<ExamDto, Guid>
|
public interface IExamService : IBaseService<ExamDto, Guid>
|
||||||
{
|
{
|
||||||
Task<ApiResponse> GetAllExamPreview(Guid user);
|
Task<ApiResponse> GetAllExamPreview(Guid user);
|
||||||
|
QuestionGroupDto MapAssignmentGroupToDto(AssignmentGroup ag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
26
TechHelper.Server/Services/IExamService2.cs
Normal file
26
TechHelper.Server/Services/IExamService2.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public interface IExamService2 : IBaseService<ExamDto, Guid>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 ID 获取试卷 DTO。
|
||||||
|
/// </summary>
|
||||||
|
Task<ExamDto> GetExamByIdAsync(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定用户的所有试卷预览。
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<ExamDto>> GetAllExamPreviewsAsync(Guid userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个新的试卷。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>创建成功的试卷ID</returns>
|
||||||
|
Task<Guid> CreateExamAsync(ExamDto examDto, Guid creatorId);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
9
TechHelper.Server/Services/IQuestionGroupService.cs
Normal file
9
TechHelper.Server/Services/IQuestionGroupService.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public interface IQuestionGroupService : IBaseService<QuestionGroup, Guid>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
11
TechHelper.Server/Services/IQuestionService.cs
Normal file
11
TechHelper.Server/Services/IQuestionService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Entities.Contracts;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public interface IQuestionService : IBaseService<Question, Guid>
|
||||||
|
{
|
||||||
|
Task<ApiResponse> FindByTitle(string title);
|
||||||
|
Task<ApiResponse> CheckTitlesExistence(IEnumerable<string> titles);
|
||||||
|
}
|
||||||
|
}
|
66
TechHelper.Server/Services/QuestionGroupService.cs
Normal file
66
TechHelper.Server/Services/QuestionGroupService.cs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
|
using SharedDATA.Api;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public class QuestionGroupService : IAssignmentGroupService
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly IUnitOfWork _work;
|
||||||
|
// 如果不再需要 AutoMapper 进行实体到 DTO 的映射,可以移除 _mapper 字段
|
||||||
|
// 但如果 AutoMapper 在其他服务中用于其他映射,或者将来可能需要,可以保留
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IExamService _examService;
|
||||||
|
|
||||||
|
public QuestionGroupService(IUnitOfWork work, IMapper mapper, IExamService examService)
|
||||||
|
{
|
||||||
|
_work = work;
|
||||||
|
_mapper = mapper;
|
||||||
|
_examService = examService;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task<ApiResponse> AddAsync(AssignmentGroup model)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> GetAllAsync(QueryParameter query)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetAsync(Guid id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var result = await _work.GetRepository<AssignmentGroup>().GetFirstOrDefaultAsync(predicate: ag => ag.Id == id);
|
||||||
|
QuestionGroupDto qgd = new QuestionGroupDto();
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
qgd = _examService.MapAssignmentGroupToDto(result);
|
||||||
|
return ApiResponse.Success(result: qgd);
|
||||||
|
}
|
||||||
|
return ApiResponse.Error("没找到问题组");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"出现了一点问题: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> UpdateAsync(AssignmentGroup model)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
261
TechHelper.Server/Services/QuestionService.cs
Normal file
261
TechHelper.Server/Services/QuestionService.cs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Entities.Contracts;
|
||||||
|
using SharedDATA.Api;
|
||||||
|
using TechHelper.Services;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Linq;
|
||||||
|
using Entities; // 引入你的 Question 实体所在的命名空间
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Entities.DTO; // 引入 EF Core 用于 Include (如果需要)
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public class QuestionService : IQuestionService
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _work;
|
||||||
|
// 如果不再需要 AutoMapper 进行实体到 DTO 的映射,可以移除 _mapper 字段
|
||||||
|
// 但如果 AutoMapper 在其他服务中用于其他映射,或者将来可能需要,可以保留
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly IExamService _examService;
|
||||||
|
|
||||||
|
public QuestionService(IUnitOfWork work, IMapper mapper, IExamService examService)
|
||||||
|
{
|
||||||
|
_work = work;
|
||||||
|
_mapper = mapper;
|
||||||
|
_examService = examService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> AddAsync(Question model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 可以在此处进行业务逻辑校验,例如检查题目是否已存在
|
||||||
|
var existingQuestion = await _work.GetRepository<Question>().GetFirstOrDefaultAsync(
|
||||||
|
predicate: q => q.QuestionText == model.QuestionText && !q.IsDeleted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingQuestion != null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"题目 '{model.QuestionText}' 已存在,请勿重复添加。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置创建时间、创建者等通用属性
|
||||||
|
model.Id = Guid.NewGuid();
|
||||||
|
model.CreatedAt = DateTime.UtcNow;
|
||||||
|
model.UpdatedAt = DateTime.UtcNow;
|
||||||
|
model.IsDeleted = false;
|
||||||
|
model.ValidQuestion = true; // 假设新添加的题目默认为有效
|
||||||
|
// model.CreatedBy = ... // 实际应用中,这里应该从当前用户上下文获取
|
||||||
|
|
||||||
|
await _work.GetRepository<Question>().InsertAsync(model);
|
||||||
|
await _work.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 直接返回 Question 实体
|
||||||
|
return ApiResponse.Success("题目添加成功。", model);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"添加题目失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> DeleteAsync(Guid id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var questionToDelete = await _work.GetRepository<Question>().GetFirstOrDefaultAsync(predicate: q => q.Id == id && !q.IsDeleted);
|
||||||
|
|
||||||
|
if (questionToDelete == null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"找不到 ID 为 {id} 的题目,或者该题目已被删除。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 软删除
|
||||||
|
questionToDelete.IsDeleted = true;
|
||||||
|
questionToDelete.UpdatedAt = DateTime.UtcNow;
|
||||||
|
_work.GetRepository<Question>().Update(questionToDelete);
|
||||||
|
|
||||||
|
await _work.SaveChangesAsync();
|
||||||
|
|
||||||
|
return ApiResponse.Success($"ID 为 {id} 的题目已成功删除 (软删除)。");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"删除题目时发生错误: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> FindByTitle(string title)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var question = await _work.GetRepository<Question>().GetFirstOrDefaultAsync(
|
||||||
|
predicate: q => q.QuestionText == title && !q.IsDeleted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (question == null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"未找到题目 '{title}'。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回 Question 实体
|
||||||
|
return ApiResponse.Success(result: question);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"查找题目时出现问题: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> CheckTitlesExistence(IEnumerable<string> titles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (titles == null || !titles.Any())
|
||||||
|
{
|
||||||
|
return ApiResponse.Success("未指定查询的题目文本,返回空结果。", new Dictionary<string, bool>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var distinctTitles = titles.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var existingQuestions = await _work.GetRepository<Question>().GetAllAsync(
|
||||||
|
predicate: q => distinctTitles.Contains(q.QuestionText) && !q.IsDeleted
|
||||||
|
);
|
||||||
|
|
||||||
|
var existingQuestionTexts = new HashSet<string>(existingQuestions.Select(q => q.QuestionText), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var resultDictionary = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var title in titles)
|
||||||
|
{
|
||||||
|
resultDictionary[title] = existingQuestionTexts.Contains(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.Success(result: resultDictionary);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"批量查找题目存在性时出现问题: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetAllAsync(QueryParameter query)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Expression<Func<Question, bool>> predicate = q => !q.IsDeleted;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Search))
|
||||||
|
{
|
||||||
|
predicate = predicate.And(q => q.QuestionText.Contains(query.Search));
|
||||||
|
}
|
||||||
|
|
||||||
|
Func<IQueryable<Question>, IOrderedQueryable<Question>> orderBy = null;
|
||||||
|
if (true)
|
||||||
|
{
|
||||||
|
|
||||||
|
orderBy = q => q.OrderByDescending(x => x.CreatedAt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
orderBy = q => q.OrderByDescending(x => x.CreatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var questions = await _work.GetRepository<Question>().GetPagedListAsync(
|
||||||
|
predicate: predicate,
|
||||||
|
orderBy: orderBy,
|
||||||
|
pageIndex: query.PageIndex,
|
||||||
|
pageSize: query.PageSize
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!questions.Items.Any())
|
||||||
|
{
|
||||||
|
return ApiResponse.Error("未找到任何题目。", Enumerable.Empty<Question>()); // 返回空 Question 集合
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回 Question 实体集合
|
||||||
|
return ApiResponse.Success(result: questions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"获取题目列表时出现问题: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetAsync(Guid id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var question = await _work.GetRepository<Question>().GetFirstOrDefaultAsync(predicate: q => q.Id == id && !q.IsDeleted);
|
||||||
|
|
||||||
|
if (question == null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"找不到 ID 为 {id} 的题目。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回 Question 实体
|
||||||
|
return ApiResponse.Success(result: question);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"获取题目时发生错误: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> UpdateAsync(Question model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var existingQuestion = await _work.GetRepository<Question>().GetFirstOrDefaultAsync(predicate: q => q.Id == model.Id && !q.IsDeleted);
|
||||||
|
|
||||||
|
if (existingQuestion == null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"找不到 ID 为 {model.Id} 的题目,无法更新。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查更新后的题目文本是否与现有其他题目重复
|
||||||
|
var duplicateCheck = await _work.GetRepository<Question>().GetFirstOrDefaultAsync(
|
||||||
|
predicate: q => q.Id != model.Id && q.QuestionText == model.QuestionText && !q.IsDeleted
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicateCheck != null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"题目文本 '{model.QuestionText}' 已被其他题目占用,请修改。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动复制属性或使用 AutoMapper (如果保留了 _mapper 字段)
|
||||||
|
// 如果选择手动复制,请确保复制所有需要更新的属性
|
||||||
|
existingQuestion = model;
|
||||||
|
|
||||||
|
_work.GetRepository<Question>().Update(existingQuestion);
|
||||||
|
await _work.SaveChangesAsync();
|
||||||
|
|
||||||
|
// 直接返回更新后的 Question 实体
|
||||||
|
return ApiResponse.Success("题目更新成功。", existingQuestion);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"更新题目失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// PredicateBuilder 保持不变,如果你没有使用 LinqKit,这部分是必需的
|
||||||
|
public static class PredicateBuilder
|
||||||
|
{
|
||||||
|
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
|
||||||
|
{
|
||||||
|
var invokedExpr = Expression.Invoke(second, first.Parameters.Cast<Expression>());
|
||||||
|
return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(first.Body, invokedExpr), first.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second)
|
||||||
|
{
|
||||||
|
var invokedExpr = Expression.Invoke(second, first.Parameters.Cast<Expression>());
|
||||||
|
return Expression.Lambda<Func<T, bool>>(Expression.OrElse(first.Body, invokedExpr), first.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression<Func<T, bool>> True<T>() { return f => true; }
|
||||||
|
public static Expression<Func<T, bool>> False<T>() { return f => false; }
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user