feat: tenant-scoped routing and portal navigation

This commit is contained in:
2026-01-08 21:30:46 +08:00
parent f3aa92078a
commit 3e095c57f3
52 changed files with 1111 additions and 670 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("生成令牌失败")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}`;
}

View File

@@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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