fix: 枚举入参校验(500→400)、5xx 错误串不再外泄、健康检查探库、限流键清理
服务端一批「会误导现场」的缺陷,全部补了测试: - 枚举型入参(events 的 event_type/result_mode/result_type/compare/status、 scoring_profiles 的 tie_mode/record_bonus_mode、entries.status、results.status) 过去完全没有校验,非法值一路落到 SQLite 的 CHECK 约束,被统一错误处理当成 服务端异常报成 500;现在统一在 enums.go 拦成 400 VALIDATION_FAILED, message 带字段名与合法取值、detail 带原始输入值。空值仍走原来的默认值。 - 5xx 响应体不再回显内部错误原文:新增 apiError.logDetail,SQLite / 引擎的 原始错误串只进 ERROR 日志的 detail 字段。此前正好是反的——前端拿得到原文, 日志里只有「服务端异常」。同时校准 newAPIError / internalMsg 的 skip 层数, 拦截点不再退化成 response.go 里的构造函数。 - /api/health 新增 db / dbError:用 SELECT 1 探库(Ping 探不到「文件在、库读不动」), 超时 5s 与 busy_timeout 同量级。状态码仍恒 200 —— Console 拿状态码当存活信号, 数据库故障时回 5xx 只会变成重启循环,两个事实分两个字段表达。 - 修改密码时原密码错误由 401 改为 400 AUTH_FAILED(新增 wrongCredential): 401 的语义是「本请求没有有效会话」,而这里会话是好的。 - 限流 hits map 在键数超过阈值时清理窗口内已无记录的键(窗口内活跃的键不动, 否则堆键数就能冲掉别人的计数)。 - createEntry / updateEntry 拒绝「A 小项的报名挂在 B 小项的组别上」。 数据库层仍无 (event_id, category_id) 复合外键(重建 entries 会牵动 results / score_ledger 的级联,风险高不做),这一层先兜住。 - 补测试:枚举字段逐项正反例、CSRF 反向用例、写限流 429、健康探库(含库故障)、 5xx 不外泄原文、限流键清理不误删。
This commit is contained in:
1 parent
23fee5b818
commit
2c983bcfdf
14 files changed
+1112
-21
No files matched your search
@@ -819,6 +819,14 @@ func (s *server) createProfile(ctx context.Context, in profileDTO) (profileDTO,
|
||||
if strings.TrimSpace(in.Code) == "" || strings.TrimSpace(in.Name) == "" {
|
||||
return profileDTO{}, badRequest("code 与 name 必填", nil)
|
||||
}
|
||||
tieMode, err := normalizeTieMode(in.TieMode)
|
||||
if err != nil {
|
||||
return profileDTO{}, err
|
||||
}
|
||||
recordBonusMode, err := normalizeRecordBonusMode(in.RecordBonusMode)
|
||||
if err != nil {
|
||||
return profileDTO{}, err
|
||||
}
|
||||
tx, err := s.ev.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return profileDTO{}, internal(err)
|
||||
@@ -828,7 +836,7 @@ func (s *server) createProfile(ctx context.Context, in profileDTO) (profileDTO,
|
||||
res, err := tx.ExecContext(ctx,
|
||||
`INSERT INTO scoring_profiles (code, name, tie_mode, record_bonus_mode, award_ranks, last_place_zero)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
in.Code, in.Name, orDefault(in.TieMode, "skip"), orDefault(in.RecordBonusMode, "max"), in.AwardRanks, in.LastPlaceZero)
|
||||
in.Code, in.Name, tieMode, recordBonusMode, in.AwardRanks, in.LastPlaceZero)
|
||||
if isUniqueViolation(err) {
|
||||
return profileDTO{}, conflict("积分模板编码已存在")
|
||||
}
|
||||
@@ -849,6 +857,14 @@ func (s *server) createProfile(ctx context.Context, in profileDTO) (profileDTO,
|
||||
}
|
||||
|
||||
func (s *server) updateProfile(ctx context.Context, id int64, in profileDTO) (profileDTO, error) {
|
||||
tieMode, err := normalizeTieMode(in.TieMode)
|
||||
if err != nil {
|
||||
return profileDTO{}, err
|
||||
}
|
||||
recordBonusMode, err := normalizeRecordBonusMode(in.RecordBonusMode)
|
||||
if err != nil {
|
||||
return profileDTO{}, err
|
||||
}
|
||||
tx, err := s.ev.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return profileDTO{}, internal(err)
|
||||
@@ -859,7 +875,7 @@ func (s *server) updateProfile(ctx context.Context, id int64, in profileDTO) (pr
|
||||
`UPDATE scoring_profiles SET code = ?, name = ?, tie_mode = ?, record_bonus_mode = ?,
|
||||
award_ranks = ?, last_place_zero = ?, updated_at = datetime('now')
|
||||
WHERE id = ?`,
|
||||
in.Code, in.Name, orDefault(in.TieMode, "skip"), orDefault(in.RecordBonusMode, "max"), in.AwardRanks, in.LastPlaceZero, id)
|
||||
in.Code, in.Name, tieMode, recordBonusMode, in.AwardRanks, in.LastPlaceZero, id)
|
||||
if isUniqueViolation(err) {
|
||||
return profileDTO{}, conflict("积分模板编码已存在")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user