init
This commit is contained in:
@@ -0,0 +1,685 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user