686 lines
20 KiB
Go
686 lines
20 KiB
Go
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 检查模型是否有有效的 ID(ID != 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
|
||
}
|