u
This commit is contained in:
1 parent
4ce951dd56
commit
8a3dea8545
44 files changed
+1599
-30
No files matched your search
@@ -0,0 +1,13 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
// IsNotFound 判断 err 是否因为数据库中没找到数据
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, sql.ErrNoRows) || errors.Is(err, pgx.ErrNoRows)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3/client"
|
||||
)
|
||||
|
||||
// 全局 Fiber Client
|
||||
var cc = client.New()
|
||||
|
||||
func init() {
|
||||
cc.SetTimeout(10 * time.Second)
|
||||
}
|
||||
|
||||
// mergeConfig 合并默认配置和用户配置
|
||||
func mergeConfig(defaultCfg, userCfg client.Config) client.Config {
|
||||
if userCfg.Ctx != nil {
|
||||
defaultCfg.Ctx = userCfg.Ctx
|
||||
}
|
||||
if userCfg.Body != nil {
|
||||
defaultCfg.Body = userCfg.Body
|
||||
}
|
||||
if userCfg.Header != nil {
|
||||
if defaultCfg.Header == nil {
|
||||
defaultCfg.Header = map[string]string{}
|
||||
}
|
||||
for k, v := range userCfg.Header {
|
||||
defaultCfg.Header[k] = v
|
||||
}
|
||||
}
|
||||
if userCfg.Param != nil {
|
||||
defaultCfg.Param = userCfg.Param
|
||||
}
|
||||
if userCfg.PathParam != nil {
|
||||
defaultCfg.PathParam = userCfg.PathParam
|
||||
}
|
||||
if userCfg.FormData != nil {
|
||||
defaultCfg.FormData = userCfg.FormData
|
||||
}
|
||||
if userCfg.Timeout != 0 {
|
||||
defaultCfg.Timeout = userCfg.Timeout
|
||||
}
|
||||
if userCfg.UserAgent != "" {
|
||||
defaultCfg.UserAgent = userCfg.UserAgent
|
||||
}
|
||||
if userCfg.Referer != "" {
|
||||
defaultCfg.Referer = userCfg.Referer
|
||||
}
|
||||
defaultCfg.DisablePathNormalizing = userCfg.DisablePathNormalizing
|
||||
defaultCfg.MaxRedirects = userCfg.MaxRedirects
|
||||
|
||||
return defaultCfg
|
||||
}
|
||||
|
||||
func Get(url string, cfg client.Config) (string, int, error) {
|
||||
defaultCfg := client.Config{
|
||||
Ctx: context.Background(),
|
||||
}
|
||||
finalCfg := mergeConfig(defaultCfg, cfg)
|
||||
|
||||
resp, err := cc.Get(url, finalCfg)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
|
||||
return string(resp.Body()), resp.StatusCode(), nil
|
||||
}
|
||||
|
||||
func PostJSON(url string, data any, cfg client.Config) (string, int, error) {
|
||||
defaultCfg := client.Config{
|
||||
Ctx: context.Background(),
|
||||
Body: data,
|
||||
Header: map[string]string{"Content-Type": "application/json"},
|
||||
}
|
||||
finalCfg := mergeConfig(defaultCfg, cfg)
|
||||
|
||||
resp, err := cc.Post(url, finalCfg)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return string(resp.Body()), resp.StatusCode(), nil
|
||||
}
|
||||
|
||||
func PostForm(url string, formData map[string]string, cfg client.Config) (string, int, error) {
|
||||
defaultCfg := client.Config{
|
||||
Ctx: context.Background(),
|
||||
FormData: formData,
|
||||
Header: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
|
||||
}
|
||||
finalCfg := mergeConfig(defaultCfg, cfg)
|
||||
|
||||
resp, err := cc.Post(url, finalCfg)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return string(resp.Body()), resp.StatusCode(), nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// JSONStringToStruct 将 JSON 字符串反序列化到指定结构体类型
|
||||
func JSONStringToStruct[T any](jsonStr string) (*T, error) {
|
||||
var result T
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func JSONStringToMap(jsonStr string) (map[string]interface{}, error) {
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(jsonStr), &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package jwtx
|
||||
import (
|
||||
"allapp/config"
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// CustomClaims 定义 JWT 自定义 Claims
|
||||
type CustomClaims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
jwt.RegisteredClaims
|
||||
@@ -26,8 +26,8 @@ var (
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// InitJwt 初始化 JWT 单例
|
||||
func InitJwt() {
|
||||
// getManager 自动初始化单例
|
||||
func getManager() *jwtManager {
|
||||
once.Do(func() {
|
||||
cfg := config.GetConf().JWT
|
||||
manager = &jwtManager{
|
||||
@@ -35,20 +35,19 @@ func InitJwt() {
|
||||
tokenExpiry: cfg.AccessExpiry,
|
||||
}
|
||||
})
|
||||
return manager
|
||||
}
|
||||
|
||||
// CreateToken 生成 JWT
|
||||
func CreateToken(ctx context.Context, userID int64) (string, error) {
|
||||
if manager == nil {
|
||||
return "", errors.New("jwt manager not initialized")
|
||||
}
|
||||
return manager.createToken(userID)
|
||||
m := getManager()
|
||||
return m.createToken(userID)
|
||||
}
|
||||
|
||||
// VerifyToken 校验 token 并返回 claims
|
||||
func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, error) {
|
||||
func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, bool) {
|
||||
if tokenString == "" || manager == nil {
|
||||
return nil, errors.New("token empty or manager not initialized")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
@@ -56,15 +55,15 @@ func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, error)
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*CustomClaims)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid claims")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
return claims, true
|
||||
}
|
||||
|
||||
// createToken 私有方法
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package pgtypex
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func NumericToString(n pgtype.Numeric) string {
|
||||
if n.Int == nil || n.NaN {
|
||||
return "0.00"
|
||||
}
|
||||
|
||||
// 转成 decimal 保证精度
|
||||
dec := decimal.NewFromBigInt(n.Int, int32(n.Exp))
|
||||
return dec.StringFixed(2) // 保留两位小数
|
||||
}
|
||||
func NumericToDecimal(n pgtype.Numeric) decimal.Decimal {
|
||||
if n.Int == nil || n.NaN {
|
||||
return decimal.Zero
|
||||
}
|
||||
return decimal.NewFromBigInt(n.Int, int32(n.Exp))
|
||||
}
|
||||
|
||||
// TimestampToDateString 转为 "YYYY-MM-DD"
|
||||
func TimestampToDateString(ts pgtype.Timestamp) string {
|
||||
if ts.Time.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ts.Time.Format("2006-01-02")
|
||||
}
|
||||
|
||||
// TimestampToDateTimeString 转为 "YYYY-MM-DD HH:MM:SS"
|
||||
func TimestampToDateTimeString(ts pgtype.Timestamp) string {
|
||||
if ts.Time.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ts.Time.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// StringToNumeric 将字符串金额转换为 pgtype.Numeric
|
||||
func StringToNumeric(amountStr string) pgtype.Numeric {
|
||||
if strings.TrimSpace(amountStr) == "" {
|
||||
return pgtype.Numeric{Valid: false}
|
||||
}
|
||||
|
||||
dec, err := decimal.NewFromString(amountStr)
|
||||
if err != nil {
|
||||
return pgtype.Numeric{Valid: false}
|
||||
}
|
||||
|
||||
// 保留两位小数,四舍五入
|
||||
dec = dec.Round(2)
|
||||
|
||||
// 乘以 100 转整数
|
||||
intVal := dec.Mul(decimal.NewFromInt(100)).BigInt()
|
||||
|
||||
return pgtype.Numeric{
|
||||
Int: intVal,
|
||||
Exp: -2, // 固定两位小数
|
||||
NaN: false,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// StringToText 将字符串转换为 pgtype.Text
|
||||
// 空字符串返回零值 Text
|
||||
func StringToText(s string) pgtype.Text {
|
||||
if s == "" {
|
||||
return pgtype.Text{Valid: false}
|
||||
}
|
||||
return pgtype.Text{
|
||||
String: s,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
// StringToTimestamp 将字符串解析为 pgtype.Timestamp
|
||||
// 支持格式:YYYY-MM-DD HH:MM:SS 或 YYYY-MM-DD
|
||||
// 解析失败或空字符串返回零值 Timestamp
|
||||
func StringToTimestamp(s string) pgtype.Timestamp {
|
||||
if s == "" {
|
||||
return pgtype.Timestamp{Valid: false}
|
||||
}
|
||||
|
||||
var t time.Time
|
||||
var err error
|
||||
|
||||
layouts := []string{
|
||||
"2006-01-02 15:04:05", // 完整时间
|
||||
"2006-01-02", // 仅日期也可解析
|
||||
}
|
||||
|
||||
for _, layout := range layouts {
|
||||
t, err = time.Parse(layout, s)
|
||||
if err == nil {
|
||||
return pgtype.Timestamp{
|
||||
Time: t,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pgtype.Timestamp{Valid: false}
|
||||
}
|
||||
|
||||
// StringToDate 将字符串解析为 pgtype.Date
|
||||
// 支持格式:YYYY-MM-DD
|
||||
// 解析失败或空字符串返回零值 Date
|
||||
func StringToDate(s string) pgtype.Date {
|
||||
if s == "" {
|
||||
return pgtype.Date{Valid: false}
|
||||
}
|
||||
|
||||
t, err := time.Parse("2006-01-02", s)
|
||||
if err != nil {
|
||||
return pgtype.Date{Valid: false}
|
||||
}
|
||||
|
||||
return pgtype.Date{
|
||||
Time: t,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TimestampToMillis(ts pgtype.Timestamp) int64 {
|
||||
return ts.Time.UnixMilli()
|
||||
}
|
||||
|
||||
func TimestamptzToMillis(ts pgtype.Timestamptz) int64 {
|
||||
if !ts.Valid {
|
||||
return 0
|
||||
}
|
||||
return ts.Time.UnixMilli()
|
||||
}
|
||||
|
||||
func DateToString(d pgtype.Date) string {
|
||||
if !d.Valid {
|
||||
return ""
|
||||
}
|
||||
return d.Time.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func Int32ToPgInt4(value int32) pgtype.Int4 {
|
||||
return pgtype.Int4{
|
||||
Int32: value,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TimePtrToPgDate(t *time.Time) pgtype.Date {
|
||||
if t == nil {
|
||||
return pgtype.Date{Valid: false} // ⭐ SQL NULL → 不限时间
|
||||
}
|
||||
return pgtype.Date{
|
||||
Time: *t, // ⭐ 关键:解引用
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package redisx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// 统一 key 生成函数
|
||||
func spaceMemberKey(spaceID int64) string {
|
||||
return fmt.Sprintf("space:member:%d", spaceID)
|
||||
}
|
||||
|
||||
// SpaceAddOrUpdateMember 添加 / 更新成员角色
|
||||
func SpaceAddOrUpdateMember(spaceID int64, userID int64, role int) error {
|
||||
key := spaceMemberKey(spaceID)
|
||||
|
||||
return Client.HSet(
|
||||
context.Background(),
|
||||
key,
|
||||
strconv.FormatInt(userID, 10),
|
||||
role,
|
||||
).Err()
|
||||
}
|
||||
|
||||
// SpaceRemoveMember 删除成员
|
||||
func SpaceRemoveMember(spaceID int64, userID int64) error {
|
||||
key := spaceMemberKey(spaceID)
|
||||
|
||||
return Client.HDel(
|
||||
context.Background(),
|
||||
key,
|
||||
strconv.FormatInt(userID, 10),
|
||||
).Err()
|
||||
}
|
||||
|
||||
// SpaceGetUserRole 获取成员角色
|
||||
func SpaceGetUserRole(spaceID int64, userID int64) (int, bool, error) {
|
||||
key := spaceMemberKey(spaceID)
|
||||
|
||||
val, err := Client.HGet(
|
||||
context.Background(),
|
||||
key,
|
||||
strconv.FormatInt(userID, 10),
|
||||
).Result()
|
||||
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return 0, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
role, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
return role, true, nil
|
||||
}
|
||||
|
||||
// SpaceIsMember 判断是否在空间
|
||||
func SpaceIsMember(spaceID int64, userID int64) (bool, error) {
|
||||
_, ok, err := SpaceGetUserRole(spaceID, userID)
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// SpaceInitMembers 初始化用户成员
|
||||
func SpaceInitMembers(spaceID int64, members map[int64]int) error {
|
||||
key := spaceMemberKey(spaceID)
|
||||
|
||||
data := make(map[string]interface{})
|
||||
for userID, role := range members {
|
||||
data[strconv.FormatInt(userID, 10)] = role
|
||||
}
|
||||
|
||||
return Client.HSet(
|
||||
context.Background(),
|
||||
key,
|
||||
data,
|
||||
).Err()
|
||||
}
|
||||
|
||||
// SpaceDelete 删除空间
|
||||
func SpaceDelete(spaceID int64) error {
|
||||
key := spaceMemberKey(spaceID)
|
||||
|
||||
return Client.Del(
|
||||
context.Background(),
|
||||
key,
|
||||
).Err()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package redisx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
Client *redis.Client
|
||||
)
|
||||
|
||||
func Set(key string, value interface{}, expire time.Duration) error {
|
||||
return Client.Set(context.Background(), key, value, expire).Err()
|
||||
}
|
||||
|
||||
// Get 获取任意 key 的值,返回 string
|
||||
func Get(key string) (string, error) {
|
||||
val, err := Client.Get(context.Background(), key).Result()
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return "", nil
|
||||
}
|
||||
return val, err
|
||||
}
|
||||
|
||||
// Delete 删除任意 key
|
||||
func Delete(key string) error {
|
||||
return Client.Del(context.Background(), key).Err()
|
||||
}
|
||||
|
||||
// Exists 判断 key 是否存在
|
||||
func Exists(key string) (bool, error) {
|
||||
cnt, err := Client.Exists(context.Background(), key).Result()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return cnt > 0, nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package requestx
|
||||
|
||||
import (
|
||||
"allapp/utils/responsex"
|
||||
"errors"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// BindAndValidate 封装请求体解析和校验
|
||||
func BindAndValidate(c fiber.Ctx, v any) error {
|
||||
if err := c.Bind().Body(v); err != nil {
|
||||
// Handle validation errors
|
||||
var validationErrors validator.ValidationErrors
|
||||
if errors.As(err, &validationErrors) {
|
||||
return responsex.Error(c, "参数校验失败")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1 +1,72 @@
|
||||
package responsex
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
// Result 响应结构
|
||||
type Result struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
TrackId string `json:"trackId,omitempty"`
|
||||
}
|
||||
|
||||
// 业务码
|
||||
var BizCode = struct {
|
||||
Success int // 成功
|
||||
Unauthorized int // 未授权/Token失效
|
||||
SpaceError int // 空间异常(已退出/被踢出等)
|
||||
BusinessError int // 业务异常,例如参数错误、操作不允许
|
||||
SystemError int // 系统异常,例如数据库崩溃、未知错误
|
||||
}{
|
||||
Success: 1000,
|
||||
Unauthorized: 1001,
|
||||
SpaceError: 1002,
|
||||
BusinessError: 2000,
|
||||
SystemError: 3000,
|
||||
}
|
||||
|
||||
// ------------------- 统一响应方法 -------------------
|
||||
|
||||
// Respond 返回通用响应,code和msg必填,data可选
|
||||
func Respond(c fiber.Ctx, code int, msg string, data interface{}) error {
|
||||
trackId := ""
|
||||
if v := c.Context().Value("request_id"); v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
trackId = s
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(Result{
|
||||
Code: code,
|
||||
Message: msg,
|
||||
Data: data,
|
||||
TrackId: trackId,
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------- 便捷封装 -------------------
|
||||
|
||||
// OK 成功响应
|
||||
func OK(c fiber.Ctx, data interface{}) error {
|
||||
return Respond(c, BizCode.Success, "success", data)
|
||||
}
|
||||
|
||||
// Error 业务异常
|
||||
func Error(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.BusinessError, msg, nil)
|
||||
}
|
||||
|
||||
// SystemError 系统异常
|
||||
func SystemError(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.SystemError, msg, nil)
|
||||
}
|
||||
|
||||
// Unauthorized 授权失败
|
||||
func Unauthorized(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.Unauthorized, msg, nil)
|
||||
}
|
||||
|
||||
// SpaceError 空间异常
|
||||
func SpaceError(c fiber.Ctx, msg string) error {
|
||||
return Respond(c, BizCode.SpaceError, msg, nil)
|
||||
}
|
||||
Reference in new issue
Block a user