335 lines
10 KiB
Go
Executable File
335 lines
10 KiB
Go
Executable File
package ginx_test
|
||
|
||
// 可运行示例:gin 接入 responsex 的完整用法。
|
||
// 每个示例都用 httptest 真实走一遍路由,输出即真实响应体。
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"testing"
|
||
|
||
"code.yun.ink/pkg/responsex"
|
||
ginx "code.yun.ink/pkg/responsex/ginx"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/yuninks/errorx"
|
||
"github.com/yuninks/langx"
|
||
)
|
||
|
||
// ---------- 业务错误码:项目里通常在 errors 包声明一次 ----------
|
||
|
||
var (
|
||
// 第一种写法:text 直接给默认语言文案
|
||
ErrSignInvalid = errorx.NewCode("ex_sign_invalid", 41001, "签名校验失败")
|
||
// 第二种写法:文案由 langx 的语言表提供(推荐,便于集中维护多语言)
|
||
ErrStockNotEnough = errorx.NewCode("ex_stock_not_enough", 50001, "库存不足")
|
||
)
|
||
|
||
// TestMain 里注册一次语言表,等价于项目启动时执行 langx.RegisterDir / RegisterEmbed
|
||
func TestMain(m *testing.M) {
|
||
langx.RegisterTrans("zh", map[string]string{
|
||
"ex_stock_not_enough": "库存不足",
|
||
"ex_sign_invalid": "签名校验失败",
|
||
})
|
||
langx.RegisterTrans("en", map[string]string{
|
||
"ex_stock_not_enough": "out of stock",
|
||
"ex_sign_invalid": "invalid signature",
|
||
})
|
||
// 示例演示用:把默认 logger 换成静默实现,避免响应日志混入示例输出。
|
||
// 真实项目应接入自己的日志组件,例如:
|
||
// responsex.InitOptions(responsex.SetLogger(loggerx.New(...)))
|
||
responsex.InitOptions(responsex.SetLogger(silentLogger{}))
|
||
gin.SetMode(gin.TestMode)
|
||
os.Exit(m.Run())
|
||
}
|
||
|
||
// silentLogger 实现 responsex.Logger,什么都不做
|
||
type silentLogger struct{}
|
||
|
||
func (silentLogger) Info(ctx context.Context, args ...interface{}) {}
|
||
func (silentLogger) Infof(ctx context.Context, format string, args ...interface{}) {}
|
||
func (silentLogger) Error(ctx context.Context, args ...interface{}) {}
|
||
func (silentLogger) Errorf(ctx context.Context, format string, args ...interface{}) {
|
||
}
|
||
|
||
// ---------- 辅助 ----------
|
||
|
||
func newExampleEngine() *gin.Engine {
|
||
r := gin.New()
|
||
r.Use(ginx.Middleware())
|
||
return r
|
||
}
|
||
|
||
// do 发一次请求,返回响应记录器
|
||
func do(r *gin.Engine, method, path string, headers map[string]string) *httptest.ResponseRecorder {
|
||
req := httptest.NewRequest(method, path, nil)
|
||
for k, v := range headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
w := httptest.NewRecorder()
|
||
r.ServeHTTP(w, req)
|
||
return w
|
||
}
|
||
|
||
// body 返回按 key 排序后的紧凑 JSON,便于示例输出稳定
|
||
func body(w *httptest.ResponseRecorder) string {
|
||
var m map[string]any
|
||
if err := json.Unmarshal(w.Body.Bytes(), &m); err != nil {
|
||
return w.Body.String()
|
||
}
|
||
return compact(m)
|
||
}
|
||
|
||
func compact(v any) string {
|
||
bs, err := json.Marshal(v)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return string(bs)
|
||
}
|
||
|
||
// ---------- 1. 最小可用:挂中间件 + 成功响应 ----------
|
||
|
||
func Example() {
|
||
r := newExampleEngine()
|
||
r.GET("/ping", func(c *gin.Context) {
|
||
ginx.Success(c, gin.H{"pong": true})
|
||
})
|
||
|
||
w := do(r, http.MethodGet, "/ping", nil)
|
||
fmt.Println("HTTP", w.Code)
|
||
fmt.Println(w.Body.String())
|
||
// Output:
|
||
// HTTP 200
|
||
// {"code":200,"message":"请求成功","data":{"pong":true}}
|
||
}
|
||
|
||
// ---------- 2. Middleware:trace_id 桥接 ----------
|
||
|
||
func ExampleMiddleware() {
|
||
r := newExampleEngine()
|
||
r.GET("/order", func(c *gin.Context) {
|
||
// 业务侧照旧用 c.Set,不需要关心核心包怎么取值
|
||
c.Set("trace_id", "trace-9f8e7d")
|
||
ginx.Success(c, gin.H{"order_sn": "SN20240101"})
|
||
})
|
||
|
||
w := do(r, http.MethodGet, "/order", nil)
|
||
fmt.Println("c.Set :", w.Body.String())
|
||
|
||
// 没有 c.Set 时,回落到请求头 X-Request-Id
|
||
r2 := newExampleEngine()
|
||
r2.GET("/order", func(c *gin.Context) {
|
||
ginx.Success(c, "ok")
|
||
})
|
||
w = do(r2, http.MethodGet, "/order", map[string]string{"X-Request-Id": "req-123"})
|
||
fmt.Println("header :", w.Body.String())
|
||
|
||
// 两者都没有 → 不出现 trace_id 字段
|
||
w = do(r2, http.MethodGet, "/order", nil)
|
||
fmt.Println("none :", w.Body.String())
|
||
|
||
// Output:
|
||
// c.Set : {"code":200,"message":"请求成功","data":{"order_sn":"SN20240101"},"trace_id":"trace-9f8e7d"}
|
||
// header : {"code":200,"message":"请求成功","data":"ok","trace_id":"req-123"}
|
||
// none : {"code":200,"message":"请求成功","data":"ok"}
|
||
}
|
||
|
||
// ---------- 3. 参数绑定失败:直接回错误 ----------
|
||
|
||
func ExampleError() {
|
||
r := newExampleEngine()
|
||
r.GET("/user", func(c *gin.Context) {
|
||
ginx.Error(c, errors.New("EOF: 请求体为空"),
|
||
responsex.SetHttpStatusCode(http.StatusBadRequest),
|
||
)
|
||
})
|
||
|
||
w := do(r, http.MethodGet, "/user", nil)
|
||
fmt.Println("HTTP", w.Code)
|
||
fmt.Println(w.Body.String())
|
||
// Output:
|
||
// HTTP 400
|
||
// {"code":400,"message":"EOF: 请求体为空","data":""}
|
||
}
|
||
|
||
// ---------- 4. 业务错误码:code 与多语言文案自动带出 ----------
|
||
|
||
func ExampleError_businessCode() {
|
||
r := newExampleEngine()
|
||
r.POST("/order", func(c *gin.Context) {
|
||
// errorx 的 New(ctx) 会带上当前请求语言,Error 自动取 Code() 与译文
|
||
ginx.Error(c, ErrStockNotEnough.New(c.Request.Context()),
|
||
responsex.SetHttpStatusCode(http.StatusConflict))
|
||
})
|
||
|
||
w := do(r, http.MethodPost, "/order", nil)
|
||
fmt.Println("zh:", w.Body.String())
|
||
|
||
w = do(r, http.MethodPost, "/order", map[string]string{"Accept-Language": "en"})
|
||
fmt.Println("en:", w.Body.String())
|
||
|
||
// Output:
|
||
// zh: {"code":50001,"message":"库存不足","data":""}
|
||
// en: {"code":50001,"message":"out of stock","data":""}
|
||
}
|
||
|
||
// ---------- 5. 校验错误带上字段信息 ----------
|
||
|
||
func ExampleErrorWithData() {
|
||
r := newExampleEngine()
|
||
r.POST("/register", func(c *gin.Context) {
|
||
ginx.ErrorWithData(c, errors.New("参数校验失败"),
|
||
gin.H{
|
||
"fields": []string{"email", "phone"},
|
||
"detail": "邮箱格式不正确",
|
||
},
|
||
responsex.SetHttpStatusCode(http.StatusUnprocessableEntity),
|
||
)
|
||
})
|
||
|
||
w := do(r, http.MethodPost, "/register", nil)
|
||
fmt.Println("HTTP", w.Code)
|
||
fmt.Println(body(w))
|
||
// Output:
|
||
// HTTP 422
|
||
// {"code":400,"data":{"detail":"邮箱格式不正确","fields":["email","phone"]},"message":"参数校验失败"}
|
||
}
|
||
|
||
// ---------- 6. 列表分页 ----------
|
||
|
||
func ExampleSuccessWithPage() {
|
||
r := newExampleEngine()
|
||
r.GET("/users", func(c *gin.Context) {
|
||
// Query: ?page=2&size=10
|
||
var page, size int64 = 2, 10
|
||
total := int64(35)
|
||
|
||
// 1) 换算 SQL 的 offset/limit
|
||
offset, limit := responsex.GetOffset(int(page), int(size))
|
||
_, _ = offset, limit
|
||
|
||
// 2) 查库(此处给假数据)
|
||
rows := []gin.H{{"id": 11, "name": "Alice"}, {"id": 12, "name": "Bob"}}
|
||
|
||
// 3) 输出
|
||
ginx.SuccessWithPage(c, rows, responsex.GetPage(page, size, total))
|
||
})
|
||
|
||
w := do(r, http.MethodGet, "/users?page=2&size=10", nil)
|
||
fmt.Println(w.Body.String())
|
||
// Output:
|
||
// {"code":200,"message":"请求成功","data":[{"id":11,"name":"Alice"},{"id":12,"name":"Bob"}],"pagination":{"page":2,"size":10,"total_page":4,"total_count":35}}
|
||
}
|
||
|
||
// ---------- 7. 多语言文案 + 占位符 ----------
|
||
|
||
func ExampleFormatMessage() {
|
||
langx.AppendTrans("zh", map[string]string{
|
||
"ex_order_created": "订单 #sn# 创建成功,金额 #amount# 元",
|
||
})
|
||
langx.AppendTrans("en", map[string]string{
|
||
"ex_order_created": "Order #sn# created, amount #amount#",
|
||
})
|
||
|
||
r := newExampleEngine()
|
||
r.POST("/order", func(c *gin.Context) {
|
||
ginx.MsgMapData(c, "ex_order_created",
|
||
map[string]string{"sn": "SN20240101", "amount": "99.00"},
|
||
gin.H{"order_sn": "SN20240101"},
|
||
)
|
||
})
|
||
|
||
w := do(r, http.MethodPost, "/order", nil)
|
||
fmt.Println("zh:", w.Body.String())
|
||
|
||
w = do(r, http.MethodPost, "/order", map[string]string{"Accept-Language": "en"})
|
||
fmt.Println("en:", w.Body.String())
|
||
|
||
// Output:
|
||
// zh: {"code":200,"message":"订单 SN20240101 创建成功,金额 99.00 元","data":{"order_sn":"SN20240101"}}
|
||
// en: {"code":200,"message":"Order SN20240101 created, amount 99.00","data":{"order_sn":"SN20240101"}}
|
||
}
|
||
|
||
// ---------- 8. 单占位符快捷方法 ----------
|
||
|
||
func ExampleMsgKV() {
|
||
langx.AppendTrans("zh", map[string]string{
|
||
"ex_welcome": "欢迎回来,#name#",
|
||
})
|
||
|
||
r := newExampleEngine()
|
||
r.POST("/login", func(c *gin.Context) {
|
||
ginx.MsgKV(c, "ex_welcome", "name", "Yun")
|
||
})
|
||
|
||
w := do(r, http.MethodPost, "/login", nil)
|
||
fmt.Println(w.Body.String())
|
||
// Output:
|
||
// {"code":200,"message":"欢迎回来,Yun"}
|
||
}
|
||
|
||
// ---------- 9. 自定义响应:非标准 code / 自定义响应头 ----------
|
||
|
||
func ExampleCustom() {
|
||
r := newExampleEngine()
|
||
r.POST("/pay", func(c *gin.Context) {
|
||
ginx.Custom(c,
|
||
responsex.SetCode(20001),
|
||
responsex.SetMessage("支付受理中"),
|
||
responsex.SetData(gin.H{"pay_no": "PAY20240101"}),
|
||
responsex.SetHttpStatusCode(http.StatusAccepted),
|
||
responsex.SetHeader("X-Pay-No", "PAY20240101"),
|
||
)
|
||
})
|
||
|
||
w := do(r, http.MethodPost, "/pay", nil)
|
||
fmt.Println("HTTP", w.Code)
|
||
fmt.Println("X-Pay-No:", w.Header().Get("X-Pay-No"))
|
||
fmt.Println(w.Body.String())
|
||
// Output:
|
||
// HTTP 202
|
||
// X-Pay-No: PAY20240101
|
||
// {"code":20001,"message":"支付受理中","data":{"pay_no":"PAY20240101"}}
|
||
}
|
||
|
||
// ---------- 10. 完整骨架:全局初始化 + 中间件 + 业务错误 + 分页 ----------
|
||
|
||
func Example_completeServer() {
|
||
// 1) 启动时全局初始化(一般放 main / wire)
|
||
responsex.InitOptions(
|
||
responsex.SetDefaultSuccessCode(0),
|
||
responsex.SetDefaultErrorCode(-1),
|
||
)
|
||
defer responsex.InitOptions( // 示例里恢复,避免影响其它用例
|
||
responsex.SetDefaultSuccessCode(200),
|
||
responsex.SetDefaultErrorCode(400),
|
||
)
|
||
|
||
// 2) 路由
|
||
r := gin.New()
|
||
r.Use(ginx.Middleware()) // 关键:桥接 trace_id 与语言
|
||
|
||
r.GET("/goods", func(c *gin.Context) {
|
||
if c.Query("sign") == "" {
|
||
ginx.Error(c, ErrSignInvalid.New(c.Request.Context()))
|
||
return
|
||
}
|
||
ginx.Success(c, gin.H{"goods": []string{"apple", "pear"}})
|
||
})
|
||
|
||
w := do(r, http.MethodGet, "/goods", map[string]string{"X-Request-Id": "t-1"})
|
||
fmt.Println("fail:", w.Body.String())
|
||
|
||
w = do(r, http.MethodGet, "/goods?sign=abc", map[string]string{"X-Request-Id": "t-2"})
|
||
fmt.Println("ok: ", w.Body.String())
|
||
|
||
// Output:
|
||
// fail: {"code":41001,"message":"签名校验失败","data":"","trace_id":"t-1"}
|
||
// ok: {"code":0,"message":"请求成功","data":{"goods":["apple","pear"]},"trace_id":"t-2"}
|
||
}
|