fix: 添加批量导入题目通能 #1
This commit is contained in:
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()">×</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
44
test_import.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user