fix: 添加批量导入题目通能 #1

This commit is contained in:
Yakumo Hokori
2026-03-07 19:20:09 +08:00
parent b71a358ed2
commit b53743a68b
6 changed files with 249 additions and 14 deletions

View File

@@ -33,6 +33,23 @@ public class AdminController {
return "admin"; return "admin";
} }
// API: 批量导入题目
@PostMapping("/admin/api/questions/batch-import")
@ResponseBody
public ResponseEntity<Map<String, Object>> batchImportQuestions(@RequestBody List<Map<String, Object>> questions) {
Map<String, Object> result = new HashMap<>();
try {
int importedCount = adminService.batchImportQuestions(questions);
result.put("success", true);
result.put("importedCount", importedCount);
result.put("message", "成功导入 " + importedCount + " 道题目");
} catch (Exception e) {
result.put("success", false);
result.put("message", e.getMessage());
}
return ResponseEntity.ok(result);
}
// API: 获取题目列表(支持全部、按类型、按分值、按标题模糊查询、联合查询) // API: 获取题目列表(支持全部、按类型、按分值、按标题模糊查询、联合查询)
@GetMapping("/admin/api/questions") @GetMapping("/admin/api/questions")
@ResponseBody @ResponseBody
@@ -173,7 +190,6 @@ public class AdminController {
@RequestParam(required = false) String answerB, @RequestParam(required = false) String answerB,
@RequestParam(required = false) String answerC, @RequestParam(required = false) String answerC,
@RequestParam(required = false) String answerD, @RequestParam(required = false) String answerD,
@RequestParam(required = false) String answer,
@RequestParam(required = false) String rAnswer, @RequestParam(required = false) String rAnswer,
@RequestParam Integer score) { @RequestParam Integer score) {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
@@ -198,9 +214,6 @@ public class AdminController {
.answerB(answerB) .answerB(answerB)
.answerC(answerC) .answerC(answerC)
.answerD(answerD); .answerD(answerD);
} else {
// 简答题
builder.answer(answer);
} }
adminService.updateQuestion(builder.build()); adminService.updateQuestion(builder.build());

View File

@@ -4,6 +4,7 @@ import com.baobaot.exam.dao.question;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List; import java.util.List;
import java.util.Map;
public interface adminService extends IService<question> { public interface adminService extends IService<question> {
@@ -50,4 +51,11 @@ public interface adminService extends IService<question> {
* 更新题目 * 更新题目
*/ */
boolean updateQuestion(question q); boolean updateQuestion(question q);
/**
* 批量导入题目
* @param questions 题目数据列表
* @return 成功导入的题目数量
*/
int batchImportQuestions(List<Map<String, Object>> questions);
} }

View File

@@ -6,8 +6,10 @@ import com.baobaot.exam.mapper.questionMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.List;
import java.util.Map;
@Service @Service
public class adminServiceImpl extends ServiceImpl<questionMapper, question> implements adminService { public class adminServiceImpl extends ServiceImpl<questionMapper, question> implements adminService {
@@ -33,7 +35,6 @@ public class adminServiceImpl extends ServiceImpl<questionMapper, question> impl
question q = question.builder() question q = question.builder()
.title(title) .title(title)
.type(type) .type(type)
.answer(answer)
.rAnswer(r_answer) .rAnswer(r_answer)
.score(score) .score(score)
.build(); .build();
@@ -84,4 +85,39 @@ public class adminServiceImpl extends ServiceImpl<questionMapper, question> impl
public boolean updateQuestion(question q) { public boolean updateQuestion(question q) {
return updateById(q); return updateById(q);
} }
@Override
@Transactional
public int batchImportQuestions(List<Map<String, Object>> questions) {
int importedCount = 0;
for (Map<String, Object> qData : questions) {
String title = (String) qData.get("title");
Integer type = ((Number) qData.get("type")).intValue();
Integer score = ((Number) qData.get("score")).intValue();
question.questionBuilder builder = question.builder()
.title(title)
.type(type)
.score(score);
if (type == 1) {
// 选择题
builder.answerA((String) qData.get("answerA"))
.answerB((String) qData.get("answerB"))
.answerC((String) qData.get("answerC"))
.answerD((String) qData.get("answerD"))
.rAnswer((String) qData.get("rAnswer"));
} else {
// 简答题
builder.rAnswer((String) qData.get("rAnswer"));
}
if (save(builder.build())) {
importedCount++;
}
}
return importedCount;
}
} }

View File

@@ -15,7 +15,6 @@ public class question {
private Integer id; private Integer id;
private String title; private String title;
private Integer type; private Integer type;
private String answer;
private String answerA; private String answerA;
private String answerB; private String answerB;
private String answerC; private String answerC;

View File

@@ -304,6 +304,7 @@
</div> </div>
<div style="margin-top: 16px; display: flex; gap: 12px; flex-wrap: wrap;"> <div style="margin-top: 16px; display: flex; gap: 12px; flex-wrap: wrap;">
<button type="button" class="btn btn-primary" onclick="loadAllQuestions()">显示全部</button> <button type="button" class="btn btn-primary" onclick="loadAllQuestions()">显示全部</button>
<button type="button" class="btn btn-success" onclick="openBatchImportModal()">批量导入</button>
<a th:href="@{/admin/mkexam}" class="btn btn-success">创建试卷</a> <a th:href="@{/admin/mkexam}" class="btn btn-success">创建试卷</a>
<a th:href="@{/admin/listexam}" class="btn btn-success">查看试卷</a> <a th:href="@{/admin/listexam}" class="btn btn-success">查看试卷</a>
<a th:href="@{/admin/correct}" class="btn btn-success" style="background: #f59e0b;">批改试卷</a> <a th:href="@{/admin/correct}" class="btn btn-success" style="background: #f59e0b;">批改试卷</a>
@@ -502,7 +503,6 @@
<th width="60">ID</th> <th width="60">ID</th>
<th>题目</th> <th>题目</th>
<th width="100">类型</th> <th width="100">类型</th>
<th>选项/学生答案</th>
<th>正确答案</th> <th>正确答案</th>
<th width="80">分值</th> <th width="80">分值</th>
<th width="150">操作</th> <th width="150">操作</th>
@@ -525,7 +525,7 @@
if (q.answerD) parts.push(`D: ${escapeHtml(q.answerD)}`); if (q.answerD) parts.push(`D: ${escapeHtml(q.answerD)}`);
optionsHtml = parts.join('<br>'); optionsHtml = parts.join('<br>');
} else { } else {
optionsHtml = q.answer ? escapeHtml(q.answer) : '无答案'; optionsHtml = '<span style="color: #999;">-</span>';
} }
const rAnswerHtml = q.rAnswer ? const rAnswerHtml = q.rAnswer ?
@@ -630,6 +630,101 @@
let currentEditId = null; let currentEditId = null;
let currentEditType = null; let currentEditType = null;
// 打开批量导入弹窗
function openBatchImportModal() {
document.getElementById('batchImportModal').style.display = 'block';
document.getElementById('batchImportJson').value = '';
document.getElementById('batchImportResult').style.display = 'none';
}
// 关闭批量导入弹窗
function closeBatchImportModal() {
document.getElementById('batchImportModal').style.display = 'none';
document.getElementById('batchImportJson').value = '';
document.getElementById('batchImportResult').style.display = 'none';
}
// 提交批量导入
function submitBatchImport() {
const jsonText = document.getElementById('batchImportJson').value.trim();
const resultDiv = document.getElementById('batchImportResult');
if (!jsonText) {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">请输入JSON数据</div>';
resultDiv.style.display = 'block';
return;
}
let questions;
try {
questions = JSON.parse(jsonText);
} catch (e) {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">JSON格式错误: ' + e.message + '</div>';
resultDiv.style.display = 'block';
return;
}
if (!Array.isArray(questions)) {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">数据必须是JSON数组格式</div>';
resultDiv.style.display = 'block';
return;
}
if (questions.length === 0) {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">题目数组不能为空</div>';
resultDiv.style.display = 'block';
return;
}
// 验证每道题的必填字段
for (let i = 0; i < questions.length; i++) {
const q = questions[i];
if (!q.title || q.title.trim() === '') {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">第 ' + (i + 1) + ' 题缺少题目内容(title)</div>';
resultDiv.style.display = 'block';
return;
}
if (!q.type || (q.type !== 1 && q.type !== 2)) {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">第 ' + (i + 1) + ' 题类型错误type必须是1(选择题)或2(简答题)</div>';
resultDiv.style.display = 'block';
return;
}
if (!q.score || q.score < 0) {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">第 ' + (i + 1) + ' 题分值错误</div>';
resultDiv.style.display = 'block';
return;
}
}
// 发送请求
fetch('/admin/api/questions/batch-import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(questions)
})
.then(res => res.json())
.then(data => {
if (data.success) {
resultDiv.innerHTML = '<div class="alert" style="background: #efe; color: #2e7d32;">导入成功!共导入 ' + data.importedCount + ' 道题目</div>';
resultDiv.style.display = 'block';
loadAllQuestions();
setTimeout(() => {
closeBatchImportModal();
}, 1500);
} else {
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">导入失败: ' + (data.message || '未知错误') + '</div>';
resultDiv.style.display = 'block';
}
})
.catch(err => {
console.error(err);
resultDiv.innerHTML = '<div class="alert" style="background: #fee; color: #c33;">导入失败,请检查网络</div>';
resultDiv.style.display = 'block';
});
}
// 打开编辑弹窗 // 打开编辑弹窗
function editQuestion(id, type) { function editQuestion(id, type) {
currentEditId = id; currentEditId = id;
@@ -676,7 +771,6 @@
document.getElementById('editEssayForm').style.display = 'block'; document.getElementById('editEssayForm').style.display = 'block';
document.getElementById('editTypeLabel').textContent = '简答题'; document.getElementById('editTypeLabel').textContent = '简答题';
document.getElementById('editAnswer').value = q.answer || '';
document.getElementById('editRAnswerEssay').value = q.rAnswer || ''; document.getElementById('editRAnswerEssay').value = q.rAnswer || '';
} }
} }
@@ -705,7 +799,6 @@
formData.append('answerD', document.getElementById('editAnswerD').value); formData.append('answerD', document.getElementById('editAnswerD').value);
formData.append('rAnswer', document.getElementById('editRAnswer').value); formData.append('rAnswer', document.getElementById('editRAnswer').value);
} else { } else {
formData.append('answer', document.getElementById('editAnswer').value);
formData.append('rAnswer', document.getElementById('editRAnswerEssay').value); formData.append('rAnswer', document.getElementById('editRAnswerEssay').value);
} }
@@ -756,8 +849,7 @@
if (answerD) parts.push(`D: ${escapeHtml(answerD)}`); if (answerD) parts.push(`D: ${escapeHtml(answerD)}`);
optionsHtml = parts.join('<br>'); optionsHtml = parts.join('<br>');
} else { } else {
const answer = formData.get('answer'); optionsHtml = '<span style="color: #999;">-</span>';
optionsHtml = answer ? escapeHtml(answer) : '无答案';
} }
const rAnswerHtml = rAnswer ? const rAnswerHtml = rAnswer ?
@@ -784,6 +876,49 @@
</script> </script>
<!-- 批量导入弹窗 -->
<div id="batchImportModal" class="modal">
<div class="modal-content" style="max-width: 700px; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h2>批量导入题目</h2>
<span class="close-btn" onclick="closeBatchImportModal()">&times;</span>
</div>
<div class="alert alert-info" style="margin-bottom: 16px;">
<strong>格式说明:</strong>JSON数组格式每道题包含 title(必填), type(1=选择题,2=简答题), score(分值) 等字段
</div>
<div class="form-group">
<label>JSON数据 <span class="required">*</span></label>
<textarea id="batchImportJson" rows="15" placeholder='[
{
"title": "选择题示例",
"type": 1,
"answerA": "选项A",
"answerB": "选项B",
"answerC": "选项C",
"answerD": "选项D",
"rAnswer": "A",
"score": 5
},
{
"title": "简答题示例",
"type": 2,
"rAnswer": "参考答案",
"score": 10
}
]' style="font-family: monospace; font-size: 13px;"></textarea>
</div>
<div id="batchImportResult" style="display: none; margin-bottom: 16px;"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeBatchImportModal()">取消</button>
<button type="button" class="btn btn-primary" onclick="submitBatchImport()">确认导入</button>
</div>
</div>
</div>
<!-- 编辑弹窗 --> <!-- 编辑弹窗 -->
<div id="editModal" class="modal"> <div id="editModal" class="modal">
<div class="modal-content" style="max-width: 600px; max-height: 90vh; overflow-y: auto;"> <div class="modal-content" style="max-width: 600px; max-height: 90vh; overflow-y: auto;">
@@ -834,10 +969,10 @@
<!-- 简答题表单 --> <!-- 简答题表单 -->
<div id="editEssayForm" style="display: none;"> <div id="editEssayForm" style="display: none;">
<div class="form-group"> <!-- <div class="form-group">
<label>学生答案参考</label> <label>学生答案参考</label>
<textarea id="editAnswer" rows="2"></textarea> <textarea id="editAnswer" rows="2"></textarea>
</div> </div> -->
<div class="form-group"> <div class="form-group">
<label>参考答案</label> <label>参考答案</label>
<textarea id="editRAnswerEssay" rows="2"></textarea> <textarea id="editRAnswerEssay" rows="2"></textarea>

44
test_import.json Normal file
View File

@@ -0,0 +1,44 @@
[
{
"title": "Java中以下哪个关键字用于定义接口",
"type": 1,
"answerA": "class",
"answerB": "interface",
"answerC": "extends",
"answerD": "implements",
"rAnswer": "B",
"score": 5
},
{
"title": "Spring框架的核心特性是什么",
"type": 1,
"answerA": "AOP面向切面编程",
"answerB": "IOC控制反转",
"answerC": "MVC设计模式",
"answerD": "以上都是",
"rAnswer": "D",
"score": 5
},
{
"title": "简述Java中Exception和Error的区别。",
"type": 2,
"rAnswer": "Exception是程序可以处理的异常通常由程序逻辑错误引起Error是严重错误如内存溢出程序无法处理。",
"score": 10
},
{
"title": "MySQL中如何优化慢查询",
"type": 2,
"rAnswer": "1. 添加索引2. 优化SQL语句3. 使用EXPLAIN分析查询计划4. 避免全表扫描5. 适当分表分库。",
"score": 15
},
{
"title": "以下哪个不是Java的基本数据类型",
"type": 1,
"answerA": "int",
"answerB": "String",
"answerC": "boolean",
"answerD": "double",
"rAnswer": "B",
"score": 5
}
]