From 590662964a57c34844ee1a24089e281d099b111a Mon Sep 17 00:00:00 2001 From: Rogee Date: Sun, 8 Feb 2026 18:30:42 +0800 Subject: [PATCH] feat: enhance order processing and tenant management services Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- backend/app/http/v1/tenant.go | 135 +++++++++++++++++++++++++++++++-- backend/app/http/v1/user.go | 8 +- backend/app/services/common.go | 3 + backend/app/services/order.go | 61 ++++++++++++--- 4 files changed, 183 insertions(+), 24 deletions(-) diff --git a/backend/app/http/v1/tenant.go b/backend/app/http/v1/tenant.go index c78b1ea..5dad745 100644 --- a/backend/app/http/v1/tenant.go +++ b/backend/app/http/v1/tenant.go @@ -3,6 +3,7 @@ package v1 import ( "quyun/v2/app/errorx" "quyun/v2/app/http/v1/dto" + "quyun/v2/app/requests" "quyun/v2/app/services" "github.com/gofiber/fiber/v3" @@ -11,17 +12,135 @@ import ( // @provider type Tenant struct{} -// List creator contents +// List tenants // -// @Router /v1/t/:tenantCode/creators/:id/contents [get] // @Router /v1/t/:tenantCode/tenants [get] -// @Router /v1/t/:tenantCode/tenants/:id [get] -// @Router /v1/t/:tenantCode/tenants/:id/follow [post] -// @Router /v1/t/:tenantCode/tenants/:id/follow [delete] -// @Router /v1/t/:tenantCode/tenants/:id/join [post] -// @Router /v1/t/:tenantCode/tenants/:id/join [delete] -// @Router /v1/t/:tenantCode/tenants/:id/invites/accept [post] +// @Summary List tenants +// @Description List public tenants under current tenant scope +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param page query int false "Page number" +// @Param limit query int false "Page size" +// @Param keyword query string false "Search keyword" +// @Success 200 {object} requests.Pager{items=[]dto.TenantProfile} +// @Bind filter query +func (t *Tenant) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) { + tenantID := getTenantID(ctx) + return services.Tenant.List(ctx, tenantID, filter) +} + +// Get tenant profile +// +// @Router /v1/t/:tenantCode/tenants/:id [get] +// @Summary Get tenant profile +// @Description Get public tenant profile by tenant ID +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param id path int64 true "Tenant ID" +// @Success 200 {object} dto.TenantProfile +// @Bind id path +func (t *Tenant) Get(ctx fiber.Ctx, id int64) (*dto.TenantProfile, error) { + tenantID := getTenantID(ctx) + if tenantID > 0 && id != tenantID { + return nil, errorx.ErrForbidden.WithMsg("租户不匹配") + } + userID := getUserID(ctx) + + return services.Tenant.GetPublicProfile(ctx, id, userID) +} + +// Follow tenant +// +// @Router /v1/t/:tenantCode/tenants/:id/follow [post] +// @Summary Follow tenant +// @Description Follow a tenant +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param id path int64 true "Tenant ID" +// @Success 200 {string} string "Followed" +// @Bind id path +func (t *Tenant) Follow(ctx fiber.Ctx, id int64) error { + tenantID := getTenantID(ctx) + if tenantID > 0 && id != tenantID { + return errorx.ErrForbidden.WithMsg("租户不匹配") + } + userID := getUserID(ctx) + + return services.Tenant.Follow(ctx, id, userID) +} + +// Unfollow tenant +// +// @Router /v1/t/:tenantCode/tenants/:id/follow [delete] +// @Summary Unfollow tenant +// @Description Unfollow a tenant +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param id path int64 true "Tenant ID" +// @Success 200 {string} string "Unfollowed" +// @Bind id path +func (t *Tenant) Unfollow(ctx fiber.Ctx, id int64) error { + tenantID := getTenantID(ctx) + if tenantID > 0 && id != tenantID { + return errorx.ErrForbidden.WithMsg("租户不匹配") + } + userID := getUserID(ctx) + + return services.Tenant.Unfollow(ctx, id, userID) +} + +// Apply to join tenant +// +// @Router /v1/t/:tenantCode/tenants/:id/join [post] +// @Summary Apply to join tenant +// @Description Apply to join a tenant +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param id path int64 true "Tenant ID" +// @Param form body dto.TenantJoinApplyForm true "Join apply form" +// @Success 200 {string} string "Applied" +// @Bind id path +// @Bind form body +func (t *Tenant) ApplyJoin(ctx fiber.Ctx, id int64, form *dto.TenantJoinApplyForm) error { + tenantID := getTenantID(ctx) + if tenantID > 0 && id != tenantID { + return errorx.ErrForbidden.WithMsg("租户不匹配") + } + userID := getUserID(ctx) + + return services.Tenant.ApplyJoin(ctx, id, userID, form) +} + +// Cancel join application +// +// @Router /v1/t/:tenantCode/tenants/:id/join [delete] +// @Summary Cancel join application +// @Description Cancel pending tenant join application +// @Tags TenantPublic +// @Accept json +// @Produce json +// @Param id path int64 true "Tenant ID" +// @Success 200 {string} string "Canceled" +// @Bind id path +func (t *Tenant) CancelJoin(ctx fiber.Ctx, id int64) error { + tenantID := getTenantID(ctx) + if tenantID > 0 && id != tenantID { + return errorx.ErrForbidden.WithMsg("租户不匹配") + } + userID := getUserID(ctx) + + return services.Tenant.CancelJoin(ctx, id, userID) +} + +// Accept tenant invite +// +// @Router /v1/t/:tenantCode/tenants/:id/invites/accept [post] // @Summary Accept tenant invite // @Description Accept a tenant invite by code // @Tags TenantPublic diff --git a/backend/app/http/v1/user.go b/backend/app/http/v1/user.go index 104228a..9c0ff3e 100644 --- a/backend/app/http/v1/user.go +++ b/backend/app/http/v1/user.go @@ -181,7 +181,7 @@ func (u *User) Favorites(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, e // @Param content_id query int64 true "Content ID" // @Success 200 {string} string "Added" // @Bind user local key(__ctx_user) -// @Bind contentId query key(content_id) +// @Bind contentID query key(content_id) func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentID int64) error { tenantID := getTenantID(ctx) @@ -199,7 +199,7 @@ func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentID int64) er // @Param contentId path int64 true "Content ID" // @Success 200 {string} string "Removed" // @Bind user local key(__ctx_user) -// @Bind contentId path +// @Bind contentID path key(contentId) func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentID int64) error { tenantID := getTenantID(ctx) @@ -233,7 +233,7 @@ func (u *User) Likes(ctx fiber.Ctx, user *models.User) ([]dto.ContentItem, error // @Param content_id query int64 true "Content ID" // @Success 200 {string} string "Liked" // @Bind user local key(__ctx_user) -// @Bind contentId query key(content_id) +// @Bind contentID query key(content_id) func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentID int64) error { tenantID := getTenantID(ctx) @@ -251,7 +251,7 @@ func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentID int64) error // @Param contentId path int64 true "Content ID" // @Success 200 {string} string "Unliked" // @Bind user local key(__ctx_user) -// @Bind contentId path +// @Bind contentID path key(contentId) func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentID int64) error { tenantID := getTenantID(ctx) diff --git a/backend/app/services/common.go b/backend/app/services/common.go index 2d1e4d0..6e1ae30 100644 --- a/backend/app/services/common.go +++ b/backend/app/services/common.go @@ -594,6 +594,9 @@ func (s *common) GetAssetURL(objectKey string) string { if objectKey == "" { return "" } + if strings.HasPrefix(objectKey, "http://") || strings.HasPrefix(objectKey, "https://") { + return objectKey + } url, _ := s.storage.SignURL("GET", objectKey, 1*time.Hour) return url diff --git a/backend/app/services/order.go b/backend/app/services/order.go index 28e7299..995834c 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -263,12 +263,35 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti return &transaction_dto.OrderPayResponse{Status: string(consts.OrderStatusPaid)}, nil } -func (s *order) settleRechargeOrder(ctx context.Context, order *models.Order) error { +func (s *order) settleRechargeOrder(ctx context.Context, tx *models.Query, order *models.Order) error { if order == nil { return errorx.ErrInvalidParameter.WithMsg("充值订单不存在") } + if tx == nil { + return errorx.ErrInvalidParameter.WithMsg("事务上下文缺失") + } - return s.settleOrder(ctx, order, "recharge") + _, err := tx.User.WithContext(ctx). + Where(tx.User.ID.Eq(order.UserID)). + Update(tx.User.Balance, gorm.Expr("balance + ?", order.AmountPaid)) + if err != nil { + return err + } + + now := time.Now() + info, err := tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(order.ID)).Updates(&models.Order{ + Status: consts.OrderStatusPaid, + PaidAt: now, + UpdatedAt: now, + }) + if err != nil { + return err + } + if info.RowsAffected == 0 { + return errorx.ErrRecordNotFound.WithMsg("充值订单不存在") + } + + return nil } func (s *order) settleOrder(ctx context.Context, o *models.Order, method string) error { @@ -314,22 +337,36 @@ func (s *order) settleOrder(ctx context.Context, o *models.Order, method string) // 3. Grant Content Access items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find() for _, item := range items { - // Check if access already exists (idempotency) - exists, _ := tx.ContentAccess.WithContext(ctx). - Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.Eq(item.ContentID)). - Exists() - if exists { + tblAccess, qAccess := tx.ContentAccess.QueryContext(ctx) + existingAccess, err := qAccess.Where( + tblAccess.UserID.Eq(o.UserID), + tblAccess.ContentID.Eq(item.ContentID), + ).First() + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + access := &models.ContentAccess{ + TenantID: item.TenantID, + UserID: o.UserID, + ContentID: item.ContentID, + OrderID: o.ID, + Status: consts.ContentAccessStatusActive, + } + if err := qAccess.Create(access); err != nil { + return err + } continue } - access := &models.ContentAccess{ + _, err = qAccess.Where(tblAccess.ID.Eq(existingAccess.ID)).Updates(&models.ContentAccess{ TenantID: item.TenantID, - UserID: o.UserID, - ContentID: item.ContentID, OrderID: o.ID, Status: consts.ContentAccessStatusActive, - } - if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil { + RevokedAt: time.Time{}, + UpdatedAt: time.Now(), + }) + if err != nil { return err } }