Compare commits
5 Commits
0f04cc02ed
...
86d8e1dd94
| Author | SHA1 | Date | |
|---|---|---|---|
| 86d8e1dd94 | |||
| f712436b2c | |||
| 590662964a | |||
| 9bf3a87b32 | |||
| d6550e9e1a |
@@ -60,6 +60,7 @@ func Serve(_ *cobra.Command, _ []string) error {
|
||||
"system_configs",
|
||||
"notification_templates",
|
||||
"notifications",
|
||||
"recharge_codes",
|
||||
"tenant_invites",
|
||||
"tenant_join_requests",
|
||||
"content_access",
|
||||
@@ -596,6 +597,607 @@ func Serve(_ *cobra.Command, _ []string) error {
|
||||
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
|
||||
|
||||
@@ -136,7 +136,7 @@ func (c *Common) CompleteUpload(ctx fiber.Ctx, user *models.User, form *dto.Uplo
|
||||
// @Param uploadId path string true "Upload ID"
|
||||
// @Success 200 {string} string "OK"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind uploadId path
|
||||
// @Bind uploadID path key(uploadId)
|
||||
func (c *Common) AbortUpload(ctx fiber.Ctx, user *models.User, uploadID string) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package v1
|
||||
import (
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
@@ -12,54 +12,576 @@ import (
|
||||
// @provider
|
||||
type Creator struct{}
|
||||
|
||||
// Apply to become a creator
|
||||
// Apply creator profile
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/apply [post]
|
||||
// @Router /v1/t/:tenantCode/creator/members/:id<int>/review [post]
|
||||
// @Router /v1/t/:tenantCode/creator/members/invite [post]
|
||||
// @Router /v1/t/:tenantCode/creator/members [get]
|
||||
// @Router /v1/t/:tenantCode/creator/members/invites [get]
|
||||
// @Router /v1/t/:tenantCode/creator/members/invites/:id<int> [delete]
|
||||
// @Router /v1/t/:tenantCode/creator/members/join-requests [get]
|
||||
// @Router /v1/t/:tenantCode/creator/members/:id<int> [delete]
|
||||
// @Router /v1/t/:tenantCode/creator/reports/overview [get]
|
||||
// @Router /v1/t/:tenantCode/creator/reports/export [post]
|
||||
// @Router /v1/t/:tenantCode/creator/dashboard [get]
|
||||
// @Router /v1/t/:tenantCode/creator/contents/:id<int> [get]
|
||||
// @Router /v1/t/:tenantCode/creator/contents [get]
|
||||
// @Router /v1/t/:tenantCode/creator/contents [post]
|
||||
// @Router /v1/t/:tenantCode/creator/contents/:id<int> [put]
|
||||
// @Router /v1/t/:tenantCode/creator/contents/:id<int> [delete]
|
||||
// @Router /v1/t/:tenantCode/creator/orders [get]
|
||||
// @Router /v1/t/:tenantCode/creator/orders/:id<int>/refund [post]
|
||||
// @Router /v1/t/:tenantCode/creator/settings [get]
|
||||
// @Router /v1/t/:tenantCode/creator/settings [put]
|
||||
// @Router /v1/t/:tenantCode/creator/payout-accounts [get]
|
||||
// @Router /v1/t/:tenantCode/creator/payout-accounts [post]
|
||||
// @Router /v1/t/:tenantCode/creator/payout-accounts [delete]
|
||||
// @Router /v1/t/:tenantCode/creator/withdraw [post]
|
||||
// @Router /v1/t/:tenantCode/creator/coupons [post]
|
||||
// @Router /v1/t/:tenantCode/creator/coupons [get]
|
||||
// @Router /v1/t/:tenantCode/creator/coupons/:id<int> [get]
|
||||
// @Router /v1/t/:tenantCode/creator/coupons/:id<int> [put]
|
||||
// @Router /v1/t/:tenantCode/creator/coupons/:id<int>/grant [post]
|
||||
// @Summary Apply creator profile
|
||||
// @Description 申请成为创作者并创建频道
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.ApplyForm true "Apply form"
|
||||
// @Success 200 {string} string "Applied"
|
||||
// @Bind form body
|
||||
func (c *Creator) Apply(ctx fiber.Ctx, form *dto.ApplyForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
// @Summary Grant coupon
|
||||
// @Description Grant coupon to users
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.Apply(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// Get creator dashboard
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/dashboard [get]
|
||||
// @Summary Get creator dashboard
|
||||
// @Description 获取创作者看板统计
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.DashboardStats
|
||||
func (c *Creator) Dashboard(ctx fiber.Ctx) (*dto.DashboardStats, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.Dashboard(ctx, tenantID, userID)
|
||||
}
|
||||
|
||||
// List creator contents
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/contents [get]
|
||||
// @Summary List creator contents
|
||||
// @Description 创作者内容列表(分页/筛选)
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param status query string false "Status"
|
||||
// @Param visibility query string false "Visibility"
|
||||
// @Param genre query string false "Genre"
|
||||
// @Param key query string false "Key"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Param sort query string false "Sort"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.CreatorContentItem}
|
||||
// @Bind filter query
|
||||
func (c *Creator) ListContents(ctx fiber.Ctx, filter *dto.CreatorContentListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.CreatorContentListFilter{}
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.ListContents(ctx, tenantID, userID, filter)
|
||||
}
|
||||
|
||||
// Get creator content detail
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/contents/:id<int> [get]
|
||||
// @Summary Get creator content detail
|
||||
// @Description 获取创作者内容编辑详情
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Success 200 {object} dto.ContentEditDTO
|
||||
// @Bind id path
|
||||
func (c *Creator) GetContent(ctx fiber.Ctx, id int64) (*dto.ContentEditDTO, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.GetContent(ctx, tenantID, userID, id)
|
||||
}
|
||||
|
||||
// Create creator content
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/contents [post]
|
||||
// @Summary Create creator content
|
||||
// @Description 创建创作者内容
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.ContentCreateForm true "Create form"
|
||||
// @Success 200 {string} string "Created"
|
||||
// @Bind form body
|
||||
func (c *Creator) CreateContent(ctx fiber.Ctx, form *dto.ContentCreateForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.CreateContent(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// Update creator content
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/contents/:id<int> [put]
|
||||
// @Summary Update creator content
|
||||
// @Description 更新创作者内容
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Param form body dto.ContentUpdateForm true "Update form"
|
||||
// @Success 200 {string} string "Updated"
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) UpdateContent(ctx fiber.Ctx, id int64, form *dto.ContentUpdateForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.UpdateContent(ctx, tenantID, userID, id, form)
|
||||
}
|
||||
|
||||
// Delete creator content
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/contents/:id<int> [delete]
|
||||
// @Summary Delete creator content
|
||||
// @Description 删除创作者内容
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Deleted"
|
||||
// @Bind id path
|
||||
func (c *Creator) DeleteContent(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.DeleteContent(ctx, tenantID, userID, id)
|
||||
}
|
||||
|
||||
// List creator orders
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/orders [get]
|
||||
// @Summary List creator orders
|
||||
// @Description 创作者订单列表
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param status query string false "Status"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {array} dto.Order
|
||||
// @Bind filter query
|
||||
func (c *Creator) ListOrders(ctx fiber.Ctx, filter *dto.CreatorOrderListFilter) ([]dto.Order, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.CreatorOrderListFilter{}
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.ListOrders(ctx, tenantID, userID, filter)
|
||||
}
|
||||
|
||||
// Process order refund
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/orders/:id<int>/refund [post]
|
||||
// @Summary Process order refund
|
||||
// @Description 处理创作者订单退款(同意/拒绝)
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Order ID"
|
||||
// @Param form body dto.RefundForm true "Refund form"
|
||||
// @Success 200 {string} string "Processed"
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) RefundOrder(ctx fiber.Ctx, id int64, form *dto.RefundForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.ProcessRefund(ctx, tenantID, userID, id, form)
|
||||
}
|
||||
|
||||
// Get creator settings
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/settings [get]
|
||||
// @Summary Get creator settings
|
||||
// @Description 获取创作者频道设置
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.Settings
|
||||
func (c *Creator) GetSettings(ctx fiber.Ctx) (*dto.Settings, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.GetSettings(ctx, tenantID, userID)
|
||||
}
|
||||
|
||||
// Update creator settings
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/settings [put]
|
||||
// @Summary Update creator settings
|
||||
// @Description 更新创作者频道设置
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.Settings true "Settings form"
|
||||
// @Success 200 {string} string "Updated"
|
||||
// @Bind form body
|
||||
func (c *Creator) UpdateSettings(ctx fiber.Ctx, form *dto.Settings) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.UpdateSettings(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// List payout accounts
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/payout-accounts [get]
|
||||
// @Summary List payout accounts
|
||||
// @Description 获取创作者收款账户列表
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.PayoutAccount
|
||||
func (c *Creator) ListPayoutAccounts(ctx fiber.Ctx) ([]dto.PayoutAccount, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.ListPayoutAccounts(ctx, tenantID, userID)
|
||||
}
|
||||
|
||||
// Add payout account
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/payout-accounts [post]
|
||||
// @Summary Add payout account
|
||||
// @Description 新增创作者收款账户
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.PayoutAccount true "Payout account form"
|
||||
// @Success 200 {string} string "Created"
|
||||
// @Bind form body
|
||||
func (c *Creator) AddPayoutAccount(ctx fiber.Ctx, form *dto.PayoutAccount) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.AddPayoutAccount(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// Remove payout account
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/payout-accounts [delete]
|
||||
// @Summary Remove payout account
|
||||
// @Description 删除创作者收款账户
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id query int64 true "Payout account ID"
|
||||
// @Success 200 {string} string "Deleted"
|
||||
// @Bind id query
|
||||
func (c *Creator) RemovePayoutAccount(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.RemovePayoutAccount(ctx, tenantID, userID, id)
|
||||
}
|
||||
|
||||
// Create withdraw order
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/withdraw [post]
|
||||
// @Summary Create withdraw order
|
||||
// @Description 发起提现申请
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.WithdrawForm true "Withdraw form"
|
||||
// @Success 200 {string} string "Submitted"
|
||||
// @Bind form body
|
||||
func (c *Creator) Withdraw(ctx fiber.Ctx, form *dto.WithdrawForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.Withdraw(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// List creator members
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members [get]
|
||||
// @Summary List creator members
|
||||
// @Description 查询创作者成员列表
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Param role query string false "Role"
|
||||
// @Param status query string false "Status"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.TenantMemberItem}
|
||||
// @Bind filter query
|
||||
func (c *Creator) ListMembers(ctx fiber.Ctx, filter *dto.TenantMemberListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.TenantMemberListFilter{}
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.ListMembers(ctx, tenantID, userID, filter)
|
||||
}
|
||||
|
||||
// Remove creator member
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members/:id<int> [delete]
|
||||
// @Summary Remove creator member
|
||||
// @Description 移除创作者成员
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Member relation ID"
|
||||
// @Success 200 {string} string "Removed"
|
||||
// @Bind id path
|
||||
func (c *Creator) RemoveMember(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.RemoveMember(ctx, tenantID, userID, id)
|
||||
}
|
||||
|
||||
// List creator member invites
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members/invites [get]
|
||||
// @Summary List creator member invites
|
||||
// @Description 查询创作者成员邀请记录
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param status query string false "Status"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.TenantInviteListItem}
|
||||
// @Bind filter query
|
||||
func (c *Creator) ListMemberInvites(ctx fiber.Ctx, filter *dto.TenantInviteListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.TenantInviteListFilter{}
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.ListInvites(ctx, tenantID, userID, filter)
|
||||
}
|
||||
|
||||
// Create creator member invite
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members/invite [post]
|
||||
// @Summary Create creator member invite
|
||||
// @Description 创建成员邀请
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.TenantInviteCreateForm true "Invite form"
|
||||
// @Success 200 {object} dto.TenantInviteItem
|
||||
// @Bind form body
|
||||
func (c *Creator) CreateMemberInvite(ctx fiber.Ctx, form *dto.TenantInviteCreateForm) (*dto.TenantInviteItem, error) {
|
||||
if form == nil {
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.CreateInvite(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// Disable creator member invite
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members/invites/:id<int> [delete]
|
||||
// @Summary Disable creator member invite
|
||||
// @Description 撤销成员邀请
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Invite ID"
|
||||
// @Success 200 {string} string "Disabled"
|
||||
// @Bind id path
|
||||
func (c *Creator) DisableMemberInvite(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.DisableInvite(ctx, tenantID, userID, id)
|
||||
}
|
||||
|
||||
// List creator join requests
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members/join-requests [get]
|
||||
// @Summary List creator join requests
|
||||
// @Description 查询成员加入申请
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param status query string false "Status"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.TenantJoinRequestItem}
|
||||
// @Bind filter query
|
||||
func (c *Creator) ListMemberJoinRequests(ctx fiber.Ctx, filter *dto.TenantJoinRequestListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.TenantJoinRequestListFilter{}
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.ListJoinRequests(ctx, tenantID, userID, filter)
|
||||
}
|
||||
|
||||
// Review creator join request
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/members/:id<int>/review [post]
|
||||
// @Summary Review creator join request
|
||||
// @Description 审核成员加入申请
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Join request ID"
|
||||
// @Param form body dto.TenantJoinReviewForm true "Review form"
|
||||
// @Success 200 {string} string "Reviewed"
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) ReviewMemberJoinRequest(ctx fiber.Ctx, id int64, form *dto.TenantJoinReviewForm) error {
|
||||
if form == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.ReviewJoin(ctx, tenantID, userID, id, form)
|
||||
}
|
||||
|
||||
// List creator coupons
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/coupons [get]
|
||||
// @Summary List creator coupons
|
||||
// @Description 查询创作者优惠券
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page"
|
||||
// @Param limit query int false "Limit"
|
||||
// @Param type query string false "Coupon type"
|
||||
// @Param status query string false "Coupon status"
|
||||
// @Param keyword query string false "Keyword"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.CouponItem}
|
||||
// @Bind filter query
|
||||
func (c *Creator) ListCoupons(ctx fiber.Ctx, filter *dto.CouponListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &dto.CouponListFilter{}
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
return services.Coupon.List(ctx, tenantID, filter)
|
||||
}
|
||||
|
||||
// Get creator coupon
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/coupons/:id<int> [get]
|
||||
// @Summary Get creator coupon
|
||||
// @Description 查询创作者优惠券详情
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Coupon ID"
|
||||
// @Success 200 {object} dto.CouponItem
|
||||
// @Bind id path
|
||||
func (c *Creator) GetCoupon(ctx fiber.Ctx, id int64) (*dto.CouponItem, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
return services.Coupon.Get(ctx, tenantID, id)
|
||||
}
|
||||
|
||||
// Create creator coupon
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/coupons [post]
|
||||
// @Summary Create creator coupon
|
||||
// @Description 创建创作者优惠券
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.CouponCreateForm true "Coupon create form"
|
||||
// @Success 200 {object} dto.CouponItem
|
||||
// @Bind form body
|
||||
func (c *Creator) CreateCoupon(ctx fiber.Ctx, form *dto.CouponCreateForm) (*dto.CouponItem, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Coupon.Create(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
// Update creator coupon
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/coupons/:id<int> [put]
|
||||
// @Summary Update creator coupon
|
||||
// @Description 更新创作者优惠券
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Coupon ID"
|
||||
// @Param form body dto.CouponUpdateForm true "Coupon update form"
|
||||
// @Success 200 {object} dto.CouponItem
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) UpdateCoupon(ctx fiber.Ctx, id int64, form *dto.CouponUpdateForm) (*dto.CouponItem, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Coupon.Update(ctx, tenantID, userID, id, form)
|
||||
}
|
||||
|
||||
// Grant creator coupon
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/coupons/:id<int>/grant [post]
|
||||
// @Summary Grant creator coupon
|
||||
// @Description 向用户发放优惠券
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Coupon ID"
|
||||
// @Param form body dto.CouponGrantForm true "Grant form"
|
||||
// @Success 200 {string} string "Granted"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) GrantCoupon(ctx fiber.Ctx, _ *models.User, id int64, form *dto.CouponGrantForm) (string, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
func (c *Creator) GrantCoupon(ctx fiber.Ctx, id int64, form *dto.CouponGrantForm) (string, error) {
|
||||
if form == nil {
|
||||
return "", errorx.ErrInvalidParameter.WithMsg("参数无效")
|
||||
}
|
||||
|
||||
tenantID := getTenantID(ctx)
|
||||
_, err := services.Coupon.Grant(ctx, tenantID, id, form.UserIDs)
|
||||
if err != nil {
|
||||
return "", errorx.ErrOperationFailed.WithCause(err)
|
||||
@@ -67,3 +589,41 @@ func (c *Creator) GrantCoupon(ctx fiber.Ctx, _ *models.User, id int64, form *dto
|
||||
|
||||
return "Granted", nil
|
||||
}
|
||||
|
||||
// Creator report overview
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/reports/overview [get]
|
||||
// @Summary Creator report overview
|
||||
// @Description 创作者经营看板概览
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param start_at query string false "Start at (RFC3339)"
|
||||
// @Param end_at query string false "End at (RFC3339)"
|
||||
// @Param granularity query string false "Granularity"
|
||||
// @Success 200 {object} dto.ReportOverviewResponse
|
||||
// @Bind filter query
|
||||
func (c *Creator) ReportOverview(ctx fiber.Ctx, filter *dto.ReportOverviewFilter) (*dto.ReportOverviewResponse, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.ReportOverview(ctx, tenantID, userID, filter)
|
||||
}
|
||||
|
||||
// Creator export report
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creator/reports/export [post]
|
||||
// @Summary Creator export report
|
||||
// @Description 导出创作者经营报表
|
||||
// @Tags CreatorCenter
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.ReportExportForm true "Export form"
|
||||
// @Success 200 {object} dto.ReportExportResponse
|
||||
// @Bind form body
|
||||
func (c *Creator) ExportReport(ctx fiber.Ctx, form *dto.ReportExportForm) (*dto.ReportExportResponse, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Creator.ExportReport(ctx, tenantID, userID, form)
|
||||
}
|
||||
|
||||
@@ -150,6 +150,154 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
r.content.AddLike,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
// Register routes for controller: Creator
|
||||
r.log.Debugf("Registering route: Delete /v1/t/:tenantCode/creator/contents/:id<int> -> creator.DeleteContent")
|
||||
router.Delete("/v1/t/:tenantCode/creator/contents/:id<int>"[len(r.Path()):], Func1(
|
||||
r.creator.DeleteContent,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/t/:tenantCode/creator/members/:id<int> -> creator.RemoveMember")
|
||||
router.Delete("/v1/t/:tenantCode/creator/members/:id<int>"[len(r.Path()):], Func1(
|
||||
r.creator.RemoveMember,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/t/:tenantCode/creator/members/invites/:id<int> -> creator.DisableMemberInvite")
|
||||
router.Delete("/v1/t/:tenantCode/creator/members/invites/:id<int>"[len(r.Path()):], Func1(
|
||||
r.creator.DisableMemberInvite,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/t/:tenantCode/creator/payout-accounts -> creator.RemovePayoutAccount")
|
||||
router.Delete("/v1/t/:tenantCode/creator/payout-accounts"[len(r.Path()):], Func1(
|
||||
r.creator.RemovePayoutAccount,
|
||||
QueryParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/contents -> creator.ListContents")
|
||||
router.Get("/v1/t/:tenantCode/creator/contents"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListContents,
|
||||
Query[dto.CreatorContentListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/contents/:id<int> -> creator.GetContent")
|
||||
router.Get("/v1/t/:tenantCode/creator/contents/:id<int>"[len(r.Path()):], DataFunc1(
|
||||
r.creator.GetContent,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/coupons -> creator.ListCoupons")
|
||||
router.Get("/v1/t/:tenantCode/creator/coupons"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListCoupons,
|
||||
Query[dto.CouponListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/coupons/:id<int> -> creator.GetCoupon")
|
||||
router.Get("/v1/t/:tenantCode/creator/coupons/:id<int>"[len(r.Path()):], DataFunc1(
|
||||
r.creator.GetCoupon,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/dashboard -> creator.Dashboard")
|
||||
router.Get("/v1/t/:tenantCode/creator/dashboard"[len(r.Path()):], DataFunc0(
|
||||
r.creator.Dashboard,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/members -> creator.ListMembers")
|
||||
router.Get("/v1/t/:tenantCode/creator/members"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListMembers,
|
||||
Query[dto.TenantMemberListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/members/invites -> creator.ListMemberInvites")
|
||||
router.Get("/v1/t/:tenantCode/creator/members/invites"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListMemberInvites,
|
||||
Query[dto.TenantInviteListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/members/join-requests -> creator.ListMemberJoinRequests")
|
||||
router.Get("/v1/t/:tenantCode/creator/members/join-requests"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListMemberJoinRequests,
|
||||
Query[dto.TenantJoinRequestListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/orders -> creator.ListOrders")
|
||||
router.Get("/v1/t/:tenantCode/creator/orders"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListOrders,
|
||||
Query[dto.CreatorOrderListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/payout-accounts -> creator.ListPayoutAccounts")
|
||||
router.Get("/v1/t/:tenantCode/creator/payout-accounts"[len(r.Path()):], DataFunc0(
|
||||
r.creator.ListPayoutAccounts,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/reports/overview -> creator.ReportOverview")
|
||||
router.Get("/v1/t/:tenantCode/creator/reports/overview"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ReportOverview,
|
||||
Query[dto.ReportOverviewFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/settings -> creator.GetSettings")
|
||||
router.Get("/v1/t/:tenantCode/creator/settings"[len(r.Path()):], DataFunc0(
|
||||
r.creator.GetSettings,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/apply -> creator.Apply")
|
||||
router.Post("/v1/t/:tenantCode/creator/apply"[len(r.Path()):], Func1(
|
||||
r.creator.Apply,
|
||||
Body[dto.ApplyForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/contents -> creator.CreateContent")
|
||||
router.Post("/v1/t/:tenantCode/creator/contents"[len(r.Path()):], Func1(
|
||||
r.creator.CreateContent,
|
||||
Body[dto.ContentCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/coupons -> creator.CreateCoupon")
|
||||
router.Post("/v1/t/:tenantCode/creator/coupons"[len(r.Path()):], DataFunc1(
|
||||
r.creator.CreateCoupon,
|
||||
Body[dto.CouponCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/coupons/:id<int>/grant -> creator.GrantCoupon")
|
||||
router.Post("/v1/t/:tenantCode/creator/coupons/:id<int>/grant"[len(r.Path()):], DataFunc2(
|
||||
r.creator.GrantCoupon,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.CouponGrantForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/members/:id<int>/review -> creator.ReviewMemberJoinRequest")
|
||||
router.Post("/v1/t/:tenantCode/creator/members/:id<int>/review"[len(r.Path()):], Func2(
|
||||
r.creator.ReviewMemberJoinRequest,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.TenantJoinReviewForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/members/invite -> creator.CreateMemberInvite")
|
||||
router.Post("/v1/t/:tenantCode/creator/members/invite"[len(r.Path()):], DataFunc1(
|
||||
r.creator.CreateMemberInvite,
|
||||
Body[dto.TenantInviteCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/orders/:id<int>/refund -> creator.RefundOrder")
|
||||
router.Post("/v1/t/:tenantCode/creator/orders/:id<int>/refund"[len(r.Path()):], Func2(
|
||||
r.creator.RefundOrder,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.RefundForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/payout-accounts -> creator.AddPayoutAccount")
|
||||
router.Post("/v1/t/:tenantCode/creator/payout-accounts"[len(r.Path()):], Func1(
|
||||
r.creator.AddPayoutAccount,
|
||||
Body[dto.PayoutAccount]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/reports/export -> creator.ExportReport")
|
||||
router.Post("/v1/t/:tenantCode/creator/reports/export"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ExportReport,
|
||||
Body[dto.ReportExportForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/creator/withdraw -> creator.Withdraw")
|
||||
router.Post("/v1/t/:tenantCode/creator/withdraw"[len(r.Path()):], Func1(
|
||||
r.creator.Withdraw,
|
||||
Body[dto.WithdrawForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/t/:tenantCode/creator/contents/:id<int> -> creator.UpdateContent")
|
||||
router.Put("/v1/t/:tenantCode/creator/contents/:id<int>"[len(r.Path()):], Func2(
|
||||
r.creator.UpdateContent,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.ContentUpdateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/t/:tenantCode/creator/coupons/:id<int> -> creator.UpdateCoupon")
|
||||
router.Put("/v1/t/:tenantCode/creator/coupons/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.creator.UpdateCoupon,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.CouponUpdateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/t/:tenantCode/creator/settings -> creator.UpdateSettings")
|
||||
router.Put("/v1/t/:tenantCode/creator/settings"[len(r.Path()):], Func1(
|
||||
r.creator.UpdateSettings,
|
||||
Body[dto.Settings]("form"),
|
||||
))
|
||||
// Register routes for controller: Storage
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/storage/* -> storage.Download")
|
||||
router.Get("/v1/t/:tenantCode/storage/*"[len(r.Path()):], Func2(
|
||||
@@ -163,6 +311,44 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
QueryParam[string]("expires"),
|
||||
QueryParam[string]("sign"),
|
||||
))
|
||||
// Register routes for controller: Tenant
|
||||
r.log.Debugf("Registering route: Delete /v1/t/:tenantCode/tenants/:id<int>/follow -> tenant.Unfollow")
|
||||
router.Delete("/v1/t/:tenantCode/tenants/:id<int>/follow"[len(r.Path()):], Func1(
|
||||
r.tenant.Unfollow,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/t/:tenantCode/tenants/:id<int>/join -> tenant.CancelJoin")
|
||||
router.Delete("/v1/t/:tenantCode/tenants/:id<int>/join"[len(r.Path()):], Func1(
|
||||
r.tenant.CancelJoin,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/tenants -> tenant.List")
|
||||
router.Get("/v1/t/:tenantCode/tenants"[len(r.Path()):], DataFunc1(
|
||||
r.tenant.List,
|
||||
Query[dto.TenantListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/tenants/:id<int> -> tenant.Get")
|
||||
router.Get("/v1/t/:tenantCode/tenants/:id<int>"[len(r.Path()):], DataFunc1(
|
||||
r.tenant.Get,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/tenants/:id<int>/follow -> tenant.Follow")
|
||||
router.Post("/v1/t/:tenantCode/tenants/:id<int>/follow"[len(r.Path()):], Func1(
|
||||
r.tenant.Follow,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/tenants/:id<int>/invites/accept -> tenant.AcceptInvite")
|
||||
router.Post("/v1/t/:tenantCode/tenants/:id<int>/invites/accept"[len(r.Path()):], Func2(
|
||||
r.tenant.AcceptInvite,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.TenantInviteAcceptForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/t/:tenantCode/tenants/:id<int>/join -> tenant.ApplyJoin")
|
||||
router.Post("/v1/t/:tenantCode/tenants/:id<int>/join"[len(r.Path()):], Func2(
|
||||
r.tenant.ApplyJoin,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.TenantJoinApplyForm]("form"),
|
||||
))
|
||||
// Register routes for controller: Transaction
|
||||
r.log.Debugf("Registering route: Get /v1/t/:tenantCode/orders/:id<int>/status -> transaction.Status")
|
||||
router.Get("/v1/t/:tenantCode/orders/:id<int>/status"[len(r.Path()):], DataFunc1(
|
||||
|
||||
@@ -3,6 +3,7 @@ package v1
|
||||
import (
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
@@ -11,17 +12,135 @@ import (
|
||||
// @provider
|
||||
type Tenant struct{}
|
||||
|
||||
// List creator contents
|
||||
// List tenants
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/creators/:id<int>/contents [get]
|
||||
// @Router /v1/t/:tenantCode/tenants [get]
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int> [get]
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/follow [post]
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/follow [delete]
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/join [post]
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/join [delete]
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/invites/accept [post]
|
||||
// @Summary List tenants
|
||||
// @Description List public tenants under current tenant scope
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Param keyword query string false "Search keyword"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.TenantProfile}
|
||||
// @Bind filter query
|
||||
func (t *Tenant) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
return services.Tenant.List(ctx, tenantID, filter)
|
||||
}
|
||||
|
||||
// Get tenant profile
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int> [get]
|
||||
// @Summary Get tenant profile
|
||||
// @Description Get public tenant profile by tenant ID
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Success 200 {object} dto.TenantProfile
|
||||
// @Bind id path
|
||||
func (t *Tenant) Get(ctx fiber.Ctx, id int64) (*dto.TenantProfile, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.GetPublicProfile(ctx, id, userID)
|
||||
}
|
||||
|
||||
// Follow tenant
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/follow [post]
|
||||
// @Summary Follow tenant
|
||||
// @Description Follow a tenant
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Success 200 {string} string "Followed"
|
||||
// @Bind id path
|
||||
func (t *Tenant) Follow(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.Follow(ctx, id, userID)
|
||||
}
|
||||
|
||||
// Unfollow tenant
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/follow [delete]
|
||||
// @Summary Unfollow tenant
|
||||
// @Description Unfollow a tenant
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Success 200 {string} string "Unfollowed"
|
||||
// @Bind id path
|
||||
func (t *Tenant) Unfollow(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.Unfollow(ctx, id, userID)
|
||||
}
|
||||
|
||||
// Apply to join tenant
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/join [post]
|
||||
// @Summary Apply to join tenant
|
||||
// @Description Apply to join a tenant
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Param form body dto.TenantJoinApplyForm true "Join apply form"
|
||||
// @Success 200 {string} string "Applied"
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (t *Tenant) ApplyJoin(ctx fiber.Ctx, id int64, form *dto.TenantJoinApplyForm) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.ApplyJoin(ctx, id, userID, form)
|
||||
}
|
||||
|
||||
// Cancel join application
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/join [delete]
|
||||
// @Summary Cancel join application
|
||||
// @Description Cancel pending tenant join application
|
||||
// @Tags TenantPublic
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Tenant ID"
|
||||
// @Success 200 {string} string "Canceled"
|
||||
// @Bind id path
|
||||
func (t *Tenant) CancelJoin(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
userID := getUserID(ctx)
|
||||
|
||||
return services.Tenant.CancelJoin(ctx, id, userID)
|
||||
}
|
||||
|
||||
// Accept tenant invite
|
||||
//
|
||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/invites/accept [post]
|
||||
// @Summary Accept tenant invite
|
||||
// @Description Accept a tenant invite by code
|
||||
// @Tags TenantPublic
|
||||
|
||||
@@ -181,7 +181,7 @@ func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, e
|
||||
// @Param content_id query int64 true "Content ID"
|
||||
// @Success 200 {string} string "Added"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId query key(content_id)
|
||||
// @Bind contentID query key(content_id)
|
||||
func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
@@ -199,7 +199,7 @@ func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentID int64) er
|
||||
// @Param contentId path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Removed"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId path
|
||||
// @Bind contentID path key(contentId)
|
||||
func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
@@ -233,7 +233,7 @@ func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error
|
||||
// @Param content_id query int64 true "Content ID"
|
||||
// @Success 200 {string} string "Liked"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId query key(content_id)
|
||||
// @Bind contentID query key(content_id)
|
||||
func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
@@ -251,7 +251,7 @@ func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentID int64) error
|
||||
// @Param contentId path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Unliked"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId path
|
||||
// @Bind contentID path key(contentId)
|
||||
func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
|
||||
|
||||
@@ -61,7 +61,6 @@ func Test_MediaProcessWorkerLocal(t *testing.T) {
|
||||
testx.Serve(providers, t, func(p MediaProcessWorkerTestSuiteInjectParams) {
|
||||
suite.Run(t, &MediaProcessWorkerLocalSuite{MediaProcessWorkerTestSuiteInjectParams: p})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *MediaProcessWorkerLocalSuite) Test_Work_Local() {
|
||||
|
||||
@@ -46,7 +46,6 @@ func Test_Middlewares(t *testing.T) {
|
||||
testx.Serve(providers, t, func(p MiddlewaresTestSuiteInjectParams) {
|
||||
suite.Run(t, &MiddlewaresTestSuite{MiddlewaresTestSuiteInjectParams: p})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *MiddlewaresTestSuite) newTestApp() *fiber.App {
|
||||
|
||||
@@ -594,6 +594,9 @@ func (s *common) GetAssetURL(objectKey string) string {
|
||||
if objectKey == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(objectKey, "http://") || strings.HasPrefix(objectKey, "https://") {
|
||||
return objectKey
|
||||
}
|
||||
url, _ := s.storage.SignURL("GET", objectKey, 1*time.Hour)
|
||||
|
||||
return url
|
||||
|
||||
@@ -263,12 +263,35 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
|
||||
return &transaction_dto.OrderPayResponse{Status: string(consts.OrderStatusPaid)}, nil
|
||||
}
|
||||
|
||||
func (s *order) settleRechargeOrder(ctx context.Context, order *models.Order) error {
|
||||
func (s *order) settleRechargeOrder(ctx context.Context, tx *models.Query, order *models.Order) error {
|
||||
if order == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("充值订单不存在")
|
||||
}
|
||||
if tx == nil {
|
||||
return errorx.ErrInvalidParameter.WithMsg("事务上下文缺失")
|
||||
}
|
||||
|
||||
return s.settleOrder(ctx, order, "recharge")
|
||||
_, err := tx.User.WithContext(ctx).
|
||||
Where(tx.User.ID.Eq(order.UserID)).
|
||||
Update(tx.User.Balance, gorm.Expr("balance + ?", order.AmountPaid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
info, err := tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(order.ID)).Updates(&models.Order{
|
||||
Status: consts.OrderStatusPaid,
|
||||
PaidAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.RowsAffected == 0 {
|
||||
return errorx.ErrRecordNotFound.WithMsg("充值订单不存在")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *order) settleOrder(ctx context.Context, o *models.Order, method string) error {
|
||||
@@ -314,14 +337,15 @@ func (s *order) settleOrder(ctx context.Context, o *models.Order, method string)
|
||||
// 3. Grant Content Access
|
||||
items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find()
|
||||
for _, item := range items {
|
||||
// Check if access already exists (idempotency)
|
||||
exists, _ := tx.ContentAccess.WithContext(ctx).
|
||||
Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.Eq(item.ContentID)).
|
||||
Exists()
|
||||
if exists {
|
||||
continue
|
||||
tblAccess, qAccess := tx.ContentAccess.QueryContext(ctx)
|
||||
existingAccess, err := qAccess.Where(
|
||||
tblAccess.UserID.Eq(o.UserID),
|
||||
tblAccess.ContentID.Eq(item.ContentID),
|
||||
).First()
|
||||
if err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
access := &models.ContentAccess{
|
||||
TenantID: item.TenantID,
|
||||
UserID: o.UserID,
|
||||
@@ -329,7 +353,20 @@ func (s *order) settleOrder(ctx context.Context, o *models.Order, method string)
|
||||
OrderID: o.ID,
|
||||
Status: consts.ContentAccessStatusActive,
|
||||
}
|
||||
if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil {
|
||||
if err := qAccess.Create(access); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = qAccess.Where(tblAccess.ID.Eq(existingAccess.ID)).Updates(&models.ContentAccess{
|
||||
TenantID: item.TenantID,
|
||||
OrderID: o.ID,
|
||||
Status: consts.ContentAccessStatusActive,
|
||||
RevokedAt: time.Time{},
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
BIN
breadcrumb_jingju.png
Normal file
BIN
breadcrumb_jingju.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
147
docs/plan.md
147
docs/plan.md
@@ -0,0 +1,147 @@
|
||||
# Implementation Plan: v1 Creator 路由恢复与闭环验证
|
||||
|
||||
**Branch**: `[current-working-branch]` | **Date**: 2026-02-07 | **Spec**: 会话需求(修复 `/v1/t/:tenantCode/creator/*` 404)
|
||||
**Input**: 用户需求:开始修复 Creator 路由缺失问题,恢复 Portal 创作者中心 API 可用性并完成回归验证。
|
||||
|
||||
## Summary
|
||||
|
||||
当前 `backend/app/http/v1/creator.go` 仅保留 `GrantCoupon` 方法,但注释块包含大量与该方法签名不匹配的 `@Router` 声明,导致 `atomctl gen route` 未生成任何 `/v1/t/:tenantCode/creator/*` 路由。计划将按“前端真实调用路径 -> 后端服务能力 -> 控制器显式方法”逐项恢复,最小改动修复,随后通过路由生成、Go 测试与关键 API 冒烟验证闭环。
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**:
|
||||
- Backend: Go (Fiber + GORM-Gen)
|
||||
- Frontend reference: Vue 3(仅用于接口映射,不改前端)
|
||||
|
||||
**Primary Dependencies**:
|
||||
- `atomctl gen route`(路由生成)
|
||||
- `backend/app/services`(Creator/Tenant/Coupon 服务)
|
||||
- `backend/app/http/v1/dto`(Creator 与 TenantMember DTO)
|
||||
|
||||
**Storage**: PostgreSQL(使用现有 schema 与 seed 数据)
|
||||
|
||||
**Testing**:
|
||||
- `cd backend && env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go test ./...`
|
||||
- API 冒烟:`/v1/t/:tenantCode/creator/orders` 等关键路径不再 404
|
||||
|
||||
**Target Platform**: Linux 本地环境(backend: `127.0.0.1:18080`)
|
||||
|
||||
**Project Type**: Web backend API
|
||||
|
||||
**Performance Goals**:
|
||||
- 本次以功能恢复为目标,不新增性能指标
|
||||
|
||||
**Constraints**:
|
||||
- 不手改任何 `*.gen.go`
|
||||
- 控制器保持薄层(参数绑定 -> services.* -> 返回)
|
||||
- 路由参数使用 `camelCase`,数值 path 参数使用 `:id<int>`
|
||||
- 仅做最小修复,不做与问题无关重构
|
||||
|
||||
**Scale/Scope**:
|
||||
- 仅修复 `backend/app/http/v1/creator.go` 路由缺失问题
|
||||
- 影响生成文件:`backend/app/http/v1/routes.gen.go`(通过生成器更新)
|
||||
- 更新回归记录:`docs/test-matrix.md`
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- ✅ 控制器薄层:方法将仅负责绑定和调用 service。
|
||||
- ✅ 生成文件规范:仅通过 `atomctl gen route` 更新路由生成文件。
|
||||
- ✅ 事务与数据访问边界:不在 controller 做任何 DB 操作。
|
||||
- ✅ 验证要求:包含 `go test ./...` + 关键页面流 API 冒烟证据。
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
docs/
|
||||
├── plan.md # 当前计划(本文件)
|
||||
└── test-matrix.md # 回归记录更新
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
backend/
|
||||
├── app/http/v1/
|
||||
│ ├── creator.go # 本次主要修复文件
|
||||
│ └── routes.gen.go # 由 atomctl 生成
|
||||
├── app/http/v1/dto/
|
||||
│ ├── creator.go
|
||||
│ ├── creator_report.go
|
||||
│ └── tenant_member.go
|
||||
└── app/services/
|
||||
├── creator.go
|
||||
├── creator_report.go
|
||||
├── tenant_member.go
|
||||
└── coupon.go
|
||||
|
||||
frontend/portal/
|
||||
└── src/api/creator.js # 仅用于接口映射核对
|
||||
```
|
||||
|
||||
**Structure Decision**: 使用现有后端 v1 模块结构,控制器集中恢复,服务层复用现有能力,不新增新模块。
|
||||
|
||||
## Plan Phases
|
||||
|
||||
### Phase 1 — 路由映射与控制器设计
|
||||
- 依据 `frontend/portal/src/api/creator.js` 提取全部 `/creator/*` 调用。
|
||||
- 将调用映射到现有 service 方法(Creator/Tenant/Coupon)。
|
||||
- 为每个 endpoint 设计独立 controller 方法与准确 `@Router/@Bind`。
|
||||
|
||||
### Phase 2 — 控制器实现与路由生成
|
||||
- 重写 `backend/app/http/v1/creator.go`:一条路由一个方法,去除“多路由堆在单方法注释”反模式。
|
||||
- 执行 `atomctl gen route`,确认 `routes.gen.go` 产出完整 `/v1/t/:tenantCode/creator/*` 注册项。
|
||||
|
||||
### Phase 3 — 回归验证与文档更新
|
||||
- 运行 `go test ./...`。
|
||||
- 用 token 对至少以下接口冒烟:
|
||||
- `GET /v1/t/:tenantCode/creator/orders`
|
||||
- `GET /v1/t/:tenantCode/creator/contents`
|
||||
- `GET /v1/t/:tenantCode/creator/settings`
|
||||
- 将修复结果与证据补充到 `docs/test-matrix.md`。
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] T1 从 `frontend/portal/src/api/creator.js` 提取 endpoint 清单并建立 service 映射。
|
||||
- [ ] T2 在 `backend/app/http/v1/creator.go` 实现 creator 核心接口:apply/dashboard/contents/orders/settings/payout/withdraw。
|
||||
- [ ] T3 在 `backend/app/http/v1/creator.go` 实现成员与邀请相关接口:members/invites/join-requests/review。
|
||||
- [ ] T4 在 `backend/app/http/v1/creator.go` 实现优惠券相关接口:list/get/create/update/grant。
|
||||
- [ ] T5 在 `backend/app/http/v1/creator.go` 实现报表相关接口:reports/overview 与 reports/export。
|
||||
- [ ] T6 执行 `atomctl gen route` 并确认 `/creator/*` 路由已注册。
|
||||
- [ ] T7 执行 `go test ./...`,修复由本次改动引入的问题。
|
||||
- [ ] T8 执行 Creator 关键接口冒烟并记录结果。
|
||||
- [ ] T9 更新 `docs/test-matrix.md` 记录 Creator 修复闭环结果。
|
||||
|
||||
## Dependencies
|
||||
|
||||
- T1 -> T2/T3/T4/T5(先映射后编码)
|
||||
- T2/T3/T4/T5 -> T6(代码落地后生成路由)
|
||||
- T6 -> T7/T8(先确认路由注册,再跑测试与冒烟)
|
||||
- T7/T8 -> T9(测试证据写入文档)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
1. `backend/app/http/v1/routes.gen.go` 中存在并注册 `/v1/t/:tenantCode/creator/*` 对应路由。
|
||||
2. `GET /v1/t/:tenantCode/creator/orders` 不再返回 404(认证通过前提下)。
|
||||
3. `go test ./...` 通过(若有历史失败,需明确标注非本次引入)。
|
||||
4. `docs/test-matrix.md` 新增 Creator 路由修复结果与可复现命令。
|
||||
|
||||
## Risks
|
||||
|
||||
- **DTO 不匹配风险**:部分接口需复用 `tenant_member` DTO,可能出现绑定字段不一致。
|
||||
- 缓解:按现有 service 签名逐项对齐 `@Bind`。
|
||||
|
||||
- **路由冲突风险**:新增 `/creator/*` 可能与其他路径发生顺序/匹配冲突。
|
||||
- 缓解:依赖生成器产出并通过启动日志核对注册项。
|
||||
|
||||
- **权限语义偏差风险**:Controller 恢复后可能暴露 service 内已有“仅租户主”限制,导致与前端预期不一致。
|
||||
- 缓解:先恢复 404 问题;权限语义差异单独记录为后续优化项。
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| N/A | N/A | N/A |
|
||||
|
||||
362
docs/test-matrix.md
Normal file
362
docs/test-matrix.md
Normal file
@@ -0,0 +1,362 @@
|
||||
# 全项目页面逐按钮测试矩阵(落盘存档)
|
||||
|
||||
更新日期:2026-02-07(Asia/Shanghai)
|
||||
|
||||
## 1) 测试范围
|
||||
|
||||
- Portal:`frontend/portal/src/router/index.js` 所有业务路由(含登录、主页、频道、内容详情、用户中心、创作者中心、结算支付)。
|
||||
- Superadmin:`frontend/superadmin/src/router/index.js` 所有 superadmin 路由 + uikit/pages/landing 路由。
|
||||
- 粒度:页面级 + 按钮/链接级(可见可交互控件采样执行)。
|
||||
- 数据前置:已执行补齐后的 seed(见 `backend/app/commands/seed/seed.go`)。
|
||||
|
||||
## 2) 测试环境
|
||||
|
||||
- Backend API: `http://127.0.0.1:18080`(healthz=204)
|
||||
- Superadmin: `http://127.0.0.1:5173`
|
||||
- Portal: `http://127.0.0.1:5174`
|
||||
- 租户代码:`meipai_990`
|
||||
- Portal登录:手机号 `13800138000` + OTP `1234`
|
||||
- Superadmin登录:`superadmin / superadmin123`
|
||||
|
||||
## 3) Portal 页面矩阵(按页面)
|
||||
|
||||
> 说明:`ok=false` 表示页面可打开但检测到错误关键词(如 404/not found 等),通常来自页面内某些资源/API,不一定是整页不可用。
|
||||
|
||||
| Route | 结果 | 代表按钮(采样) | 备注 |
|
||||
|---|---|---|---|
|
||||
| `/t/meipai_990` | ⚠️ 部分异常 | 退出登录 / 推荐 / 最新 / 热门 / 全部 / 京剧... | 存在 `/v1/t/:tenantCode/tenants` 404 |
|
||||
| `/t/meipai_990/channel` | ⚠️ 部分异常 | 退出登录 / 私信 / 主页 / 关于 / 最新 / 最热 | 同上,频道页伴随 tenants 404 |
|
||||
| `/t/meipai_990/contents/1` | ✅ | 发布评论 / 回复 / 关注 / 私信 | 详情页主流程可用 |
|
||||
| `/t/meipai_990/explore` | ⚠️ 部分异常 | 分类过滤按钮组、免费/付费 | 存在 tenants 404 |
|
||||
| `/t/meipai_990/topics` | ✅ | 导航/跳转链接可用 | 页面可达 |
|
||||
| `/t/meipai_990/creator/apply` | ✅ | 立即申请入驻 | 可提交入口存在 |
|
||||
| `/t/meipai_990/creator/contents/new` | ✅ | 存草稿 / 发布 / 免费/付费/会员专享 | 创作发布页可达 |
|
||||
| `/t/meipai_990/me` | ✅ | 退出登录 / 清空历史 | 个人中心可达 |
|
||||
| `/t/meipai_990/me/orders` | ✅ | 全部订单/待支付/已完成/退款售后/去支付/取消订单 | 订单列表动作可见 |
|
||||
| `/t/meipai_990/me/orders/1` | ✅ | 复制 / 联系商家 | 详情页可达 |
|
||||
| `/t/meipai_990/me/wallet` | ✅ | 立即充值 | 钱包页可达 |
|
||||
| `/t/meipai_990/me/coupons` | ✅ | 未使用/已使用/已过期 | 状态切换可见 |
|
||||
| `/t/meipai_990/me/library` | ✅ | 立即阅读 | 已购库可达 |
|
||||
| `/t/meipai_990/me/favorites` | ✅ | 批量管理 / 取消收藏 | 收藏页可达 |
|
||||
| `/t/meipai_990/me/likes` | ✅ | 取消点赞 | 点赞页可达 |
|
||||
| `/t/meipai_990/me/notifications` | ✅ | 全部/系统/订单/审核/互动/全部已读 | 通知中心可达 |
|
||||
| `/t/meipai_990/me/profile` | ✅ | 保存修改 | 资料页可达 |
|
||||
| `/t/meipai_990/me/security` | ✅ | 更换 / 去认证 | 安全页可达 |
|
||||
| `/t/meipai_990/checkout?contentId=1` | ✅ | 提交订单 / 取消 | 下单页可达 |
|
||||
| `/t/meipai_990/payment/1` | ✅ | 确认余额支付 | 支付页可达 |
|
||||
|
||||
### Portal 关键异常
|
||||
|
||||
1. `GET /v1/t/meipai_990/tenants?limit=5` 返回 404(页面顶部若依赖该接口,会触发异常标记)。
|
||||
2. 多条 `/v1/storage/https://images.unsplash.com/...` 返回 404(种子中外链被当作 storage path 访问,导致图片资源请求失败)。
|
||||
|
||||
## 4) Superadmin 页面矩阵(按页面)
|
||||
|
||||
| Route | 结果 | 按钮数量(采样统计) | 代表按钮(采样) |
|
||||
|---|---|---:|---|
|
||||
| `/super/` | ✅ | 6 | Aura/Lara/Nora/Static/Overlay/Logout |
|
||||
| `/super/superadmin/tenants` | ✅ | 20 | 健康概览/创建租户/查询/重置/展开 |
|
||||
| `/super/superadmin/health` | ✅ | 10 | 刷新/查询/重置/展开 |
|
||||
| `/super/superadmin/users` | ✅ | 34 | 查询/重置/展开/分页 |
|
||||
| `/super/superadmin/users/1` | ✅ | 44 | 资料/角色/状态/拥有租户/加入租户 |
|
||||
| `/super/superadmin/orders` | ✅ | 41 | 查询/重置/退款/标记问题/完成对账 |
|
||||
| `/super/superadmin/orders/1` | ✅ | 9 | 取消标记/撤销对账/退款 |
|
||||
| `/super/superadmin/contents` | ✅ | 36 | 内容列表/评论治理/举报治理/批量通过/批量驳回 |
|
||||
| `/super/superadmin/creators` | ✅ | 45 | 创作者列表/申请审核/成员审核/结算账户 |
|
||||
| `/super/superadmin/coupons` | ✅ | 34 | 券模板/发放记录/异常核查/新建优惠券 |
|
||||
| `/super/superadmin/finance` | ✅ | 31 | 提现审核/钱包流水/异常排查/刷新 |
|
||||
| `/super/superadmin/reports` | ✅ | 23 | 导出CSV/查询/重置 |
|
||||
| `/super/superadmin/assets` | ✅ | 11 | 查询/重置/展开 |
|
||||
| `/super/superadmin/notifications` | ✅ | 23 | 通知列表/模板管理/群发通知 |
|
||||
| `/super/superadmin/audit-logs` | ✅ | 11 | 查询/重置/展开 |
|
||||
| `/super/superadmin/system-configs` | ✅ | 13 | 新建配置/查询/重置/编辑 |
|
||||
| `/super/pages/empty` | ✅ | 6 | 基础布局按钮 |
|
||||
| `/super/pages/crud` | ✅ | 12 | New/Delete/Export/分页 |
|
||||
| `/super/documentation` | ✅ | 6 | 基础布局按钮 |
|
||||
| `/super/landing` | ✅ | 4 | Register/Get Started |
|
||||
| `/super/uikit/button` | ✅ | 66 | Submit/Disabled/Link/Primary... |
|
||||
| `/super/uikit/formlayout` | ✅ | 7 | Submit |
|
||||
| `/super/uikit/input` | ✅ | 11 | Option1/2/3/Search |
|
||||
| `/super/uikit/table` | ✅ | 15 | Clear/分页 |
|
||||
| `/super/uikit/list` | ✅ | 12 | Buy Now |
|
||||
| `/super/uikit/tree` | ✅ | 6 | 基础布局按钮 |
|
||||
| `/super/uikit/panel` | ✅ | 16 | Save/Header tabs |
|
||||
| `/super/uikit/overlay` | ✅ | 11 | Show/Save/Confirm/Delete |
|
||||
| `/super/uikit/media` | ✅ | 6 | 基础布局按钮 |
|
||||
| `/super/uikit/message` | ✅ | 10 | Success/Info/Warn/Error |
|
||||
| `/super/uikit/file` | ✅ | 11 | Choose/Upload/Cancel |
|
||||
| `/super/uikit/menu` | ✅ | 13 | Header I/II/III |
|
||||
| `/super/uikit/charts` | ✅ | 6 | 基础布局按钮 |
|
||||
| `/super/uikit/misc` | ✅ | 8 | Emails/Messages |
|
||||
| `/super/uikit/timeline` | ✅ | 10 | Read more |
|
||||
|
||||
## 5) 失败/阻塞项复现
|
||||
|
||||
### F1(高)Portal tenants 接口 404
|
||||
- 现象:首页/频道/探索页出现 `ok=false`,并在后端日志出现 404。
|
||||
- 复现:登录 Portal 后访问 `/t/meipai_990` 或 `/t/meipai_990/channel`。
|
||||
- 后端日志:`GET /v1/t/meipai_990/tenants?limit=5` -> 404。
|
||||
- 影响:门户首页顶部/频道信息可能缺失或报错。
|
||||
- 建议优先级:**P1**(核心入口页异常)。
|
||||
|
||||
### F2(中)Portal 外链图片走 storage 路由导致 404
|
||||
- 现象:大量 `/v1/storage/https://images.unsplash.com/...` 404。
|
||||
- 复现:访问首页/列表相关页面即可触发。
|
||||
- 根因倾向:内容封面 URL 为外链,但前端拼接了 storage 下载前缀。
|
||||
- 影响:缩略图/封面显示失败,体验受损。
|
||||
- 建议优先级:**P2**。
|
||||
|
||||
## 6) 覆盖统计
|
||||
|
||||
- Portal 路由覆盖:20/20(按执行清单)
|
||||
- Superadmin 路由覆盖:35/35(按执行清单)
|
||||
- 可见按钮/操作覆盖:
|
||||
- Portal:每页采样记录按钮,均有可见交互控件(详情见上表)
|
||||
- Superadmin:每页记录按钮数量与代表动作(详见上表)
|
||||
|
||||
## 7) 结论
|
||||
|
||||
- **总体:可用,但存在两类明显缺陷(F1/F2)**。
|
||||
- Superadmin 页面可达与按钮可见性整体正常。
|
||||
- Portal 核心流程(登录、订单、钱包、个人中心、支付页)可访问;但租户信息接口与外链图片路径处理需要修复。
|
||||
|
||||
---
|
||||
|
||||
## 附:证据来源
|
||||
|
||||
- 浏览器自动化:Playwright MCP(页面快照 + 按钮/链接枚举 + 路由巡检)
|
||||
- 后端证据:`pty_0e911c68` 运行日志中的 404 记录
|
||||
- 环境会话:
|
||||
- backend: `pty_0e911c68`
|
||||
- superadmin: `pty_28cea2be`
|
||||
- portal: `pty_1773d898`
|
||||
|
||||
## 8) 闭环测试执行上下文(Action -> API -> DB)
|
||||
|
||||
- 执行时间:2026-02-07 11:56 ~ 12:00(Asia/Shanghai)
|
||||
- 数据前置:`cd backend && env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go run . seed`
|
||||
- 本轮租户:`meipai_157`(`tenant_id=1`)
|
||||
- 账号:
|
||||
- Portal:`13800138000 + 1234`
|
||||
- Superadmin:`superadmin / superadmin123`
|
||||
- 验证方式:
|
||||
- 通过 API 触发页面等效动作(UI-equivalent)
|
||||
- 每个场景记录前置状态/动作/后置状态/SQL证据
|
||||
|
||||
### 8.1 全局前后快照
|
||||
|
||||
| 指标 | 前置 | 后置 | Delta |
|
||||
|---|---:|---:|---:|
|
||||
| `user_content_actions` | 2 | 3 | +1 |
|
||||
| `comments` | 1 | 2 | +1 |
|
||||
| `orders` | 12 | 14 | +2 |
|
||||
| `order_items` | 3 | 4 | +1 |
|
||||
| `content_access` | 3 | 3 | +0 |
|
||||
| 买家余额(分) | 5000 | 104010 | +99010 |
|
||||
| 买家未读通知(tenant=1) | 8 | 0 | -8 |
|
||||
|
||||
## 9) 闭环场景结果表
|
||||
|
||||
### 9.1 Portal 闭环场景
|
||||
|
||||
| 场景 | 前置状态 | 动作(UI 等效 API) | 后置状态(DB/API 证据) | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 点赞内容 | `content_id=1` 已有 like 记录 1 条 | `POST /v1/t/meipai_157/contents/1/like` | HTTP 200;like 记录 1 -> 1(幂等) | ✅ Pass |
|
||||
| 收藏内容 | `content_id=2` favorite 记录 0 条 | `POST /v1/t/meipai_157/contents/2/favorite` | HTTP 200;favorite 记录 0 -> 1 | ✅ Pass |
|
||||
| 评论内容 | `content_id=3` 评论数 0 | `POST /v1/t/meipai_157/contents/3/comments`(`闭环评论 cc25fadc2c`) | HTTP 200;评论数 0 -> 1;新评论 `id=2` | ✅ Pass |
|
||||
| 下单+支付+发放访问权限 | `content_id=4` | `POST /orders` -> `order_id=13`;`POST /orders/13/pay` | 订单状态=paid、order_item=1;**content_access(order_id=13)=0**,总量无增量 | ❌ Fail |
|
||||
| 充值码兑换 | `RC_ACTIVE_1000` 为 active | `POST /me/wallet/recharge`(code=RC_ACTIVE_1000) | HTTP 200;code: active->redeemed;余额 +100000 分;`redeemed_order_id=14`;**order(14) status=created** | ❌ Fail |
|
||||
| 全部通知已读 | 未读 8 | `POST /me/notifications/read-all` | HTTP 200;未读 8 -> 0 | ✅ Pass |
|
||||
|
||||
### 9.2 Superadmin 闭环场景
|
||||
|
||||
| 场景 | 前置状态 | 动作 | 后置状态(DB/API 证据) | 结果 |
|
||||
|---|---|---|---|---|
|
||||
| 系统配置 create/update | 无 `qa_closure_84f9cb6e` | `POST /super/v1/system-configs` -> `PATCH /super/v1/system-configs/3` | 配置值更新为 `{"value":"v2","source":"qa-update"}`;description=`qa closure updated`;审计日志 2 条(create+update) | ✅ Pass |
|
||||
| 通知模板 create/update | 无 `qa_template_f342df9a` | `POST /super/v1/notifications/templates` -> `PATCH /super/v1/notifications/templates/5` | 标题更新为 `QA Template Title Updated`;`is_active=false`;审计日志 1 条 | ✅ Pass |
|
||||
| 群发通知下游落库 | 目标用户未读=0 | `POST /super/v1/notifications/broadcast`(tenant=1) | API 返回 200,但通知表无新增(title 查询=0);补测显式 user_ids 仍无落库 | ⚠️ Blocked |
|
||||
|
||||
## 10) 失败/阻塞场景复现与证据
|
||||
|
||||
### F3(高)支付成功但未发放内容访问权限
|
||||
- 复现:
|
||||
1. `POST /v1/t/meipai_157/orders`(`content_id=4`)
|
||||
2. `POST /v1/t/meipai_157/orders/13/pay`(`method=balance`)
|
||||
- 实际:
|
||||
- `orders.id=13 status=paid`
|
||||
- `order_items(order_id=13)=1`
|
||||
- `content_access(order_id=13)=0`
|
||||
- 证据 SQL(节选):
|
||||
- `SELECT id,status,order_id FROM content_access WHERE user_id=2 AND content_id=4;` -> `id=3,status=expired,order_id=9`
|
||||
- `SELECT id,status,amount_paid FROM orders WHERE id=13;` -> `13,paid,990`
|
||||
- 影响:用户完成支付后可能无权访问已购内容。
|
||||
- 优先级:**P1**。
|
||||
|
||||
### F4(严重)充值码兑换后订单状态未结算为 paid
|
||||
- 复现:
|
||||
1. `POST /v1/t/meipai_157/me/wallet/recharge`(`code=RC_ACTIVE_1000`)
|
||||
- 实际:
|
||||
- 接口 200,返回 `order_id=14`
|
||||
- 充值码状态 active->redeemed,`redeemed_order_id=14`
|
||||
- 用户余额 +100000 分(到账)
|
||||
- 但 `orders.id=14 status=created`(应为 paid)
|
||||
- 证据 SQL(节选):
|
||||
- `SELECT code,status,redeemed_order_id,amount FROM recharge_codes WHERE code='RC_ACTIVE_1000';` -> `redeemed,14,100000`
|
||||
- `SELECT id,status,type,amount_paid FROM orders WHERE id=14;` -> `14,created,recharge,100000`
|
||||
- 影响:资金到账与订单状态不一致,影响对账/风控/财务准确性。
|
||||
- 优先级:**P0**。
|
||||
|
||||
### B1(阻塞)超管群发通知闭环无法在当前环境确认
|
||||
- 复现:
|
||||
1. `POST /super/v1/notifications/broadcast`(tenant=1)返回 200
|
||||
2. 查询 `notifications` 未见新记录
|
||||
- 说明:`services/notification.go` 默认走异步任务队列(`job.Add`);仅 `JOB_INLINE=1` 才同步落库。当前环境未启动消费方,导致“动作成功但下游证据缺失”。
|
||||
- 处理建议:
|
||||
- 启用 worker,或
|
||||
- 在闭环环境设置 `JOB_INLINE=1` 再复测。
|
||||
- 状态:**Blocked(非功能结论,环境前置不足)**。
|
||||
|
||||
### F1(高)Portal tenants 接口 404(回归确认)
|
||||
- 请求:`GET /v1/t/meipai_157/tenants?limit=5`
|
||||
- 结果:HTTP **404**
|
||||
- 影响:门户首页/频道页租户信息区块异常。
|
||||
- 优先级:**P1**。
|
||||
|
||||
### F2(中)外链图片被当作 storage 路径(回归确认)
|
||||
- 请求:`GET /v1/storage/https://images.unsplash.com/photo-1514306191717-452ec28c7f31`
|
||||
- 结果:HTTP **404**
|
||||
- 影响:封面图/缩略图加载失败。
|
||||
- 优先级:**P2**。
|
||||
|
||||
## 11) 优先级缺陷清单(P0/P1/P2)
|
||||
|
||||
### P0
|
||||
1. **充值码兑换订单状态不一致**:余额到账 + code 已 redeem,但 recharge 订单仍为 `created`(F4)。
|
||||
|
||||
### P1
|
||||
1. **支付后未发放访问权限**:订单 paid 但未新增 active content_access(F3)。
|
||||
2. **Portal tenants 接口 404**:核心入口页租户数据缺失(F1)。
|
||||
|
||||
### P2
|
||||
1. **外链图片走 storage 前缀导致 404**(F2)。
|
||||
|
||||
### Blocked(需环境补齐后再定级)
|
||||
1. **超管群发通知下游落库验证**:当前队列消费未启用(B1)。
|
||||
|
||||
## 12) 最终闭环覆盖总结
|
||||
|
||||
- 闭环场景总数:9
|
||||
- ✅ 通过:6
|
||||
- ❌ 失败:2
|
||||
- ⚠️ 阻塞:1
|
||||
|
||||
- Fully Closed:
|
||||
- Portal:点赞、收藏、评论、通知全部已读
|
||||
- Superadmin:系统配置 create/update、通知模板 create/update
|
||||
|
||||
- Partially Closed / Failed:
|
||||
- Portal:下单支付后访问权限发放(失败)
|
||||
- Portal:充值兑换状态一致性(失败)
|
||||
|
||||
- Blocked:
|
||||
- Superadmin:群发通知下游落库验证(需 worker/JOB_INLINE 前置)
|
||||
|
||||
## 13) 2026-02-07 回归复测(修复后)
|
||||
|
||||
- 执行时间:2026-02-07 12:57 ~ 13:07(Asia/Shanghai)
|
||||
- 后端:`go run . serve`(`http://127.0.0.1:18080`)
|
||||
- 复测前置:`cd backend && env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go run . seed`
|
||||
- 本轮租户:`meipai_569`(`tenant_id=1`)
|
||||
- Portal 登录:`POST /v1/auth/otp` + `POST /v1/auth/login`(`13800138000` / OTP `1234`)
|
||||
|
||||
### 13.1 关键接口复测结果(Action -> API -> DB)
|
||||
|
||||
| 场景 | 动作 | 结果 | 证据 |
|
||||
|---|---|---|---|
|
||||
| 支付后 content_access 恢复 active | `POST /v1/t/meipai_569/orders`(`content_id=4`)-> `order_id=13`;`POST /v1/t/meipai_569/orders/13/pay` | ✅ 订单支付成功,已有 `expired` 访问记录被恢复为 `active` 并绑定新订单 | `orders.id=13 status=paid`; `order_items(order_id=13)=1`; `content_access(user_id=2,content_id=4)` 从 `status=expired,order_id=9` 变为 `status=active,order_id=13` |
|
||||
| 充值兑换订单状态一致性 | `POST /v1/t/meipai_569/me/wallet/recharge`(`code=RC_ACTIVE_1000`)-> `order_id=14` | ✅ 兑换后订单状态为 `paid`,与余额到账一致 | `recharge_codes.code='RC_ACTIVE_1000' status=redeemed, redeemed_order_id=14`; `orders.id=14 status=paid,type=recharge`; `users.id=2 balance=104010` |
|
||||
| Portal tenants 列表接口 | `GET /v1/t/meipai_569/tenants?limit=5`(带/不带 token) | ✅ 返回 200,不再 404 | 响应体包含分页与 tenant 项:`{"page":1,"total":1,"items":[...]}` |
|
||||
| 外链图片 URL | `GET /v1/t/meipai_569/contents?page=1&limit=3` | ✅ `cover` 返回外链绝对 URL,不再拼接 `/v1/storage/https://...` | 响应中 `cover` 字段为 `https://images.unsplash.com/...`;`media_assets.object_key` 为 `http(s)` 外链时直出 |
|
||||
|
||||
### 13.2 新发现(回归期间)
|
||||
|
||||
- `Creator` 路由缺失(与本轮四项缺陷不同):
|
||||
- 复测请求:`GET /v1/t/meipai_569/creator/orders` 返回 404。
|
||||
- 服务启动日志中无任何 `/v1/t/:tenantCode/creator/...` 路由注册项。
|
||||
- 代码现状:`backend/app/http/v1/creator.go` 只有一个方法 `GrantCoupon`,但其注释块包含大量不匹配该方法签名的 `@Router` 声明,导致路由生成器未产出 creator 路由。
|
||||
- 影响:Portal 创作者中心依赖的 `/creator/*` API 存在不可用风险(需单独修复)。
|
||||
|
||||
### 13.3 构建/测试验证
|
||||
|
||||
- `cd backend && env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go test ./...`
|
||||
- 结果:✅ 全量通过。
|
||||
|
||||
### 13.4 状态更新(P0/P1/P2)
|
||||
|
||||
- P0 充值兑换状态不一致(F4):✅ **已修复并复测通过**
|
||||
- P1 支付后未发放访问权限(F3):✅ **已修复并复测通过**
|
||||
- P1 Portal tenants 接口 404(F1):✅ **已修复并复测通过**
|
||||
- P2 外链图片拼接 storage 前缀(F2):✅ **已修复并复测通过**
|
||||
- Blocked(群发通知异步落库):⚠️ 仍依赖 worker/JOB_INLINE 环境前置
|
||||
|
||||
## 14) 2026-02-07 Creator 路由恢复验证(回归补录)
|
||||
|
||||
- 执行时间:2026-02-07 19:28 ~ 19:31(Asia/Shanghai)
|
||||
- 后端:`go run . serve`(`http://127.0.0.1:18080`)
|
||||
- 路由生成:`cd backend && atomctl gen route`
|
||||
- 全量回归:`cd backend && env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go test ./...`(✅ pass)
|
||||
|
||||
### 14.1 前端 Creator API -> 后端 Controller/Service 映射
|
||||
|
||||
| 前端函数(`frontend/portal/src/api/creator.js`) | 后端路由 | Controller 方法 | Service 方法 |
|
||||
|---|---|---|---|
|
||||
| `apply` | `POST /v1/t/:tenantCode/creator/apply` | `Creator.Apply` | `services.Creator.Apply` |
|
||||
| `getDashboard` | `GET /v1/t/:tenantCode/creator/dashboard` | `Creator.Dashboard` | `services.Creator.Dashboard` |
|
||||
| `listContents` | `GET /v1/t/:tenantCode/creator/contents` | `Creator.ListContents` | `services.Creator.ListContents` |
|
||||
| `getContent` | `GET /v1/t/:tenantCode/creator/contents/:id<int>` | `Creator.GetContent` | `services.Creator.GetContent` |
|
||||
| `createContent` | `POST /v1/t/:tenantCode/creator/contents` | `Creator.CreateContent` | `services.Creator.CreateContent` |
|
||||
| `updateContent` | `PUT /v1/t/:tenantCode/creator/contents/:id<int>` | `Creator.UpdateContent` | `services.Creator.UpdateContent` |
|
||||
| `deleteContent` | `DELETE /v1/t/:tenantCode/creator/contents/:id<int>` | `Creator.DeleteContent` | `services.Creator.DeleteContent` |
|
||||
| `listOrders` | `GET /v1/t/:tenantCode/creator/orders` | `Creator.ListOrders` | `services.Creator.ListOrders` |
|
||||
| `refundOrder` | `POST /v1/t/:tenantCode/creator/orders/:id<int>/refund` | `Creator.RefundOrder` | `services.Creator.ProcessRefund` |
|
||||
| `getSettings` | `GET /v1/t/:tenantCode/creator/settings` | `Creator.GetSettings` | `services.Creator.GetSettings` |
|
||||
| `updateSettings` | `PUT /v1/t/:tenantCode/creator/settings` | `Creator.UpdateSettings` | `services.Creator.UpdateSettings` |
|
||||
| `listPayoutAccounts` | `GET /v1/t/:tenantCode/creator/payout-accounts` | `Creator.ListPayoutAccounts` | `services.Creator.ListPayoutAccounts` |
|
||||
| `addPayoutAccount` | `POST /v1/t/:tenantCode/creator/payout-accounts` | `Creator.AddPayoutAccount` | `services.Creator.AddPayoutAccount` |
|
||||
| `removePayoutAccount` | `DELETE /v1/t/:tenantCode/creator/payout-accounts?id=:id` | `Creator.RemovePayoutAccount` | `services.Creator.RemovePayoutAccount` |
|
||||
| `withdraw` | `POST /v1/t/:tenantCode/creator/withdraw` | `Creator.Withdraw` | `services.Creator.Withdraw` |
|
||||
| `listMembers` | `GET /v1/t/:tenantCode/creator/members` | `Creator.ListMembers` | `services.Tenant.ListMembers` |
|
||||
| `removeMember` | `DELETE /v1/t/:tenantCode/creator/members/:id<int>` | `Creator.RemoveMember` | `services.Tenant.RemoveMember` |
|
||||
| `listMemberInvites` | `GET /v1/t/:tenantCode/creator/members/invites` | `Creator.ListMemberInvites` | `services.Tenant.ListInvites` |
|
||||
| `createMemberInvite` | `POST /v1/t/:tenantCode/creator/members/invite` | `Creator.CreateMemberInvite` | `services.Tenant.CreateInvite` |
|
||||
| `disableMemberInvite` | `DELETE /v1/t/:tenantCode/creator/members/invites/:id<int>` | `Creator.DisableMemberInvite` | `services.Tenant.DisableInvite` |
|
||||
| `listMemberJoinRequests` | `GET /v1/t/:tenantCode/creator/members/join-requests` | `Creator.ListMemberJoinRequests` | `services.Tenant.ListJoinRequests` |
|
||||
| `reviewMemberJoinRequest` | `POST /v1/t/:tenantCode/creator/members/:id<int>/review` | `Creator.ReviewMemberJoinRequest` | `services.Tenant.ReviewJoin` |
|
||||
| `listCoupons` | `GET /v1/t/:tenantCode/creator/coupons` | `Creator.ListCoupons` | `services.Coupon.List` |
|
||||
| `getCoupon` | `GET /v1/t/:tenantCode/creator/coupons/:id<int>` | `Creator.GetCoupon` | `services.Coupon.Get` |
|
||||
| `createCoupon` | `POST /v1/t/:tenantCode/creator/coupons` | `Creator.CreateCoupon` | `services.Coupon.Create` |
|
||||
| `updateCoupon` | `PUT /v1/t/:tenantCode/creator/coupons/:id<int>` | `Creator.UpdateCoupon` | `services.Coupon.Update` |
|
||||
| `grantCoupon` | `POST /v1/t/:tenantCode/creator/coupons/:id<int>/grant` | `Creator.GrantCoupon` | `services.Coupon.Grant` |
|
||||
| (Portal 报表页预留) | `GET /v1/t/:tenantCode/creator/reports/overview` | `Creator.ReportOverview` | `services.Creator.ReportOverview` |
|
||||
| (Portal 报表页预留) | `POST /v1/t/:tenantCode/creator/reports/export` | `Creator.ExportReport` | `services.Creator.ExportReport` |
|
||||
|
||||
### 14.2 路由恢复证据
|
||||
|
||||
- `atomctl gen route` 输出包含 Creator 路由发现日志(`Found route ... /v1/t/:tenantCode/creator/...`)。
|
||||
- `backend/app/http/v1/routes.gen.go` 已出现 29 条 creator 路由注册项(`/v1/t/:tenantCode/creator/*`)。
|
||||
- 服务启动日志显示 Creator 路由已注册(`Registering route: ... /v1/t/:tenantCode/creator/...`)。
|
||||
|
||||
### 14.3 冒烟结果(至少 /creator/orders 非 404)
|
||||
|
||||
| 请求 | 鉴权 | 结果 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `GET /v1/t/meipai_569/creator/orders` | 无 token | `401` | 命中路由,返回缺失 token(非 404) |
|
||||
| `GET /v1/t/meipai_569/creator/orders` | 买家 token(13800138000) | `403` | 命中路由,权限校验 `非创作者`(非 404) |
|
||||
| `GET /v1/t/meipai_569/creator/orders` | 创作者 token(13800000001) | `200` | 返回订单列表 JSON |
|
||||
| `GET /v1/t/meipai_569/creator/contents` | 创作者 token | `200` | 返回分页结构(`page/limit/total/items`) |
|
||||
| `GET /v1/t/meipai_569/creator/settings` | 创作者 token | `200` | 返回频道设置对象 |
|
||||
|
||||
### 14.4 剩余风险
|
||||
|
||||
1. 本轮为 Creator 路由恢复回归,重点覆盖“路由可达 + 鉴权行为 + 关键读接口”。
|
||||
2. 写接口(如内容编辑、成员审核、优惠券发放、提现)尚未逐一做 Action->DB 闭环,建议在下一轮专项回归补齐。
|
||||
BIN
explore_global.png
Normal file
BIN
explore_global.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
explore_tenant_filtered.png
Normal file
BIN
explore_tenant_filtered.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Reference in New Issue
Block a user