diff --git a/backend/app/commands/seed/seed.go b/backend/app/commands/seed/seed.go index 190e92b..964e534 100644 --- a/backend/app/commands/seed/seed.go +++ b/backend/app/commands/seed/seed.go @@ -67,7 +67,6 @@ func Serve(cmd *cobra.Command, args []string) error { } // Buyer - buyer := &models.User{ Username: "test", Phone: "13800138000", @@ -82,8 +81,22 @@ func Serve(cmd *cobra.Command, args []string) error { buyer, _ = models.UserQuery.WithContext(ctx).Where(models.UserQuery.Phone.Eq("13800138000")).First() } - // 2. Tenant + // 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: "梅派艺术工作室", @@ -96,6 +109,17 @@ func Serve(cmd *cobra.Command, args []string) error { 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{ "《锁麟囊》春秋亭 (程砚秋)", "昆曲《牡丹亭》游园惊梦", "越剧《红楼梦》葬花", @@ -109,6 +133,7 @@ func Serve(cmd *cobra.Command, args []string) error { "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 { @@ -126,7 +151,9 @@ func Serve(cmd *cobra.Command, args []string) error { Views: int32(rand.Intn(10000)), Likes: int32(rand.Intn(1000)), } - models.ContentQuery.WithContext(ctx).Create(c) + 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, @@ -163,30 +190,142 @@ func Serve(cmd *cobra.Command, args []string) error { cp1 := &models.Coupon{ TenantID: tenant.ID, Title: "新人立减券", - Type: "fix_amount", + 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), } - models.CouponQuery.WithContext(ctx).Create(cp1) + 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: "unused", + Status: consts.UserCouponStatusUnused, }) - // 5. Notifications - models.NotificationQuery.WithContext(ctx).Create(&models.Notification{ - UserID: buyer.ID, - Type: "system", - Title: "欢迎注册", - Content: "欢迎来到曲韵平台!", - IsRead: false, + // 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 diff --git a/docs/plan.md b/docs/plan.md index e69de29..cd2ab2e 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -0,0 +1,92 @@ +# Implementation Plan: Staging Seed Data + +**Branch**: `main` | **Date**: 2026-01-26 | **Spec**: `docs/staging_smoke_test.md` +**Input**: staging 冒烟测试需要快速准备基础数据。 + +**Note**: 本计划遵循 `docs/templates/plan-template.md`。 + +## Summary + +扩展 Go seed 命令,生成 staging 冒烟测试所需的最小数据集(租户、用户、超管、内容、订单、优惠券、通知等),确保前后台关键页面可快速验证。 + +## Technical Context + +**Language/Version**: Go 1.22 +**Primary Dependencies**: Fiber, GORM-Gen +**Storage**: PostgreSQL + Redis +**Testing**: `go test ./...` +**Target Platform**: Linux server (staging) +**Project Type**: Web application (backend + frontend) +**Performance Goals**: N/A +**Constraints**: 不修改生成文件,Service/Controller 规范保持不变 +**Scale/Scope**: 最小可用冒烟数据 + +## Constitution Check + +- 遵循 `backend/llm.txt` 规则(不改生成文件、中文注释、服务层规范)。 +- seed 逻辑仅在 `backend/app/commands/seed/seed.go` 修改。 + +## Project Structure + +### Documentation (this feature) + +```text +docs/ +└── plan.md +``` + +### Source Code (repository root) + +```text +backend/ +└── app/commands/seed/seed.go +``` + +**Structure Decision**: 使用现有 seed 命令注入 staging 数据。 + +## Plan Phases + +### Phase 1: 数据范围确认 +- 明确需覆盖的冒烟用例与最小数据集。 + +### Phase 2: Seed 扩展实现 +- 增加超管用户、租户成员、订单与内容访问等基础数据。 +- 确保幂等运行(重复执行不报错)。 + +### Phase 3: 文档与验证 +- 记录 seed 运行方式与输出数据说明。 + +## Tasks + +**Format**: `[ID] [P?] [Story] Description` + +### Phase 1: Foundational +- [x] T001 [US0] 明确冒烟必需数据项(用户/租户/内容/订单/通知) + +### Phase 2: Seed 扩展 +- [x] T010 [US1] 创建超管用户与角色 +- [x] T011 [US1] 创建租户成员与权限关系 +- [x] T012 [US1] 创建订单、订单项、内容访问记录 +- [x] T013 [US1] 创建通知模板/系统通知 + +### Phase 3: Docs +- [x] T020 [US2] 更新 seed 说明文档/输出示例 + +## Dependencies + +- Phase 1 → Phase 2 → Phase 3。 + +## Acceptance Criteria + +- `go run ./backend/main.go seed` 可成功生成数据。 +- seed 后 staging 冒烟清单主要页面可打开并有数据展示。 +- 重复运行 seed 不产生致命错误。 + +## Risks + +- 真实 staging 数据冲突(唯一索引)。 +- 订单/访问数据缺失导致部分页面空白。 + +## Complexity Tracking + +无。 diff --git a/docs/staging_smoke_test.md b/docs/staging_smoke_test.md index 75d3029..ed77ec7 100644 --- a/docs/staging_smoke_test.md +++ b/docs/staging_smoke_test.md @@ -9,6 +9,7 @@ - Test user credentials (OTP or seeded user) - Superadmin credentials (role: `super_admin`) - Storage bucket configured for staging +- Seed data loaded via `go run ./backend/main.go seed` ## Portal (Tenant) @@ -94,6 +95,13 @@ 10. **Health** - Check health endpoints (service/storage status). +## Seed Data Notes + +- Tenant: seed creates one tenant with code like `meipai_`. +- Users: `creator` (creator role), `test` (regular user), `superadmin` (super admin). +- Orders: one paid order with library access to first content. +- Notifications, templates, audit log, system config, content report included. + ## Pass/Fail Criteria - All pages load without 5xx/4xx errors (except expected auth errors).