This commit is contained in:
oneao committed 2026-02-26 17:31:08 +08:00
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
}
+20
View File
@@ -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
}
+11 -12
View File
@@ -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)
}