feat: complete staging seed coverage
This commit is contained in:
@@ -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.")
|
||||
|
||||
60
docs/plan.md
60
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user