Compare commits

...

5 Commits

Author SHA1 Message Date
86d8e1dd94 docs: update creator recovery plan and verification evidence
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-08 18:31:11 +08:00
f712436b2c fix: stabilize media job and middleware tests
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-08 18:30:56 +08:00
590662964a feat: enhance order processing and tenant management services
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-08 18:30:42 +08:00
9bf3a87b32 feat: restore creator controller methods and regenerate routes
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-08 18:29:43 +08:00
d6550e9e1a feat: expand seed data with diverse test entities
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-08 18:29:28 +08:00
15 changed files with 2076 additions and 62 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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(

View File

@@ -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

View File

@@ -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)

View File

@@ -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() {

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -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
View File

@@ -0,0 +1,362 @@
# 全项目页面逐按钮测试矩阵(落盘存档)
更新日期2026-02-07Asia/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) 失败/阻塞项复现
### F1Portal tenants 接口 404
- 现象:首页/频道/探索页出现 `ok=false`,并在后端日志出现 404。
- 复现:登录 Portal 后访问 `/t/meipai_990``/t/meipai_990/channel`
- 后端日志:`GET /v1/t/meipai_990/tenants?limit=5` -> 404。
- 影响:门户首页顶部/频道信息可能缺失或报错。
- 建议优先级:**P1**(核心入口页异常)。
### F2Portal 外链图片走 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:00Asia/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 200like 记录 1 -> 1幂等 | ✅ Pass |
| 收藏内容 | `content_id=2` favorite 记录 0 条 | `POST /v1/t/meipai_157/contents/2/favorite` | HTTP 200favorite 记录 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 200code: 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非功能结论环境前置不足**。
### F1Portal 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_accessF3
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:07Asia/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 接口 404F1**已修复并复测通过**
- P2 外链图片拼接 storage 前缀F2**已修复并复测通过**
- Blocked群发通知异步落库 仍依赖 worker/JOB_INLINE 环境前置
## 14) 2026-02-07 Creator 路由恢复验证(回归补录)
- 执行时间2026-02-07 19:28 ~ 19:31Asia/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` | 买家 token13800138000 | `403` | 命中路由,权限校验 `非创作者`(非 404 |
| `GET /v1/t/meipai_569/creator/orders` | 创作者 token13800000001 | `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

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
explore_tenant_filtered.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB