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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
151
backend/app/services/content_test.go
Normal file
151
backend/app/services/content_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
106
backend/app/services/creator_test.go
Normal file
106
backend/app/services/creator_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
125
backend/app/services/order_test.go
Normal file
125
backend/app/services/order_test.go
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
91
backend/app/services/super_test.go
Normal file
91
backend/app/services/super_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -189,4 +189,4 @@ func (s *user) toAuthUserDTO(u *models.User) *auth_dto.User {
|
||||
Points: u.Points,
|
||||
IsRealNameVerified: u.IsRealNameVerified,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
96
backend/app/services/wallet_test.go
Normal file
96
backend/app/services/wallet_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "未知类型"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user