依据《代码分析结果.md》P3 章节,逐条修复 20 项细节问题:
正确性修复:
- P3-1 锁定剩余分钟改用 math.Ceil,避免「0.9 分钟」显示为「0 分钟」
- P3-12 移除 CheckAndUpgradeLevel 中恒真的 hasUpdate 标志
- P3-14 移除 CheckTrialExpiry 中恒真表达式(count > 0 || true)调试残留,改用 RowsAffected
- P3-2 成员不存在统一返回 NewNotFoundError(404),与其它方法语义一致
安全加固:
- P3-6 X-Request-Id 加格式校验 ^[A-Za-z0-9-]{1,64}$,防日志注入
- P3-7 新增公共 EscapeLikePattern,转义 admin LIKE 搜索的 %/_ 通配符
- P3-8 Encrypt 输出加 v1: 版本前缀,Decrypt 兼容无前缀历史密文
- P3-9 turnstile 响应体读取加 64KB 上限,防内存耗尽
- P3-11 backup codes 熵由 48-bit 提升到 64-bit
- P3-15 recordAudit 对 multipart/二进制体跳过 Unmarshal 与原文落库
- P3-17 错误日志按 Content-Type 跳过 multipart/二进制体读取
- P3-16 HandleRealtime 加 panic recover 与连接生命周期日志
可维护性/一致性:
- P3-3 oauth.go 时间格式统一为 time.RFC3339
- P3-5 MsgSuccess 统一为 ok,response.go 改用常量引用
- P3-4 错误码分段策略加注释,说明空洞与跳跃的成因
- P3-13 ModelMultiplier 占位语义加注释(计费公式设计位 + 快照列)
- P3-19 抽取 registerCronJobs,主启动流程清爽
重构:
- P3-18 删除死代码 BuildBillingSummary/SummaryParams(仅测试引用),保留共用 helper
- P3-20 抽取 relay.InvalidateMemberModelScopeCache 单一失效入口,
消除三处 member_model 前缀字面量重复(原为有意跨包失效,原建议改名会破坏)
经核实保留 + 文档化:
- P3-10 TOTP 暂保留 SHA-1:totp.Validate 硬编码 SHA-1 且存储不保存算法元信息,
直接切 SHA-256 会导致校验恒失败,需配合 schema 改动,超出 P3 范围
149 lines
4.5 KiB
Go
149 lines
4.5 KiB
Go
package response
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
"github.com/gogf/gf/v2/net/ghttp"
|
|
"github.com/gogf/gf/v2/util/gvalid"
|
|
|
|
"github.com/qianfree/team-api/internal/consts"
|
|
)
|
|
|
|
// jsonResp is the unified response structure for all API endpoints.
|
|
type jsonResp struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data"`
|
|
RequestID string `json:"request_id"`
|
|
}
|
|
|
|
// Success writes a successful response: HTTP 200 + {"code":0, "message":"ok", "data":..., "request_id":...}.
|
|
func Success(r *ghttp.Request, data interface{}) {
|
|
requestID := getRequestID(r)
|
|
r.Response.WriteJson(jsonResp{
|
|
Code: 0,
|
|
Message: consts.MsgSuccess,
|
|
Data: data,
|
|
RequestID: requestID,
|
|
})
|
|
r.SetError(nil) // 清除请求上的错误,防止 ErrorHandler 中间件重复处理
|
|
}
|
|
|
|
// Error writes an error response with automatic sanitization.
|
|
//
|
|
// Security: Only business errors (code >= 10000), validation errors, and standard HTTP 4xx errors
|
|
// expose their messages to the client. All other errors (database, network,
|
|
// internal) are replaced with "服务器内部错误" to prevent information leakage.
|
|
// The original error is logged with request_id for debugging.
|
|
func Error(r *ghttp.Request, err error) {
|
|
code := 500
|
|
msg := "服务器内部错误"
|
|
|
|
// Check for GoFrame validation error first (gvalid.Error is NOT a gerror.Error,
|
|
// so the gerror branch below won't match it — must check separately)
|
|
var validErr gvalid.Error
|
|
if errors.As(err, &validErr) {
|
|
code = 422
|
|
msg = validErr.Current().Error()
|
|
} else {
|
|
// Extract code from GoFrame gerror (supports wrapped errors via errors.As)
|
|
var gerr *gerror.Error
|
|
if errors.As(err, &gerr) {
|
|
code = gerr.Code().Code()
|
|
|
|
// Business errors (>= 10000) and HTTP 4xx have safe user-facing messages
|
|
if code >= 10000 || (code >= 400 && code < 500) {
|
|
msg = gerr.Error()
|
|
} else {
|
|
// Internal error — sanitize message, log real cause
|
|
g.Log().Warning(r.Context(), "[Response] Internal error sanitized",
|
|
"request_id", getRequestID(r),
|
|
"code", code,
|
|
"error", err.Error(),
|
|
)
|
|
// Persist system error to sys_error_logs
|
|
writeErrorLog(&ErrorLogRecord{
|
|
RequestId: getRequestID(r),
|
|
ErrorCode: code,
|
|
ErrorMessage: err.Error(),
|
|
StackTrace: captureStackTrace(3),
|
|
HttpMethod: r.Method,
|
|
RequestPath: r.URL.Path,
|
|
RequestBody: safeRequestBodyForLog(r),
|
|
Source: "api",
|
|
})
|
|
}
|
|
} else {
|
|
// Non-gerror (raw Go error, e.g. database driver error) — always sanitize
|
|
g.Log().Warning(r.Context(), "[Response] Raw error sanitized",
|
|
"request_id", getRequestID(r),
|
|
"error", err.Error(),
|
|
)
|
|
// Persist raw error to sys_error_logs
|
|
writeErrorLog(&ErrorLogRecord{
|
|
RequestId: getRequestID(r),
|
|
ErrorCode: 500,
|
|
ErrorMessage: err.Error(),
|
|
StackTrace: captureStackTrace(3),
|
|
HttpMethod: r.Method,
|
|
RequestPath: r.URL.Path,
|
|
RequestBody: safeRequestBodyForLog(r),
|
|
Source: "api",
|
|
})
|
|
}
|
|
}
|
|
|
|
if code == 0 {
|
|
code = 500
|
|
}
|
|
|
|
// Determine HTTP status
|
|
httpStatus := code
|
|
if code >= 10000 {
|
|
httpStatus = 422
|
|
} else if httpStatus < 100 || httpStatus > 599 {
|
|
// GoFrame gcode 可能返回非标准错误码(如 68=CodeNotModified),
|
|
// 不能直接用作 HTTP 状态码,否则 WriteHeader 会 panic
|
|
httpStatus = 500
|
|
}
|
|
|
|
requestID := getRequestID(r)
|
|
r.Response.WriteHeader(httpStatus)
|
|
r.Response.WriteJson(jsonResp{
|
|
Code: code,
|
|
Message: msg,
|
|
Data: nil,
|
|
RequestID: requestID,
|
|
})
|
|
r.SetError(nil) // 清除请求上的错误,防止 ErrorHandler 中间件重复处理
|
|
}
|
|
|
|
// Use this for 400/401/403/404/409/429/500 errors.
|
|
func ErrorMsg(r *ghttp.Request, code int, message string) {
|
|
ErrorWithCode(r, code, code, message)
|
|
}
|
|
|
|
// ErrorWithCode writes an error with separate HTTP status and business code.
|
|
// Use this when HTTP status differs from the business code (e.g., HTTP 401 with code 10020).
|
|
func ErrorWithCode(r *ghttp.Request, httpStatus int, code int, message string) {
|
|
requestID := getRequestID(r)
|
|
r.Response.WriteHeader(httpStatus)
|
|
r.Response.WriteJson(jsonResp{
|
|
Code: code,
|
|
Message: message,
|
|
Data: nil,
|
|
RequestID: requestID,
|
|
})
|
|
r.SetError(nil) // 清除请求上的错误,防止 ErrorHandler 中间件重复处理
|
|
}
|
|
|
|
// getRequestID extracts the request ID from context.
|
|
func getRequestID(r *ghttp.Request) string {
|
|
if v := r.GetCtxVar("RequestId"); v != nil {
|
|
return v.String()
|
|
}
|
|
return ""
|
|
}
|