topfans/backend/gateway/pkg/response/response.go
2026-06-15 16:28:35 +08:00

197 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package response
import (
"encoding/json"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
)
// Response 统一响应格式
// 字段说明:
// Code — google.rpc.Code 数字(0=OK, 3=INVALID_ARGUMENT, 5=NOT_FOUND, 7=PERMISSION_DENIED, 8=RESOURCE_EXHAUSTED, 13=INTERNAL, 16=UNAUTHENTICATED)
// Message — 错误信息(成功时为 "ok" 或自定义)
// Data — 业务数据(成功时填,失败时省略)
type Response struct {
Code uint32 `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// writeJSON 统一输出 JSON,并关闭 HTML 转义(避免将 & 转成 &,导致预签名 URL 失效)
func writeJSON(c *gin.Context, httpCode int, payload interface{}) {
c.Status(httpCode)
c.Header("Content-Type", "application/json; charset=utf-8")
enc := json.NewEncoder(c.Writer)
enc.SetEscapeHTML(false)
_ = enc.Encode(payload)
}
// Success 业务成功响应(HTTP 200, body.code = 0)
func Success(c *gin.Context, data interface{}) {
writeJSON(c, http.StatusOK, Response{
Code: uint32(codes.OK),
Message: "ok",
Data: data,
})
}
// SuccessWithMessage 业务成功响应(自定义消息)
func SuccessWithMessage(c *gin.Context, message string, data interface{}) {
writeJSON(c, http.StatusOK, Response{
Code: uint32(codes.OK),
Message: message,
Data: data,
})
}
// Error transport error 响应 — 用于 controller 自身做参数校验 / 鉴权检查时的失败
// - HTTP 状态码: 使用调用方传入的 httpCode(4xx/5xx)
// - body.code: 同样使用 httpCode(transport error 用 HTTP 数字保持兼容,等待 controller 全部迁到 gRPC code 后改为 gRPC code)
// - 调用 c.Abort() 告知 GRPCStatusInterceptor 保留原 HTTP 状态(不要强制改 200)
func Error(c *gin.Context, httpCode int, message string) {
c.Abort()
writeJSON(c, httpCode, Response{
Code: uint32(httpCode),
Message: message,
})
}
// ErrorWithCode 业务错误响应 — 用于 Dubbo 返回 gRPC code 时的失败
// - 第一个参数 code: Dubbo 响应的 BaseResponse.code(gRPC code 数字)
// - HTTP 强制 200,body.code = gRPC code(由 GRPCStatusInterceptor 兜底)
// - 不调用 Abort() — 业务响应会被拦截器统一处理
func ErrorWithCode(c *gin.Context, code int, message string) {
writeJSON(c, http.StatusOK, Response{
Code: uint32(code),
Message: message,
})
}
// BadRequest 400 错误请求(transport error)
func BadRequest(c *gin.Context, message string) {
if message == "" {
message = "请求参数错误"
}
Error(c, http.StatusBadRequest, message)
}
// Unauthorized 401 未授权(transport error)
func Unauthorized(c *gin.Context, message string) {
if message == "" {
message = "未授权,请先登录"
}
Error(c, http.StatusUnauthorized, message)
}
// Forbidden 403 禁止访问(transport error)
func Forbidden(c *gin.Context, message string) {
if message == "" {
message = "权限不足,禁止访问"
}
Error(c, http.StatusForbidden, message)
}
// NotFound 404 未找到(transport error)
func NotFound(c *gin.Context, message string) {
if message == "" {
message = "资源不存在"
}
Error(c, http.StatusNotFound, message)
}
// InternalError 500 服务器内部错误(transport error)
func InternalError(c *gin.Context, message string) {
if message == "" {
message = "服务器内部错误"
}
Error(c, http.StatusInternalServerError, message)
}
// CleanErrorMessage 清理错误消息,转换为中文友好提示
func CleanErrorMessage(err error) string {
if err == nil {
return ""
}
msg := err.Error()
// 移除 gRPC 错误前缀
if idx := strings.Index(msg, "code_"); idx >= 0 {
if colonIdx := strings.Index(msg[idx:], ": "); colonIdx >= 0 {
msg = msg[idx+colonIdx+2:]
}
}
// 中英文错误映射
errorMap := map[string]string{
"user not found": "用户不存在",
"user already exists": "该手机号已被注册",
"invalid password": "密码错误",
"invalid mobile format": "手机号格式不正确",
"password too short": "密码长度不足至少需要6位",
"star not found": "明星不存在",
"fan profile not found": "粉丝档案不存在",
"invalid token": "Token无效",
"token expired": "Token已过期",
"user is inactive": "用户已被禁用",
"maximum number of identities reached": "已达到最大身份数量限制",
"invalid star_id": "明星ID无效",
"invalid user_id": "用户ID无效",
"invalid nickname": "昵称格式不正确",
"nickname too long": "昵称过长最多20个字符",
"nickname too short": "昵称过短至少需要2个字符",
"user info not found in Dubbo attachments": "请先登录",
"missing or invalid authorization token": "Token缺失或无效",
"已达到最大好友数量限制": "已达到最大好友数量限制",
}
msgLower := strings.ToLower(msg)
for eng, chn := range errorMap {
if strings.Contains(msgLower, eng) {
return chn
}
}
// 如果没有匹配,返回原始消息
return msg
}
// HandleError 统一处理错误并返回(根据 error message 字符串猜测类型 — 留给未来用 typed error 替换)
func HandleError(c *gin.Context, err error) {
if err == nil {
InternalError(c, "未知错误")
return
}
msg := CleanErrorMessage(err)
// 根据错误类型返回对应的 HTTP 状态码
switch {
case strings.Contains(msg, "not found") || strings.Contains(msg, "不存在"):
NotFound(c, msg)
case strings.Contains(msg, "unauthorized") ||
strings.Contains(msg, "token") ||
strings.Contains(msg, "请先登录"):
Unauthorized(c, msg)
case strings.Contains(msg, "invalid") ||
strings.Contains(msg, "格式不正确") ||
strings.Contains(msg, "过长") ||
strings.Contains(msg, "过短"):
BadRequest(c, msg)
case strings.Contains(msg, "already exists") ||
strings.Contains(msg, "已被注册"):
Error(c, http.StatusConflict, msg)
case strings.Contains(msg, "forbidden") ||
strings.Contains(msg, "permission denied") ||
strings.Contains(msg, "权限不足") ||
strings.Contains(msg, "账号已被冻结") ||
strings.Contains(msg, "账号已被封禁"):
Forbidden(c, msg)
default:
InternalError(c, msg)
}
}