change
This commit is contained in:
@@ -23,12 +23,15 @@ namespace Entities.Contracts
|
|||||||
[Column("description")]
|
[Column("description")]
|
||||||
public string Description { get; set; }
|
public string Description { get; set; }
|
||||||
|
|
||||||
|
[Column("subject_area")]
|
||||||
|
public string SubjectArea { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("due_date")]
|
[Column("due_date")]
|
||||||
public DateTime DueDate { get; set; }
|
public DateTime DueDate { get; set; }
|
||||||
|
|
||||||
[Column("total_points")]
|
[Column("total_points")]
|
||||||
public decimal? TotalPoints { get; set; }
|
public float? TotalPoints { get; set; }
|
||||||
|
|
||||||
[Column("created_by")]
|
[Column("created_by")]
|
||||||
[ForeignKey("Creator")]
|
[ForeignKey("Creator")]
|
||||||
|
@@ -23,11 +23,14 @@ namespace Entities.Contracts
|
|||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("question_number")]
|
[Column("question_number")]
|
||||||
public uint QuestionNumber { get; set; }
|
public byte QuestionNumber { get; set; }
|
||||||
|
|
||||||
[Column("created_at")]
|
[Column("created_at")]
|
||||||
public DateTime CreatedAt { get; set; }
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
[Column("score")]
|
||||||
|
public float? Score { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
[Column("detail_id")]
|
[Column("detail_id")]
|
||||||
[ForeignKey("AssignmentGroup")]
|
[ForeignKey("AssignmentGroup")]
|
||||||
|
@@ -34,8 +34,7 @@ namespace Entities.Contracts
|
|||||||
public DateTime SubmissionTime { get; set; }
|
public DateTime SubmissionTime { get; set; }
|
||||||
|
|
||||||
[Column("overall_grade")]
|
[Column("overall_grade")]
|
||||||
[Precision(5, 2)]
|
public float? OverallGrade { get; set; }
|
||||||
public decimal? OverallGrade { get; set; }
|
|
||||||
|
|
||||||
[Column("overall_feedback")]
|
[Column("overall_feedback")]
|
||||||
public string OverallFeedback { get; set; }
|
public string OverallFeedback { get; set; }
|
||||||
|
@@ -38,8 +38,7 @@ namespace Entities.Contracts
|
|||||||
public bool? IsCorrect { get; set; }
|
public bool? IsCorrect { get; set; }
|
||||||
|
|
||||||
[Column("points_awarded")]
|
[Column("points_awarded")]
|
||||||
[Precision(5, 2)]
|
public float? PointsAwarded { get; set; }
|
||||||
public decimal? PointsAwarded { get; set; }
|
|
||||||
|
|
||||||
[Column("teacher_feedback")]
|
[Column("teacher_feedback")]
|
||||||
public string TeacherFeedback { get; set; }
|
public string TeacherFeedback { get; set; }
|
||||||
|
@@ -14,6 +14,10 @@
|
|||||||
this.Result = result;
|
this.Result = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ApiResponse()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
|
|
||||||
public bool Status { get; set; }
|
public bool Status { get; set; }
|
||||||
|
55
Entities/DTO/ExamDto.cs
Normal file
55
Entities/DTO/ExamDto.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Entities.DTO
|
||||||
|
{
|
||||||
|
public class ExamDto
|
||||||
|
{
|
||||||
|
public Guid? AssignmentId { get; set; }
|
||||||
|
public string AssignmentTitle { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; }
|
||||||
|
public string SubjectArea { get; set; }
|
||||||
|
public List<QuestionGroupDto> QuestionGroups { get; set; } = new List<QuestionGroupDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuestionGroupDto
|
||||||
|
{
|
||||||
|
public int Index { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public int Score { get; set; }
|
||||||
|
|
||||||
|
public string QuestionReference { get; set; }
|
||||||
|
|
||||||
|
public List<SubQuestionDto> SubQuestions { get; set; } = new List<SubQuestionDto>();
|
||||||
|
|
||||||
|
public List<QuestionGroupDto> SubQuestionGroups { get; set; } = new List<QuestionGroupDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubQuestionDto
|
||||||
|
{
|
||||||
|
|
||||||
|
public byte Index { get; set; }
|
||||||
|
|
||||||
|
public string Stem { get; set; }
|
||||||
|
|
||||||
|
public float Score { get; set; }
|
||||||
|
|
||||||
|
public List<OptionDto> Options { get; set; } = new List<OptionDto>();
|
||||||
|
|
||||||
|
public string SampleAnswer { get; set; }
|
||||||
|
|
||||||
|
public string QuestionType { get; set; }
|
||||||
|
public string DifficultyLevel { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OptionDto
|
||||||
|
{
|
||||||
|
public string Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@@ -166,8 +166,6 @@
|
|||||||
</SQs>
|
</SQs>
|
||||||
</QG>";
|
</QG>";
|
||||||
|
|
||||||
|
public static string Format { get; internal set; }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,6 @@ namespace TechHelper.Client.AI
|
|||||||
string content = response.Choices[0].Message?.Content;
|
string content = response.Choices[0].Message?.Content;
|
||||||
if (!string.IsNullOrEmpty(content))
|
if (!string.IsNullOrEmpty(content))
|
||||||
{
|
{
|
||||||
// 移除 <think>...</think> 标签及其内容
|
|
||||||
int startIndex = content.IndexOf("<think>");
|
int startIndex = content.IndexOf("<think>");
|
||||||
int endIndex = content.IndexOf("</think>");
|
int endIndex = content.IndexOf("</think>");
|
||||||
if (startIndex != -1 && endIndex != -1 && endIndex > startIndex)
|
if (startIndex != -1 && endIndex != -1 && endIndex > startIndex)
|
||||||
@@ -46,11 +45,11 @@ namespace TechHelper.Client.AI
|
|||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"API 请求错误:{ex.Message}");
|
throw;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"发生未知错误:{ex.Message}");
|
throw;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ namespace TechHelper.Client.Exam
|
|||||||
[JsonProperty("题号")]
|
[JsonProperty("题号")]
|
||||||
// XML 特性:作为 <QG Id="X"> 属性
|
// XML 特性:作为 <QG Id="X"> 属性
|
||||||
[XmlAttribute("Id")]
|
[XmlAttribute("Id")]
|
||||||
public string Id { get; set; }
|
public byte Id { get; set; }
|
||||||
|
|
||||||
[JsonProperty("标题")]
|
[JsonProperty("标题")]
|
||||||
[XmlElement("T")] // T for Title
|
[XmlElement("T")] // T for Title
|
||||||
@@ -60,9 +60,10 @@ namespace TechHelper.Client.Exam
|
|||||||
// 子题目类
|
// 子题目类
|
||||||
public class SubQuestion
|
public class SubQuestion
|
||||||
{
|
{
|
||||||
|
|
||||||
[JsonProperty("子题号")]
|
[JsonProperty("子题号")]
|
||||||
[XmlAttribute("Id")] // Id for SubId
|
[XmlAttribute("Id")] // Id for SubId
|
||||||
public string SubId { get; set; }
|
public byte SubId { get; set; }
|
||||||
|
|
||||||
[JsonProperty("题干")]
|
[JsonProperty("题干")]
|
||||||
[XmlElement("T")] // T for Text (Stem)
|
[XmlElement("T")] // T for Text (Stem)
|
||||||
|
429
TechHelper.Client/Exam/ExamParse.cs
Normal file
429
TechHelper.Client/Exam/ExamParse.cs
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace TechHelper.Client.Exam.Parse
|
||||||
|
{
|
||||||
|
public class ExamPaper
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = "未识别试卷标题";
|
||||||
|
public string Descript { get; set; } = "未识别试卷描述";
|
||||||
|
public string SubjectArea { get; set; } = "试卷类别";
|
||||||
|
public List<MajorQuestionGroup> MajorQuestionGroups { get; set; } = new List<MajorQuestionGroup>();
|
||||||
|
public List<Question> TopLevelQuestions { get; set; } = new List<Question>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MajorQuestionGroup
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Descript { get; set; } = string.Empty;
|
||||||
|
public float Score { get; set; }
|
||||||
|
public List<MajorQuestionGroup> SubMajorQuestionGroups { get; set; } = new List<MajorQuestionGroup>();
|
||||||
|
public List<Question> Questions { get; set; } = new List<Question>();
|
||||||
|
public int Priority { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Question
|
||||||
|
{
|
||||||
|
public string Number { get; set; } = string.Empty;
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
public float Score { get; set; }
|
||||||
|
public List<Option> Options { get; set; } = new List<Option>();
|
||||||
|
public List<Question> SubQuestions { get; set; } = new List<Question>();
|
||||||
|
public int Priority { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Option
|
||||||
|
{
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
public string Text { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 表示一个带有优先级的正则表达式配置
|
||||||
|
/// </summary>
|
||||||
|
public class RegexPatternConfig
|
||||||
|
{
|
||||||
|
public string Pattern { get; set; } // 正则表达式字符串
|
||||||
|
public int Priority { get; set; } // 优先级,数字越小优先级越高
|
||||||
|
public Regex Regex { get; private set; } // 编译后的Regex对象,用于性能优化
|
||||||
|
|
||||||
|
public RegexPatternConfig(string pattern, int priority)
|
||||||
|
{
|
||||||
|
Pattern = pattern;
|
||||||
|
Priority = priority;
|
||||||
|
Regex = new Regex(pattern, RegexOptions.Multiline | RegexOptions.Compiled); // 多行模式,编译以提高性能
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 试卷解析的配置类,包含所有正则表达式
|
||||||
|
/// </summary>
|
||||||
|
public class ExamParserConfig
|
||||||
|
{
|
||||||
|
public List<RegexPatternConfig> MajorQuestionGroupPatterns { get; set; } = new List<RegexPatternConfig>();
|
||||||
|
public List<RegexPatternConfig> QuestionPatterns { get; set; } = new List<RegexPatternConfig>();
|
||||||
|
public List<RegexPatternConfig> OptionPatterns { get; set; } = new List<RegexPatternConfig>();
|
||||||
|
|
||||||
|
public ExamParserConfig()
|
||||||
|
{
|
||||||
|
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^[一二三四五六七八九十]+\s*[、.]\s*(.+?)(?:\s*\((\d+)\s*分\))?$", 1)); // 如: 一、选择题 (5分)
|
||||||
|
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^\d+\.\s*(.+?)(?:\s*\((\d+)\s*分\))?$", 2)); // 如: 1. 填空题 (10分)
|
||||||
|
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^(\(.+\))\s*(.+?)(?:\s*\((\d+)\s*分\))?$", 3)); // 如: (一) 文言文阅读 (8分)
|
||||||
|
|
||||||
|
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.\s*(.*)$", 1)); // 如: 1. 题干
|
||||||
|
OptionPatterns.Add(new RegexPatternConfig(@"^[A-D]\.\s*(.*)$", 1)); // 如: A. 选项内容
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public class PotentialMatch
|
||||||
|
{
|
||||||
|
public int StartIndex { get; set; }
|
||||||
|
public int EndIndex { get; set; } // 匹配到的结构在原始文本中的结束位置
|
||||||
|
public string MatchedText { get; set; } // 匹配到的完整行或段落
|
||||||
|
public Match RegexMatch { get; set; } // 原始的Regex.Match对象,方便获取捕获组
|
||||||
|
public RegexPatternConfig PatternConfig { get; set; } // 匹配到的模式配置
|
||||||
|
public MatchType Type { get; set; } // 枚举:MajorQuestionGroup, Question, Option, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MatchType
|
||||||
|
{
|
||||||
|
MajorQuestionGroup,
|
||||||
|
Question,
|
||||||
|
Option,
|
||||||
|
Other // 如果有其他需要识别的类型
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负责扫描原始文本,收集所有潜在的匹配项(题组、题目、选项)。
|
||||||
|
/// 它只进行匹配,不进行结构化归属。
|
||||||
|
/// </summary>
|
||||||
|
public class ExamDocumentScanner
|
||||||
|
{
|
||||||
|
private readonly ExamParserConfig _config;
|
||||||
|
|
||||||
|
public ExamDocumentScanner(ExamParserConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫描给定的文本,返回所有潜在的匹配项,并按起始位置排序。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="text">要扫描的文本</param>
|
||||||
|
/// <returns>所有匹配到的 PotentialMatch 列表</returns>
|
||||||
|
public List<PotentialMatch> Scan(string text)
|
||||||
|
{
|
||||||
|
var allPotentialMatches = new List<PotentialMatch>();
|
||||||
|
|
||||||
|
// 扫描所有题组模式
|
||||||
|
foreach (var patternConfig in _config.MajorQuestionGroupPatterns)
|
||||||
|
{
|
||||||
|
foreach (Match match in patternConfig.Regex.Matches(text))
|
||||||
|
{
|
||||||
|
allPotentialMatches.Add(new PotentialMatch
|
||||||
|
{
|
||||||
|
StartIndex = match.Index,
|
||||||
|
EndIndex = match.Index + match.Length,
|
||||||
|
MatchedText = match.Value,
|
||||||
|
RegexMatch = match,
|
||||||
|
PatternConfig = patternConfig,
|
||||||
|
Type = MatchType.MajorQuestionGroup
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫描所有题目模式
|
||||||
|
foreach (var patternConfig in _config.QuestionPatterns)
|
||||||
|
{
|
||||||
|
foreach (Match match in patternConfig.Regex.Matches(text))
|
||||||
|
{
|
||||||
|
allPotentialMatches.Add(new PotentialMatch
|
||||||
|
{
|
||||||
|
StartIndex = match.Index,
|
||||||
|
EndIndex = match.Index + match.Length,
|
||||||
|
MatchedText = match.Value,
|
||||||
|
RegexMatch = match,
|
||||||
|
PatternConfig = patternConfig,
|
||||||
|
Type = MatchType.Question
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫描所有选项模式
|
||||||
|
foreach (var patternConfig in _config.OptionPatterns)
|
||||||
|
{
|
||||||
|
foreach (Match match in patternConfig.Regex.Matches(text))
|
||||||
|
{
|
||||||
|
allPotentialMatches.Add(new PotentialMatch
|
||||||
|
{
|
||||||
|
StartIndex = match.Index,
|
||||||
|
EndIndex = match.Index + match.Length,
|
||||||
|
MatchedText = match.Value,
|
||||||
|
RegexMatch = match,
|
||||||
|
PatternConfig = patternConfig,
|
||||||
|
Type = MatchType.Option
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一按起始位置排序
|
||||||
|
return allPotentialMatches.OrderBy(pm => pm.StartIndex).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExamStructureBuilder
|
||||||
|
{
|
||||||
|
private readonly ExamParserConfig _config;
|
||||||
|
|
||||||
|
public ExamStructureBuilder(ExamParserConfig config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExamPaper BuildExamPaper(string fullExamText, List<PotentialMatch> allPotentialMatches)
|
||||||
|
{
|
||||||
|
var examPaper = new ExamPaper();
|
||||||
|
examPaper.Title = GetExamTitle(fullExamText);
|
||||||
|
|
||||||
|
var majorQGStack = new Stack<MajorQuestionGroup>();
|
||||||
|
MajorQuestionGroup currentMajorQG = null;
|
||||||
|
|
||||||
|
var questionStack = new Stack<Question>();
|
||||||
|
Question currentQuestion = null;
|
||||||
|
|
||||||
|
int currentContentStart = 0;
|
||||||
|
|
||||||
|
|
||||||
|
if (allPotentialMatches.Any() && allPotentialMatches[0].StartIndex > 0)
|
||||||
|
{
|
||||||
|
string introText = fullExamText.Substring(0, allPotentialMatches[0].StartIndex).Trim();
|
||||||
|
// 可以选择将这部分文本存储到 ExamPaper 的某个属性,例如 ExamPaper.Description
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 这里需要处理currentContentStart的位置,到allPotentialMatches[0].StartIndex
|
||||||
|
|
||||||
|
|
||||||
|
for (int i = 0; i < allPotentialMatches.Count; i++)
|
||||||
|
{
|
||||||
|
var pm = allPotentialMatches[i];
|
||||||
|
|
||||||
|
string precedingText = fullExamText.Substring(currentContentStart, pm.StartIndex - currentContentStart).Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(precedingText))
|
||||||
|
{
|
||||||
|
if (currentQuestion != null)
|
||||||
|
{
|
||||||
|
ProcessQuestionContent(currentQuestion, precedingText,
|
||||||
|
GetSubMatchesForRange(allPotentialMatches, currentContentStart, pm.StartIndex));
|
||||||
|
}
|
||||||
|
else if (currentMajorQG != null)
|
||||||
|
{
|
||||||
|
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + precedingText;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 暂时忽略,或可以添加到 ExamPaper.Description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pm.Type == MatchType.MajorQuestionGroup)
|
||||||
|
{
|
||||||
|
// 1. 确定当前 MajorQuestionGroup 的层级关系
|
||||||
|
while (majorQGStack.Any() && pm.PatternConfig.Priority <= majorQGStack.Peek().Priority)
|
||||||
|
{
|
||||||
|
// 当前 QG 的优先级等于或高于栈顶 QG,说明栈顶 QG 已经结束
|
||||||
|
majorQGStack.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
MajorQuestionGroup newMajorQG = new MajorQuestionGroup
|
||||||
|
{
|
||||||
|
Title = pm.RegexMatch.Groups[1].Value.Trim(),
|
||||||
|
Score = (pm.RegexMatch.Groups.Count > 2 && pm.RegexMatch.Groups[2].Success) ? float.Parse(pm.RegexMatch.Groups[2].Value) : 0,
|
||||||
|
Priority = pm.PatternConfig.Priority
|
||||||
|
};
|
||||||
|
|
||||||
|
if (majorQGStack.Any())
|
||||||
|
{
|
||||||
|
majorQGStack.Peek().SubMajorQuestionGroups.Add(newMajorQG);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
examPaper.MajorQuestionGroups.Add(newMajorQG);
|
||||||
|
}
|
||||||
|
|
||||||
|
majorQGStack.Push(newMajorQG);
|
||||||
|
currentMajorQG = newMajorQG;
|
||||||
|
questionStack.Clear();
|
||||||
|
currentQuestion = null;
|
||||||
|
}
|
||||||
|
else if (pm.Type == MatchType.Question)
|
||||||
|
{
|
||||||
|
// 1. 确定当前 Question 的层级关系(子题目)
|
||||||
|
// 找到比当前 Question 优先级高或相等的 Question 作为其父级
|
||||||
|
while (questionStack.Any() && pm.PatternConfig.Priority <= questionStack.Peek().Priority)
|
||||||
|
{
|
||||||
|
// 如果当前 Question 的优先级等于或高于栈顶 Question,说明栈顶 Question 已经结束
|
||||||
|
questionStack.Pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
Question newQuestion = new Question
|
||||||
|
{
|
||||||
|
Number = pm.RegexMatch.Groups[1].Value.Trim(),
|
||||||
|
Text = pm.RegexMatch.Groups[2].Value.Trim(),
|
||||||
|
Priority = pm.PatternConfig.Priority
|
||||||
|
};
|
||||||
|
if (pm.RegexMatch.Groups.Count > 2 && pm.RegexMatch.Groups[2].Success)
|
||||||
|
{
|
||||||
|
float.TryParse(pm.RegexMatch.Groups[2].Value, out float score);
|
||||||
|
newQuestion.Score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (questionStack.Any())
|
||||||
|
{
|
||||||
|
questionStack.Peek().SubQuestions.Add(newQuestion);
|
||||||
|
}
|
||||||
|
else if (currentMajorQG != null)
|
||||||
|
{
|
||||||
|
// 归属于当前活跃的 MajorQuestionGroup
|
||||||
|
currentMajorQG.Questions.Add(newQuestion);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 没有活跃的 MajorQuestionGroup 或 Question,作为 ExamPaper 的顶级 Questions
|
||||||
|
examPaper.TopLevelQuestions.Add(newQuestion);
|
||||||
|
}
|
||||||
|
|
||||||
|
questionStack.Push(newQuestion); // 新的 Question 入栈,成为当前活跃 Question
|
||||||
|
currentQuestion = newQuestion;
|
||||||
|
}
|
||||||
|
else if (pm.Type == MatchType.Option)
|
||||||
|
{
|
||||||
|
// 选项必须归属于一个题目
|
||||||
|
if (currentQuestion != null)
|
||||||
|
{
|
||||||
|
Option newOption = new Option
|
||||||
|
{
|
||||||
|
Label = pm.RegexMatch.Groups[1].Value.Trim(),
|
||||||
|
Text = pm.RegexMatch.Groups[2].Value.Trim()
|
||||||
|
};
|
||||||
|
currentQuestion.Options.Add(newOption);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 孤立的选项,可能需要日志记录或错误处理
|
||||||
|
Console.WriteLine($"Warning: Found isolated Option at index {pm.StartIndex}: {pm.MatchedText}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 步骤3: 更新 currentContentStart 为当前匹配点的 EndIndex ---
|
||||||
|
// 下一次循环将从这里开始提取内容
|
||||||
|
currentContentStart = pm.EndIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 步骤4: 处理循环结束后,最后一个匹配点之后到文本末尾的剩余内容 ---
|
||||||
|
if (currentContentStart < fullExamText.Length)
|
||||||
|
{
|
||||||
|
string remainingText = fullExamText.Substring(currentContentStart).Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(remainingText))
|
||||||
|
{
|
||||||
|
if (currentQuestion != null)
|
||||||
|
{
|
||||||
|
// 最后一个题目后面的内容(可能是选项或多行描述)
|
||||||
|
ProcessQuestionContent(currentQuestion, remainingText,
|
||||||
|
GetSubMatchesForRange(allPotentialMatches, currentContentStart, fullExamText.Length));
|
||||||
|
}
|
||||||
|
else if (currentMajorQG != null)
|
||||||
|
{
|
||||||
|
// 最后一个题组后面的内容(可能是描述或题目)
|
||||||
|
currentMajorQG.Descript += (string.IsNullOrWhiteSpace(currentMajorQG.Descript) ? "" : "\n") + remainingText;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 顶级剩余文本,可能作为 ExamPaper 的整体描述
|
||||||
|
// examPaper.Description += remainingText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return examPaper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提取试卷标题 (简单实现)
|
||||||
|
/// </summary>
|
||||||
|
private string GetExamTitle(string examPaperText)
|
||||||
|
{
|
||||||
|
var firstLine = examPaperText.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.FirstOrDefault(line => !string.IsNullOrWhiteSpace(line));
|
||||||
|
return firstLine ?? "未识别试卷标题";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取给定 PotentialMatch 列表在指定范围内的子集。
|
||||||
|
/// 这个方法用于辅助 ProcessQuestionContent,为其提供该范围内的 Options 和 SubQuestions。
|
||||||
|
/// </summary>
|
||||||
|
private List<PotentialMatch> GetSubMatchesForRange(List<PotentialMatch> allMatches, int start, int end)
|
||||||
|
{
|
||||||
|
// 注意:这里需要考虑 potentialMatches 的索引与 fullExamText 索引的映射
|
||||||
|
// 这里的 StartIndex 是相对于 fullExamText 的
|
||||||
|
return allMatches.Where(pm => pm.StartIndex >= start && pm.StartIndex < end).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理 Question 的内容,主要用于解析 Options 和识别非结构化文本。
|
||||||
|
/// </summary>
|
||||||
|
private void ProcessQuestionContent(Question question, string contentText, List<PotentialMatch> potentialMatchesInScope)
|
||||||
|
{
|
||||||
|
// 遍历当前范围内的所有 PotentialMatch,找出 Options
|
||||||
|
var optionsText = new System.Text.StringBuilder();
|
||||||
|
int lastOptionEndIndex = 0; // 记录最后一个处理的选项的结束位置
|
||||||
|
|
||||||
|
foreach (var pm in potentialMatchesInScope.OrderBy(p => p.StartIndex))
|
||||||
|
{
|
||||||
|
// 检查是否是选项
|
||||||
|
if (pm.Type == MatchType.Option)
|
||||||
|
{
|
||||||
|
// 收集选项之间的文本作为题干的延续或描述
|
||||||
|
if (pm.StartIndex > lastOptionEndIndex)
|
||||||
|
{
|
||||||
|
string textBeforeOption = contentText.Substring(lastOptionEndIndex, pm.StartIndex - lastOptionEndIndex).Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(textBeforeOption))
|
||||||
|
{
|
||||||
|
question.Text += (string.IsNullOrWhiteSpace(question.Text) ? "" : "\n") + textBeforeOption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var newOption = new Option
|
||||||
|
{
|
||||||
|
Label = pm.RegexMatch.Groups[1].Value.Trim(),
|
||||||
|
Text = pm.RegexMatch.Groups[2].Value.Trim()
|
||||||
|
};
|
||||||
|
question.Options.Add(newOption);
|
||||||
|
lastOptionEndIndex = pm.EndIndex;
|
||||||
|
}
|
||||||
|
// TODO: 如果有 SubQuestion 类型,在这里也可以类似处理
|
||||||
|
// else if (pm.Type == MatchType.Question && pm.PatternConfig.Priority > question.Priority)
|
||||||
|
// {
|
||||||
|
// // 这是一个子题目,需要进一步解析
|
||||||
|
// // 递归调用,但这里的逻辑会更复杂,因为需要识别子题目自己的Options
|
||||||
|
// // 可能会在这里创建一个临时的 Question,然后递归 ProcessQuestionContent
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理所有选项之后剩余的文本
|
||||||
|
if (lastOptionEndIndex < contentText.Length)
|
||||||
|
{
|
||||||
|
string remainingContent = contentText.Substring(lastOptionEndIndex).Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(remainingContent))
|
||||||
|
{
|
||||||
|
question.Text += (string.IsNullOrWhiteSpace(question.Text) ? "" : "\n") + remainingContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
171
TechHelper.Client/Exam/ExamService.cs
Normal file
171
TechHelper.Client/Exam/ExamService.cs
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
using System.Xml.Serialization;
|
||||||
|
using TechHelper.Client.AI;
|
||||||
|
using TechHelper.Services;
|
||||||
|
using Entities.DTO;
|
||||||
|
|
||||||
|
|
||||||
|
namespace TechHelper.Client.Exam
|
||||||
|
{
|
||||||
|
public class ExamService : IExamService
|
||||||
|
{
|
||||||
|
private IAIService aIService;
|
||||||
|
|
||||||
|
public ExamService(IAIService aIService)
|
||||||
|
{
|
||||||
|
this.aIService = aIService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse ConvertToXML<T>(string xmlContent)
|
||||||
|
{
|
||||||
|
string cleanedXml = xmlContent.Trim();
|
||||||
|
XmlSerializer serializer = new XmlSerializer(typeof(T));
|
||||||
|
|
||||||
|
using (StringReader reader = new StringReader(cleanedXml))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T deserializedObject = (T)serializer.Deserialize(reader);
|
||||||
|
|
||||||
|
// 成功时返回 ApiResponse
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = true,
|
||||||
|
Result = deserializedObject,
|
||||||
|
Message = "XML 反序列化成功。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = $"XML 反序列化操作错误: {ex.Message}. 内部异常: {ex.InnerException?.Message ?? "无"}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = $"处理 XML 反序列化时发生未知错误: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> DividExam(string examContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string respon = await aIService.CallGLM(examContent, AIConfiguration.BreakQuestions);
|
||||||
|
|
||||||
|
if (respon != null)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = true,
|
||||||
|
Result = respon,
|
||||||
|
Message = "试题分割成功。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = "AI 服务未能返回有效内容,或返回内容为空。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = $"处理试题分割时发生内部错误: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> FormatExam(string examContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string respon = await aIService.CallGLM(examContent, AIConfiguration.Format);
|
||||||
|
|
||||||
|
if (respon != null)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = true,
|
||||||
|
Result = respon,
|
||||||
|
Message = "试题格式化成功。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = "AI 服务未能返回有效内容,或返回内容为空。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = $"处理试题格式化时发生内部错误: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> ParseSingleQuestionGroup(string examContent)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string respon = await aIService.CallGLM(examContent, AIConfiguration.ParseSignelQuestion2);
|
||||||
|
|
||||||
|
if (respon != null)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = true,
|
||||||
|
Result = respon,
|
||||||
|
Message = "试题解析成功。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = "AI 服务未能返回有效内容,或返回内容为空。"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Status = false,
|
||||||
|
Result = null,
|
||||||
|
Message = $"处理试题解析时发生内部错误: {ex.Message}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> SaveParsedExam(ExamDto examDto)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
TechHelper.Client/Exam/IExamService.cs
Normal file
14
TechHelper.Client/Exam/IExamService.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Entities.DTO;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Client.Exam
|
||||||
|
{
|
||||||
|
public interface IExamService
|
||||||
|
{
|
||||||
|
public Task<ApiResponse> FormatExam(string examContent);
|
||||||
|
public Task<ApiResponse> DividExam(string examContent);
|
||||||
|
public Task<ApiResponse> SaveParsedExam(ExamDto examDto);
|
||||||
|
public Task<ApiResponse> ParseSingleQuestionGroup(string examContent);
|
||||||
|
public ApiResponse ConvertToXML<T>(string xmlContent);
|
||||||
|
}
|
||||||
|
}
|
@@ -5,29 +5,38 @@
|
|||||||
<MudPopoverProvider />
|
<MudPopoverProvider />
|
||||||
|
|
||||||
|
|
||||||
|
<MudPaper Style="position: fixed;
|
||||||
<MudPaper Class="d-flex flex-column flex-grow-1" Style="height: 100vh;">
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
<MudPaper Class="d-flex flex-column flex-grow-1 overflow-hidden">
|
height: 100vh;
|
||||||
|
background-image: url('/ref/bg4.jpg');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
filter: blur(10px);
|
||||||
|
z-index: -1;">
|
||||||
|
</MudPaper>
|
||||||
|
<MudPaper Class="d-flex flex-column flex-grow-0 overflow-auto" Style="height: 100vh; background-color:#22222200">
|
||||||
|
|
||||||
|
|
||||||
<MudPaper Height="5%" Class=" d-flex flex-grow-1" Style="background-color:mediumseagreen">
|
<MudPaper Class="d-flex flex-column flex-grow-1 overflow-hidden" Style="background-color:transparent">
|
||||||
<MudSpacer> </MudSpacer>
|
|
||||||
<AuthLinks/>
|
|
||||||
|
<MudPaper Elevation="3" Height="10%" Class=" d-flex justify-content-around flex-grow-0" Style="background-color:#ffffff55">
|
||||||
|
<NavBar Class="flex-column flex-grow-1 " Style="background-color:transparent" />
|
||||||
|
<AuthLinks Class="flex-column flex-grow-0 " Style="background-color:transparent" />
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
<MudPaper Height="95%" Class="d-flex flex-row flex-grow-1 overflow-hidden">
|
<MudPaper Elevation="3" Class="d-flex flex-row flex-grow-1 overflow-hidden" Style="background-color:transparent">
|
||||||
|
|
||||||
|
|
||||||
<MudPaper Width="5%" Class="pa-2 mr-1 d-flex flex-column flex-grow-0 justify-content-between">
|
<MudPaper Width="10%" Class="pa-2 ma-1 d-flex flex-column flex-grow-0 justify-content-between" Style="background-color:#ffffffaa">
|
||||||
<NavBar Class="flex-column flex-grow-0 rounded-pill" />
|
|
||||||
<AccountView Class="flex-column flex-grow-0 rounded-pill" />
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
<MudPaper Class="d-flex flex-grow-1 pa-3 ma-1 ">
|
<MudPaper Elevation="3" Class="d-flex flex-grow-1 pa-3 ma-1 overflow-hidden" Style="background-color:#ffffff22 ">
|
||||||
@Body
|
@Body
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
@@ -1,13 +1,17 @@
|
|||||||
<MudPaper Class=@Class>
|
<MudPaper Class=@Class Style=@Style>
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.Home" Class="py-5 px-3" Href="" Match="NavLinkMatch.All"></MudNavLink>
|
<MudStack Row="true">
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.Person" Class="py-5 px-3" Href="Account/Manage"></MudNavLink>
|
<MudNavLink Class="py-5 px-3" Href="" Match="NavLinkMatch.All"> 主页 </MudNavLink>
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.Edit" Class="py-5 px-3" Href="Edit"></MudNavLink>
|
<MudNavLink Class="py-5 px-3" Href="Account/Manage"> 个人中心 </MudNavLink>
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.Air" Class="py-5 px-3" Href="ai"></MudNavLink>
|
<MudNavLink Class="py-5 px-3" Href="Edit"> 编辑器 </MudNavLink>
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.SportsTennis" Class="py-5 px-3" Href="test"></MudNavLink>
|
<MudNavLink Class="py-5 px-3" Href="ai"> AI </MudNavLink>
|
||||||
|
<MudNavLink Class="py-5 px-3" Href="test"> 测试页面 </MudNavLink>
|
||||||
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Class { get; set; }
|
public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Style { get; set; }
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
<MudPaper Class=@Class>
|
<MudPaper Class=@Class Style="background-color:transparent">
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.Settings" Href="" Match="NavLinkMatch.All" Class="py-5 px-3"></MudNavLink>
|
<MudNavLink Icon="@Icons.Material.Filled.Settings" Href="" Match="NavLinkMatch.All" Class="py-5 px-3"></MudNavLink>
|
||||||
<MudNavLink Icon="@Icons.Material.Filled.Person4" Href="Account/Manage" Class="py-5 px-3"></MudNavLink>
|
<MudNavLink Icon="@Icons.Material.Filled.Person4" Href="Account/Manage" Class="py-5 px-3"></MudNavLink>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
@@ -1,23 +1,31 @@
|
|||||||
@inject IAuthenticationClientService AuthenticationClientService
|
@inject IAuthenticationClientService AuthenticationClientService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
<MudPaper Class=@Class Style=@Style>
|
||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
<MudText>
|
<MudText>
|
||||||
Hello, @context.User.Identity.Name!
|
Hello, @context.User.Identity.Name!
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudButton OnClick="Logout"> LOGOUT </MudButton>
|
<MudButton OnClick="Logout"> LOGOUT </MudButton>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
<NotAuthorized>
|
<NotAuthorized>
|
||||||
<MudButton Class="" Href="Register"> Register </MudButton>
|
<MudButton Class="" Href="Register"> Register </MudButton>
|
||||||
<MudButton Class="" Href="Login"> Login </MudButton>
|
<MudButton Class="" Href="Login"> Login </MudButton>
|
||||||
</NotAuthorized>
|
</NotAuthorized>
|
||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
|
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Class { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Style { get; set; }
|
||||||
|
|
||||||
private async Task Logout()
|
private async Task Logout()
|
||||||
{
|
{
|
||||||
await AuthenticationClientService.LogoutAsync();
|
await AuthenticationClientService.LogoutAsync();
|
||||||
|
@@ -2,85 +2,138 @@
|
|||||||
@using Blazored.TextEditor
|
@using Blazored.TextEditor
|
||||||
@using System.Text.RegularExpressions
|
@using System.Text.RegularExpressions
|
||||||
@using TechHelper.Client.Pages.Exam
|
@using TechHelper.Client.Pages.Exam
|
||||||
<MudPaper Class="d-flex flex-column flex-grow-1">
|
<MudPaper Class="d-flex flex-column flex-grow-1 pa-4" Elevation="0" Style="background-color:#ffffff55">
|
||||||
<MudPaper class="d-flex flex-grow-0 flex-column">
|
|
||||||
|
|
||||||
@if (@lode == true)
|
<MudPaper Class="d-flex flex-column flex-grow-0 mb-4" Elevation="1" Style="background-color:#FFFFFF22">
|
||||||
{
|
|
||||||
<MudStack Row="true">
|
|
||||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
}
|
<MudPaper Class="d-flex flex-row flex-grow-0 justify-content-between mb-4" Elevation="1" Style="background-color:#FFFFFF22">
|
||||||
<MudButtonGroup Color="Color.Primary" Variant="Variant.Filled">
|
<MudButtonGroup Variant="Variant.Filled" OverrideStyles="true" Class="pa-2">
|
||||||
<MudButton OnClick="GetHTML">One</MudButton>
|
<MudButton OnClick="TriggerFullAIParsingProcessAsync" Disabled="@_isProcessing"
|
||||||
<MudButton OnClick="ParseQuestions">ParseQuestions</MudButton>
|
Style="background-color: var(--mud-palette-primary); color: white;">
|
||||||
<MudButton OnClick="ParseXML">ParseXML</MudButton>
|
@if (_isProcessing)
|
||||||
<MudButton OnClick="ParseWithAI">ParseWithAI</MudButton>
|
{
|
||||||
<MudButton OnClick="ReCorrectXMLAsync">ReCorrectXML</MudButton>
|
<MudProgressCircular Indeterminate="true" Size="Size.Small" Class="mr-2" />
|
||||||
<MudButton OnClick="ReCorrectXMLAsync">AplyAIResult</MudButton>
|
}
|
||||||
<MudButton OnClick="ReCorrectXMLAsync">Save</MudButton>
|
**全自动流程 (AI)**
|
||||||
<MudButton OnClick="ReCorrectXMLAsync">Public</MudButton>
|
</MudButton>
|
||||||
<MudButton OnClick="CopyToClipboard">Copy</MudButton>
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="GetEditorTextContentAsync" Disabled="@_isProcessing">
|
||||||
</MudButtonGroup>
|
获取编辑器HTML
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="DivideExamContentByAIAsync" Disabled="@_isProcessing">
|
||||||
|
AI 分割题组 (仅文本)
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="ConvertDividedXmlToQuestionList" Disabled="@_isProcessing">
|
||||||
|
转换为题组列表
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="ParseEachQuestionGroupAsync" Disabled="@_isProcessing">
|
||||||
|
AI 解析每个题组 (仅文本)
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="ConvertParsedXmlsToQuestionGroups" Disabled="@_isProcessing">
|
||||||
|
转换为题组对象
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Tertiary" OnClick="CopyToClipboard" Disabled="@_isProcessing">
|
||||||
|
复制当前结果
|
||||||
|
</MudButton>
|
||||||
|
|
||||||
|
|
||||||
|
</MudButtonGroup>
|
||||||
|
|
||||||
|
<MudButtonGroup Variant="Variant.Filled" OverrideStyles="true" Class="pa-2">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Info" Disabled="@_isProcessing">
|
||||||
|
保存
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Success" Disabled="@_isProcessing">
|
||||||
|
发布
|
||||||
|
</MudButton>
|
||||||
|
</MudButtonGroup>
|
||||||
|
</MudPaper>
|
||||||
|
<MudPaper Style="background-color:#FFFFFF22">
|
||||||
|
<MudText Typo="Typo.body2" Class="mt-2 ml-2">**当前状态:** @_processingStatusMessage</MudText>
|
||||||
|
</MudPaper>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden">
|
|
||||||
|
|
||||||
|
<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden" Elevation="0" Style="background-color:#FFFFFF77">
|
||||||
|
|
||||||
|
<MudPaper Class="d-flex flex-column flex-grow-1 mr-4" Style="min-width: 400px; max-width: 70%;background-color:#FFFFFFaa" Elevation="2">
|
||||||
|
<MudTabs Elevation="0" Rounded="true" PanelClass="pa-4 flex-grow-1 justify-content-around overflow-auto" Style="background-color:#FFFFFF11"
|
||||||
|
Class="flex-grow-1 d-flex flex-column overflow-auto">
|
||||||
|
|
||||||
<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 overflow-auto">
|
<MudTabPanel Text="编辑器内容" Icon="@Icons.Material.Filled.Edit" Style="background-color:#FFFFFF11">
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h6">编辑器原始HTML/文本内容:</MudText>
|
||||||
|
<MudTextField T="string" @bind-value="@_editorHtmlContent" MaxLines="20" Variant="Variant.Outlined" AutoGrow="true" />
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
<MudTabPanel Text="分割XML" Icon="@Icons.Material.Filled.Code">
|
||||||
|
<MudText Typo="Typo.h6">AI 分割后的原始XML内容:</MudText>
|
||||||
|
<MudTextField T="string" @bind-value="@_rawDividedExamXmlContent" MaxLines="20" Variant="Variant.Outlined" AutoGrow="true" />
|
||||||
|
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
@if (QuestionS != null && QuestionS.Any())
|
<MudTabPanel Text="题组列表" Icon="@Icons.Material.Filled.List">
|
||||||
{
|
@if (_dividedQuestionGroupList != null && _dividedQuestionGroupList.Items.Any())
|
||||||
@foreach (var item in QuestionS)
|
{
|
||||||
{
|
<MudText Typo="Typo.h6">转换为题组列表 (StringsList):</MudText>
|
||||||
<QuestionGroupDisplay QuestionGroup="item" IsNested="false" />
|
@foreach (var item in _dividedQuestionGroupList.Items)
|
||||||
}
|
{
|
||||||
}
|
<MudTextField T="string" Value="@item" MaxLines="5" Variant="Variant.Outlined" Class="mb-2" AutoGrow="true" />
|
||||||
else
|
}
|
||||||
{
|
}
|
||||||
<MudText Typo="Typo.body1">暂无试题内容。</MudText>
|
else
|
||||||
}
|
{
|
||||||
|
<MudText Typo="Typo.body1">将分割XML转换为题组列表 (StringsList) 后将显示在此处。</MudText>
|
||||||
|
}
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
@* Tab 4: 每个题组的原始 XML (Raw Parsed XMLs) *@
|
||||||
|
<MudTabPanel Text="解析XML" Icon="@Icons.Material.Filled.Article">
|
||||||
|
<ChildContent>
|
||||||
|
<MudText Typo="Typo.h6">AI 解析的每个题组原始XML内容:</MudText>
|
||||||
|
@for (int i = 0; i < _rawParsedQuestionXmls.Count; i++)
|
||||||
|
{
|
||||||
|
int index = i; // 捕获迭代变量
|
||||||
|
<MudCard Class="mb-3" Style="background-color:#ffffff88">
|
||||||
|
<MudCardHeader>
|
||||||
|
<MudCardActions>
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="(() => DeleteFromParse(index))">删除</MudButton>
|
||||||
|
</MudCardActions>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent>
|
||||||
|
<MudTextField Class="ma-0" AutoGrow="true" @bind-Value="_rawParsedQuestionXmls[index]" Variant="Variant.Outlined" />
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
}
|
||||||
|
|
||||||
|
</ChildContent>
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
@* Tab 5: 最终的 QuestionGroup 对象 *@
|
||||||
|
<MudTabPanel Text="最终结果" Icon="@Icons.Material.Filled.Check">
|
||||||
|
@if (_finalQuestionGroups != null && _finalQuestionGroups.Any())
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h6">最终解析的题组对象:</MudText>
|
||||||
|
@foreach (var item in _finalQuestionGroups)
|
||||||
|
{
|
||||||
|
<QuestionGroupDisplay QuestionGroup="item" IsNested="false" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body1">最终解析为 QuestionGroup 对象的试题内容将显示在此处。</MudText>
|
||||||
|
}
|
||||||
|
</MudTabPanel>
|
||||||
|
|
||||||
|
</MudTabs>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
<MudPaper Class="d-flex flex-column flex-grow-1" Style="min-width: 200px; max-width: 30%; background-color:#FFFFFF77;" Elevation="2">
|
||||||
|
<BlazoredTextEditor @ref="@_quillHtmlEditor" EditorCssStyle="height: 93%;" >
|
||||||
<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 justify-content-between overflow-auto">
|
<ToolbarContent >
|
||||||
<MudText Typo="Typo.body1">@ProgStatues</MudText>
|
|
||||||
|
|
||||||
@for (int i = 0; i < ParseResult.Count; i++)
|
|
||||||
{
|
|
||||||
int index = i;
|
|
||||||
<MudCard Style="background-color:#ffffff88">
|
|
||||||
<MudCardHeader>
|
|
||||||
<MudCardActions>
|
|
||||||
<MudButton Variant="Variant.Filled" Color ="Color.Primary" OnClick="(()=>DeleteFromParse(index))"> Delete </MudButton>
|
|
||||||
</MudCardActions>
|
|
||||||
</MudCardHeader>
|
|
||||||
<MudCardContent>
|
|
||||||
|
|
||||||
<MudTextField Class="ma-3" AutoGrow="true" @bind-Value="ParseResult[index]"></MudTextField>
|
|
||||||
</MudCardContent>
|
|
||||||
</MudCard>
|
|
||||||
}
|
|
||||||
<MudText>@Error</MudText>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 overflow-auto">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<BlazoredTextEditor @ref="@QuillHtml">
|
|
||||||
<ToolbarContent>
|
|
||||||
<select class="ql-header">
|
<select class="ql-header">
|
||||||
<option selected=""></option>
|
<option selected=""></option>
|
||||||
<option value="1"></option>
|
<option value="1"></option>
|
||||||
@@ -107,17 +160,10 @@
|
|||||||
<button class="ql-link"></button>
|
<button class="ql-link"></button>
|
||||||
</span>
|
</span>
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
|
<EditorContent >
|
||||||
<EditorContent>
|
|
||||||
</EditorContent>
|
</EditorContent>
|
||||||
</BlazoredTextEditor>
|
</BlazoredTextEditor>
|
||||||
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</MudPaper>
|
</MudPaper>
|
@@ -1,172 +1,528 @@
|
|||||||
using Blazored.TextEditor;
|
using Blazored.TextEditor;
|
||||||
using Entities.Contracts;
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using TechHelper.Client.AI;
|
using TechHelper.Client.AI;
|
||||||
using TechHelper.Client.Exam;
|
using TechHelper.Client.Exam;
|
||||||
|
using TechHelper.Services;
|
||||||
using static Org.BouncyCastle.Crypto.Engines.SM2Engine;
|
using static Org.BouncyCastle.Crypto.Engines.SM2Engine;
|
||||||
|
|
||||||
namespace TechHelper.Client.Pages.Editor
|
namespace TechHelper.Client.Pages.Editor
|
||||||
{
|
{
|
||||||
public enum ProgEnum
|
public enum ProcessingStage
|
||||||
{
|
{
|
||||||
AIPrase,
|
Idle, // <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>״̬
|
||||||
AIRectify
|
FetchingContent, // <20><><EFBFBD>ڻ<EFBFBD>ȡ<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
DividingExam, // <20><><EFBFBD>ڷָ<DAB7><D6B8><EFBFBD><EFBFBD><EFBFBD> (AI<41><49><EFBFBD><EFBFBD>ԭʼXML)
|
||||||
|
ConvertingDividedXml, // <20><><EFBFBD>ڽ<EFBFBD><DABD>ָ<EFBFBD>XMLת<4C><D7AA>ΪStringsList
|
||||||
|
ParsingGroups, // <20><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (AI<41><49><EFBFBD><EFBFBD>ԭʼXML)
|
||||||
|
ConvertingParsedXmls, // <20><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>XMLת<4C><D7AA>ΪQuestionGroup<75><70><EFBFBD><EFBFBD>
|
||||||
|
Completed, // <20><><EFBFBD>д<EFBFBD><D0B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
ErrorOccurred, // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
Saving
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class EditorMain
|
public partial class EditorMain
|
||||||
{
|
{
|
||||||
private List<QuestionGroup> QuestionS = new List<QuestionGroup>();
|
|
||||||
|
|
||||||
private bool lode = false;
|
|
||||||
BlazoredTextEditor QuillHtml;
|
|
||||||
string QuillHTMLContent;
|
|
||||||
string AIParseResult;
|
|
||||||
string Error;
|
|
||||||
string ProgStatues = string.Empty;
|
|
||||||
List<string> ParseResult = new List<string>();
|
|
||||||
|
|
||||||
public async Task GetHTML()
|
|
||||||
{
|
|
||||||
QuillHTMLContent = await this.QuillHtml.GetHTML();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task GetText()
|
|
||||||
{
|
|
||||||
QuillHTMLContent = await this.QuillHtml.GetText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string EditorHtmlContent { get; set; } = string.Empty;
|
|
||||||
private List<ParsedQuestion>? ParsedQuestions { get; set; }
|
|
||||||
private bool _parseAttempted = false;
|
|
||||||
|
|
||||||
public class ParsedQuestion
|
|
||||||
{
|
|
||||||
public int Id { get; set; } // <20><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
public string? Content { get; set; } // <20><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><><EFBFBD><EFBFBD>
|
|
||||||
public string? Title { get; set; } // <20><>Ŀ<EFBFBD>ı<EFBFBD><C4B1>ⲿ<EFBFBD>֣<EFBFBD><D6A3><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD> Content <20><><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1>
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
public IAIService aIService { get; set; }
|
public IExamService ExamService { get; set; } = default!;
|
||||||
[Inject]
|
[Inject]
|
||||||
public ISnackbar Snackbar { get; set; }
|
public ISnackbar Snackbar { get; set; } = default!;
|
||||||
private async void ParseWithAI()
|
|
||||||
|
// --- UI <20><EFBFBD>״̬<D7B4><CCAC><EFBFBD><EFBFBD> ---
|
||||||
|
private BlazoredTextEditor? _quillHtmlEditor; // <20><><EFBFBD>ı<EFBFBD><C4B1>༭<EFBFBD><E0BCAD>ʵ<EFBFBD><CAB5>
|
||||||
|
private string _editorHtmlContent = string.Empty; // <20>洢<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD> HTML/<2F>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
private bool _isProcessing = false; // <20><><EFBFBD>Ƽ<EFBFBD><C6BC><EFBFBD>״̬<D7B4>ı<EFBFBD>־
|
||||||
|
private string _processingStatusMessage = "<22>ȴ<EFBFBD><C8B4><EFBFBD><EFBFBD><EFBFBD>..."; // <20><>ʾ<EFBFBD><CABE><EFBFBD>û<EFBFBD><C3BB>ĵ<EFBFBD>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD>״̬<D7B4><CCAC>Ϣ
|
||||||
|
|
||||||
|
// --- <20>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD>ݽṹ (<28>洢ÿһ<C3BF><D2BB><EFBFBD>Ľ<EFBFBD><C4BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԭʼ<D4AD>ı<EFBFBD>) ---
|
||||||
|
// 1. AI <20>ָ<EFBFBD>ԭʼ<D4AD><CABC>Ӧ (XML <20>ı<EFBFBD>)
|
||||||
|
private string? _rawDividedExamXmlContent;
|
||||||
|
// 2. <20><> _rawDividedExamXmlContent ת<><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> StringsList
|
||||||
|
private StringsList? _dividedQuestionGroupList;
|
||||||
|
|
||||||
|
// 3. AI <20><><EFBFBD><EFBFBD>ÿ<EFBFBD><C3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԭʼ<D4AD><CABC>Ӧ (XML <20>ı<EFBFBD><C4B1>б<EFBFBD>)
|
||||||
|
private List<string> _rawParsedQuestionXmls = new();
|
||||||
|
// 4. <20><> _rawParsedQuestionXmls ת<><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> QuestionGroup <20><><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>
|
||||||
|
private List<QuestionGroup> _finalQuestionGroups = new();
|
||||||
|
|
||||||
|
|
||||||
|
// --- Blazor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڷ<EFBFBD><DAB7><EFBFBD> ---
|
||||||
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
QuestionS.Clear();
|
_processingStatusMessage = ProcessingStage.Idle.ToString();
|
||||||
ParseResult.Clear();
|
}
|
||||||
|
|
||||||
await GetText();
|
// --- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ---
|
||||||
lode = true;
|
|
||||||
|
// ͳһ<CDB3><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><F3B2A2B8><EFBFBD> UI
|
||||||
|
private void HandleProcessError(string errorMessage, Exception? ex = null)
|
||||||
|
{
|
||||||
|
_processingStatusMessage = $"{ProcessingStage.ErrorOccurred}: {errorMessage}";
|
||||||
|
Snackbar.Add(errorMessage, Severity.Error);
|
||||||
|
_isProcessing = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
|
if (ex != null)
|
||||||
ProgStatues = ProgEnum.AIPrase.ToString();
|
|
||||||
ProgStatues = $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<2C><><EFBFBD>ȴ<EFBFBD>";
|
|
||||||
Snackbar.Add("<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<2C><><EFBFBD>ȴ<EFBFBD>", Severity.Info);
|
|
||||||
StateHasChanged();
|
|
||||||
string respon = await aIService.CallGLM(QuillHTMLContent, AIConfiguration.BreakQuestions);
|
|
||||||
if (respon == null)
|
|
||||||
{
|
{
|
||||||
lode = false;
|
// <20><>¼<EFBFBD><C2BC><EFBFBD><EFBFBD>ϸ<EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD>־<EFBFBD><D6BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̨<EFBFBD><CCA8><EFBFBD><EFBFBD>־ϵͳ
|
||||||
Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>");
|
Console.Error.WriteLine($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}\n{ex.StackTrace}\n<>ڲ<EFBFBD><DAB2>쳣: {ex.InnerException?.Message}");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var ParRespon = ExamParser.ParseExamXml<StringsList>(respon);
|
// <20><><EFBFBD>´<EFBFBD><C2B4><EFBFBD>״̬<D7B4><CCAC> UI
|
||||||
|
private void UpdateProcessingStatus(ProcessingStage stage, string message)
|
||||||
|
{
|
||||||
if (ParRespon != null)
|
_processingStatusMessage = $"{stage}: {message}";
|
||||||
{
|
Snackbar.Add(message, Severity.Info);
|
||||||
int i = 1;
|
|
||||||
foreach (var item in ParRespon.Items)
|
|
||||||
{
|
|
||||||
ProgStatues = $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>, <20><><EFBFBD>ȴ<EFBFBD>";
|
|
||||||
Snackbar.Add($"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>, <20><><EFBFBD>ȴ<EFBFBD>", Severity.Info);
|
|
||||||
StateHasChanged();
|
|
||||||
i++;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var parResult = await aIService.CallGLM(item, AIConfiguration.ParseSignelQuestion2);
|
|
||||||
ParseResult.Add(parResult);
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD>Ϊ:{ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AIParseResult = respon;
|
|
||||||
|
|
||||||
ProgStatues = ProgEnum.AIRectify.ToString();
|
|
||||||
Snackbar.Add($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>", Severity.Info);
|
|
||||||
//await ReCorrectXMLAsync();
|
|
||||||
|
|
||||||
|
|
||||||
ProgStatues = string.Empty;
|
|
||||||
lode = false;
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߷<EFBFBD><DFB7><EFBFBD> (<28><><EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڻ<EFBFBD>ȡ<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>) ---
|
||||||
|
|
||||||
private async Task ReCorrectXMLAsync()
|
// <20><>ȡ<EFBFBD>༭<EFBFBD><E0BCAD> HTML <20><><EFBFBD><EFBFBD>
|
||||||
|
public async Task GetEditorHtmlContentAsync()
|
||||||
{
|
{
|
||||||
string respon = string.Empty;
|
if (_isProcessing) return;
|
||||||
|
_isProcessing = true;
|
||||||
|
UpdateProcessingStatus(ProcessingStage.FetchingContent, "<22><><EFBFBD>ڻ<EFBFBD>ȡ<EFBFBD>༭<EFBFBD><E0BCAD>HTML<4D><4C><EFBFBD><EFBFBD>...");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (var item in ParseResult)
|
if (_quillHtmlEditor != null)
|
||||||
{
|
{
|
||||||
//respon = await aIService.CallGLM(AIParseResult, AIConfiguration.ParseSignelQuestion);
|
_editorHtmlContent = await _quillHtmlEditor.GetHTML();
|
||||||
var xmlResult = ExamParser.ParseExamXml<QuestionGroup>(item);
|
UpdateProcessingStatus(ProcessingStage.FetchingContent, "<22>༭<EFBFBD><E0BCAD>HTML<4D><4C><EFBFBD><EFBFBD><EFBFBD>ѳɹ<D1B3><C9B9><EFBFBD>ȡ<EFBFBD><C8A1>");
|
||||||
QuestionS.Add(xmlResult);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleProcessError("<22>༭<EFBFBD><E0BCAD>ʵ<EFBFBD><CAB5>δ<CEB4><D7BC><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><DEB7><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ݡ<EFBFBD>");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>" + ex.Message, Severity.Error);
|
HandleProcessError($"<22><>ȡ<EFBFBD>༭<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(respon))
|
finally
|
||||||
{
|
{
|
||||||
lode = false;
|
_isProcessing = false;
|
||||||
Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>", Severity.Error);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
AIParseResult = respon;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ParseXML()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var paper = ExamParser.ParseExamXml<QuestionGroup>(AIParseResult);
|
|
||||||
//QuestionS = paper.QuestionGroups;
|
|
||||||
Error = string.Empty;
|
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" + ex.Message, Severity.Error);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" + ex.Message, Severity.Error);
|
|
||||||
Error = ex.Message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void DeleteFromParse(int index)
|
|
||||||
{
|
|
||||||
if (index >= 0 && index < ParseResult.Count)
|
|
||||||
{
|
|
||||||
ParseResult.RemoveAt(index);
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// <20><>ȡ<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7><EFBFBD><EFBFBD><EFBFBD>)
|
||||||
|
public async Task GetEditorTextContentAsync()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return;
|
||||||
|
_isProcessing = true;
|
||||||
|
UpdateProcessingStatus(ProcessingStage.FetchingContent, "<22><><EFBFBD>ڻ<EFBFBD>ȡ<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD>...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_quillHtmlEditor != null)
|
||||||
|
{
|
||||||
|
_editorHtmlContent = await _quillHtmlEditor.GetText();
|
||||||
|
UpdateProcessingStatus(ProcessingStage.FetchingContent, "<22>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѳɹ<D1B3><C9B9><EFBFBD>ȡ<EFBFBD><C8A1>");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
HandleProcessError("<22>༭<EFBFBD><E0BCAD>ʵ<EFBFBD><CAB5>δ<CEB4><D7BC><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><DEB7><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ݡ<EFBFBD>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"<22><>ȡ<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- <20><><EFBFBD><EFBFBD>ҵ<EFBFBD><D2B5><EFBFBD><EFBFBD><DFBC><EFBFBD><EFBFBD><EFBFBD> (ÿ<><C3BF><EFBFBD><EFBFBD><EFBFBD>趼<EFBFBD><E8B6BC><EFBFBD>ֶ<EFBFBD><D6B6><EFBFBD><EFBFBD><EFBFBD>) ---
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> 1: <20><><EFBFBD><EFBFBD> AI <20><><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD> (ֻ<><D6BB>ȡԭʼ XML <20>ı<EFBFBD>)
|
||||||
|
public async Task DivideExamContentByAIAsync()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return;
|
||||||
|
_isProcessing = true;
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڴ˲<DAB4><CBB2><EFBFBD><EFBFBD>Ľ<EFBFBD><C4BD><EFBFBD>
|
||||||
|
_rawDividedExamXmlContent = null;
|
||||||
|
_dividedQuestionGroupList = null;
|
||||||
|
_rawParsedQuestionXmls.Clear();
|
||||||
|
_finalQuestionGroups.Clear();
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.DividingExam, "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AI <20>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD>飬<EFBFBD><E9A3AC><EFBFBD>ȴ<EFBFBD>...");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_editorHtmlContent))
|
||||||
|
{
|
||||||
|
HandleProcessError("<22>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD>գ<EFBFBD><D5A3><EFBFBD><EFBFBD>Ȼ<EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ݡ<EFBFBD>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await ExamService.DividExam(_editorHtmlContent);
|
||||||
|
if (!response.Status)
|
||||||
|
{
|
||||||
|
HandleProcessError(response.Message ?? "AI<41><49><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD>ʧ<EFBFBD>ܡ<EFBFBD>", response.Result as Exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// **<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԭʼ<D4AD><CABC> XML <20>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڴ˲<DAB4><CBB2><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>**
|
||||||
|
_rawDividedExamXmlContent = response.Result as string;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_rawDividedExamXmlContent))
|
||||||
|
{
|
||||||
|
HandleProcessError("AI <20><><EFBFBD>صķָ<C4B7><D6B8><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD>ա<EFBFBD>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UpdateProcessingStatus(ProcessingStage.DividingExam, response.Message ?? "AI<41><49><EFBFBD><EFBFBD><EFBFBD>ָ<EFBFBD><D6B8>ɹ<EFBFBD><C9B9><EFBFBD>ԭʼXML<4D>ѱ<EFBFBD><D1B1>档");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"<22>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> 2: <20><>ԭʼ<D4AD>ָ<EFBFBD> XML <20>ı<EFBFBD>ת<EFBFBD><D7AA>Ϊ StringsList
|
||||||
|
public void ConvertDividedXmlToQuestionList()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return; // <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><CEAA>UI<55><49><EFBFBD>ð<EFBFBD>ť<EFBFBD><C5A5><EFBFBD>Լ<EFBFBD><D4BC><EFBFBD>
|
||||||
|
_isProcessing = true;
|
||||||
|
_dividedQuestionGroupList = null; // <20><><EFBFBD><EFBFBD><EFBFBD>ϴν<CFB4><CEBD><EFBFBD>
|
||||||
|
_rawParsedQuestionXmls.Clear();
|
||||||
|
_finalQuestionGroups.Clear();
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ConvertingDividedXml, "<22><><EFBFBD>ڽ<EFBFBD><DABD>ָ<EFBFBD>XMLת<4C><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>...");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_rawDividedExamXmlContent))
|
||||||
|
{
|
||||||
|
HandleProcessError("û<><C3BB>ԭʼ<D4AD>ָ<EFBFBD>XML<4D>ı<EFBFBD><C4B1>ɹ<EFBFBD>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ִ<EFBFBD><D6B4> '<27>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD> (AI)' <20><><EFBFBD>衣");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD><EFBFBD> ExamService <20><>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>
|
||||||
|
var xmlConversionResponse = ExamService.ConvertToXML<StringsList>(_rawDividedExamXmlContent);
|
||||||
|
|
||||||
|
if (!xmlConversionResponse.Status)
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԭʼXMLת<4C><D7AA>ΪStringsListʧ<74>ܣ<EFBFBD><DCA3><EFBFBD>ԭʼXML<4D><4C>Ȼ<EFBFBD><C8BB><EFBFBD><EFBFBD>
|
||||||
|
HandleProcessError(xmlConversionResponse.Message ?? "AI<41><49><EFBFBD>ص<EFBFBD>XML<4D><EFBFBD>ת<EFBFBD><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD>", xmlConversionResponse.Result as Exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dividedQuestionGroupList = xmlConversionResponse.Result as StringsList;
|
||||||
|
if (_dividedQuestionGroupList == null || !_dividedQuestionGroupList.Items.Any())
|
||||||
|
{
|
||||||
|
HandleProcessError("AI <20><><EFBFBD>ص<F1B7B5BB>XML<4D><4C>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>Ϊ<EFBFBD>ա<EFBFBD>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ConvertingDividedXml, "<22>ָ<EFBFBD>XML<4D>ѳɹ<D1B3>ת<EFBFBD><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD>");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"ת<><D7AA><EFBFBD>ָ<EFBFBD>XML<4D><4C><EFBFBD>б<EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> 3: ѭ<><D1AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AI <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ÿ<EFBFBD><C3BF><EFBFBD>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (ֻ<><D6BB>ȡԭʼ XML <20>ı<EFBFBD>)
|
||||||
|
public async Task ParseEachQuestionGroupAsync()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return;
|
||||||
|
_isProcessing = true;
|
||||||
|
_rawParsedQuestionXmls.Clear(); // <20><><EFBFBD><EFBFBD><EFBFBD>ϴν<CFB4><CEBD><EFBFBD>
|
||||||
|
_finalQuestionGroups.Clear();
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ParsingGroups, "<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD>ÿ<EFBFBD><C3BF><EFBFBD><EFBFBD><EFBFBD>飬<EFBFBD><E9A3AC><EFBFBD>ȴ<EFBFBD>...");
|
||||||
|
|
||||||
|
if (_dividedQuestionGroupList == null || !_dividedQuestionGroupList.Items.Any())
|
||||||
|
{
|
||||||
|
HandleProcessError("û<>пɽ<D0BF><C9BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD><D0B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ִ<EFBFBD><D6B4> 'ת<><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD>б<EFBFBD>' <20><><EFBFBD>衣");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int currentGroupIndex = 1;
|
||||||
|
foreach (var itemXml in _dividedQuestionGroupList.Items)
|
||||||
|
{
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ParsingGroups, $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD> {currentGroupIndex} <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>...");
|
||||||
|
|
||||||
|
var parseResponse = await ExamService.ParseSingleQuestionGroup(itemXml);
|
||||||
|
if (!parseResponse.Status)
|
||||||
|
{
|
||||||
|
// <20><>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD>ʧ<EFBFBD>ܣ<EFBFBD>ԭʼ<D4AD><CABC> _dividedQuestionGroupList <20><>Ȼ<EFBFBD><C8BB><EFBFBD><EFBFBD>
|
||||||
|
HandleProcessError($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> {currentGroupIndex} <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: {parseResponse.Message}", parseResponse.Result as Exception);
|
||||||
|
// <20><><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰʧ<C7B0>ܵ<EFBFBD><DCB5><EFBFBD><EEB2A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1>ֹͣ
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// **<2A><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԭʼ<D4AD><CABC> XML <20>ı<EFBFBD><C4B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڴ˲<DAB4><CBB2><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>**
|
||||||
|
_rawParsedQuestionXmls.Add(parseResponse.Result as string ?? string.Empty);
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ParsingGroups, $"<22><> {currentGroupIndex} <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD><C9B9><EFBFBD>");
|
||||||
|
currentGroupIndex++;
|
||||||
|
}
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ParsingGroups, "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѳɹ<D1B3><C9B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ԭʼXML<4D>ѱ<EFBFBD><D1B1>档");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> 4: <20><>ԭʼ<D4AD><CABC><EFBFBD><EFBFBD> XML <20>ı<EFBFBD>ת<EFBFBD><D7AA>Ϊ QuestionGroup <20><><EFBFBD><EFBFBD>
|
||||||
|
public void ConvertParsedXmlsToQuestionGroups()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return; // <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
_isProcessing = true;
|
||||||
|
_finalQuestionGroups.Clear(); // <20><><EFBFBD><EFBFBD><EFBFBD>ϴν<CFB4><CEBD><EFBFBD>
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ConvertingParsedXmls, "<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>XMLת<4C><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȴ<EFBFBD>...");
|
||||||
|
|
||||||
|
if (!_rawParsedQuestionXmls.Any())
|
||||||
|
{
|
||||||
|
HandleProcessError("û<>п<EFBFBD>ת<EFBFBD><D7AA><EFBFBD><EFBFBD>ԭʼ<D4AD><CABC><EFBFBD><EFBFBD>XML<4D><4C><EFBFBD>ݡ<EFBFBD><DDA1><EFBFBD><EFBFBD><EFBFBD>ִ<EFBFBD><D6B4> '<27><><EFBFBD><EFBFBD>ÿ<EFBFBD><C3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (AI)' <20><><EFBFBD>衣");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var xmlString in _rawParsedQuestionXmls)
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD><EFBFBD> ExamService <20><>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>
|
||||||
|
ApiResponse xmlConversionResponse = ExamService.ConvertToXML<QuestionGroup>(xmlString);
|
||||||
|
|
||||||
|
if (!xmlConversionResponse.Status)
|
||||||
|
{
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>ʧ<EFBFBD>ܣ<EFBFBD><DCA3><EFBFBD>ԭʼXML<4D><4C>Ȼ<EFBFBD><C8BB><EFBFBD><EFBFBD>
|
||||||
|
HandleProcessError($"XML ת<><D7AA>Ϊ QuestionGroup ʧ<><CAA7>: {xmlConversionResponse.Message}", xmlConversionResponse.Result as Exception);
|
||||||
|
// <20><><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰʧ<C7B0>ܵ<EFBFBD><DCB5><EFBFBD><EEB2A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѡ<EFBFBD><D1A1>ֹͣ
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QuestionGroup? questionGroup = xmlConversionResponse.Result as QuestionGroup;
|
||||||
|
if (questionGroup != null)
|
||||||
|
{
|
||||||
|
_finalQuestionGroups.Add(questionGroup);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// <20><>ʹ Status Ϊ true<75><65>Result Ҳ<><D2B2><EFBFBD>ܲ<EFBFBD><DCB2><EFBFBD>Ԥ<EFBFBD>ڵ<EFBFBD><DAB5><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
HandleProcessError("XML ת<><D7AA><EFBFBD>ɹ<EFBFBD><C9B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ؽ<EFBFBD><D8BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ͳ<EFBFBD>ƥ<EFBFBD><C6A5> QuestionGroup<75><70>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UpdateProcessingStatus(ProcessingStage.ConvertingParsedXmls, "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>XML<4D>ѳɹ<D1B3>ת<EFBFBD><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"ת<><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>XML<4D><4C><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_finalQuestionGroups.Count > 0)
|
||||||
|
OrderQuestionGroup(_finalQuestionGroups);
|
||||||
|
_isProcessing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void OrderQuestionGroup(List<QuestionGroup> QG)
|
||||||
|
{
|
||||||
|
int index = 1;
|
||||||
|
QG.ForEach(qg =>
|
||||||
|
{
|
||||||
|
qg.Id = (byte)index++;
|
||||||
|
int sqIndex = 1;
|
||||||
|
qg.SubQuestions.ForEach(sq => sq.SubId = (byte)sqIndex++);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
QG.ForEach(qg => OrderQuestionGroup(qg.SubQuestionGroups));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ExamDto MapToCreateExamDto(List<QuestionGroup> questionGroups)
|
||||||
|
{
|
||||||
|
var createDto = new ExamDto();
|
||||||
|
createDto.QuestionGroups = MapQuestionGroupsToDto(questionGroups);
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҪΪ<D2AA><CEAA><EFBFBD>α<EFBFBD><CEB1><EFBFBD>ָ<EFBFBD><D6B8>һ<EFBFBD><D2BB>AssignmentId<49><64><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
// createDto.AssignmentId = YourCurrentAssignmentId;
|
||||||
|
return createDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<QuestionGroupDto> MapQuestionGroupsToDto(List<QuestionGroup> qgs)
|
||||||
|
{
|
||||||
|
var dtos = new List<QuestionGroupDto>();
|
||||||
|
foreach (var qg in qgs)
|
||||||
|
{
|
||||||
|
var qgDto = new QuestionGroupDto
|
||||||
|
{
|
||||||
|
Title = qg.Title,
|
||||||
|
Score = qg.Score,
|
||||||
|
QuestionReference = qg.QuestionReference,
|
||||||
|
SubQuestions = MapSubQuestionsToDto(qg.SubQuestions),
|
||||||
|
SubQuestionGroups = MapQuestionGroupsToDto(qg.SubQuestionGroups)
|
||||||
|
};
|
||||||
|
dtos.Add(qgDto);
|
||||||
|
}
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SubQuestionDto> MapSubQuestionsToDto(List<SubQuestion> sqs)
|
||||||
|
{
|
||||||
|
var dtos = new List<SubQuestionDto>();
|
||||||
|
foreach (var sq in sqs)
|
||||||
|
{
|
||||||
|
var sqDto = new SubQuestionDto
|
||||||
|
{
|
||||||
|
Index = sq.SubId,
|
||||||
|
Stem = sq.Stem,
|
||||||
|
Score = sq.Score,
|
||||||
|
SampleAnswer = sq.SampleAnswer,
|
||||||
|
Options = sq.Options.Select(o => new OptionDto { Value = o.Value }).ToList(),
|
||||||
|
// TODO: <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Щֵ<D0A9>ܴ<EFBFBD>AI<41><49><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ij<EFBFBD><C4B3>Ĭ<EFBFBD><C4AC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>л<EFBFBD>ȡ
|
||||||
|
// <20><><EFBFBD>磺QuestionType = "SingleChoice",
|
||||||
|
// DifficultyLevel = "Medium",
|
||||||
|
// SubjectArea = "Math"
|
||||||
|
};
|
||||||
|
dtos.Add(sqDto);
|
||||||
|
}
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Save()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return;
|
||||||
|
_isProcessing = true;
|
||||||
|
UpdateProcessingStatus(ProcessingStage.Saving, "<22><><EFBFBD><EFBFBD><EFBFBD><D7BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݵ<EFBFBD><DDB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>...");
|
||||||
|
|
||||||
|
if (!_finalQuestionGroups.Any())
|
||||||
|
{
|
||||||
|
HandleProcessError("û<>пɱ<D0BF><C9B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݡ<EFBFBD><DDA1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD><C7B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>н<EFBFBD><D0BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>衣");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ȷ<><C8B7> _finalQuestionGroups <20>Ѿ<EFBFBD><D1BE><EFBFBD><EFBFBD><EFBFBD> Index <20><><EFBFBD><EFBFBD> (<28><><EFBFBD><EFBFBD>֮ǰ<D6AE><C7B0> ConvertParsedXmlsToQuestionGroups ֮<><D6AE><EFBFBD><EFBFBD> Save <20><>ʼʱ<CABC><CAB1><EFBFBD><EFBFBD>)
|
||||||
|
_finalQuestionGroups = _finalQuestionGroups.OrderBy(qg => qg.Id).ToList();
|
||||||
|
|
||||||
|
// ӳ<>䵽 DTO
|
||||||
|
var createExamDto = MapToCreateExamDto(_finalQuestionGroups);
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> ExamService <20><><EFBFBD><EFBFBD> DTO
|
||||||
|
// <20><><EFBFBD><EFBFBD> ExamService <20><>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> DTO
|
||||||
|
var response = await ExamService.SaveParsedExam(createExamDto);
|
||||||
|
|
||||||
|
if (!response.Status)
|
||||||
|
{
|
||||||
|
HandleProcessError(response.Message ?? "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><E2B5BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܡ<EFBFBD>", response.Result as Exception);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.Saving, response.Message ?? "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѳɹ<D1B3><C9B9><EFBFBD><EFBFBD>浽<EFBFBD><E6B5BD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
|
||||||
|
Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѳɹ<D1B3><C9B9><EFBFBD><EFBFBD>档", Severity.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݵ<EFBFBD><DDB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// --- ȫ<>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> ---
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD> AI <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (ȫ<>Զ<EFBFBD>)
|
||||||
|
public async Task TriggerFullAIParsingProcessAsync()
|
||||||
|
{
|
||||||
|
if (_isProcessing) return;
|
||||||
|
_isProcessing = true; // <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>״̬
|
||||||
|
// ȫ<><C8AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>н<EFBFBD><D0BD><EFBFBD>
|
||||||
|
_rawDividedExamXmlContent = null;
|
||||||
|
_dividedQuestionGroupList = null;
|
||||||
|
_rawParsedQuestionXmls.Clear();
|
||||||
|
_finalQuestionGroups.Clear();
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.Idle, "<22><>ʼȫ<CABC>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. <20><>ȡ<EFBFBD>༭<EFBFBD><E0BCAD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
await GetEditorTextContentAsync();
|
||||||
|
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||||||
|
|
||||||
|
// 2. <20><><EFBFBD><EFBFBD> AI <20>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD> (<28><>ȡԭʼ XML)
|
||||||
|
await DivideExamContentByAIAsync();
|
||||||
|
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||||||
|
|
||||||
|
// 3. ת<><D7AA><EFBFBD>ָ<EFBFBD> XML Ϊ StringsList
|
||||||
|
ConvertDividedXmlToQuestionList(); // ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||||||
|
|
||||||
|
// 4. ѭ<><D1AD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ÿ<EFBFBD><C3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (<28><>ȡԭʼ XML)
|
||||||
|
await ParseEachQuestionGroupAsync();
|
||||||
|
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||||||
|
|
||||||
|
// 5. ת<><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> XML Ϊ QuestionGroup <20><><EFBFBD><EFBFBD>
|
||||||
|
ConvertParsedXmlsToQuestionGroups(); // ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͬ<EFBFBD><CDAC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
|
if (_processingStatusMessage.Contains(ProcessingStage.ErrorOccurred.ToString())) return;
|
||||||
|
|
||||||
|
UpdateProcessingStatus(ProcessingStage.Completed, "ȫ<>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȫ<EFBFBD><C8AB><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandleProcessError($"ȫ<>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>з<EFBFBD><D0B7><EFBFBD>δԤ<CEB4>ڴ<EFBFBD><DAB4><EFBFBD>: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false; // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>״̬
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private void DeleteFromParse(int index)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < _finalQuestionGroups.Count)
|
||||||
|
{
|
||||||
|
_finalQuestionGroups.RemoveAt(index);
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#region JS
|
||||||
[Inject]
|
[Inject]
|
||||||
public IJSRuntime JSRuntime { get; set; }
|
public IJSRuntime JSRuntime { get; set; }
|
||||||
|
|
||||||
@@ -199,60 +555,7 @@ namespace TechHelper.Client.Pages.Editor
|
|||||||
Console.WriteLine($"<22><><EFBFBD>Ƶ<EFBFBD><C6B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>: {ex.Message}");
|
Console.WriteLine($"<22><><EFBFBD>Ƶ<EFBFBD><C6B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD>: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
private void ParseQuestions()
|
|
||||||
{
|
|
||||||
ParsedQuestions = new List<ParsedQuestion>();
|
|
||||||
_parseAttempted = true;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(QuillHTMLContent))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ʼ<EFBFBD>ͽ<EFBFBD><CDBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD>
|
|
||||||
string startTag = "[<5B><>Ŀ<EFBFBD><C4BF>ʼ]";
|
|
||||||
string endTag = "[<5B><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>]";
|
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD>ƥ<EFBFBD><C6A5><EFBFBD>ӿ<EFBFBD>ʼ<EFBFBD><CABC><EFBFBD>ǵ<EFBFBD><C7B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮<EFBFBD><D6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݣ<EFBFBD><DDA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>У<EFBFBD>
|
|
||||||
// (?s) <20><><EFBFBD>õ<EFBFBD><C3B5><EFBFBD>ģʽ<C4A3><CABD><EFBFBD><EFBFBD> . ƥ<>任<EFBFBD>з<EFBFBD>
|
|
||||||
string pattern = Regex.Escape(startTag) + "(.*?)" + Regex.Escape(endTag);
|
|
||||||
var matches = Regex.Matches(QuillHTMLContent, pattern, RegexOptions.Singleline);
|
|
||||||
|
|
||||||
int questionId = 1;
|
|
||||||
foreach (Match match in matches)
|
|
||||||
{
|
|
||||||
// <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮<EFBFBD><D6AE><EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
string rawQuestionHtml = match.Groups[1].Value;
|
|
||||||
|
|
||||||
// <20>Ƴ<EFBFBD><C6B3><EFBFBD><EFBFBD>ܲ<EFBFBD><DCB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>еı<D0B5><C4B1>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD>Ѿ<EFBFBD><D1BE>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
|
||||||
string cleanedQuestionContent = rawQuestionHtml
|
|
||||||
.Replace(startTag, "")
|
|
||||||
.Replace(endTag, "")
|
|
||||||
.Trim();
|
|
||||||
|
|
||||||
// <20><><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>⣨<EFBFBD><E2A3A8><EFBFBD>磬ƥ<E7A3AC>䡰һ<E4A1B0><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>1<EFBFBD><31><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD>У<EFBFBD>
|
|
||||||
string? questionTitle = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
|
|
||||||
var firstLineMatch = Regex.Match(cleanedQuestionContent, @"^(<p>)?\s*([һ<><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߰˾<DFB0>ʮ]+\s*[<5B><><EFBFBD><EFBFBD>]|\d+\s*[<5B><>\.]).*?</p>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
|
||||||
if (firstLineMatch.Success)
|
|
||||||
{
|
|
||||||
// <20><> HTML <20><><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ı<EFBFBD><C4B1><EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD>
|
|
||||||
questionTitle = Regex.Replace(firstLineMatch.Value, "<[^>]*>", "").Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex) { }
|
|
||||||
|
|
||||||
ParsedQuestions.Add(new ParsedQuestion
|
|
||||||
{
|
|
||||||
Id = questionId++,
|
|
||||||
Content = cleanedQuestionContent,
|
|
||||||
Title = questionTitle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,41 +1,45 @@
|
|||||||
@using TechHelper.Client.Exam
|
@using TechHelper.Client.Exam
|
||||||
|
|
||||||
<MudCard Class="@(IsNested ? "mb-3 pa-2" : "my-4")" Outlined="@IsNested">
|
<MudCard Class="@(IsNested ? "mb-3 pa-2" : "my-4")" Outlined="@IsNested">
|
||||||
@* 嵌套时添加边框和内边距 *@
|
@if (QuestionGroup.Title != string.Empty)
|
||||||
<MudCardHeader>
|
{
|
||||||
<MudStack>
|
<MudCardHeader>
|
||||||
|
<MudStack>
|
||||||
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||||
<MudText Typo="@(IsNested ? Typo.h6 : Typo.h5)">@QuestionGroup.Id. </MudText> @* 嵌套时字号稍小 *@
|
<MudText Typo="@(IsNested ? Typo.h6 : Typo.h5)">@QuestionGroup.Id. </MudText> @* 嵌套时字号稍小 *@
|
||||||
<MudText Typo="@(IsNested ? Typo.h6 : Typo.h5)">@QuestionGroup.Title</MudText>
|
<MudText Typo="@(IsNested ? Typo.h6 : Typo.h5)">@QuestionGroup.Title</MudText>
|
||||||
|
</MudStack>
|
||||||
|
@if (!string.IsNullOrEmpty(QuestionGroup.QuestionReference))
|
||||||
|
{
|
||||||
|
<MudText Class="mt-2" Style="white-space: pre-wrap;">@QuestionGroup.QuestionReference</MudText>
|
||||||
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@if (!string.IsNullOrEmpty(QuestionGroup.QuestionReference))
|
</MudCardHeader>
|
||||||
{
|
}
|
||||||
<MudText Class="mt-2" Style="white-space: pre-wrap;">@QuestionGroup.QuestionReference</MudText>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
</MudCardHeader>
|
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
@* 渲染直接子题目 *@
|
@* 渲染直接子题目 *@
|
||||||
@if (QuestionGroup.SubQuestions != null && QuestionGroup.SubQuestions.Any())
|
@if (QuestionGroup.SubQuestions != null && QuestionGroup.SubQuestions.Any())
|
||||||
{
|
{
|
||||||
@if (!IsNested) // 只有顶级大题才显示“子题目”标题
|
|
||||||
{
|
|
||||||
<MudText Typo="Typo.subtitle1" Class="mb-2">题目详情:</MudText>
|
|
||||||
}
|
|
||||||
@foreach (var qitem in QuestionGroup.SubQuestions)
|
@foreach (var qitem in QuestionGroup.SubQuestions)
|
||||||
{
|
{
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Baseline" Class="mb-2">
|
<MudStack Row="true" AlignItems="AlignItems.Baseline" Class="mb-2">
|
||||||
<MudText Typo="Typo.body1">@qitem.SubId. </MudText>
|
<MudText Typo="Typo.body1">@qitem.SubId. </MudText>
|
||||||
<MudText Typo="Typo.body1">@qitem.Stem</MudText>
|
<MudText Typo="Typo.body1">@qitem.Stem</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
|
@if (qitem.Options != null && qitem.Options.Any())
|
||||||
|
{
|
||||||
|
@foreach (var oitem in qitem.Options)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.body2" Class="ml-6 mb-2">@oitem.Value</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(qitem.SampleAnswer))
|
@if (!string.IsNullOrEmpty(qitem.SampleAnswer))
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.body2" Color="Color.Tertiary" Class="ml-6 mb-2">示例答案: @qitem.SampleAnswer</MudText>
|
<MudText Typo="Typo.body2" Color="Color.Tertiary" Class="ml-6 mb-2">示例答案: @qitem.SampleAnswer</MudText>
|
||||||
}
|
}
|
||||||
@if (qitem.Options != null && qitem.Options.Any())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,6 +13,7 @@ using TechHelper.Client.Services;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
using TechHelper.Client.AI;
|
using TechHelper.Client.AI;
|
||||||
|
using TechHelper.Client.Exam;
|
||||||
|
|
||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
@@ -30,6 +31,7 @@ builder.Services.AddAuthorizationCore();
|
|||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
builder.Services.AddLocalStorageServices();
|
builder.Services.AddLocalStorageServices();
|
||||||
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
|
builder.Services.AddScoped<IAuthenticationClientService, AuthenticationClientService>();
|
||||||
|
builder.Services.AddScoped<IExamService, ExamService>();
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
|
||||||
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
|
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetSection("ApiConfiguration"));
|
||||||
builder.Services.AddScoped<RefreshTokenService>();
|
builder.Services.AddScoped<RefreshTokenService>();
|
||||||
|
BIN
TechHelper.Client/wwwroot/ref/background.jpg
Normal file
BIN
TechHelper.Client/wwwroot/ref/background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
BIN
TechHelper.Client/wwwroot/ref/bg.jpg
Normal file
BIN
TechHelper.Client/wwwroot/ref/bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
TechHelper.Client/wwwroot/ref/bg2.jpg
Normal file
BIN
TechHelper.Client/wwwroot/ref/bg2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 231 KiB |
BIN
TechHelper.Client/wwwroot/ref/bg3.jpg
Normal file
BIN
TechHelper.Client/wwwroot/ref/bg3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
TechHelper.Client/wwwroot/ref/bg4.jpg
Normal file
BIN
TechHelper.Client/wwwroot/ref/bg4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
TechHelper.Client/wwwroot/ref/bg5.jpg
Normal file
BIN
TechHelper.Client/wwwroot/ref/bg5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 285 KiB |
@@ -19,6 +19,28 @@ namespace TechHelper.Context
|
|||||||
|
|
||||||
CreateMap<ClassDto, Class>()
|
CreateMap<ClassDto, Class>()
|
||||||
.ForMember(d => d.Number, o => o.MapFrom(src => src.Class)).ReverseMap();
|
.ForMember(d => d.Number, o => o.MapFrom(src => src.Class)).ReverseMap();
|
||||||
|
|
||||||
|
CreateMap<SubQuestionDto, Question>()
|
||||||
|
.ForMember(dest => dest.Id, opt => opt.Ignore())
|
||||||
|
.ForMember(dest => dest.QuestionText, opt => opt.MapFrom(src => src.Stem))
|
||||||
|
.ForMember(dest => dest.CorrectAnswer, opt => opt.MapFrom(src => src.SampleAnswer))
|
||||||
|
.ForMember(dest => dest.QuestionType, opt => opt.MapFrom(src => Enum.Parse<QuestionType>(src.QuestionType, true)))
|
||||||
|
.ForMember(dest => dest.DifficultyLevel, opt => opt.MapFrom(src => Enum.Parse<DifficultyLevel>(src.DifficultyLevel, true)))
|
||||||
|
.ForMember(dest => dest.SubjectArea, opt => opt.Ignore()) // SubjectArea 来自 Assignment 而不是 SubQuestionDto
|
||||||
|
.ForMember(dest => dest.CreatedBy, opt => opt.Ignore())
|
||||||
|
.ForMember(dest => dest.CreatedAt, opt => opt.Ignore())
|
||||||
|
.ForMember(dest => dest.UpdatedAt, opt => opt.Ignore())
|
||||||
|
.ForMember(dest => dest.IsDeleted, opt => opt.Ignore());
|
||||||
|
|
||||||
|
// 2. Question -> SubQuestionDto (查看时)
|
||||||
|
CreateMap<Question, SubQuestionDto>()
|
||||||
|
.ForMember(dest => dest.Stem, opt => opt.MapFrom(src => src.QuestionText))
|
||||||
|
.ForMember(dest => dest.Score, opt => opt.Ignore()) // Question 实体没有 Score 字段,需要从 AssignmentQuestion 获取
|
||||||
|
.ForMember(dest => dest.SampleAnswer, opt => opt.MapFrom(src => src.CorrectAnswer))
|
||||||
|
.ForMember(dest => dest.QuestionType, opt => opt.MapFrom(src => src.QuestionType.ToString()))
|
||||||
|
.ForMember(dest => dest.DifficultyLevel, opt => opt.MapFrom(src => src.DifficultyLevel.ToString()))
|
||||||
|
.ForMember(dest => dest.Options, opt => opt.Ignore()); // Options 需要单独处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -35,6 +35,9 @@ namespace TechHelper.Context.Configuration
|
|||||||
.HasColumnName("created_at")
|
.HasColumnName("created_at")
|
||||||
.IsRequired(); // 通常创建时间字段是非空的
|
.IsRequired(); // 通常创建时间字段是非空的
|
||||||
|
|
||||||
|
builder.Property(aq => aq.Score)
|
||||||
|
.HasColumnName("score");
|
||||||
|
|
||||||
// 配置 AssignmentGroupId 列
|
// 配置 AssignmentGroupId 列
|
||||||
// 该列在数据库中名为 "detail_id"
|
// 该列在数据库中名为 "detail_id"
|
||||||
builder.Property(aq => aq.AssignmentGroupId)
|
builder.Property(aq => aq.AssignmentGroupId)
|
||||||
|
@@ -12,7 +12,7 @@ using TechHelper.Context;
|
|||||||
namespace TechHelper.Server.Migrations
|
namespace TechHelper.Server.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(ApplicationContext))]
|
[DbContext(typeof(ApplicationContext))]
|
||||||
[Migration("20250520094348_init")]
|
[Migration("20250528090233_init")]
|
||||||
partial class init
|
partial class init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -53,14 +53,19 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasColumnType("tinyint(1)")
|
.HasColumnType("tinyint(1)")
|
||||||
.HasColumnName("deleted");
|
.HasColumnName("deleted");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectArea")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("subject_area");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
.HasColumnType("varchar(255)")
|
.HasColumnType("varchar(255)")
|
||||||
.HasColumnName("title");
|
.HasColumnName("title");
|
||||||
|
|
||||||
b.Property<decimal?>("TotalPoints")
|
b.Property<float?>("TotalPoints")
|
||||||
.HasColumnType("decimal(65,30)")
|
.HasColumnType("float")
|
||||||
.HasColumnName("total_points");
|
.HasColumnName("total_points");
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
@@ -221,10 +226,14 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasColumnType("char(36)")
|
.HasColumnType("char(36)")
|
||||||
.HasColumnName("question_id");
|
.HasColumnName("question_id");
|
||||||
|
|
||||||
b.Property<uint>("QuestionNumber")
|
b.Property<byte>("QuestionNumber")
|
||||||
.HasColumnType("int unsigned")
|
.HasColumnType("tinyint unsigned")
|
||||||
.HasColumnName("question_number");
|
.HasColumnName("question_number");
|
||||||
|
|
||||||
|
b.Property<float?>("Score")
|
||||||
|
.HasColumnType("float")
|
||||||
|
.HasColumnName("score");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("AssignmentGroupId");
|
b.HasIndex("AssignmentGroupId");
|
||||||
@@ -436,9 +445,9 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasColumnType("longtext")
|
.HasColumnType("longtext")
|
||||||
.HasColumnName("overall_feedback");
|
.HasColumnName("overall_feedback");
|
||||||
|
|
||||||
b.Property<decimal?>("OverallGrade")
|
b.Property<float?>("OverallGrade")
|
||||||
.HasPrecision(5, 2)
|
.HasPrecision(5, 2)
|
||||||
.HasColumnType("decimal(5,2)")
|
.HasColumnType("float")
|
||||||
.HasColumnName("overall_grade");
|
.HasColumnName("overall_grade");
|
||||||
|
|
||||||
b.Property<string>("Status")
|
b.Property<string>("Status")
|
||||||
@@ -494,9 +503,9 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasDefaultValue(false)
|
.HasDefaultValue(false)
|
||||||
.HasColumnName("deleted");
|
.HasColumnName("deleted");
|
||||||
|
|
||||||
b.Property<decimal?>("PointsAwarded")
|
b.Property<float?>("PointsAwarded")
|
||||||
.HasPrecision(5, 2)
|
.HasPrecision(5, 2)
|
||||||
.HasColumnType("decimal(5,2)")
|
.HasColumnType("float")
|
||||||
.HasColumnName("points_awarded");
|
.HasColumnName("points_awarded");
|
||||||
|
|
||||||
b.Property<string>("StudentAnswer")
|
b.Property<string>("StudentAnswer")
|
||||||
@@ -644,19 +653,19 @@ namespace TechHelper.Server.Migrations
|
|||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("ab2f54ba-5885-423b-b854-93bc63f8e93e"),
|
Id = new Guid("ea0c88d8-1a52-4034-bb37-5a95043821eb"),
|
||||||
Name = "Student",
|
Name = "Student",
|
||||||
NormalizedName = "STUDENT"
|
NormalizedName = "STUDENT"
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("d54985cb-1616-48fd-8687-00d9c38c900d"),
|
Id = new Guid("9de22e41-c096-4d5a-b55a-ce0122aa3ada"),
|
||||||
Name = "Teacher",
|
Name = "Teacher",
|
||||||
NormalizedName = "TEACHER"
|
NormalizedName = "TEACHER"
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("e6af92bf-1745-458f-b5c6-b51458261aaf"),
|
Id = new Guid("dee718d9-b731-485f-96bb-a59ce777870f"),
|
||||||
Name = "Administrator",
|
Name = "Administrator",
|
||||||
NormalizedName = "ADMINISTRATOR"
|
NormalizedName = "ADMINISTRATOR"
|
||||||
});
|
});
|
@@ -207,8 +207,10 @@ namespace TechHelper.Server.Migrations
|
|||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
description = table.Column<string>(type: "longtext", nullable: false)
|
description = table.Column<string>(type: "longtext", nullable: false)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
subject_area = table.Column<string>(type: "longtext", nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
due_date = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
due_date = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
total_points = table.Column<decimal>(type: "decimal(65,30)", nullable: true),
|
total_points = table.Column<float>(type: "float", nullable: true),
|
||||||
created_by = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
created_by = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
updated_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
updated_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
@@ -360,7 +362,7 @@ namespace TechHelper.Server.Migrations
|
|||||||
student_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
student_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
attempt_number = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
attempt_number = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
submission_time = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
submission_time = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
overall_grade = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: true),
|
overall_grade = table.Column<float>(type: "float", precision: 5, scale: 2, nullable: true),
|
||||||
overall_feedback = table.Column<string>(type: "longtext", nullable: false)
|
overall_feedback = table.Column<string>(type: "longtext", nullable: false)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
graded_by = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
graded_by = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||||
@@ -480,8 +482,9 @@ namespace TechHelper.Server.Migrations
|
|||||||
{
|
{
|
||||||
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
question_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
question_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
question_number = table.Column<uint>(type: "int unsigned", nullable: false),
|
question_number = table.Column<byte>(type: "tinyint unsigned", nullable: false),
|
||||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
score = table.Column<float>(type: "float", nullable: true),
|
||||||
detail_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
detail_id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
deleted = table.Column<bool>(type: "tinyint(1)", nullable: false, defaultValue: false)
|
||||||
},
|
},
|
||||||
@@ -514,7 +517,7 @@ namespace TechHelper.Server.Migrations
|
|||||||
student_answer = table.Column<string>(type: "longtext", nullable: false)
|
student_answer = table.Column<string>(type: "longtext", nullable: false)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
is_correct = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
is_correct = table.Column<bool>(type: "tinyint(1)", nullable: true),
|
||||||
points_awarded = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: true),
|
points_awarded = table.Column<float>(type: "float", precision: 5, scale: 2, nullable: true),
|
||||||
teacher_feedback = table.Column<string>(type: "longtext", nullable: false)
|
teacher_feedback = table.Column<string>(type: "longtext", nullable: false)
|
||||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
created_at = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
@@ -551,9 +554,9 @@ namespace TechHelper.Server.Migrations
|
|||||||
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
|
columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" },
|
||||||
values: new object[,]
|
values: new object[,]
|
||||||
{
|
{
|
||||||
{ new Guid("ab2f54ba-5885-423b-b854-93bc63f8e93e"), null, "Student", "STUDENT" },
|
{ new Guid("9de22e41-c096-4d5a-b55a-ce0122aa3ada"), null, "Teacher", "TEACHER" },
|
||||||
{ new Guid("d54985cb-1616-48fd-8687-00d9c38c900d"), null, "Teacher", "TEACHER" },
|
{ new Guid("dee718d9-b731-485f-96bb-a59ce777870f"), null, "Administrator", "ADMINISTRATOR" },
|
||||||
{ new Guid("e6af92bf-1745-458f-b5c6-b51458261aaf"), null, "Administrator", "ADMINISTRATOR" }
|
{ new Guid("ea0c88d8-1a52-4034-bb37-5a95043821eb"), null, "Student", "STUDENT" }
|
||||||
});
|
});
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
@@ -50,14 +50,19 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasColumnType("tinyint(1)")
|
.HasColumnType("tinyint(1)")
|
||||||
.HasColumnName("deleted");
|
.HasColumnName("deleted");
|
||||||
|
|
||||||
|
b.Property<string>("SubjectArea")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("longtext")
|
||||||
|
.HasColumnName("subject_area");
|
||||||
|
|
||||||
b.Property<string>("Title")
|
b.Property<string>("Title")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(255)
|
.HasMaxLength(255)
|
||||||
.HasColumnType("varchar(255)")
|
.HasColumnType("varchar(255)")
|
||||||
.HasColumnName("title");
|
.HasColumnName("title");
|
||||||
|
|
||||||
b.Property<decimal?>("TotalPoints")
|
b.Property<float?>("TotalPoints")
|
||||||
.HasColumnType("decimal(65,30)")
|
.HasColumnType("float")
|
||||||
.HasColumnName("total_points");
|
.HasColumnName("total_points");
|
||||||
|
|
||||||
b.Property<DateTime>("UpdatedAt")
|
b.Property<DateTime>("UpdatedAt")
|
||||||
@@ -218,10 +223,14 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasColumnType("char(36)")
|
.HasColumnType("char(36)")
|
||||||
.HasColumnName("question_id");
|
.HasColumnName("question_id");
|
||||||
|
|
||||||
b.Property<uint>("QuestionNumber")
|
b.Property<byte>("QuestionNumber")
|
||||||
.HasColumnType("int unsigned")
|
.HasColumnType("tinyint unsigned")
|
||||||
.HasColumnName("question_number");
|
.HasColumnName("question_number");
|
||||||
|
|
||||||
|
b.Property<float?>("Score")
|
||||||
|
.HasColumnType("float")
|
||||||
|
.HasColumnName("score");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("AssignmentGroupId");
|
b.HasIndex("AssignmentGroupId");
|
||||||
@@ -433,9 +442,9 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasColumnType("longtext")
|
.HasColumnType("longtext")
|
||||||
.HasColumnName("overall_feedback");
|
.HasColumnName("overall_feedback");
|
||||||
|
|
||||||
b.Property<decimal?>("OverallGrade")
|
b.Property<float?>("OverallGrade")
|
||||||
.HasPrecision(5, 2)
|
.HasPrecision(5, 2)
|
||||||
.HasColumnType("decimal(5,2)")
|
.HasColumnType("float")
|
||||||
.HasColumnName("overall_grade");
|
.HasColumnName("overall_grade");
|
||||||
|
|
||||||
b.Property<string>("Status")
|
b.Property<string>("Status")
|
||||||
@@ -491,9 +500,9 @@ namespace TechHelper.Server.Migrations
|
|||||||
.HasDefaultValue(false)
|
.HasDefaultValue(false)
|
||||||
.HasColumnName("deleted");
|
.HasColumnName("deleted");
|
||||||
|
|
||||||
b.Property<decimal?>("PointsAwarded")
|
b.Property<float?>("PointsAwarded")
|
||||||
.HasPrecision(5, 2)
|
.HasPrecision(5, 2)
|
||||||
.HasColumnType("decimal(5,2)")
|
.HasColumnType("float")
|
||||||
.HasColumnName("points_awarded");
|
.HasColumnName("points_awarded");
|
||||||
|
|
||||||
b.Property<string>("StudentAnswer")
|
b.Property<string>("StudentAnswer")
|
||||||
@@ -641,19 +650,19 @@ namespace TechHelper.Server.Migrations
|
|||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("ab2f54ba-5885-423b-b854-93bc63f8e93e"),
|
Id = new Guid("ea0c88d8-1a52-4034-bb37-5a95043821eb"),
|
||||||
Name = "Student",
|
Name = "Student",
|
||||||
NormalizedName = "STUDENT"
|
NormalizedName = "STUDENT"
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("d54985cb-1616-48fd-8687-00d9c38c900d"),
|
Id = new Guid("9de22e41-c096-4d5a-b55a-ce0122aa3ada"),
|
||||||
Name = "Teacher",
|
Name = "Teacher",
|
||||||
NormalizedName = "TEACHER"
|
NormalizedName = "TEACHER"
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = new Guid("e6af92bf-1745-458f-b5c6-b51458261aaf"),
|
Id = new Guid("dee718d9-b731-485f-96bb-a59ce777870f"),
|
||||||
Name = "Administrator",
|
Name = "Administrator",
|
||||||
NormalizedName = "ADMINISTRATOR"
|
NormalizedName = "ADMINISTRATOR"
|
||||||
});
|
});
|
||||||
|
@@ -2,22 +2,46 @@
|
|||||||
{
|
{
|
||||||
public class ApiResponse
|
public class ApiResponse
|
||||||
{
|
{
|
||||||
public ApiResponse(string message, bool status = false)
|
public string Message { get; set; }
|
||||||
|
public bool Status { get; set; }
|
||||||
|
public object? Result { get; set; }
|
||||||
|
|
||||||
|
private ApiResponse(bool status, string message, object? result)
|
||||||
{
|
{
|
||||||
this.Message = message;
|
Status = status;
|
||||||
this.Status = status;
|
Message = message;
|
||||||
|
Result = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ApiResponse(string message, bool status = false)
|
||||||
|
: this(status, message, null) { }
|
||||||
|
|
||||||
|
|
||||||
public ApiResponse(bool status, object result)
|
public ApiResponse(bool status, object result)
|
||||||
|
: this(status, string.Empty, result) { }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个表示成功响应的 ApiResponse 实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">成功消息。</param>
|
||||||
|
/// <param name="result">可选的返回数据。</param>
|
||||||
|
/// <returns>ApiResponse 实例。</returns>
|
||||||
|
public static ApiResponse Success(string message = "操作成功。", object? result = null)
|
||||||
{
|
{
|
||||||
this.Status = status;
|
return new ApiResponse(true, message, result);
|
||||||
this.Result = result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Message { get; set; }
|
/// <summary>
|
||||||
|
/// 创建一个表示失败响应的 ApiResponse 实例。
|
||||||
public bool Status { get; set; }
|
/// </summary>
|
||||||
|
/// <param name="message">错误消息。</param>
|
||||||
public object Result { get; set; }
|
/// <param name="result">可选的错误详情或数据。</param>
|
||||||
|
/// <returns>ApiResponse 实例。</returns>
|
||||||
|
public static ApiResponse Error(string message = "操作失败。", object? result = null)
|
||||||
|
{
|
||||||
|
return new ApiResponse(false, message, result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
306
TechHelper.Server/Services/ExamService.cs
Normal file
306
TechHelper.Server/Services/ExamService.cs
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Entities.Contracts;
|
||||||
|
using Entities.DTO;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SharedDATA.Api;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public class ExamService : IExamService
|
||||||
|
{
|
||||||
|
private readonly IUnitOfWork _unitOfWork;
|
||||||
|
private readonly IRepository<Assignment> _assignmentRepo;
|
||||||
|
private readonly IRepository<AssignmentGroup> _assignmentGroupRepo;
|
||||||
|
private readonly IRepository<AssignmentQuestion> _assignmentQuestionRepo;
|
||||||
|
private readonly IRepository<Question> _questionRepo;
|
||||||
|
public ExamService(IUnitOfWork unitOfWork, IMapper mapper, UserManager<User> userManager)
|
||||||
|
{
|
||||||
|
_unitOfWork = unitOfWork;
|
||||||
|
_mapper = mapper;
|
||||||
|
_userManager = userManager;
|
||||||
|
|
||||||
|
_assignmentRepo = _unitOfWork.GetRepository<Assignment>();
|
||||||
|
_assignmentGroupRepo = _unitOfWork.GetRepository<AssignmentGroup>();
|
||||||
|
_assignmentQuestionRepo = _unitOfWork.GetRepository<AssignmentQuestion>();
|
||||||
|
_questionRepo = _unitOfWork.GetRepository<Question>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
private readonly UserManager<User> _userManager;
|
||||||
|
|
||||||
|
public async Task<ApiResponse> AddAsync(ExamDto model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await SaveParsedExam(model);
|
||||||
|
if (result.Status)
|
||||||
|
{
|
||||||
|
return ApiResponse.Success("保存试题成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"保存试题数据失败{result.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"保存试题数据失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 GetExamByIdAsync(id);
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"获取试题数据失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ApiResponse> UpdateAsync(ExamDto model)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<ApiResponse> GetExamByIdAsync(Guid assignmentId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var assignment = await _unitOfWork.GetRepository<Assignment>().GetFirstOrDefaultAsync(
|
||||||
|
predicate: a => a.Id == assignmentId && !a.IsDeleted);
|
||||||
|
|
||||||
|
if (assignment == null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"找不到 ID 为 {assignmentId} 的试卷。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有相关题组和题目,并过滤掉已删除的
|
||||||
|
var allGroups = await _unitOfWork.GetRepository<AssignmentGroup>().GetAllAsync(
|
||||||
|
predicate: ag => ag.AssignmentId == assignmentId && !ag.IsDeleted,
|
||||||
|
include: source => source
|
||||||
|
.Include(ag => ag.AssignmentQuestions.Where(aq => !aq.IsDeleted))
|
||||||
|
.ThenInclude(aq => aq.Question)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allGroups == null || !allGroups.Any())
|
||||||
|
{
|
||||||
|
// 试卷存在但没有内容,返回一个空的 ExamDto
|
||||||
|
return ApiResponse.Success("试卷没有内容。", new ExamDto
|
||||||
|
{
|
||||||
|
AssignmentId = assignment.Id,
|
||||||
|
AssignmentTitle = assignment.Title,
|
||||||
|
Description = assignment.Description,
|
||||||
|
SubjectArea = assignment.Submissions.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootGroups = allGroups
|
||||||
|
.Where(ag => ag.ParentGroup == null)
|
||||||
|
.OrderBy(ag => ag.Number)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 递归映射到 ExamDto
|
||||||
|
var examDto = new ExamDto
|
||||||
|
{
|
||||||
|
AssignmentId = assignment.Id,
|
||||||
|
AssignmentTitle = assignment.Title,
|
||||||
|
Description = assignment.Description,
|
||||||
|
SubjectArea = assignment.Submissions.ToString(),
|
||||||
|
QuestionGroups = MapAssignmentGroupsToDto(rootGroups, allGroups)
|
||||||
|
};
|
||||||
|
|
||||||
|
return ApiResponse.Success("试卷信息已成功获取。", examDto);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"获取试卷时发生错误: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<QuestionGroupDto> MapAssignmentGroupsToDto(
|
||||||
|
List<AssignmentGroup> currentLevelGroups,
|
||||||
|
IEnumerable<AssignmentGroup> allFetchedGroups)
|
||||||
|
{
|
||||||
|
var dtos = new List<QuestionGroupDto>();
|
||||||
|
|
||||||
|
foreach (var group in currentLevelGroups.OrderBy(g => g.Number))
|
||||||
|
{
|
||||||
|
var groupDto = new QuestionGroupDto
|
||||||
|
{
|
||||||
|
|
||||||
|
Title = group.Title,
|
||||||
|
Score = (int)(group.TotalPoints ?? 0),
|
||||||
|
QuestionReference = group.Descript,
|
||||||
|
SubQuestions = group.AssignmentQuestions
|
||||||
|
.OrderBy(aq => aq.QuestionNumber)
|
||||||
|
.Select(aq => new SubQuestionDto
|
||||||
|
{
|
||||||
|
Index = aq.QuestionNumber,
|
||||||
|
Stem = aq.Question.QuestionText,
|
||||||
|
Score = aq.Score?? 0, // 使用 AssignmentQuestion 上的 Score
|
||||||
|
SampleAnswer = aq.Question.CorrectAnswer,
|
||||||
|
QuestionType = aq.Question.QuestionType.ToString(),
|
||||||
|
DifficultyLevel = aq.Question.DifficultyLevel.ToString(),
|
||||||
|
Options = new List<OptionDto>() // 这里需要您根据实际存储方式填充 Option
|
||||||
|
}).ToList(),
|
||||||
|
// 递归映射子题组
|
||||||
|
SubQuestionGroups = MapAssignmentGroupsToDto(
|
||||||
|
allFetchedGroups.Where(ag => ag.ParentGroup == group.Id && !ag.IsDeleted).ToList(), // 从所有已获取的组中筛选子组
|
||||||
|
allFetchedGroups)
|
||||||
|
};
|
||||||
|
dtos.Add(groupDto);
|
||||||
|
}
|
||||||
|
return dtos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<TechHelper.Services.ApiResponse> SaveParsedExam(ExamDto examData)
|
||||||
|
{
|
||||||
|
// 获取当前登录用户
|
||||||
|
var currentUser = await _userManager.GetUserAsync(null);
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error("未找到当前登录用户,无法保存试题。");
|
||||||
|
}
|
||||||
|
var currentUserId = currentUser.Id;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Guid assignmentId;
|
||||||
|
|
||||||
|
// 创建新的 Assignment 实体
|
||||||
|
var newAssignment = new Assignment
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Title = examData.AssignmentTitle,
|
||||||
|
Description = examData.Description,
|
||||||
|
SubjectArea = examData.SubjectArea,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CreatedBy = currentUserId,
|
||||||
|
IsDeleted = false
|
||||||
|
};
|
||||||
|
await _assignmentRepo.InsertAsync(newAssignment);
|
||||||
|
assignmentId = newAssignment.Id;
|
||||||
|
|
||||||
|
|
||||||
|
// 从 ExamDto.QuestionGroups 获取根题组。
|
||||||
|
// 确保只有一个根题组,因为您的模型是“试卷只有一个根节点”。
|
||||||
|
if (examData.QuestionGroups == null || examData.QuestionGroups.Count != 1)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("试卷必须包含且只能包含一个根题组。");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归处理根题组及其所有子题组和题目
|
||||||
|
// 传入的 assignmentId 仅用于设置根题组的 AssignmentId 字段
|
||||||
|
// 对于子题组,ProcessAndSaveAssignmentGroupsRecursive 会将 AssignmentId 设置为 null
|
||||||
|
await ProcessAndSaveAssignmentGroupsRecursive(
|
||||||
|
examData.QuestionGroups.Single(),
|
||||||
|
examData.SubjectArea.ToString(),
|
||||||
|
assignmentId,
|
||||||
|
null, // 根题组没有父级
|
||||||
|
currentUserId);
|
||||||
|
|
||||||
|
if (await _unitOfWork.SaveChangesAsync() > 0)
|
||||||
|
{
|
||||||
|
return ApiResponse.Success("试卷数据已成功保存。", new ExamDto { AssignmentId = assignmentId, AssignmentTitle = examData.AssignmentTitle });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return ApiResponse.Success("没有新的试卷数据需要保存。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ApiResponse.Error($"保存试卷数据失败: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessAndSaveAssignmentGroupsRecursive(
|
||||||
|
QuestionGroupDto qgDto,
|
||||||
|
string subjectarea,
|
||||||
|
Guid assignmentId,
|
||||||
|
Guid? parentAssignmentGroupId,
|
||||||
|
Guid createdById)
|
||||||
|
{
|
||||||
|
byte groupNumber = 1;
|
||||||
|
var newAssignmentGroup = new AssignmentGroup
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(), // 后端生成 GUID
|
||||||
|
Title = qgDto.Title,
|
||||||
|
Descript = qgDto.QuestionReference,
|
||||||
|
TotalPoints = qgDto.Score,
|
||||||
|
Number = (byte)qgDto.Index, // 使用 DTO 的 Index 作为 Number
|
||||||
|
ParentGroup = parentAssignmentGroupId, // 设置父级题组 GUID
|
||||||
|
|
||||||
|
// 关键修正:只有当 parentAssignmentGroupId 为 null 时,才设置 AssignmentId
|
||||||
|
// 这意味着当前题组是顶级题组
|
||||||
|
AssignmentId = parentAssignmentGroupId == null ? assignmentId : Guid.Empty,
|
||||||
|
IsDeleted = false
|
||||||
|
};
|
||||||
|
await _unitOfWork.GetRepository<AssignmentGroup>().InsertAsync(newAssignmentGroup);
|
||||||
|
|
||||||
|
// 处理子题目
|
||||||
|
uint questionNumber = 1;
|
||||||
|
foreach (var sqDto in qgDto.SubQuestions.OrderBy(s => s.Index))
|
||||||
|
{
|
||||||
|
var newQuestion = _mapper.Map<Question>(sqDto);
|
||||||
|
newQuestion.Id = Guid.NewGuid();
|
||||||
|
newQuestion.CreatedBy = createdById;
|
||||||
|
newQuestion.CreatedAt = DateTime.UtcNow;
|
||||||
|
newQuestion.UpdatedAt = DateTime.UtcNow;
|
||||||
|
newQuestion.IsDeleted = false;
|
||||||
|
newQuestion.SubjectArea = (SubjectAreaEnum)Enum.Parse(typeof(SubjectAreaEnum), subjectarea, true);
|
||||||
|
|
||||||
|
// 处理 Options:如果 Options 是 JSON 字符串或需要其他存储方式,在这里处理
|
||||||
|
// 例如:newQuestion.QuestionText += (JsonConvert.SerializeObject(sqDto.Options));
|
||||||
|
|
||||||
|
await _unitOfWork.GetRepository<Question>().InsertAsync(newQuestion);
|
||||||
|
|
||||||
|
var newAssignmentQuestion = new AssignmentQuestion
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
QuestionId = newQuestion.Id,
|
||||||
|
QuestionNumber = (byte)questionNumber, // 使用递增的 questionNumber
|
||||||
|
AssignmentGroupId = newAssignmentGroup.Id, // 关联到当前题组
|
||||||
|
Score = sqDto.Score, // 从 DTO 获取单个子题分数
|
||||||
|
IsDeleted = false,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
await _unitOfWork.GetRepository<AssignmentQuestion>().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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,10 +2,10 @@
|
|||||||
{
|
{
|
||||||
public interface IBaseService<T, TId>
|
public interface IBaseService<T, TId>
|
||||||
{
|
{
|
||||||
Task<ApiResponse> GetAllAsync(QueryParameter query);
|
Task<TechHelper.Services.ApiResponse> GetAllAsync(QueryParameter query);
|
||||||
Task<ApiResponse> GetAsync(TId id);
|
Task<TechHelper.Services.ApiResponse> GetAsync(TId id);
|
||||||
Task<ApiResponse> AddAsync(T model);
|
Task<TechHelper.Services.ApiResponse> AddAsync(T model);
|
||||||
Task<ApiResponse> UpdateAsync(T model);
|
Task<TechHelper.Services.ApiResponse> UpdateAsync(T model);
|
||||||
Task<ApiResponse> DeleteAsync(TId id);
|
Task<TechHelper.Services.ApiResponse> DeleteAsync(TId id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
TechHelper.Server/Services/IExamService.cs
Normal file
10
TechHelper.Server/Services/IExamService.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Entities.DTO;
|
||||||
|
using TechHelper.Services;
|
||||||
|
|
||||||
|
namespace TechHelper.Server.Services
|
||||||
|
{
|
||||||
|
public interface IExamService : IBaseService<ExamDto, Guid>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user