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

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

30
.vscode/launch.json vendored
View File

@@ -1,16 +1,16 @@
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "main",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/cmd/main",
"cwd": "${workspaceFolder}",
}
]
}
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "main",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceFolder}/cmd/main",
"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=

552
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
TradeExpire = 30 * 60 // 交易过期时间,单位秒
)
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
}
RedisHost = "localhost"
RedisPort = "6379"
RedisPassword = ""
_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 = ""
)
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

@@ -5,32 +5,32 @@
-- logs_request
drop table if exists logs_request cascade;
create table logs_request (
id serial primary key,
id serial primary key,
identity int not null,
visitor int,
ip varchar(45) not null,
ua varchar(255),
ip varchar(45) not null,
ua varchar(255) not null,
user_id int,
client_id int,
method varchar(10) not null,
path varchar(255) not null,
method varchar(10) not null,
path varchar(255) not null,
status int not null,
error text,
status int not null,
error text,
time timestamp not null,
latency varchar(255) not null
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 '响应状态码';
@@ -131,8 +131,8 @@ create table admin (
last_login timestamp,
last_login_host varchar(45),
last_login_agent varchar(255),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index admin_status_index on admin (status);
@@ -161,8 +161,8 @@ create table admin_role (
id serial primary key,
name varchar(255) not null unique,
description varchar(255),
active bool default true,
sort int default 0,
active bool default true,
sort int default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -190,8 +190,8 @@ create table announcement (
pin bool not null default false,
status int not null default 1,
sort int not null default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index announcement_status_index on announcement (status);
@@ -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),
@@ -239,8 +237,8 @@ create table "user" (
last_login timestamp,
last_login_host varchar(45),
last_login_agent varchar(255),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index user_admin_id_index on "user" (admin_id);
@@ -278,8 +276,8 @@ create table user_role (
id serial primary key,
name varchar(255) not null unique,
description varchar(255),
active bool default true,
sort int default 0,
active bool default true,
sort int default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
@@ -305,21 +303,18 @@ comment on column user_role.deleted_at is '删除时间';
drop table if exists client cascade;
create table client (
id serial primary key,
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,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
id serial primary key,
client_id varchar(255) not null unique,
client_secret varchar(255) not null,
redirect_uri varchar(255),
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
);
create index client_client_id_index on client (client_id);
@@ -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,25 +347,22 @@ 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,
refresh_token_expires timestamp,
scopes varchar(255),
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
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,
@@ -614,10 +579,10 @@ create table edge (
city varchar(255) not null,
proxy_port int,
status int not null default 0,
rtt int default 0,
loss int default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
rtt int default 0,
loss int default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index edge_proxy_id_index on edge (proxy_id);
@@ -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),
@@ -699,8 +654,8 @@ create table channel (
username varchar(255),
password varchar(255),
expiration timestamp not null,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index channel_user_id_index on channel (user_id);
@@ -748,8 +703,8 @@ create table product (
description varchar(255),
sort int not null default 0,
status int not null default 1,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index product_deleted_at_index on product (deleted_at);
@@ -770,14 +725,12 @@ 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,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index resource_user_id_index on resource (user_id);
@@ -797,13 +750,11 @@ comment on column resource.created_at is '创建时间';
comment on column resource.updated_at is '更新时间';
comment on column resource.deleted_at is '删除时间';
-- resource_short
-- resource_short
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,
@@ -887,8 +834,8 @@ create table trade (
payment_url text,
completed_at timestamp,
canceled_at timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index trade_user_id_index on trade (user_id);
@@ -923,17 +870,13 @@ 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,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index refund_trade_id_index on refund (trade_id);
@@ -956,24 +899,16 @@ 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,
amount decimal(12, 2) not null default 0,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index bill_user_id_index on bill (user_id);
@@ -1003,17 +938,15 @@ 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,
min_amount decimal(12, 2) not null default 0,
status int not null default 0,
expire_at timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
created_at timestamp default current_timestamp,
updated_at timestamp default current_timestamp,
deleted_at timestamp
);
create index coupon_user_id_index on coupon (user_id);
@@ -1035,4 +968,119 @@ comment on column coupon.created_at is '创建时间';
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]
if bcrypt.CompareHashAndPassword([]byte(client.ClientSecret), []byte(clientSecret)) != nil {
return nil, errors.New("客户端密钥错误")
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 {
return nil
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
}
func authAuthorizationCode() {
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"`
}
func authClientCredential() {
type GrantClientData struct {
}
func authRefreshToken() {
type GrantRefreshData struct {
RefreshToken string `json:"refresh_token" form:"refresh_token"`
}
func authPassword() {
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"`
}
func authPasswordSecret() {
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"`
}
func authPasswordPhone() {
type TokenErrResp struct {
Error string `json:"error"`
Description string `json:"error_description,omitempty"`
}
func authPasswordEmail() {
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 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 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]
}
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

@@ -14,21 +14,18 @@ const TableNameClient = "client"
// Client mapped from table <client>
type Client struct {
ID int32 `gorm:"column:id;type:integer;primaryKey;autoIncrement:true;comment:客户端ID" json:"id"` // 客户端ID
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
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-正常
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"` // 删除时间
ID int32 `gorm:"column:id;type:integer;primaryKey;autoIncrement:true;comment:客户端ID" json:"id"` // 客户端ID
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
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"` // 删除时间
}
// TableName Client's table name

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

@@ -10,17 +10,17 @@ 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"` // 用户代理
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"` // 请求时间
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"` // 用户代理
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"` // 请求路径
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

@@ -14,20 +14,23 @@ const TableNameSession = "session"
// Session mapped from table <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
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-密码模式
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"` // 刷新令牌
RefreshTokenExpires *orm.LocalDateTime `gorm:"column:refresh_token_expires;type:timestamp without time zone;comment:刷新令牌过期时间" json:"refresh_token_expires"` // 刷新令牌过期时间
Scopes_ *string `gorm:"column:scopes;type:character varying(255);comment:权限范围" json:"scopes"` // 权限范围
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"` // 删除时间
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"` // 用户代理
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"` // 刷新令牌
RefreshTokenExpires *orm.LocalDateTime `gorm:"column:refresh_token_expires;type:timestamp without time zone;comment:刷新令牌过期时间" json:"refresh_token_expires"` // 刷新令牌过期时间
Scopes_ *string `gorm:"column:scopes;type:character varying(255);comment:权限范围" json:"scopes"` // 权限范围
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

@@ -15,26 +15,26 @@ const TableNameTrade = "trade"
// Trade mapped from table <trade>
type Trade 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
InnerNo string `gorm:"column:inner_no;type:character varying(255);not null;comment:内部订单号" json:"inner_no"` // 内部订单号
OuterNo *string `gorm:"column:outer_no;type:character varying(255);comment:外部订单号" json:"outer_no"` // 外部订单号
Type int32 `gorm:"column:type;type:integer;not null;comment:订单类型1-购买产品2-充值余额" json:"type"` // 订单类型1-购买产品2-充值余额
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-银联
Platform int32 `gorm:"column:platform;type:integer;not null;comment:支付平台1-电脑网站2-手机网站" json:"platform"` // 支付平台1-电脑网站2-手机网站
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
InnerNo string `gorm:"column:inner_no;type:character varying(255);not null;comment:内部订单号" json:"inner_no"` // 内部订单号
OuterNo *string `gorm:"column:outer_no;type:character varying(255);comment:外部订单号" json:"outer_no"` // 外部订单号
Type int32 `gorm:"column:type;type:integer;not null;comment:订单类型1-购买产品2-充值余额" json:"type"` // 订单类型1-购买产品2-充值余额
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-商福通渠道支付宝,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")
@@ -51,22 +48,19 @@ func newClient(db *gorm.DB, opts ...gen.DOOption) client {
type client struct {
clientDo
ALL field.Asterisk
ID field.Int32 // 客户端ID
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
Name field.String // 名称
Icon field.String // 图标URL
Status field.Int32 // 状态0-禁用1-正常
CreatedAt field.Field // 创建时间
UpdatedAt field.Field // 更新时间
DeletedAt field.Field // 删除时间
ALL field.Asterisk
ID field.Int32 // 客户端ID
ClientID field.String // OAuth2客户端标识符
ClientSecret field.String // OAuth2客户端密钥
RedirectURI field.String // OAuth2 重定向URI
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 // 删除时间
fieldMap map[string]field.Expr
}
@@ -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