struct&&assiQues

This commit is contained in:
SpecialX
2025-06-20 15:37:39 +08:00
parent f37262d72e
commit d20c051c51
68 changed files with 1927 additions and 2869 deletions

View File

@@ -1,67 +1,173 @@
using Entities.DTO;
using System.Text.Json.Serialization;
using System.Text.Json;
using Entities.Contracts;
using Microsoft.Extensions.Options;
namespace TechHelper.Client.Exam
{
public class ParentStructInfo
{
public string Number { get; set; }
public SubjectAreaEnum SubjectArea { get; set; }
public byte Index { get; set; }
}
public static class ExamPaperExtensions
{
public static ExamDto ConvertToExamDTO(this ExamPaper examPaper)
public static AssignmentDto ConvertToExamDTO(this ExamPaper examPaper)
{
ExamDto dto = new ExamDto();
AssignmentDto dto = new AssignmentDto();
dto.AssignmentTitle = examPaper.AssignmentTitle;
dto.Title = examPaper.AssignmentTitle;
dto.Description = examPaper.Description;
dto.SubjectArea = examPaper.SubjectArea;
dto.QuestionGroups.Title = examPaper.AssignmentTitle;
dto.QuestionGroups.Descript = examPaper.Description;
var SubjectArea = SubjectAreaEnum.Literature;
Enum.TryParse<SubjectAreaEnum>(examPaper.SubjectArea, out SubjectArea);
dto.SubjectArea = SubjectArea;
AssignmentStructDto examStruct = new AssignmentStructDto();
foreach (var qg in examPaper.QuestionGroups)
{
var qgd = new QuestionGroupDto();
ParseMajorQuestionGroup(qg, qgd, false);
dto.QuestionGroups.SubQuestionGroups.Add(qgd);
examStruct.ChildrenGroups.Add(ParseMajorQuestionGroup(qg));
examStruct.ChildrenGroups.Last().Index = (byte)(examStruct.ChildrenGroups.Count());
}
foreach (var question in examPaper.TopLevelQuestions)
{
if (question.SubQuestions != null && question.SubQuestions.Any())
{
var qgDto = new QuestionGroupDto
{
Title = question.Stem,
Score = (int)question.Score,
Descript = "",
};
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
ParseQuestionWithSubQuestions(question, qgDto, qgDto.ValidQuestionGroup);
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
}
else
{
var qgDto = new QuestionGroupDto
{
Title = question.Stem,
Score = (int)question.Score,
Descript = "",
};
qgDto.ValidQuestionGroup = !string.IsNullOrEmpty(qgDto.Descript);
var subQuestionDto = new SubQuestionDto();
ParseSingleQuestion(question, subQuestionDto, !qgDto.ValidQuestionGroup);
qgDto.SubQuestions.Add(subQuestionDto);
dto.QuestionGroups.SubQuestionGroups.Add(qgDto);
}
}
dto.ExamStruct = examStruct;
return dto;
}
private static AssignmentStructDto ParseMajorQuestionGroup(MajorQuestionGroup sqg)
{
var examStruct = new AssignmentStructDto();
if (sqg.SubQuestionGroups != null)
{
examStruct.Title = sqg.Title;
examStruct.Score = sqg.Score;
examStruct.ChildrenGroups = new List<AssignmentStructDto>();
sqg.SubQuestionGroups?.ForEach(ssqg =>
{
if (string.IsNullOrEmpty(ssqg.Descript))
{
examStruct.ChildrenGroups.Add(ParseMajorQuestionGroup(ssqg));
examStruct.ChildrenGroups.Last().Index = (byte)(examStruct.ChildrenGroups.Count());
}
else
{
examStruct.AssignmentQuestions.Add(ParseGroupToAssignmentQuestion(ssqg, false));
examStruct.AssignmentQuestions.Last().Index = (byte)(examStruct.AssignmentQuestions.Count());
}
});
}
if (sqg.SubQuestions != null)
{
sqg.SubQuestions?.ForEach(sq =>
{
if(sq.SubQuestions.Any())
{
}
examStruct.AssignmentQuestions.Add(ParseAssignmentQuestion(sq));
examStruct.AssignmentQuestions.Last().Index = (byte)(examStruct.AssignmentQuestions.Count());
});
}
return examStruct;
}
public static List<string> ParseOptionsFromText(this string optionsText)
{
return optionsText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
.Where(line => !string.IsNullOrWhiteSpace(line)).ToList();
}
private static QuestionDto ParseGroupToQuestion(MajorQuestionGroup qg, bool subQ = true)
{
var dq = new QuestionDto();
dq.Title = qg.Title + Environment.NewLine + qg.Descript;
if (subQ) dq.GroupState = QuestionGroupState.Subquestion;
else dq.GroupState = QuestionGroupState.Group;
qg.SubQuestions?.ForEach(ssq =>
{
dq.ChildrenQuestion.Add(ParseQuestion(ssq));
});
qg.SubQuestionGroups?.ForEach(sqg =>
{
dq.ChildrenQuestion.Add(ParseGroupToQuestion(sqg));
});
return dq;
}
private static AssignmentQuestionDto ParseGroupToAssignmentQuestion(MajorQuestionGroup qg, bool subQ = true)
{
var aq = new AssignmentQuestionDto();
aq.Score = qg.Score;
qg.SubQuestions?.ForEach(ssq =>
{
aq.Question.ChildrenQuestion.Add(ParseQuestion(ssq));
aq.Question.ChildrenQuestion.Last().Index = (byte)aq.Question.ChildrenQuestion.Count;
});
qg.SubQuestionGroups?.ForEach(sqg =>
{
aq.Question.ChildrenQuestion.Add(ParseGroupToQuestion(sqg));
aq.Question.ChildrenQuestion.Last().Index = (byte)aq.Question.ChildrenQuestion.Count;
});
return aq;
}
private static AssignmentQuestionDto ParseAssignmentQuestion(PaperQuestion sq)
{
var aq = new AssignmentQuestionDto();
aq.Score = sq.Score;
aq.Question = ParseQuestion(sq);
sq.SubQuestions?.ForEach(ssq =>
{
aq.Question.ChildrenQuestion.Add(ParseQuestion(ssq));
aq.Question.ChildrenQuestion.Last().Index = (byte)aq.Question.ChildrenQuestion.Count;
});
return aq;
}
private static QuestionDto ParseQuestion(PaperQuestion sq)
{
var dq = new QuestionDto();
dq.Title = sq.Stem;
dq.Options = string.Join(Environment.NewLine, sq.Options.Select(opt => $"{opt.Label} {opt.Text}"));
dq.Score = sq.Score;
sq.SubQuestions?.ForEach(ssq =>
{
dq.ChildrenQuestion.Add(ParseQuestion(ssq));
dq.ChildrenQuestion.Last().Index = (byte)dq.ChildrenQuestion.Count;
});
return dq;
}
private static void ParseMajorQuestionGroup(MajorQuestionGroup qg, QuestionGroupDto qgd, bool isParentGroupValidChain)
{
qgd.Title = qg.Title;
@@ -86,12 +192,10 @@ namespace TechHelper.Client.Exam
});
}
// 处理 MajorQuestionGroup 下的 SubQuestions
if (qg.SubQuestions != null)
{
qg.SubQuestions.ForEach(sq =>
{
// 如果 MajorQuestionGroup 下的 Question 包含子问题,则转为 QuestionGroupDto
if (sq.SubQuestions != null && sq.SubQuestions.Any())
{
var subQgd = new QuestionGroupDto
@@ -101,7 +205,6 @@ namespace TechHelper.Client.Exam
Score = (int)sq.Score,
Descript = "" // 默认为空
};
// 判断当前组是否有效:如果有描述,并且其父级链中没有任何一个组是有效组,则当前组有效
subQgd.ValidQuestionGroup = !string.IsNullOrEmpty(subQgd.Descript) && !nextIsParentGroupValidChain;
ParseQuestionWithSubQuestions(sq, subQgd, subQgd.ValidQuestionGroup || nextIsParentGroupValidChain);
@@ -121,7 +224,7 @@ namespace TechHelper.Client.Exam
// 解析包含子问题的 Question将其转换为 QuestionGroupDto
// isParentGroupValidChain 参数表示从顶层到当前组的任一父组是否已经是“有效组”
private static void ParseQuestionWithSubQuestions(Question question, QuestionGroupDto qgd, bool isParentGroupValidChain)
private static void ParseQuestionWithSubQuestions(PaperQuestion question, QuestionGroupDto qgd, bool isParentGroupValidChain)
{
qgd.Title = question.Stem;
qgd.Score = (int)question.Score;
@@ -165,7 +268,7 @@ namespace TechHelper.Client.Exam
}
// 解析单个 Question (没有子问题) 为 SubQuestionDto
private static void ParseSingleQuestion(Question question, SubQuestionDto subQd, bool validQ)
private static void ParseSingleQuestion(PaperQuestion question, SubQuestionDto subQd, bool validQ)
{
subQd.Stem = question.Stem;
subQd.Score = (int)question.Score;
@@ -187,7 +290,7 @@ namespace TechHelper.Client.Exam
public static void SeqIndex(this ExamDto dto)
{
dto.QuestionGroups.SeqQGroupIndex();
dto.ExamStruct.SeqQGroupIndex();
}

View File

@@ -51,7 +51,7 @@ namespace TechHelper.Client.Exam
public string Description { get; set; } = "未识别试卷描述";
public string SubjectArea { get; set; } = "试卷类别";
public List<MajorQuestionGroup> QuestionGroups { get; set; } = new List<MajorQuestionGroup>();
public List<Question> TopLevelQuestions { get; set; } = new List<Question>();
public List<PaperQuestion> TopLevelQuestions { get; set; } = new List<PaperQuestion>();
public List<ParseError> Errors { get; set; } = new List<ParseError>();
}
@@ -61,17 +61,18 @@ namespace TechHelper.Client.Exam
public string Descript { get; set; } = string.Empty;
public float Score { get; set; }
public List<MajorQuestionGroup> SubQuestionGroups { get; set; } = new List<MajorQuestionGroup>();
public List<Question> SubQuestions { get; set; } = new List<Question>();
public List<PaperQuestion> SubQuestions { get; set; } = new List<PaperQuestion>();
public int Priority { get; set; }
public bool bGroup { get; set; } = true;
}
public class Question
public class PaperQuestion
{
public string Number { get; set; } = string.Empty;
public string Stem { 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 List<PaperQuestion> SubQuestions { get; set; } = new List<PaperQuestion>();
public string SampleAnswer { get; set; } = string.Empty;
public string QuestionType { get; set; } = string.Empty;
public int Priority { get; set; }
@@ -120,17 +121,17 @@ namespace TechHelper.Client.Exam
public ExamParserConfig()
{
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^([一二三四五六七八九十]+)[、\.]\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1));
MajorQuestionGroupPatterns.Add(new RegexPatternConfig(@"^\(([一二三四五六七八九十]{1,2}|十[一二三四五六七八九])\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2));
QuestionPatterns.Add(new RegexPatternConfig(@"^\(([一二三四五六七八九十]{1,2}|十[一二三四五六七八九])\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1));
// 模式 1: "1. 这是一个题目 (5分)" 或 "1. 这是一个题目"
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 1));
QuestionPatterns.Add(new RegexPatternConfig(@"^(\d+)\.\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2));
// 模式 2: "(1) 这是一个子题目 (3分)" 或 "(1) 这是一个子题目"
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 2));
QuestionPatterns.Add(new RegexPatternConfig(@"^\((\d+)\)\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 3));
// 模式 3: "① 这是一个更深层次的子题目 (2分)" 或 "① 这是一个更深层次的子题目"
QuestionPatterns.Add(new RegexPatternConfig(@"^[①②③④⑤⑥⑦⑧⑨⑩]+\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 3));
QuestionPatterns.Add(new RegexPatternConfig(@"^[①②③④⑤⑥⑦⑧⑨⑩]+\s*(.+?)(?:\s*\(((\d+(?:\.\d+)?))\s*分\))?\s*$", 4));
OptionPatterns.Add(new RegexPatternConfig(@"([A-Z]\.)\s*(.*?)(?=[A-Z]\.|$)", 1)); // 大写字母选项
@@ -251,6 +252,23 @@ namespace TechHelper.Client.Exam
_config = config ?? throw new ArgumentNullException(nameof(config), "ExamParserConfig cannot be null.");
}
///
/// 一.基础
/// 1.听写
/// 2.阅读
/// 二.提升
/// 1.阅读
/// (1).选择
/// (2).填空
/// 三.写
/// (一)课文
///
///
///
/// <summary>
/// Builds the ExamPaper structure from raw text and potential matches.
/// Collects and returns parsing errors encountered during the process.
@@ -260,7 +278,7 @@ namespace TechHelper.Client.Exam
/// <returns>An ExamPaper object containing the parsed structure and a list of errors.</returns>
/// <exception cref="ArgumentException">Thrown if fullExamText is null or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown if allPotentialMatches is null.</exception>
public ExamPaper BuildExamPaper(string fullExamText, List<PotentialMatch> allPotentialMatches)
public ExamPaper BuildExam(string fullExamText, List<PotentialMatch> allPotentialMatches)
{
// 核心输入验证仍然是必要的,因为这些错误是无法恢复的
if (string.IsNullOrWhiteSpace(fullExamText))
@@ -289,8 +307,8 @@ namespace TechHelper.Client.Exam
var majorQGStack = new Stack<MajorQuestionGroup>();
MajorQuestionGroup currentMajorQG = null;
var questionStack = new Stack<Question>();
Question currentQuestion = null;
var questionStack = new Stack<PaperQuestion>();
PaperQuestion currentQuestion = null;
int currentContentStart = 0;
@@ -388,6 +406,7 @@ namespace TechHelper.Client.Exam
Title = pm.RegexMatch.Groups[2].Value.Trim(), // 标题是 Group 2
Score = score,
Priority = pm.PatternConfig.Priority,
bGroup = true
};
if (majorQGStack.Any())
@@ -446,7 +465,7 @@ namespace TechHelper.Client.Exam
}
}
Question newQuestion = new Question
PaperQuestion newQuestion = new PaperQuestion
{
Number = pm.RegexMatch.Groups[1].Value.Trim(),
Stem = pm.RegexMatch.Groups[2].Value.Trim(),
@@ -618,7 +637,7 @@ namespace TechHelper.Client.Exam
/// Processes the content of a Question, mainly for parsing Options and identifying unstructured text.
/// Logs errors to the provided error list instead of throwing.
/// </summary>
private void ProcessQuestionContent(Question question, string contentText, List<PotentialMatch> potentialMatchesInScope, List<ParseError> errors)
private void ProcessQuestionContent(PaperQuestion question, string contentText, List<PotentialMatch> potentialMatchesInScope, List<ParseError> errors)
{
// 参数验证,这些是内部方法的契约,如果违反则直接抛出,因为这意味着调用者有错
if (question == null) throw new ArgumentNullException(nameof(question), "Question cannot be null in ProcessQuestionContent.");
@@ -674,8 +693,10 @@ namespace TechHelper.Client.Exam
question.Options.Add(newOption);
lastOptionEndIndex = pm.EndIndex;
}
// TODO: If there are SubQuestion types, they can be processed similarly here.
// 你可以在此处添加对子问题的处理逻辑,同样需要小心处理其内容和嵌套。
else
{
question.Stem += contentText;
}
}
catch (Exception innerEx)
{
@@ -734,7 +755,7 @@ namespace TechHelper.Client.Exam
// 2. 构建:根据扫描结果和原始文本,线性遍历并构建层级结构
// BuildExamPaper 现在会返回一个包含错误列表的 ExamPaper 对象
// 外部不再需要捕获内部解析异常,只需检查 ExamPaper.Errors 列表
return _builder.BuildExamPaper(examPaperText, allPotentialMatches);
return _builder.BuildExam(examPaperText, allPotentialMatches);
}
}
}

View File

@@ -1,163 +0,0 @@
using System.Xml.Serialization; // 用于 XML 序列化/反序列化
using TechHelper.Client.AI;
using TechHelper.Services;
using Entities.DTO;
using System.Net.Http.Json; // 用于 PostAsJsonAsync
using Newtonsoft.Json; // 用于 JSON 反序列化
namespace TechHelper.Client.Exam
{
public class ExamService : IExamService
{
private readonly IAIService _aIService; // 遵循命名规范,字段前加下划线
private readonly HttpClient _client; // 直接注入 HttpClient
public ExamService(IAIService aIService, HttpClient client) // 修正点:直接注入 HttpClient
{
_aIService = aIService;
_client = client; // 赋值注入的 HttpClient 实例
}
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);
return ApiResponse.Success(result: deserializedObject, message: "XML 反序列化成功。");
}
catch (InvalidOperationException ex)
{
return ApiResponse.Error(
message: $"XML 反序列化操作错误: {ex.Message}. 内部异常: {ex.InnerException?.Message ?? ""}");
}
catch (Exception ex)
{
return ApiResponse.Error(message: $"处理 XML 反序列化时发生未知错误: {ex.Message}");
}
}
}
public async Task<ApiResponse> DividExam(string examContent)
{
try
{
string? response = await _aIService.CallGLM(examContent, AIConfiguration.BreakQuestions);
if (response != null)
{
return ApiResponse.Success(result: response, message: "试题分割成功。");
}
else
{
return ApiResponse.Error(message: "AI 服务未能返回有效内容,或返回内容为空。");
}
}
catch (Exception ex)
{
// 实际应用中,这里应该加入日志记录
return ApiResponse.Error(message: $"处理试题分割时发生内部错误: {ex.Message}");
}
}
public async Task<ApiResponse> FormatExam(string examContent)
{
try
{
string? response = await _aIService.CallGLM(examContent, AIConfiguration.Format);
if (response != null)
{
return ApiResponse.Success(result: response, message: "试题格式化成功。");
}
else
{
return ApiResponse.Error(message: "AI 服务未能返回有效内容,或返回内容为空。");
}
}
catch (Exception ex)
{
// 实际应用中,这里应该加入日志记录
return ApiResponse.Error(message: $"处理试题格式化时发生内部错误: {ex.Message}");
}
}
public async Task<ApiResponse> GetAllExam(string user)
{
// 直接使用注入的 _client 实例
var response = await _client.GetAsync($"exam/getAllPreview?user={user}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<List<ExamDto>>(content);
return ApiResponse.Success(result: result);
}
else
{
// 读取错误信息,并返回 ApiResponse
var errorContent = await response.Content.ReadAsStringAsync();
return ApiResponse.Error(message: $"获取所有试题失败: {response.StatusCode} - {errorContent}");
}
}
public async Task<ApiResponse> GetExam(Guid guid)
{
var response = await _client.GetAsync($"exam/get?id={guid}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
var exam = JsonConvert.DeserializeObject<ExamDto>(content);
return ApiResponse.Success(result: exam);
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
return ApiResponse.Error(message: $"获取试题失败: {response.StatusCode} - {errorContent}");
}
}
public async Task<ApiResponse> ParseSingleQuestionGroup(string examContent)
{
try
{
string? response = await _aIService.CallGLM(examContent, AIConfiguration.ParseSignelQuestion2);
if (response != null)
{
return ApiResponse.Success(result: response, message: "试题解析成功。");
}
else
{
return ApiResponse.Error(message: "AI 服务未能返回有效内容,或返回内容为空。");
}
}
catch (Exception ex)
{
// 实际应用中,这里应该加入日志记录
return ApiResponse.Error(message: $"处理试题解析时发生内部错误: {ex.Message}");
}
}
public async Task<ApiResponse> SaveParsedExam(ExamDto examDto)
{
// 直接使用注入的 _client 实例
var response = await _client.PostAsJsonAsync("exam/add", examDto);
if (response.IsSuccessStatusCode) // 检查是否是成功的状态码,例如 200 OK, 201 Created 等
{
return ApiResponse.Success(message: "试题保存成功。");
}
else
{
var errorContent = await response.Content.ReadAsStringAsync();
return ApiResponse.Error(message: $"保存试题失败: {response.StatusCode} - {errorContent}");
}
}
}
}

View File

@@ -52,7 +52,7 @@ namespace TechHelper.Client.Exam
Title = dto.AssignmentTitle
};
GetSeqRecursive(dto.QuestionGroups, null, examStruct.Questions);
GetSeqRecursive(dto.ExamStruct, null, examStruct.Questions);
return examStruct;
}

View File

@@ -1,17 +0,0 @@
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);
public Task<ApiResponse> GetAllExam(string user);
public Task<ApiResponse> GetExam(Guid guid);
}
}