package responsex // Author: Yun // Version: 2023年6月12日19:25:54 import ( "context" "encoding/json" "errors" "net/http" "github.com/yuninks/errorx" "github.com/yuninks/langx" ) type response struct { Code int `json:"code"` Message string `json:"message"` Data any `json:"data,omitempty"` Pagination *Pagination `json:"pagination,omitempty"` TraceId string `json:"trace_id,omitempty"` } // 成功文案的多语言 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) } func resolveSuccessMessage(ctx context.Context) string { if op != nil && op.successMsgSet { return op.defaultSuccessMessage } if msg := langx.GetMsgCtx(ctx, successMsgKey); msg != successMsgKey { return msg } if op != nil { return op.defaultSuccessMessage } return defaultSuccessMessage } // Msg格式化响应 func FormatMessage(ctx context.Context, w http.ResponseWriter, msgKey string, format map[string]string, data interface{}, ops ...Option) { code, msg := langx.GetTransFormatCtx(ctx, msgKey, format) // langx v0.1 起「未知 key」的默认码是 -1(v0.0.x 是 200)。responsex 的成功码 // 由 SetDefaultSuccessCode 决定,所以把默认码视为「没有业务码」,回落到它, // 保持与升级前一致的 code:200。 if code == langx.GetDefaultCode() { code = op.defaultSuccessCode } baseOps := []Option{SetCode(code), SetMessage(msg), SetData(data)} baseOps = append(baseOps, ops...) ResponseCtx(ctx, w, baseOps...) } // 成功的响应 func Success(ctx context.Context, w http.ResponseWriter, data any, ops ...Option) { 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(resolveSuccessMessage(ctx)), SetData(data), SetPage(page)} baseOps = append(baseOps, ops...) ResponseCtx(ctx, w, baseOps...) } // 失败的响应 func Error(ctx context.Context, w http.ResponseWriter, err error, ops ...Option) { ErrorWithData(ctx, w, err, "", ops...) } func ErrorWithData(ctx context.Context, w http.ResponseWriter, err error, data interface{}, ops ...Option) { code := op.defaultErrorCode msg := "请求失败" if err != nil { // errorx v1.2 起错误值不再持有 context:错误常量只负责定义与构造, // 文案要在出口用 Msg(ctx) 按请求语言渲染,业务码用 GetCode() 取。 var ei errorx.ErrorInterface if errors.As(err, &ei) { if bizCode := ei.GetCode(); langx.GetDefaultCode() != bizCode { code = bizCode } msg = ei.Msg(ctx) } else { msg = err.Error() } } baseOps := []Option{SetCode(code), SetMessage(msg), SetData(data)} baseOps = append(baseOps, ops...) ResponseCtx(ctx, w, baseOps...) } func ErrorStr(ctx context.Context, w http.ResponseWriter, msg string, ops ...Option) { 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(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) } resp := response{} resp.Code = def.code resp.Message = def.message resp.Data = def.data resp.Pagination = def.pagination if def.traceFunc != nil { resp.TraceId = def.traceFunc(ctx) } 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 { // 记录日志 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) } w.Header().Set("Content-Type", "application/json; charset=utf-8") if httpStatus == 0 { httpStatus = 200 } w.WriteHeader(httpStatus) n, err := w.Write(bs) if err != nil || n != len(bs) { 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...) }