Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1233 lines
40 KiB
Go
1233 lines
40 KiB
Go
package seed
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"quyun/v2/app/commands"
|
|
"quyun/v2/app/errorx"
|
|
"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(_ *cobra.Command, _ []string) error {
|
|
err := container.Container.Invoke(func(ctx context.Context, svc Service) error {
|
|
models.SetDefault(svc.DB)
|
|
fmt.Println("Cleaning existing data...")
|
|
|
|
truncateTables := []string{
|
|
"user_content_actions",
|
|
"comments",
|
|
"content_reports",
|
|
"audit_logs",
|
|
"system_configs",
|
|
"notification_templates",
|
|
"notifications",
|
|
"recharge_codes",
|
|
"tenant_invites",
|
|
"tenant_join_requests",
|
|
"content_access",
|
|
"order_items",
|
|
"orders",
|
|
"user_coupons",
|
|
"coupons",
|
|
"content_assets",
|
|
"media_assets",
|
|
"content_prices",
|
|
"contents",
|
|
"tenant_users",
|
|
"tenants",
|
|
"payout_accounts",
|
|
"tenant_ledgers",
|
|
"users",
|
|
}
|
|
for _, table := range truncateTables {
|
|
if err := svc.DB.Exec(fmt.Sprintf("TRUNCATE TABLE %s RESTART IDENTITY CASCADE", table)).Error; err != nil {
|
|
fmt.Printf("Truncate table %s failed: %v\n", table, err)
|
|
}
|
|
}
|
|
|
|
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",
|
|
Password: "superadmin123",
|
|
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()
|
|
}
|
|
if superAdmin != nil && superAdmin.Password == "" {
|
|
superAdmin.Password = "superadmin123"
|
|
if _, err := superAdmin.Update(ctx); err != nil {
|
|
fmt.Printf("Update superadmin password failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// 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_" + tenantCodeSuffix,
|
|
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
|
|
|
|
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,
|
|
Title: title,
|
|
Description: fmt.Sprintf("这是关于 %s 的详细介绍...", title),
|
|
Genre: "京剧",
|
|
Status: consts.ContentStatusPublished,
|
|
Visibility: consts.ContentVisibilityPublic,
|
|
Views: int32(viewsValue),
|
|
Likes: int32(likesValue),
|
|
}
|
|
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: "符合要求",
|
|
})
|
|
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(inviteSuffix),
|
|
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)
|
|
}
|
|
|
|
reviewUser := &models.User{
|
|
Username: "review_user",
|
|
Phone: "13800009997",
|
|
Nickname: "待审核用户",
|
|
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=ReviewUser",
|
|
Balance: 1200,
|
|
Status: consts.UserStatusPendingVerify,
|
|
Roles: types.Array[consts.Role]{consts.RoleUser},
|
|
}
|
|
if err := models.UserQuery.WithContext(ctx).Create(reviewUser); err != nil {
|
|
fmt.Printf("Create review user failed: %v\n", err)
|
|
}
|
|
|
|
bannedUser := &models.User{
|
|
Username: "banned_user",
|
|
Phone: "13800009996",
|
|
Nickname: "封禁用户",
|
|
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=BannedUser",
|
|
Balance: 200,
|
|
Status: consts.UserStatusBanned,
|
|
Roles: types.Array[consts.Role]{consts.RoleUser},
|
|
}
|
|
if err := models.UserQuery.WithContext(ctx).Create(bannedUser); err != nil {
|
|
fmt.Printf("Create banned user failed: %v\n", err)
|
|
}
|
|
|
|
inactiveUser := &models.User{
|
|
Username: "inactive_user",
|
|
Phone: "13800009995",
|
|
Nickname: "停用用户",
|
|
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=InactiveUser",
|
|
Balance: 300,
|
|
Status: consts.UserStatusInactive,
|
|
Roles: types.Array[consts.Role]{consts.RoleUser},
|
|
}
|
|
if err := models.UserQuery.WithContext(ctx).Create(inactiveUser); err != nil {
|
|
fmt.Printf("Create inactive user failed: %v\n", err)
|
|
}
|
|
|
|
creatorApplicant := &models.User{
|
|
Username: "creator_apply_user",
|
|
Phone: "13800009994",
|
|
Nickname: "申请创作者用户",
|
|
Avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=CreatorApplyUser",
|
|
Balance: 888,
|
|
Status: consts.UserStatusVerified,
|
|
Roles: types.Array[consts.Role]{consts.RoleUser, consts.RoleCreator},
|
|
}
|
|
if err := models.UserQuery.WithContext(ctx).Create(creatorApplicant); err != nil {
|
|
fmt.Printf("Create creator applicant user failed: %v\n", err)
|
|
}
|
|
|
|
if reviewUser != nil {
|
|
if err := models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{
|
|
TenantID: tenant.ID,
|
|
UserID: reviewUser.ID,
|
|
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
|
|
Status: consts.UserStatusPendingVerify,
|
|
}); err != nil {
|
|
fmt.Printf("Create pending tenant user failed: %v\n", err)
|
|
}
|
|
}
|
|
if bannedUser != nil {
|
|
if err := models.TenantUserQuery.WithContext(ctx).Create(&models.TenantUser{
|
|
TenantID: tenant.ID,
|
|
UserID: bannedUser.ID,
|
|
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
|
|
Status: consts.UserStatusBanned,
|
|
}); err != nil {
|
|
fmt.Printf("Create banned tenant user failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
emptyTenant := &models.Tenant{
|
|
UserID: creator.ID,
|
|
Name: "空态租户",
|
|
Code: "empty_" + tenantCodeSuffix,
|
|
UUID: types.UUID(uuid.New()),
|
|
Status: consts.TenantStatusVerified,
|
|
}
|
|
if err := models.TenantQuery.WithContext(ctx).Create(emptyTenant); err != nil {
|
|
fmt.Printf("Create empty tenant failed: %v\n", err)
|
|
}
|
|
|
|
pendingTenant := &models.Tenant{
|
|
UserID: creator.ID,
|
|
Name: "待审核租户",
|
|
Code: "pending_" + tenantCodeSuffix,
|
|
UUID: types.UUID(uuid.New()),
|
|
Status: consts.TenantStatusPendingVerify,
|
|
}
|
|
if err := models.TenantQuery.WithContext(ctx).Create(pendingTenant); err != nil {
|
|
fmt.Printf("Create pending tenant failed: %v\n", err)
|
|
}
|
|
|
|
bannedTenant := &models.Tenant{
|
|
UserID: creator.ID,
|
|
Name: "封禁租户",
|
|
Code: "banned_" + tenantCodeSuffix,
|
|
UUID: types.UUID(uuid.New()),
|
|
Status: consts.TenantStatusBanned,
|
|
}
|
|
if err := models.TenantQuery.WithContext(ctx).Create(bannedTenant); err != nil {
|
|
fmt.Printf("Create banned tenant failed: %v\n", err)
|
|
}
|
|
|
|
if creatorApplicant != nil {
|
|
if err := models.TenantJoinRequestQuery.WithContext(ctx).Create(&models.TenantJoinRequest{
|
|
TenantID: pendingTenant.ID,
|
|
UserID: creatorApplicant.ID,
|
|
Status: string(consts.TenantJoinRequestStatusRejected),
|
|
Reason: "资料不完整",
|
|
DecidedAt: time.Now().Add(-30 * time.Minute),
|
|
DecidedOperatorUserID: creator.ID,
|
|
DecidedReason: "资料不完整,驳回",
|
|
}); err != nil {
|
|
fmt.Printf("Create rejected join request failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
disabledInviteSuffix, err := randomIntWithLimit(100000)
|
|
if err != nil {
|
|
return fmt.Errorf("generate disabled invite code: %w", err)
|
|
}
|
|
if err := models.TenantInviteQuery.WithContext(ctx).Create(&models.TenantInvite{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Code: "disabled" + cast.ToString(disabledInviteSuffix),
|
|
Status: string(consts.TenantInviteStatusDisabled),
|
|
MaxUses: 3,
|
|
UsedCount: 1,
|
|
DisabledAt: time.Now().Add(-2 * time.Hour),
|
|
DisabledOperatorUserID: superAdmin.ID,
|
|
Remark: "seed disabled invite",
|
|
ExpiresAt: time.Now().Add(3 * 24 * time.Hour),
|
|
}); err != nil {
|
|
fmt.Printf("Create disabled invite failed: %v\n", err)
|
|
}
|
|
|
|
expiredInviteSuffix, err := randomIntWithLimit(100000)
|
|
if err != nil {
|
|
return fmt.Errorf("generate expired invite code: %w", err)
|
|
}
|
|
if err := models.TenantInviteQuery.WithContext(ctx).Create(&models.TenantInvite{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Code: "expired" + cast.ToString(expiredInviteSuffix),
|
|
Status: string(consts.TenantInviteStatusExpired),
|
|
MaxUses: 2,
|
|
UsedCount: 2,
|
|
Remark: "seed expired invite",
|
|
ExpiresAt: time.Now().Add(-2 * time.Hour),
|
|
}); err != nil {
|
|
fmt.Printf("Create expired invite failed: %v\n", err)
|
|
}
|
|
|
|
draftContent := &models.Content{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Title: "草稿内容(待完善)",
|
|
Description: "用于测试草稿态按钮与筛选",
|
|
Genre: "京剧",
|
|
Status: consts.ContentStatusDraft,
|
|
Visibility: consts.ContentVisibilityPrivate,
|
|
Views: 0,
|
|
Likes: 0,
|
|
}
|
|
if err := models.ContentQuery.WithContext(ctx).Create(draftContent); err != nil {
|
|
fmt.Printf("Create draft content failed: %v\n", err)
|
|
}
|
|
|
|
reviewingContent := &models.Content{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Title: "审核中内容(待审核)",
|
|
Description: "用于测试审核流操作",
|
|
Genre: "越剧",
|
|
Status: consts.ContentStatusReviewing,
|
|
Visibility: consts.ContentVisibilityTenantOnly,
|
|
Views: 18,
|
|
Likes: 2,
|
|
}
|
|
if err := models.ContentQuery.WithContext(ctx).Create(reviewingContent); err != nil {
|
|
fmt.Printf("Create reviewing content failed: %v\n", err)
|
|
}
|
|
|
|
unpublishedContent := &models.Content{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Title: "下架内容(可重发)",
|
|
Description: "用于测试下架态展示",
|
|
Genre: "黄梅戏",
|
|
Status: consts.ContentStatusUnpublished,
|
|
Visibility: consts.ContentVisibilityTenantOnly,
|
|
Views: 87,
|
|
Likes: 9,
|
|
}
|
|
if err := models.ContentQuery.WithContext(ctx).Create(unpublishedContent); err != nil {
|
|
fmt.Printf("Create unpublished content failed: %v\n", err)
|
|
}
|
|
|
|
blockedContent := &models.Content{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Title: "封禁内容(违规)",
|
|
Description: "用于测试封禁态展示",
|
|
Genre: "昆曲",
|
|
Status: consts.ContentStatusBlocked,
|
|
Visibility: consts.ContentVisibilityPrivate,
|
|
Views: 120,
|
|
Likes: 0,
|
|
}
|
|
if err := models.ContentQuery.WithContext(ctx).Create(blockedContent); err != nil {
|
|
fmt.Printf("Create blocked content failed: %v\n", err)
|
|
}
|
|
|
|
if reviewingContent != nil {
|
|
if err := models.ContentReportQuery.WithContext(ctx).Create(&models.ContentReport{
|
|
TenantID: tenant.ID,
|
|
ContentID: reviewingContent.ID,
|
|
ReporterID: buyer.ID,
|
|
Reason: "quality",
|
|
Detail: "音画不同步",
|
|
Status: string(consts.TenantJoinRequestStatusApproved),
|
|
HandledBy: superAdmin.ID,
|
|
HandledAction: "approve",
|
|
HandledReason: "确认问题,已处理",
|
|
HandledAt: time.Now().Add(-90 * time.Minute),
|
|
}); err != nil {
|
|
fmt.Printf("Create approved content report failed: %v\n", err)
|
|
}
|
|
}
|
|
if blockedContent != nil {
|
|
if err := models.ContentReportQuery.WithContext(ctx).Create(&models.ContentReport{
|
|
TenantID: tenant.ID,
|
|
ContentID: blockedContent.ID,
|
|
ReporterID: buyer.ID,
|
|
Reason: "abuse",
|
|
Detail: "标题涉嫌误导",
|
|
Status: string(consts.TenantJoinRequestStatusRejected),
|
|
HandledBy: superAdmin.ID,
|
|
HandledAction: "reject",
|
|
HandledReason: "证据不足,驳回",
|
|
HandledAt: time.Now().Add(-45 * time.Minute),
|
|
}); err != nil {
|
|
fmt.Printf("Create rejected content report failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
processingAsset := &models.MediaAsset{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Type: consts.MediaAssetTypeVideo,
|
|
Status: consts.MediaAssetStatusProcessing,
|
|
Provider: "mock",
|
|
ObjectKey: "seed/video-processing.mp4",
|
|
Meta: types.NewJSONType(fields.MediaAssetMeta{
|
|
Size: 4096,
|
|
}),
|
|
UpdatedAt: time.Now().Add(-8 * time.Hour),
|
|
}
|
|
if err := models.MediaAssetQuery.WithContext(ctx).Create(processingAsset); err != nil {
|
|
fmt.Printf("Create processing asset failed: %v\n", err)
|
|
}
|
|
|
|
failedAsset := &models.MediaAsset{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Type: consts.MediaAssetTypeVideo,
|
|
Status: consts.MediaAssetStatusFailed,
|
|
Provider: "mock",
|
|
ObjectKey: "seed/video-failed.mp4",
|
|
Meta: types.NewJSONType(fields.MediaAssetMeta{
|
|
Size: 5120,
|
|
}),
|
|
}
|
|
if err := models.MediaAssetQuery.WithContext(ctx).Create(failedAsset); err != nil {
|
|
fmt.Printf("Create failed asset failed: %v\n", err)
|
|
}
|
|
|
|
expiredCoupon := &models.Coupon{
|
|
TenantID: tenant.ID,
|
|
Title: "过期券",
|
|
Type: consts.CouponTypeFixAmount,
|
|
Value: 200,
|
|
MinOrderAmount: 500,
|
|
TotalQuantity: 2,
|
|
UsedQuantity: 2,
|
|
StartAt: time.Now().Add(-10 * 24 * time.Hour),
|
|
EndAt: time.Now().Add(-24 * time.Hour),
|
|
}
|
|
if err := models.CouponQuery.WithContext(ctx).Create(expiredCoupon); err != nil {
|
|
fmt.Printf("Create expired coupon failed: %v\n", err)
|
|
}
|
|
|
|
upcomingCoupon := &models.Coupon{
|
|
TenantID: tenant.ID,
|
|
Title: "未生效券",
|
|
Type: consts.CouponTypeDiscount,
|
|
Value: 85,
|
|
MaxDiscount: 300,
|
|
MinOrderAmount: 1000,
|
|
TotalQuantity: 50,
|
|
UsedQuantity: 0,
|
|
StartAt: time.Now().Add(48 * time.Hour),
|
|
EndAt: time.Now().Add(30 * 24 * time.Hour),
|
|
}
|
|
if err := models.CouponQuery.WithContext(ctx).Create(upcomingCoupon); err != nil {
|
|
fmt.Printf("Create upcoming coupon failed: %v\n", err)
|
|
}
|
|
|
|
exhaustedCoupon := &models.Coupon{
|
|
TenantID: tenant.ID,
|
|
Title: "已领完券",
|
|
Type: consts.CouponTypeFixAmount,
|
|
Value: 300,
|
|
MinOrderAmount: 600,
|
|
TotalQuantity: 1,
|
|
UsedQuantity: 1,
|
|
StartAt: time.Now().Add(-24 * time.Hour),
|
|
EndAt: time.Now().Add(3 * 24 * time.Hour),
|
|
}
|
|
if err := models.CouponQuery.WithContext(ctx).Create(exhaustedCoupon); err != nil {
|
|
fmt.Printf("Create exhausted coupon failed: %v\n", err)
|
|
}
|
|
|
|
if expiredCoupon != nil {
|
|
if err := models.UserCouponQuery.WithContext(ctx).Create(&models.UserCoupon{
|
|
UserID: buyer.ID,
|
|
CouponID: expiredCoupon.ID,
|
|
Status: consts.UserCouponStatusExpired,
|
|
}); err != nil {
|
|
fmt.Printf("Create expired user coupon failed: %v\n", err)
|
|
}
|
|
}
|
|
if cp1 != nil {
|
|
if err := models.UserCouponQuery.WithContext(ctx).Create(&models.UserCoupon{
|
|
UserID: buyer.ID,
|
|
CouponID: cp1.ID,
|
|
OrderID: 0,
|
|
Status: consts.UserCouponStatusUsed,
|
|
UsedAt: time.Now().Add(-3 * time.Hour),
|
|
}); err != nil {
|
|
fmt.Printf("Create used user coupon failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
purchaseCreated := &models.Order{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
Type: consts.OrderTypeContentPurchase,
|
|
Status: consts.OrderStatusCreated,
|
|
Currency: consts.CurrencyCNY,
|
|
AmountOriginal: 1100,
|
|
AmountDiscount: 0,
|
|
AmountPaid: 1100,
|
|
IdempotencyKey: uuid.NewString(),
|
|
}
|
|
if err := models.OrderQuery.WithContext(ctx).Create(purchaseCreated); err != nil {
|
|
fmt.Printf("Create created purchase order failed: %v\n", err)
|
|
}
|
|
|
|
purchaseRefunding := &models.Order{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
Type: consts.OrderTypeContentPurchase,
|
|
Status: consts.OrderStatusRefunding,
|
|
Currency: consts.CurrencyCNY,
|
|
AmountOriginal: 1300,
|
|
AmountDiscount: 100,
|
|
AmountPaid: 1200,
|
|
IdempotencyKey: uuid.NewString(),
|
|
PaidAt: time.Now().Add(-5 * time.Hour),
|
|
}
|
|
if err := models.OrderQuery.WithContext(ctx).Create(purchaseRefunding); err != nil {
|
|
fmt.Printf("Create refunding purchase order failed: %v\n", err)
|
|
}
|
|
|
|
purchaseCanceled := &models.Order{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
Type: consts.OrderTypeContentPurchase,
|
|
Status: consts.OrderStatusCanceled,
|
|
Currency: consts.CurrencyCNY,
|
|
AmountOriginal: 900,
|
|
AmountDiscount: 0,
|
|
AmountPaid: 0,
|
|
IdempotencyKey: uuid.NewString(),
|
|
}
|
|
if err := models.OrderQuery.WithContext(ctx).Create(purchaseCanceled); err != nil {
|
|
fmt.Printf("Create canceled purchase order failed: %v\n", err)
|
|
}
|
|
|
|
purchaseFailed := &models.Order{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
Type: consts.OrderTypeContentPurchase,
|
|
Status: consts.OrderStatusFailed,
|
|
Currency: consts.CurrencyCNY,
|
|
AmountOriginal: 1400,
|
|
AmountDiscount: 0,
|
|
AmountPaid: 1400,
|
|
IdempotencyKey: uuid.NewString(),
|
|
}
|
|
if err := models.OrderQuery.WithContext(ctx).Create(purchaseFailed); err != nil {
|
|
fmt.Printf("Create failed purchase order failed: %v\n", err)
|
|
}
|
|
|
|
orderSeedContentID := int64(0)
|
|
if len(seededContents) > 1 {
|
|
orderSeedContentID = seededContents[1].ID
|
|
}
|
|
if orderSeedContentID > 0 {
|
|
if purchaseCreated != nil {
|
|
if err := models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
OrderID: purchaseCreated.ID,
|
|
ContentID: orderSeedContentID,
|
|
ContentUserID: creator.ID,
|
|
AmountPaid: purchaseCreated.AmountPaid,
|
|
}); err != nil {
|
|
fmt.Printf("Create order item for created order failed: %v\n", err)
|
|
}
|
|
}
|
|
if purchaseRefunding != nil {
|
|
if err := models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
OrderID: purchaseRefunding.ID,
|
|
ContentID: orderSeedContentID,
|
|
ContentUserID: creator.ID,
|
|
AmountPaid: purchaseRefunding.AmountPaid,
|
|
}); err != nil {
|
|
fmt.Printf("Create order item for refunding order failed: %v\n", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(seededContents) > 2 {
|
|
if err := models.ContentAccessQuery.WithContext(ctx).Create(&models.ContentAccess{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
ContentID: seededContents[2].ID,
|
|
OrderID: purchaseRefunding.ID,
|
|
Status: consts.ContentAccessStatusRevoked,
|
|
RevokedAt: time.Now().Add(-40 * time.Minute),
|
|
}); err != nil {
|
|
fmt.Printf("Create revoked content access failed: %v\n", err)
|
|
}
|
|
}
|
|
if len(seededContents) > 3 {
|
|
if err := models.ContentAccessQuery.WithContext(ctx).Create(&models.ContentAccess{
|
|
TenantID: tenant.ID,
|
|
UserID: buyer.ID,
|
|
ContentID: seededContents[3].ID,
|
|
OrderID: purchaseRefunding.ID,
|
|
Status: consts.ContentAccessStatusExpired,
|
|
RevokedAt: time.Now().Add(-24 * time.Hour),
|
|
}); err != nil {
|
|
fmt.Printf("Create expired content access failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
if err := models.PayoutAccountQuery.WithContext(ctx).Create(&models.PayoutAccount{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Type: consts.PayoutAccountTypeBank,
|
|
Name: "招商银行",
|
|
Account: "6222000000000001",
|
|
Realname: "梅派传人小林",
|
|
Status: consts.PayoutAccountStatusPending,
|
|
}); err != nil {
|
|
fmt.Printf("Create pending payout account failed: %v\n", err)
|
|
}
|
|
|
|
if err := models.PayoutAccountQuery.WithContext(ctx).Create(&models.PayoutAccount{
|
|
TenantID: tenant.ID,
|
|
UserID: creator.ID,
|
|
Type: consts.PayoutAccountTypeAlipay,
|
|
Name: "支付宝(驳回样例)",
|
|
Account: "creator-rejected@example.com",
|
|
Realname: "梅派传人小林",
|
|
Status: consts.PayoutAccountStatusRejected,
|
|
ReviewedBy: superAdmin.ID,
|
|
ReviewedAt: time.Now().Add(-2 * time.Hour),
|
|
ReviewReason: "账户实名不匹配",
|
|
}); err != nil {
|
|
fmt.Printf("Create rejected payout account failed: %v\n", err)
|
|
}
|
|
|
|
if err := models.NotificationQuery.WithContext(ctx).Create(&models.Notification{
|
|
UserID: buyer.ID,
|
|
TenantID: tenant.ID,
|
|
Type: string(consts.NotificationTypeOrder),
|
|
Title: "订单支付成功",
|
|
Content: "您有一笔订单已支付成功。",
|
|
IsRead: true,
|
|
}); err != nil {
|
|
fmt.Printf("Create read notification failed: %v\n", err)
|
|
}
|
|
if err := models.NotificationQuery.WithContext(ctx).Create(&models.Notification{
|
|
UserID: buyer.ID,
|
|
TenantID: tenant.ID,
|
|
Type: string(consts.NotificationTypeAudit),
|
|
Title: "内容审核提醒",
|
|
Content: "您提交的内容正在审核中。",
|
|
IsRead: false,
|
|
}); err != nil {
|
|
fmt.Printf("Create unread audit notification failed: %v\n", err)
|
|
}
|
|
|
|
inactiveTemplate := &models.NotificationTemplate{
|
|
TenantID: 0,
|
|
Name: "已停用模板",
|
|
Type: consts.NotificationTypeSystem,
|
|
Title: "停用模板",
|
|
Content: "该模板用于测试停用状态。",
|
|
IsActive: true,
|
|
}
|
|
if err := models.NotificationTemplateQuery.WithContext(ctx).Create(inactiveTemplate); err != nil {
|
|
fmt.Printf("Create inactive notification template failed: %v\n", err)
|
|
} else {
|
|
if _, err := models.NotificationTemplateQuery.WithContext(ctx).
|
|
Where(models.NotificationTemplateQuery.ID.Eq(inactiveTemplate.ID)).
|
|
UpdateSimple(models.NotificationTemplateQuery.IsActive.Value(false)); err != nil {
|
|
fmt.Printf("Update inactive notification template failed: %v\n", err)
|
|
}
|
|
}
|
|
|
|
redeemedRechargeOrder := &models.Order{
|
|
TenantID: 0,
|
|
UserID: buyer.ID,
|
|
Type: consts.OrderTypeRecharge,
|
|
Status: consts.OrderStatusPaid,
|
|
Currency: consts.CurrencyCNY,
|
|
AmountOriginal: 3000,
|
|
AmountDiscount: 0,
|
|
AmountPaid: 3000,
|
|
IdempotencyKey: uuid.NewString(),
|
|
PaidAt: time.Now().Add(-90 * time.Minute),
|
|
}
|
|
if err := models.OrderQuery.WithContext(ctx).Create(redeemedRechargeOrder); err != nil {
|
|
fmt.Printf("Create redeemed recharge order failed: %v\n", err)
|
|
}
|
|
|
|
if err := models.RechargeCodeQuery.WithContext(ctx).Create(&models.RechargeCode{
|
|
Code: "RC_ACTIVE_1000",
|
|
Amount: 100000,
|
|
Status: "active",
|
|
ActivatedBy: superAdmin.ID,
|
|
ActivatedAt: time.Now().Add(-30 * time.Minute),
|
|
Remark: "seed active recharge code",
|
|
}); err != nil {
|
|
fmt.Printf("Create active recharge code failed: %v\n", err)
|
|
}
|
|
|
|
if err := models.RechargeCodeQuery.WithContext(ctx).Create(&models.RechargeCode{
|
|
Code: "RC_REDEEMED_3000",
|
|
Amount: 300000,
|
|
Status: "redeemed",
|
|
ActivatedBy: superAdmin.ID,
|
|
ActivatedAt: time.Now().Add(-4 * time.Hour),
|
|
RedeemedBy: buyer.ID,
|
|
RedeemedAt: time.Now().Add(-2 * time.Hour),
|
|
RedeemedOrderID: redeemedRechargeOrder.ID,
|
|
Remark: "seed redeemed recharge code",
|
|
}); err != nil {
|
|
fmt.Printf("Create redeemed recharge code failed: %v\n", err)
|
|
}
|
|
|
|
if err := models.RechargeCodeQuery.WithContext(ctx).Create(&models.RechargeCode{
|
|
Code: "RC_INACTIVE_500",
|
|
Amount: 50000,
|
|
Status: "inactive",
|
|
ActivatedBy: 0,
|
|
Remark: "seed inactive recharge code",
|
|
}); err != nil {
|
|
fmt.Printf("Create inactive recharge code failed: %v\n", err)
|
|
}
|
|
|
|
for i := 0; i < 12; i++ {
|
|
models.NotificationQuery.WithContext(ctx).Create(&models.Notification{
|
|
UserID: buyer.ID,
|
|
TenantID: tenant.ID,
|
|
Type: string(consts.NotificationTypeSystem),
|
|
Title: fmt.Sprintf("系统通知 #%d", i+1),
|
|
Content: "分页测试通知",
|
|
IsRead: i%2 == 0,
|
|
})
|
|
}
|
|
for i := 0; i < 12; i++ {
|
|
models.AuditLogQuery.WithContext(ctx).Create(&models.AuditLog{
|
|
TenantID: tenant.ID,
|
|
OperatorID: superAdmin.ID,
|
|
Action: "seed_extra",
|
|
TargetID: fmt.Sprintf("extra:%d", i+1),
|
|
Detail: "extra audit log for pagination",
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|