u
This commit is contained in:
1 parent
4ce951dd56
commit
8a3dea8545
44 files changed
+1599
-30
No files matched your search
+1
-2
@@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="DataSourcePerFileMappings">
|
<component name="DataSourcePerFileMappings">
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d/console.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
<file url="file://$PROJECT_DIR$/db/queries/space.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
||||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d/console_1.sql" value="4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="db-tree-configuration">
|
||||||
|
<option name="data" value="---------------------------------------- 1:0:4db2dbd4-857e-4234-ba5e-f8cbf0ba6e5d 2:0:a77cbfeb-5761-4963-b061-a21951db7e82 " />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
+5
@@ -1,6 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="SqlDialectMappings">
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/db/queries/space.sql" dialect="PostgreSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/db/queries/space_member.sql" dialect="GenericSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/db/queries/user.sql" dialect="GenericSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/db/queries/user_oauth.sql" dialect="GenericSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/db/schema.sql" dialect="GenericSQL" />
|
||||||
<file url="PROJECT" dialect="PostgreSQL" />
|
<file url="PROJECT" dialect="PostgreSQL" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"allapp/utils/jwtx"
|
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hello handle api status
|
// Hello handle api status
|
||||||
func Hello(c fiber.Ctx) error {
|
func Hello(c fiber.Ctx) error {
|
||||||
return c.JSON(fiber.Map{"status": "success", "message": "Hello i'm ok!", "data": nil})
|
panic("测试 panic")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allapp/api/types"
|
||||||
|
"allapp/config"
|
||||||
|
"allapp/db"
|
||||||
|
"allapp/db/repo"
|
||||||
|
"allapp/utils"
|
||||||
|
"allapp/utils/errorx"
|
||||||
|
"allapp/utils/httpx"
|
||||||
|
"allapp/utils/jwtx"
|
||||||
|
"allapp/utils/requestx"
|
||||||
|
"allapp/utils/responsex"
|
||||||
|
"allapp/utils/uniqueid"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/gofiber/fiber/v3/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoginQq qq登录
|
||||||
|
func LoginQq(c fiber.Ctx) error {
|
||||||
|
dto := new(types.LoginQqDTO)
|
||||||
|
vo := new(types.LoginVO)
|
||||||
|
|
||||||
|
if err := requestx.BindAndValidate(c, dto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleThirdLogin(
|
||||||
|
c,
|
||||||
|
dto.Openid,
|
||||||
|
dto.Nickname,
|
||||||
|
dto.Avatar,
|
||||||
|
1, // QQ
|
||||||
|
vo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginWechat(c fiber.Ctx) error {
|
||||||
|
dto := new(types.LoginWeixinDTO)
|
||||||
|
vo := new(types.LoginVO)
|
||||||
|
|
||||||
|
if err := requestx.BindAndValidate(c, dto); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
openid, accessToken, err := getWechatAccess(dto.Code)
|
||||||
|
if err != nil {
|
||||||
|
return responsex.Error(c, "微信登录失败,请重试")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查是否已存在用户
|
||||||
|
_, err = db.Queries.FindUserByOpenID(c.Context(), repo.FindUserByOpenIDParams{
|
||||||
|
Openid: openid,
|
||||||
|
Type: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return handleThirdLogin(c, openid, "", "", 0, vo)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !errorx.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新用户 → 拉用户信息
|
||||||
|
nickname, avatar, err := getWechatUserInfo(accessToken, openid)
|
||||||
|
if err != nil {
|
||||||
|
return responsex.Error(c, "微信登录失败,请重试")
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleThirdLogin(c, openid, nickname, avatar, 0, vo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWechatAccess(code string) (openid string, accessToken string, err error) {
|
||||||
|
bodyData, bodyCode, err := httpx.Get(
|
||||||
|
"https://api.weixin.qq.com/sns/oauth2/access_token",
|
||||||
|
client.Config{
|
||||||
|
Param: map[string]string{
|
||||||
|
"appid": config.GetConf().Wechat.AppId,
|
||||||
|
"secret": config.GetConf().Wechat.AppSecret,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bodyCode != 200 {
|
||||||
|
return "", "", fmt.Errorf("微信Http请求失败: %d", bodyCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
accessMap, err := utils.JSONStringToMap(bodyData)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken, _ = accessMap["access_token"].(string)
|
||||||
|
openid, _ = accessMap["openid"].(string)
|
||||||
|
|
||||||
|
if accessToken == "" || openid == "" {
|
||||||
|
return "", "", errors.New("获取微信登录凭证失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWechatUserInfo(accessToken, openid string) (nickname, avatar string, err error) {
|
||||||
|
bodyData, bodyCode, err := httpx.Get(
|
||||||
|
"https://api.weixin.qq.com/sns/userinfo",
|
||||||
|
client.Config{
|
||||||
|
Param: map[string]string{
|
||||||
|
"access_token": accessToken,
|
||||||
|
"openid": openid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bodyCode != 200 {
|
||||||
|
return "", "", fmt.Errorf("微信Http请求失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap, err := utils.JSONStringToMap(bodyData)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nickname, _ = userMap["nickname"].(string)
|
||||||
|
avatar, _ = userMap["headimgurl"].(string)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleThirdLogin(
|
||||||
|
c fiber.Ctx,
|
||||||
|
openid string,
|
||||||
|
nickname string,
|
||||||
|
avatar string,
|
||||||
|
loginType int32, // 0=微信 1=QQ
|
||||||
|
vo *types.LoginVO,
|
||||||
|
) error {
|
||||||
|
user, err := db.Queries.FindUserByOpenID(c.Context(), repo.FindUserByOpenIDParams{
|
||||||
|
Openid: openid,
|
||||||
|
Type: loginType,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errorx.IsNotFound(err) {
|
||||||
|
// 用户不存在 → 注册
|
||||||
|
return registerUser(c, openid, nickname, avatar, loginType, vo)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户存在
|
||||||
|
if user.Status == 0 {
|
||||||
|
return responsex.Error(c, "账号已被禁用")
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = db.Queries.UpdateUserLastLoginTime(c.Context(), user.UserID)
|
||||||
|
|
||||||
|
token, err := jwtx.CreateToken(c, user.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vo.Token = token
|
||||||
|
vo.UserId = user.UserID
|
||||||
|
vo.Nickname = user.Nickname
|
||||||
|
vo.Avatar = user.Avatar
|
||||||
|
|
||||||
|
return responsex.OK(c, vo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册用户
|
||||||
|
func registerUser(c fiber.Ctx, openid, nickname, avatar string, loginType int32, vo *types.LoginVO) error {
|
||||||
|
userId := uniqueid.NextId()
|
||||||
|
|
||||||
|
if nickname == "" {
|
||||||
|
nickname = fmt.Sprintf("用户_%d", userId)
|
||||||
|
}
|
||||||
|
if avatar == "" {
|
||||||
|
avatar = "https://default-avatar-url.com/default.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.WithTx(c.Context(), func(q *repo.Queries) error {
|
||||||
|
// 插入用户
|
||||||
|
if err := q.InsertUser(c.Context(), repo.InsertUserParams{
|
||||||
|
ID: userId,
|
||||||
|
Nickname: nickname,
|
||||||
|
Avatar: avatar,
|
||||||
|
Status: 1,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入第三方登录信息
|
||||||
|
if err := q.InsertUserOAuth(c.Context(), repo.InsertUserOAuthParams{
|
||||||
|
ID: uniqueid.NextId(),
|
||||||
|
UserID: userId,
|
||||||
|
Type: loginType,
|
||||||
|
Openid: openid,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, _ := jwtx.CreateToken(c, userId)
|
||||||
|
|
||||||
|
vo.Token = token
|
||||||
|
vo.UserId = userId
|
||||||
|
vo.Nickname = nickname
|
||||||
|
vo.Avatar = avatar
|
||||||
|
|
||||||
|
return responsex.OK(c, vo)
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allapp/utils/jwtx"
|
||||||
|
"allapp/utils/responsex"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CtxUserIDKey = "user_id"
|
||||||
|
|
||||||
|
// Auth 登录校验中间件
|
||||||
|
func Auth() fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
|
||||||
|
token := strings.TrimSpace(c.Get("Token"))
|
||||||
|
if token == "" {
|
||||||
|
return responsex.Unauthorized(c, "未登录")
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
|
||||||
|
if !ok {
|
||||||
|
return responsex.Unauthorized(c, "登录已过期")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Locals(CtxUserIDKey, verifyToken.UserID)
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserID(c fiber.Ctx) (int64, error) {
|
||||||
|
v := c.Locals(CtxUserIDKey)
|
||||||
|
|
||||||
|
if v == nil {
|
||||||
|
return 0, responsex.Unauthorized(c, "登录已过期")
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := v.(int64)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return 0, responsex.Unauthorized(c, "登录已过期")
|
||||||
|
}
|
||||||
|
|
||||||
|
return userID, nil
|
||||||
|
}
|
||||||
+1
-1
@@ -20,7 +20,7 @@ func Logger() fiber.Handler {
|
|||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
// 记录日志
|
// 记录日志
|
||||||
log.WithContext(c.Context()).Infow("API Request",
|
log.Infow("API Request",
|
||||||
"method", c.Method(),
|
"method", c.Method(),
|
||||||
"path", c.Path(),
|
"path", c.Path(),
|
||||||
"status", c.Response().StatusCode(),
|
"status", c.Response().StatusCode(),
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allapp/db"
|
||||||
|
"allapp/db/repo"
|
||||||
|
"allapp/utils/errorx"
|
||||||
|
"allapp/utils/redisx"
|
||||||
|
"allapp/utils/responsex"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const CtxSpaceIDKey = "space_id"
|
||||||
|
|
||||||
|
func SpaceGuard() fiber.Handler {
|
||||||
|
return func(c fiber.Ctx) error {
|
||||||
|
userID, err := GetUserID(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
spaceIDStr := strings.TrimSpace(c.Get("SPACE_ID"))
|
||||||
|
|
||||||
|
spaceID, err := strconv.ParseInt(spaceIDStr, 10, 64)
|
||||||
|
if err != nil || spaceID <= 0 {
|
||||||
|
return responsex.SpaceError(c, "请选择空间")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验
|
||||||
|
inSpace, err := redisx.SpaceIsMember(spaceID, userID)
|
||||||
|
|
||||||
|
if inSpace {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检验空间是否存在
|
||||||
|
space, err := db.Queries.GetSpaceById(c.Context(), spaceID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errorx.IsNotFound(err) {
|
||||||
|
// 未找到
|
||||||
|
return responsex.SpaceError(c, "空间已失效")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空间有效,判断是否在该空间内
|
||||||
|
role, err := db.Queries.GetUserSpaceRole(c.Context(), repo.GetUserSpaceRoleParams{
|
||||||
|
SpaceID: spaceID,
|
||||||
|
UserID: spaceID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errorx.IsNotFound(err) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"allapp/api/handler"
|
"allapp/api/handler/auth"
|
||||||
"allapp/config"
|
"allapp/config"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
@@ -10,5 +10,13 @@ import (
|
|||||||
func SetupRouters(app *fiber.App) {
|
func SetupRouters(app *fiber.App) {
|
||||||
api := app.Group(config.GetConf().Server.BaseUrl)
|
api := app.Group(config.GetConf().Server.BaseUrl)
|
||||||
|
|
||||||
api.Get("/", handler.Hello)
|
public := api.Group("")
|
||||||
|
//private := api.Group("", middleware.Auth())
|
||||||
|
|
||||||
|
// auth
|
||||||
|
authRouter := public.Group("/auth")
|
||||||
|
authRouter.Post("/login/qq", auth.LoginQq)
|
||||||
|
authRouter.Post("/login/wechat", auth.LoginWechat)
|
||||||
|
|
||||||
|
// 其他的都带中间件
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type LoginQqDTO struct {
|
||||||
|
Openid string `json:"openid" validate:"required"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginWeixinDTO struct {
|
||||||
|
Code string `json:"code" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginVO struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
UserId int64 `json:"userId"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
}
|
||||||
@@ -1,28 +1,61 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"allapp/api/middleware"
|
|
||||||
"allapp/api/router"
|
"allapp/api/router"
|
||||||
|
"allapp/api/router/middleware"
|
||||||
"allapp/config"
|
"allapp/config"
|
||||||
|
"allapp/utils/responsex"
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
"github.com/gofiber/fiber/v3/log"
|
"github.com/gofiber/fiber/v3/log"
|
||||||
recoverer "github.com/gofiber/fiber/v3/middleware/recover"
|
recover2 "github.com/gofiber/fiber/v3/middleware/recover"
|
||||||
"github.com/gofiber/fiber/v3/middleware/requestid"
|
"github.com/gofiber/fiber/v3/middleware/requestid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
//func errorHandler(ctx fiber.Ctx, err error) error {
|
type structValidator struct {
|
||||||
//
|
validate *validator.Validate
|
||||||
//}
|
}
|
||||||
|
|
||||||
|
// Validate Validator needs to implement the Validate method
|
||||||
|
func (v *structValidator) Validate(out any) error {
|
||||||
|
return v.validate.Struct(out)
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := config.InitConfig(); err != nil {
|
if err := config.InitConfig(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app := fiber.New()
|
app := fiber.New(fiber.Config{
|
||||||
app.Use(recoverer.New(), requestid.New(), middleware.Logger())
|
StructValidator: &structValidator{validate: validator.New()},
|
||||||
|
ErrorHandler: func(ctx fiber.Ctx, err error) error {
|
||||||
|
log.WithContext(ctx.Context()).Errorw("系统异常",
|
||||||
|
"method", ctx.Method(),
|
||||||
|
"path", ctx.Path(),
|
||||||
|
"status", ctx.Response().StatusCode(),
|
||||||
|
"error", err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return responsex.SystemError(ctx, fmt.Sprintf("系统异常 - %v", err))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Use(recover2.New(recover2.Config{
|
||||||
|
EnableStackTrace: true,
|
||||||
|
StackTraceHandler: func(ctx fiber.Ctx, err any) {
|
||||||
|
stack := debug.Stack() // 获取 panic 堆栈
|
||||||
|
|
||||||
|
// 打日志到 zap
|
||||||
|
log.WithContext(ctx.Context()).Errorw("系统堆栈异常",
|
||||||
|
"error", err,
|
||||||
|
"stack", string(stack),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}), requestid.New(), middleware.Logger())
|
||||||
|
|
||||||
router.SetupRouters(app)
|
router.SetupRouters(app)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allapp/db/repo"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initDB() (*pgxpool.Pool, *repo.Queries, error) {
|
||||||
|
cfg := conf.Postgres
|
||||||
|
|
||||||
|
sslMode := "disable"
|
||||||
|
if cfg.SslMode {
|
||||||
|
sslMode = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf(
|
||||||
|
"postgres://%s:%s@%s:%d/%s?sslmode=%s&TimeZone=%s",
|
||||||
|
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Dbname, sslMode, cfg.TimeZone,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 使用可修改配置
|
||||||
|
cfgPool, err := pgxpool.ParseConfig(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("parse pgx config failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgPool.MaxConns = cfg.MaxOpenConns
|
||||||
|
cfgPool.MinConns = cfg.MaxIdleConns
|
||||||
|
cfgPool.MaxConnLifetime = cfg.ConnMaxLifetime
|
||||||
|
|
||||||
|
pool, err := pgxpool.NewWithConfig(ctx, cfgPool)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("create pgx pool failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试连接
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("ping database failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
queries := repo.New(pool)
|
||||||
|
return pool, queries, nil
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"allapp/db"
|
||||||
|
"allapp/utils/redisx"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -25,6 +27,22 @@ func InitConfig() error {
|
|||||||
initLogger()
|
initLogger()
|
||||||
initUniqueId()
|
initUniqueId()
|
||||||
|
|
||||||
|
// 加载数据库
|
||||||
|
pool, queries, err := initDB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加载数据库失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Pool = pool
|
||||||
|
db.Queries = queries
|
||||||
|
|
||||||
|
// 加载 redis
|
||||||
|
redis, err := initRedis()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("加载Redis失败: %w", err)
|
||||||
|
}
|
||||||
|
redisx.Client = redis
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +77,9 @@ func defaultConfig() *Config {
|
|||||||
MaxAge: 30,
|
MaxAge: 30,
|
||||||
Compress: true,
|
Compress: true,
|
||||||
},
|
},
|
||||||
|
Redis: RedisConfig{
|
||||||
|
Port: 6379,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func initLogger() {
|
|||||||
ExtraKeys: []string{"request_id"},
|
ExtraKeys: []string{"request_id"},
|
||||||
ZapOptions: []zap.Option{
|
ZapOptions: []zap.Option{
|
||||||
zap.AddCaller(), // 打开 caller
|
zap.AddCaller(), // 打开 caller
|
||||||
zap.AddCallerSkip(2), // 跳过一层 Fiber 调用
|
zap.AddCallerSkip(2), // 跳过 2 层 Fiber 调用
|
||||||
},
|
},
|
||||||
CoreConfigs: []fiberZap.CoreConfig{
|
CoreConfigs: []fiberZap.CoreConfig{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initRedis() (*redis.Client, error) {
|
||||||
|
cfg := GetConf().Redis
|
||||||
|
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||||
|
Password: cfg.Password,
|
||||||
|
DB: cfg.Db,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
||||||
|
_ = rdb.Close()
|
||||||
|
return nil, fmt.Errorf("redis ping failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rdb, nil
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ type Config struct {
|
|||||||
Postgres PostgresConfig `mapstructure:"postgres"`
|
Postgres PostgresConfig `mapstructure:"postgres"`
|
||||||
Log LogConfig `mapstructure:"log"`
|
Log LogConfig `mapstructure:"log"`
|
||||||
Wechat WechatConfig `mapstructure:"wechat"`
|
Wechat WechatConfig `mapstructure:"wechat"`
|
||||||
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -55,3 +56,10 @@ type WechatConfig struct {
|
|||||||
AppId string `mapstructure:"app_id"`
|
AppId string `mapstructure:"app_id"`
|
||||||
AppSecret string `mapstructure:"app_secret"`
|
AppSecret string `mapstructure:"app_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedisConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Db int `mapstructure:"db"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"allapp/db/repo"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Pool *pgxpool.Pool
|
||||||
|
Queries *repo.Queries
|
||||||
|
)
|
||||||
|
|
||||||
|
type SQLCBatch interface {
|
||||||
|
Exec(func(int, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTx 执行事务,支持 panic 和 error 自动回滚
|
||||||
|
// 默认使用全局 DB,调用更简洁
|
||||||
|
func WithTx(ctx context.Context, fn func(q *repo.Queries) error) (err error) {
|
||||||
|
return WithTxPool(ctx, Pool, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTxPool 支持自定义连接池
|
||||||
|
func WithTxPool(ctx context.Context, pool *pgxpool.Pool, fn func(q *repo.Queries) error) (err error) {
|
||||||
|
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
// panic 时回滚事务
|
||||||
|
if rbErr := tx.Rollback(ctx); rbErr != nil {
|
||||||
|
fmt.Printf("rollback failed during panic: %v\n", rbErr)
|
||||||
|
}
|
||||||
|
panic(p)
|
||||||
|
} else if err != nil {
|
||||||
|
// 回滚事务,并捕获 rollback 错误
|
||||||
|
if rbErr := tx.Rollback(ctx); rbErr != nil {
|
||||||
|
err = fmt.Errorf("rollback failed: %v, original error: %w", rbErr, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 提交事务,并捕获 commit 错误
|
||||||
|
if commitErr := tx.Commit(ctx); commitErr != nil {
|
||||||
|
err = fmt.Errorf("commit failed: %w", commitErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
q := Queries.WithTx(tx)
|
||||||
|
err = fn(q)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunBatch 通用批处理执行器(适配所有 sqlc Batch)
|
||||||
|
func RunBatch(ctx context.Context, batch SQLCBatch) error {
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
|
batch.Exec(func(i int, err error) {
|
||||||
|
if err != nil && firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
-- 根据id获取分享空间信息
|
||||||
|
-- name: GetSpaceById :one
|
||||||
|
SELECT * FROM space WHERE id = $1;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 获取用户在指定空间的角色
|
||||||
|
-- name: GetUserSpaceRole :one
|
||||||
|
SELECT role
|
||||||
|
FROM space_member
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND space_id = $2;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
-- name: FindUserByOpenID :one
|
||||||
|
SELECT
|
||||||
|
u.id AS user_id,
|
||||||
|
u.nickname,
|
||||||
|
u.avatar,
|
||||||
|
u.status,
|
||||||
|
o.type AS login_type,
|
||||||
|
o.openid
|
||||||
|
FROM user_oauth o
|
||||||
|
JOIN "user" u ON u.id = o.user_id
|
||||||
|
WHERE o.openid = $1
|
||||||
|
AND o.type = $2
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
-- name: InsertUser :exec
|
||||||
|
INSERT INTO "user" (
|
||||||
|
id,
|
||||||
|
nickname,
|
||||||
|
avatar,
|
||||||
|
status,
|
||||||
|
create_time,
|
||||||
|
update_time,
|
||||||
|
last_login_time
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3,$4, NOW(), NOW(),now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: UpdateUserLastLoginTime :exec
|
||||||
|
UPDATE "user"
|
||||||
|
SET last_login_time = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- name: InsertUserOAuth :exec
|
||||||
|
INSERT INTO user_oauth (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
type,
|
||||||
|
openid,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, NOW(), NOW()
|
||||||
|
);
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||||
|
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||||
|
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Space struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
InviteCode string
|
||||||
|
OwnerID int64
|
||||||
|
CreateTime pgtype.Timestamptz
|
||||||
|
UpdateTime pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpaceMember struct {
|
||||||
|
ID int64
|
||||||
|
SpaceID int64
|
||||||
|
UserID int64
|
||||||
|
Role int16
|
||||||
|
JoinTime pgtype.Timestamptz
|
||||||
|
UpdateTime pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID int64
|
||||||
|
Nickname string
|
||||||
|
Avatar string
|
||||||
|
Status int32
|
||||||
|
LastLoginTime pgtype.Timestamptz
|
||||||
|
CreateTime pgtype.Timestamptz
|
||||||
|
UpdateTime pgtype.Timestamptz
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserOauth struct {
|
||||||
|
ID int64
|
||||||
|
UserID int64
|
||||||
|
Type int32
|
||||||
|
Openid string
|
||||||
|
CreateTime pgtype.Timestamptz
|
||||||
|
UpdateTime pgtype.Timestamptz
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: space.sql
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getSpaceById = `-- name: GetSpaceById :one
|
||||||
|
SELECT id, name, invite_code, owner_id, create_time, update_time FROM space WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// 根据id获取分享空间信息
|
||||||
|
func (q *Queries) GetSpaceById(ctx context.Context, id int64) (Space, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getSpaceById, id)
|
||||||
|
var i Space
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.Name,
|
||||||
|
&i.InviteCode,
|
||||||
|
&i.OwnerID,
|
||||||
|
&i.CreateTime,
|
||||||
|
&i.UpdateTime,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: space_member.sql
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getUserSpaceRole = `-- name: GetUserSpaceRole :one
|
||||||
|
SELECT role
|
||||||
|
FROM space_member
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND space_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetUserSpaceRoleParams struct {
|
||||||
|
UserID int64
|
||||||
|
SpaceID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户在指定空间的角色
|
||||||
|
func (q *Queries) GetUserSpaceRole(ctx context.Context, arg GetUserSpaceRoleParams) (int16, error) {
|
||||||
|
row := q.db.QueryRow(ctx, getUserSpaceRole, arg.UserID, arg.SpaceID)
|
||||||
|
var role int16
|
||||||
|
err := row.Scan(&role)
|
||||||
|
return role, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: user.sql
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const findUserByOpenID = `-- name: FindUserByOpenID :one
|
||||||
|
SELECT
|
||||||
|
u.id AS user_id,
|
||||||
|
u.nickname,
|
||||||
|
u.avatar,
|
||||||
|
u.status,
|
||||||
|
o.type AS login_type,
|
||||||
|
o.openid
|
||||||
|
FROM user_oauth o
|
||||||
|
JOIN "user" u ON u.id = o.user_id
|
||||||
|
WHERE o.openid = $1
|
||||||
|
AND o.type = $2
|
||||||
|
LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
type FindUserByOpenIDParams struct {
|
||||||
|
Openid string
|
||||||
|
Type int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type FindUserByOpenIDRow struct {
|
||||||
|
UserID int64
|
||||||
|
Nickname string
|
||||||
|
Avatar string
|
||||||
|
Status int32
|
||||||
|
LoginType int32
|
||||||
|
Openid string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) FindUserByOpenID(ctx context.Context, arg FindUserByOpenIDParams) (FindUserByOpenIDRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, findUserByOpenID, arg.Openid, arg.Type)
|
||||||
|
var i FindUserByOpenIDRow
|
||||||
|
err := row.Scan(
|
||||||
|
&i.UserID,
|
||||||
|
&i.Nickname,
|
||||||
|
&i.Avatar,
|
||||||
|
&i.Status,
|
||||||
|
&i.LoginType,
|
||||||
|
&i.Openid,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertUser = `-- name: InsertUser :exec
|
||||||
|
INSERT INTO "user" (
|
||||||
|
id,
|
||||||
|
nickname,
|
||||||
|
avatar,
|
||||||
|
status,
|
||||||
|
create_time,
|
||||||
|
update_time,
|
||||||
|
last_login_time
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3,$4, NOW(), NOW(),now()
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserParams struct {
|
||||||
|
ID int64
|
||||||
|
Nickname string
|
||||||
|
Avatar string
|
||||||
|
Status int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, insertUser,
|
||||||
|
arg.ID,
|
||||||
|
arg.Nickname,
|
||||||
|
arg.Avatar,
|
||||||
|
arg.Status,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUserLastLoginTime = `-- name: UpdateUserLastLoginTime :exec
|
||||||
|
UPDATE "user"
|
||||||
|
SET last_login_time = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) UpdateUserLastLoginTime(ctx context.Context, id int64) error {
|
||||||
|
_, err := q.db.Exec(ctx, updateUserLastLoginTime, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.30.0
|
||||||
|
// source: user_oauth.sql
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const insertUserOAuth = `-- name: InsertUserOAuth :exec
|
||||||
|
INSERT INTO user_oauth (
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
type,
|
||||||
|
openid,
|
||||||
|
create_time,
|
||||||
|
update_time
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, NOW(), NOW()
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertUserOAuthParams struct {
|
||||||
|
ID int64
|
||||||
|
UserID int64
|
||||||
|
Type int32
|
||||||
|
Openid string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertUserOAuth(ctx context.Context, arg InsertUserOAuthParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, insertUserOAuth,
|
||||||
|
arg.ID,
|
||||||
|
arg.UserID,
|
||||||
|
arg.Type,
|
||||||
|
arg.Openid,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
create table "user"
|
||||||
|
(
|
||||||
|
id bigint not null
|
||||||
|
primary key,
|
||||||
|
nickname varchar(255) not null,
|
||||||
|
avatar varchar(255) not null,
|
||||||
|
status int default 0 not null,
|
||||||
|
last_login_time timestamp with time zone,
|
||||||
|
create_time timestamp with time zone default now(),
|
||||||
|
update_time timestamp with time zone default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table user_oauth
|
||||||
|
(
|
||||||
|
id bigint not null
|
||||||
|
primary key,
|
||||||
|
user_id bigint not null,
|
||||||
|
type int default 0 not null,
|
||||||
|
openid varchar(255) not null,
|
||||||
|
create_time timestamp with time zone default now(),
|
||||||
|
update_time timestamp with time zone default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table space
|
||||||
|
(
|
||||||
|
id bigint not null
|
||||||
|
primary key,
|
||||||
|
name varchar(255) not null,
|
||||||
|
invite_code varchar(30) not null,
|
||||||
|
owner_id bigint not null,
|
||||||
|
create_time timestamp with time zone default now(),
|
||||||
|
update_time timestamp with time zone default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table space_member
|
||||||
|
(
|
||||||
|
id bigint not null
|
||||||
|
primary key,
|
||||||
|
space_id bigint not null,
|
||||||
|
user_id bigint not null,
|
||||||
|
role smallint default 2 not null,
|
||||||
|
join_time timestamp with time zone default now(),
|
||||||
|
update_time timestamp with time zone default now()
|
||||||
|
);
|
||||||
@@ -14,17 +14,31 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gofiber/schema v1.7.0 // indirect
|
github.com/gofiber/schema v1.7.0 // indirect
|
||||||
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
github.com/gofiber/utils/v2 v2.0.2 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0 // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/klauspost/compress v1.18.4 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
@@ -33,10 +47,13 @@ require (
|
|||||||
github.com/tinylib/msgp v1.6.3 // indirect
|
github.com/tinylib/msgp v1.6.3 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.69.0 // indirect
|
github.com/valyala/fasthttp v1.69.0 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.50.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -2,8 +2,25 @@ github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+G
|
|||||||
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||||
@@ -22,8 +39,18 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y
|
|||||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
|
||||||
|
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
|
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
@@ -32,10 +59,17 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
|
|||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||||
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
@@ -46,6 +80,11 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||||
@@ -54,6 +93,10 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
@@ -64,6 +107,10 @@ golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
|||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
@@ -72,3 +119,6 @@ golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
|||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
postgres:
|
||||||
|
host: 117.72.182.135
|
||||||
|
port: 5432
|
||||||
|
user: postgres
|
||||||
|
password: zhang520..
|
||||||
|
dbname: app
|
||||||
|
timezone: Asia/Shanghai
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 5
|
||||||
|
conn_max_lifetime: 30m
|
||||||
|
sll_mode: false
|
||||||
|
|
||||||
|
redis:
|
||||||
|
host: 117.72.182.135
|
||||||
|
port: 6379
|
||||||
|
password: zhang520..
|
||||||
|
db: 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- engine: "postgresql" # 数据库
|
||||||
|
queries: "db/queries" # query.sql文件的路径,该文件主要用于存储DML
|
||||||
|
schema: "db/schema.sql" # schema.sql文件的路径,该文件主要用于存储DDL
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: "repo" # 模块的包名
|
||||||
|
out: "db/repo" # 代码生成路径
|
||||||
|
sql_package: "pgx/v5"
|
||||||
@@ -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 (
|
import (
|
||||||
"allapp/config"
|
"allapp/config"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CustomClaims 定义 JWT 自定义 Claims
|
||||||
type CustomClaims struct {
|
type CustomClaims struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
jwt.RegisteredClaims
|
jwt.RegisteredClaims
|
||||||
@@ -26,8 +26,8 @@ var (
|
|||||||
once sync.Once
|
once sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitJwt 初始化 JWT 单例
|
// getManager 自动初始化单例
|
||||||
func InitJwt() {
|
func getManager() *jwtManager {
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
cfg := config.GetConf().JWT
|
cfg := config.GetConf().JWT
|
||||||
manager = &jwtManager{
|
manager = &jwtManager{
|
||||||
@@ -35,20 +35,19 @@ func InitJwt() {
|
|||||||
tokenExpiry: cfg.AccessExpiry,
|
tokenExpiry: cfg.AccessExpiry,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateToken 生成 JWT
|
// CreateToken 生成 JWT
|
||||||
func CreateToken(ctx context.Context, userID int64) (string, error) {
|
func CreateToken(ctx context.Context, userID int64) (string, error) {
|
||||||
if manager == nil {
|
m := getManager()
|
||||||
return "", errors.New("jwt manager not initialized")
|
return m.createToken(userID)
|
||||||
}
|
|
||||||
return manager.createToken(userID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyToken 校验 token 并返回 claims
|
// 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 {
|
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) {
|
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 {
|
if err != nil || !token.Valid {
|
||||||
return nil, errors.New("invalid token")
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
claims, ok := token.Claims.(*CustomClaims)
|
claims, ok := token.Claims.(*CustomClaims)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("invalid claims")
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
return claims, nil
|
return claims, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// createToken 私有方法
|
// 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
|
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)
|
||||||
|
}
|
||||||
@@ -224,6 +224,7 @@ func registerUser(ctx context.Context, c *app.RequestContext, openid, nickname,
|
|||||||
Username: nickname,
|
Username: nickname,
|
||||||
Avatar: avatar,
|
Avatar: avatar,
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c).Data(resp).Send()
|
response.Success(c).Data(resp).Send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ func Auth() app.HandlerFunc {
|
|||||||
user := &ContextUser{
|
user := &ContextUser{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = setContextUser(ctx, user)
|
ctx = setContextUser(ctx, user)
|
||||||
|
|
||||||
c.Next(ctx)
|
c.Next(ctx)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ DELETE FROM space WHERE id = $1;
|
|||||||
-- name: GetSpaceById :one
|
-- name: GetSpaceById :one
|
||||||
SELECT * FROM space WHERE id = $1;
|
SELECT * FROM space WHERE id = $1;
|
||||||
|
|
||||||
|
|
||||||
-- 查询用户所在的空间信息
|
-- 查询用户所在的空间信息
|
||||||
-- name: GetSpaceByUserId :one
|
-- name: GetSpaceByUserId :one
|
||||||
SELECT s.*, m.role
|
SELECT s.*, m.role
|
||||||
|
|||||||
Reference in new issue
Block a user