diff --git a/Entities/Contracts/AssignmentQuestion.cs b/Entities/Contracts/AssignmentQuestion.cs index d36e5f6..0b9d1cd 100644 --- a/Entities/Contracts/AssignmentQuestion.cs +++ b/Entities/Contracts/AssignmentQuestion.cs @@ -16,10 +16,12 @@ namespace Entities.Contracts [Column("id")] public Guid Id { get; set; } - [Required] [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] [Column("group_id")] @@ -38,11 +40,16 @@ namespace Entities.Contracts [Column("score")] public float? Score { get; set; } + [Required] + [Column("bgroup")] + public bool IsGroup { get; set; } + [Column("deleted")] public bool IsDeleted { get; set; } public Question Question { get; set; } + public QuestionGroup QuestionGroup { get; set; } public ICollection SubmissionDetails { get; set; } public AssignmentGroup AssignmentGroup { get; set; } diff --git a/Entities/Contracts/Question.cs b/Entities/Contracts/Question.cs index fc2f7c8..a51e91d 100644 --- a/Entities/Contracts/Question.cs +++ b/Entities/Contracts/Question.cs @@ -29,6 +29,9 @@ namespace Entities.Contracts [MaxLength(65535)] public string CorrectAnswer { get; set; } + [Column("question_group_id")] + public Guid? QuestionGroupId { get; set; } + [Column("difficulty_level")] [MaxLength(10)] public DifficultyLevel DifficultyLevel { get; set; } @@ -36,6 +39,9 @@ namespace Entities.Contracts [Column("subject_area")] public SubjectAreaEnum SubjectArea { get; set; } + [Column("options")] + public string? Options { get; set; } + [Required] [Column("created_by")] [ForeignKey("Creator")] @@ -55,6 +61,7 @@ namespace Entities.Contracts // Navigation Properties public User Creator { get; set; } + public QuestionGroup QuestionGroup { get; set; } public ICollection AssignmentQuestions { get; set; } public Question() diff --git a/Entities/Contracts/QuestionGroup.cs b/Entities/Contracts/QuestionGroup.cs new file mode 100644 index 0000000..df6f656 --- /dev/null +++ b/Entities/Contracts/QuestionGroup.cs @@ -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 ChildQuestionGroups { get; set; } + public ICollection AssignmentQuestions { get; set; } + public ICollection Questions { get; set; } + + public QuestionGroup() + { + Id = Guid.NewGuid(); + Questions = new HashSet(); + CreatedAt = DateTime.UtcNow; + UpdatedAt = DateTime.UtcNow; + IsDeleted = false; + ValidGroup = true; + } + } +} diff --git a/Entities/Contracts/SubmissionDetail.cs b/Entities/Contracts/SubmissionDetail.cs index a68d5eb..eae0e4c 100644 --- a/Entities/Contracts/SubmissionDetail.cs +++ b/Entities/Contracts/SubmissionDetail.cs @@ -52,7 +52,6 @@ namespace Entities.Contracts [Column("deleted")] public bool IsDeleted { get; set; } - // Navigation Properties public Submission Submission { get; set; } public User User { get; set; } public AssignmentQuestion AssignmentQuestion { get; set; } diff --git a/Entities/DTO/ExamDto.cs b/Entities/DTO/ExamDto.cs index 6f0229d..4e13389 100644 --- a/Entities/DTO/ExamDto.cs +++ b/Entities/DTO/ExamDto.cs @@ -27,7 +27,6 @@ namespace Entities.DTO public string? Descript { get; set; } public List SubQuestions { get; set; } = new List(); - public List SubQuestionGroups { get; set; } = new List(); public bool ValidQuestionGroup { get; set; } = false; } diff --git a/TechHelper.Client/Exam/ExamPaperExtensions .cs b/TechHelper.Client/Exam/ExamPaperExtensions .cs index cad9978..d4cb2e9 100644 --- a/TechHelper.Client/Exam/ExamPaperExtensions .cs +++ b/TechHelper.Client/Exam/ExamPaperExtensions .cs @@ -15,49 +15,44 @@ namespace TechHelper.Client.Exam dto.SubjectArea = examPaper.SubjectArea; dto.QuestionGroups.Title = examPaper.AssignmentTitle; dto.QuestionGroups.Descript = examPaper.Description; - // 处理顶级 QuestionGroups + foreach (var qg in examPaper.QuestionGroups) { var qgd = new QuestionGroupDto(); - // 顶级 QuestionGroup,其父组当然无效 (false),所以 isParentGroupValidChain 为 false ParseMajorQuestionGroup(qg, qgd, false); dto.QuestionGroups.SubQuestionGroups.Add(qgd); } - // 处理 TopLevelQuestions + foreach (var question in examPaper.TopLevelQuestions) { - // 对于 TopLevelQuestions,它们没有父组,所以 isParentGroupValidChain 初始为 false - // 如果顶级 Question 包含子问题,则将其视为一个 QuestionGroupDto if (question.SubQuestions != null && question.SubQuestions.Any()) { var qgDto = new QuestionGroupDto { Title = question.Stem, Score = (int)question.Score, - Descript = "", // 顶级 Question 默认无描述 + Descript = "", }; - // 判断当前组是否有效:如果有描述,则为有效组 + qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript); - // 传递给子项的 isParentGroupValidChain 状态:如果当前组有效,则传递 true;否则继承父级状态 (此处为 false) + ParseQuestionWithSubQuestions(question, qgDto, qgDto.ValidQuestionGroup); dto.QuestionGroups.SubQuestionGroups.Add(qgDto); } - else // 如果顶级 Question 没有子问题,则它本身就是一个独立的 SubQuestionDto,放在一个容器 QuestionGroupDto 中 + else { var qgDto = new QuestionGroupDto { Title = question.Stem, Score = (int)question.Score, - Descript = "", // 独立题目的容器组通常无描述 + Descript = "", }; - // 独立题目的容器组,如果没有描述,则不是“有效组” + qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript); var subQuestionDto = new SubQuestionDto(); - // 此时,qgDto.ValidQuestionGroup 为 false,所以传入 true,表示题目是有效的 - // 因为其父组链 (此处为自身) 不是有效组 ParseSingleQuestion(question, subQuestionDto, !qgDto.ValidQuestionGroup); qgDto.SubQuestions.Add(subQuestionDto); dto.QuestionGroups.SubQuestionGroups.Add(qgDto); @@ -67,28 +62,25 @@ namespace TechHelper.Client.Exam return dto; } - // 解析 MajorQuestionGroup 及其子项 - // isParentGroupValidChain 参数表示从顶层到当前组的任一父组是否已经是“有效组” private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain) { qgd.Title = qg.Title; qgd.Score = (int)qg.Score; qgd.Descript = qg.Descript; - // 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效 + qgd.ValidQuestionGroup = !string.IsNullOrEmpty(qg.Descript) && !isParentGroupValidChain; - // 更新传递给子项的 isParentGroupValidChain 状态: - // 如果当前组是有效组 (即 qgd.ValidQuestionGroup 为 true),那么子项的父级链就包含了有效组 - // 否则,子项的父级链有效性继承自其父级 (isParentGroupValidChain) + bool nextIsParentGroupValidChain = qgd.ValidQuestionGroup || isParentGroupValidChain; - // 处理子 QuestionGroup + if (qg.SubQuestionGroups != null) { qg.SubQuestionGroups.ForEach(sqg => { var sqgd = new QuestionGroupDto(); + sqgd.Index = (byte)qg.SubQuestionGroups.IndexOf(sqg); ParseMajorQuestionGroup(sqg, sqgd, nextIsParentGroupValidChain); qgd.SubQuestionGroups.Add(sqgd); }); @@ -105,6 +97,7 @@ namespace TechHelper.Client.Exam var subQgd = new QuestionGroupDto { Title = sq.Stem, + Index = (byte)qg.SubQuestions.IndexOf(sq), Score = (int)sq.Score, Descript = "" // 默认为空 }; @@ -119,6 +112,7 @@ namespace TechHelper.Client.Exam var subQd = new SubQuestionDto(); // 只有当所有父组(包括当前组)都不是有效组时,这个题目才有效 ParseSingleQuestion(sq, subQd, !nextIsParentGroupValidChain); + subQd.Index = (byte)qg.SubQuestions.IndexOf(sq); qgd.SubQuestions.Add(subQd); } }); @@ -191,15 +185,35 @@ 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) { // 配置序列化选项(可选) var options = new JsonSerializerOptions { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; return JsonSerializer.Serialize(dto, options); @@ -207,10 +221,10 @@ namespace TechHelper.Client.Exam public static ExamDto DeserializeExamDto(string jsonString) { - + var options = new JsonSerializerOptions { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase + PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return JsonSerializer.Deserialize(jsonString, options); diff --git a/TechHelper.Client/Pages/Exam/ExamCreate.razor b/TechHelper.Client/Pages/Exam/ExamCreate.razor index e924918..92222c4 100644 --- a/TechHelper.Client/Pages/Exam/ExamCreate.razor +++ b/TechHelper.Client/Pages/Exam/ExamCreate.razor @@ -101,6 +101,7 @@ Snackbar.Add("试卷解析成功。", Severity.Success); Snackbar.Add($"{_parsedExam.Errors}。", Severity.Success); ExamContent = _parsedExam.ConvertToExamDTO(); + ExamContent.SeqIndex(); } catch (Exception ex) { diff --git a/TechHelper.Server/Context/ApplicationContext.cs b/TechHelper.Server/Context/ApplicationContext.cs index ea3a01c..72351b1 100644 --- a/TechHelper.Server/Context/ApplicationContext.cs +++ b/TechHelper.Server/Context/ApplicationContext.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Entities.Contracts; +using TechHelper.Server.Context.Configuration; namespace TechHelper.Context { @@ -35,6 +36,7 @@ namespace TechHelper.Context builder.ApplyConfiguration(new ClassTeacherConfiguration()); builder.ApplyConfiguration(new QuestionConfiguration()); builder.ApplyConfiguration(new SubmissionConfiguration()); + builder.ApplyConfiguration(new QuestionGroupConfiguration()); builder.ApplyConfiguration(new SubmissionDetailConfiguration()); } } diff --git a/TechHelper.Server/Context/AutoMapperProFile.cs b/TechHelper.Server/Context/AutoMapperProFile.cs index 0fe3b1f..e250cc9 100644 --- a/TechHelper.Server/Context/AutoMapperProFile.cs +++ b/TechHelper.Server/Context/AutoMapperProFile.cs @@ -58,10 +58,33 @@ namespace TechHelper.Context CreateMap() .ForMember(dest => dest.AssignmentTitle, opt => opt.MapFrom(src => src.Title)) .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())); + CreateMap() + .ForMember(dest => dest.SubQuestionGroups, opt => opt.MapFrom(src => src.ChildAssignmentGroups)) + .ForMember(dest => dest.SubQuestions, opt => opt.MapFrom(src => src.AssignmentQuestions)); + + CreateMap() + .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() + .ForMember(dest => dest.ChildAssignmentGroups, opt => opt.MapFrom(src => src.SubQuestionGroups)) + .ForMember(dest => dest.AssignmentQuestions, opt => opt.MapFrom(src => src.SubQuestions)); + + CreateMap() + .ForMember(dest => dest.Question, opt => opt.MapFrom(src => src)); // 映射到嵌套的 Question 对象 + + + CreateMap(); } } - + } diff --git a/TechHelper.Server/Context/Configuration/AssignmentQuestionConfiguration.cs b/TechHelper.Server/Context/Configuration/AssignmentQuestionConfiguration.cs index cb5fe65..c9d7a46 100644 --- a/TechHelper.Server/Context/Configuration/AssignmentQuestionConfiguration.cs +++ b/TechHelper.Server/Context/Configuration/AssignmentQuestionConfiguration.cs @@ -22,8 +22,12 @@ namespace TechHelper.Context.Configuration // 配置 QuestionId 列 (已修正拼写) builder.Property(aq => aq.QuestionId) - .HasColumnName("question_id") - .IsRequired(); + .HasColumnName("question_id"); + + + builder.Property(aq => aq.QuestionGroupId) + .HasColumnName("question_group_id"); + // 配置 QuestionNumber 列 builder.Property(aq => aq.QuestionNumber) @@ -36,7 +40,7 @@ namespace TechHelper.Context.Configuration .IsRequired(); // 通常创建时间字段是非空的 builder.Property(aq => aq.Score) - .HasColumnName("score"); + .HasColumnName("score"); // 配置 AssignmentGroupId 列 // 该列在数据库中名为 "detail_id" @@ -44,7 +48,11 @@ namespace TechHelper.Context.Configuration .HasColumnName("group_id") .IsRequired(); - // 配置 IsDeleted 列 + + builder.Property(aq => aq.IsGroup) + .HasColumnName("is_group") // 修正为一致的列名 + .IsRequired(); // IsGroup 应该是必需的 + // 配置 IsDeleted 列 builder.Property(aq => aq.IsDeleted) .HasColumnName("deleted") .HasDefaultValue(false); // 适用于软删除策略 @@ -61,6 +69,12 @@ namespace TechHelper.Context.Configuration .HasForeignKey(aq => aq.QuestionId) // 外键是 AssignmentQuestion.QuestionId .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。 diff --git a/TechHelper.Server/Context/Configuration/QuestionConfiguration.cs b/TechHelper.Server/Context/Configuration/QuestionConfiguration.cs index aac8ccd..0013354 100644 --- a/TechHelper.Server/Context/Configuration/QuestionConfiguration.cs +++ b/TechHelper.Server/Context/Configuration/QuestionConfiguration.cs @@ -14,6 +14,8 @@ namespace TechHelper.Context.Configuration // 2. 设置主键 builder.HasKey(q => q.Id); + builder.HasIndex(q => q.QuestionText); + // 3. 配置列名、必需性、长度及其他属性 // Id @@ -21,6 +23,11 @@ namespace TechHelper.Context.Configuration .HasColumnName("id"); // 对于 Guid 类型的主键,EF Core 默认由应用程序生成值,无需 ValueGeneratedOnAdd() + builder.Property(q => q.QuestionGroupId) + .HasColumnName("question_group_id") + .IsRequired(false); // 可为空,因为题目不一定属于某个题组 + + // QuestionText builder.Property(q => q.QuestionText) .HasColumnName("question_text") @@ -97,6 +104,13 @@ namespace TechHelper.Context.Configuration builder.HasMany(q => q.AssignmentQuestions) // 当前 Question 有多个 AssignmentQuestion .WithOne(aq => aq.Question); // 每一个 AssignmentQuestion 都有一个 Question // .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 } } } + diff --git a/TechHelper.Server/Context/Configuration/QuestionGroupConfiguration.cs b/TechHelper.Server/Context/Configuration/QuestionGroupConfiguration.cs new file mode 100644 index 0000000..209abd9 --- /dev/null +++ b/TechHelper.Server/Context/Configuration/QuestionGroupConfiguration.cs @@ -0,0 +1,111 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore; +using Entities.Contracts; + +namespace TechHelper.Server.Context.Configuration +{ + public class QuestionGroupConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder 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() // 将枚举转换为字符串存储 + .HasMaxLength(10); + + // SubjectArea 科目领域 (枚举映射为字符串) + builder.Property(qg => qg.SubjectArea) + .HasColumnName("subject_area") + .HasConversion(); // 将枚举转换为字符串存储 + + // 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 作为默认安全选项。 + } + } +} diff --git a/TechHelper.Server/Repository/ExamRepository.cs b/TechHelper.Server/Repository/ExamRepository.cs new file mode 100644 index 0000000..489cdc8 --- /dev/null +++ b/TechHelper.Server/Repository/ExamRepository.cs @@ -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 _assignmentRepo; + private readonly IRepository _assignmentGroupRepo; + + public ExamRepository(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + _assignmentRepo = _unitOfWork.GetRepository(); + _assignmentGroupRepo = _unitOfWork.GetRepository(); + } + + public async Task 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> GetExamPreviewsByUserAsync(Guid userId) + { + return await _assignmentRepo.GetAllAsync( + predicate: a => a.CreatedBy == userId && !a.IsDeleted); + } + + public async Task AddAsync(Assignment assignment) + { + await _assignmentRepo.InsertAsync(assignment); + } + } +} diff --git a/TechHelper.Server/Repository/IExamRepository.cs b/TechHelper.Server/Repository/IExamRepository.cs new file mode 100644 index 0000000..2b5642e --- /dev/null +++ b/TechHelper.Server/Repository/IExamRepository.cs @@ -0,0 +1,27 @@ +using Entities.Contracts; + +namespace TechHelper.Server.Repository +{ + public interface IExamRepository + { + /// + /// 根据ID异步获取一个完整的试卷实体,包括所有子题组和题目。 + /// + /// 试卷ID + /// 完整的 Assignment 实体,如果找不到则返回 null。 + Task GetFullExamByIdAsync(Guid assignmentId); + + /// + /// 获取指定用户创建的所有试卷的预览信息。 + /// + /// 用户ID + /// Assignment 实体集合。 + Task> GetExamPreviewsByUserAsync(Guid userId); + + /// + /// 向数据库添加一个新的试卷。 + /// + /// 要添加的试卷实体。 + Task AddAsync(Assignment assignment); + } +} diff --git a/TechHelper.Server/Services/ExamService.cs b/TechHelper.Server/Services/ExamService.cs index 85c4b9f..a7a2aa4 100644 --- a/TechHelper.Server/Services/ExamService.cs +++ b/TechHelper.Server/Services/ExamService.cs @@ -352,63 +352,79 @@ namespace TechHelper.Server.Services Guid? parentAssignmentGroupId, Guid createdById) { - byte groupNumber = 1; - var newAssignmentGroup = new AssignmentGroup + if (qgDto.ValidQuestionGroup) { - Id = Guid.NewGuid(), // 后端生成 GUID - Title = qgDto.Title, - Descript = qgDto.Descript, - TotalPoints = qgDto.Score, - Number = (byte)qgDto.Index, - ValidQuestionGroup = qgDto.ValidQuestionGroup, - ParentGroup = parentAssignmentGroupId, - AssignmentId = parentAssignmentGroupId == null ? assignmentId : (Guid?)null, - IsDeleted = false - }; - await _unitOfWork.GetRepository().InsertAsync(newAssignmentGroup); - - // 处理子题目 - uint questionNumber = 1; - foreach (var sqDto in qgDto.SubQuestions.OrderBy(s => s.Index)) + await SaveQuestionGroup(qgDto); + } + else { - var newQuestion = _mapper.Map(sqDto); - newQuestion.Id = Guid.NewGuid(); - newQuestion.CreatedBy = createdById; - newQuestion.CreatedAt = DateTime.UtcNow; - newQuestion.UpdatedAt = DateTime.UtcNow; - newQuestion.IsDeleted = false; - newQuestion.SubjectArea = EnumMappingHelpers.ParseEnumSafe(subjectarea, SubjectAreaEnum.Unknown); + byte groupNumber = 1; - await _unitOfWork.GetRepository().InsertAsync(newQuestion); - var newAssignmentQuestion = new AssignmentQuestion + var newAssignmentGroup = new AssignmentGroup { - Id = Guid.NewGuid(), - QuestionId = newQuestion.Id, - QuestionNumber = (byte)questionNumber, - AssignmentGroupId = newAssignmentGroup.Id, - Score = sqDto.Score, - IsDeleted = false, - CreatedAt = DateTime.UtcNow + Id = Guid.NewGuid(), // 后端生成 GUID + Title = qgDto.Title, + Descript = qgDto.Descript, + TotalPoints = qgDto.Score, + Number = (byte)qgDto.Index, + ValidQuestionGroup = qgDto.ValidQuestionGroup, + ParentGroup = parentAssignmentGroupId, + AssignmentId = parentAssignmentGroupId == null ? assignmentId : (Guid?)null, + IsDeleted = false }; - await _unitOfWork.GetRepository().InsertAsync(newAssignmentQuestion); + await _unitOfWork.GetRepository().InsertAsync(newAssignmentGroup); - questionNumber++; - } + // 处理子题目 + uint questionNumber = 1; + foreach (var sqDto in qgDto.SubQuestions.OrderBy(s => s.Index)) + { + var newQuestion = _mapper.Map(sqDto); + newQuestion.Id = Guid.NewGuid(); + newQuestion.CreatedBy = createdById; + newQuestion.CreatedAt = DateTime.UtcNow; + newQuestion.UpdatedAt = DateTime.UtcNow; + newQuestion.IsDeleted = false; + newQuestion.SubjectArea = EnumMappingHelpers.ParseEnumSafe(subjectarea, SubjectAreaEnum.Unknown); - // 递归处理子题组 - // 这里需要遍历 SubQuestionGroups,并对每个子组进行递归调用 - foreach (var subQgDto in qgDto.SubQuestionGroups.OrderBy(s => s.Index)) - { - await ProcessAndSaveAssignmentGroupsRecursive( - subQgDto, // 传入当前的子题组 DTO - subjectarea, - assignmentId, // 顶层 AssignmentId 依然传递下去,但子组不会直接使用它 - newAssignmentGroup.Id, // 将当前题组的 ID 作为下一层递归的 parentAssignmentGroupId - createdById); + await _unitOfWork.GetRepository().InsertAsync(newQuestion); + + var newAssignmentQuestion = new AssignmentQuestion + { + Id = Guid.NewGuid(), + QuestionId = newQuestion.Id, + QuestionNumber = (byte)questionNumber, + AssignmentGroupId = newAssignmentGroup.Id, + Score = sqDto.Score, + IsDeleted = false, + CreatedAt = DateTime.UtcNow + }; + await _unitOfWork.GetRepository().InsertAsync(newAssignmentQuestion); + + questionNumber++; + } + + + + // 递归处理子题组 + // 这里需要遍历 SubQuestionGroups,并对每个子组进行递归调用 + foreach (var subQgDto in qgDto.SubQuestionGroups.OrderBy(s => s.Index)) + { + await ProcessAndSaveAssignmentGroupsRecursive( + subQgDto, // 传入当前的子题组 DTO + subjectarea, + assignmentId, // 顶层 AssignmentId 依然传递下去,但子组不会直接使用它 + newAssignmentGroup.Id, // 将当前题组的 ID 作为下一层递归的 parentAssignmentGroupId + createdById); + } } } + private async Task SaveQuestionGroup(QuestionGroupDto qgDto) + { + + } + public async Task GetAllExamPreview(Guid user) { try @@ -419,7 +435,7 @@ namespace TechHelper.Server.Services if (assignments.Any()) { var exam = _mapper.Map>(assignments); - return ApiResponse.Success(result: exam); + return ApiResponse.Success(result: exam); } return ApiResponse.Error("你还没有创建任何试卷"); diff --git a/TechHelper.Server/Services/ExamService2.cs b/TechHelper.Server/Services/ExamService2.cs new file mode 100644 index 0000000..ead8236 --- /dev/null +++ b/TechHelper.Server/Services/ExamService2.cs @@ -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 CreateExamAsync(ExamDto examDto, Guid creatorId) + { + if (examDto.QuestionGroups == null) + { + throw new ArgumentException("试卷必须包含一个根题组。"); + } + + // 使用 AutoMapper 将 DTO 映射到实体 + var assignment = _mapper.Map(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 GetExamByIdAsync(Guid id) + { + var assignment = await _examRepository.GetFullExamByIdAsync(id); + if (assignment == null) + { + + throw new InvalidOperationException(""); + } + + return _mapper.Map(assignment); + } + + public async Task> GetAllExamPreviewsAsync(Guid userId) + { + var assignments = await _examRepository.GetExamPreviewsByUserAsync(userId); + return _mapper.Map>(assignments); + } + + public Task GetAllAsync(QueryParameter query) + { + throw new NotImplementedException(); + } + + public Task GetAsync(Guid id) + { + throw new NotImplementedException(); + } + + public Task AddAsync(ExamDto model) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(ExamDto model) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(Guid id) + { + throw new NotImplementedException(); + } + } + +} diff --git a/TechHelper.Server/Services/IAssignmentGroupService.cs b/TechHelper.Server/Services/IAssignmentGroupService.cs new file mode 100644 index 0000000..685b281 --- /dev/null +++ b/TechHelper.Server/Services/IAssignmentGroupService.cs @@ -0,0 +1,9 @@ +using Entities.Contracts; +using TechHelper.Services; + +namespace TechHelper.Server.Services +{ + public interface IAssignmentGroupService : IBaseService + { + } +} diff --git a/TechHelper.Server/Services/IExamService.cs b/TechHelper.Server/Services/IExamService.cs index c9a39b1..ef29d9a 100644 --- a/TechHelper.Server/Services/IExamService.cs +++ b/TechHelper.Server/Services/IExamService.cs @@ -1,4 +1,5 @@ -using Entities.DTO; +using Entities.Contracts; +using Entities.DTO; using TechHelper.Services; namespace TechHelper.Server.Services @@ -6,5 +7,6 @@ namespace TechHelper.Server.Services public interface IExamService : IBaseService { Task GetAllExamPreview(Guid user); + QuestionGroupDto MapAssignmentGroupToDto(AssignmentGroup ag); } } diff --git a/TechHelper.Server/Services/IExamService2.cs b/TechHelper.Server/Services/IExamService2.cs new file mode 100644 index 0000000..7836722 --- /dev/null +++ b/TechHelper.Server/Services/IExamService2.cs @@ -0,0 +1,26 @@ +using Entities.Contracts; +using Entities.DTO; +using TechHelper.Services; + +namespace TechHelper.Server.Services +{ + public interface IExamService2 : IBaseService + { + /// + /// 根据 ID 获取试卷 DTO。 + /// + Task GetExamByIdAsync(Guid id); + + /// + /// 获取指定用户的所有试卷预览。 + /// + Task> GetAllExamPreviewsAsync(Guid userId); + + /// + /// 创建一个新的试卷。 + /// + /// 创建成功的试卷ID + Task CreateExamAsync(ExamDto examDto, Guid creatorId); + + } +} diff --git a/TechHelper.Server/Services/IQuestionGroupService.cs b/TechHelper.Server/Services/IQuestionGroupService.cs new file mode 100644 index 0000000..65e69aa --- /dev/null +++ b/TechHelper.Server/Services/IQuestionGroupService.cs @@ -0,0 +1,9 @@ +using Entities.Contracts; +using TechHelper.Services; + +namespace TechHelper.Server.Services +{ + public interface IQuestionGroupService : IBaseService + { + } +} diff --git a/TechHelper.Server/Services/IQuestionService.cs b/TechHelper.Server/Services/IQuestionService.cs new file mode 100644 index 0000000..9eda26c --- /dev/null +++ b/TechHelper.Server/Services/IQuestionService.cs @@ -0,0 +1,11 @@ +using Entities.Contracts; +using TechHelper.Services; + +namespace TechHelper.Server.Services +{ + public interface IQuestionService : IBaseService + { + Task FindByTitle(string title); + Task CheckTitlesExistence(IEnumerable titles); + } +} diff --git a/TechHelper.Server/Services/QuestionGroupService.cs b/TechHelper.Server/Services/QuestionGroupService.cs new file mode 100644 index 0000000..a200b26 --- /dev/null +++ b/TechHelper.Server/Services/QuestionGroupService.cs @@ -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 AddAsync(AssignmentGroup model) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(Guid id) + { + throw new NotImplementedException(); + } + + public Task GetAllAsync(QueryParameter query) + { + throw new NotImplementedException(); + } + + public async Task GetAsync(Guid id) + { + try + { + + var result = await _work.GetRepository().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 UpdateAsync(AssignmentGroup model) + { + throw new NotImplementedException(); + } + } +} diff --git a/TechHelper.Server/Services/QuestionService.cs b/TechHelper.Server/Services/QuestionService.cs new file mode 100644 index 0000000..cdba5a6 --- /dev/null +++ b/TechHelper.Server/Services/QuestionService.cs @@ -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 AddAsync(Question model) + { + try + { + // 可以在此处进行业务逻辑校验,例如检查题目是否已存在 + var existingQuestion = await _work.GetRepository().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().InsertAsync(model); + await _work.SaveChangesAsync(); + + // 直接返回 Question 实体 + return ApiResponse.Success("题目添加成功。", model); + } + catch (Exception ex) + { + return ApiResponse.Error($"添加题目失败: {ex.Message}"); + } + } + + public async Task DeleteAsync(Guid id) + { + try + { + var questionToDelete = await _work.GetRepository().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().Update(questionToDelete); + + await _work.SaveChangesAsync(); + + return ApiResponse.Success($"ID 为 {id} 的题目已成功删除 (软删除)。"); + } + catch (Exception ex) + { + return ApiResponse.Error($"删除题目时发生错误: {ex.Message}"); + } + } + + public async Task FindByTitle(string title) + { + try + { + var question = await _work.GetRepository().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 CheckTitlesExistence(IEnumerable titles) + { + try + { + if (titles == null || !titles.Any()) + { + return ApiResponse.Success("未指定查询的题目文本,返回空结果。", new Dictionary()); + } + + var distinctTitles = titles.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + var existingQuestions = await _work.GetRepository().GetAllAsync( + predicate: q => distinctTitles.Contains(q.QuestionText) && !q.IsDeleted + ); + + var existingQuestionTexts = new HashSet(existingQuestions.Select(q => q.QuestionText), StringComparer.OrdinalIgnoreCase); + + var resultDictionary = new Dictionary(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 GetAllAsync(QueryParameter query) + { + try + { + Expression> predicate = q => !q.IsDeleted; + + if (!string.IsNullOrWhiteSpace(query.Search)) + { + predicate = predicate.And(q => q.QuestionText.Contains(query.Search)); + } + + Func, IOrderedQueryable> orderBy = null; + if (true) + { + + orderBy = q => q.OrderByDescending(x => x.CreatedAt); + } + else + { + orderBy = q => q.OrderByDescending(x => x.CreatedAt); + } + + var questions = await _work.GetRepository().GetPagedListAsync( + predicate: predicate, + orderBy: orderBy, + pageIndex: query.PageIndex, + pageSize: query.PageSize + ); + + if (!questions.Items.Any()) + { + return ApiResponse.Error("未找到任何题目。", Enumerable.Empty()); // 返回空 Question 集合 + } + + // 直接返回 Question 实体集合 + return ApiResponse.Success(result: questions); + } + catch (Exception ex) + { + return ApiResponse.Error($"获取题目列表时出现问题: {ex.Message}"); + } + } + + public async Task GetAsync(Guid id) + { + try + { + var question = await _work.GetRepository().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 UpdateAsync(Question model) + { + try + { + var existingQuestion = await _work.GetRepository().GetFirstOrDefaultAsync(predicate: q => q.Id == model.Id && !q.IsDeleted); + + if (existingQuestion == null) + { + return ApiResponse.Error($"找不到 ID 为 {model.Id} 的题目,无法更新。"); + } + + // 检查更新后的题目文本是否与现有其他题目重复 + var duplicateCheck = await _work.GetRepository().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().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> And(this Expression> first, Expression> second) + { + var invokedExpr = Expression.Invoke(second, first.Parameters.Cast()); + return Expression.Lambda>(Expression.AndAlso(first.Body, invokedExpr), first.Parameters); + } + + public static Expression> Or(this Expression> first, Expression> second) + { + var invokedExpr = Expression.Invoke(second, first.Parameters.Cast()); + return Expression.Lambda>(Expression.OrElse(first.Body, invokedExpr), first.Parameters); + } + + public static Expression> True() { return f => true; } + public static Expression> False() { return f => false; } + } +} \ No newline at end of file