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"} }