package services import ( "context" "errors" "time" "quyun/v2/app/errorx" creator_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database/fields" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/google/uuid" "github.com/spf13/cast" "go.ipao.vip/gen/types" "gorm.io/gorm" ) // @provider type creator struct{} func (s *creator) Apply(ctx context.Context, form *creator_dto.ApplyForm) error { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return errorx.ErrUnauthorized } uid := cast.ToInt64(userID) tbl, q := models.TenantQuery.QueryContext(ctx) // Check if already has a tenant count, _ := q.Where(tbl.UserID.Eq(uid)).Count() if count > 0 { return errorx.ErrBadRequest.WithMsg("您已是创作者") } // Create Tenant tenant := &models.Tenant{ UserID: uid, Name: form.Name, // Bio/Avatar in config Code: uuid.NewString()[:8], // Generate random code UUID: types.UUID(uuid.New()), Status: consts.TenantStatusPendingVerify, } if err := q.Create(tenant); err != nil { return errorx.ErrDatabaseError.WithCause(err) } // Also add user as tenant_admin in tenant_users tu := &models.TenantUser{ TenantID: tenant.ID, UserID: uid, Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleTenantAdmin}, Status: consts.UserStatusVerified, } if err := models.TenantUserQuery.WithContext(ctx).Create(tu); err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *creator) Dashboard(ctx context.Context) (*creator_dto.DashboardStats, error) { tid, err := s.getTenantID(ctx) if err != nil { return nil, err } // Followers: count tenant_users followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid)).Count() // Revenue: sum tenant_ledgers (income) var revenue float64 // GORM doesn't have a direct Sum method in Gen yet easily accessible without raw SQL or result mapping // But we can use underlying DB models.TenantLedgerQuery.WithContext(ctx).UnderlyingDB(). Model(&models.TenantLedger{}). Where("tenant_id = ? AND type = ?", tid, consts.TenantLedgerTypeDebitPurchase). Select("COALESCE(SUM(amount), 0)"). Scan(&revenue) // Pending Refunds: count orders in refunding pendingRefunds, _ := models.OrderQuery.WithContext(ctx). Where(models.OrderQuery.TenantID.Eq(tid), models.OrderQuery.Status.Eq(consts.OrderStatusRefunding)). Count() stats := &creator_dto.DashboardStats{ TotalFollowers: creator_dto.IntStatItem{Value: int(followers)}, TotalRevenue: creator_dto.FloatStatItem{Value: revenue / 100.0}, PendingRefunds: int(pendingRefunds), NewMessages: 0, } return stats, nil } func (s *creator) ListContents(ctx context.Context, filter *creator_dto.CreatorContentListFilter) ([]creator_dto.ContentItem, error) { tid, err := s.getTenantID(ctx) if err != nil { return nil, err } tbl, q := models.ContentQuery.QueryContext(ctx) q = q.Where(tbl.TenantID.Eq(tid)) if filter.Status != nil && *filter.Status != "" { q = q.Where(tbl.Status.Eq(consts.ContentStatus(*filter.Status))) } if filter.Genre != nil && *filter.Genre != "" { q = q.Where(tbl.Genre.Eq(*filter.Genre)) } if filter.Keyword != nil && *filter.Keyword != "" { q = q.Where(tbl.Title.Like("%" + *filter.Keyword + "%")) } list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var data []creator_dto.ContentItem for _, item := range list { data = append(data, creator_dto.ContentItem{ ID: cast.ToString(item.ID), Title: item.Title, Genre: item.Genre, Views: int(item.Views), Likes: int(item.Likes), IsPurchased: false, }) } return data, nil } func (s *creator) CreateContent(ctx context.Context, form *creator_dto.ContentCreateForm) error { tid, err := s.getTenantID(ctx) if err != nil { return err } uid := cast.ToInt64(ctx.Value(consts.CtxKeyUser)) return models.Q.Transaction(func(tx *models.Query) error { // 1. Create Content content := &models.Content{ TenantID: tid, UserID: uid, Title: form.Title, Genre: form.Genre, Status: consts.ContentStatusPublished, } if err := tx.Content.WithContext(ctx).Create(content); err != nil { return err } // 2. Link Assets if len(form.MediaIDs) > 0 { var assets []*models.ContentAsset for i, mid := range form.MediaIDs { assets = append(assets, &models.ContentAsset{ TenantID: tid, UserID: uid, ContentID: content.ID, AssetID: cast.ToInt64(mid), Sort: int32(i), Role: consts.ContentAssetRoleMain, }) } if err := tx.ContentAsset.WithContext(ctx).Create(assets...); err != nil { return err } } // 3. Set Price price := &models.ContentPrice{ TenantID: tid, UserID: uid, ContentID: content.ID, PriceAmount: int64(form.Price * 100), // Convert to cents Currency: consts.CurrencyCNY, } if err := tx.ContentPrice.WithContext(ctx).Create(price); err != nil { return err } return nil }) } func (s *creator) UpdateContent(ctx context.Context, id string, form *creator_dto.ContentUpdateForm) error { tid, err := s.getTenantID(ctx) if err != nil { return err } cid := cast.ToInt64(id) uid := cast.ToInt64(ctx.Value(consts.CtxKeyUser)) return models.Q.Transaction(func(tx *models.Query) error { // 1. Check Ownership c, err := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid), tx.Content.TenantID.Eq(tid)).First() if err != nil { return errorx.ErrRecordNotFound } // 2. Update Content _, err = tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).Updates(&models.Content{ Title: form.Title, Genre: form.Genre, }) if err != nil { return err } // 3. Update Price // Check if price exists count, _ := tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).Count() newPrice := int64(form.Price * 100) if count > 0 { _, err = tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).UpdateSimple(tx.ContentPrice.PriceAmount.Value(newPrice)) } else { err = tx.ContentPrice.WithContext(ctx).Create(&models.ContentPrice{ TenantID: tid, UserID: c.UserID, ContentID: cid, PriceAmount: newPrice, Currency: consts.CurrencyCNY, }) } if err != nil { return err } // 4. Update Assets (Full replacement strategy) if len(form.MediaIDs) > 0 { _, err = tx.ContentAsset.WithContext(ctx).Where(tx.ContentAsset.ContentID.Eq(cid)).Delete() if err != nil { return err } var assets []*models.ContentAsset for i, mid := range form.MediaIDs { assets = append(assets, &models.ContentAsset{ TenantID: tid, UserID: uid, ContentID: cid, AssetID: cast.ToInt64(mid), Sort: int32(i), Role: consts.ContentAssetRoleMain, // Default to main }) } if err := tx.ContentAsset.WithContext(ctx).Create(assets...); err != nil { return err } } return nil }) } func (s *creator) DeleteContent(ctx context.Context, id string) error { cid := cast.ToInt64(id) tid, err := s.getTenantID(ctx) if err != nil { return err } _, err = models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid), models.ContentQuery.TenantID.Eq(tid)).Delete() if err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *creator) ListOrders(ctx context.Context, filter *creator_dto.CreatorOrderListFilter) ([]creator_dto.Order, error) { tid, err := s.getTenantID(ctx) if err != nil { return nil, err } tbl, q := models.OrderQuery.QueryContext(ctx) q = q.Where(tbl.TenantID.Eq(tid)) if filter.Status != nil && *filter.Status != "" { q = q.Where(tbl.Status.Eq(consts.OrderStatus(*filter.Status))) } // Keyword could match ID or other fields if needed list, err := q.Order(tbl.CreatedAt.Desc()).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var data []creator_dto.Order for _, o := range list { data = append(data, creator_dto.Order{ ID: cast.ToString(o.ID), Status: string(o.Status), // Enum conversion Amount: float64(o.AmountPaid) / 100.0, CreateTime: o.CreatedAt.Format(time.RFC3339), }) } return data, nil } func (s *creator) ProcessRefund(ctx context.Context, id string, form *creator_dto.RefundForm) error { tid, err := s.getTenantID(ctx) if err != nil { return err } oid := cast.ToInt64(id) uid := cast.ToInt64(ctx.Value(consts.CtxKeyUser)) // Creator ID // Fetch Order o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid), models.OrderQuery.TenantID.Eq(tid)).First() if err != nil { return errorx.ErrRecordNotFound } // Validate Status // Allow refunding 'refunding' orders. Or 'paid' if we treat this as "Initiate Refund". // Given "Action" (accept/reject), assume 'refunding'. if o.Status != consts.OrderStatusRefunding { return errorx.ErrStatusConflict.WithMsg("订单状态不是退款中") } if form.Action == "reject" { _, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid)).Updates(&models.Order{ Status: consts.OrderStatusPaid, RefundReason: form.Reason, // Store reject reason? Or clear it? }) return err } if form.Action == "accept" { return models.Q.Transaction(func(tx *models.Query) error { // 1. Deduct Creator Balance // We credited Creator User Balance in Order.Pay. Now deduct it. info, err := tx.User.WithContext(ctx). Where(tx.User.ID.Eq(uid), tx.User.Balance.Gte(o.AmountPaid)). Update(tx.User.Balance, gorm.Expr("balance - ?", o.AmountPaid)) if err != nil { return err } if info.RowsAffected == 0 { return errorx.ErrQuotaExceeded.WithMsg("余额不足,无法退款") } // 2. Credit Buyer Balance _, err = tx.User.WithContext(ctx). Where(tx.User.ID.Eq(o.UserID)). Update(tx.User.Balance, gorm.Expr("balance + ?", o.AmountPaid)) if err != nil { return err } // 3. Update Order Status _, err = tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(oid)).Updates(&models.Order{ Status: consts.OrderStatusRefunded, RefundedAt: time.Now(), RefundOperatorUserID: uid, RefundReason: form.Reason, }) if err != nil { return err } // 4. Revoke Content Access // Fetch order items to get content IDs items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(oid)).Find() contentIDs := make([]int64, len(items)) for i, item := range items { contentIDs[i] = item.ContentID } if len(contentIDs) > 0 { _, err = tx.ContentAccess.WithContext(ctx). Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.In(contentIDs...)). UpdateSimple(tx.ContentAccess.Status.Value(consts.ContentAccessStatusRevoked)) if err != nil { return err } } // 5. Create Tenant Ledger ledger := &models.TenantLedger{ TenantID: tid, UserID: uid, OrderID: oid, Type: consts.TenantLedgerTypeCreditRefund, Amount: o.AmountPaid, Remark: "退款: " + form.Reason, OperatorUserID: uid, IdempotencyKey: uuid.NewString(), } if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil { return err } return nil }) } return errorx.ErrBadRequest.WithMsg("无效的操作") } func (s *creator) GetSettings(ctx context.Context) (*creator_dto.Settings, error) { tid, err := s.getTenantID(ctx) if err != nil { return nil, err } t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tid)).First() if err != nil { return nil, errorx.ErrRecordNotFound } // Extract from t.Config return &creator_dto.Settings{ Name: t.Name, // Bio/Avatar from Config }, nil } func (s *creator) UpdateSettings(ctx context.Context, form *creator_dto.Settings) error { return nil } func (s *creator) ListPayoutAccounts(ctx context.Context) ([]creator_dto.PayoutAccount, error) { tid, err := s.getTenantID(ctx) if err != nil { return nil, err } list, err := models.PayoutAccountQuery.WithContext(ctx).Where(models.PayoutAccountQuery.TenantID.Eq(tid)).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var data []creator_dto.PayoutAccount for _, v := range list { data = append(data, creator_dto.PayoutAccount{ ID: cast.ToString(v.ID), Type: v.Type, Name: v.Name, Account: v.Account, Realname: v.Realname, }) } return data, nil } func (s *creator) AddPayoutAccount(ctx context.Context, form *creator_dto.PayoutAccount) error { tid, err := s.getTenantID(ctx) if err != nil { return err } uid := cast.ToInt64(ctx.Value(consts.CtxKeyUser)) pa := &models.PayoutAccount{ TenantID: tid, UserID: uid, Type: form.Type, Name: form.Name, Account: form.Account, Realname: form.Realname, } if err := models.PayoutAccountQuery.WithContext(ctx).Create(pa); err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *creator) RemovePayoutAccount(ctx context.Context, id string) error { tid, err := s.getTenantID(ctx) if err != nil { return err } pid := cast.ToInt64(id) _, err = models.PayoutAccountQuery.WithContext(ctx). Where(models.PayoutAccountQuery.ID.Eq(pid), models.PayoutAccountQuery.TenantID.Eq(tid)). Delete() if err != nil { return errorx.ErrDatabaseError.WithCause(err) } return nil } func (s *creator) Withdraw(ctx context.Context, form *creator_dto.WithdrawForm) error { tid, err := s.getTenantID(ctx) if err != nil { return err } uid := cast.ToInt64(ctx.Value(consts.CtxKeyUser)) amount := int64(form.Amount * 100) if amount <= 0 { return errorx.ErrBadRequest.WithMsg("金额无效") } // Validate Payout Account _, err = models.PayoutAccountQuery.WithContext(ctx). Where(models.PayoutAccountQuery.ID.Eq(cast.ToInt64(form.AccountID)), models.PayoutAccountQuery.TenantID.Eq(tid)). First() if err != nil { return errorx.ErrRecordNotFound.WithMsg("收款账户不存在") } return models.Q.Transaction(func(tx *models.Query) error { // 1. Deduct Balance info, err := tx.User.WithContext(ctx). Where(tx.User.ID.Eq(uid), tx.User.Balance.Gte(amount)). Update(tx.User.Balance, gorm.Expr("balance - ?", amount)) if err != nil { return err } if info.RowsAffected == 0 { return errorx.ErrQuotaExceeded.WithMsg("余额不足") } // 2. Create Order (Withdrawal) order := &models.Order{ TenantID: tid, UserID: uid, Type: consts.OrderTypeWithdrawal, Status: consts.OrderStatusCreated, // Created = Pending Processing Currency: consts.CurrencyCNY, AmountOriginal: amount, AmountPaid: amount, // Actually Amount Withdrawn IdempotencyKey: uuid.NewString(), Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Can store account details here } if err := tx.Order.WithContext(ctx).Create(order); err != nil { return err } // 3. Create Tenant Ledger ledger := &models.TenantLedger{ TenantID: tid, UserID: uid, OrderID: order.ID, Type: consts.TenantLedgerTypeCreditWithdrawal, Amount: amount, Remark: "提现申请", OperatorUserID: uid, IdempotencyKey: uuid.NewString(), } if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil { return err } return nil }) } // Helpers func (s *creator) getTenantID(ctx context.Context) (int64, error) { userID := ctx.Value(consts.CtxKeyUser) if userID == nil { return 0, errorx.ErrUnauthorized } uid := cast.ToInt64(userID) // Simple check: User owns tenant t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(uid)).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return 0, errorx.ErrPermissionDenied.WithMsg("非创作者") } return 0, errorx.ErrDatabaseError.WithCause(err) } return t.ID, nil }