diff --git a/backend/app/commands/seed/seed.go b/backend/app/commands/seed/seed.go new file mode 100644 index 0000000..83f419a --- /dev/null +++ b/backend/app/commands/seed/seed.go @@ -0,0 +1,194 @@ +package seed + +import ( + "context" + "fmt" + "math/rand" + "time" + + "quyun/v2/app/commands" + "quyun/v2/database" + "quyun/v2/database/fields" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + "quyun/v2/providers/postgres" + + "github.com/google/uuid" + "github.com/spf13/cast" + "github.com/spf13/cobra" + "go.ipao.vip/atom" + "go.ipao.vip/atom/container" + "go.ipao.vip/gen/types" + "go.uber.org/dig" + "gorm.io/gorm" +) + +func defaultProviders() container.Providers { + return commands.Default(container.Providers{ + postgres.DefaultProvider(), + database.DefaultProvider(), + }...) +} + +func Command() atom.Option { + return atom.Command( + atom.Name("seed"), + atom.Short("seed initial data"), + atom.RunE(Serve), + atom.Providers(defaultProviders()), + ) +} + +type Service struct { + dig.In + + DB *gorm.DB +} + +func Serve(cmd *cobra.Command, args []string) error { + return container.Container.Invoke(func(ctx context.Context, svc Service) error { + models.SetDefault(svc.DB) + fmt.Println("Seeding data...") + + // 1. Users + // Creator + creator := &models.User{ + Username: "creator", + Phone: "13800000001", + Nickname: "梅派传人小林", + Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Master1", + Balance: 10000, + Status: consts.UserStatusVerified, + Roles: types.Array[consts.Role]{consts.RoleCreator}, + } + if err := models.UserQuery.WithContext(ctx).Create(creator); err != nil { + fmt.Printf("Create creator failed (maybe exists): %v\n", err) + creator, _ = models.UserQuery.WithContext(ctx).Where(models.UserQuery.Phone.Eq("13800000001")).First() + } + + // Buyer + + buyer := &models.User{ + Username: "test", + Phone: "13800138000", + Nickname: "戏迷小张", + Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Zhang", + Balance: 5000, + Status: consts.UserStatusVerified, + Roles: types.Array[consts.Role]{consts.RoleUser}, + } + if err := models.UserQuery.WithContext(ctx).Create(buyer); err != nil { + fmt.Printf("Create buyer failed: %v\n", err) + buyer, _ = models.UserQuery.WithContext(ctx).Where(models.UserQuery.Phone.Eq("13800138000")).First() + } + + // 2. Tenant + + tenant := &models.Tenant{ + UserID: creator.ID, + Name: "梅派艺术工作室", + Code: "meipai_" + cast.ToString(rand.Intn(1000)), + UUID: types.UUID(uuid.New()), + Status: consts.TenantStatusVerified, + } + if err := models.TenantQuery.WithContext(ctx).Create(tenant); err != nil { + fmt.Printf("Create tenant failed: %v\n", err) + tenant, _ = models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(creator.ID)).First() + } + + // 3. Contents + titles := []string{ + "《锁麟囊》春秋亭 (程砚秋)", "昆曲《牡丹亭》游园惊梦", "越剧《红楼梦》葬花", + "京剧《霸王别姬》全本实录", "京剧打击乐基础教程", "豫剧唱腔发音技巧", + "黄梅戏《女驸马》选段", "评剧《花为媒》报花名", "秦腔《三滴血》", + "河北梆子《大登殿》", + } + covers := []string{ + "https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60", + "https://images.unsplash.com/photo-1557683316-973673baf926?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60", + "https://images.unsplash.com/photo-1469571486292-0ba58a3f068b?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60", + } + + for i, title := range titles { + price := int64((i % 3) * 1000) // 0, 10.00, 20.00 + if i == 3 { + price = 990 + } // 9.90 + + c := &models.Content{ + TenantID: tenant.ID, + UserID: creator.ID, + Title: title, + Description: fmt.Sprintf("这是关于 %s 的详细介绍...", title), + Genre: "京剧", + Status: consts.ContentStatusPublished, + Visibility: consts.ContentVisibilityPublic, + Views: int32(rand.Intn(10000)), + Likes: int32(rand.Intn(1000)), + } + models.ContentQuery.WithContext(ctx).Create(c) + // Price + models.ContentPriceQuery.WithContext(ctx).Create(&models.ContentPrice{ + TenantID: tenant.ID, + UserID: creator.ID, + ContentID: c.ID, + PriceAmount: price, + Currency: "CNY", + }) + + // Asset (Cover) + ma := &models.MediaAsset{ + TenantID: tenant.ID, + UserID: creator.ID, + Type: consts.MediaAssetTypeImage, + Status: consts.MediaAssetStatusReady, + Provider: "mock", + ObjectKey: covers[i%len(covers)], + Meta: types.NewJSONType(fields.MediaAssetMeta{ + Size: 1024, + }), + } + models.MediaAssetQuery.WithContext(ctx).Create(ma) + + models.ContentAssetQuery.WithContext(ctx).Create(&models.ContentAsset{ + TenantID: tenant.ID, + UserID: creator.ID, + ContentID: c.ID, + AssetID: ma.ID, + Role: consts.ContentAssetRoleCover, + }) + } + + // 4. Coupons + cp1 := &models.Coupon{ + TenantID: tenant.ID, + Title: "新人立减券", + Type: "fix_amount", + Value: 500, // 5.00 + MinOrderAmount: 1000, + TotalQuantity: 100, + StartAt: time.Now().Add(-24 * time.Hour), + EndAt: time.Now().Add(30 * 24 * time.Hour), + } + models.CouponQuery.WithContext(ctx).Create(cp1) + + // Give to buyer + models.UserCouponQuery.WithContext(ctx).Create(&models.UserCoupon{ + UserID: buyer.ID, + CouponID: cp1.ID, + Status: "unused", + }) + + // 5. Notifications + models.NotificationQuery.WithContext(ctx).Create(&models.Notification{ + UserID: buyer.ID, + Type: "system", + Title: "欢迎注册", + Content: "欢迎来到曲韵平台!", + IsRead: false, + }) + + fmt.Println("Seed done.") + return nil + }) +} diff --git a/backend/app/services/user.go b/backend/app/services/user.go index 9e213e8..f094c36 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -34,7 +34,7 @@ func (s *user) SendOTP(ctx context.Context, phone string) error { // LoginWithOTP 手机号验证码登录/注册 func (s *user) LoginWithOTP(ctx context.Context, phone, otp string) (*auth_dto.LoginResponse, error) { // 1. 校验验证码 (模拟:固定 123456) - if otp != "123456" { + if otp != "1234" { return nil, errorx.ErrInvalidCredentials.WithMsg("验证码错误") } diff --git a/backend/main.go b/backend/main.go index 5583468..665f904 100644 --- a/backend/main.go +++ b/backend/main.go @@ -3,6 +3,7 @@ package main import ( "quyun/v2/app/commands/http" "quyun/v2/app/commands/migrate" + "quyun/v2/app/commands/seed" "quyun/v2/pkg/utils" log "github.com/sirupsen/logrus" @@ -32,6 +33,7 @@ func main() { atom.Name("v2"), http.Command(), migrate.Command(), + seed.Command(), } if err := atom.Serve(opts...); err != nil { diff --git a/backend/pkg/consts/consts.gen.go b/backend/pkg/consts/consts.gen.go index 2b7d3a8..3abec59 100644 --- a/backend/pkg/consts/consts.gen.go +++ b/backend/pkg/consts/consts.gen.go @@ -1853,6 +1853,8 @@ const ( RoleUser Role = "user" // RoleSuperAdmin is a Role of type super_admin. RoleSuperAdmin Role = "super_admin" + // RoleCreator is a Role of type creator. + RoleCreator Role = "creator" ) var ErrInvalidRole = fmt.Errorf("not a valid Role, try [%s]", strings.Join(_RoleNames, ", ")) @@ -1860,6 +1862,7 @@ var ErrInvalidRole = fmt.Errorf("not a valid Role, try [%s]", strings.Join(_Role var _RoleNames = []string{ string(RoleUser), string(RoleSuperAdmin), + string(RoleCreator), } // RoleNames returns a list of possible string values of Role. @@ -1874,6 +1877,7 @@ func RoleValues() []Role { return []Role{ RoleUser, RoleSuperAdmin, + RoleCreator, } } @@ -1892,6 +1896,7 @@ func (x Role) IsValid() bool { var _RoleValue = map[string]Role{ "user": RoleUser, "super_admin": RoleSuperAdmin, + "creator": RoleCreator, } // ParseRole attempts to convert a string to a Role. diff --git a/backend/pkg/consts/consts.go b/backend/pkg/consts/consts.go index 48edb49..4800299 100644 --- a/backend/pkg/consts/consts.go +++ b/backend/pkg/consts/consts.go @@ -14,7 +14,7 @@ import ( // // ) // swagger:enum Role -// ENUM( user, super_admin) +// ENUM( user, super_admin, creator) type Role string // Description returns the Chinese label for the specific enum value. @@ -24,6 +24,8 @@ func (t Role) Description() string { return "用户" case RoleSuperAdmin: return "超级管理员" + case RoleCreator: + return "创作者" default: return "未知角色" } diff --git a/frontend/portal/src/components/TopNavbar.vue b/frontend/portal/src/components/TopNavbar.vue index 8b8422b..3e7d211 100644 --- a/frontend/portal/src/components/TopNavbar.vue +++ b/frontend/portal/src/components/TopNavbar.vue @@ -44,15 +44,14 @@