197 lines
6.4 KiB
Go
197 lines
6.4 KiB
Go
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)
|
||
}
|
||
}
|