AnCao/internal/handlers/exam_handler.go
yanlongqi 03f3e14f6e 添加共享试卷参与人数统计功能
功能说明:
- 后端:GetExamList 接口新增 participant_count 字段
- 前端:试卷卡片显示"X 人参与"标签
- 统计逻辑:计算原始试卷和所有分享副本的不同用户数

实现细节:
- 自动识别原始试卷和分享副本
- 统计所有已完成考试的不同用户
- 使用团队图标(TeamOutlined)展示参与人数

修改文件:
- internal/handlers/exam_handler.go: 添加参与人数统计逻辑
- web/src/types/exam.ts: 更新类型定义
- web/src/pages/ExamManagement.tsx: 显示参与人数标签

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 21:27:03 +08:00

978 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
// 使用固定的题型配置总分100分
questionTypes := []models.QuestionTypeConfig{
{Type: "fill-in-blank", Count: 20, Score: 2.0}, // 40分
{Type: "true-false", Count: 10, Score: 1.0}, // 10分
{Type: "multiple-choice", Count: 10, Score: 1.0}, // 10分
{Type: "multiple-selection", Count: 10, Score: 2.0}, // 20分
{Type: "short-answer", Count: 1, Score: 10.0}, // 10分
{Type: "ordinary-essay", Count: 1, Score: 10.0}, // 10分普通涉密人员论述题
{Type: "management-essay", Count: 1, Score: 10.0}, // 10分保密管理人员论述题
}
// 按题型配置随机抽取题目
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").
Preload("SharedBy", func(db *gorm.DB) *gorm.DB {
return db.Select("id", "username", "nickname")
}).
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
ParticipantCount int `json:"participant_count"` // 共享试卷的参与人数(所有用户)
}
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
}
// 计算共享试卷的参与人数
// 获取所有相关试卷ID包括原始试卷和所有分享副本
var relatedExamIDs []uint
relatedExamIDs = append(relatedExamIDs, exam.ID)
// 如果这是被分享的试卷,找到原始试卷
if exam.IsShared && exam.SharedByID != nil {
var originalExam models.Exam
if err := db.Where("user_id = ? AND question_ids = ? AND deleted_at IS NULL",
*exam.SharedByID, exam.QuestionIDs).First(&originalExam).Error; err == nil {
relatedExamIDs = append(relatedExamIDs, originalExam.ID)
}
}
// 查找所有基于该试卷的分享副本
var sharedExams []models.Exam
sharedByID := exam.UserID
if exam.IsShared && exam.SharedByID != nil {
sharedByID = *exam.SharedByID
}
if err := db.Where("shared_by_id = ? AND question_ids = ? AND deleted_at IS NULL",
sharedByID, exam.QuestionIDs).Find(&sharedExams).Error; err == nil {
for _, se := range sharedExams {
relatedExamIDs = append(relatedExamIDs, se.ID)
}
}
// 统计所有相关试卷的已完成考试的不同用户数
var participantCount int64
db.Model(&models.ExamRecord{}).
Where("exam_id IN ? AND status = ?", relatedExamIDs, "graded").
Distinct("user_id").
Count(&participantCount)
stats.ParticipantCount = int(participantCount)
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
}
// 检查是否包含论述题,如果没有则添加两种论述题
hasOrdinaryEssay := false
hasManagementEssay := false
for _, q := range questions {
if q.Type == "ordinary-essay" {
hasOrdinaryEssay = true
}
if q.Type == "management-essay" {
hasManagementEssay = true
}
}
// 如果缺少论述题,则补充
var additionalQuestions []models.PracticeQuestion
if !hasOrdinaryEssay {
var ordinaryEssay models.PracticeQuestion
if err := db.Where("type = ?", "ordinary-essay").First(&ordinaryEssay).Error; err == nil {
additionalQuestions = append(additionalQuestions, ordinaryEssay)
}
}
if !hasManagementEssay {
var managementEssay models.PracticeQuestion
if err := db.Where("type = ?", "management-essay").First(&managementEssay).Error; err == nil {
additionalQuestions = append(additionalQuestions, managementEssay)
}
}
// 将补充的题目添加到题目映射中
for _, q := range additionalQuestions {
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)
}
}
// 添加补充的论述题到结果中
for _, q := range additionalQuestions {
dto := convertToDTO(q)
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:
// 字符串类型:直接使用 AnswerDatatrue-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,
})
}
// GetShareableUsers 获取可分享的用户列表(排除当前用户)
func GetShareableUsers(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 users []models.User
if err := db.Where("id != ?", userID).Select("id", "username", "nickname", "avatar").Find(&users).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": users,
})
}
// ShareExamRequest 分享试卷请求
type ShareExamRequest struct {
UserIDs []uint `json:"user_ids" binding:"required,min=1"` // 分享给哪些用户
}
// ShareExam 分享试卷
func ShareExam(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
}
var req ShareExamRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的请求数据: " + err.Error()})
return
}
db := database.GetDB()
// 查询原始试卷
var originalExam models.Exam
if err := db.Where("id = ? AND user_id = ?", examID, userID).First(&originalExam).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在或无权限"})
return
}
// 开始事务
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
sharedByID := uint(userID.(int64))
sharedCount := 0
// 为每个用户创建分享副本
for _, targetUserID := range req.UserIDs {
// 检查是否已经分享给该用户
var existingExam models.Exam
err := tx.Where("user_id = ? AND shared_by_id = ? AND question_ids = ? AND deleted_at IS NULL",
targetUserID, sharedByID, originalExam.QuestionIDs).First(&existingExam).Error
if err == nil {
// 已存在,跳过
continue
} else if err != gorm.ErrRecordNotFound {
tx.Rollback()
log.Printf("检查分享记录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "分享失败"})
return
}
// 创建试卷副本
sharedExam := models.Exam{
UserID: targetUserID,
Title: originalExam.Title,
TotalScore: originalExam.TotalScore,
Duration: originalExam.Duration,
PassScore: originalExam.PassScore,
QuestionIDs: originalExam.QuestionIDs,
Status: "active",
IsShared: true,
SharedByID: &sharedByID,
}
if err := tx.Create(&sharedExam).Error; err != nil {
tx.Rollback()
log.Printf("创建分享试卷失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "分享失败"})
return
}
sharedCount++
}
// 提交事务
if err := tx.Commit().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": fmt.Sprintf("成功分享给 %d 个用户", sharedCount),
"data": gin.H{
"shared_count": sharedCount,
},
})
}