主要改进: - 重构静态文件服务: 实现自定义 StaticFileHandler,完善 SPA 路由支持和 Content-Type 处理 - 优化 Docker 构建: 简化前端资源复制逻辑,直接复制整个 dist 目录 - 添加 K8s 部署配置: 包含健康检查探针、资源限制和服务配置 - 前端配置优化: Vite 使用相对路径 base,确保打包后资源路径正确 - 代码清理: 删除已废弃的数据迁移脚本 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
153 lines
3.8 KiB
Go
153 lines
3.8 KiB
Go
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)
|
||
}
|
||
}
|