新增功能: - 用户管理页面:展示所有用户及答题统计 - 用户详情页面:查看单个用户的详细答题数据 - 管理员权限中间件:仅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>
144 lines
4.0 KiB
Go
144 lines
4.0 KiB
Go
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,
|
||
},
|
||
})
|
||
}
|