package handlers import ( "ankao/internal/database" "ankao/internal/models" "net/http" "os" "path/filepath" "strconv" "strings" "github.com/gin-gonic/gin" ) // HomeHandler 首页处理器 func HomeHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "欢迎使用AnKao Web服务", "version": "1.0.0", }) } // HealthCheckHandler 健康检查处理器 func HealthCheckHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", }) } // GetDailyRanking 获取今日排行榜 func GetDailyRanking(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 AND DATE(ar.answered_at) = CURRENT_DATE 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 { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 获取请求路径 path := r.URL.Path // 构建完整文件路径 fullPath := filepath.Join(root, path) // 检查文件是否存在 info, err := os.Stat(fullPath) if err != nil { // 文件不存在,尝试返回 index.html(SPA 应用) indexPath := filepath.Join(root, "index.html") if _, err := os.Stat(indexPath); err == nil { http.ServeFile(w, r, indexPath) return } http.NotFound(w, r) return } // 如果是目录,尝试返回目录下的 index.html if info.IsDir() { indexPath := filepath.Join(fullPath, "index.html") if _, err := os.Stat(indexPath); err == nil { http.ServeFile(w, r, indexPath) return } http.NotFound(w, r) return } // 设置正确的 Content-Type setContentType(w, fullPath) // 返回文件 http.ServeFile(w, r, fullPath) }) } // setContentType 根据文件扩展名设置正确的 Content-Type func setContentType(w http.ResponseWriter, filePath string) { ext := strings.ToLower(filepath.Ext(filePath)) contentTypes := map[string]string{ ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject", } if contentType, ok := contentTypes[ext]; ok { w.Header().Set("Content-Type", contentType) } }