主要改进: 1. 新增ExamAnswerView页面和样式文件 2. 优化填空题间距,减少过大的垂直边距 3. 紧凑化题型之间的间距,提升页面密度 4. 去掉题型标题的背景色和左侧竖线 5. 为题型标题添加汉字序号(一、二、三等) 6. 去掉选择题表格的边框,简化界面 7. 解决打印时显示"试卷答案"标题的问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
776 lines
22 KiB
Go
776 lines
22 KiB
Go
package handlers
|
||
|
||
import (
|
||
"ankao/internal/database"
|
||
"ankao/internal/models"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"math/rand"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// CreateExamRequest 创建试卷请求
|
||
type CreateExamRequest struct {
|
||
Title string `json:"title" binding:"required"`
|
||
}
|
||
|
||
// CreateExam 创建试卷
|
||
func CreateExam(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
var req CreateExamRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询用户信息,获取用户类型
|
||
var user models.User
|
||
if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||
log.Printf("查询用户失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询用户信息失败"})
|
||
return
|
||
}
|
||
|
||
// 根据用户类型决定论述题类型
|
||
essayType := "ordinary-essay" // 默认普通涉密人员论述题
|
||
if user.UserType == "management-person" {
|
||
essayType = "management-essay" // 保密管理人员论述题
|
||
}
|
||
|
||
// 使用固定的题型配置(总分100分)
|
||
questionTypes := []models.QuestionTypeConfig{
|
||
{Type: "fill-in-blank", Count: 10, Score: 2.0}, // 20分
|
||
{Type: "true-false", Count: 10, Score: 2.0}, // 20分
|
||
{Type: "multiple-choice", Count: 10, Score: 1.0}, // 10分
|
||
{Type: "multiple-selection", Count: 10, Score: 2.5}, // 25分
|
||
{Type: "short-answer", Count: 2, Score: 10.0}, // 20分
|
||
{Type: essayType, Count: 1, Score: 5.0}, // 5分(根据用户类型选择论述题)
|
||
}
|
||
|
||
// 按题型配置随机抽取题目
|
||
var allQuestionIDs []int64
|
||
totalScore := 0.0
|
||
|
||
for _, qtConfig := range questionTypes {
|
||
var questions []models.PracticeQuestion
|
||
query := db.Where("type = ?", qtConfig.Type)
|
||
|
||
if err := query.Find(&questions).Error; err != nil {
|
||
log.Printf("查询题目失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"})
|
||
return
|
||
}
|
||
|
||
// 检查题目数量是否足够
|
||
if len(questions) < qtConfig.Count {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"message": fmt.Sprintf("题型 %s 题目数量不足,需要 %d 道,实际 %d 道", qtConfig.Type, qtConfig.Count, len(questions)),
|
||
})
|
||
return
|
||
}
|
||
|
||
// 随机抽取 (Fisher-Yates 洗牌算法)
|
||
rand.Seed(time.Now().UnixNano())
|
||
for i := len(questions) - 1; i > 0; i-- {
|
||
j := rand.Intn(i + 1)
|
||
questions[i], questions[j] = questions[j], questions[i]
|
||
}
|
||
selectedQuestions := questions[:qtConfig.Count]
|
||
|
||
// 收集题目ID
|
||
for _, q := range selectedQuestions {
|
||
allQuestionIDs = append(allQuestionIDs, q.ID)
|
||
}
|
||
|
||
// 计算总分
|
||
totalScore += float64(qtConfig.Count) * qtConfig.Score
|
||
}
|
||
|
||
// 随机打乱题目ID顺序
|
||
rand.Seed(time.Now().UnixNano())
|
||
for i := len(allQuestionIDs) - 1; i > 0; i-- {
|
||
j := rand.Intn(i + 1)
|
||
allQuestionIDs[i], allQuestionIDs[j] = allQuestionIDs[j], allQuestionIDs[i]
|
||
}
|
||
|
||
// 序列化题目ID
|
||
questionIDsJSON, err := json.Marshal(allQuestionIDs)
|
||
if err != nil {
|
||
log.Printf("序列化题目ID失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "生成试卷失败"})
|
||
return
|
||
}
|
||
|
||
// 创建试卷
|
||
exam := models.Exam{
|
||
UserID: uint(userID.(int64)),
|
||
Title: req.Title,
|
||
TotalScore: int(totalScore), // 总分100分
|
||
Duration: 60, // 固定60分钟
|
||
PassScore: 80, // 固定80分及格
|
||
QuestionIDs: questionIDsJSON,
|
||
Status: "active",
|
||
}
|
||
|
||
if err := db.Create(&exam).Error; err != nil {
|
||
log.Printf("创建试卷失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "创建试卷失败"})
|
||
return
|
||
}
|
||
|
||
// 返回试卷信息
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"id": exam.ID,
|
||
"title": exam.Title,
|
||
"total_score": exam.TotalScore,
|
||
"duration": exam.Duration,
|
||
"pass_score": exam.PassScore,
|
||
"question_count": len(allQuestionIDs),
|
||
"created_at": exam.CreatedAt,
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetExamList 获取试卷列表
|
||
func GetExamList(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询用户创建的试卷
|
||
var exams []models.Exam
|
||
if err := db.Where("user_id = ? AND status = ?", userID, "active").
|
||
Order("created_at DESC").
|
||
Find(&exams).Error; err != nil {
|
||
log.Printf("查询试卷列表失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询试卷列表失败"})
|
||
return
|
||
}
|
||
|
||
// 为每个试卷计算题目数量和获取考试记录统计
|
||
type ExamWithStats struct {
|
||
models.Exam
|
||
QuestionCount int `json:"question_count"`
|
||
AttemptCount int `json:"attempt_count"` // 考试次数
|
||
BestScore float64 `json:"best_score"` // 最高分
|
||
HasInProgressExam bool `json:"has_in_progress_exam"` // 是否有进行中的考试
|
||
InProgressRecordID uint `json:"in_progress_record_id,omitempty"` // 进行中的考试记录ID
|
||
}
|
||
|
||
result := make([]ExamWithStats, 0, len(exams))
|
||
for _, exam := range exams {
|
||
var questionIDs []uint
|
||
if err := json.Unmarshal(exam.QuestionIDs, &questionIDs); err == nil {
|
||
stats := ExamWithStats{
|
||
Exam: exam,
|
||
QuestionCount: len(questionIDs),
|
||
}
|
||
|
||
// 查询该试卷的考试记录统计
|
||
var count int64
|
||
db.Model(&models.ExamRecord{}).Where("exam_id = ? AND user_id = ?", exam.ID, userID).Count(&count)
|
||
stats.AttemptCount = int(count)
|
||
|
||
// 查询最高分
|
||
var record models.ExamRecord
|
||
if err := db.Where("exam_id = ? AND user_id = ?", exam.ID, userID).
|
||
Order("score DESC").
|
||
First(&record).Error; err == nil {
|
||
stats.BestScore = record.Score
|
||
}
|
||
|
||
// 查询是否有进行中的考试(status为in_progress)
|
||
var inProgressRecord models.ExamRecord
|
||
if err := db.Where("exam_id = ? AND user_id = ? AND status = ?", exam.ID, userID, "in_progress").
|
||
Order("created_at DESC").
|
||
First(&inProgressRecord).Error; err == nil {
|
||
stats.HasInProgressExam = true
|
||
stats.InProgressRecordID = inProgressRecord.ID
|
||
}
|
||
|
||
result = append(result, stats)
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": result,
|
||
})
|
||
}
|
||
|
||
// GetExamDetail 获取试卷详情
|
||
func GetExamDetail(c *gin.Context) {
|
||
_, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
examIDStr := c.Param("id")
|
||
examID, err := strconv.ParseUint(examIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的试卷ID"})
|
||
return
|
||
}
|
||
|
||
// 检查是否需要显示答案
|
||
showAnswer := c.Query("show_answer") == "true"
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询试卷
|
||
var exam models.Exam
|
||
if err := db.Where("id = ?", examID).First(&exam).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"})
|
||
return
|
||
}
|
||
|
||
// 解析题目ID列表
|
||
var questionIDs []int64
|
||
if err := json.Unmarshal(exam.QuestionIDs, &questionIDs); err != nil {
|
||
log.Printf("解析题目ID失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "解析试卷数据失败"})
|
||
return
|
||
}
|
||
|
||
// 查询题目详情
|
||
var questions []models.PracticeQuestion
|
||
if err := db.Where("id IN ?", questionIDs).Find(&questions).Error; err != nil {
|
||
log.Printf("查询题目失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"})
|
||
return
|
||
}
|
||
|
||
// 按原始顺序排序题目并转换为DTO
|
||
questionMap := make(map[int64]models.PracticeQuestion)
|
||
for _, q := range questions {
|
||
questionMap[q.ID] = q
|
||
}
|
||
orderedDTOs := make([]models.PracticeQuestionDTO, 0, len(questionIDs))
|
||
for _, id := range questionIDs {
|
||
if q, ok := questionMap[id]; ok {
|
||
dto := convertToDTO(q)
|
||
// 根据showAnswer参数决定是否显示答案
|
||
if !showAnswer {
|
||
dto.Answer = nil // 不显示答案
|
||
}
|
||
orderedDTOs = append(orderedDTOs, dto)
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"exam": exam,
|
||
"questions": orderedDTOs,
|
||
},
|
||
})
|
||
}
|
||
|
||
// StartExam 开始考试(创建考试记录)
|
||
func StartExam(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
examIDStr := c.Param("id")
|
||
examID, err := strconv.ParseUint(examIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的试卷ID"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询试卷
|
||
var exam models.Exam
|
||
if err := db.Where("id = ?", examID).First(&exam).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"})
|
||
return
|
||
}
|
||
|
||
// 创建考试记录
|
||
now := time.Now()
|
||
record := models.ExamRecord{
|
||
ExamID: uint(examID),
|
||
UserID: uint(userID.(int64)),
|
||
StartTime: &now,
|
||
TotalScore: exam.TotalScore,
|
||
Status: "in_progress",
|
||
}
|
||
|
||
if err := db.Create(&record).Error; err != nil {
|
||
log.Printf("创建考试记录失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "开始考试失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"record_id": record.ID,
|
||
"start_time": record.StartTime,
|
||
"duration": exam.Duration,
|
||
},
|
||
})
|
||
}
|
||
|
||
// SubmitExam 提交试卷答案
|
||
func SubmitExam(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
recordIDStr := c.Param("record_id")
|
||
recordID, err := strconv.ParseUint(recordIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"})
|
||
return
|
||
}
|
||
|
||
// 解析请求体
|
||
var req struct {
|
||
Answers map[string]interface{} `json:"answers"` // question_id -> answer (可选)
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询考试记录
|
||
var record models.ExamRecord
|
||
if err := db.Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"})
|
||
return
|
||
}
|
||
|
||
// 检查是否已提交
|
||
if record.Status == "submitted" || record.Status == "graded" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "考试已提交"})
|
||
return
|
||
}
|
||
|
||
// 查询试卷
|
||
var exam models.Exam
|
||
if err := db.Where("id = ?", record.ExamID).First(&exam).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"})
|
||
return
|
||
}
|
||
|
||
// 更新考试记录状态为已提交
|
||
now := time.Now()
|
||
record.Status = "submitted"
|
||
record.SubmitTime = &now
|
||
|
||
// 计算用时(秒)
|
||
if record.StartTime != nil {
|
||
duration := now.Sub(*record.StartTime)
|
||
record.TimeSpent = int(duration.Seconds())
|
||
|
||
// 确保用时不为负数(容错处理)
|
||
if record.TimeSpent < 0 {
|
||
log.Printf("警告: 计算出负的用时,开始时间=%v, 结束时间=%v", *record.StartTime, now)
|
||
record.TimeSpent = 0
|
||
}
|
||
}
|
||
|
||
if err := db.Save(&record).Error; err != nil {
|
||
log.Printf("保存考试记录失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "提交考试失败"})
|
||
return
|
||
}
|
||
|
||
// 立即返回成功响应
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "提交成功,正在阅卷中...",
|
||
"data": gin.H{
|
||
"record_id": record.ID,
|
||
"status": "submitted",
|
||
"time_spent": record.TimeSpent,
|
||
"total_score": exam.TotalScore,
|
||
},
|
||
})
|
||
|
||
// 异步执行阅卷(从 exam_user_answers 表读取答案)
|
||
go gradeExam(uint(recordID), exam.ID, uint(userID.(int64)))
|
||
}
|
||
|
||
// GetExamRecord 获取考试记录详情
|
||
func GetExamRecord(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
recordIDStr := c.Param("record_id")
|
||
recordID, err := strconv.ParseUint(recordIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询考试记录
|
||
var record models.ExamRecord
|
||
if err := db.Where("id = ? AND user_id = ?", recordID, userID).
|
||
Preload("Exam").
|
||
First(&record).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"})
|
||
return
|
||
}
|
||
|
||
// 从 exam_user_answers 表读取所有答案
|
||
var userAnswers []models.ExamUserAnswer
|
||
if err := db.Where("exam_record_id = ?", recordID).Find(&userAnswers).Error; err != nil {
|
||
log.Printf("查询用户答案失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询答案失败"})
|
||
return
|
||
}
|
||
|
||
// 查询所有题目以获取正确答案
|
||
var questionIDs []uint
|
||
if err := json.Unmarshal(record.Exam.QuestionIDs, &questionIDs); err != nil {
|
||
log.Printf("解析题目ID失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "解析题目失败"})
|
||
return
|
||
}
|
||
|
||
var questions []models.PracticeQuestion
|
||
if err := db.Where("id IN ?", questionIDs).Find(&questions).Error; err != nil {
|
||
log.Printf("查询题目失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询题目失败"})
|
||
return
|
||
}
|
||
|
||
// 构建题目映射
|
||
questionMap := make(map[int64]models.PracticeQuestion)
|
||
for _, q := range questions {
|
||
questionMap[q.ID] = q
|
||
}
|
||
|
||
// 构建 ExamAnswer 列表
|
||
examAnswers := make([]models.ExamAnswer, 0, len(userAnswers))
|
||
for _, ua := range userAnswers {
|
||
// 解析用户答案
|
||
var userAnswer interface{}
|
||
if err := json.Unmarshal(ua.Answer, &userAnswer); err != nil {
|
||
log.Printf("解析用户答案失败: %v", err)
|
||
continue
|
||
}
|
||
|
||
// 获取题目并解析正确答案
|
||
question, ok := questionMap[ua.QuestionID]
|
||
if !ok {
|
||
continue
|
||
}
|
||
|
||
var correctAnswerRaw interface{}
|
||
switch question.Type {
|
||
case "fill-in-blank", "multiple-selection", "multiple-choice":
|
||
// 数组类型:需要 JSON 解析(单选题也是数组格式A)
|
||
var arr []string
|
||
if err := json.Unmarshal([]byte(question.AnswerData), &arr); err != nil {
|
||
// 尝试解析为单个字符串(兼容旧数据格式)
|
||
var singleStr string
|
||
if err2 := json.Unmarshal([]byte(question.AnswerData), &singleStr); err2 == nil {
|
||
// 成功解析为字符串,单选题直接使用,其他类型转为数组
|
||
if question.Type == "multiple-choice" {
|
||
correctAnswerRaw = singleStr
|
||
} else {
|
||
correctAnswerRaw = []string{singleStr}
|
||
|
||
}
|
||
} else {
|
||
correctAnswerRaw = "解析失败"
|
||
}
|
||
} else {
|
||
// 单选题只取第一个元素
|
||
if question.Type == "multiple-choice" && len(arr) > 0 {
|
||
correctAnswerRaw = arr[0]
|
||
} else {
|
||
correctAnswerRaw = arr
|
||
}
|
||
}
|
||
default:
|
||
// 字符串类型:直接使用 AnswerData(true-false, short-answer, essay)
|
||
correctAnswerRaw = question.AnswerData
|
||
}
|
||
|
||
// 构建答案对象
|
||
examAnswer := models.ExamAnswer{
|
||
QuestionID: ua.QuestionID,
|
||
Answer: userAnswer,
|
||
CorrectAnswer: correctAnswerRaw,
|
||
IsCorrect: ua.IsCorrect != nil && *ua.IsCorrect,
|
||
Score: ua.Score,
|
||
}
|
||
|
||
// 添加 AI 评分信息
|
||
if len(ua.AIGradingData) > 0 {
|
||
var aiGrading models.AIGrading
|
||
if err := json.Unmarshal(ua.AIGradingData, &aiGrading); err == nil {
|
||
examAnswer.AIGrading = &aiGrading
|
||
}
|
||
}
|
||
|
||
examAnswers = append(examAnswers, examAnswer)
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"record": record,
|
||
"answers": examAnswers,
|
||
},
|
||
})
|
||
}
|
||
|
||
// GetExamRecordList 获取考试记录列表
|
||
func GetExamRecordList(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
examIDStr := c.Query("exam_id")
|
||
|
||
db := database.GetDB()
|
||
query := db.Where("user_id = ?", userID)
|
||
|
||
// 如果指定了试卷ID,只查询该试卷的记录
|
||
if examIDStr != "" {
|
||
examID, err := strconv.ParseUint(examIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的试卷ID"})
|
||
return
|
||
}
|
||
query = query.Where("exam_id = ?", examID)
|
||
}
|
||
|
||
var records []models.ExamRecord
|
||
if err := query.Preload("Exam").
|
||
Order("created_at DESC").
|
||
Find(&records).Error; err != nil {
|
||
log.Printf("查询考试记录失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询考试记录失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": records,
|
||
})
|
||
}
|
||
|
||
// DeleteExam 删除试卷
|
||
func DeleteExam(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
examIDStr := c.Param("id")
|
||
examID, err := strconv.ParseUint(examIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的试卷ID"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询试卷
|
||
var exam models.Exam
|
||
if err := db.Where("id = ? AND user_id = ?", examID, userID).First(&exam).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"})
|
||
return
|
||
}
|
||
|
||
// 软删除
|
||
if err := db.Delete(&exam).Error; err != nil {
|
||
log.Printf("删除试卷失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "删除试卷失败"})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "删除成功",
|
||
})
|
||
}
|
||
|
||
// SaveExamProgressRequest 保存考试进度请求
|
||
type SaveExamProgressRequest struct {
|
||
QuestionID int64 `json:"question_id"` // 题目ID
|
||
Answer interface{} `json:"answer"` // 答案数据
|
||
}
|
||
|
||
// SaveExamProgress 保存单题答案
|
||
func SaveExamProgress(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
recordIDStr := c.Param("record_id")
|
||
recordID, err := strconv.ParseInt(recordIDStr, 10, 64)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"})
|
||
return
|
||
}
|
||
|
||
var req SaveExamProgressRequest
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询考试记录
|
||
var record models.ExamRecord
|
||
if err := db.Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"})
|
||
return
|
||
}
|
||
|
||
// 检查考试状态
|
||
if record.Status != "in_progress" {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "考试已结束,无法保存答案"})
|
||
return
|
||
}
|
||
|
||
// 序列化答案数据
|
||
answerJSON, err := json.Marshal(req.Answer)
|
||
if err != nil {
|
||
log.Printf("序列化答案失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存失败"})
|
||
return
|
||
}
|
||
|
||
now := time.Now()
|
||
|
||
// 查找是否已存在该题的答案
|
||
var userAnswer models.ExamUserAnswer
|
||
result := db.Where("exam_record_id = ? AND question_id = ?", recordID, req.QuestionID).First(&userAnswer)
|
||
|
||
if result.Error == gorm.ErrRecordNotFound {
|
||
// 不存在,创建新记录
|
||
userAnswer = models.ExamUserAnswer{
|
||
ExamRecordID: recordID,
|
||
QuestionID: req.QuestionID,
|
||
UserID: userID.(int64),
|
||
Answer: answerJSON,
|
||
AnsweredAt: &now,
|
||
LastModifiedAt: now,
|
||
}
|
||
if err := db.Create(&userAnswer).Error; err != nil {
|
||
log.Printf("创建答案记录失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "保存答案失败"})
|
||
return
|
||
}
|
||
} else if result.Error != nil {
|
||
log.Printf("查询答案记录失败: %v", result.Error)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询答案失败"})
|
||
return
|
||
} else {
|
||
// 已存在,更新答案
|
||
updates := map[string]interface{}{
|
||
"answer": answerJSON,
|
||
"last_modified_at": now,
|
||
}
|
||
if err := db.Model(&userAnswer).Updates(updates).Error; err != nil {
|
||
log.Printf("更新答案记录失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "更新答案失败"})
|
||
return
|
||
}
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"message": "保存成功",
|
||
})
|
||
}
|
||
|
||
// GetExamUserAnswers 获取用户在考试中的所有答案
|
||
func GetExamUserAnswers(c *gin.Context) {
|
||
userID, exists := c.Get("user_id")
|
||
if !exists {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "message": "未登录"})
|
||
return
|
||
}
|
||
|
||
recordIDStr := c.Param("record_id")
|
||
recordID, err := strconv.ParseUint(recordIDStr, 10, 32)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的考试记录ID"})
|
||
return
|
||
}
|
||
|
||
db := database.GetDB()
|
||
|
||
// 查询考试记录,确保用户有权限
|
||
var record models.ExamRecord
|
||
if err := db.Where("id = ? AND user_id = ?", recordID, userID).First(&record).Error; err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"})
|
||
return
|
||
}
|
||
|
||
// 查询所有已保存的答案
|
||
var userAnswers []models.ExamUserAnswer
|
||
if err := db.Where("exam_record_id = ?", recordID).Find(&userAnswers).Error; err != nil {
|
||
log.Printf("查询用户答案失败: %v", err)
|
||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "查询答案失败"})
|
||
return
|
||
}
|
||
|
||
// 转换为前端需要的格式: { question_id: answer }
|
||
answers := make(map[string]interface{})
|
||
for _, ua := range userAnswers {
|
||
var answer interface{}
|
||
if err := json.Unmarshal(ua.Answer, &answer); err != nil {
|
||
log.Printf("解析答案失败: %v", err)
|
||
continue
|
||
}
|
||
// 使用 q_<question_id> 格式作为key,与前端表单字段名保持一致
|
||
fieldName := fmt.Sprintf("q_%d", ua.QuestionID)
|
||
answers[fieldName] = answer
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": answers,
|
||
})
|
||
}
|