chore: stabilize lint and verify builds

This commit is contained in:
2026-02-06 11:51:32 +08:00
parent edede17880
commit 1782f64417
114 changed files with 3032 additions and 1345 deletions

View File

@@ -3,16 +3,18 @@ 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/errorx"
"quyun/v2/app/events/subscribers"
"quyun/v2/providers/app"
"quyun/v2/providers/event"
"quyun/v2/providers/postgres"
log "github.com/sirupsen/logrus"
"go.ipao.vip/atom"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/contracts"
logrus "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.uber.org/dig"
)
@@ -45,14 +47,19 @@ type Service struct {
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{})
func Serve(_ *cobra.Command, _ []string) error {
err := container.Container.Invoke(func(ctx context.Context, svc Service) error {
logrus.SetFormatter(&logrus.JSONFormatter{})
if svc.App.IsDevMode() {
log.SetLevel(log.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
}
return svc.PubSub.Serve(ctx)
})
if err != nil {
return errorx.ErrOperationFailed.WithCause(err)
}
return nil
}

View File

@@ -1,17 +1,18 @@
package grpc
import (
"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/grpc/users"
"quyun/v2/providers/app"
"quyun/v2/providers/grpc"
"quyun/v2/providers/postgres"
log "github.com/sirupsen/logrus"
logrus "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.ipao.vip/atom"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/contracts"
"go.uber.org/dig"
)
@@ -44,14 +45,19 @@ type Service struct {
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{})
func Serve(_ *cobra.Command, _ []string) error {
err := container.Container.Invoke(func(svc Service) error {
logrus.SetFormatter(&logrus.JSONFormatter{})
if svc.App.IsDevMode() {
log.SetLevel(log.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
}
return svc.Grpc.Serve()
})
if err != nil {
return errorx.ErrOperationFailed.WithCause(err)
}
return nil
}

View File

@@ -11,7 +11,7 @@ import (
"quyun/v2/app/middlewares"
"quyun/v2/app/services"
"quyun/v2/database"
_ "quyun/v2/docs"
docs "quyun/v2/docs"
"quyun/v2/providers/app"
"quyun/v2/providers/http"
"quyun/v2/providers/http/swagger"
@@ -25,7 +25,7 @@ import (
"go.ipao.vip/atom/contracts"
"github.com/gofiber/fiber/v3/middleware/favicon"
log "github.com/sirupsen/logrus"
logrus "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.uber.org/dig"
)
@@ -65,31 +65,36 @@ type Service struct {
App *app.Config
Job *job.Job
Http *http.Service
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{})
func Serve(_ *cobra.Command, _ []string) error {
if err := container.Container.Invoke(func(ctx context.Context, svc Service) error {
_ = docs.SwaggerSpec
logrus.SetFormatter(&logrus.JSONFormatter{})
if svc.App.Mode == app.AppModeDevelopment {
log.SetLevel(log.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
svc.Http.Engine.Get("/swagger/*", swagger.HandlerDefault)
svc.HTTP.Engine.Get("/swagger/*", swagger.HandlerDefault)
}
svc.Http.Engine.Use(errorx.Middleware)
svc.Http.Engine.Use(favicon.New(favicon.Config{
svc.HTTP.Engine.Use(errorx.Middleware)
svc.HTTP.Engine.Use(favicon.New(favicon.Config{
Data: []byte{},
}))
for _, route := range svc.Routes {
group := svc.Http.Engine.Group(route.Path(), route.Middlewares()...).Name(route.Name())
group := svc.HTTP.Engine.Group(route.Path(), route.Middlewares()...).Name(route.Name())
route.Register(group)
}
return svc.Http.Serve(ctx)
})
return svc.HTTP.Serve(ctx)
}); err != nil {
return errorx.ErrOperationFailed.WithCause(err)
}
return nil
}

View File

@@ -5,11 +5,12 @@ import (
"database/sql"
"quyun/v2/app/commands"
"quyun/v2/app/errorx"
"quyun/v2/database"
"quyun/v2/providers/postgres"
"github.com/pressly/goose/v3"
log "github.com/sirupsen/logrus"
logrus "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.ipao.vip/atom"
"go.ipao.vip/atom/container"
@@ -42,8 +43,8 @@ type Service struct {
}
// migrate
func Serve(cmd *cobra.Command, args []string) error {
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
func Serve(_ *cobra.Command, args []string) error {
err := container.Container.Invoke(func(ctx context.Context, svc Service) error {
if len(args) == 0 {
args = append(args, "up")
}
@@ -53,7 +54,7 @@ func Serve(cmd *cobra.Command, args []string) error {
}
action, args := args[0], args[1:]
log.Infof("migration action: %s args: %+v", action, args)
logrus.Infof("migration action: %s args: %+v", action, args)
goose.SetBaseFS(database.MigrationFS)
goose.SetTableName("migrations")
@@ -66,6 +67,7 @@ func Serve(cmd *cobra.Command, args []string) error {
}
_, err = migrator.Migrate(ctx, rivermigrate.DirectionUp, &rivermigrate.MigrateOpts{TargetVersion: -1})
return err
},
func(ctx context.Context, db *sql.DB) error {
@@ -75,9 +77,15 @@ func Serve(cmd *cobra.Command, args []string) error {
}
_, err = migrator.Migrate(ctx, rivermigrate.DirectionDown, &rivermigrate.MigrateOpts{TargetVersion: -1})
return err
})
return goose.RunContext(context.Background(), action, svc.DB, "migrations", args...)
return goose.RunContext(ctx, action, svc.DB, "migrations", args...)
})
if err != nil {
return errorx.ErrOperationFailed.WithCause(err)
}
return nil
}

View File

@@ -5,19 +5,21 @@ import (
"github.com/riverqueue/river"
"github.com/riverqueue/river/rivertype"
log "github.com/sirupsen/logrus"
logrus "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)
func (*CustomErrorHandler) HandleError(_ context.Context, _ *rivertype.JobRow, err error) *river.ErrorHandlerResult {
logrus.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)
func (*CustomErrorHandler) HandlePanic(_ context.Context, _ *rivertype.JobRow, panicVal any, trace string) *river.ErrorHandlerResult {
logrus.Infof("Job panicked with: %v\n", panicVal)
logrus.Infof("Stack trace: %s\n", trace)
return &river.ErrorHandlerResult{
SetCancelled: true,
}

View File

@@ -3,11 +3,8 @@ 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/errorx"
"quyun/v2/app/jobs"
"quyun/v2/app/services"
"quyun/v2/database"
@@ -17,8 +14,11 @@ import (
"quyun/v2/providers/postgres"
"quyun/v2/providers/storage"
log "github.com/sirupsen/logrus"
logrus "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.ipao.vip/atom"
"go.ipao.vip/atom/container"
"go.ipao.vip/atom/contracts"
"go.uber.org/dig"
)
@@ -56,20 +56,25 @@ type Service struct {
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{})
func Serve(_ *cobra.Command, _ []string) error {
if err := container.Container.Invoke(func(ctx context.Context, svc Service) error {
logrus.SetFormatter(&logrus.JSONFormatter{})
if svc.App.IsDevMode() {
log.SetLevel(log.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
}
if err := svc.Job.Start(ctx); err != nil {
return err
return errorx.ErrOperationFailed.WithCause(err)
}
defer svc.Job.Close()
<-ctx.Done()
return nil
})
}); err != nil {
return errorx.ErrOperationFailed.WithCause(err)
}
return nil
}

View File

@@ -2,11 +2,13 @@ package seed
import (
"context"
"crypto/rand"
"fmt"
"math/rand"
"math/big"
"time"
"quyun/v2/app/commands"
"quyun/v2/app/errorx"
"quyun/v2/database"
"quyun/v2/database/fields"
"quyun/v2/database/models"
@@ -45,8 +47,8 @@ type Service struct {
DB *gorm.DB
}
func Serve(cmd *cobra.Command, args []string) error {
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
func Serve(_ *cobra.Command, _ []string) error {
err := container.Container.Invoke(func(ctx context.Context, svc Service) error {
models.SetDefault(svc.DB)
fmt.Println("Cleaning existing data...")
@@ -137,10 +139,14 @@ func Serve(cmd *cobra.Command, args []string) error {
}
// 2. Tenant
tenantCodeSuffix, err := randomIntString(1000)
if err != nil {
return fmt.Errorf("generate tenant code: %w", err)
}
tenant := &models.Tenant{
UserID: creator.ID,
Name: "梅派艺术工作室",
Code: "meipai_" + cast.ToString(rand.Intn(1000)),
Code: "meipai_" + tenantCodeSuffix,
UUID: types.UUID(uuid.New()),
Status: consts.TenantStatusVerified,
}
@@ -189,6 +195,15 @@ func Serve(cmd *cobra.Command, args []string) error {
price = 990
} // 9.90
viewsValue, err := randomIntWithLimit(10000)
if err != nil {
return fmt.Errorf("generate views: %w", err)
}
likesValue, err := randomIntWithLimit(1000)
if err != nil {
return fmt.Errorf("generate likes: %w", err)
}
c := &models.Content{
TenantID: tenant.ID,
UserID: creator.ID,
@@ -197,8 +212,8 @@ func Serve(cmd *cobra.Command, args []string) error {
Genre: "京剧",
Status: consts.ContentStatusPublished,
Visibility: consts.ContentVisibilityPublic,
Views: int32(rand.Intn(10000)),
Likes: int32(rand.Intn(1000)),
Views: int32(viewsValue),
Likes: int32(likesValue),
}
if err := models.ContentQuery.WithContext(ctx).Create(c); err == nil {
seededContents = append(seededContents, c)
@@ -413,10 +428,14 @@ func Serve(cmd *cobra.Command, args []string) error {
DecidedOperatorUserID: creator.ID,
DecidedReason: "符合要求",
})
inviteSuffix, err := randomIntWithLimit(100000)
if err != nil {
return fmt.Errorf("generate invite code: %w", err)
}
models.TenantInviteQuery.WithContext(ctx).Create(&models.TenantInvite{
TenantID: tenant.ID,
UserID: creator.ID,
Code: "invite" + cast.ToString(rand.Intn(100000)),
Code: "invite" + cast.ToString(inviteSuffix),
Status: "active",
MaxUses: 5,
UsedCount: 0,
@@ -459,6 +478,7 @@ func Serve(cmd *cobra.Command, args []string) error {
})
// 8. System config
models.SystemConfigQuery.WithContext(ctx).Create(&models.SystemConfig{
ConfigKey: "site_name",
Value: types.JSON([]byte(`{"value":"曲韵平台"}`)),
@@ -577,6 +597,34 @@ func Serve(cmd *cobra.Command, args []string) error {
}
fmt.Println("Seed done.")
return nil
})
if err != nil {
return errorx.ErrOperationFailed.WithCause(err)
}
return nil
}
func randomIntString(limitValue int64) (string, error) {
value, err := randomIntWithLimit(limitValue)
if err != nil {
return "", err
}
return cast.ToString(value), nil
}
func randomIntWithLimit(limitValue int64) (int64, error) {
if limitValue <= 0 {
return 0, nil
}
limit := big.NewInt(limitValue)
value, err := rand.Int(rand.Reader, limit)
if err != nil {
return 0, errorx.ErrOperationFailed.WithCause(err)
}
return value.Int64(), nil
}

View File

@@ -1,9 +1,10 @@
package commands
import (
"go.ipao.vip/atom/container"
"quyun/v2/providers/app"
"quyun/v2/providers/event"
"go.ipao.vip/atom/container"
)
func Default(providers ...container.ProviderContainer) container.Providers {

View File

@@ -2,7 +2,7 @@ package storage_migrate
import (
"context"
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
@@ -36,7 +36,7 @@ func defaultProviders() container.Providers {
func Command() atom.Option {
return atom.Command(
atom.Name("storage-migrate"),
atom.Short("migrate media assets to md5 object keys"),
atom.Short("migrate media assets to sha256 object keys"),
atom.Arguments(func(cmd *cobra.Command) {
cmd.Flags().Bool("dry-run", false, "preview changes without writing")
cmd.Flags().Int("batch", 200, "batch size per scan")
@@ -54,7 +54,7 @@ type Service struct {
Storage *storage.Storage
}
func Serve(cmd *cobra.Command, args []string) error {
func Serve(cmd *cobra.Command, _ []string) error {
return container.Container.Invoke(func(ctx context.Context, svc Service) error {
models.SetDefault(svc.DB)
@@ -84,7 +84,6 @@ func Serve(cmd *cobra.Command, args []string) error {
}
for _, asset := range list {
// 仅处理本地存储且有实际文件路径的资源。
if strings.ToLower(asset.Provider) != "local" {
continue
}
@@ -96,13 +95,15 @@ func Serve(cmd *cobra.Command, args []string) error {
}
srcPath := asset.ObjectKey
if !filepath.IsAbs(srcPath) {
srcPath = filepath.Join(localPath, filepath.FromSlash(srcPath))
}
hash, size, err := fileMD5(srcPath)
hash, size, err := fileSHA256(srcPath)
if err != nil {
fmt.Printf("skip asset=%d err=%v\n", asset.ID, err)
continue
}
@@ -164,26 +165,27 @@ func Serve(cmd *cobra.Command, args []string) error {
}
func buildObjectKey(tenant *models.Tenant, hash, filename string) string {
// 按租户维度组织对象路径quyun/<tenant_uuid>/<md5>.<ext>
tenantUUID := "public"
if tenant != nil && tenant.UUID.String() != "" {
tenantUUID = tenant.UUID.String()
}
ext := strings.ToLower(filepath.Ext(filename))
return path.Join("quyun", tenantUUID, hash+ext)
}
func fileMD5(filename string) (string, int64, error) {
func fileSHA256(filename string) (string, int64, error) {
f, err := os.Open(filename)
if err != nil {
return "", 0, err
}
defer f.Close()
h := md5.New()
h := sha256.New()
size, err := io.Copy(h, f)
if err != nil {
return "", size, err
}
return hex.EncodeToString(h.Sum(nil)), size, nil
}

View File

@@ -2,6 +2,7 @@ package testx
import (
"context"
"fmt"
"os"
"testing"
@@ -21,7 +22,7 @@ import (
"go.uber.org/dig"
"github.com/rogeecn/fabfile"
. "github.com/smartystreets/goconvey/convey"
convey "github.com/smartystreets/goconvey/convey"
)
func Default(providers ...container.ProviderContainer) container.Providers {
@@ -39,7 +40,7 @@ type orderRefundTestWorker struct {
river.WorkerDefaults[jobs_args.OrderRefundJob]
}
func (w *orderRefundTestWorker) Work(ctx context.Context, job *river.Job[jobs_args.OrderRefundJob]) error {
func (w *orderRefundTestWorker) Work(_ context.Context, _ *river.Job[jobs_args.OrderRefundJob]) error {
return nil
}
@@ -47,7 +48,7 @@ type mediaAssetProcessTestWorker struct {
river.WorkerDefaults[jobs_args.MediaAssetProcessJob]
}
func (w *mediaAssetProcessTestWorker) Work(ctx context.Context, job *river.Job[jobs_args.MediaAssetProcessJob]) error {
func (w *mediaAssetProcessTestWorker) Work(_ context.Context, _ *river.Job[jobs_args.MediaAssetProcessJob]) error {
return nil
}
@@ -65,26 +66,29 @@ func (w *notificationTestWorker) Work(ctx context.Context, job *river.Job[jobs_a
Content: arg.Content,
IsRead: false,
}
return models.NotificationQuery.WithContext(ctx).Create(n)
}
func testJobWorkersProvider() container.ProviderContainer {
return container.ProviderContainer{
Provider: func(opts ...opt.Option) error {
Provider: func(_ ...opt.Option) error {
return container.Container.Provide(func(__job *job.Job) (contracts.Initial, error) {
obj := &orderRefundTestWorker{}
if err := river.AddWorkerSafely(__job.Workers, obj); err != nil {
return nil, err
return nil, fmt.Errorf("register order refund test worker: %w", err)
}
obj2 := &mediaAssetProcessTestWorker{}
if err := river.AddWorkerSafely(__job.Workers, obj2); err != nil {
return nil, err
return nil, fmt.Errorf("register media process test worker: %w", err)
}
obj3 := &notificationTestWorker{}
if err := river.AddWorkerSafely(__job.Workers, obj3); err != nil {
return nil, err
return nil, fmt.Errorf("register notification test worker: %w", err)
}
return obj, nil
}, atom.GroupInitial)
},
@@ -92,40 +96,38 @@ func testJobWorkersProvider() container.ProviderContainer {
}
func Serve(providers container.Providers, t *testing.T, invoke any) {
Convey("tests boot up", t, func() {
// 关键语义:测试用例可能会在同一进程内多次调用 Serve。
// atom/config.Load 会向全局 dig 容器重复 Provide *viper.Viper若不重置会导致 “already provided”。
// 因此每次测试启动前都重置容器,保证各测试套件相互独立。
convey.Convey("tests boot up", t, func() {
baseCtx := context.Background()
container.Close()
container.Container = dig.New()
So(container.Container.Provide(func() context.Context { return context.Background() }), ShouldBeNil)
convey.So(container.Container.Provide(func() context.Context { return baseCtx }), convey.ShouldBeNil)
file := fabfile.MustFind("config.toml")
// 支持通过 ENV_LOCAL 指定测试环境配置config.<env>.toml
localEnv := os.Getenv("ENV_LOCAL")
if localEnv != "" {
file = fabfile.MustFind("config." + localEnv + ".toml")
}
So(atom.LoadProviders(file, providers), ShouldBeNil)
So(os.Setenv("JOB_INLINE", "1"), ShouldBeNil)
convey.So(atom.LoadProviders(file, providers), convey.ShouldBeNil)
convey.So(os.Setenv("JOB_INLINE", "1"), convey.ShouldBeNil)
t.Cleanup(func() {
_ = os.Unsetenv("JOB_INLINE")
})
So(container.Container.Invoke(func(p struct {
convey.So(container.Container.Invoke(func(params struct {
dig.In
Initials []contracts.Initial `group:"initials"`
Job *job.Job
},
) error {
_ = p.Initials
ctx, cancel := context.WithCancel(context.Background())
_ = params.Initials
jobCtx, cancel := context.WithCancel(baseCtx)
t.Cleanup(cancel)
go func() {
_ = p.Job.Start(ctx)
_ = params.Job.Start(jobCtx)
}()
return nil
}), ShouldBeNil)
So(container.Container.Invoke(invoke), ShouldBeNil)
}), convey.ShouldBeNil)
convey.So(container.Container.Invoke(invoke), convey.ShouldBeNil)
})
}