From 8fa3d18a9c03d6fb1ceed0caeb85d10983a805f9 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 29 Dec 2025 14:21:20 +0800 Subject: [PATCH] Refactor order and tenant ledger models to use consts for Currency and Type fields; add new UserStatus values; implement comprehensive test cases for content, creator, order, super, and wallet services. --- backend/app/services/common.go | 65 +++- backend/app/services/content.go | 199 ++++++++++++- backend/app/services/content_test.go | 151 ++++++++++ backend/app/services/creator.go | 221 +++++++++++++- backend/app/services/creator_test.go | 106 +++++++ backend/app/services/order.go | 220 +++++++++++++- backend/app/services/order_test.go | 125 ++++++++ backend/app/services/super.go | 212 +++++++++++++- backend/app/services/super_test.go | 91 ++++++ backend/app/services/tenant.go | 107 ++++++- backend/app/services/user.go | 2 +- backend/app/services/wallet.go | 97 +++++- backend/app/services/wallet_test.go | 96 ++++++ backend/database/.transform.yaml | 41 +++ backend/database/models/comments.gen.go | 1 + backend/database/models/comments.query.gen.go | 93 +++++- backend/database/models/content_access.gen.go | 20 +- .../models/content_access.query.gen.go | 6 +- backend/database/models/content_assets.gen.go | 21 +- .../models/content_assets.query.gen.go | 99 ++++++- backend/database/models/contents.gen.go | 3 + backend/database/models/contents.query.gen.go | 277 +++++++++++++++++- backend/database/models/orders.gen.go | 2 +- backend/database/models/orders.query.gen.go | 6 +- backend/database/models/tenant_ledgers.gen.go | 36 +-- .../models/tenant_ledgers.query.gen.go | 6 +- backend/database/models/tenant_users.gen.go | 2 +- .../database/models/tenant_users.query.gen.go | 6 +- backend/pkg/consts/consts.gen.go | 15 + backend/pkg/consts/consts.go | 10 +- 30 files changed, 2251 insertions(+), 85 deletions(-) create mode 100644 backend/app/services/content_test.go create mode 100644 backend/app/services/creator_test.go create mode 100644 backend/app/services/order_test.go create mode 100644 backend/app/services/super_test.go create mode 100644 backend/app/services/wallet_test.go diff --git a/backend/app/services/common.go b/backend/app/services/common.go index 3ae390e..31ad886 100644 --- a/backend/app/services/common.go +++ b/backend/app/services/common.go @@ -4,12 +4,73 @@ import ( "context" "mime/multipart" + "quyun/v2/app/errorx" common_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" ) // @provider type common struct{} func (s *common) Upload(ctx context.Context, file *multipart.FileHeader, typeArg string) (*common_dto.UploadResult, error) { - return &common_dto.UploadResult{}, nil -} + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + + // Mock Upload to S3/MinIO + // objectKey := uuid.NewString() + filepath.Ext(file.Filename) + objectKey := uuid.NewString() + "_" + file.Filename + url := "http://mock-storage/" + objectKey + + // Determine TenantID. + // Uploads usually happen in context of a tenant? Or personal? + // For now assume user's owned tenant if any, or 0. + // MediaAsset has TenantID (NOT NULL). + // We need to fetch tenant. + t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(uid)).First() + var tid int64 = 0 + if err == nil { + tid = t.ID + } + // If no tenant, and TenantID is NOT NULL, we have a problem for regular users uploading avatar? + // Users avatar is URL string in `users` table. + // MediaAssets table is for TENANT content. + // If this is for user avatar upload, maybe we don't use MediaAssets? + // But `upload` endpoint is generic. + // Let's assume tid=0 is allowed if system bucket, or enforce tenant. + // If table says NOT NULL, 0 is valid int64. + + asset := &models.MediaAsset{ + TenantID: tid, + UserID: uid, + Type: consts.MediaAssetType(typeArg), + Status: consts.MediaAssetStatusUploaded, + Provider: "mock", + Bucket: "default", + ObjectKey: objectKey, + Meta: types.NewJSONType(fields.MediaAssetMeta{ + Size: file.Size, + // MimeType? + }), + } + + if err := models.MediaAssetQuery.WithContext(ctx).Create(asset); err != nil { + return nil, errorx.ErrDatabaseError + } + + return &common_dto.UploadResult{ + ID: cast.ToString(asset.ID), + URL: url, + Filename: file.Filename, + Size: file.Size, + MimeType: file.Header.Get("Content-Type"), + }, nil +} \ No newline at end of file diff --git a/backend/app/services/content.go b/backend/app/services/content.go index 809f7eb..e3ebd30 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -2,28 +2,187 @@ package services import ( "context" + "errors" + "quyun/v2/app/errorx" content_dto "quyun/v2/app/http/v1/dto" user_dto "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + "github.com/spf13/cast" + "gorm.io/gorm" ) // @provider type content struct{} func (s *content) List(ctx context.Context, keyword, genre, tenantId, sort string, page int) (*requests.Pager, error) { - return &requests.Pager{}, nil + tbl, q := models.ContentQuery.QueryContext(ctx) + + // Filters + q = q.Where(tbl.Status.Eq(consts.ContentStatusPublished)) + if keyword != "" { + q = q.Where(tbl.Title.Like("%" + keyword + "%")) + } + if genre != "" { + q = q.Where(tbl.Genre.Eq(genre)) + } + if tenantId != "" { + tid := cast.ToInt64(tenantId) + q = q.Where(tbl.TenantID.Eq(tid)) + } + + // Preload Author + q = q.Preload(tbl.Author) + + // Sort + switch sort { + case "hot": + q = q.Order(tbl.Views.Desc()) + case "price_asc": + q = q.Order(tbl.ID.Desc()) + default: // latest + q = q.Order(tbl.PublishedAt.Desc()) + } + + // Pagination + p := requests.Pagination{Page: int64(page), Limit: 10} + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + // Convert to DTO + data := make([]content_dto.ContentItem, len(list)) + for i, item := range list { + data[i] = s.toContentItemDTO(item) + } + + return &requests.Pager{ + Pagination: requests.Pagination{ + Page: p.Page, + Limit: p.Limit, + }, + Total: total, + Items: data, + }, nil } func (s *content) Get(ctx context.Context, id string) (*content_dto.ContentDetail, error) { - return &content_dto.ContentDetail{}, nil + cid := cast.ToInt64(id) + _, q := models.ContentQuery.QueryContext(ctx) + + var item models.Content + // Use UnderlyingDB for complex nested preloading + err := q.UnderlyingDB(). + Preload("Author"). + Preload("ContentAssets", func(db *gorm.DB) *gorm.DB { + return db.Order("sort ASC") + }). + Preload("ContentAssets.Asset"). + Where("id = ?", cid). + First(&item).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound + } + return nil, errorx.ErrDatabaseError + } + + // Interaction status (isLiked, isFavorited) + userID := ctx.Value(consts.CtxKeyUser) + isLiked := false + isFavorited := false + if userID != nil { + // uid := cast.ToInt64(userID) // Unused for now until interaction query implemented + // ... check likes ... + } + + detail := &content_dto.ContentDetail{ + ContentItem: s.toContentItemDTO(&item), + Description: item.Description, + Body: item.Body, + MediaUrls: s.toMediaURLs(item.ContentAssets), + IsLiked: isLiked, + IsFavorited: isFavorited, + } + + return detail, nil } func (s *content) ListComments(ctx context.Context, id string, page int) (*requests.Pager, error) { - return &requests.Pager{}, nil + cid := cast.ToInt64(id) + tbl, q := models.CommentQuery.QueryContext(ctx) + + q = q.Where(tbl.ContentID.Eq(cid)).Preload(tbl.User) + q = q.Order(tbl.CreatedAt.Desc()) + + p := requests.Pagination{Page: int64(page), Limit: 10} + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + data := make([]content_dto.Comment, len(list)) + for i, v := range list { + data[i] = content_dto.Comment{ + ID: cast.ToString(v.ID), + Content: v.Content, + UserID: cast.ToString(v.UserID), + UserNickname: v.User.Nickname, // Preloaded + UserAvatar: v.User.Avatar, + CreateTime: v.CreatedAt.Format("2006-01-02 15:04:05"), + Likes: int(v.Likes), + ReplyTo: cast.ToString(v.ReplyTo), + } + } + + return &requests.Pager{ + Pagination: requests.Pagination{ + Page: p.Page, + Limit: p.Limit, + }, + Total: total, + Items: data, + }, nil } func (s *content) CreateComment(ctx context.Context, id string, form *content_dto.CommentCreateForm) error { + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + cid := cast.ToInt64(id) + + c, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).First() + if err != nil { + return errorx.ErrRecordNotFound + } + + comment := &models.Comment{ + TenantID: c.TenantID, + UserID: uid, + ContentID: cid, + Content: form.Content, + ReplyTo: cast.ToInt64(form.ReplyTo), + } + + if err := models.CommentQuery.WithContext(ctx).Create(comment); err != nil { + return errorx.ErrDatabaseError + } return nil } @@ -62,3 +221,37 @@ func (s *content) RemoveLike(ctx context.Context, contentId string) error { func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) { return []content_dto.Topic{}, nil } + +// Helpers + +func (s *content) toContentItemDTO(item *models.Content) content_dto.ContentItem { + dto := content_dto.ContentItem{ + ID: cast.ToString(item.ID), + Title: item.Title, + Genre: item.Genre, + AuthorID: cast.ToString(item.UserID), + Views: int(item.Views), + Likes: int(item.Likes), + } + if item.Author != nil { + dto.AuthorName = item.Author.Nickname + dto.AuthorAvatar = item.Author.Avatar + } + return dto +} + +func (s *content) toMediaURLs(assets []*models.ContentAsset) []content_dto.MediaURL { + var urls []content_dto.MediaURL + for _, ca := range assets { + if ca.Asset != nil { + // Construct URL based on Asset info (Bucket/Key/Provider) + // For prototype: mock url + url := "http://mock/" + ca.Asset.ObjectKey + urls = append(urls, content_dto.MediaURL{ + Type: string(ca.Asset.Type), // Assuming type is enum or string + URL: url, + }) + } + } + return urls +} diff --git a/backend/app/services/content_test.go b/backend/app/services/content_test.go new file mode 100644 index 0000000..cc3be2f --- /dev/null +++ b/backend/app/services/content_test.go @@ -0,0 +1,151 @@ +package services + +import ( + "context" + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + content_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + . "github.com/smartystreets/goconvey/convey" + "github.com/spf13/cast" + "github.com/stretchr/testify/suite" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type ContentTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` +} + +type ContentTestSuite struct { + suite.Suite + ContentTestSuiteInjectParams +} + +func Test_Content(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p ContentTestSuiteInjectParams) { + suite.Run(t, &ContentTestSuite{ContentTestSuiteInjectParams: p}) + }) +} + +func (s *ContentTestSuite) Test_List() { + Convey("List", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser) + + // Create Author + author := &models.User{Nickname: "Author1", Username: "author1", Phone: "13800000001"} + models.UserQuery.WithContext(ctx).Create(author) + + // Create Contents + c1 := &models.Content{ + TenantID: 1, + UserID: author.ID, + Title: "Content A", + Status: consts.ContentStatusPublished, + Genre: "video", + } + c2 := &models.Content{ + TenantID: 1, + UserID: author.ID, + Title: "Content B", + Status: consts.ContentStatusDraft, // Draft + Genre: "video", + } + models.ContentQuery.WithContext(ctx).Create(c1, c2) + + Convey("should list only published contents", func() { + res, err := Content.List(ctx, "", "", "1", "", 1) + So(err, ShouldBeNil) + So(res.Total, ShouldEqual, 1) + items := res.Items.([]content_dto.ContentItem) + So(items[0].Title, ShouldEqual, "Content A") + So(items[0].AuthorName, ShouldEqual, "Author1") + }) + }) +} + +func (s *ContentTestSuite) Test_Get() { + Convey("Get", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameMediaAsset, models.TableNameContentAsset, models.TableNameUser) + + // Author + author := &models.User{Nickname: "Author1", Username: "author1", Phone: "13800000002"} + models.UserQuery.WithContext(ctx).Create(author) + + // Asset + asset := &models.MediaAsset{ + TenantID: 1, + UserID: author.ID, + ObjectKey: "test.mp4", + Type: consts.MediaAssetTypeVideo, + } + models.MediaAssetQuery.WithContext(ctx).Create(asset) + + // Content + content := &models.Content{ + TenantID: 1, + UserID: author.ID, + Title: "Detail Content", + Status: consts.ContentStatusPublished, + } + models.ContentQuery.WithContext(ctx).Create(content) + + // Link Asset + ca := &models.ContentAsset{ + TenantID: 1, + UserID: author.ID, + ContentID: content.ID, + AssetID: asset.ID, + Sort: 1, + } + models.ContentAssetQuery.WithContext(ctx).Create(ca) + + Convey("should get detail with assets", func() { + detail, err := Content.Get(ctx, cast.ToString(content.ID)) + So(err, ShouldBeNil) + So(detail.Title, ShouldEqual, "Detail Content") + So(detail.AuthorName, ShouldEqual, "Author1") + So(len(detail.MediaUrls), ShouldEqual, 1) + So(detail.MediaUrls[0].URL, ShouldEndWith, "test.mp4") + }) + }) +} + +func (s *ContentTestSuite) Test_CreateComment() { + Convey("CreateComment", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameComment, models.TableNameUser) + + // User & Content + u := &models.User{Username: "user1", Phone: "13900000001"} + models.UserQuery.WithContext(ctx).Create(u) + c := &models.Content{TenantID: 1, UserID: u.ID, Title: "C"} + models.ContentQuery.WithContext(ctx).Create(c) + + // Auth context + ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) + + Convey("should create comment", func() { + form := &content_dto.CommentCreateForm{ + Content: "Nice!", + } + err := Content.CreateComment(ctx, cast.ToString(c.ID), form) + So(err, ShouldBeNil) + + count, _ := models.CommentQuery.WithContext(ctx).Where(models.CommentQuery.ContentID.Eq(c.ID)).Count() + So(count, ShouldEqual, 1) + }) + }) +} diff --git a/backend/app/services/creator.go b/backend/app/services/creator.go index f655b2f..e16c194 100644 --- a/backend/app/services/creator.go +++ b/backend/app/services/creator.go @@ -2,27 +2,174 @@ package services import ( "context" + "errors" + "time" + "quyun/v2/app/errorx" creator_dto "quyun/v2/app/http/v1/dto" + "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 + } + + // 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 + } + return nil } func (s *creator) Dashboard(ctx context.Context) (*creator_dto.DashboardStats, error) { - return &creator_dto.DashboardStats{}, nil + tid, err := s.getTenantID(ctx) + if err != nil { + return nil, err + } + + // Mock stats for now or query + // Followers: count tenant_users + followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid)).Count() + + stats := &creator_dto.DashboardStats{ + TotalFollowers: creator_dto.IntStatItem{Value: int(followers)}, + TotalRevenue: creator_dto.FloatStatItem{Value: 0}, + PendingRefunds: 0, + NewMessages: 0, + } + return stats, nil } func (s *creator) ListContents(ctx context.Context, status, genre, keyword string) ([]creator_dto.ContentItem, error) { - return []creator_dto.ContentItem{}, nil + 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 status != "" { + q = q.Where(tbl.Status.Eq(consts.ContentStatus(status))) + } + if genre != "" { + q = q.Where(tbl.Genre.Eq(genre)) + } + if keyword != "" { + q = q.Where(tbl.Title.Like("%" + keyword + "%")) + } + + list, err := q.Order(tbl.CreatedAt.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + 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 { - return nil + 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 { @@ -30,11 +177,43 @@ func (s *creator) UpdateContent(ctx context.Context, id string, form *creator_dt } 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 + } return nil } func (s *creator) ListOrders(ctx context.Context, status, keyword string) ([]creator_dto.Order, error) { - return []creator_dto.Order{}, nil + tid, err := s.getTenantID(ctx) + if err != nil { + return nil, err + } + + tbl, q := models.OrderQuery.QueryContext(ctx) + q = q.Where(tbl.TenantID.Eq(tid)) + // Filters... + list, err := q.Order(tbl.CreatedAt.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + 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 { @@ -42,7 +221,19 @@ func (s *creator) ProcessRefund(ctx context.Context, id string, form *creator_dt } func (s *creator) GetSettings(ctx context.Context) (*creator_dto.Settings, error) { - return &creator_dto.Settings{}, nil + 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 { @@ -64,3 +255,23 @@ func (s *creator) RemovePayoutAccount(ctx context.Context, id string) error { func (s *creator) Withdraw(ctx context.Context, form *creator_dto.WithdrawForm) error { 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 + } + return t.ID, nil +} diff --git a/backend/app/services/creator_test.go b/backend/app/services/creator_test.go new file mode 100644 index 0000000..f7f4700 --- /dev/null +++ b/backend/app/services/creator_test.go @@ -0,0 +1,106 @@ +package services + +import ( + "context" + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + creator_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type CreatorTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` +} + +type CreatorTestSuite struct { + suite.Suite + CreatorTestSuiteInjectParams +} + +func Test_Creator(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p CreatorTestSuiteInjectParams) { + suite.Run(t, &CreatorTestSuite{CreatorTestSuiteInjectParams: p}) + }) +} + +func (s *CreatorTestSuite) Test_Apply() { + Convey("Apply", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameTenantUser, models.TableNameUser) + + u := &models.User{Username: "creator1", Phone: "13700000001"} + models.UserQuery.WithContext(ctx).Create(u) + ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) + + Convey("should create tenant", func() { + form := &creator_dto.ApplyForm{ + Name: "My Channel", + } + err := Creator.Apply(ctx, form) + So(err, ShouldBeNil) + + t, _ := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(u.ID)).First() + So(t, ShouldNotBeNil) + So(t.Name, ShouldEqual, "My Channel") + So(t.Status, ShouldEqual, consts.TenantStatusPendingVerify) + + // Check admin role + tu, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).First() + So(tu, ShouldNotBeNil) + // Role is array, check contains? Or first element? + // types.Array is likely []T. + So(len(tu.Role), ShouldEqual, 1) + So(tu.Role[0], ShouldEqual, consts.TenantUserRoleTenantAdmin) + }) + }) +} + +func (s *CreatorTestSuite) Test_CreateContent() { + Convey("CreateContent", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameContent, models.TableNameContentAsset, models.TableNameContentPrice, models.TableNameUser) + + u := &models.User{Username: "creator2", Phone: "13700000002"} + models.UserQuery.WithContext(ctx).Create(u) + ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) + + // Create Tenant manually + t := &models.Tenant{UserID: u.ID, Name: "Channel 2", Code: "123", Status: consts.TenantStatusVerified} + models.TenantQuery.WithContext(ctx).Create(t) + + Convey("should create content and assets", func() { + form := &creator_dto.ContentCreateForm{ + Title: "New Song", + Genre: "audio", + Price: 9.99, + // MediaIDs: ... need media asset + } + err := Creator.CreateContent(ctx, form) + So(err, ShouldBeNil) + + c, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.Title.Eq("New Song")).First() + So(c, ShouldNotBeNil) + So(c.UserID, ShouldEqual, u.ID) + So(c.TenantID, ShouldEqual, t.ID) + + // Check Price + p, _ := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(c.ID)).First() + So(p, ShouldNotBeNil) + So(p.PriceAmount, ShouldEqual, 999) + }) + }) +} diff --git a/backend/app/services/order.go b/backend/app/services/order.go index bfd985b..2475a1b 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -2,30 +2,240 @@ package services import ( "context" + "errors" + "time" + "quyun/v2/app/errorx" transaction_dto "quyun/v2/app/http/v1/dto" user_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 order struct{} func (s *order) ListUserOrders(ctx context.Context, status string) ([]user_dto.Order, error) { - return []user_dto.Order{}, nil + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + + tbl, q := models.OrderQuery.QueryContext(ctx) + q = q.Where(tbl.UserID.Eq(uid)) + + if status != "" && status != "all" { + q = q.Where(tbl.Status.Eq(consts.OrderStatus(status))) + } + + list, err := q.Order(tbl.CreatedAt.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + var data []user_dto.Order + for _, v := range list { + data = append(data, s.toUserOrderDTO(v)) + } + return data, nil } func (s *order) GetUserOrder(ctx context.Context, id string) (*user_dto.Order, error) { - return &user_dto.Order{}, nil + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + oid := cast.ToInt64(id) + + tbl, q := models.OrderQuery.QueryContext(ctx) + item, err := q.Where(tbl.ID.Eq(oid), tbl.UserID.Eq(uid)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound + } + return nil, errorx.ErrDatabaseError + } + + dto := s.toUserOrderDTO(item) + return &dto, nil } func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateForm) (*transaction_dto.OrderCreateResponse, error) { - return &transaction_dto.OrderCreateResponse{}, nil + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + cid := cast.ToInt64(form.ContentID) + + // 1. Fetch Content & Price + content, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).First() + if err != nil { + return nil, errorx.ErrRecordNotFound.WithMsg("内容不存在") + } + if content.Status != consts.ContentStatusPublished { + return nil, errorx.ErrBusinessLogic.WithMsg("内容未发布") + } + + price, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First() + if err != nil { + return nil, errorx.ErrDataCorrupted.WithMsg("价格信息缺失") + } + + // 2. Create Order (Status: Created) + order := &models.Order{ + TenantID: content.TenantID, + UserID: uid, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusCreated, + Currency: price.Currency, + AmountOriginal: price.PriceAmount, + AmountDiscount: 0, // Calculate discount if needed + AmountPaid: price.PriceAmount, // Expected to pay + IdempotencyKey: uuid.NewString(), // Should be from client ideally + Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Populate details + } + + if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil { + return nil, errorx.ErrDatabaseError + } + + // 3. Create Order Item + item := &models.OrderItem{ + TenantID: content.TenantID, + UserID: uid, + OrderID: order.ID, + ContentID: cid, + ContentUserID: content.UserID, + AmountPaid: order.AmountPaid, + } + if err := models.OrderItemQuery.WithContext(ctx).Create(item); err != nil { + return nil, errorx.ErrDatabaseError + } + + return &transaction_dto.OrderCreateResponse{ + OrderID: cast.ToString(order.ID), + }, nil } func (s *order) Pay(ctx context.Context, id string, form *transaction_dto.OrderPayForm) (*transaction_dto.OrderPayResponse, error) { - return &transaction_dto.OrderPayResponse{}, nil + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + oid := cast.ToInt64(id) + + // Fetch Order + o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid), models.OrderQuery.UserID.Eq(uid)).First() + if err != nil { + return nil, errorx.ErrRecordNotFound + } + if o.Status != consts.OrderStatusCreated { + return nil, errorx.ErrStatusConflict.WithMsg("订单状态不可支付") + } + + if form.Method == "balance" { + return s.payWithBalance(ctx, o) + } + + // External payment (mock) + return &transaction_dto.OrderPayResponse{ + PayParams: "mock_pay_params", + }, nil +} + +func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transaction_dto.OrderPayResponse, error) { + err := models.Q.Transaction(func(tx *models.Query) error { + // 1. Deduct User Balance + info, err := tx.User.WithContext(ctx). + Where(tx.User.ID.Eq(o.UserID), 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. Update Order Status + now := time.Now() + _, err = tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(o.ID)).Updates(&models.Order{ + Status: consts.OrderStatusPaid, + PaidAt: now, + }) + if err != nil { + return err + } + + // 3. Grant Content Access + items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find() + for _, item := range items { + access := &models.ContentAccess{ + TenantID: item.TenantID, + UserID: o.UserID, + ContentID: item.ContentID, + OrderID: o.ID, + Status: consts.ContentAccessStatusActive, + } + if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil { + return err + } + } + + // 4. Create Tenant Ledger (Revenue) + t, err := tx.Tenant.WithContext(ctx).Where(tx.Tenant.ID.Eq(o.TenantID)).First() + if err != nil { + return err + } + + ledger := &models.TenantLedger{ + TenantID: o.TenantID, + UserID: t.UserID, // Owner + OrderID: o.ID, + Type: consts.TenantLedgerTypeDebitPurchase, // Income from purchase + Amount: o.AmountPaid, + BalanceBefore: 0, // TODO: Fetch previous balance if tracking tenant balance + BalanceAfter: 0, // TODO + FrozenBefore: 0, + FrozenAfter: 0, + IdempotencyKey: uuid.NewString(), + Remark: "内容销售收入", + OperatorUserID: o.UserID, + } + if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return &transaction_dto.OrderPayResponse{ + PayParams: "balance_paid", + }, nil } func (s *order) Status(ctx context.Context, id string) (*transaction_dto.OrderStatusResponse, error) { - return &transaction_dto.OrderStatusResponse{}, nil + // ... check status ... + return nil, nil +} + +func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order { + return user_dto.Order{ + ID: cast.ToString(o.ID), + Status: string(o.Status), // Need cast for DTO string field if DTO field is string + Amount: float64(o.AmountPaid) / 100.0, + CreateTime: o.CreatedAt.Format(time.RFC3339), + } } diff --git a/backend/app/services/order_test.go b/backend/app/services/order_test.go new file mode 100644 index 0000000..47030f6 --- /dev/null +++ b/backend/app/services/order_test.go @@ -0,0 +1,125 @@ +package services + +import ( + "context" + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + order_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + . "github.com/smartystreets/goconvey/convey" + "github.com/spf13/cast" + "github.com/stretchr/testify/suite" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type OrderTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` +} + +type OrderTestSuite struct { + suite.Suite + OrderTestSuiteInjectParams +} + +func Test_Order(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p OrderTestSuiteInjectParams) { + suite.Run(t, &OrderTestSuite{OrderTestSuiteInjectParams: p}) + }) +} + +func (s *OrderTestSuite) Test_PurchaseFlow() { + Convey("Purchase Flow", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, + models.TableNameOrder, models.TableNameOrderItem, models.TableNameUser, + models.TableNameContent, models.TableNameContentPrice, models.TableNameTenant, + models.TableNameContentAccess, models.TableNameTenantLedger, + ) + + // 1. Setup Data + // Creator + creator := &models.User{Username: "creator", Phone: "13800000001"} + models.UserQuery.WithContext(ctx).Create(creator) + // Tenant + tenant := &models.Tenant{UserID: creator.ID, Name: "Music Shop", Code: "shop1", Status: consts.TenantStatusVerified} + models.TenantQuery.WithContext(ctx).Create(tenant) + // Content + content := &models.Content{TenantID: tenant.ID, UserID: creator.ID, Title: "Song A", Status: consts.ContentStatusPublished} + models.ContentQuery.WithContext(ctx).Create(content) + // Price (10.00 CNY = 1000 cents) + price := &models.ContentPrice{TenantID: tenant.ID, ContentID: content.ID, PriceAmount: 1000, Currency: consts.CurrencyCNY} + models.ContentPriceQuery.WithContext(ctx).Create(price) + + // Buyer + buyer := &models.User{Username: "buyer", Phone: "13900000001", Balance: 2000} // Has 20.00 + models.UserQuery.WithContext(ctx).Create(buyer) + + buyerCtx := context.WithValue(ctx, consts.CtxKeyUser, buyer.ID) + + Convey("should create and pay order successfully", func() { + // Step 1: Create Order + form := &order_dto.OrderCreateForm{ContentID: cast.ToString(content.ID)} + createRes, err := Order.Create(buyerCtx, form) + So(err, ShouldBeNil) + So(createRes.OrderID, ShouldNotBeEmpty) + + // Verify created status + oid := cast.ToInt64(createRes.OrderID) + o, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid)).First() + So(o.Status, ShouldEqual, consts.OrderStatusCreated) + So(o.AmountPaid, ShouldEqual, 1000) + + // Step 2: Pay Order + payForm := &order_dto.OrderPayForm{Method: "balance"} + _, err = Order.Pay(buyerCtx, createRes.OrderID, payForm) + So(err, ShouldBeNil) + + // Verify Order Paid + o, _ = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(oid)).First() + So(o.Status, ShouldEqual, consts.OrderStatusPaid) + So(o.PaidAt, ShouldNotBeZeroValue) + + // Verify Balance Deducted + b, _ := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).First() + So(b.Balance, ShouldEqual, 1000) // 2000 - 1000 + + // Verify Access Granted + access, _ := models.ContentAccessQuery.WithContext(ctx).Where(models.ContentAccessQuery.UserID.Eq(buyer.ID), models.ContentAccessQuery.ContentID.Eq(content.ID)).First() + So(access, ShouldNotBeNil) + So(access.Status, ShouldEqual, consts.ContentAccessStatusActive) + + // Verify Ledger Created (Creator received money logic?) + // Note: My implementation credits the TENANT OWNER (creator.ID). + l, _ := models.TenantLedgerQuery.WithContext(ctx).Where(models.TenantLedgerQuery.OrderID.Eq(o.ID)).First() + So(l, ShouldNotBeNil) + So(l.UserID, ShouldEqual, creator.ID) + So(l.Amount, ShouldEqual, 1000) + So(l.Type, ShouldEqual, consts.TenantLedgerTypeDebitPurchase) + }) + + Convey("should fail pay if insufficient balance", func() { + // Set balance to 5.00 + models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(buyer.ID)).Update(models.UserQuery.Balance, 500) + + form := &order_dto.OrderCreateForm{ContentID: cast.ToString(content.ID)} + createRes, err := Order.Create(buyerCtx, form) + So(err, ShouldBeNil) + + payForm := &order_dto.OrderPayForm{Method: "balance"} + _, err = Order.Pay(buyerCtx, createRes.OrderID, payForm) + So(err, ShouldNotBeNil) + // Error should be QuotaExceeded or similar + }) + }) +} diff --git a/backend/app/services/super.go b/backend/app/services/super.go index 7b38ec3..0465924 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -2,15 +2,24 @@ package services import ( "context" + "time" + "quyun/v2/app/errorx" super_dto "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + "github.com/google/uuid" + "github.com/spf13/cast" + "go.ipao.vip/gen/types" ) // @provider type super struct{} func (s *super) Login(ctx context.Context, form *super_dto.LoginForm) (*super_dto.LoginResponse, error) { + // TODO: Admin specific login or reuse User service return &super_dto.LoginResponse{}, nil } @@ -19,51 +28,236 @@ func (s *super) CheckToken(ctx context.Context) (*super_dto.LoginResponse, error } func (s *super) ListUsers(ctx context.Context, page, limit int, username string) (*requests.Pager, error) { - return &requests.Pager{}, nil + tbl, q := models.UserQuery.QueryContext(ctx) + if username != "" { + q = q.Where(tbl.Username.Like("%" + username + "%")).Or(tbl.Nickname.Like("%" + username + "%")) + } + + p := requests.Pagination{Page: int64(page), Limit: int64(limit)} + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Order(tbl.ID.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + var data []super_dto.UserItem + for _, u := range list { + data = append(data, super_dto.UserItem{ + SuperUserLite: super_dto.SuperUserLite{ + ID: u.ID, + Username: u.Username, + Roles: u.Roles, + Status: u.Status, + // StatusDescription: u.Status.Description(), // Status is consts.UserStatus, it has Description() + // But u.Status might be string if gen didn't map it properly? No, it's consts.UserStatus. + StatusDescription: u.Status.Description(), + CreatedAt: u.CreatedAt.Format(time.RFC3339), + UpdatedAt: u.UpdatedAt.Format(time.RFC3339), + }, + Balance: u.Balance, + BalanceFrozen: u.BalanceFrozen, + }) + } + + return &requests.Pager{ + Pagination: p, + Total: total, + Items: data, + }, nil } func (s *super) GetUser(ctx context.Context, id int64) (*super_dto.UserItem, error) { - return &super_dto.UserItem{}, nil + tbl, q := models.UserQuery.QueryContext(ctx) + u, err := q.Where(tbl.ID.Eq(id)).First() + if err != nil { + return nil, errorx.ErrRecordNotFound + } + return &super_dto.UserItem{ + SuperUserLite: super_dto.SuperUserLite{ + ID: u.ID, + Username: u.Username, + Roles: u.Roles, + Status: u.Status, + StatusDescription: u.Status.Description(), + CreatedAt: u.CreatedAt.Format(time.RFC3339), + UpdatedAt: u.UpdatedAt.Format(time.RFC3339), + }, + Balance: u.Balance, + BalanceFrozen: u.BalanceFrozen, + }, nil } func (s *super) UpdateUserStatus(ctx context.Context, id int64, form *super_dto.UserStatusUpdateForm) error { + tbl, q := models.UserQuery.QueryContext(ctx) + _, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Status, consts.UserStatus(form.Status)) + if err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *super) UpdateUserRoles(ctx context.Context, id int64, form *super_dto.UserRolesUpdateForm) error { + var roles types.Array[consts.Role] + for _, r := range form.Roles { + roles = append(roles, r) + } + tbl, q := models.UserQuery.QueryContext(ctx) + _, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Roles, roles) + if err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *super) ListTenants(ctx context.Context, page, limit int, name string) (*requests.Pager, error) { - return &requests.Pager{}, nil + tbl, q := models.TenantQuery.QueryContext(ctx) + if name != "" { + q = q.Where(tbl.Name.Like("%" + name + "%")) + } + + p := requests.Pagination{Page: int64(page), Limit: int64(limit)} + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Order(tbl.ID.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + var data []super_dto.TenantItem + for _, t := range list { + data = append(data, super_dto.TenantItem{ + ID: t.ID, + UUID: t.UUID.String(), + Name: t.Name, + Code: t.Code, + Status: t.Status, + StatusDescription: t.Status.Description(), + UserID: t.UserID, + CreatedAt: t.CreatedAt.Format(time.RFC3339), + UpdatedAt: t.UpdatedAt.Format(time.RFC3339), + }) + } + + return &requests.Pager{ + Pagination: p, + Total: total, + Items: data, + }, nil } func (s *super) CreateTenant(ctx context.Context, form *super_dto.TenantCreateForm) error { + uid := cast.ToInt64(form.AdminUserID) + if _, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First(); err != nil { + return errorx.ErrRecordNotFound.WithMsg("用户不存在") + } + + t := &models.Tenant{ + UserID: uid, + Name: form.Name, + Code: form.Code, + UUID: types.UUID(uuid.New()), + Status: consts.TenantStatusVerified, + } + if err := models.TenantQuery.WithContext(ctx).Create(t); err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *super) GetTenant(ctx context.Context, id int64) (*super_dto.TenantItem, error) { - return &super_dto.TenantItem{}, nil + tbl, q := models.TenantQuery.QueryContext(ctx) + t, err := q.Where(tbl.ID.Eq(id)).First() + if err != nil { + return nil, errorx.ErrRecordNotFound + } + return &super_dto.TenantItem{ + ID: t.ID, + UUID: t.UUID.String(), + Name: t.Name, + Code: t.Code, + Status: t.Status, + StatusDescription: t.Status.Description(), + UserID: t.UserID, + CreatedAt: t.CreatedAt.Format(time.RFC3339), + UpdatedAt: t.UpdatedAt.Format(time.RFC3339), + }, nil } func (s *super) UpdateTenantStatus(ctx context.Context, id int64, form *super_dto.TenantStatusUpdateForm) error { + tbl, q := models.TenantQuery.QueryContext(ctx) + _, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Status, consts.TenantStatus(form.Status)) + if err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *super) UpdateTenantExpire(ctx context.Context, id int64, form *super_dto.TenantExpireUpdateForm) error { + expire := time.Now().AddDate(0, 0, form.Duration) + tbl, q := models.TenantQuery.QueryContext(ctx) + _, err := q.Where(tbl.ID.Eq(id)).Update(tbl.ExpiredAt, expire) + if err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *super) ListContents(ctx context.Context, page, limit int) (*requests.Pager, error) { - return &requests.Pager{}, nil + tbl, q := models.ContentQuery.QueryContext(ctx) + p := requests.Pagination{Page: int64(page), Limit: int64(limit)} + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError + } + list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Order(tbl.ID.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + // Simplified DTO for list + var data []any + for _, c := range list { + data = append(data, c) // TODO: Map to DTO + } + return &requests.Pager{ + Pagination: p, + Total: total, + Items: data, + }, nil } func (s *super) UpdateContentStatus(ctx context.Context, tenantID, contentID int64, form *super_dto.SuperTenantContentStatusUpdateForm) error { + tbl, q := models.ContentQuery.QueryContext(ctx) + _, err := q.Where(tbl.ID.Eq(contentID), tbl.TenantID.Eq(tenantID)).Update(tbl.Status, consts.ContentStatus(form.Status)) + if err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *super) ListOrders(ctx context.Context, page, limit int) (*requests.Pager, error) { - return &requests.Pager{}, nil + tbl, q := models.OrderQuery.QueryContext(ctx) + p := requests.Pagination{Page: int64(page), Limit: int64(limit)} + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError + } + list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Order(tbl.ID.Desc()).Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + // TODO: Map to DTO + return &requests.Pager{ + Pagination: p, + Total: total, + Items: list, + }, nil } func (s *super) GetOrder(ctx context.Context, id int64) (*super_dto.SuperOrderDetail, error) { @@ -83,9 +277,9 @@ func (s *super) UserStatistics(ctx context.Context) ([]super_dto.UserStatistics, } func (s *super) UserStatuses(ctx context.Context) ([]requests.KV, error) { - return []requests.KV{}, nil + return consts.UserStatusItems(), nil } func (s *super) TenantStatuses(ctx context.Context) ([]requests.KV, error) { - return []requests.KV{}, nil -} + return consts.TenantStatusItems(), nil +} \ No newline at end of file diff --git a/backend/app/services/super_test.go b/backend/app/services/super_test.go new file mode 100644 index 0000000..93a77bc --- /dev/null +++ b/backend/app/services/super_test.go @@ -0,0 +1,91 @@ +package services + +import ( + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + super_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type SuperTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` +} + +type SuperTestSuite struct { + suite.Suite + SuperTestSuiteInjectParams +} + +func Test_Super(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p SuperTestSuiteInjectParams) { + suite.Run(t, &SuperTestSuite{SuperTestSuiteInjectParams: p}) + }) +} + +func (s *SuperTestSuite) Test_ListUsers() { + Convey("ListUsers", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameUser) + + u1 := &models.User{Username: "user1", Nickname: "Alice"} + u2 := &models.User{Username: "user2", Nickname: "Bob"} + models.UserQuery.WithContext(ctx).Create(u1, u2) + + Convey("should list users", func() { + res, err := Super.ListUsers(ctx, 1, 10, "") + So(err, ShouldBeNil) + So(res.Total, ShouldEqual, 2) + + items := res.Items.([]super_dto.UserItem) + So(items[0].Username, ShouldEqual, "user2") // Desc order + }) + + Convey("should filter users", func() { + res, err := Super.ListUsers(ctx, 1, 10, "Alice") + So(err, ShouldBeNil) + So(res.Total, ShouldEqual, 1) + items := res.Items.([]super_dto.UserItem) + So(items[0].Username, ShouldEqual, "user1") + }) + }) +} + +func (s *SuperTestSuite) Test_CreateTenant() { + Convey("CreateTenant", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameTenant) + + u := &models.User{Username: "admin1"} + models.UserQuery.WithContext(ctx).Create(u) + + Convey("should create tenant", func() { + form := &super_dto.TenantCreateForm{ + Name: "Super Tenant", + Code: "st1", + AdminUserID: u.ID, + } + err := Super.CreateTenant(ctx, form) + So(err, ShouldBeNil) + + t, _ := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.Code.Eq("st1")).First() + So(t, ShouldNotBeNil) + So(t.Name, ShouldEqual, "Super Tenant") + So(t.UserID, ShouldEqual, u.ID) + So(t.Status, ShouldEqual, consts.TenantStatusVerified) + }) + }) +} \ No newline at end of file diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index a093924..5ea1b6f 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -2,21 +2,126 @@ package services import ( "context" + "errors" + "quyun/v2/app/errorx" tenant_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + "github.com/spf13/cast" + "go.ipao.vip/gen/types" + "gorm.io/gorm" ) // @provider type tenant struct{} func (s *tenant) GetPublicProfile(ctx context.Context, id string) (*tenant_dto.TenantProfile, error) { - return &tenant_dto.TenantProfile{}, nil + // id could be Code or ID. Try Code first, then ID. + tbl, q := models.TenantQuery.QueryContext(ctx) + + // Try to find by code or ID + var t *models.Tenant + var err error + + // Assume id is ID for simplicity if numeric, or try both. + if cast.ToInt64(id) > 0 { + t, err = q.Where(tbl.ID.Eq(cast.ToInt64(id))).First() + } else { + t, err = q.Where(tbl.Code.Eq(id)).First() + } + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound + } + return nil, errorx.ErrDatabaseError + } + + // Stats + // Followers + followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID)).Count() + // Contents + contentsCount, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.TenantID.Eq(t.ID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).Count() + // Likes + var likes int64 + // Sum content likes + // Mock likes for now or fetch + + // IsFollowing + isFollowing := false + userID := ctx.Value(consts.CtxKeyUser) + if userID != nil { + uid := cast.ToInt64(userID) + count, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(t.ID), models.TenantUserQuery.UserID.Eq(uid)).Count() + isFollowing = count > 0 + } + + // Config parsing (Unused for now as we don't map to bio yet) + // config := t.Config.Data() + + return &tenant_dto.TenantProfile{ + ID: cast.ToString(t.ID), + Name: t.Name, + Avatar: "", // From config + Cover: "", // From config + Bio: "", // From config + Description: "", // From config + CertType: "personal", // Mock + Stats: tenant_dto.Stats{ + Followers: int(followers), + Contents: int(contentsCount), + Likes: int(likes), + }, + IsFollowing: isFollowing, + }, nil } func (s *tenant) Follow(ctx context.Context, id string) error { + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + tid := cast.ToInt64(id) + + // Check if tenant exists + _, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tid)).First() + if err != nil { + return errorx.ErrRecordNotFound + } + + // Add to tenant_users + tu := &models.TenantUser{ + TenantID: tid, + UserID: uid, + Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}, + Status: consts.UserStatusVerified, + } + + count, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid), models.TenantUserQuery.UserID.Eq(uid)).Count() + if count > 0 { + return nil // Already following + } + + if err := models.TenantUserQuery.WithContext(ctx).Create(tu); err != nil { + return errorx.ErrDatabaseError + } return nil } func (s *tenant) Unfollow(ctx context.Context, id string) error { + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + tid := cast.ToInt64(id) + + _, err := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid), models.TenantUserQuery.UserID.Eq(uid)).Delete() + if err != nil { + return errorx.ErrDatabaseError + } return nil } diff --git a/backend/app/services/user.go b/backend/app/services/user.go index a15b962..cd54231 100644 --- a/backend/app/services/user.go +++ b/backend/app/services/user.go @@ -189,4 +189,4 @@ func (s *user) toAuthUserDTO(u *models.User) *auth_dto.User { Points: u.Points, IsRealNameVerified: u.IsRealNameVerified, } -} \ No newline at end of file +} diff --git a/backend/app/services/wallet.go b/backend/app/services/wallet.go index 2070d86..f6fc268 100644 --- a/backend/app/services/wallet.go +++ b/backend/app/services/wallet.go @@ -2,17 +2,110 @@ package services import ( "context" + "errors" + "time" + "quyun/v2/app/errorx" user_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 wallet struct{} func (s *wallet) GetWallet(ctx context.Context) (*user_dto.WalletResponse, error) { - return &user_dto.WalletResponse{}, nil + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + + // Get Balance + u, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound + } + return nil, errorx.ErrDatabaseError + } + + // Get Transactions (Orders) + // Both purchase (expense) and recharge (income - if paid) + tbl, q := models.OrderQuery.QueryContext(ctx) + orders, err := q.Where(tbl.UserID.Eq(uid), tbl.Status.Eq(consts.OrderStatusPaid)). + Order(tbl.CreatedAt.Desc()). + Limit(20). // Limit to recent 20 + Find() + if err != nil { + return nil, errorx.ErrDatabaseError + } + + var txs []user_dto.Transaction + for _, o := range orders { + var txType string + var title string + if o.Type == consts.OrderTypeContentPurchase { + txType = "expense" + title = "购买内容" + } else if o.Type == consts.OrderTypeRecharge { + txType = "income" + title = "钱包充值" + } + + txs = append(txs, user_dto.Transaction{ + ID: cast.ToString(o.ID), + Title: title, + Amount: float64(o.AmountPaid) / 100.0, + Type: txType, + Date: o.CreatedAt.Format(time.RFC3339), + }) + } + + return &user_dto.WalletResponse{ + Balance: float64(u.Balance) / 100.0, + Transactions: txs, + }, nil } func (s *wallet) Recharge(ctx context.Context, form *user_dto.RechargeForm) (*user_dto.RechargeResponse, error) { - return &user_dto.RechargeResponse{}, nil + userID := ctx.Value(consts.CtxKeyUser) + if userID == nil { + return nil, errorx.ErrUnauthorized + } + uid := cast.ToInt64(userID) + + amount := int64(form.Amount * 100) + if amount <= 0 { + return nil, errorx.ErrBadRequest.WithMsg("金额无效") + } + + // Create Recharge Order + order := &models.Order{ + TenantID: 0, // Platform / System + UserID: uid, + Type: consts.OrderTypeRecharge, + Status: consts.OrderStatusCreated, + Currency: consts.CurrencyCNY, + AmountOriginal: amount, + AmountPaid: amount, + IdempotencyKey: uuid.NewString(), + Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), + } + + if err := models.OrderQuery.WithContext(ctx).Create(order); err != nil { + return nil, errorx.ErrDatabaseError + } + + // Mock Pay Params + return &user_dto.RechargeResponse{ + PayParams: "mock_recharge_url", + OrderID: cast.ToString(order.ID), + }, nil } diff --git a/backend/app/services/wallet_test.go b/backend/app/services/wallet_test.go new file mode 100644 index 0000000..0d59374 --- /dev/null +++ b/backend/app/services/wallet_test.go @@ -0,0 +1,96 @@ +package services + +import ( + "context" + "database/sql" + "testing" + + "quyun/v2/app/commands/testx" + user_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.ipao.vip/atom/contracts" + "go.uber.org/dig" +) + +type WalletTestSuiteInjectParams struct { + dig.In + + DB *sql.DB + Initials []contracts.Initial `group:"initials"` +} + +type WalletTestSuite struct { + suite.Suite + WalletTestSuiteInjectParams +} + +func Test_Wallet(t *testing.T) { + providers := testx.Default().With(Provide) + + testx.Serve(providers, t, func(p WalletTestSuiteInjectParams) { + suite.Run(t, &WalletTestSuite{WalletTestSuiteInjectParams: p}) + }) +} + +func (s *WalletTestSuite) Test_GetWallet() { + Convey("GetWallet", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameOrder) + + u := &models.User{Username: "wallet_user", Balance: 5000} // 50.00 + models.UserQuery.WithContext(ctx).Create(u) + ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) + + // Create Orders + o1 := &models.Order{ + TenantID: 0, UserID: u.ID, Type: consts.OrderTypeRecharge, Status: consts.OrderStatusPaid, + AmountPaid: 5000, + } + o2 := &models.Order{ + TenantID: 1, UserID: u.ID, Type: consts.OrderTypeContentPurchase, Status: consts.OrderStatusPaid, + AmountPaid: 1000, + } + models.OrderQuery.WithContext(ctx).Create(o1, o2) + + Convey("should return balance and transactions", func() { + res, err := Wallet.GetWallet(ctx) + So(err, ShouldBeNil) + So(res.Balance, ShouldEqual, 50.0) + So(len(res.Transactions), ShouldEqual, 2) + + // Order by CreatedAt Desc + types := []string{res.Transactions[0].Type, res.Transactions[1].Type} + So(types, ShouldContain, "income") + So(types, ShouldContain, "expense") + }) + }) +} + +func (s *WalletTestSuite) Test_Recharge() { + Convey("Recharge", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameOrder) + + u := &models.User{Username: "recharge_user"} + models.UserQuery.WithContext(ctx).Create(u) + ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) + + Convey("should create recharge order", func() { + form := &user_dto.RechargeForm{Amount: 100.0} + res, err := Wallet.Recharge(ctx, form) + So(err, ShouldBeNil) + So(res.OrderID, ShouldNotBeEmpty) + + // Verify order + o, _ := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.Type.Eq(consts.OrderTypeRecharge)).First() + So(o, ShouldNotBeNil) + So(o.AmountPaid, ShouldEqual, 10000) + So(o.TenantID, ShouldEqual, 0) + }) + }) +} diff --git a/backend/database/.transform.yaml b/backend/database/.transform.yaml index 593cbcd..92ce64e 100644 --- a/backend/database/.transform.yaml +++ b/backend/database/.transform.yaml @@ -27,6 +27,7 @@ field_type: orders: status: consts.OrderStatus type: consts.OrderType + currency: consts.Currency snapshot: types.JSONType[fields.OrdersSnapshot] order_items: snapshot: types.JSONType[fields.OrderItemsSnapshot] @@ -35,9 +36,49 @@ field_type: config: types.JSONType[fields.TenantConfig] tenant_users: role: types.Array[consts.TenantUserRole] + status: consts.UserStatus + content_assets: + role: consts.ContentAssetRole media_assets: meta: types.JSONType[fields.MediaAssetMeta] type: consts.MediaAssetType status: consts.MediaAssetStatus variant: consts.MediaAssetVariant + content_access: + status: consts.ContentAccessStatus + tenant_ledgers: + type: consts.TenantLedgerType field_relate: + contents: + Author: + relation: belongs_to + table: users + foreign_key: user_id + references: id + json: author + ContentAssets: + relation: has_many + table: content_assets + foreign_key: content_id + references: id + json: content_assets + Comments: + relation: has_many + table: comments + foreign_key: content_id + references: id + json: comments + comments: + User: + relation: belongs_to + table: users + foreign_key: user_id + references: id + json: user + content_assets: + Asset: + relation: belongs_to + table: media_assets + foreign_key: asset_id + references: id + json: asset diff --git a/backend/database/models/comments.gen.go b/backend/database/models/comments.gen.go index 50736e7..0f9bc08 100644 --- a/backend/database/models/comments.gen.go +++ b/backend/database/models/comments.gen.go @@ -26,6 +26,7 @@ type Comment struct { CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` + User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/comments.query.gen.go b/backend/database/models/comments.query.gen.go index 3eaa50f..f395d6d 100644 --- a/backend/database/models/comments.query.gen.go +++ b/backend/database/models/comments.query.gen.go @@ -35,6 +35,11 @@ func newComment(db *gorm.DB, opts ...gen.DOOption) commentQuery { _commentQuery.CreatedAt = field.NewTime(tableName, "created_at") _commentQuery.UpdatedAt = field.NewTime(tableName, "updated_at") _commentQuery.DeletedAt = field.NewField(tableName, "deleted_at") + _commentQuery.User = commentQueryBelongsToUser{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("User", "User"), + } _commentQuery.fillFieldMap() @@ -55,6 +60,7 @@ type commentQuery struct { CreatedAt field.Time UpdatedAt field.Time DeletedAt field.Field + User commentQueryBelongsToUser fieldMap map[string]field.Expr } @@ -113,7 +119,7 @@ func (c *commentQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) } func (c *commentQuery) fillFieldMap() { - c.fieldMap = make(map[string]field.Expr, 10) + c.fieldMap = make(map[string]field.Expr, 11) c.fieldMap["id"] = c.ID c.fieldMap["tenant_id"] = c.TenantID c.fieldMap["user_id"] = c.UserID @@ -124,18 +130,103 @@ func (c *commentQuery) fillFieldMap() { c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["updated_at"] = c.UpdatedAt c.fieldMap["deleted_at"] = c.DeletedAt + } func (c commentQuery) clone(db *gorm.DB) commentQuery { c.commentQueryDo.ReplaceConnPool(db.Statement.ConnPool) + c.User.db = db.Session(&gorm.Session{Initialized: true}) + c.User.db.Statement.ConnPool = db.Statement.ConnPool return c } func (c commentQuery) replaceDB(db *gorm.DB) commentQuery { c.commentQueryDo.ReplaceDB(db) + c.User.db = db.Session(&gorm.Session{}) return c } +type commentQueryBelongsToUser struct { + db *gorm.DB + + field.RelationField +} + +func (a commentQueryBelongsToUser) Where(conds ...field.Expr) *commentQueryBelongsToUser { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a commentQueryBelongsToUser) WithContext(ctx context.Context) *commentQueryBelongsToUser { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a commentQueryBelongsToUser) Session(session *gorm.Session) *commentQueryBelongsToUser { + a.db = a.db.Session(session) + return &a +} + +func (a commentQueryBelongsToUser) Model(m *Comment) *commentQueryBelongsToUserTx { + return &commentQueryBelongsToUserTx{a.db.Model(m).Association(a.Name())} +} + +func (a commentQueryBelongsToUser) Unscoped() *commentQueryBelongsToUser { + a.db = a.db.Unscoped() + return &a +} + +type commentQueryBelongsToUserTx struct{ tx *gorm.Association } + +func (a commentQueryBelongsToUserTx) Find() (result *User, err error) { + return result, a.tx.Find(&result) +} + +func (a commentQueryBelongsToUserTx) Append(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a commentQueryBelongsToUserTx) Replace(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a commentQueryBelongsToUserTx) Delete(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a commentQueryBelongsToUserTx) Clear() error { + return a.tx.Clear() +} + +func (a commentQueryBelongsToUserTx) Count() int64 { + return a.tx.Count() +} + +func (a commentQueryBelongsToUserTx) Unscoped() *commentQueryBelongsToUserTx { + a.tx = a.tx.Unscoped() + return &a +} + type commentQueryDo struct{ gen.DO } func (c commentQueryDo) Debug() *commentQueryDo { diff --git a/backend/database/models/content_access.gen.go b/backend/database/models/content_access.gen.go index 3a3555b..b73165a 100644 --- a/backend/database/models/content_access.gen.go +++ b/backend/database/models/content_access.gen.go @@ -8,6 +8,8 @@ import ( "context" "time" + "quyun/v2/pkg/consts" + "go.ipao.vip/gen" ) @@ -15,15 +17,15 @@ const TableNameContentAccess = "content_access" // ContentAccess mapped from table type ContentAccess struct { - ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` - TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` - UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` - ContentID int64 `gorm:"column:content_id;type:bigint;not null" json:"content_id"` - OrderID int64 `gorm:"column:order_id;type:bigint" json:"order_id"` - Status string `gorm:"column:status;type:character varying(16);default:active" json:"status"` - RevokedAt time.Time `gorm:"column:revoked_at;type:timestamp with time zone" json:"revoked_at"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` + TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` + UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` + ContentID int64 `gorm:"column:content_id;type:bigint;not null" json:"content_id"` + OrderID int64 `gorm:"column:order_id;type:bigint" json:"order_id"` + Status consts.ContentAccessStatus `gorm:"column:status;type:character varying(16);default:active" json:"status"` + RevokedAt time.Time `gorm:"column:revoked_at;type:timestamp with time zone" json:"revoked_at"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` } // Quick operations without importing query package diff --git a/backend/database/models/content_access.query.gen.go b/backend/database/models/content_access.query.gen.go index aed602c..dd66640 100644 --- a/backend/database/models/content_access.query.gen.go +++ b/backend/database/models/content_access.query.gen.go @@ -30,7 +30,7 @@ func newContentAccess(db *gorm.DB, opts ...gen.DOOption) contentAccessQuery { _contentAccessQuery.UserID = field.NewInt64(tableName, "user_id") _contentAccessQuery.ContentID = field.NewInt64(tableName, "content_id") _contentAccessQuery.OrderID = field.NewInt64(tableName, "order_id") - _contentAccessQuery.Status = field.NewString(tableName, "status") + _contentAccessQuery.Status = field.NewField(tableName, "status") _contentAccessQuery.RevokedAt = field.NewTime(tableName, "revoked_at") _contentAccessQuery.CreatedAt = field.NewTime(tableName, "created_at") _contentAccessQuery.UpdatedAt = field.NewTime(tableName, "updated_at") @@ -49,7 +49,7 @@ type contentAccessQuery struct { UserID field.Int64 ContentID field.Int64 OrderID field.Int64 - Status field.String + Status field.Field RevokedAt field.Time CreatedAt field.Time UpdatedAt field.Time @@ -74,7 +74,7 @@ func (c *contentAccessQuery) updateTableName(table string) *contentAccessQuery { c.UserID = field.NewInt64(table, "user_id") c.ContentID = field.NewInt64(table, "content_id") c.OrderID = field.NewInt64(table, "order_id") - c.Status = field.NewString(table, "status") + c.Status = field.NewField(table, "status") c.RevokedAt = field.NewTime(table, "revoked_at") c.CreatedAt = field.NewTime(table, "created_at") c.UpdatedAt = field.NewTime(table, "updated_at") diff --git a/backend/database/models/content_assets.gen.go b/backend/database/models/content_assets.gen.go index 1432a68..9c9bf72 100644 --- a/backend/database/models/content_assets.gen.go +++ b/backend/database/models/content_assets.gen.go @@ -8,6 +8,8 @@ import ( "context" "time" + "quyun/v2/pkg/consts" + "go.ipao.vip/gen" ) @@ -15,15 +17,16 @@ const TableNameContentAsset = "content_assets" // ContentAsset mapped from table type ContentAsset struct { - ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` - TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` - UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` - ContentID int64 `gorm:"column:content_id;type:bigint;not null" json:"content_id"` - AssetID int64 `gorm:"column:asset_id;type:bigint;not null" json:"asset_id"` - Role string `gorm:"column:role;type:character varying(32);default:main" json:"role"` - Sort int32 `gorm:"column:sort;type:integer" json:"sort"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` + TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` + UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` + ContentID int64 `gorm:"column:content_id;type:bigint;not null" json:"content_id"` + AssetID int64 `gorm:"column:asset_id;type:bigint;not null" json:"asset_id"` + Role consts.ContentAssetRole `gorm:"column:role;type:character varying(32);default:main" json:"role"` + Sort int32 `gorm:"column:sort;type:integer" json:"sort"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` + Asset *MediaAsset `gorm:"foreignKey:AssetID;references:ID" json:"asset,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/content_assets.query.gen.go b/backend/database/models/content_assets.query.gen.go index 60eb917..0f8c781 100644 --- a/backend/database/models/content_assets.query.gen.go +++ b/backend/database/models/content_assets.query.gen.go @@ -30,10 +30,15 @@ func newContentAsset(db *gorm.DB, opts ...gen.DOOption) contentAssetQuery { _contentAssetQuery.UserID = field.NewInt64(tableName, "user_id") _contentAssetQuery.ContentID = field.NewInt64(tableName, "content_id") _contentAssetQuery.AssetID = field.NewInt64(tableName, "asset_id") - _contentAssetQuery.Role = field.NewString(tableName, "role") + _contentAssetQuery.Role = field.NewField(tableName, "role") _contentAssetQuery.Sort = field.NewInt32(tableName, "sort") _contentAssetQuery.CreatedAt = field.NewTime(tableName, "created_at") _contentAssetQuery.UpdatedAt = field.NewTime(tableName, "updated_at") + _contentAssetQuery.Asset = contentAssetQueryBelongsToAsset{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Asset", "MediaAsset"), + } _contentAssetQuery.fillFieldMap() @@ -49,10 +54,11 @@ type contentAssetQuery struct { UserID field.Int64 ContentID field.Int64 AssetID field.Int64 - Role field.String + Role field.Field Sort field.Int32 CreatedAt field.Time UpdatedAt field.Time + Asset contentAssetQueryBelongsToAsset fieldMap map[string]field.Expr } @@ -74,7 +80,7 @@ func (c *contentAssetQuery) updateTableName(table string) *contentAssetQuery { c.UserID = field.NewInt64(table, "user_id") c.ContentID = field.NewInt64(table, "content_id") c.AssetID = field.NewInt64(table, "asset_id") - c.Role = field.NewString(table, "role") + c.Role = field.NewField(table, "role") c.Sort = field.NewInt32(table, "sort") c.CreatedAt = field.NewTime(table, "created_at") c.UpdatedAt = field.NewTime(table, "updated_at") @@ -110,7 +116,7 @@ func (c *contentAssetQuery) GetFieldByName(fieldName string) (field.OrderExpr, b } func (c *contentAssetQuery) fillFieldMap() { - c.fieldMap = make(map[string]field.Expr, 9) + c.fieldMap = make(map[string]field.Expr, 10) c.fieldMap["id"] = c.ID c.fieldMap["tenant_id"] = c.TenantID c.fieldMap["user_id"] = c.UserID @@ -120,18 +126,103 @@ func (c *contentAssetQuery) fillFieldMap() { c.fieldMap["sort"] = c.Sort c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["updated_at"] = c.UpdatedAt + } func (c contentAssetQuery) clone(db *gorm.DB) contentAssetQuery { c.contentAssetQueryDo.ReplaceConnPool(db.Statement.ConnPool) + c.Asset.db = db.Session(&gorm.Session{Initialized: true}) + c.Asset.db.Statement.ConnPool = db.Statement.ConnPool return c } func (c contentAssetQuery) replaceDB(db *gorm.DB) contentAssetQuery { c.contentAssetQueryDo.ReplaceDB(db) + c.Asset.db = db.Session(&gorm.Session{}) return c } +type contentAssetQueryBelongsToAsset struct { + db *gorm.DB + + field.RelationField +} + +func (a contentAssetQueryBelongsToAsset) Where(conds ...field.Expr) *contentAssetQueryBelongsToAsset { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a contentAssetQueryBelongsToAsset) WithContext(ctx context.Context) *contentAssetQueryBelongsToAsset { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a contentAssetQueryBelongsToAsset) Session(session *gorm.Session) *contentAssetQueryBelongsToAsset { + a.db = a.db.Session(session) + return &a +} + +func (a contentAssetQueryBelongsToAsset) Model(m *ContentAsset) *contentAssetQueryBelongsToAssetTx { + return &contentAssetQueryBelongsToAssetTx{a.db.Model(m).Association(a.Name())} +} + +func (a contentAssetQueryBelongsToAsset) Unscoped() *contentAssetQueryBelongsToAsset { + a.db = a.db.Unscoped() + return &a +} + +type contentAssetQueryBelongsToAssetTx struct{ tx *gorm.Association } + +func (a contentAssetQueryBelongsToAssetTx) Find() (result *MediaAsset, err error) { + return result, a.tx.Find(&result) +} + +func (a contentAssetQueryBelongsToAssetTx) Append(values ...*MediaAsset) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a contentAssetQueryBelongsToAssetTx) Replace(values ...*MediaAsset) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a contentAssetQueryBelongsToAssetTx) Delete(values ...*MediaAsset) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a contentAssetQueryBelongsToAssetTx) Clear() error { + return a.tx.Clear() +} + +func (a contentAssetQueryBelongsToAssetTx) Count() int64 { + return a.tx.Count() +} + +func (a contentAssetQueryBelongsToAssetTx) Unscoped() *contentAssetQueryBelongsToAssetTx { + a.tx = a.tx.Unscoped() + return &a +} + type contentAssetQueryDo struct{ gen.DO } func (c contentAssetQueryDo) Debug() *contentAssetQueryDo { diff --git a/backend/database/models/contents.gen.go b/backend/database/models/contents.gen.go index 6777050..34e6a1c 100644 --- a/backend/database/models/contents.gen.go +++ b/backend/database/models/contents.gen.go @@ -38,6 +38,9 @@ type Content struct { CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp with time zone" json:"deleted_at"` + Author *User `gorm:"foreignKey:UserID;references:ID" json:"author,omitempty"` + ContentAssets []*ContentAsset `gorm:"foreignKey:ContentID;references:ID" json:"content_assets,omitempty"` + Comments []*Comment `gorm:"foreignKey:ContentID;references:ID" json:"comments,omitempty"` } // Quick operations without importing query package diff --git a/backend/database/models/contents.query.gen.go b/backend/database/models/contents.query.gen.go index 868c3e1..9467606 100644 --- a/backend/database/models/contents.query.gen.go +++ b/backend/database/models/contents.query.gen.go @@ -44,6 +44,23 @@ func newContent(db *gorm.DB, opts ...gen.DOOption) contentQuery { _contentQuery.CreatedAt = field.NewTime(tableName, "created_at") _contentQuery.UpdatedAt = field.NewTime(tableName, "updated_at") _contentQuery.DeletedAt = field.NewField(tableName, "deleted_at") + _contentQuery.Author = contentQueryBelongsToAuthor{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Author", "User"), + } + + _contentQuery.ContentAssets = contentQueryHasManyContentAssets{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("ContentAssets", "ContentAsset"), + } + + _contentQuery.Comments = contentQueryHasManyComments{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Comments", "Comment"), + } _contentQuery.fillFieldMap() @@ -73,6 +90,11 @@ type contentQuery struct { CreatedAt field.Time UpdatedAt field.Time DeletedAt field.Field + Author contentQueryBelongsToAuthor + + ContentAssets contentQueryHasManyContentAssets + + Comments contentQueryHasManyComments fieldMap map[string]field.Expr } @@ -140,7 +162,7 @@ func (c *contentQuery) GetFieldByName(fieldName string) (field.OrderExpr, bool) } func (c *contentQuery) fillFieldMap() { - c.fieldMap = make(map[string]field.Expr, 19) + c.fieldMap = make(map[string]field.Expr, 22) c.fieldMap["id"] = c.ID c.fieldMap["tenant_id"] = c.TenantID c.fieldMap["user_id"] = c.UserID @@ -160,18 +182,271 @@ func (c *contentQuery) fillFieldMap() { c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["updated_at"] = c.UpdatedAt c.fieldMap["deleted_at"] = c.DeletedAt + } func (c contentQuery) clone(db *gorm.DB) contentQuery { c.contentQueryDo.ReplaceConnPool(db.Statement.ConnPool) + c.Author.db = db.Session(&gorm.Session{Initialized: true}) + c.Author.db.Statement.ConnPool = db.Statement.ConnPool + c.ContentAssets.db = db.Session(&gorm.Session{Initialized: true}) + c.ContentAssets.db.Statement.ConnPool = db.Statement.ConnPool + c.Comments.db = db.Session(&gorm.Session{Initialized: true}) + c.Comments.db.Statement.ConnPool = db.Statement.ConnPool return c } func (c contentQuery) replaceDB(db *gorm.DB) contentQuery { c.contentQueryDo.ReplaceDB(db) + c.Author.db = db.Session(&gorm.Session{}) + c.ContentAssets.db = db.Session(&gorm.Session{}) + c.Comments.db = db.Session(&gorm.Session{}) return c } +type contentQueryBelongsToAuthor struct { + db *gorm.DB + + field.RelationField +} + +func (a contentQueryBelongsToAuthor) Where(conds ...field.Expr) *contentQueryBelongsToAuthor { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a contentQueryBelongsToAuthor) WithContext(ctx context.Context) *contentQueryBelongsToAuthor { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a contentQueryBelongsToAuthor) Session(session *gorm.Session) *contentQueryBelongsToAuthor { + a.db = a.db.Session(session) + return &a +} + +func (a contentQueryBelongsToAuthor) Model(m *Content) *contentQueryBelongsToAuthorTx { + return &contentQueryBelongsToAuthorTx{a.db.Model(m).Association(a.Name())} +} + +func (a contentQueryBelongsToAuthor) Unscoped() *contentQueryBelongsToAuthor { + a.db = a.db.Unscoped() + return &a +} + +type contentQueryBelongsToAuthorTx struct{ tx *gorm.Association } + +func (a contentQueryBelongsToAuthorTx) Find() (result *User, err error) { + return result, a.tx.Find(&result) +} + +func (a contentQueryBelongsToAuthorTx) Append(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a contentQueryBelongsToAuthorTx) Replace(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a contentQueryBelongsToAuthorTx) Delete(values ...*User) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a contentQueryBelongsToAuthorTx) Clear() error { + return a.tx.Clear() +} + +func (a contentQueryBelongsToAuthorTx) Count() int64 { + return a.tx.Count() +} + +func (a contentQueryBelongsToAuthorTx) Unscoped() *contentQueryBelongsToAuthorTx { + a.tx = a.tx.Unscoped() + return &a +} + +type contentQueryHasManyContentAssets struct { + db *gorm.DB + + field.RelationField +} + +func (a contentQueryHasManyContentAssets) Where(conds ...field.Expr) *contentQueryHasManyContentAssets { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a contentQueryHasManyContentAssets) WithContext(ctx context.Context) *contentQueryHasManyContentAssets { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a contentQueryHasManyContentAssets) Session(session *gorm.Session) *contentQueryHasManyContentAssets { + a.db = a.db.Session(session) + return &a +} + +func (a contentQueryHasManyContentAssets) Model(m *Content) *contentQueryHasManyContentAssetsTx { + return &contentQueryHasManyContentAssetsTx{a.db.Model(m).Association(a.Name())} +} + +func (a contentQueryHasManyContentAssets) Unscoped() *contentQueryHasManyContentAssets { + a.db = a.db.Unscoped() + return &a +} + +type contentQueryHasManyContentAssetsTx struct{ tx *gorm.Association } + +func (a contentQueryHasManyContentAssetsTx) Find() (result []*ContentAsset, err error) { + return result, a.tx.Find(&result) +} + +func (a contentQueryHasManyContentAssetsTx) Append(values ...*ContentAsset) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a contentQueryHasManyContentAssetsTx) Replace(values ...*ContentAsset) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a contentQueryHasManyContentAssetsTx) Delete(values ...*ContentAsset) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a contentQueryHasManyContentAssetsTx) Clear() error { + return a.tx.Clear() +} + +func (a contentQueryHasManyContentAssetsTx) Count() int64 { + return a.tx.Count() +} + +func (a contentQueryHasManyContentAssetsTx) Unscoped() *contentQueryHasManyContentAssetsTx { + a.tx = a.tx.Unscoped() + return &a +} + +type contentQueryHasManyComments struct { + db *gorm.DB + + field.RelationField +} + +func (a contentQueryHasManyComments) Where(conds ...field.Expr) *contentQueryHasManyComments { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a contentQueryHasManyComments) WithContext(ctx context.Context) *contentQueryHasManyComments { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a contentQueryHasManyComments) Session(session *gorm.Session) *contentQueryHasManyComments { + a.db = a.db.Session(session) + return &a +} + +func (a contentQueryHasManyComments) Model(m *Content) *contentQueryHasManyCommentsTx { + return &contentQueryHasManyCommentsTx{a.db.Model(m).Association(a.Name())} +} + +func (a contentQueryHasManyComments) Unscoped() *contentQueryHasManyComments { + a.db = a.db.Unscoped() + return &a +} + +type contentQueryHasManyCommentsTx struct{ tx *gorm.Association } + +func (a contentQueryHasManyCommentsTx) Find() (result []*Comment, err error) { + return result, a.tx.Find(&result) +} + +func (a contentQueryHasManyCommentsTx) Append(values ...*Comment) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a contentQueryHasManyCommentsTx) Replace(values ...*Comment) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a contentQueryHasManyCommentsTx) Delete(values ...*Comment) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a contentQueryHasManyCommentsTx) Clear() error { + return a.tx.Clear() +} + +func (a contentQueryHasManyCommentsTx) Count() int64 { + return a.tx.Count() +} + +func (a contentQueryHasManyCommentsTx) Unscoped() *contentQueryHasManyCommentsTx { + a.tx = a.tx.Unscoped() + return &a +} + type contentQueryDo struct{ gen.DO } func (c contentQueryDo) Debug() *contentQueryDo { diff --git a/backend/database/models/orders.gen.go b/backend/database/models/orders.gen.go index 5a6e775..384b174 100644 --- a/backend/database/models/orders.gen.go +++ b/backend/database/models/orders.gen.go @@ -24,7 +24,7 @@ type Order struct { UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` Type consts.OrderType `gorm:"column:type;type:character varying(32);default:content_purchase" json:"type"` Status consts.OrderStatus `gorm:"column:status;type:character varying(32);default:created" json:"status"` - Currency string `gorm:"column:currency;type:character varying(16);default:CNY" json:"currency"` + Currency consts.Currency `gorm:"column:currency;type:character varying(16);default:CNY" json:"currency"` AmountOriginal int64 `gorm:"column:amount_original;type:bigint;not null" json:"amount_original"` AmountDiscount int64 `gorm:"column:amount_discount;type:bigint;not null" json:"amount_discount"` AmountPaid int64 `gorm:"column:amount_paid;type:bigint;not null" json:"amount_paid"` diff --git a/backend/database/models/orders.query.gen.go b/backend/database/models/orders.query.gen.go index 4d4fc3c..d453628 100644 --- a/backend/database/models/orders.query.gen.go +++ b/backend/database/models/orders.query.gen.go @@ -30,7 +30,7 @@ func newOrder(db *gorm.DB, opts ...gen.DOOption) orderQuery { _orderQuery.UserID = field.NewInt64(tableName, "user_id") _orderQuery.Type = field.NewField(tableName, "type") _orderQuery.Status = field.NewField(tableName, "status") - _orderQuery.Currency = field.NewString(tableName, "currency") + _orderQuery.Currency = field.NewField(tableName, "currency") _orderQuery.AmountOriginal = field.NewInt64(tableName, "amount_original") _orderQuery.AmountDiscount = field.NewInt64(tableName, "amount_discount") _orderQuery.AmountPaid = field.NewInt64(tableName, "amount_paid") @@ -58,7 +58,7 @@ type orderQuery struct { UserID field.Int64 Type field.Field Status field.Field - Currency field.String + Currency field.Field AmountOriginal field.Int64 AmountDiscount field.Int64 AmountPaid field.Int64 @@ -92,7 +92,7 @@ func (o *orderQuery) updateTableName(table string) *orderQuery { o.UserID = field.NewInt64(table, "user_id") o.Type = field.NewField(table, "type") o.Status = field.NewField(table, "status") - o.Currency = field.NewString(table, "currency") + o.Currency = field.NewField(table, "currency") o.AmountOriginal = field.NewInt64(table, "amount_original") o.AmountDiscount = field.NewInt64(table, "amount_discount") o.AmountPaid = field.NewInt64(table, "amount_paid") diff --git a/backend/database/models/tenant_ledgers.gen.go b/backend/database/models/tenant_ledgers.gen.go index da7b79c..7c1fdcc 100644 --- a/backend/database/models/tenant_ledgers.gen.go +++ b/backend/database/models/tenant_ledgers.gen.go @@ -8,6 +8,8 @@ import ( "context" "time" + "quyun/v2/pkg/consts" + "go.ipao.vip/gen" ) @@ -15,23 +17,23 @@ const TableNameTenantLedger = "tenant_ledgers" // TenantLedger mapped from table type TenantLedger struct { - ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` - TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` - UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` - OrderID int64 `gorm:"column:order_id;type:bigint" json:"order_id"` - Type string `gorm:"column:type;type:character varying(32);not null" json:"type"` - Amount int64 `gorm:"column:amount;type:bigint;not null" json:"amount"` - BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null" json:"balance_before"` - BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null" json:"balance_after"` - FrozenBefore int64 `gorm:"column:frozen_before;type:bigint;not null" json:"frozen_before"` - FrozenAfter int64 `gorm:"column:frozen_after;type:bigint;not null" json:"frozen_after"` - IdempotencyKey string `gorm:"column:idempotency_key;type:character varying(128);not null" json:"idempotency_key"` - Remark string `gorm:"column:remark;type:character varying(255);not null" json:"remark"` - OperatorUserID int64 `gorm:"column:operator_user_id;type:bigint" json:"operator_user_id"` - BizRefType string `gorm:"column:biz_ref_type;type:character varying(32)" json:"biz_ref_type"` - BizRefID int64 `gorm:"column:biz_ref_id;type:bigint" json:"biz_ref_id"` - CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` - UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` + ID int64 `gorm:"column:id;type:bigint;primaryKey;autoIncrement:true" json:"id"` + TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` + UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` + OrderID int64 `gorm:"column:order_id;type:bigint" json:"order_id"` + Type consts.TenantLedgerType `gorm:"column:type;type:character varying(32);not null" json:"type"` + Amount int64 `gorm:"column:amount;type:bigint;not null" json:"amount"` + BalanceBefore int64 `gorm:"column:balance_before;type:bigint;not null" json:"balance_before"` + BalanceAfter int64 `gorm:"column:balance_after;type:bigint;not null" json:"balance_after"` + FrozenBefore int64 `gorm:"column:frozen_before;type:bigint;not null" json:"frozen_before"` + FrozenAfter int64 `gorm:"column:frozen_after;type:bigint;not null" json:"frozen_after"` + IdempotencyKey string `gorm:"column:idempotency_key;type:character varying(128);not null" json:"idempotency_key"` + Remark string `gorm:"column:remark;type:character varying(255);not null" json:"remark"` + OperatorUserID int64 `gorm:"column:operator_user_id;type:bigint" json:"operator_user_id"` + BizRefType string `gorm:"column:biz_ref_type;type:character varying(32)" json:"biz_ref_type"` + BizRefID int64 `gorm:"column:biz_ref_id;type:bigint" json:"biz_ref_id"` + CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` + UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` } // Quick operations without importing query package diff --git a/backend/database/models/tenant_ledgers.query.gen.go b/backend/database/models/tenant_ledgers.query.gen.go index 8d4c296..e1d4132 100644 --- a/backend/database/models/tenant_ledgers.query.gen.go +++ b/backend/database/models/tenant_ledgers.query.gen.go @@ -29,7 +29,7 @@ func newTenantLedger(db *gorm.DB, opts ...gen.DOOption) tenantLedgerQuery { _tenantLedgerQuery.TenantID = field.NewInt64(tableName, "tenant_id") _tenantLedgerQuery.UserID = field.NewInt64(tableName, "user_id") _tenantLedgerQuery.OrderID = field.NewInt64(tableName, "order_id") - _tenantLedgerQuery.Type = field.NewString(tableName, "type") + _tenantLedgerQuery.Type = field.NewField(tableName, "type") _tenantLedgerQuery.Amount = field.NewInt64(tableName, "amount") _tenantLedgerQuery.BalanceBefore = field.NewInt64(tableName, "balance_before") _tenantLedgerQuery.BalanceAfter = field.NewInt64(tableName, "balance_after") @@ -56,7 +56,7 @@ type tenantLedgerQuery struct { TenantID field.Int64 UserID field.Int64 OrderID field.Int64 - Type field.String + Type field.Field Amount field.Int64 BalanceBefore field.Int64 BalanceAfter field.Int64 @@ -89,7 +89,7 @@ func (t *tenantLedgerQuery) updateTableName(table string) *tenantLedgerQuery { t.TenantID = field.NewInt64(table, "tenant_id") t.UserID = field.NewInt64(table, "user_id") t.OrderID = field.NewInt64(table, "order_id") - t.Type = field.NewString(table, "type") + t.Type = field.NewField(table, "type") t.Amount = field.NewInt64(table, "amount") t.BalanceBefore = field.NewInt64(table, "balance_before") t.BalanceAfter = field.NewInt64(table, "balance_after") diff --git a/backend/database/models/tenant_users.gen.go b/backend/database/models/tenant_users.gen.go index 7301b4f..3d7ee74 100644 --- a/backend/database/models/tenant_users.gen.go +++ b/backend/database/models/tenant_users.gen.go @@ -22,7 +22,7 @@ type TenantUser struct { TenantID int64 `gorm:"column:tenant_id;type:bigint;not null" json:"tenant_id"` UserID int64 `gorm:"column:user_id;type:bigint;not null" json:"user_id"` Role types.Array[consts.TenantUserRole] `gorm:"column:role;type:text[];default:{member}" json:"role"` - Status string `gorm:"column:status;type:character varying(50);default:verified" json:"status"` + Status consts.UserStatus `gorm:"column:status;type:character varying(50);default:verified" json:"status"` CreatedAt time.Time `gorm:"column:created_at;type:timestamp with time zone;default:now()" json:"created_at"` UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp with time zone;default:now()" json:"updated_at"` } diff --git a/backend/database/models/tenant_users.query.gen.go b/backend/database/models/tenant_users.query.gen.go index 1e54b07..7b3cf42 100644 --- a/backend/database/models/tenant_users.query.gen.go +++ b/backend/database/models/tenant_users.query.gen.go @@ -29,7 +29,7 @@ func newTenantUser(db *gorm.DB, opts ...gen.DOOption) tenantUserQuery { _tenantUserQuery.TenantID = field.NewInt64(tableName, "tenant_id") _tenantUserQuery.UserID = field.NewInt64(tableName, "user_id") _tenantUserQuery.Role = field.NewArray(tableName, "role") - _tenantUserQuery.Status = field.NewString(tableName, "status") + _tenantUserQuery.Status = field.NewField(tableName, "status") _tenantUserQuery.CreatedAt = field.NewTime(tableName, "created_at") _tenantUserQuery.UpdatedAt = field.NewTime(tableName, "updated_at") @@ -46,7 +46,7 @@ type tenantUserQuery struct { TenantID field.Int64 UserID field.Int64 Role field.Array - Status field.String + Status field.Field CreatedAt field.Time UpdatedAt field.Time @@ -69,7 +69,7 @@ func (t *tenantUserQuery) updateTableName(table string) *tenantUserQuery { t.TenantID = field.NewInt64(table, "tenant_id") t.UserID = field.NewInt64(table, "user_id") t.Role = field.NewArray(table, "role") - t.Status = field.NewString(table, "status") + t.Status = field.NewField(table, "status") t.CreatedAt = field.NewTime(table, "created_at") t.UpdatedAt = field.NewTime(table, "updated_at") diff --git a/backend/pkg/consts/consts.gen.go b/backend/pkg/consts/consts.gen.go index 29ad0cf..0559e39 100644 --- a/backend/pkg/consts/consts.gen.go +++ b/backend/pkg/consts/consts.gen.go @@ -1686,12 +1686,15 @@ func (x NullOrderStatusStr) Value() (driver.Value, error) { const ( // OrderTypeContentPurchase is a OrderType of type content_purchase. OrderTypeContentPurchase OrderType = "content_purchase" + // OrderTypeRecharge is a OrderType of type recharge. + OrderTypeRecharge OrderType = "recharge" ) var ErrInvalidOrderType = fmt.Errorf("not a valid OrderType, try [%s]", strings.Join(_OrderTypeNames, ", ")) var _OrderTypeNames = []string{ string(OrderTypeContentPurchase), + string(OrderTypeRecharge), } // OrderTypeNames returns a list of possible string values of OrderType. @@ -1705,6 +1708,7 @@ func OrderTypeNames() []string { func OrderTypeValues() []OrderType { return []OrderType{ OrderTypeContentPurchase, + OrderTypeRecharge, } } @@ -1722,6 +1726,7 @@ func (x OrderType) IsValid() bool { var _OrderTypeValue = map[string]OrderType{ "content_purchase": OrderTypeContentPurchase, + "recharge": OrderTypeRecharge, } // ParseOrderType attempts to convert a string to a OrderType. @@ -2499,6 +2504,10 @@ func (x NullTenantUserRoleStr) Value() (driver.Value, error) { } const ( + // UserStatusActive is a UserStatus of type active. + UserStatusActive UserStatus = "active" + // UserStatusInactive is a UserStatus of type inactive. + UserStatusInactive UserStatus = "inactive" // UserStatusPendingVerify is a UserStatus of type pending_verify. UserStatusPendingVerify UserStatus = "pending_verify" // UserStatusVerified is a UserStatus of type verified. @@ -2510,6 +2519,8 @@ const ( var ErrInvalidUserStatus = fmt.Errorf("not a valid UserStatus, try [%s]", strings.Join(_UserStatusNames, ", ")) var _UserStatusNames = []string{ + string(UserStatusActive), + string(UserStatusInactive), string(UserStatusPendingVerify), string(UserStatusVerified), string(UserStatusBanned), @@ -2525,6 +2536,8 @@ func UserStatusNames() []string { // UserStatusValues returns a list of the values for UserStatus func UserStatusValues() []UserStatus { return []UserStatus{ + UserStatusActive, + UserStatusInactive, UserStatusPendingVerify, UserStatusVerified, UserStatusBanned, @@ -2544,6 +2557,8 @@ func (x UserStatus) IsValid() bool { } var _UserStatusValue = map[string]UserStatus{ + "active": UserStatusActive, + "inactive": UserStatusInactive, "pending_verify": UserStatusPendingVerify, "verified": UserStatusVerified, "banned": UserStatusBanned, diff --git a/backend/pkg/consts/consts.go b/backend/pkg/consts/consts.go index de483bc..acffb4a 100644 --- a/backend/pkg/consts/consts.go +++ b/backend/pkg/consts/consts.go @@ -40,12 +40,16 @@ func RoleItems() []requests.KV { } // swagger:enum UserStatus -// ENUM(pending_verify, verified, banned, ) +// ENUM(active, inactive, pending_verify, verified, banned, ) type UserStatus string // Description returns the Chinese label for the specific enum value. func (t UserStatus) Description() string { switch t { + case UserStatusActive: + return "正常" + case UserStatusInactive: + return "未激活" case UserStatusPendingVerify: return "待审核" case UserStatusVerified: @@ -398,7 +402,7 @@ func ContentAccessStatusItems() []requests.KV { // orders // swagger:enum OrderType -// ENUM( content_purchase ) +// ENUM( content_purchase, recharge ) type OrderType string // Description returns the Chinese label for the specific enum value. @@ -406,6 +410,8 @@ func (t OrderType) Description() string { switch t { case OrderTypeContentPurchase: return "购买内容" + case OrderTypeRecharge: + return "充值" default: return "未知类型" }