feat(server): 实现统一响应格式和错误码
- 添加 server/go.mod,模块路径为 github.com/1024XEngineer/xinfra/server
- 实现 response.go 统一 JSON 响应结构:
- 响应信封:{ code, message, data, details }
- 业务错误码 (10001-20002) 与 MVP 规范一致
- 便捷函数:Success, Error, BadRequest, Unauthorized 等
- 分页响应支持,包含 total 和 items
- 添加 recovery 中间件捕获 panic 并返回 500 错误
- 添加 logger 中间件记录请求日志
为所有后端处理器建立统一的 API 响应基础,与前端期望的格式保持一致。
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
module github.com/1024XEngineer/xinfra/server
|
||||
|
||||
go 1.21
|
||||
|
||||
require github.com/gin-gonic/gin v1.9.1
|
||||
@@ -0,0 +1,143 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response 统一响应结构
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 业务错误码,0 表示成功
|
||||
Message string `json:"message"` // 响应消息
|
||||
Data interface{} `json:"data,omitempty"` // 响应数据,成功时返回
|
||||
Details string `json:"details,omitempty"` // 错误详情,失败时返回
|
||||
}
|
||||
|
||||
// 分页响应结构
|
||||
type PaginatedData struct {
|
||||
Total int64 `json:"total"`
|
||||
Items interface{} `json:"items"`
|
||||
}
|
||||
|
||||
// 业务错误码定义
|
||||
const (
|
||||
// 成功
|
||||
CodeSuccess = 0
|
||||
|
||||
// 认证相关错误 10001-10099
|
||||
CodeLDAPAuthFailed = 10001 // LDAP 认证失败
|
||||
CodeInvalidCredentials = 10002 // 用户名或密码错误
|
||||
CodeTokenExpired = 10003 // Token 已过期
|
||||
CodeSSONotConfigured = 10004 // 子系统未配置 SSO
|
||||
CodeSSOGenerateFailed = 10005 // SSO 跳转生成失败
|
||||
|
||||
// 任务相关错误 20001-20099
|
||||
CodeTaskCreateFailed = 20001 // Ansible 任务创建失败
|
||||
CodeTaskExecTimeout = 20002 // Ansible 任务执行超时
|
||||
)
|
||||
|
||||
// 错误码对应的默认消息
|
||||
var codeMessages = map[int]string{
|
||||
CodeSuccess: "success",
|
||||
CodeLDAPAuthFailed: "LDAP 认证失败",
|
||||
CodeInvalidCredentials: "用户名或密码错误",
|
||||
CodeTokenExpired: "Token 已过期",
|
||||
CodeSSONotConfigured: "子系统未配置 SSO",
|
||||
CodeSSOGenerateFailed: "SSO 跳转生成失败",
|
||||
CodeTaskCreateFailed: "Ansible 任务创建失败",
|
||||
CodeTaskExecTimeout: "Ansible 任务执行超时",
|
||||
}
|
||||
|
||||
// GetMessage 根据错误码获取默认消息
|
||||
func GetMessage(code int) string {
|
||||
if msg, ok := codeMessages[code]; ok {
|
||||
return msg
|
||||
}
|
||||
return "未知错误"
|
||||
}
|
||||
|
||||
// Success 返回成功响应
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: CodeSuccess,
|
||||
Message: GetMessage(CodeSuccess),
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// SuccessWithMessage 返回自定义消息的成功响应
|
||||
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: CodeSuccess,
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// SuccessWithPaginated 返回分页成功响应
|
||||
func SuccessWithPaginated(c *gin.Context, total int64, items interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: CodeSuccess,
|
||||
Message: GetMessage(CodeSuccess),
|
||||
Data: PaginatedData{
|
||||
Total: total,
|
||||
Items: items,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Error 返回错误响应
|
||||
func Error(c *gin.Context, httpCode int, bizCode int, details string) {
|
||||
c.JSON(httpCode, Response{
|
||||
Code: bizCode,
|
||||
Message: GetMessage(bizCode),
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
|
||||
// ErrorWithMessage 返回自定义消息的错误响应
|
||||
func ErrorWithMessage(c *gin.Context, httpCode int, bizCode int, message string, details string) {
|
||||
c.JSON(httpCode, Response{
|
||||
Code: bizCode,
|
||||
Message: message,
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequest 返回 400 错误
|
||||
func BadRequest(c *gin.Context, details string) {
|
||||
Error(c, http.StatusBadRequest, CodeInvalidCredentials, details)
|
||||
}
|
||||
|
||||
// Unauthorized 返回 401 错误
|
||||
func Unauthorized(c *gin.Context, details string) {
|
||||
Error(c, http.StatusUnauthorized, CodeTokenExpired, details)
|
||||
}
|
||||
|
||||
// Forbidden 返回 403 错误
|
||||
func Forbidden(c *gin.Context, details string) {
|
||||
c.JSON(http.StatusForbidden, Response{
|
||||
Code: http.StatusForbidden,
|
||||
Message: "无权限访问",
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
|
||||
// NotFound 返回 404 错误
|
||||
func NotFound(c *gin.Context, details string) {
|
||||
c.JSON(http.StatusNotFound, Response{
|
||||
Code: http.StatusNotFound,
|
||||
Message: "资源不存在",
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
|
||||
// InternalError 返回 500 错误
|
||||
func InternalError(c *gin.Context, details string) {
|
||||
c.JSON(http.StatusInternalServerError, Response{
|
||||
Code: http.StatusInternalServerError,
|
||||
Message: "服务器内部错误",
|
||||
Details: details,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Logger 返回请求日志中间件
|
||||
// 记录请求方法、路径、状态码、耗时、客户端 IP
|
||||
func Logger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 请求开始时间
|
||||
startTime := time.Now()
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 计算耗时
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 获取请求信息
|
||||
method := c.Request.Method
|
||||
path := c.Request.URL.Path
|
||||
statusCode := c.Writer.Status()
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// 记录日志
|
||||
log.Printf("[REQUEST] %s %s | %d | %v | %s",
|
||||
method,
|
||||
path,
|
||||
statusCode,
|
||||
duration,
|
||||
clientIP,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/handler"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Recovery 返回 panic 恢复中间件
|
||||
// 捕获请求处理过程中的 panic,返回 500 错误而不是崩溃
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 记录 panic 堆栈信息
|
||||
log.Printf("[PANIC] %v\n%s", err, debug.Stack())
|
||||
|
||||
// 返回 500 错误
|
||||
handler.InternalError(c, "服务器内部错误")
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user