Files
2026-01-16 15:49:34 +08:00

686 lines
20 KiB
Go
Raw Permalink 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 (
"reflect"
"github.com/goravel/framework/contracts/database/orm"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
apperrors "goravel/app/errors"
"goravel/app/http/helpers"
"goravel/app/http/trans"
"goravel/app/services"
"goravel/app/utils/errorlog"
"goravel/app/utils/traceid"
)
// 错误日志信息的 context key
const (
errorLogModuleKey = "error_log_module"
errorLogMessageKey = "error_log_message"
errorLogAttributesKey = "error_log_attributes"
)
// Success 成功响应(支持多语言,自动包含 trace_id)
// messageKey 可选,如果不传则使用默认值 "success"
// 使用方式:
// - response.Success(ctx)
// - response.Success(ctx, data)
// - response.Success(ctx, "custom_message", data)
func Success(ctx http.Context, args ...any) http.Response {
var messageKey string
var data any
// 智能识别参数:如果第一个参数是 string 且长度合理(<=50),则认为是 messageKey
if len(args) > 0 {
if msgKey, ok := args[0].(string); ok && len(msgKey) <= 50 {
// 方式1:传了 messageKey
messageKey = msgKey
if len(args) > 1 {
data = args[1]
}
} else {
// 方式2:没传 messageKey,第一个参数就是 data
messageKey = "success" // 默认值
data = args[0]
}
} else {
// 没有参数,使用默认值
messageKey = "success"
}
message := trans.Get(ctx, messageKey)
response := http.Json{
"code": 200,
"message": message,
}
// 自动包含 trace_id,方便前端追踪
if traceID := traceid.FromHTTPContext(ctx); traceID != "" {
response["trace_id"] = traceID
}
// 如果有数据,添加到响应中
if data != nil {
// 转换时间字段到对应时区
convertedData := helpers.ConvertTimesInData(ctx, data)
response["data"] = convertedData
}
return ctx.Response().Success().Json(response)
}
// SuccessWithHeader 成功响应(支持多语言和自定义Header,自动包含 trace_id
func SuccessWithHeader(ctx http.Context, messageKey string, headerKey, headerValue string, data ...http.Json) http.Response {
message := trans.Get(ctx, messageKey)
response := http.Json{
"code": 200,
"message": message,
}
// 自动包含 trace_id,方便前端追踪
if traceID := traceid.FromHTTPContext(ctx); traceID != "" {
response["trace_id"] = traceID
}
if len(data) > 0 {
// 转换时间字段到对应时区
convertedData := helpers.ConvertTimesInData(ctx, data[0])
if convertedMap, ok := convertedData.(map[string]any); ok {
response["data"] = http.Json(convertedMap)
} else {
response["data"] = convertedData
}
}
return ctx.Response().Header(headerKey, headerValue).Success().Json(response)
}
// Error 错误响应(支持多语言,自动包含 trace_id 和 error_code
// 支持两种调用方式:
// 1. response.Error(ctx, code, messageKey) - 使用翻译键
// 2. response.Error(ctx, code, err) - 自动检测 BusinessError 并处理占位符替换
//
// 当 code >= 500 时,如果 context 中包含错误日志信息,会自动记录日志
func Error(ctx http.Context, code int, messageOrErr any) http.Response {
var message string
var messageKey string
// 判断第二个参数是 error 还是 string
switch v := messageOrErr.(type) {
case error:
// 如果是 error,检查是否是 BusinessError
if businessErr, ok := apperrors.GetBusinessError(v); ok {
// 使用 GetFormattedMessage 自动处理翻译和占位符替换
message = businessErr.GetFormattedMessage(ctx)
messageKey = businessErr.Code
} else {
// 普通错误,直接使用错误消息
message = v.Error()
messageKey = "operation_failed"
}
case string:
// 如果是 string,当作翻译键处理
messageKey = v
message = trans.Get(ctx, messageKey)
default:
// 其他类型,转换为字符串
messageKey = "operation_failed"
message = trans.Get(ctx, messageKey)
}
response := http.Json{
"code": code,
"message": message,
"error_code": messageKey, // 添加错误码字段,方便前端判断
}
// 自动包含 trace_id,方便前端显示和用户报告错误
if traceID := traceid.FromHTTPContext(ctx); traceID != "" {
response["trace_id"] = traceID
}
// 系统级错误(500+)自动记录日志(如果 context 中有日志信息)
if code >= 500 {
if module, ok := ctx.Value(errorLogModuleKey).(string); ok && module != "" {
logMessage := ctx.Value(errorLogMessageKey)
if logMsg, ok := logMessage.(string); ok && logMsg != "" {
attributes := ctx.Value(errorLogAttributesKey)
var attrs map[string]any
if attributes != nil {
if attrsMap, ok := attributes.(map[string]any); ok {
attrs = attrsMap
}
}
errorlog.RecordHTTP(ctx, module, logMsg, attrs, "%s: %s", module, logMsg)
}
}
}
return ctx.Response().Json(code, response)
}
// SetErrorLog 设置错误日志信息到 context(用于系统级错误)
// 在调用 response.Error 之前调用此函数,Error 函数会自动记录日志
func SetErrorLog(ctx http.Context, module, logMessage string, attributes map[string]any) {
ctx.WithValue(errorLogModuleKey, module)
ctx.WithValue(errorLogMessageKey, logMessage)
if attributes != nil {
ctx.WithValue(errorLogAttributesKey, attributes)
}
}
// ErrorWithLog 错误响应并自动记录日志(用于系统级错误)
// 支持多种调用方式,自动推断参数:
//
// 最简洁方式(推荐):
// - response.ErrorWithLog(ctx, "module", err)
// - response.ErrorWithLog(ctx, "module", err, map[string]any{"extra": "value"})
//
// 完整方式:
// - response.ErrorWithLog(ctx, code, messageKey, module, logMessage, err)
// - response.ErrorWithLog(ctx, code, messageKey, module, logMessage, err, map[string]any{...})
func ErrorWithLog(ctx http.Context, args ...any) http.Response {
// 智能识别调用方式
if len(args) == 0 {
return Error(ctx, http.StatusInternalServerError, "operation_failed")
}
// 方式1:最简洁方式 - ErrorWithLog(ctx, "module", err) 或 ErrorWithLog(ctx, "module", err, attrs)
if len(args) >= 2 {
if module, ok := args[0].(string); ok {
var err error
var attributes map[string]any
// 查找 error 和 attributes
for i := 1; i < len(args); i++ {
switch v := args[i].(type) {
case error:
if err == nil {
err = v
}
case map[string]any:
if attributes == nil {
attributes = v
}
}
}
if err == nil {
return Error(ctx, http.StatusInternalServerError, "operation_failed")
}
// 自动生成日志消息
logMessage := err.Error()
if len(logMessage) > 100 {
logMessage = logMessage[:100] + "..."
}
// 设置属性
if attributes == nil {
attributes = make(map[string]any)
}
if _, exists := attributes["error"]; !exists {
attributes["error"] = err.Error()
}
// 自动记录日志
SetErrorLog(ctx, module, logMessage, attributes)
return Error(ctx, http.StatusInternalServerError, "operation_failed")
}
}
// 方式2:完整方式 - ErrorWithLog(ctx, code, messageKey, module, logMessage, ...)
if len(args) >= 4 {
code, codeOk := args[0].(int)
messageKey, msgOk := args[1].(string)
module, modOk := args[2].(string)
logMessage, logOk := args[3].(string)
if codeOk && msgOk && modOk && logOk {
var attributes map[string]any
var err error
// 解析剩余参数
for i := 4; i < len(args); i++ {
switch v := args[i].(type) {
case error:
if err == nil {
err = v
}
case map[string]any:
if attributes == nil {
attributes = v
}
}
}
// 设置属性
if attributes == nil {
attributes = make(map[string]any)
}
if err != nil {
if _, exists := attributes["error"]; !exists {
attributes["error"] = err.Error()
}
}
// 系统级错误(500+)设置日志信息,Error 函数会自动记录
if code >= 500 {
SetErrorLog(ctx, module, logMessage, attributes)
}
return Error(ctx, code, messageKey)
}
}
// 无法识别参数格式,返回通用错误
return Error(ctx, http.StatusInternalServerError, "operation_failed")
}
// ErrorWithLogAuto 超简洁版本:自动推断所有参数
// 只需要传入 module 和 err,其他参数自动推断
// 默认使用 HTTP 500 状态码和通用的错误消息
//
// 使用方式:
// - response.ErrorWithLogAuto(ctx, "operation-log", err)
// - response.ErrorWithLogAuto(ctx, "operation-log", err, map[string]any{"extra": "value"})
func ErrorWithLogAuto(ctx http.Context, module string, args ...any) http.Response {
var err error
var attributes map[string]any
// 解析参数
for i := len(args) - 1; i >= 0; i-- {
switch v := args[i].(type) {
case error:
if err == nil {
err = v
}
case map[string]any:
if attributes == nil {
attributes = v
}
}
}
// 如果没有 error,返回通用错误
if err == nil {
return Error(ctx, http.StatusInternalServerError, "operation_failed")
}
// 如果没有提供 attributes,创建一个
if attributes == nil {
attributes = make(map[string]any)
}
// 自动添加 error 字段
if _, exists := attributes["error"]; !exists {
attributes["error"] = err.Error()
}
// 自动生成日志消息:使用 err.Error() 作为日志消息
logMessage := err.Error()
// 如果错误信息太长,截取前100个字符
if len(logMessage) > 100 {
logMessage = logMessage[:100] + "..."
}
// 自动推断 messageKey:根据 module 生成通用的错误消息键
messageKey := "operation_failed"
if module != "" {
// 尝试使用 module 相关的错误消息键
// 例如 "operation-log" -> "operation_failed"
// 或者保持通用
messageKey = "operation_failed"
}
// 系统级错误(500)自动记录日志
SetErrorLog(ctx, module, logMessage, attributes)
return Error(ctx, http.StatusInternalServerError, messageKey)
}
// ValidationError 验证错误响应(支持多语言,自动包含 trace_id 和 error_code
// 自动提取第一个错误信息并添加到 message 中,方便前端直接显示
func ValidationError(ctx http.Context, code int, messageKey string, errors map[string]map[string]string) http.Response {
baseMessage := trans.Get(ctx, messageKey)
// 提取第一个错误信息
var firstError string
for _, fieldErrors := range errors {
for _, errorMsg := range fieldErrors {
firstError = errorMsg
break
}
if firstError != "" {
break
}
}
// 如果有具体错误信息,将其添加到 message 中
var message string
if firstError != "" {
message = firstError
} else {
message = baseMessage
}
response := http.Json{
"code": code,
"message": message,
"error_code": messageKey, // 添加错误码字段,方便前端判断
"errors": errors,
}
// 自动包含 trace_id,方便前端显示和用户报告错误
if traceID := traceid.FromHTTPContext(ctx); traceID != "" {
response["trace_id"] = traceID
}
return ctx.Response().Json(code, response)
}
// Paginate 分页响应(支持多语言,自动包含 trace_id)
// messageKey 可选,如果不传则使用默认值 "get_success"
// 使用方式:
// - response.Paginate(ctx, list, total, page, pageSize)
// - response.Paginate(ctx, "custom_message", list, total, page, pageSize)
func Paginate(ctx http.Context, args ...any) http.Response {
var messageKey string
var list any
var total int64
var page, pageSize int
// 智能识别参数:如果第一个参数是 string 且长度合理(<=50),则认为是 messageKey
if len(args) >= 4 {
if msgKey, ok := args[0].(string); ok && len(msgKey) <= 50 {
// 方式1:传了 messageKey
messageKey = msgKey
list = args[1]
if t, ok := args[2].(int64); ok {
total = t
} else if t, ok := args[2].(int); ok {
total = int64(t)
}
if p, ok := args[3].(int); ok {
page = p
}
if len(args) >= 5 {
if ps, ok := args[4].(int); ok {
pageSize = ps
}
}
} else {
// 方式2:没传 messageKey
messageKey = "get_success" // 默认值
list = args[0]
if t, ok := args[1].(int64); ok {
total = t
} else if t, ok := args[1].(int); ok {
total = int64(t)
}
if p, ok := args[2].(int); ok {
page = p
}
if len(args) >= 4 {
if ps, ok := args[3].(int); ok {
pageSize = ps
}
}
}
}
// 如果 messageKey 为空,使用默认值
if messageKey == "" {
messageKey = "get_success"
}
message := trans.Get(ctx, messageKey)
// 转换列表中的时间字段到对应时区
convertedList := helpers.ConvertTimesInData(ctx, list)
response := http.Json{
"code": 200,
"message": message,
"data": http.Json{
"list": convertedList,
"total": total,
"page": page,
"page_size": pageSize,
},
}
// 自动包含 trace_id,方便前端追踪
if traceID := traceid.FromHTTPContext(ctx); traceID != "" {
response["trace_id"] = traceID
}
return ctx.Response().Success().Json(response)
}
// PaginateQueryOptions 分页查询选项
type PaginateQueryOptions struct {
// WithRelations 预加载关联,例如 []string{"Department", "Roles"}
WithRelations []string
// Transform 数据转换函数,可以对查询结果进行转换
Transform func(any) any
// ErrorHandler 自定义错误处理函数,如果为 nil 则使用默认错误处理
ErrorHandler func(ctx http.Context, err error, module string) http.Response
// ErrorModule 错误日志模块名,用于 ErrorWithLog
ErrorModule string
}
// PaginateQuery 通用的分页查询封装
// query: 构建好的查询对象(已包含所有查询条件)
// list: 用于接收查询结果的切片指针,例如 &[]models.Dictionary{}
// options: 可选配置
//
// 使用示例:
//
// - 基础用法:
// return response.PaginateQuery(ctx, query, &dictionaries, nil)
//
// - 带预加载关联:
// return response.PaginateQuery(ctx, query, &roles, &response.PaginateQueryOptions{
// WithRelations: []string{"Permissions", "Menus"},
// })
//
// - 带数据转换:
// return response.PaginateQuery(ctx, query, &admins, &response.PaginateQueryOptions{
// WithRelations: []string{"Department", "Roles"},
// Transform: func(data any) any {
// admins := data.(*[]models.Admin)
// // 转换逻辑
// return adminList
// },
// })
//
// - 带错误日志模块:
// return response.PaginateQuery(ctx, query, &logs, &response.PaginateQueryOptions{
// WithRelations: []string{"Admin"},
// ErrorModule: "login-log",
// })
func PaginateQuery(ctx http.Context, query orm.Query, list any, options *PaginateQueryOptions) http.Response {
// 获取分页参数
page := helpers.GetIntQuery(ctx, "page", 1)
pageSize := helpers.GetIntQuery(ctx, "page_size", 10)
page, pageSize = helpers.ValidatePagination(page, pageSize)
// 获取总数
total, err := query.Count()
if err != nil {
if options != nil && options.ErrorHandler != nil {
return options.ErrorHandler(ctx, err, options.ErrorModule)
}
if options != nil && options.ErrorModule != "" {
return ErrorWithLog(ctx, options.ErrorModule, err)
}
return Error(ctx, http.StatusInternalServerError, "query_failed")
}
// 应用预加载关联
if options != nil && len(options.WithRelations) > 0 {
for _, relation := range options.WithRelations {
query = query.With(relation)
}
}
// 分页查询
offset := (page - 1) * pageSize
if err := query.Offset(offset).Limit(pageSize).Get(list); err != nil {
if options != nil && options.ErrorHandler != nil {
return options.ErrorHandler(ctx, err, options.ErrorModule)
}
if options != nil && options.ErrorModule != "" {
return ErrorWithLog(ctx, options.ErrorModule, err)
}
return Error(ctx, http.StatusInternalServerError, "query_failed")
}
// 数据转换
var result any = list
if options != nil && options.Transform != nil {
result = options.Transform(list)
}
return Paginate(ctx, result, total, page, pageSize)
}
// Export 导出响应(支持多语言)
// headers: CSV表头(可以是翻译键数组或字符串数组)
// data: 数据行,每行是一个字符串切片
// filename: 文件名(不含扩展名)
func Export(ctx http.Context, messageKey string, headers []string, data [][]string, filename string) http.Response {
message := trans.Get(ctx, messageKey)
// 翻译表头(如果表头是翻译键,则翻译;如果是普通字符串,则保持原样)
translatedHeaders := make([]string, len(headers))
for i, header := range headers {
// 尝试翻译,如果翻译键不存在则返回原字符串
translated := trans.Get(ctx, header)
if translated == header {
// 如果翻译结果和原字符串相同,说明不是翻译键,直接使用原字符串
translatedHeaders[i] = header
} else {
// 如果翻译成功,使用翻译后的文本
translatedHeaders[i] = translated
}
}
exportService := services.NewExportService(ctx)
filePath, err := exportService.ExportToFile(translatedHeaders, data, filename)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "export_failed")
}
exportURL := exportService.GetExportURL(filePath)
response := http.Json{
"code": 200,
"message": message,
"data": http.Json{
"file_path": filePath,
"file_url": exportURL,
},
}
// 自动包含 trace_id,方便前端追踪
if traceID := traceid.FromHTTPContext(ctx); traceID != "" {
response["trace_id"] = traceID
}
return ctx.Response().Success().Json(response)
}
// FindByIDOptions 查找选项
type FindByIDOptions struct {
// WithRelations 预加载关联,例如 []string{"Department", "Roles"}
WithRelations []string
// NotFoundMessageKey 未找到时的错误消息键,例如 "admin_not_found"
// 如果不提供,默认使用 "record_not_found"
NotFoundMessageKey string
}
// FindByID 通用的根据ID查找记录函数
// T 必须是嵌入了 orm.Model 的模型类型
// 使用示例:
//
// // 简单查找
// admin, resp := response.FindByID[models.Admin](ctx, id, nil)
// if resp != nil {
// return resp
// }
//
// // 带关联预加载
// admin, resp := response.FindByID[models.Admin](ctx, id, &response.FindByIDOptions{
// WithRelations: []string{"Department", "Roles"},
// NotFoundMessageKey: "admin_not_found",
// })
// if resp != nil {
// return resp
// }
func FindByID[T any](ctx http.Context, id uint, options *FindByIDOptions) (*T, http.Response) {
// 验证ID
if id == 0 {
return nil, Error(ctx, http.StatusBadRequest, apperrors.ErrIDRequired.Code)
}
// 创建查询
query := facades.Orm().Query().Where("id", id)
// 应用关联预加载
if options != nil && len(options.WithRelations) > 0 {
for _, relation := range options.WithRelations {
query = query.With(relation)
}
}
// 查询记录
var model T
if err := query.First(&model); err != nil {
// 确定错误消息键,默认使用 record_not_found
messageKey := apperrors.ErrRecordNotFound.Code
if options != nil && options.NotFoundMessageKey != "" {
messageKey = options.NotFoundMessageKey
}
return nil, Error(ctx, http.StatusNotFound, messageKey)
}
// 检查记录是否存在(防御性编程)
// 通过反射检查 ID 字段,如果 ID 为 0,说明记录不存在
modelPtr := &model
if !hasValidID(modelPtr) {
// 确定错误消息键,默认使用 record_not_found
messageKey := apperrors.ErrRecordNotFound.Code
if options != nil && options.NotFoundMessageKey != "" {
messageKey = options.NotFoundMessageKey
}
return nil, Error(ctx, http.StatusNotFound, messageKey)
}
return modelPtr, nil
}
// hasValidID 检查模型是否有有效的 IDID != 0)
// 使用反射来检查模型的 ID 字段
func hasValidID(model any) bool {
v := reflect.ValueOf(model)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
// 查找 ID 字段
idField := v.FieldByName("ID")
if !idField.IsValid() {
// 如果没有找到 ID 字段,假设记录有效(防御性编程)
return true
}
// 检查 ID 是否为 0
if idField.Kind() == reflect.Uint || idField.Kind() == reflect.Uint32 || idField.Kind() == reflect.Uint64 {
return idField.Uint() != 0
}
return true
}