Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ModerBox.Cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ dotnet run -- cd --source "C:\data" --target "C:\result.csv"
# 题库转换
dotnet run -- qb --source "input.txt" --target "output.xlsx" --source-format Txt --target-format Mtb

# 题库合并(多个源文件自动检测格式并去重,支持 .xlsx/.xls 输入)
# 题库合并(多个源文件自动检测格式并去重,支持风控平台等 .xlsx/.xls 输入)
dotnet run -- qb merge --sources "bank1.txt" "bank2.xlsx" "bank3.xls" --target "merged.xlsx" --target-format Mtb

# 电缆走向绘制
Expand Down
4 changes: 2 additions & 2 deletions ModerBox.MCP/Tools/QuestionBankTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static QuestionBankConversionResult ConvertQuestionBank(
[Description("Source question bank file path")] string sourcePath,
[Description("Target output file path")] string targetPath,
[Description("Target format (Ksb, Mtb, Wldx, Wldx4, Xiaobao, XiaobaoTxt)")] QuestionBankTargetFormat targetFormat,
[Description("Source format (AutoDetect, Txt, Wldx, Wldx4, Exc, Gdpx, Simple). If not specified, format will be auto-detected.")] QuestionBankSourceFormat? sourceFormat = null)
[Description("Source format (AutoDetect, Txt, Ksb, Mtb, Wldx, Wldx4, Exc, Gdpx, RiskControlPlatform, Simple). If not specified, format will be auto-detected.")] QuestionBankSourceFormat? sourceFormat = null)
{
var result = new QuestionBankConversionResult
{
Expand Down Expand Up @@ -89,7 +89,7 @@ public static QuestionBankMergeResult MergeQuestionBanks(
[Description("Source question bank file paths to merge")] string[] sourcePaths,
[Description("Target output file path")] string targetPath,
[Description("Target format (Ksb, Mtb, Wldx, Wldx4, Xiaobao, XiaobaoTxt)")] QuestionBankTargetFormat targetFormat,
[Description("Source format (AutoDetect, Txt, Ksb, Mtb, Wldx, Wldx4, Exc, Gdpx, Simple). If not specified, each source file will be auto-detected.")] QuestionBankSourceFormat? sourceFormat = null,
[Description("Source format (AutoDetect, Txt, Ksb, Mtb, Wldx, Wldx4, Exc, Gdpx, RiskControlPlatform, Simple). If not specified, each source file will be auto-detected.")] QuestionBankSourceFormat? sourceFormat = null,
[Description("Remove duplicate questions while merging. Defaults to true.")] bool deduplicate = true)
{
var result = new QuestionBankMergeResult
Expand Down
154 changes: 154 additions & 0 deletions ModerBox.QuestionBank.Test/QuestionBankServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Text;
using ClosedXML.Excel;
using ModerBox.QuestionBank;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
Expand All @@ -22,6 +23,7 @@ public void FormatOptionsProvider_ReturnsDisplayNamesAndDescriptionsFromAttribut
Assert.AreEqual("自动检测", sourceOptions.First(o => o.Format == QuestionBankSourceFormat.AutoDetect).DisplayName);
Assert.IsFalse(sourceDescriptions.Any(d => d.DisplayName == "自动检测"));
Assert.IsTrue(sourceDescriptions.Any(d => d.DisplayName == "TXT 文本" && d.Detail.Contains("Word格式题库")));
Assert.IsTrue(sourceDescriptions.Any(d => d.DisplayName == "风控平台格式题库"));
Assert.IsTrue(targetOptions.Any(o => o.DisplayName == "小包搜题 TXT (.txt)"));
}

Expand Down Expand Up @@ -385,6 +387,86 @@ public void QuestionBankConversionService_DetectSourceFormat_ForLegacyXlsKsbAndM
}
}

[TestMethod]
public void QuestionBankConversionService_DetectSourceFormat_ForRiskControlPlatformXlsxWithXlsExtension_ReadsQuestions()
{
var service = new QuestionBankConversionService();
var path = Path.Combine(Path.GetTempPath(), $"questionbank_risk_control_{Guid.NewGuid():N}.xls");

try
{
WriteRiskControlPlatformXlsx(path);

Assert.AreEqual(QuestionBankSourceFormat.RiskControlPlatform, service.DetectSourceFormat(path));

var questions = service.Read(path, QuestionBankSourceFormat.AutoDetect);

Assert.AreEqual(4, questions.Count);
Assert.AreEqual("作业前应确认?", questions[0].Topic);
Assert.AreEqual(QuestionType.SingleChoice, questions[0].TopicType);
CollectionAssert.AreEqual(new List<string> { "A. 带电", "B. 停电" }, questions[0].Answer);
Assert.AreEqual("B", questions[0].CorrectAnswer);
Assert.AreEqual("安全生产 / 作业组织", questions[0].Chapter);
Assert.AreEqual("解析内容", questions[0].Analysis);

Assert.AreEqual(QuestionType.MultipleChoice, questions[1].TopicType);
CollectionAssert.AreEqual(new List<string> { "A. 计划", "B. 措施", "C. 监护" }, questions[1].Answer);
Assert.AreEqual("AC", questions[1].CorrectAnswer);

Assert.AreEqual(QuestionType.ShortAnswer, questions[2].TopicType);
Assert.AreEqual(0, questions[2].Answer.Count);
Assert.AreEqual("管行业必须管安全、管业务必须管安全、管生产经营必须管安全。", questions[2].CorrectAnswer);

Assert.AreEqual(QuestionType.TrueFalse, questions[3].TopicType);
Assert.AreEqual("判断解析", questions[3].Analysis);
}
finally
{
if (File.Exists(path)) File.Delete(path);
var xlsxPath = Path.ChangeExtension(path, ".xlsx");
if (File.Exists(xlsxPath)) File.Delete(xlsxPath);
}
}

[TestMethod]
public void QuestionBankConversionService_DetectSourceFormat_ForRiskControlPlatformLegacyXls_ReadsQuestions()
{
var service = new QuestionBankConversionService();
var path = Path.Combine(Path.GetTempPath(), $"questionbank_risk_control_legacy_{Guid.NewGuid():N}.xls");

try
{
WriteLegacyXls(path, sheet =>
{
FillRiskControlPlatformHeader((row, column, value) => SetCell(sheet, row - 1, column - 1, value));
SetCell(sheet, 1, 0, "1");
SetCell(sheet, 1, 1, "安全生产");
SetCell(sheet, 1, 2, "\\");
SetCell(sheet, 1, 3, "通用题库");
SetCell(sheet, 1, 4, "判断题");
SetCell(sheet, 1, 5, "工作前应开展风险辨识。( )");
SetCell(sheet, 1, 6, "A-正确|B-错误");
SetCell(sheet, 1, 7, "A");
SetCell(sheet, 1, 12, "\\");
SetCell(sheet, 1, 13, "判断题解析");
});

Assert.AreEqual(QuestionBankSourceFormat.RiskControlPlatform, service.DetectSourceFormat(path));

var questions = service.Read(path, QuestionBankSourceFormat.AutoDetect);

Assert.AreEqual(1, questions.Count);
Assert.AreEqual(QuestionType.TrueFalse, questions[0].TopicType);
CollectionAssert.AreEqual(new List<string> { "A. 正确", "B. 错误" }, questions[0].Answer);
Assert.AreEqual("A", questions[0].CorrectAnswer);
Assert.AreEqual("判断题解析", questions[0].Analysis);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}

private static void WriteLegacyXls(string path, Action<ISheet> fillSheet)
{
using var workbook = new HSSFWorkbook();
Expand All @@ -400,4 +482,76 @@ private static void SetCell(ISheet sheet, int rowIndex, int columnIndex, string
var row = sheet.GetRow(rowIndex) ?? sheet.CreateRow(rowIndex);
row.CreateCell(columnIndex).SetCellValue(value);
}

private static void WriteRiskControlPlatformXlsx(string pathWithXlsExtension)
{
var xlsxPath = Path.ChangeExtension(pathWithXlsExtension, ".xlsx");
using (var workbook = new XLWorkbook())
{
var sheet = workbook.Worksheets.Add("题库格式");
FillRiskControlPlatformHeader((row, column, value) => sheet.Cell(row, column).Value = value);

SetCell(sheet, 2, 1, "1");
SetCell(sheet, 2, 2, "安全生产");
SetCell(sheet, 2, 3, "作业组织");
SetCell(sheet, 2, 4, "通用题库");
SetCell(sheet, 2, 5, "单选题");
SetCell(sheet, 2, 6, "作业前应确认?");
SetCell(sheet, 2, 7, "A-带电|B-停电");
SetCell(sheet, 2, 8, "B");
SetCell(sheet, 2, 13, "解析内容");

SetCell(sheet, 3, 1, "2");
SetCell(sheet, 3, 2, "安全生产");
SetCell(sheet, 3, 3, "\\");
SetCell(sheet, 3, 4, "通用题库");
SetCell(sheet, 3, 5, "多选题");
SetCell(sheet, 3, 6, "风险管控应包含哪些内容?");
SetCell(sheet, 3, 7, "A-计划|B-措施|C-监护");
SetCell(sheet, 3, 8, "AC");

SetCell(sheet, 4, 1, "3");
SetCell(sheet, 4, 2, "安全生产");
SetCell(sheet, 4, 3, "\\");
SetCell(sheet, 4, 4, "通用题库");
SetCell(sheet, 4, 5, "简答题");
SetCell(sheet, 4, 6, "安全生产“三管三必须”指的是什么?");
SetCell(sheet, 4, 7, "\\");
SetCell(sheet, 4, 8, "管行业必须管安全、管业务必须管安全、管生产经营必须管安全。");

SetCell(sheet, 5, 1, "4");
SetCell(sheet, 5, 2, "安全生产");
SetCell(sheet, 5, 3, "\\");
SetCell(sheet, 5, 4, "通用题库");
SetCell(sheet, 5, 5, "判断题");
SetCell(sheet, 5, 6, "工作前应开展风险辨识。( )");
SetCell(sheet, 5, 7, "A-正确|B-错误");
SetCell(sheet, 5, 8, "A");
SetCell(sheet, 5, 14, "判断解析");

workbook.SaveAs(xlsxPath);
}

File.Copy(xlsxPath, pathWithXlsExtension, overwrite: true);
File.Delete(xlsxPath);
}

private static void FillRiskControlPlatformHeader(Action<int, int, string> setCell)
{
var headers = new[]
{
"序号", "一级纲要", "二级纲要", "题目分类", "题型", "题干", "选项",
"答案", "题目依据", "试题分数", "试题编码", "备注", "说明", "判断题解析"
};

for (var i = 0; i < headers.Length; i++)
{
setCell(1, i + 1, headers[i]);
}
}

private static void SetCell(IXLWorksheet sheet, int rowNumber, int columnNumber, string value)
{
sheet.Cell(rowNumber, columnNumber).Value = value;
}
}
1 change: 1 addition & 0 deletions ModerBox.QuestionBank/Formats/Excel/ExcelReadCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static QuestionType ParseQuestionType(string typeString) {
if (s.Contains("单选")) return QuestionType.SingleChoice;
if (s.Contains("多选")) return QuestionType.MultipleChoice;
if (s.Contains("判断")) return QuestionType.TrueFalse;
if (s.Contains("简答")) return QuestionType.ShortAnswer;
return QuestionType.SingleChoice;
}

Expand Down
4 changes: 4 additions & 0 deletions ModerBox.QuestionBank/Formats/Excel/LegacyExcelReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ public static bool IsSimpleFormat(string filePath) {

public static QuestionBankSourceFormat DetectFormat(string filePath) {
var worksheet = ReadFirstWorksheet(filePath);
if (RiskControlPlatformReader.IsMatchingWorksheet(worksheet)) {
return QuestionBankSourceFormat.RiskControlPlatform;
}

if (IsKsbFormat(worksheet)) {
return QuestionBankSourceFormat.Ksb;
}
Expand Down
1 change: 1 addition & 0 deletions ModerBox.QuestionBank/Formats/Ksb/KsbReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public static List<Question> ReadFromFile(string filePath) {
private static QuestionType ParseQuestionType(string typeText) {
if (typeText.Contains("多选")) return QuestionType.MultipleChoice;
if (typeText.Contains("判断")) return QuestionType.TrueFalse;
if (typeText.Contains("简答")) return QuestionType.ShortAnswer;
return QuestionType.SingleChoice;
}
}
1 change: 1 addition & 0 deletions ModerBox.QuestionBank/Formats/Ksb/KsbWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ private static string QuestionTypeToString(QuestionType type) {
QuestionType.SingleChoice => "单选题",
QuestionType.MultipleChoice => "多选题",
QuestionType.TrueFalse => "判断题",
QuestionType.ShortAnswer => "简答题",
_ => "单选题"
};
}
Expand Down
1 change: 1 addition & 0 deletions ModerBox.QuestionBank/Formats/Mtb/MtbReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static List<Question> ReadFromFile(string filePath) {
private static QuestionType ParseQuestionType(string typeText) {
if (typeText.Contains("多选")) return QuestionType.MultipleChoice;
if (typeText.Contains("判断")) return QuestionType.TrueFalse;
if (typeText.Contains("简答")) return QuestionType.ShortAnswer;
return QuestionType.SingleChoice;
}
}
1 change: 1 addition & 0 deletions ModerBox.QuestionBank/Formats/Mtb/MtbWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private static string QuestionTypeToString(QuestionType type) {
QuestionType.SingleChoice => "单选题",
QuestionType.MultipleChoice => "多选题",
QuestionType.TrueFalse => "判断题",
QuestionType.ShortAnswer => "简答题",
_ => "单选题"
};
}
Expand Down
Loading
Loading