阶段2(上):后端专题 Go/Gin/PostgreSQL/MySQL/Redis/MQ/gRPC 的课程、企业场景、面试题和可运行示例
This commit is contained in:
1 parent
0bae9916a9
commit
5eb67c5d08
65 files changed
+6457
-77
No files matched your search
@@ -0,0 +1,185 @@
|
||||
// Package unifiedapi 演示企业后台接口规范:统一响应、业务错误码、请求 ID、参数校验中文提示、panic 兜底。
|
||||
package unifiedapi
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// AppError 业务错误:HTTP 状态码 + 业务错误码 + 给用户看的提示
|
||||
type AppError struct {
|
||||
Status int
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string { return e.Message }
|
||||
|
||||
var (
|
||||
ErrNotFound = &AppError{http.StatusNotFound, 40400, "资源不存在"}
|
||||
ErrInternal = &AppError{http.StatusInternalServerError, 50000, "服务器开小差了,请稍后再试"}
|
||||
)
|
||||
|
||||
func Invalid(msg string) *AppError { return &AppError{http.StatusBadRequest, 40000, msg} }
|
||||
|
||||
// Body 统一响应格式
|
||||
type Body struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
func OK(c *gin.Context, status int, data any) {
|
||||
c.JSON(status, Body{Code: 0, Message: "ok", Data: data, RequestID: c.GetString("requestID")})
|
||||
}
|
||||
|
||||
func writeErr(c *gin.Context, e *AppError) {
|
||||
c.AbortWithStatusJSON(e.Status, Body{Code: e.Code, Message: e.Message, RequestID: c.GetString("requestID")})
|
||||
}
|
||||
|
||||
// RequestID 沿用客户端传来的 X-Request-ID,没有就生成一个,并写回响应头
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.GetHeader("X-Request-ID")
|
||||
if id == "" || len(id) > 64 {
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
id = hex.EncodeToString(b)
|
||||
}
|
||||
c.Set("requestID", id)
|
||||
c.Header("X-Request-ID", id)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Errors 统一把处理函数里 c.Error(err) 记录的错误和 panic 转成统一响应;内部错误不把细节暴露给用户
|
||||
func Errors(log *slog.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("panic", "request_id", c.GetString("requestID"), "panic", r)
|
||||
writeErr(c, ErrInternal)
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
if len(c.Errors) == 0 || c.Writer.Written() {
|
||||
return
|
||||
}
|
||||
err := c.Errors.Last().Err
|
||||
var ae *AppError
|
||||
if errors.As(err, &ae) {
|
||||
writeErr(c, ae)
|
||||
return
|
||||
}
|
||||
log.Error("internal error", "request_id", c.GetString("requestID"), "err", err)
|
||||
writeErr(c, ErrInternal)
|
||||
}
|
||||
}
|
||||
|
||||
var setupOnce sync.Once
|
||||
|
||||
// useJSONFieldNames 让校验错误里显示 JSON 字段名(title)而不是结构体字段名(Title)
|
||||
func useJSONFieldNames() {
|
||||
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
|
||||
v.RegisterTagNameFunc(func(f reflect.StructField) string {
|
||||
name := strings.Split(f.Tag.Get("json"), ",")[0]
|
||||
if name == "" || name == "-" {
|
||||
return f.Name
|
||||
}
|
||||
return name
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Bind 解析并校验 JSON,失败时记录一个用户能看懂的错误
|
||||
func Bind(c *gin.Context, req any) bool {
|
||||
err := c.ShouldBindJSON(req)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
var ve validator.ValidationErrors
|
||||
if errors.As(err, &ve) {
|
||||
msgs := make([]string, 0, len(ve))
|
||||
for _, fe := range ve {
|
||||
switch fe.Tag() {
|
||||
case "required":
|
||||
msgs = append(msgs, fmt.Sprintf("%s 不能为空", fe.Field()))
|
||||
case "max":
|
||||
msgs = append(msgs, fmt.Sprintf("%s 长度不能超过 %s", fe.Field(), fe.Param()))
|
||||
case "oneof":
|
||||
msgs = append(msgs, fmt.Sprintf("%s 只能是 %s 之一", fe.Field(), fe.Param()))
|
||||
default:
|
||||
msgs = append(msgs, fmt.Sprintf("%s 不合法", fe.Field()))
|
||||
}
|
||||
}
|
||||
_ = c.Error(Invalid(strings.Join(msgs, ";")))
|
||||
} else {
|
||||
_ = c.Error(Invalid("请求体格式错误"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Priority string `json:"priority"`
|
||||
}
|
||||
|
||||
type CreateTask struct {
|
||||
Title string `json:"title" binding:"required,max=50"`
|
||||
Priority string `json:"priority" binding:"omitempty,oneof=low normal high"`
|
||||
}
|
||||
|
||||
// New 组装路由
|
||||
func New(log *slog.Logger) *gin.Engine {
|
||||
setupOnce.Do(useJSONFieldNames)
|
||||
var mu sync.Mutex
|
||||
tasks := map[int]Task{}
|
||||
next := 1
|
||||
|
||||
r := gin.New()
|
||||
r.Use(RequestID(), Errors(log))
|
||||
api := r.Group("/api")
|
||||
api.POST("/tasks", func(c *gin.Context) {
|
||||
var req CreateTask
|
||||
if !Bind(c, &req) {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
t := Task{ID: next, Title: req.Title, Priority: req.Priority}
|
||||
tasks[t.ID] = t
|
||||
next++
|
||||
mu.Unlock()
|
||||
OK(c, http.StatusCreated, t)
|
||||
})
|
||||
api.GET("/tasks/:id", func(c *gin.Context) {
|
||||
var id int
|
||||
if _, err := fmt.Sscan(c.Param("id"), &id); err != nil {
|
||||
_ = c.Error(Invalid("id 必须是整数"))
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
t, ok := tasks[id]
|
||||
mu.Unlock()
|
||||
if !ok {
|
||||
_ = c.Error(ErrNotFound)
|
||||
return
|
||||
}
|
||||
OK(c, http.StatusOK, t)
|
||||
})
|
||||
api.GET("/boom", func(c *gin.Context) { panic("数据库连接串 postgres://secret 泄漏测试") })
|
||||
api.GET("/db-error", func(c *gin.Context) { _ = c.Error(errors.New("pq: relation \"tasks\" does not exist")) })
|
||||
return r
|
||||
}
|
||||
Reference in new issue
Block a user