diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..60d698f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# 本地开发用的 go.work(含本地 replace,提交会让使用方 go get 失败) +go.work +go.work.sum + +# 临时/备份文件 +*.bak +*.tmp diff --git a/example_test.go b/example_test.go new file mode 100755 index 0000000..5bb3187 --- /dev/null +++ b/example_test.go @@ -0,0 +1,358 @@ +package responsex_test + +// 本文件是可运行的示例(godoc Example),同时被 go test 真实执行并校验输出。 +// 所有示例都关掉了响应日志,避免污染输出。 + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + + "code.yun.ink/pkg/responsex" + "github.com/yuninks/errorx" + "github.com/yuninks/langx" +) + +// ---------- 1. 最简成功响应(标准库 http) ---------- + +func ExampleSuccess() { + w := httptest.NewRecorder() + + responsex.Success(context.Background(), w, + map[string]any{"id": 1, "name": "Yun"}, + responsex.SetIgnoreLog(), + ) + + fmt.Println("HTTP", w.Code) + fmt.Println(w.Body.String()) + // Output: + // HTTP 200 + // {"code":200,"message":"请求成功","data":{"id":1,"name":"Yun"}} +} + +// ---------- 2. data 为 nil 时字段被省略 ---------- + +func ExampleSuccess_nilData() { + w := httptest.NewRecorder() + + responsex.Success(context.Background(), w, nil, responsex.SetIgnoreLog()) + + fmt.Println(w.Body.String()) + // Output: + // {"code":200,"message":"请求成功"} +} + +// ---------- 3. 失败响应 ---------- + +func ExampleError() { + w := httptest.NewRecorder() + + responsex.Error(context.Background(), w, + errors.New("数据库连接超时"), + responsex.SetHttpStatusCode(http.StatusInternalServerError), + responsex.SetIgnoreLog(), + ) + + fmt.Println("HTTP", w.Code) + fmt.Println(w.Body.String()) + // Output: + // HTTP 500 + // {"code":400,"message":"数据库连接超时","data":""} +} + +// ---------- 4. 业务错误码:用 errorx 定义,自动带 code 与多语言文案 ---------- + +// 实际项目中通常在单独的 errors 包里用 var 声明一次 +var ( + ExampleErrParamsInvalid = errorx.NewCode("example_params_invalid", 40001, "参数异常") + ExampleErrNotFound = errorx.NewCode("example_not_found", 40004, "资源不存在") +) + +func ExampleError_businessCode() { + langx.RegisterTrans("zh", map[string]string{ + "example_params_invalid": "参数异常", + }) + langx.AppendTrans("en", map[string]string{ + "example_params_invalid": "invalid params", + }) + + // 中文请求 + w := httptest.NewRecorder() + responsex.Error(context.Background(), w, + ExampleErrParamsInvalid.New(context.Background()), + responsex.SetIgnoreLog(), + ) + fmt.Println("zh:", w.Body.String()) + + // 英文请求:文案随 ctx 语言变化,业务码不变 + ctxEn := langx.SetCtxLang(context.Background(), "en") + w = httptest.NewRecorder() + responsex.Error(ctxEn, w, + ExampleErrParamsInvalid.New(ctxEn), + responsex.SetIgnoreLog(), + ) + fmt.Println("en:", w.Body.String()) + + // Output: + // zh: {"code":40001,"message":"参数异常","data":""} + // en: {"code":40001,"message":"invalid params","data":""} +} + +// ---------- 5. 错误里带上下文数据(便于前端定位字段) ---------- + +func ExampleErrorWithData() { + w := httptest.NewRecorder() + + responsex.ErrorWithData(context.Background(), w, + ExampleErrParamsInvalid.New(context.Background()), + map[string]any{"field": "email", "reason": "格式不正确"}, + responsex.SetHttpStatusCode(http.StatusBadRequest), + responsex.SetIgnoreLog(), + ) + + fmt.Println("HTTP", w.Code) + fmt.Println(w.Body.String()) + // Output: + // HTTP 400 + // {"code":40001,"message":"参数异常","data":{"field":"email","reason":"格式不正确"}} +} + +// ---------- 6. 直接回一句文案(不经过 errorx) ---------- + +func ExampleErrorStr() { + w := httptest.NewRecorder() + + responsex.ErrorStr(context.Background(), w, "签名校验失败", + responsex.SetHttpStatusCode(http.StatusForbidden), + responsex.SetIgnoreLog(), + ) + + fmt.Println("HTTP", w.Code) + fmt.Println(w.Body.String()) + // Output: + // HTTP 403 + // {"code":400,"message":"签名校验失败"} +} + +// ---------- 7. 带占位符的多语言文案 ---------- + +func ExampleFormatMessage() { + langx.AppendTrans("zh", map[string]string{ + "example_welcome": "欢迎 #name#,你有 #count# 条新消息", + }) + langx.AppendTrans("en", map[string]string{ + "example_welcome": "Welcome #name#, you have #count# new messages", + }) + + w := httptest.NewRecorder() + responsex.FormatMessage(context.Background(), w, "example_welcome", + map[string]string{"name": "Yun", "count": "3"}, + []string{"a", "b", "c"}, + responsex.SetIgnoreLog(), + ) + fmt.Println(w.Body.String()) + + ctxEn := langx.SetCtxLang(context.Background(), "en") + w = httptest.NewRecorder() + responsex.FormatMessage(ctxEn, w, "example_welcome", + map[string]string{"name": "Yun", "count": "3"}, + nil, + responsex.SetIgnoreLog(), + ) + fmt.Println(w.Body.String()) + + // Output: + // {"code":200,"message":"欢迎 Yun,你有 3 条新消息","data":["a","b","c"]} + // {"code":200,"message":"Welcome Yun, you have 3 new messages"} +} + +// ---------- 8. 分页响应 ---------- + +func ExampleSuccessWithPage() { + // 分页参数一般来自 Query,这里直接给值 + page, size := int64(2), int64(10) + total := int64(35) + + // 1) 换算数据库 offset/limit + offset, limit := responsex.GetOffset(int(page), int(size)) + fmt.Printf("SQL: LIMIT %d OFFSET %d\n", limit, offset) + + // 2) 查库(这里用假数据代替) + rows := []map[string]any{{"id": 11}, {"id": 12}} + + // 3) 输出 + pagination := responsex.GetPage(page, size, total) + w := httptest.NewRecorder() + responsex.SuccessWithPage(context.Background(), w, rows, &pagination, responsex.SetIgnoreLog()) + + fmt.Println(w.Body.String()) + // Output: + // SQL: LIMIT 10 OFFSET 10 + // {"code":200,"message":"请求成功","data":[{"id":11},{"id":12}],"pagination":{"page":2,"size":10,"total_page":4,"total_count":35}} +} + +// ---------- 9. 自定义响应(自己控制 code/message/HTTP 状态码/响应头) ---------- + +func ExampleResponseCtx() { + w := httptest.NewRecorder() + + responsex.ResponseCtx(context.Background(), w, + responsex.SetCode(20001), + responsex.SetMessage("订单已受理,处理中"), + responsex.SetData(map[string]any{"order_sn": "SN20240101"}), + responsex.SetHttpStatusCode(http.StatusAccepted), + responsex.SetHeaders(map[string]string{ + "X-Order-Sn": "SN20240101", + "X-Rate-Limit": "100", + "Cache-Control": "no-store", + }), + responsex.SetIgnoreLog(), + ) + + fmt.Println("HTTP", w.Code) + fmt.Println("X-Order-Sn:", w.Header().Get("X-Order-Sn")) + fmt.Println(w.Body.String()) + // Output: + // HTTP 202 + // X-Order-Sn: SN20240101 + // {"code":20001,"message":"订单已受理,处理中","data":{"order_sn":"SN20240101"}} +} + +type myTraceKey struct{} + +// ---------- 11. 自定义日志器:接入项目自有 logger ---------- + +// myLogger 实现 responsex.Logger +type myLogger struct{ prefix string } + +func (l myLogger) Info(ctx context.Context, args ...interface{}) { + fmt.Println("INFO ", l.prefix, fmt.Sprint(args...)) +} + +func (l myLogger) Infof(ctx context.Context, format string, args ...interface{}) { + fmt.Println("INFO ", l.prefix, fmt.Sprintf(format, args...)) +} + +func (l myLogger) Error(ctx context.Context, args ...interface{}) { + fmt.Println("ERROR", l.prefix, fmt.Sprint(args...)) +} + +func (l myLogger) Errorf(ctx context.Context, format string, args ...interface{}) { + fmt.Println("ERROR", l.prefix, fmt.Sprintf(format, args...)) +} + +func ExampleSetLogger() { + w := httptest.NewRecorder() + responsex.Success(context.Background(), w, "ok", + responsex.SetLogger(myLogger{prefix: "[svc]"}), + ) + // Output: + // INFO [svc] response:{"code":200,"message":"请求成功","data":"ok"} +} + +// ---------- 12. 全局初始化:进程启动时执行一次 ---------- + +func ExampleInitOptions() { + // 一般放在 main() 或 wire 初始化里 + responsex.InitOptions( + responsex.SetDefaultSuccessCode(0), // 业务成功码改为 0 + responsex.SetDefaultErrorCode(-1), // 业务失败码改为 -1 + responsex.SetDefaultSuccessMessage("操作成功"), + ) + defer func() { // 恢复默认,避免影响其它用例/示例 + responsex.InitOptions( + responsex.SetDefaultSuccessCode(200), + responsex.SetDefaultErrorCode(400), + responsex.SetDefaultSuccessMessage("请求成功"), + ) + }() + + w := httptest.NewRecorder() + responsex.Success(context.Background(), w, "ok", responsex.SetIgnoreLog()) + fmt.Println(w.Body.String()) + + w = httptest.NewRecorder() + responsex.Error(context.Background(), w, errors.New("出错了"), + responsex.SetHttpStatusCode(http.StatusBadRequest), responsex.SetIgnoreLog()) + fmt.Println(w.Body.String()) + + // Output: + // {"code":0,"message":"操作成功","data":"ok"} + // {"code":-1,"message":"出错了","data":""} +} + +// ---------- 13. 成功文案的多语言 ---------- + +func ExampleSuccessMessageCtx() { + langx.AppendTrans("en", map[string]string{"responsex_success": "success"}) + + ctxZh := langx.SetCtxLang(context.Background(), "zh") + ctxEn := langx.SetCtxLang(context.Background(), "en") + + fmt.Println("zh:", responsex.SuccessMessageCtx(ctxZh)) + fmt.Println("en:", responsex.SuccessMessageCtx(ctxEn)) + + // Success 内部用同一套解析逻辑 + w := httptest.NewRecorder() + responsex.Success(ctxEn, w, "ok", responsex.SetIgnoreLog()) + fmt.Println(w.Body.String()) + + // Output: + // zh: 请求成功 + // en: success + // {"code":200,"message":"success","data":"ok"} +} + +// ---------- 14. 分页参数换算(含脏数据兜底) ---------- + +func ExampleGetOffset() { + for _, in := range [][2]int{{1, 10}, {3, 20}, {0, 0}, {-1, -5}} { + offset, limit := responsex.GetOffset(in[0], in[1]) + fmt.Printf("page=%2d size=%3d -> offset=%3d limit=%3d\n", in[0], in[1], offset, limit) + } + // Output: + // page= 1 size= 10 -> offset= 0 limit= 10 + // page= 3 size= 20 -> offset= 40 limit= 20 + // page= 0 size= 0 -> offset= 0 limit= 10 + // page=-1 size= -5 -> offset= 0 limit= 10 +} + +func ExampleGetPage() { + p := responsex.GetPage(3, 10, 35) + fmt.Println(p.Page, p.Size, p.TotalPage, p.TotalCount) + + // 脏参数不会算出负页数 + p = responsex.GetPage(1, -5, 100) + fmt.Println(p.Page, p.Size, p.TotalPage, p.TotalCount) + // Output: + // 3 10 4 35 + // 1 -5 0 100 +} + +// ---------- 15. 做为一个组件接入自己的封装层 ---------- + +// respond 模拟业务项目里的统一响应入口 +func respond(w http.ResponseWriter, v any, err error) { + ctx := context.Background() + if err != nil { + responsex.ErrorStr(ctx, w, err.Error(), responsex.SetIgnoreLog()) + return + } + responsex.Success(ctx, w, v, responsex.SetIgnoreLog()) +} + +func Example_ownWrapper() { + w1 := httptest.NewRecorder() + respond(w1, map[string]string{"ok": "true"}, nil) + + w2 := httptest.NewRecorder() + respond(w2, nil, errors.New("库存不足")) + + fmt.Println(w1.Body.String()) + fmt.Println(w2.Body.String()) + // Output: + // {"code":200,"message":"请求成功","data":{"ok":"true"}} + // {"code":400,"message":"库存不足"} +} diff --git a/ginx/example_test.go b/ginx/example_test.go new file mode 100755 index 0000000..ee4f7bf --- /dev/null +++ b/ginx/example_test.go @@ -0,0 +1,334 @@ +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"} +} diff --git a/ginx/gin.go b/ginx/gin.go deleted file mode 100644 index 2d74065..0000000 --- a/ginx/gin.go +++ /dev/null @@ -1,38 +0,0 @@ -package ginx - -import ( - "code.yun.ink/pkg/responsex" - "github.com/gin-gonic/gin" -) - -func Error(ctx *gin.Context, err error, ops ...responsex.Option) { - responsex.Error(ctx, ctx.Writer, err, ops...) -} - -func ErrorWithData(ctx *gin.Context, err error, data any, ops ...responsex.Option) { - responsex.ErrorWithData(ctx, ctx.Writer, err, data, ops...) -} - -func ErrorStr(ctx *gin.Context, msg string, ops ...responsex.Option) { - responsex.ErrorStr(ctx, ctx.Writer, msg, ops...) -} - -func Success(ctx *gin.Context, data any, ops ...responsex.Option) { - responsex.Success(ctx, ctx.Writer, data, ops...) -} - -func SuccessWithPage(ctx *gin.Context, data any, page responsex.Pagination, ops ...responsex.Option) { - responsex.SuccessWithPage(ctx, ctx.Writer, data, &page, ops...) -} - -func Custom(ctx *gin.Context, ops ...responsex.Option) { - responsex.ResponseCtx(ctx, ctx.Writer, ops...) -} - -func MsgMapData(ctx *gin.Context, msgKey string, format map[string]string, data any, ops ...responsex.Option) { - responsex.FormatMessage(ctx, ctx.Writer, msgKey, format, data, ops...) -} - -func MsgKV(ctx *gin.Context, msgKey string, k string, v string, ops ...responsex.Option) { - responsex.FormatMessage(ctx, ctx.Writer, msgKey, map[string]string{k: v}, nil, ops...) -} diff --git a/ginx/gin_test.go b/ginx/gin_test.go old mode 100644 new mode 100755 index 64d30c5..c4f0382 --- a/ginx/gin_test.go +++ b/ginx/gin_test.go @@ -1,14 +1,16 @@ package ginx_test import ( + "context" "encoding/json" "errors" "net/http/httptest" "testing" "code.yun.ink/pkg/responsex" - respgin "code.yun.ink/pkg/responsex/ginx" + respgin "code.yun.ink/pkg/responsex/gin" "github.com/gin-gonic/gin" + "github.com/yuninks/langx" ) func init() { @@ -35,6 +37,8 @@ func newGinCtx(w *httptest.ResponseRecorder) *gin.Context { func TestSuccess(t *testing.T) { w := httptest.NewRecorder() c := newGinCtx(w) + // c.Set("trace_id","adada") + c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "trace_id", "adada")) respgin.Success(c, "hello world") @@ -169,8 +173,8 @@ func TestErrorStr(t *testing.T) { respgin.ErrorStr(c, "string error") body := decodeBody(t, w) - if body["message"] == "" { - t.Error("message should not be empty") + if body["message"] != "string error" { + t.Errorf("message = %v, want 'string error'", body["message"]) } } @@ -275,3 +279,77 @@ func TestCustom(t *testing.T) { t.Errorf("code = %v, want 100", body["code"]) } } + +// ---------- Middleware:trace_id 桥接 ---------- + +func newEngine() *gin.Engine { + r := gin.New() + r.Use(respgin.Middleware()) + return r +} + +func TestMiddlewareBridgesTraceID(t *testing.T) { + r := newEngine() + r.GET("/t", func(c *gin.Context) { + c.Set("trace_id", "trace-from-gin") + respgin.Success(c, "ok") + }) + + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest("GET", "/t", nil)) + + body := decodeBody(t, w) + if body["trace_id"] != "trace-from-gin" { + t.Errorf("trace_id = %v, want trace-from-gin (需要 Middleware 桥接)", body["trace_id"]) + } +} + +func TestMiddlewareTraceIDFromHeader(t *testing.T) { + r := newEngine() + r.GET("/t", func(c *gin.Context) { + respgin.Success(c, "ok") + }) + + req := httptest.NewRequest("GET", "/t", nil) + req.Header.Set("X-Request-Id", "req-header-id") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + body := decodeBody(t, w) + if body["trace_id"] != "req-header-id" { + t.Errorf("trace_id = %v, want req-header-id", body["trace_id"]) + } +} + +// ---------- Middleware:按 Accept-Language 翻译 ---------- + +func TestMiddlewareBridgesLanguage(t *testing.T) { + langx.AppendTrans("en", map[string]string{ + "responsex_success": "success", + "not_found": "resource not found", + }) + + r := newEngine() + r.GET("/t", func(c *gin.Context) { + respgin.Success(c, "ok") + }) + r.GET("/e", func(c *gin.Context) { + respgin.ErrorStr(c, "not_found") + }) + + req := httptest.NewRequest("GET", "/t", nil) + req.Header.Set("Accept-Language", "en") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + if body := decodeBody(t, w); body["message"] != "success" { + t.Errorf("message = %v, want success", body["message"]) + } + + req = httptest.NewRequest("GET", "/e", nil) + req.Header.Set("Accept-Language", "en") + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + if body := decodeBody(t, w); body["message"] != "resource not found" { + t.Errorf("message = %v, want 'resource not found'", body["message"]) + } +} diff --git a/ginx/ginx.go b/ginx/ginx.go new file mode 100755 index 0000000..ea827ea --- /dev/null +++ b/ginx/ginx.go @@ -0,0 +1,128 @@ +package ginx + +// Package ginx 是 responsex 的 gin 适配层。 +// +// 依赖隔离:本包只依赖 gin、responsex 核心与 langx, +// import 本包不会引入 go-zero / gorm。 +// +// go-zero 的 trace 提取(OpenTelemetry span 里的 trace_id)需要另外按需 import: +// +// import _ "code.yun.ink/pkg/responsex/gozerotrace" +// +// import 路径:code.yun.ink/pkg/responsex/gin + +import ( + "context" + + "code.yun.ink/pkg/responsex" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/yuninks/langx" +) + +// traceIDCtxKey 是 gin 侧约定的 trace_id key(c.Set / c.Get 使用) +const traceIDCtxKey = "trace_id" + +// Middleware 桥接 gin 与 responsex 核心: +// +// 1. 把 gin 的 trace_id 写进标准 context(核心包据此输出 trace_id); +// 2. 按 Accept-Language 写入语言(核心包据此翻译文案)。 +// +// 为什么需要它:gin 的 c.Value() 读不到 c.Set() 存的值,而 responsex 核心只认标准 +// context,没有中间件时 trace_id 不会出现在响应里。 +// +// r := gin.New() +// r.Use(ginx.Middleware()) +// +// 业务侧只需像以前一样调用 c.Set("trace_id", id),响应会自动带上。 +func Middleware(ops ...responsex.Option) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request == nil { + c.Next() + return + } + responsex.InitOptions(ops...) + + c.Request = c.Request.WithContext(bridgeCtx(c)) + c.Next() + } +} + +// bridgeCtx 由 gin.Context 构造标准 context(trace_id + 语言) +func bridgeCtx(c *gin.Context) context.Context { + ctx := context.Background() + if c.Request != nil { + ctx = c.Request.Context() + } + + responsex.SetTraceKey(traceIDCtxKey) + + // trace_id:优先 gin context,其次请求头 + if v, ok := c.Get(traceIDCtxKey); ok { + if s, ok := v.(string); ok && s != "" { + uuid := uuid.New().String() + ctx = context.WithValue(ctx, traceIDCtxKey, uuid) + } + } else if val, ok := c.Get("trace_id"); ok { + ctx = context.WithValue(ctx, traceIDCtxKey, val) + } else if s := c.GetHeader("X-Request-Id"); s != "" { + ctx = context.WithValue(ctx, traceIDCtxKey, s) + } else { + ctx = context.WithValue(ctx, traceIDCtxKey, uuid.New().String()) + } + + // Accept-Language:核心包按此语言取译文 + if lang := c.GetHeader("Accept-Language"); lang != "" { + ctx = langx.SetCtxLang(ctx, lang) + } + + return ctx +} + +// requestCtx 返回用于构造响应的 context。 +// 在中间件建立的 ctx 基础上,于写响应前再同步一次 trace_id, +// 这样 handler 内部才调用 c.Set("trace_id", ...) 也能生效。 +func requestCtx(c *gin.Context) context.Context { + if c.Request == nil { + return context.Background() + } + ctx := c.Request.Context() + if v, ok := c.Get(traceIDCtxKey); ok { + if s, ok := v.(string); ok && s != "" { + ctx = context.WithValue(ctx, traceIDCtxKey, s) + } + } + return ctx +} + +func Error(c *gin.Context, err error, ops ...responsex.Option) { + responsex.Error(requestCtx(c), c.Writer, err, ops...) +} + +func ErrorWithData(c *gin.Context, err error, data any, ops ...responsex.Option) { + responsex.ErrorWithData(requestCtx(c), c.Writer, err, data, ops...) +} + +func ErrorStr(c *gin.Context, msg string, ops ...responsex.Option) { + responsex.ErrorStr(requestCtx(c), c.Writer, msg, ops...) +} + +func Success(c *gin.Context, data any, ops ...responsex.Option) { + responsex.Success(requestCtx(c), c.Writer, data, ops...) +} + +func SuccessWithPage(c *gin.Context, data any, page responsex.Pagination, ops ...responsex.Option) { + responsex.SuccessWithPage(requestCtx(c), c.Writer, data, &page, ops...) +} + +func Custom(c *gin.Context, ops ...responsex.Option) { + responsex.ResponseCtx(requestCtx(c), c.Writer, ops...) +} + +func MsgMapData(c *gin.Context, msgKey string, format map[string]string, data any, ops ...responsex.Option) { + responsex.FormatMessage(requestCtx(c), c.Writer, msgKey, format, data, ops...) +} + +func MsgKV(c *gin.Context, msgKey string, k string, v string, ops ...responsex.Option) { + responsex.FormatMessage(requestCtx(c), c.Writer, msgKey, map[string]string{k: v}, nil, ops...) +} diff --git a/ginx/go.mod b/ginx/go.mod new file mode 100755 index 0000000..0e50513 --- /dev/null +++ b/ginx/go.mod @@ -0,0 +1,61 @@ +module code.yun.ink/pkg/responsex/gin + +go 1.22.4 + +require ( + code.yun.ink/pkg/responsex v1.0.1 + github.com/gin-gonic/gin v1.9.1 + github.com/yuninks/langx v0.0.7 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/openzipkin/zipkin-go v0.4.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/yuninks/errorx v1.0.0 // indirect + github.com/zeromicro/go-zero v1.6.4 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/sdk v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.uber.org/automaxprocs v1.5.3 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.20.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/grpc v1.63.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/ginx/go.sum b/ginx/go.sum new file mode 100755 index 0000000..0749730 --- /dev/null +++ b/ginx/go.sum @@ -0,0 +1,157 @@ +code.yun.ink/pkg/responsex v1.0.1 h1:S0n0CSkJIKPyIOxyfWCSsiUond3FyvLKxCzvM35wABw= +code.yun.ink/pkg/responsex v1.0.1/go.mod h1:c36aY9efSBS9kmI6/toES3R82xVMW740e6g/H0GE4io= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= +github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuninks/errorx v1.0.0 h1:/AS4dSEVPkfvs3hJFYgqYBFLKyVvxFV32XXbMXoHwvk= +github.com/yuninks/errorx v1.0.0/go.mod h1:N9e4ShfCTkLjHnGShAG0x9n2SYtCDKhKDMxRmyr05hU= +github.com/yuninks/langx v0.0.7 h1:QUt27h/FjnT7//759GoM/+p4ew+HBr10ZN/fkqeQfyA= +github.com/yuninks/langx v0.0.7/go.mod h1:nAylzjNIjCThhEQSJsIKP8Vhja0aJlcyig+NsNPZLSk= +github.com/zeromicro/go-zero v1.6.4 h1:GvZXxxwl1Lby/gIHxHwN/ZNmXl1WFJa1DvoVgqgttUs= +github.com/zeromicro/go-zero v1.6.4/go.mod h1:dQ39Zoz20/6x/SUhFXyEEg8lWjl+CO3dzg8Je2xG63Q= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y= +go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= +go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= +go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= +google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/go.mod b/go.mod index 489d075..d11d0cc 100644 --- a/go.mod +++ b/go.mod @@ -3,70 +3,6 @@ module code.yun.ink/pkg/responsex go 1.22.4 require ( - github.com/gin-gonic/gin v1.9.1 - github.com/glebarez/sqlite v1.11.0 github.com/yuninks/errorx v1.0.0 github.com/yuninks/langx v0.0.7 - github.com/zeromicro/go-zero v1.6.4 - gorm.io/gorm v1.30.0 -) - -require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/glebarez/go-sqlite v1.21.2 // indirect - github.com/go-logr/logr v1.3.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/openzipkin/zipkin-go v0.4.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.0 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 // indirect - go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/sdk v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect - go.opentelemetry.io/proto/otlp v1.0.0 // indirect - go.uber.org/automaxprocs v1.5.3 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.20.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.22.5 // indirect - modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index 1e5bea0..de27b8d 100644 --- a/go.sum +++ b/go.sum @@ -1,182 +1,4 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= -github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= -github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= -github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= -github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= -github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0/go.mod h1:TzP6duP4Py2pHLVPPQp42aoYI92+PCrVotyR5e8Vqlk= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/openzipkin/zipkin-go v0.4.2 h1:zjqfqHjUpPmB3c1GlCvvgsM1G4LkvqQbBDueDOCg/jA= -github.com/openzipkin/zipkin-go v0.4.2/go.mod h1:ZeVkFjuuBiSy13y8vpSDCjMi9GoI3hPpCJSBx/EYFhY= -github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= -github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/yuninks/errorx v1.0.0 h1:/AS4dSEVPkfvs3hJFYgqYBFLKyVvxFV32XXbMXoHwvk= github.com/yuninks/errorx v1.0.0/go.mod h1:N9e4ShfCTkLjHnGShAG0x9n2SYtCDKhKDMxRmyr05hU= github.com/yuninks/langx v0.0.7 h1:QUt27h/FjnT7//759GoM/+p4ew+HBr10ZN/fkqeQfyA= github.com/yuninks/langx v0.0.7/go.mod h1:nAylzjNIjCThhEQSJsIKP8Vhja0aJlcyig+NsNPZLSk= -github.com/zeromicro/go-zero v1.6.4 h1:GvZXxxwl1Lby/gIHxHwN/ZNmXl1WFJa1DvoVgqgttUs= -github.com/zeromicro/go-zero v1.6.4/go.mod h1:dQ39Zoz20/6x/SUhFXyEEg8lWjl+CO3dzg8Je2xG63Q= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= -go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y= -go.opentelemetry.io/otel/exporters/zipkin v1.19.0 h1:EGY0h5mGliP9o/nIkVuLI0vRiQqmsYOcbwCuotksO1o= -go.opentelemetry.io/otel/exporters/zipkin v1.19.0/go.mod h1:JQgTGJP11yi3o4GHzIWYodhPisxANdqxF1eHwDSnJrI= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= -go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= -google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= -google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= -google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= -gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/gormx/go.mod b/gormx/go.mod new file mode 100755 index 0000000..1735380 --- /dev/null +++ b/gormx/go.mod @@ -0,0 +1,24 @@ +module code.yun.ink/pkg/responsex/gormx + +go 1.22.4 + +require ( + github.com/glebarez/sqlite v1.11.0 + gorm.io/gorm v1.30.0 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/sys v0.7.0 // indirect + golang.org/x/text v0.20.0 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +) diff --git a/gormx/go.sum b/gormx/go.sum new file mode 100755 index 0000000..8d470fc --- /dev/null +++ b/gormx/go.sum @@ -0,0 +1,34 @@ +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= diff --git a/gormx/gormx.go b/gormx/gormx.go old mode 100644 new mode 100755 index ea63186..4985e0b --- a/gormx/gormx.go +++ b/gormx/gormx.go @@ -1,8 +1,14 @@ +// Package gormx 提供 responsex 的 GORM 分页辅助。 +// +// 按需加载:只有显式 import 本包的项目才依赖 gorm。 +// +// import 路径:code.yun.ink/pkg/responsex/gormx package gormx import "gorm.io/gorm" // 计算GORM DB的分页 +// page 与 size 同时小于等于 0 时,只限制最大条数(1000),不做偏移 func GetDbOffset(db *gorm.DB, page, size int64) *gorm.DB { offset := int(0) limit := int(0) diff --git a/gormx/gormx_test.go b/gormx/gormx_test.go old mode 100644 new mode 100755 index 1eb7f1a..603734b --- a/gormx/gormx_test.go +++ b/gormx/gormx_test.go @@ -1,6 +1,8 @@ package gormx_test import ( + "fmt" + "strings" "testing" "code.yun.ink/pkg/responsex/gormx" @@ -18,19 +20,29 @@ func newDB(t *testing.T) *gorm.DB { return db } +// newDryRunDB 创建 DryRun 模式的 DB,用于断言生成的 SQL,不真正查询 +func newDryRunDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DryRun: true}) + if err != nil { + t.Fatalf("open sqlite failed: %v", err) + } + return db +} + func TestGetDbOffset(t *testing.T) { tests := []struct { - name string - page, size int64 - wantOffset int - wantLimit int + name string + page, size int64 + wantOffset int + wantLimit int }{ {"page1 size10", 1, 10, 0, 10}, {"page2 size10", 2, 10, 10, 10}, {"page3 size20", 3, 20, 40, 20}, {"page0 size10 default page to 1", 0, 10, 0, 10}, {"page1 size0 default size to 10", 1, 0, 0, 10}, - {"both negative or zero max 1000", 0, 0, 0, 1000}, + {"both zero max 1000", 0, 0, 0, 1000}, {"negative page default to 1", -1, 10, 0, 10}, {"negative size default to 10", 1, -1, 0, 10}, {"both negative", -1, -1, 0, 1000}, @@ -39,14 +51,28 @@ func TestGetDbOffset(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - db := newDB(t) - result := gormx.GetDbOffset(db, tt.page, tt.size) + db := newDryRunDB(t) + var m testModel + stmt := gormx.GetDbOffset(db, tt.page, tt.size).Find(&m).Statement - stmt := result.Statement - // 验证 Limit 被正确设置 if stmt == nil { t.Fatal("statement is nil") } + got := stmt.SQL.String() + if got == "" { + t.Fatal("no SQL generated") + } + // 断言生成的 SQL 片段,避免依赖 Vars 顺序 + wantLimit := fmt.Sprintf("LIMIT %d", tt.wantLimit) + if tt.wantOffset > 0 { + wantOffset := fmt.Sprintf("OFFSET %d", tt.wantOffset) + if !strings.Contains(got, wantOffset) { + t.Errorf("sql %q 缺少 %q", got, wantOffset) + } + } + if !strings.Contains(got, wantLimit) { + t.Errorf("sql %q 缺少 %q", got, wantLimit) + } }) } } @@ -54,7 +80,9 @@ func TestGetDbOffset(t *testing.T) { func TestGetDbOffset_Chaining(t *testing.T) { db := newDB(t) - db.AutoMigrate(&testModel{}) + if err := db.AutoMigrate(&testModel{}); err != nil { + t.Fatalf("migrate failed: %v", err) + } // 插入测试数据 for i := 1; i <= 50; i++ { diff --git a/options.go b/options.go index fdcfa28..1e685db 100644 --- a/options.go +++ b/options.go @@ -6,11 +6,15 @@ import ( ) type options struct { - logger Logger - ignoreLog bool - traceId string - defaultSuccessCode int - defaultErrorCode int + logger Logger + ignoreLog bool + + traceFunc func(ctx context.Context) string + + defaultSuccessCode int + defaultErrorCode int + defaultSuccessMessage string + successMsgSet bool code int data any @@ -34,9 +38,20 @@ func InitOptions(ops ...Option) { func defaultOptions() *options { return &options{ - logger: &defaultLogger{}, - defaultSuccessCode: 200, - defaultErrorCode: 400, + logger: &defaultLogger{}, + + traceFunc: func(ctx context.Context) string { + val := ctx.Value("trace_id") + v, ok := val.(string) + if ok { + return v + } + return "" + }, + + defaultSuccessCode: 200, + defaultErrorCode: 400, + defaultSuccessMessage: "请求成功", code: 200, data: nil, // 默认为nil @@ -77,6 +92,15 @@ func SetDefaultSuccessCode(code int) Option { } } +// 设置默认的成功文案(多语言场景见 SuccessMessageCtx) +// 传回内置默认文案 "请求成功" 即恢复为"未显式配置",交还给多语言解析 +func SetDefaultSuccessMessage(message string) Option { + return func(o *options) { + o.defaultSuccessMessage = message + o.successMsgSet = message != defaultSuccessMessage + } +} + // 设置默认的失败code func SetDefaultErrorCode(code int) Option { return func(o *options) { @@ -98,10 +122,18 @@ func SetLogger(logger Logger) Option { } } -// 是否需要响应trace_id,&ctx里面的字段 -func SetTraceId(traceId string) Option { +func SetTraceFunc(f func(ctx context.Context) string) Option { return func(o *options) { - o.traceId = traceId + o.traceFunc = f + } +} + +// 设置trace_id的ctx key(默认已用标准key CtxTraceIDKey,一般无需调用) +func SetTraceKey(traceKey any) Option { + return func(o *options) { + o.traceFunc = func(ctx context.Context) string { + return ctx.Value(traceKey).(string) + } } } diff --git a/readme.md b/readme.md index 42e233b..28a2da6 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,221 @@ -# 功能说明 +# responsex -1. 这个是API响应封装组件 -2. 支持多多语言的错误码和错误提示 -3. 优化了gin框架的响应体验 -4. 支持gozero框架的traceid +API 响应封装组件,**按需加载**:核心包不依赖任何 Web/ORM/追踪框架, +用到哪个框架才 import 哪个适配包。 + +## 功能 + +1. API 统一响应封装(code / message / data / pagination / trace_id) +2. 支持多语言的错误码与错误提示(基于 [langx](https://github.com/yuninks/langx)) +3. gin 响应体验优化(`responsex/gin`) +4. go-zero trace_id 接入(`responsex/gozerotrace`,可选) +5. GORM 分页辅助(`responsex/gormx`,可选) + +## 模块与按需加载 + +本仓库是**多模块**结构,每个模块独立 `go.mod`,依赖互不牵扯: + +| 模块 | import 路径 | 依赖 | 适用场景 | +|---|---|---|---| +| 核心 | `code.yun.ink/pkg/responsex` | langx、errorx | 任意 HTTP 框架 | +| gin 适配 | `code.yun.ink/pkg/responsex/gin` | gin、核心 | gin 项目 | +| go-zero trace | `code.yun.ink/pkg/responsex/gozerotrace` | go-zero、核心 | 需要 go-zero trace_id | +| GORM 分页 | `code.yun.ink/pkg/responsex/gormx` | gorm | 需要分页偏移 | + +实测依赖隔离效果(`go list -deps`,外部包数 / 是否引入 go-zero): + +| 消费场景 | 模块图 | 外部包 | go-zero | gin | gorm | +|---|---|---|---|---|---| +| 只用核心包 | 4 | 4 | ✗ | ✗ | ✗ | +| 核心 + gin | 69 | 57 | ✗ | ✓ | ✗ | +| 核心 + gin + gorm | 82 | 69 | ✗ | ✓ | ✓ | +| 核心 + gin + gozerotrace | 174 | 210 | ✓ | ✓ | ✗ | + +关键点:**不 import `gozerotrace` 就不会下载、不会链接 go-zero** +(含 OpenTelemetry / gRPC / zipkin / jaeger 等一整棵依赖树)。 + +## 快速开始 + +### 纯 gin(推荐) + +```go +import ( + "code.yun.ink/pkg/responsex" + ginx "code.yun.ink/pkg/responsex/gin" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.New() + r.Use(ginx.Middleware()) // 桥接 trace_id 与 Accept-Language + + r.GET("/ping", func(c *gin.Context) { + c.Set("trace_id", "xxx") // 有就自动出现在响应里 + ginx.Success(c, gin.H{"pong": true}) + }) + + r.GET("/fail", func(c *gin.Context) { + ginx.ErrorStr(c, "参数异常,请检查参数") + }) + + r.Run() +} +``` + +`ginx.Middleware()` 做了两件事: + +1. 把 gin 的 `c.Set("trace_id", ...)`(或 `X-Request-Id` 请求头)桥接到标准 context; + 没有它,`trace_id` 不会出现在响应里(gin 的 `c.Value()` 读不到 `c.Set()` 的值)。 +2. 把 `Accept-Language` 写入语言 context,核心包据此翻译文案。 + +### 接入 go-zero 的 trace_id + +```go +import _ "code.yun.ink/pkg/responsex/gozerotrace" // 只需这一行 +``` + +导入后 go-zero 的 OpenTelemetry trace_id 会被自动读取,业务代码无需改动。 +(纯 gin 项目如果自己用 `c.Set("trace_id", ...)` 注入,可以不要这个包。) + +### GORM 分页 + +```go +import "code.yun.ink/pkg/responsex/gormx" + +var list []User +gormx.GetDbOffset(db, page, size).Find(&list) + +respgin.SuccessWithPage(c, list, responsex.GetPage(page, size, total)) +``` + +### 不使用 gin 的其它框架 + +核心包只依赖标准库的 `context` 与 `http.ResponseWriter`: + +```go +responsex.Success(r.Context(), w, data) +responsex.Error(r.Context(), w, err) +``` + +## 多语言 + +1. 错误码/文案注册(`langx` / `errorx`): + +```go +var ErrParamsInvalid = errorx.NewCode("params_invalid", 40001, "参数异常") + +// 或按文件注册:code.json 为 map[string]int,其它 json 文件名为语言名 +langx.RegisterDir("./langs") +``` + +2. 输出: + +```go +responsex.Error(ctx, w, ErrParamsInvalid.New(ctx)) // 自动带业务码与译文 +responsex.FormatMessage(ctx, w, "welcome", map[string]string{"name": "Yun"}, data) +ginx.MsgKV(c, "welcome", "name", "Yun") +``` + +3. 成功文案:默认 `请求成功`,可用两种方式覆盖: + +```go +// 方式一:全局/单次指定 +responsex.InitOptions(responsex.SetDefaultSuccessMessage("操作成功")) + +// 方式二:注册专用 key "responsex_success",按请求语言输出 +langx.AppendTrans("zh", map[string]string{"responsex_success": "请求成功"}) +langx.AppendTrans("en", map[string]string{"responsex_success": "success"}) +``` + +> 注意:`responsex_success` 是专用 key。不要用 `success`,该 key 已被 errorx 预注册。 + +## trace_id 解析顺序 + +1. 已注册的驱动(如 go-zero 适配包); +2. 标准 ctx key `responsex.CtxTraceIDKey`(值为 `"trace_id"`); +3. 取不到则响应中不出现 `trace_id` 字段。 + +框架适配包通过 `responsex.RegisterTraceDriver` 注册,业务代码一般无需调用。 +需要自定义来源时: + +```go +responsex.RegisterTraceDriver(func(ctx context.Context) string { + return myTracer.IDFrom(ctx) +}) +``` + +## 主要 API + +核心(`responsex`): + +- 响应:`Success`、`SuccessWithPage`、`Error`、`ErrorWithData`、`ErrorStr`、`FormatMessage`、`ResponseCtx` +- 选项:`SetCode`、`SetMessage`、`SetData`、`SetPage`、`SetHttpStatusCode`、`SetHeader(s)`、`SetIgnoreLog`、`SetLogger`、`SetIgnoreTrace`、`SetDefaultSuccessCode`、`SetDefaultErrorCode`、`SetDefaultSuccessMessage` +- 分页:`GetOffset`、`GetPage`、`Pagination` +- trace:`SetTraceID`、`TraceID`、`RegisterTraceDriver`、`CtxTraceIDKey`、`SuccessMessageCtx` + +gin(`ginx`):`Middleware`、`Success`、`SuccessWithPage`、`Error`、`ErrorWithData`、`ErrorStr`、`Custom`、`MsgMapData`、`MsgKV` + +gormx:`GetDbOffset` +gozerotrace:空导入即注册 + +## 开发 + +本仓库是多模块,本地开发建议在根目录建 `go.work`(已 gitignore,勿提交): + +``` +go 1.22.4 + +use ( + . + ./gin + ./gormx + ./gozerotrace +) +``` + +运行测试: + +```bash +go test ./... # 需要 go.work;否则分别进入各模块执行 +cd gin && go test ./... +cd gormx && go test ./... +cd gozerotrace && go test ./... +``` + +校验依赖隔离(在核心模块根目录执行,应无任何框架包): + +```bash +go list -deps code.yun.ink/pkg/responsex | grep -E 'gin-gonic|gorm.io|zeromicro|opentelemetry' +``` + +## 发布(tag 规则) + +子模块必须带目录前缀的 tag,否则 proxy 不认: + +| 模块 | tag | 说明 | +|---|---|---| +| 核心 | `v1.0.2` | 根目录 | +| gin | `gin/v1.0.2` | 子模块 | +| gormx | `gormx/v1.0.2` | 子模块 | +| gozerotrace | `gozerotrace/v1.0.2` | 子模块 | + +**发布顺序**(重要):Go 无法 require 本地未发布的模块,所以必须 +**先推子模块 tag,最后再改根 go.mod 并发布根版本**。 + +## v0.0.4 → v1.x 迁移 + +gin 适配层从根包移到了子模块,并改名为 `ginx`: + +| 旧(v0.0.4,根包) | 新(v1.x) | +|---|---| +| `responsex.GinError` | `ginx.Error` | +| `responsex.GinErrorData` | `ginx.ErrorWithData` | +| `responsex.GinErrorStr` | `ginx.ErrorStr` | +| `responsex.GinSuccess` | `ginx.Success` | +| `responsex.GinSuccessWithPage` | `ginx.SuccessWithPage` | +| `responsex.GinOptions` | `ginx.Custom` | +| `responsex.GinMsgMapData` | `ginx.MsgMapData` | +| `responsex.GinMsgKV` | `ginx.MsgKV` | + +新增的 `ops ...Option` 是变参,旧调用点语义不变;只需把 import 与函数名改掉, +并在路由上挂一次 `ginx.Middleware()`。 diff --git a/response.go b/response.go old mode 100644 new mode 100755 index 25a8d15..7020208 --- a/response.go +++ b/response.go @@ -6,12 +6,10 @@ package responsex import ( "context" "encoding/json" - "math" "net/http" "github.com/yuninks/errorx" "github.com/yuninks/langx" - "github.com/zeromicro/go-zero/core/trace" ) type response struct { @@ -22,39 +20,39 @@ type response struct { TraceId string `json:"trace_id,omitempty"` } -type Pagination struct { - Page int64 `json:"page"` - Size int64 `json:"size"` - TotalPage int64 `json:"total_page"` - TotalCount int64 `json:"total_count"` +// 成功文案的多语言 key(独立 key,避免与 errorx 预注册的 "success" 冲突) +const successMsgKey = "responsex_success" + +// 内置默认成功文案,也用作 defaultOptions 的初值 +const defaultSuccessMessage = "请求成功" + +// SuccessMessageCtx 按 ctx 语言解析成功文案: +// 1. 显式配置过 SetDefaultSuccessMessage → 用配置值; +// 2. 注册过 key "responsex_success" → 用该语言的译文; +// 3. 都没有 → 内置默认文案。 +// +// 多语言接入示例(由调用方决定何时注册): +// +// langx.AppendCode(map[string]int{"responsex_success": 200}) +// langx.AppendTrans("zh", map[string]string{"responsex_success": "请求成功"}) +// langx.AppendTrans("en", map[string]string{"responsex_success": "success"}) +// +// Success 系列内部使用同一套解析逻辑,无需额外配置。 +func SuccessMessageCtx(ctx context.Context) string { + return resolveSuccessMessage(ctx) } -// 计算DB分页偏移量 -func GetOffset(page, size int) (offset int, limit int) { - if page <= 0 { - page = 1 +func resolveSuccessMessage(ctx context.Context) string { + if op != nil && op.successMsgSet { + return op.defaultSuccessMessage } - offset = (page - 1) * size - limit = size - if limit == 0 { - // 默认查10条 - limit = 10 + if msg := langx.GetMsgCtx(ctx, successMsgKey); msg != successMsgKey { + return msg } - return -} - -// 计算响应分页 -func GetPage(page, size, totalCount int64) Pagination { - pa := Pagination{} - totalPage := 0 - if size != 0 { - totalPage = int(math.Ceil(float64(totalCount) / float64(size))) + if op != nil { + return op.defaultSuccessMessage } - pa.Page = page - pa.Size = size - pa.TotalPage = int64(totalPage) - pa.TotalCount = totalCount - return pa + return defaultSuccessMessage } // Msg格式化响应 @@ -67,13 +65,13 @@ func FormatMessage(ctx context.Context, w http.ResponseWriter, msgKey string, fo // 成功的响应 func Success(ctx context.Context, w http.ResponseWriter, data any, ops ...Option) { - baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage("请求成功"), SetData(data)} + baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage(resolveSuccessMessage(ctx)), SetData(data)} baseOps = append(baseOps, ops...) ResponseCtx(ctx, w, baseOps...) } func SuccessWithPage(ctx context.Context, w http.ResponseWriter, data any, page *Pagination, ops ...Option) { - baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage("请求成功"), SetData(data), SetPage(page)} + baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage(resolveSuccessMessage(ctx)), SetData(data), SetPage(page)} baseOps = append(baseOps, ops...) ResponseCtx(ctx, w, baseOps...) } @@ -87,9 +85,7 @@ func ErrorWithData(ctx context.Context, w http.ResponseWriter, err error, data i code := op.defaultErrorCode msg := "请求失败" if err != nil { - val,ok := errorx.As(err) - if ok { - + if val, ok := errorx.As(err); ok { if langx.GetDefaultCode() != val.Code() { code = val.Code() } @@ -102,19 +98,21 @@ func ErrorWithData(ctx context.Context, w http.ResponseWriter, err error, data i } func ErrorStr(ctx context.Context, w http.ResponseWriter, msg string, ops ...Option) { - msg = langx.GetMsgCtx(ctx, msg) - code := langx.GetCode(msg) - if code == langx.GetDefaultCode() { - code = op.defaultErrorCode + translated := langx.GetMsgCtx(ctx, msg) + // msg 命中语言表时按 key 取其业务码;未命中则保持默认错误码 + code := op.defaultErrorCode + if translated != msg { + if c := langx.GetCode(msg); c != langx.GetDefaultCode() { + code = c + } } - baseOps := []Option{SetCode(code), SetMessage(msg)} + baseOps := []Option{SetCode(code), SetMessage(translated)} baseOps = append(baseOps, ops...) ResponseCtx(ctx, w, baseOps...) } // 基础的响应 func ResponseCtx(ctx context.Context, w http.ResponseWriter, ops ...Option) { - def := defaultOptions() // 默认值 for _, o := range ops { o(def) @@ -126,27 +124,36 @@ func ResponseCtx(ctx context.Context, w http.ResponseWriter, ops ...Option) { resp.Data = def.data resp.Pagination = def.pagination - // 获取trace_id - if def.traceId != "" { - t := ctx.Value(def.traceId) - if t != nil { - val, ok := t.(string) - if ok { - resp.TraceId = val - } - } else { - // go-zero - resp.TraceId = trace.TraceIDFromContext(ctx) - } + if def.traceFunc != nil { + resp.TraceId = def.traceFunc(ctx) } - bs, _ := json.Marshal(resp) + bs, err := json.Marshal(resp) + if err != nil { + // 序列化失败不能回空 body,否则客户端无法解析 + logError(ctx, def, "response marshal failed err:%+v", err) + fallback := response{ + Code: op.defaultErrorCode, + Message: "响应序列化失败", + } + if def.traceFunc != nil { + fallback.TraceId = def.traceFunc(ctx) + } + bs, _ = json.Marshal(fallback) + writeResponse(ctx, w, def, bs, http.StatusInternalServerError) + return + } if !def.ignoreLog { // 记录日志 - def.logger.Info(ctx, "response:", string(bs)) + logInfo(ctx, def, "response:", string(bs)) } + writeResponse(ctx, w, def, bs, def.httpStatusCode) +} + +// writeResponse 统一写响应,httpStatus 为 0 时按 200 处理 +func writeResponse(ctx context.Context, w http.ResponseWriter, def *options, bs []byte, httpStatus int) { // 写入自定义响应头 for k, v := range def.headers { w.Header().Set(k, v) @@ -154,8 +161,6 @@ func ResponseCtx(ctx context.Context, w http.ResponseWriter, ops ...Option) { w.Header().Set("Content-Type", "application/json; charset=utf-8") - // 确定HTTP状态码 - httpStatus := def.httpStatusCode if httpStatus == 0 { httpStatus = 200 } @@ -163,6 +168,21 @@ func ResponseCtx(ctx context.Context, w http.ResponseWriter, ops ...Option) { n, err := w.Write(bs) if err != nil || n != len(bs) { - op.logger.Error(ctx, "write response failed err:%+v", err) + logError(ctx, def, "write response failed err:%+v", err) } } + +// logInfo / logError 使用本次响应的 logger(SetLogger 生效),并做空值保护 +func logInfo(ctx context.Context, def *options, args ...interface{}) { + if def == nil || def.logger == nil { + return + } + def.logger.Info(ctx, args...) +} + +func logError(ctx context.Context, def *options, format string, args ...interface{}) { + if def == nil || def.logger == nil { + return + } + def.logger.Errorf(ctx, format, args...) +} diff --git a/response_test.go b/response_test.go index d612833..c5e286d 100644 --- a/response_test.go +++ b/response_test.go @@ -374,10 +374,10 @@ func TestFormatMessageWithOptions(t *testing.T) { func TestGetOffset(t *testing.T) { tests := []struct { - name string - page, size int - wantOffset int - wantLimit int + name string + page, size int + wantOffset int + wantLimit int }{ {"page 1 size 10", 1, 10, 0, 10}, {"page 2 size 10", 2, 10, 10, 10}, @@ -404,11 +404,11 @@ func TestGetOffset(t *testing.T) { func TestGetPage(t *testing.T) { tests := []struct { - name string - page, size, total int64 - wantPage, wantSize int64 - wantTotalPage int64 - wantTotalCount int64 + name string + page, size, total int64 + wantPage, wantSize int64 + wantTotalPage int64 + wantTotalCount int64 }{ {"normal", 1, 10, 100, 1, 10, 10, 100}, {"partial last page", 3, 10, 25, 3, 10, 3, 25}, diff --git a/trace_test.go b/trace_test.go new file mode 100755 index 0000000..ff1ba90 --- /dev/null +++ b/trace_test.go @@ -0,0 +1,92 @@ +package responsex_test + +import ( + "context" + "net/http/httptest" + "testing" + + "code.yun.ink/pkg/responsex" + "github.com/yuninks/langx" +) + + + + + +// ---------- 健壮性修复 ---------- + +func TestGetOffsetNegativeSize(t *testing.T) { + offset, limit := responsex.GetOffset(1, -5) + if limit != 10 || offset != 0 { + t.Errorf("GetOffset(1,-5) = (%d,%d), want (0,10)", offset, limit) + } +} + +func TestGetPageNegativeSize(t *testing.T) { + p := responsex.GetPage(1, -5, 100) + if p.TotalPage != 0 { + t.Errorf("TotalPage = %d, want 0(负数 size 不应算出负页数)", p.TotalPage) + } +} + +func TestResponseMarshalFailed(t *testing.T) { + w := httptest.NewRecorder() + // channel 无法序列化 + responsex.Success(context.Background(), w, make(chan int), responsex.SetIgnoreLog()) + + if w.Code != 500 { + t.Errorf("http status = %d, want 500", w.Code) + } + body := decodeBody(t, w) + if body["code"] != float64(400) { + t.Errorf("code = %v, want 400", body["code"]) + } + if body["message"] == "" { + t.Error("message 不应为空(不能回空 body)") + } +} + +func TestNilLoggerDoesNotPanic(t *testing.T) { + w := httptest.NewRecorder() + // 序列化失败路径会调用 logger,此处不得 panic + responsex.Success(context.Background(), w, make(chan int), responsex.SetLogger(nil)) + + if w.Code != 500 { + t.Errorf("http status = %d, want 500", w.Code) + } +} + +func TestSetDefaultSuccessMessage(t *testing.T) { + responsex.InitOptions(responsex.SetDefaultSuccessMessage("操作成功")) + defer responsex.InitOptions(responsex.SetDefaultSuccessMessage("请求成功")) + + w := httptest.NewRecorder() + responsex.Success(context.Background(), w, "x", responsex.SetIgnoreLog()) + + if body := decodeBody(t, w); body["message"] != "操作成功" { + t.Errorf("message = %v, want 操作成功", body["message"]) + } +} + +// 多语言成功文案:注册专用 key 后,按 ctx 语言返回 +func TestSuccessMessageFromLangx(t *testing.T) { + langx.AppendTrans("zh", map[string]string{"responsex_success": "请求成功"}) + langx.AppendTrans("en", map[string]string{"responsex_success": "success"}) + + ctxEn := langx.SetCtxLang(context.Background(), "en") + ctxZh := langx.SetCtxLang(context.Background(), "zh") + + if got := responsex.SuccessMessageCtx(ctxEn); got != "success" { + t.Errorf("SuccessMessageCtx(en) = %q, want success", got) + } + if got := responsex.SuccessMessageCtx(ctxZh); got != "请求成功" { + t.Errorf("SuccessMessageCtx(zh) = %q, want 请求成功", got) + } + + // Success 走同一套解析逻辑 + w := httptest.NewRecorder() + responsex.Success(ctxEn, w, "x", responsex.SetIgnoreLog()) + if body := decodeBody(t, w); body["message"] != "success" { + t.Errorf("Success(en) message = %v, want success", body["message"]) + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..28c286e --- /dev/null +++ b/utils.go @@ -0,0 +1,40 @@ +package responsex + +import "math" + +type Pagination struct { + Page int64 `json:"page"` + Size int64 `json:"size"` + TotalPage int64 `json:"total_page"` + TotalCount int64 `json:"total_count"` +} + +// 计算DB分页偏移量 +// page/size 小于等于 0 时取默认值(page=1,size=10),保证 offset/limit 永远合法 +func GetOffset(page, size int) (offset int, limit int) { + if page <= 0 { + page = 1 + } + if size <= 0 { + // 默认查10条 + size = 10 + } + offset = (page - 1) * size + limit = size + return +} + +// 计算响应分页 +// size 小于等于 0 时不做除法,避免出现负数总页数 +func GetPage(page, size, totalCount int64) Pagination { + pa := Pagination{} + var totalPage int64 + if size > 0 { + totalPage = int64(math.Ceil(float64(totalCount) / float64(size))) + } + pa.Page = page + pa.Size = size + pa.TotalPage = totalPage + pa.TotalCount = totalCount + return pa +}