添加共享试卷参与人数统计功能

功能说明:
- 后端: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>
This commit is contained in:
燕陇琪 2025-12-01 21:27:03 +08:00
parent 3704d52a26
commit 03f3e14f6e
3 changed files with 59 additions and 5 deletions

View File

@ -169,10 +169,11 @@ func GetExamList(c *gin.Context) {
type ExamWithStats struct {
models.Exam
QuestionCount int `json:"question_count"`
AttemptCount int `json:"attempt_count"` // 考试次数
BestScore float64 `json:"best_score"` // 最高分
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))
@ -184,12 +185,12 @@ func GetExamList(c *gin.Context) {
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").
@ -206,6 +207,41 @@ func GetExamList(c *gin.Context) {
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)
}
}

View File

@ -30,7 +30,8 @@ import {
PrinterOutlined,
ArrowLeftOutlined,
ShareAltOutlined,
UserOutlined
UserOutlined,
TeamOutlined
} from '@ant-design/icons'
import * as examApi from '../api/exam'
import styles from './ExamManagement.module.less'
@ -46,6 +47,7 @@ interface ExamListItem {
best_score: number
has_in_progress_exam: boolean
in_progress_record_id?: number
participant_count: number // 共享试卷的参与人数
created_at: string
is_shared?: boolean
shared_by?: {
@ -396,6 +398,14 @@ const ExamManagement: React.FC = () => {
<span> {exam.attempt_count}</span>
</Tag>
</div>
{exam.participant_count > 0 && (
<div className={styles.statItem}>
<Tag color="blue" className={styles.participantTag}>
<TeamOutlined />
<span>{exam.participant_count} </span>
</Tag>
</div>
)}
{exam.has_in_progress_exam && (
<div className={styles.statItem}>
<Tag color="processing" className={styles.progressTag}>

View File

@ -98,6 +98,14 @@ export type ExamListResponse = Array<{
best_score: number
has_in_progress_exam: boolean
in_progress_record_id?: number
participant_count: number // 共享试卷的参与人数
is_shared: boolean // 是否为分享的试卷
shared_by_id?: number // 分享人ID
shared_by?: { // 分享人信息
id: number
username: string
nickname: string
}
created_at: string
}>