2024-06-30 17:42:27 +08:00
|
|
|
|
package responsex
|
|
|
|
|
|
|
|
|
|
|
|
// Author: Yun
|
|
|
|
|
|
// Version: 2023年6月12日19:25:54
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
2026-10-02 22:53:10 +08:00
|
|
|
|
"errors"
|
2024-06-30 17:42:27 +08:00
|
|
|
|
"net/http"
|
|
|
|
|
|
|
2025-10-26 21:17:00 +08:00
|
|
|
|
"github.com/yuninks/errorx"
|
2025-12-06 22:57:01 +08:00
|
|
|
|
"github.com/yuninks/langx"
|
2024-06-30 17:42:27 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-07 11:21:00 +08:00
|
|
|
|
type response struct {
|
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
|
Data any `json:"data,omitempty"`
|
2026-07-22 01:17:07 +08:00
|
|
|
|
Pagination *Pagination `json:"pagination,omitempty"`
|
2025-12-07 11:21:00 +08:00
|
|
|
|
TraceId string `json:"trace_id,omitempty"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 01:31:34 +08:00
|
|
|
|
// 成功文案的多语言 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)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 01:31:34 +08:00
|
|
|
|
func resolveSuccessMessage(ctx context.Context) string {
|
|
|
|
|
|
if op != nil && op.successMsgSet {
|
|
|
|
|
|
return op.defaultSuccessMessage
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
2026-09-19 01:31:34 +08:00
|
|
|
|
if msg := langx.GetMsgCtx(ctx, successMsgKey); msg != successMsgKey {
|
|
|
|
|
|
return msg
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
2026-09-19 01:31:34 +08:00
|
|
|
|
if op != nil {
|
|
|
|
|
|
return op.defaultSuccessMessage
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
2026-09-19 01:31:34 +08:00
|
|
|
|
return defaultSuccessMessage
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Msg格式化响应
|
2026-07-22 01:17:07 +08:00
|
|
|
|
func FormatMessage(ctx context.Context, w http.ResponseWriter, msgKey string, format map[string]string, data interface{}, ops ...Option) {
|
2025-12-07 01:51:01 +08:00
|
|
|
|
code, msg := langx.GetTransFormatCtx(ctx, msgKey, format)
|
2026-10-02 22:53:10 +08:00
|
|
|
|
// langx v0.1 起「未知 key」的默认码是 -1(v0.0.x 是 200)。responsex 的成功码
|
|
|
|
|
|
// 由 SetDefaultSuccessCode 决定,所以把默认码视为「没有业务码」,回落到它,
|
|
|
|
|
|
// 保持与升级前一致的 code:200。
|
|
|
|
|
|
if code == langx.GetDefaultCode() {
|
|
|
|
|
|
code = op.defaultSuccessCode
|
|
|
|
|
|
}
|
2026-07-22 01:17:07 +08:00
|
|
|
|
baseOps := []Option{SetCode(code), SetMessage(msg), SetData(data)}
|
|
|
|
|
|
baseOps = append(baseOps, ops...)
|
|
|
|
|
|
ResponseCtx(ctx, w, baseOps...)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 成功的响应
|
2026-07-22 01:17:07 +08:00
|
|
|
|
func Success(ctx context.Context, w http.ResponseWriter, data any, ops ...Option) {
|
2026-09-19 01:31:34 +08:00
|
|
|
|
baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage(resolveSuccessMessage(ctx)), SetData(data)}
|
2026-07-22 01:17:07 +08:00
|
|
|
|
baseOps = append(baseOps, ops...)
|
|
|
|
|
|
ResponseCtx(ctx, w, baseOps...)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-22 01:17:07 +08:00
|
|
|
|
func SuccessWithPage(ctx context.Context, w http.ResponseWriter, data any, page *Pagination, ops ...Option) {
|
2026-09-19 01:31:34 +08:00
|
|
|
|
baseOps := []Option{SetCode(op.defaultSuccessCode), SetMessage(resolveSuccessMessage(ctx)), SetData(data), SetPage(page)}
|
2026-07-22 01:17:07 +08:00
|
|
|
|
baseOps = append(baseOps, ops...)
|
|
|
|
|
|
ResponseCtx(ctx, w, baseOps...)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 失败的响应
|
2026-07-22 01:17:07 +08:00
|
|
|
|
func Error(ctx context.Context, w http.ResponseWriter, err error, ops ...Option) {
|
|
|
|
|
|
ErrorWithData(ctx, w, err, "", ops...)
|
2025-12-06 22:57:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-22 01:17:07 +08:00
|
|
|
|
func ErrorWithData(ctx context.Context, w http.ResponseWriter, err error, data interface{}, ops ...Option) {
|
2024-07-10 17:32:12 +08:00
|
|
|
|
code := op.defaultErrorCode
|
2024-06-30 17:42:27 +08:00
|
|
|
|
msg := "请求失败"
|
|
|
|
|
|
if err != nil {
|
2026-10-02 22:53:10 +08:00
|
|
|
|
// 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
|
2024-07-10 17:32:12 +08:00
|
|
|
|
}
|
2026-10-02 22:53:10 +08:00
|
|
|
|
msg = ei.Msg(ctx)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
msg = err.Error()
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-22 01:17:07 +08:00
|
|
|
|
baseOps := []Option{SetCode(code), SetMessage(msg), SetData(data)}
|
|
|
|
|
|
baseOps = append(baseOps, ops...)
|
|
|
|
|
|
ResponseCtx(ctx, w, baseOps...)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-22 01:17:07 +08:00
|
|
|
|
func ErrorStr(ctx context.Context, w http.ResponseWriter, msg string, ops ...Option) {
|
2026-09-19 01:31:34 +08:00
|
|
|
|
translated := langx.GetMsgCtx(ctx, msg)
|
|
|
|
|
|
// msg 命中语言表时按 key 取其业务码;未命中则保持默认错误码
|
|
|
|
|
|
code := op.defaultErrorCode
|
|
|
|
|
|
if translated != msg {
|
|
|
|
|
|
if c := langx.GetCode(msg); c != langx.GetDefaultCode() {
|
|
|
|
|
|
code = c
|
|
|
|
|
|
}
|
2024-07-10 17:32:12 +08:00
|
|
|
|
}
|
2026-09-19 01:31:34 +08:00
|
|
|
|
baseOps := []Option{SetCode(code), SetMessage(translated)}
|
2026-07-22 01:17:07 +08:00
|
|
|
|
baseOps = append(baseOps, ops...)
|
|
|
|
|
|
ResponseCtx(ctx, w, baseOps...)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 基础的响应
|
2025-12-07 01:51:01 +08:00
|
|
|
|
func ResponseCtx(ctx context.Context, w http.ResponseWriter, ops ...Option) {
|
|
|
|
|
|
def := defaultOptions() // 默认值
|
|
|
|
|
|
for _, o := range ops {
|
|
|
|
|
|
o(def)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-07 11:21:00 +08:00
|
|
|
|
resp := response{}
|
|
|
|
|
|
resp.Code = def.code
|
|
|
|
|
|
resp.Message = def.message
|
|
|
|
|
|
resp.Data = def.data
|
|
|
|
|
|
resp.Pagination = def.pagination
|
2024-06-30 17:42:27 +08:00
|
|
|
|
|
2026-09-19 01:31:34 +08:00
|
|
|
|
if def.traceFunc != nil {
|
|
|
|
|
|
resp.TraceId = def.traceFunc(ctx)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 01:31:34 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2025-12-07 12:07:21 +08:00
|
|
|
|
|
2025-12-07 01:51:01 +08:00
|
|
|
|
if !def.ignoreLog {
|
2024-06-30 17:42:27 +08:00
|
|
|
|
// 记录日志
|
2026-09-19 01:31:34 +08:00
|
|
|
|
logInfo(ctx, def, "response:", string(bs))
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-19 01:31:34 +08:00
|
|
|
|
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) {
|
2026-07-22 01:17:07 +08:00
|
|
|
|
// 写入自定义响应头
|
|
|
|
|
|
for k, v := range def.headers {
|
|
|
|
|
|
w.Header().Set(k, v)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-30 17:42:27 +08:00
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2026-07-22 01:17:07 +08:00
|
|
|
|
|
|
|
|
|
|
if httpStatus == 0 {
|
|
|
|
|
|
httpStatus = 200
|
|
|
|
|
|
}
|
|
|
|
|
|
w.WriteHeader(httpStatus)
|
|
|
|
|
|
|
2024-06-30 17:42:27 +08:00
|
|
|
|
n, err := w.Write(bs)
|
|
|
|
|
|
if err != nil || n != len(bs) {
|
2026-09-19 01:31:34 +08:00
|
|
|
|
logError(ctx, def, "write response failed err:%+v", err)
|
2024-06-30 17:42:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-19 01:31:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 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...)
|
|
|
|
|
|
}
|