diff --git a/backend/app/commands/seed/seed.go b/backend/app/commands/seed/seed.go index 964e534..5553e9e 100644 --- a/backend/app/commands/seed/seed.go +++ b/backend/app/commands/seed/seed.go @@ -119,6 +119,15 @@ func Serve(cmd *cobra.Command, args []string) error { 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{ @@ -222,6 +231,14 @@ func Serve(cmd *cobra.Command, args []string) error { 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) @@ -242,6 +259,102 @@ func Serve(cmd *cobra.Command, args []string) error { 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 @@ -251,6 +364,15 @@ func Serve(cmd *cobra.Command, args []string) error { 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, @@ -287,6 +409,14 @@ func Serve(cmd *cobra.Command, args []string) error { 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{ @@ -294,6 +424,11 @@ func Serve(cmd *cobra.Command, args []string) error { 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{ @@ -316,7 +451,7 @@ func Serve(cmd *cobra.Command, args []string) error { }) } - // 11. Comments + // 11. Comments & interactions if len(seededContents) > 0 { models.CommentQuery.WithContext(ctx).Create(&models.Comment{ TenantID: tenant.ID, @@ -325,6 +460,80 @@ func Serve(cmd *cobra.Command, args []string) error { 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.") diff --git a/docs/plan.md b/docs/plan.md index cd2ab2e..0731d16 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,13 +1,13 @@ -# Implementation Plan: Staging Seed Data +# Implementation Plan: Staging Seed Full Coverage **Branch**: `main` | **Date**: 2026-01-26 | **Spec**: `docs/staging_smoke_test.md` -**Input**: staging 冒烟测试需要快速准备基础数据。 +**Input**: 全覆盖冒烟测试需要补齐 seed 数据。 **Note**: 本计划遵循 `docs/templates/plan-template.md`。 ## Summary -扩展 Go seed 命令,生成 staging 冒烟测试所需的最小数据集(租户、用户、超管、内容、订单、优惠券、通知等),确保前后台关键页面可快速验证。 +扩展 seed 数据覆盖冒烟测试未覆盖项:上传素材、交互记录、创作者流程完成态、财务账本/异常、退款/对账样例,并复核冒烟清单是否仍有缺口。 ## Technical Context @@ -15,16 +15,16 @@ **Primary Dependencies**: Fiber, GORM-Gen **Storage**: PostgreSQL + Redis **Testing**: `go test ./...` -**Target Platform**: Linux server (staging) -**Project Type**: Web application (backend + frontend) +**Target Platform**: staging +**Project Type**: Web application **Performance Goals**: N/A -**Constraints**: 不修改生成文件,Service/Controller 规范保持不变 -**Scale/Scope**: 最小可用冒烟数据 +**Constraints**: 不改生成文件;seed 逻辑仅在 `backend/app/commands/seed/seed.go`。 +**Scale/Scope**: 覆盖冒烟清单全场景 ## Constitution Check -- 遵循 `backend/llm.txt` 规则(不改生成文件、中文注释、服务层规范)。 -- seed 逻辑仅在 `backend/app/commands/seed/seed.go` 修改。 +- 遵循 `backend/llm.txt`。 +- 只改非生成文件。 ## Project Structure @@ -42,35 +42,35 @@ backend/ └── app/commands/seed/seed.go ``` -**Structure Decision**: 使用现有 seed 命令注入 staging 数据。 +**Structure Decision**: 继续扩展 seed 命令。 ## Plan Phases -### Phase 1: 数据范围确认 -- 明确需覆盖的冒烟用例与最小数据集。 +### Phase 1: 缺口确认 +- 对照冒烟清单明确缺失数据类型。 -### Phase 2: Seed 扩展实现 -- 增加超管用户、租户成员、订单与内容访问等基础数据。 -- 确保幂等运行(重复执行不报错)。 +### Phase 2: Seed 扩展 +- 添加交互记录、退款/对账样例、财务账本、异常/风控数据、创作者完成态、上传素材。 -### Phase 3: 文档与验证 -- 记录 seed 运行方式与输出数据说明。 +### Phase 3: 复核 +- 深度复查冒烟清单覆盖率并记录残余缺口。 ## Tasks **Format**: `[ID] [P?] [Story] Description` -### Phase 1: Foundational -- [x] T001 [US0] 明确冒烟必需数据项(用户/租户/内容/订单/通知) +### Phase 1 +- [x] T001 [US0] 标记冒烟缺口(上传/交互/财务/退款/创作者) -### Phase 2: Seed 扩展 -- [x] T010 [US1] 创建超管用户与角色 -- [x] T011 [US1] 创建租户成员与权限关系 -- [x] T012 [US1] 创建订单、订单项、内容访问记录 -- [x] T013 [US1] 创建通知模板/系统通知 +### Phase 2 +- [x] T010 [US1] 创建交互记录(点赞/收藏) +- [x] T011 [US1] 创建退款/对账样例订单 +- [x] T012 [US1] 创建租户账本/异常数据 +- [x] T013 [US1] 创建创作者审核通过与成员角色 +- [x] T014 [US1] 创建上传素材示例数据 -### Phase 3: Docs -- [x] T020 [US2] 更新 seed 说明文档/输出示例 +### Phase 3 +- [x] T020 [US2] 深度 review 冒烟清单覆盖率 ## Dependencies @@ -78,14 +78,12 @@ backend/ ## Acceptance Criteria -- `go run ./backend/main.go seed` 可成功生成数据。 -- seed 后 staging 冒烟清单主要页面可打开并有数据展示。 -- 重复运行 seed 不产生致命错误。 +- seed 后冒烟清单所有场景具备至少一条数据支撑。 +- 记录残余缺口(如必须手动步骤)。 ## Risks -- 真实 staging 数据冲突(唯一索引)。 -- 订单/访问数据缺失导致部分页面空白。 +- staging 数据冲突导致重复创建失败。 ## Complexity Tracking