feat: tenant-scoped routing and portal navigation
This commit is contained in:
@@ -115,7 +115,8 @@ func Serve(providers container.Providers, t *testing.T, invoke any) {
|
||||
dig.In
|
||||
Initials []contracts.Initial `group:"initials"`
|
||||
Job *job.Job
|
||||
}) error {
|
||||
},
|
||||
) error {
|
||||
_ = p.Initials
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
@@ -3,6 +3,8 @@ package auth
|
||||
import (
|
||||
"quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
@@ -12,7 +14,7 @@ type Auth struct{}
|
||||
|
||||
// SendOTP sends an OTP to the provided phone number.
|
||||
//
|
||||
// @Router /v1/auth/otp [post]
|
||||
// @Router /t/:tenantCode/v1/auth/otp [post]
|
||||
// @Summary Send OTP
|
||||
// @Description Send OTP to phone number
|
||||
// @Tags Auth
|
||||
@@ -27,7 +29,7 @@ func (a *Auth) SendOTP(ctx fiber.Ctx, form *dto.SendOTPForm) error {
|
||||
|
||||
// Login logs in or registers a user with OTP.
|
||||
//
|
||||
// @Router /v1/auth/login [post]
|
||||
// @Router /t/:tenantCode/v1/auth/login [post]
|
||||
// @Summary Login or Register with OTP
|
||||
// @Description Login or register user using phone number and OTP
|
||||
// @Tags Auth
|
||||
@@ -37,5 +39,11 @@ func (a *Auth) SendOTP(ctx fiber.Ctx, form *dto.SendOTPForm) error {
|
||||
// @Success 200 {object} dto.LoginResponse
|
||||
// @Bind form body
|
||||
func (a *Auth) Login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, error) {
|
||||
return services.User.LoginWithOTP(ctx, form.Phone, form.OTP)
|
||||
tenantID := int64(0)
|
||||
if t := ctx.Locals(consts.CtxKeyTenant); t != nil {
|
||||
if tenant, ok := t.(*models.Tenant); ok {
|
||||
tenantID = tenant.ID
|
||||
}
|
||||
}
|
||||
return services.User.LoginWithOTP(ctx, tenantID, form.Phone, form.OTP)
|
||||
}
|
||||
|
||||
@@ -42,13 +42,13 @@ func (r *Routes) Name() string {
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: Auth
|
||||
r.log.Debugf("Registering route: Post /v1/auth/login -> auth.Login")
|
||||
router.Post("/v1/auth/login"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/auth/login -> auth.Login")
|
||||
router.Post("/t/:tenantCode/v1/auth/login"[len(r.Path()):], DataFunc1(
|
||||
r.auth.Login,
|
||||
Body[dto.LoginForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/auth/otp -> auth.SendOTP")
|
||||
router.Post("/v1/auth/otp"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/auth/otp -> auth.SendOTP")
|
||||
router.Post("/t/:tenantCode/v1/auth/otp"[len(r.Path()):], Func1(
|
||||
r.auth.SendOTP,
|
||||
Body[dto.SendOTPForm]("form"),
|
||||
))
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// @provider
|
||||
type Common struct{}
|
||||
|
||||
// @Router /v1/upload [post]
|
||||
// @Router /t/:tenantCode/v1/upload [post]
|
||||
// @Summary Upload file
|
||||
// @Description Upload file
|
||||
// @Tags Common
|
||||
@@ -31,16 +31,17 @@ func (c *Common) Upload(
|
||||
file *multipart.FileHeader,
|
||||
form *dto.UploadForm,
|
||||
) (*dto.UploadResult, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
val := ""
|
||||
if form != nil {
|
||||
val = form.Type
|
||||
}
|
||||
return services.Common.Upload(ctx, user.ID, file, val)
|
||||
return services.Common.Upload(ctx, tenantID, user.ID, file, val)
|
||||
}
|
||||
|
||||
// Get options (enums)
|
||||
//
|
||||
// @Router /v1/common/options [get]
|
||||
// @Router /t/:tenantCode/v1/common/options [get]
|
||||
// @Summary Get options
|
||||
// @Description Get global options (enums)
|
||||
// @Tags Common
|
||||
@@ -53,7 +54,7 @@ func (c *Common) GetOptions(ctx fiber.Ctx) (*dto.OptionsResponse, error) {
|
||||
|
||||
// Check file hash for deduplication
|
||||
//
|
||||
// @Router /v1/upload/check [get]
|
||||
// @Router /t/:tenantCode/v1/upload/check [get]
|
||||
// @Summary Check hash
|
||||
// @Description Check if file hash exists
|
||||
// @Tags Common
|
||||
@@ -64,10 +65,11 @@ func (c *Common) GetOptions(ctx fiber.Ctx) (*dto.OptionsResponse, error) {
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind hash query
|
||||
func (c *Common) CheckHash(ctx fiber.Ctx, user *models.User, hash string) (*dto.UploadResult, error) {
|
||||
return services.Common.CheckHash(ctx, user.ID, hash)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Common.CheckHash(ctx, tenantID, user.ID, hash)
|
||||
}
|
||||
|
||||
// @Router /v1/upload/init [post]
|
||||
// @Router /t/:tenantCode/v1/upload/init [post]
|
||||
// @Summary Init multipart upload
|
||||
// @Description Initialize multipart upload
|
||||
// @Tags Common
|
||||
@@ -78,10 +80,11 @@ func (c *Common) CheckHash(ctx fiber.Ctx, user *models.User, hash string) (*dto.
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Common) InitUpload(ctx fiber.Ctx, user *models.User, form *dto.UploadInitForm) (*dto.UploadInitResponse, error) {
|
||||
return services.Common.InitUpload(ctx.Context(), user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Common.InitUpload(ctx.Context(), tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// @Router /v1/upload/part [post]
|
||||
// @Router /t/:tenantCode/v1/upload/part [post]
|
||||
// @Summary Upload part
|
||||
// @Description Upload a part
|
||||
// @Tags Common
|
||||
@@ -94,10 +97,11 @@ func (c *Common) InitUpload(ctx fiber.Ctx, user *models.User, form *dto.UploadIn
|
||||
// @Bind file file
|
||||
// @Bind form body
|
||||
func (c *Common) UploadPart(ctx fiber.Ctx, user *models.User, file *multipart.FileHeader, form *dto.UploadPartForm) error {
|
||||
return services.Common.UploadPart(ctx.Context(), user.ID, file, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Common.UploadPart(ctx.Context(), tenantID, user.ID, file, form)
|
||||
}
|
||||
|
||||
// @Router /v1/upload/complete [post]
|
||||
// @Router /t/:tenantCode/v1/upload/complete [post]
|
||||
// @Summary Complete upload
|
||||
// @Description Complete multipart upload
|
||||
// @Tags Common
|
||||
@@ -108,10 +112,11 @@ func (c *Common) UploadPart(ctx fiber.Ctx, user *models.User, file *multipart.Fi
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Common) CompleteUpload(ctx fiber.Ctx, user *models.User, form *dto.UploadCompleteForm) (*dto.UploadResult, error) {
|
||||
return services.Common.CompleteUpload(ctx.Context(), user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Common.CompleteUpload(ctx.Context(), tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// @Router /v1/upload/:uploadId [delete]
|
||||
// @Router /t/:tenantCode/v1/upload/:uploadId [delete]
|
||||
// @Summary Abort upload
|
||||
// @Description Abort multipart upload
|
||||
// @Tags Common
|
||||
@@ -122,10 +127,11 @@ func (c *Common) CompleteUpload(ctx fiber.Ctx, user *models.User, form *dto.Uplo
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind uploadId path
|
||||
func (c *Common) AbortUpload(ctx fiber.Ctx, user *models.User, uploadId string) error {
|
||||
return services.Common.AbortUpload(ctx.Context(), user.ID, uploadId)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Common.AbortUpload(ctx.Context(), tenantID, user.ID, uploadId)
|
||||
}
|
||||
|
||||
// @Router /v1/media-assets/:id<int> [delete]
|
||||
// @Router /t/:tenantCode/v1/media-assets/:id<int> [delete]
|
||||
// @Summary Delete media asset
|
||||
// @Description Delete media asset
|
||||
// @Tags Common
|
||||
@@ -136,7 +142,8 @@ func (c *Common) AbortUpload(ctx fiber.Ctx, user *models.User, uploadId string)
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (c *Common) DeleteMediaAsset(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
return services.Common.DeleteMediaAsset(ctx.Context(), user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Common.DeleteMediaAsset(ctx.Context(), tenantID, user.ID, id)
|
||||
}
|
||||
|
||||
// Upload file
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
@@ -15,7 +14,7 @@ type Content struct{}
|
||||
|
||||
// List contents (Explore / Search)
|
||||
//
|
||||
// @Router /v1/contents [get]
|
||||
// @Router /t/:tenantCode/v1/contents [get]
|
||||
// @Summary List contents
|
||||
// @Description List contents with filtering and pagination
|
||||
// @Tags Content
|
||||
@@ -32,12 +31,19 @@ func (c *Content) List(
|
||||
ctx fiber.Ctx,
|
||||
filter *dto.ContentListFilter,
|
||||
) (*requests.Pager, error) {
|
||||
return services.Content.List(ctx, filter)
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 {
|
||||
if filter.TenantID != nil && *filter.TenantID != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
filter.TenantID = &tenantID
|
||||
}
|
||||
return services.Content.List(ctx, tenantID, filter)
|
||||
}
|
||||
|
||||
// Get content detail
|
||||
//
|
||||
// @Router /v1/contents/:id<int> [get]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int> [get]
|
||||
// @Summary Get content detail
|
||||
// @Description Get content detail by ID
|
||||
// @Tags Content
|
||||
@@ -47,13 +53,14 @@ func (c *Content) List(
|
||||
// @Success 200 {object} dto.ContentDetail
|
||||
// @Bind id path
|
||||
func (c *Content) Get(ctx fiber.Ctx, id int64) (*dto.ContentDetail, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.Get(ctx, uid, id)
|
||||
return services.Content.Get(ctx, tenantID, uid, id)
|
||||
}
|
||||
|
||||
// Get comments for a content
|
||||
//
|
||||
// @Router /v1/contents/:id<int>/comments [get]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int>/comments [get]
|
||||
// @Summary Get comments
|
||||
// @Description Get comments for a content
|
||||
// @Tags Content
|
||||
@@ -65,13 +72,14 @@ func (c *Content) Get(ctx fiber.Ctx, id int64) (*dto.ContentDetail, error) {
|
||||
// @Bind id path
|
||||
// @Bind page query
|
||||
func (c *Content) ListComments(ctx fiber.Ctx, id int64, page int) (*requests.Pager, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.ListComments(ctx, uid, id, page)
|
||||
return services.Content.ListComments(ctx, tenantID, uid, id, page)
|
||||
}
|
||||
|
||||
// Post a comment
|
||||
//
|
||||
// @Router /v1/contents/:id<int>/comments [post]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int>/comments [post]
|
||||
// @Summary Post comment
|
||||
// @Description Post a comment to a content
|
||||
// @Tags Content
|
||||
@@ -83,13 +91,14 @@ func (c *Content) ListComments(ctx fiber.Ctx, id int64, page int) (*requests.Pag
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Content) CreateComment(ctx fiber.Ctx, id int64, form *dto.CommentCreateForm) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.CreateComment(ctx, uid, id, form)
|
||||
return services.Content.CreateComment(ctx, tenantID, uid, id, form)
|
||||
}
|
||||
|
||||
// Like a comment
|
||||
//
|
||||
// @Router /v1/comments/:id<int>/like [post]
|
||||
// @Router /t/:tenantCode/v1/comments/:id<int>/like [post]
|
||||
// @Summary Like comment
|
||||
// @Description Like a comment
|
||||
// @Tags Content
|
||||
@@ -99,65 +108,70 @@ func (c *Content) CreateComment(ctx fiber.Ctx, id int64, form *dto.CommentCreate
|
||||
// @Success 200 {string} string "Liked"
|
||||
// @Bind id path
|
||||
func (c *Content) LikeComment(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.LikeComment(ctx, uid, id)
|
||||
return services.Content.LikeComment(ctx, tenantID, uid, id)
|
||||
}
|
||||
|
||||
// Add like
|
||||
//
|
||||
// @Router /v1/contents/:id<int>/like [post]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int>/like [post]
|
||||
// @Summary Add like
|
||||
// @Tags Content
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Liked"
|
||||
// @Bind id path
|
||||
func (c *Content) AddLike(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.AddLike(ctx, uid, id)
|
||||
return services.Content.AddLike(ctx, tenantID, uid, id)
|
||||
}
|
||||
|
||||
// Remove like
|
||||
//
|
||||
// @Router /v1/contents/:id<int>/like [delete]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int>/like [delete]
|
||||
// @Summary Remove like
|
||||
// @Tags Content
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Unliked"
|
||||
// @Bind id path
|
||||
func (c *Content) RemoveLike(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.RemoveLike(ctx, uid, id)
|
||||
return services.Content.RemoveLike(ctx, tenantID, uid, id)
|
||||
}
|
||||
|
||||
// Add favorite
|
||||
//
|
||||
// @Router /v1/contents/:id<int>/favorite [post]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int>/favorite [post]
|
||||
// @Summary Add favorite
|
||||
// @Tags Content
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Favorited"
|
||||
// @Bind id path
|
||||
func (c *Content) AddFavorite(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.AddFavorite(ctx, uid, id)
|
||||
return services.Content.AddFavorite(ctx, tenantID, uid, id)
|
||||
}
|
||||
|
||||
// Remove favorite
|
||||
//
|
||||
// @Router /v1/contents/:id<int>/favorite [delete]
|
||||
// @Router /t/:tenantCode/v1/contents/:id<int>/favorite [delete]
|
||||
// @Summary Remove favorite
|
||||
// @Tags Content
|
||||
// @Param id path int64 true "Content ID"
|
||||
// @Success 200 {string} string "Unfavorited"
|
||||
// @Bind id path
|
||||
func (c *Content) RemoveFavorite(ctx fiber.Ctx, id int64) error {
|
||||
tenantID := getTenantID(ctx)
|
||||
uid := getUserID(ctx)
|
||||
return services.Content.RemoveFavorite(ctx, uid, id)
|
||||
return services.Content.RemoveFavorite(ctx, tenantID, uid, id)
|
||||
}
|
||||
|
||||
// List curated topics
|
||||
//
|
||||
// @Router /v1/topics [get]
|
||||
// @Router /t/:tenantCode/v1/topics [get]
|
||||
// @Summary List topics
|
||||
// @Description List curated topics
|
||||
// @Tags Content
|
||||
@@ -165,14 +179,6 @@ func (c *Content) RemoveFavorite(ctx fiber.Ctx, id int64) error {
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.Topic
|
||||
func (c *Content) ListTopics(ctx fiber.Ctx) ([]dto.Topic, error) {
|
||||
return services.Content.ListTopics(ctx)
|
||||
}
|
||||
|
||||
func getUserID(ctx fiber.Ctx) int64 {
|
||||
if u := ctx.Locals(consts.CtxKeyUser); u != nil {
|
||||
if user, ok := u.(*models.User); ok {
|
||||
return user.ID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.ListTopics(ctx, tenantID)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ type Creator struct{}
|
||||
|
||||
// Apply to become a creator
|
||||
//
|
||||
// @Router /v1/creator/apply [post]
|
||||
// @Router /t/:tenantCode/v1/creator/apply [post]
|
||||
// @Summary Apply creator
|
||||
// @Description Apply to become a creator
|
||||
// @Tags CreatorCenter
|
||||
@@ -25,12 +25,13 @@ type Creator struct{}
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Creator) Apply(ctx fiber.Ctx, user *models.User, form *dto.ApplyForm) error {
|
||||
return services.Creator.Apply(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.Apply(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// Get creator dashboard stats
|
||||
//
|
||||
// @Router /v1/creator/dashboard [get]
|
||||
// @Router /t/:tenantCode/v1/creator/dashboard [get]
|
||||
// @Summary Dashboard stats
|
||||
// @Description Get creator dashboard stats
|
||||
// @Tags CreatorCenter
|
||||
@@ -39,12 +40,13 @@ func (c *Creator) Apply(ctx fiber.Ctx, user *models.User, form *dto.ApplyForm) e
|
||||
// @Success 200 {object} dto.DashboardStats
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (c *Creator) Dashboard(ctx fiber.Ctx, user *models.User) (*dto.DashboardStats, error) {
|
||||
return services.Creator.Dashboard(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.Dashboard(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Get content details for edit
|
||||
//
|
||||
// @Router /v1/creator/contents/:id<int> [get]
|
||||
// @Router /t/:tenantCode/v1/creator/contents/:id<int> [get]
|
||||
// @Summary Get content
|
||||
// @Description Get content details for edit
|
||||
// @Tags CreatorCenter
|
||||
@@ -55,12 +57,13 @@ func (c *Creator) Dashboard(ctx fiber.Ctx, user *models.User) (*dto.DashboardSta
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (c *Creator) GetContent(ctx fiber.Ctx, user *models.User, id int64) (*dto.ContentEditDTO, error) {
|
||||
return services.Creator.GetContent(ctx, user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.GetContent(ctx, tenantID, user.ID, id)
|
||||
}
|
||||
|
||||
// List creator contents
|
||||
//
|
||||
// @Router /v1/creator/contents [get]
|
||||
// @Router /t/:tenantCode/v1/creator/contents [get]
|
||||
// @Summary List contents
|
||||
// @Description List creator contents
|
||||
// @Tags CreatorCenter
|
||||
@@ -77,12 +80,13 @@ func (c *Creator) ListContents(
|
||||
user *models.User,
|
||||
filter *dto.CreatorContentListFilter,
|
||||
) (*requests.Pager, error) {
|
||||
return services.Creator.ListContents(ctx, user.ID, filter)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.ListContents(ctx, tenantID, user.ID, filter)
|
||||
}
|
||||
|
||||
// Create/Publish content
|
||||
//
|
||||
// @Router /v1/creator/contents [post]
|
||||
// @Router /t/:tenantCode/v1/creator/contents [post]
|
||||
// @Summary Create content
|
||||
// @Description Create/Publish content
|
||||
// @Tags CreatorCenter
|
||||
@@ -93,12 +97,13 @@ func (c *Creator) ListContents(
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Creator) CreateContent(ctx fiber.Ctx, user *models.User, form *dto.ContentCreateForm) error {
|
||||
return services.Creator.CreateContent(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.CreateContent(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// Update content
|
||||
//
|
||||
// @Router /v1/creator/contents/:id<int> [put]
|
||||
// @Router /t/:tenantCode/v1/creator/contents/:id<int> [put]
|
||||
// @Summary Update content
|
||||
// @Description Update content
|
||||
// @Tags CreatorCenter
|
||||
@@ -111,12 +116,13 @@ func (c *Creator) CreateContent(ctx fiber.Ctx, user *models.User, form *dto.Cont
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) UpdateContent(ctx fiber.Ctx, user *models.User, id int64, form *dto.ContentUpdateForm) error {
|
||||
return services.Creator.UpdateContent(ctx, user.ID, id, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.UpdateContent(ctx, tenantID, user.ID, id, form)
|
||||
}
|
||||
|
||||
// Delete content
|
||||
//
|
||||
// @Router /v1/creator/contents/:id<int> [delete]
|
||||
// @Router /t/:tenantCode/v1/creator/contents/:id<int> [delete]
|
||||
// @Summary Delete content
|
||||
// @Description Delete content
|
||||
// @Tags CreatorCenter
|
||||
@@ -127,12 +133,13 @@ func (c *Creator) UpdateContent(ctx fiber.Ctx, user *models.User, id int64, form
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (c *Creator) DeleteContent(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
return services.Creator.DeleteContent(ctx, user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.DeleteContent(ctx, tenantID, user.ID, id)
|
||||
}
|
||||
|
||||
// List sales orders
|
||||
//
|
||||
// @Router /v1/creator/orders [get]
|
||||
// @Router /t/:tenantCode/v1/creator/orders [get]
|
||||
// @Summary List sales orders
|
||||
// @Description List sales orders
|
||||
// @Tags CreatorCenter
|
||||
@@ -148,12 +155,13 @@ func (c *Creator) ListOrders(
|
||||
user *models.User,
|
||||
filter *dto.CreatorOrderListFilter,
|
||||
) ([]dto.Order, error) {
|
||||
return services.Creator.ListOrders(ctx, user.ID, filter)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.ListOrders(ctx, tenantID, user.ID, filter)
|
||||
}
|
||||
|
||||
// Process refund
|
||||
//
|
||||
// @Router /v1/creator/orders/:id<int>/refund [post]
|
||||
// @Router /t/:tenantCode/v1/creator/orders/:id<int>/refund [post]
|
||||
// @Summary Process refund
|
||||
// @Description Process refund
|
||||
// @Tags CreatorCenter
|
||||
@@ -166,12 +174,13 @@ func (c *Creator) ListOrders(
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *Creator) Refund(ctx fiber.Ctx, user *models.User, id int64, form *dto.RefundForm) error {
|
||||
return services.Creator.ProcessRefund(ctx, user.ID, id, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.ProcessRefund(ctx, tenantID, user.ID, id, form)
|
||||
}
|
||||
|
||||
// Get channel settings
|
||||
//
|
||||
// @Router /v1/creator/settings [get]
|
||||
// @Router /t/:tenantCode/v1/creator/settings [get]
|
||||
// @Summary Get settings
|
||||
// @Description Get channel settings
|
||||
// @Tags CreatorCenter
|
||||
@@ -180,12 +189,13 @@ func (c *Creator) Refund(ctx fiber.Ctx, user *models.User, id int64, form *dto.R
|
||||
// @Success 200 {object} dto.Settings
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (c *Creator) GetSettings(ctx fiber.Ctx, user *models.User) (*dto.Settings, error) {
|
||||
return services.Creator.GetSettings(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.GetSettings(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Update channel settings
|
||||
//
|
||||
// @Router /v1/creator/settings [put]
|
||||
// @Router /t/:tenantCode/v1/creator/settings [put]
|
||||
// @Summary Update settings
|
||||
// @Description Update channel settings
|
||||
// @Tags CreatorCenter
|
||||
@@ -196,12 +206,13 @@ func (c *Creator) GetSettings(ctx fiber.Ctx, user *models.User) (*dto.Settings,
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Creator) UpdateSettings(ctx fiber.Ctx, user *models.User, form *dto.Settings) error {
|
||||
return services.Creator.UpdateSettings(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.UpdateSettings(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// List payout accounts
|
||||
//
|
||||
// @Router /v1/creator/payout-accounts [get]
|
||||
// @Router /t/:tenantCode/v1/creator/payout-accounts [get]
|
||||
// @Summary List payout accounts
|
||||
// @Description List payout accounts
|
||||
// @Tags CreatorCenter
|
||||
@@ -210,12 +221,13 @@ func (c *Creator) UpdateSettings(ctx fiber.Ctx, user *models.User, form *dto.Set
|
||||
// @Success 200 {array} dto.PayoutAccount
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (c *Creator) ListPayoutAccounts(ctx fiber.Ctx, user *models.User) ([]dto.PayoutAccount, error) {
|
||||
return services.Creator.ListPayoutAccounts(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.ListPayoutAccounts(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Add payout account
|
||||
//
|
||||
// @Router /v1/creator/payout-accounts [post]
|
||||
// @Router /t/:tenantCode/v1/creator/payout-accounts [post]
|
||||
// @Summary Add payout account
|
||||
// @Description Add payout account
|
||||
// @Tags CreatorCenter
|
||||
@@ -226,12 +238,13 @@ func (c *Creator) ListPayoutAccounts(ctx fiber.Ctx, user *models.User) ([]dto.Pa
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Creator) AddPayoutAccount(ctx fiber.Ctx, user *models.User, form *dto.PayoutAccount) error {
|
||||
return services.Creator.AddPayoutAccount(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.AddPayoutAccount(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// Remove payout account
|
||||
//
|
||||
// @Router /v1/creator/payout-accounts [delete]
|
||||
// @Router /t/:tenantCode/v1/creator/payout-accounts [delete]
|
||||
// @Summary Remove payout account
|
||||
// @Description Remove payout account
|
||||
// @Tags CreatorCenter
|
||||
@@ -242,12 +255,13 @@ func (c *Creator) AddPayoutAccount(ctx fiber.Ctx, user *models.User, form *dto.P
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id query
|
||||
func (c *Creator) RemovePayoutAccount(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
return services.Creator.RemovePayoutAccount(ctx, user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.RemovePayoutAccount(ctx, tenantID, user.ID, id)
|
||||
}
|
||||
|
||||
// Request withdrawal
|
||||
//
|
||||
// @Router /v1/creator/withdraw [post]
|
||||
// @Router /t/:tenantCode/v1/creator/withdraw [post]
|
||||
// @Summary Request withdrawal
|
||||
// @Description Request withdrawal
|
||||
// @Tags CreatorCenter
|
||||
@@ -258,5 +272,6 @@ func (c *Creator) RemovePayoutAccount(ctx fiber.Ctx, user *models.User, id int64
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (c *Creator) Withdraw(ctx fiber.Ctx, user *models.User, form *dto.WithdrawForm) error {
|
||||
return services.Creator.Withdraw(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Creator.Withdraw(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
26
backend/app/http/v1/helpers.go
Normal file
26
backend/app/http/v1/helpers.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func getUserID(ctx fiber.Ctx) int64 {
|
||||
if u := ctx.Locals(consts.CtxKeyUser); u != nil {
|
||||
if user, ok := u.(*models.User); ok {
|
||||
return user.ID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func getTenantID(ctx fiber.Ctx) int64 {
|
||||
if t := ctx.Locals(consts.CtxKeyTenant); t != nil {
|
||||
if tenant, ok := t.(*models.Tenant); ok {
|
||||
return tenant.ID
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -50,368 +50,368 @@ func (r *Routes) Name() string {
|
||||
// Each route is registered with its corresponding controller action and parameter bindings.
|
||||
func (r *Routes) Register(router fiber.Router) {
|
||||
// Register routes for controller: Common
|
||||
r.log.Debugf("Registering route: Delete /v1/media-assets/:id<int> -> common.DeleteMediaAsset")
|
||||
router.Delete("/v1/media-assets/:id<int>"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/media-assets/:id<int> -> common.DeleteMediaAsset")
|
||||
router.Delete("/t/:tenantCode/v1/media-assets/:id<int>"[len(r.Path()):], Func2(
|
||||
r.common.DeleteMediaAsset,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/upload/:uploadId -> common.AbortUpload")
|
||||
router.Delete("/v1/upload/:uploadId"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/upload/:uploadId -> common.AbortUpload")
|
||||
router.Delete("/t/:tenantCode/v1/upload/:uploadId"[len(r.Path()):], Func2(
|
||||
r.common.AbortUpload,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[string]("uploadId"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/common/options -> common.GetOptions")
|
||||
router.Get("/v1/common/options"[len(r.Path()):], DataFunc0(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/common/options -> common.GetOptions")
|
||||
router.Get("/t/:tenantCode/v1/common/options"[len(r.Path()):], DataFunc0(
|
||||
r.common.GetOptions,
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/upload/check -> common.CheckHash")
|
||||
router.Get("/v1/upload/check"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/upload/check -> common.CheckHash")
|
||||
router.Get("/t/:tenantCode/v1/upload/check"[len(r.Path()):], DataFunc2(
|
||||
r.common.CheckHash,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[string]("hash"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/upload -> common.Upload")
|
||||
router.Post("/v1/upload"[len(r.Path()):], DataFunc3(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/upload -> common.Upload")
|
||||
router.Post("/t/:tenantCode/v1/upload"[len(r.Path()):], DataFunc3(
|
||||
r.common.Upload,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
File[multipart.FileHeader]("file"),
|
||||
Body[dto.UploadForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/upload/complete -> common.CompleteUpload")
|
||||
router.Post("/v1/upload/complete"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/upload/complete -> common.CompleteUpload")
|
||||
router.Post("/t/:tenantCode/v1/upload/complete"[len(r.Path()):], DataFunc2(
|
||||
r.common.CompleteUpload,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.UploadCompleteForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/upload/init -> common.InitUpload")
|
||||
router.Post("/v1/upload/init"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/upload/init -> common.InitUpload")
|
||||
router.Post("/t/:tenantCode/v1/upload/init"[len(r.Path()):], DataFunc2(
|
||||
r.common.InitUpload,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.UploadInitForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/upload/part -> common.UploadPart")
|
||||
router.Post("/v1/upload/part"[len(r.Path()):], Func3(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/upload/part -> common.UploadPart")
|
||||
router.Post("/t/:tenantCode/v1/upload/part"[len(r.Path()):], Func3(
|
||||
r.common.UploadPart,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
File[multipart.FileHeader]("file"),
|
||||
Body[dto.UploadPartForm]("form"),
|
||||
))
|
||||
// Register routes for controller: Content
|
||||
r.log.Debugf("Registering route: Delete /v1/contents/:id<int>/favorite -> content.RemoveFavorite")
|
||||
router.Delete("/v1/contents/:id<int>/favorite"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/contents/:id<int>/favorite -> content.RemoveFavorite")
|
||||
router.Delete("/t/:tenantCode/v1/contents/:id<int>/favorite"[len(r.Path()):], Func1(
|
||||
r.content.RemoveFavorite,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/contents/:id<int>/like -> content.RemoveLike")
|
||||
router.Delete("/v1/contents/:id<int>/like"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/contents/:id<int>/like -> content.RemoveLike")
|
||||
router.Delete("/t/:tenantCode/v1/contents/:id<int>/like"[len(r.Path()):], Func1(
|
||||
r.content.RemoveLike,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/contents -> content.List")
|
||||
router.Get("/v1/contents"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/contents -> content.List")
|
||||
router.Get("/t/:tenantCode/v1/contents"[len(r.Path()):], DataFunc1(
|
||||
r.content.List,
|
||||
Query[dto.ContentListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/contents/:id<int> -> content.Get")
|
||||
router.Get("/v1/contents/:id<int>"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/contents/:id<int> -> content.Get")
|
||||
router.Get("/t/:tenantCode/v1/contents/:id<int>"[len(r.Path()):], DataFunc1(
|
||||
r.content.Get,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/contents/:id<int>/comments -> content.ListComments")
|
||||
router.Get("/v1/contents/:id<int>/comments"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/contents/:id<int>/comments -> content.ListComments")
|
||||
router.Get("/t/:tenantCode/v1/contents/:id<int>/comments"[len(r.Path()):], DataFunc2(
|
||||
r.content.ListComments,
|
||||
PathParam[int64]("id"),
|
||||
QueryParam[int]("page"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/topics -> content.ListTopics")
|
||||
router.Get("/v1/topics"[len(r.Path()):], DataFunc0(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/topics -> content.ListTopics")
|
||||
router.Get("/t/:tenantCode/v1/topics"[len(r.Path()):], DataFunc0(
|
||||
r.content.ListTopics,
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/comments/:id<int>/like -> content.LikeComment")
|
||||
router.Post("/v1/comments/:id<int>/like"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/comments/:id<int>/like -> content.LikeComment")
|
||||
router.Post("/t/:tenantCode/v1/comments/:id<int>/like"[len(r.Path()):], Func1(
|
||||
r.content.LikeComment,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/contents/:id<int>/comments -> content.CreateComment")
|
||||
router.Post("/v1/contents/:id<int>/comments"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/contents/:id<int>/comments -> content.CreateComment")
|
||||
router.Post("/t/:tenantCode/v1/contents/:id<int>/comments"[len(r.Path()):], Func2(
|
||||
r.content.CreateComment,
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.CommentCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/contents/:id<int>/favorite -> content.AddFavorite")
|
||||
router.Post("/v1/contents/:id<int>/favorite"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/contents/:id<int>/favorite -> content.AddFavorite")
|
||||
router.Post("/t/:tenantCode/v1/contents/:id<int>/favorite"[len(r.Path()):], Func1(
|
||||
r.content.AddFavorite,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/contents/:id<int>/like -> content.AddLike")
|
||||
router.Post("/v1/contents/:id<int>/like"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/contents/:id<int>/like -> content.AddLike")
|
||||
router.Post("/t/:tenantCode/v1/contents/:id<int>/like"[len(r.Path()):], Func1(
|
||||
r.content.AddLike,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
// Register routes for controller: Creator
|
||||
r.log.Debugf("Registering route: Delete /v1/creator/contents/:id<int> -> creator.DeleteContent")
|
||||
router.Delete("/v1/creator/contents/:id<int>"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/creator/contents/:id<int> -> creator.DeleteContent")
|
||||
router.Delete("/t/:tenantCode/v1/creator/contents/:id<int>"[len(r.Path()):], Func2(
|
||||
r.creator.DeleteContent,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/creator/payout-accounts -> creator.RemovePayoutAccount")
|
||||
router.Delete("/v1/creator/payout-accounts"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/creator/payout-accounts -> creator.RemovePayoutAccount")
|
||||
router.Delete("/t/:tenantCode/v1/creator/payout-accounts"[len(r.Path()):], Func2(
|
||||
r.creator.RemovePayoutAccount,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creator/contents -> creator.ListContents")
|
||||
router.Get("/v1/creator/contents"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/contents -> creator.ListContents")
|
||||
router.Get("/t/:tenantCode/v1/creator/contents"[len(r.Path()):], DataFunc2(
|
||||
r.creator.ListContents,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Query[dto.CreatorContentListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creator/contents/:id<int> -> creator.GetContent")
|
||||
router.Get("/v1/creator/contents/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/contents/:id<int> -> creator.GetContent")
|
||||
router.Get("/t/:tenantCode/v1/creator/contents/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.creator.GetContent,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creator/dashboard -> creator.Dashboard")
|
||||
router.Get("/v1/creator/dashboard"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/dashboard -> creator.Dashboard")
|
||||
router.Get("/t/:tenantCode/v1/creator/dashboard"[len(r.Path()):], DataFunc1(
|
||||
r.creator.Dashboard,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creator/orders -> creator.ListOrders")
|
||||
router.Get("/v1/creator/orders"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/orders -> creator.ListOrders")
|
||||
router.Get("/t/:tenantCode/v1/creator/orders"[len(r.Path()):], DataFunc2(
|
||||
r.creator.ListOrders,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Query[dto.CreatorOrderListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creator/payout-accounts -> creator.ListPayoutAccounts")
|
||||
router.Get("/v1/creator/payout-accounts"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/payout-accounts -> creator.ListPayoutAccounts")
|
||||
router.Get("/t/:tenantCode/v1/creator/payout-accounts"[len(r.Path()):], DataFunc1(
|
||||
r.creator.ListPayoutAccounts,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creator/settings -> creator.GetSettings")
|
||||
router.Get("/v1/creator/settings"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/settings -> creator.GetSettings")
|
||||
router.Get("/t/:tenantCode/v1/creator/settings"[len(r.Path()):], DataFunc1(
|
||||
r.creator.GetSettings,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/creator/apply -> creator.Apply")
|
||||
router.Post("/v1/creator/apply"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/apply -> creator.Apply")
|
||||
router.Post("/t/:tenantCode/v1/creator/apply"[len(r.Path()):], Func2(
|
||||
r.creator.Apply,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.ApplyForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/creator/contents -> creator.CreateContent")
|
||||
router.Post("/v1/creator/contents"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/contents -> creator.CreateContent")
|
||||
router.Post("/t/:tenantCode/v1/creator/contents"[len(r.Path()):], Func2(
|
||||
r.creator.CreateContent,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.ContentCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/creator/orders/:id<int>/refund -> creator.Refund")
|
||||
router.Post("/v1/creator/orders/:id<int>/refund"[len(r.Path()):], Func3(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/orders/:id<int>/refund -> creator.Refund")
|
||||
router.Post("/t/:tenantCode/v1/creator/orders/:id<int>/refund"[len(r.Path()):], Func3(
|
||||
r.creator.Refund,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.RefundForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/creator/payout-accounts -> creator.AddPayoutAccount")
|
||||
router.Post("/v1/creator/payout-accounts"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/payout-accounts -> creator.AddPayoutAccount")
|
||||
router.Post("/t/:tenantCode/v1/creator/payout-accounts"[len(r.Path()):], Func2(
|
||||
r.creator.AddPayoutAccount,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.PayoutAccount]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/creator/withdraw -> creator.Withdraw")
|
||||
router.Post("/v1/creator/withdraw"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/withdraw -> creator.Withdraw")
|
||||
router.Post("/t/:tenantCode/v1/creator/withdraw"[len(r.Path()):], Func2(
|
||||
r.creator.Withdraw,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.WithdrawForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/creator/contents/:id<int> -> creator.UpdateContent")
|
||||
router.Put("/v1/creator/contents/:id<int>"[len(r.Path()):], Func3(
|
||||
r.log.Debugf("Registering route: Put /t/:tenantCode/v1/creator/contents/:id<int> -> creator.UpdateContent")
|
||||
router.Put("/t/:tenantCode/v1/creator/contents/:id<int>"[len(r.Path()):], Func3(
|
||||
r.creator.UpdateContent,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.ContentUpdateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/creator/settings -> creator.UpdateSettings")
|
||||
router.Put("/v1/creator/settings"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Put /t/:tenantCode/v1/creator/settings -> creator.UpdateSettings")
|
||||
router.Put("/t/:tenantCode/v1/creator/settings"[len(r.Path()):], Func2(
|
||||
r.creator.UpdateSettings,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.Settings]("form"),
|
||||
))
|
||||
// Register routes for controller: Storage
|
||||
r.log.Debugf("Registering route: Get /v1/storage/* -> storage.Download")
|
||||
router.Get("/v1/storage/*"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/storage/* -> storage.Download")
|
||||
router.Get("/t/:tenantCode/v1/storage/*"[len(r.Path()):], Func2(
|
||||
r.storage.Download,
|
||||
QueryParam[string]("expires"),
|
||||
QueryParam[string]("sign"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/storage/* -> storage.Upload")
|
||||
router.Put("/v1/storage/*"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Put /t/:tenantCode/v1/storage/* -> storage.Upload")
|
||||
router.Put("/t/:tenantCode/v1/storage/*"[len(r.Path()):], DataFunc2(
|
||||
r.storage.Upload,
|
||||
QueryParam[string]("expires"),
|
||||
QueryParam[string]("sign"),
|
||||
))
|
||||
// Register routes for controller: Tenant
|
||||
r.log.Debugf("Registering route: Delete /v1/tenants/:id<int>/follow -> tenant.Unfollow")
|
||||
router.Delete("/v1/tenants/:id<int>/follow"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/tenants/:id<int>/follow -> tenant.Unfollow")
|
||||
router.Delete("/t/:tenantCode/v1/tenants/:id<int>/follow"[len(r.Path()):], Func2(
|
||||
r.tenant.Unfollow,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/creators/:id<int>/contents -> tenant.ListContents")
|
||||
router.Get("/v1/creators/:id<int>/contents"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creators/:id<int>/contents -> tenant.ListContents")
|
||||
router.Get("/t/:tenantCode/v1/creators/:id<int>/contents"[len(r.Path()):], DataFunc2(
|
||||
r.tenant.ListContents,
|
||||
PathParam[int64]("id"),
|
||||
Query[dto.ContentListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/tenants -> tenant.List")
|
||||
router.Get("/v1/tenants"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/tenants -> tenant.List")
|
||||
router.Get("/t/:tenantCode/v1/tenants"[len(r.Path()):], DataFunc1(
|
||||
r.tenant.List,
|
||||
Query[dto.TenantListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/tenants/:id<int> -> tenant.Get")
|
||||
router.Get("/v1/tenants/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/tenants/:id<int> -> tenant.Get")
|
||||
router.Get("/t/:tenantCode/v1/tenants/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.tenant.Get,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/tenants/:id<int>/follow -> tenant.Follow")
|
||||
router.Post("/v1/tenants/:id<int>/follow"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/tenants/:id<int>/follow -> tenant.Follow")
|
||||
router.Post("/t/:tenantCode/v1/tenants/:id<int>/follow"[len(r.Path()):], Func2(
|
||||
r.tenant.Follow,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
// Register routes for controller: Transaction
|
||||
r.log.Debugf("Registering route: Get /v1/orders/:id<int>/status -> transaction.Status")
|
||||
router.Get("/v1/orders/:id<int>/status"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/orders/:id<int>/status -> transaction.Status")
|
||||
router.Get("/t/:tenantCode/v1/orders/:id<int>/status"[len(r.Path()):], DataFunc1(
|
||||
r.transaction.Status,
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/orders -> transaction.Create")
|
||||
router.Post("/v1/orders"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/orders -> transaction.Create")
|
||||
router.Post("/t/:tenantCode/v1/orders"[len(r.Path()):], DataFunc2(
|
||||
r.transaction.Create,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.OrderCreateForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/orders/:id<int>/pay -> transaction.Pay")
|
||||
router.Post("/v1/orders/:id<int>/pay"[len(r.Path()):], DataFunc3(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/orders/:id<int>/pay -> transaction.Pay")
|
||||
router.Post("/t/:tenantCode/v1/orders/:id<int>/pay"[len(r.Path()):], DataFunc3(
|
||||
r.transaction.Pay,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.OrderPayForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/webhook/payment/notify -> transaction.Webhook")
|
||||
router.Post("/v1/webhook/payment/notify"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/webhook/payment/notify -> transaction.Webhook")
|
||||
router.Post("/t/:tenantCode/v1/webhook/payment/notify"[len(r.Path()):], DataFunc1(
|
||||
r.transaction.Webhook,
|
||||
Body[WebhookForm]("form"),
|
||||
))
|
||||
// Register routes for controller: User
|
||||
r.log.Debugf("Registering route: Delete /v1/me/favorites/:contentId<int> -> user.RemoveFavorite")
|
||||
router.Delete("/v1/me/favorites/:contentId<int>"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/me/favorites/:contentId<int> -> user.RemoveFavorite")
|
||||
router.Delete("/t/:tenantCode/v1/me/favorites/:contentId<int>"[len(r.Path()):], Func2(
|
||||
r.user.RemoveFavorite,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("contentId"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Delete /v1/me/likes/:contentId<int> -> user.RemoveLike")
|
||||
router.Delete("/v1/me/likes/:contentId<int>"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Delete /t/:tenantCode/v1/me/likes/:contentId<int> -> user.RemoveLike")
|
||||
router.Delete("/t/:tenantCode/v1/me/likes/:contentId<int>"[len(r.Path()):], Func2(
|
||||
r.user.RemoveLike,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("contentId"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me -> user.Me")
|
||||
router.Get("/v1/me"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me -> user.Me")
|
||||
router.Get("/t/:tenantCode/v1/me"[len(r.Path()):], DataFunc1(
|
||||
r.user.Me,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/coupons -> user.MyCoupons")
|
||||
router.Get("/v1/me/coupons"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/coupons -> user.MyCoupons")
|
||||
router.Get("/t/:tenantCode/v1/me/coupons"[len(r.Path()):], DataFunc2(
|
||||
r.user.MyCoupons,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[string]("status"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/favorites -> user.Favorites")
|
||||
router.Get("/v1/me/favorites"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/favorites -> user.Favorites")
|
||||
router.Get("/t/:tenantCode/v1/me/favorites"[len(r.Path()):], DataFunc1(
|
||||
r.user.Favorites,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/following -> user.Following")
|
||||
router.Get("/v1/me/following"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/following -> user.Following")
|
||||
router.Get("/t/:tenantCode/v1/me/following"[len(r.Path()):], DataFunc1(
|
||||
r.user.Following,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/library -> user.Library")
|
||||
router.Get("/v1/me/library"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/library -> user.Library")
|
||||
router.Get("/t/:tenantCode/v1/me/library"[len(r.Path()):], DataFunc1(
|
||||
r.user.Library,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/likes -> user.Likes")
|
||||
router.Get("/v1/me/likes"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/likes -> user.Likes")
|
||||
router.Get("/t/:tenantCode/v1/me/likes"[len(r.Path()):], DataFunc1(
|
||||
r.user.Likes,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/notifications -> user.Notifications")
|
||||
router.Get("/v1/me/notifications"[len(r.Path()):], DataFunc3(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/notifications -> user.Notifications")
|
||||
router.Get("/t/:tenantCode/v1/me/notifications"[len(r.Path()):], DataFunc3(
|
||||
r.user.Notifications,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[string]("type"),
|
||||
QueryParam[int]("page"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/orders -> user.ListOrders")
|
||||
router.Get("/v1/me/orders"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/orders -> user.ListOrders")
|
||||
router.Get("/t/:tenantCode/v1/me/orders"[len(r.Path()):], DataFunc2(
|
||||
r.user.ListOrders,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[string]("status"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/orders/:id<int> -> user.GetOrder")
|
||||
router.Get("/v1/me/orders/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/orders/:id<int> -> user.GetOrder")
|
||||
router.Get("/t/:tenantCode/v1/me/orders/:id<int>"[len(r.Path()):], DataFunc2(
|
||||
r.user.GetOrder,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Get /v1/me/wallet -> user.Wallet")
|
||||
router.Get("/v1/me/wallet"[len(r.Path()):], DataFunc1(
|
||||
r.log.Debugf("Registering route: Get /t/:tenantCode/v1/me/wallet -> user.Wallet")
|
||||
router.Get("/t/:tenantCode/v1/me/wallet"[len(r.Path()):], DataFunc1(
|
||||
r.user.Wallet,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/me/favorites -> user.AddFavorite")
|
||||
router.Post("/v1/me/favorites"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/me/favorites -> user.AddFavorite")
|
||||
router.Post("/t/:tenantCode/v1/me/favorites"[len(r.Path()):], Func2(
|
||||
r.user.AddFavorite,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[int64]("contentId"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/me/likes -> user.AddLike")
|
||||
router.Post("/v1/me/likes"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/me/likes -> user.AddLike")
|
||||
router.Post("/t/:tenantCode/v1/me/likes"[len(r.Path()):], Func2(
|
||||
r.user.AddLike,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
QueryParam[int64]("contentId"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/me/notifications/:id<int>/read -> user.MarkNotificationRead")
|
||||
router.Post("/v1/me/notifications/:id<int>/read"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/me/notifications/:id<int>/read -> user.MarkNotificationRead")
|
||||
router.Post("/t/:tenantCode/v1/me/notifications/:id<int>/read"[len(r.Path()):], Func2(
|
||||
r.user.MarkNotificationRead,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/me/notifications/read-all -> user.MarkAllNotificationsRead")
|
||||
router.Post("/v1/me/notifications/read-all"[len(r.Path()):], Func1(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/me/notifications/read-all -> user.MarkAllNotificationsRead")
|
||||
router.Post("/t/:tenantCode/v1/me/notifications/read-all"[len(r.Path()):], Func1(
|
||||
r.user.MarkAllNotificationsRead,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/me/realname -> user.RealName")
|
||||
router.Post("/v1/me/realname"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/me/realname -> user.RealName")
|
||||
router.Post("/t/:tenantCode/v1/me/realname"[len(r.Path()):], Func2(
|
||||
r.user.RealName,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.RealNameForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /v1/me/wallet/recharge -> user.Recharge")
|
||||
router.Post("/v1/me/wallet/recharge"[len(r.Path()):], DataFunc2(
|
||||
r.log.Debugf("Registering route: Post /t/:tenantCode/v1/me/wallet/recharge -> user.Recharge")
|
||||
router.Post("/t/:tenantCode/v1/me/wallet/recharge"[len(r.Path()):], DataFunc2(
|
||||
r.user.Recharge,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.RechargeForm]("form"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Put /v1/me -> user.Update")
|
||||
router.Put("/v1/me"[len(r.Path()):], Func2(
|
||||
r.log.Debugf("Registering route: Put /t/:tenantCode/v1/me -> user.Update")
|
||||
router.Put("/t/:tenantCode/v1/me"[len(r.Path()):], Func2(
|
||||
r.user.Update,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
Body[dto.UserUpdate]("form"),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package v1
|
||||
|
||||
func (r *Routes) Path() string {
|
||||
return "/v1"
|
||||
return "/t/:tenantCode/v1"
|
||||
}
|
||||
|
||||
func (r *Routes) Middlewares() []any {
|
||||
return []any{
|
||||
r.middlewares.TenantResolver,
|
||||
r.middlewares.Auth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ type Storage struct {
|
||||
|
||||
// Upload file
|
||||
//
|
||||
// @Router /v1/storage/* [put]
|
||||
// @Router /t/:tenantCode/v1/storage/* [put]
|
||||
// @Summary Upload file
|
||||
// @Tags Storage
|
||||
// @Accept octet-stream
|
||||
@@ -58,7 +58,7 @@ func (s *Storage) Upload(ctx fiber.Ctx, expires, sign string) (string, error) {
|
||||
|
||||
// Download file
|
||||
//
|
||||
// @Router /v1/storage/* [get]
|
||||
// @Router /t/:tenantCode/v1/storage/* [get]
|
||||
// @Summary Download file
|
||||
// @Tags Storage
|
||||
// @Accept json
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
@@ -14,7 +15,7 @@ type Tenant struct{}
|
||||
|
||||
// List creator contents
|
||||
//
|
||||
// @Router /v1/creators/:id<int>/contents [get]
|
||||
// @Router /t/:tenantCode/v1/creators/:id<int>/contents [get]
|
||||
// @Summary List creator contents
|
||||
// @Description List contents of a specific creator
|
||||
// @Tags TenantPublic
|
||||
@@ -27,16 +28,20 @@ type Tenant struct{}
|
||||
// @Bind id path
|
||||
// @Bind filter query
|
||||
func (t *Tenant) ListContents(ctx fiber.Ctx, id int64, filter *dto.ContentListFilter) (*requests.Pager, error) {
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
if filter == nil {
|
||||
filter = &dto.ContentListFilter{}
|
||||
}
|
||||
filter.TenantID = &id
|
||||
return services.Content.List(ctx, filter)
|
||||
filter.TenantID = &tenantID
|
||||
return services.Content.List(ctx, tenantID, filter)
|
||||
}
|
||||
|
||||
// List tenants (search)
|
||||
//
|
||||
// @Router /v1/tenants [get]
|
||||
// @Router /t/:tenantCode/v1/tenants [get]
|
||||
// @Summary List tenants
|
||||
// @Description Search tenants
|
||||
// @Tags TenantPublic
|
||||
@@ -48,12 +53,13 @@ func (t *Tenant) ListContents(ctx fiber.Ctx, id int64, filter *dto.ContentListFi
|
||||
// @Success 200 {object} requests.Pager
|
||||
// @Bind filter query
|
||||
func (t *Tenant) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) {
|
||||
return services.Tenant.List(ctx, filter)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Tenant.List(ctx, tenantID, filter)
|
||||
}
|
||||
|
||||
// Get tenant public profile
|
||||
//
|
||||
// @Router /v1/tenants/:id<int> [get]
|
||||
// @Router /t/:tenantCode/v1/tenants/:id<int> [get]
|
||||
// @Summary Get tenant profile
|
||||
// @Description Get tenant public profile
|
||||
// @Tags TenantPublic
|
||||
@@ -68,12 +74,16 @@ func (t *Tenant) Get(ctx fiber.Ctx, user *models.User, id int64) (*dto.TenantPro
|
||||
if user != nil {
|
||||
uid = user.ID
|
||||
}
|
||||
return services.Tenant.GetPublicProfile(ctx, uid, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
return services.Tenant.GetPublicProfile(ctx, tenantID, uid)
|
||||
}
|
||||
|
||||
// Follow a tenant
|
||||
//
|
||||
// @Router /v1/tenants/:id<int>/follow [post]
|
||||
// @Router /t/:tenantCode/v1/tenants/:id<int>/follow [post]
|
||||
// @Summary Follow tenant
|
||||
// @Description Follow a tenant
|
||||
// @Tags TenantPublic
|
||||
@@ -84,12 +94,16 @@ func (t *Tenant) Get(ctx fiber.Ctx, user *models.User, id int64) (*dto.TenantPro
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (t *Tenant) Follow(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
return services.Tenant.Follow(ctx, user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
return services.Tenant.Follow(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Unfollow a tenant
|
||||
//
|
||||
// @Router /v1/tenants/:id<int>/follow [delete]
|
||||
// @Router /t/:tenantCode/v1/tenants/:id<int>/follow [delete]
|
||||
// @Summary Unfollow tenant
|
||||
// @Description Unfollow a tenant
|
||||
// @Tags TenantPublic
|
||||
@@ -100,5 +114,9 @@ func (t *Tenant) Follow(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (t *Tenant) Unfollow(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
return services.Tenant.Unfollow(ctx, user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
if tenantID > 0 && id != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
return services.Tenant.Unfollow(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type Transaction struct{}
|
||||
|
||||
// Create Order
|
||||
//
|
||||
// @Router /v1/orders [post]
|
||||
// @Router /t/:tenantCode/v1/orders [post]
|
||||
// @Summary Create Order
|
||||
// @Description Create Order
|
||||
// @Tags Transaction
|
||||
@@ -28,12 +28,13 @@ func (t *Transaction) Create(
|
||||
user *models.User,
|
||||
form *dto.OrderCreateForm,
|
||||
) (*dto.OrderCreateResponse, error) {
|
||||
return services.Order.Create(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Order.Create(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// Pay for order
|
||||
//
|
||||
// @Router /v1/orders/:id<int>/pay [post]
|
||||
// @Router /t/:tenantCode/v1/orders/:id<int>/pay [post]
|
||||
// @Summary Pay for order
|
||||
// @Description Pay for order
|
||||
// @Tags Transaction
|
||||
@@ -51,12 +52,13 @@ func (t *Transaction) Pay(
|
||||
id int64,
|
||||
form *dto.OrderPayForm,
|
||||
) (*dto.OrderPayResponse, error) {
|
||||
return services.Order.Pay(ctx, user.ID, id, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Order.Pay(ctx, tenantID, user.ID, id, form)
|
||||
}
|
||||
|
||||
// Check order payment status
|
||||
//
|
||||
// @Router /v1/orders/:id<int>/status [get]
|
||||
// @Router /t/:tenantCode/v1/orders/:id<int>/status [get]
|
||||
// @Summary Check order status
|
||||
// @Description Check order payment status
|
||||
// @Tags Transaction
|
||||
@@ -66,7 +68,8 @@ func (t *Transaction) Pay(
|
||||
// @Success 200 {object} dto.OrderStatusResponse
|
||||
// @Bind id path
|
||||
func (t *Transaction) Status(ctx fiber.Ctx, id int64) (*dto.OrderStatusResponse, error) {
|
||||
return services.Order.Status(ctx, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Order.Status(ctx, tenantID, id)
|
||||
}
|
||||
|
||||
type WebhookForm struct {
|
||||
@@ -76,7 +79,7 @@ type WebhookForm struct {
|
||||
|
||||
// Payment Webhook
|
||||
//
|
||||
// @Router /v1/webhook/payment/notify [post]
|
||||
// @Router /t/:tenantCode/v1/webhook/payment/notify [post]
|
||||
// @Summary Payment Webhook
|
||||
// @Description Payment Webhook
|
||||
// @Tags Transaction
|
||||
@@ -86,7 +89,8 @@ type WebhookForm struct {
|
||||
// @Success 200 {string} string "success"
|
||||
// @Bind form body
|
||||
func (t *Transaction) Webhook(ctx fiber.Ctx, form *WebhookForm) (string, error) {
|
||||
err := services.Order.ProcessExternalPayment(ctx, form.OrderID, form.ExternalID)
|
||||
tenantID := getTenantID(ctx)
|
||||
err := services.Order.ProcessExternalPayment(ctx, tenantID, form.OrderID, form.ExternalID)
|
||||
if err != nil {
|
||||
return "fail", err
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ type User struct{}
|
||||
|
||||
// Get current user profile
|
||||
//
|
||||
// @Router /v1/me [get]
|
||||
// @Router /t/:tenantCode/v1/me [get]
|
||||
// @Summary Get user profile
|
||||
// @Description Get current user profile
|
||||
// @Tags UserCenter
|
||||
@@ -30,7 +30,7 @@ func (u *User) Me(ctx fiber.Ctx, user *models.User) (*auth_dto.User, error) {
|
||||
|
||||
// Update user profile
|
||||
//
|
||||
// @Router /v1/me [put]
|
||||
// @Router /t/:tenantCode/v1/me [put]
|
||||
// @Summary Update user profile
|
||||
// @Description Update user profile
|
||||
// @Tags UserCenter
|
||||
@@ -46,7 +46,7 @@ func (u *User) Update(ctx fiber.Ctx, user *models.User, form *dto.UserUpdate) er
|
||||
|
||||
// Submit real-name authentication
|
||||
//
|
||||
// @Router /v1/me/realname [post]
|
||||
// @Router /t/:tenantCode/v1/me/realname [post]
|
||||
// @Summary Realname auth
|
||||
// @Description Submit real-name authentication
|
||||
// @Tags UserCenter
|
||||
@@ -62,7 +62,7 @@ func (u *User) RealName(ctx fiber.Ctx, user *models.User, form *dto.RealNameForm
|
||||
|
||||
// Get wallet balance and transactions
|
||||
//
|
||||
// @Router /v1/me/wallet [get]
|
||||
// @Router /t/:tenantCode/v1/me/wallet [get]
|
||||
// @Summary Get wallet
|
||||
// @Description Get wallet balance and transactions
|
||||
// @Tags UserCenter
|
||||
@@ -71,12 +71,13 @@ func (u *User) RealName(ctx fiber.Ctx, user *models.User, form *dto.RealNameForm
|
||||
// @Success 200 {object} dto.WalletResponse
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (u *User) Wallet(ctx fiber.Ctx, user *models.User) (*dto.WalletResponse, error) {
|
||||
return services.Wallet.GetWallet(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Wallet.GetWallet(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Recharge wallet
|
||||
//
|
||||
// @Router /v1/me/wallet/recharge [post]
|
||||
// @Router /t/:tenantCode/v1/me/wallet/recharge [post]
|
||||
// @Summary Recharge wallet
|
||||
// @Description Recharge wallet
|
||||
// @Tags UserCenter
|
||||
@@ -87,12 +88,13 @@ func (u *User) Wallet(ctx fiber.Ctx, user *models.User) (*dto.WalletResponse, er
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind form body
|
||||
func (u *User) Recharge(ctx fiber.Ctx, user *models.User, form *dto.RechargeForm) (*dto.RechargeResponse, error) {
|
||||
return services.Wallet.Recharge(ctx, user.ID, form)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Wallet.Recharge(ctx, tenantID, user.ID, form)
|
||||
}
|
||||
|
||||
// List user orders
|
||||
//
|
||||
// @Router /v1/me/orders [get]
|
||||
// @Router /t/:tenantCode/v1/me/orders [get]
|
||||
// @Summary List orders
|
||||
// @Description List user orders
|
||||
// @Tags UserCenter
|
||||
@@ -103,12 +105,13 @@ func (u *User) Recharge(ctx fiber.Ctx, user *models.User, form *dto.RechargeForm
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind status query
|
||||
func (u *User) ListOrders(ctx fiber.Ctx, user *models.User, status string) ([]dto.Order, error) {
|
||||
return services.Order.ListUserOrders(ctx, user.ID, status)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Order.ListUserOrders(ctx, tenantID, user.ID, status)
|
||||
}
|
||||
|
||||
// Get user order detail
|
||||
//
|
||||
// @Router /v1/me/orders/:id<int> [get]
|
||||
// @Router /t/:tenantCode/v1/me/orders/:id<int> [get]
|
||||
// @Summary Get order detail
|
||||
// @Description Get user order detail
|
||||
// @Tags UserCenter
|
||||
@@ -119,12 +122,13 @@ func (u *User) ListOrders(ctx fiber.Ctx, user *models.User, status string) ([]dt
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (u *User) GetOrder(ctx fiber.Ctx, user *models.User, id int64) (*dto.Order, error) {
|
||||
return services.Order.GetUserOrder(ctx, user.ID, id)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Order.GetUserOrder(ctx, tenantID, user.ID, id)
|
||||
}
|
||||
|
||||
// Get purchased content
|
||||
//
|
||||
// @Router /v1/me/library [get]
|
||||
// @Router /t/:tenantCode/v1/me/library [get]
|
||||
// @Summary Get library
|
||||
// @Description Get purchased content
|
||||
// @Tags UserCenter
|
||||
@@ -133,12 +137,13 @@ func (u *User) GetOrder(ctx fiber.Ctx, user *models.User, id int64) (*dto.Order,
|
||||
// @Success 200 {array} dto.ContentItem
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (u *User) Library(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) {
|
||||
return services.Content.GetLibrary(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.GetLibrary(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Get favorites
|
||||
//
|
||||
// @Router /v1/me/favorites [get]
|
||||
// @Router /t/:tenantCode/v1/me/favorites [get]
|
||||
// @Summary Get favorites
|
||||
// @Description Get favorites
|
||||
// @Tags UserCenter
|
||||
@@ -147,12 +152,13 @@ func (u *User) Library(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, err
|
||||
// @Success 200 {array} dto.ContentItem
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) {
|
||||
return services.Content.GetFavorites(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.GetFavorites(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Add to favorites
|
||||
//
|
||||
// @Router /v1/me/favorites [post]
|
||||
// @Router /t/:tenantCode/v1/me/favorites [post]
|
||||
// @Summary Add favorite
|
||||
// @Description Add to favorites
|
||||
// @Tags UserCenter
|
||||
@@ -163,12 +169,13 @@ func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, e
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId query
|
||||
func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentId int64) error {
|
||||
return services.Content.AddFavorite(ctx, user.ID, contentId)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.AddFavorite(ctx, tenantID, user.ID, contentId)
|
||||
}
|
||||
|
||||
// Remove from favorites
|
||||
//
|
||||
// @Router /v1/me/favorites/:contentId<int> [delete]
|
||||
// @Router /t/:tenantCode/v1/me/favorites/:contentId<int> [delete]
|
||||
// @Summary Remove favorite
|
||||
// @Description Remove from favorites
|
||||
// @Tags UserCenter
|
||||
@@ -179,12 +186,13 @@ func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentId int64) er
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId path
|
||||
func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentId int64) error {
|
||||
return services.Content.RemoveFavorite(ctx, user.ID, contentId)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.RemoveFavorite(ctx, tenantID, user.ID, contentId)
|
||||
}
|
||||
|
||||
// Get liked contents
|
||||
//
|
||||
// @Router /v1/me/likes [get]
|
||||
// @Router /t/:tenantCode/v1/me/likes [get]
|
||||
// @Summary Get likes
|
||||
// @Description Get liked contents
|
||||
// @Tags UserCenter
|
||||
@@ -193,12 +201,13 @@ func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentId int64)
|
||||
// @Success 200 {array} dto.ContentItem
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error) {
|
||||
return services.Content.GetLikes(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.GetLikes(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Like content
|
||||
//
|
||||
// @Router /v1/me/likes [post]
|
||||
// @Router /t/:tenantCode/v1/me/likes [post]
|
||||
// @Summary Like content
|
||||
// @Description Like content
|
||||
// @Tags UserCenter
|
||||
@@ -209,12 +218,13 @@ func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId query
|
||||
func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentId int64) error {
|
||||
return services.Content.AddLike(ctx, user.ID, contentId)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.AddLike(ctx, tenantID, user.ID, contentId)
|
||||
}
|
||||
|
||||
// Unlike content
|
||||
//
|
||||
// @Router /v1/me/likes/:contentId<int> [delete]
|
||||
// @Router /t/:tenantCode/v1/me/likes/:contentId<int> [delete]
|
||||
// @Summary Unlike content
|
||||
// @Description Unlike content
|
||||
// @Tags UserCenter
|
||||
@@ -225,12 +235,13 @@ func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentId int64) error
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind contentId path
|
||||
func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentId int64) error {
|
||||
return services.Content.RemoveLike(ctx, user.ID, contentId)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Content.RemoveLike(ctx, tenantID, user.ID, contentId)
|
||||
}
|
||||
|
||||
// Get following tenants
|
||||
//
|
||||
// @Router /v1/me/following [get]
|
||||
// @Router /t/:tenantCode/v1/me/following [get]
|
||||
// @Summary Get following
|
||||
// @Description Get following tenants
|
||||
// @Tags UserCenter
|
||||
@@ -239,12 +250,13 @@ func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentId int64) err
|
||||
// @Success 200 {array} dto.TenantProfile
|
||||
// @Bind user local key(__ctx_user)
|
||||
func (u *User) Following(ctx fiber.Ctx, user *models.User) ([]dto.TenantProfile, error) {
|
||||
return services.Tenant.ListFollowed(ctx, user.ID)
|
||||
tenantID := getTenantID(ctx)
|
||||
return services.Tenant.ListFollowed(ctx, tenantID, user.ID)
|
||||
}
|
||||
|
||||
// Get notifications
|
||||
//
|
||||
// @Router /v1/me/notifications [get]
|
||||
// @Router /t/:tenantCode/v1/me/notifications [get]
|
||||
// @Summary Get notifications
|
||||
// @Description Get notifications
|
||||
// @Tags UserCenter
|
||||
@@ -262,7 +274,7 @@ func (u *User) Notifications(ctx fiber.Ctx, user *models.User, typeArg string, p
|
||||
|
||||
// Mark notification as read
|
||||
//
|
||||
// @Router /v1/me/notifications/:id<int>/read [post]
|
||||
// @Router /t/:tenantCode/v1/me/notifications/:id<int>/read [post]
|
||||
// @Summary Mark as read
|
||||
// @Tags UserCenter
|
||||
// @Accept json
|
||||
@@ -277,7 +289,7 @@ func (u *User) MarkNotificationRead(ctx fiber.Ctx, user *models.User, id int64)
|
||||
|
||||
// Mark all notifications as read
|
||||
//
|
||||
// @Router /v1/me/notifications/read-all [post]
|
||||
// @Router /t/:tenantCode/v1/me/notifications/read-all [post]
|
||||
// @Summary Mark all as read
|
||||
// @Tags UserCenter
|
||||
// @Accept json
|
||||
@@ -290,7 +302,7 @@ func (u *User) MarkAllNotificationsRead(ctx fiber.Ctx, user *models.User) error
|
||||
|
||||
// List my coupons
|
||||
//
|
||||
// @Router /v1/me/coupons [get]
|
||||
// @Router /t/:tenantCode/v1/me/coupons [get]
|
||||
// @Summary List coupons
|
||||
// @Description List my coupons
|
||||
// @Tags UserCenter
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
"quyun/v2/providers/jwt"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"go.ipao.vip/gen/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Middlewares provides reusable Fiber middlewares shared across modules.
|
||||
@@ -49,13 +52,18 @@ func (m *Middlewares) Auth(ctx fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// Set Context
|
||||
ctx.Locals("__ctx_user", user)
|
||||
if claims.TenantID > 0 {
|
||||
tenant, err := services.Tenant.GetModelByID(ctx, claims.TenantID)
|
||||
ctx.Locals(consts.CtxKeyUser, user)
|
||||
|
||||
if tenant := ctx.Locals(consts.CtxKeyTenant); tenant != nil {
|
||||
if model, ok := tenant.(*models.Tenant); ok && claims.TenantID > 0 && model.ID != claims.TenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
} else if claims.TenantID > 0 {
|
||||
tenantModel, err := services.Tenant.GetModelByID(ctx, claims.TenantID)
|
||||
if err != nil {
|
||||
return errorx.ErrUnauthorized.WithCause(err).WithMsg("TenantNotFound")
|
||||
}
|
||||
ctx.Locals("__ctx_tenant", tenant)
|
||||
ctx.Locals(consts.CtxKeyTenant, tenantModel)
|
||||
}
|
||||
|
||||
return ctx.Next()
|
||||
@@ -84,7 +92,26 @@ func (m *Middlewares) SuperAuth(ctx fiber.Ctx) error {
|
||||
return errorx.ErrForbidden.WithMsg("无权限访问")
|
||||
}
|
||||
|
||||
ctx.Locals("__ctx_user", user)
|
||||
ctx.Locals(consts.CtxKeyUser, user)
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
func (m *Middlewares) TenantResolver(ctx fiber.Ctx) error {
|
||||
tenantCode := strings.TrimSpace(ctx.Params("tenantCode"))
|
||||
if tenantCode == "" {
|
||||
return errorx.ErrMissingParameter.WithMsg("缺少租户编码")
|
||||
}
|
||||
|
||||
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||
tenant, err := q.Where(tbl.Code.Eq(tenantCode)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errorx.ErrRecordNotFound.WithMsg("租户不存在")
|
||||
}
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
ctx.Locals(consts.CtxKeyTenant, tenant)
|
||||
return ctx.Next()
|
||||
}
|
||||
|
||||
@@ -98,7 +125,7 @@ func hasRole(roles types.Array[consts.Role], role consts.Role) bool {
|
||||
}
|
||||
|
||||
func isPublicRoute(ctx fiber.Ctx) bool {
|
||||
path := ctx.Path()
|
||||
path := normalizeTenantPath(ctx.Path())
|
||||
method := ctx.Method()
|
||||
|
||||
if method == fiber.MethodGet {
|
||||
@@ -127,6 +154,13 @@ func isPublicRoute(ctx fiber.Ctx) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if method == fiber.MethodPost {
|
||||
switch path {
|
||||
case "/v1/auth/otp", "/v1/auth/login":
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if method == fiber.MethodPut && strings.HasPrefix(path, "/v1/storage/") {
|
||||
return true
|
||||
}
|
||||
@@ -144,3 +178,19 @@ func isSuperPublicRoute(ctx fiber.Ctx) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeTenantPath(path string) string {
|
||||
if !strings.HasPrefix(path, "/t/") {
|
||||
return path
|
||||
}
|
||||
rest := strings.TrimPrefix(path, "/t/")
|
||||
slash := strings.Index(rest, "/")
|
||||
if slash == -1 {
|
||||
return path
|
||||
}
|
||||
rest = rest[slash:]
|
||||
if strings.HasPrefix(rest, "/v1") {
|
||||
return rest
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgconn"
|
||||
"go.ipao.vip/gen/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// @provider
|
||||
@@ -49,7 +50,7 @@ func (s *common) Options(ctx context.Context) (*common_dto.OptionsResponse, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *common) CheckHash(ctx context.Context, userID int64, hash string) (*common_dto.UploadResult, error) {
|
||||
func (s *common) CheckHash(ctx context.Context, tenantID, userID int64, hash string) (*common_dto.UploadResult, error) {
|
||||
existing, err := models.MediaAssetQuery.WithContext(ctx).Where(models.MediaAssetQuery.Hash.Eq(hash)).First()
|
||||
if err != nil {
|
||||
return nil, nil // Not found, proceed to upload
|
||||
@@ -58,18 +59,25 @@ func (s *common) CheckHash(ctx context.Context, userID int64, hash string) (*com
|
||||
// Found existing file (Global deduplication hit)
|
||||
|
||||
// Check if user already has it (Logic deduplication hit)
|
||||
myExisting, err := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID)).
|
||||
First()
|
||||
myQuery := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID))
|
||||
if tenantID > 0 {
|
||||
myQuery = myQuery.Where(models.MediaAssetQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
myExisting, err := myQuery.First()
|
||||
if err == nil {
|
||||
return s.composeUploadResult(myExisting), nil
|
||||
}
|
||||
|
||||
// Create new record for this user reusing existing ObjectKey
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(userID)).First()
|
||||
var tid int64 = 0
|
||||
if err == nil {
|
||||
tid = t.ID
|
||||
// 优先使用路径租户,避免跨租户写入。
|
||||
tenant, err := s.resolveTenant(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tid int64
|
||||
if tenant != nil {
|
||||
tid = tenant.ID
|
||||
}
|
||||
|
||||
asset := &models.MediaAsset{
|
||||
@@ -107,13 +115,47 @@ func (s *common) buildObjectKey(tenant *models.Tenant, hash, filename string) st
|
||||
return path.Join("quyun", tenantUUID, hash+ext)
|
||||
}
|
||||
|
||||
func (s *common) InitUpload(ctx context.Context, userID int64, form *common_dto.UploadInitForm) (*common_dto.UploadInitResponse, error) {
|
||||
func (s *common) resolveTenant(ctx context.Context, tenantID, userID int64) (*models.Tenant, error) {
|
||||
if tenantID > 0 {
|
||||
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||
tenant, err := q.Where(tbl.ID.Eq(tenantID)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在")
|
||||
}
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
if userID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||
tenant, err := q.Where(tbl.UserID.Eq(userID)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
return tenant, nil
|
||||
}
|
||||
|
||||
func (s *common) uploadTempDir(localPath string, tenantID int64, uploadID string) string {
|
||||
tenantKey := "public"
|
||||
if tenantID > 0 {
|
||||
tenantKey = strconv.FormatInt(tenantID, 10)
|
||||
}
|
||||
return filepath.Join(localPath, "temp", tenantKey, uploadID)
|
||||
}
|
||||
|
||||
func (s *common) InitUpload(ctx context.Context, tenantID, userID int64, form *common_dto.UploadInitForm) (*common_dto.UploadInitResponse, error) {
|
||||
uploadID := uuid.NewString()
|
||||
localPath := s.storage.Config.LocalPath
|
||||
if localPath == "" {
|
||||
localPath = "./storage"
|
||||
}
|
||||
tempDir := filepath.Join(localPath, "temp", uploadID)
|
||||
tempDir := s.uploadTempDir(localPath, tenantID, uploadID)
|
||||
if err := os.MkdirAll(tempDir, 0o755); err != nil {
|
||||
return nil, errorx.ErrInternalError.WithCause(err)
|
||||
}
|
||||
@@ -134,12 +176,12 @@ func (s *common) InitUpload(ctx context.Context, userID int64, form *common_dto.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *common) UploadPart(ctx context.Context, userID int64, file *multipart.FileHeader, form *common_dto.UploadPartForm) error {
|
||||
func (s *common) UploadPart(ctx context.Context, tenantID, userID int64, file *multipart.FileHeader, form *common_dto.UploadPartForm) error {
|
||||
localPath := s.storage.Config.LocalPath
|
||||
if localPath == "" {
|
||||
localPath = "./storage"
|
||||
}
|
||||
partPath := filepath.Join(localPath, "temp", form.UploadID, strconv.Itoa(form.PartNumber))
|
||||
partPath := filepath.Join(s.uploadTempDir(localPath, tenantID, form.UploadID), strconv.Itoa(form.PartNumber))
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
@@ -159,12 +201,12 @@ func (s *common) UploadPart(ctx context.Context, userID int64, file *multipart.F
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_dto.UploadCompleteForm) (*common_dto.UploadResult, error) {
|
||||
func (s *common) CompleteUpload(ctx context.Context, tenantID, userID int64, form *common_dto.UploadCompleteForm) (*common_dto.UploadResult, error) {
|
||||
localPath := s.storage.Config.LocalPath
|
||||
if localPath == "" {
|
||||
localPath = "./storage"
|
||||
}
|
||||
tempDir := filepath.Join(localPath, "temp", form.UploadID)
|
||||
tempDir := s.uploadTempDir(localPath, tenantID, form.UploadID)
|
||||
|
||||
// Read Meta
|
||||
var meta UploadMeta
|
||||
@@ -220,21 +262,27 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
|
||||
dst.Close() // Ensure flush before potential removal
|
||||
|
||||
// Deduplication Logic (Similar to Upload)
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(userID)).First()
|
||||
var tid int64 = 0
|
||||
if err == nil {
|
||||
tid = t.ID
|
||||
tenant, err := s.resolveTenant(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tid int64
|
||||
if tenant != nil {
|
||||
tid = tenant.ID
|
||||
}
|
||||
|
||||
objectKey := s.buildObjectKey(t, hash, meta.Filename)
|
||||
objectKey := s.buildObjectKey(tenant, hash, meta.Filename)
|
||||
existing, err := models.MediaAssetQuery.WithContext(ctx).Where(models.MediaAssetQuery.Hash.Eq(hash)).First()
|
||||
var asset *models.MediaAsset
|
||||
|
||||
if err == nil {
|
||||
os.Remove(mergedPath) // Delete duplicate
|
||||
myExisting, err := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID)).
|
||||
First()
|
||||
myQuery := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID))
|
||||
if tenantID > 0 {
|
||||
myQuery = myQuery.Where(models.MediaAssetQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
myExisting, err := myQuery.First()
|
||||
if err == nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return s.composeUploadResult(myExisting), nil
|
||||
@@ -282,10 +330,13 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_
|
||||
return s.composeUploadResult(asset), nil
|
||||
}
|
||||
|
||||
func (s *common) DeleteMediaAsset(ctx context.Context, userID, id int64) error {
|
||||
asset, err := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.ID.Eq(id), models.MediaAssetQuery.UserID.Eq(userID)).
|
||||
First()
|
||||
func (s *common) DeleteMediaAsset(ctx context.Context, tenantID, userID, id int64) error {
|
||||
query := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.ID.Eq(id), models.MediaAssetQuery.UserID.Eq(userID))
|
||||
if tenantID > 0 {
|
||||
query = query.Where(models.MediaAssetQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
asset, err := query.First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
@@ -308,17 +359,18 @@ func (s *common) DeleteMediaAsset(ctx context.Context, userID, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *common) AbortUpload(ctx context.Context, userID int64, uploadId string) error {
|
||||
func (s *common) AbortUpload(ctx context.Context, tenantID, userID int64, uploadId string) error {
|
||||
localPath := s.storage.Config.LocalPath
|
||||
if localPath == "" {
|
||||
localPath = "./storage"
|
||||
}
|
||||
tempDir := filepath.Join(localPath, "temp", uploadId)
|
||||
tempDir := s.uploadTempDir(localPath, tenantID, uploadId)
|
||||
return os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
func (s *common) Upload(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
file *multipart.FileHeader,
|
||||
typeArg string,
|
||||
@@ -357,13 +409,16 @@ func (s *common) Upload(
|
||||
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(userID)).First()
|
||||
var tid int64 = 0
|
||||
if err == nil {
|
||||
tid = t.ID
|
||||
tenant, err := s.resolveTenant(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tid int64
|
||||
if tenant != nil {
|
||||
tid = tenant.ID
|
||||
}
|
||||
|
||||
objectKey := s.buildObjectKey(t, hash, file.Filename)
|
||||
objectKey := s.buildObjectKey(tenant, hash, file.Filename)
|
||||
var asset *models.MediaAsset
|
||||
|
||||
// Deduplication Check
|
||||
@@ -374,9 +429,12 @@ func (s *common) Upload(
|
||||
os.RemoveAll(tmpDir)
|
||||
|
||||
// Check if user already has it (Logic Deduplication)
|
||||
myExisting, err := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID)).
|
||||
First()
|
||||
myQuery := models.MediaAssetQuery.WithContext(ctx).
|
||||
Where(models.MediaAssetQuery.Hash.Eq(hash), models.MediaAssetQuery.UserID.Eq(userID))
|
||||
if tenantID > 0 {
|
||||
myQuery = myQuery.Where(models.MediaAssetQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
myExisting, err := myQuery.First()
|
||||
if err == nil {
|
||||
return s.composeUploadResult(myExisting), nil
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ import (
|
||||
// @provider
|
||||
type content struct{}
|
||||
|
||||
func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilter) (*requests.Pager, error) {
|
||||
func (s *content) List(ctx context.Context, tenantID int64, filter *content_dto.ContentListFilter) (*requests.Pager, error) {
|
||||
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||||
|
||||
// Filters
|
||||
q = q.Where(tbl.Status.Eq(consts.ContentStatusPublished))
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
if filter.Keyword != nil && *filter.Keyword != "" {
|
||||
keyword := "%" + *filter.Keyword + "%"
|
||||
q = q.Where(tbl.Title.Like(keyword)).Or(tbl.Description.Like(keyword))
|
||||
@@ -31,6 +34,9 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte
|
||||
q = q.Where(tbl.Genre.Eq(*filter.Genre))
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
if tenantID > 0 && *filter.TenantID != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
if filter.IsPinned != nil {
|
||||
@@ -128,16 +134,22 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *content) Get(ctx context.Context, userID, id int64) (*content_dto.ContentDetail, error) {
|
||||
func (s *content) Get(ctx context.Context, tenantID, userID, id int64) (*content_dto.ContentDetail, error) {
|
||||
// Increment Views
|
||||
_, _ = models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.ID.Eq(id)).
|
||||
UpdateSimple(models.ContentQuery.Views.Add(1))
|
||||
update := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(id))
|
||||
if tenantID > 0 {
|
||||
update = update.Where(models.ContentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
_, _ = update.UpdateSimple(models.ContentQuery.Views.Add(1))
|
||||
|
||||
_, q := models.ContentQuery.QueryContext(ctx)
|
||||
|
||||
var item models.Content
|
||||
err := q.UnderlyingDB().
|
||||
db := q.UnderlyingDB()
|
||||
if tenantID > 0 {
|
||||
db = db.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
err := db.
|
||||
Preload("Author").
|
||||
Preload("ContentAssets", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("sort ASC")
|
||||
@@ -232,10 +244,25 @@ func (s *content) Get(ctx context.Context, userID, id int64) (*content_dto.Conte
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (s *content) ListComments(ctx context.Context, userID, id int64, page int) (*requests.Pager, error) {
|
||||
func (s *content) ListComments(ctx context.Context, tenantID, userID, id int64, page int) (*requests.Pager, error) {
|
||||
if tenantID > 0 {
|
||||
_, err := models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.ID.Eq(id), models.ContentQuery.TenantID.Eq(tenantID)).
|
||||
First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound
|
||||
}
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
}
|
||||
|
||||
tbl, q := models.CommentQuery.QueryContext(ctx)
|
||||
|
||||
q = q.Where(tbl.ContentID.Eq(id)).Preload(tbl.User)
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
|
||||
p := requests.Pagination{Page: int64(page), Limit: 10}
|
||||
@@ -291,6 +318,7 @@ func (s *content) ListComments(ctx context.Context, userID, id int64, page int)
|
||||
|
||||
func (s *content) CreateComment(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
id int64,
|
||||
form *content_dto.CommentCreateForm,
|
||||
@@ -300,7 +328,11 @@ func (s *content) CreateComment(
|
||||
}
|
||||
uid := userID
|
||||
|
||||
c, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(id)).First()
|
||||
query := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(id))
|
||||
if tenantID > 0 {
|
||||
query = query.Where(models.ContentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
c, err := query.First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
@@ -319,14 +351,18 @@ func (s *content) CreateComment(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *content) LikeComment(ctx context.Context, userID, id int64) error {
|
||||
func (s *content) LikeComment(ctx context.Context, tenantID, userID, id int64) error {
|
||||
if userID == 0 {
|
||||
return errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
// Fetch comment for author
|
||||
cm, err := models.CommentQuery.WithContext(ctx).Where(models.CommentQuery.ID.Eq(id)).First()
|
||||
query := models.CommentQuery.WithContext(ctx).Where(models.CommentQuery.ID.Eq(id))
|
||||
if tenantID > 0 {
|
||||
query = query.Where(models.CommentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
cm, err := query.First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
@@ -357,14 +393,18 @@ func (s *content) LikeComment(ctx context.Context, userID, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *content) GetLibrary(ctx context.Context, userID int64) ([]user_dto.ContentItem, error) {
|
||||
func (s *content) GetLibrary(ctx context.Context, tenantID, userID int64) ([]user_dto.ContentItem, error) {
|
||||
if userID == 0 {
|
||||
return nil, errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
tbl, q := models.ContentAccessQuery.QueryContext(ctx)
|
||||
accessList, err := q.Where(tbl.UserID.Eq(uid), tbl.Status.Eq(consts.ContentAccessStatusActive)).Find()
|
||||
q = q.Where(tbl.UserID.Eq(uid), tbl.Status.Eq(consts.ContentAccessStatusActive))
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
accessList, err := q.Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
@@ -380,7 +420,11 @@ func (s *content) GetLibrary(ctx context.Context, userID int64) ([]user_dto.Cont
|
||||
|
||||
ctbl, cq := models.ContentQuery.QueryContext(ctx)
|
||||
var list []*models.Content
|
||||
err = cq.Where(ctbl.ID.In(contentIDs...)).
|
||||
cq = cq.Where(ctbl.ID.In(contentIDs...))
|
||||
if tenantID > 0 {
|
||||
cq = cq.Where(ctbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
err = cq.
|
||||
UnderlyingDB().
|
||||
Preload("Author").
|
||||
Preload("ContentAssets.Asset").
|
||||
@@ -398,36 +442,40 @@ func (s *content) GetLibrary(ctx context.Context, userID int64) ([]user_dto.Cont
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *content) GetFavorites(ctx context.Context, userID int64) ([]user_dto.ContentItem, error) {
|
||||
return s.getInteractList(ctx, userID, "favorite")
|
||||
func (s *content) GetFavorites(ctx context.Context, tenantID, userID int64) ([]user_dto.ContentItem, error) {
|
||||
return s.getInteractList(ctx, tenantID, userID, "favorite")
|
||||
}
|
||||
|
||||
func (s *content) AddFavorite(ctx context.Context, userID, contentId int64) error {
|
||||
return s.addInteract(ctx, userID, contentId, "favorite")
|
||||
func (s *content) AddFavorite(ctx context.Context, tenantID, userID, contentId int64) error {
|
||||
return s.addInteract(ctx, tenantID, userID, contentId, "favorite")
|
||||
}
|
||||
|
||||
func (s *content) RemoveFavorite(ctx context.Context, userID, contentId int64) error {
|
||||
return s.removeInteract(ctx, userID, contentId, "favorite")
|
||||
func (s *content) RemoveFavorite(ctx context.Context, tenantID, userID, contentId int64) error {
|
||||
return s.removeInteract(ctx, tenantID, userID, contentId, "favorite")
|
||||
}
|
||||
|
||||
func (s *content) GetLikes(ctx context.Context, userID int64) ([]user_dto.ContentItem, error) {
|
||||
return s.getInteractList(ctx, userID, "like")
|
||||
func (s *content) GetLikes(ctx context.Context, tenantID, userID int64) ([]user_dto.ContentItem, error) {
|
||||
return s.getInteractList(ctx, tenantID, userID, "like")
|
||||
}
|
||||
|
||||
func (s *content) AddLike(ctx context.Context, userID, contentId int64) error {
|
||||
return s.addInteract(ctx, userID, contentId, "like")
|
||||
func (s *content) AddLike(ctx context.Context, tenantID, userID, contentId int64) error {
|
||||
return s.addInteract(ctx, tenantID, userID, contentId, "like")
|
||||
}
|
||||
|
||||
func (s *content) RemoveLike(ctx context.Context, userID, contentId int64) error {
|
||||
return s.removeInteract(ctx, userID, contentId, "like")
|
||||
func (s *content) RemoveLike(ctx context.Context, tenantID, userID, contentId int64) error {
|
||||
return s.removeInteract(ctx, tenantID, userID, contentId, "like")
|
||||
}
|
||||
|
||||
func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) {
|
||||
func (s *content) ListTopics(ctx context.Context, tenantID int64) ([]content_dto.Topic, error) {
|
||||
var results []struct {
|
||||
Genre string
|
||||
Count int
|
||||
}
|
||||
err := models.ContentQuery.WithContext(ctx).UnderlyingDB().
|
||||
db := models.ContentQuery.WithContext(ctx).UnderlyingDB()
|
||||
if tenantID > 0 {
|
||||
db = db.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
err := db.
|
||||
Model(&models.Content{}).
|
||||
Where("status = ?", consts.ContentStatusPublished).
|
||||
Select("genre, count(*) as count").
|
||||
@@ -445,8 +493,12 @@ func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) {
|
||||
|
||||
// Fetch latest content in this genre to get a cover
|
||||
var c models.Content
|
||||
models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.Genre.Eq(r.Genre), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).
|
||||
query := models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.Genre.Eq(r.Genre), models.ContentQuery.Status.Eq(consts.ContentStatusPublished))
|
||||
if tenantID > 0 {
|
||||
query = query.Where(models.ContentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
query.
|
||||
Order(models.ContentQuery.PublishedAt.Desc()).
|
||||
UnderlyingDB().
|
||||
Preload("ContentAssets").
|
||||
@@ -554,15 +606,19 @@ func (s *content) toMediaURLs(assets []*models.ContentAsset) []content_dto.Media
|
||||
return urls
|
||||
}
|
||||
|
||||
func (s *content) addInteract(ctx context.Context, userID, contentId int64, typ string) error {
|
||||
func (s *content) addInteract(ctx context.Context, tenantID, userID, contentId int64, typ string) error {
|
||||
if userID == 0 {
|
||||
return errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
// Fetch content for author
|
||||
c, err := models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.ID.Eq(contentId)).
|
||||
query := models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.ID.Eq(contentId))
|
||||
if tenantID > 0 {
|
||||
query = query.Where(models.ContentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
c, err := query.
|
||||
Select(models.ContentQuery.UserID, models.ContentQuery.Title).
|
||||
First()
|
||||
if err != nil {
|
||||
@@ -583,7 +639,11 @@ func (s *content) addInteract(ctx context.Context, userID, contentId int64, typ
|
||||
}
|
||||
|
||||
if typ == "like" {
|
||||
_, err := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(contentId)).UpdateSimple(tx.Content.Likes.Add(1))
|
||||
contentQuery := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(contentId))
|
||||
if tenantID > 0 {
|
||||
contentQuery = contentQuery.Where(tx.Content.TenantID.Eq(tenantID))
|
||||
}
|
||||
_, err := contentQuery.UpdateSimple(tx.Content.Likes.Add(1))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -605,7 +665,7 @@ func (s *content) addInteract(ctx context.Context, userID, contentId int64, typ
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *content) removeInteract(ctx context.Context, userID, contentId int64, typ string) error {
|
||||
func (s *content) removeInteract(ctx context.Context, tenantID, userID, contentId int64, typ string) error {
|
||||
if userID == 0 {
|
||||
return errorx.ErrUnauthorized
|
||||
}
|
||||
@@ -623,14 +683,18 @@ func (s *content) removeInteract(ctx context.Context, userID, contentId int64, t
|
||||
}
|
||||
|
||||
if typ == "like" {
|
||||
_, err := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(contentId)).UpdateSimple(tx.Content.Likes.Sub(1))
|
||||
contentQuery := tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(contentId))
|
||||
if tenantID > 0 {
|
||||
contentQuery = contentQuery.Where(tx.Content.TenantID.Eq(tenantID))
|
||||
}
|
||||
_, err := contentQuery.UpdateSimple(tx.Content.Likes.Sub(1))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *content) getInteractList(ctx context.Context, userID int64, typ string) ([]user_dto.ContentItem, error) {
|
||||
func (s *content) getInteractList(ctx context.Context, tenantID, userID int64, typ string) ([]user_dto.ContentItem, error) {
|
||||
if userID == 0 {
|
||||
return nil, errorx.ErrUnauthorized
|
||||
}
|
||||
@@ -653,7 +717,11 @@ func (s *content) getInteractList(ctx context.Context, userID int64, typ string)
|
||||
|
||||
ctbl, cq := models.ContentQuery.QueryContext(ctx)
|
||||
var list []*models.Content
|
||||
err = cq.Where(ctbl.ID.In(contentIDs...)).
|
||||
cq = cq.Where(ctbl.ID.In(contentIDs...))
|
||||
if tenantID > 0 {
|
||||
cq = cq.Where(ctbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
err = cq.
|
||||
UnderlyingDB().
|
||||
Preload("Author").
|
||||
Preload("ContentAssets.Asset").
|
||||
|
||||
@@ -41,6 +41,7 @@ func Test_Content(t *testing.T) {
|
||||
func (s *ContentTestSuite) Test_List() {
|
||||
Convey("List", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser)
|
||||
|
||||
// Create Author
|
||||
@@ -73,7 +74,7 @@ func (s *ContentTestSuite) Test_List() {
|
||||
Limit: 10,
|
||||
},
|
||||
}
|
||||
res, err := Content.List(ctx, filter)
|
||||
res, err := Content.List(ctx, tenantID, filter)
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Total, ShouldEqual, 1)
|
||||
items := res.Items.([]content_dto.ContentItem)
|
||||
@@ -86,6 +87,7 @@ func (s *ContentTestSuite) Test_List() {
|
||||
func (s *ContentTestSuite) Test_Get() {
|
||||
Convey("Get", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameMediaAsset, models.TableNameContentAsset, models.TableNameUser)
|
||||
|
||||
// Author
|
||||
@@ -125,7 +127,7 @@ func (s *ContentTestSuite) Test_Get() {
|
||||
ctx = context.WithValue(ctx, consts.CtxKeyUser, author.ID)
|
||||
|
||||
Convey("should get detail with assets", func() {
|
||||
detail, err := Content.Get(ctx, author.ID, content.ID)
|
||||
detail, err := Content.Get(ctx, tenantID, author.ID, content.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(detail.Title, ShouldEqual, "Detail Content")
|
||||
So(detail.AuthorName, ShouldEqual, "Author1")
|
||||
@@ -138,6 +140,7 @@ func (s *ContentTestSuite) Test_Get() {
|
||||
func (s *ContentTestSuite) Test_CreateComment() {
|
||||
Convey("CreateComment", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameComment, models.TableNameUser)
|
||||
|
||||
// User & Content
|
||||
@@ -153,7 +156,7 @@ func (s *ContentTestSuite) Test_CreateComment() {
|
||||
form := &content_dto.CommentCreateForm{
|
||||
Content: "Nice!",
|
||||
}
|
||||
err := Content.CreateComment(ctx, u.ID, c.ID, form)
|
||||
err := Content.CreateComment(ctx, tenantID, u.ID, c.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
count, _ := models.CommentQuery.WithContext(ctx).Where(models.CommentQuery.ContentID.Eq(c.ID)).Count()
|
||||
@@ -165,6 +168,7 @@ func (s *ContentTestSuite) Test_CreateComment() {
|
||||
func (s *ContentTestSuite) Test_Library() {
|
||||
Convey("Library", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameContentAccess, models.TableNameUser, models.TableNameContentAsset, models.TableNameMediaAsset)
|
||||
|
||||
// User
|
||||
@@ -192,7 +196,7 @@ func (s *ContentTestSuite) Test_Library() {
|
||||
})
|
||||
|
||||
Convey("should get library content with details", func() {
|
||||
list, err := Content.GetLibrary(ctx, u.ID)
|
||||
list, err := Content.GetLibrary(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(list), ShouldEqual, 1)
|
||||
So(list[0].Title, ShouldEqual, "Paid Content")
|
||||
@@ -206,6 +210,7 @@ func (s *ContentTestSuite) Test_Library() {
|
||||
func (s *ContentTestSuite) Test_Interact() {
|
||||
Convey("Interact", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUserContentAction, models.TableNameUser)
|
||||
|
||||
// User & Content
|
||||
@@ -218,7 +223,7 @@ func (s *ContentTestSuite) Test_Interact() {
|
||||
|
||||
Convey("Like flow", func() {
|
||||
// Add Like
|
||||
err := Content.AddLike(ctx, u.ID, c.ID)
|
||||
err := Content.AddLike(ctx, tenantID, u.ID, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify count
|
||||
@@ -226,13 +231,13 @@ func (s *ContentTestSuite) Test_Interact() {
|
||||
So(cReload.Likes, ShouldEqual, 1)
|
||||
|
||||
// Get Likes
|
||||
likes, err := Content.GetLikes(ctx, u.ID)
|
||||
likes, err := Content.GetLikes(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(likes), ShouldEqual, 1)
|
||||
So(likes[0].ID, ShouldEqual, c.ID)
|
||||
|
||||
// Remove Like
|
||||
err = Content.RemoveLike(ctx, u.ID, c.ID)
|
||||
err = Content.RemoveLike(ctx, tenantID, u.ID, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify count
|
||||
@@ -242,21 +247,21 @@ func (s *ContentTestSuite) Test_Interact() {
|
||||
|
||||
Convey("Favorite flow", func() {
|
||||
// Add Favorite
|
||||
err := Content.AddFavorite(ctx, u.ID, c.ID)
|
||||
err := Content.AddFavorite(ctx, tenantID, u.ID, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Get Favorites
|
||||
favs, err := Content.GetFavorites(ctx, u.ID)
|
||||
favs, err := Content.GetFavorites(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(favs), ShouldEqual, 1)
|
||||
So(favs[0].ID, ShouldEqual, c.ID)
|
||||
|
||||
// Remove Favorite
|
||||
err = Content.RemoveFavorite(ctx, u.ID, c.ID)
|
||||
err = Content.RemoveFavorite(ctx, tenantID, u.ID, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Get Favorites
|
||||
favs, err = Content.GetFavorites(ctx, u.ID)
|
||||
favs, err = Content.GetFavorites(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(favs), ShouldEqual, 0)
|
||||
})
|
||||
@@ -266,6 +271,7 @@ func (s *ContentTestSuite) Test_Interact() {
|
||||
func (s *ContentTestSuite) Test_ListTopics() {
|
||||
Convey("ListTopics", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser)
|
||||
|
||||
u := &models.User{Username: "user_t", Phone: "13900000005"}
|
||||
@@ -280,7 +286,7 @@ func (s *ContentTestSuite) Test_ListTopics() {
|
||||
)
|
||||
|
||||
Convey("should aggregate topics", func() {
|
||||
topics, err := Content.ListTopics(ctx)
|
||||
topics, err := Content.ListTopics(ctx, tenantID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(topics), ShouldBeGreaterThanOrEqualTo, 2)
|
||||
|
||||
@@ -302,6 +308,7 @@ func (s *ContentTestSuite) Test_ListTopics() {
|
||||
func (s *ContentTestSuite) Test_PreviewLogic() {
|
||||
Convey("Preview Logic", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameContentAsset, models.TableNameContentAccess, models.TableNameUser, models.TableNameMediaAsset)
|
||||
|
||||
author := &models.User{Username: "author_p", Phone: "13900000006"}
|
||||
@@ -324,7 +331,7 @@ func (s *ContentTestSuite) Test_PreviewLogic() {
|
||||
models.UserQuery.WithContext(ctx).Create(guest)
|
||||
guestCtx := context.WithValue(ctx, consts.CtxKeyUser, guest.ID)
|
||||
|
||||
detail, err := Content.Get(guestCtx, 0, c.ID)
|
||||
detail, err := Content.Get(guestCtx, tenantID, 0, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(detail.MediaUrls), ShouldEqual, 1)
|
||||
So(detail.MediaUrls[0].URL, ShouldContainSubstring, "preview.mp4")
|
||||
@@ -333,7 +340,7 @@ func (s *ContentTestSuite) Test_PreviewLogic() {
|
||||
|
||||
Convey("owner should see all", func() {
|
||||
ownerCtx := context.WithValue(ctx, consts.CtxKeyUser, author.ID)
|
||||
detail, err := Content.Get(ownerCtx, author.ID, c.ID)
|
||||
detail, err := Content.Get(ownerCtx, tenantID, author.ID, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(detail.MediaUrls), ShouldEqual, 2)
|
||||
So(detail.IsPurchased, ShouldBeTrue)
|
||||
@@ -348,7 +355,7 @@ func (s *ContentTestSuite) Test_PreviewLogic() {
|
||||
UserID: buyer.ID, ContentID: c.ID, Status: consts.ContentAccessStatusActive,
|
||||
})
|
||||
|
||||
detail, err := Content.Get(buyerCtx, buyer.ID, c.ID)
|
||||
detail, err := Content.Get(buyerCtx, tenantID, buyer.ID, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(detail.MediaUrls), ShouldEqual, 2)
|
||||
So(detail.IsPurchased, ShouldBeTrue)
|
||||
@@ -359,6 +366,7 @@ func (s *ContentTestSuite) Test_PreviewLogic() {
|
||||
func (s *ContentTestSuite) Test_ViewCounting() {
|
||||
Convey("ViewCounting", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameContent, models.TableNameUser)
|
||||
|
||||
author := &models.User{Username: "author_v", Phone: "13900000009"}
|
||||
@@ -368,7 +376,7 @@ func (s *ContentTestSuite) Test_ViewCounting() {
|
||||
models.ContentQuery.WithContext(ctx).Create(c)
|
||||
|
||||
Convey("should increment views", func() {
|
||||
_, err := Content.Get(ctx, 0, c.ID)
|
||||
_, err := Content.Get(ctx, tenantID, 0, c.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
cReload, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(c.ID)).First()
|
||||
|
||||
@@ -39,6 +39,7 @@ func Test_Coupon(t *testing.T) {
|
||||
func (s *CouponTestSuite) Test_CouponFlow() {
|
||||
Convey("Coupon Flow", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -83,9 +84,10 @@ func (s *CouponTestSuite) Test_CouponFlow() {
|
||||
|
||||
Convey("should apply in Order.Create", func() {
|
||||
// Setup Content
|
||||
c := &models.Content{UserID: 99, Title: "Test", Status: consts.ContentStatusPublished}
|
||||
c := &models.Content{TenantID: tenantID, UserID: 99, Title: "Test", Status: consts.ContentStatusPublished}
|
||||
models.ContentQuery.WithContext(ctx).Create(c)
|
||||
models.ContentPriceQuery.WithContext(ctx).Create(&models.ContentPrice{
|
||||
TenantID: tenantID,
|
||||
ContentID: c.ID,
|
||||
PriceAmount: 2000, // 20.00 CNY
|
||||
Currency: "CNY",
|
||||
@@ -96,7 +98,7 @@ func (s *CouponTestSuite) Test_CouponFlow() {
|
||||
UserCouponID: uc.ID,
|
||||
}
|
||||
// Simulate Auth context for Order service
|
||||
res, err := Order.Create(ctx, user.ID, form)
|
||||
res, err := Order.Create(ctx, tenantID, user.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Order
|
||||
|
||||
@@ -31,7 +31,7 @@ var genreMap = map[string]string{
|
||||
"Qinqiang": "秦腔",
|
||||
}
|
||||
|
||||
func (s *creator) Apply(ctx context.Context, userID int64, form *creator_dto.ApplyForm) error {
|
||||
func (s *creator) Apply(ctx context.Context, tenantID, userID int64, form *creator_dto.ApplyForm) error {
|
||||
if userID == 0 {
|
||||
return errorx.ErrUnauthorized
|
||||
}
|
||||
@@ -72,8 +72,8 @@ func (s *creator) Apply(ctx context.Context, userID int64, form *creator_dto.App
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *creator) Dashboard(ctx context.Context, userID int64) (*creator_dto.DashboardStats, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) Dashboard(ctx context.Context, tenantID, userID int64) (*creator_dto.DashboardStats, error) {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -107,10 +107,11 @@ func (s *creator) Dashboard(ctx context.Context, userID int64) (*creator_dto.Das
|
||||
|
||||
func (s *creator) ListContents(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
filter *creator_dto.CreatorContentListFilter,
|
||||
) (*requests.Pager, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -248,8 +249,8 @@ func (s *creator) ListContents(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *creator) CreateContent(ctx context.Context, userID int64, form *creator_dto.ContentCreateForm) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) CreateContent(ctx context.Context, tenantID, userID int64, form *creator_dto.ContentCreateForm) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -321,11 +322,12 @@ func (s *creator) CreateContent(ctx context.Context, userID int64, form *creator
|
||||
|
||||
func (s *creator) UpdateContent(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
id int64,
|
||||
form *creator_dto.ContentUpdateForm,
|
||||
) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -451,8 +453,8 @@ func (s *creator) UpdateContent(
|
||||
})
|
||||
}
|
||||
|
||||
func (s *creator) DeleteContent(ctx context.Context, userID, id int64) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) DeleteContent(ctx context.Context, tenantID, userID, id int64) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -472,8 +474,8 @@ func (s *creator) DeleteContent(ctx context.Context, userID, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *creator) GetContent(ctx context.Context, userID, id int64) (*creator_dto.ContentEditDTO, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) GetContent(ctx context.Context, tenantID, userID, id int64) (*creator_dto.ContentEditDTO, error) {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -548,10 +550,11 @@ func (s *creator) GetContent(ctx context.Context, userID, id int64) (*creator_dt
|
||||
|
||||
func (s *creator) ListOrders(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
filter *creator_dto.CreatorOrderListFilter,
|
||||
) ([]creator_dto.Order, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -634,8 +637,8 @@ func (s *creator) ListOrders(
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *creator) ProcessRefund(ctx context.Context, userID, id int64, form *creator_dto.RefundForm) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) ProcessRefund(ctx context.Context, tenantID, userID, id int64, form *creator_dto.RefundForm) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -738,8 +741,8 @@ func (s *creator) ProcessRefund(ctx context.Context, userID, id int64, form *cre
|
||||
return errorx.ErrBadRequest.WithMsg("无效的操作")
|
||||
}
|
||||
|
||||
func (s *creator) GetSettings(ctx context.Context, userID int64) (*creator_dto.Settings, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) GetSettings(ctx context.Context, tenantID, userID int64) (*creator_dto.Settings, error) {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -758,8 +761,8 @@ func (s *creator) GetSettings(ctx context.Context, userID int64) (*creator_dto.S
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *creator) UpdateSettings(ctx context.Context, userID int64, form *creator_dto.Settings) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) UpdateSettings(ctx context.Context, tenantID, userID int64, form *creator_dto.Settings) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -782,8 +785,8 @@ func (s *creator) UpdateSettings(ctx context.Context, userID int64, form *creato
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *creator) ListPayoutAccounts(ctx context.Context, userID int64) ([]creator_dto.PayoutAccount, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) ListPayoutAccounts(ctx context.Context, tenantID, userID int64) ([]creator_dto.PayoutAccount, error) {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -806,8 +809,8 @@ func (s *creator) ListPayoutAccounts(ctx context.Context, userID int64) ([]creat
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *creator) AddPayoutAccount(ctx context.Context, userID int64, form *creator_dto.PayoutAccount) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) AddPayoutAccount(ctx context.Context, tenantID, userID int64, form *creator_dto.PayoutAccount) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -827,8 +830,8 @@ func (s *creator) AddPayoutAccount(ctx context.Context, userID int64, form *crea
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *creator) RemovePayoutAccount(ctx context.Context, userID, id int64) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) RemovePayoutAccount(ctx context.Context, tenantID, userID, id int64) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -842,8 +845,8 @@ func (s *creator) RemovePayoutAccount(ctx context.Context, userID, id int64) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *creator) Withdraw(ctx context.Context, userID int64, form *creator_dto.WithdrawForm) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
func (s *creator) Withdraw(ctx context.Context, tenantID, userID int64, form *creator_dto.WithdrawForm) error {
|
||||
tid, err := s.getTenantID(ctx, tenantID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -920,7 +923,7 @@ func (s *creator) Withdraw(ctx context.Context, userID int64, form *creator_dto.
|
||||
|
||||
// Helpers
|
||||
|
||||
func (s *creator) getTenantID(ctx context.Context, userID int64) (int64, error) {
|
||||
func (s *creator) getTenantID(ctx context.Context, tenantID, userID int64) (int64, error) {
|
||||
if userID == 0 {
|
||||
return 0, errorx.ErrUnauthorized
|
||||
}
|
||||
@@ -934,5 +937,8 @@ func (s *creator) getTenantID(ctx context.Context, userID int64) (int64, error)
|
||||
}
|
||||
return 0, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if tenantID > 0 && t.ID != tenantID {
|
||||
return 0, errorx.ErrPermissionDenied.WithMsg("无权限访问该租户")
|
||||
}
|
||||
return t.ID, nil
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ func Test_Creator(t *testing.T) {
|
||||
func (s *CreatorTestSuite) Test_Apply() {
|
||||
Convey("Apply", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameTenantUser, models.TableNameUser)
|
||||
|
||||
u := &models.User{Username: "creator1", Phone: "13700000001"}
|
||||
@@ -50,7 +51,7 @@ func (s *CreatorTestSuite) Test_Apply() {
|
||||
form := &creator_dto.ApplyForm{
|
||||
Name: "My Channel",
|
||||
}
|
||||
err := Creator.Apply(ctx, u.ID, form)
|
||||
err := Creator.Apply(ctx, tenantID, u.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
t, _ := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.UserID.Eq(u.ID)).First()
|
||||
@@ -72,6 +73,7 @@ func (s *CreatorTestSuite) Test_Apply() {
|
||||
func (s *CreatorTestSuite) Test_CreateContent() {
|
||||
Convey("CreateContent", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -89,6 +91,7 @@ func (s *CreatorTestSuite) Test_CreateContent() {
|
||||
// Create Tenant manually
|
||||
t := &models.Tenant{UserID: u.ID, Name: "Channel 2", Code: "123", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
Convey("should create content and assets", func() {
|
||||
form := &creator_dto.ContentCreateForm{
|
||||
@@ -97,7 +100,7 @@ func (s *CreatorTestSuite) Test_CreateContent() {
|
||||
Price: 9.99,
|
||||
// MediaIDs: ... need media asset
|
||||
}
|
||||
err := Creator.CreateContent(ctx, u.ID, form)
|
||||
err := Creator.CreateContent(ctx, tenantID, u.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
c, _ := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.Title.Eq("New Song")).First()
|
||||
@@ -116,6 +119,7 @@ func (s *CreatorTestSuite) Test_CreateContent() {
|
||||
func (s *CreatorTestSuite) Test_UpdateContent() {
|
||||
Convey("UpdateContent", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -132,6 +136,7 @@ func (s *CreatorTestSuite) Test_UpdateContent() {
|
||||
|
||||
t := &models.Tenant{UserID: u.ID, Name: "Channel 3", Code: "124", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
c := &models.Content{TenantID: t.ID, UserID: u.ID, Title: "Old Title", Genre: "audio"}
|
||||
models.ContentQuery.WithContext(ctx).Create(c)
|
||||
@@ -145,7 +150,7 @@ func (s *CreatorTestSuite) Test_UpdateContent() {
|
||||
Genre: "video",
|
||||
Price: &price,
|
||||
}
|
||||
err := Creator.UpdateContent(ctx, u.ID, c.ID, form)
|
||||
err := Creator.UpdateContent(ctx, tenantID, u.ID, c.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify
|
||||
@@ -162,6 +167,7 @@ func (s *CreatorTestSuite) Test_UpdateContent() {
|
||||
func (s *CreatorTestSuite) Test_Dashboard() {
|
||||
Convey("Dashboard", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -178,6 +184,7 @@ func (s *CreatorTestSuite) Test_Dashboard() {
|
||||
|
||||
t := &models.Tenant{UserID: u.ID, Name: "Channel 4", Code: "125", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
// Mock Data
|
||||
// 1. Followers
|
||||
@@ -198,7 +205,7 @@ func (s *CreatorTestSuite) Test_Dashboard() {
|
||||
)
|
||||
|
||||
Convey("should get stats", func() {
|
||||
stats, err := Creator.Dashboard(ctx, u.ID)
|
||||
stats, err := Creator.Dashboard(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(stats.TotalFollowers.Value, ShouldEqual, 2)
|
||||
// Implementation sums 'debit_purchase' only based on my code
|
||||
@@ -210,6 +217,7 @@ func (s *CreatorTestSuite) Test_Dashboard() {
|
||||
func (s *CreatorTestSuite) Test_PayoutAccount() {
|
||||
Convey("PayoutAccount", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNamePayoutAccount, models.TableNameUser)
|
||||
|
||||
u := &models.User{Username: "creator5", Phone: "13700000005"}
|
||||
@@ -218,6 +226,7 @@ func (s *CreatorTestSuite) Test_PayoutAccount() {
|
||||
|
||||
t := &models.Tenant{UserID: u.ID, Name: "Channel 5", Code: "126", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
Convey("should CRUD payout account", func() {
|
||||
// Add
|
||||
@@ -227,21 +236,21 @@ func (s *CreatorTestSuite) Test_PayoutAccount() {
|
||||
Account: "user@example.com",
|
||||
Realname: "John Doe",
|
||||
}
|
||||
err := Creator.AddPayoutAccount(ctx, u.ID, form)
|
||||
err := Creator.AddPayoutAccount(ctx, tenantID, u.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// List
|
||||
list, err := Creator.ListPayoutAccounts(ctx, u.ID)
|
||||
list, err := Creator.ListPayoutAccounts(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(list), ShouldEqual, 1)
|
||||
So(list[0].Account, ShouldEqual, "user@example.com")
|
||||
|
||||
// Remove
|
||||
err = Creator.RemovePayoutAccount(ctx, u.ID, list[0].ID)
|
||||
err = Creator.RemovePayoutAccount(ctx, tenantID, u.ID, list[0].ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Empty
|
||||
list, err = Creator.ListPayoutAccounts(ctx, u.ID)
|
||||
list, err = Creator.ListPayoutAccounts(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(list), ShouldEqual, 0)
|
||||
})
|
||||
@@ -251,6 +260,7 @@ func (s *CreatorTestSuite) Test_PayoutAccount() {
|
||||
func (s *CreatorTestSuite) Test_Withdraw() {
|
||||
Convey("Withdraw", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -267,6 +277,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
||||
|
||||
t := &models.Tenant{UserID: u.ID, Name: "Channel 6", Code: "127", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
pa := &models.PayoutAccount{
|
||||
TenantID: t.ID,
|
||||
@@ -283,7 +294,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
||||
Amount: 20.00,
|
||||
AccountID: pa.ID,
|
||||
}
|
||||
err := Creator.Withdraw(ctx, u.ID, form)
|
||||
err := Creator.Withdraw(ctx, tenantID, u.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Balance Deducted
|
||||
@@ -308,7 +319,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
||||
Amount: 100.00,
|
||||
AccountID: pa.ID,
|
||||
}
|
||||
err := Creator.Withdraw(ctx, u.ID, form)
|
||||
err := Creator.Withdraw(ctx, tenantID, u.ID, form)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
@@ -317,6 +328,7 @@ func (s *CreatorTestSuite) Test_Withdraw() {
|
||||
func (s *CreatorTestSuite) Test_Refund() {
|
||||
Convey("Refund", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(ctx, s.DB,
|
||||
models.TableNameTenant, models.TableNameUser, models.TableNameOrder,
|
||||
models.TableNameOrderItem, models.TableNameContentAccess, models.TableNameTenantLedger,
|
||||
@@ -330,6 +342,7 @@ func (s *CreatorTestSuite) Test_Refund() {
|
||||
// Tenant
|
||||
t := &models.Tenant{UserID: creator.ID, Name: "Channel 7", Code: "128", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
// Buyer
|
||||
buyer := &models.User{Username: "buyer7", Phone: "13900000007", Balance: 0}
|
||||
@@ -349,7 +362,7 @@ func (s *CreatorTestSuite) Test_Refund() {
|
||||
|
||||
Convey("should accept refund", func() {
|
||||
form := &creator_dto.RefundForm{Action: "accept", Reason: "Defective"}
|
||||
err := Creator.ProcessRefund(ctx, creator.ID, o.ID, form)
|
||||
err := Creator.ProcessRefund(ctx, tenantID, creator.ID, o.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Order
|
||||
|
||||
@@ -21,14 +21,19 @@ import (
|
||||
// @provider
|
||||
type order struct{}
|
||||
|
||||
func (s *order) ListUserOrders(ctx context.Context, userID int64, status string) ([]user_dto.Order, error) {
|
||||
func (s *order) ListUserOrders(ctx context.Context, tenantID, userID int64, status string) ([]user_dto.Order, error) {
|
||||
if userID == 0 {
|
||||
return nil, errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.UserID.Eq(uid), tbl.TenantID.Eq(tenantID)).
|
||||
Or(tbl.UserID.Eq(uid), tbl.Type.Eq(consts.OrderTypeRecharge))
|
||||
} else {
|
||||
q = q.Where(tbl.UserID.Eq(uid))
|
||||
}
|
||||
|
||||
if status != "" && status != "all" {
|
||||
q = q.Where(tbl.Status.Eq(consts.OrderStatus(status)))
|
||||
@@ -46,14 +51,21 @@ func (s *order) ListUserOrders(ctx context.Context, userID int64, status string)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (s *order) GetUserOrder(ctx context.Context, userID, id int64) (*user_dto.Order, error) {
|
||||
func (s *order) GetUserOrder(ctx context.Context, tenantID, userID, id int64) (*user_dto.Order, error) {
|
||||
if userID == 0 {
|
||||
return nil, errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
item, err := q.Where(tbl.ID.Eq(id), tbl.UserID.Eq(uid)).First()
|
||||
itemQuery := q
|
||||
if tenantID > 0 {
|
||||
itemQuery = itemQuery.Where(tbl.ID.Eq(id), tbl.UserID.Eq(uid), tbl.TenantID.Eq(tenantID)).
|
||||
Or(tbl.ID.Eq(id), tbl.UserID.Eq(uid), tbl.Type.Eq(consts.OrderTypeRecharge))
|
||||
} else {
|
||||
itemQuery = itemQuery.Where(tbl.ID.Eq(id), tbl.UserID.Eq(uid))
|
||||
}
|
||||
item, err := itemQuery.First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound
|
||||
@@ -70,6 +82,7 @@ func (s *order) GetUserOrder(ctx context.Context, userID, id int64) (*user_dto.O
|
||||
|
||||
func (s *order) Create(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
form *transaction_dto.OrderCreateForm,
|
||||
) (*transaction_dto.OrderCreateResponse, error) {
|
||||
@@ -86,7 +99,11 @@ func (s *order) Create(
|
||||
}
|
||||
if idempotencyKey != "" {
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
existing, err := q.Where(tbl.UserID.Eq(uid), tbl.IdempotencyKey.Eq(idempotencyKey)).First()
|
||||
q = q.Where(tbl.UserID.Eq(uid), tbl.IdempotencyKey.Eq(idempotencyKey))
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
existing, err := q.First()
|
||||
if err == nil {
|
||||
return &transaction_dto.OrderCreateResponse{OrderID: existing.ID}, nil
|
||||
}
|
||||
@@ -96,7 +113,11 @@ func (s *order) Create(
|
||||
}
|
||||
|
||||
// 1. Fetch Content & Price
|
||||
content, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).First()
|
||||
contentQuery := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid))
|
||||
if tenantID > 0 {
|
||||
contentQuery = contentQuery.Where(models.ContentQuery.TenantID.Eq(tenantID))
|
||||
}
|
||||
content, err := contentQuery.First()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrRecordNotFound.WithMsg("内容不存在")
|
||||
}
|
||||
@@ -188,6 +209,7 @@ func (s *order) Create(
|
||||
|
||||
func (s *order) Pay(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
id int64,
|
||||
form *transaction_dto.OrderPayForm,
|
||||
@@ -204,6 +226,9 @@ func (s *order) Pay(
|
||||
if err != nil {
|
||||
return nil, errorx.ErrRecordNotFound
|
||||
}
|
||||
if tenantID > 0 && o.TenantID > 0 && o.TenantID != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
if o.Status != consts.OrderStatusCreated {
|
||||
return nil, errorx.ErrStatusConflict.WithMsg("订单状态不可支付")
|
||||
}
|
||||
@@ -219,11 +244,14 @@ func (s *order) Pay(
|
||||
}
|
||||
|
||||
// ProcessExternalPayment handles callback from payment gateway
|
||||
func (s *order) ProcessExternalPayment(ctx context.Context, orderID int64, externalID string) error {
|
||||
func (s *order) ProcessExternalPayment(ctx context.Context, tenantID, orderID int64, externalID string) error {
|
||||
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(orderID)).First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
if tenantID > 0 && o.TenantID > 0 && o.TenantID != tenantID {
|
||||
return errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
if o.Status != consts.OrderStatusCreated {
|
||||
return nil // Already processed idempotency
|
||||
}
|
||||
@@ -365,7 +393,7 @@ func (s *order) settleOrder(ctx context.Context, o *models.Order, method, extern
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *order) Status(ctx context.Context, id int64) (*transaction_dto.OrderStatusResponse, error) {
|
||||
func (s *order) Status(ctx context.Context, tenantID, id int64) (*transaction_dto.OrderStatusResponse, error) {
|
||||
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -373,6 +401,9 @@ func (s *order) Status(ctx context.Context, id int64) (*transaction_dto.OrderSta
|
||||
}
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
if tenantID > 0 && o.TenantID > 0 && o.TenantID != tenantID {
|
||||
return nil, errorx.ErrForbidden.WithMsg("租户不匹配")
|
||||
}
|
||||
|
||||
return &transaction_dto.OrderStatusResponse{
|
||||
Status: string(o.Status),
|
||||
|
||||
@@ -39,6 +39,7 @@ func Test_Order(t *testing.T) {
|
||||
func (s *OrderTestSuite) Test_PurchaseFlow() {
|
||||
Convey("Purchase Flow", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(ctx, s.DB,
|
||||
models.TableNameOrder, models.TableNameOrderItem, models.TableNameUser,
|
||||
models.TableNameContent, models.TableNameContentPrice, models.TableNameTenant,
|
||||
@@ -57,6 +58,7 @@ func (s *OrderTestSuite) Test_PurchaseFlow() {
|
||||
Status: consts.TenantStatusVerified,
|
||||
}
|
||||
models.TenantQuery.WithContext(ctx).Create(tenant)
|
||||
tenantID = tenant.ID
|
||||
// Content
|
||||
content := &models.Content{
|
||||
TenantID: tenant.ID,
|
||||
@@ -83,7 +85,7 @@ func (s *OrderTestSuite) Test_PurchaseFlow() {
|
||||
Convey("should create and pay order successfully", func() {
|
||||
// Step 1: Create Order
|
||||
form := &order_dto.OrderCreateForm{ContentID: content.ID}
|
||||
createRes, err := Order.Create(ctx, buyer.ID, form)
|
||||
createRes, err := Order.Create(ctx, tenantID, buyer.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
So(createRes.OrderID, ShouldNotBeEmpty)
|
||||
|
||||
@@ -95,7 +97,7 @@ func (s *OrderTestSuite) Test_PurchaseFlow() {
|
||||
|
||||
// Step 2: Pay Order
|
||||
payForm := &order_dto.OrderPayForm{Method: "balance"}
|
||||
_, err = Order.Pay(ctx, buyer.ID, createRes.OrderID, payForm)
|
||||
_, err = Order.Pay(ctx, tenantID, buyer.ID, createRes.OrderID, payForm)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Order Paid
|
||||
@@ -130,11 +132,11 @@ func (s *OrderTestSuite) Test_PurchaseFlow() {
|
||||
Update(models.UserQuery.Balance, 500)
|
||||
|
||||
form := &order_dto.OrderCreateForm{ContentID: content.ID}
|
||||
createRes, err := Order.Create(ctx, buyer.ID, form)
|
||||
createRes, err := Order.Create(ctx, tenantID, buyer.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
payForm := &order_dto.OrderPayForm{Method: "balance"}
|
||||
_, err = Order.Pay(ctx, buyer.ID, createRes.OrderID, payForm)
|
||||
_, err = Order.Pay(ctx, tenantID, buyer.ID, createRes.OrderID, payForm)
|
||||
So(err, ShouldNotBeNil)
|
||||
// Error should be QuotaExceeded or similar
|
||||
})
|
||||
@@ -144,6 +146,7 @@ func (s *OrderTestSuite) Test_PurchaseFlow() {
|
||||
func (s *OrderTestSuite) Test_OrderDetails() {
|
||||
Convey("Order Details", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -164,6 +167,7 @@ func (s *OrderTestSuite) Test_OrderDetails() {
|
||||
models.UserQuery.WithContext(ctx).Create(creator)
|
||||
tenant := &models.Tenant{UserID: creator.ID, Name: "Best Shop", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(tenant)
|
||||
tenantID = tenant.ID
|
||||
content := &models.Content{
|
||||
TenantID: tenant.ID,
|
||||
UserID: creator.ID,
|
||||
@@ -199,13 +203,14 @@ func (s *OrderTestSuite) Test_OrderDetails() {
|
||||
// Create & Pay
|
||||
createRes, _ := Order.Create(
|
||||
ctx,
|
||||
tenantID,
|
||||
buyer.ID,
|
||||
&order_dto.OrderCreateForm{ContentID: content.ID},
|
||||
)
|
||||
Order.Pay(ctx, buyer.ID, createRes.OrderID, &order_dto.OrderPayForm{Method: "balance"})
|
||||
Order.Pay(ctx, tenantID, buyer.ID, createRes.OrderID, &order_dto.OrderPayForm{Method: "balance"})
|
||||
|
||||
// Get Detail
|
||||
detail, err := Order.GetUserOrder(ctx, buyer.ID, createRes.OrderID)
|
||||
detail, err := Order.GetUserOrder(ctx, tenantID, buyer.ID, createRes.OrderID)
|
||||
So(err, ShouldBeNil)
|
||||
So(detail.TenantName, ShouldEqual, "Best Shop")
|
||||
So(len(detail.Items), ShouldEqual, 1)
|
||||
@@ -219,6 +224,7 @@ func (s *OrderTestSuite) Test_OrderDetails() {
|
||||
func (s *OrderTestSuite) Test_PlatformCommission() {
|
||||
Convey("Platform Commission", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -236,6 +242,7 @@ func (s *OrderTestSuite) Test_PlatformCommission() {
|
||||
// Tenant
|
||||
t := &models.Tenant{UserID: creator.ID, Name: "Shop C", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
// Buyer
|
||||
buyer := &models.User{Username: "buyer_c", Balance: 2000}
|
||||
models.UserQuery.WithContext(ctx).Create(buyer)
|
||||
@@ -253,7 +260,7 @@ func (s *OrderTestSuite) Test_PlatformCommission() {
|
||||
|
||||
Convey("should deduct 10% fee", func() {
|
||||
payForm := &order_dto.OrderPayForm{Method: "balance"}
|
||||
_, err := Order.Pay(ctx, buyer.ID, o.ID, payForm)
|
||||
_, err := Order.Pay(ctx, tenantID, buyer.ID, o.ID, payForm)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Creator Balance (1000 - 10% = 900)
|
||||
@@ -270,6 +277,7 @@ func (s *OrderTestSuite) Test_PlatformCommission() {
|
||||
func (s *OrderTestSuite) Test_ExternalPayment() {
|
||||
Convey("External Payment", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(
|
||||
ctx,
|
||||
s.DB,
|
||||
@@ -287,6 +295,7 @@ func (s *OrderTestSuite) Test_ExternalPayment() {
|
||||
// Tenant
|
||||
t := &models.Tenant{UserID: creator.ID, Name: "Shop Ext", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
// Buyer (Balance 0)
|
||||
buyer := &models.User{Username: "buyer_ext", Balance: 0}
|
||||
models.UserQuery.WithContext(ctx).Create(buyer)
|
||||
@@ -302,7 +311,7 @@ func (s *OrderTestSuite) Test_ExternalPayment() {
|
||||
models.OrderItemQuery.WithContext(ctx).Create(&models.OrderItem{OrderID: o.ID, ContentID: 999})
|
||||
|
||||
Convey("should process external payment callback", func() {
|
||||
err := Order.ProcessExternalPayment(ctx, o.ID, "ext_tx_id_123")
|
||||
err := Order.ProcessExternalPayment(ctx, tenantID, o.ID, "ext_tx_id_123")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify Status
|
||||
|
||||
@@ -704,7 +704,7 @@ func (s *super) RefundOrder(ctx context.Context, id int64, form *super_dto.Super
|
||||
return errorx.ErrRecordNotFound.WithMsg("租户不存在")
|
||||
}
|
||||
|
||||
return Creator.ProcessRefund(ctx, t.UserID, id, &v1_dto.RefundForm{
|
||||
return Creator.ProcessRefund(ctx, t.ID, t.UserID, id, &v1_dto.RefundForm{
|
||||
Action: "accept",
|
||||
Reason: form.Reason,
|
||||
})
|
||||
|
||||
@@ -17,9 +17,12 @@ import (
|
||||
// @provider
|
||||
type tenant struct{}
|
||||
|
||||
func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*requests.Pager, error) {
|
||||
func (s *tenant) List(ctx context.Context, tenantID int64, filter *dto.TenantListFilter) (*requests.Pager, error) {
|
||||
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||
q = q.Where(tbl.Status.Eq(consts.TenantStatusVerified))
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(tenantID))
|
||||
}
|
||||
|
||||
if filter.Keyword != nil && *filter.Keyword != "" {
|
||||
q = q.Where(tbl.Name.Like("%" + *filter.Keyword + "%"))
|
||||
@@ -73,8 +76,8 @@ func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*reque
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *tenant) GetPublicProfile(ctx context.Context, userID, id int64) (*dto.TenantProfile, error) {
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(id)).First()
|
||||
func (s *tenant) GetPublicProfile(ctx context.Context, tenantID, userID int64) (*dto.TenantProfile, error) {
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenantID)).First()
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errorx.ErrRecordNotFound
|
||||
@@ -83,9 +86,9 @@ func (s *tenant) GetPublicProfile(ctx context.Context, userID, id int64) (*dto.T
|
||||
}
|
||||
|
||||
// Stats
|
||||
followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(id)).Count()
|
||||
followers, _ := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.TenantID.Eq(tenantID)).Count()
|
||||
contents, _ := models.ContentQuery.WithContext(ctx).
|
||||
Where(models.ContentQuery.TenantID.Eq(id), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).
|
||||
Where(models.ContentQuery.TenantID.Eq(tenantID), models.ContentQuery.Status.Eq(consts.ContentStatusPublished)).
|
||||
Count()
|
||||
|
||||
// Following status
|
||||
@@ -93,7 +96,7 @@ func (s *tenant) GetPublicProfile(ctx context.Context, userID, id int64) (*dto.T
|
||||
if userID > 0 {
|
||||
uid := userID
|
||||
isFollowing, _ = models.TenantUserQuery.WithContext(ctx).
|
||||
Where(models.TenantUserQuery.TenantID.Eq(id), models.TenantUserQuery.UserID.Eq(uid)).
|
||||
Where(models.TenantUserQuery.TenantID.Eq(tenantID), models.TenantUserQuery.UserID.Eq(uid)).
|
||||
Exists()
|
||||
}
|
||||
|
||||
@@ -113,20 +116,20 @@ func (s *tenant) GetPublicProfile(ctx context.Context, userID, id int64) (*dto.T
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *tenant) Follow(ctx context.Context, userID, id int64) error {
|
||||
func (s *tenant) Follow(ctx context.Context, tenantID, userID int64) error {
|
||||
if userID == 0 {
|
||||
return errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
// Check if tenant exists
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(id)).First()
|
||||
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(tenantID)).First()
|
||||
if err != nil {
|
||||
return errorx.ErrRecordNotFound
|
||||
}
|
||||
|
||||
tu := &models.TenantUser{
|
||||
TenantID: id,
|
||||
TenantID: tenantID,
|
||||
UserID: uid,
|
||||
Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember},
|
||||
Status: consts.UserStatusVerified,
|
||||
@@ -142,14 +145,14 @@ func (s *tenant) Follow(ctx context.Context, userID, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenant) Unfollow(ctx context.Context, userID, id int64) error {
|
||||
func (s *tenant) Unfollow(ctx context.Context, tenantID, userID int64) error {
|
||||
if userID == 0 {
|
||||
return errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
_, err := models.TenantUserQuery.WithContext(ctx).
|
||||
Where(models.TenantUserQuery.TenantID.Eq(id), models.TenantUserQuery.UserID.Eq(uid)).
|
||||
Where(models.TenantUserQuery.TenantID.Eq(tenantID), models.TenantUserQuery.UserID.Eq(uid)).
|
||||
Delete()
|
||||
if err != nil {
|
||||
return errorx.ErrDatabaseError.WithCause(err)
|
||||
@@ -157,14 +160,18 @@ func (s *tenant) Unfollow(ctx context.Context, userID, id int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tenant) ListFollowed(ctx context.Context, userID int64) ([]dto.TenantProfile, error) {
|
||||
func (s *tenant) ListFollowed(ctx context.Context, tenantID, userID int64) ([]dto.TenantProfile, error) {
|
||||
if userID == 0 {
|
||||
return nil, errorx.ErrUnauthorized
|
||||
}
|
||||
uid := userID
|
||||
|
||||
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
||||
list, err := q.Where(tbl.UserID.Eq(uid)).Find()
|
||||
q = q.Where(tbl.UserID.Eq(uid))
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(tenantID))
|
||||
}
|
||||
list, err := q.Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func Test_Tenant(t *testing.T) {
|
||||
func (s *TenantTestSuite) Test_Follow() {
|
||||
Convey("Follow Flow", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(0)
|
||||
database.Truncate(ctx, s.DB, models.TableNameTenant, models.TableNameTenantUser, models.TableNameUser)
|
||||
|
||||
// User
|
||||
@@ -49,29 +50,30 @@ func (s *TenantTestSuite) Test_Follow() {
|
||||
// Tenant
|
||||
t := &models.Tenant{Name: "Tenant A", Status: consts.TenantStatusVerified}
|
||||
models.TenantQuery.WithContext(ctx).Create(t)
|
||||
tenantID = t.ID
|
||||
|
||||
Convey("should follow tenant", func() {
|
||||
err := Tenant.Follow(ctx, u.ID, t.ID)
|
||||
err := Tenant.Follow(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify stats
|
||||
profile, err := Tenant.GetPublicProfile(ctx, u.ID, t.ID)
|
||||
profile, err := Tenant.GetPublicProfile(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(profile.IsFollowing, ShouldBeTrue)
|
||||
So(profile.Stats.Followers, ShouldEqual, 1)
|
||||
|
||||
// List Followed
|
||||
list, err := Tenant.ListFollowed(ctx, u.ID)
|
||||
list, err := Tenant.ListFollowed(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(len(list), ShouldEqual, 1)
|
||||
So(list[0].Name, ShouldEqual, "Tenant A")
|
||||
|
||||
// Unfollow
|
||||
err = Tenant.Unfollow(ctx, u.ID, t.ID)
|
||||
err = Tenant.Unfollow(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Verify
|
||||
profile, err = Tenant.GetPublicProfile(ctx, u.ID, t.ID)
|
||||
profile, err = Tenant.GetPublicProfile(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(profile.IsFollowing, ShouldBeFalse)
|
||||
So(profile.Stats.Followers, ShouldEqual, 0)
|
||||
|
||||
@@ -31,7 +31,7 @@ func (s *user) SendOTP(ctx context.Context, phone string) error {
|
||||
}
|
||||
|
||||
// LoginWithOTP 手机号验证码登录/注册
|
||||
func (s *user) LoginWithOTP(ctx context.Context, phone, otp string) (*auth_dto.LoginResponse, error) {
|
||||
func (s *user) LoginWithOTP(ctx context.Context, tenantID int64, phone, otp string) (*auth_dto.LoginResponse, error) {
|
||||
// 1. 校验验证码 (模拟:固定 123456)
|
||||
if otp != "1234" {
|
||||
return nil, errorx.ErrInvalidCredentials.WithMsg("验证码错误")
|
||||
@@ -68,7 +68,7 @@ func (s *user) LoginWithOTP(ctx context.Context, phone, otp string) (*auth_dto.L
|
||||
// 4. 生成 Token
|
||||
token, err := s.jwt.CreateToken(s.jwt.CreateClaims(jwt.BaseClaims{
|
||||
UserID: u.ID,
|
||||
// TenantID: 0, // 初始登录无租户上下文
|
||||
TenantID: tenantID,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
|
||||
|
||||
@@ -40,11 +40,12 @@ func Test_User(t *testing.T) {
|
||||
func (s *UserTestSuite) Test_LoginWithOTP() {
|
||||
Convey("LoginWithOTP", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser)
|
||||
|
||||
Convey("should create user and login success with correct OTP", func() {
|
||||
phone := "13800138000"
|
||||
resp, err := User.LoginWithOTP(ctx, phone, "1234")
|
||||
resp, err := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp, ShouldNotBeNil)
|
||||
So(resp.Token, ShouldNotBeEmpty)
|
||||
@@ -55,17 +56,17 @@ func (s *UserTestSuite) Test_LoginWithOTP() {
|
||||
Convey("should login existing user", func() {
|
||||
phone := "13800138001"
|
||||
// Pre-create user
|
||||
_, err := User.LoginWithOTP(ctx, phone, "1234")
|
||||
_, err := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Login again
|
||||
resp, err := User.LoginWithOTP(ctx, phone, "1234")
|
||||
resp, err := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
So(err, ShouldBeNil)
|
||||
So(resp.User.Phone, ShouldEqual, phone)
|
||||
})
|
||||
|
||||
Convey("should fail with incorrect OTP", func() {
|
||||
resp, err := User.LoginWithOTP(ctx, "13800138002", "000000")
|
||||
resp, err := User.LoginWithOTP(ctx, tenantID, "13800138002", "000000")
|
||||
So(err, ShouldNotBeNil)
|
||||
So(resp, ShouldBeNil)
|
||||
})
|
||||
@@ -75,11 +76,12 @@ func (s *UserTestSuite) Test_LoginWithOTP() {
|
||||
func (s *UserTestSuite) Test_Me() {
|
||||
Convey("Me", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser)
|
||||
|
||||
// Create user
|
||||
phone := "13800138003"
|
||||
resp, _ := User.LoginWithOTP(ctx, phone, "1234")
|
||||
resp, _ := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
userID := resp.User.ID
|
||||
|
||||
Convey("should return user profile", func() {
|
||||
@@ -104,10 +106,11 @@ func (s *UserTestSuite) Test_Me() {
|
||||
func (s *UserTestSuite) Test_Update() {
|
||||
Convey("Update", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser)
|
||||
|
||||
phone := "13800138004"
|
||||
resp, _ := User.LoginWithOTP(ctx, phone, "1234")
|
||||
resp, _ := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
userID := resp.User.ID
|
||||
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
|
||||
|
||||
@@ -132,10 +135,11 @@ func (s *UserTestSuite) Test_Update() {
|
||||
func (s *UserTestSuite) Test_RealName() {
|
||||
Convey("RealName", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser)
|
||||
|
||||
phone := "13800138005"
|
||||
resp, _ := User.LoginWithOTP(ctx, phone, "1234")
|
||||
resp, _ := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
userID := resp.User.ID
|
||||
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
|
||||
|
||||
@@ -157,10 +161,11 @@ func (s *UserTestSuite) Test_RealName() {
|
||||
func (s *UserTestSuite) Test_GetNotifications() {
|
||||
Convey("GetNotifications", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameNotification)
|
||||
|
||||
phone := "13800138006"
|
||||
resp, _ := User.LoginWithOTP(ctx, phone, "1234")
|
||||
resp, _ := User.LoginWithOTP(ctx, tenantID, phone, "1234")
|
||||
userID := resp.User.ID
|
||||
ctx = context.WithValue(ctx, consts.CtxKeyUser, userID)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
// @provider
|
||||
type wallet struct{}
|
||||
|
||||
func (s *wallet) GetWallet(ctx context.Context, userID int64) (*user_dto.WalletResponse, error) {
|
||||
func (s *wallet) GetWallet(ctx context.Context, tenantID, userID int64) (*user_dto.WalletResponse, error) {
|
||||
// Get Balance
|
||||
u, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(userID)).First()
|
||||
if err != nil {
|
||||
@@ -33,7 +33,13 @@ func (s *wallet) GetWallet(ctx context.Context, userID int64) (*user_dto.WalletR
|
||||
// Get Transactions (Orders)
|
||||
// Both purchase (expense) and recharge (income - if paid)
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
orders, err := q.Where(tbl.UserID.Eq(userID), tbl.Status.Eq(consts.OrderStatusPaid)).
|
||||
if tenantID > 0 {
|
||||
q = q.Where(tbl.UserID.Eq(userID), tbl.Status.Eq(consts.OrderStatusPaid), tbl.TenantID.Eq(tenantID)).
|
||||
Or(tbl.UserID.Eq(userID), tbl.Status.Eq(consts.OrderStatusPaid), tbl.Type.Eq(consts.OrderTypeRecharge))
|
||||
} else {
|
||||
q = q.Where(tbl.UserID.Eq(userID), tbl.Status.Eq(consts.OrderStatusPaid))
|
||||
}
|
||||
orders, err := q.
|
||||
Order(tbl.CreatedAt.Desc()).
|
||||
Limit(20). // Limit to recent 20
|
||||
Find()
|
||||
@@ -71,6 +77,7 @@ func (s *wallet) GetWallet(ctx context.Context, userID int64) (*user_dto.WalletR
|
||||
|
||||
func (s *wallet) Recharge(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
userID int64,
|
||||
form *user_dto.RechargeForm,
|
||||
) (*user_dto.RechargeResponse, error) {
|
||||
@@ -98,7 +105,7 @@ func (s *wallet) Recharge(
|
||||
|
||||
// MOCK: Automatically pay for recharge order to close the loop
|
||||
// In production, this would be a callback from payment gateway
|
||||
if err := Order.ProcessExternalPayment(ctx, order.ID, "mock_auto_pay"); err != nil {
|
||||
if err := Order.ProcessExternalPayment(ctx, tenantID, order.ID, "mock_auto_pay"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ func Test_Wallet(t *testing.T) {
|
||||
func (s *WalletTestSuite) Test_GetWallet() {
|
||||
Convey("GetWallet", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameOrder)
|
||||
|
||||
u := &models.User{Username: "wallet_user", Balance: 5000} // 50.00
|
||||
@@ -58,7 +59,7 @@ func (s *WalletTestSuite) Test_GetWallet() {
|
||||
models.OrderQuery.WithContext(ctx).Create(o1, o2)
|
||||
|
||||
Convey("should return balance and transactions", func() {
|
||||
res, err := Wallet.GetWallet(ctx, u.ID)
|
||||
res, err := Wallet.GetWallet(ctx, tenantID, u.ID)
|
||||
So(err, ShouldBeNil)
|
||||
So(res.Balance, ShouldEqual, 50.0)
|
||||
So(len(res.Transactions), ShouldEqual, 2)
|
||||
@@ -74,6 +75,7 @@ func (s *WalletTestSuite) Test_GetWallet() {
|
||||
func (s *WalletTestSuite) Test_Recharge() {
|
||||
Convey("Recharge", s.T(), func() {
|
||||
ctx := s.T().Context()
|
||||
tenantID := int64(1)
|
||||
database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameOrder)
|
||||
|
||||
u := &models.User{Username: "recharge_user"}
|
||||
@@ -82,7 +84,7 @@ func (s *WalletTestSuite) Test_Recharge() {
|
||||
|
||||
Convey("should create recharge order", func() {
|
||||
form := &user_dto.RechargeForm{Amount: 100.0}
|
||||
res, err := Wallet.Recharge(ctx, u.ID, form)
|
||||
res, err := Wallet.Recharge(ctx, tenantID, u.ID, form)
|
||||
So(err, ShouldBeNil)
|
||||
So(res.OrderID, ShouldNotBeEmpty)
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
<nav class="fixed top-0 w-full z-50 bg-white border-b border-slate-200 h-16">
|
||||
<div class="mx-auto max-w-screen-xl h-full flex items-center justify-between">
|
||||
<!-- Left: Logo -->
|
||||
<router-link to="/" class="flex items-center gap-2">
|
||||
<router-link :to="tenantRoute('/')" class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center text-white font-bold text-xl">Q</div>
|
||||
<span class="text-xl font-bold text-slate-900 hidden sm:block">Quyun</span>
|
||||
</router-link>
|
||||
|
||||
<!-- Center-Left: Nav Links (Desktop) -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<router-link to="/" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">首页</router-link>
|
||||
<router-link to="/explore" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">发现</router-link>
|
||||
<router-link to="/topics" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">专题</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">首页</router-link>
|
||||
<router-link :to="tenantRoute('/explore')" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">发现</router-link>
|
||||
<router-link :to="tenantRoute('/topics')" class="text-slate-600 font-medium hover:text-primary-600" active-class="text-primary-600">专题</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Center-Right: Global Search -->
|
||||
@@ -30,13 +30,13 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<template v-if="isLoggedIn">
|
||||
<!-- Notification -->
|
||||
<router-link to="/me/notifications" class="relative w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-50 text-slate-600">
|
||||
<router-link :to="tenantRoute('/me/notifications')" class="relative w-10 h-10 flex items-center justify-center rounded-full hover:bg-slate-50 text-slate-600">
|
||||
<i class="pi pi-bell text-xl"></i>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
|
||||
</router-link>
|
||||
|
||||
<!-- Creator Entry -->
|
||||
<router-link to="/creator/apply" class="hidden sm:flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200">
|
||||
<router-link :to="tenantRoute('/creator/apply')" class="hidden sm:flex items-center gap-1 px-3 py-1.5 text-sm font-medium text-slate-600 hover:bg-slate-50 rounded-lg border border-slate-200">
|
||||
<i class="pi pi-pencil"></i>
|
||||
<span>创作</span>
|
||||
</router-link>
|
||||
@@ -53,8 +53,8 @@
|
||||
<p class="text-sm font-bold text-slate-900">{{ user.nickname }}</p>
|
||||
<p class="text-xs text-slate-500 truncate">{{ user.phone }}</p>
|
||||
</div>
|
||||
<router-link to="/me" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">个人中心</router-link>
|
||||
<router-link to="/creator" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">创作者中心</router-link>
|
||||
<router-link :to="tenantRoute('/me')" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">个人中心</router-link>
|
||||
<router-link :to="tenantRoute('/creator')" class="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">创作者中心</router-link>
|
||||
<div class="border-t border-slate-50 mt-1"></div>
|
||||
<button @click="logout" class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50">退出登录</button>
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<router-link to="/auth/login" class="bg-primary-600 text-white px-6 py-2 rounded-full font-medium hover:bg-primary-700 transition-all shadow-sm shadow-primary-100 active:scale-95">登录 / 注册</router-link>
|
||||
<router-link :to="tenantRoute('/auth/login')" class="bg-primary-600 text-white px-6 py-2 rounded-full font-medium hover:bg-primary-700 transition-all shadow-sm shadow-primary-100 active:scale-95">登录 / 注册</router-link>
|
||||
</template>
|
||||
|
||||
<!-- Mobile Menu Button -->
|
||||
@@ -78,11 +78,13 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const isLoggedIn = ref(false);
|
||||
const user = ref({});
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const checkAuth = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -113,6 +115,6 @@ const logout = () => {
|
||||
localStorage.removeItem('user');
|
||||
isLoggedIn.value = false;
|
||||
user.value = {};
|
||||
router.push('/');
|
||||
router.push(tenantRoute('/'));
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<!-- Menus -->
|
||||
<nav class="p-4 space-y-1 flex-1">
|
||||
<router-link to="/creator"
|
||||
<router-link :to="tenantRoute('/creator')"
|
||||
exact-active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
|
||||
<i class="pi pi-th-large text-lg group-hover:scale-110 transition-transform"></i>
|
||||
@@ -32,13 +32,13 @@
|
||||
|
||||
<div class="px-4 py-2 text-xs font-bold text-slate-500 uppercase tracking-wider mt-4">内容与交易</div>
|
||||
|
||||
<router-link to="/creator/contents"
|
||||
<router-link :to="tenantRoute('/creator/contents')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
|
||||
<i class="pi pi-file-edit text-lg group-hover:scale-110 transition-transform"></i>
|
||||
<span class="font-medium">内容管理</span>
|
||||
</router-link>
|
||||
<router-link to="/creator/orders"
|
||||
<router-link :to="tenantRoute('/creator/orders')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
|
||||
<i class="pi pi-shopping-cart text-lg group-hover:scale-110 transition-transform"></i>
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<div class="px-4 py-2 text-xs font-bold text-slate-500 uppercase tracking-wider mt-4">配置</div>
|
||||
|
||||
<router-link to="/creator/settings"
|
||||
<router-link :to="tenantRoute('/creator/settings')"
|
||||
active-class="bg-primary-600 text-white shadow-md shadow-primary-900/20"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-slate-800 hover:text-white transition-all group">
|
||||
<i class="pi pi-cog text-lg group-hover:scale-110 transition-transform"></i>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<!-- Footer Link -->
|
||||
<div class="p-4 border-t border-slate-800">
|
||||
<router-link :to="'/t/' + (tenantId || '1')"
|
||||
<router-link :to="tenantRoute('/')"
|
||||
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">
|
||||
<i class="pi pi-external-link"></i> 预览我的主页
|
||||
</router-link>
|
||||
@@ -76,27 +76,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import AppFooter from '../components/AppFooter.vue';
|
||||
import TopNavbar from '../components/TopNavbar.vue';
|
||||
import { creatorApi } from '../api/creator';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantId = ref('');
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const isFullWidth = computed(() => {
|
||||
return ['creator-content-new', 'creator-content-edit'].includes(route.name);
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await creatorApi.getSettings();
|
||||
if (res && res.id) {
|
||||
tenantId.value = res.id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,52 +20,52 @@
|
||||
|
||||
<!-- Menus -->
|
||||
<nav class="p-4 space-y-1">
|
||||
<router-link to="/me" exact-active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me')" exact-active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-home text-lg"></i>
|
||||
<span>概览</span>
|
||||
</router-link>
|
||||
<router-link to="/me/orders" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/orders')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-shopping-bag text-lg"></i>
|
||||
<span>我的订单</span>
|
||||
</router-link>
|
||||
<router-link to="/me/wallet" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/wallet')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-wallet text-lg"></i>
|
||||
<span>我的钱包</span>
|
||||
</router-link>
|
||||
<router-link to="/me/coupons" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/coupons')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-ticket text-lg"></i>
|
||||
<span>我的优惠券</span>
|
||||
</router-link>
|
||||
<router-link to="/me/library" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/library')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-book text-lg"></i>
|
||||
<span>已购内容</span>
|
||||
</router-link>
|
||||
<router-link to="/me/favorites" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/favorites')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-heart text-lg"></i>
|
||||
<span>我的收藏</span>
|
||||
</router-link>
|
||||
<router-link to="/me/likes" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/likes')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-thumbs-up text-lg"></i>
|
||||
<span>我的点赞</span>
|
||||
</router-link>
|
||||
<router-link to="/me/notifications" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<router-link :to="tenantRoute('/me/notifications')" active-class="bg-primary-50 text-primary-600 font-semibold" class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-bell text-lg"></i>
|
||||
<span>消息中心</span>
|
||||
</router-link>
|
||||
<div class="my-2 border-t border-slate-100"></div>
|
||||
<router-link to="/me/profile" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/profile')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-user text-lg"></i>
|
||||
<span>个人资料</span>
|
||||
</router-link>
|
||||
<router-link to="/me/security" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
<router-link :to="tenantRoute('/me/security')" active-class="bg-primary-50 text-primary-600 font-semibold"
|
||||
class="flex items-center gap-3 px-4 py-3 rounded-lg text-slate-600 hover:bg-slate-50 transition-colors">
|
||||
<i class="pi pi-shield text-lg"></i>
|
||||
<span>账号安全</span>
|
||||
@@ -86,8 +86,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import AppFooter from '../components/AppFooter.vue';
|
||||
import TopNavbar from '../components/TopNavbar.vue';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const user = ref(JSON.parse(localStorage.getItem('user') || '{}'));
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@ const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
path: '/t/:tenantCode',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
@@ -49,7 +49,7 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/auth',
|
||||
path: '/t/:tenantCode/auth',
|
||||
component: LayoutAuth,
|
||||
children: [
|
||||
{
|
||||
@@ -60,7 +60,7 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/me',
|
||||
path: '/t/:tenantCode/me',
|
||||
component: LayoutUser,
|
||||
children: [
|
||||
{
|
||||
@@ -121,7 +121,7 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/creator',
|
||||
path: '/t/:tenantCode/creator',
|
||||
component: LayoutCreator,
|
||||
children: [
|
||||
{
|
||||
@@ -157,7 +157,7 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/checkout',
|
||||
path: '/t/:tenantCode/checkout',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
@@ -168,7 +168,7 @@ const router = createRouter({
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/payment/:id',
|
||||
path: '/t/:tenantCode/payment/:id',
|
||||
component: LayoutMain,
|
||||
children: [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Simple Fetch Wrapper
|
||||
const BASE_URL = '/v1';
|
||||
import { getTenantCode } from './tenant';
|
||||
|
||||
export async function request(endpoint, options = {}) {
|
||||
const tenantCode = getTenantCode();
|
||||
const baseUrl = tenantCode ? `/t/${tenantCode}/v1` : '/v1';
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const headers = {
|
||||
@@ -22,7 +24,7 @@ export async function request(endpoint, options = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${BASE_URL}${endpoint}`, {
|
||||
const res = await fetch(`${baseUrl}${endpoint}`, {
|
||||
...options,
|
||||
headers
|
||||
});
|
||||
@@ -40,9 +42,10 @@ export async function request(endpoint, options = {}) {
|
||||
if (res.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
const loginPath = tenantCode ? `/t/${tenantCode}/auth/login` : '/auth/login';
|
||||
// Redirect to login if not already there
|
||||
if (!window.location.pathname.startsWith('/auth/login')) {
|
||||
window.location.href = '/auth/login';
|
||||
if (!window.location.pathname.includes('/auth/login')) {
|
||||
window.location.href = loginPath;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
frontend/portal/src/utils/tenant.js
Normal file
18
frontend/portal/src/utils/tenant.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function getTenantCode() {
|
||||
const match = window.location.pathname.match(/^\/t\/([^/]+)(?:\/|$)/);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
export function resolveTenantCode(route) {
|
||||
if (route && route.params && route.params.tenantCode) {
|
||||
return String(route.params.tenantCode);
|
||||
}
|
||||
return getTenantCode();
|
||||
}
|
||||
|
||||
export function tenantPath(path, route) {
|
||||
const tenantCode = resolveTenantCode(route);
|
||||
const base = tenantCode ? `/t/${tenantCode}` : '';
|
||||
const normalized = path.startsWith('/') ? path : `/${path}`;
|
||||
return `${base}${normalized}`;
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div v-for="item in contents" :key="item.id" class="bg-white rounded-xl border border-slate-100 overflow-hidden hover:shadow-lg transition-all group cursor-pointer active:scale-[0.99]" @click="$router.push(`/contents/${item.id}`)">
|
||||
<div v-for="item in contents" :key="item.id" class="bg-white rounded-xl border border-slate-100 overflow-hidden hover:shadow-lg transition-all group cursor-pointer active:scale-[0.99]" @click="$router.push(tenantRoute(`/contents/${item.id}`))">
|
||||
<!-- Cover -->
|
||||
<div class="aspect-video bg-slate-100 relative">
|
||||
<img :src="item.cover || `https://images.unsplash.com/photo-1514306191717-452ec28c7f31?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60`" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105">
|
||||
@@ -92,8 +92,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { contentApi } from '../api/content';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const selectedGenre = ref('全部');
|
||||
const selectedPrice = ref('all');
|
||||
const sort = ref('latest');
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { contentApi } from '../../api/content';
|
||||
import { tenantApi } from '../../api/tenant';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const contents = ref([]);
|
||||
const bannerItems = ref([]);
|
||||
const trendingItems = ref([]);
|
||||
@@ -97,7 +101,7 @@ onMounted(fetchData);
|
||||
<div class="absolute bottom-0 left-0 p-10 max-w-2xl text-white">
|
||||
<div class="inline-block px-3 py-1 bg-red-600 text-white text-xs font-bold rounded mb-3">置顶推荐</div>
|
||||
<h2 class="text-4xl font-bold mb-4 leading-tight cursor-pointer hover:underline"
|
||||
@click="$router.push(`/contents/${item.id}`)">{{ item.title }}</h2>
|
||||
@click="$router.push(tenantRoute(`/contents/${item.id}`))">{{ item.title }}</h2>
|
||||
<p class="text-lg text-slate-200 line-clamp-2">{{ item.description || item.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,7 +171,7 @@ onMounted(fetchData);
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div v-for="creator in matchedCreators" :key="creator.id"
|
||||
class="flex items-center gap-3 p-3 rounded-xl hover:bg-slate-50 transition-colors cursor-pointer border border-transparent hover:border-slate-200"
|
||||
@click="$router.push(`/creators/${creator.id}`)">
|
||||
@click="$router.push(tenantRoute(`/t/${creator.id}`))">
|
||||
<img :src="creator.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${creator.id}`"
|
||||
class="w-12 h-12 rounded-full border border-slate-100">
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -178,7 +182,7 @@ onMounted(fetchData);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<router-link v-for="item in contents" :key="item.id" :to="`/contents/${item.id}`"
|
||||
<router-link v-for="item in contents" :key="item.id" :to="tenantRoute(`/contents/${item.id}`)"
|
||||
class="block bg-white rounded-2xl shadow-sm border border-slate-100 p-6 hover:shadow-xl hover:border-primary-100 transition-all duration-300 group cursor-pointer active:scale-[0.99]">
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1 min-w-0 flex flex-col">
|
||||
@@ -254,10 +258,10 @@ onMounted(fetchData);
|
||||
<div class="space-y-4">
|
||||
<div v-for="creator in recommendedCreators" :key="creator.id" class="flex items-center gap-3">
|
||||
<img :src="creator.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${creator.id}`"
|
||||
class="w-10 h-10 rounded-full cursor-pointer" @click="$router.push(`/creators/${creator.id}`)">
|
||||
class="w-10 h-10 rounded-full cursor-pointer" @click="$router.push(tenantRoute(`/t/${creator.id}`))">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-bold text-slate-900 text-sm truncate hover:text-primary-600 cursor-pointer"
|
||||
@click="$router.push(`/creators/${creator.id}`)">{{ creator.name }}</div>
|
||||
@click="$router.push(tenantRoute(`/t/${creator.id}`))">{{ creator.name }}</div>
|
||||
<div class="text-xs text-slate-500 truncate">粉丝 {{ creator.stats?.followers || 0 }}</div>
|
||||
</div>
|
||||
<button
|
||||
@@ -278,7 +282,7 @@ onMounted(fetchData);
|
||||
:class="index === 0 ? 'text-red-500' : (index === 1 ? 'text-orange-500' : 'text-yellow-500')">{{
|
||||
index + 1 }}</span>
|
||||
<div class="flex-1">
|
||||
<h4 @click="$router.push(`/contents/${item.id}`)"
|
||||
<h4 @click="$router.push(tenantRoute(`/contents/${item.id}`))"
|
||||
class="text-sm font-medium text-slate-800 line-clamp-2 hover:text-primary-600 cursor-pointer">
|
||||
{{ item.title }}</h4>
|
||||
<span class="text-xs text-slate-400 mt-1 block">{{ item.views }} 阅读</span>
|
||||
@@ -301,4 +305,3 @@ onMounted(fetchData);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 1. Hero Topic -->
|
||||
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden mb-12 group cursor-pointer shadow-lg" @click="$router.push(`/explore?topic=${topics[0].id}`)">
|
||||
<div class="relative w-full h-[400px] rounded-2xl overflow-hidden mb-12 group cursor-pointer shadow-lg" @click="$router.push(tenantRoute(`/explore?topic=${topics[0].id}`))">
|
||||
<img :src="topics[0].cover" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/20 to-transparent"></div>
|
||||
<div class="absolute bottom-0 left-0 p-10 max-w-3xl text-white">
|
||||
@@ -30,7 +30,7 @@
|
||||
<div v-for="(topic, idx) in topics.slice(1)" :key="topic.id"
|
||||
class="group bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden hover:shadow-xl transition-all cursor-pointer flex flex-col"
|
||||
:class="{ 'lg:col-span-2 flex-row': idx === 0 }"
|
||||
@click="$router.push(`/explore?topic=${topic.id}`)"
|
||||
@click="$router.push(tenantRoute(`/explore?topic=${topic.id}`))"
|
||||
>
|
||||
<!-- Cover -->
|
||||
<div class="relative overflow-hidden" :class="idx === 0 ? 'w-1/2' : 'h-48'">
|
||||
@@ -72,7 +72,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const topics = ref([
|
||||
{
|
||||
id: 1,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import Toast from 'primevue/toast';
|
||||
import { authApi } from '../../api/auth';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const toast = useToast();
|
||||
const step = ref(1);
|
||||
const phone = ref('');
|
||||
@@ -30,7 +32,7 @@ const login = async () => {
|
||||
localStorage.setItem('user', JSON.stringify(res.user));
|
||||
toast.add({ severity: 'success', summary: '登录成功', detail: '欢迎回来', life: 1000 });
|
||||
setTimeout(() => {
|
||||
router.push('/');
|
||||
router.push(tenantPath('/', route));
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '登录失败', detail: e.message, life: 3000 });
|
||||
|
||||
@@ -101,8 +101,8 @@
|
||||
<p class="text-lg text-slate-600 mb-12 max-w-lg mx-auto">您的入驻申请已成功提交,平台将在 1-3 个工作日内完成审核。审核结果将通过短信和系统通知发送给您。</p>
|
||||
|
||||
<div class="flex justify-center gap-4">
|
||||
<router-link to="/" class="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium">返回首页</router-link>
|
||||
<router-link to="/me" class="px-8 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium">查看个人中心</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="px-8 py-3 bg-white border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 font-medium">返回首页</router-link>
|
||||
<router-link :to="tenantRoute('/me')" class="px-8 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 font-medium">查看个人中心</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -111,7 +111,11 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const step = ref(1);
|
||||
const submitting = ref(false);
|
||||
const fileInput = ref(null);
|
||||
|
||||
@@ -228,9 +228,11 @@ import { computed, reactive, ref, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const fileInput = ref(null);
|
||||
const currentUploadType = ref('');
|
||||
@@ -499,7 +501,7 @@ const saveContent = async (targetStatus) => {
|
||||
toast.add({ severity: 'success', summary: targetStatus === 'draft' ? '保存成功' : '发布成功', detail: targetStatus === 'draft' ? '已保存为草稿' : '内容已提交审核', life: 3000 });
|
||||
}
|
||||
|
||||
setTimeout(() => router.push('/creator/contents'), 1500);
|
||||
setTimeout(() => router.push(tenantRoute('/creator/contents')), 1500);
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: '操作失败', detail: e.message, life: 3000 });
|
||||
} finally {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900">内容管理</h1>
|
||||
<router-link to="/creator/contents/new"
|
||||
<router-link :to="tenantRoute('/creator/contents/new')"
|
||||
class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2">
|
||||
<i class="pi pi-plus"></i> 发布新内容
|
||||
</router-link>
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
<h3 class="text-slate-900 font-bold mb-1">暂无内容</h3>
|
||||
<p class="text-slate-500 text-sm mb-6">您还没有发布任何内容,快去创作吧!</p>
|
||||
<router-link to="/creator/contents/new" class="px-5 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors">
|
||||
<router-link :to="tenantRoute('/creator/contents/new')" class="px-5 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors">
|
||||
立即发布
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@
|
||||
class="w-full h-full object-cover">
|
||||
<div
|
||||
class="absolute inset-0 bg-black/50 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<router-link :to="`/creator/contents/${item.id}`"
|
||||
<router-link :to="tenantRoute(`/creator/contents/${item.id}`)"
|
||||
class="text-white text-xs font-bold border border-white px-3 py-1 rounded hover:bg-white hover:text-black transition-colors">编辑</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -119,7 +119,7 @@
|
||||
class="bg-blue-50 text-blue-600 text-[11px] px-2 py-0.5 rounded-full font-bold whitespace-nowrap">{{
|
||||
item.key }}</span>
|
||||
<h3 class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
@click="$router.push(tenantRoute(`/creator/contents/${item.id}`))">
|
||||
{{ item.title }}</h3>
|
||||
</div>
|
||||
<!-- Status Badge -->
|
||||
@@ -170,7 +170,7 @@
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-3">
|
||||
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
@click="$router.push(tenantRoute(`/creator/contents/${item.id}`))">
|
||||
<i class="pi pi-file-edit"></i> 编辑
|
||||
</button>
|
||||
<button v-if="item.status === 'published'"
|
||||
@@ -211,13 +211,17 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-2xl font-bold text-slate-900">管理概览</h1>
|
||||
<div class="flex gap-4">
|
||||
<router-link to="/creator/contents/new"
|
||||
<router-link :to="tenantRoute('/creator/contents/new')"
|
||||
class="px-6 py-2.5 bg-primary-600 text-white rounded-lg font-bold hover:bg-primary-700 transition-colors shadow-sm shadow-primary-200 cursor-pointer active:scale-95 flex items-center gap-2">
|
||||
<i class="pi pi-plus"></i> 发布新内容
|
||||
</router-link>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
class="flex-1 p-4 bg-orange-50 border border-orange-100 rounded-xl flex items-center justify-between cursor-pointer hover:bg-orange-100 transition-colors"
|
||||
@click="$router.push('/creator/orders')">
|
||||
@click="$router.push(tenantRoute('/creator/orders'))">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-200 text-orange-700 flex items-center justify-center"><i
|
||||
class="pi pi-refresh"></i></div>
|
||||
@@ -83,7 +83,7 @@
|
||||
<div class="text-xs text-slate-500" v-if="hasPayoutAccount">已绑定:{{ payoutAccounts[0].name }} ({{ payoutAccounts[0].account.slice(-4) }})</div>
|
||||
<div class="text-xs text-orange-600 font-bold flex items-center gap-1" v-else>
|
||||
<i class="pi pi-exclamation-circle"></i> 未配置收款账户
|
||||
<router-link to="/creator/settings"
|
||||
<router-link :to="tenantRoute('/creator/settings')"
|
||||
class="underline hover:text-orange-800 ml-1">去配置</router-link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,8 +134,12 @@ import Dialog from 'primevue/dialog';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const showWithdraw = ref(false);
|
||||
const withdrawMethod = ref('wallet');
|
||||
|
||||
@@ -8,9 +8,17 @@
|
||||
<h1 class="text-4xl font-bold text-slate-900 mb-4">404</h1>
|
||||
<p class="text-xl text-slate-600 mb-8">抱歉,您访问的页面走丢了。</p>
|
||||
<div class="flex justify-center gap-4">
|
||||
<router-link to="/" class="px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">返回首页</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="px-6 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors">返回首页</router-link>
|
||||
<button @click="$router.back()" class="px-6 py-3 border border-slate-300 text-slate-700 rounded-lg hover:bg-slate-50 transition-colors">返回上一页</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
</script>
|
||||
|
||||
@@ -104,9 +104,11 @@
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { orderApi } from '../../api/order';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const orderId = route.params.id || '82934712';
|
||||
const amount = '9.90'; // Should fetch order details first
|
||||
@@ -135,7 +137,7 @@ const simulateSuccess = () => {
|
||||
isScanning.value = false;
|
||||
isSuccess.value = true;
|
||||
setTimeout(() => {
|
||||
router.replace(`/me/orders/${orderId}`);
|
||||
router.replace(tenantRoute(`/me/orders/${orderId}`));
|
||||
}, 1500);
|
||||
}, 1000);
|
||||
};
|
||||
@@ -153,7 +155,7 @@ onMounted(() => {
|
||||
isScanning.value = false;
|
||||
isSuccess.value = true;
|
||||
clearInterval(pollTimer);
|
||||
setTimeout(() => router.replace(`/me/orders/${orderId}`), 1500);
|
||||
setTimeout(() => router.replace(tenantRoute(`/me/orders/${orderId}`)), 1500);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Poll status failed', e);
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<!-- Featured (Pinned) -->
|
||||
<div class="relative h-[400px] rounded-2xl overflow-hidden group cursor-pointer"
|
||||
v-if="featuredContent"
|
||||
@click="$router.push(`/contents/${featuredContent.id}`)">
|
||||
@click="$router.push(tenantRoute(`/contents/${featuredContent.id}`))">
|
||||
<img :src="featuredContent.cover" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105">
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
|
||||
<div class="absolute top-4 left-4 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">置顶</div>
|
||||
@@ -115,7 +115,7 @@
|
||||
<div v-for="item in contents" :key="item.id"
|
||||
v-show="!featuredContent || item.id !== featuredContent.id"
|
||||
class="bg-white rounded-xl border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group cursor-pointer"
|
||||
@click="$router.push(`/contents/${item.id}`)">
|
||||
@click="$router.push(tenantRoute(`/contents/${item.id}`))">
|
||||
<div class="w-64 h-36 bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
|
||||
<img :src="item.cover || 'https://via.placeholder.com/300x168?text=No+Cover'" class="w-full h-full object-cover">
|
||||
</div>
|
||||
@@ -172,8 +172,10 @@ import { reactive, ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { tenantApi } from '../../api/tenant';
|
||||
import { contentApi } from '../../api/content';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const toast = useToast();
|
||||
const currentTab = ref('home');
|
||||
const isFollowing = ref(false);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<!-- Stat Cards -->
|
||||
<router-link to="/me/wallet"
|
||||
<router-link :to="tenantRoute('/me/wallet')"
|
||||
class="bg-white p-6 rounded-xl shadow-sm border border-slate-100 flex items-center gap-4 hover:shadow-md hover:border-primary-100 transition-all cursor-pointer">
|
||||
<div class="w-12 h-12 rounded-full bg-blue-50 text-blue-600 flex items-center justify-center text-xl"><i
|
||||
class="pi pi-wallet"></i></div>
|
||||
@@ -42,13 +42,13 @@
|
||||
<div class="mt-8 bg-white rounded-xl shadow-sm border border-slate-100 p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-slate-900">最近订单</h2>
|
||||
<router-link to="/me/orders"
|
||||
<router-link :to="tenantRoute('/me/orders')"
|
||||
class="text-sm text-primary-600 hover:text-primary-700 font-medium px-2 py-1 rounded hover:bg-primary-50 transition-colors">查看全部
|
||||
<i class="pi pi-angle-right"></i></router-link>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="order in recentOrders" :key="order.id" @click="$router.push(`/me/orders/${order.id}`)"
|
||||
<div v-for="order in recentOrders" :key="order.id" @click="$router.push(tenantRoute(`/me/orders/${order.id}`))"
|
||||
class="flex items-center gap-4 p-4 border border-slate-100 rounded-lg hover:border-primary-100 hover:shadow-sm transition-all cursor-pointer active:scale-[0.99] group">
|
||||
<div class="w-16 h-16 bg-slate-100 rounded flex-shrink-0 flex items-center justify-center relative overflow-hidden">
|
||||
<template v-if="order.type === 'recharge' || !order.items?.length">
|
||||
@@ -89,11 +89,15 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { userApi } from '../../api/user';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const wallet = ref({ balance: 0, points: 0 });
|
||||
const couponCount = ref(0);
|
||||
const recentOrders = ref([]);
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const statusColor = (status) => {
|
||||
const map = {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(`/contents/${item.id}`)">
|
||||
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(tenantRoute(`/contents/${item.id}`))">
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="aspect-video bg-slate-100 relative overflow-hidden">
|
||||
@@ -43,7 +43,7 @@
|
||||
<i class="pi pi-star text-2xl text-slate-300"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-lg">暂无收藏内容</p>
|
||||
<router-link to="/" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
|
||||
</div>
|
||||
|
||||
<Toast />
|
||||
@@ -52,15 +52,17 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { userApi } from '../../api/user';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const items = ref([]);
|
||||
const loading = ref(true);
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const fetchFavorites = async () => {
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { userApi } from '../../api/user';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const libraryItems = ref([]);
|
||||
const loading = ref(true);
|
||||
|
||||
@@ -53,7 +57,7 @@ const getStatusLabel = (item) => {
|
||||
<div
|
||||
v-for="item in libraryItems"
|
||||
:key="item.id"
|
||||
@click="item.status === 'published' ? $router.push(`/contents/${item.id}`) : null"
|
||||
@click="item.status === 'published' ? $router.push(tenantRoute(`/contents/${item.id}`)) : null"
|
||||
class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 flex flex-col sm:flex-row"
|
||||
:class="item.status === 'published' ? 'cursor-pointer active:scale-[0.99]' : 'opacity-75 cursor-not-allowed'"
|
||||
>
|
||||
@@ -98,7 +102,7 @@ const getStatusLabel = (item) => {
|
||||
<div v-if="!loading && libraryItems.length === 0" class="flex flex-col items-center justify-center py-20">
|
||||
<div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-4"><i class="pi pi-book text-3xl text-slate-300"></i></div>
|
||||
<p class="text-slate-500">暂无已购内容</p>
|
||||
<router-link to="/" class="mt-4 text-primary-600 font-medium hover:underline">去发现好内容</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="mt-4 text-primary-600 font-medium hover:underline">去发现好内容</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- Content Grid -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(`/contents/${item.id}`)">
|
||||
<div v-for="item in items" :key="item.id" class="group relative bg-white border border-slate-200 rounded-xl overflow-hidden hover:shadow-md transition-all hover:border-primary-200 cursor-pointer" @click="$router.push(tenantRoute(`/contents/${item.id}`))">
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="aspect-video bg-slate-100 relative overflow-hidden">
|
||||
@@ -38,7 +38,7 @@
|
||||
<i class="pi pi-thumbs-up text-2xl text-slate-300"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 text-lg">暂无点赞内容</p>
|
||||
<router-link to="/" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
|
||||
<router-link :to="tenantRoute('/')" class="mt-4 inline-block text-primary-600 font-medium hover:underline">去发现好内容</router-link>
|
||||
</div>
|
||||
|
||||
<Toast />
|
||||
@@ -47,10 +47,14 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<!-- Order Body -->
|
||||
<div class="p-4 flex flex-col sm:flex-row gap-6">
|
||||
<!-- Product Info (Clickable Area) -->
|
||||
<div class="flex-1 flex gap-4 cursor-pointer" @click="$router.push(`/me/orders/${order.id}`)">
|
||||
<div class="flex-1 flex gap-4 cursor-pointer" @click="$router.push(tenantRoute(`/me/orders/${order.id}`))">
|
||||
<div class="w-24 h-16 bg-slate-100 rounded flex-shrink-0 relative overflow-hidden group-hover:opacity-90 transition-opacity flex items-center justify-center">
|
||||
<template v-if="order.type === 'recharge' || !order.items?.length">
|
||||
<i class="pi pi-wallet text-3xl text-primary-500"></i>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button v-if="order.status === 'created'" class="px-4 py-1.5 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition-colors shadow-sm active:scale-95 cursor-pointer">去支付</button>
|
||||
<router-link :to="`/me/orders/${order.id}`" v-if="order.status === 'paid' || order.status === 'completed'" class="px-4 py-1.5 border border-slate-300 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 hover:border-slate-400 transition-colors inline-block text-center cursor-pointer active:scale-95">查看详情</router-link>
|
||||
<router-link :to="tenantRoute(`/me/orders/${order.id}`)" v-if="order.status === 'paid' || order.status === 'completed'" class="px-4 py-1.5 border border-slate-300 text-slate-700 text-sm font-medium rounded-lg hover:bg-slate-50 hover:border-slate-400 transition-colors inline-block text-center cursor-pointer active:scale-95">查看详情</router-link>
|
||||
<button v-if="order.status === 'completed' && order.type !== 'recharge'" class="px-4 py-1.5 text-primary-600 text-sm hover:underline cursor-pointer">申请售后</button>
|
||||
<button v-if="order.status === 'created'" class="text-xs text-slate-400 hover:text-slate-600 cursor-pointer">取消订单</button>
|
||||
</div>
|
||||
@@ -99,10 +99,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { userApi } from '../../api/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
const currentTab = ref('all');
|
||||
const tabs = [
|
||||
{ label: '全部订单', value: 'all' },
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<div class="text-sm text-slate-500">未认证,发布内容前需完成认证</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="$router.push('/creator/apply')" class="px-4 py-2 text-primary-600 font-medium hover:text-primary-700 text-sm transition-colors">去认证</button>
|
||||
<button @click="$router.push(tenantRoute('/creator/apply'))" class="px-4 py-2 text-primary-600 font-medium hover:text-primary-700 text-sm transition-colors">去认证</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,16 +72,20 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import Toast from 'primevue/toast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { tenantPath } from '../../utils/tenant';
|
||||
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const verifyDialog = ref(false);
|
||||
const currentAction = ref('');
|
||||
const route = useRoute();
|
||||
const tenantRoute = (path) => tenantPath(path, route);
|
||||
|
||||
const openVerify = (action) => {
|
||||
currentAction.value = action;
|
||||
|
||||
Reference in New Issue
Block a user