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() } // Superadmin superAdmin := &models.User{ Username: "superadmin", Phone: "13800009999", Nickname: "平台管理员", Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Admin", Balance: 0, Status: consts.UserStatusVerified, Roles: types.Array[consts.Role]{consts.RoleSuperAdmin}, } if err := models.UserQuery.WithContext(ctx).Create(superAdmin); err != nil { fmt.Printf("Create superadmin failed: %v\n", err) superAdmin, _ = models.UserQuery.WithContext(ctx).Where(models.UserQuery.Username.Eq("superadmin")).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() } // Tenant membership (buyer joins as member) member := &models.TenantUser{ TenantID: tenant.ID, UserID: buyer.ID, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}, Status: consts.UserStatusVerified, } if err := models.TenantUserQuery.WithContext(ctx).Create(member); err != nil { fmt.Printf("Create tenant member failed: %v\n", err) } // 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", } var seededContents []*models.Content 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)), } if err := models.ContentQuery.WithContext(ctx).Create(c); err == nil { seededContents = append(seededContents, 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: consts.CouponTypeFixAmount, Value: 500, // 5.00 MinOrderAmount: 1000, TotalQuantity: 100, StartAt: time.Now().Add(-24 * time.Hour), EndAt: time.Now().Add(30 * 24 * time.Hour), } if err := models.CouponQuery.WithContext(ctx).Create(cp1); err != nil { fmt.Printf("Create coupon failed: %v\n", err) } // Give to buyer models.UserCouponQuery.WithContext(ctx).Create(&models.UserCoupon{ UserID: buyer.ID, CouponID: cp1.ID, Status: consts.UserCouponStatusUnused, }) // 5. Orders & library access (first content) if len(seededContents) > 0 { content := seededContents[0] order := &models.Order{ TenantID: tenant.ID, UserID: buyer.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, Currency: consts.CurrencyCNY, AmountOriginal: 990, AmountDiscount: 0, AmountPaid: 990, IdempotencyKey: uuid.NewString(), PaidAt: time.Now().Add(-2 * time.Hour), } if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil { fmt.Printf("Create order failed: %v\n", err) } else { models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{ TenantID: tenant.ID, UserID: buyer.ID, OrderID: order.ID, ContentID: content.ID, ContentUserID: creator.ID, AmountPaid: order.AmountPaid, }) models.ContentAccessQuery.WithContext(ctx).Create(&models.ContentAccess{ TenantID: tenant.ID, UserID: buyer.ID, ContentID: content.ID, OrderID: order.ID, Status: consts.ContentAccessStatusActive, }) } } // 6. Creator join request & invite models.TenantJoinRequestQuery.WithContext(ctx).Create(&models.TenantJoinRequest{ TenantID: tenant.ID, UserID: buyer.ID, Status: "pending", Reason: "申请加入租户用于创作", }) models.TenantInviteQuery.WithContext(ctx).Create(&models.TenantInvite{ TenantID: tenant.ID, UserID: creator.ID, Code: "invite" + cast.ToString(rand.Intn(100000)), Status: "active", MaxUses: 5, UsedCount: 0, Remark: "staging seed invite", ExpiresAt: time.Now().Add(7 * 24 * time.Hour), }) // 7. Notifications & templates models.NotificationQuery.WithContext(ctx).Create(&models.Notification{ UserID: buyer.ID, TenantID: tenant.ID, Type: string(consts.NotificationTypeSystem), Title: "欢迎注册", Content: "欢迎来到曲韵平台!", IsRead: false, }) models.NotificationTemplateQuery.WithContext(ctx).Create(&models.NotificationTemplate{ TenantID: 0, Name: "订单支付通知", Type: consts.NotificationTypeOrder, Title: "订单支付成功", Content: "您的订单已支付成功。", IsActive: true, }) models.NotificationTemplateQuery.WithContext(ctx).Create(&models.NotificationTemplate{ TenantID: 0, Name: "内容审核通知", Type: consts.NotificationTypeAudit, Title: "内容审核通过", Content: "您提交的内容已通过审核。", IsActive: true, }) // 8. System config models.SystemConfigQuery.WithContext(ctx).Create(&models.SystemConfig{ ConfigKey: "site_name", Value: types.JSON([]byte(`{"value":"曲韵平台"}`)), Description: "站点名称", }) // 9. Audit log models.AuditLogQuery.WithContext(ctx).Create(&models.AuditLog{ TenantID: tenant.ID, OperatorID: superAdmin.ID, Action: "seed", TargetID: fmt.Sprintf("tenant:%d", tenant.ID), Detail: "staging seed data", }) // 10. Content report if len(seededContents) > 0 { models.ContentReportQuery.WithContext(ctx).Create(&models.ContentReport{ TenantID: tenant.ID, ContentID: seededContents[0].ID, ReporterID: buyer.ID, Reason: "spam", Detail: "疑似广告内容", Status: "pending", }) } // 11. Comments if len(seededContents) > 0 { models.CommentQuery.WithContext(ctx).Create(&models.Comment{ TenantID: tenant.ID, UserID: buyer.ID, ContentID: seededContents[0].ID, Content: "好喜欢这段演出!", Likes: 1, }) } fmt.Println("Seed done.") return nil }) }