This commit is contained in:
oneao committed 2025-11-05 22:25:09 +08:00
1 parent c7a1f83c67
commit 6849579652
19 files changed
+600 -249

No files matched your search

@@ -1,77 +0,0 @@
package jwt
import (
"base-go-v2/internal/config"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenExpired = errors.New("token expired")
ErrTokenInvalid = errors.New("token invalid")
)
// CustomClaims 定义自己的 payload 结构
type CustomClaims struct {
UserID string `json:"user_id"`
jwt.RegisteredClaims
}
// CreateToken 创建一个 JWT token
// 支持 config.App.JWT.Expiry == -1 永不过期
func CreateToken(orgID, userID string) (string, error) {
claims := CustomClaims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
expiry := config.App.JWT.Expiry
switch {
case expiry > 0:
expireAt := time.Now().Add(expiry)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
case expiry == -1:
// 永不过期,ExpiresAt 设置为 nil
claims.ExpiresAt = nil
default:
// 默认过期时间,例如 1 小时
expireAt := time.Now().Add(time.Hour)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.App.JWT.Secret))
}
// VerifyToken 验证并解析 JWT token
func VerifyToken(tokenString string) (*CustomClaims, error) {
signKey := []byte(config.App.JWT.Secret)
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
// 校验签名算法
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, ErrTokenInvalid
}
return signKey, nil
})
if err != nil {
return nil, ErrTokenInvalid
}
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return nil, ErrTokenInvalid
}
// 检查过期:ExpiresAt 为 nil 时永不过期
if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
return nil, ErrTokenExpired
}
return claims, nil
}
@@ -0,0 +1,82 @@
package jwtx
import (
"base-go-v2/internal/config"
"time"
"github.com/golang-jwt/jwt/v5"
)
// CustomClaims 自定义 payload,可扩展其他信息
type CustomClaims struct {
UserID int `json:"user_id"` // 改为 int
jwt.RegisteredClaims
}
// TokenResult 用于返回验证结果
type TokenResult struct {
Claims *CustomClaims
Valid bool
Expired bool
}
// CreateAccessToken 创建 Access Token,只返回 token 和 error
func CreateAccessToken(userID int) (string, error) {
return createToken(userID, config.App.JWT.AccessExpiry)
}
// CreateRefreshToken 创建 Refresh Token,只返回 token 和 error
func CreateRefreshToken(userID int) (string, error) {
return createToken(userID, config.App.JWT.RefreshExpiry)
}
// 内部方法,根据过期时间生成 token
func createToken(userID int, expiry time.Duration) (string, error) {
claims := &CustomClaims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
switch {
case expiry > 0:
expireAt := time.Now().Add(expiry)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
case expiry == -1:
claims.ExpiresAt = nil // 永不过期
default:
expireAt := time.Now().Add(time.Hour)
claims.ExpiresAt = jwt.NewNumericDate(expireAt)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(config.App.JWT.Secret))
if err != nil {
return "", err
}
return signed, nil
}
// VerifyToken 验证 token 并返回结构化结果
func VerifyToken(tokenString string) TokenResult {
result := TokenResult{Valid: false}
token, _ := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(config.App.JWT.Secret), nil
})
claims, ok := token.Claims.(*CustomClaims)
if !ok || !token.Valid {
return result // token 无效
}
result.Claims = claims
result.Valid = true
if claims.ExpiresAt != nil && time.Now().After(claims.ExpiresAt.Time) {
result.Expired = true
}
return result
}
@@ -1,5 +1,10 @@
package mapx
import (
"encoding/json"
"strconv"
)
// M 是 map[string]interface{} 的别名
type M map[string]interface{}
@@ -29,6 +34,35 @@ func (m M) SetKV(kvs ...interface{}) M {
return m
}
// ---------------- 新增方法 ----------------
// SetIntToString 将 int 或 int64 直接转为 string 存储,用于前端安全传输
func (m M) SetIntToString(key string, val interface{}) M {
var s string
switch v := val.(type) {
case int:
s = strconv.Itoa(v) // 直接转为 string
case int64:
s = strconv.FormatInt(v, 10)
default:
panic("SetIntToString: unsupported type")
}
m[key] = s
return m
}
// GetIntFromString 将前端传来的字符串解析为 int64
func (m M) GetIntFromString(key string) int64 {
if v, ok := m[key]; ok {
if s, ok := v.(string); ok {
if i, err := strconv.ParseInt(s, 10, 64); err == nil {
return i
}
}
}
return 0
}
// Get 获取值
func (m M) Get(key string) interface{} {
return m[key]
@@ -62,44 +96,15 @@ func (m M) GetInt(key string) int {
return int(val)
case float64:
return int(val)
case string:
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
return int(i)
}
}
}
return 0
}
// GetFloat 获取浮点数类型
func (m M) GetFloat(key string) float64 {
if v, ok := m[key]; ok {
switch val := v.(type) {
case float32:
return float64(val)
case float64:
return val
case int:
return float64(val)
case int8:
return float64(val)
case int16:
return float64(val)
case int32:
return float64(val)
case int64:
return float64(val)
}
}
return 0
}
// GetBool 获取布尔类型
func (m M) GetBool(key string) bool {
if v, ok := m[key]; ok {
if b, ok := v.(bool); ok {
return b
}
}
return false
}
// Merge 合并另一个 M,后者会覆盖前者同名字段
func (m M) Merge(other M) M {
for k, v := range other {
@@ -107,3 +112,8 @@ func (m M) Merge(other M) M {
}
return m
}
// MarshalJSON 支持 M 自定义 JSON 序列化
func (m M) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}(m))
}
@@ -1,42 +1,65 @@
package validate
import (
"base-go-v2/internal/errs"
"errors"
"reflect"
"fmt"
"strings"
)
// ValidateNotEmpty 校验结构体中指定字段不能为空
func ValidateNotEmpty(s interface{}, fields ...string) error {
v := reflect.ValueOf(s)
if v.Kind() == reflect.Ptr {
v = v.Elem()
// ValidationError 用于收集多个字段错误,同时实现 error 接口
type ValidationError struct {
Errors map[string]string
}
// Error 实现 error 接口
func (v *ValidationError) Error() string {
msgs := make([]string, 0, len(v.Errors))
for field, err := range v.Errors {
msgs = append(msgs, fmt.Sprintf("%s: %s", field, err))
}
return strings.Join(msgs, "; ")
}
// NotEmpty 校验 map[string]interface{} 中指定字段不能为空
func NotEmpty(data map[string]interface{}, fields ...string) error {
errs := make(map[string]string)
for _, field := range fields {
f := v.FieldByName(field)
if !f.IsValid() {
return errors.New("field " + field + " does not exist")
}
switch f.Kind() {
case reflect.String:
if strings.TrimSpace(f.String()) == "" {
return errs.NewValidationError(field, "不能为空")
}
case reflect.Slice, reflect.Array, reflect.Map:
if f.Len() == 0 {
return errs.NewValidationError(field, "不能为空")
}
case reflect.Ptr, reflect.Interface:
if f.IsNil() {
return errs.NewValidationError(field, "不能为空")
}
default:
continue
}
collectErrors(data[field], field, errs)
}
if len(errs) > 0 {
return &ValidationError{Errors: errs}
}
return nil
}
// collectErrors 递归收集字段错误
func collectErrors(v interface{}, path string, errs map[string]string) {
if v == nil {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
return
}
switch val := v.(type) {
case string:
if strings.TrimSpace(val) == "" {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
}
case []interface{}:
if len(val) == 0 {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
}
for i, elem := range val {
collectErrors(elem, fmt.Sprintf("%s[%d]", path, i), errs)
}
case map[string]interface{}:
if len(val) == 0 {
errs[path] = fmt.Sprintf("字段 %s 不能为空", path)
}
for k, elem := range val {
collectErrors(elem, fmt.Sprintf("%s.%s", path, k), errs)
}
default:
// 其他类型暂不校验
}
}