ffcb1a77b9
对 60 个非测试源文件做文档/注释/代码一致性审查,修复 23 项。全部为注释/文档 改动 + 1 处死代码删除,无运行逻辑变更,无 breaking change。 A 档·实质错误(10 项,会误导下游): - app.go: closeResources 三连契约注释统一修正--failAfterInit 走 rollbackReplacedResources 不走 closeResources,closeResources 仅供 doShutdown 复用 - database/manager.go: RandomPicker 注释从过时的 rand.Intn/issue 54899 改为 crypto/rand - model/base.go: datetime(0) 与“亚秒精度”矛盾,改为 datetime(6) - cache/keybuilder.go: 三处示例下划线分隔符改为冒号(与默认分隔符 : 一致) - repository/repository.go: Delete 接口注释从绝对“软删除”改为条件性; UpdateFields 示例移除把字段名当 WHERE 条件的误用写法 - middleware/csrf.go: CSRFToken 注释从“用于 API 模式”改为 Cookie/双重提交模式说明 - examples/full/main.go: POST /users 补上“需 Authorization”标注;删除 _ = app 死代码 - utils/random.go: RandInt/RandInt64 补 min==max 边界说明(对齐 RandIntSecure 写法) B 档·导出符号注释缺失/不完整(6 项): - ws/ws.go: 5 个 Type* 常量、4 个 Err* 变量补注释 - console/console.go: 5 个 Level 常量补注释 - jwt/jwt.go: 2 个 Blacklist 常量补注释 - router/router.go: 包级 GroupWithMiddlewareGroup 补注释 - middleware/ratelimit.go: NewRateLimiter 补 panic 条件与 Stop 生命周期说明 C 档·描述不完整(7 项): - utils/strings.go Substr、convert.go CalcPageCount/CalcOffset、datetime.go StartOfMonth/EndOfMonth、file.go CopyFile、validator.go IsChinese、crypto.go HashFile 补全边界/默认行为/返回值语义 - response/error.go: WithDetail 补 nil 接收者行为说明 验证:go build / go vet / go test -race 全绿。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
234 lines
8.3 KiB
Go
234 lines
8.3 KiB
Go
package response
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// 业务错误码定义
|
||
// 格式:模块(2位) + 功能(2位) + 错误类型(2位)
|
||
//
|
||
// 约定:
|
||
// - CodeSuccess = 0:成功
|
||
// - CodeFail = 1:通用失败
|
||
// - 其余 framework 内置错误码使用 HTTP 风格(401/403/404/429/500/503)或 5 位业务码段
|
||
// - 参数错误等业务化错误码请由业务项目自行定义,框架不再内置 CodeInvalidParams
|
||
const (
|
||
// 通用错误 00xxxx
|
||
CodeSuccess = 0 // 成功
|
||
CodeFail = 1 // 通用失败
|
||
CodeUnauthorized = 401 // 未授权
|
||
CodeForbidden = 403 // 无权限
|
||
CodeNotFound = 404 // 资源不存在
|
||
CodeRateLimit = 429 // 请求过于频繁
|
||
CodeServerError = 500 // 服务器错误
|
||
CodeServiceUnavailable = 503 // 服务不可用
|
||
|
||
// 用户模块错误 01xxxx
|
||
CodeUserNotFound = 10001 // 用户不存在
|
||
CodeUserAlreadyExists = 10002 // 用户已存在
|
||
CodeUserDisabled = 10003 // 用户已禁用
|
||
CodePasswordWrong = 10004 // 密码错误
|
||
CodePasswordWeak = 10005 // 密码强度不足
|
||
CodePhoneInvalid = 10006 // 手机号无效
|
||
CodeEmailInvalid = 10007 // 邮箱无效
|
||
CodeLoginFailed = 10008 // 登录失败
|
||
CodeTokenExpired = 10009 // Token 已过期
|
||
CodeTokenInvalid = 10010 // Token 无效
|
||
|
||
// 文件模块错误 02xxxx
|
||
CodeFileNotFound = 20001 // 文件不存在
|
||
CodeFileTooLarge = 20002 // 文件过大
|
||
CodeFileTypeInvalid = 20003 // 文件类型不支持
|
||
CodeFileUploadFailed = 20004 // 文件上传失败
|
||
|
||
// 数据模块错误 03xxxx
|
||
CodeDataNotFound = 30001 // 数据不存在
|
||
CodeDataAlreadyExists = 30002 // 数据已存在
|
||
CodeDataInvalid = 30003 // 数据无效
|
||
CodeDataConflict = 30004 // 数据冲突
|
||
|
||
// 业务模块错误 04xxxx
|
||
CodeOperationFailed = 40001 // 操作失败
|
||
CodeOperationTimeout = 40002 // 操作超时
|
||
CodeBusinessRuleError = 40003 // 业务规则错误
|
||
)
|
||
|
||
// Error 业务错误
|
||
type Error struct {
|
||
Code int // 错误码
|
||
Message string // 错误消息
|
||
Detail string // 详细信息(可选)
|
||
}
|
||
|
||
// NewError 创建业务错误
|
||
func NewError(code int, message string) *Error {
|
||
return &Error{
|
||
Code: code,
|
||
Message: message,
|
||
}
|
||
}
|
||
|
||
// NewErrorWithDetail 创建带详细信息的业务错误
|
||
func NewErrorWithDetail(code int, message, detail string) *Error {
|
||
return &Error{
|
||
Code: code,
|
||
Message: message,
|
||
Detail: detail,
|
||
}
|
||
}
|
||
|
||
// Error 实现 error 接口
|
||
func (e *Error) Error() string {
|
||
if e.Detail != "" {
|
||
return fmt.Sprintf("[%d] %s: %s", e.Code, e.Message, e.Detail)
|
||
}
|
||
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
|
||
}
|
||
|
||
// WithDetail 添加详细信息。
|
||
//
|
||
// H-10 修复:返回新 *Error 拷贝,不 mutate 接收者。预定义 Err*(如 ErrNotFound)是
|
||
// 包级共享指针,原实现 e.Detail = detail 会污染全局且并发调用存在数据竞争。
|
||
//
|
||
// nil 接收者:返回 &Error{Detail: detail}(Code=0/Message=""),不 panic;调用方应避免
|
||
// 在 nil 上调用本方法以拿到无 Code/Message 的半成品错误。
|
||
func (e *Error) WithDetail(detail string) *Error {
|
||
if e == nil {
|
||
return &Error{Detail: detail}
|
||
}
|
||
return &Error{
|
||
Code: e.Code,
|
||
Message: e.Message,
|
||
Detail: detail,
|
||
}
|
||
}
|
||
|
||
// ToResponse 转换为响应结构。
|
||
// 把 Detail 放入 data.detail(若非空),保留链路细节(M7:原实现丢 Detail)。
|
||
// P1 #15:仅当 detailExposed()(默认 true;App 生产环境置 false)时才向客户端输出 Detail,
|
||
// 避免内部错误细节在生产泄露。
|
||
func (e *Error) ToResponse() Response {
|
||
if e == nil {
|
||
return Response{
|
||
Code: CodeServerError,
|
||
Msg: ErrServerError.Message,
|
||
Data: nil,
|
||
}
|
||
}
|
||
if e.Detail != "" && detailExposed() {
|
||
return Response{
|
||
Code: e.Code,
|
||
Msg: e.Message,
|
||
Data: gin.H{"detail": e.Detail},
|
||
}
|
||
}
|
||
return Response{
|
||
Code: e.Code,
|
||
Msg: e.Message,
|
||
}
|
||
}
|
||
|
||
// 预定义错误
|
||
var (
|
||
ErrUnauthorized = NewError(CodeUnauthorized, "请先登录")
|
||
ErrForbidden = NewError(CodeForbidden, "无权限访问")
|
||
ErrNotFound = NewError(CodeNotFound, "资源不存在")
|
||
ErrRateLimit = NewError(CodeRateLimit, "请求过于频繁")
|
||
ErrServerError = NewError(CodeServerError, "服务器错误")
|
||
ErrServiceUnavailable = NewError(CodeServiceUnavailable, "服务暂时不可用")
|
||
|
||
// 用户相关
|
||
ErrUserNotFound = NewError(CodeUserNotFound, "用户不存在")
|
||
ErrUserAlreadyExists = NewError(CodeUserAlreadyExists, "用户已存在")
|
||
ErrUserDisabled = NewError(CodeUserDisabled, "用户已禁用")
|
||
ErrPasswordWrong = NewError(CodePasswordWrong, "密码错误")
|
||
ErrPasswordWeak = NewError(CodePasswordWeak, "密码强度不足")
|
||
ErrPhoneInvalid = NewError(CodePhoneInvalid, "手机号无效")
|
||
ErrEmailInvalid = NewError(CodeEmailInvalid, "邮箱无效")
|
||
ErrLoginFailed = NewError(CodeLoginFailed, "登录失败")
|
||
ErrTokenExpired = NewError(CodeTokenExpired, "登录已过期")
|
||
ErrTokenInvalid = NewError(CodeTokenInvalid, "Token 无效")
|
||
|
||
// 文件相关
|
||
ErrFileNotFound = NewError(CodeFileNotFound, "文件不存在")
|
||
ErrFileTooLarge = NewError(CodeFileTooLarge, "文件过大")
|
||
ErrFileTypeInvalid = NewError(CodeFileTypeInvalid, "文件类型不支持")
|
||
ErrFileUploadFailed = NewError(CodeFileUploadFailed, "文件上传失败")
|
||
|
||
// 数据相关
|
||
ErrDataNotFound = NewError(CodeDataNotFound, "数据不存在")
|
||
ErrDataAlreadyExists = NewError(CodeDataAlreadyExists, "数据已存在")
|
||
ErrDataInvalid = NewError(CodeDataInvalid, "数据无效")
|
||
ErrDataConflict = NewError(CodeDataConflict, "数据冲突")
|
||
|
||
// 业务相关
|
||
ErrOperationFailed = NewError(CodeOperationFailed, "操作失败")
|
||
ErrOperationTimeout = NewError(CodeOperationTimeout, "操作超时")
|
||
ErrBusinessRule = NewError(CodeBusinessRuleError, "业务规则错误")
|
||
)
|
||
|
||
// _errorCodeUniquenessGuard 在编译期保证所有内置 Code* 不重复。
|
||
// Go spec: 同一个常量 key 在 map 字面量中出现两次会触发
|
||
// "duplicate key in map literal" 编译错误,从而把"错误码不能撞"
|
||
// 这件事写进类型系统,比 init() 里 panic 检查更早、更安全。
|
||
//
|
||
// 维护规则:新增 Code* 常量时,**必须**把它登记到这里。
|
||
// 注意:变量名故意以下划线开头并赋给 _,避免 unused 报错且不会进入 runtime。
|
||
var _ = map[int]string{
|
||
CodeSuccess: "CodeSuccess",
|
||
CodeFail: "CodeFail",
|
||
CodeUnauthorized: "CodeUnauthorized",
|
||
CodeForbidden: "CodeForbidden",
|
||
CodeNotFound: "CodeNotFound",
|
||
CodeRateLimit: "CodeRateLimit",
|
||
CodeServerError: "CodeServerError",
|
||
CodeServiceUnavailable: "CodeServiceUnavailable",
|
||
|
||
CodeUserNotFound: "CodeUserNotFound",
|
||
CodeUserAlreadyExists: "CodeUserAlreadyExists",
|
||
CodeUserDisabled: "CodeUserDisabled",
|
||
CodePasswordWrong: "CodePasswordWrong",
|
||
CodePasswordWeak: "CodePasswordWeak",
|
||
CodePhoneInvalid: "CodePhoneInvalid",
|
||
CodeEmailInvalid: "CodeEmailInvalid",
|
||
CodeLoginFailed: "CodeLoginFailed",
|
||
CodeTokenExpired: "CodeTokenExpired",
|
||
CodeTokenInvalid: "CodeTokenInvalid",
|
||
|
||
CodeFileNotFound: "CodeFileNotFound",
|
||
CodeFileTooLarge: "CodeFileTooLarge",
|
||
CodeFileTypeInvalid: "CodeFileTypeInvalid",
|
||
CodeFileUploadFailed: "CodeFileUploadFailed",
|
||
|
||
CodeDataNotFound: "CodeDataNotFound",
|
||
CodeDataAlreadyExists: "CodeDataAlreadyExists",
|
||
CodeDataInvalid: "CodeDataInvalid",
|
||
CodeDataConflict: "CodeDataConflict",
|
||
|
||
CodeOperationFailed: "CodeOperationFailed",
|
||
CodeOperationTimeout: "CodeOperationTimeout",
|
||
CodeBusinessRuleError: "CodeBusinessRuleError",
|
||
}
|
||
|
||
// FailWithError 使用预定义错误响应(M7 修复:通过 ToResponse 保留 Detail)。
|
||
func FailWithError(c *gin.Context, err *Error) {
|
||
resp := err.ToResponse()
|
||
writeResp(c, resp.Code, resp.Msg, resp.Data)
|
||
}
|
||
|
||
// FailWithDetail 使用预定义错误并添加详细信息。
|
||
// P1 #15:detail 仅在 detailExposed()(默认 true;App 生产环境置 false)时输出给客户端。
|
||
func FailWithDetail(c *gin.Context, err *Error, detail string) {
|
||
if err == nil {
|
||
FailWithError(c, nil)
|
||
return
|
||
}
|
||
if detailExposed() {
|
||
writeResp(c, err.Code, err.Message, gin.H{"detail": detail})
|
||
return
|
||
}
|
||
writeResp(c, err.Code, err.Message, nil)
|
||
}
|