363 lines
16 KiB
Java
363 lines
16 KiB
Java
package top.tqx.demo_1.controller;
|
||
|
||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||
import io.swagger.v3.oas.annotations.Operation;
|
||
import io.swagger.v3.oas.annotations.Parameter;
|
||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||
import jakarta.servlet.http.HttpServletRequest;
|
||
import lombok.extern.slf4j.Slf4j;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.web.bind.annotation.*;
|
||
import top.tqx.demo_1.common.Result;
|
||
import top.tqx.demo_1.dto.*;
|
||
import top.tqx.demo_1.entity.Question;
|
||
import top.tqx.demo_1.enums.AnswerType;
|
||
import top.tqx.demo_1.service.AuthService;
|
||
import top.tqx.demo_1.service.ExamService;
|
||
import top.tqx.demo_1.service.ExamRecordService;
|
||
import top.tqx.demo_1.service.UserAnswerService;
|
||
import top.tqx.demo_1.service.UserService;
|
||
import top.tqx.demo_1.vo.UserVO;
|
||
|
||
import java.io.IOException;
|
||
import java.util.HashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
import java.util.UUID;
|
||
|
||
@Tag(name = "考试管理", description = "题目生成与答案批改接口")
|
||
@RestController
|
||
@RequestMapping("/exam")
|
||
@Slf4j
|
||
public class ExamController extends BaseController {
|
||
|
||
@Autowired
|
||
private ExamService examService;
|
||
|
||
@Autowired
|
||
private AuthService authService;
|
||
|
||
@Autowired
|
||
private UserService userService;
|
||
|
||
@Autowired
|
||
private UserAnswerService userAnswerService;
|
||
|
||
@Autowired
|
||
private top.tqx.demo_1.service.ExamRecordService examRecordService;
|
||
|
||
@Operation(summary = "开始考试(获取题目)", description = "通过试卷ID开始模拟考试,同时返回试卷题目内容")
|
||
@PostMapping("/paper/{paperId}/start")
|
||
public Result<ExamRecordResponse> startExamByPaperId(
|
||
@PathVariable String paperId,
|
||
HttpServletRequest httpRequest) {
|
||
try {
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
ExamRecordResponse response = examRecordService.startExamByPaperId(paperId, userId, AnswerType.PRACTICE.getCode());
|
||
return Result.success(response);
|
||
} catch (Exception e) {
|
||
log.error("开始考试失败: {}", e.getMessage());
|
||
return Result.error(500, e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "获取考试结果", description = "获取模拟考试的完整结果,包括题目、答案、分数等信息")
|
||
@GetMapping("/record/{recordId}/result")
|
||
public Result<ExamRecordResponse> getExamResult(
|
||
@PathVariable String recordId,
|
||
HttpServletRequest httpRequest) {
|
||
try {
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
ExamRecordResponse response = examRecordService.getRecordById(recordId);
|
||
|
||
if (response == null) {
|
||
return Result.error(404, "记录不存在");
|
||
}
|
||
|
||
if (!response.getUserId().equals(userId)) {
|
||
return Result.error(403, "无权查看此记录");
|
||
}
|
||
|
||
return Result.success(response);
|
||
} catch (Exception e) {
|
||
log.error("获取考试结果失败: {}", e.getMessage());
|
||
return Result.error(500, e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "生成试卷", description = "根据权限范围从题库中动态生成试卷")
|
||
@PostMapping("/paper/generate")
|
||
public Result<ExamPaperGenerateResponse> generatePaper(
|
||
@RequestBody ExamPaperGenerateRequest request,
|
||
HttpServletRequest httpRequest) {
|
||
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
if (userId == null) {
|
||
return Result.error(401, "用户未登录");
|
||
}
|
||
|
||
UserVO userVO = userService.getUserById(userId);
|
||
if (userVO == null) {
|
||
return Result.error(401, "用户不存在");
|
||
}
|
||
|
||
if (request.getIncludePersonal() == null) {
|
||
request.setIncludePersonal(false);
|
||
}
|
||
|
||
log.info("生成试卷请求: userId={}, deptId={}, userType={}, includePersonal={}, singleChoiceCount={}, multipleChoiceCount={}, trueFalseCount={}, fillBlankCount={}, subjectiveCount={}, difficulty={}",
|
||
userId, userVO.getDeptId(), userVO.getUserType(), request.getIncludePersonal(),
|
||
request.getSingleChoiceCount(), request.getMultipleChoiceCount(),
|
||
request.getTrueFalseCount(), request.getFillBlankCount(),
|
||
request.getSubjectiveCount(), request.getDifficulty());
|
||
|
||
try {
|
||
ExamPaperGenerateResponse response = examService.generatePaper(request, userId, userVO.getDeptId(), userVO.getUserType());
|
||
return Result.success("试卷生成成功", response);
|
||
} catch (Exception e) {
|
||
log.error("生成试卷异常: {}", e.getMessage(), e);
|
||
return Result.error(500, "生成试卷异常: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "生成题目", description = "根据知识库内容生成考试题目,支持单选题、多选题、判断题、填空题、简答题")
|
||
@PostMapping("/generate")
|
||
public Result<Map<String, Object>> generateQuestions(
|
||
@RequestBody ExamGenerateRequest request,
|
||
HttpServletRequest httpRequest) {
|
||
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
if (userId == null) {
|
||
return Result.error(401, "用户未登录");
|
||
}
|
||
|
||
if (request.getQuestionTypes() == null) {
|
||
return Result.error(400, "question_types 不能为空,请指定题型及数量");
|
||
}
|
||
|
||
boolean hasCollection = (request.getCollection() != null) ||
|
||
(request.getCollectionName() != null && !request.getCollectionName().isEmpty());
|
||
if (!hasCollection) {
|
||
return Result.error(400, "collection 或 collection_name 不能为空,请指定向量库");
|
||
}
|
||
|
||
if (request.getFilePath() == null || request.getFilePath().isEmpty()) {
|
||
return Result.error(400, "file_path 不能为空,请指定文件路径");
|
||
}
|
||
|
||
if (request.getRequestId() == null || request.getRequestId().isEmpty()) {
|
||
request.setRequestId(UUID.randomUUID().toString());
|
||
}
|
||
|
||
log.info("生成题目请求: userId={}, requestId={}, questionTypes={}, difficulty={}, collection={}, collectionName={}, filePath={}",
|
||
userId, request.getRequestId(), request.getQuestionTypes(),
|
||
request.getDifficulty(), request.getCollection(), request.getCollectionName(), request.getFilePath());
|
||
|
||
examService.generateQuestionsAsync(request);
|
||
|
||
Map<String, Object> result = new HashMap<>();
|
||
result.put("requestId", request.getRequestId());
|
||
result.put("message", "收到请求,正在生成题目,请稍后查看文件状态");
|
||
|
||
return Result.success("收到请求", result);
|
||
}
|
||
|
||
@Operation(summary = "提交考试并批改(正式考试专用)", description = "通过考试记录ID提交正式考试答案并批改")
|
||
@PostMapping("/record/{recordId}/grade")
|
||
public Result<ExamRecordResponse> gradeExamRecord(
|
||
@PathVariable String recordId,
|
||
@RequestBody ExamSaveRequest request,
|
||
HttpServletRequest httpRequest) {
|
||
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
if (userId == null) {
|
||
return Result.error(401, "用户未登录");
|
||
}
|
||
|
||
log.info("提交正式考试: userId={}, recordId={}", userId, recordId);
|
||
|
||
try {
|
||
ExamRecordResponse response = examService.gradeAnswersForRecord(recordId, request, userId, "formal_exam");
|
||
return Result.success("提交成功", response);
|
||
} catch (Exception e) {
|
||
log.error("提交正式考试失败: {}", e.getMessage(), e);
|
||
return Result.error(500, "提交失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "批改答案(日常练习/模拟考/正式考试通用)", description = "对学生提交的答案进行批改,支持客观题本地批改和主观题LLM评分")
|
||
@PostMapping("/grade")
|
||
public Result<Object> gradeAnswers(
|
||
@RequestBody ExamGradeRequest request,
|
||
HttpServletRequest httpRequest) {
|
||
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
if (userId == null) {
|
||
return Result.error(401, "用户未登录");
|
||
}
|
||
|
||
if (request.getAnswers() == null || request.getAnswers().isEmpty()) {
|
||
return Result.error(400, "答案列表不能为空");
|
||
}
|
||
|
||
// 检查是否是通过 recordId 提交的正式考试
|
||
if (request.getRecordId() != null && !request.getRecordId().isEmpty()) {
|
||
// 需要转换 ExamGradeRequest 为 ExamSaveRequest
|
||
ExamSaveRequest saveRequest = new ExamSaveRequest();
|
||
List<ExamSaveRequest.AnswerItem> saveItems = request.getAnswers().stream().map(gradeItem -> {
|
||
ExamSaveRequest.AnswerItem saveItem = new ExamSaveRequest.AnswerItem();
|
||
saveItem.setQuestionId(gradeItem.getQuestionId());
|
||
saveItem.setQuestionType(gradeItem.getQuestionType());
|
||
saveItem.setAnswer(gradeItem.getStudentAnswer());
|
||
return saveItem;
|
||
}).collect(java.util.stream.Collectors.toList());
|
||
saveRequest.setAnswers(saveItems);
|
||
|
||
log.info("通过recordId提交考试: userId={}, recordId={}", userId, request.getRecordId());
|
||
|
||
try {
|
||
ExamRecordResponse response = examService.gradeAnswersForRecord(
|
||
request.getRecordId(),
|
||
saveRequest,
|
||
userId,
|
||
request.getAnswerType() != null ? request.getAnswerType() : "formal_exam");
|
||
return Result.success("批阅成功", response);
|
||
} catch (Exception e) {
|
||
log.error("批改失败: {}", e.getMessage(), e);
|
||
return Result.error(500, "批改失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
// 日常练习/模拟考模式
|
||
for (int i = 0; i < request.getAnswers().size(); i++) {
|
||
ExamGradeRequest.AnswerItem answer = request.getAnswers().get(i);
|
||
if (answer.getQuestionId() == null || answer.getQuestionId().isEmpty()) {
|
||
return Result.error(400, "第" + (i + 1) + "个答案的question_id不能为空");
|
||
}
|
||
if (answer.getQuestionType() == null || answer.getQuestionType().isEmpty()) {
|
||
return Result.error(400, "第" + (i + 1) + "个答案的question_type不能为空");
|
||
}
|
||
if (answer.getQuestionContent() == null) {
|
||
return Result.error(400, "第" + (i + 1) + "个答案的question_content不能为空");
|
||
}
|
||
if (answer.getMaxScore() == null) {
|
||
return Result.error(400, "第" + (i + 1) + "个答案的max_score不能为空");
|
||
}
|
||
}
|
||
|
||
if (request.getRequestId() == null || request.getRequestId().isEmpty()) {
|
||
request.setRequestId(UUID.randomUUID().toString());
|
||
}
|
||
|
||
if (request.getPaperId() == null || request.getPaperId().isEmpty()) {
|
||
String generatedPaperId = UUID.randomUUID().toString();
|
||
request.setPaperId(generatedPaperId);
|
||
log.info("自动生成试卷ID: paperId={}", generatedPaperId);
|
||
}
|
||
|
||
if (request.getSessionId() == null || request.getSessionId().isEmpty()) {
|
||
request.setSessionId("grade_" + UUID.randomUUID().toString().substring(0, 8));
|
||
}
|
||
|
||
log.info("批改答案请求: userId={}, requestId={}, paperId={}, sessionId={}, type={}, answers count={}",
|
||
userId, request.getRequestId(), request.getPaperId(), request.getSessionId(),
|
||
request.getAnswerType(), request.getAnswers().size());
|
||
|
||
try {
|
||
ExamGradeResponse response = examService.gradeAnswers(request, userId, request.getPaperId(),
|
||
request.getSessionId(), request.getAnswerType());
|
||
|
||
if (Boolean.TRUE.equals(response.getSuccess())) {
|
||
return Result.success("批阅成功", response);
|
||
} else {
|
||
String errorMessage = response.getError() != null ? response.getError() : "答案批改失败";
|
||
return Result.error(500, errorMessage);
|
||
}
|
||
|
||
} catch (IOException e) {
|
||
log.error("批改答案失败: {}", e.getMessage(), e);
|
||
return Result.error(500, "批改答案失败: " + e.getMessage());
|
||
} catch (Exception e) {
|
||
log.error("批改答案异常: {}", e.getMessage(), e);
|
||
return Result.error(500, "批改答案异常: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "查询用户答题记录", description = "根据试卷ID或会话ID查询用户答题记录,支持按试卷/练习维度分组展示")
|
||
@PostMapping("/answers/query")
|
||
public Result<UserAnswerQueryResponse> queryUserAnswers(
|
||
@RequestBody UserAnswerQueryRequest request,
|
||
HttpServletRequest httpRequest) {
|
||
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
if (userId == null) {
|
||
return Result.error(401, "用户未登录");
|
||
}
|
||
|
||
boolean hasPaperId = request.getPaperId() != null && !request.getPaperId().isEmpty();
|
||
boolean hasSessionId = request.getSessionId() != null && !request.getSessionId().isEmpty();
|
||
|
||
if (!hasPaperId && !hasSessionId) {
|
||
return Result.error(400, "paper_id 或 session_id 不能为空");
|
||
}
|
||
|
||
if (hasPaperId && hasSessionId) {
|
||
return Result.error(400, "paper_id 和 session_id 不能同时提供");
|
||
}
|
||
|
||
request.setUserId(userId);
|
||
|
||
log.info("查询用户答题记录: userId={}, paperId={}, sessionId={}",
|
||
userId, request.getPaperId(), request.getSessionId());
|
||
|
||
try {
|
||
UserAnswerQueryResponse response = userAnswerService.queryUserAnswers(request);
|
||
|
||
if (response.getTotalCount() == 0) {
|
||
return Result.success("未找到答题记录", response);
|
||
}
|
||
|
||
return Result.success("查询成功", response);
|
||
|
||
} catch (Exception e) {
|
||
log.error("查询用户答题记录异常: {}", e.getMessage(), e);
|
||
return Result.error(500, "查询用户答题记录异常: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
@Operation(summary = "获取考试记录列表", description = "根据类型获取用户的考试/练习记录,支持日常练习(practice)、模拟考(mock_exam)、正式考(formal_exam)")
|
||
@GetMapping("/records")
|
||
public Result<Object> getExamRecords(
|
||
@Parameter(description = "记录类型:practice(日常练习), mock_exam(模拟考), formal_exam(正式考)", example = "practice")
|
||
@RequestParam(defaultValue = "practice") String type,
|
||
HttpServletRequest httpRequest) {
|
||
|
||
Long userId = getCurrentUserId(httpRequest);
|
||
if (userId == null) {
|
||
return Result.error(401, "用户未登录");
|
||
}
|
||
|
||
log.info("查询考试记录: userId={}, type={}", userId, type);
|
||
|
||
try {
|
||
// 根据类型选择不同的查询逻辑
|
||
if ("practice".equals(type)) {
|
||
// 日常练习:从 user_answer 表查询,按 session_id 分组
|
||
UserAnswerQueryRequest queryRequest = new UserAnswerQueryRequest();
|
||
queryRequest.setUserId(userId);
|
||
UserAnswerQueryResponse response = userAnswerService.queryUserAnswers(queryRequest);
|
||
return Result.success(response);
|
||
} else if ("mock_exam".equals(type) || "formal_exam".equals(type)) {
|
||
// 模拟考/正式考:从 exam_record 表查询
|
||
List<ExamRecordResponse> records = examRecordService.getUserPapers(userId, type);
|
||
return Result.success(records);
|
||
} else {
|
||
return Result.error(400, "无效的类型参数,支持的值: practice, mock_exam, formal_exam");
|
||
}
|
||
} catch (Exception e) {
|
||
log.error("查询考试记录失败: {}", e.getMessage(), e);
|
||
return Result.error(500, "查询考试记录失败: " + e.getMessage());
|
||
}
|
||
}
|
||
|
||
} |