Compare commits
2 Commits
1823ef89b9
...
619e17fc15
| Author | SHA1 | Date | |
|---|---|---|---|
| 619e17fc15 | |||
| 9607b914c9 |
@@ -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
|
||||
|
||||
92
docs/plan.md
92
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
|
||||
|
||||
无。
|
||||
|
||||
110
docs/staging_smoke_test.md
Normal file
110
docs/staging_smoke_test.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Staging Smoke Test Checklist
|
||||
|
||||
**Environment**: staging
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Staging base URL available
|
||||
- Valid `tenantCode` (from staging data)
|
||||
- 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)
|
||||
|
||||
1. **Tenant landing**
|
||||
- Open `/t/:tenantCode` and verify homepage renders.
|
||||
- Verify content list loads (no auth required).
|
||||
|
||||
2. **Content listing & detail**
|
||||
- Browse `/t/:tenantCode/contents/:id` and verify detail renders.
|
||||
- Confirm `author_id` and `tenant_id` filters work via list page.
|
||||
|
||||
3. **Auth**
|
||||
- Login via `/t/:tenantCode/auth/login`.
|
||||
- Verify token stored and subsequent requests authenticated.
|
||||
|
||||
4. **User center**
|
||||
- `/t/:tenantCode/me` loads user profile.
|
||||
- Update profile fields and save.
|
||||
|
||||
5. **Favorites & Likes**
|
||||
- Add/remove favorite (`/me/favorites` with `content_id`).
|
||||
- Add/remove like (`/me/likes` with `content_id`).
|
||||
|
||||
6. **Orders & Library**
|
||||
- Create order (checkout flow) and pay.
|
||||
- Verify order detail page renders.
|
||||
- Verify purchased content appears in library.
|
||||
|
||||
7. **Notifications**
|
||||
- Visit `/t/:tenantCode/me/notifications`.
|
||||
- Mark single notification read and read-all.
|
||||
|
||||
8. **Coupons**
|
||||
- List available coupons and receive one.
|
||||
- Confirm coupon appears in `/me/coupons`.
|
||||
|
||||
9. **Creator flows**
|
||||
- Apply for creator role.
|
||||
- Create or update content as creator.
|
||||
- List creator orders and verify data shows.
|
||||
|
||||
10. **Upload**
|
||||
- Init upload, upload part, complete upload.
|
||||
- Verify uploaded asset shows in content editor.
|
||||
|
||||
## Superadmin
|
||||
|
||||
1. **Login**
|
||||
- `/super/v1/auth/login` returns token.
|
||||
- Refresh/check token endpoint succeeds.
|
||||
|
||||
2. **Dashboard & stats**
|
||||
- Open dashboard; verify counters load.
|
||||
|
||||
3. **Users**
|
||||
- List users with filters (status/role).
|
||||
- View user detail, wallet, coupons, library, favorites, likes.
|
||||
|
||||
4. **Tenants**
|
||||
- List tenants, view settings, update settings.
|
||||
- Review join requests and creator applications.
|
||||
|
||||
5. **Contents & comments**
|
||||
- List contents by tenant.
|
||||
- Review content status and process reports.
|
||||
|
||||
6. **Orders & finance**
|
||||
- List orders; check detail.
|
||||
- Trigger flag/reconcile/refund flows.
|
||||
- Verify ledgers and anomalies endpoints.
|
||||
|
||||
7. **Coupons & risks**
|
||||
- List coupons, update status.
|
||||
- Check coupon risk list.
|
||||
|
||||
8. **Notifications**
|
||||
- List notification templates and update template.
|
||||
|
||||
9. **System configs & audits**
|
||||
- List/update system configs.
|
||||
- View audit logs with filters.
|
||||
|
||||
10. **Health**
|
||||
- Check health endpoints (service/storage status).
|
||||
|
||||
## Seed Data Notes
|
||||
|
||||
- Tenant: seed creates one tenant with code like `meipai_<rand>`.
|
||||
- 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).
|
||||
- Key CRUD flows succeed end-to-end.
|
||||
- Auth and tenant isolation behave correctly.
|
||||
- Uploads persist and render in content flows.
|
||||
Reference in New Issue
Block a user