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.

This commit is contained in:
2025-12-29 14:21:20 +08:00
parent d648a1e45b
commit 8fa3d18a9c
30 changed files with 2251 additions and 85 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -189,4 +189,4 @@ func (s *user) toAuthUserDTO(u *models.User) *auth_dto.User {
Points: u.Points,
IsRealNameVerified: u.IsRealNameVerified,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"`
}

View File

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

View File

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

View File

@@ -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 "未知类型"
}