feat: enhance order processing and tenant management services
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -3,6 +3,7 @@ package v1
|
|||||||
import (
|
import (
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
"quyun/v2/app/http/v1/dto"
|
"quyun/v2/app/http/v1/dto"
|
||||||
|
"quyun/v2/app/requests"
|
||||||
"quyun/v2/app/services"
|
"quyun/v2/app/services"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
@@ -11,17 +12,135 @@ import (
|
|||||||
// @provider
|
// @provider
|
||||||
type Tenant struct{}
|
type Tenant struct{}
|
||||||
|
|
||||||
// List creator contents
|
// List tenants
|
||||||
//
|
//
|
||||||
// @Router /v1/t/:tenantCode/creators/:id<int>/contents [get]
|
|
||||||
// @Router /v1/t/:tenantCode/tenants [get]
|
// @Router /v1/t/:tenantCode/tenants [get]
|
||||||
// @Router /v1/t/:tenantCode/tenants/:id<int> [get]
|
// @Summary List tenants
|
||||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/follow [post]
|
// @Description List public tenants under current tenant scope
|
||||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/follow [delete]
|
// @Tags TenantPublic
|
||||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/join [post]
|
// @Accept json
|
||||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/join [delete]
|
// @Produce json
|
||||||
// @Router /v1/t/:tenantCode/tenants/:id<int>/invites/accept [post]
|
// @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<int> [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<int>/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<int>/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<int>/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<int>/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<int>/invites/accept [post]
|
||||||
// @Summary Accept tenant invite
|
// @Summary Accept tenant invite
|
||||||
// @Description Accept a tenant invite by code
|
// @Description Accept a tenant invite by code
|
||||||
// @Tags TenantPublic
|
// @Tags TenantPublic
|
||||||
|
|||||||
@@ -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"
|
// @Param content_id query int64 true "Content ID"
|
||||||
// @Success 200 {string} string "Added"
|
// @Success 200 {string} string "Added"
|
||||||
// @Bind user local key(__ctx_user)
|
// @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 {
|
func (u *User) AddFavorite(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||||
tenantID := getTenantID(ctx)
|
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"
|
// @Param contentId path int64 true "Content ID"
|
||||||
// @Success 200 {string} string "Removed"
|
// @Success 200 {string} string "Removed"
|
||||||
// @Bind user local key(__ctx_user)
|
// @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 {
|
func (u *User) RemoveFavorite(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||||
tenantID := getTenantID(ctx)
|
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"
|
// @Param content_id query int64 true "Content ID"
|
||||||
// @Success 200 {string} string "Liked"
|
// @Success 200 {string} string "Liked"
|
||||||
// @Bind user local key(__ctx_user)
|
// @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 {
|
func (u *User) AddLike(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||||
tenantID := getTenantID(ctx)
|
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"
|
// @Param contentId path int64 true "Content ID"
|
||||||
// @Success 200 {string} string "Unliked"
|
// @Success 200 {string} string "Unliked"
|
||||||
// @Bind user local key(__ctx_user)
|
// @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 {
|
func (u *User) RemoveLike(ctx fiber.Ctx, user *models.User, contentID int64) error {
|
||||||
tenantID := getTenantID(ctx)
|
tenantID := getTenantID(ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -594,6 +594,9 @@ func (s *common) GetAssetURL(objectKey string) string {
|
|||||||
if objectKey == "" {
|
if objectKey == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(objectKey, "http://") || strings.HasPrefix(objectKey, "https://") {
|
||||||
|
return objectKey
|
||||||
|
}
|
||||||
url, _ := s.storage.SignURL("GET", objectKey, 1*time.Hour)
|
url, _ := s.storage.SignURL("GET", objectKey, 1*time.Hour)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|||||||
@@ -263,12 +263,35 @@ func (s *order) payWithBalance(ctx context.Context, o *models.Order) (*transacti
|
|||||||
return &transaction_dto.OrderPayResponse{Status: string(consts.OrderStatusPaid)}, nil
|
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 {
|
if order == nil {
|
||||||
return errorx.ErrInvalidParameter.WithMsg("充值订单不存在")
|
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 {
|
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
|
// 3. Grant Content Access
|
||||||
items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find()
|
items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find()
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
// Check if access already exists (idempotency)
|
tblAccess, qAccess := tx.ContentAccess.QueryContext(ctx)
|
||||||
exists, _ := tx.ContentAccess.WithContext(ctx).
|
existingAccess, err := qAccess.Where(
|
||||||
Where(tx.ContentAccess.UserID.Eq(o.UserID), tx.ContentAccess.ContentID.Eq(item.ContentID)).
|
tblAccess.UserID.Eq(o.UserID),
|
||||||
Exists()
|
tblAccess.ContentID.Eq(item.ContentID),
|
||||||
if exists {
|
).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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
access := &models.ContentAccess{
|
_, err = qAccess.Where(tblAccess.ID.Eq(existingAccess.ID)).Updates(&models.ContentAccess{
|
||||||
TenantID: item.TenantID,
|
TenantID: item.TenantID,
|
||||||
UserID: o.UserID,
|
|
||||||
ContentID: item.ContentID,
|
|
||||||
OrderID: o.ID,
|
OrderID: o.ID,
|
||||||
Status: consts.ContentAccessStatusActive,
|
Status: consts.ContentAccessStatusActive,
|
||||||
}
|
RevokedAt: time.Time{},
|
||||||
if err := tx.ContentAccess.WithContext(ctx).Save(access); err != nil {
|
UpdatedAt: time.Now(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user