重构代码结构与认证体系,集成异步任务消费者

This commit is contained in:
2025-11-17 18:38:10 +08:00
parent a97c970166
commit a245229bc2
70 changed files with 2000 additions and 2334 deletions

2
.gitignore vendored
View File

@@ -7,7 +7,7 @@
!.env.example
bin/
*.exe
*.exe*
*.pem
*.http

4
.vscode/launch.json vendored
View File

@@ -8,9 +8,9 @@
"name": "main",
"type": "go",
"request": "launch",
"mode": "auto",
"mode": "debug",
"program": "${workspaceFolder}/cmd/main",
"cwd": "${workspaceFolder}",
"cwd": "${workspaceFolder}"
}
]
}

View File

@@ -1,128 +0,0 @@
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"log/slog"
"platform/pkg/env"
"platform/pkg/logs"
"platform/pkg/u"
client2 "platform/web/domains/client"
proxy2 "platform/web/domains/proxy"
m "platform/web/models"
q "platform/web/queries"
)
func main() {
env.Init()
logs.Init()
// 初始化数据库连接
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai",
env.DbHost, env.DbUserName, env.DbPassword, env.DbName, env.DbPort,
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
})
if err != nil {
slog.Error("gorm 初始化数据库失败:", slog.Any("err", err))
panic(err)
}
q.SetDefault(db)
// 填充数据
err = q.Q.Transaction(func(tx *q.Query) (err error) {
// 代理
err = q.Proxy.
Select(q.Proxy.Version, q.Proxy.Name, q.Proxy.Host, q.Proxy.Type, q.Proxy.Secret).
Create(&m.Proxy{
Version: 1,
Name: "7a17e8b4-cdc3-4500-bf16-4a665991a7f6",
Host: "110.40.82.248",
Type: int32(proxy2.TypeSelfHosted),
Secret: u.P("api:123456"),
}, &m.Proxy{
Version: 1,
Name: "58e03f38-4cef-429c-8bb8-530142d0a745",
Host: "123.6.147.241",
Type: int32(proxy2.TypeThirdParty),
Secret: u.P("api:123456"),
})
if err != nil {
return err
}
// 客户端
testSecret, err := bcrypt.GenerateFromPassword([]byte("test"), bcrypt.DefaultCost)
if err != nil {
return err
}
tasksSecret, err := bcrypt.GenerateFromPassword([]byte("tasks"), bcrypt.DefaultCost)
if err != nil {
return err
}
proxySecret, err := bcrypt.GenerateFromPassword([]byte("proxy"), bcrypt.DefaultCost)
if err != nil {
return err
}
err = q.Client.
Select(
q.Client.ClientID,
q.Client.ClientSecret,
q.Client.GrantClient,
q.Client.GrantRefresh,
q.Client.GrantPassword,
q.Client.Spec,
q.Client.Name,
).
Create(&m.Client{
ClientID: "test",
ClientSecret: string(testSecret),
GrantCode: true,
GrantClient: true,
GrantRefresh: true,
GrantPassword: true,
Spec: int32(client2.SpecTrusted),
Name: "默认客户端",
}, &m.Client{
ClientID: "tasks",
ClientSecret: string(tasksSecret),
GrantClient: true,
Spec: int32(client2.SpecTrusted),
Name: "异步任务处理服务",
}, &m.Client{
ClientID: "proxy",
ClientSecret: string(proxySecret),
GrantClient: true,
Spec: int32(client2.SpecTrusted),
Name: "代理转发服务",
}, &m.Client{
ClientID: "edge",
GrantClient: true,
Spec: int32(client2.SpecWeb),
Name: "代理边缘节点",
})
if err != nil {
return err
}
return nil
})
if err != nil {
panic(err)
}
slog.Info("✔ Data inserted successfully")
}

View File

@@ -1,12 +1,13 @@
package main
import (
"strings"
"gorm.io/driver/postgres"
"gorm.io/gen"
"gorm.io/gen/field"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"strings"
)
var g *gen.Generator
@@ -15,7 +16,7 @@ func main() {
// 初始化
db, _ := gorm.Open(
postgres.Open("host=localhost user=test password=test dbname=app port=5432 sslmode=disable TimeZone=Asia/Shanghai"),
postgres.Open("host=localhost user=dev password=dev dbname=app port=5432 sslmode=disable TimeZone=Asia/Shanghai"),
&gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
@@ -47,11 +48,13 @@ func main() {
return field
}),
gen.FieldRename("contact_qq", "ContactQQ"),
gen.FieldRename("ua", "UA"),
}
// 生成模型
customs := make(map[string]any)
// resource
resourceShort := g.GenerateModel("resource_short", common...)
customs["resource_short"] = resourceShort
@@ -76,6 +79,7 @@ func main() {
)...)
customs["resource"] = resource
// trade
trade := g.GenerateModel("trade", common...)
customs["trade"] = trade
@@ -104,6 +108,7 @@ func main() {
)...)
customs["bill"] = bill
// proxy
edge := g.GenerateModel("edge", common...)
customs["edge"] = edge
@@ -117,9 +122,42 @@ func main() {
)...)
customs["proxy"] = proxy
// session
user := g.GenerateModel("user", common...)
customs["user"] = user
admin := g.GenerateModel("admin", common...)
customs["admin"] = admin
client := g.GenerateModel("client", common...)
customs["client"] = client
session := g.GenerateModel("session", append(common,
gen.FieldRelate(field.BelongsTo, "User", user, &field.RelateConfig{
RelatePointer: true,
GORMTag: field.GormTag{
"foreignKey": []string{"UserID"},
},
}),
gen.FieldRelate(field.BelongsTo, "Admin", admin, &field.RelateConfig{
RelatePointer: true,
GORMTag: field.GormTag{
"foreignKey": []string{"AdminID"},
},
}),
gen.FieldRelate(field.BelongsTo, "Client", client, &field.RelateConfig{
RelatePointer: true,
GORMTag: field.GormTag{
"foreignKey": []string{"ClientID"},
"belongsTo": []string{"ID"},
},
}),
)...)
customs["session"] = session
// 生成表结构
tables, _ := db.Migrator().GetTables()
models := make([]interface{}, len(tables))
models := make([]any, len(tables))
for i, name := range tables {
if customs[name] != nil {
models[i] = customs[name]

View File

@@ -1,6 +1,8 @@
package main
import (
"context"
"fmt"
"log/slog"
"os"
"os/signal"
@@ -11,45 +13,16 @@ import (
)
func main() {
// 退出信号
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
// 初始化应用
env.Init()
logs.Init()
// 创建服务
app, err := web.New(&web.Config{
Listen: ":8080",
})
err := web.RunApp(ctx)
if err != nil {
slog.Error("Failed to create server", slog.Any("err", err))
return
}
// 异步运行服务
errCh := make(chan error)
defer close(errCh)
go func() {
err := app.Run()
if err != nil {
slog.Error("Failed to run server", slog.Any("err", err))
errCh <- err
}
errCh <- nil
}()
// 关闭服务
select {
case err = <-errCh:
case <-shutdown:
slog.Debug("捕获结束信号")
app.Stop()
err = <-errCh
}
if err != nil {
slog.Error("Server error", slog.Any("err", err))
slog.Error(fmt.Sprintf("%v", err))
}
}

View File

@@ -1,7 +1,6 @@
name: server-dev
services:
postgres:
image: postgres:17
environment:
@@ -9,24 +8,19 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5432:5432"
- "${DB_PORT}:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
postgres-migration:
image: postgres:17
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5433:5432"
restart: unless-stopped
redis:
image: redis:7.4
restart: always
ports:
- "6379:6379"
- "${REDIS_PORT}:6379"
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
postgres_data:
redis_data:

48
go.mod
View File

@@ -1,6 +1,6 @@
module platform
go 1.24.5
go 1.25.3
require (
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.7
@@ -9,18 +9,20 @@ require (
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.26.0
github.com/go-redsync/redsync/v4 v4.13.0
github.com/gofiber/fiber/v2 v2.52.6
github.com/gofiber/contrib/otelfiber/v2 v2.2.3
github.com/gofiber/fiber/v2 v2.52.9
github.com/google/uuid v1.6.0
github.com/hibiken/asynq v0.25.1
github.com/jdcloud-api/jdcloud-sdk-go v1.64.0
github.com/joho/godotenv v1.5.1
github.com/jxskiss/base62 v1.1.0
github.com/lmittmann/tint v1.0.7
github.com/redis/go-redis/v9 v9.8.0
github.com/redis/go-redis/v9 v9.12.1
github.com/shopspring/decimal v1.4.0
github.com/smartwalle/alipay/v3 v3.2.25
github.com/wechatpay-apiv3/wechatpay-go v0.2.20
golang.org/x/crypto v0.36.0
golang.org/x/crypto v0.43.0
golang.org/x/sync v0.17.0
gorm.io/driver/postgres v1.5.11
gorm.io/gen v0.3.27
gorm.io/gorm v1.25.12
@@ -36,15 +38,18 @@ require (
github.com/alibabacloud-go/tea v1.3.8 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/aliyun/credentials-go v1.4.5 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-sql-driver/mysql v1.9.1 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -54,31 +59,36 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/smartwalle/ncrypto v1.0.4 // indirect
github.com/smartwalle/ngx v1.0.9 // indirect
github.com/smartwalle/nsign v1.0.9 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
github.com/valyala/fasthttp v1.68.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib v1.20.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.31.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/datatypes v1.2.5 // indirect
gorm.io/driver/mysql v1.5.7 // indirect

96
go.sum
View File

@@ -53,8 +53,8 @@ github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTs
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -66,6 +66,10 @@ github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -79,6 +83,11 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -96,10 +105,12 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq
github.com/go-redsync/redsync/v4 v4.13.0 h1:49X6GJfnbLGaIpBBREM/zA4uIMDXKAh1NDkvQ1EkZKA=
github.com/go-redsync/redsync/v4 v4.13.0/go.mod h1:HMW4Q224GZQz6x1Xc7040Yfgacukdzu7ifTDAKiyErQ=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gofiber/contrib/otelfiber/v2 v2.2.3 h1:WKW1XezHFAoohGZwnvC0R8TFJcNkabQwB5YIpdKmz00=
github.com/gofiber/contrib/otelfiber/v2 v2.2.3/go.mod h1:WdQ1tYbL83IYC6oBaWvKBMVGSAYvSTRuUWTcr0wK1T4=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
@@ -160,8 +171,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -176,12 +187,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -195,17 +206,14 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI=
github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo=
github.com/redis/rueidis v1.0.19/go.mod h1:8B+r5wdnjwK3lTFml5VtxjzGOQAC+5UmujoD12pDrEo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/smartwalle/alipay/v3 v3.2.25 h1:cRDN+fpDWTVHnuHIF/vsJETskRXS/S+fDOdAkzXmV/Q=
@@ -231,8 +239,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM=
github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
@@ -240,8 +248,8 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/wechatpay-apiv3/wechatpay-go v0.2.20 h1:gS8oFn1bHGnyapR2Zb4aqTV6l4kJWgbtqjCq6k1L9DQ=
github.com/wechatpay-apiv3/wechatpay-go v0.2.20/go.mod h1:A254AUBVB6R+EqQFo3yTgeh7HtyqRRtN2w9hQSOrd4Q=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@@ -249,6 +257,16 @@ github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3i
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib v1.20.0 h1:oXUiIQLlkbi9uZB/bt5B1WRLsrTKqb7bPpAQ+6htn2w=
go.opentelemetry.io/contrib v1.20.0/go.mod h1:gIzjwWFoGazJmtCaDgViqOSJPde2mCWzv60o0bWPcZs=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -265,8 +283,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -277,8 +295,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -300,8 +318,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -313,8 +331,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -335,8 +353,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -359,8 +377,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -375,8 +393,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -394,8 +412,8 @@ google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQ
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

546
pkg/env/env.go vendored
View File

@@ -1,274 +1,54 @@
package env
import (
"fmt"
"log/slog"
"os"
"platform/pkg/u"
"slices"
"strconv"
"github.com/gofiber/fiber/v2/log"
"github.com/joho/godotenv"
)
// region app
const (
RunModeDev = "debug"
RunModeDev = "development"
RunModeProd = "production"
)
var (
RunMode = RunModeDev
RunMode = RunModeProd
LogLevel = slog.LevelDebug
TradeExpire = 30 * 60 // 交易过期时间,单位秒
)
SessionAccessExpire = 60 * 60 * 2 // 默认 2 小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 默认 7 天
DebugHttpDump = false // 是否打印请求和响应的原始数据
DebugExternalChange = true // 是否实际执行外部非幂等接口调用,在开发调试时可以关闭,避免对外部数据产生影响
func loadApp() {
_RunMode := os.Getenv("RUN_MODE")
switch _RunMode {
case RunModeDev, RunModeProd:
RunMode = _RunMode
case "":
break
default:
panic("环境变量 RUN_MODE 的值只能是 " + RunModeDev + " 或 " + RunModeProd)
}
_TradeExpire := os.Getenv("TRADE_EXPIRE")
if _TradeExpire != "" {
value, err := strconv.Atoi(_TradeExpire)
if err != nil {
panic("环境变量 TRADE_EXPIRE 的值不是数字")
}
TradeExpire = value
}
}
// endregion
// region auth
var (
SessionAccessExpire = 60 * 60 * 2 // 2小时
SessionRefreshExpire = 60 * 60 * 24 * 7 // 7天
)
func loadAuth() {
_SessionAccessExpire := os.Getenv("SESSION_ACCESS_EXPIRE")
if _SessionAccessExpire != "" {
value, err := strconv.Atoi(_SessionAccessExpire)
if err != nil {
panic("环境变量 SESSION_ACCESS_EXPIRE 的值不是数字")
}
SessionAccessExpire = value
}
_SessionRefreshExpire := os.Getenv("SESSION_REFRESH_EXPIRE")
if _SessionRefreshExpire != "" {
value, err := strconv.Atoi(_SessionRefreshExpire)
if err != nil {
panic("环境变量 SESSION_REFRESH_EXPIRE 的值不是数字")
}
SessionRefreshExpire = value
}
}
// endregion
// region db
var (
DbHost = "localhost"
DbPort = "5432"
DbName string
DbUserName string
DbPassword string
)
func loadDb() {
_DbHost := os.Getenv("DB_HOST")
if _DbHost != "" {
DbHost = _DbHost
}
_DbPort := os.Getenv("DB_PORT")
if _DbPort != "" {
DbPort = _DbPort
}
_DbName := os.Getenv("DB_NAME")
if _DbName != "" {
DbName = _DbName
} else {
panic("环境变量 DB_NAME 的值不能为空")
}
_DbUserName := os.Getenv("DB_USERNAME")
if _DbUserName != "" {
DbUserName = _DbUserName
} else {
panic("环境变量 DB_USERNAME 的值不能为空")
}
_DbPassword := os.Getenv("DB_PASSWORD")
if _DbPassword != "" {
DbPassword = _DbPassword
} else {
panic("环境变量 DB_PASSWORD 的值不能为空")
}
}
// endregion
// region redis
var (
RedisHost = "localhost"
RedisPort = "6379"
RedisDb = 0
RedisPass = ""
)
RedisPassword = ""
func loadRedis() {
_RedisHost := os.Getenv("REDIS_HOST")
if _RedisHost != "" {
RedisHost = _RedisHost
}
_RedisPort := os.Getenv("REDIS_PORT")
if _RedisPort != "" {
RedisPort = _RedisPort
}
_RedisDb := os.Getenv("REDIS_DB")
if _RedisDb != "" {
atoi, err := strconv.Atoi(_RedisDb)
if err != nil {
panic("环境变量 REDIS_DB 的值不是数字")
}
RedisDb = atoi
}
_RedisPass := os.Getenv("REDIS_PASS")
if _RedisPass != "" {
RedisPass = _RedisPass
}
}
// endregion
// region log
var (
LogLevel = slog.LevelDebug
)
func loadLog() {
_LogLevel := os.Getenv("LOG_LEVEL")
switch _LogLevel {
case "debug":
LogLevel = slog.LevelDebug
case "info":
LogLevel = slog.LevelInfo
case "warn":
LogLevel = slog.LevelWarn
case "error":
LogLevel = slog.LevelError
}
}
// endregion
// region remote
var (
BaiyinAddr = "http://103.139.212.110:9989"
BaiyinTokenUrl string
)
var (
IdenCallbackUrl string
IdenAccessKey string
IdenSecretKey string
)
func loadRemote() {
_BaiyinAddr := os.Getenv("BAIYIN_ADDR")
if _BaiyinAddr != "" {
BaiyinAddr = _BaiyinAddr
}
_BaiyinTokenUrl := os.Getenv("BAIYIN_TOKEN_URL")
if _BaiyinTokenUrl == "" {
panic("环境变量 BAIYIN_TOKEN_URL 的值不能为空")
}
BaiyinTokenUrl = _BaiyinTokenUrl
_IdenCallbackUrl := os.Getenv("IDEN_CALLBACK_URL")
if _IdenCallbackUrl == "" {
panic("环境变量 IDEN_CALLBACK_URL 的值不能为空")
}
IdenCallbackUrl = _IdenCallbackUrl
_IdenAccessKey := os.Getenv("IDEN_ACCESS_KEY")
if _IdenAccessKey == "" {
panic("环境变量 IDEN_ACCESS_KEY 的值不能为空")
}
IdenAccessKey = _IdenAccessKey
_IdenSecretKey := os.Getenv("IDEN_SECRET_KEY")
if _IdenSecretKey == "" {
panic("环境变量 IDEN_SECRET_KEY 的值不能为空")
}
IdenSecretKey = _IdenSecretKey
}
// endregion
// region alipay
var (
AlipayAppId string
AlipayAppPrivateKey string
AlipayPublicKey string
AlipayApiCert string
AlipayProduction = false
)
func loadAlipay() {
AlipayAppId = os.Getenv("ALIPAY_APP_ID")
if AlipayAppId == "" {
panic("环境变量 ALIPAY_APP_ID 的值不能为空")
}
AlipayAppPrivateKey = os.Getenv("ALIPAY_APP_PRIVATE_KEY")
if AlipayAppPrivateKey == "" {
panic("环境变量 ALIPAY_APP_PRIVATE_KEY 的值不能为空")
}
AlipayPublicKey = os.Getenv("ALIPAY_PUBLIC_KEY")
if AlipayPublicKey == "" {
panic("环境变量 ALIPAY_PUBLIC_KEY 的值不能为空")
}
AlipayApiCert = os.Getenv("ALIPAY_API_CERT")
if AlipayApiCert == "" {
panic("环境变量 ALIPAY_API_CERT 的值不能为空")
}
_AlipayProduction := os.Getenv("ALIPAY_PRODUCTION")
if _AlipayProduction != "" {
value, err := strconv.ParseBool(_AlipayProduction)
if err != nil {
panic("环境变量 ALIPAY_PRODUCTION 的值不是布尔值")
}
AlipayProduction = value
}
}
// endregion
// region wechatpay
var (
WechatPayAppId string
WechatPayMchId string
WechatPayMchPrivateKeySerial string
@@ -277,185 +57,21 @@ var (
WechatPayPublicKey string
WechatPayApiCert string
WechatPayCallbackUrl string
)
func loadWechatPay() {
WechatPayAppId = os.Getenv("WECHATPAY_APP_ID")
if WechatPayAppId == "" {
panic("环境变量 WECHATPAY_APP_ID 的值不能为空")
}
WechatPayMchId = os.Getenv("WECHATPAY_MCH_ID")
if WechatPayMchId == "" {
panic("环境变量 WECHATPAY_MCH_ID 的值不能为空")
}
WechatPayMchPrivateKeySerial = os.Getenv("WECHATPAY_MCH_PRIVATE_KEY_SERIAL")
if WechatPayMchPrivateKeySerial == "" {
panic("环境变量 WECHATPAY_MCH_PRIVATE_KEY_SERIAL 的值不能为空")
}
WechatPayMchPrivateKey = os.Getenv("WECHATPAY_MCH_PRIVATE_KEY")
if WechatPayMchPrivateKey == "" {
panic("环境变量 WECHATPAY_MCH_PRIVATE_KEY 的值不能为空")
}
WechatPayPublicKeyId = os.Getenv("WECHATPAY_PUBLIC_KEY_ID")
if WechatPayPublicKeyId == "" {
panic("环境变量 WECHATPAY_PUBLIC_KEY_ID 的值不能为空")
}
WechatPayPublicKey = os.Getenv("WECHATPAY_PUBLIC_KEY")
if WechatPayPublicKey == "" {
panic("环境变量 WECHATPAY_PUBLIC_KEY 的值不能为空")
}
WechatPayApiCert = os.Getenv("WECHATPAY_API_CERT")
if WechatPayApiCert == "" {
panic("环境变量 WECHATPAY_API_CERT 的值不能为空")
}
WechatPayCallbackUrl = os.Getenv("WECHATPAY_CALLBACK_URL")
if WechatPayCallbackUrl == "" {
panic("环境变量 WECHATPAY_CALLBACK_URL 的值不能为空")
}
}
// endregion
// region aliyun
var (
AliyunAccessKey string
AliyunAccessKeySecret string
AliyunSmsSignature string
AliyunSmsTemplateLogin string
)
func loadAliyun() {
AliyunAccessKey = os.Getenv("ALIYUN_ACCESS_KEY")
if AliyunAccessKey == "" {
panic("环境变量 ALIYUN_ACCESS_KEY 的值不能为空")
}
AliyunAccessKeySecret = os.Getenv("ALIYUN_ACCESS_KEY_SECRET")
if AliyunAccessKeySecret == "" {
panic("环境变量 ALIYUN_ACCESS_KEY_SECRET 的值不能为空")
}
AliyunSmsSignature = os.Getenv("ALIYUN_SMS_SIGNATURE")
if AliyunSmsSignature == "" {
panic("环境变量 ALIYUN_SMS_SIGNATURE 的值不能为空")
}
AliyunSmsTemplateLogin = os.Getenv("ALIYUN_SMS_TEMPLATE_LOGIN")
if AliyunSmsTemplateLogin == "" {
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
}
}
// endregion
// region 商福通
var (
SftPayEnable = false
SftPayAppId string
SftPayRouteId string
SftPayAppPrivateKey string
SftPayPublicKey string
SftReturnUrl *string
SftNotifyUrl *string
SftReturnUrl string
SftNotifyUrl string
)
func loadSftPay() {
var value string
value = os.Getenv("SFTPAY_ENABLE")
if value != "" {
enabled, err := strconv.ParseBool(value)
if err != nil {
panic("环境变量 SFTPAY_ENABLE 的值不是布尔值")
}
SftPayEnable = enabled
}
value = os.Getenv("SFTPAY_APP_ID")
if value == "" {
panic("环境变量 ALIYUN_SMS_TEMPLATE_LOGIN 的值不能为空")
} else {
SftPayAppId = value
}
value = os.Getenv("SFTPAY_ROUTE_ID")
if value != "" {
SftPayRouteId = value
}
value = os.Getenv("SFTPAY_APP_PRIVATE_KEY")
if value == "" {
panic("环境变量 SFTPAY_APP_PRIVATE_KEY 的值不能为空")
} else {
SftPayAppPrivateKey = value
}
value = os.Getenv("SFTPAY_PUBLIC_KEY")
if value == "" {
panic("环境变量 SFTPAY_PUBLIC_KEY 的值不能为空")
} else {
SftPayPublicKey = value
}
value = os.Getenv("SFTPAY_RETURN_URL")
if value != "" {
SftReturnUrl = &value
} else {
SftReturnUrl = nil
}
value = os.Getenv("SFTPAY_NOTIFY_URL")
if value != "" {
SftNotifyUrl = &value
} else {
SftNotifyUrl = nil
}
}
// endregion
// region debug
var (
// DebugHttpDump 是否打印请求和响应的原始数据
DebugHttpDump = false
// DebugExternalChange 是否实际执行非幂等外部接口的调用。
// 例如外部数据修改接口,在内部接口调试时可以关闭,避免对外部数据产生影响
DebugExternalChange = true
)
func loadDebug() {
debugHttpDump := os.Getenv("DEBUG_HTTP_DUMP")
if debugHttpDump != "" {
value, err := strconv.ParseBool(debugHttpDump)
if err != nil {
panic("环境变量 DEBUG_HTTP_DUMP 的值不是布尔值")
}
DebugHttpDump = value
}
debugExternalChange := os.Getenv("DEBUG_EXTERNAL_CHANGE")
if debugExternalChange != "" {
value, err := strconv.ParseBool(debugExternalChange)
if err != nil {
panic("环境变量 DEBUG_EXTERNAL_CHANGE 的值不是布尔值")
}
DebugExternalChange = value
}
}
// endregion
func Init() {
err := godotenv.Load()
if err != nil {
@@ -464,15 +80,129 @@ func Init() {
log.Debug("✔ 加载本地环境变量")
}
loadApp()
loadAuth()
loadDb()
loadRedis()
loadLog()
loadDebug()
loadRemote()
loadAlipay()
loadWechatPay()
loadAliyun()
loadSftPay()
// 收集所有错误
var errs []error
errs = append(errs, parse(&RunMode, "RUN_MODE", true, &[]string{RunModeDev, RunModeProd}))
errs = append(errs, parse(&LogLevel, "LOG_LEVEL", true, nil, func(value string) (slog.Level, error) {
switch value {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("无效的日志级别: %s", value)
}
}))
errs = append(errs, parse(&TradeExpire, "TRADE_EXPIRE", true, nil))
errs = append(errs, parse(&SessionAccessExpire, "SESSION_ACCESS_EXPIRE", true, nil))
errs = append(errs, parse(&SessionRefreshExpire, "SESSION_REFRESH_EXPIRE", true, nil))
errs = append(errs, parse(&DebugHttpDump, "DEBUG_HTTP_DUMP", true, nil))
errs = append(errs, parse(&DebugExternalChange, "DEBUG_EXTERNAL_CHANGE", true, nil))
errs = append(errs, parse(&DbHost, "DB_HOST", true, nil))
errs = append(errs, parse(&DbPort, "DB_PORT", true, nil))
errs = append(errs, parse(&DbName, "DB_NAME", false, nil))
errs = append(errs, parse(&DbUserName, "DB_USERNAME", false, nil))
errs = append(errs, parse(&DbPassword, "DB_PASSWORD", false, nil))
errs = append(errs, parse(&RedisHost, "REDIS_HOST", true, nil))
errs = append(errs, parse(&RedisPort, "REDIS_PORT", true, nil))
errs = append(errs, parse(&RedisPassword, "REDIS_PASS", true, nil))
errs = append(errs, parse(&BaiyinAddr, "BAIYIN_ADDR", true, nil))
errs = append(errs, parse(&BaiyinTokenUrl, "BAIYIN_TOKEN_URL", false, nil))
errs = append(errs, parse(&IdenCallbackUrl, "IDEN_CALLBACK_URL", false, nil))
errs = append(errs, parse(&IdenAccessKey, "IDEN_ACCESS_KEY", false, nil))
errs = append(errs, parse(&IdenSecretKey, "IDEN_SECRET_KEY", false, nil))
errs = append(errs, parse(&AlipayAppId, "ALIPAY_APP_ID", false, nil))
errs = append(errs, parse(&AlipayAppPrivateKey, "ALIPAY_APP_PRIVATE_KEY", false, nil))
errs = append(errs, parse(&AlipayPublicKey, "ALIPAY_PUBLIC_KEY", false, nil))
errs = append(errs, parse(&AlipayApiCert, "ALIPAY_API_CERT", false, nil))
errs = append(errs, parse(&AlipayProduction, "ALIPAY_PRODUCTION", true, nil))
errs = append(errs, parse(&WechatPayAppId, "WECHATPAY_APP_ID", false, nil))
errs = append(errs, parse(&WechatPayMchId, "WECHATPAY_MCH_ID", false, nil))
errs = append(errs, parse(&WechatPayMchPrivateKeySerial, "WECHATPAY_MCH_PRIVATE_KEY_SERIAL", false, nil))
errs = append(errs, parse(&WechatPayMchPrivateKey, "WECHATPAY_MCH_PRIVATE_KEY", false, nil))
errs = append(errs, parse(&WechatPayPublicKeyId, "WECHATPAY_PUBLIC_KEY_ID", false, nil))
errs = append(errs, parse(&WechatPayPublicKey, "WECHATPAY_PUBLIC_KEY", false, nil))
errs = append(errs, parse(&WechatPayApiCert, "WECHATPAY_API_CERT", false, nil))
errs = append(errs, parse(&WechatPayCallbackUrl, "WECHATPAY_CALLBACK_URL", false, nil))
errs = append(errs, parse(&AliyunAccessKey, "ALIYUN_ACCESS_KEY", false, nil))
errs = append(errs, parse(&AliyunAccessKeySecret, "ALIYUN_ACCESS_KEY_SECRET", false, nil))
errs = append(errs, parse(&AliyunSmsSignature, "ALIYUN_SMS_SIGNATURE", false, nil))
errs = append(errs, parse(&AliyunSmsTemplateLogin, "ALIYUN_SMS_TEMPLATE_LOGIN", false, nil))
errs = append(errs, parse(&SftPayEnable, "SFTPAY_ENABLE", true, nil))
errs = append(errs, parse(&SftPayAppId, "SFTPAY_APP_ID", false, nil))
errs = append(errs, parse(&SftPayRouteId, "SFTPAY_ROUTE_ID", true, nil))
errs = append(errs, parse(&SftPayAppPrivateKey, "SFTPAY_APP_PRIVATE_KEY", false, nil))
errs = append(errs, parse(&SftPayPublicKey, "SFTPAY_PUBLIC_KEY", false, nil))
errs = append(errs, parse(&SftReturnUrl, "SFTPAY_RETURN_URL", true, nil))
errs = append(errs, parse(&SftNotifyUrl, "SFTPAY_NOTIFY_URL", true, nil))
// 统一处理错误
if err := u.CombineErrors(errs); err != nil {
panic(err)
}
}
func parse[T comparable](ptr *T, key string, inited bool, enum *[]string, convOpt ...func(value string) (T, error)) error {
value := os.Getenv(key)
// 处理空值
if value == "" {
if inited {
return nil
} else {
return fmt.Errorf("环境变量 %s 未设置", key)
}
}
// 处理枚举映射
if enum != nil {
valid := slices.Contains(*enum, value)
if !valid {
return fmt.Errorf("环境变量 %s 的值 '%s' 必须是 %v 之一", key, value, *enum)
}
}
// 根据指针类型进行赋值和类型转换
switch p := any(ptr).(type) {
case *string:
*p = value
case *int:
intValue, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("环境变量 %s 的值 '%s' 不是有效的整数: %v", key, value, err)
}
*p = intValue
case *bool:
boolValue, err := strconv.ParseBool(value)
if err != nil {
return fmt.Errorf("环境变量 %s 的值 '%s' 不是有效的布尔值: %v", key, value, err)
}
*p = boolValue
default:
if len(convOpt) == 0 {
return fmt.Errorf("环境变量 %s 的值 '%s' 无法赋值到目标类型", key, value)
}
conv := convOpt[0]
convertedValue, err := conv(value)
if err != nil {
return fmt.Errorf("环境变量 %s 的值 '%s' 转换失败: %v", key, value, err)
}
*ptr = convertedValue
}
return nil
}

View File

@@ -1,10 +1,11 @@
package logs
import (
"github.com/lmittmann/tint"
"log/slog"
"os"
"platform/pkg/env"
"github.com/lmittmann/tint"
)
func Init() {
@@ -14,7 +15,7 @@ func Init() {
var handler slog.Handler
switch env.RunMode {
case "debug":
case env.RunModeDev:
handler = tint.NewHandler(writer, &tint.Options{
Level: env.LogLevel,
TimeFormat: timeFormat,
@@ -26,7 +27,7 @@ func Init() {
return attr
},
})
case "production":
case env.RunModeProd:
handler = slog.NewJSONHandler(writer, &slog.HandlerOptions{
Level: env.LogLevel,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {

View File

@@ -1,12 +1,31 @@
package u
import "time"
import (
"fmt"
"time"
)
// P 是一个工具函数,用于在表达式内原地创建一个指针
func P[T any](v T) *T {
return &v
}
func Z[T any](v *T) T {
if v == nil {
var zero T
return zero
}
return *v
}
func X[T comparable](v T) *T {
var zero T
if v == zero {
return nil
}
return &v
}
func Today() time.Time {
var now = time.Now()
return time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
@@ -21,14 +40,6 @@ func SameDate(date time.Time) bool {
return date.Year() == now.Year() && date.Month() == now.Month() && date.Day() == now.Day()
}
func Z[T any](v *T) T {
if v == nil {
var zero T
return zero
}
return *v
}
func Or[T any](v *T, or T) T {
if v == nil {
return or
@@ -36,3 +47,17 @@ func Or[T any](v *T, or T) T {
return *v
}
}
func CombineErrors(errs []error) error {
var combinedErr error = nil
for _, err := range errs {
if err != nil {
if combinedErr == nil {
combinedErr = err
} else {
combinedErr = fmt.Errorf("%v; %w", combinedErr, err)
}
}
}
return combinedErr
}

View File

@@ -1,50 +0,0 @@
name: server-pre
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
ports:
- "5434:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7.4
restart: always
ports:
- "6380:6379"
platform:
build:
context: ../..
dockerfile: Dockerfile
environment:
- RUN_MODE=production
- DB_PORT=5434
- REDIS_PORT=6380
ports:
- "8081:8080"
depends_on:
- postgres
- redis
vector:
image: timberio/vector:0.47.0-alpine
volumes:
- ./vector/vector.toml:/etc/vector/vector.toml
- vector_data:/var/lib/vector
ports:
- "9000:9000"
command: ["vector", "-c", "/etc/vector/vector.toml"]
depends_on:
- postgres
- platform
volumes:
postgres_data:
vector_data:

View File

@@ -1,43 +0,0 @@
## 源配置:从 Docker 获取容器日志
[sources.platform_logs]
type = "docker_logs"
include_containers = ["platform"]
## 转换配置:为日志添加元数据
[transforms.platform_logs_parse]
type = "remap"
inputs = ["platform_logs"]
source = '''
.container = "platform"
json, err = parse_json(.message)
if err != null {
log.error("日志转换 json 格式失败: {}", err)
.tag = "error"
return
}
. = merge(., json)
'''
[transform.platform_logs_route]
type = "route"
inputs = ["platform_logs_parse"]
[transform.platform_logs_route.route]
request = '.message == "接口请求"'
usage = '.message == "创建通道"'
## 输出配置:将日志保存到 postgresql
[sinks.platform_logs_request]
type = "postgres"
inputs = ["platform_logs_route.request"]
[sinks.platform_logs_login]
type = "postgres"
inputs = ["platform_logs_route.login"]
[sinks.platform_logs_usage]
type = "postgres"
inputs = ["platform_logs_route.usage"]

View File

@@ -7,10 +7,10 @@ drop table if exists logs_request cascade;
create table logs_request (
id serial primary key,
identity int not null,
visitor int,
ip varchar(45) not null,
ua varchar(255),
ua varchar(255) not null,
user_id int,
client_id int,
method varchar(10) not null,
path varchar(255) not null,
@@ -21,16 +21,16 @@ create table logs_request (
time timestamp not null,
latency varchar(255) not null
);
create index logs_request_identity_index on logs_request (identity);
create index logs_request_visitor_index on logs_request (visitor);
create index logs_request_user_id_index on logs_request (user_id);
create index logs_request_client_id_index on logs_request (client_id);
-- logs_access表字段注释
comment on table logs_request is '访问日志表';
comment on column logs_request.id is '访问日志ID';
comment on column logs_request.identity is '访客身份0-游客1-用户2-管理员3-公共服务4-安全服务5-内部服务';
comment on column logs_request.visitor is '访客ID';
comment on column logs_request.ip is 'IP地址';
comment on column logs_request.ua is '用户代理';
comment on column logs_request.user_id is '用户ID';
comment on column logs_request.client_id is '客户端ID';
comment on column logs_request.method is '请求方法';
comment on column logs_request.path is '请求路径';
comment on column logs_request.status is '响应状态码';
@@ -220,9 +220,7 @@ comment on column announcement.deleted_at is '删除时间';
drop table if exists "user" cascade;
create table "user" (
id serial primary key,
admin_id int references admin (id) --
on update cascade --
on delete set null,
admin_id int,
phone varchar(255) not null unique,
username varchar(255),
email varchar(255),
@@ -309,14 +307,11 @@ create table client (
client_id varchar(255) not null unique,
client_secret varchar(255) not null,
redirect_uri varchar(255),
grant_code bool not null default false,
grant_client bool not null default false,
grant_refresh bool not null default false,
grant_password bool not null default false,
spec int not null,
name varchar(255) not null,
icon varchar(255),
status int not null default 1,
type int not null default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -333,14 +328,11 @@ comment on column client.id is '客户端ID';
comment on column client.client_id is 'OAuth2客户端标识符';
comment on column client.client_secret is 'OAuth2客户端密钥';
comment on column client.redirect_uri is 'OAuth2 重定向URI';
comment on column client.grant_code is '允许授权码授予';
comment on column client.grant_client is '允许客户端凭证授予';
comment on column client.grant_refresh is '允许刷新令牌授予';
comment on column client.grant_password is '允许密码授予';
comment on column client.spec is '安全规范1-native2-browser3-web4-trusted';
comment on column client.spec is '安全规范1-native2-browser3-web4-api';
comment on column client.name is '名称';
comment on column client.icon is '图标URL';
comment on column client.status is '状态0-禁用1-正常';
comment on column client.type is '类型0-普通1-官方';
comment on column client.created_at is '创建时间';
comment on column client.updated_at is '更新时间';
comment on column client.deleted_at is '删除时间';
@@ -355,15 +347,11 @@ comment on column client.deleted_at is '删除时间';
drop table if exists session cascade;
create table session (
id serial primary key,
user_id int references "user" (id)
on update cascade
on delete cascade,
client_id int references client (id)
on update cascade
on delete cascade,
user_id int,
admin_id int,
client_id int,
ip varchar(45),
ua varchar(255),
grant_type varchar(255) not null default 0,
access_token varchar(255) not null unique,
access_token_expires timestamp not null,
refresh_token varchar(255) unique,
@@ -374,6 +362,7 @@ create table session (
deleted_at timestamp
);
create index session_user_id_index on session (user_id);
create index session_admin_id_index on session (admin_id);
create index session_client_id_index on session (client_id);
create index session_access_token_index on session (access_token);
create index session_refresh_token_index on session (refresh_token);
@@ -384,10 +373,10 @@ create index session_deleted_at_index on session (deleted_at);
comment on table session is '会话表';
comment on column session.id is '会话ID';
comment on column session.user_id is '用户ID';
comment on column session.admin_id is '管理员ID';
comment on column session.client_id is '客户端ID';
comment on column session.ip is 'IP地址';
comment on column session.ua is '用户代理';
comment on column session.grant_type is '授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式';
comment on column session.access_token is '访问令牌';
comment on column session.access_token_expires is '访问令牌过期时间';
comment on column session.refresh_token is '刷新令牌';
@@ -401,9 +390,7 @@ comment on column session.deleted_at is '删除时间';
drop table if exists permission cascade;
create table permission (
id serial primary key,
parent_id int references permission (id)
on update cascade
on delete cascade,
parent_id int,
name varchar(255) not null unique,
description varchar(255),
created_at timestamp default current_timestamp,
@@ -428,12 +415,8 @@ comment on column permission.deleted_at is '删除时间';
drop table if exists user_role_link cascade;
create table user_role_link (
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
role_id int not null references user_role (id)
on update cascade
on delete cascade,
user_id int not null,
role_id int not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -455,12 +438,8 @@ comment on column user_role_link.deleted_at is '删除时间';
drop table if exists admin_role_link cascade;
create table admin_role_link (
id serial primary key,
admin_id int not null references admin (id)
on update cascade
on delete cascade,
role_id int not null references admin_role (id)
on update cascade
on delete cascade,
admin_id int not null,
role_id int not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -482,12 +461,8 @@ comment on column admin_role_link.deleted_at is '删除时间';
drop table if exists user_role_permission_link cascade;
create table user_role_permission_link (
id serial primary key,
role_id int not null references user_role (id)
on update cascade
on delete cascade,
permission_id int not null references permission (id)
on update cascade
on delete cascade,
role_id int not null,
permission_id int not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -509,12 +484,8 @@ comment on column user_role_permission_link.deleted_at is '删除时间';
drop table if exists admin_role_permission_link cascade;
create table admin_role_permission_link (
id serial primary key,
role_id int not null references admin_role (id)
on update cascade
on delete cascade,
permission_id int not null references permission (id)
on update cascade
on delete cascade,
role_id int not null,
permission_id int not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -536,12 +507,8 @@ comment on column admin_role_permission_link.deleted_at is '删除时间';
drop table if exists client_permission_link cascade;
create table client_permission_link (
id serial primary key,
client_id int not null references client (id)
on update cascade
on delete cascade,
permission_id int not null references permission (id)
on update cascade
on delete cascade,
client_id int not null,
permission_id int not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -602,9 +569,7 @@ comment on column proxy.deleted_at is '删除时间';
drop table if exists edge cascade;
create table edge (
id serial primary key,
proxy_id int references proxy (id)
on update cascade
on delete cascade,
proxy_id int,
type int not null,
version int not null,
name varchar(255) not null unique,
@@ -650,9 +615,7 @@ comment on column edge.deleted_at is '删除时间';
drop table if exists whitelist cascade;
create table whitelist (
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
user_id int not null,
host varchar(45) not null,
remark varchar(255),
created_at timestamp default current_timestamp,
@@ -677,18 +640,10 @@ comment on column whitelist.deleted_at is '删除时间';
drop table if exists channel cascade;
create table channel (
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
proxy_id int not null references proxy (id) --
on update cascade --
on delete set null,
edge_id int references edge (id) --
on update cascade --
on delete set null,
resource_id int not null references resource (id) --
on update cascade --
on delete set null,
user_id int not null,
proxy_id int not null,
edge_id int,
resource_id int not null,
proxy_host varchar(255) not null default '',
proxy_port int not null,
edge_host varchar(255),
@@ -770,9 +725,7 @@ comment on column product.deleted_at is '删除时间';
drop table if exists resource cascade;
create table resource (
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
user_id int not null,
resource_no varchar(255) unique,
active bool not null default true,
type int not null,
@@ -801,9 +754,7 @@ comment on column resource.deleted_at is '删除时间';
drop table if exists resource_short cascade;
create table resource_short (
id serial primary key,
resource_id int not null references resource (id)
on update cascade
on delete cascade,
resource_id int not null,
type int not null,
live int not null,
expire timestamp,
@@ -832,9 +783,7 @@ comment on column resource_short.daily_last is '今日最后使用时间';
drop table if exists resource_long cascade;
create table resource_long (
id serial primary key,
resource_id int not null references resource (id)
on update cascade
on delete cascade,
resource_id int not null,
type int not null,
live int not null,
expire timestamp,
@@ -869,9 +818,7 @@ comment on column resource_long.daily_last is '今日最后使用时间';
drop table if exists trade cascade;
create table trade (
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
user_id int not null,
inner_no varchar(255) not null unique,
outer_no varchar(255),
type int not null,
@@ -923,12 +870,8 @@ comment on column trade.deleted_at is '删除时间';
drop table if exists refund cascade;
create table refund (
id serial primary key,
trade_id int not null references trade (id)
on update cascade
on delete cascade,
product_id int references product (id) --
on update cascade --
on delete set null,
trade_id int not null,
product_id int,
amount decimal(12, 2) not null default 0,
reason varchar(255),
status int not null default 0,
@@ -956,18 +899,10 @@ comment on column refund.deleted_at is '删除时间';
drop table if exists bill cascade;
create table bill (
id serial primary key,
user_id int not null references "user" (id)
on update cascade
on delete cascade,
trade_id int references trade (id) --
on update cascade --
on delete set null,
resource_id int references resource (id) --
on update cascade --
on delete set null,
refund_id int references refund (id) --
on update cascade --
on delete set null,
user_id int not null,
trade_id int,
resource_id int,
refund_id int,
bill_no varchar(255) not null unique,
info varchar(255),
type int not null,
@@ -1003,9 +938,7 @@ comment on column bill.deleted_at is '删除时间';
drop table if exists coupon cascade;
create table coupon (
id serial primary key,
user_id int references "user" (id)
on update cascade
on delete cascade,
user_id int,
code varchar(255) not null unique,
remark varchar(255),
amount decimal(12, 2) not null default 0,
@@ -1036,3 +969,118 @@ comment on column coupon.updated_at is '更新时间';
comment on column coupon.deleted_at is '删除时间';
-- endregion
-- ====================
-- region 外键约束
-- ====================
-- user表外键
alter table "user"
add constraint fk_user_admin_id foreign key (admin_id) references admin (id) on delete set null;
-- session表外键
alter table session
add constraint fk_session_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table session
add constraint fk_session_client_id foreign key (client_id) references client (id) on delete cascade;
-- permission表外键
alter table permission
add constraint fk_permission_parent_id foreign key (parent_id) references permission (id) on delete set null;
-- user_role_link表外键
alter table user_role_link
add constraint fk_user_role_link_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table user_role_link
add constraint fk_user_role_link_role_id foreign key (role_id) references user_role (id) on delete cascade;
-- admin_role_link表外键
alter table admin_role_link
add constraint fk_admin_role_link_admin_id foreign key (admin_id) references admin (id) on delete cascade;
alter table admin_role_link
add constraint fk_admin_role_link_role_id foreign key (role_id) references admin_role (id) on delete cascade;
-- user_role_permission_link表外键
alter table user_role_permission_link
add constraint fk_user_role_permission_link_role_id foreign key (role_id) references user_role (id) on delete cascade;
alter table user_role_permission_link
add constraint fk_user_role_permission_link_permission_id foreign key (permission_id) references permission (id) on delete cascade;
-- admin_role_permission_link表外键
alter table admin_role_permission_link
add constraint fk_admin_role_permission_link_role_id foreign key (role_id) references admin_role (id) on delete cascade;
alter table admin_role_permission_link
add constraint fk_admin_role_permission_link_permission_id foreign key (permission_id) references permission (id) on delete cascade;
-- client_permission_link表外键
alter table client_permission_link
add constraint fk_client_permission_link_client_id foreign key (client_id) references client (id) on delete cascade;
alter table client_permission_link
add constraint fk_client_permission_link_permission_id foreign key (permission_id) references permission (id) on delete cascade;
-- edge表外键
alter table edge
add constraint fk_edge_proxy_id foreign key (proxy_id) references proxy (id) on delete cascade;
-- whitelist表外键
alter table whitelist
add constraint fk_whitelist_user_id foreign key (user_id) references "user" (id) on delete cascade;
-- channel表外键
alter table channel
add constraint fk_channel_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table channel
add constraint fk_channel_proxy_id foreign key (proxy_id) references proxy (id) on delete set null;
alter table channel
add constraint fk_channel_edge_id foreign key (edge_id) references edge (id) on delete set null;
alter table channel
add constraint fk_channel_resource_id foreign key (resource_id) references resource (id) on delete set null;
-- resource表外键
alter table resource
add constraint fk_resource_user_id foreign key (user_id) references "user" (id) on delete cascade;
-- resource_short表外键
alter table resource_short
add constraint fk_resource_short_resource_id foreign key (resource_id) references resource (id) on delete cascade;
-- resource_long表外键
alter table resource_long
add constraint fk_resource_long_resource_id foreign key (resource_id) references resource (id) on delete cascade;
-- trade表外键
alter table trade
add constraint fk_trade_user_id foreign key (user_id) references "user" (id) on delete set null;
-- refund表外键
alter table refund
add constraint fk_refund_trade_id foreign key (trade_id) references trade (id) on delete cascade;
alter table refund
add constraint fk_refund_product_id foreign key (product_id) references product (id) on delete set null;
-- bill表外键
alter table bill
add constraint fk_bill_user_id foreign key (user_id) references "user" (id) on delete cascade;
alter table bill
add constraint fk_bill_trade_id foreign key (trade_id) references trade (id) on delete set null;
alter table bill
add constraint fk_bill_resource_id foreign key (resource_id) references resource (id) on delete set null;
alter table bill
add constraint fk_bill_refund_id foreign key (refund_id) references refund (id) on delete set null;
-- coupon表外键
alter table coupon
add constraint fk_coupon_user_id foreign key (user_id) references "user" (id) on delete cascade;
-- endregion
-- ====================
-- region 填充数据
-- ====================
insert into client (
client_id, client_secret, redirect_uri, spec, name, type
)
values ('web', '$2a$10$Ss12mXQgpYyo1CKIZ3URouDm.Lc2KcYJzsvEK2PTIXlv6fHQht45a', '', 3, 'web', 1)
-- endregion

View File

@@ -4,112 +4,101 @@ import (
"context"
"encoding/base64"
"errors"
"fmt"
"log/slog"
"platform/web/core"
client2 "platform/web/domains/client"
m "platform/web/models"
q "platform/web/queries"
"slices"
s "platform/web/services"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
)
type ProtectBuilder struct {
c *fiber.Ctx
types []PayloadType
scopes []string
func Authenticate() fiber.Handler {
return func(ctx *fiber.Ctx) error {
header := ctx.Get(fiber.HeaderAuthorization)
authCtx, err := authHeader(ctx.Context(), header)
if err != nil {
return err
}
if authCtx == nil {
authCtx = &AuthCtx{}
}
SetAuthCtx(ctx, authCtx)
return ctx.Next()
}
}
func NewProtect(c *fiber.Ctx) *ProtectBuilder {
return &ProtectBuilder{c, []PayloadType{}, []string{}}
}
func authHeader(ctx context.Context, header string) (*AuthCtx, error) {
if header == "" {
return nil, nil
}
func (p *ProtectBuilder) Payload(types ...PayloadType) *ProtectBuilder {
p.types = types
return p
}
func (p *ProtectBuilder) Scopes(scopes ...string) *ProtectBuilder {
p.scopes = scopes
return p
}
func (p *ProtectBuilder) Do() (*Context, error) {
return Protect(p.c, p.types, p.scopes)
}
func Protect(c *fiber.Ctx, types []PayloadType, permissions []string) (*Context, error) {
// 获取令牌
var header = c.Get("Authorization")
var split = strings.Split(header, " ")
if len(split) != 2 {
slog.Debug("Authorization 头格式不正确")
return nil, ErrUnauthorize
return nil, ErrAuthenticateUnauthorize
}
var token = strings.TrimSpace(split[1])
if token == "" {
slog.Debug("提供的令牌为空")
return nil, ErrUnauthorize
return nil, ErrAuthenticateUnauthorize
}
var auth *Context
var authCtx *AuthCtx
var err error
switch split[0] {
case "Bearer":
auth, err = authBearer(c.Context(), token)
authCtx, err = authBearer(ctx, token)
if err != nil {
slog.Debug("Bearer 认证失败", "err", err)
return nil, ErrUnauthorize
return nil, ErrAuthenticateUnauthorize
}
case "Basic":
if !slices.Contains(types, PayloadInternalServer) {
slog.Debug("禁止使用 Basic 认证方式")
return nil, ErrUnauthorize
}
auth, err = authBasic(c.Context(), token)
authCtx, err = authBasic(ctx, token)
if err != nil {
slog.Debug("Basic 认证失败", "err", err)
return nil, ErrUnauthorize
return nil, ErrAuthenticateUnauthorize
}
default:
slog.Debug("无效的认证方式", "method", split[0])
return nil, ErrUnauthorize
return nil, ErrAuthenticateUnauthorize
}
// 检查权限
if !slices.Contains(types, auth.Payload.Type) {
slog.Debug("无效的负载类型", "except", types, "actual", auth.Payload.Type)
return nil, ErrForbidden
}
if len(permissions) > 0 && !auth.AnyPermission(permissions...) {
slog.Debug("无效的认证权限", "except", permissions, "actual", auth.Permissions)
return nil, ErrForbidden
}
// 保存到上下文
Locals(c, auth)
return auth, nil
return authCtx, err
}
func Locals(c *fiber.Ctx, auth *Context) {
c.Locals("auth", auth)
}
func authBearer(ctx context.Context, token string) (*Context, error) {
auth, err := FindSession(ctx, token)
func authBearer(_ context.Context, token string) (*AuthCtx, error) {
session, err := FindSession(token, time.Now())
if err != nil {
slog.Debug(err.Error())
return nil, err
slog.Debug("Bearer 认证失败", "err", err)
return nil, ErrAuthenticateUnauthorize
}
return auth, nil
scopes := []string{}
if session.Scopes_ != nil {
scopes = strings.Split(*session.Scopes_, " ")
}
return &AuthCtx{
User: session.User,
Admin: session.Admin,
Client: session.Client,
Scopes: scopes,
Session: session,
}, nil
}
func authBasic(_ context.Context, token string) (*Context, error) {
func authBasic(_ context.Context, token string) (*AuthCtx, error) {
// 解析 Basic 认证信息
var base, err = base64.RawURLEncoding.DecodeString(token)
@@ -125,14 +114,23 @@ func authBasic(_ context.Context, token string) (*Context, error) {
return nil, errors.New("令牌格式错误,必须是 <client_id>:<client_secret> 格式")
}
var clientID = split[0]
client, err := authClient(split[0], split[1])
if err != nil {
return nil, fmt.Errorf("客户端认证失败:%w", err)
}
return &AuthCtx{
Client: client,
Scopes: []string{},
}, nil
}
func authClient(clientId, clientSecret string) (*m.Client, error) {
// 获取客户端信息
client, err := q.Client.
Where(
q.Client.ClientID.Eq(clientID),
q.Client.Spec.In(int32(client2.SpecWeb), int32(client2.SpecTrusted)),
q.Client.GrantClient.Is(true),
q.Client.ClientID.Eq(clientId),
q.Client.Status.Eq(1)).
Take()
if err != nil {
@@ -140,33 +138,57 @@ func authBasic(_ context.Context, token string) (*Context, error) {
}
// 检查客户端密钥
var clientSecret = split[1]
spec := client2.Spec(client.Spec)
if spec == client2.SpecWeb || spec == client2.SpecApi {
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, errors.New("客户端密钥错误")
}
}
// todo 查询客户端关联权限
// 组织授权信息(一次性请求)
return &Context{
Payload: Payload{
Id: client.ID,
Type: PayloadTypeFromClientSpec(client2.Spec(client.Spec)),
Name: client.Name,
Avatar: client.Icon,
},
Permissions: nil,
Metadata: nil,
}, nil
return client, nil
}
type AuthenticationErr string
func authUserBySms(tx *q.Query, username, code string) (*m.User, error) {
// 验证验证码
err := s.Verifier.VerifySms(context.Background(), username, code)
if err != nil {
if errors.Is(err, s.ErrVerifierServiceInvalid) {
return nil, ErrAuthorizeInvalidRequest
}
return nil, err
}
func (e AuthenticationErr) Error() string {
return string(e)
// 查找用户
return tx.User.Where(tx.User.Phone.Eq(username)).Take()
}
var (
ErrUnauthorize = AuthenticationErr("令牌无效")
ErrForbidden = AuthenticationErr("没有权限")
)
func authUserByEmail(tx *q.Query, username, code string) (*m.User, error) {
return nil, core.NewServErr("邮箱登录不可用")
}
func authUserByPassword(tx *q.Query, username, password string) (*m.User, error) {
user, err := tx.User.
Where(tx.User.Phone.Eq(username)).
Or(tx.User.Email.Eq(username)).
Or(tx.User.Username.Eq(username)).
Take()
if err != nil {
slog.Debug("查找用户失败", "error", err)
return nil, core.NewBizErr("用户不存在或密码错误")
}
// 验证密码
if user.Password == nil || *user.Password == "" {
slog.Debug("用户未设置密码", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(password)) != nil {
slog.Debug("密码验证失败", "username", username)
return nil, core.NewBizErr("用户不存在或密码错误")
}
return user, nil
}

View File

@@ -1,5 +1,28 @@
package auth
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"log/slog"
"platform/pkg/env"
"platform/pkg/u"
"platform/web/core"
user2 "platform/web/domains/user"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"gorm.io/gorm"
)
type GrantType string
const (
@@ -17,36 +40,352 @@ const (
GrantPasswordEmail = PasswordGrantType("email_code") // 邮箱验证码
)
func Token(grant GrantType) error {
type TokenReq struct {
GrantType GrantType `json:"grant_type" form:"grant_type"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
Scope string `json:"scope" form:"scope"`
GrantCodeData
GrantClientData
GrantRefreshData
GrantPasswordData
}
type GrantCodeData struct {
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
}
type GrantClientData struct {
}
type GrantRefreshData struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"`
}
type GrantPasswordData struct {
LoginType PasswordGrantType `json:"login_type" form:"login_type"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Remember bool `json:"remember" form:"remember"`
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
func Token(c *fiber.Ctx) error {
now := time.Now()
// 验证请求参数
req := new(TokenReq)
if err := c.BodyParser(req); err != nil {
return sendError(c, ErrAuthorizeInvalidRequest, "无法解析请求参数")
}
if req.GrantType == "" {
return sendError(c, ErrAuthorizeInvalidRequest, "缺少必要参数: grant_type")
}
switch req.GrantType {
// 授权码模式
case GrantAuthorizationCode:
if req.Code == "" {
return sendError(c, ErrAuthorizeInvalidRequest, "缺少必要参数: code")
}
// 刷新令牌模式
case GrantRefreshToken:
if req.RefreshToken == "" {
return sendError(c, ErrAuthorizeInvalidRequest, "缺少必要参数: refresh_token")
}
// 密码模式
case GrantPassword:
if req.LoginType == "" {
return sendError(c, ErrAuthorizeInvalidRequest, "缺少必要参数: password_type")
}
if req.Username == "" {
return sendError(c, ErrAuthorizeInvalidRequest, "缺少必要参数: username")
}
if req.Password == "" {
return sendError(c, ErrAuthorizeInvalidRequest, "缺少必要参数: password")
}
}
// 验证客户端身份
authCtx := GetAuthCtx(c)
if authCtx == nil {
authCtx = &AuthCtx{}
}
if authCtx.Client == nil {
client, err := authClient(req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
authCtx.Client = client
}
// 处理授权
var session *m.Session
var err error
switch req.GrantType {
// 授权码模式
case GrantAuthorizationCode:
session, err = authAuthorizationCode(c, authCtx, req, now)
// 客户端凭证模式
case GrantClientCredentials:
session, err = authClientCredential(c, authCtx, req, now)
// 刷新令牌模式
case GrantRefreshToken:
session, err = authRefreshToken(c, authCtx, req, now)
// 密码模式
case GrantPassword:
session, err = authPassword(c, authCtx, req, now)
default:
return sendError(c, ErrAuthorizeUnsupportedGrantType)
}
if err != nil {
return sendError(c, err)
}
// 返回响应
return c.JSON(&TokenResp{
TokenType: "Bearer",
AccessToken: session.AccessToken,
RefreshToken: u.Z(session.RefreshToken),
ExpiresIn: int(time.Time(session.AccessTokenExpires).Sub(now).Seconds()),
Scope: u.Z(session.Scopes_),
})
}
func authAuthorizationCode(ctx *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
// 检查 code 获取用户授权信息
data, err := g.Redis.Get(context.Background(), req.Code).Result()
if err != nil {
return nil, err
}
var codeCtx CodeContext
if err := json.Unmarshal([]byte(data), &codeCtx); err != nil {
return nil, err
}
// 检查 PKCE
if codeCtx.CodeChallengeMethod != "" {
if req.CodeVerifier == "" {
return nil, ErrAuthorizeInvalidPKCE
}
switch codeCtx.CodeChallengeMethod {
case "plain":
if req.CodeVerifier != codeCtx.CodeChallenge {
return nil, ErrAuthorizeInvalidPKCE
}
case "S256":
hash := sha256.Sum256([]byte(req.CodeVerifier))
verifier := base64.RawURLEncoding.EncodeToString(hash[:])
if verifier != codeCtx.CodeChallenge {
return nil, ErrAuthorizeInvalidPKCE
}
default:
return nil, ErrAuthorizeInvalidPKCE
}
}
user, err := q.User.Where(
q.User.ID.Eq(codeCtx.UserID),
q.User.Status.Eq(int32(user2.StatusEnabled)),
).First()
if err != nil {
return nil, err
}
// todo 检查 scope
// 生成会话
session := &m.Session{
IP: u.X(ctx.IP()),
UA: u.X(ctx.Get(fiber.HeaderUserAgent)),
UserID: &user.ID,
ClientID: &auth.Client.ID,
Scopes_: u.P(strings.Join(codeCtx.Scopes, " ")),
AccessToken: uuid.NewString(),
AccessTokenExpires: orm.LocalDateTime(now.Add(time.Duration(env.SessionAccessExpire) * time.Second)),
}
if codeCtx.Remember {
session.RefreshToken = u.P(uuid.NewString())
session.RefreshTokenExpires = u.P(orm.LocalDateTime(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second)))
}
err = SaveSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func authClientCredential(ctx *fiber.Ctx, auth *AuthCtx, _ *TokenReq, now time.Time) (*m.Session, error) {
// todo 检查 scope
// 生成会话
session := &m.Session{
IP: u.X(ctx.IP()),
UA: u.X(ctx.Get(fiber.HeaderUserAgent)),
ClientID: &auth.Client.ID,
AccessToken: uuid.NewString(),
AccessTokenExpires: orm.LocalDateTime(now.Add(time.Duration(env.SessionAccessExpire) * time.Second)),
}
// 保存会话
err := SaveSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func authPassword(ctx *fiber.Ctx, auth *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
var user *m.User
err := q.Q.Transaction(func(tx *q.Query) (err error) {
switch req.LoginType {
case GrantPasswordPhone:
user, err = authUserBySms(tx, req.Username, req.Password)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if user == nil {
user = &m.User{
Phone: req.Username,
Username: u.P(req.Username),
Status: int32(user2.StatusEnabled),
}
}
case GrantPasswordEmail:
user, err = authUserByEmail(tx, req.Username, req.Password)
if err != nil {
return err
}
case GrantPasswordSecret:
user, err = authUserByPassword(tx, req.Username, req.Password)
if err != nil {
return err
}
default:
return ErrAuthorizeInvalidRequest
}
// 账户状态
if user2.Status(user.Status) == user2.StatusDisabled {
slog.Debug("账户状态异常", "username", req.Username, "status", user.Status)
return core.NewBizErr("账号无法登录")
}
// 更新用户的登录时间
user.LastLogin = u.P(orm.LocalDateTime(time.Now()))
user.LastLoginHost = u.X(ctx.IP())
user.LastLoginAgent = u.X(ctx.Get(fiber.HeaderUserAgent))
if err := tx.User.Save(user); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
// 生成会话
session := &m.Session{
IP: u.X(ctx.IP()),
UA: u.X(ctx.Get(fiber.HeaderUserAgent)),
UserID: &user.ID,
ClientID: &auth.Client.ID,
Scopes_: u.X(req.Scope),
AccessToken: uuid.NewString(),
AccessTokenExpires: orm.LocalDateTime(now.Add(time.Duration(env.SessionAccessExpire) * time.Second)),
}
if req.Remember {
session.RefreshToken = u.P(uuid.NewString())
session.RefreshTokenExpires = u.P(orm.LocalDateTime(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second)))
}
err = SaveSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func authAuthorizationCode() {
func authRefreshToken(_ *fiber.Ctx, _ *AuthCtx, req *TokenReq, now time.Time) (*m.Session, error) {
session, err := FindSessionByRefresh(req.RefreshToken, now)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrAuthorizeInvalidGrant
}
return nil, err
}
// todo 检查权限
// 生成令牌
session.AccessToken = uuid.NewString()
session.AccessTokenExpires = orm.LocalDateTime(now.Add(time.Duration(env.SessionAccessExpire) * time.Second))
if session.RefreshToken != nil {
session.RefreshToken = u.P(uuid.NewString())
session.RefreshTokenExpires = u.P(orm.LocalDateTime(now.Add(time.Duration(env.SessionRefreshExpire) * time.Second)))
}
// 保存令牌
err = SaveSession(session)
if err != nil {
return nil, err
}
return session, nil
}
func authClientCredential() {
func sendError(c *fiber.Ctx, err error, description ...string) error {
var sErr AuthErr
if errors.As(err, &sErr) {
status := fiber.StatusBadRequest
var desc string
switch {
case errors.Is(sErr, ErrAuthorizeInvalidRequest):
desc = "无效的请求"
case errors.Is(sErr, ErrAuthorizeInvalidClient):
status = fiber.StatusUnauthorized
desc = "无效的客户端凭证"
case errors.Is(sErr, ErrAuthorizeInvalidGrant):
desc = "无效的授权凭证"
case errors.Is(sErr, ErrAuthorizeInvalidScope):
desc = "无效的授权范围"
case errors.Is(sErr, ErrAuthorizeUnauthorizedClient):
desc = "未授权的客户端"
case errors.Is(sErr, ErrAuthorizeUnsupportedGrantType):
desc = "不支持的授权类型"
}
if len(description) > 0 {
desc = description[0]
}
}
func authRefreshToken() {
}
func authPassword() {
}
func authPasswordSecret() {
}
func authPasswordPhone() {
}
func authPasswordEmail() {
return c.Status(status).JSON(TokenErrResp{
Error: string(sErr),
Description: desc,
})
}
return err
}
func Revoke() error {
@@ -56,3 +395,12 @@ func Revoke() error {
func Introspect() error {
return nil
}
type CodeContext struct {
UserID int32 `json:"user_id"`
ClientID int32 `json:"client_id"`
Scopes []string `json:"scopes"`
Remember bool `json:"remember"`
CodeChallenge string `json:"code_challenge"`
CodeChallengeMethod string `json:"code_challenge_method"`
}

99
web/auth/check.go Normal file
View File

@@ -0,0 +1,99 @@
package auth
import (
"platform/web/domains/client"
m "platform/web/models"
"github.com/gofiber/fiber/v2"
)
type AuthCtx struct {
User *m.User `json:"account,omitempty"`
Admin *m.Admin `json:"admin,omitempty"`
Client *m.Client `json:"client,omitempty"`
Scopes []string `json:"scopes,omitempty"`
Session *m.Session `json:"session,omitempty"`
smap map[string]struct{}
}
func (a *AuthCtx) PermitUser(scopes ...string) (*AuthCtx, error) {
if a.User == nil {
return a, ErrAuthenticateForbidden
}
if !a.checkScopes(scopes...) {
return a, ErrAuthenticateForbidden
}
return a, nil
}
func (a *AuthCtx) PermitAdmin(scopes ...string) (*AuthCtx, error) {
if a.Admin == nil {
return a, ErrAuthenticateForbidden
}
if !a.checkScopes(scopes...) {
return a, ErrAuthenticateForbidden
}
return a, nil
}
func (a *AuthCtx) PermitSecretClient(scopes ...string) (*AuthCtx, error) {
if a.Client == nil {
return a, ErrAuthenticateForbidden
}
spec := client.Spec(a.Client.Spec)
if spec != client.SpecApi && spec != client.SpecWeb {
return a, ErrAuthenticateForbidden
}
if !a.checkScopes(scopes...) {
return a, ErrAuthenticateForbidden
}
return a, nil
}
func (a *AuthCtx) PermitInternalClient(scopes ...string) (*AuthCtx, error) {
if a.Client == nil {
return a, ErrAuthenticateForbidden
}
spec := client.Spec(a.Client.Spec)
if spec != client.SpecApi && spec != client.SpecWeb {
return a, ErrAuthenticateForbidden
}
cType := client.Type(a.Client.Type)
if cType != client.TypeInternal {
return a, ErrAuthenticateForbidden
}
if !a.checkScopes(scopes...) {
return a, ErrAuthenticateForbidden
}
return a, nil
}
func (a *AuthCtx) checkScopes(scopes ...string) bool {
if len(scopes) == 0 || len(a.Scopes) == 0 {
return true
}
if len(a.smap) == 0 && len(a.Scopes) > 0 {
for _, scope := range scopes {
a.smap[scope] = struct{}{}
}
}
for _, scope := range scopes {
if _, ok := a.smap[scope]; ok {
return true
}
}
return false
}
const AuthCtxKey = "session"
func SetAuthCtx(c *fiber.Ctx, auth *AuthCtx) {
c.Locals(AuthCtxKey, auth)
}
func GetAuthCtx(c *fiber.Ctx) *AuthCtx {
if authCtx, ok := c.Locals(AuthCtxKey).(*AuthCtx); ok {
return authCtx
}
return nil
}

View File

@@ -1,103 +0,0 @@
package auth
import (
client2 "platform/web/domains/client"
)
// Context 定义认证信息
type Context struct {
Payload Payload `json:"payload"`
Permissions map[string]struct{} `json:"permissions,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (a *Context) AnyType(types ...PayloadType) bool {
if a == nil {
return false
}
for _, t := range types {
if a.Payload.Type == t {
return true
}
}
return false
}
// AnyPermission 检查认证是否包含指定权限
func (a *Context) AnyPermission(requiredPermission ...string) bool {
if a == nil || a.Permissions == nil {
return false
}
for _, permission := range requiredPermission {
if _, ok := a.Permissions[permission]; ok {
return true
}
}
return false
}
// Payload 定义负载信息
type Payload struct {
Id int32 `json:"id,omitempty"`
Type PayloadType `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Avatar *string `json:"avatar,omitempty"`
}
type PayloadType int
const (
PayloadNone PayloadType = iota // 游客
PayloadUser // 用户
PayloadAdmin // 管理员
PayloadPublicServer // 公共服务public_client
PayloadSecuredServer // 安全服务credential_client
PayloadInternalServer // 内部服务
)
func (t PayloadType) ToStr() string {
switch t {
case PayloadUser:
return "user"
case PayloadAdmin:
return "admn"
case PayloadPublicServer:
return "cpub"
case PayloadSecuredServer:
return "ccnf"
case PayloadInternalServer:
return "inte"
default:
return "none"
}
}
func PayloadTypeFromStr(name string) PayloadType {
switch name {
case "user":
return PayloadUser
case "admn":
return PayloadAdmin
case "cpub":
return PayloadPublicServer
case "ccnf":
return PayloadSecuredServer
case "inte":
return PayloadInternalServer
default:
return PayloadNone
}
}
func PayloadTypeFromClientSpec(spec client2.Spec) PayloadType {
var clientType PayloadType
switch spec {
case client2.SpecNative, client2.SpecBrowser:
clientType = PayloadPublicServer
case client2.SpecWeb:
clientType = PayloadSecuredServer
case client2.SpecTrusted:
clientType = PayloadInternalServer
}
return clientType
}

24
web/auth/errors.go Normal file
View File

@@ -0,0 +1,24 @@
package auth
type AuthErr string
func (e AuthErr) Error() string {
return string(e)
}
// 认证错误
const (
ErrAuthenticateUnauthorize = AuthErr("令牌无效")
ErrAuthenticateForbidden = AuthErr("没有权限")
)
// 授权错误
const (
ErrAuthorizeInvalidRequest = AuthErr("invalid_request")
ErrAuthorizeInvalidClient = AuthErr("invalid_client")
ErrAuthorizeInvalidGrant = AuthErr("invalid_grant")
ErrAuthorizeInvalidScope = AuthErr("invalid_scope")
ErrAuthorizeUnauthorizedClient = AuthErr("unauthorized_client")
ErrAuthorizeUnsupportedGrantType = AuthErr("unsupported_grant_type")
ErrAuthorizeInvalidPKCE = AuthErr("invalid_pkce")
)

View File

@@ -2,160 +2,36 @@ package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"platform/pkg/env"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"time"
"gorm.io/gen/field"
)
type Session struct {
// 认证主体
Payload *Payload
// 令牌信息
TokenDetails *TokenDetails
func FindSession(accessToken string, now time.Time) (*m.Session, error) {
return q.Session.
Preload(field.Associations).
Where(
q.Session.AccessToken.Eq(accessToken),
q.Session.AccessTokenExpires.Gt(orm.LocalDateTime(now)),
).First()
}
func FindSession(ctx context.Context, token string) (*Context, error) {
// 读取认证数据
authJSON, err := g.Redis.Get(ctx, accessKey(token)).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, errors.New("invalid_token")
}
return nil, err
}
// 反序列化
auth := new(Context)
if err := json.Unmarshal([]byte(authJSON), auth); err != nil {
return nil, err
}
return auth, nil
func FindSessionByRefresh(refreshToken string, now time.Time) (*m.Session, error) {
return q.Session.
Preload(field.Associations).
Where(
q.Session.RefreshToken.Eq(refreshToken),
q.Session.RefreshTokenExpires.Gt(orm.LocalDateTime(now)),
).First()
}
func CreateSession(ctx context.Context, authCtx *Context, remember bool) (*TokenDetails, error) {
var now = time.Now()
// 生成令牌组
accessToken := genToken()
refreshToken := genToken()
// 序列化认证数据
authData, err := json.Marshal(authCtx)
if err != nil {
return nil, err
}
// 序列化刷新令牌数据
refreshData, err := json.Marshal(RefreshData{
AuthContext: authCtx,
AccessToken: accessToken,
})
if err != nil {
return nil, err
}
// 事务保存数据到 Redis
var accessExpire = time.Duration(env.SessionAccessExpire) * time.Second
var refreshExpire = time.Duration(env.SessionRefreshExpire) * time.Second
pipe := g.Redis.TxPipeline()
pipe.Set(ctx, accessKey(accessToken), authData, accessExpire)
if remember {
pipe.Set(ctx, refreshKey(refreshToken), refreshData, refreshExpire)
}
_, err = pipe.Exec(ctx)
if err != nil {
return nil, err
}
return &TokenDetails{
AccessToken: accessToken,
AccessTokenExpires: now.Add(accessExpire),
RefreshToken: refreshToken,
RefreshTokenExpires: now.Add(refreshExpire),
Auth: authCtx,
}, nil
}
func RefreshSession(ctx context.Context, refreshToken string, renew bool) (*TokenDetails, error) {
var now = time.Now()
rKey := refreshKey(refreshToken)
var tokenDetails *TokenDetails
// 刷新令牌
err := g.Redis.Watch(ctx, func(tx *redis.Tx) error {
// 先获取刷新令牌数据
refreshJson, err := tx.Get(ctx, rKey).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return ErrInvalidRefreshToken
}
return err
}
// 解析刷新令牌数据
refreshData := new(RefreshData)
if err := json.Unmarshal([]byte(refreshJson), refreshData); err != nil {
return err
}
// 生成新的令牌
newAccessToken := genToken()
newRefreshToken := genToken()
authData, err := json.Marshal(refreshData.AuthContext)
if err != nil {
return err
}
newRefreshData, err := json.Marshal(RefreshData{
AuthContext: refreshData.AuthContext,
AccessToken: newAccessToken,
})
if err != nil {
return err
}
pipeline := tx.Pipeline()
// 保存新的令牌
var accessExpire = time.Duration(env.SessionAccessExpire) * time.Second
var refreshExpire = time.Duration(env.SessionRefreshExpire) * time.Second
pipeline.Set(ctx, accessKey(newAccessToken), authData, accessExpire)
pipeline.Set(ctx, refreshKey(newRefreshToken), newRefreshData, refreshExpire)
// 删除旧的令牌
pipeline.Del(ctx, accessKey(refreshData.AccessToken))
pipeline.Del(ctx, refreshKey(refreshToken))
_, err = pipeline.Exec(ctx)
if err != nil {
return err
}
tokenDetails = &TokenDetails{
AccessToken: newAccessToken,
RefreshToken: newRefreshToken,
AccessTokenExpires: now.Add(accessExpire),
RefreshTokenExpires: now.Add(refreshExpire),
Auth: refreshData.AuthContext,
}
return nil
}, rKey)
if err != nil {
return nil, fmt.Errorf("刷新令牌失败: %w", err)
}
return tokenDetails, nil
func SaveSession(session *m.Session) error {
return q.Session.Save(session)
}
func RemoveSession(ctx context.Context, accessToken string, refreshToken string) error {
@@ -163,11 +39,6 @@ func RemoveSession(ctx context.Context, accessToken string, refreshToken string)
return nil
}
// 生成一个新的令牌
func genToken() string {
return uuid.NewString()
}
// 令牌键的格式为 "session:<token>"
func accessKey(token string) string {
return fmt.Sprintf("session:%s", token)
@@ -177,32 +48,3 @@ func accessKey(token string) string {
func refreshKey(token string) string {
return fmt.Sprintf("session:refresh:%s", token)
}
// TokenDetails 存储令牌详细信息
type TokenDetails struct {
// 访问令牌
AccessToken string
// 刷新令牌
RefreshToken string
// 访问令牌过期时间
AccessTokenExpires time.Time
// 刷新令牌过期时间
RefreshTokenExpires time.Time
// 认证信息
Auth *Context
}
type RefreshData struct {
AuthContext *Context
AccessToken string
}
type SessionErr string
func (e SessionErr) Error() string {
return string(e)
}
const (
ErrInvalidRefreshToken = SessionErr("无效的刷新令牌")
)

View File

@@ -60,7 +60,7 @@ type Err struct {
func (e *Err) Error() string {
if e.err != nil {
return e.msg + "" + e.err.Error()
return e.msg + ": " + e.err.Error()
}
return e.msg
}

View File

@@ -6,5 +6,12 @@ const (
SpecNative Spec = iota + 1 // 原生客户端
SpecBrowser // 浏览器客户端
SpecWeb // Web 服务
SpecTrusted // 可信服务
SpecApi // Api 服务
)
type Type int32
const (
TypeNormal Type = iota // 普通客户端
TypeInternal // 内部客户端
)

View File

@@ -16,7 +16,7 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
var message = "服务器异常"
var fiberErr *fiber.Error
var authErr auth.AuthenticationErr
var authErr auth.AuthErr
var bizErr *core.BizErr
var servErr *core.ServErr
@@ -30,9 +30,9 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
// 认证授权错误
case errors.As(err, &authErr):
switch {
case errors.Is(err, auth.ErrUnauthorize):
case errors.Is(err, auth.ErrAuthenticateUnauthorize):
code = fiber.StatusUnauthorized
case errors.Is(err, auth.ErrForbidden):
case errors.Is(err, auth.ErrAuthenticateForbidden):
code = fiber.StatusForbidden
default:
code = fiber.StatusBadRequest

View File

@@ -1,4 +1,4 @@
package tasks
package events
import (
"encoding/json"

View File

@@ -1,4 +1,4 @@
package tasks
package events
import (
"encoding/json"

View File

@@ -1,10 +1,11 @@
package tasks
package events
import (
"encoding/json"
"github.com/hibiken/asynq"
"log/slog"
trade2 "platform/web/domains/trade"
"github.com/hibiken/asynq"
)
const CancelTrade = "trade:update"

View File

@@ -1,6 +1,7 @@
package globals
import (
"fmt"
"platform/pkg/env"
"github.com/smartwalle/alipay/v3"
@@ -8,25 +9,26 @@ import (
var Alipay *alipay.Client
func initAlipay() {
func initAlipay() error {
var client, err = alipay.New(
env.AlipayAppId,
env.AlipayAppPrivateKey,
env.AlipayProduction,
)
if err != nil {
panic("初始化支付宝客户端失败: " + err.Error())
return fmt.Errorf("初始化支付宝客户端失败: %w", err)
}
err = client.LoadAliPayPublicKey(env.AlipayPublicKey)
if err != nil {
panic("加载支付宝公钥失败: " + err.Error())
return fmt.Errorf("加载支付宝公钥失败: %w", err)
}
err = client.SetEncryptKey(env.AlipayApiCert)
if err != nil {
panic("设置支付宝加密密钥失败: " + err.Error())
return fmt.Errorf("设置支付宝加密证书失败: %w", err)
}
Alipay = client
return nil
}

View File

@@ -1,6 +1,7 @@
package globals
import (
"fmt"
"platform/pkg/env"
"platform/pkg/u"
@@ -14,17 +15,18 @@ type aliyunClient struct {
Sms *sms.Client
}
func initAliyun() {
func initAliyun() error {
client, err := sms.NewClient(&openapi.Config{
AccessKeyId: &env.AliyunAccessKey,
AccessKeySecret: &env.AliyunAccessKeySecret,
Endpoint: u.P("dysmsapi.aliyuncs.com"),
})
if err != nil {
panic(err)
return fmt.Errorf("初始化阿里云客户端失败: %w", err)
}
Aliyun = &aliyunClient{
Sms: client,
}
return nil
}

View File

@@ -35,10 +35,11 @@ type cloud struct {
var Cloud CloudClient
func initBaiyin() {
func initBaiyin() error {
Cloud = &cloud{
url: env.BaiyinAddr,
}
return nil
}
type AutoConfig struct {

View File

@@ -1,14 +1,38 @@
package globals
func Init() {
initBaiyin()
initAlipay()
initWechatPay()
initAliyun()
initValidator()
initRedis()
initOrm()
initProxy()
initAsynq()
initSft()
import (
"context"
"platform/pkg/u"
)
func Init(ctx context.Context) error {
errs := make([]error, 0)
errs = append(errs, initBaiyin())
errs = append(errs, initAlipay())
errs = append(errs, initWechatPay())
errs = append(errs, initAliyun())
errs = append(errs, initValidator())
errs = append(errs, initRedis())
errs = append(errs, initOrm())
errs = append(errs, initProxy())
errs = append(errs, initSft())
return u.CombineErrors(errs)
}
func Stop() error {
var errs = make([]error, 0)
err := stopRedis()
if err != nil {
errs = append(errs, err)
}
err = stopOrm()
if err != nil {
errs = append(errs, err)
}
return u.CombineErrors(errs)
}

View File

@@ -1,17 +1,20 @@
package globals
import (
"database/sql"
"fmt"
"platform/pkg/env"
"platform/web/queries"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"log/slog"
"platform/pkg/env"
)
var DB *gorm.DB
var Conn *sql.DB
func initOrm() {
func initOrm() error {
// 连接数据库
dsn := fmt.Sprintf(
@@ -25,27 +28,29 @@ func initOrm() {
},
})
if err != nil {
slog.Error("gorm 初始化数据库失败:", slog.Any("err", err))
panic(err)
return fmt.Errorf("连接数据库失败: %w", err)
}
// 连接池
conn, err := db.DB()
if err != nil {
slog.Error("gorm 初始化数据库失败:", slog.Any("err", err))
panic(err)
return fmt.Errorf("配置连接池失败: %w", err)
}
conn.SetMaxIdleConns(10)
conn.SetMaxOpenConns(100)
queries.SetDefault(db)
DB = db
Conn = conn
return nil
}
func ExitOrm() error {
func stopOrm() error {
if DB != nil {
conn, err := DB.DB()
if err != nil {
return err
return fmt.Errorf("关闭数据库连接失败: %w", err)
}
return conn.Close()
}

View File

@@ -23,8 +23,9 @@ var Proxy *ProxyClient
type ProxyClient struct {
}
func initProxy() {
func initProxy() error {
Proxy = &ProxyClient{}
return nil
}
type ProxyPermitConfig struct {

View File

@@ -1,12 +1,13 @@
package globals
import (
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
"log/slog"
"net"
"platform/pkg/env"
"platform/web/core"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
"github.com/go-redsync/redsync/v4"
"github.com/redis/go-redis/v9"
)
@@ -18,11 +19,10 @@ type ExtendRedSync struct {
*redsync.Redsync
}
func initRedis() {
func initRedis() error {
client := redis.NewClient(&redis.Options{
Addr: net.JoinHostPort(env.RedisHost, env.RedisPort),
DB: env.RedisDb,
Password: env.RedisPass,
Password: env.RedisPassword,
})
pool := goredis.NewPool(client)
@@ -30,9 +30,11 @@ func initRedis() {
Redis = client
Redsync = &ExtendRedSync{sync}
return nil
}
func ExitRedis() error {
func stopRedis() error {
if Redis != nil {
return Redis.Close()
}

View File

@@ -28,9 +28,9 @@ type SftClient struct {
publicKey *rsa.PublicKey
}
func initSft() {
func initSft() error {
if !env.SftPayEnable {
panic("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
return fmt.Errorf("商福通支付未启用,请检查环境变量 SFTPAY_ENABLE")
}
SFTPay = SftClient{
@@ -41,7 +41,7 @@ func initSft() {
// 加载私钥
private, err := base64.StdEncoding.DecodeString(env.SftPayAppPrivateKey)
if err != nil {
panic("解析商福通私钥失败: " + err.Error())
return fmt.Errorf("解析商福通私钥失败: %w", err)
}
var privateKey *rsa.PrivateKey
@@ -49,13 +49,13 @@ func initSft() {
if err != nil {
pkcs8, err := x509.ParsePKCS8PrivateKey(private)
if err != nil {
panic("解析商福通私钥失败: " + err.Error())
return fmt.Errorf("解析商福通私钥失败: %w", err)
}
var ok bool
privateKey, ok = pkcs8.(*rsa.PrivateKey)
if !ok {
panic("解析商福通私钥失败")
return fmt.Errorf("解析商福通私钥失败")
}
}
SFTPay.privateKey = privateKey
@@ -63,35 +63,36 @@ func initSft() {
// 加载公钥
public, err := base64.StdEncoding.DecodeString(env.SftPayPublicKey)
if err != nil {
panic("解析商福通公钥失败: " + err.Error())
return fmt.Errorf("解析商福通公钥失败: %w", err)
}
var publicKey *rsa.PublicKey
pkix, err := x509.ParsePKIXPublicKey(public)
if err != nil {
panic("解析商福通公钥失败: " + err.Error())
return fmt.Errorf("解析商福通公钥失败: %w", err)
}
var ok bool
publicKey, ok = pkix.(*rsa.PublicKey)
if !ok {
panic("解析商福通公钥失败")
return fmt.Errorf("解析商福通公钥失败")
}
SFTPay.publicKey = publicKey
return nil
}
func (s *SftClient) PaymentScanPay(req *PaymentScanPayReq) (*PaymentScanPayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/scanpay"
req.ReturnUrl = env.SftReturnUrl
req.NotifyUrl = env.SftNotifyUrl
req.ReturnUrl = u.X(env.SftReturnUrl)
req.NotifyUrl = u.X(env.SftNotifyUrl)
req.RouteNo = u.P(s.routeId)
return call[PaymentScanPayResp](s, url, req)
}
func (s *SftClient) PaymentH5Pay(req *PaymentH5PayReq) (*PaymentH5PayResp, error) {
const url = "https://pay.rscygroup.com/api/open/payment/h5pay"
req.ReturnUrl = env.SftReturnUrl
req.NotifyUrl = env.SftNotifyUrl
req.ReturnUrl = u.X(env.SftReturnUrl)
req.NotifyUrl = u.X(env.SftNotifyUrl)
req.RouteNo = u.P(s.routeId)
return call[PaymentH5PayResp](s, url, req)
}
@@ -256,7 +257,7 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
encode, err := s.sign(req)
if err != nil {
return nil, fmt.Errorf("加密请求内容失败%w", err)
return nil, fmt.Errorf("加密请求内容失败: %w", err)
}
bytes, err := json.Marshal(encode)
@@ -266,33 +267,33 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
request, err := http.NewRequest("POST", url, strings.NewReader(string(bytes)))
if err != nil {
return nil, fmt.Errorf("创建请求失败%w", err)
return nil, fmt.Errorf("创建请求失败: %w", err)
}
request.Header.Set("Content-Type", "application/json")
if env.DebugHttpDump == true {
reqDump, err := httputil.DumpRequest(request, true)
if err != nil {
return nil, fmt.Errorf("请求内容转储失败%w", err)
return nil, fmt.Errorf("请求内容转储失败: %w", err)
}
println(string(reqDump) + "\n\n")
}
response, err := http.DefaultClient.Do(request)
if err != nil {
return nil, fmt.Errorf("请求失败%w", err)
return nil, fmt.Errorf("请求失败: %w", err)
}
if env.DebugHttpDump == true {
respDump, err := httputil.DumpResponse(response, true)
if err != nil {
return nil, fmt.Errorf("响应内容转储失败%w", err)
return nil, fmt.Errorf("响应内容转储失败: %w", err)
}
println(string(respDump) + "\n\n")
}
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("请求响应失败%d", response.StatusCode)
return nil, fmt.Errorf("请求响应失败: %d", response.StatusCode)
}
defer func(body io.ReadCloser) {
_ = body.Close()
@@ -300,18 +301,18 @@ func call[T any](s *SftClient, url string, req any) (*T, error) {
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("读取响应内容失败%w", err)
return nil, fmt.Errorf("读取响应内容失败: %w", err)
}
decode, err := s.verify(body)
if err != nil {
return nil, fmt.Errorf("解密响应内容失败%w", err)
return nil, fmt.Errorf("解密响应内容失败: %w", err)
}
var resp = new(T)
err = json.Unmarshal([]byte(decode), resp)
if err != nil {
return nil, fmt.Errorf("响应正文解析失败%w", err)
return nil, fmt.Errorf("响应正文解析失败: %w", err)
}
return resp, nil
@@ -321,7 +322,7 @@ func (s *SftClient) sign(msg any) (*request, error) {
bytes, err := json.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("格式化加密正文失败%w", err)
return nil, fmt.Errorf("格式化加密正文失败: %w", err)
}
if env.DebugHttpDump {
@@ -341,7 +342,7 @@ func (s *SftClient) sign(msg any) (*request, error) {
hashed := sha256.Sum256([]byte(body.String()))
signature, err := rsa.SignPKCS1v15(nil, s.privateKey, crypto.SHA256, hashed[:])
if err != nil {
return nil, fmt.Errorf("签名失败%w", err)
return nil, fmt.Errorf("签名失败: %w", err)
}
body.Sign = base64.StdEncoding.EncodeToString(signature)
@@ -353,11 +354,11 @@ func (s *SftClient) verify(str []byte) (string, error) {
var resp = new(response)
err := json.Unmarshal(str, resp)
if err != nil {
return "", fmt.Errorf("解析响应正文失败%w", err)
return "", fmt.Errorf("解析响应正文失败: %w", err)
}
if resp.Code != "000000" {
return "", fmt.Errorf("请求业务响应失败%s", u.Z(resp.Msg))
return "", fmt.Errorf("请求业务响应失败: %s", u.Z(resp.Msg))
}
if resp.Sign == nil {
@@ -371,13 +372,13 @@ func (s *SftClient) verify(str []byte) (string, error) {
ser, err := resp.String()
if err != nil {
return "", fmt.Errorf("格式化响应内容失败%w", err)
return "", fmt.Errorf("格式化响应内容失败: %w", err)
}
hashed := sha256.Sum256([]byte(ser))
err = rsa.VerifyPKCS1v15(s.publicKey, crypto.SHA256, hashed[:], sign)
if err != nil {
return "", fmt.Errorf("验签失败%w", err)
return "", fmt.Errorf("验签失败: %w", err)
}
return *resp.BizData, nil
@@ -412,7 +413,7 @@ type response struct {
func (r response) String() (string, error) {
if r.BizData == nil || r.Msg == nil || r.SignType == nil {
return "", core.NewServErr(fmt.Sprintf(
"上游数据返回有空值BizData %vMsg %v, SignType %v",
"上游数据返回有空值: BizData %vMsg %v, SignType %v",
r.BizData == nil, r.Msg == nil, r.SignType == nil,
))
}

View File

@@ -2,12 +2,14 @@ package globals
import (
"errors"
"fmt"
"strings"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zhtrans "github.com/go-playground/validator/v10/translations/zh"
"github.com/gofiber/fiber/v2"
"strings"
)
var Validator *ValidatorClient
@@ -38,17 +40,18 @@ func (v *ValidatorClient) Validate(c *fiber.Ctx, data any) error {
return nil
}
func initValidator() {
func initValidator() error {
var validate = validator.New(validator.WithRequiredStructEnabled())
var translator = ut.New(zh.New()).GetFallback()
err := zhtrans.RegisterDefaultTranslations(validate, translator)
if err != nil {
panic(err)
return fmt.Errorf("初始化验证器失败: %w", err)
}
Validator = &ValidatorClient{
validator: validate,
translator: translator,
}
return nil
}

View File

@@ -3,6 +3,7 @@ package globals
import (
"context"
"encoding/base64"
"fmt"
"platform/pkg/env"
"github.com/wechatpay-apiv3/wechatpay-go/core"
@@ -20,28 +21,28 @@ type WechatPayClient struct {
Notify *notify.Handler
}
func initWechatPay() {
func initWechatPay() error {
// 加载商户私钥
private, err := base64.StdEncoding.DecodeString(env.WechatPayMchPrivateKey)
if err != nil {
panic(err)
return fmt.Errorf("加载微信支付商户私钥失败: %w", err)
}
appPrivateKey, err := utils.LoadPrivateKey(string(private))
if err != nil {
panic(err)
return fmt.Errorf("解析微信支付商户私钥失败: %w", err)
}
// 加载微信支付公钥
public, err := base64.StdEncoding.DecodeString(env.WechatPayPublicKey)
if err != nil {
panic(err)
return fmt.Errorf("加载微信支付公钥失败: %w", err)
}
wechatPublicKey, err := utils.LoadPublicKey(string(public))
if err != nil {
panic(err)
return fmt.Errorf("解析微信支付公钥失败: %w", err)
}
// 创建 WechatPay 客户端
@@ -55,7 +56,7 @@ func initWechatPay() {
),
)
if err != nil {
panic(err)
return fmt.Errorf("创建微信支付客户端失败: %w", err)
}
// 创建 WechatPay 通知处理器
@@ -64,7 +65,7 @@ func initWechatPay() {
*wechatPublicKey,
))
if err != nil {
panic(err)
return fmt.Errorf("创建微信支付通知处理器失败: %w", err)
}
// 创建 WechatPay 服务
@@ -72,4 +73,5 @@ func initWechatPay() {
Native: &native.NativeApiService{Client: client},
Notify: handler,
}
return nil
}

View File

@@ -1,10 +1,11 @@
package handlers
import (
"github.com/gofiber/fiber/v2"
"platform/web/auth"
"platform/web/core"
q "platform/web/queries"
"github.com/gofiber/fiber/v2"
)
// region ListAnnouncements
@@ -16,7 +17,7 @@ type ListAnnouncementsRequest struct {
func ListAnnouncements(c *fiber.Ctx) error {
// 检查权限
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}

View File

@@ -1,277 +1,14 @@
package handlers
import (
"encoding/base64"
"errors"
"log/slog"
"platform/pkg/u"
auth2 "platform/web/auth"
client2 "platform/web/domains/client"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// region /token
type TokenReq struct {
GrantType auth2.GrantType `json:"grant_type" form:"grant_type"`
ClientID string `json:"client_id" form:"client_id"`
ClientSecret string `json:"client_secret" form:"client_secret"`
Scope string `json:"scope" form:"scope"`
s.GrantCodeData
s.GrantClientData
s.GrantRefreshData
s.GrantPasswordData
}
type TokenResp struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token,omitempty"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope,omitempty"`
}
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
// Token 处理 OAuth2.0 授权请求
func Token(c *fiber.Ctx) error {
// 验证请求参数
req := new(TokenReq)
if err := c.BodyParser(req); err != nil {
return sendError(c, s.ErrOauthInvalidRequest, "无法解析请求参数")
}
if req.GrantType == "" {
return sendError(c, s.ErrOauthInvalidRequest, "缺少必要参数grant_type")
}
slog.Debug("oauth token", slog.String("grant_type",
string(req.GrantType)),
slog.String("client_id", req.ClientID),
)
// 基于授权类型处理请求
switch req.GrantType {
// 授权码模式
case auth2.GrantAuthorizationCode:
if req.Code == "" {
return sendError(c, s.ErrOauthInvalidRequest, "缺少必要参数code")
}
client, err := protect(c, req.GrantType, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
token, err := s.Auth.OauthAuthorizationCode(c.Context(), client, req.Code, req.RedirectURI, req.CodeVerifier)
if err != nil {
return sendError(c, err.(s.AuthServiceError))
}
return sendSuccess(c, token)
// 客户端凭证模式
case auth2.GrantClientCredentials:
client, err := protect(c, req.GrantType, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
scope := strings.Split(req.Scope, ",")
token, err := s.Auth.OauthClientCredentials(c.Context(), client, scope...)
if err != nil {
return sendError(c, err.(s.AuthServiceError))
}
return sendSuccess(c, token)
// 刷新令牌模式
case auth2.GrantRefreshToken:
if req.RefreshToken == "" {
return sendError(c, s.ErrOauthInvalidRequest, "缺少必要参数refresh_token")
}
client, err := protect(c, req.GrantType, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
scope := strings.Split(req.Scope, ",")
token, err := s.Auth.OauthRefreshToken(c.Context(), client, req.RefreshToken, scope)
if err != nil {
if errors.Is(err, auth2.ErrInvalidRefreshToken) {
return sendError(c, s.ErrOauthInvalidGrant)
}
return sendError(c, err)
}
return sendSuccess(c, token)
// 密码模式
case auth2.GrantPassword:
if req.LoginType == "" {
return sendError(c, s.ErrOauthInvalidRequest, "缺少必要参数password_type")
}
if req.Username == "" {
return sendError(c, s.ErrOauthInvalidRequest, "缺少必要参数username")
}
if req.Password == "" {
return sendError(c, s.ErrOauthInvalidRequest, "缺少必要参数password")
}
client, err := protect(c, req.GrantType, req.ClientID, req.ClientSecret)
if err != nil {
return sendError(c, err)
}
token, err := s.Auth.OauthPassword(c.Context(), client, &req.GrantPasswordData, c.IP(), c.Get("User-Agent"))
if err != nil {
return sendError(c, err)
}
return sendSuccess(c, token)
default:
return sendError(c, s.ErrOauthUnsupportedGrantType)
}
}
// 检查客户端凭证
func protect(c *fiber.Ctx, grant auth2.GrantType, clientId, clientSecret string) (*m.Client, error) {
header := c.Get("Authorization")
if header != "" {
basic := strings.TrimPrefix(header, "Basic ")
if basic != "" {
base, err := base64.RawURLEncoding.DecodeString(basic)
if err != nil {
return nil, err
}
parts := strings.SplitN(string(base), ":", 2)
if len(parts) == 2 {
clientId = parts[0]
clientSecret = parts[1]
}
}
}
// 查找客户端
if clientId == "" {
return nil, s.ErrOauthInvalidRequest
}
client, err := q.Client.Where(q.Client.ClientID.Eq(clientId)).Take()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, s.ErrOauthInvalidClient
}
return nil, err
}
// 验证客户端状态
if client.Status != 1 {
return nil, s.ErrOauthUnauthorizedClient
}
// 验证授权类型
switch grant {
case auth2.GrantAuthorizationCode:
if !client.GrantCode {
return nil, s.ErrOauthUnauthorizedClient
}
case auth2.GrantClientCredentials:
if !client.GrantClient || client.Spec != int32(client2.SpecWeb) || client.Spec != int32(client2.SpecTrusted) {
return nil, s.ErrOauthUnauthorizedClient
}
case auth2.GrantRefreshToken:
if !client.GrantRefresh {
return nil, s.ErrOauthUnauthorizedClient
}
case auth2.GrantPassword:
if !client.GrantPassword {
return nil, s.ErrOauthUnauthorizedClient
}
}
// 如果客户端是 confidential验证 client_secret失败返回错误
if client.Spec == int32(client2.SpecWeb) || client.Spec == int32(client2.SpecTrusted) {
if clientSecret == "" {
return nil, s.ErrOauthInvalidRequest
}
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, s.ErrOauthInvalidClient
}
}
// 保存 auth 信息到上下文(以兼容通用 auth 处理逻辑)
auth2.Locals(c, &auth2.Context{
Payload: auth2.Payload{
Id: client.ID,
Type: auth2.PayloadSecuredServer,
Name: client.Name,
Avatar: client.Icon,
},
})
return client, nil
}
// 发送成功响应
func sendSuccess(c *fiber.Ctx, details *auth2.TokenDetails) error {
return c.JSON(TokenResp{
AccessToken: details.AccessToken,
TokenType: "Bearer",
ExpiresIn: int(time.Until(details.AccessTokenExpires).Seconds()),
RefreshToken: details.RefreshToken,
})
}
// 发送错误响应
func sendError(c *fiber.Ctx, err error, description ...string) error {
var sErr s.AuthServiceError
if errors.As(err, &sErr) {
status := fiber.StatusBadRequest
var desc string
switch {
case errors.Is(sErr, s.ErrOauthInvalidRequest):
desc = "无效的请求"
case errors.Is(sErr, s.ErrOauthInvalidClient):
status = fiber.StatusUnauthorized
desc = "无效的客户端凭证"
case errors.Is(sErr, s.ErrOauthInvalidGrant):
desc = "无效的授权凭证"
case errors.Is(sErr, s.ErrOauthInvalidScope):
desc = "无效的授权范围"
case errors.Is(sErr, s.ErrOauthUnauthorizedClient):
desc = "未授权的客户端"
case errors.Is(sErr, s.ErrOauthUnsupportedGrantType):
desc = "不支持的授权类型"
}
if len(description) > 0 {
desc = description[0]
}
return c.Status(status).JSON(TokenErrResp{
Error: string(sErr),
Description: desc,
})
}
return err
}
// endregion
// region /revoke
type RevokeReq struct {
@@ -280,7 +17,7 @@ type RevokeReq struct {
}
func Revoke(c *fiber.Ctx) error {
_, err := auth2.Protect(c, []auth2.PayloadType{auth2.PayloadUser}, []string{})
_, err := auth2.GetAuthCtx(c).PermitUser()
if err != nil {
// 用户未登录
return nil
@@ -312,14 +49,14 @@ type IntrospectResp struct {
func Introspect(c *fiber.Ctx) error {
// 验证权限
authCtx, err := auth2.Protect(c, []auth2.PayloadType{auth2.PayloadUser}, []string{})
authCtx, err := auth2.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 获取用户信息
profile, err := q.User.
Where(q.User.ID.Eq(authCtx.Payload.Id)).
Where(q.User.ID.Eq(authCtx.User.ID)).
Omit(q.User.DeletedAt).
Take()
if err != nil {

View File

@@ -23,7 +23,7 @@ type ListBillReq struct {
// ListBill 获取账单列表
func ListBill(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -36,7 +36,7 @@ func ListBill(c *fiber.Ctx) error {
// 查询账单列表
do := q.Bill.
Where(q.Bill.UserID.Eq(authContext.Payload.Id))
Where(q.Bill.UserID.Eq(authCtx.User.ID))
if req.Type != nil {
do.Where(q.Bill.Type.Eq(int32(*req.Type)))

View File

@@ -24,7 +24,7 @@ type ListChannelsReq struct {
func ListChannels(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authContext, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -37,7 +37,7 @@ func ListChannels(c *fiber.Ctx) error {
// 构造查询条件
cond := q.Channel.
Where(q.Channel.UserID.Eq(authContext.Payload.Id))
Where(q.Channel.UserID.Eq(authContext.User.ID))
switch req.AuthType {
case s.ChannelAuthTypeIp:
cond.Where(q.Channel.AuthIP.Is(true))
@@ -110,24 +110,19 @@ type CreateChannelRespItem struct {
func CreateChannel(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
// 检查用户其他权限
user, err := q.User.
Where(q.User.ID.Eq(authContext.Payload.Id)).
Take()
if err != nil {
return err
}
user := authCtx.User
if user.IDToken == nil || *user.IDToken == "" {
return fiber.NewError(fiber.StatusForbidden, "账号未实名")
}
count, err := q.Whitelist.Where(
q.Whitelist.UserID.Eq(authContext.Payload.Id),
q.Whitelist.UserID.Eq(user.ID),
q.Whitelist.Host.Eq(c.IP()),
).Count()
if err != nil {
@@ -155,7 +150,7 @@ func CreateChannel(c *fiber.Ctx) error {
// 创建通道
result, err := s.Channel.CreateChannel(
c,
authContext.Payload.Id,
user.ID,
req.ResourceId,
req.Protocol,
req.AuthType,
@@ -198,7 +193,7 @@ type RemoveChannelsReq struct {
func RemoveChannels(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.NewProtect(c).Payload(auth.PayloadUser).Do()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -210,31 +205,7 @@ func RemoveChannels(c *fiber.Ctx) error {
}
// 删除通道
err = s.Channel.RemoveChannels(req.ByIds, authCtx.Payload.Id)
if err != nil {
return err
}
return c.SendStatus(fiber.StatusOK)
}
type RemoveChannelByTaskReq []int32
func RemoveChannelByTask(c *fiber.Ctx) error {
// 检查权限
_, err := auth.NewProtect(c).Payload(auth.PayloadInternalServer).Do()
if err != nil {
return err
}
// 解析请求参数
var req RemoveChannelByTaskReq
if err := c.BodyParser(&req); err != nil {
return err
}
// 删除通道
err = s.Channel.RemoveChannels(req)
err = s.Channel.RemoveChannels(req.ByIds, authCtx.User.ID)
if err != nil {
return err
}

View File

@@ -2,8 +2,6 @@ package handlers
import (
"errors"
"gorm.io/gen/field"
"gorm.io/gorm"
"log/slog"
"platform/pkg/u"
"platform/web/auth"
@@ -14,6 +12,9 @@ import (
q "platform/web/queries"
s "platform/web/services"
"gorm.io/gen/field"
"gorm.io/gorm"
"github.com/gofiber/fiber/v2"
)
@@ -120,7 +121,7 @@ type AllEdgesAvailableRespItem struct {
func AllEdgesAvailable(c *fiber.Ctx) (err error) {
// 检查权限
_, err = auth.NewProtect(c).Payload(auth.PayloadInternalServer).Do()
_, err = auth.GetAuthCtx(c).PermitSecretClient()
if err != nil {
return err
}

View File

@@ -37,17 +37,11 @@ type IdentifyRes struct {
func Identify(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
if err != nil {
return err
}
user, err := q.User.
Where(q.User.ID.Eq(authCtx.Payload.Id)).
Select(q.User.ID, q.User.IDToken).
Take()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
user := authCtx.User
if user.IDToken != nil && *user.IDToken != "" {
// 用户已实名认证
return c.JSON(IdentifyRes{
@@ -86,7 +80,7 @@ func Identify(c *fiber.Ctx) error {
// 保存认证中间状态
info := idenInfo{
Uid: authCtx.Payload.Id,
Uid: user.ID,
Type: req.Type,
Name: req.Name,
IdNo: req.IdenNo,

View File

@@ -40,9 +40,7 @@ type ProxyReportOnlineResp struct {
func ProxyReportOnline(c *fiber.Ctx) (err error) {
// 检查接口权限
_, err = auth2.NewProtect(c).Payload(
auth2.PayloadInternalServer,
).Do()
_, err = auth2.GetAuthCtx(c).PermitSecretClient()
if err != nil {
return err
}
@@ -149,9 +147,7 @@ type ProxyReportOfflineReq struct {
func ProxyReportOffline(c *fiber.Ctx) (err error) {
// 检查接口权限
_, err = auth2.NewProtect(c).Payload(
auth2.PayloadInternalServer,
).Do()
_, err = auth2.GetAuthCtx(c).PermitSecretClient()
if err != nil {
return err
}
@@ -193,9 +189,7 @@ type ProxyReportUpdateReq struct {
func ProxyReportUpdate(c *fiber.Ctx) (err error) {
// 检查接口权限
_, err = auth2.NewProtect(c).Payload(
auth2.PayloadInternalServer,
).Do()
_, err = auth2.GetAuthCtx(c).PermitSecretClient()
if err != nil {
return err
}

View File

@@ -1,7 +1,6 @@
package handlers
import (
"gorm.io/gen/field"
"platform/pkg/u"
"platform/web/auth"
"platform/web/core"
@@ -12,6 +11,8 @@ import (
s "platform/web/services"
"time"
"gorm.io/gen/field"
"github.com/gofiber/fiber/v2"
)
@@ -28,7 +29,7 @@ type ListResourceShortReq struct {
func ListResourceShort(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -41,7 +42,7 @@ func ListResourceShort(c *fiber.Ctx) error {
// 查询套餐列表
do := q.Resource.Where(
q.Resource.UserID.Eq(authContext.Payload.Id),
q.Resource.UserID.Eq(authCtx.User.ID),
q.Resource.Type.Eq(int32(resource2.TypeShort)),
)
if req.ResourceNo != nil && *req.ResourceNo != "" {
@@ -109,7 +110,7 @@ type ListResourceLongReq struct {
func ListResourceLong(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -122,7 +123,7 @@ func ListResourceLong(c *fiber.Ctx) error {
// 查询套餐列表
do := q.Resource.Where(
q.Resource.UserID.Eq(authContext.Payload.Id),
q.Resource.UserID.Eq(authCtx.User.ID),
q.Resource.Type.Eq(int32(resource2.TypeLong)),
)
if req.ResourceNo != nil && *req.ResourceNo != "" {
@@ -182,7 +183,7 @@ type AllResourceReq struct {
func AllActiveResource(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.NewProtect(c).Payload(auth.PayloadUser).Do()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -198,7 +199,7 @@ func AllActiveResource(c *fiber.Ctx) error {
q.Resource.Long,
).
Where(
q.Resource.UserID.Eq(authCtx.Payload.Id),
q.Resource.UserID.Eq(authCtx.User.ID),
q.Resource.Active.Is(true),
q.Resource.Where(
q.Resource.Type.Eq(int32(resource2.TypeShort)),
@@ -254,7 +255,7 @@ type StatisticLong struct {
func StatisticResourceFree(c *fiber.Ctx) error {
// 检查权限
session, err := auth.NewProtect(c).Payload(auth.PayloadUser).Do()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -266,7 +267,7 @@ func StatisticResourceFree(c *fiber.Ctx) error {
q.Resource.Long,
).
Where(
q.Resource.UserID.Eq(session.Payload.Id),
q.Resource.UserID.Eq(authCtx.User.ID),
q.Resource.Active.Is(true),
).
Select(q.Resource.ID, q.Resource.Type).
@@ -347,7 +348,7 @@ type StatisticResourceUsageResp []struct {
func StatisticResourceUsage(c *fiber.Ctx) error {
// 检查权限
session, err := auth.NewProtect(c).Payload(auth.PayloadUser).Do()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -359,12 +360,12 @@ func StatisticResourceUsage(c *fiber.Ctx) error {
}
// 统计套餐提取数量
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(session.Payload.Id))
do := q.LogsUserUsage.Where(q.LogsUserUsage.UserID.Eq(authCtx.User.ID))
if req.ResourceNo != nil && *req.ResourceNo != "" {
var resourceID int32
err := q.Resource.
Where(
q.Resource.UserID.Eq(session.Payload.Id),
q.Resource.UserID.Eq(authCtx.User.ID),
q.Resource.ResourceNo.Eq(*req.ResourceNo),
).
Select(q.Resource.ID).
@@ -409,7 +410,7 @@ type CreateResourceReq struct {
func CreateResource(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.NewProtect(c).Payload(auth.PayloadUser).Do()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -421,7 +422,7 @@ func CreateResource(c *fiber.Ctx) error {
}
// 创建套餐
err = s.Resource.CreateResourceByBalance(authCtx.Payload.Id, time.Now(), req.CreateResourceData)
err = s.Resource.CreateResourceByBalance(authCtx.User.ID, time.Now(), req.CreateResourceData)
if err != nil {
return err
}
@@ -431,7 +432,7 @@ func CreateResource(c *fiber.Ctx) error {
func ResourcePrice(c *fiber.Ctx) error {
// 检查权限
_, err := auth.NewProtect(c).Payload(auth.PayloadInternalServer).Do()
_, err := auth.GetAuthCtx(c).PermitSecretClient()
if err != nil {
return err
}

View File

@@ -7,7 +7,6 @@ import (
trade2 "platform/web/domains/trade"
g "platform/web/globals"
s "platform/web/services"
"platform/web/tasks"
"time"
"github.com/gofiber/fiber/v2"
@@ -27,7 +26,7 @@ type TradeCreateResp struct {
func TradeCreate(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.NewProtect(c).Payload(auth.PayloadUser).Do()
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -52,7 +51,7 @@ func TradeCreate(c *fiber.Ctx) error {
}
// 创建交易
result, err := s.Trade.CreateTrade(authCtx.Payload.Id, time.Now(), &req.CreateTradeData)
result, err := s.Trade.CreateTrade(authCtx.User.ID, time.Now(), &req.CreateTradeData)
if err != nil {
slog.Error("创建交易失败", "error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "创建交易失败"})
@@ -70,7 +69,7 @@ type TradeCompleteReq struct {
func TradeComplete(c *fiber.Ctx) error {
// 检查权限
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -99,7 +98,7 @@ type TradeCancelReq struct {
func TradeCancel(c *fiber.Ctx) error {
// 检查权限
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
_, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -119,29 +118,3 @@ func TradeCancel(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusNoContent)
}
type TradeCheckReq struct {
tasks.CancelTradeData
}
func TradeCancelByTask(c *fiber.Ctx) error {
// 检查权限
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadInternalServer}, []string{})
if err != nil {
return err
}
// 取消交易
req := new(TradeCheckReq)
if err := c.BodyParser(req); err != nil {
return err
}
// 检查订单状态
err = s.Trade.CancelTrade(req.TradeNo, req.Method, time.Now())
if err != nil {
return err
}
return nil
}

View File

@@ -1,12 +1,13 @@
package handlers
import (
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
"platform/web/auth"
m "platform/web/models"
q "platform/web/queries"
s "platform/web/services"
"github.com/gofiber/fiber/v2"
"golang.org/x/crypto/bcrypt"
)
// region /update
@@ -20,7 +21,7 @@ type UpdateUserReq struct {
func UpdateUser(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -33,7 +34,7 @@ func UpdateUser(c *fiber.Ctx) error {
// 更新用户信息
_, err = q.User.
Where(q.User.ID.Eq(authCtx.Payload.Id)).
Where(q.User.ID.Eq(authCtx.User.ID)).
Updates(m.User{
Username: &req.Username,
Email: &req.Email,
@@ -59,7 +60,7 @@ type UpdateAccountReq struct {
func UpdateAccount(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -72,7 +73,7 @@ func UpdateAccount(c *fiber.Ctx) error {
// 更新用户信息
_, err = q.User.
Where(q.User.ID.Eq(authCtx.Payload.Id)).
Where(q.User.ID.Eq(authCtx.User.ID)).
Updates(m.User{
Username: &req.Username,
Password: &req.Password,
@@ -97,7 +98,7 @@ type UpdatePasswordReq struct {
func UpdatePassword(c *fiber.Ctx) error {
// 检查权限
authCtx, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -124,7 +125,7 @@ func UpdatePassword(c *fiber.Ctx) error {
}
_, err = q.User.
Where(q.User.ID.Eq(authCtx.Payload.Id)).
Where(q.User.ID.Eq(authCtx.User.ID)).
UpdateColumn(q.User.Password, newHash)
if err != nil {
return err

View File

@@ -2,12 +2,14 @@ package handlers
import (
"errors"
"platform/pkg/env"
"platform/web/auth"
"platform/web/services"
"regexp"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/redis/go-redis/v9"
)
type VerifierReq struct {
@@ -17,7 +19,7 @@ type VerifierReq struct {
func SmsCode(c *fiber.Ctx) error {
_, err := auth.Protect(c, []auth.PayloadType{auth.PayloadInternalServer}, []string{})
_, err := auth.GetAuthCtx(c).PermitInternalClient()
if err != nil {
return err
}
@@ -48,3 +50,19 @@ func SmsCode(c *fiber.Ctx) error {
// 发送成功
return nil
}
func DebugGetSmsCode(c *fiber.Ctx) error {
if env.RunMode != env.RunModeDev {
return fiber.NewError(fiber.StatusForbidden, "not allowed")
}
code, err := services.Verifier.GetSms(c.Context(), c.Params("phone"))
if err != nil {
if errors.Is(err, redis.Nil) {
return c.SendString("还没有验证码")
}
return err
}
return c.SendString(code)
}

View File

@@ -26,7 +26,7 @@ type ListWhitelistResp struct {
func ListWhitelist(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -38,8 +38,7 @@ func ListWhitelist(c *fiber.Ctx) error {
}
// 获取白名单信息
do := q.Whitelist.
Where(q.Whitelist.UserID.Eq(authContext.Payload.Id))
do := q.Whitelist.Where(q.Whitelist.UserID.Eq(authCtx.User.ID))
list, err := q.Whitelist.Where(do).
Offset(req.GetOffset()).
@@ -77,7 +76,7 @@ type CreateWhitelistReq struct {
func CreateWhitelist(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -96,7 +95,7 @@ func CreateWhitelist(c *fiber.Ctx) error {
// 创建白名单
err = q.Whitelist.Create(&m.Whitelist{
UserID: authContext.Payload.Id,
UserID: authCtx.User.ID,
Host: req.Host,
Remark: &req.Remark,
})
@@ -111,7 +110,7 @@ type UpdateWhitelistReq struct {
func UpdateWhitelist(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -129,7 +128,7 @@ func UpdateWhitelist(c *fiber.Ctx) error {
_, err = q.Whitelist.
Where(
q.Whitelist.ID.Eq(req.ID),
q.Whitelist.UserID.Eq(authContext.Payload.Id),
q.Whitelist.UserID.Eq(authCtx.User.ID),
).
Updates(&m.Whitelist{
ID: req.ID,
@@ -149,7 +148,7 @@ type RemoveWhitelistReq struct {
func RemoveWhitelist(c *fiber.Ctx) error {
// 检查权限
authContext, err := auth.Protect(c, []auth.PayloadType{auth.PayloadUser}, []string{})
authCtx, err := auth.GetAuthCtx(c).PermitUser()
if err != nil {
return err
}
@@ -175,7 +174,7 @@ func RemoveWhitelist(c *fiber.Ctx) error {
_, err = q.Whitelist.
Where(
q.Whitelist.ID.In(ids...),
q.Whitelist.UserID.Eq(authContext.Payload.Id),
q.Whitelist.UserID.Eq(authCtx.User.ID),
).
Update(
q.Whitelist.DeletedAt, time.Now(),

42
web/middlewares.go Normal file
View File

@@ -0,0 +1,42 @@
package web
import (
"platform/web/auth"
"github.com/gofiber/contrib/otelfiber/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/google/uuid"
"github.com/jxskiss/base62"
)
func ApplyMiddlewares(app *fiber.App) {
// recover
app.Use(recover.New(recover.Config{
EnableStackTrace: true,
}))
// metric
app.Use(otelfiber.Middleware())
// logger
app.Use(logger.New(logger.Config{
Next: func(c *fiber.Ctx) bool {
return c.Path() == "/favicon.ico"
},
}))
// request id
app.Use(requestid.New(requestid.Config{
Generator: func() string {
binary, _ := uuid.New().MarshalBinary()
return base62.EncodeToString(binary)
},
}))
// authenticate
app.Use(auth.Authenticate())
}

View File

@@ -17,10 +17,14 @@ type Channel struct {
ID int32 `gorm:"column:id;type:integer;primaryKey;autoIncrement:true;comment:通道ID" json:"id"` // 通道ID
UserID int32 `gorm:"column:user_id;type:integer;not null;comment:用户ID" json:"user_id"` // 用户ID
ProxyID int32 `gorm:"column:proxy_id;type:integer;not null;comment:代理ID" json:"proxy_id"` // 代理ID
EdgeID *int32 `gorm:"column:edge_id;type:integer;comment:节点ID" json:"edge_id"` // 节点ID
ResourceID int32 `gorm:"column:resource_id;type:integer;not null;comment:套餐ID" json:"resource_id"` // 套餐ID
ProxyHost string `gorm:"column:proxy_host;type:character varying(255);not null;comment:代理地址" json:"proxy_host"` // 代理地址
ProxyPort int32 `gorm:"column:proxy_port;type:integer;not null;comment:转发端口" json:"proxy_port"` // 转发端口
EdgeHost *string `gorm:"column:edge_host;type:character varying(255);comment:节点地址" json:"edge_host"` // 节点地址
Protocol *int32 `gorm:"column:protocol;type:integer;comment:协议类型1-http2-https3-socks5" json:"protocol"` // 协议类型1-http2-https3-socks5
AuthIP bool `gorm:"column:auth_ip;type:boolean;not null;comment:IP认证" json:"auth_ip"` // IP认证
Whitelists *string `gorm:"column:whitelists;type:text;comment:IP白名单逗号分隔" json:"whitelists"` // IP白名单逗号分隔
AuthPass bool `gorm:"column:auth_pass;type:boolean;not null;comment:密码认证" json:"auth_pass"` // 密码认证
Username *string `gorm:"column:username;type:character varying(255);comment:用户名" json:"username"` // 用户名
Password *string `gorm:"column:password;type:character varying(255);comment:密码" json:"password"` // 密码
@@ -28,10 +32,6 @@ type Channel struct {
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
EdgeHost *string `gorm:"column:edge_host;type:character varying(255);comment:节点地址" json:"edge_host"` // 节点地址
EdgeID *int32 `gorm:"column:edge_id;type:integer;comment:节点ID" json:"edge_id"` // 节点ID
Whitelists *string `gorm:"column:whitelists;type:text;comment:IP白名单逗号分隔" json:"whitelists"` // IP白名单逗号分隔
ResourceID int32 `gorm:"column:resource_id;type:integer;not null;comment:套餐ID" json:"resource_id"` // 套餐ID
}
// TableName Channel's table name

View File

@@ -18,14 +18,11 @@ type Client struct {
ClientID string `gorm:"column:client_id;type:character varying(255);not null;comment:OAuth2客户端标识符" json:"client_id"` // OAuth2客户端标识符
ClientSecret string `gorm:"column:client_secret;type:character varying(255);not null;comment:OAuth2客户端密钥" json:"client_secret"` // OAuth2客户端密钥
RedirectURI *string `gorm:"column:redirect_uri;type:character varying(255);comment:OAuth2 重定向URI" json:"redirect_uri"` // OAuth2 重定向URI
GrantCode bool `gorm:"column:grant_code;type:boolean;not null;comment:允许授权码授予" json:"grant_code"` // 允许授权码授予
GrantClient bool `gorm:"column:grant_client;type:boolean;not null;comment:允许客户端凭证授予" json:"grant_client"` // 允许客户端凭证授予
GrantRefresh bool `gorm:"column:grant_refresh;type:boolean;not null;comment:允许刷新令牌授予" json:"grant_refresh"` // 允许刷新令牌授予
GrantPassword bool `gorm:"column:grant_password;type:boolean;not null;comment:允许密码授予" json:"grant_password"` // 允许密码授予
Spec int32 `gorm:"column:spec;type:integer;not null;comment:安全规范1-native2-browser3-web4-trusted" json:"spec"` // 安全规范1-native2-browser3-web4-trusted
Spec int32 `gorm:"column:spec;type:integer;not null;comment:安全规范1-native2-browser3-web4-api" json:"spec"` // 安全规范1-native2-browser3-web4-api
Name string `gorm:"column:name;type:character varying(255);not null;comment:名称" json:"name"` // 名称
Icon *string `gorm:"column:icon;type:character varying(255);comment:图标URL" json:"icon"` // 图标URL
Status int32 `gorm:"column:status;type:integer;not null;default:1;comment:状态0-禁用1-正常" json:"status"` // 状态0-禁用1-正常
Type int32 `gorm:"column:type;type:integer;not null;comment:类型0-普通1-官方" json:"type"` // 类型0-普通1-官方
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间

View File

@@ -12,12 +12,12 @@ const TableNameLogsLogin = "logs_login"
type LogsLogin struct {
ID int32 `gorm:"column:id;type:integer;primaryKey;autoIncrement:true;comment:登录日志ID" json:"id"` // 登录日志ID
IP string `gorm:"column:ip;type:character varying(45);not null;comment:IP地址" json:"ip"` // IP地址
Ua string `gorm:"column:ua;type:character varying(255);not null;comment:用户代理" json:"ua"` // 用户代理
UA string `gorm:"column:ua;type:character varying(255);not null;comment:用户代理" json:"ua"` // 用户代理
GrantType string `gorm:"column:grant_type;type:character varying(255);not null;comment:授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式" json:"grant_type"` // 授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式
PasswordGrantType string `gorm:"column:password_grant_type;type:character varying(255);not null;comment:密码模式子授权类型password-账号密码phone_code-手机验证码email_code-邮箱验证码" json:"password_grant_type"` // 密码模式子授权类型password-账号密码phone_code-手机验证码email_code-邮箱验证码
Success bool `gorm:"column:success;type:boolean;not null;comment:登录是否成功" json:"success"` // 登录是否成功
Time orm.LocalDateTime `gorm:"column:time;type:timestamp without time zone;not null;comment:登录时间" json:"time"` // 登录时间
UserID *int32 `gorm:"column:user_id;type:integer;comment:用户ID" json:"user_id"` // 用户ID
Time orm.LocalDateTime `gorm:"column:time;type:timestamp without time zone;not null;comment:登录时间" json:"time"` // 登录时间
}
// TableName LogsLogin's table name

View File

@@ -11,16 +11,16 @@ const TableNameLogsRequest = "logs_request"
// LogsRequest mapped from table <logs_request>
type LogsRequest struct {
ID int32 `gorm:"column:id;type:integer;primaryKey;autoIncrement:true;comment:访问日志ID" json:"id"` // 访问日志ID
Identity int32 `gorm:"column:identity;type:integer;not null;comment:访客身份0-游客1-用户2-管理员3-公共服务4-安全服务5-内部服务" json:"identity"` // 访客身份0-游客1-用户2-管理员3-公共服务4-安全服务5-内部服务
Visitor *int32 `gorm:"column:visitor;type:integer;comment:访客ID" json:"visitor"` // 访客ID
IP string `gorm:"column:ip;type:character varying(45);not null;comment:IP地址" json:"ip"` // IP地址
Ua *string `gorm:"column:ua;type:character varying(255);comment:用户代理" json:"ua"` // 用户代理
UA string `gorm:"column:ua;type:character varying(255);not null;comment:用户代理" json:"ua"` // 用户代理
UserID *int32 `gorm:"column:user_id;type:integer;comment:用户ID" json:"user_id"` // 用户ID
ClientID *int32 `gorm:"column:client_id;type:integer;comment:客户端ID" json:"client_id"` // 客户端ID
Method string `gorm:"column:method;type:character varying(10);not null;comment:请求方法" json:"method"` // 请求方法
Path string `gorm:"column:path;type:character varying(255);not null;comment:请求路径" json:"path"` // 请求路径
Latency string `gorm:"column:latency;type:character varying(255);not null;comment:请求延迟" json:"latency"` // 请求延迟
Status int32 `gorm:"column:status;type:integer;not null;comment:响应状态码" json:"status"` // 响应状态码
Error *string `gorm:"column:error;type:text;comment:错误信息" json:"error"` // 错误信息
Time orm.LocalDateTime `gorm:"column:time;type:timestamp without time zone;not null;comment:请求时间" json:"time"` // 请求时间
Latency string `gorm:"column:latency;type:character varying(255);not null;comment:请求延迟" json:"latency"` // 请求延迟
}
// TableName LogsRequest's table name

View File

@@ -18,12 +18,12 @@ type Proxy struct {
Version int32 `gorm:"column:version;type:integer;not null;comment:代理服务版本" json:"version"` // 代理服务版本
Name string `gorm:"column:name;type:character varying(255);not null;comment:代理服务名称" json:"name"` // 代理服务名称
Host string `gorm:"column:host;type:character varying(255);not null;comment:代理服务地址" json:"host"` // 代理服务地址
Type int32 `gorm:"column:type;type:integer;not null;comment:代理服务类型1-三方2-自有" json:"type"` // 代理服务类型1-三方2-自有
Secret *string `gorm:"column:secret;type:character varying(255);comment:代理服务密钥" json:"secret"` // 代理服务密钥
Type int32 `gorm:"column:type;type:integer;not null;comment:代理服务类型1-三方2-自有" json:"type"` // 代理服务类型1-三方2-自有
Status int32 `gorm:"column:status;type:integer;not null;comment:代理服务状态0-离线1-在线" json:"status"` // 代理服务状态0-离线1-在线
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
Status int32 `gorm:"column:status;type:integer;not null;comment:代理服务状态0-离线1-在线" json:"status"` // 代理服务状态0-离线1-在线
Edges []Edge `gorm:"foreignKey:ProxyID;references:ID" json:"edges"`
}

View File

@@ -16,10 +16,10 @@ const TableNameSession = "session"
type Session struct {
ID int32 `gorm:"column:id;type:integer;primaryKey;autoIncrement:true;comment:会话ID" json:"id"` // 会话ID
UserID *int32 `gorm:"column:user_id;type:integer;comment:用户ID" json:"user_id"` // 用户ID
AdminID *int32 `gorm:"column:admin_id;type:integer;comment:管理员ID" json:"admin_id"` // 管理员ID
ClientID *int32 `gorm:"column:client_id;type:integer;comment:客户端ID" json:"client_id"` // 客户端ID
IP *string `gorm:"column:ip;type:character varying(45);comment:IP地址" json:"ip"` // IP地址
Ua *string `gorm:"column:ua;type:character varying(255);comment:用户代理" json:"ua"` // 用户代理
GrantType string `gorm:"column:grant_type;type:character varying(255);not null;default:0;comment:授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式" json:"grant_type"` // 授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式
UA *string `gorm:"column:ua;type:character varying(255);comment:用户代理" json:"ua"` // 用户代理
AccessToken string `gorm:"column:access_token;type:character varying(255);not null;comment:访问令牌" json:"access_token"` // 访问令牌
AccessTokenExpires orm.LocalDateTime `gorm:"column:access_token_expires;type:timestamp without time zone;not null;comment:访问令牌过期时间" json:"access_token_expires"` // 访问令牌过期时间
RefreshToken *string `gorm:"column:refresh_token;type:character varying(255);comment:刷新令牌" json:"refresh_token"` // 刷新令牌
@@ -28,6 +28,9 @@ type Session struct {
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
User *User `gorm:"foreignKey:UserID" json:"user"`
Admin *Admin `gorm:"foreignKey:UserID" json:"admin"`
Client *Client `gorm:"belongsTo:ID;foreignKey:ClientID" json:"client"`
}
// TableName Session's table name

View File

@@ -23,18 +23,18 @@ type Trade struct {
Subject string `gorm:"column:subject;type:character varying(255);not null;comment:订单主题" json:"subject"` // 订单主题
Remark *string `gorm:"column:remark;type:character varying(255);comment:订单备注" json:"remark"` // 订单备注
Amount decimal.Decimal `gorm:"column:amount;type:numeric(12,2);not null;comment:订单总金额" json:"amount"` // 订单总金额
Payment decimal.Decimal `gorm:"column:payment;type:numeric(12,2);not null;comment:支付金额" json:"payment"` // 支付金额
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式1-支付宝2-微信3-商福通渠道支付宝,4-商福通渠道微信" json:"method"` // 支付方式1-支付宝2-微信3-商福通渠道支付宝,4-商福通渠道微信
Status int32 `gorm:"column:status;type:integer;not null;comment:订单状态0-待支付1-已支付2-已取消" json:"status"` // 订单状态0-待支付1-已支付2-已取消
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
Acquirer *int32 `gorm:"column:acquirer;type:integer;comment:收单机构1-支付宝2-微信3-银联" json:"acquirer"` // 收单机构1-支付宝2-微信3-银联
Payment decimal.Decimal `gorm:"column:payment;type:numeric(12,2);not null;comment:实际支付金额" json:"payment"` // 实际支付金额
Method int32 `gorm:"column:method;type:integer;not null;comment:支付方式1-支付宝2-微信3-商福通4-商福通渠道支付宝,5-商福通渠道微信" json:"method"` // 支付方式1-支付宝2-微信3-商福通4-商福通渠道支付宝,5-商福通渠道微信
Platform int32 `gorm:"column:platform;type:integer;not null;comment:支付平台1-电脑网站2-手机网站" json:"platform"` // 支付平台1-电脑网站2-手机网站
Acquirer *int32 `gorm:"column:acquirer;type:integer;comment:收单机构1-支付宝2-微信3-银联" json:"acquirer"` // 收单机构1-支付宝2-微信3-银联
Status int32 `gorm:"column:status;type:integer;not null;comment:订单状态0-待支付1-已支付2-已取消" json:"status"` // 订单状态0-待支付1-已支付2-已取消
Refunded bool `gorm:"column:refunded;type:boolean;not null" json:"refunded"`
PaymentURL *string `gorm:"column:payment_url;type:text;comment:支付链接" json:"payment_url"` // 支付链接
CompletedAt *orm.LocalDateTime `gorm:"column:completed_at;type:timestamp without time zone;comment:支付时间" json:"completed_at"` // 支付时间
CanceledAt *orm.LocalDateTime `gorm:"column:canceled_at;type:timestamp without time zone;comment:取消时间" json:"canceled_at"` // 取消时间
Refunded bool `gorm:"column:refunded;type:boolean;not null" json:"refunded"`
CreatedAt *orm.LocalDateTime `gorm:"column:created_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt *orm.LocalDateTime `gorm:"column:updated_at;type:timestamp without time zone;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp without time zone;comment:删除时间" json:"deleted_at"` // 删除时间
}
// TableName Trade's table name

View File

@@ -30,10 +30,14 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
_channel.ID = field.NewInt32(tableName, "id")
_channel.UserID = field.NewInt32(tableName, "user_id")
_channel.ProxyID = field.NewInt32(tableName, "proxy_id")
_channel.EdgeID = field.NewInt32(tableName, "edge_id")
_channel.ResourceID = field.NewInt32(tableName, "resource_id")
_channel.ProxyHost = field.NewString(tableName, "proxy_host")
_channel.ProxyPort = field.NewInt32(tableName, "proxy_port")
_channel.EdgeHost = field.NewString(tableName, "edge_host")
_channel.Protocol = field.NewInt32(tableName, "protocol")
_channel.AuthIP = field.NewBool(tableName, "auth_ip")
_channel.Whitelists = field.NewString(tableName, "whitelists")
_channel.AuthPass = field.NewBool(tableName, "auth_pass")
_channel.Username = field.NewString(tableName, "username")
_channel.Password = field.NewString(tableName, "password")
@@ -41,10 +45,6 @@ func newChannel(db *gorm.DB, opts ...gen.DOOption) channel {
_channel.CreatedAt = field.NewField(tableName, "created_at")
_channel.UpdatedAt = field.NewField(tableName, "updated_at")
_channel.DeletedAt = field.NewField(tableName, "deleted_at")
_channel.EdgeHost = field.NewString(tableName, "edge_host")
_channel.EdgeID = field.NewInt32(tableName, "edge_id")
_channel.Whitelists = field.NewString(tableName, "whitelists")
_channel.ResourceID = field.NewInt32(tableName, "resource_id")
_channel.fillFieldMap()
@@ -58,10 +58,14 @@ type channel struct {
ID field.Int32 // 通道ID
UserID field.Int32 // 用户ID
ProxyID field.Int32 // 代理ID
EdgeID field.Int32 // 节点ID
ResourceID field.Int32 // 套餐ID
ProxyHost field.String // 代理地址
ProxyPort field.Int32 // 转发端口
EdgeHost field.String // 节点地址
Protocol field.Int32 // 协议类型1-http2-https3-socks5
AuthIP field.Bool // IP认证
Whitelists field.String // IP白名单逗号分隔
AuthPass field.Bool // 密码认证
Username field.String // 用户名
Password field.String // 密码
@@ -69,10 +73,6 @@ type channel struct {
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
EdgeHost field.String // 节点地址
EdgeID field.Int32 // 节点ID
Whitelists field.String // IP白名单逗号分隔
ResourceID field.Int32 // 套餐ID
fieldMap map[string]field.Expr
}
@@ -92,10 +92,14 @@ func (c *channel) updateTableName(table string) *channel {
c.ID = field.NewInt32(table, "id")
c.UserID = field.NewInt32(table, "user_id")
c.ProxyID = field.NewInt32(table, "proxy_id")
c.EdgeID = field.NewInt32(table, "edge_id")
c.ResourceID = field.NewInt32(table, "resource_id")
c.ProxyHost = field.NewString(table, "proxy_host")
c.ProxyPort = field.NewInt32(table, "proxy_port")
c.EdgeHost = field.NewString(table, "edge_host")
c.Protocol = field.NewInt32(table, "protocol")
c.AuthIP = field.NewBool(table, "auth_ip")
c.Whitelists = field.NewString(table, "whitelists")
c.AuthPass = field.NewBool(table, "auth_pass")
c.Username = field.NewString(table, "username")
c.Password = field.NewString(table, "password")
@@ -103,10 +107,6 @@ func (c *channel) updateTableName(table string) *channel {
c.CreatedAt = field.NewField(table, "created_at")
c.UpdatedAt = field.NewField(table, "updated_at")
c.DeletedAt = field.NewField(table, "deleted_at")
c.EdgeHost = field.NewString(table, "edge_host")
c.EdgeID = field.NewInt32(table, "edge_id")
c.Whitelists = field.NewString(table, "whitelists")
c.ResourceID = field.NewInt32(table, "resource_id")
c.fillFieldMap()
@@ -127,10 +127,14 @@ func (c *channel) fillFieldMap() {
c.fieldMap["id"] = c.ID
c.fieldMap["user_id"] = c.UserID
c.fieldMap["proxy_id"] = c.ProxyID
c.fieldMap["edge_id"] = c.EdgeID
c.fieldMap["resource_id"] = c.ResourceID
c.fieldMap["proxy_host"] = c.ProxyHost
c.fieldMap["proxy_port"] = c.ProxyPort
c.fieldMap["edge_host"] = c.EdgeHost
c.fieldMap["protocol"] = c.Protocol
c.fieldMap["auth_ip"] = c.AuthIP
c.fieldMap["whitelists"] = c.Whitelists
c.fieldMap["auth_pass"] = c.AuthPass
c.fieldMap["username"] = c.Username
c.fieldMap["password"] = c.Password
@@ -138,10 +142,6 @@ func (c *channel) fillFieldMap() {
c.fieldMap["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["deleted_at"] = c.DeletedAt
c.fieldMap["edge_host"] = c.EdgeHost
c.fieldMap["edge_id"] = c.EdgeID
c.fieldMap["whitelists"] = c.Whitelists
c.fieldMap["resource_id"] = c.ResourceID
}
func (c channel) clone(db *gorm.DB) channel {

View File

@@ -31,14 +31,11 @@ func newClient(db *gorm.DB, opts ...gen.DOOption) client {
_client.ClientID = field.NewString(tableName, "client_id")
_client.ClientSecret = field.NewString(tableName, "client_secret")
_client.RedirectURI = field.NewString(tableName, "redirect_uri")
_client.GrantCode = field.NewBool(tableName, "grant_code")
_client.GrantClient = field.NewBool(tableName, "grant_client")
_client.GrantRefresh = field.NewBool(tableName, "grant_refresh")
_client.GrantPassword = field.NewBool(tableName, "grant_password")
_client.Spec = field.NewInt32(tableName, "spec")
_client.Name = field.NewString(tableName, "name")
_client.Icon = field.NewString(tableName, "icon")
_client.Status = field.NewInt32(tableName, "status")
_client.Type = field.NewInt32(tableName, "type")
_client.CreatedAt = field.NewField(tableName, "created_at")
_client.UpdatedAt = field.NewField(tableName, "updated_at")
_client.DeletedAt = field.NewField(tableName, "deleted_at")
@@ -56,14 +53,11 @@ type client struct {
ClientID field.String // OAuth2客户端标识符
ClientSecret field.String // OAuth2客户端密钥
RedirectURI field.String // OAuth2 重定向URI
GrantCode field.Bool // 允许授权码授予
GrantClient field.Bool // 允许客户端凭证授予
GrantRefresh field.Bool // 允许刷新令牌授予
GrantPassword field.Bool // 允许密码授予
Spec field.Int32 // 安全规范1-native2-browser3-web4-trusted
Spec field.Int32 // 安全规范1-native2-browser3-web4-api
Name field.String // 名称
Icon field.String // 图标URL
Status field.Int32 // 状态0-禁用1-正常
Type field.Int32 // 类型0-普通1-官方
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
@@ -87,14 +81,11 @@ func (c *client) updateTableName(table string) *client {
c.ClientID = field.NewString(table, "client_id")
c.ClientSecret = field.NewString(table, "client_secret")
c.RedirectURI = field.NewString(table, "redirect_uri")
c.GrantCode = field.NewBool(table, "grant_code")
c.GrantClient = field.NewBool(table, "grant_client")
c.GrantRefresh = field.NewBool(table, "grant_refresh")
c.GrantPassword = field.NewBool(table, "grant_password")
c.Spec = field.NewInt32(table, "spec")
c.Name = field.NewString(table, "name")
c.Icon = field.NewString(table, "icon")
c.Status = field.NewInt32(table, "status")
c.Type = field.NewInt32(table, "type")
c.CreatedAt = field.NewField(table, "created_at")
c.UpdatedAt = field.NewField(table, "updated_at")
c.DeletedAt = field.NewField(table, "deleted_at")
@@ -114,19 +105,16 @@ func (c *client) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (c *client) fillFieldMap() {
c.fieldMap = make(map[string]field.Expr, 15)
c.fieldMap = make(map[string]field.Expr, 12)
c.fieldMap["id"] = c.ID
c.fieldMap["client_id"] = c.ClientID
c.fieldMap["client_secret"] = c.ClientSecret
c.fieldMap["redirect_uri"] = c.RedirectURI
c.fieldMap["grant_code"] = c.GrantCode
c.fieldMap["grant_client"] = c.GrantClient
c.fieldMap["grant_refresh"] = c.GrantRefresh
c.fieldMap["grant_password"] = c.GrantPassword
c.fieldMap["spec"] = c.Spec
c.fieldMap["name"] = c.Name
c.fieldMap["icon"] = c.Icon
c.fieldMap["status"] = c.Status
c.fieldMap["type"] = c.Type
c.fieldMap["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt
c.fieldMap["deleted_at"] = c.DeletedAt

View File

@@ -29,12 +29,12 @@ func newLogsLogin(db *gorm.DB, opts ...gen.DOOption) logsLogin {
_logsLogin.ALL = field.NewAsterisk(tableName)
_logsLogin.ID = field.NewInt32(tableName, "id")
_logsLogin.IP = field.NewString(tableName, "ip")
_logsLogin.Ua = field.NewString(tableName, "ua")
_logsLogin.UA = field.NewString(tableName, "ua")
_logsLogin.GrantType = field.NewString(tableName, "grant_type")
_logsLogin.PasswordGrantType = field.NewString(tableName, "password_grant_type")
_logsLogin.Success = field.NewBool(tableName, "success")
_logsLogin.Time = field.NewField(tableName, "time")
_logsLogin.UserID = field.NewInt32(tableName, "user_id")
_logsLogin.Time = field.NewField(tableName, "time")
_logsLogin.fillFieldMap()
@@ -47,12 +47,12 @@ type logsLogin struct {
ALL field.Asterisk
ID field.Int32 // 登录日志ID
IP field.String // IP地址
Ua field.String // 用户代理
UA field.String // 用户代理
GrantType field.String // 授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式
PasswordGrantType field.String // 密码模式子授权类型password-账号密码phone_code-手机验证码email_code-邮箱验证码
Success field.Bool // 登录是否成功
Time field.Field // 登录时间
UserID field.Int32 // 用户ID
Time field.Field // 登录时间
fieldMap map[string]field.Expr
}
@@ -71,12 +71,12 @@ func (l *logsLogin) updateTableName(table string) *logsLogin {
l.ALL = field.NewAsterisk(table)
l.ID = field.NewInt32(table, "id")
l.IP = field.NewString(table, "ip")
l.Ua = field.NewString(table, "ua")
l.UA = field.NewString(table, "ua")
l.GrantType = field.NewString(table, "grant_type")
l.PasswordGrantType = field.NewString(table, "password_grant_type")
l.Success = field.NewBool(table, "success")
l.Time = field.NewField(table, "time")
l.UserID = field.NewInt32(table, "user_id")
l.Time = field.NewField(table, "time")
l.fillFieldMap()
@@ -96,12 +96,12 @@ func (l *logsLogin) fillFieldMap() {
l.fieldMap = make(map[string]field.Expr, 8)
l.fieldMap["id"] = l.ID
l.fieldMap["ip"] = l.IP
l.fieldMap["ua"] = l.Ua
l.fieldMap["ua"] = l.UA
l.fieldMap["grant_type"] = l.GrantType
l.fieldMap["password_grant_type"] = l.PasswordGrantType
l.fieldMap["success"] = l.Success
l.fieldMap["time"] = l.Time
l.fieldMap["user_id"] = l.UserID
l.fieldMap["time"] = l.Time
}
func (l logsLogin) clone(db *gorm.DB) logsLogin {

View File

@@ -28,16 +28,16 @@ func newLogsRequest(db *gorm.DB, opts ...gen.DOOption) logsRequest {
tableName := _logsRequest.logsRequestDo.TableName()
_logsRequest.ALL = field.NewAsterisk(tableName)
_logsRequest.ID = field.NewInt32(tableName, "id")
_logsRequest.Identity = field.NewInt32(tableName, "identity")
_logsRequest.Visitor = field.NewInt32(tableName, "visitor")
_logsRequest.IP = field.NewString(tableName, "ip")
_logsRequest.Ua = field.NewString(tableName, "ua")
_logsRequest.UA = field.NewString(tableName, "ua")
_logsRequest.UserID = field.NewInt32(tableName, "user_id")
_logsRequest.ClientID = field.NewInt32(tableName, "client_id")
_logsRequest.Method = field.NewString(tableName, "method")
_logsRequest.Path = field.NewString(tableName, "path")
_logsRequest.Latency = field.NewString(tableName, "latency")
_logsRequest.Status = field.NewInt32(tableName, "status")
_logsRequest.Error = field.NewString(tableName, "error")
_logsRequest.Time = field.NewField(tableName, "time")
_logsRequest.Latency = field.NewString(tableName, "latency")
_logsRequest.fillFieldMap()
@@ -49,16 +49,16 @@ type logsRequest struct {
ALL field.Asterisk
ID field.Int32 // 访问日志ID
Identity field.Int32 // 访客身份0-游客1-用户2-管理员3-公共服务4-安全服务5-内部服务
Visitor field.Int32 // 访客ID
IP field.String // IP地址
Ua field.String // 用户代理
UA field.String // 用户代理
UserID field.Int32 // 用户ID
ClientID field.Int32 // 客户端ID
Method field.String // 请求方法
Path field.String // 请求路径
Latency field.String // 请求延迟
Status field.Int32 // 响应状态码
Error field.String // 错误信息
Time field.Field // 请求时间
Latency field.String // 请求延迟
fieldMap map[string]field.Expr
}
@@ -76,16 +76,16 @@ func (l logsRequest) As(alias string) *logsRequest {
func (l *logsRequest) updateTableName(table string) *logsRequest {
l.ALL = field.NewAsterisk(table)
l.ID = field.NewInt32(table, "id")
l.Identity = field.NewInt32(table, "identity")
l.Visitor = field.NewInt32(table, "visitor")
l.IP = field.NewString(table, "ip")
l.Ua = field.NewString(table, "ua")
l.UA = field.NewString(table, "ua")
l.UserID = field.NewInt32(table, "user_id")
l.ClientID = field.NewInt32(table, "client_id")
l.Method = field.NewString(table, "method")
l.Path = field.NewString(table, "path")
l.Latency = field.NewString(table, "latency")
l.Status = field.NewInt32(table, "status")
l.Error = field.NewString(table, "error")
l.Time = field.NewField(table, "time")
l.Latency = field.NewString(table, "latency")
l.fillFieldMap()
@@ -104,16 +104,16 @@ func (l *logsRequest) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
func (l *logsRequest) fillFieldMap() {
l.fieldMap = make(map[string]field.Expr, 11)
l.fieldMap["id"] = l.ID
l.fieldMap["identity"] = l.Identity
l.fieldMap["visitor"] = l.Visitor
l.fieldMap["ip"] = l.IP
l.fieldMap["ua"] = l.Ua
l.fieldMap["ua"] = l.UA
l.fieldMap["user_id"] = l.UserID
l.fieldMap["client_id"] = l.ClientID
l.fieldMap["method"] = l.Method
l.fieldMap["path"] = l.Path
l.fieldMap["latency"] = l.Latency
l.fieldMap["status"] = l.Status
l.fieldMap["error"] = l.Error
l.fieldMap["time"] = l.Time
l.fieldMap["latency"] = l.Latency
}
func (l logsRequest) clone(db *gorm.DB) logsRequest {

View File

@@ -31,12 +31,12 @@ func newProxy(db *gorm.DB, opts ...gen.DOOption) proxy {
_proxy.Version = field.NewInt32(tableName, "version")
_proxy.Name = field.NewString(tableName, "name")
_proxy.Host = field.NewString(tableName, "host")
_proxy.Type = field.NewInt32(tableName, "type")
_proxy.Secret = field.NewString(tableName, "secret")
_proxy.Type = field.NewInt32(tableName, "type")
_proxy.Status = field.NewInt32(tableName, "status")
_proxy.CreatedAt = field.NewField(tableName, "created_at")
_proxy.UpdatedAt = field.NewField(tableName, "updated_at")
_proxy.DeletedAt = field.NewField(tableName, "deleted_at")
_proxy.Status = field.NewInt32(tableName, "status")
_proxy.Edges = proxyHasManyEdges{
db: db.Session(&gorm.Session{}),
@@ -56,12 +56,12 @@ type proxy struct {
Version field.Int32 // 代理服务版本
Name field.String // 代理服务名称
Host field.String // 代理服务地址
Type field.Int32 // 代理服务类型1-三方2-自有
Secret field.String // 代理服务密钥
Type field.Int32 // 代理服务类型1-三方2-自有
Status field.Int32 // 代理服务状态0-离线1-在线
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
Status field.Int32 // 代理服务状态0-离线1-在线
Edges proxyHasManyEdges
fieldMap map[string]field.Expr
@@ -83,12 +83,12 @@ func (p *proxy) updateTableName(table string) *proxy {
p.Version = field.NewInt32(table, "version")
p.Name = field.NewString(table, "name")
p.Host = field.NewString(table, "host")
p.Type = field.NewInt32(table, "type")
p.Secret = field.NewString(table, "secret")
p.Type = field.NewInt32(table, "type")
p.Status = field.NewInt32(table, "status")
p.CreatedAt = field.NewField(table, "created_at")
p.UpdatedAt = field.NewField(table, "updated_at")
p.DeletedAt = field.NewField(table, "deleted_at")
p.Status = field.NewInt32(table, "status")
p.fillFieldMap()
@@ -110,12 +110,12 @@ func (p *proxy) fillFieldMap() {
p.fieldMap["version"] = p.Version
p.fieldMap["name"] = p.Name
p.fieldMap["host"] = p.Host
p.fieldMap["type"] = p.Type
p.fieldMap["secret"] = p.Secret
p.fieldMap["type"] = p.Type
p.fieldMap["status"] = p.Status
p.fieldMap["created_at"] = p.CreatedAt
p.fieldMap["updated_at"] = p.UpdatedAt
p.fieldMap["deleted_at"] = p.DeletedAt
p.fieldMap["status"] = p.Status
}

View File

@@ -29,10 +29,10 @@ func newSession(db *gorm.DB, opts ...gen.DOOption) session {
_session.ALL = field.NewAsterisk(tableName)
_session.ID = field.NewInt32(tableName, "id")
_session.UserID = field.NewInt32(tableName, "user_id")
_session.AdminID = field.NewInt32(tableName, "admin_id")
_session.ClientID = field.NewInt32(tableName, "client_id")
_session.IP = field.NewString(tableName, "ip")
_session.Ua = field.NewString(tableName, "ua")
_session.GrantType = field.NewString(tableName, "grant_type")
_session.UA = field.NewString(tableName, "ua")
_session.AccessToken = field.NewString(tableName, "access_token")
_session.AccessTokenExpires = field.NewField(tableName, "access_token_expires")
_session.RefreshToken = field.NewString(tableName, "refresh_token")
@@ -41,6 +41,23 @@ func newSession(db *gorm.DB, opts ...gen.DOOption) session {
_session.CreatedAt = field.NewField(tableName, "created_at")
_session.UpdatedAt = field.NewField(tableName, "updated_at")
_session.DeletedAt = field.NewField(tableName, "deleted_at")
_session.User = sessionBelongsToUser{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("User", "models.User"),
}
_session.Admin = sessionBelongsToAdmin{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Admin", "models.Admin"),
}
_session.Client = sessionBelongsToClient{
db: db.Session(&gorm.Session{}),
RelationField: field.NewRelation("Client", "models.Client"),
}
_session.fillFieldMap()
@@ -53,10 +70,10 @@ type session struct {
ALL field.Asterisk
ID field.Int32 // 会话ID
UserID field.Int32 // 用户ID
AdminID field.Int32 // 管理员ID
ClientID field.Int32 // 客户端ID
IP field.String // IP地址
Ua field.String // 用户代理
GrantType field.String // 授权类型authorization_code-授权码模式client_credentials-客户端凭证模式refresh_token-刷新令牌模式password-密码模式
UA field.String // 用户代理
AccessToken field.String // 访问令牌
AccessTokenExpires field.Field // 访问令牌过期时间
RefreshToken field.String // 刷新令牌
@@ -65,6 +82,11 @@ type session struct {
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
User sessionBelongsToUser
Admin sessionBelongsToAdmin
Client sessionBelongsToClient
fieldMap map[string]field.Expr
}
@@ -83,10 +105,10 @@ func (s *session) updateTableName(table string) *session {
s.ALL = field.NewAsterisk(table)
s.ID = field.NewInt32(table, "id")
s.UserID = field.NewInt32(table, "user_id")
s.AdminID = field.NewInt32(table, "admin_id")
s.ClientID = field.NewInt32(table, "client_id")
s.IP = field.NewString(table, "ip")
s.Ua = field.NewString(table, "ua")
s.GrantType = field.NewString(table, "grant_type")
s.UA = field.NewString(table, "ua")
s.AccessToken = field.NewString(table, "access_token")
s.AccessTokenExpires = field.NewField(table, "access_token_expires")
s.RefreshToken = field.NewString(table, "refresh_token")
@@ -111,13 +133,13 @@ func (s *session) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (s *session) fillFieldMap() {
s.fieldMap = make(map[string]field.Expr, 14)
s.fieldMap = make(map[string]field.Expr, 17)
s.fieldMap["id"] = s.ID
s.fieldMap["user_id"] = s.UserID
s.fieldMap["admin_id"] = s.AdminID
s.fieldMap["client_id"] = s.ClientID
s.fieldMap["ip"] = s.IP
s.fieldMap["ua"] = s.Ua
s.fieldMap["grant_type"] = s.GrantType
s.fieldMap["ua"] = s.UA
s.fieldMap["access_token"] = s.AccessToken
s.fieldMap["access_token_expires"] = s.AccessTokenExpires
s.fieldMap["refresh_token"] = s.RefreshToken
@@ -126,18 +148,271 @@ func (s *session) fillFieldMap() {
s.fieldMap["created_at"] = s.CreatedAt
s.fieldMap["updated_at"] = s.UpdatedAt
s.fieldMap["deleted_at"] = s.DeletedAt
}
func (s session) clone(db *gorm.DB) session {
s.sessionDo.ReplaceConnPool(db.Statement.ConnPool)
s.User.db = db.Session(&gorm.Session{Initialized: true})
s.User.db.Statement.ConnPool = db.Statement.ConnPool
s.Admin.db = db.Session(&gorm.Session{Initialized: true})
s.Admin.db.Statement.ConnPool = db.Statement.ConnPool
s.Client.db = db.Session(&gorm.Session{Initialized: true})
s.Client.db.Statement.ConnPool = db.Statement.ConnPool
return s
}
func (s session) replaceDB(db *gorm.DB) session {
s.sessionDo.ReplaceDB(db)
s.User.db = db.Session(&gorm.Session{})
s.Admin.db = db.Session(&gorm.Session{})
s.Client.db = db.Session(&gorm.Session{})
return s
}
type sessionBelongsToUser struct {
db *gorm.DB
field.RelationField
}
func (a sessionBelongsToUser) Where(conds ...field.Expr) *sessionBelongsToUser {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a sessionBelongsToUser) WithContext(ctx context.Context) *sessionBelongsToUser {
a.db = a.db.WithContext(ctx)
return &a
}
func (a sessionBelongsToUser) Session(session *gorm.Session) *sessionBelongsToUser {
a.db = a.db.Session(session)
return &a
}
func (a sessionBelongsToUser) Model(m *models.Session) *sessionBelongsToUserTx {
return &sessionBelongsToUserTx{a.db.Model(m).Association(a.Name())}
}
func (a sessionBelongsToUser) Unscoped() *sessionBelongsToUser {
a.db = a.db.Unscoped()
return &a
}
type sessionBelongsToUserTx struct{ tx *gorm.Association }
func (a sessionBelongsToUserTx) Find() (result *models.User, err error) {
return result, a.tx.Find(&result)
}
func (a sessionBelongsToUserTx) Append(values ...*models.User) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a sessionBelongsToUserTx) Replace(values ...*models.User) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a sessionBelongsToUserTx) Delete(values ...*models.User) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a sessionBelongsToUserTx) Clear() error {
return a.tx.Clear()
}
func (a sessionBelongsToUserTx) Count() int64 {
return a.tx.Count()
}
func (a sessionBelongsToUserTx) Unscoped() *sessionBelongsToUserTx {
a.tx = a.tx.Unscoped()
return &a
}
type sessionBelongsToAdmin struct {
db *gorm.DB
field.RelationField
}
func (a sessionBelongsToAdmin) Where(conds ...field.Expr) *sessionBelongsToAdmin {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a sessionBelongsToAdmin) WithContext(ctx context.Context) *sessionBelongsToAdmin {
a.db = a.db.WithContext(ctx)
return &a
}
func (a sessionBelongsToAdmin) Session(session *gorm.Session) *sessionBelongsToAdmin {
a.db = a.db.Session(session)
return &a
}
func (a sessionBelongsToAdmin) Model(m *models.Session) *sessionBelongsToAdminTx {
return &sessionBelongsToAdminTx{a.db.Model(m).Association(a.Name())}
}
func (a sessionBelongsToAdmin) Unscoped() *sessionBelongsToAdmin {
a.db = a.db.Unscoped()
return &a
}
type sessionBelongsToAdminTx struct{ tx *gorm.Association }
func (a sessionBelongsToAdminTx) Find() (result *models.Admin, err error) {
return result, a.tx.Find(&result)
}
func (a sessionBelongsToAdminTx) Append(values ...*models.Admin) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a sessionBelongsToAdminTx) Replace(values ...*models.Admin) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a sessionBelongsToAdminTx) Delete(values ...*models.Admin) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a sessionBelongsToAdminTx) Clear() error {
return a.tx.Clear()
}
func (a sessionBelongsToAdminTx) Count() int64 {
return a.tx.Count()
}
func (a sessionBelongsToAdminTx) Unscoped() *sessionBelongsToAdminTx {
a.tx = a.tx.Unscoped()
return &a
}
type sessionBelongsToClient struct {
db *gorm.DB
field.RelationField
}
func (a sessionBelongsToClient) Where(conds ...field.Expr) *sessionBelongsToClient {
if len(conds) == 0 {
return &a
}
exprs := make([]clause.Expression, 0, len(conds))
for _, cond := range conds {
exprs = append(exprs, cond.BeCond().(clause.Expression))
}
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
return &a
}
func (a sessionBelongsToClient) WithContext(ctx context.Context) *sessionBelongsToClient {
a.db = a.db.WithContext(ctx)
return &a
}
func (a sessionBelongsToClient) Session(session *gorm.Session) *sessionBelongsToClient {
a.db = a.db.Session(session)
return &a
}
func (a sessionBelongsToClient) Model(m *models.Session) *sessionBelongsToClientTx {
return &sessionBelongsToClientTx{a.db.Model(m).Association(a.Name())}
}
func (a sessionBelongsToClient) Unscoped() *sessionBelongsToClient {
a.db = a.db.Unscoped()
return &a
}
type sessionBelongsToClientTx struct{ tx *gorm.Association }
func (a sessionBelongsToClientTx) Find() (result *models.Client, err error) {
return result, a.tx.Find(&result)
}
func (a sessionBelongsToClientTx) Append(values ...*models.Client) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Append(targetValues...)
}
func (a sessionBelongsToClientTx) Replace(values ...*models.Client) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Replace(targetValues...)
}
func (a sessionBelongsToClientTx) Delete(values ...*models.Client) (err error) {
targetValues := make([]interface{}, len(values))
for i, v := range values {
targetValues[i] = v
}
return a.tx.Delete(targetValues...)
}
func (a sessionBelongsToClientTx) Clear() error {
return a.tx.Clear()
}
func (a sessionBelongsToClientTx) Count() int64 {
return a.tx.Count()
}
func (a sessionBelongsToClientTx) Unscoped() *sessionBelongsToClientTx {
a.tx = a.tx.Unscoped()
return &a
}
type sessionDo struct{ gen.DO }
func (s sessionDo) Debug() *sessionDo {

View File

@@ -37,16 +37,16 @@ func newTrade(db *gorm.DB, opts ...gen.DOOption) trade {
_trade.Amount = field.NewField(tableName, "amount")
_trade.Payment = field.NewField(tableName, "payment")
_trade.Method = field.NewInt32(tableName, "method")
_trade.Status = field.NewInt32(tableName, "status")
_trade.CreatedAt = field.NewField(tableName, "created_at")
_trade.UpdatedAt = field.NewField(tableName, "updated_at")
_trade.DeletedAt = field.NewField(tableName, "deleted_at")
_trade.Acquirer = field.NewInt32(tableName, "acquirer")
_trade.Platform = field.NewInt32(tableName, "platform")
_trade.Acquirer = field.NewInt32(tableName, "acquirer")
_trade.Status = field.NewInt32(tableName, "status")
_trade.Refunded = field.NewBool(tableName, "refunded")
_trade.PaymentURL = field.NewString(tableName, "payment_url")
_trade.CompletedAt = field.NewField(tableName, "completed_at")
_trade.CanceledAt = field.NewField(tableName, "canceled_at")
_trade.Refunded = field.NewBool(tableName, "refunded")
_trade.CreatedAt = field.NewField(tableName, "created_at")
_trade.UpdatedAt = field.NewField(tableName, "updated_at")
_trade.DeletedAt = field.NewField(tableName, "deleted_at")
_trade.fillFieldMap()
@@ -65,18 +65,18 @@ type trade struct {
Subject field.String // 订单主题
Remark field.String // 订单备注
Amount field.Field // 订单总金额
Payment field.Field // 支付金额
Method field.Int32 // 支付方式1-支付宝2-微信3-商福通渠道支付宝,4-商福通渠道微信
Status field.Int32 // 订单状态0-待支付1-已支付2-已取消
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
Acquirer field.Int32 // 收单机构1-支付宝2-微信3-银联
Payment field.Field // 实际支付金额
Method field.Int32 // 支付方式1-支付宝2-微信3-商福通4-商福通渠道支付宝,5-商福通渠道微信
Platform field.Int32 // 支付平台1-电脑网站2-手机网站
Acquirer field.Int32 // 收单机构1-支付宝2-微信3-银联
Status field.Int32 // 订单状态0-待支付1-已支付2-已取消
Refunded field.Bool
PaymentURL field.String // 支付链接
CompletedAt field.Field // 支付时间
CanceledAt field.Field // 取消时间
Refunded field.Bool
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
fieldMap map[string]field.Expr
}
@@ -103,16 +103,16 @@ func (t *trade) updateTableName(table string) *trade {
t.Amount = field.NewField(table, "amount")
t.Payment = field.NewField(table, "payment")
t.Method = field.NewInt32(table, "method")
t.Status = field.NewInt32(table, "status")
t.CreatedAt = field.NewField(table, "created_at")
t.UpdatedAt = field.NewField(table, "updated_at")
t.DeletedAt = field.NewField(table, "deleted_at")
t.Acquirer = field.NewInt32(table, "acquirer")
t.Platform = field.NewInt32(table, "platform")
t.Acquirer = field.NewInt32(table, "acquirer")
t.Status = field.NewInt32(table, "status")
t.Refunded = field.NewBool(table, "refunded")
t.PaymentURL = field.NewString(table, "payment_url")
t.CompletedAt = field.NewField(table, "completed_at")
t.CanceledAt = field.NewField(table, "canceled_at")
t.Refunded = field.NewBool(table, "refunded")
t.CreatedAt = field.NewField(table, "created_at")
t.UpdatedAt = field.NewField(table, "updated_at")
t.DeletedAt = field.NewField(table, "deleted_at")
t.fillFieldMap()
@@ -140,16 +140,16 @@ func (t *trade) fillFieldMap() {
t.fieldMap["amount"] = t.Amount
t.fieldMap["payment"] = t.Payment
t.fieldMap["method"] = t.Method
t.fieldMap["status"] = t.Status
t.fieldMap["created_at"] = t.CreatedAt
t.fieldMap["updated_at"] = t.UpdatedAt
t.fieldMap["deleted_at"] = t.DeletedAt
t.fieldMap["acquirer"] = t.Acquirer
t.fieldMap["platform"] = t.Platform
t.fieldMap["acquirer"] = t.Acquirer
t.fieldMap["status"] = t.Status
t.fieldMap["refunded"] = t.Refunded
t.fieldMap["payment_url"] = t.PaymentURL
t.fieldMap["completed_at"] = t.CompletedAt
t.fieldMap["canceled_at"] = t.CanceledAt
t.fieldMap["refunded"] = t.Refunded
t.fieldMap["created_at"] = t.CreatedAt
t.fieldMap["updated_at"] = t.UpdatedAt
t.fieldMap["deleted_at"] = t.DeletedAt
}
func (t trade) clone(db *gorm.DB) trade {

View File

@@ -1,7 +1,7 @@
package web
import (
"platform/web/core"
auth2 "platform/web/auth"
"platform/web/handlers"
"github.com/gofiber/fiber/v2"
@@ -12,7 +12,7 @@ func ApplyRouters(app *fiber.App) {
// 认证
auth := api.Group("/auth")
auth.Post("/token", handlers.Token)
auth.Post("/token", auth2.Token)
auth.Post("/revoke", handlers.Revoke)
auth.Post("/introspect", handlers.Introspect)
auth.Post("/verify/sms", handlers.SmsCode)
@@ -47,7 +47,6 @@ func ApplyRouters(app *fiber.App) {
channel.Post("/list", handlers.ListChannels)
channel.Post("/create", handlers.CreateChannel)
channel.Post("/remove", handlers.RemoveChannels)
channel.Post("/remove/by-task", handlers.RemoveChannelByTask)
// 交易
trade := api.Group("/trade")
@@ -75,12 +74,6 @@ func ApplyRouters(app *fiber.App) {
edge.Post("/all", handlers.AllEdgesAvailable)
// 临时
app.Get("/test", func(c *fiber.Ctx) error {
return core.NewBizErr("测试错误")
})
// 异步任务客户端
tasks := api.Group("/tasks")
tasks.Post("/channel/remove", handlers.RemoveChannelByTask)
tasks.Post("/trade/cancel", handlers.TradeCancelByTask)
debug := app.Group("/debug")
debug.Get("/sms/:phone", handlers.DebugGetSmsCode)
}

View File

@@ -1,201 +0,0 @@
package services
import (
"context"
"errors"
"golang.org/x/crypto/bcrypt"
"log/slog"
"platform/pkg/u"
auth2 "platform/web/auth"
"platform/web/core"
client2 "platform/web/domains/client"
user2 "platform/web/domains/user"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"time"
"gorm.io/gorm"
)
var Auth = &authService{}
type authService struct{}
// OauthAuthorizationCode 验证授权码
func (s *authService) OauthAuthorizationCode(ctx context.Context, client *m.Client, code, redirectURI, codeVerifier string) (*auth2.TokenDetails, error) {
return nil, errors.New("TODO")
}
// OauthClientCredentials 验证客户端凭证
func (s *authService) OauthClientCredentials(ctx context.Context, client *m.Client, scope ...string) (*auth2.TokenDetails, error) {
var clientType = auth2.PayloadTypeFromClientSpec(client2.Spec(client.Spec))
var permissions = make(map[string]struct{}, len(scope))
for _, item := range scope {
permissions[item] = struct{}{}
}
// 保存会话并返回令牌
authCtx := auth2.Context{
Permissions: permissions,
Payload: auth2.Payload{
Id: client.ID,
Type: clientType,
Name: client.Name,
},
}
token, err := auth2.CreateSession(ctx, &authCtx, false)
if err != nil {
return nil, err
}
return token, nil
}
// OauthRefreshToken 验证刷新令牌
func (s *authService) OauthRefreshToken(ctx context.Context, _ *m.Client, refreshToken string, scope ...[]string) (*auth2.TokenDetails, error) {
details, err := auth2.RefreshSession(ctx, refreshToken, true)
if err != nil {
return nil, err
}
return details, nil
}
// OauthPassword 验证密码
func (s *authService) OauthPassword(ctx context.Context, _ *m.Client, data *GrantPasswordData, ip, agent string) (*auth2.TokenDetails, error) {
var user *m.User
err := q.Q.Transaction(func(tx *q.Query) error {
switch data.LoginType {
case auth2.GrantPasswordPhone:
// 验证验证码
err := Verifier.VerifySms(ctx, data.Username, data.Password)
if err != nil {
if errors.Is(err, ErrVerifierServiceInvalid) {
return ErrOauthInvalidRequest
}
return err
}
// 查找用户
user, err =
tx.User.Where(tx.User.Phone.Eq(data.Username)).Take()
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
case auth2.GrantPasswordEmail:
return core.NewServErr("邮箱登录暂不可用")
case auth2.GrantPasswordSecret:
var err error
user, err = tx.User.
Where(tx.User.Phone.Eq(data.Username)).
Or(tx.User.Email.Eq(data.Username)).
Or(tx.User.Username.Eq(data.Username)).
Take()
if err != nil {
slog.Debug("查找用户失败", "error", err)
return core.NewBizErr("用户不存在或密码错误")
}
// 账户状态
if user2.Status(user.Status) == user2.StatusDisabled {
slog.Debug("账户状态异常", "username", data.Username, "status", user.Status)
return core.NewBizErr("用户不存在或密码错误")
}
// 验证密码
if user.Password == nil || *user.Password == "" {
slog.Debug("用户未设置密码", "username", data.Username)
return core.NewBizErr("用户不存在或密码错误")
}
if bcrypt.CompareHashAndPassword([]byte(*user.Password), []byte(data.Password)) != nil {
slog.Debug("密码验证失败", "username", data.Username)
return core.NewBizErr("用户不存在或密码错误")
}
default:
return ErrOauthInvalidRequest
}
// 如果用户不存在,初始化用户 todo 初始化默认权限信息
if user == nil {
user = &m.User{
Phone: data.Username,
Username: u.P(data.Username),
}
}
// 更新用户的登录时间
user.LastLogin = u.P(orm.LocalDateTime(time.Now()))
user.LastLoginHost = u.P(ip)
user.LastLoginAgent = u.P(agent)
if err := tx.User.Save(user); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
// 保存到会话
var name = ""
if user.Name != nil {
name = *user.Name
}
authCtx := auth2.Context{
Payload: auth2.Payload{
Id: user.ID,
Type: auth2.PayloadUser,
Name: name,
Avatar: user.Avatar,
},
}
token, err := auth2.CreateSession(ctx, &authCtx, data.Remember)
if err != nil {
return nil, err
}
return token, nil
}
type GrantCodeData struct {
Code string `json:"code" form:"code"`
RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
CodeVerifier string `json:"code_verifier" form:"code_verifier"`
}
type GrantClientData struct {
}
type GrantRefreshData struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"`
}
type GrantPasswordData struct {
LoginType auth2.PasswordGrantType `json:"login_type" form:"login_type"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Remember bool `json:"remember" form:"remember"`
}
type AuthServiceError string
func (e AuthServiceError) Error() string {
return string(e)
}
const (
ErrOauthInvalidRequest = AuthServiceError("invalid_request")
ErrOauthInvalidClient = AuthServiceError("invalid_client")
ErrOauthInvalidGrant = AuthServiceError("invalid_grant")
ErrOauthInvalidScope = AuthServiceError("invalid_scope")
ErrOauthUnauthorizedClient = AuthServiceError("unauthorized_client")
ErrOauthUnsupportedGrantType = AuthServiceError("unsupported_grant_type")
)

View File

@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"fmt"
"github.com/gofiber/fiber/v2"
"log/slog"
"math"
"math/rand/v2"
@@ -15,15 +14,17 @@ import (
edge2 "platform/web/domains/edge"
proxy2 "platform/web/domains/proxy"
resource2 "platform/web/domains/resource"
"platform/web/events"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"platform/web/tasks"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/hibiken/asynq"
"gorm.io/gen/field"
@@ -296,7 +297,7 @@ func (s *channelService) CreateChannel(
ids[i] = channels[i].ID
}
_, err = g.Asynq.Enqueue(
tasks.NewRemoveChannel(ids),
events.NewRemoveChannel(ids),
asynq.ProcessIn(duration),
)
if err != nil {

View File

@@ -12,11 +12,11 @@ import (
"platform/web/core"
coupon2 "platform/web/domains/coupon"
trade2 "platform/web/domains/trade"
"platform/web/events"
g "platform/web/globals"
"platform/web/globals/orm"
m "platform/web/models"
q "platform/web/queries"
"platform/web/tasks"
"time"
"github.com/shopspring/decimal"
@@ -240,7 +240,7 @@ func (s *tradeService) CreateTrade(uid int32, now time.Time, data *CreateTradeDa
}
// 提交异步关闭事件
_, err = g.Asynq.Enqueue(tasks.NewCancelTrade(tasks.CancelTradeData{
_, err = g.Asynq.Enqueue(events.NewCancelTrade(events.CancelTradeData{
TradeNo: tradeNo,
Method: method,
}))
@@ -417,7 +417,7 @@ func (s *tradeService) CancelTrade(tradeNo string, method trade2.Method, now tim
MchOrderNo: &tradeNo,
})
if err != nil {
slog.Debug(fmt.Sprintf("订单无需关闭%s", err.Error()))
slog.Debug(fmt.Sprintf("订单无需关闭: %s", err.Error()))
return nil
}
@@ -546,11 +546,11 @@ func (s *tradeService) CheckTrade(data *ModifyTradeData) (*CheckTradeResult, err
return nil, core.NewBizErr("订单不存在")
}
return nil, core.NewServErr(
fmt.Sprintf("微信上游接口异常code=%vmessage=%v", apiErr.Code, apiErr.Message),
fmt.Sprintf("微信上游接口异常: code=%vmessage=%v", apiErr.Code, apiErr.Message),
apiErr,
)
}
return nil, core.NewServErr(fmt.Sprintf("微信上游支付接口异常%s", err.Error()))
return nil, core.NewServErr(fmt.Sprintf("微信上游支付接口异常: %s", err.Error()))
}
// 填充返回值

View File

@@ -19,28 +19,6 @@ import (
var Verifier = &verifierService{}
type VerifierServiceError string
func (e VerifierServiceError) Error() string {
return string(e)
}
var (
ErrVerifierServiceInvalid = VerifierServiceError("验证码错误")
)
type VerifierServiceSendLimitErr int
func (e VerifierServiceSendLimitErr) Error() string {
return "发送频率过快"
}
type VerifierSmsPurpose int
const (
VerifierSmsPurposeLogin VerifierSmsPurpose = iota
)
type verifierService struct {
}
@@ -148,6 +126,43 @@ func (s *verifierService) VerifySms(ctx context.Context, phone, code string) err
return nil
}
func (s *verifierService) GetSms(ctx context.Context, phone string) (string, error) {
key := smsKey(phone, VerifierSmsPurposeLogin)
val, err := g.Redis.Get(ctx, key).Result()
if err != nil {
return "", fmt.Errorf("验证码获取失败: %w", err)
}
return val, nil
}
func smsKey(phone string, purpose VerifierSmsPurpose) string {
return fmt.Sprintf("verify:sms:%d:%s", purpose, phone)
}
// region 短信目的
type VerifierSmsPurpose int
const (
VerifierSmsPurposeLogin VerifierSmsPurpose = iota // 登录
)
// region 服务异常
type VerifierServiceError string
func (e VerifierServiceError) Error() string {
return string(e)
}
var (
ErrVerifierServiceInvalid = VerifierServiceError("验证码错误")
)
type VerifierServiceSendLimitErr int
func (e VerifierServiceSendLimitErr) Error() string {
return "发送频率过快"
}

41
web/tasks/task.go Normal file
View File

@@ -0,0 +1,41 @@
package tasks
import (
"context"
"encoding/json"
"fmt"
"platform/web/events"
s "platform/web/services"
"time"
"github.com/hibiken/asynq"
)
func HandleCancelTrade(_ context.Context, task *asynq.Task) (err error) {
data := new(events.CancelTradeData)
err = json.Unmarshal(task.Payload(), data)
if err != nil {
return fmt.Errorf("解析任务参数失败: %w", err)
}
err = s.Trade.CancelTrade(data.TradeNo, data.Method, time.Now())
if err != nil {
return fmt.Errorf("取消交易失败: %w", err)
}
return nil
}
func HandleRemoveChannel(_ context.Context, task *asynq.Task) (err error) {
data := make([]int32, 0)
err = json.Unmarshal(task.Payload(), &data)
if err != nil {
return fmt.Errorf("解析任务参数失败: %w", err)
}
err = s.Channel.RemoveChannels(data)
if err != nil {
return fmt.Errorf("删除通道失败: %w", err)
}
return nil
}

View File

@@ -1,166 +1,89 @@
package web
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/middleware/requestid"
"github.com/google/uuid"
"github.com/jxskiss/base62"
"context"
"fmt"
"log/slog"
"net/http"
_ "net/http/pprof"
"platform/web/auth"
g "platform/web/globals"
q "platform/web/queries"
"runtime"
"strings"
"time"
"platform/web/events"
base "platform/web/globals"
"platform/web/tasks"
"github.com/gofiber/fiber/v2"
"github.com/hibiken/asynq"
"golang.org/x/sync/errgroup"
)
// region web
func RunApp(pCtx context.Context) error {
g, ctx := errgroup.WithContext(pCtx)
type Config struct {
Listen string
}
type Server struct {
config *Config
fiber *fiber.App
}
func New(config *Config) (*Server, error) {
_config := config
if config == nil {
_config = &Config{}
// 初始化依赖
err := base.Init(ctx)
if err != nil {
return fmt.Errorf("初始化依赖失败: %w", err)
}
return &Server{
config: _config,
}, nil
// 运行服务
g.Go(func() error {
return RunWeb(ctx)
})
g.Go(func() error {
return RunTask(ctx)
})
return g.Wait()
}
func (s *Server) Run() error {
func RunWeb(ctx context.Context) error {
// inits
g.Init()
q.SetDefault(g.DB)
// config
s.fiber = fiber.New(fiber.Config{
fiber := fiber.New(fiber.Config{
ProxyHeader: fiber.HeaderXForwardedFor,
ErrorHandler: ErrorHandler,
})
// middlewares
s.fiber.Use(newRecover())
s.fiber.Use(newRequestId())
s.fiber.Use(newLogger())
ApplyMiddlewares(fiber)
ApplyRouters(fiber)
// routes
ApplyRouters(s.fiber)
// pprof
// 停止服务
go func() {
runtime.SetBlockProfileRate(1)
err := http.ListenAndServe(":6060", nil)
<-ctx.Done()
err := fiber.Shutdown()
if err != nil {
slog.Error("pprof 服务错误", slog.Any("err", err))
slog.Error("服务停止失败", "error", err)
}
}()
// listen
slog.Info("服务开始监听 :8080")
err := s.fiber.Listen("0.0.0.0:8080")
// 启动服务
slog.Info("web 服务开始监听 :8080")
err := fiber.Listen("0.0.0.0:8080")
if err != nil {
slog.Error("Failed to start server", slog.Any("err", err))
return fmt.Errorf("web 服务监听失败: %w", err)
}
slog.Info("服务已停止")
slog.Info("web 服务已停止")
return nil
}
func (s *Server) Stop() {
err := g.ExitRedis()
func RunTask(ctx context.Context) error {
var server = asynq.NewServerFromRedisClient(base.Redis, asynq.Config{})
var mux = asynq.NewServeMux()
mux.HandleFunc(events.RemoveChannel, tasks.HandleRemoveChannel)
mux.HandleFunc(events.CancelTrade, tasks.HandleCancelTrade)
// 停止服务
go func() {
<-ctx.Done()
server.Shutdown()
}()
// 启动服务
err := server.Run(mux)
if err != nil {
slog.Error("Failed to close Redis connection", slog.Any("err", err))
return fmt.Errorf("任务服务运行失败: %w", err)
}
err = g.ExitOrm()
if err != nil {
slog.Error("Failed to close database connection", slog.Any("err", err))
}
err = s.fiber.Shutdown()
if err != nil {
slog.Error("Failed to shutdown server", slog.Any("err", err))
}
return nil
}
// endregion
// region middlewares
func newRequestId() fiber.Handler {
return requestid.New(requestid.Config{
Generator: func() string {
binary, _ := uuid.New().MarshalBinary()
return base62.EncodeToString(binary)
},
})
}
func newLogger() fiber.Handler {
return logger.New(logger.Config{
DisableColors: true,
Format: "🚀 ${time} | ${locals:authtype} ${locals:authid} | ${method} ${path} | ${status} | ${latency} | ${error}\n",
TimeFormat: "2006-01-02 15:04:05",
TimeZone: "Asia/Shanghai",
Next: func(c *fiber.Ctx) bool {
authCtx, ok := c.Locals("auth").(*auth.Context)
if ok {
c.Locals("authtype", authCtx.Payload.Type.ToStr())
c.Locals("authid", authCtx.Payload.Id)
} else {
c.Locals("authtype", auth.PayloadNone.ToStr())
c.Locals("authid", 0)
}
return false
},
Done: func(c *fiber.Ctx, logBytes []byte) {
var logStr = strings.TrimPrefix(string(logBytes), "🚀")
var logVars = strings.Split(logStr, "|")
var reqTimeStr = strings.TrimSpace(logVars[0])
reqTime, err := time.ParseInLocation("2006-01-02 15:04:05", reqTimeStr, time.Local)
if err != nil {
slog.Error("时间解析错误", slog.Any("err", err))
return
}
var latency = strings.TrimSpace(logVars[4])
var errStr = strings.TrimSpace(logVars[5])
slog.Info("接口请求",
slog.String("identity", c.Locals("authtype").(string)),
slog.Int("visitor", c.Locals("authid").(int)),
slog.String("ip", c.IP()),
slog.String("ua", c.Get("User-Agent")),
slog.String("method", c.Method()),
slog.String("path", c.Path()),
slog.Int("status", c.Response().StatusCode()),
slog.String("error", errStr),
slog.String("latency", latency),
slog.Time("time", reqTime),
)
},
})
}
func newRecover() fiber.Handler {
return recover.New(recover.Config{
EnableStackTrace: true,
})
}
// endregion