feat: 重构内容列表接口,使用过滤器结构体简化参数传递;更新相关服务和测试用例
This commit is contained in:
@@ -38,7 +38,7 @@ func (e *AppError) copy() *AppError {
|
|||||||
func (e *AppError) WithCause(err error) *AppError {
|
func (e *AppError) WithCause(err error) *AppError {
|
||||||
newErr := e.copy()
|
newErr := e.copy()
|
||||||
newErr.originalErr = err
|
newErr.originalErr = err
|
||||||
|
|
||||||
// 记录调用者位置
|
// 记录调用者位置
|
||||||
if _, file, line, ok := runtime.Caller(1); ok {
|
if _, file, line, ok := runtime.Caller(1); ok {
|
||||||
newErr.file = fmt.Sprintf("%s:%d", file, line)
|
newErr.file = fmt.Sprintf("%s:%d", file, line)
|
||||||
@@ -90,4 +90,4 @@ func NewError(code ErrorCode, statusCode int, message string) *AppError {
|
|||||||
Message: message,
|
Message: message,
|
||||||
StatusCode: statusCode,
|
StatusCode: statusCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,17 +25,12 @@ type Content struct{}
|
|||||||
// @Param sort query string false "Sort order" Enums(latest, hot, price_asc)
|
// @Param sort query string false "Sort order" Enums(latest, hot, price_asc)
|
||||||
// @Param page query int false "Page number"
|
// @Param page query int false "Page number"
|
||||||
// @Success 200 {object} requests.Pager{items=[]dto.ContentItem}
|
// @Success 200 {object} requests.Pager{items=[]dto.ContentItem}
|
||||||
// @Bind keyword query
|
// @Bind filter query
|
||||||
// @Bind genre query
|
|
||||||
// @Bind tenantId query
|
|
||||||
// @Bind sort query
|
|
||||||
// @Bind page query
|
|
||||||
func (c *Content) List(
|
func (c *Content) List(
|
||||||
ctx fiber.Ctx,
|
ctx fiber.Ctx,
|
||||||
keyword, genre, tenantId, sort string,
|
filter *dto.ContentListFilter,
|
||||||
page int,
|
|
||||||
) (*requests.Pager, error) {
|
) (*requests.Pager, error) {
|
||||||
return services.Content.List(ctx.Context(), keyword, genre, tenantId, sort, page)
|
return services.Content.List(ctx.Context(), filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get content detail
|
// Get content detail
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
|
import "quyun/v2/app/requests"
|
||||||
|
|
||||||
|
type ContentListFilter struct {
|
||||||
|
requests.Pagination
|
||||||
|
Keyword string `query:"keyword"`
|
||||||
|
Genre string `query:"genre"`
|
||||||
|
TenantID string `query:"tenantId"`
|
||||||
|
Sort string `query:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
type ContentItem struct {
|
type ContentItem struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
|
|||||||
@@ -69,13 +69,9 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
))
|
))
|
||||||
// Register routes for controller: Content
|
// Register routes for controller: Content
|
||||||
r.log.Debugf("Registering route: Get /v1/contents -> content.List")
|
r.log.Debugf("Registering route: Get /v1/contents -> content.List")
|
||||||
router.Get("/v1/contents"[len(r.Path()):], DataFunc5(
|
router.Get("/v1/contents"[len(r.Path()):], DataFunc1(
|
||||||
r.content.List,
|
r.content.List,
|
||||||
QueryParam[string]("keyword"),
|
Query[dto.ContentListFilter]("filter"),
|
||||||
QueryParam[string]("genre"),
|
|
||||||
QueryParam[string]("tenantId"),
|
|
||||||
QueryParam[string]("sort"),
|
|
||||||
QueryParam[int]("page"),
|
|
||||||
))
|
))
|
||||||
r.log.Debugf("Registering route: Get /v1/contents/:id -> content.Get")
|
r.log.Debugf("Registering route: Get /v1/contents/:id -> content.Get")
|
||||||
router.Get("/v1/contents/:id"[len(r.Path()):], DataFunc1(
|
router.Get("/v1/contents/:id"[len(r.Path()):], DataFunc1(
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (s *common) Upload(ctx context.Context, file *multipart.FileHeader, typeArg
|
|||||||
objectKey := uuid.NewString() + "_" + file.Filename
|
objectKey := uuid.NewString() + "_" + file.Filename
|
||||||
url := "http://mock-storage/" + objectKey
|
url := "http://mock-storage/" + objectKey
|
||||||
|
|
||||||
// Determine TenantID.
|
// Determine TenantID.
|
||||||
// Uploads usually happen in context of a tenant? Or personal?
|
// Uploads usually happen in context of a tenant? Or personal?
|
||||||
// For now assume user's owned tenant if any, or 0.
|
// For now assume user's owned tenant if any, or 0.
|
||||||
// MediaAsset has TenantID (NOT NULL).
|
// MediaAsset has TenantID (NOT NULL).
|
||||||
@@ -41,7 +41,7 @@ func (s *common) Upload(ctx context.Context, file *multipart.FileHeader, typeArg
|
|||||||
tid = t.ID
|
tid = t.ID
|
||||||
}
|
}
|
||||||
// If no tenant, and TenantID is NOT NULL, we have a problem for regular users uploading avatar?
|
// 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.
|
// Users avatar is URL string in `users` table.
|
||||||
// MediaAssets table is for TENANT content.
|
// MediaAssets table is for TENANT content.
|
||||||
// If this is for user avatar upload, maybe we don't use MediaAssets?
|
// If this is for user avatar upload, maybe we don't use MediaAssets?
|
||||||
// But `upload` endpoint is generic.
|
// But `upload` endpoint is generic.
|
||||||
@@ -56,7 +56,7 @@ func (s *common) Upload(ctx context.Context, file *multipart.FileHeader, typeArg
|
|||||||
Provider: "mock",
|
Provider: "mock",
|
||||||
Bucket: "default",
|
Bucket: "default",
|
||||||
ObjectKey: objectKey,
|
ObjectKey: objectKey,
|
||||||
Meta: types.NewJSONType(fields.MediaAssetMeta{
|
Meta: types.NewJSONType(fields.MediaAssetMeta{
|
||||||
Size: file.Size,
|
Size: file.Size,
|
||||||
// MimeType?
|
// MimeType?
|
||||||
}),
|
}),
|
||||||
@@ -73,4 +73,4 @@ func (s *common) Upload(ctx context.Context, file *multipart.FileHeader, typeArg
|
|||||||
Size: file.Size,
|
Size: file.Size,
|
||||||
MimeType: file.Header.Get("Content-Type"),
|
MimeType: file.Header.Get("Content-Type"),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,19 +18,19 @@ import (
|
|||||||
// @provider
|
// @provider
|
||||||
type content struct{}
|
type content struct{}
|
||||||
|
|
||||||
func (s *content) List(ctx context.Context, keyword, genre, tenantId, sort string, page int) (*requests.Pager, error) {
|
func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilter) (*requests.Pager, error) {
|
||||||
tbl, q := models.ContentQuery.QueryContext(ctx)
|
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||||
|
|
||||||
// Filters
|
// Filters
|
||||||
q = q.Where(tbl.Status.Eq(consts.ContentStatusPublished))
|
q = q.Where(tbl.Status.Eq(consts.ContentStatusPublished))
|
||||||
if keyword != "" {
|
if filter.Keyword != "" {
|
||||||
q = q.Where(tbl.Title.Like("%" + keyword + "%"))
|
q = q.Where(tbl.Title.Like("%" + filter.Keyword + "%"))
|
||||||
}
|
}
|
||||||
if genre != "" {
|
if filter.Genre != "" {
|
||||||
q = q.Where(tbl.Genre.Eq(genre))
|
q = q.Where(tbl.Genre.Eq(filter.Genre))
|
||||||
}
|
}
|
||||||
if tenantId != "" {
|
if filter.TenantID != "" {
|
||||||
tid := cast.ToInt64(tenantId)
|
tid := cast.ToInt64(filter.TenantID)
|
||||||
q = q.Where(tbl.TenantID.Eq(tid))
|
q = q.Where(tbl.TenantID.Eq(tid))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ func (s *content) List(ctx context.Context, keyword, genre, tenantId, sort strin
|
|||||||
q = q.Preload(tbl.Author)
|
q = q.Preload(tbl.Author)
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
switch sort {
|
switch filter.Sort {
|
||||||
case "hot":
|
case "hot":
|
||||||
q = q.Order(tbl.Views.Desc())
|
q = q.Order(tbl.Views.Desc())
|
||||||
case "price_asc":
|
case "price_asc":
|
||||||
@@ -48,13 +48,14 @@ func (s *content) List(ctx context.Context, keyword, genre, tenantId, sort strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
p := requests.Pagination{Page: int64(page), Limit: 10}
|
// Use embedded pagination directly
|
||||||
|
filter.Pagination.Format()
|
||||||
total, err := q.Count()
|
total, err := q.Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := q.Offset(int(p.Offset())).Limit(int(p.Limit)).Find()
|
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
@@ -66,12 +67,9 @@ func (s *content) List(ctx context.Context, keyword, genre, tenantId, sort strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &requests.Pager{
|
return &requests.Pager{
|
||||||
Pagination: requests.Pagination{
|
Pagination: filter.Pagination,
|
||||||
Page: p.Page,
|
Total: total,
|
||||||
Limit: p.Limit,
|
Items: data,
|
||||||
},
|
|
||||||
Total: total,
|
|
||||||
Items: data,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"quyun/v2/app/commands/testx"
|
"quyun/v2/app/commands/testx"
|
||||||
content_dto "quyun/v2/app/http/v1/dto"
|
content_dto "quyun/v2/app/http/v1/dto"
|
||||||
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/database"
|
"quyun/v2/database"
|
||||||
"quyun/v2/database/models"
|
"quyun/v2/database/models"
|
||||||
"quyun/v2/pkg/consts"
|
"quyun/v2/pkg/consts"
|
||||||
@@ -65,7 +66,14 @@ func (s *ContentTestSuite) Test_List() {
|
|||||||
models.ContentQuery.WithContext(ctx).Create(c1, c2)
|
models.ContentQuery.WithContext(ctx).Create(c1, c2)
|
||||||
|
|
||||||
Convey("should list only published contents", func() {
|
Convey("should list only published contents", func() {
|
||||||
res, err := Content.List(ctx, "", "", "1", "", 1)
|
filter := &content_dto.ContentListFilter{
|
||||||
|
TenantID: "1",
|
||||||
|
Pagination: requests.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
res, err := Content.List(ctx, filter)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(res.Total, ShouldEqual, 1)
|
So(res.Total, ShouldEqual, 1)
|
||||||
items := res.Items.([]content_dto.ContentItem)
|
items := res.Items.([]content_dto.ContentItem)
|
||||||
|
|||||||
@@ -275,4 +275,4 @@ func (s *creator) getTenantID(ctx context.Context) (int64, error) {
|
|||||||
return 0, errorx.ErrDatabaseError.WithCause(err)
|
return 0, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
return t.ID, nil
|
return t.ID, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ func (s *order) Create(ctx context.Context, form *transaction_dto.OrderCreateFor
|
|||||||
Status: consts.OrderStatusCreated,
|
Status: consts.OrderStatusCreated,
|
||||||
Currency: consts.Currency(price.Currency), // price.Currency is consts.Currency in DB? Yes.
|
Currency: consts.Currency(price.Currency), // price.Currency is consts.Currency in DB? Yes.
|
||||||
AmountOriginal: price.PriceAmount,
|
AmountOriginal: price.PriceAmount,
|
||||||
AmountDiscount: 0, // Calculate discount if needed
|
AmountDiscount: 0, // Calculate discount if needed
|
||||||
AmountPaid: price.PriceAmount, // Expected to pay
|
AmountPaid: price.PriceAmount, // Expected to pay
|
||||||
IdempotencyKey: uuid.NewString(), // Should be from client ideally
|
IdempotencyKey: uuid.NewString(), // Should be from client ideally
|
||||||
Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Populate details
|
Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), // Populate details
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,6 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
|
|||||||
info, err := tx.User.WithContext(ctx).
|
info, err := tx.User.WithContext(ctx).
|
||||||
Where(tx.User.ID.Eq(o.UserID), tx.User.Balance.Gte(o.AmountPaid)).
|
Where(tx.User.ID.Eq(o.UserID), tx.User.Balance.Gte(o.AmountPaid)).
|
||||||
Update(tx.User.Balance, gorm.Expr("balance - ?", o.AmountPaid))
|
Update(tx.User.Balance, gorm.Expr("balance - ?", o.AmountPaid))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -197,7 +196,7 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ledger := &models.TenantLedger{
|
ledger := &models.TenantLedger{
|
||||||
TenantID: o.TenantID,
|
TenantID: o.TenantID,
|
||||||
UserID: t.UserID, // Owner
|
UserID: t.UserID, // Owner
|
||||||
@@ -218,7 +217,6 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*errorx.AppError); ok {
|
if _, ok := err.(*errorx.AppError); ok {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -243,4 +241,4 @@ func (s *order) toUserOrderDTO(o *models.Order) user_dto.Order {
|
|||||||
Amount: float64(o.AmountPaid) / 100.0,
|
Amount: float64(o.AmountPaid) / 100.0,
|
||||||
CreateTime: o.CreatedAt.Format(time.RFC3339),
|
CreateTime: o.CreatedAt.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,4 +282,4 @@ func (s *super) UserStatuses(ctx context.Context) ([]requests.KV, error) {
|
|||||||
|
|
||||||
func (s *super) TenantStatuses(ctx context.Context) ([]requests.KV, error) {
|
func (s *super) TenantStatuses(ctx context.Context) ([]requests.KV, error) {
|
||||||
return consts.TenantStatusItems(), nil
|
return consts.TenantStatusItems(), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (s *SuperTestSuite) Test_ListUsers() {
|
|||||||
res, err := Super.ListUsers(ctx, 1, 10, "")
|
res, err := Super.ListUsers(ctx, 1, 10, "")
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(res.Total, ShouldEqual, 2)
|
So(res.Total, ShouldEqual, 2)
|
||||||
|
|
||||||
items := res.Items.([]super_dto.UserItem)
|
items := res.Items.([]super_dto.UserItem)
|
||||||
So(items[0].Username, ShouldEqual, "user2") // Desc order
|
So(items[0].Username, ShouldEqual, "user2") // Desc order
|
||||||
})
|
})
|
||||||
@@ -88,4 +88,4 @@ func (s *SuperTestSuite) Test_CreateTenant() {
|
|||||||
So(t.Status, ShouldEqual, consts.TenantStatusVerified)
|
So(t.Status, ShouldEqual, consts.TenantStatusVerified)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ type tenant struct{}
|
|||||||
func (s *tenant) GetPublicProfile(ctx context.Context, id string) (*tenant_dto.TenantProfile, error) {
|
func (s *tenant) GetPublicProfile(ctx context.Context, id string) (*tenant_dto.TenantProfile, error) {
|
||||||
// id could be Code or ID. Try Code first, then ID.
|
// id could be Code or ID. Try Code first, then ID.
|
||||||
tbl, q := models.TenantQuery.QueryContext(ctx)
|
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||||
|
|
||||||
// Try to find by code or ID
|
// Try to find by code or ID
|
||||||
var t *models.Tenant
|
var t *models.Tenant
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
// Assume id is ID for simplicity if numeric, or try both.
|
// Assume id is ID for simplicity if numeric, or try both.
|
||||||
if cast.ToInt64(id) > 0 {
|
if cast.ToInt64(id) > 0 {
|
||||||
t, err = q.Where(tbl.ID.Eq(cast.ToInt64(id))).First()
|
t, err = q.Where(tbl.ID.Eq(cast.ToInt64(id))).First()
|
||||||
@@ -48,7 +48,7 @@ func (s *tenant) GetPublicProfile(ctx context.Context, id string) (*tenant_dto.T
|
|||||||
var likes int64
|
var likes int64
|
||||||
// Sum content likes
|
// Sum content likes
|
||||||
// Mock likes for now or fetch
|
// Mock likes for now or fetch
|
||||||
|
|
||||||
// IsFollowing
|
// IsFollowing
|
||||||
isFollowing := false
|
isFollowing := false
|
||||||
userID := ctx.Value(consts.CtxKeyUser)
|
userID := ctx.Value(consts.CtxKeyUser)
|
||||||
@@ -60,14 +60,14 @@ func (s *tenant) GetPublicProfile(ctx context.Context, id string) (*tenant_dto.T
|
|||||||
|
|
||||||
// Config parsing (Unused for now as we don't map to bio yet)
|
// Config parsing (Unused for now as we don't map to bio yet)
|
||||||
// config := t.Config.Data()
|
// config := t.Config.Data()
|
||||||
|
|
||||||
return &tenant_dto.TenantProfile{
|
return &tenant_dto.TenantProfile{
|
||||||
ID: cast.ToString(t.ID),
|
ID: cast.ToString(t.ID),
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Avatar: "", // From config
|
Avatar: "", // From config
|
||||||
Cover: "", // From config
|
Cover: "", // From config
|
||||||
Bio: "", // From config
|
Bio: "", // From config
|
||||||
Description: "", // From config
|
Description: "", // From config
|
||||||
CertType: "personal", // Mock
|
CertType: "personal", // Mock
|
||||||
Stats: tenant_dto.Stats{
|
Stats: tenant_dto.Stats{
|
||||||
Followers: int(followers),
|
Followers: int(followers),
|
||||||
@@ -99,7 +99,7 @@ func (s *tenant) Follow(ctx context.Context, id string) error {
|
|||||||
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
|
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
|
||||||
Status: consts.UserStatusVerified,
|
Status: consts.UserStatusVerified,
|
||||||
}
|
}
|
||||||
|
|
||||||
count, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid), models.TenantUserQuery.UserID.Eq(uid)).Count()
|
count, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tid), models.TenantUserQuery.UserID.Eq(uid)).Count()
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
return nil // Already following
|
return nil // Already following
|
||||||
@@ -124,4 +124,4 @@ func (s *tenant) Unfollow(ctx context.Context, id string) error {
|
|||||||
return errorx.ErrDatabaseError.WithCause(err)
|
return errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,4 +108,4 @@ func (s *wallet) Recharge(ctx context.Context, form *user_dto.RechargeForm) (*us
|
|||||||
PayParams: "mock_recharge_url",
|
PayParams: "mock_recharge_url",
|
||||||
OrderID: cast.ToString(order.ID),
|
OrderID: cast.ToString(order.ID),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (s *WalletTestSuite) Test_GetWallet() {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(res.Balance, ShouldEqual, 50.0)
|
So(res.Balance, ShouldEqual, 50.0)
|
||||||
So(len(res.Transactions), ShouldEqual, 2)
|
So(len(res.Transactions), ShouldEqual, 2)
|
||||||
|
|
||||||
// Order by CreatedAt Desc
|
// Order by CreatedAt Desc
|
||||||
types := []string{res.Transactions[0].Type, res.Transactions[1].Type}
|
types := []string{res.Transactions[0].Type, res.Transactions[1].Type}
|
||||||
So(types, ShouldContain, "income")
|
So(types, ShouldContain, "income")
|
||||||
|
|||||||
Reference in New Issue
Block a user