This commit is contained in:
Joe
2026-01-16 15:49:34 +08:00
commit 550d3e1f42
380 changed files with 62024 additions and 0 deletions
+40
View File
@@ -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
}
+79
View File
@@ -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
}
+6
View File
@@ -0,0 +1,6 @@
package helpers
// 注意:FindByID 函数已移动到 response 包中,以避免循环导入
// 请使用 response.FindByID 代替 helpers.FindByID
// 相关的 FindByIDOptions 类型也在 response 包中
+45
View File
@@ -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
}
+105
View File
@@ -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
}
+282
View File
@@ -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
}
+213
View File
@@ -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)
}
+58
View File
@@ -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
}
+175
View File
@@ -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)
}
+97
View File
@@ -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
}