Files
quyun-v2/backend/app/commands/seed/seed.go

543 lines
17 KiB
Go

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)
}
adminMember := &models.TenantUser{
TenantID: tenant.ID,
UserID: creator.ID,
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleTenantAdmin},
Status: consts.UserStatusVerified,
}
if err := models.TenantUserQuery.WithContext(ctx).Create(adminMember); err != nil {
fmt.Printf("Create tenant admin 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),
IsFlagged: true,
FlagReason: "seed risk",
FlaggedBy: superAdmin.ID,
FlaggedAt: time.Now().Add(-1 * time.Hour),
IsReconciled: true,
ReconcileNote: "seed reconcile",
ReconciledBy: superAdmin.ID,
ReconciledAt: time.Now().Add(-30 * time.Minute),
}
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,
})
}
refundOrder := &models.Order{
TenantID: tenant.ID,
UserID: buyer.ID,
Type: consts.OrderTypeContentPurchase,
Status: consts.OrderStatusRefunded,
Currency: consts.CurrencyCNY,
AmountOriginal: 1200,
AmountDiscount: 0,
AmountPaid: 1200,
IdempotencyKey: uuid.NewString(),
PaidAt: time.Now().Add(-6 * time.Hour),
RefundedAt: time.Now().Add(-2 * time.Hour),
}
if err := models.OrderQuery.WithContext(ctx).Create(refundOrder); err != nil {
fmt.Printf("Create refund order failed: %v\n", err)
}
missingPaid := &models.Order{
TenantID: tenant.ID,
UserID: buyer.ID,
Type: consts.OrderTypeContentPurchase,
Status: consts.OrderStatusPaid,
Currency: consts.CurrencyCNY,
AmountOriginal: 1500,
AmountDiscount: 0,
AmountPaid: 1500,
IdempotencyKey: uuid.NewString(),
}
if err := models.OrderQuery.WithContext(ctx).Create(missingPaid); err != nil {
fmt.Printf("Create missing paid order failed: %v\n", err)
}
missingRefund := &models.Order{
TenantID: tenant.ID,
UserID: buyer.ID,
Type: consts.OrderTypeContentPurchase,
Status: consts.OrderStatusRefunded,
Currency: consts.CurrencyCNY,
AmountOriginal: 800,
AmountDiscount: 0,
AmountPaid: 800,
IdempotencyKey: uuid.NewString(),
PaidAt: time.Now().Add(-8 * time.Hour),
}
if err := models.OrderQuery.WithContext(ctx).Create(missingRefund); err != nil {
fmt.Printf("Create missing refund order failed: %v\n", err)
}
withdrawOrder := &models.Order{
TenantID: tenant.ID,
UserID: creator.ID,
Type: consts.OrderTypeWithdrawal,
Status: consts.OrderStatusCreated,
Currency: consts.CurrencyCNY,
AmountOriginal: 300,
AmountDiscount: 0,
AmountPaid: 300,
IdempotencyKey: uuid.NewString(),
CreatedAt: time.Now().Add(-4 * time.Hour),
}
if err := models.OrderQuery.WithContext(ctx).Create(withdrawOrder); err != nil {
fmt.Printf("Create withdrawal order failed: %v\n", err)
}
withdrawApproved := &models.Order{
TenantID: tenant.ID,
UserID: creator.ID,
Type: consts.OrderTypeWithdrawal,
Status: consts.OrderStatusPaid,
Currency: consts.CurrencyCNY,
AmountOriginal: 500,
AmountDiscount: 0,
AmountPaid: 500,
IdempotencyKey: uuid.NewString(),
PaidAt: time.Now().Add(-3 * time.Hour),
}
if err := models.OrderQuery.WithContext(ctx).Create(withdrawApproved); err != nil {
fmt.Printf("Create approved withdrawal failed: %v\n", err)
}
withdrawRejected := &models.Order{
TenantID: tenant.ID,
UserID: creator.ID,
Type: consts.OrderTypeWithdrawal,
Status: consts.OrderStatusFailed,
Currency: consts.CurrencyCNY,
AmountOriginal: 200,
AmountDiscount: 0,
AmountPaid: 200,
IdempotencyKey: uuid.NewString(),
UpdatedAt: time.Now().Add(-1 * time.Hour),
}
if err := models.OrderQuery.WithContext(ctx).Create(withdrawRejected); err != nil {
fmt.Printf("Create rejected withdrawal failed: %v\n", err)
}
}
// 6. Creator join request & invite
models.TenantJoinRequestQuery.WithContext(ctx).Create(&models.TenantJoinRequest{
TenantID: tenant.ID,
UserID: buyer.ID,
Status: "pending",
Reason: "申请加入租户用于创作",
})
models.TenantJoinRequestQuery.WithContext(ctx).Create(&models.TenantJoinRequest{
TenantID: tenant.ID,
UserID: buyer.ID,
Status: "approved",
Reason: "已通过审核",
DecidedAt: time.Now().Add(-1 * time.Hour),
DecidedOperatorUserID: creator.ID,
DecidedReason: "符合要求",
})
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,
})
models.NotificationTemplateQuery.WithContext(ctx).Create(&models.NotificationTemplate{
TenantID: 0,
Name: "互动提醒",
Type: consts.NotificationTypeInteraction,
Title: "有人点赞了你",
Content: "有用户对你的内容进行了点赞。",
IsActive: true,
})
// 8. System config
models.SystemConfigQuery.WithContext(ctx).Create(&models.SystemConfig{
ConfigKey: "site_name",
Value: types.JSON([]byte(`{"value":"曲韵平台"}`)),
Description: "站点名称",
})
models.SystemConfigQuery.WithContext(ctx).Create(&models.SystemConfig{
ConfigKey: "support_email",
Value: types.JSON([]byte(`{"value":"support@quyun.example"}`)),
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 & interactions
if len(seededContents) > 0 {
models.CommentQuery.WithContext(ctx).Create(&models.Comment{
TenantID: tenant.ID,
UserID: buyer.ID,
ContentID: seededContents[0].ID,
Content: "好喜欢这段演出!",
Likes: 1,
})
models.UserContentActionQuery.WithContext(ctx).Create(&models.UserContentAction{
UserID: buyer.ID,
ContentID: seededContents[0].ID,
Type: "like",
})
models.UserContentActionQuery.WithContext(ctx).Create(&models.UserContentAction{
UserID: buyer.ID,
ContentID: seededContents[0].ID,
Type: "favorite",
})
media := &models.MediaAsset{
TenantID: tenant.ID,
UserID: creator.ID,
Type: consts.MediaAssetTypeVideo,
Status: consts.MediaAssetStatusReady,
Provider: "mock",
ObjectKey: "seed/video-demo.mp4",
Meta: types.NewJSONType(fields.MediaAssetMeta{
Size: 2048,
}),
}
models.MediaAssetQuery.WithContext(ctx).Create(media)
models.ContentAssetQuery.WithContext(ctx).Create(&models.ContentAsset{
TenantID: tenant.ID,
UserID: creator.ID,
ContentID: seededContents[0].ID,
AssetID: media.ID,
Role: consts.ContentAssetRoleMain,
})
}
// 12. Ledger & payout account
models.TenantLedgerQuery.WithContext(ctx).Create(&models.TenantLedger{
TenantID: tenant.ID,
UserID: creator.ID,
OrderID: 0,
Type: consts.TenantLedgerTypeDebitPurchase,
Amount: 990,
BalanceBefore: 0,
BalanceAfter: 990,
FrozenBefore: 0,
FrozenAfter: 0,
IdempotencyKey: uuid.NewString(),
Remark: "内容销售收入",
OperatorUserID: creator.ID,
BizRefType: "order",
BizRefID: 0,
})
models.PayoutAccountQuery.WithContext(ctx).Create(&models.PayoutAccount{
TenantID: tenant.ID,
UserID: creator.ID,
Type: consts.PayoutAccountTypeAlipay,
Name: "支付宝",
Account: "creator@example.com",
Realname: "梅派传人小林",
Status: consts.PayoutAccountStatusApproved,
ReviewedBy: superAdmin.ID,
ReviewedAt: time.Now().Add(-1 * time.Hour),
ReviewReason: "seed approved",
})
// 13. Balance anomaly user
negativeUser := &models.User{
Username: "negative",
Phone: "13800009998",
Nickname: "负余额用户",
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Negative",
Balance: -100,
BalanceFrozen: -10,
Status: consts.UserStatusVerified,
Roles: types.Array[consts.Role]{consts.RoleUser},
}
if err := models.UserQuery.WithContext(ctx).Create(negativeUser); err != nil {
fmt.Printf("Create negative user failed: %v\n", err)
}
fmt.Println("Seed done.")
return nil
})
}