diff --git a/cmd/cleanup/main.go b/cmd/cleanup/main.go
new file mode 100644
index 0000000..df0593f
--- /dev/null
+++ b/cmd/cleanup/main.go
@@ -0,0 +1,38 @@
+package main
+
+import (
+ "ankao/pkg/config"
+ "fmt"
+ "log"
+
+ "gorm.io/driver/postgres"
+ "gorm.io/gorm"
+ "gorm.io/gorm/logger"
+)
+
+func main() {
+ // 直接连接数据库,不使用 InitDB
+ cfg := config.GetDatabaseConfig()
+ dsn := cfg.GetDSN()
+
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
+ Logger: logger.Default.LogMode(logger.Info),
+ })
+ if err != nil {
+ log.Fatal("数据库连接失败:", err)
+ }
+
+ log.Println("开始清理 exam_shares 表...")
+
+ // 删除 exam_shares 表(如果存在)
+ if err := db.Exec("DROP TABLE IF EXISTS exam_shares CASCADE").Error; err != nil {
+ log.Fatal("删除 exam_shares 表失败:", err)
+ }
+
+ log.Println("✓ 已删除 exam_shares 表")
+ log.Println("\n清理完成!现在可以重新运行主程序。")
+ fmt.Println("\n执行步骤:")
+ fmt.Println("1. go run main.go # 这会自动创建正确的表结构")
+ fmt.Println("2. 如果有旧的分享数据需要迁移,运行:")
+ fmt.Println(" go run cmd/migrate_exam_shares.go")
+}
diff --git a/cmd/migrate_exam_shares.go b/cmd/migrate_exam_shares.go
new file mode 100644
index 0000000..6e13c99
--- /dev/null
+++ b/cmd/migrate_exam_shares.go
@@ -0,0 +1,189 @@
+package main
+
+import (
+ "ankao/internal/database"
+ "ankao/internal/models"
+ "fmt"
+ "log"
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// OldExam 旧的试卷模型(用于迁移)
+type OldExam struct {
+ ID uint `gorm:"primaryKey"`
+ CreatedAt time.Time
+ UpdatedAt time.Time
+ DeletedAt gorm.DeletedAt `gorm:"index"`
+ UserID uint `gorm:"not null;index"`
+ Title string `gorm:"type:varchar(200);default:''"`
+ TotalScore int `gorm:"not null;default:100"`
+ Duration int `gorm:"not null;default:60"`
+ PassScore int `gorm:"not null;default:60"`
+ QuestionIDs []byte `gorm:"type:json"`
+ Status string `gorm:"type:varchar(20);not null;default:'active'"`
+ IsShared bool `gorm:"default:false"`
+ SharedByID *uint `gorm:"index"`
+}
+
+func (OldExam) TableName() string {
+ return "exams"
+}
+
+func main() {
+ // 初始化数据库
+ if err := database.InitDB(); err != nil {
+ log.Fatal("数据库初始化失败:", err)
+ }
+ db := database.GetDB()
+
+ log.Println("开始迁移试卷分享数据...")
+
+ // 1. 查找所有被分享的试卷副本
+ var sharedExams []OldExam
+ if err := db.Where("is_shared = ? AND shared_by_id IS NOT NULL", true).
+ Find(&sharedExams).Error; err != nil {
+ log.Fatal("查询分享试卷失败:", err)
+ }
+
+ log.Printf("找到 %d 份分享试卷副本", len(sharedExams))
+
+ if len(sharedExams) == 0 {
+ log.Println("没有需要迁移的数据,退出。")
+ return
+ }
+
+ // 2. 按 shared_by_id + question_ids 分组
+ type ShareGroup struct {
+ SharedByID uint
+ QuestionIDs string
+ Exams []OldExam
+ }
+
+ groupMap := make(map[string]*ShareGroup)
+ for _, exam := range sharedExams {
+ if exam.SharedByID == nil {
+ continue
+ }
+ key := fmt.Sprintf("%d_%s", *exam.SharedByID, string(exam.QuestionIDs))
+ if group, exists := groupMap[key]; exists {
+ group.Exams = append(group.Exams, exam)
+ } else {
+ groupMap[key] = &ShareGroup{
+ SharedByID: *exam.SharedByID,
+ QuestionIDs: string(exam.QuestionIDs),
+ Exams: []OldExam{exam},
+ }
+ }
+ }
+
+ log.Printf("分组后共 %d 组", len(groupMap))
+
+ successCount := 0
+ failCount := 0
+
+ // 3. 处理每个分组
+ for _, group := range groupMap {
+ // 查找原始试卷
+ var originalExam OldExam
+ if err := db.Where("user_id = ? AND question_ids = ? AND is_shared = ?",
+ group.SharedByID, group.QuestionIDs, false).
+ First(&originalExam).Error; err != nil {
+ log.Printf("未找到原始试卷: shared_by_id=%d, 跳过该组", group.SharedByID)
+ failCount += len(group.Exams)
+ continue
+ }
+
+ // 开始事务
+ tx := db.Begin()
+
+ migrationSuccess := true
+
+ // 4. 为每个分享副本创建关联记录
+ for _, sharedExam := range group.Exams {
+ // 创建分享记录
+ share := models.ExamShare{
+ ExamID: originalExam.ID,
+ SharedByID: group.SharedByID,
+ SharedToID: sharedExam.UserID,
+ SharedAt: sharedExam.CreatedAt,
+ }
+
+ if err := tx.Create(&share).Error; err != nil {
+ log.Printf("创建分享记录失败: %v", err)
+ tx.Rollback()
+ failCount += len(group.Exams)
+ migrationSuccess = false
+ break
+ }
+
+ // 5. 更新考试记录,将 exam_id 指向原始试卷
+ if err := tx.Model(&models.ExamRecord{}).
+ Where("exam_id = ?", sharedExam.ID).
+ Update("exam_id", originalExam.ID).Error; err != nil {
+ log.Printf("更新考试记录失败: %v", err)
+ tx.Rollback()
+ failCount += len(group.Exams)
+ migrationSuccess = false
+ break
+ }
+
+ // 6. 软删除分享副本
+ if err := tx.Delete(&sharedExam).Error; err != nil {
+ log.Printf("删除分享副本失败: %v", err)
+ tx.Rollback()
+ failCount += len(group.Exams)
+ migrationSuccess = false
+ break
+ }
+
+ if migrationSuccess {
+ successCount++
+ }
+ }
+
+ // 提交事务
+ if migrationSuccess {
+ if err := tx.Commit().Error; err != nil {
+ log.Printf("提交事务失败: %v", err)
+ failCount += len(group.Exams)
+ }
+ }
+ }
+
+ log.Printf("迁移完成: 成功 %d, 失败 %d", successCount, failCount)
+
+ // 7. 验证迁移结果
+ log.Println("\n开始验证迁移结果...")
+
+ // 统计 exam_shares 表记录数
+ var shareCount int64
+ db.Model(&models.ExamShare{}).Count(&shareCount)
+ log.Printf("exam_shares 表记录数: %d", shareCount)
+
+ // 统计剩余的分享副本数(应该为0)
+ var remainingSharedExams int64
+ db.Model(&OldExam{}).Where("is_shared = ?", true).Count(&remainingSharedExams)
+ log.Printf("剩余分享副本数: %d (应该为0)", remainingSharedExams)
+
+ // 检查是否有孤立的考试记录
+ var orphanRecords int64
+ db.Raw(`
+ SELECT COUNT(*) FROM exam_records er
+ WHERE NOT EXISTS (
+ SELECT 1 FROM exams e WHERE e.id = er.exam_id AND e.deleted_at IS NULL
+ )
+ `).Scan(&orphanRecords)
+ log.Printf("孤立的考试记录数: %d (应该为0)", orphanRecords)
+
+ if remainingSharedExams == 0 && orphanRecords == 0 {
+ log.Println("\n✓ 迁移验证通过!")
+ } else {
+ log.Println("\n✗ 迁移验证失败,请检查数据!")
+ }
+
+ log.Println("\n注意: 如果验证通过,可以考虑在未来某个时间点执行以下SQL移除旧字段:")
+ log.Println(" ALTER TABLE exams DROP COLUMN is_shared;")
+ log.Println(" ALTER TABLE exams DROP COLUMN shared_by_id;")
+}
diff --git a/internal/database/database.go b/internal/database/database.go
index e61ca6d..13166a6 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -38,6 +38,7 @@ func InitDB() error {
&models.WrongQuestionHistory{}, // 错题历史表
&models.UserAnswerRecord{}, // 用户答题记录表
&models.Exam{}, // 考试表(试卷)
+ &models.ExamShare{}, // 试卷分享关联表
&models.ExamRecord{}, // 考试记录表
&models.ExamUserAnswer{}, // 用户答案表
)
diff --git a/internal/handlers/exam_handler.go b/internal/handlers/exam_handler.go
index 7855e57..faea29d 100644
--- a/internal/handlers/exam_handler.go
+++ b/internal/handlers/exam_handler.go
@@ -152,20 +152,15 @@ func GetExamList(c *gin.Context) {
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 {
+ // 获取用户可访问的试卷(拥有的 + 被分享的)
+ exams, err := models.GetAccessibleExams(userID.(int64), db)
+ if 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"`
@@ -174,76 +169,74 @@ func GetExamList(c *gin.Context) {
HasInProgressExam bool `json:"has_in_progress_exam"` // 是否有进行中的考试
InProgressRecordID uint `json:"in_progress_record_id,omitempty"` // 进行中的考试记录ID
ParticipantCount int `json:"participant_count"` // 共享试卷的参与人数(所有用户)
+ IsShared bool `json:"is_shared"` // 是否为分享的试卷
+ SharedBy *struct {
+ ID int64 `json:"id"`
+ Username string `json:"username"`
+ Nickname string `json:"nickname"`
+ } `json:"shared_by,omitempty"`
}
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)
+ if err := json.Unmarshal(exam.QuestionIDs, &questionIDs); err != nil {
+ continue
}
+
+ stats := ExamWithStats{
+ Exam: exam,
+ QuestionCount: len(questionIDs),
+ }
+
+ // 检查是否为分享试卷
+ if exam.UserID != uint(userID.(int64)) {
+ stats.IsShared = true
+ // 查询分享人信息
+ var share models.ExamShare
+ if err := db.Where("exam_id = ? AND shared_to_id = ?", exam.ID, userID.(int64)).
+ Preload("SharedBy").First(&share).Error; err == nil && share.SharedBy != nil {
+ stats.SharedBy = &struct {
+ ID int64 `json:"id"`
+ Username string `json:"username"`
+ Nickname string `json:"nickname"`
+ }{
+ ID: share.SharedBy.ID,
+ Username: share.SharedBy.Username,
+ Nickname: share.SharedBy.Nickname,
+ }
+ }
+ }
+
+ // 当前用户的统计
+ 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
+ }
+
+ 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
+ }
+
+ // 参与人数统计(简化:直接统计该试卷的不同用户)
+ var participantCount int64
+ db.Model(&models.ExamRecord{}).
+ Where("exam_id = ? AND status = ?", exam.ID, "graded").
+ Distinct("user_id").
+ Count(&participantCount)
+ stats.ParticipantCount = int(participantCount)
+
+ result = append(result, stats)
}
c.JSON(http.StatusOK, gin.H{
@@ -515,15 +508,32 @@ func GetExamRecord(c *gin.Context) {
db := database.GetDB()
- // 查询考试记录
+ // 查询考试记录(不限制用户,因为可能是查看共享试卷的其他用户记录)
var record models.ExamRecord
- if err := db.Where("id = ? AND user_id = ?", recordID, userID).
+ if err := db.Where("id = ?", recordID).
Preload("Exam").
First(&record).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "考试记录不存在"})
return
}
+ // 检查权限:只有以下情况可以查看
+ // 1. 记录属于当前用户
+ // 2. 当前用户有权限访问该试卷
+ if record.UserID != uint(userID.(int64)) {
+ // 不是自己的记录,检查是否有权限访问该试卷
+ 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
+ }
+
+ if !exam.IsAccessibleBy(userID.(int64), db) {
+ c.JSON(http.StatusForbidden, 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 {
@@ -639,10 +649,9 @@ func GetExamRecordList(c *gin.Context) {
}
examIDStr := c.Query("exam_id")
-
db := database.GetDB()
- // 如果指定了试卷ID,需要判断是否为共享试卷
+ // 如果指定了试卷ID
if examIDStr != "" {
examID, err := strconv.ParseUint(examIDStr, 10, 32)
if err != nil {
@@ -650,42 +659,22 @@ func GetExamRecordList(c *gin.Context) {
return
}
- // 查询试卷信息
+ // 查询试卷并检查权限
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": "试卷不存在或无权限"})
+ if err := db.Where("id = ?", examID).First(&exam).Error; err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "试卷不存在"})
return
}
- // 获取所有相关试卷ID(包括原始试卷和所有分享副本)
- var relatedExamIDs []uint
- relatedExamIDs = append(relatedExamIDs, uint(examID))
-
- // 如果这是被分享的试卷,找到原始试卷
- 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)
- }
+ // 检查用户是否有权限访问该试卷
+ if !exam.IsAccessibleBy(userID.(int64), db) {
+ c.JSON(http.StatusForbidden, gin.H{"success": false, "message": "无权限访问"})
+ return
}
- // 查找所有基于该试卷的分享副本
- 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 records []models.ExamRecord
- if err := db.Where("exam_id IN ? AND status = ?", relatedExamIDs, "graded").
+ if err := db.Where("exam_id = ? AND status = ?", examID, "graded").
Preload("Exam").
Preload("User", func(db *gorm.DB) *gorm.DB {
return db.Select("id", "username", "nickname", "avatar")
@@ -704,7 +693,7 @@ func GetExamRecordList(c *gin.Context) {
return
}
- // 如果没有指定试卷ID,只返回当前用户的记录
+ // 没有指定试卷ID,返回当前用户的所有记录
var records []models.ExamRecord
if err := db.Where("user_id = ?", userID).
Preload("Exam").
@@ -955,9 +944,9 @@ func ShareExam(c *gin.Context) {
db := database.GetDB()
- // 查询原始试卷
- var originalExam models.Exam
- if err := db.Where("id = ? AND user_id = ?", examID, userID).First(&originalExam).Error; err != nil {
+ // 查询原始试卷,确认用户有权限分享
+ 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
}
@@ -970,19 +959,19 @@ func ShareExam(c *gin.Context) {
}
}()
- sharedByID := uint(userID.(int64))
+ sharedByID := userID.(int64)
sharedCount := 0
+ now := time.Now()
- // 为每个用户创建分享副本
+ // 为每个用户创建分享记录
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
+ // 检查是否已分享
+ var existingShare models.ExamShare
+ err := tx.Where("exam_id = ? AND shared_to_id = ?", uint(examID), int64(targetUserID)).
+ First(&existingShare).Error
if err == nil {
- // 已存在,跳过
- continue
+ continue // 已存在,跳过
} else if err != gorm.ErrRecordNotFound {
tx.Rollback()
log.Printf("检查分享记录失败: %v", err)
@@ -990,26 +979,20 @@ func ShareExam(c *gin.Context) {
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,
+ // 创建分享记录
+ share := models.ExamShare{
+ ExamID: uint(examID),
+ SharedByID: sharedByID,
+ SharedToID: int64(targetUserID),
+ SharedAt: now,
}
- if err := tx.Create(&sharedExam).Error; err != nil {
+ if err := tx.Create(&share).Error; err != nil {
tx.Rollback()
- log.Printf("创建分享试卷失败: %v", err)
+ log.Printf("创建分享记录失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "分享失败"})
return
}
-
sharedCount++
}
diff --git a/internal/models/exam.go b/internal/models/exam.go
index 7b88fe1..0c5b938 100644
--- a/internal/models/exam.go
+++ b/internal/models/exam.go
@@ -20,11 +20,37 @@ type Exam struct {
PassScore int `gorm:"not null;default:60" json:"pass_score"` // 及格分数
QuestionIDs datatypes.JSON `gorm:"type:json" json:"question_ids"` // 题目ID列表 (JSON数组)
Status string `gorm:"type:varchar(20);not null;default:'active'" json:"status"` // 状态: active, archived
- IsShared bool `gorm:"default:false" json:"is_shared"` // 是否为分享的试卷
- SharedByID *uint `gorm:"index" json:"shared_by_id,omitempty"` // 分享人ID (如果是分享的试卷)
- // 关联
- SharedBy *User `gorm:"foreignKey:SharedByID" json:"shared_by,omitempty"` // 分享人信息
+ // 关联关系
+ Shares []ExamShare `gorm:"foreignKey:ExamID" json:"-"` // 该试卷的分享记录(作为被分享试卷)
+ SharedToMe []ExamShare `gorm:"foreignKey:SharedToID" json:"-"` // 分享给我的记录(作为接收者)
+}
+
+// IsAccessibleBy 检查用户是否有权限访问试卷
+func (e *Exam) IsAccessibleBy(userID int64, db *gorm.DB) bool {
+ // 用户是试卷创建者
+ if int64(e.UserID) == userID {
+ return true
+ }
+ // 检查是否被分享给该用户
+ var count int64
+ db.Model(&ExamShare{}).Where("exam_id = ? AND shared_to_id = ?", e.ID, userID).Count(&count)
+ return count > 0
+}
+
+// GetAccessibleExams 获取用户可访问的所有试卷(拥有的+被分享的)
+func GetAccessibleExams(userID int64, db *gorm.DB) ([]Exam, error) {
+ var exams []Exam
+
+ // 子查询:被分享的试卷ID
+ subQuery := db.Model(&ExamShare{}).Select("exam_id").Where("shared_to_id = ?", userID)
+
+ // 查询:用户拥有的 OR 被分享的试卷
+ err := db.Where("user_id = ? OR id IN (?)", uint(userID), subQuery).
+ Order("created_at DESC").
+ Find(&exams).Error
+
+ return exams, err
}
// ExamRecord 考试记录
diff --git a/internal/models/exam_share.go b/internal/models/exam_share.go
new file mode 100644
index 0000000..814b3b8
--- /dev/null
+++ b/internal/models/exam_share.go
@@ -0,0 +1,30 @@
+package models
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+// ExamShare 试卷分享关联表
+type ExamShare struct {
+ ID uint `gorm:"primaryKey" json:"id"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+
+ ExamID uint `gorm:"not null;uniqueIndex:uk_exam_shared_to" json:"exam_id"`
+ SharedByID int64 `gorm:"not null;index" json:"shared_by_id"`
+ SharedToID int64 `gorm:"not null;uniqueIndex:uk_exam_shared_to" json:"shared_to_id"`
+ SharedAt time.Time `gorm:"not null;default:CURRENT_TIMESTAMP" json:"shared_at"`
+
+ // 关联关系
+ Exam *Exam `gorm:"foreignKey:ExamID" json:"exam,omitempty"`
+ SharedBy *User `gorm:"foreignKey:SharedByID;references:ID" json:"shared_by,omitempty"`
+ SharedTo *User `gorm:"foreignKey:SharedToID;references:ID" json:"shared_to,omitempty"`
+}
+
+// TableName 指定表名
+func (ExamShare) TableName() string {
+ return "exam_shares"
+}
diff --git a/web/src/pages/ExamManagement.tsx b/web/src/pages/ExamManagement.tsx
index 8e0022f..24a56e6 100644
--- a/web/src/pages/ExamManagement.tsx
+++ b/web/src/pages/ExamManagement.tsx
@@ -465,7 +465,13 @@ const ExamManagement: React.FC = () => {
{record.status === 'in_progress' ? (
-
) : (
-
+
{record.score} 分
)}
@@ -504,18 +510,6 @@ const ExamManagement: React.FC = () => {
查看详情
)}
- {record.status === 'graded' && (
- }
- onClick={() => {
- setRecordsDrawerVisible(false)
- navigate(`/exam/result/${record.id}`)
- }}
- >
- 查看答案
-
- )}
)}
diff --git a/web/src/types/exam.ts b/web/src/types/exam.ts
index ddaf1e6..f37c77e 100644
--- a/web/src/types/exam.ts
+++ b/web/src/types/exam.ts
@@ -105,9 +105,9 @@ export type ExamListResponse = Array<{
has_in_progress_exam: boolean
in_progress_record_id?: number
participant_count: number // 共享试卷的参与人数
- is_shared: boolean // 是否为分享的试卷
- shared_by_id?: number // 分享人ID
- shared_by?: { // 分享人信息
+ is_shared: boolean // 是否为被分享的试卷(原:是否为分享副本)
+ shared_by_id?: number // 分享人ID(已废弃,使用 shared_by)
+ shared_by?: { // 分享人信息(原:副本创建者)
id: number
username: string
nickname: string