实现总排行榜功能

1. 在后端添加GetTotalRanking函数和API路由
2. 在前端添加总排行榜展示和切换功能
3. 用户现在可以在首页切换查看今日排行榜和总排行榜

🤖 Generated with [Claude Code](https://claude.com/claude-code)&#10;&#10;Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
燕陇琪 2025-11-17 21:17:28 +08:00
parent 69ae78b009
commit 6efc437198
4 changed files with 163 additions and 8 deletions

View File

@ -84,6 +84,61 @@ func GetDailyRanking(c *gin.Context) {
})
}
// GetTotalRanking 获取总排行榜
func GetTotalRanking(c *gin.Context) {
db := database.GetDB()
// 获取查询参数
limitStr := c.DefaultQuery("limit", "10")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 || limit > 100 {
limit = 10
}
// 查询总排行榜(按总答题数量和正确率排序)
var rankings []models.UserStats
query := `
SELECT
u.id as user_id,
u.username,
u.nickname,
u.avatar,
u.user_type,
COALESCE(COUNT(ar.id), 0) as total_answers,
COALESCE(SUM(CASE WHEN ar.is_correct = true THEN 1 ELSE 0 END), 0) as correct_count,
COALESCE(SUM(CASE WHEN ar.is_correct = false THEN 1 ELSE 0 END), 0) as wrong_count,
CASE
WHEN COUNT(ar.id) > 0 THEN
ROUND(CAST(CAST(SUM(CASE WHEN ar.is_correct = true THEN 1 ELSE 0 END) AS FLOAT) / COUNT(ar.id) * 100 AS NUMERIC), 2)
ELSE 0
END as accuracy,
u.created_at,
MAX(ar.answered_at) as last_answer_at
FROM users u
LEFT JOIN user_answer_records ar ON u.id = ar.user_id AND ar.deleted_at IS NULL
WHERE u.deleted_at IS NULL
GROUP BY u.id, u.username, u.nickname, u.avatar, u.user_type, u.created_at
HAVING COUNT(ar.id) > 0
ORDER BY total_answers DESC, accuracy DESC, correct_count DESC
LIMIT ?
`
if err := db.Raw(query, limit).Scan(&rankings).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "获取总排行榜数据失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": rankings,
})
}
// StaticFileHandler 静态文件处理器,用于服务前端静态资源
// 使用 NoRoute 避免与 API 路由冲突
func StaticFileHandler(root string) http.Handler {

View File

@ -45,6 +45,7 @@ func main() {
// 排行榜API
auth.GET("/ranking/daily", handlers.GetDailyRanking) // 获取今日排行榜
auth.GET("/ranking/total", handlers.GetTotalRanking) // 获取总排行榜
// 练习题相关API需要登录
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表

View File

@ -199,3 +199,8 @@ export const getUserDetailStats = (userId: number) => {
export const getDailyRanking = (limit: number = 10) => {
return request.get<ApiResponse<UserStats[]>>('/ranking/daily', { params: { limit } })
}
// 获取总排行榜
export const getTotalRanking = (limit: number = 10) => {
return request.get<ApiResponse<UserStats[]>>('/ranking/total', { params: { limit } })
}

View File

@ -105,7 +105,9 @@ const Home: React.FC = () => {
// 排行榜状态
const [dailyRanking, setDailyRanking] = useState<questionApi.UserStats[]>([])
const [totalRanking, setTotalRanking] = useState<questionApi.UserStats[]>([])
const [rankingLoading, setRankingLoading] = useState(false)
const [rankingType, setRankingType] = useState<'daily' | 'total'>('daily') // 排行榜类型:每日或总榜
// 答题设置状态
const [autoNext, setAutoNext] = useState(() => {
@ -150,6 +152,30 @@ const Home: React.FC = () => {
}
}
// 加载总排行榜数据
const loadTotalRanking = async () => {
setRankingLoading(true)
try {
const res = await questionApi.getTotalRanking(10)
if (res.success && res.data) {
setTotalRanking(res.data)
}
} catch (error) {
console.error('加载总排行榜失败:', error)
} finally {
setRankingLoading(false)
}
}
// 加载当前选中的排行榜数据
const loadCurrentRanking = async () => {
if (rankingType === 'daily') {
await loadDailyRanking()
} else {
await loadTotalRanking()
}
}
// 加载用户信息
useEffect(() => {
const token = localStorage.getItem('token')
@ -172,8 +198,8 @@ const Home: React.FC = () => {
useEffect(() => {
loadStatistics()
loadDailyRanking()
}, [])
loadCurrentRanking()
}, [rankingType])
// 动态加载聊天插件(仅在首页加载)
useEffect(() => {
@ -655,25 +681,93 @@ const Home: React.FC = () => {
</Row>
</div>
{/* 今日排行榜 */}
{/* 排行榜 */}
<div className={styles.rankingSection}>
<Title level={4} className={styles.sectionTitle}>
<TrophyOutlined />
<TrophyOutlined />
</Title>
<div style={{ marginBottom: 16 }}>
<Button
type={rankingType === 'daily' ? 'primary' : 'default'}
onClick={() => setRankingType('daily')}
style={{ marginRight: 8 }}
>
</Button>
<Button
type={rankingType === 'total' ? 'primary' : 'default'}
onClick={() => setRankingType('total')}
>
</Button>
</div>
{rankingLoading ? (
<Card className={styles.rankingCard} loading={true} />
) : dailyRanking.length === 0 ? (
) : rankingType === 'daily' ? (
dailyRanking.length === 0 ? (
<Card className={styles.rankingCard}>
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
<TrophyOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
<div></div>
<div style={{ fontSize: 13, marginTop: 8 }}></div>
</div>
</Card>
) : (
<Card className={styles.rankingCard}>
<div className={styles.rankingList}>
{dailyRanking.map((user, index) => (
<div key={user.user_id} className={styles.rankingItem}>
<div className={styles.rankingLeft}>
{index < 3 ? (
<div className={`${styles.rankBadge} ${styles[`rank${index + 1}`]}`}>
{index === 0 && <CrownOutlined />}
{index === 1 && <CrownOutlined />}
{index === 2 && <CrownOutlined />}
</div>
) : (
<div className={styles.rankNumber}>{index + 1}</div>
)}
<Avatar
src={user.avatar || undefined}
size={40}
icon={<UserOutlined />}
className={styles.rankAvatar}
/>
<div className={styles.rankUserInfo}>
<div className={styles.rankNickname}>{user.nickname}</div>
<div className={styles.rankUsername}>@{user.username}</div>
</div>
</div>
<div className={styles.rankingRight}>
<div className={styles.rankStat}>
<div className={styles.rankStatValue}>{user.total_answers}</div>
<div className={styles.rankStatLabel}></div>
</div>
<div className={styles.rankDivider}></div>
<div className={styles.rankStat}>
<div className={styles.rankStatValue} style={{ color: user.accuracy >= 80 ? '#52c41a' : user.accuracy >= 60 ? '#faad14' : '#ff4d4f' }}>
{user.accuracy.toFixed(0)}%
</div>
<div className={styles.rankStatLabel}></div>
</div>
</div>
</div>
))}
</div>
</Card>
)
) : totalRanking.length === 0 ? (
<Card className={styles.rankingCard}>
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
<TrophyOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
<div></div>
<div></div>
<div style={{ fontSize: 13, marginTop: 8 }}></div>
</div>
</Card>
) : (
<Card className={styles.rankingCard}>
<div className={styles.rankingList}>
{dailyRanking.map((user, index) => (
{totalRanking.map((user, index) => (
<div key={user.user_id} className={styles.rankingItem}>
<div className={styles.rankingLeft}>
{index < 3 ? (
@ -699,7 +793,7 @@ const Home: React.FC = () => {
<div className={styles.rankingRight}>
<div className={styles.rankStat}>
<div className={styles.rankStatValue}>{user.total_answers}</div>
<div className={styles.rankStatLabel}></div>
<div className={styles.rankStatLabel}></div>
</div>
<div className={styles.rankDivider}></div>
<div className={styles.rankStat}>