Files
responsex/ginx/example_test.go
T
2026-09-19 01:31:34 +08:00

335 lines
10 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/gin"
"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"}
}