AnCao/internal/handlers/admin_handler.go
yanlongqi 3ecc1c6a18 添加用户管理功能
新增功能:
- 用户管理页面:展示所有用户及答题统计
- 用户详情页面:查看单个用户的详细答题数据
- 管理员权限中间件:仅yanlongqi用户可访问
- 后端API接口:用户列表和详情统计

后端更新:
- 新增 admin_handler.go:用户管理相关处理器
- 新增 admin.go 中间件:管理员权限验证
- 新增 user_stats.go 模型:用户统计数据结构
- 更新 main.go:注册用户管理API路由

前端更新:
- 新增 UserManagement 页面:用户列表和统计卡片
- 新增 UserDetail 页面:用户详细信息和题型统计
- 更新 Home 页面:添加用户管理入口(仅管理员可见)
- 更新 App.tsx:添加用户管理路由和权限保护
- 更新 API 接口:添加用户管理相关接口定义

UI优化:
- 用户管理页面标题居中显示,参考错题本设计
- 统计卡片使用16px padding
- 返回按钮使用绝对定位,标题居中
- 字体大小统一为18px,字重700

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 06:21:15 +08:00

144 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"ankao/internal/database"
"ankao/internal/models"
"net/http"
"github.com/gin-gonic/gin"
)
// GetAllUsersWithStats 获取所有用户及其答题统计(仅管理员可访问)
func GetAllUsersWithStats(c *gin.Context) {
db := database.GetDB()
// 查询所有用户及其答题统计
var userStats []models.UserStats
// SQL查询联合查询用户表和答题记录表
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
ORDER BY total_answers DESC, accuracy DESC
`
if err := db.Raw(query).Scan(&userStats).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": userStats,
})
}
// GetUserDetailStats 获取指定用户的详细统计信息
func GetUserDetailStats(c *gin.Context) {
userID := c.Param("id")
db := database.GetDB()
// 查询用户基本信息
var user models.User
if err := db.First(&user, userID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{
"success": false,
"message": "用户不存在",
})
return
}
// 查询用户答题统计
var stats 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.id = ? AND u.deleted_at IS NULL
GROUP BY u.id, u.username, u.nickname, u.avatar, u.user_type, u.created_at
`
if err := db.Raw(query, userID).Scan(&stats).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": "获取用户统计数据失败",
"error": err.Error(),
})
return
}
// 查询按题型分类的统计
var typeStats []struct {
QuestionType string `json:"question_type"`
QuestionTypeName string `json:"question_type_name"`
TotalAnswers int `json:"total_answers"`
CorrectCount int `json:"correct_count"`
Accuracy float64 `json:"accuracy"`
}
typeQuery := `
SELECT
pq.type as question_type,
pq.type_name as question_type_name,
COUNT(ar.id) as total_answers,
SUM(CASE WHEN ar.is_correct = true THEN 1 ELSE 0 END) as correct_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
FROM user_answer_records ar
JOIN practice_questions pq ON ar.question_id = pq.id
WHERE ar.user_id = ? AND ar.deleted_at IS NULL
GROUP BY pq.type, pq.type_name
ORDER BY total_answers DESC
`
db.Raw(typeQuery, userID).Scan(&typeStats)
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"user_info": stats,
"type_stats": typeStats,
},
})
}