实现总排行榜功能 1. 在后端添加GetTotalRanking函数和API路由 2. 在前端添加总排行榜展示和切换功能 3. 用户现在可以在首页切换查看今日排行榜和总排行榜
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
69ae78b009
commit
6efc437198
@ -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 静态文件处理器,用于服务前端静态资源
|
// StaticFileHandler 静态文件处理器,用于服务前端静态资源
|
||||||
// 使用 NoRoute 避免与 API 路由冲突
|
// 使用 NoRoute 避免与 API 路由冲突
|
||||||
func StaticFileHandler(root string) http.Handler {
|
func StaticFileHandler(root string) http.Handler {
|
||||||
|
|||||||
1
main.go
1
main.go
@ -45,6 +45,7 @@ func main() {
|
|||||||
|
|
||||||
// 排行榜API
|
// 排行榜API
|
||||||
auth.GET("/ranking/daily", handlers.GetDailyRanking) // 获取今日排行榜
|
auth.GET("/ranking/daily", handlers.GetDailyRanking) // 获取今日排行榜
|
||||||
|
auth.GET("/ranking/total", handlers.GetTotalRanking) // 获取总排行榜
|
||||||
|
|
||||||
// 练习题相关API(需要登录)
|
// 练习题相关API(需要登录)
|
||||||
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表
|
auth.GET("/practice/questions", handlers.GetPracticeQuestions) // 获取练习题目列表
|
||||||
|
|||||||
@ -199,3 +199,8 @@ export const getUserDetailStats = (userId: number) => {
|
|||||||
export const getDailyRanking = (limit: number = 10) => {
|
export const getDailyRanking = (limit: number = 10) => {
|
||||||
return request.get<ApiResponse<UserStats[]>>('/ranking/daily', { params: { limit } })
|
return request.get<ApiResponse<UserStats[]>>('/ranking/daily', { params: { limit } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取总排行榜
|
||||||
|
export const getTotalRanking = (limit: number = 10) => {
|
||||||
|
return request.get<ApiResponse<UserStats[]>>('/ranking/total', { params: { limit } })
|
||||||
|
}
|
||||||
|
|||||||
@ -105,7 +105,9 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
// 排行榜状态
|
// 排行榜状态
|
||||||
const [dailyRanking, setDailyRanking] = useState<questionApi.UserStats[]>([])
|
const [dailyRanking, setDailyRanking] = useState<questionApi.UserStats[]>([])
|
||||||
|
const [totalRanking, setTotalRanking] = useState<questionApi.UserStats[]>([])
|
||||||
const [rankingLoading, setRankingLoading] = useState(false)
|
const [rankingLoading, setRankingLoading] = useState(false)
|
||||||
|
const [rankingType, setRankingType] = useState<'daily' | 'total'>('daily') // 排行榜类型:每日或总榜
|
||||||
|
|
||||||
// 答题设置状态
|
// 答题设置状态
|
||||||
const [autoNext, setAutoNext] = useState(() => {
|
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(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
@ -172,8 +198,8 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadStatistics()
|
loadStatistics()
|
||||||
loadDailyRanking()
|
loadCurrentRanking()
|
||||||
}, [])
|
}, [rankingType])
|
||||||
|
|
||||||
// 动态加载聊天插件(仅在首页加载)
|
// 动态加载聊天插件(仅在首页加载)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -655,25 +681,93 @@ const Home: React.FC = () => {
|
|||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 今日排行榜 */}
|
{/* 排行榜 */}
|
||||||
<div className={styles.rankingSection}>
|
<div className={styles.rankingSection}>
|
||||||
<Title level={4} className={styles.sectionTitle}>
|
<Title level={4} className={styles.sectionTitle}>
|
||||||
<TrophyOutlined /> 今日排行榜
|
<TrophyOutlined /> 排行榜
|
||||||
</Title>
|
</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 ? (
|
{rankingLoading ? (
|
||||||
<Card className={styles.rankingCard} loading={true} />
|
<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}>
|
<Card className={styles.rankingCard}>
|
||||||
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
|
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#8c8c8c' }}>
|
||||||
<TrophyOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
|
<TrophyOutlined style={{ fontSize: 48, marginBottom: 16, opacity: 0.3 }} />
|
||||||
<div>今日暂无排行数据</div>
|
<div>暂无排行数据</div>
|
||||||
<div style={{ fontSize: 13, marginTop: 8 }}>快去刷题吧!</div>
|
<div style={{ fontSize: 13, marginTop: 8 }}>快去刷题吧!</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<Card className={styles.rankingCard}>
|
<Card className={styles.rankingCard}>
|
||||||
<div className={styles.rankingList}>
|
<div className={styles.rankingList}>
|
||||||
{dailyRanking.map((user, index) => (
|
{totalRanking.map((user, index) => (
|
||||||
<div key={user.user_id} className={styles.rankingItem}>
|
<div key={user.user_id} className={styles.rankingItem}>
|
||||||
<div className={styles.rankingLeft}>
|
<div className={styles.rankingLeft}>
|
||||||
{index < 3 ? (
|
{index < 3 ? (
|
||||||
@ -699,7 +793,7 @@ const Home: React.FC = () => {
|
|||||||
<div className={styles.rankingRight}>
|
<div className={styles.rankingRight}>
|
||||||
<div className={styles.rankStat}>
|
<div className={styles.rankStat}>
|
||||||
<div className={styles.rankStatValue}>{user.total_answers}</div>
|
<div className={styles.rankStatValue}>{user.total_answers}</div>
|
||||||
<div className={styles.rankStatLabel}>今日刷题</div>
|
<div className={styles.rankStatLabel}>总刷题</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rankDivider}></div>
|
<div className={styles.rankDivider}></div>
|
||||||
<div className={styles.rankStat}>
|
<div className={styles.rankStat}>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user