init
This commit is contained in:
58
backend/app/commands/event/event.go
Normal file
58
backend/app/commands/event/event.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"quyun/v2/app/commands"
|
||||
"quyun/v2/app/events/subscribers"
|
||||
"quyun/v2/providers/app"
|
||||
"quyun/v2/providers/event"
|
||||
"quyun/v2/providers/postgres"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func defaultProviders() container.Providers {
|
||||
return commands.Default(container.Providers{
|
||||
postgres.DefaultProvider(),
|
||||
}...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("event"),
|
||||
atom.Short("start event processor"),
|
||||
atom.RunE(Serve),
|
||||
atom.Providers(
|
||||
defaultProviders().
|
||||
With(
|
||||
subscribers.Provide,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dig.In
|
||||
|
||||
App *app.Config
|
||||
PubSub *event.PubSub
|
||||
Initials []contracts.Initial `group:"initials"`
|
||||
}
|
||||
|
||||
func Serve(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
if svc.App.IsDevMode() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
return svc.PubSub.Serve(ctx)
|
||||
})
|
||||
}
|
||||
57
backend/app/commands/grpc/grpc.go
Normal file
57
backend/app/commands/grpc/grpc.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"quyun/v2/app/commands"
|
||||
"quyun/v2/app/grpc/users"
|
||||
"quyun/v2/providers/app"
|
||||
"quyun/v2/providers/grpc"
|
||||
"quyun/v2/providers/postgres"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func defaultProviders() container.Providers {
|
||||
return commands.Default(container.Providers{
|
||||
postgres.DefaultProvider(),
|
||||
grpc.DefaultProvider(),
|
||||
}...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("grpc"),
|
||||
atom.Short("run grpc server"),
|
||||
atom.RunE(Serve),
|
||||
atom.Providers(
|
||||
defaultProviders().
|
||||
With(
|
||||
users.Provide,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dig.In
|
||||
|
||||
App *app.Config
|
||||
Grpc *grpc.Grpc
|
||||
Initials []contracts.Initial `group:"initials"`
|
||||
}
|
||||
|
||||
func Serve(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(svc Service) error {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
if svc.App.IsDevMode() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
return svc.Grpc.Serve()
|
||||
})
|
||||
}
|
||||
100
backend/app/commands/http/http.go
Normal file
100
backend/app/commands/http/http.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"quyun/v2/app/commands"
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/api"
|
||||
"quyun/v2/app/http/super"
|
||||
"quyun/v2/app/http/web"
|
||||
"quyun/v2/app/jobs"
|
||||
"quyun/v2/app/tenancy"
|
||||
_ "quyun/v2/docs"
|
||||
"quyun/v2/providers/app"
|
||||
"quyun/v2/providers/http"
|
||||
"quyun/v2/providers/http/swagger"
|
||||
"quyun/v2/providers/job"
|
||||
"quyun/v2/providers/jwt"
|
||||
"quyun/v2/providers/postgres"
|
||||
|
||||
"github.com/gofiber/fiber/v3/middleware/favicon"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func defaultProviders() container.Providers {
|
||||
return commands.Default(container.Providers{
|
||||
http.DefaultProvider(),
|
||||
postgres.DefaultProvider(),
|
||||
jwt.DefaultProvider(),
|
||||
job.DefaultProvider(),
|
||||
{Provider: api.Provide},
|
||||
{Provider: super.Provide},
|
||||
{Provider: web.Provide},
|
||||
}...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("serve"),
|
||||
atom.Short("run http server"),
|
||||
atom.RunE(Serve),
|
||||
atom.Providers(
|
||||
defaultProviders().
|
||||
With(
|
||||
jobs.Provide,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dig.In
|
||||
|
||||
App *app.Config
|
||||
Job *job.Job
|
||||
Http *http.Service
|
||||
DB *sql.DB
|
||||
Initials []contracts.Initial `group:"initials"`
|
||||
Routes []contracts.HttpRoute `group:"routes"`
|
||||
}
|
||||
|
||||
func Serve(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
if svc.App.Mode == app.AppModeDevelopment {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
|
||||
svc.Http.Engine.Get("/swagger/*", swagger.HandlerDefault)
|
||||
}
|
||||
svc.Http.Engine.Use(errorx.Middleware)
|
||||
svc.Http.Engine.Use(favicon.New(favicon.Config{
|
||||
Data: []byte{},
|
||||
}))
|
||||
|
||||
rootGroup := svc.Http.Engine.Group("")
|
||||
tenantGroup := svc.Http.Engine.Group("/t/:tenant_code", tenancy.Middleware(svc.DB))
|
||||
|
||||
sort.SliceStable(svc.Routes, func(i, j int) bool {
|
||||
return svc.Routes[i].Name() < svc.Routes[j].Name()
|
||||
})
|
||||
for _, route := range svc.Routes {
|
||||
if strings.HasPrefix(route.Name(), "super") {
|
||||
route.Register(rootGroup)
|
||||
continue
|
||||
}
|
||||
route.Register(tenantGroup)
|
||||
}
|
||||
|
||||
return svc.Http.Serve(ctx)
|
||||
})
|
||||
}
|
||||
35
backend/app/commands/migrate/20140202000000_river_queue.go
Normal file
35
backend/app/commands/migrate/20140202000000_river_queue.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pressly/goose/v3"
|
||||
"github.com/riverqueue/river/riverdriver/riverdatabasesql"
|
||||
"github.com/riverqueue/river/rivermigrate"
|
||||
)
|
||||
|
||||
func init() {
|
||||
goose.AddMigrationNoTxContext(RiverQueueUp, RiverQueueDown)
|
||||
}
|
||||
|
||||
func RiverQueueUp(ctx context.Context, db *sql.DB) error {
|
||||
migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "river migrate up failed")
|
||||
}
|
||||
|
||||
_, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{TargetVersion: -1})
|
||||
return err
|
||||
}
|
||||
|
||||
func RiverQueueDown(ctx context.Context, db *sql.DB) error {
|
||||
migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "river migrate down failed")
|
||||
}
|
||||
|
||||
_, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{TargetVersion: -1})
|
||||
return err
|
||||
}
|
||||
89
backend/app/commands/migrate/migrate.go
Normal file
89
backend/app/commands/migrate/migrate.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package migrate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"quyun/v2/app/commands"
|
||||
"quyun/v2/database"
|
||||
"quyun/v2/providers/postgres"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.uber.org/dig"
|
||||
|
||||
"github.com/riverqueue/river/riverdriver/riverdatabasesql"
|
||||
"github.com/riverqueue/river/rivermigrate"
|
||||
)
|
||||
|
||||
func defaultProviders() container.Providers {
|
||||
return commands.Default(container.Providers{
|
||||
postgres.DefaultProvider(),
|
||||
}...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("migrate"),
|
||||
atom.Short("run migrations"),
|
||||
atom.RunE(Serve),
|
||||
atom.Providers(defaultProviders()),
|
||||
atom.Example("migrate [up|up-by-one|up-to|create|down|down-to|fix|redo|reset|status|version]"),
|
||||
)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dig.In
|
||||
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
// migrate
|
||||
func Serve(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
|
||||
if len(args) == 0 {
|
||||
args = append(args, "up")
|
||||
}
|
||||
|
||||
if args[0] == "create" {
|
||||
return nil
|
||||
}
|
||||
|
||||
action, args := args[0], args[1:]
|
||||
log.Infof("migration action: %s args: %+v", action, args)
|
||||
|
||||
goose.SetBaseFS(database.MigrationFS)
|
||||
goose.SetTableName("migrations")
|
||||
goose.AddNamedMigrationNoTxContext("0001_river_job.go", RiverUp, RiverDown)
|
||||
|
||||
return goose.RunContext(context.Background(), action, svc.DB, "migrations", args...)
|
||||
})
|
||||
}
|
||||
|
||||
func RiverUp(ctx context.Context, db *sql.DB) error {
|
||||
migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Migrate up. An empty MigrateOpts will migrate all the way up, but
|
||||
// best practice is to specify a specific target version.
|
||||
_, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{})
|
||||
return err
|
||||
}
|
||||
|
||||
func RiverDown(ctx context.Context, db *sql.DB) error {
|
||||
migrator, err := rivermigrate.New(riverdatabasesql.New(db), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TargetVersion -1 removes River's schema completely.
|
||||
_, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{
|
||||
TargetVersion: -1,
|
||||
})
|
||||
return err
|
||||
}
|
||||
24
backend/app/commands/queue/error.go
Normal file
24
backend/app/commands/queue/error.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/riverqueue/river"
|
||||
"github.com/riverqueue/river/rivertype"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CustomErrorHandler struct{}
|
||||
|
||||
func (*CustomErrorHandler) HandleError(ctx context.Context, job *rivertype.JobRow, err error) *river.ErrorHandlerResult {
|
||||
log.Infof("Job errored with: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*CustomErrorHandler) HandlePanic(ctx context.Context, job *rivertype.JobRow, panicVal any, trace string) *river.ErrorHandlerResult {
|
||||
log.Infof("Job panicked with: %v\n", panicVal)
|
||||
log.Infof("Stack trace: %s\n", trace)
|
||||
return &river.ErrorHandlerResult{
|
||||
SetCancelled: true,
|
||||
}
|
||||
}
|
||||
67
backend/app/commands/queue/river.go
Normal file
67
backend/app/commands/queue/river.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
|
||||
"quyun/v2/app/commands"
|
||||
"quyun/v2/app/jobs"
|
||||
"quyun/v2/providers/app"
|
||||
"quyun/v2/providers/job"
|
||||
"quyun/v2/providers/postgres"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
func defaultProviders() container.Providers {
|
||||
return commands.Default(container.Providers{
|
||||
postgres.DefaultProvider(),
|
||||
job.DefaultProvider(),
|
||||
}...)
|
||||
}
|
||||
|
||||
func Command() atom.Option {
|
||||
return atom.Command(
|
||||
atom.Name("queue"),
|
||||
atom.Short("start queue processor"),
|
||||
atom.RunE(Serve),
|
||||
atom.Providers(
|
||||
defaultProviders().
|
||||
With(
|
||||
jobs.Provide,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
dig.In
|
||||
|
||||
App *app.Config
|
||||
Job *job.Job
|
||||
Initials []contracts.Initial `group:"initials"`
|
||||
CronJobs []contracts.CronJob `group:"cron_jobs"`
|
||||
}
|
||||
|
||||
func Serve(cmd *cobra.Command, args []string) error {
|
||||
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
|
||||
if svc.App.IsDevMode() {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
|
||||
if err := svc.Job.Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
defer svc.Job.Close()
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
14
backend/app/commands/service.go
Normal file
14
backend/app/commands/service.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom/container"
|
||||
"quyun/v2/providers/app"
|
||||
"quyun/v2/providers/event"
|
||||
)
|
||||
|
||||
func Default(providers ...container.ProviderContainer) container.Providers {
|
||||
return append(container.Providers{
|
||||
app.DefaultProvider(),
|
||||
event.DefaultProvider(),
|
||||
}, providers...)
|
||||
}
|
||||
39
backend/app/commands/testx/testing.go
Normal file
39
backend/app/commands/testx/testing.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package testx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"quyun/v2/database"
|
||||
"quyun/v2/providers/job"
|
||||
"quyun/v2/providers/jwt"
|
||||
"quyun/v2/providers/postgres"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
|
||||
"github.com/rogeecn/fabfile"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func Default(providers ...container.ProviderContainer) container.Providers {
|
||||
return append(container.Providers{
|
||||
postgres.DefaultProvider(),
|
||||
jwt.DefaultProvider(),
|
||||
job.DefaultProvider(),
|
||||
database.DefaultProvider(),
|
||||
}, providers...)
|
||||
}
|
||||
|
||||
func Serve(providers container.Providers, t *testing.T, invoke any) {
|
||||
Convey("tests boot up", t, func() {
|
||||
file := fabfile.MustFind("config.toml")
|
||||
|
||||
// localEnv := os.Getenv("ENV_LOCAL")
|
||||
// if localEnv != "" {
|
||||
// file = fabfile.MustFind("config." + localEnv + ".toml")
|
||||
// }
|
||||
|
||||
So(atom.LoadProviders(file, providers), ShouldBeNil)
|
||||
So(container.Container.Invoke(invoke), ShouldBeNil)
|
||||
})
|
||||
}
|
||||
0
backend/app/console/.gitkeep
Normal file
0
backend/app/console/.gitkeep
Normal file
65
backend/app/errorx/app_error.go
Normal file
65
backend/app/errorx/app_error.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// AppError 应用错误结构
|
||||
type AppError struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
StatusCode int `json:"-"`
|
||||
Data any `json:"data,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
// 调试信息
|
||||
originalErr error
|
||||
file string
|
||||
params []any
|
||||
sql string
|
||||
}
|
||||
|
||||
// Error 实现 error 接口
|
||||
func (e *AppError) Error() string {
|
||||
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap 允许通过 errors.Unwrap 遍历到原始错误
|
||||
func (e *AppError) Unwrap() error { return e.originalErr }
|
||||
|
||||
// WithData 添加数据
|
||||
func (e *AppError) WithData(data any) *AppError {
|
||||
e.Data = data
|
||||
return e
|
||||
}
|
||||
|
||||
// WithMsg 设置消息
|
||||
func (e *AppError) WithMsg(msg string) *AppError {
|
||||
e.Message = msg
|
||||
return e
|
||||
}
|
||||
|
||||
// WithSQL 记录SQL信息
|
||||
func (e *AppError) WithSQL(sql string) *AppError {
|
||||
e.sql = sql
|
||||
return e
|
||||
}
|
||||
|
||||
// WithParams 记录参数信息,并自动获取调用位置
|
||||
func (e *AppError) WithParams(params ...any) *AppError {
|
||||
e.params = params
|
||||
if _, file, line, ok := runtime.Caller(1); ok {
|
||||
e.file = fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// NewError 创建应用错误
|
||||
func NewError(code ErrorCode, statusCode int, message string) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
StatusCode: statusCode,
|
||||
}
|
||||
}
|
||||
90
backend/app/errorx/codes.go
Normal file
90
backend/app/errorx/codes.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package errorx
|
||||
|
||||
// ErrorCode 错误码类型
|
||||
type ErrorCode int
|
||||
|
||||
const (
|
||||
// 1000-1099: 数据相关错误
|
||||
CodeRecordNotFound ErrorCode = 1001
|
||||
CodeRecordDuplicated ErrorCode = 1002
|
||||
CodeDataCorrupted ErrorCode = 1003
|
||||
CodeDataTooLarge ErrorCode = 1004
|
||||
CodeDataValidationFail ErrorCode = 1005
|
||||
CodeConstraintViolated ErrorCode = 1006
|
||||
CodeDataExpired ErrorCode = 1007
|
||||
CodeDataLocked ErrorCode = 1008
|
||||
|
||||
// 1100-1199: 请求相关错误
|
||||
CodeBadRequest ErrorCode = 1101
|
||||
CodeMissingParameter ErrorCode = 1102
|
||||
CodeInvalidParameter ErrorCode = 1103
|
||||
CodeParameterTooLong ErrorCode = 1104
|
||||
CodeParameterTooShort ErrorCode = 1105
|
||||
CodeInvalidFormat ErrorCode = 1106
|
||||
CodeUnsupportedMethod ErrorCode = 1107
|
||||
CodeRequestTooLarge ErrorCode = 1108
|
||||
CodeInvalidJSON ErrorCode = 1109
|
||||
CodeInvalidXML ErrorCode = 1110
|
||||
|
||||
// 1200-1299: 认证授权错误
|
||||
CodeUnauthorized ErrorCode = 1201
|
||||
CodeForbidden ErrorCode = 1202
|
||||
CodeTokenExpired ErrorCode = 1203
|
||||
CodeTokenInvalid ErrorCode = 1204
|
||||
CodeTokenMissing ErrorCode = 1205
|
||||
CodePermissionDenied ErrorCode = 1206
|
||||
CodeAccountDisabled ErrorCode = 1207
|
||||
CodeAccountLocked ErrorCode = 1208
|
||||
CodeInvalidCredentials ErrorCode = 1209
|
||||
CodeSessionExpired ErrorCode = 1210
|
||||
|
||||
// 1300-1399: 业务逻辑错误
|
||||
CodeBusinessLogic ErrorCode = 1301
|
||||
CodeWorkflowError ErrorCode = 1302
|
||||
CodeStatusConflict ErrorCode = 1303
|
||||
CodeOperationFailed ErrorCode = 1304
|
||||
CodeResourceConflict ErrorCode = 1305
|
||||
CodePreconditionFailed ErrorCode = 1306
|
||||
CodeQuotaExceeded ErrorCode = 1307
|
||||
CodeResourceExhausted ErrorCode = 1308
|
||||
|
||||
// 1400-1499: 外部服务错误
|
||||
CodeExternalService ErrorCode = 1401
|
||||
CodeServiceUnavailable ErrorCode = 1402
|
||||
CodeServiceTimeout ErrorCode = 1403
|
||||
CodeThirdPartyError ErrorCode = 1404
|
||||
CodeNetworkError ErrorCode = 1405
|
||||
CodeDatabaseError ErrorCode = 1406
|
||||
CodeCacheError ErrorCode = 1407
|
||||
CodeMessageQueueError ErrorCode = 1408
|
||||
|
||||
// 1500-1599: 系统错误
|
||||
CodeInternalError ErrorCode = 1501
|
||||
CodeConfigurationError ErrorCode = 1502
|
||||
CodeFileSystemError ErrorCode = 1503
|
||||
CodeMemoryError ErrorCode = 1504
|
||||
CodeConcurrencyError ErrorCode = 1505
|
||||
CodeDeadlockError ErrorCode = 1506
|
||||
|
||||
// 1600-1699: 限流和频率控制
|
||||
CodeRateLimitExceeded ErrorCode = 1601
|
||||
CodeTooManyRequests ErrorCode = 1602
|
||||
CodeConcurrentLimit ErrorCode = 1603
|
||||
CodeAPIQuotaExceeded ErrorCode = 1604
|
||||
|
||||
// 1700-1799: 文件和上传错误
|
||||
CodeFileNotFound ErrorCode = 1701
|
||||
CodeFileTooBig ErrorCode = 1702
|
||||
CodeInvalidFileType ErrorCode = 1703
|
||||
CodeFileCorrupted ErrorCode = 1704
|
||||
CodeUploadFailed ErrorCode = 1705
|
||||
CodeDownloadFailed ErrorCode = 1706
|
||||
CodeFilePermission ErrorCode = 1707
|
||||
|
||||
// 1800-1899: 加密和安全错误
|
||||
CodeEncryptionError ErrorCode = 1801
|
||||
CodeDecryptionError ErrorCode = 1802
|
||||
CodeSignatureInvalid ErrorCode = 1803
|
||||
CodeCertificateInvalid ErrorCode = 1804
|
||||
CodeSecurityViolation ErrorCode = 1805
|
||||
)
|
||||
105
backend/app/errorx/handler.go
Normal file
105
backend/app/errorx/handler.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ErrorHandler 错误处理器
|
||||
type ErrorHandler struct{}
|
||||
|
||||
// NewErrorHandler 创建错误处理器
|
||||
func NewErrorHandler() *ErrorHandler {
|
||||
return &ErrorHandler{}
|
||||
}
|
||||
|
||||
// Handle 处理错误并返回统一格式
|
||||
func (h *ErrorHandler) Handle(err error) *AppError {
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
return appErr
|
||||
}
|
||||
|
||||
// 处理 Fiber 错误
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return h.handleFiberError(fiberErr)
|
||||
}
|
||||
|
||||
// 处理 GORM 错误
|
||||
if appErr := h.handleGormError(err); appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
// 默认内部错误
|
||||
return &AppError{
|
||||
Code: ErrInternalError.Code,
|
||||
Message: err.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
originalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
// handleFiberError 处理 Fiber 错误
|
||||
func (h *ErrorHandler) handleFiberError(fiberErr *fiber.Error) *AppError {
|
||||
var appErr *AppError
|
||||
|
||||
switch fiberErr.Code {
|
||||
case http.StatusBadRequest:
|
||||
appErr = ErrBadRequest
|
||||
case http.StatusUnauthorized:
|
||||
appErr = ErrUnauthorized
|
||||
case http.StatusForbidden:
|
||||
appErr = ErrForbidden
|
||||
case http.StatusNotFound:
|
||||
appErr = ErrRecordNotFound
|
||||
case http.StatusMethodNotAllowed:
|
||||
appErr = ErrUnsupportedMethod
|
||||
case http.StatusRequestEntityTooLarge:
|
||||
appErr = ErrRequestTooLarge
|
||||
case http.StatusTooManyRequests:
|
||||
appErr = ErrTooManyRequests
|
||||
default:
|
||||
appErr = ErrInternalError
|
||||
}
|
||||
|
||||
return &AppError{
|
||||
Code: appErr.Code,
|
||||
Message: fiberErr.Message,
|
||||
StatusCode: fiberErr.Code,
|
||||
originalErr: fiberErr,
|
||||
}
|
||||
}
|
||||
|
||||
// handleGormError 处理 GORM 错误
|
||||
func (h *ErrorHandler) handleGormError(err error) *AppError {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return &AppError{
|
||||
Code: ErrRecordNotFound.Code,
|
||||
Message: ErrRecordNotFound.Message,
|
||||
StatusCode: ErrRecordNotFound.StatusCode,
|
||||
originalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return &AppError{
|
||||
Code: ErrRecordDuplicated.Code,
|
||||
Message: ErrRecordDuplicated.Message,
|
||||
StatusCode: ErrRecordDuplicated.StatusCode,
|
||||
originalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Is(err, gorm.ErrInvalidTransaction) {
|
||||
return &AppError{
|
||||
Code: ErrConcurrencyError.Code,
|
||||
Message: "事务无效",
|
||||
StatusCode: ErrConcurrencyError.StatusCode,
|
||||
originalErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
38
backend/app/errorx/middleware.go
Normal file
38
backend/app/errorx/middleware.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package errorx
|
||||
|
||||
import "github.com/gofiber/fiber/v3"
|
||||
|
||||
// 全局实例
|
||||
var DefaultSender = NewResponseSender()
|
||||
|
||||
// Middleware 错误处理中间件
|
||||
func Middleware(c fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
if err != nil {
|
||||
return DefaultSender.SendError(c, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 便捷函数
|
||||
func Wrap(err error) *AppError {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
return &AppError{
|
||||
Code: appErr.Code,
|
||||
Message: appErr.Message,
|
||||
StatusCode: appErr.StatusCode,
|
||||
Data: appErr.Data,
|
||||
originalErr: appErr,
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultSender.handler.Handle(err)
|
||||
}
|
||||
|
||||
func SendError(ctx fiber.Ctx, err error) error {
|
||||
return DefaultSender.SendError(ctx, err)
|
||||
}
|
||||
105
backend/app/errorx/predefined.go
Normal file
105
backend/app/errorx/predefined.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package errorx
|
||||
|
||||
import "net/http"
|
||||
|
||||
// 预定义错误 - 数据相关
|
||||
var (
|
||||
ErrRecordNotFound = NewError(CodeRecordNotFound, http.StatusNotFound, "记录不存在")
|
||||
ErrRecordDuplicated = NewError(CodeRecordDuplicated, http.StatusConflict, "记录重复")
|
||||
ErrDataCorrupted = NewError(CodeDataCorrupted, http.StatusBadRequest, "数据损坏")
|
||||
ErrDataTooLarge = NewError(CodeDataTooLarge, http.StatusRequestEntityTooLarge, "数据过大")
|
||||
ErrDataValidationFail = NewError(CodeDataValidationFail, http.StatusBadRequest, "数据验证失败")
|
||||
ErrConstraintViolated = NewError(CodeConstraintViolated, http.StatusConflict, "约束违规")
|
||||
ErrDataExpired = NewError(CodeDataExpired, http.StatusGone, "数据已过期")
|
||||
ErrDataLocked = NewError(CodeDataLocked, http.StatusLocked, "数据已锁定")
|
||||
)
|
||||
|
||||
// 预定义错误 - 请求相关
|
||||
var (
|
||||
ErrBadRequest = NewError(CodeBadRequest, http.StatusBadRequest, "请求错误")
|
||||
ErrMissingParameter = NewError(CodeMissingParameter, http.StatusBadRequest, "缺少必需参数")
|
||||
ErrInvalidParameter = NewError(CodeInvalidParameter, http.StatusBadRequest, "参数无效")
|
||||
ErrParameterTooLong = NewError(CodeParameterTooLong, http.StatusBadRequest, "参数过长")
|
||||
ErrParameterTooShort = NewError(CodeParameterTooShort, http.StatusBadRequest, "参数过短")
|
||||
ErrInvalidFormat = NewError(CodeInvalidFormat, http.StatusBadRequest, "格式无效")
|
||||
ErrUnsupportedMethod = NewError(CodeUnsupportedMethod, http.StatusMethodNotAllowed, "不支持的请求方法")
|
||||
ErrRequestTooLarge = NewError(CodeRequestTooLarge, http.StatusRequestEntityTooLarge, "请求体过大")
|
||||
ErrInvalidJSON = NewError(CodeInvalidJSON, http.StatusBadRequest, "JSON格式错误")
|
||||
ErrInvalidXML = NewError(CodeInvalidXML, http.StatusBadRequest, "XML格式错误")
|
||||
)
|
||||
|
||||
// 预定义错误 - 认证授权
|
||||
var (
|
||||
ErrUnauthorized = NewError(CodeUnauthorized, http.StatusUnauthorized, "未授权")
|
||||
ErrForbidden = NewError(CodeForbidden, http.StatusForbidden, "禁止访问")
|
||||
ErrTokenExpired = NewError(CodeTokenExpired, http.StatusUnauthorized, "Token已过期")
|
||||
ErrTokenInvalid = NewError(CodeTokenInvalid, http.StatusUnauthorized, "Token无效")
|
||||
ErrTokenMissing = NewError(CodeTokenMissing, http.StatusUnauthorized, "Token缺失")
|
||||
ErrPermissionDenied = NewError(CodePermissionDenied, http.StatusForbidden, "权限不足")
|
||||
ErrAccountDisabled = NewError(CodeAccountDisabled, http.StatusForbidden, "账户已禁用")
|
||||
ErrAccountLocked = NewError(CodeAccountLocked, http.StatusLocked, "账户已锁定")
|
||||
ErrInvalidCredentials = NewError(CodeInvalidCredentials, http.StatusUnauthorized, "凭据无效")
|
||||
ErrSessionExpired = NewError(CodeSessionExpired, http.StatusUnauthorized, "会话已过期")
|
||||
)
|
||||
|
||||
// 预定义错误 - 业务逻辑
|
||||
var (
|
||||
ErrBusinessLogic = NewError(CodeBusinessLogic, http.StatusBadRequest, "业务逻辑错误")
|
||||
ErrWorkflowError = NewError(CodeWorkflowError, http.StatusBadRequest, "工作流错误")
|
||||
ErrStatusConflict = NewError(CodeStatusConflict, http.StatusConflict, "状态冲突")
|
||||
ErrOperationFailed = NewError(CodeOperationFailed, http.StatusInternalServerError, "操作失败")
|
||||
ErrResourceConflict = NewError(CodeResourceConflict, http.StatusConflict, "资源冲突")
|
||||
ErrPreconditionFailed = NewError(CodePreconditionFailed, http.StatusPreconditionFailed, "前置条件失败")
|
||||
ErrQuotaExceeded = NewError(CodeQuotaExceeded, http.StatusForbidden, "配额超限")
|
||||
ErrResourceExhausted = NewError(CodeResourceExhausted, http.StatusTooManyRequests, "资源耗尽")
|
||||
)
|
||||
|
||||
// 预定义错误 - 外部服务
|
||||
var (
|
||||
ErrExternalService = NewError(CodeExternalService, http.StatusBadGateway, "外部服务错误")
|
||||
ErrServiceUnavailable = NewError(CodeServiceUnavailable, http.StatusServiceUnavailable, "服务不可用")
|
||||
ErrServiceTimeout = NewError(CodeServiceTimeout, http.StatusRequestTimeout, "服务超时")
|
||||
ErrThirdPartyError = NewError(CodeThirdPartyError, http.StatusBadGateway, "第三方服务错误")
|
||||
ErrNetworkError = NewError(CodeNetworkError, http.StatusBadGateway, "网络错误")
|
||||
ErrDatabaseError = NewError(CodeDatabaseError, http.StatusInternalServerError, "数据库错误")
|
||||
ErrCacheError = NewError(CodeCacheError, http.StatusInternalServerError, "缓存错误")
|
||||
ErrMessageQueueError = NewError(CodeMessageQueueError, http.StatusInternalServerError, "消息队列错误")
|
||||
)
|
||||
|
||||
// 预定义错误 - 系统错误
|
||||
var (
|
||||
ErrInternalError = NewError(CodeInternalError, http.StatusInternalServerError, "内部错误")
|
||||
ErrConfigurationError = NewError(CodeConfigurationError, http.StatusInternalServerError, "配置错误")
|
||||
ErrFileSystemError = NewError(CodeFileSystemError, http.StatusInternalServerError, "文件系统错误")
|
||||
ErrMemoryError = NewError(CodeMemoryError, http.StatusInternalServerError, "内存错误")
|
||||
ErrConcurrencyError = NewError(CodeConcurrencyError, http.StatusInternalServerError, "并发错误")
|
||||
ErrDeadlockError = NewError(CodeDeadlockError, http.StatusInternalServerError, "死锁错误")
|
||||
)
|
||||
|
||||
// 预定义错误 - 限流
|
||||
var (
|
||||
ErrRateLimitExceeded = NewError(CodeRateLimitExceeded, http.StatusTooManyRequests, "请求频率超限")
|
||||
ErrTooManyRequests = NewError(CodeTooManyRequests, http.StatusTooManyRequests, "请求过多")
|
||||
ErrConcurrentLimit = NewError(CodeConcurrentLimit, http.StatusTooManyRequests, "并发数超限")
|
||||
ErrAPIQuotaExceeded = NewError(CodeAPIQuotaExceeded, http.StatusTooManyRequests, "API配额超限")
|
||||
)
|
||||
|
||||
// 预定义错误 - 文件处理
|
||||
var (
|
||||
ErrFileNotFound = NewError(CodeFileNotFound, http.StatusNotFound, "文件不存在")
|
||||
ErrFileTooBig = NewError(CodeFileTooBig, http.StatusRequestEntityTooLarge, "文件过大")
|
||||
ErrInvalidFileType = NewError(CodeInvalidFileType, http.StatusBadRequest, "文件类型无效")
|
||||
ErrFileCorrupted = NewError(CodeFileCorrupted, http.StatusBadRequest, "文件损坏")
|
||||
ErrUploadFailed = NewError(CodeUploadFailed, http.StatusInternalServerError, "上传失败")
|
||||
ErrDownloadFailed = NewError(CodeDownloadFailed, http.StatusInternalServerError, "下载失败")
|
||||
ErrFilePermission = NewError(CodeFilePermission, http.StatusForbidden, "文件权限不足")
|
||||
)
|
||||
|
||||
// 预定义错误 - 安全相关
|
||||
var (
|
||||
ErrEncryptionError = NewError(CodeEncryptionError, http.StatusInternalServerError, "加密错误")
|
||||
ErrDecryptionError = NewError(CodeDecryptionError, http.StatusInternalServerError, "解密错误")
|
||||
ErrSignatureInvalid = NewError(CodeSignatureInvalid, http.StatusUnauthorized, "签名无效")
|
||||
ErrCertificateInvalid = NewError(CodeCertificateInvalid, http.StatusUnauthorized, "证书无效")
|
||||
ErrSecurityViolation = NewError(CodeSecurityViolation, http.StatusForbidden, "安全违规")
|
||||
)
|
||||
127
backend/app/errorx/response.go
Normal file
127
backend/app/errorx/response.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package errorx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/gofiber/fiber/v3/binder"
|
||||
"github.com/gofiber/utils/v2"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ResponseSender 响应发送器
|
||||
type ResponseSender struct {
|
||||
handler *ErrorHandler
|
||||
}
|
||||
|
||||
// NewResponseSender 创建响应发送器
|
||||
func NewResponseSender() *ResponseSender {
|
||||
return &ResponseSender{
|
||||
handler: NewErrorHandler(),
|
||||
}
|
||||
}
|
||||
|
||||
// SendError 发送错误响应
|
||||
func (s *ResponseSender) SendError(ctx fiber.Ctx, err error) error {
|
||||
appErr := s.handler.Handle(err)
|
||||
|
||||
// 记录错误日志
|
||||
s.logError(appErr)
|
||||
|
||||
// 根据 Content-Type 返回不同格式
|
||||
return s.sendResponse(ctx, appErr)
|
||||
}
|
||||
|
||||
// logError 记录错误日志
|
||||
func (s *ResponseSender) logError(appErr *AppError) {
|
||||
// 确保每个错误实例都有唯一ID,便于日志关联
|
||||
if appErr.ID == "" {
|
||||
appErr.ID = uuid.NewString()
|
||||
}
|
||||
|
||||
// 构造详细的错误级联链路(包含类型、状态、定位等)
|
||||
chain := make([]map[string]any, 0, 4)
|
||||
var e error = appErr
|
||||
for e != nil {
|
||||
entry := map[string]any{
|
||||
"type": fmt.Sprintf("%T", e),
|
||||
"error": e.Error(),
|
||||
}
|
||||
switch v := e.(type) {
|
||||
case *AppError:
|
||||
entry["code"] = v.Code
|
||||
entry["statusCode"] = v.StatusCode
|
||||
if v.file != "" {
|
||||
entry["file"] = v.file
|
||||
}
|
||||
if len(v.params) > 0 {
|
||||
entry["params"] = v.params
|
||||
}
|
||||
if v.sql != "" {
|
||||
entry["sql"] = v.sql
|
||||
}
|
||||
if v.ID != "" {
|
||||
entry["id"] = v.ID
|
||||
}
|
||||
case *fiber.Error:
|
||||
entry["statusCode"] = v.Code
|
||||
entry["message"] = v.Message
|
||||
}
|
||||
|
||||
// GORM 常见错误归类标记
|
||||
if errors.Is(e, gorm.ErrRecordNotFound) {
|
||||
entry["gorm"] = "record_not_found"
|
||||
} else if errors.Is(e, gorm.ErrDuplicatedKey) {
|
||||
entry["gorm"] = "duplicated_key"
|
||||
} else if errors.Is(e, gorm.ErrInvalidTransaction) {
|
||||
entry["gorm"] = "invalid_transaction"
|
||||
}
|
||||
|
||||
chain = append(chain, entry)
|
||||
e = errors.Unwrap(e)
|
||||
}
|
||||
|
||||
root := chain[len(chain)-1]["error"]
|
||||
|
||||
logEntry := log.WithFields(log.Fields{
|
||||
"id": appErr.ID,
|
||||
"code": appErr.Code,
|
||||
"statusCode": appErr.StatusCode,
|
||||
"file": appErr.file,
|
||||
"sql": appErr.sql,
|
||||
"params": appErr.params,
|
||||
"error_chain": chain,
|
||||
"root_error": root,
|
||||
})
|
||||
|
||||
if appErr.originalErr != nil {
|
||||
logEntry = logEntry.WithError(appErr.originalErr)
|
||||
}
|
||||
|
||||
// 根据错误级别记录不同级别的日志
|
||||
if appErr.StatusCode >= 500 {
|
||||
logEntry.Error("系统错误: ", appErr.Message)
|
||||
} else if appErr.StatusCode >= 400 {
|
||||
logEntry.Warn("客户端错误: ", appErr.Message)
|
||||
} else {
|
||||
logEntry.Info("应用错误: ", appErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// sendResponse 发送响应
|
||||
func (s *ResponseSender) sendResponse(ctx fiber.Ctx, appErr *AppError) error {
|
||||
contentType := utils.ToLower(utils.UnsafeString(ctx.Request().Header.ContentType()))
|
||||
contentType = binder.FilterFlags(utils.ParseVendorSpecificContentType(contentType))
|
||||
|
||||
switch contentType {
|
||||
case fiber.MIMETextXML, fiber.MIMEApplicationXML:
|
||||
return ctx.Status(appErr.StatusCode).XML(appErr)
|
||||
case fiber.MIMETextHTML, fiber.MIMETextPlain:
|
||||
return ctx.Status(appErr.StatusCode).SendString(appErr.Message)
|
||||
default:
|
||||
return ctx.Status(appErr.StatusCode).JSON(appErr)
|
||||
}
|
||||
}
|
||||
26
backend/app/events/publishers/user_register.go
Normal file
26
backend/app/events/publishers/user_register.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package publishers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"quyun/v2/app/events"
|
||||
"quyun/v2/providers/event"
|
||||
|
||||
"go.ipao.vip/atom/contracts"
|
||||
)
|
||||
|
||||
var _ contracts.EventPublisher = (*UserRegister)(nil)
|
||||
|
||||
type UserRegister struct {
|
||||
event.DefaultChannel
|
||||
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (e *UserRegister) Marshal() ([]byte, error) {
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
func (e *UserRegister) Topic() string {
|
||||
return events.TopicUserRegister
|
||||
}
|
||||
27
backend/app/events/subscribers/provider.gen.go
Executable file
27
backend/app/events/subscribers/provider.gen.go
Executable file
@@ -0,0 +1,27 @@
|
||||
package subscribers
|
||||
|
||||
import (
|
||||
"quyun/v2/providers/event"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
__event *event.PubSub,
|
||||
) (contracts.Initial, error) {
|
||||
obj := &UserRegister{}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
__event.Handle("handler:UserRegister", obj)
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupInitial); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
45
backend/app/events/subscribers/user_register.go
Normal file
45
backend/app/events/subscribers/user_register.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package subscribers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"quyun/v2/app/events"
|
||||
"quyun/v2/app/events/publishers"
|
||||
"quyun/v2/providers/event"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
"github.com/sirupsen/logrus"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
)
|
||||
|
||||
var _ contracts.EventHandler = (*UserRegister)(nil)
|
||||
|
||||
// @provider(event)
|
||||
type UserRegister struct {
|
||||
event.DefaultChannel
|
||||
event.DefaultPublishTo
|
||||
|
||||
log *logrus.Entry `inject:"false"`
|
||||
}
|
||||
|
||||
func (e *UserRegister) Prepare() error {
|
||||
e.log = logrus.WithField("module", "events.subscribers.user_register")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Topic implements contracts.EventHandler.
|
||||
func (e *UserRegister) Topic() string {
|
||||
return events.TopicUserRegister
|
||||
}
|
||||
|
||||
// Handler implements contracts.EventHandler.
|
||||
func (e *UserRegister) Handler(msg *message.Message) ([]*message.Message, error) {
|
||||
var payload publishers.UserRegister
|
||||
err := json.Unmarshal(msg.Payload, &payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.log.Infof("received event %s", msg.Payload)
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
24
backend/app/events/subscribers/utils.go
Normal file
24
backend/app/events/subscribers/utils.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package subscribers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ThreeDotsLabs/watermill"
|
||||
"github.com/ThreeDotsLabs/watermill/message"
|
||||
)
|
||||
|
||||
func toMessage(event any) (*message.Message, error) {
|
||||
b, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return message.NewMessage(watermill.NewUUID(), b), nil
|
||||
}
|
||||
|
||||
func toMessageList(event any) ([]*message.Message, error) {
|
||||
m, err := toMessage(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*message.Message{m}, nil
|
||||
}
|
||||
6
backend/app/events/topics.go
Normal file
6
backend/app/events/topics.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package events
|
||||
|
||||
const (
|
||||
TopicProcessed = "event:processed"
|
||||
TopicUserRegister = "event:user_register"
|
||||
)
|
||||
26
backend/app/grpc/users/handler.go
Normal file
26
backend/app/grpc/users/handler.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
userv1 "quyun/v2/pkg/proto/user/v1"
|
||||
)
|
||||
|
||||
// @provider(grpc) userv1.RegisterUserServiceServer
|
||||
type Users struct {
|
||||
userv1.UnimplementedUserServiceServer
|
||||
}
|
||||
|
||||
func (u *Users) ListUsers(ctx context.Context, in *userv1.ListUsersRequest) (*userv1.ListUsersResponse, error) {
|
||||
// userv1.UserServiceServer
|
||||
return &userv1.ListUsersResponse{}, nil
|
||||
}
|
||||
|
||||
// GetUser implements userv1.UserServiceServer
|
||||
func (u *Users) GetUser(ctx context.Context, in *userv1.GetUserRequest) (*userv1.GetUserResponse, error) {
|
||||
return &userv1.GetUserResponse{
|
||||
User: &userv1.User{
|
||||
Id: in.Id,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
25
backend/app/grpc/users/provider.gen.go
Executable file
25
backend/app/grpc/users/provider.gen.go
Executable file
@@ -0,0 +1,25 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
userv1 "quyun/v2/pkg/proto/user/v1"
|
||||
"quyun/v2/providers/grpc"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
__grpc *grpc.Grpc,
|
||||
) (contracts.Initial, error) {
|
||||
obj := &Users{}
|
||||
userv1.RegisterUserServiceServer(__grpc.Server, obj)
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupInitial); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
207
backend/app/http/api/controller.go
Normal file
207
backend/app/http/api/controller.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/tenancy"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type ApiController struct{}
|
||||
|
||||
type PostsQuery struct {
|
||||
Page int `query:"page"`
|
||||
Limit int `query:"limit"`
|
||||
Keyword string `query:"keyword"`
|
||||
}
|
||||
|
||||
type UpdateUsernameReq struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// WeChatOAuthStart
|
||||
//
|
||||
// @Router /v1/auth/wechat [get]
|
||||
func (a *ApiController) WeChatOAuthStart(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "wechat oauth: /auth/wechat"})
|
||||
}
|
||||
|
||||
// WeChatOAuthCallback
|
||||
//
|
||||
// @Router /v1/auth/login [get]
|
||||
func (a *ApiController) WeChatOAuthCallback(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "wechat oauth: /auth/login"})
|
||||
}
|
||||
|
||||
// ListPosts
|
||||
//
|
||||
// @Router /v1/posts [get]
|
||||
// @Bind query query
|
||||
func (a *ApiController) ListPosts(ctx fiber.Ctx, query *PostsQuery) error {
|
||||
page := 1
|
||||
limit := 10
|
||||
if query != nil {
|
||||
if query.Page > 0 {
|
||||
page = query.Page
|
||||
}
|
||||
if query.Limit > 0 {
|
||||
limit = query.Limit
|
||||
}
|
||||
}
|
||||
return ctx.JSON(fiber.Map{
|
||||
"items": []any{},
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// ShowPost
|
||||
//
|
||||
// @Router /v1/posts/:id/show [get]
|
||||
// @Bind id path
|
||||
func (a *ApiController) ShowPost(ctx fiber.Ctx, id int) error {
|
||||
if id <= 0 {
|
||||
return errorx.ErrInvalidParameter.WithMsg("invalid id")
|
||||
}
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
// PlayPost
|
||||
//
|
||||
// @Router /v1/posts/:id/play [get]
|
||||
// @Bind id path
|
||||
func (a *ApiController) PlayPost(ctx fiber.Ctx, id int) error {
|
||||
if id <= 0 {
|
||||
return errorx.ErrInvalidParameter.WithMsg("invalid id")
|
||||
}
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "post play"})
|
||||
}
|
||||
|
||||
// MinePosts
|
||||
//
|
||||
// @Router /v1/posts/mine [get]
|
||||
// @Bind query query
|
||||
func (a *ApiController) MinePosts(ctx fiber.Ctx, query *PostsQuery) error {
|
||||
page := 1
|
||||
limit := 10
|
||||
if query != nil {
|
||||
if query.Page > 0 {
|
||||
page = query.Page
|
||||
}
|
||||
if query.Limit > 0 {
|
||||
limit = query.Limit
|
||||
}
|
||||
}
|
||||
return ctx.JSON(fiber.Map{
|
||||
"items": []any{},
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
// BuyPost
|
||||
//
|
||||
// @Router /v1/posts/:id/buy [post]
|
||||
// @Bind id path
|
||||
func (a *ApiController) BuyPost(ctx fiber.Ctx, id int) error {
|
||||
if id <= 0 {
|
||||
return errorx.ErrInvalidParameter.WithMsg("invalid id")
|
||||
}
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "post buy"})
|
||||
}
|
||||
|
||||
// UserProfile
|
||||
//
|
||||
// @Router /v1/users/profile [get]
|
||||
func (a *ApiController) UserProfile(ctx fiber.Ctx) error {
|
||||
tenantCode := ctx.Locals(tenancy.LocalTenantCode)
|
||||
return ctx.JSON(fiber.Map{
|
||||
"id": 0,
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"username": "",
|
||||
"avatar": "",
|
||||
"balance": 0,
|
||||
"tenant": tenantCode,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUsername
|
||||
//
|
||||
// @Router /v1/users/username [put]
|
||||
// @Bind req body
|
||||
func (a *ApiController) UpdateUsername(ctx fiber.Ctx, req *UpdateUsernameReq) error {
|
||||
if req == nil {
|
||||
return errorx.ErrInvalidJSON
|
||||
}
|
||||
name := strings.TrimSpace(req.Username)
|
||||
if name == "" {
|
||||
return errorx.ErrDataValidationFail.WithMsg("username required")
|
||||
}
|
||||
if len([]rune(name)) > 12 {
|
||||
return errorx.ErrDataValidationFail.WithMsg("username too long")
|
||||
}
|
||||
return ctx.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// WechatJSSDK
|
||||
//
|
||||
// @Router /v1/wechats/js-sdk [get]
|
||||
func (a *ApiController) WechatJSSDK(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "wechat js-sdk"})
|
||||
}
|
||||
|
||||
// AdminAuth (stub)
|
||||
//
|
||||
// @Router /v1/admin/auth [post]
|
||||
func (a *ApiController) AdminAuth(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin auth"})
|
||||
}
|
||||
|
||||
// AdminStatistics (stub)
|
||||
//
|
||||
// @Router /v1/admin/statistics [get]
|
||||
func (a *ApiController) AdminStatistics(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin statistics"})
|
||||
}
|
||||
|
||||
// AdminOrders (stub)
|
||||
//
|
||||
// @Router /v1/admin/orders [get]
|
||||
func (a *ApiController) AdminOrders(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin orders list"})
|
||||
}
|
||||
|
||||
// AdminOrderRefund (stub)
|
||||
//
|
||||
// @Router /v1/admin/orders/:id/refund [post]
|
||||
// @Bind id path
|
||||
func (a *ApiController) AdminOrderRefund(ctx fiber.Ctx, id int) error {
|
||||
if id <= 0 {
|
||||
return errorx.ErrInvalidParameter.WithMsg("invalid id")
|
||||
}
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin orders refund"})
|
||||
}
|
||||
|
||||
// AdminMedias (stub)
|
||||
//
|
||||
// @Router /v1/admin/medias [get]
|
||||
func (a *ApiController) AdminMedias(ctx fiber.Ctx) error {
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin medias list"})
|
||||
}
|
||||
|
||||
// AdminMediaShow (stub)
|
||||
//
|
||||
// @Router /v1/admin/medias/:id [get]
|
||||
func (a *ApiController) AdminMediaShow(ctx fiber.Ctx) error {
|
||||
_, err := strconv.ParseInt(ctx.Params("id"), 10, 64)
|
||||
if err != nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("invalid id")
|
||||
}
|
||||
return ctx.Status(fiber.StatusNotImplemented).JSON(fiber.Map{"ok": false, "todo": "admin medias show"})
|
||||
}
|
||||
33
backend/app/http/api/provider.gen.go
Executable file
33
backend/app/http/api/provider.gen.go
Executable file
@@ -0,0 +1,33 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func() (*ApiController, error) {
|
||||
obj := &ApiController{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
apiController *ApiController,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
apiController: apiController,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
114
backend/app/http/api/routes.gen.go
Normal file
114
backend/app/http/api/routes.gen.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// Code generated by atomctl. DO NOT EDIT.
|
||||
|
||||
// Package api provides HTTP route definitions and registration
|
||||
// for the quyun/v2 application.
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
. "go.ipao.vip/atom/fen"
|
||||
)
|
||||
|
||||
// Routes implements the HttpRoute contract and provides route registration
|
||||
// for all controllers in the api module.
|
||||
//
|
||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||
type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
// Controller instances
|
||||
apiController *ApiController
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
func (r *Routes) Prepare() error {
|
||||
r.log = log.WithField("module", "routes.api")
|
||||
r.log.Info("Initializing routes module")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for this routes provider.
|
||||
func (r *Routes) Name() string {
|
||||
return "api"
|
||||
}
|
||||
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: ApiController
|
||||
r.log.Debugf("Registering route: Get /v1/admin/medias -> apiController.AdminMedias")
|
||||
router.Get("/v1/admin/medias", Func0(
|
||||
r.apiController.AdminMedias,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/admin/medias/:id -> apiController.AdminMediaShow")
|
||||
router.Get("/v1/admin/medias/:id", Func0(
|
||||
r.apiController.AdminMediaShow,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/admin/orders -> apiController.AdminOrders")
|
||||
router.Get("/v1/admin/orders", Func0(
|
||||
r.apiController.AdminOrders,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/admin/statistics -> apiController.AdminStatistics")
|
||||
router.Get("/v1/admin/statistics", Func0(
|
||||
r.apiController.AdminStatistics,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/auth/login -> apiController.WeChatOAuthCallback")
|
||||
router.Get("/v1/auth/login", Func0(
|
||||
r.apiController.WeChatOAuthCallback,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/auth/wechat -> apiController.WeChatOAuthStart")
|
||||
router.Get("/v1/auth/wechat", Func0(
|
||||
r.apiController.WeChatOAuthStart,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/posts -> apiController.ListPosts")
|
||||
router.Get("/v1/posts", Func1(
|
||||
r.apiController.ListPosts,
|
||||
Query[PostsQuery]("query"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/posts/:id/play -> apiController.PlayPost")
|
||||
router.Get("/v1/posts/:id/play", Func1(
|
||||
r.apiController.PlayPost,
|
||||
PathParam[int]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/posts/:id/show -> apiController.ShowPost")
|
||||
router.Get("/v1/posts/:id/show", Func1(
|
||||
r.apiController.ShowPost,
|
||||
PathParam[int]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/posts/mine -> apiController.MinePosts")
|
||||
router.Get("/v1/posts/mine", Func1(
|
||||
r.apiController.MinePosts,
|
||||
Query[PostsQuery]("query"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/users/profile -> apiController.UserProfile")
|
||||
router.Get("/v1/users/profile", Func0(
|
||||
r.apiController.UserProfile,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/wechats/js-sdk -> apiController.WechatJSSDK")
|
||||
router.Get("/v1/wechats/js-sdk", Func0(
|
||||
r.apiController.WechatJSSDK,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/admin/auth -> apiController.AdminAuth")
|
||||
router.Post("/v1/admin/auth", Func0(
|
||||
r.apiController.AdminAuth,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/admin/orders/:id/refund -> apiController.AdminOrderRefund")
|
||||
router.Post("/v1/admin/orders/:id/refund", Func1(
|
||||
r.apiController.AdminOrderRefund,
|
||||
PathParam[int]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/posts/:id/buy -> apiController.BuyPost")
|
||||
router.Post("/v1/posts/:id/buy", Func1(
|
||||
r.apiController.BuyPost,
|
||||
PathParam[int]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/users/username -> apiController.UpdateUsername")
|
||||
router.Put("/v1/users/username", Func1(
|
||||
r.apiController.UpdateUsername,
|
||||
Body[UpdateUsernameReq]("req"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
17
backend/app/http/super/auth.go
Normal file
17
backend/app/http/super/auth.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package super
|
||||
|
||||
import (
|
||||
"quyun/v2/providers/app"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type authController struct {
|
||||
app *app.Config
|
||||
}
|
||||
|
||||
func (s *authController) auth(ctx fiber.Ctx) error {
|
||||
// user,err:=
|
||||
return nil
|
||||
}
|
||||
345
backend/app/http/super/controller.go
Normal file
345
backend/app/http/super/controller.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package super
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/providers/app"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type SuperController struct {
|
||||
DB *sql.DB
|
||||
App *app.Config
|
||||
}
|
||||
|
||||
func (s *SuperController) auth(ctx fiber.Ctx) error {
|
||||
token := ""
|
||||
if s.App != nil && s.App.Super != nil {
|
||||
token = strings.TrimSpace(s.App.Super.Token)
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
if s.App != nil && s.App.IsDevMode() {
|
||||
return nil
|
||||
}
|
||||
return errorx.ErrUnauthorized.WithMsg("missing super admin token")
|
||||
}
|
||||
|
||||
auth := strings.TrimSpace(ctx.Get(fiber.HeaderAuthorization))
|
||||
const prefix = "Bearer "
|
||||
if !strings.HasPrefix(auth, prefix) {
|
||||
return errorx.ErrUnauthorized.WithMsg("invalid authorization")
|
||||
}
|
||||
if strings.TrimSpace(strings.TrimPrefix(auth, prefix)) != token {
|
||||
return errorx.ErrUnauthorized.WithMsg("invalid token")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type SuperStatisticsResp struct {
|
||||
TenantsTotal int64 `json:"tenants_total"`
|
||||
TenantsEnabled int64 `json:"tenants_enabled"`
|
||||
TenantAdminsTotal int64 `json:"tenant_admins_total"`
|
||||
TenantAdminsExpired int64 `json:"tenant_admins_expired"`
|
||||
}
|
||||
|
||||
// Statistics
|
||||
//
|
||||
// @Router /super/v1/statistics [get]
|
||||
func (s *SuperController) Statistics(ctx fiber.Ctx) error {
|
||||
if err := s.auth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var out SuperStatisticsResp
|
||||
|
||||
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM tenants`).Scan(&out.TenantsTotal); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM tenants WHERE status = 0`).Scan(&out.TenantsEnabled); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM user_roles WHERE role_code = 'tenant_admin'`).Scan(&out.TenantAdminsTotal); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
if err := s.DB.QueryRowContext(ctx.Context(), `SELECT count(*) FROM user_roles WHERE role_code = 'tenant_admin' AND expires_at IS NOT NULL AND expires_at <= now()`).Scan(&out.TenantAdminsExpired); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
|
||||
return ctx.JSON(out)
|
||||
}
|
||||
|
||||
type TenantsQuery struct {
|
||||
Page int `query:"page"`
|
||||
Limit int `query:"limit"`
|
||||
Keyword string `query:"keyword"`
|
||||
}
|
||||
|
||||
type SuperTenantRow struct {
|
||||
ID int64 `json:"id"`
|
||||
TenantCode string `json:"tenant_code"`
|
||||
TenantUUID string `json:"tenant_uuid"`
|
||||
Name string `json:"name"`
|
||||
Status int16 `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
TenantAdminCount int64 `json:"tenant_admin_count"`
|
||||
TenantAdminExpireAt *time.Time `json:"tenant_admin_expire_at,omitempty"`
|
||||
}
|
||||
|
||||
// Tenants
|
||||
//
|
||||
// @Router /super/v1/tenants [get]
|
||||
// @Bind query query
|
||||
func (s *SuperController) Tenants(ctx fiber.Ctx, query *TenantsQuery) error {
|
||||
if err := s.auth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
page := 1
|
||||
limit := 20
|
||||
keyword := ""
|
||||
if query != nil {
|
||||
if query.Page > 0 {
|
||||
page = query.Page
|
||||
}
|
||||
if query.Limit > 0 {
|
||||
limit = query.Limit
|
||||
}
|
||||
keyword = strings.TrimSpace(query.Keyword)
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
offset := (page - 1) * limit
|
||||
|
||||
where := "1=1"
|
||||
args := []any{}
|
||||
if keyword != "" {
|
||||
where += " AND (lower(t.tenant_code) LIKE $1 OR lower(t.name) LIKE $1)"
|
||||
args = append(args, "%"+strings.ToLower(keyword)+"%")
|
||||
}
|
||||
|
||||
countSQL := "SELECT count(*) FROM tenants t WHERE " + where
|
||||
var total int64
|
||||
if err := s.DB.QueryRowContext(ctx.Context(), countSQL, args...).Scan(&total); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
|
||||
args = append(args, limit, offset)
|
||||
limitArg := "$" + strconv.Itoa(len(args)-1)
|
||||
offsetArg := "$" + strconv.Itoa(len(args))
|
||||
|
||||
sqlStr := `
|
||||
SELECT
|
||||
t.id, t.tenant_code, t.tenant_uuid, t.name, t.status, t.created_at, t.updated_at,
|
||||
COALESCE(a.admin_count, 0) AS admin_count,
|
||||
a.max_expires_at
|
||||
FROM tenants t
|
||||
LEFT JOIN (
|
||||
SELECT tenant_id,
|
||||
count(*) AS admin_count,
|
||||
max(expires_at) AS max_expires_at
|
||||
FROM user_roles
|
||||
WHERE role_code = 'tenant_admin' AND tenant_id IS NOT NULL
|
||||
GROUP BY tenant_id
|
||||
) a ON a.tenant_id = t.id
|
||||
WHERE ` + where + `
|
||||
ORDER BY t.id DESC
|
||||
LIMIT ` + limitArg + ` OFFSET ` + offsetArg
|
||||
|
||||
rows, err := s.DB.QueryContext(ctx.Context(), sqlStr, args...)
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]SuperTenantRow, 0, limit)
|
||||
for rows.Next() {
|
||||
var (
|
||||
it SuperTenantRow
|
||||
uuidStr string
|
||||
)
|
||||
if err := rows.Scan(
|
||||
&it.ID,
|
||||
&it.TenantCode,
|
||||
&uuidStr,
|
||||
&it.Name,
|
||||
&it.Status,
|
||||
&it.CreatedAt,
|
||||
&it.UpdatedAt,
|
||||
&it.TenantAdminCount,
|
||||
&it.TenantAdminExpireAt,
|
||||
); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
it.TenantUUID = uuidStr
|
||||
items = append(items, it)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
|
||||
return ctx.JSON(fiber.Map{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
})
|
||||
}
|
||||
|
||||
type RoleRow struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Status int16 `json:"status"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Roles
|
||||
//
|
||||
// @Router /super/v1/roles [get]
|
||||
func (s *SuperController) Roles(ctx fiber.Ctx) error {
|
||||
if err := s.auth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows, err := s.DB.QueryContext(ctx.Context(), `SELECT code, name, status, updated_at FROM roles ORDER BY code`)
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]RoleRow, 0, 8)
|
||||
for rows.Next() {
|
||||
var it RoleRow
|
||||
if err := rows.Scan(&it.Code, &it.Name, &it.Status, &it.UpdatedAt); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
items = append(items, it)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
|
||||
return ctx.JSON(fiber.Map{"items": items})
|
||||
}
|
||||
|
||||
type UpdateRoleReq struct {
|
||||
Name *string `json:"name"`
|
||||
Status *int16 `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateRole
|
||||
//
|
||||
// @Router /super/v1/roles/:code [put]
|
||||
// @Bind code path
|
||||
// @Bind req body
|
||||
func (s *SuperController) UpdateRole(ctx fiber.Ctx, code string, req *UpdateRoleReq) error {
|
||||
if err := s.auth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code = strings.TrimSpace(code)
|
||||
if code == "" {
|
||||
return errorx.ErrInvalidParameter.WithMsg("missing code")
|
||||
}
|
||||
if req == nil {
|
||||
return errorx.ErrInvalidJSON
|
||||
}
|
||||
|
||||
switch code {
|
||||
case "user", "tenant_admin", "super_admin":
|
||||
default:
|
||||
return errorx.ErrInvalidParameter.WithMsg("unknown role code")
|
||||
}
|
||||
|
||||
set := make([]string, 0, 2)
|
||||
args := make([]any, 0, 3)
|
||||
i := 1
|
||||
|
||||
if req.Name != nil {
|
||||
set = append(set, "name = $"+strconv.Itoa(i))
|
||||
args = append(args, strings.TrimSpace(*req.Name))
|
||||
i++
|
||||
}
|
||||
if req.Status != nil {
|
||||
set = append(set, "status = $"+strconv.Itoa(i))
|
||||
args = append(args, *req.Status)
|
||||
i++
|
||||
}
|
||||
if len(set) == 0 {
|
||||
return ctx.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
set = append(set, "updated_at = now()")
|
||||
args = append(args, code)
|
||||
|
||||
sqlStr := "UPDATE roles SET " + strings.Join(set, ", ") + " WHERE code = $" + strconv.Itoa(i)
|
||||
res, err := s.DB.ExecContext(ctx.Context(), sqlStr, args...)
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
aff, _ := res.RowsAffected()
|
||||
if aff == 0 {
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
return ctx.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
func resolveDistDir(primary, fallback string) string {
|
||||
if st, err := os.Stat(primary); err == nil && st.IsDir() {
|
||||
return primary
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func sendIndex(ctx fiber.Ctx, distDir string) error {
|
||||
indexPath := filepath.Join(distDir, "index.html")
|
||||
if st, err := os.Stat(indexPath); err == nil && !st.IsDir() {
|
||||
return ctx.SendFile(indexPath)
|
||||
}
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
func sendAssetOrIndex(ctx fiber.Ctx, distDir, rel string) error {
|
||||
rel = filepath.Clean(strings.TrimSpace(rel))
|
||||
if rel == "." || rel == "/" {
|
||||
rel = ""
|
||||
}
|
||||
if strings.HasPrefix(rel, "..") {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
if rel != "" {
|
||||
assetPath := filepath.Join(distDir, rel)
|
||||
if st, err := os.Stat(assetPath); err == nil && !st.IsDir() {
|
||||
return ctx.SendFile(assetPath)
|
||||
}
|
||||
}
|
||||
|
||||
return sendIndex(ctx, distDir)
|
||||
}
|
||||
|
||||
// SuperIndex
|
||||
//
|
||||
// @Router /super [get]
|
||||
func (s *SuperController) SuperIndex(ctx fiber.Ctx) error {
|
||||
dist := resolveDistDir("frontend/superadmin/dist", "../frontend/superadmin/dist")
|
||||
return sendIndex(ctx, dist)
|
||||
}
|
||||
|
||||
// SuperWildcard
|
||||
//
|
||||
// @Router /super/* [get]
|
||||
func (s *SuperController) SuperWildcard(ctx fiber.Ctx) error {
|
||||
dist := resolveDistDir("frontend/superadmin/dist", "../frontend/superadmin/dist")
|
||||
return sendAssetOrIndex(ctx, dist, ctx.Params("*"))
|
||||
}
|
||||
54
backend/app/http/super/provider.gen.go
Executable file
54
backend/app/http/super/provider.gen.go
Executable file
@@ -0,0 +1,54 @@
|
||||
package super
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"quyun/v2/providers/app"
|
||||
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
app *app.Config,
|
||||
) (*authController, error) {
|
||||
obj := &authController{
|
||||
app: app,
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
App *app.Config,
|
||||
DB *sql.DB,
|
||||
) (*SuperController, error) {
|
||||
obj := &SuperController{
|
||||
App: App,
|
||||
DB: DB,
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
superController *SuperController,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
superController: superController,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
70
backend/app/http/super/routes.gen.go
Normal file
70
backend/app/http/super/routes.gen.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Code generated by atomctl. DO NOT EDIT.
|
||||
|
||||
// Package super provides HTTP route definitions and registration
|
||||
// for the quyun/v2 application.
|
||||
package super
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
. "go.ipao.vip/atom/fen"
|
||||
)
|
||||
|
||||
// Routes implements the HttpRoute contract and provides route registration
|
||||
// for all controllers in the super module.
|
||||
//
|
||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||
type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
// Controller instances
|
||||
superController *SuperController
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
func (r *Routes) Prepare() error {
|
||||
r.log = log.WithField("module", "routes.super")
|
||||
r.log.Info("Initializing routes module")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for this routes provider.
|
||||
func (r *Routes) Name() string {
|
||||
return "super"
|
||||
}
|
||||
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: SuperController
|
||||
r.log.Debugf("Registering route: Get /super -> superController.SuperIndex")
|
||||
router.Get("/super", Func0(
|
||||
r.superController.SuperIndex,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/* -> superController.SuperWildcard")
|
||||
router.Get("/super/*", Func0(
|
||||
r.superController.SuperWildcard,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/roles -> superController.Roles")
|
||||
router.Get("/super/v1/roles", Func0(
|
||||
r.superController.Roles,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/statistics -> superController.Statistics")
|
||||
router.Get("/super/v1/statistics", Func0(
|
||||
r.superController.Statistics,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /super/v1/tenants -> superController.Tenants")
|
||||
router.Get("/super/v1/tenants", Func1(
|
||||
r.superController.Tenants,
|
||||
Query[TenantsQuery]("query"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /super/v1/roles/:code -> superController.UpdateRole")
|
||||
router.Put("/super/v1/roles/:code", Func2(
|
||||
r.superController.UpdateRole,
|
||||
PathParam[string]("code"),
|
||||
Body[UpdateRoleReq]("req"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
76
backend/app/http/v1/demo.go
Normal file
76
backend/app/http/v1/demo.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type demo struct{}
|
||||
|
||||
type FooUploadReq struct {
|
||||
Folder string `json:"folder" form:"folder"` // 上传到指定文件夹
|
||||
}
|
||||
|
||||
type FooQuery struct {
|
||||
Search string `query:"search"` // 搜索关键词
|
||||
}
|
||||
|
||||
type FooHeader struct {
|
||||
ContentType string `header:"Content-Type"` // 内容类型
|
||||
}
|
||||
type Filter struct {
|
||||
Name string `query:"name"` // 名称
|
||||
Age int `query:"age"` // 年龄
|
||||
}
|
||||
|
||||
type ResponseItem struct{}
|
||||
|
||||
// Foo
|
||||
//
|
||||
// @Summary Test
|
||||
// @Description Test
|
||||
// @Tags Test
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
//
|
||||
// @Param id path int true "ID"
|
||||
// @Param query query Filter true "Filter"
|
||||
// @Param pager query requests.Pagination true "Pager"
|
||||
// @Success 200 {object} requests.Pager{list=ResponseItem} "成功"
|
||||
//
|
||||
// @Router /v1/medias/:id [post]
|
||||
// @Bind query query
|
||||
// @Bind pager query
|
||||
// @Bind header header
|
||||
// @Bind id path
|
||||
// @Bind req body
|
||||
// @Bind file file
|
||||
// @Bind claim local
|
||||
func (d *demo) Foo(
|
||||
ctx fiber.Ctx,
|
||||
id int,
|
||||
pager *requests.Pagination,
|
||||
query *FooQuery,
|
||||
header *FooHeader,
|
||||
claim *jwt.Claims,
|
||||
file *multipart.FileHeader,
|
||||
req *FooUploadReq,
|
||||
) error {
|
||||
_, err := services.Test.Test(ctx)
|
||||
if err != nil {
|
||||
// 示例:在控制器层自定义错误消息/附加数据
|
||||
appErr := errorx.Wrap(err).
|
||||
WithMsg("获取测试失败").
|
||||
WithData(fiber.Map{"route": "/v1/test"}).
|
||||
WithParams("handler", "Test.Hello")
|
||||
return appErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
33
backend/app/http/v1/provider.gen.go
Executable file
33
backend/app/http/v1/provider.gen.go
Executable file
@@ -0,0 +1,33 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func() (*demo, error) {
|
||||
obj := &demo{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
demo *demo,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
demo: demo,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
backend/app/http/v1/routes.gen.go
Normal file
57
backend/app/http/v1/routes.gen.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Code generated by atomctl. DO NOT EDIT.
|
||||
|
||||
// Package v1 provides HTTP route definitions and registration
|
||||
// for the quyun/v2 application.
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
. "go.ipao.vip/atom/fen"
|
||||
"mime/multipart"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/providers/jwt"
|
||||
)
|
||||
|
||||
// Routes implements the HttpRoute contract and provides route registration
|
||||
// for all controllers in the v1 module.
|
||||
//
|
||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||
type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
// Controller instances
|
||||
demo *demo
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
func (r *Routes) Prepare() error {
|
||||
r.log = log.WithField("module", "routes.v1")
|
||||
r.log.Info("Initializing routes module")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for this routes provider.
|
||||
func (r *Routes) Name() string {
|
||||
return "v1"
|
||||
}
|
||||
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: demo
|
||||
r.log.Debugf("Registering route: Post /v1/medias/:id -> demo.Foo")
|
||||
router.Post("/v1/medias/:id", Func7(
|
||||
r.demo.Foo,
|
||||
PathParam[int]("id"),
|
||||
Query[requests.Pagination]("pager"),
|
||||
Query[FooQuery]("query"),
|
||||
Header[FooHeader]("header"),
|
||||
Local[*jwt.Claims]("claim"),
|
||||
File[multipart.FileHeader]("file"),
|
||||
Body[FooUploadReq]("req"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
78
backend/app/http/web/controller.go
Normal file
78
backend/app/http/web/controller.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type WebController struct{}
|
||||
|
||||
func resolveDistDir(primary, fallback string) string {
|
||||
if st, err := os.Stat(primary); err == nil && st.IsDir() {
|
||||
return primary
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func sendIndex(ctx fiber.Ctx, distDir string) error {
|
||||
indexPath := filepath.Join(distDir, "index.html")
|
||||
if st, err := os.Stat(indexPath); err == nil && !st.IsDir() {
|
||||
return ctx.SendFile(indexPath)
|
||||
}
|
||||
return fiber.ErrNotFound
|
||||
}
|
||||
|
||||
func sendAssetOrIndex(ctx fiber.Ctx, distDir, rel string) error {
|
||||
rel = filepath.Clean(strings.TrimSpace(rel))
|
||||
if rel == "." || rel == "/" {
|
||||
rel = ""
|
||||
}
|
||||
if strings.HasPrefix(rel, "..") {
|
||||
return fiber.ErrBadRequest
|
||||
}
|
||||
|
||||
if rel != "" {
|
||||
assetPath := filepath.Join(distDir, rel)
|
||||
if st, err := os.Stat(assetPath); err == nil && !st.IsDir() {
|
||||
return ctx.SendFile(assetPath)
|
||||
}
|
||||
}
|
||||
|
||||
return sendIndex(ctx, distDir)
|
||||
}
|
||||
|
||||
// AdminIndex
|
||||
//
|
||||
// @Router /admin [get]
|
||||
func (w *WebController) AdminIndex(ctx fiber.Ctx) error {
|
||||
adminDist := resolveDistDir("frontend/admin/dist", "../frontend/admin/dist")
|
||||
return sendIndex(ctx, adminDist)
|
||||
}
|
||||
|
||||
// AdminWildcard
|
||||
//
|
||||
// @Router /admin/* [get]
|
||||
func (w *WebController) AdminWildcard(ctx fiber.Ctx) error {
|
||||
adminDist := resolveDistDir("frontend/admin/dist", "../frontend/admin/dist")
|
||||
return sendAssetOrIndex(ctx, adminDist, ctx.Params("*"))
|
||||
}
|
||||
|
||||
// UserIndex
|
||||
//
|
||||
// @Router / [get]
|
||||
func (w *WebController) UserIndex(ctx fiber.Ctx) error {
|
||||
userDist := resolveDistDir("frontend/user/dist", "../frontend/user/dist")
|
||||
return sendIndex(ctx, userDist)
|
||||
}
|
||||
|
||||
// UserWildcard
|
||||
//
|
||||
// @Router /* [get]
|
||||
func (w *WebController) UserWildcard(ctx fiber.Ctx) error {
|
||||
userDist := resolveDistDir("frontend/user/dist", "../frontend/user/dist")
|
||||
return sendAssetOrIndex(ctx, userDist, ctx.Params("*"))
|
||||
}
|
||||
33
backend/app/http/web/provider.gen.go
Executable file
33
backend/app/http/web/provider.gen.go
Executable file
@@ -0,0 +1,33 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func() (*WebController, error) {
|
||||
obj := &WebController{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
webController *WebController,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
webController: webController,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupRoutes); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
59
backend/app/http/web/routes.gen.go
Normal file
59
backend/app/http/web/routes.gen.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Code generated by atomctl. DO NOT EDIT.
|
||||
|
||||
// Package web provides HTTP route definitions and registration
|
||||
// for the quyun/v2 application.
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
. "go.ipao.vip/atom/fen"
|
||||
)
|
||||
|
||||
// Routes implements the HttpRoute contract and provides route registration
|
||||
// for all controllers in the web module.
|
||||
//
|
||||
// @provider contracts.HttpRoute atom.GroupRoutes
|
||||
type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
// Controller instances
|
||||
webController *WebController
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
func (r *Routes) Prepare() error {
|
||||
r.log = log.WithField("module", "routes.web")
|
||||
r.log.Info("Initializing routes module")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Name returns the unique identifier for this routes provider.
|
||||
func (r *Routes) Name() string {
|
||||
return "web"
|
||||
}
|
||||
|
||||
// Register registers all HTTP routes with the provided fiber router.
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: WebController
|
||||
r.log.Debugf("Registering route: Get / -> webController.UserIndex")
|
||||
router.Get("/", Func0(
|
||||
r.webController.UserIndex,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /* -> webController.UserWildcard")
|
||||
router.Get("/*", Func0(
|
||||
r.webController.UserWildcard,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /admin -> webController.AdminIndex")
|
||||
router.Get("/admin", Func0(
|
||||
r.webController.AdminIndex,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /admin/* -> webController.AdminWildcard")
|
||||
router.Get("/admin/*", Func0(
|
||||
r.webController.AdminWildcard,
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
36
backend/app/jobs/demo_cron.go
Normal file
36
backend/app/jobs/demo_cron.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
. "github.com/riverqueue/river"
|
||||
"github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
)
|
||||
|
||||
var _ contracts.CronJob = (*DemoCronJob)(nil)
|
||||
|
||||
// @provider(cronjob)
|
||||
type DemoCronJob struct {
|
||||
log *logrus.Entry `inject:"false"`
|
||||
}
|
||||
|
||||
// Prepare implements contracts.CronJob.
|
||||
func (DemoCronJob) Prepare() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// JobArgs implements contracts.CronJob.
|
||||
func (DemoCronJob) Args() []contracts.CronJobArg {
|
||||
return []contracts.CronJobArg{
|
||||
{
|
||||
Arg: DemoJob{
|
||||
Strings: []string{"a", "b", "c", "d"},
|
||||
},
|
||||
|
||||
PeriodicInterval: PeriodicInterval(time.Second * 10),
|
||||
RunOnStart: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
53
backend/app/jobs/demo_job.go
Normal file
53
backend/app/jobs/demo_job.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
. "github.com/riverqueue/river"
|
||||
log "github.com/sirupsen/logrus"
|
||||
_ "go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
_ "go.ipao.vip/atom/contracts"
|
||||
)
|
||||
|
||||
var _ contracts.JobArgs = DemoJob{}
|
||||
|
||||
type DemoJob struct {
|
||||
Strings []string `json:"strings"`
|
||||
}
|
||||
|
||||
func (s DemoJob) InsertOpts() InsertOpts {
|
||||
return InsertOpts{
|
||||
Queue: QueueDefault,
|
||||
Priority: PriorityDefault,
|
||||
}
|
||||
}
|
||||
|
||||
func (DemoJob) Kind() string { return "demo_job" }
|
||||
func (a DemoJob) UniqueID() string { return a.Kind() }
|
||||
|
||||
var _ Worker[DemoJob] = (*DemoJobWorker)(nil)
|
||||
|
||||
// @provider(job)
|
||||
type DemoJobWorker struct {
|
||||
WorkerDefaults[DemoJob]
|
||||
}
|
||||
|
||||
func (w *DemoJobWorker) NextRetry(job *Job[DemoJob]) time.Time {
|
||||
return time.Now().Add(30 * time.Second)
|
||||
}
|
||||
|
||||
func (w *DemoJobWorker) Work(ctx context.Context, job *Job[DemoJob]) error {
|
||||
logger := log.WithField("job", job.Args.Kind())
|
||||
|
||||
logger.Infof("[START] %s args: %v", job.Args.Kind(), job.Args.Strings)
|
||||
defer logger.Infof("[END] %s", job.Args.Kind())
|
||||
|
||||
// modify below
|
||||
sort.Strings(job.Args.Strings)
|
||||
logger.Infof("[%s] Sorted strings: %v\n", time.Now().Format(time.TimeOnly), job.Args.Strings)
|
||||
|
||||
return nil
|
||||
}
|
||||
56
backend/app/jobs/demo_job_test.go
Normal file
56
backend/app/jobs/demo_job_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build legacytests
|
||||
// +build legacytests
|
||||
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"quyun/v2/app/commands/testx"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
. "github.com/riverqueue/river"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/suite"
|
||||
_ "go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
type DemoJobSuiteInjectParams struct {
|
||||
dig.In
|
||||
|
||||
Initials []contracts.Initial `group:"initials"` // nolint:structcheck
|
||||
}
|
||||
|
||||
type DemoJobSuite struct {
|
||||
suite.Suite
|
||||
|
||||
DemoJobSuiteInjectParams
|
||||
}
|
||||
|
||||
func Test_DemoJob(t *testing.T) {
|
||||
providers := testx.Default().With(Provide, services.Provide)
|
||||
|
||||
testx.Serve(providers, t, func(p DemoJobSuiteInjectParams) {
|
||||
suite.Run(t, &DemoJobSuite{DemoJobSuiteInjectParams: p})
|
||||
})
|
||||
}
|
||||
|
||||
func (t *DemoJobSuite) Test_Work() {
|
||||
Convey("test_work", t.T(), func() {
|
||||
Convey("step 1", func() {
|
||||
job := &Job[DemoJob]{
|
||||
Args: DemoJob{
|
||||
Strings: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
worker := &DemoJobWorker{}
|
||||
|
||||
err := worker.Work(context.Background(), job)
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
41
backend/app/jobs/provider.gen.go
Executable file
41
backend/app/jobs/provider.gen.go
Executable file
@@ -0,0 +1,41 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"quyun/v2/providers/job"
|
||||
|
||||
"github.com/riverqueue/river"
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
__job *job.Job,
|
||||
) (contracts.Initial, error) {
|
||||
obj := &DemoCronJob{}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
container.Later(func() error { return __job.AddPeriodicJobs(obj) })
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupInitial); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
__job *job.Job,
|
||||
) (contracts.Initial, error) {
|
||||
obj := &DemoJobWorker{}
|
||||
if err := river.AddWorkerSafely(__job.Workers, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupInitial); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
9
backend/app/middlewares/mid_debug.go
Normal file
9
backend/app/middlewares/mid_debug.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func (f *Middlewares) DebugMode(c fiber.Ctx) error {
|
||||
return c.Next()
|
||||
}
|
||||
15
backend/app/middlewares/middlewares.go
Normal file
15
backend/app/middlewares/middlewares.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type Middlewares struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
}
|
||||
|
||||
func (f *Middlewares) Prepare() error {
|
||||
f.log = log.WithField("module", "middleware")
|
||||
return nil
|
||||
}
|
||||
20
backend/app/middlewares/provider.gen.go
Executable file
20
backend/app/middlewares/provider.gen.go
Executable file
@@ -0,0 +1,20 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/opt"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func() (*Middlewares, error) {
|
||||
obj := &Middlewares{}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
30
backend/app/requests/pagination.go
Normal file
30
backend/app/requests/pagination.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package requests
|
||||
|
||||
import "github.com/samber/lo"
|
||||
|
||||
type Pager struct {
|
||||
Pagination `json:",inline"`
|
||||
Total int64 `json:"total"`
|
||||
Items any `json:"items"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int64 `json:"page" form:"page" query:"page"`
|
||||
Limit int64 `json:"limit" form:"limit" query:"limit"`
|
||||
}
|
||||
|
||||
func (filter *Pagination) Offset() int64 {
|
||||
return (filter.Page - 1) * filter.Limit
|
||||
}
|
||||
|
||||
func (filter *Pagination) Format() *Pagination {
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
|
||||
if !lo.Contains([]int64{10, 20, 50, 100}, filter.Limit) {
|
||||
filter.Limit = 10
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
41
backend/app/requests/sort.go
Normal file
41
backend/app/requests/sort.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type SortQueryFilter struct {
|
||||
Asc *string `json:"asc" form:"asc"`
|
||||
Desc *string `json:"desc" form:"desc"`
|
||||
}
|
||||
|
||||
func (s *SortQueryFilter) AscFields() []string {
|
||||
if s.Asc == nil {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(*s.Asc, ",")
|
||||
}
|
||||
|
||||
func (s *SortQueryFilter) DescFields() []string {
|
||||
if s.Desc == nil {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(*s.Desc, ",")
|
||||
}
|
||||
|
||||
func (s *SortQueryFilter) DescID() *SortQueryFilter {
|
||||
if s.Desc == nil {
|
||||
s.Desc = lo.ToPtr("id")
|
||||
}
|
||||
|
||||
items := s.DescFields()
|
||||
if lo.Contains(items, "id") {
|
||||
return s
|
||||
}
|
||||
|
||||
items = append(items, "id")
|
||||
s.Desc = lo.ToPtr(strings.Join(items, ","))
|
||||
return s
|
||||
}
|
||||
45
backend/app/services/provider.gen.go
Executable file
45
backend/app/services/provider.gen.go
Executable file
@@ -0,0 +1,45 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/container"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/atom/opt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Provide(opts ...opt.Option) error {
|
||||
if err := container.Container.Provide(func(
|
||||
db *gorm.DB,
|
||||
test *test,
|
||||
user *user,
|
||||
) (contracts.Initial, error) {
|
||||
obj := &services{
|
||||
db: db,
|
||||
test: test,
|
||||
user: user,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}, atom.GroupInitial); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*test, error) {
|
||||
obj := &test{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*user, error) {
|
||||
obj := &user{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
31
backend/app/services/services.gen.go
Normal file
31
backend/app/services/services.gen.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var _db *gorm.DB
|
||||
|
||||
// exported CamelCase Services
|
||||
var (
|
||||
Test *test
|
||||
User *user
|
||||
)
|
||||
|
||||
// @provider(model)
|
||||
type services struct {
|
||||
db *gorm.DB
|
||||
// define Services
|
||||
test *test
|
||||
user *user
|
||||
}
|
||||
|
||||
func (svc *services) Prepare() error {
|
||||
_db = svc.db
|
||||
|
||||
// set exported Services here
|
||||
Test = svc.test
|
||||
User = svc.user
|
||||
|
||||
return nil
|
||||
}
|
||||
10
backend/app/services/test.go
Normal file
10
backend/app/services/test.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package services
|
||||
|
||||
import "context"
|
||||
|
||||
// @provider
|
||||
type test struct{}
|
||||
|
||||
func (t *test) Test(ctx context.Context) (string, error) {
|
||||
return "Test", nil
|
||||
}
|
||||
44
backend/app/services/test_test.go
Normal file
44
backend/app/services/test_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
//go:build legacytests
|
||||
// +build legacytests
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"quyun/v2/app/commands/testx"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
_ "go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
type TestSuiteInjectParams struct {
|
||||
dig.In
|
||||
|
||||
Initials []contracts.Initial `group:"initials"` // nolint:structcheck
|
||||
}
|
||||
|
||||
type TestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
TestSuiteInjectParams
|
||||
}
|
||||
|
||||
func Test_Test(t *testing.T) {
|
||||
providers := testx.Default().With(Provide)
|
||||
|
||||
testx.Serve(providers, t, func(p TestSuiteInjectParams) {
|
||||
suite.Run(t, &TestSuite{TestSuiteInjectParams: p})
|
||||
})
|
||||
}
|
||||
|
||||
func (t *TestSuite) Test_Test() {
|
||||
Convey("test_work", t.T(), func() {
|
||||
t.T().Log("start test at", time.Now())
|
||||
})
|
||||
}
|
||||
33
backend/app/services/user.go
Normal file
33
backend/app/services/user.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"quyun/v2/database/models"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type user struct{}
|
||||
|
||||
func (t *user) FindByUsername(ctx context.Context, username string) (*models.User, error) {
|
||||
tbl, query := models.UserQuery.QueryContext(ctx)
|
||||
|
||||
model, err := query.Where(tbl.Username.Eq(username)).First()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "FindByusername failed, %s", username)
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func (t *user) Create(ctx context.Context, user *models.User) (*models.User, error) {
|
||||
if err := user.EncryptPassword(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "encrypt user password failed")
|
||||
}
|
||||
|
||||
if err := user.Create(ctx); err != nil {
|
||||
return nil, errors.Wrapf(err, "Create user failed, %s", user.Username)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
96
backend/app/services/user_test.go
Normal file
96
backend/app/services/user_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"quyun/v2/app/commands/testx"
|
||||
"quyun/v2/database"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
_ "go.ipao.vip/atom"
|
||||
"go.ipao.vip/atom/contracts"
|
||||
"go.ipao.vip/gen/types"
|
||||
"go.uber.org/dig"
|
||||
)
|
||||
|
||||
type UserTestSuiteInjectParams struct {
|
||||
dig.In
|
||||
|
||||
DB *sql.DB
|
||||
Initials []contracts.Initial `group:"initials"` // nolint:structcheck
|
||||
}
|
||||
|
||||
type UserTestSuite struct {
|
||||
suite.Suite
|
||||
|
||||
UserTestSuiteInjectParams
|
||||
}
|
||||
|
||||
func Test_User(t *testing.T) {
|
||||
providers := testx.Default().With(Provide)
|
||||
|
||||
testx.Serve(providers, t, func(p UserTestSuiteInjectParams) {
|
||||
suite.Run(t, &UserTestSuite{UserTestSuiteInjectParams: p})
|
||||
})
|
||||
}
|
||||
|
||||
func (t *UserTestSuite) Test_Create() {
|
||||
Convey("test user create", t.T(), func() {
|
||||
database.Truncate(t.T().Context(), t.DB, models.TableNameUser)
|
||||
|
||||
m := &models.User{
|
||||
Username: "test-user",
|
||||
Password: "test-password",
|
||||
Roles: types.NewArray([]consts.Role{consts.RoleUser}),
|
||||
Status: consts.UserStatusPendingVerify,
|
||||
}
|
||||
|
||||
err := m.Create(t.T().Context())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(m.ID, ShouldBeGreaterThan, 0)
|
||||
|
||||
same := m.ComparePassword(t.T().Context(), "test-password")
|
||||
So(same, ShouldBeTrue)
|
||||
|
||||
same = m.ComparePassword(t.T().Context(), "test-password1")
|
||||
So(same, ShouldBeFalse)
|
||||
})
|
||||
}
|
||||
|
||||
// FindByUsername
|
||||
func (t *UserTestSuite) Test_FindByUsername() {
|
||||
Convey("test user FindByUsername", t.T(), func() {
|
||||
database.Truncate(t.T().Context(), t.DB, models.TableNameUser)
|
||||
|
||||
Convey("user table is empty", func() {
|
||||
m, err := User.FindByUsername(t.T().Context(), "test-user")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(m, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("insert one record", func() {
|
||||
username := "test-user"
|
||||
m := &models.User{
|
||||
Username: username,
|
||||
Password: "test-password",
|
||||
Roles: types.NewArray([]consts.Role{consts.RoleUser}),
|
||||
Status: consts.UserStatusPendingVerify,
|
||||
}
|
||||
|
||||
err := m.Create(t.T().Context())
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("user table is not empty", func() {
|
||||
m, err := User.FindByUsername(t.T().Context(), username)
|
||||
So(err, ShouldBeNil)
|
||||
So(m, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
71
backend/app/tenancy/tenancy.go
Normal file
71
backend/app/tenancy/tenancy.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package tenancy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
LocalTenantCode = "tenant_code"
|
||||
LocalTenantID = "tenant_id"
|
||||
LocalTenantUUID = "tenant_uuid"
|
||||
)
|
||||
|
||||
var tenantCodeRe = regexp.MustCompile(`^[a-z0-9_-]+$`)
|
||||
|
||||
type Tenant struct {
|
||||
ID int64
|
||||
Code string
|
||||
UUID uuid.UUID
|
||||
}
|
||||
|
||||
func ResolveTenant(c fiber.Ctx, db *sql.DB) (*Tenant, error) {
|
||||
raw := strings.TrimSpace(c.Params("tenant_code"))
|
||||
code := strings.ToLower(raw)
|
||||
if code == "" || !tenantCodeRe.MatchString(code) {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("invalid tenant_code")
|
||||
}
|
||||
|
||||
var (
|
||||
id int64
|
||||
tenantUUID uuid.UUID
|
||||
status int16
|
||||
)
|
||||
err := db.QueryRowContext(
|
||||
c.Context(),
|
||||
`SELECT id, tenant_uuid, status FROM tenants WHERE lower(tenant_code) = $1 LIMIT 1`,
|
||||
code,
|
||||
).Scan(&id, &tenantUUID, &status)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fiber.ErrNotFound
|
||||
}
|
||||
return nil, errorx.ErrDatabaseError.WithMsg("database error").WithParams(err.Error())
|
||||
}
|
||||
|
||||
// status: 0 enabled (by default)
|
||||
if status != 0 {
|
||||
return nil, fiber.ErrNotFound
|
||||
}
|
||||
|
||||
return &Tenant{ID: id, Code: code, UUID: tenantUUID}, nil
|
||||
}
|
||||
|
||||
func Middleware(db *sql.DB) fiber.Handler {
|
||||
return func(c fiber.Ctx) error {
|
||||
tenant, err := ResolveTenant(c, db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Locals(LocalTenantCode, tenant.Code)
|
||||
c.Locals(LocalTenantID, tenant.ID)
|
||||
c.Locals(LocalTenantUUID, tenant.UUID.String())
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user