本次提交重构了试卷分享功能,从"副本模式"改为"关联表模式",并修复了关键的类型不匹配问题。 ## 主要更新 ### 后端架构重构 - 新增 ExamShare 关联表模型,替代原有的试卷副本方式 - 修复 User.ID (int64) 与 ExamShare 外键的类型不匹配问题 - 更新所有相关 API 以使用新的关联表架构 - 添加 IsAccessibleBy 和 GetAccessibleExams 权限检查方法 ### 类型系统修复 - ExamShare.SharedByID/SharedToID: uint → int64 - IsAccessibleBy/GetAccessibleExams 参数: uint → int64 - 修复所有涉及用户ID类型转换的代码 ### 新增工具 - cmd/migrate_exam_shares.go: 数据迁移脚本(旧数据迁移) - cmd/cleanup/main.go: 数据库表清理工具 ### API 更新 - ShareExam: 创建分享关联记录而非复制试卷 - GetExamList: 返回分享人信息和参与人数统计 - GetExamRecord: 支持查看共享试卷的其他用户记录 - GetExamRecordList: 按试卷ID查询所有用户的考试记录 ### 前端更新 - 更新 TypeScript 类型定义以匹配新的 API 响应 - 添加分享人标签显示("来自 XXX") - 考试记录列表显示所有参与者信息 ## 技术细节 - 使用 GORM 外键关联和 Preload 优化查询 - 添加唯一索引防止重复分享 - 事务保护数据一致性 - 软删除支持数据恢复 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
162 lines
7.8 KiB
Go
162 lines
7.8 KiB
Go
package models
|
||
|
||
import (
|
||
"time"
|
||
|
||
"gorm.io/datatypes"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// Exam 试卷模型
|
||
type Exam 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:"-"`
|
||
UserID uint `gorm:"not null;index" json:"user_id"` // 创建者ID
|
||
Title string `gorm:"type:varchar(200);default:''" json:"title"` // 试卷标题
|
||
TotalScore int `gorm:"not null;default:100" json:"total_score"` // 总分
|
||
Duration int `gorm:"not null;default:60" json:"duration"` // 考试时长(分钟)
|
||
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
|
||
|
||
// 关联关系
|
||
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 考试记录
|
||
type ExamRecord 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;index" json:"exam_id"` // 试卷ID
|
||
UserID uint `gorm:"not null;index" json:"user_id"` // 考生ID
|
||
StartTime *time.Time `json:"start_time"` // 开始时间
|
||
SubmitTime *time.Time `json:"submit_time"` // 提交时间
|
||
TimeSpent int `json:"time_spent"` // 实际用时(秒)
|
||
Score float64 `gorm:"type:decimal(5,2)" json:"score"` // 得分
|
||
TotalScore int `json:"total_score"` // 总分
|
||
Status string `gorm:"type:varchar(20);not null;default:'in_progress'" json:"status"` // 状态: in_progress, submitted, graded
|
||
IsPassed bool `json:"is_passed"` // 是否通过
|
||
|
||
// 关联
|
||
Exam *Exam `gorm:"foreignKey:ExamID" json:"exam,omitempty"`
|
||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||
}
|
||
|
||
// ExamUserAnswer 用户答案表(记录每道题的答案)
|
||
type ExamUserAnswer struct {
|
||
ID int64 `gorm:"primaryKey" json:"id"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||
ExamRecordID int64 `gorm:"not null;index:idx_record_question" json:"exam_record_id"` // 考试记录ID
|
||
QuestionID int64 `gorm:"not null;index:idx_record_question" json:"question_id"` // 题目ID
|
||
UserID int64 `gorm:"not null;index" json:"user_id"` // 用户ID
|
||
Answer datatypes.JSON `gorm:"type:json" json:"answer"` // 用户答案 (JSON格式,支持各种题型)
|
||
IsCorrect *bool `json:"is_correct,omitempty"` // 是否正确(提交后评分)
|
||
Score float64 `gorm:"type:decimal(5,2);default:0" json:"score"` // 得分
|
||
AIGradingData datatypes.JSON `gorm:"type:json" json:"ai_grading_data,omitempty"` // AI评分数据
|
||
AnsweredAt *time.Time `json:"answered_at"` // 答题时间
|
||
LastModifiedAt time.Time `json:"last_modified_at"` // 最后修改时间
|
||
|
||
// 关联
|
||
ExamRecord *ExamRecord `gorm:"foreignKey:ExamRecordID" json:"-"`
|
||
Question *PracticeQuestion `gorm:"foreignKey:QuestionID" json:"-"`
|
||
}
|
||
|
||
// ExamConfig 试卷配置结构
|
||
type ExamConfig struct {
|
||
QuestionTypes []QuestionTypeConfig `json:"question_types"` // 题型配置
|
||
Categories []string `json:"categories"` // 题目分类筛选
|
||
Difficulty []string `json:"difficulty"` // 难度筛选
|
||
RandomOrder bool `json:"random_order"` // 是否随机顺序
|
||
}
|
||
|
||
// QuestionTypeConfig 题型配置
|
||
type QuestionTypeConfig struct {
|
||
Type string `json:"type"` // 题目类型
|
||
Count int `json:"count"` // 题目数量
|
||
Score float64 `json:"score"` // 每题分数
|
||
}
|
||
|
||
// ExamAnswer 考试答案结构
|
||
type ExamAnswer struct {
|
||
QuestionID int64 `json:"question_id"`
|
||
Answer interface{} `json:"answer"` // 用户答案
|
||
CorrectAnswer interface{} `json:"correct_answer"` // 正确答案
|
||
IsCorrect bool `json:"is_correct"`
|
||
Score float64 `json:"score"`
|
||
AIGrading *AIGrading `json:"ai_grading,omitempty"`
|
||
}
|
||
|
||
// ExamQuestionConfig 考试题目配置
|
||
type ExamQuestionConfig struct {
|
||
FillInBlank int `json:"fill_in_blank"` // 填空题数量
|
||
TrueFalse int `json:"true_false"` // 判断题数量
|
||
MultipleChoice int `json:"multiple_choice"` // 单选题数量
|
||
MultipleSelection int `json:"multiple_selection"` // 多选题数量
|
||
ShortAnswer int `json:"short_answer"` // 简答题数量
|
||
OrdinaryEssay int `json:"ordinary_essay"` // 普通涉密人员论述题数量
|
||
ManagementEssay int `json:"management_essay"` // 保密管理人员论述题数量
|
||
}
|
||
|
||
// DefaultExamConfig 默认考试配置
|
||
var DefaultExamConfig = ExamQuestionConfig{
|
||
FillInBlank: 10, // 填空题10道
|
||
TrueFalse: 10, // 判断题10道
|
||
MultipleChoice: 10, // 单选题10道
|
||
MultipleSelection: 10, // 多选题10道
|
||
ShortAnswer: 2, // 简答题2道
|
||
OrdinaryEssay: 1, // 普通论述题1道
|
||
ManagementEssay: 1, // 管理论述题1道
|
||
}
|
||
|
||
// ExamScoreConfig 考试分值配置
|
||
type ExamScoreConfig struct {
|
||
FillInBlank float64 `json:"fill_in_blank"` // 填空题分值
|
||
TrueFalse float64 `json:"true_false"` // 判断题分值
|
||
MultipleChoice float64 `json:"multiple_choice"` // 单选题分值
|
||
MultipleSelection float64 `json:"multiple_selection"` // 多选题分值
|
||
Essay float64 `json:"essay"` // 论述题分值
|
||
}
|
||
|
||
// DefaultScoreConfig 默认分值配置
|
||
var DefaultScoreConfig = ExamScoreConfig{
|
||
FillInBlank: 2.0, // 填空题每题2分 (共20分)
|
||
TrueFalse: 2.0, // 判断题每题2分 (共20分)
|
||
MultipleChoice: 1.0, // 单选题每题1分 (共10分)
|
||
MultipleSelection: 2.5, // 多选题每题2.5分 (共25分)
|
||
Essay: 25.0, // 论述题25分
|
||
}
|