From 6efc4371985553fea22c6d43c8532dc2b12738d7 Mon Sep 17 00:00:00 2001 From: yanlongqi Date: Mon, 17 Nov 2025 21:17:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=80=BB=E6=8E=92=E8=A1=8C?= =?UTF-8?q?=E6=A6=9C=E5=8A=9F=E8=83=BD 1.=20=E5=9C=A8=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=B7=BB=E5=8A=A0GetTotalRanking=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=92=8CAPI=E8=B7=AF=E7=94=B1 2.=20=E5=9C=A8=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E6=B7=BB=E5=8A=A0=E6=80=BB=E6=8E=92=E8=A1=8C=E6=A6=9C?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E5=92=8C=E5=88=87=E6=8D=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q? 3.=20=E7=94=A8=E6=88=B7=E7=8E=B0=E5=9C=A8=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E5=9C=A8=E9=A6=96=E9=A1=B5=E5=88=87=E6=8D=A2=E6=9F=A5?= =?UTF-8?q?=E7=9C=8B=E4=BB=8A=E6=97=A5=E6=8E=92=E8=A1=8C=E6=A6=9C=E5=92=8C?= =?UTF-8?q?=E6=80=BB=E6=8E=92=E8=A1=8C=E6=A6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/handlers/handlers.go | 55 +++++++++++++++++ main.go | 1 + web/src/api/question.ts | 5 ++ web/src/pages/Home.tsx | 110 +++++++++++++++++++++++++++++++--- 4 files changed, 163 insertions(+), 8 deletions(-) diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 213fa2c..1bf5c8f 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -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 { diff --git a/main.go b/main.go index 6373f52..036af67 100644 --- a/main.go +++ b/main.go @@ -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) // 获取练习题目列表 diff --git a/web/src/api/question.ts b/web/src/api/question.ts index b0f1202..178d68b 100644 --- a/web/src/api/question.ts +++ b/web/src/api/question.ts @@ -199,3 +199,8 @@ export const getUserDetailStats = (userId: number) => { export const getDailyRanking = (limit: number = 10) => { return request.get>('/ranking/daily', { params: { limit } }) } + +// 获取总排行榜 +export const getTotalRanking = (limit: number = 10) => { + return request.get>('/ranking/total', { params: { limit } }) +} diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 69dc0fd..7cb2bcd 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -105,7 +105,9 @@ const Home: React.FC = () => { // 排行榜状态 const [dailyRanking, setDailyRanking] = useState([]) + const [totalRanking, setTotalRanking] = useState([]) 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 = () => { - {/* 今日排行榜 */} + {/* 排行榜 */}
- <TrophyOutlined /> 今日排行榜 + <TrophyOutlined /> 排行榜 +
+ + +
{rankingLoading ? ( - ) : dailyRanking.length === 0 ? ( + ) : rankingType === 'daily' ? ( + dailyRanking.length === 0 ? ( + +
+ +
今日暂无排行数据
+
快去刷题吧!
+
+
+ ) : ( + +
+ {dailyRanking.map((user, index) => ( +
+
+ {index < 3 ? ( +
+ {index === 0 && } + {index === 1 && } + {index === 2 && } +
+ ) : ( +
{index + 1}
+ )} + } + className={styles.rankAvatar} + /> +
+
{user.nickname}
+
@{user.username}
+
+
+
+
+
{user.total_answers}
+
今日刷题
+
+
+
+
= 80 ? '#52c41a' : user.accuracy >= 60 ? '#faad14' : '#ff4d4f' }}> + {user.accuracy.toFixed(0)}% +
+
正确率
+
+
+
+ ))} +
+
+ ) + ) : totalRanking.length === 0 ? (
-
今日暂无排行数据
+
暂无排行数据
快去刷题吧!
) : (
- {dailyRanking.map((user, index) => ( + {totalRanking.map((user, index) => (
{index < 3 ? ( @@ -699,7 +793,7 @@ const Home: React.FC = () => {
{user.total_answers}
-
今日刷题
+
总刷题