package services import ( "context" "errors" "fmt" "time" "quyun/v2/app/errorx" creator_dto "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" "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{} var genreMap = map[string]string{ "Jingju": "京剧", "Kunqu": "昆曲", "Yueju": "越剧", "Yuju": "豫剧", "Huangmeixi": "黄梅戏", "Pingju": "评剧", "Qinqiang": "秦腔", } func (s *creator) Apply(ctx context.Context, userID int64, form *creator_dto.ApplyForm) error { if userID == 0 { return errorx.ErrUnauthorized } uid := 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, userID int64) (*creator_dto.DashboardStats, error) { tid, err := s.getTenantID(ctx, userID) 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, userID int64, filter *creator_dto.CreatorContentListFilter, ) (*requests.Pager, error) { tid, err := s.getTenantID(ctx, userID) 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.Visibility != nil && *filter.Visibility != "" { q = q.Where(tbl.Visibility.Eq(consts.ContentVisibility(*filter.Visibility))) } if filter.Visibility != nil && *filter.Visibility != "" { q = q.Where(tbl.Visibility.Eq(consts.ContentVisibility(*filter.Visibility))) } if filter.Genre != nil && *filter.Genre != "" { val := *filter.Genre if cn, ok := genreMap[val]; ok { q = q.Where(tbl.Genre.In(val, cn)) } else { q = q.Where(tbl.Genre.Eq(val)) } } if filter.Key != nil && *filter.Key != "" { fmt.Printf("DEBUG: Filter Key: '%s'\n", *filter.Key) q = q.Where(tbl.Key.Eq(*filter.Key)) } if filter.Keyword != nil && *filter.Keyword != "" { q = q.Where(tbl.Title.Like("%" + *filter.Keyword + "%")) } // Pagination filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var list []*models.Content // Sorting sort := "latest" if filter.Sort != nil && *filter.Sort != "" { sort = *filter.Sort } switch sort { case "oldest": q = q.Order(tbl.ID.Asc()) case "views": q = q.Order(tbl.Views.Desc()) case "likes": q = q.Order(tbl.Likes.Desc()) default: q = q.Order(tbl.ID.Desc()) } err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)). UnderlyingDB(). Preload("ContentAssets"). Preload("ContentAssets.Asset"). Find(&list).Error if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } // Fetch Prices priceMap := make(map[int64]float64) if len(list) > 0 { ids := make([]int64, len(list)) for i, item := range list { ids[i] = item.ID } pTbl, pQ := models.ContentPriceQuery.QueryContext(ctx) prices, _ := pQ.Where(pTbl.ContentID.In(ids...)).Find() for _, p := range prices { priceMap[p.ContentID] = float64(p.PriceAmount) / 100.0 } } var data []creator_dto.CreatorContentItem for _, item := range list { var imageCount, videoCount, audioCount int var cover string var firstImage string for _, ca := range item.ContentAssets { if ca.Asset == nil { continue } // Count logic switch ca.Asset.Type { case consts.MediaAssetTypeImage: imageCount++ if firstImage == "" { firstImage = Common.GetAssetURL(ca.Asset.ObjectKey) } case consts.MediaAssetTypeVideo: videoCount++ case consts.MediaAssetTypeAudio: audioCount++ } // Cover logic if ca.Role == consts.ContentAssetRoleCover && cover == "" { cover = Common.GetAssetURL(ca.Asset.ObjectKey) } } if cover == "" { cover = firstImage } data = append(data, creator_dto.CreatorContentItem{ ID: cast.ToString(item.ID), Title: item.Title, Genre: item.Genre, Key: item.Key, Price: priceMap[item.ID], Views: int(item.Views), Likes: int(item.Likes), Cover: cover, ImageCount: imageCount, VideoCount: videoCount, AudioCount: audioCount, Status: string(item.Status), Visibility: string(item.Visibility), CreatedAt: item.CreatedAt.Format("2006-01-02 15:04"), IsPinned: item.IsPinned, IsPurchased: false, }) } return &requests.Pager{ Pagination: filter.Pagination, Total: total, Items: data, }, nil } func (s *creator) CreateContent(ctx context.Context, userID int64, form *creator_dto.ContentCreateForm) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err } uid := userID return models.Q.Transaction(func(tx *models.Query) error { status := consts.ContentStatusPublished if form.Status != "" { status = consts.ContentStatus(form.Status) } // 1. Create Content content := &models.Content{ TenantID: tid, UserID: uid, Title: form.Title, Genre: form.Genre, Key: form.Key, Status: status, } if err := tx.Content.WithContext(ctx).Create(content); err != nil { return err } // 2. Link Assets var assets []*models.ContentAsset // Covers for i, mid := range form.CoverIDs { assets = append(assets, &models.ContentAsset{ TenantID: tid, UserID: uid, ContentID: content.ID, AssetID: cast.ToInt64(mid), Sort: int32(i), Role: consts.ContentAssetRoleCover, }) } // Main Media 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 len(assets) > 0 { 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, userID int64, id string, form *creator_dto.ContentUpdateForm, ) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err } cid := cast.ToInt64(id) uid := userID 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 contentUpdates := &models.Content{ Title: form.Title, Genre: form.Genre, Key: form.Key, } if form.Status != "" { contentUpdates.Status = consts.ContentStatus(form.Status) } // Determine final status finalStatus := c.Status if form.Status != "" { finalStatus = consts.ContentStatus(form.Status) } // Validation: Only published content can be pinned if form.IsPinned != nil && *form.IsPinned && finalStatus != consts.ContentStatusPublished { return errorx.ErrBadRequest.WithMsg("只有已发布的内容支持置顶") } // Perform standard updates _, err = tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).Updates(contentUpdates) if err != nil { return err } // Handle IsPinned Logic if finalStatus != consts.ContentStatusPublished { // Force Unpin if not published _, err = tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).UpdateSimple(tx.Content.IsPinned.Value(false)) if err != nil { return err } } else if form.IsPinned != nil { // Explicit Pin Update requested _, err = tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).UpdateSimple(tx.Content.IsPinned.Value(*form.IsPinned)) if err != nil { return err } // If setting to true, unpin others if *form.IsPinned { if _, err := tx.Content.WithContext(ctx). Where(tx.Content.TenantID.Eq(tid), tx.Content.ID.Neq(cid)). UpdateSimple(tx.Content.IsPinned.Value(false)); err != nil { return err } } } // 3. Update Price // Check if price exists if form.Price != nil { 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) _, err = tx.ContentAsset.WithContext(ctx).Where(tx.ContentAsset.ContentID.Eq(cid)).Delete() if err != nil { return err } var assets []*models.ContentAsset // Covers for i, mid := range form.CoverIDs { assets = append(assets, &models.ContentAsset{ TenantID: tid, UserID: uid, ContentID: cid, AssetID: cast.ToInt64(mid), Sort: int32(i), Role: consts.ContentAssetRoleCover, }) } // Main Media 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, }) } if len(assets) > 0 { if err := tx.ContentAsset.WithContext(ctx).Create(assets...); err != nil { return err } } return nil }) } func (s *creator) DeleteContent(ctx context.Context, userID int64, id string) error { cid := cast.ToInt64(id) tid, err := s.getTenantID(ctx, userID) if err != nil { return err } // Check if purchased (ContentAccess exists) count, _ := models.ContentAccessQuery.WithContext(ctx).Where(models.ContentAccessQuery.ContentID.Eq(cid)).Count() if count > 0 { return errorx.ErrPreconditionFailed.WithMsg("该内容已被购买,无法删除") } _, 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) GetContent(ctx context.Context, userID int64, id string) (*creator_dto.ContentEditDTO, error) { tid, err := s.getTenantID(ctx, userID) if err != nil { return nil, err } cid := cast.ToInt64(id) // Fetch Content with preloads var c models.Content err = models.ContentQuery.WithContext(ctx). Where(models.ContentQuery.ID.Eq(cid), models.ContentQuery.TenantID.Eq(tid)). UnderlyingDB(). Preload("ContentAssets", func(db *gorm.DB) *gorm.DB { return db.Order("sort ASC") }). Preload("ContentAssets.Asset"). First(&c).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errorx.ErrRecordNotFound } return nil, errorx.ErrDatabaseError.WithCause(err) } // Fetch Price var price float64 cp, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First() if err == nil { price = float64(cp.PriceAmount) / 100.0 } dto := &creator_dto.ContentEditDTO{ ID: cast.ToString(c.ID), Title: c.Title, Genre: c.Genre, Key: c.Key, Description: c.Description, Status: string(c.Status), Price: price, EnableTrial: c.PreviewSeconds > 0, PreviewSeconds: int(c.PreviewSeconds), Assets: make([]creator_dto.AssetDTO, 0), } for _, ca := range c.ContentAssets { if ca.Asset != nil { meta := ca.Asset.Meta.Data() name := meta.Filename if name == "" { // Fallback: strip UUID prefix (36 chars + 1 underscore = 37) if len(ca.Asset.ObjectKey) > 37 && ca.Asset.ObjectKey[36] == '_' { name = ca.Asset.ObjectKey[37:] } else { name = ca.Asset.ObjectKey } } sizeBytes := meta.Size sizeMB := float64(sizeBytes) / 1024.0 / 1024.0 sizeStr := cast.ToString(float64(int(sizeMB*100))/100.0) + " MB" dto.Assets = append(dto.Assets, creator_dto.AssetDTO{ ID: cast.ToString(ca.AssetID), Role: string(ca.Role), Type: string(ca.Asset.Type), URL: Common.GetAssetURL(ca.Asset.ObjectKey), Name: name, Size: sizeStr, Sort: int(ca.Sort), }) } } return dto, nil } func (s *creator) ListOrders( ctx context.Context, userID int64, filter *creator_dto.CreatorOrderListFilter, ) ([]creator_dto.Order, error) { tid, err := s.getTenantID(ctx, userID) 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, userID int64, id string, form *creator_dto.RefundForm) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err } oid := cast.ToInt64(id) uid := userID // 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, userID int64) (*creator_dto.Settings, error) { tid, err := s.getTenantID(ctx, userID) 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 } cfg := t.Config.Data() return &creator_dto.Settings{ ID: cast.ToString(t.ID), Name: t.Name, Bio: cfg.Bio, Avatar: cfg.Avatar, Cover: cfg.Cover, Description: cfg.Description, }, nil } func (s *creator) UpdateSettings(ctx context.Context, userID int64, form *creator_dto.Settings) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err } t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tid)).First() if err != nil { return errorx.ErrRecordNotFound } cfg := t.Config.Data() cfg.Bio = form.Bio cfg.Avatar = form.Avatar cfg.Cover = form.Cover cfg.Description = form.Description _, err = models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tid)).Updates(&models.Tenant{ Name: form.Name, Config: types.NewJSONType(cfg), }) return err } func (s *creator) ListPayoutAccounts(ctx context.Context, userID int64) ([]creator_dto.PayoutAccount, error) { tid, err := s.getTenantID(ctx, userID) 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, userID int64, form *creator_dto.PayoutAccount) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err } uid := userID 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, userID int64, id string) error { tid, err := s.getTenantID(ctx, userID) 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, userID int64, form *creator_dto.WithdrawForm) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err } uid := userID 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, userID int64) (int64, error) { if userID == 0 { return 0, errorx.ErrUnauthorized } uid := 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 }