init
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"goravel/app/models"
|
||||
)
|
||||
|
||||
// GetAdminFromContext 从 context 中获取 admin 对象
|
||||
// 如果 context 中没有 admin 或类型不匹配,返回错误
|
||||
func GetAdminFromContext(ctx http.Context) (*models.Admin, error) {
|
||||
adminValue := ctx.Value("admin")
|
||||
if adminValue == nil {
|
||||
return nil, errors.New("admin not found in context")
|
||||
}
|
||||
|
||||
// 尝试值类型
|
||||
if admin, ok := adminValue.(models.Admin); ok {
|
||||
return &admin, nil
|
||||
}
|
||||
|
||||
// 尝试指针类型
|
||||
if adminPtr, ok := adminValue.(*models.Admin); ok {
|
||||
return adminPtr, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid admin type in context")
|
||||
}
|
||||
|
||||
// GetAdminIDFromContext 从 context 中获取 admin ID
|
||||
// 如果 context 中没有 admin 或类型不匹配,返回 0 和错误
|
||||
func GetAdminIDFromContext(ctx http.Context) (uint, error) {
|
||||
admin, err := GetAdminFromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return admin.ID, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/str"
|
||||
)
|
||||
|
||||
// GetRealIP 获取客户端真实IP地址
|
||||
// 优先从以下HTTP头获取(按顺序):
|
||||
// 1. CF-Connecting-IP (Cloudflare)
|
||||
// 2. True-Client-IP
|
||||
// 3. X-Real-IP
|
||||
// 4. X-Forwarded-For (取第一个IP)
|
||||
// 5. RemoteAddr
|
||||
func GetRealIP(ctx http.Context) string {
|
||||
// 1. Cloudflare
|
||||
if ip := ctx.Request().Header("CF-Connecting-IP", ""); ip != "" {
|
||||
if parsedIP := parseIP(ip); parsedIP != "" {
|
||||
return parsedIP
|
||||
}
|
||||
}
|
||||
|
||||
// 2. True-Client-IP
|
||||
if ip := ctx.Request().Header("True-Client-IP", ""); ip != "" {
|
||||
if parsedIP := parseIP(ip); parsedIP != "" {
|
||||
return parsedIP
|
||||
}
|
||||
}
|
||||
|
||||
// 3. X-Real-IP
|
||||
if ip := ctx.Request().Header("X-Real-IP", ""); ip != "" {
|
||||
if parsedIP := parseIP(ip); parsedIP != "" {
|
||||
return parsedIP
|
||||
}
|
||||
}
|
||||
|
||||
// 4. X-Forwarded-For (可能包含多个IP,取第一个)
|
||||
if forwardedFor := ctx.Request().Header("X-Forwarded-For", ""); !str.Of(forwardedFor).IsEmpty() {
|
||||
ips := str.Of(forwardedFor).Split(",")
|
||||
if len(ips) > 0 {
|
||||
ip := str.Of(ips[0]).Trim().String()
|
||||
if parsedIP := parseIP(ip); !str.Of(parsedIP).IsEmpty() {
|
||||
return parsedIP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. RemoteAddr
|
||||
remoteAddr := ctx.Request().Ip()
|
||||
if parsedIP := parseIP(remoteAddr); parsedIP != "" {
|
||||
return parsedIP
|
||||
}
|
||||
|
||||
return remoteAddr
|
||||
}
|
||||
|
||||
// parseIP 解析并验证IP地址
|
||||
func parseIP(ip string) string {
|
||||
ip = str.Of(ip).Trim().String()
|
||||
if str.Of(ip).IsEmpty() {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 如果包含端口,去掉端口
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
|
||||
// 验证是否为有效IP
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package helpers
|
||||
|
||||
// 注意:FindByID 函数已移动到 response 包中,以避免循环导入
|
||||
// 请使用 response.FindByID 代替 helpers.FindByID
|
||||
// 相关的 FindByIDOptions 类型也在 response 包中
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package helpers
|
||||
|
||||
// PaginateSlice 对切片进行分页处理
|
||||
// 返回分页后的切片和总数
|
||||
func PaginateSlice[T any](slice []T, page, pageSize int) ([]T, int64) {
|
||||
total := int64(len(slice))
|
||||
if total == 0 {
|
||||
return []T{}, 0
|
||||
}
|
||||
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
|
||||
// 如果起始位置超出范围,返回空切片
|
||||
if start >= len(slice) {
|
||||
return []T{}, total
|
||||
}
|
||||
|
||||
// 如果结束位置超出范围,截取到末尾
|
||||
if end > len(slice) {
|
||||
end = len(slice)
|
||||
}
|
||||
|
||||
return slice[start:end], total
|
||||
}
|
||||
|
||||
// ValidatePagination 验证并规范化分页参数
|
||||
// 返回规范化后的 page 和 pageSize
|
||||
func ValidatePagination(page, pageSize int) (int, int) {
|
||||
// 默认值
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 最大限制
|
||||
const maxPageSize = 100
|
||||
if pageSize > maxPageSize {
|
||||
pageSize = maxPageSize
|
||||
}
|
||||
|
||||
return page, pageSize
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/database/orm"
|
||||
"github.com/goravel/framework/support/str"
|
||||
)
|
||||
|
||||
// ApplySort 应用排序到查询
|
||||
// orderBy: 排序参数,格式为 "field:direction" 或 "field1:direction1,field2:direction2"
|
||||
// defaultSort: 默认排序,格式为 "field:direction",如果 orderBy 为空则使用此默认值
|
||||
// 返回: 应用了排序的查询对象
|
||||
func ApplySort(query orm.Query, orderBy string, defaultSort string) orm.Query {
|
||||
// 如果提供了排序参数,使用它;否则使用默认排序
|
||||
sortStr := orderBy
|
||||
if sortStr == "" {
|
||||
sortStr = defaultSort
|
||||
}
|
||||
|
||||
// 如果排序字符串为空,返回原查询
|
||||
if sortStr == "" {
|
||||
return query
|
||||
}
|
||||
|
||||
// 解析多个排序字段(逗号分隔)
|
||||
sortFields := str.Of(sortStr).Split(",")
|
||||
var orderClauses []string
|
||||
|
||||
for _, field := range sortFields {
|
||||
field = str.Of(field).Trim().String()
|
||||
if str.Of(field).IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析字段和方向(格式: "field:direction" 或 "field")
|
||||
parts := str.Of(field).Split(":")
|
||||
fieldName := str.Of(parts[0]).Trim().String()
|
||||
direction := "asc" // 默认升序
|
||||
|
||||
if len(parts) > 1 {
|
||||
direction = str.Of(parts[1]).Trim().Lower().String()
|
||||
}
|
||||
|
||||
// 验证方向
|
||||
if direction != "asc" && direction != "desc" {
|
||||
direction = "asc"
|
||||
}
|
||||
|
||||
// 收集排序子句
|
||||
orderClauses = append(orderClauses, fieldName+" "+direction)
|
||||
}
|
||||
|
||||
// 如果有排序子句,组合成一个字符串并应用
|
||||
if len(orderClauses) > 0 {
|
||||
var orderStr string
|
||||
if len(orderClauses) > 0 {
|
||||
orderStr = orderClauses[0]
|
||||
for i := 1; i < len(orderClauses); i++ {
|
||||
orderStr = str.Of(orderStr).Append(", ").Append(orderClauses[i]).String()
|
||||
}
|
||||
}
|
||||
query = query.Order(orderStr)
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
// ParseSort 解析排序参数
|
||||
// orderBy: 排序参数,格式为 "field:direction" 或 "field1:direction1,field2:direction2"
|
||||
// 返回: 排序字段和方向的映射
|
||||
func ParseSort(orderBy string) map[string]string {
|
||||
result := make(map[string]string)
|
||||
|
||||
if orderBy == "" {
|
||||
return result
|
||||
}
|
||||
|
||||
// 解析多个排序字段(逗号分隔)
|
||||
sortFields := str.Of(orderBy).Split(",")
|
||||
|
||||
for _, field := range sortFields {
|
||||
field = str.Of(field).Trim().String()
|
||||
if str.Of(field).IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析字段和方向
|
||||
parts := str.Of(field).Split(":")
|
||||
fieldName := str.Of(parts[0]).Trim().String()
|
||||
direction := "asc" // 默认升序
|
||||
|
||||
if len(parts) > 1 {
|
||||
direction = str.Of(parts[1]).Trim().Lower().String()
|
||||
}
|
||||
|
||||
// 验证方向
|
||||
if direction != "asc" && direction != "desc" {
|
||||
direction = "asc"
|
||||
}
|
||||
|
||||
result[fieldName] = direction
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"goravel/app/utils"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
)
|
||||
|
||||
// ConvertTimesInData 递归转换数据中的时间字段到对应时区
|
||||
// 使用 JSON 序列化和反序列化来确保正确处理所有类型
|
||||
func ConvertTimesInData(ctx http.Context, data any) any {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取请求的时区
|
||||
timezone := GetCurrentTimezone(ctx)
|
||||
|
||||
// 检查是否传了时区请求头
|
||||
hasTimezoneHeader := ctx.Request().Header("X-Timezone", "") != "" ||
|
||||
ctx.Request().Header("Timezone", "") != "" ||
|
||||
ctx.Request().Input("timezone") != ""
|
||||
|
||||
// 如果没有传时区请求头,且时区是 UTC,直接返回原数据(不做转换)
|
||||
if !hasTimezoneHeader && (timezone == carbon.UTC || timezone == "UTC") {
|
||||
return data
|
||||
}
|
||||
|
||||
// 先序列化为 JSON
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
// 如果序列化失败,尝试使用反射方法
|
||||
return convertTimesInValue(reflect.ValueOf(data), timezone)
|
||||
}
|
||||
|
||||
// 反序列化为 map[string]any
|
||||
var result any
|
||||
if err := json.Unmarshal(jsonData, &result); err != nil {
|
||||
// 如果反序列化失败,返回原数据
|
||||
return data
|
||||
}
|
||||
|
||||
// 转换时间字段
|
||||
converted := convertTimesInMap(result, timezone)
|
||||
|
||||
return converted
|
||||
}
|
||||
|
||||
// convertTimesInMap 递归处理 map 或 slice 中的时间字段
|
||||
func convertTimesInMap(data any, timezone string) any {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
result := make(map[string]any)
|
||||
for key, value := range v {
|
||||
// 检查是否是时间字段
|
||||
if isTimeField(key) {
|
||||
// 尝试解析时间字符串并转换
|
||||
if timeStr, ok := value.(string); ok && timeStr != "" {
|
||||
// 如果时区是 UTC,直接返回原时间字符串(不做转换)
|
||||
if timezone == carbon.UTC || timezone == "UTC" {
|
||||
result[key] = timeStr
|
||||
continue
|
||||
}
|
||||
// 否则进行时区转换
|
||||
converted := convertTimeString(timeStr, timezone)
|
||||
if converted != nil && converted != "" {
|
||||
result[key] = converted
|
||||
continue
|
||||
}
|
||||
// 如果转换失败,保留原值
|
||||
result[key] = timeStr
|
||||
continue
|
||||
}
|
||||
}
|
||||
// 递归处理嵌套数据
|
||||
result[key] = convertTimesInMap(value, timezone)
|
||||
}
|
||||
return result
|
||||
|
||||
case []any:
|
||||
result := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
result[i] = convertTimesInMap(item, timezone)
|
||||
}
|
||||
return result
|
||||
|
||||
default:
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
// convertTimeString 转换时间字符串到指定时区
|
||||
// 假设数据库存储的时间是 UTC 时区(如:2025-11-22 06:21:25)
|
||||
func convertTimeString(timeStr string, timezone string) any {
|
||||
if timeStr == "" || timeStr == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果目标时区是 UTC,直接返回原时间字符串
|
||||
if timezone == carbon.UTC || timezone == "UTC" {
|
||||
return timeStr
|
||||
}
|
||||
|
||||
// 加载时区
|
||||
utcLoc, _ := time.LoadLocation("UTC")
|
||||
targetLoc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
return timeStr
|
||||
}
|
||||
|
||||
// 解析时间字符串为 UTC(数据库存储格式)
|
||||
t, err := time.ParseInLocation(utils.DateTimeFormat, timeStr, utcLoc)
|
||||
if err != nil {
|
||||
// 如果标准格式失败,尝试其他格式
|
||||
t, err = time.Parse(time.RFC3339, timeStr)
|
||||
if err != nil {
|
||||
return timeStr
|
||||
}
|
||||
// RFC3339 格式可能带时区,转换为 UTC
|
||||
t = time.Unix(t.Unix(), 0).In(utcLoc)
|
||||
}
|
||||
|
||||
// 转换到目标时区并格式化
|
||||
return t.In(targetLoc).Format(utils.DateTimeFormat)
|
||||
}
|
||||
|
||||
// convertTimesInValue 使用反射方法处理值(作为备用方案)
|
||||
func convertTimesInValue(v reflect.Value, timezone string) any {
|
||||
if !v.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理指针
|
||||
if v.Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
return convertTimesInValue(v.Elem(), timezone)
|
||||
}
|
||||
|
||||
// 处理时间类型
|
||||
if v.Type() == reflect.TypeOf((*carbon.DateTime)(nil)).Elem() {
|
||||
dt := v.Interface().(carbon.DateTime)
|
||||
return dt.SetTimezone(timezone).ToDateTimeString()
|
||||
}
|
||||
|
||||
// 处理 *carbon.DateTime
|
||||
if v.Type() == reflect.TypeOf((*carbon.DateTime)(nil)) {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
dt := v.Interface().(*carbon.DateTime)
|
||||
if dt == nil {
|
||||
return nil
|
||||
}
|
||||
return dt.SetTimezone(timezone).ToDateTimeString()
|
||||
}
|
||||
|
||||
// 处理 time.Time
|
||||
if v.Type() == reflect.TypeOf(time.Time{}) {
|
||||
t := v.Interface().(time.Time)
|
||||
dt := carbon.NewDateTime(carbon.Parse(t.Format(utils.DateTimeFormat)))
|
||||
return dt.SetTimezone(timezone).ToDateTimeString()
|
||||
}
|
||||
|
||||
// 处理 *time.Time
|
||||
if v.Type() == reflect.TypeOf((*time.Time)(nil)) {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
t := v.Interface().(*time.Time)
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
dt := carbon.NewDateTime(carbon.Parse(t.Format(utils.DateTimeFormat)))
|
||||
return dt.SetTimezone(timezone).ToDateTimeString()
|
||||
}
|
||||
|
||||
// 处理切片
|
||||
if v.Kind() == reflect.Slice {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
result := make([]any, v.Len())
|
||||
for i := range result {
|
||||
result[i] = convertTimesInValue(v.Index(i), timezone)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 处理数组
|
||||
if v.Kind() == reflect.Array {
|
||||
result := make([]any, v.Len())
|
||||
for i := range result {
|
||||
result[i] = convertTimesInValue(v.Index(i), timezone)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 处理 map
|
||||
if v.Kind() == reflect.Map {
|
||||
if v.IsNil() {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]any)
|
||||
for _, key := range v.MapKeys() {
|
||||
keyStr := key.String()
|
||||
if key.Kind() == reflect.Interface {
|
||||
keyStr = reflect.ValueOf(key.Interface()).String()
|
||||
}
|
||||
result[keyStr] = convertTimesInValue(v.MapIndex(key), timezone)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 处理结构体
|
||||
if v.Kind() == reflect.Struct {
|
||||
result := make(map[string]any)
|
||||
t := v.Type()
|
||||
for i := range make([]int, v.NumField()) {
|
||||
field := t.Field(i)
|
||||
fieldValue := v.Field(i)
|
||||
|
||||
// 跳过未导出字段
|
||||
if !fieldValue.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldName := field.Name
|
||||
// 检查 json tag
|
||||
if jsonTag := field.Tag.Get("json"); jsonTag != "" && jsonTag != "-" {
|
||||
// 解析 json tag(处理 "name,omitempty" 格式)
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
if len(parts) > 0 && parts[0] != "" {
|
||||
fieldName = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
// 只处理时间相关字段
|
||||
if isTimeField(fieldName) || isTimeType(fieldValue.Type()) {
|
||||
result[fieldName] = convertTimesInValue(fieldValue, timezone)
|
||||
} else {
|
||||
// 递归处理嵌套结构
|
||||
if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Ptr || fieldValue.Kind() == reflect.Slice || fieldValue.Kind() == reflect.Map {
|
||||
result[fieldName] = convertTimesInValue(fieldValue, timezone)
|
||||
} else {
|
||||
result[fieldName] = fieldValue.Interface()
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 其他类型直接返回
|
||||
return v.Interface()
|
||||
}
|
||||
|
||||
// isTimeField 检查字段名是否是时间字段
|
||||
func isTimeField(fieldName string) bool {
|
||||
return fieldName == "created_at" || fieldName == "updated_at" || fieldName == "deleted_at" ||
|
||||
fieldName == "CreatedAt" || fieldName == "UpdatedAt" || fieldName == "DeletedAt"
|
||||
}
|
||||
|
||||
// isTimeType 检查类型是否是时间类型
|
||||
func isTimeType(t reflect.Type) bool {
|
||||
if t == reflect.TypeOf((*carbon.DateTime)(nil)).Elem() ||
|
||||
t == reflect.TypeOf((*carbon.DateTime)(nil)) ||
|
||||
t == reflect.TypeOf(time.Time{}) ||
|
||||
t == reflect.TypeOf((*time.Time)(nil)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"goravel/app/utils"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/goravel/framework/support/str"
|
||||
)
|
||||
|
||||
// GetCurrentTimezone 获取当前请求的时区
|
||||
// 优先从请求头 X-Timezone 或 Timezone 获取
|
||||
// 如果请求头没有或时区无效,使用配置的默认时区
|
||||
func GetCurrentTimezone(ctx http.Context) string {
|
||||
// 优先从 X-Timezone 请求头获取
|
||||
timezone := ctx.Request().Header("X-Timezone", "")
|
||||
if timezone == "" {
|
||||
// 尝试从 Timezone 请求头获取
|
||||
timezone = ctx.Request().Header("Timezone", "")
|
||||
}
|
||||
if timezone == "" {
|
||||
// 尝试从查询参数获取
|
||||
timezone = ctx.Request().Input("timezone")
|
||||
}
|
||||
|
||||
// 如果从请求中获取到了时区,规范化并返回
|
||||
if timezone != "" {
|
||||
return NormalizeTimezone(timezone)
|
||||
}
|
||||
|
||||
// 如果都没有,使用配置的默认时区
|
||||
defaultTimezone := facades.Config().GetString("app.timezone", carbon.UTC)
|
||||
return NormalizeTimezone(defaultTimezone)
|
||||
}
|
||||
|
||||
// isValidTimezone 验证时区是否有效
|
||||
func isValidTimezone(timezone string) bool {
|
||||
if timezone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 尝试加载时区
|
||||
_, err := time.LoadLocation(timezone)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// NormalizeTimezone 规范化时区名称(处理常见别名)
|
||||
func NormalizeTimezone(timezone string) string {
|
||||
timezone = str.Of(timezone).Trim().String()
|
||||
if str.Of(timezone).IsEmpty() {
|
||||
return carbon.UTC
|
||||
}
|
||||
|
||||
// 转换为标准时区名称
|
||||
timezoneMap := map[string]string{
|
||||
"UTC": "UTC",
|
||||
"GMT": "UTC",
|
||||
"PST": "America/Los_Angeles",
|
||||
"PDT": "America/Los_Angeles",
|
||||
"EST": "America/New_York",
|
||||
"EDT": "America/New_York",
|
||||
"CST": "America/Chicago",
|
||||
"CDT": "America/Chicago",
|
||||
"MST": "America/Denver",
|
||||
"MDT": "America/Denver",
|
||||
"Beijing": "Asia/Shanghai",
|
||||
"Shanghai": "Asia/Shanghai",
|
||||
"Hong Kong": "Asia/Hong_Kong",
|
||||
"Tokyo": "Asia/Tokyo",
|
||||
"Seoul": "Asia/Seoul",
|
||||
"Singapore": "Asia/Singapore",
|
||||
"London": "Europe/London",
|
||||
"Paris": "Europe/Paris",
|
||||
"Berlin": "Europe/Berlin",
|
||||
"Moscow": "Europe/Moscow",
|
||||
"Sydney": "Australia/Sydney",
|
||||
"Melbourne": "Australia/Melbourne",
|
||||
}
|
||||
|
||||
if normalized, ok := timezoneMap[timezone]; ok {
|
||||
return normalized
|
||||
}
|
||||
|
||||
// 如果时区有效,直接返回
|
||||
if isValidTimezone(timezone) {
|
||||
return timezone
|
||||
}
|
||||
|
||||
// 默认返回 UTC
|
||||
return carbon.UTC
|
||||
}
|
||||
|
||||
// ConvertTimeToTimezone 将时间字符串转换为指定时区
|
||||
// 返回转换后的时间字符串
|
||||
func ConvertTimeToTimezone(timeStr string, timezone string) string {
|
||||
if timeStr == "" {
|
||||
return ""
|
||||
}
|
||||
// 规范化时区
|
||||
timezone = NormalizeTimezone(timezone)
|
||||
|
||||
// 解析时间字符串
|
||||
dt := carbon.Parse(timeStr)
|
||||
if dt.IsZero() {
|
||||
return timeStr
|
||||
}
|
||||
// 转换时区并返回格式化的字符串
|
||||
return dt.SetTimezone(timezone).ToDateTimeString()
|
||||
}
|
||||
|
||||
// ConvertTimeByContext 根据请求头中的时区转换时间字符串
|
||||
// 返回转换后的时间字符串
|
||||
func ConvertTimeByContext(ctx http.Context, timeStr string) string {
|
||||
if timeStr == "" {
|
||||
return ""
|
||||
}
|
||||
timezone := GetCurrentTimezone(ctx)
|
||||
return ConvertTimeToTimezone(timeStr, timezone)
|
||||
}
|
||||
|
||||
// ConvertTimeToUTC 将本地时区的时间字符串转换为 UTC 时间字符串(用于数据库查询)
|
||||
// timeStr: 前端传入的时间字符串(本地时区格式,如 "2025-11-25 14:00:00")
|
||||
// ctx: 请求上下文,用于获取当前时区
|
||||
// 返回: UTC 时间字符串(如 "2025-11-25 06:00:00")
|
||||
func ConvertTimeToUTC(ctx http.Context, timeStr string) string {
|
||||
if timeStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 获取当前请求的时区
|
||||
timezone := GetCurrentTimezone(ctx)
|
||||
|
||||
// 如果已经是 UTC,直接返回
|
||||
if timezone == carbon.UTC || timezone == "UTC" {
|
||||
return timeStr
|
||||
}
|
||||
|
||||
// 加载时区
|
||||
targetLoc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
// 如果时区无效,假设是 UTC
|
||||
return timeStr
|
||||
}
|
||||
utcLoc, _ := time.LoadLocation("UTC")
|
||||
|
||||
// 解析时间字符串(假设是本地时区格式)
|
||||
// 尝试多种格式
|
||||
formats := []string{
|
||||
utils.DateTimeFormat,
|
||||
utils.DateTimeFormatT,
|
||||
utils.DateTimeFormatMs,
|
||||
utils.DateTimeFormatTZ,
|
||||
time.RFC3339,
|
||||
}
|
||||
|
||||
var t time.Time
|
||||
var parseErr error
|
||||
for _, format := range formats {
|
||||
t, parseErr = time.ParseInLocation(format, timeStr, targetLoc)
|
||||
if parseErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if parseErr != nil {
|
||||
// 如果所有格式都失败,尝试使用 carbon 解析
|
||||
dt := carbon.Parse(timeStr)
|
||||
if dt.IsZero() {
|
||||
return timeStr
|
||||
}
|
||||
// 假设解析的时间是本地时区,转换为 UTC
|
||||
return dt.SetTimezone(carbon.UTC).ToDateTimeString()
|
||||
}
|
||||
|
||||
// 转换为 UTC 并格式化
|
||||
return t.In(utcLoc).Format(utils.DateTimeFormat)
|
||||
}
|
||||
|
||||
// GetTimeQueryParam 获取并转换时间查询参数(统一处理时间查询)
|
||||
// 自动将前端传入的本地时区时间转换为 UTC 时间用于数据库查询
|
||||
// 支持常见的时间查询参数名称:start_time, end_time, created_at_start, created_at_end, updated_at_start, updated_at_end
|
||||
func GetTimeQueryParam(ctx http.Context, paramName string) string {
|
||||
timeStr := ctx.Request().Query(paramName, "")
|
||||
if timeStr == "" {
|
||||
return ""
|
||||
}
|
||||
return ConvertTimeToUTC(ctx, timeStr)
|
||||
}
|
||||
|
||||
// FormatTimeWithTimezone 使用指定时区格式化 time.Time
|
||||
func FormatTimeWithTimezone(t time.Time, timezone string) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
if timezone == "" {
|
||||
timezone = "UTC"
|
||||
}
|
||||
loc, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
return t.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
return t.In(loc).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// FormatCarbonWithTimezone 使用指定时区格式化 Carbon 时间
|
||||
func FormatCarbonWithTimezone(t *carbon.DateTime, timezone string) string {
|
||||
if t == nil || t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return FormatTimeWithTimezone(t.StdTime(), timezone)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/str"
|
||||
|
||||
"goravel/app/models"
|
||||
)
|
||||
|
||||
// GetUserFromContext 从 context 中获取 user 对象
|
||||
// 如果 context 中没有 user 或类型不匹配,返回错误
|
||||
func GetUserFromContext(ctx http.Context) (*models.User, error) {
|
||||
userValue := ctx.Value("user")
|
||||
if userValue == nil {
|
||||
return nil, errors.New("user not found in context")
|
||||
}
|
||||
|
||||
// 尝试值类型
|
||||
if user, ok := userValue.(models.User); ok {
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// 尝试指针类型
|
||||
if userPtr, ok := userValue.(*models.User); ok {
|
||||
return userPtr, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid user type in context")
|
||||
}
|
||||
|
||||
// GetUserIDFromContext 从 context 中获取 user ID
|
||||
// 如果 context 中没有 user 或类型不匹配,返回 0 和错误
|
||||
func GetUserIDFromContext(ctx http.Context) (uint, error) {
|
||||
user, err := GetUserFromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return user.ID, nil
|
||||
}
|
||||
|
||||
// GetTokenFromHeader 从请求头中获取token
|
||||
// 支持从Authorization header或URL参数中获取
|
||||
func GetTokenFromHeader(ctx http.Context) string {
|
||||
token := ctx.Request().Header("Authorization", "")
|
||||
|
||||
// 如果 Header 中没有 token,尝试从 URL 参数中获取
|
||||
if str.Of(token).IsEmpty() {
|
||||
token = ctx.Request().Query("_token", "")
|
||||
}
|
||||
|
||||
// 移除Bearer前缀(如果有)
|
||||
token = str.Of(token).ChopStart("Bearer ").Trim().String()
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
)
|
||||
|
||||
// ParseUserAgent 解析User-Agent字符串,返回浏览器和操作系统信息
|
||||
func ParseUserAgent(userAgent string) (browser, os string) {
|
||||
if userAgent == "" {
|
||||
return "Unknown", "Unknown"
|
||||
}
|
||||
|
||||
ua := strings.ToLower(userAgent)
|
||||
|
||||
// 解析浏览器
|
||||
browser = parseBrowser(ua)
|
||||
|
||||
// 解析操作系统
|
||||
os = parseOS(ua)
|
||||
|
||||
return browser, os
|
||||
}
|
||||
|
||||
// parseBrowser 解析浏览器类型
|
||||
func parseBrowser(ua string) string {
|
||||
// Chrome
|
||||
if strings.Contains(ua, "chrome") && !strings.Contains(ua, "edg") && !strings.Contains(ua, "opr") {
|
||||
// 提取Chrome版本
|
||||
if idx := strings.Index(ua, "chrome/"); idx != -1 {
|
||||
version := extractVersion(ua, idx+7)
|
||||
return "Chrome " + version
|
||||
}
|
||||
return "Chrome"
|
||||
}
|
||||
|
||||
// Edge
|
||||
if strings.Contains(ua, "edg") {
|
||||
if idx := strings.Index(ua, "edg/"); idx != -1 {
|
||||
version := extractVersion(ua, idx+4)
|
||||
return "Edge " + version
|
||||
}
|
||||
return "Edge"
|
||||
}
|
||||
|
||||
// Firefox
|
||||
if strings.Contains(ua, "firefox") {
|
||||
if idx := strings.Index(ua, "firefox/"); idx != -1 {
|
||||
version := extractVersion(ua, idx+8)
|
||||
return "Firefox " + version
|
||||
}
|
||||
return "Firefox"
|
||||
}
|
||||
|
||||
// Safari
|
||||
if strings.Contains(ua, "safari") && !strings.Contains(ua, "chrome") {
|
||||
if idx := strings.Index(ua, "version/"); idx != -1 {
|
||||
version := extractVersion(ua, idx+8)
|
||||
return "Safari " + version
|
||||
}
|
||||
return "Safari"
|
||||
}
|
||||
|
||||
// Opera
|
||||
if strings.Contains(ua, "opr") || strings.Contains(ua, "opera") {
|
||||
if idx := strings.Index(ua, "opr/"); idx != -1 {
|
||||
version := extractVersion(ua, idx+4)
|
||||
return "Opera " + version
|
||||
}
|
||||
if idx := strings.Index(ua, "version/"); idx != -1 {
|
||||
version := extractVersion(ua, idx+8)
|
||||
return "Opera " + version
|
||||
}
|
||||
return "Opera"
|
||||
}
|
||||
|
||||
// IE
|
||||
if strings.Contains(ua, "msie") || strings.Contains(ua, "trident") {
|
||||
if idx := strings.Index(ua, "msie "); idx != -1 {
|
||||
version := extractVersion(ua, idx+5)
|
||||
return "IE " + version
|
||||
}
|
||||
return "IE"
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// parseOS 解析操作系统
|
||||
func parseOS(ua string) string {
|
||||
// Windows
|
||||
if strings.Contains(ua, "windows") {
|
||||
if strings.Contains(ua, "windows nt 10.0") || strings.Contains(ua, "windows nt 6.3") {
|
||||
return "Windows 10/11"
|
||||
}
|
||||
if strings.Contains(ua, "windows nt 6.2") {
|
||||
return "Windows 8"
|
||||
}
|
||||
if strings.Contains(ua, "windows nt 6.1") {
|
||||
return "Windows 7"
|
||||
}
|
||||
if strings.Contains(ua, "windows nt 6.0") {
|
||||
return "Windows Vista"
|
||||
}
|
||||
if strings.Contains(ua, "windows nt 5.1") {
|
||||
return "Windows XP"
|
||||
}
|
||||
return "Windows"
|
||||
}
|
||||
|
||||
// macOS
|
||||
if strings.Contains(ua, "mac os x") || strings.Contains(ua, "macintosh") {
|
||||
if idx := strings.Index(ua, "mac os x "); idx != -1 {
|
||||
version := extractVersion(ua, idx+9)
|
||||
return "macOS " + version
|
||||
}
|
||||
return "macOS"
|
||||
}
|
||||
|
||||
// iOS
|
||||
if strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") || strings.Contains(ua, "ipod") {
|
||||
if idx := strings.Index(ua, "os "); idx != -1 {
|
||||
version := extractVersion(ua, idx+3)
|
||||
version = strings.ReplaceAll(version, "_", ".")
|
||||
return "iOS " + version
|
||||
}
|
||||
return "iOS"
|
||||
}
|
||||
|
||||
// Android
|
||||
if strings.Contains(ua, "android") {
|
||||
if idx := strings.Index(ua, "android "); idx != -1 {
|
||||
version := extractVersion(ua, idx+8)
|
||||
return "Android " + version
|
||||
}
|
||||
return "Android"
|
||||
}
|
||||
|
||||
// Linux
|
||||
if strings.Contains(ua, "linux") {
|
||||
return "Linux"
|
||||
}
|
||||
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
// extractVersion 从User-Agent字符串中提取版本号
|
||||
func extractVersion(ua string, startIdx int) string {
|
||||
if startIdx >= len(ua) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var version strings.Builder
|
||||
for i := startIdx; i < len(ua); i++ {
|
||||
c := ua[i]
|
||||
if (c >= '0' && c <= '9') || c == '.' || c == '_' {
|
||||
version.WriteByte(c)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result := version.String()
|
||||
if len(result) > 10 {
|
||||
return result[:10]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetBrowserAndOS 从HTTP上下文获取浏览器和操作系统信息
|
||||
func GetBrowserAndOS(ctx http.Context) (browser, os string) {
|
||||
userAgent := ctx.Request().Header("User-Agent", "")
|
||||
return ParseUserAgent(userAgent)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
"github.com/goravel/framework/support/str"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// GetIntQuery 获取并验证整数查询参数
|
||||
// 如果参数无效或不存在,返回默认值
|
||||
func GetIntQuery(ctx http.Context, key string, defaultValue int) int {
|
||||
value := ctx.Request().Query(key, "")
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
result := cast.ToInt(value)
|
||||
if result < 1 {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetUintQuery 获取并验证无符号整数查询参数
|
||||
// 如果参数无效或不存在,返回默认值
|
||||
func GetUintQuery(ctx http.Context, key string, defaultValue uint) uint {
|
||||
value := ctx.Request().Query(key, "")
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
result := cast.ToUint(value)
|
||||
if result == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetUintRoute 获取并验证路由中的无符号整数参数
|
||||
// 如果参数无效或不存在,返回 0
|
||||
func GetUintRoute(ctx http.Context, key string) uint {
|
||||
value := ctx.Request().Route(key)
|
||||
return cast.ToUint(value)
|
||||
}
|
||||
|
||||
// ParseIDsFromString 从逗号分隔的字符串中解析 ID 列表
|
||||
// 返回去重后的 ID 列表
|
||||
func ParseIDsFromString(idStr string) []uint {
|
||||
if str.Of(idStr).IsEmpty() {
|
||||
return []uint{}
|
||||
}
|
||||
|
||||
var ids []uint
|
||||
idMap := make(map[uint]bool)
|
||||
|
||||
// 分割字符串
|
||||
idStrs := str.Of(idStr).Split(",")
|
||||
for _, idStr := range idStrs {
|
||||
idStr = str.Of(idStr).Trim().String()
|
||||
if str.Of(idStr).IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
id := cast.ToUint(idStr)
|
||||
if id > 0 && !idMap[id] {
|
||||
idMap[id] = true
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
// ConvertUintSliceToAny 将 uint 切片转换为 []any
|
||||
// 用于 ORM 的 WhereIn 查询
|
||||
func ConvertUintSliceToAny(ids []uint) []any {
|
||||
if len(ids) == 0 {
|
||||
return []any{}
|
||||
}
|
||||
|
||||
result := make([]any, len(ids))
|
||||
for i, id := range ids {
|
||||
result[i] = id
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PrepareNumericFieldForValidation 在 PrepareForValidation 中准备数字字段
|
||||
// 将指定的数字字段转换为字符串,以便 in 规则能正确验证
|
||||
// 使用 cast.ToString 自动处理所有数字类型转换(int, int8-int64, uint, uint8-uint64, float32, float64)
|
||||
// 用法:在 PrepareForValidation 方法中调用此函数处理需要 in 验证的数字字段
|
||||
// 示例:return PrepareNumericFieldForValidation(data, "status")
|
||||
func PrepareNumericFieldForValidation(data validation.Data, fieldName string) error {
|
||||
if val, exist := data.Get(fieldName); exist {
|
||||
return data.Set(fieldName, cast.ToString(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user