6595e94677
经 deepseek/GLM/Claude 三轮对抗性评审 + 交叉核验 + 终审(见 last_report.md、cross_review_assessment.md),逐条回读源码核实并修复。 go build / vet / test -race ./... 全绿。 安全(P0): - JWT alg confusion:WithValidMethods 固定 HMAC + keyfunc 方法断言 - JWT 空密钥 / 不支持算法 fail-closed(ErrEmptySecret / ErrUnsupportedAlgorithm) - 上传大小实测封顶(enforceUploadSize),不信任客户端 file.Size - HTTP headers/cookies map 竞态(写锁+快照)+ SSRF 防护(opt-in dialer.Control) 并发/资源(主线A 闭合): - 包级可变全局一律 atomic.Pointer:DefaultRedis / DefaultStorage / DefaultManager / Validator / DefaultCache 等,消除裸指针数据竞争 - trace.Close 去 sync.Once(防 exporter 泄漏);app OnReady 失败走 Shutdown - ws.Hub.Stop stopOnce 防 double-close;cron 接入 App 生命周期(WithCron) - ratelimit failClosed atomic.Bool;Recover Written() 守卫 - cron checkAndRun 锁内收集锁外 spawn(wg.Add 在锁内防 Stop 竞态) 终审剩余项收口(H-A~M-H + L 系列): - 副本连接池 MaxOpenConns/2 截断修复(replicaMaxOpenConns) - JWT 黑名单 1s 超时 + 显式错误;ratelimit GetRedis 取一次复用 - GetPage 深分页上限;HashFile 流式;ReadFile 去 TOCTOU - config Clone() 深拷贝;cache 锁操作返 ErrRedisNotReady - compress 解压残留清理;正则预编译;EqualsIgnoreCase→EqualFold - 删 redisLimiters 死代码;CLI 输入校验 + 回滚 文档: - README/GUIDE/CHANGELOG 对齐当前 API(含破坏性变更迁移说明) - docs/README 索引修正;config 注释修正(RS256 已不支持) 破坏性变更详见 CHANGELOG.md [Unreleased]。项目处于研发初期、无下游用户。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
184 lines
5.5 KiB
Go
184 lines
5.5 KiB
Go
package handler
|
||
|
||
import (
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/EthanCodeCraft/xlgo-core/response"
|
||
"github.com/EthanCodeCraft/xlgo-core/utils"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// HealthCheck 健康检查
|
||
// @Summary 健康检查
|
||
// @Description 检查服务是否正常运行
|
||
// @Tags 系统
|
||
// @Accept json
|
||
// @Produce json
|
||
// @Success 200 {object} map[string]string
|
||
// @Router /health [get]
|
||
//
|
||
// 响应体与 router.RegisterHealthRoute 收敛为同一 schema(H8d):
|
||
// 恒 200 + {"status":"ok"},不走 response 业务信封,便于 K8s 探针直读。
|
||
// 需要依赖探活(mysql/redis…失败 503)时改用 router.RegisterHealthRoute 传入 checks。
|
||
func HealthCheck(c *gin.Context) {
|
||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||
}
|
||
|
||
// BindJSON 绑定 JSON 请求
|
||
func BindJSON(c *gin.Context, req any) error {
|
||
return c.ShouldBindJSON(req)
|
||
}
|
||
|
||
// BindQuery 绑定 Query 参数
|
||
func BindQuery(c *gin.Context, req any) error {
|
||
return c.ShouldBindQuery(req)
|
||
}
|
||
|
||
// MaxPage GetPage 允许的最大页码(M-D 修复:防深分页 DoS)。
|
||
// 超大 page 产生超大 OFFSET,大表上为性能灾难/DoS 面。钳制到该上限;
|
||
// 需更深度遍历的业务应改用游标/keyset 分页(框架不内置,避免功能扩张)。
|
||
const MaxPage = 10000
|
||
|
||
// GetPage 获取分页参数
|
||
func GetPage(c *gin.Context) (int, int) {
|
||
page := c.DefaultQuery("page", "1")
|
||
pageSize := c.DefaultQuery("page_size", "20")
|
||
|
||
p := utils.ToIntDefault(page, 1)
|
||
ps := utils.ToIntDefault(pageSize, 20)
|
||
|
||
if p < 1 {
|
||
p = 1
|
||
}
|
||
// M-D 修复:钳制 page 上限,防 ?page=999999999 产生超大 OFFSET 拖垮 DB。
|
||
if p > MaxPage {
|
||
p = MaxPage
|
||
}
|
||
if ps < 1 {
|
||
ps = 20
|
||
}
|
||
if ps > 100 {
|
||
ps = 100
|
||
}
|
||
|
||
return p, ps
|
||
}
|
||
|
||
// GetIDFromPath 从路径获取 ID
|
||
func GetIDFromPath(c *gin.Context, paramName string) (uint, bool) {
|
||
idStr := c.Param(paramName)
|
||
idStr = strings.Trim(idStr, "/")
|
||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
return 0, false
|
||
}
|
||
return uint(id), true
|
||
}
|
||
|
||
// ===== 类型安全的参数获取函数 =====
|
||
|
||
// QueryInt 获取 Query 参数中的整数(带默认值)
|
||
func QueryInt(c *gin.Context, key string, def int) int {
|
||
return utils.ToIntDefault(c.Query(key), def)
|
||
}
|
||
|
||
// QueryInt64 获取 Query 参数中的 int64(带默认值)
|
||
func QueryInt64(c *gin.Context, key string, def int64) int64 {
|
||
return utils.ToInt64Default(c.Query(key), def)
|
||
}
|
||
|
||
// QueryFloat64 获取 Query 参数中的 float64(带默认值)
|
||
func QueryFloat64(c *gin.Context, key string, def float64) float64 {
|
||
return utils.ToFloat64Default(c.Query(key), def)
|
||
}
|
||
|
||
// QueryBool 获取 Query 参数中的布尔值(带默认值)
|
||
func QueryBool(c *gin.Context, key string, def bool) bool {
|
||
val := c.Query(key)
|
||
if val == "" {
|
||
return def
|
||
}
|
||
return val == "true" || val == "1" || val == "yes"
|
||
}
|
||
|
||
// PathInt 从路径参数获取整数(带默认值)
|
||
func PathInt(c *gin.Context, key string, def int) int {
|
||
val := strings.Trim(c.Param(key), "/")
|
||
return utils.ToIntDefault(val, def)
|
||
}
|
||
|
||
// PathInt64 从路径参数获取 int64(带默认值)
|
||
func PathInt64(c *gin.Context, key string, def int64) int64 {
|
||
val := strings.Trim(c.Param(key), "/")
|
||
return utils.ToInt64Default(val, def)
|
||
}
|
||
|
||
// PathUint64 从路径参数获取 uint64(带默认值)
|
||
func PathUint64(c *gin.Context, key string, def uint64) uint64 {
|
||
val := strings.Trim(c.Param(key), "/")
|
||
return utils.ToUint64Default(val, def)
|
||
}
|
||
|
||
// FormInt 获取 POST 表单参数中的整数(带默认值)
|
||
func FormInt(c *gin.Context, key string, def int) int {
|
||
return utils.ToIntDefault(c.PostForm(key), def)
|
||
}
|
||
|
||
// FormInt64 获取 POST 表单参数中的 int64(带默认值)
|
||
func FormInt64(c *gin.Context, key string, def int64) int64 {
|
||
return utils.ToInt64Default(c.PostForm(key), def)
|
||
}
|
||
|
||
// FormUint64 获取 POST 表单参数中的 uint64(带默认值)
|
||
func FormUint64(c *gin.Context, key string, def uint64) uint64 {
|
||
return utils.ToUint64Default(c.PostForm(key), def)
|
||
}
|
||
|
||
// FormFloat64 获取 POST 表单参数中的 float64(带默认值)
|
||
func FormFloat64(c *gin.Context, key string, def float64) float64 {
|
||
return utils.ToFloat64Default(c.PostForm(key), def)
|
||
}
|
||
|
||
// FormBool 获取 POST 表单参数中的布尔值(带默认值)
|
||
func FormBool(c *gin.Context, key string, def bool) bool {
|
||
val := c.PostForm(key)
|
||
if val == "" {
|
||
return def
|
||
}
|
||
return val == "true" || val == "1" || val == "yes" || val == "on"
|
||
}
|
||
|
||
// FormString 获取 POST 表单参数中的字符串(带默认值)
|
||
func FormString(c *gin.Context, key string, def string) string {
|
||
val := c.PostForm(key)
|
||
if val == "" {
|
||
return def
|
||
}
|
||
return val
|
||
}
|
||
|
||
// ParseInt 解析整数
|
||
func ParseInt(s string, defaultValue int) int {
|
||
return utils.ToIntDefault(s, defaultValue)
|
||
}
|
||
|
||
// BadRequest 返回参数/请求错误响应。
|
||
//
|
||
// 委托 response.FailWithCode(CodeFail),遵循当前响应模式(ModeBusiness 下 HTTP 200,
|
||
// 错误信息通过 body 中的 code 表达;ModeREST 下 CodeFail 属业务失败不映射 HTTP 错误,仍 200),
|
||
// 并写入 RequestID——与 response 模式系统一致,不再硬编 HTTP 状态码、不再丢失链路追踪。
|
||
func BadRequest(c *gin.Context, msg string) {
|
||
response.FailWithCode(c, response.CodeFail, msg)
|
||
}
|
||
|
||
// InternalError 返回服务器错误响应。
|
||
//
|
||
// 委托 response.ServerError(CodeServerError),遵循当前响应模式(ModeBusiness 下 HTTP 200,
|
||
// ModeREST 下 HTTP 500),并写入 RequestID——与 response 模式系统一致,
|
||
// 不再硬编 HTTP 状态码、不再丢失链路追踪。
|
||
func InternalError(c *gin.Context, msg string) {
|
||
response.ServerError(c, msg)
|
||
}
|