mirror of
https://github.com/labring/sealos.git
synced 2026-08-28 17:22:42 +08:00
feat(account-service): support extend subscription expiration time (#6272)
* move to admin workspace subscription * support extend subscription expiration time
This commit is contained in:
@@ -27,7 +27,6 @@ import (
|
||||
"github.com/labring/sealos/controllers/account/controllers"
|
||||
"github.com/labring/sealos/controllers/account/controllers/cache"
|
||||
"github.com/labring/sealos/controllers/account/controllers/utils"
|
||||
|
||||
// devboxv1alpha1 "github.com/labring/sealos/controllers/devbox/api/v1alpha1"
|
||||
"github.com/labring/sealos/controllers/pkg/database"
|
||||
"github.com/labring/sealos/controllers/pkg/database/cockroach"
|
||||
|
||||
@@ -0,0 +1,578 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/labring/sealos/controllers/pkg/types"
|
||||
"github.com/labring/sealos/service/account/dao"
|
||||
"github.com/labring/sealos/service/account/helper"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// @Success 200 {object} gin.H
|
||||
// @Router /admin/v1alpha1/workspace-subscription/add [post]
|
||||
func AdminAddWorkspaceSubscription(c *gin.Context) {
|
||||
req, err := authenticateAndParseAdminRequest(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := setDefaultValues(c, req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
plan, price, err := validatePlanAndPrice(c, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
existingSubscription, err := validateExistingSubscription(c, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkSubscriptionQuota(c, req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Process subscription transaction
|
||||
err = processSubscriptionTransaction(req, plan, price, existingSubscription)
|
||||
if err != nil {
|
||||
dao.Logger.Errorf("Failed to add workspace subscription via admin interface: %v", err)
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to add workspace subscription: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf(
|
||||
"Workspace subscription '%s' added successfully for workspace '%s'",
|
||||
req.PlanName,
|
||||
req.Workspace,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// AdminWorkspaceSubscriptionList
|
||||
// @Summary Admin get workspace subscription list
|
||||
// @Description Admin interface to get paginated workspace subscription list with filtering options
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req body AdminWorkspaceSubscriptionListReq true "AdminWorkspaceSubscriptionListReq"
|
||||
// @Success 200 {object} AdminWorkspaceSubscriptionListResp
|
||||
// @Router /admin/v1alpha1/workspace-subscription/list [post]
|
||||
func AdminWorkspaceSubscriptionList(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := helper.ParseAdminWorkspaceSubscriptionListReq(c)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
gin.H{"error": fmt.Sprintf("failed to parse request: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Infof("Admin getting workspace subscription list: page=%d, size=%d, filters=%+v",
|
||||
req.PageIndex, req.PageSize, req)
|
||||
|
||||
// Build query conditions
|
||||
conditions := map[string]any{}
|
||||
if req.Workspace != "" {
|
||||
conditions["workspace"] = req.Workspace
|
||||
}
|
||||
if req.UserUID != uuid.Nil {
|
||||
conditions["userUid"] = req.UserUID
|
||||
}
|
||||
if req.PlanName != "" {
|
||||
conditions["planName"] = req.PlanName
|
||||
}
|
||||
if req.Status != "" {
|
||||
conditions["status"] = req.Status
|
||||
}
|
||||
if req.RegionDomain != "" {
|
||||
conditions["regionDomain"] = req.RegionDomain
|
||||
}
|
||||
|
||||
// Get subscriptions with pagination
|
||||
subscriptions, total, err := dao.DBClient.ListWorkspaceSubscriptionsWithPagination(
|
||||
conditions,
|
||||
req.PageIndex,
|
||||
req.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get workspace subscription list: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate pagination info
|
||||
totalPages := int(total) / req.PageSize
|
||||
if int(total)%req.PageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
// Format response
|
||||
type SubscriptionInfo struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
RegionDomain string `json:"regionDomain"`
|
||||
UserUID uuid.UUID `json:"userUID"`
|
||||
PlanName string `json:"planName"`
|
||||
Status string `json:"status"`
|
||||
PayStatus string `json:"payStatus"`
|
||||
PayMethod string `json:"payMethod"`
|
||||
CurrentPeriodStartAt time.Time `json:"currentPeriodStartAt"`
|
||||
CurrentPeriodEndAt time.Time `json:"currentPeriodEndAt"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
ExpireAt *time.Time `json:"expireAt"`
|
||||
}
|
||||
|
||||
subscriptionInfos := make([]SubscriptionInfo, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
subscriptionInfos[i] = SubscriptionInfo{
|
||||
ID: sub.ID,
|
||||
Workspace: sub.Workspace,
|
||||
RegionDomain: sub.RegionDomain,
|
||||
UserUID: sub.UserUID,
|
||||
PlanName: sub.PlanName,
|
||||
Status: string(sub.Status),
|
||||
PayStatus: string(sub.PayStatus),
|
||||
PayMethod: string(sub.PayMethod),
|
||||
CurrentPeriodStartAt: sub.CurrentPeriodStartAt,
|
||||
CurrentPeriodEndAt: sub.CurrentPeriodEndAt,
|
||||
CreateAt: sub.CreateAt,
|
||||
ExpireAt: sub.ExpireAt,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"subscriptions": subscriptionInfos,
|
||||
"pagination": gin.H{
|
||||
"pageIndex": req.PageIndex,
|
||||
"pageSize": req.PageSize,
|
||||
"totalRecords": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// AdminSubscriptionPlans
|
||||
// @Summary Admin get subscription plans
|
||||
// @Description Admin interface to get all available subscription plans
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req body AdminSubscriptionPlansReq true "AdminSubscriptionPlansReq"
|
||||
// @Success 200 {object} AdminSubscriptionPlansResp
|
||||
// @Router /admin/v1alpha1/subscription-plans [post]
|
||||
func AdminSubscriptionPlans(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := helper.ParseAdminSubscriptionPlansReq(c)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
gin.H{"error": fmt.Sprintf("failed to parse request: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Infof("Admin getting subscription plans: includeInactive=%v, planType=%s",
|
||||
req.IncludeInactive, req.PlanType)
|
||||
|
||||
// Get subscription plans
|
||||
plans, err := dao.DBClient.GetWorkspaceSubscriptionPlanList()
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get subscription plans: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter plans based on request parameters
|
||||
filteredPlans := []types.WorkspaceSubscriptionPlan{}
|
||||
for _, plan := range plans {
|
||||
// Filter by plan type if specified (check tags for type classification)
|
||||
if req.PlanType != "" {
|
||||
hasType := false
|
||||
for _, tag := range plan.Tags {
|
||||
if tag == req.PlanType {
|
||||
hasType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasType {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// For now, include all plans since there's no explicit status field
|
||||
// The includeInactive parameter can be used later if status tracking is added
|
||||
filteredPlans = append(filteredPlans, plan)
|
||||
}
|
||||
|
||||
// Format response with pricing information
|
||||
type PlanPriceInfo struct {
|
||||
BillingCycle string `json:"billingCycle"`
|
||||
Price int64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type PlanInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Order int `json:"order"`
|
||||
Tags []string `json:"tags"`
|
||||
Prices []PlanPriceInfo `json:"prices"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
AIQuota int64 `json:"aiQuota"`
|
||||
MaxResources string `json:"maxResources"`
|
||||
MaxSeats int `json:"maxSeats"`
|
||||
}
|
||||
|
||||
planInfos := make([]PlanInfo, len(filteredPlans))
|
||||
for i, plan := range filteredPlans {
|
||||
prices := make([]PlanPriceInfo, len(plan.Prices))
|
||||
for j, price := range plan.Prices {
|
||||
prices[j] = PlanPriceInfo{
|
||||
BillingCycle: string(price.BillingCycle),
|
||||
Price: price.Price,
|
||||
Currency: "USD", // Default currency
|
||||
}
|
||||
}
|
||||
|
||||
planInfos[i] = PlanInfo{
|
||||
ID: plan.ID.String(),
|
||||
Name: plan.Name,
|
||||
Description: plan.Description,
|
||||
Order: plan.Order,
|
||||
Tags: plan.Tags,
|
||||
Prices: prices,
|
||||
Traffic: plan.Traffic,
|
||||
AIQuota: plan.AIQuota,
|
||||
MaxResources: plan.MaxResources,
|
||||
MaxSeats: plan.MaxSeats,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"plans": planInfos,
|
||||
"total": len(planInfos),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// AdminWorkspaceSubscriptionListGET
|
||||
// @Summary Admin get workspace subscription list (GET)
|
||||
// @Description Admin interface to get paginated workspace subscription list with filtering options using GET method
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param pageIndex query int false "Page index (0-based)" example(0)
|
||||
// @Param pageSize query int false "Page size (optional, defaults to 10)" example(10)
|
||||
// @Param workspace query string false "Filter by workspace name" example("ns-8gmgq0jn")
|
||||
// @Param userUID query string false "Filter by user ID" example("36ca5ee6-7b6e-4c15-922b-e861b3fbc061")
|
||||
// @Param planName query string false "Filter by plan name" example("Hobby")
|
||||
// @Param status query string false "Filter by subscription status" example("NORMAL")
|
||||
// @Param regionDomain query string false "Filter by region domain" example("192.168.10.35.nip.io")
|
||||
// @Success 200 {object} AdminWorkspaceSubscriptionListResp
|
||||
// @Router /admin/v1alpha1/workspace-subscription/list [get]
|
||||
func AdminWorkspaceSubscriptionListGET(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
req := &helper.AdminWorkspaceSubscriptionListReq{}
|
||||
|
||||
// Parse pagination parameters
|
||||
if pageIndexStr := c.Query("pageIndex"); pageIndexStr != "" {
|
||||
if pageIndex, err := strconv.Atoi(pageIndexStr); err == nil {
|
||||
req.PageIndex = pageIndex
|
||||
}
|
||||
}
|
||||
if pageSizeStr := c.Query("pageSize"); pageSizeStr != "" {
|
||||
if pageSize, err := strconv.Atoi(pageSizeStr); err == nil {
|
||||
req.PageSize = pageSize
|
||||
}
|
||||
}
|
||||
|
||||
// Parse filter parameters
|
||||
req.Workspace = c.Query("workspace")
|
||||
req.PlanName = c.Query("planName")
|
||||
req.Status = c.Query("status")
|
||||
req.RegionDomain = c.Query("regionDomain")
|
||||
|
||||
// Parse userUID parameter
|
||||
if userUIDStr := c.Query("userUID"); userUIDStr != "" {
|
||||
if userUID, err := uuid.Parse(userUIDStr); err == nil {
|
||||
req.UserUID = userUID
|
||||
}
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if req.PageIndex < 0 {
|
||||
req.PageIndex = 0
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
if req.PageSize > 100 {
|
||||
req.PageSize = 100 // Limit max page size
|
||||
}
|
||||
|
||||
logrus.Infof("Admin getting workspace subscription list (GET): page=%d, size=%d, filters=%+v",
|
||||
req.PageIndex, req.PageSize, req)
|
||||
|
||||
// Build query conditions
|
||||
conditions := map[string]any{}
|
||||
if req.Workspace != "" {
|
||||
conditions["workspace"] = req.Workspace
|
||||
}
|
||||
if req.UserUID != uuid.Nil {
|
||||
conditions["userUid"] = req.UserUID
|
||||
}
|
||||
if req.PlanName != "" {
|
||||
conditions["planName"] = req.PlanName
|
||||
}
|
||||
if req.Status != "" {
|
||||
conditions["status"] = req.Status
|
||||
}
|
||||
if req.RegionDomain != "" {
|
||||
conditions["regionDomain"] = req.RegionDomain
|
||||
}
|
||||
|
||||
// Get subscriptions with pagination
|
||||
subscriptions, total, err := dao.DBClient.ListWorkspaceSubscriptionsWithPagination(
|
||||
conditions,
|
||||
req.PageIndex,
|
||||
req.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get workspace subscription list: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate pagination info
|
||||
totalPages := int(total) / req.PageSize
|
||||
if int(total)%req.PageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
// Format response
|
||||
type SubscriptionInfo struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
RegionDomain string `json:"regionDomain"`
|
||||
UserUID uuid.UUID `json:"userUID"`
|
||||
PlanName string `json:"planName"`
|
||||
Status string `json:"status"`
|
||||
PayStatus string `json:"payStatus"`
|
||||
PayMethod string `json:"payMethod"`
|
||||
CurrentPeriodStartAt time.Time `json:"currentPeriodStartAt"`
|
||||
CurrentPeriodEndAt time.Time `json:"currentPeriodEndAt"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
ExpireAt *time.Time `json:"expireAt"`
|
||||
}
|
||||
|
||||
subscriptionInfos := make([]SubscriptionInfo, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
subscriptionInfos[i] = SubscriptionInfo{
|
||||
ID: sub.ID,
|
||||
Workspace: sub.Workspace,
|
||||
RegionDomain: sub.RegionDomain,
|
||||
UserUID: sub.UserUID,
|
||||
PlanName: sub.PlanName,
|
||||
Status: string(sub.Status),
|
||||
PayStatus: string(sub.PayStatus),
|
||||
PayMethod: string(sub.PayMethod),
|
||||
CurrentPeriodStartAt: sub.CurrentPeriodStartAt,
|
||||
CurrentPeriodEndAt: sub.CurrentPeriodEndAt,
|
||||
CreateAt: sub.CreateAt,
|
||||
ExpireAt: sub.ExpireAt,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"subscriptions": subscriptionInfos,
|
||||
"pagination": gin.H{
|
||||
"pageIndex": req.PageIndex,
|
||||
"pageSize": req.PageSize,
|
||||
"totalRecords": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// AdminSubscriptionPlansGET
|
||||
// @Summary Admin get subscription plans (GET)
|
||||
// @Description Admin interface to get all available subscription plans using GET method
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param includeInactive query bool false "Include inactive plans" example(false)
|
||||
// @Param planType query string false "Filter by plan type" example("workspace")
|
||||
// @Success 200 {object} AdminSubscriptionPlansResp
|
||||
// @Router /admin/v1alpha1/subscription-plans [get]
|
||||
func AdminSubscriptionPlansGET(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
req := &helper.AdminSubscriptionPlansReq{}
|
||||
|
||||
if includeInactiveStr := c.Query("includeInactive"); includeInactiveStr != "" {
|
||||
if includeInactive, err := strconv.ParseBool(includeInactiveStr); err == nil {
|
||||
req.IncludeInactive = includeInactive
|
||||
}
|
||||
}
|
||||
|
||||
req.PlanType = c.Query("planType")
|
||||
|
||||
logrus.Infof("Admin getting subscription plans (GET): includeInactive=%v, planType=%s",
|
||||
req.IncludeInactive, req.PlanType)
|
||||
|
||||
// Get subscription plans
|
||||
plans, err := dao.DBClient.GetWorkspaceSubscriptionPlanList()
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get subscription plans: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter plans based on request parameters
|
||||
filteredPlans := []types.WorkspaceSubscriptionPlan{}
|
||||
for _, plan := range plans {
|
||||
// Filter by plan type if specified (check tags for type classification)
|
||||
if req.PlanType != "" {
|
||||
hasType := false
|
||||
for _, tag := range plan.Tags {
|
||||
if tag == req.PlanType {
|
||||
hasType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasType {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// For now, include all plans since there's no explicit status field
|
||||
// The includeInactive parameter can be used later if status tracking is added
|
||||
filteredPlans = append(filteredPlans, plan)
|
||||
}
|
||||
|
||||
// Format response with pricing information
|
||||
type PlanPriceInfo struct {
|
||||
BillingCycle string `json:"billingCycle"`
|
||||
Price int64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type PlanInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Order int `json:"order"`
|
||||
Tags []string `json:"tags"`
|
||||
Prices []PlanPriceInfo `json:"prices"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
AIQuota int64 `json:"aiQuota"`
|
||||
MaxResources string `json:"maxResources"`
|
||||
MaxSeats int `json:"maxSeats"`
|
||||
}
|
||||
|
||||
planInfos := make([]PlanInfo, len(filteredPlans))
|
||||
for i, plan := range filteredPlans {
|
||||
prices := make([]PlanPriceInfo, len(plan.Prices))
|
||||
for j, price := range plan.Prices {
|
||||
prices[j] = PlanPriceInfo{
|
||||
BillingCycle: string(price.BillingCycle),
|
||||
Price: price.Price,
|
||||
Currency: "USD", // Default currency
|
||||
}
|
||||
}
|
||||
|
||||
planInfos[i] = PlanInfo{
|
||||
ID: plan.ID.String(),
|
||||
Name: plan.Name,
|
||||
Description: plan.Description,
|
||||
Order: plan.Order,
|
||||
Tags: plan.Tags,
|
||||
Prices: prices,
|
||||
Traffic: plan.Traffic,
|
||||
AIQuota: plan.AIQuota,
|
||||
MaxResources: plan.MaxResources,
|
||||
MaxSeats: plan.MaxSeats,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"plans": planInfos,
|
||||
"total": len(planInfos),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -158,10 +157,14 @@ func DeleteWorkspaceSubscription(c *gin.Context) {
|
||||
sub, err := services.StripeServiceInstance.CancelSubscription(
|
||||
subscription.Stripe.SubscriptionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to cancel Stripe subscription %s: %v", subscription.Stripe.SubscriptionID, err)
|
||||
return fmt.Errorf(
|
||||
"failed to cancel Stripe subscription %s: %w",
|
||||
subscription.Stripe.SubscriptionID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
if sub == nil {
|
||||
return fmt.Errorf("stripe subscription cancel failed with nil subscription")
|
||||
return errors.New("stripe subscription cancel failed with nil subscription")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3046,30 +3049,53 @@ func createOrUpdateWorkspaceSubscription(
|
||||
var workspaceSubscription *types.WorkspaceSubscription
|
||||
if existingSubscription != nil {
|
||||
workspaceSubscription = existingSubscription
|
||||
workspaceSubscription.PlanName = req.PlanName
|
||||
workspaceSubscription.PayStatus = types.SubscriptionPayStatusNoNeed
|
||||
workspaceSubscription.TrafficStatus = types.WorkspaceTrafficStatusActive
|
||||
workspaceSubscription.Status = types.SubscriptionStatusNormal
|
||||
workspaceSubscription.PayMethod = types.PaymentMethodBalance // Admin operations use balance payment
|
||||
|
||||
// Update period for renewals or extensions
|
||||
if req.Operator == types.SubscriptionTransactionTypeRenewed ||
|
||||
req.Operator == types.SubscriptionTransactionTypeUpgraded {
|
||||
// Parse the period and add to current end time
|
||||
periodDuration, err := types.ParsePeriod(req.Period)
|
||||
if err != nil {
|
||||
// Fallback to monthly if parsing fails
|
||||
workspaceSubscription.CurrentPeriodStartAt = now
|
||||
workspaceSubscription.CurrentPeriodEndAt = now.AddDate(0, 1, 0)
|
||||
workspaceSubscription.ExpireAt = stripe.Time(
|
||||
workspaceSubscription.CurrentPeriodEndAt,
|
||||
)
|
||||
// Parse period duration
|
||||
periodDuration, err := types.ParsePeriod(req.Period)
|
||||
if err != nil {
|
||||
// Fallback to monthly if parsing fails
|
||||
periodDuration = 30 * 24 * time.Hour
|
||||
}
|
||||
|
||||
// Handle different operators
|
||||
switch req.Operator {
|
||||
case types.SubscriptionTransactionTypeRenewed:
|
||||
// For renewal: only extend ExpireAt, don't modify current period
|
||||
// The processor will handle period renewal when CurrentPeriodEndAt approaches
|
||||
if workspaceSubscription.ExpireAt == nil {
|
||||
expireTime := existingSubscription.CurrentPeriodEndAt.Add(periodDuration)
|
||||
workspaceSubscription.ExpireAt = &expireTime
|
||||
} else {
|
||||
// Extend from current end time
|
||||
workspaceSubscription.CurrentPeriodStartAt = existingSubscription.CurrentPeriodEndAt
|
||||
workspaceSubscription.CurrentPeriodEndAt = existingSubscription.CurrentPeriodEndAt.Add(periodDuration)
|
||||
workspaceSubscription.ExpireAt = stripe.Time(workspaceSubscription.CurrentPeriodEndAt)
|
||||
expireTime := workspaceSubscription.ExpireAt.Add(periodDuration)
|
||||
workspaceSubscription.ExpireAt = &expireTime
|
||||
}
|
||||
logrus.Infof(
|
||||
"Renewal: Extended ExpireAt to %s, current period unchanged (ends at %s)",
|
||||
workspaceSubscription.ExpireAt.Format(time.RFC3339),
|
||||
workspaceSubscription.CurrentPeriodEndAt.Format(time.RFC3339),
|
||||
)
|
||||
|
||||
case types.SubscriptionTransactionTypeUpgraded, types.SubscriptionTransactionTypeDowngraded:
|
||||
// For upgrade/downgrade: immediately update current period and plan
|
||||
workspaceSubscription.PlanName = req.PlanName
|
||||
workspaceSubscription.CurrentPeriodStartAt = now
|
||||
workspaceSubscription.CurrentPeriodEndAt = now.Add(periodDuration)
|
||||
|
||||
// Set ExpireAt to the new current period end if not set, or extend it
|
||||
if workspaceSubscription.ExpireAt == nil ||
|
||||
workspaceSubscription.ExpireAt.Before(workspaceSubscription.CurrentPeriodEndAt) {
|
||||
workspaceSubscription.ExpireAt = &workspaceSubscription.CurrentPeriodEndAt
|
||||
}
|
||||
logrus.Infof(
|
||||
"Upgrade/Downgrade: Updated current period to %s - %s, plan=%s",
|
||||
workspaceSubscription.CurrentPeriodStartAt.Format(time.RFC3339),
|
||||
workspaceSubscription.CurrentPeriodEndAt.Format(time.RFC3339),
|
||||
req.PlanName,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Create new subscription - parse period for correct end time
|
||||
@@ -3110,25 +3136,18 @@ func addTrafficAndAIPackages(
|
||||
existingSubscription, workspaceSubscription *types.WorkspaceSubscription,
|
||||
transactionID string,
|
||||
) error {
|
||||
// For renewal operations, check if current period is still valid
|
||||
// If current period hasn't expired, don't add traffic/AI packages for renewals
|
||||
if req.Operator == types.SubscriptionTransactionTypeRenewed &&
|
||||
existingSubscription != nil &&
|
||||
existingSubscription.CurrentPeriodEndAt.After(time.Now()) {
|
||||
// For renewal operations, skip adding packages - they will be handled by the processor
|
||||
// when the current period is about to end
|
||||
if req.Operator == types.SubscriptionTransactionTypeRenewed {
|
||||
logrus.Infof(
|
||||
"Skipping traffic/AI package addition for renewal: current period still valid until %s",
|
||||
existingSubscription.CurrentPeriodEndAt.Format(time.DateTime),
|
||||
"Skipping traffic/AI package addition for renewal: will be handled by processor before period end",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// For upgrade/downgrade/create operations, add packages immediately
|
||||
// Add traffic package
|
||||
if plan.Traffic > 0 && req.Operator != types.SubscriptionTransactionTypeDowngraded {
|
||||
period, err := types.ParsePeriod(req.Period)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid subscription period: %w", err)
|
||||
}
|
||||
|
||||
// Calculate additional traffic for upgrades
|
||||
additionalTraffic := plan.Traffic
|
||||
if req.Operator == types.SubscriptionTransactionTypeUpgraded &&
|
||||
@@ -3147,12 +3166,12 @@ func addTrafficAndAIPackages(
|
||||
}
|
||||
|
||||
if additionalTraffic > 0 {
|
||||
err = helper.AddTrafficPackage(
|
||||
err := helper.AddTrafficPackage(
|
||||
tx,
|
||||
dao.K8sManager.GetClient(),
|
||||
workspaceSubscription,
|
||||
plan,
|
||||
time.Now().Add(period),
|
||||
workspaceSubscription.CurrentPeriodEndAt,
|
||||
types.WorkspaceTrafficFromWorkspaceSubscription,
|
||||
transactionID,
|
||||
)
|
||||
@@ -3164,11 +3183,6 @@ func addTrafficAndAIPackages(
|
||||
|
||||
// Add AI quota package
|
||||
if plan.AIQuota > 0 && req.Operator != types.SubscriptionTransactionTypeDowngraded {
|
||||
period, err := types.ParsePeriod(req.Period)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid subscription period: %w", err)
|
||||
}
|
||||
|
||||
// Calculate additional AI quota for upgrades
|
||||
additionalAIQuota := plan.AIQuota
|
||||
if req.Operator == types.SubscriptionTransactionTypeUpgraded &&
|
||||
@@ -3187,11 +3201,11 @@ func addTrafficAndAIPackages(
|
||||
}
|
||||
|
||||
if additionalAIQuota > 0 {
|
||||
err = cockroach.AddWorkspaceSubscriptionAIQuotaPackage(
|
||||
err := cockroach.AddWorkspaceSubscriptionAIQuotaPackage(
|
||||
tx,
|
||||
workspaceSubscription.ID,
|
||||
additionalAIQuota,
|
||||
time.Now().Add(period),
|
||||
workspaceSubscription.CurrentPeriodEndAt,
|
||||
types.PKGFromWorkspaceSubscription,
|
||||
transactionID,
|
||||
)
|
||||
@@ -3233,7 +3247,8 @@ func processSubscriptionTransaction(
|
||||
return fmt.Errorf("failed to save workspace subscription: %w", err)
|
||||
}
|
||||
|
||||
// Update resource quota for creation or upgrade
|
||||
// Update resource quota for creation, upgrade or downgrade (not for renewal)
|
||||
// Renewal doesn't change the current period plan, so no quota update needed
|
||||
if req.Operator != types.SubscriptionTransactionTypeRenewed {
|
||||
if err := updateWorkspaceSubscriptionQuota(req.PlanName, workspaceSubscription.Workspace); err != nil {
|
||||
return fmt.Errorf("failed to update workspace subscription quota: %w", err)
|
||||
@@ -3241,6 +3256,7 @@ func processSubscriptionTransaction(
|
||||
}
|
||||
|
||||
// Add traffic and AI packages
|
||||
// For renewal, this will be skipped and handled by the processor
|
||||
if err := addTrafficAndAIPackages(tx, req, plan, existingSubscription, workspaceSubscription, transaction.ID.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -3265,54 +3281,6 @@ func processSubscriptionTransaction(
|
||||
})
|
||||
}
|
||||
|
||||
// @Success 200 {object} gin.H
|
||||
// @Router /admin/v1alpha1/workspace-subscription/add [post]
|
||||
func AdminAddWorkspaceSubscription(c *gin.Context) {
|
||||
req, err := authenticateAndParseAdminRequest(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := setDefaultValues(c, req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
plan, price, err := validatePlanAndPrice(c, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
existingSubscription, err := validateExistingSubscription(c, req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := checkSubscriptionQuota(c, req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Process subscription transaction
|
||||
err = processSubscriptionTransaction(req, plan, price, existingSubscription)
|
||||
if err != nil {
|
||||
dao.Logger.Errorf("Failed to add workspace subscription via admin interface: %v", err)
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to add workspace subscription: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf(
|
||||
"Workspace subscription '%s' added successfully for workspace '%s'",
|
||||
req.PlanName,
|
||||
req.Workspace,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkspaceSubscriptionPlans
|
||||
// @Summary Get workspace subscription plans by namespaces
|
||||
// @Description Get subscription plan names for multiple namespaces, returning "PAYG" for non-subscribed workspaces
|
||||
@@ -3388,518 +3356,3 @@ func GetWorkspaceSubscriptionPlans(c *gin.Context) {
|
||||
Plans: plans,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminWorkspaceSubscriptionList
|
||||
// @Summary Admin get workspace subscription list
|
||||
// @Description Admin interface to get paginated workspace subscription list with filtering options
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req body AdminWorkspaceSubscriptionListReq true "AdminWorkspaceSubscriptionListReq"
|
||||
// @Success 200 {object} AdminWorkspaceSubscriptionListResp
|
||||
// @Router /admin/v1alpha1/workspace-subscription/list [post]
|
||||
func AdminWorkspaceSubscriptionList(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := helper.ParseAdminWorkspaceSubscriptionListReq(c)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
gin.H{"error": fmt.Sprintf("failed to parse request: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Infof("Admin getting workspace subscription list: page=%d, size=%d, filters=%+v",
|
||||
req.PageIndex, req.PageSize, req)
|
||||
|
||||
// Build query conditions
|
||||
conditions := map[string]any{}
|
||||
if req.Workspace != "" {
|
||||
conditions["workspace"] = req.Workspace
|
||||
}
|
||||
if req.UserUID != uuid.Nil {
|
||||
conditions["userUid"] = req.UserUID
|
||||
}
|
||||
if req.PlanName != "" {
|
||||
conditions["planName"] = req.PlanName
|
||||
}
|
||||
if req.Status != "" {
|
||||
conditions["status"] = req.Status
|
||||
}
|
||||
if req.RegionDomain != "" {
|
||||
conditions["regionDomain"] = req.RegionDomain
|
||||
}
|
||||
|
||||
// Get subscriptions with pagination
|
||||
subscriptions, total, err := dao.DBClient.ListWorkspaceSubscriptionsWithPagination(
|
||||
conditions,
|
||||
req.PageIndex,
|
||||
req.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get workspace subscription list: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate pagination info
|
||||
totalPages := int(total) / req.PageSize
|
||||
if int(total)%req.PageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
// Format response
|
||||
type SubscriptionInfo struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
RegionDomain string `json:"regionDomain"`
|
||||
UserUID uuid.UUID `json:"userUID"`
|
||||
PlanName string `json:"planName"`
|
||||
Status string `json:"status"`
|
||||
PayStatus string `json:"payStatus"`
|
||||
PayMethod string `json:"payMethod"`
|
||||
CurrentPeriodStartAt time.Time `json:"currentPeriodStartAt"`
|
||||
CurrentPeriodEndAt time.Time `json:"currentPeriodEndAt"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
ExpireAt *time.Time `json:"expireAt"`
|
||||
}
|
||||
|
||||
subscriptionInfos := make([]SubscriptionInfo, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
subscriptionInfos[i] = SubscriptionInfo{
|
||||
ID: sub.ID,
|
||||
Workspace: sub.Workspace,
|
||||
RegionDomain: sub.RegionDomain,
|
||||
UserUID: sub.UserUID,
|
||||
PlanName: sub.PlanName,
|
||||
Status: string(sub.Status),
|
||||
PayStatus: string(sub.PayStatus),
|
||||
PayMethod: string(sub.PayMethod),
|
||||
CurrentPeriodStartAt: sub.CurrentPeriodStartAt,
|
||||
CurrentPeriodEndAt: sub.CurrentPeriodEndAt,
|
||||
CreateAt: sub.CreateAt,
|
||||
ExpireAt: sub.ExpireAt,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"subscriptions": subscriptionInfos,
|
||||
"pagination": gin.H{
|
||||
"pageIndex": req.PageIndex,
|
||||
"pageSize": req.PageSize,
|
||||
"totalRecords": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// AdminSubscriptionPlans
|
||||
// @Summary Admin get subscription plans
|
||||
// @Description Admin interface to get all available subscription plans
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param req body AdminSubscriptionPlansReq true "AdminSubscriptionPlansReq"
|
||||
// @Success 200 {object} AdminSubscriptionPlansResp
|
||||
// @Router /admin/v1alpha1/subscription-plans [post]
|
||||
func AdminSubscriptionPlans(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request
|
||||
req, err := helper.ParseAdminSubscriptionPlansReq(c)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
gin.H{"error": fmt.Sprintf("failed to parse request: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logrus.Infof("Admin getting subscription plans: includeInactive=%v, planType=%s",
|
||||
req.IncludeInactive, req.PlanType)
|
||||
|
||||
// Get subscription plans
|
||||
plans, err := dao.DBClient.GetWorkspaceSubscriptionPlanList()
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get subscription plans: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter plans based on request parameters
|
||||
filteredPlans := []types.WorkspaceSubscriptionPlan{}
|
||||
for _, plan := range plans {
|
||||
// Filter by plan type if specified (check tags for type classification)
|
||||
if req.PlanType != "" {
|
||||
hasType := false
|
||||
for _, tag := range plan.Tags {
|
||||
if tag == req.PlanType {
|
||||
hasType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasType {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// For now, include all plans since there's no explicit status field
|
||||
// The includeInactive parameter can be used later if status tracking is added
|
||||
filteredPlans = append(filteredPlans, plan)
|
||||
}
|
||||
|
||||
// Format response with pricing information
|
||||
type PlanPriceInfo struct {
|
||||
BillingCycle string `json:"billingCycle"`
|
||||
Price int64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type PlanInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Order int `json:"order"`
|
||||
Tags []string `json:"tags"`
|
||||
Prices []PlanPriceInfo `json:"prices"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
AIQuota int64 `json:"aiQuota"`
|
||||
MaxResources string `json:"maxResources"`
|
||||
MaxSeats int `json:"maxSeats"`
|
||||
}
|
||||
|
||||
planInfos := make([]PlanInfo, len(filteredPlans))
|
||||
for i, plan := range filteredPlans {
|
||||
prices := make([]PlanPriceInfo, len(plan.Prices))
|
||||
for j, price := range plan.Prices {
|
||||
prices[j] = PlanPriceInfo{
|
||||
BillingCycle: string(price.BillingCycle),
|
||||
Price: price.Price,
|
||||
Currency: "USD", // Default currency
|
||||
}
|
||||
}
|
||||
|
||||
planInfos[i] = PlanInfo{
|
||||
ID: plan.ID.String(),
|
||||
Name: plan.Name,
|
||||
Description: plan.Description,
|
||||
Order: plan.Order,
|
||||
Tags: plan.Tags,
|
||||
Prices: prices,
|
||||
Traffic: plan.Traffic,
|
||||
AIQuota: plan.AIQuota,
|
||||
MaxResources: plan.MaxResources,
|
||||
MaxSeats: plan.MaxSeats,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"plans": planInfos,
|
||||
"total": len(planInfos),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// AdminWorkspaceSubscriptionListGET
|
||||
// @Summary Admin get workspace subscription list (GET)
|
||||
// @Description Admin interface to get paginated workspace subscription list with filtering options using GET method
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param pageIndex query int false "Page index (0-based)" example(0)
|
||||
// @Param pageSize query int false "Page size (optional, defaults to 10)" example(10)
|
||||
// @Param workspace query string false "Filter by workspace name" example("ns-8gmgq0jn")
|
||||
// @Param userUID query string false "Filter by user ID" example("36ca5ee6-7b6e-4c15-922b-e861b3fbc061")
|
||||
// @Param planName query string false "Filter by plan name" example("Hobby")
|
||||
// @Param status query string false "Filter by subscription status" example("NORMAL")
|
||||
// @Param regionDomain query string false "Filter by region domain" example("192.168.10.35.nip.io")
|
||||
// @Success 200 {object} AdminWorkspaceSubscriptionListResp
|
||||
// @Router /admin/v1alpha1/workspace-subscription/list [get]
|
||||
func AdminWorkspaceSubscriptionListGET(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
req := &helper.AdminWorkspaceSubscriptionListReq{}
|
||||
|
||||
// Parse pagination parameters
|
||||
if pageIndexStr := c.Query("pageIndex"); pageIndexStr != "" {
|
||||
if pageIndex, err := strconv.Atoi(pageIndexStr); err == nil {
|
||||
req.PageIndex = pageIndex
|
||||
}
|
||||
}
|
||||
if pageSizeStr := c.Query("pageSize"); pageSizeStr != "" {
|
||||
if pageSize, err := strconv.Atoi(pageSizeStr); err == nil {
|
||||
req.PageSize = pageSize
|
||||
}
|
||||
}
|
||||
|
||||
// Parse filter parameters
|
||||
req.Workspace = c.Query("workspace")
|
||||
req.PlanName = c.Query("planName")
|
||||
req.Status = c.Query("status")
|
||||
req.RegionDomain = c.Query("regionDomain")
|
||||
|
||||
// Parse userUID parameter
|
||||
if userUIDStr := c.Query("userUID"); userUIDStr != "" {
|
||||
if userUID, err := uuid.Parse(userUIDStr); err == nil {
|
||||
req.UserUID = userUID
|
||||
}
|
||||
}
|
||||
|
||||
// Set default values
|
||||
if req.PageIndex < 0 {
|
||||
req.PageIndex = 0
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 10
|
||||
}
|
||||
if req.PageSize > 100 {
|
||||
req.PageSize = 100 // Limit max page size
|
||||
}
|
||||
|
||||
logrus.Infof("Admin getting workspace subscription list (GET): page=%d, size=%d, filters=%+v",
|
||||
req.PageIndex, req.PageSize, req)
|
||||
|
||||
// Build query conditions
|
||||
conditions := map[string]any{}
|
||||
if req.Workspace != "" {
|
||||
conditions["workspace"] = req.Workspace
|
||||
}
|
||||
if req.UserUID != uuid.Nil {
|
||||
conditions["userUid"] = req.UserUID
|
||||
}
|
||||
if req.PlanName != "" {
|
||||
conditions["planName"] = req.PlanName
|
||||
}
|
||||
if req.Status != "" {
|
||||
conditions["status"] = req.Status
|
||||
}
|
||||
if req.RegionDomain != "" {
|
||||
conditions["regionDomain"] = req.RegionDomain
|
||||
}
|
||||
|
||||
// Get subscriptions with pagination
|
||||
subscriptions, total, err := dao.DBClient.ListWorkspaceSubscriptionsWithPagination(
|
||||
conditions,
|
||||
req.PageIndex,
|
||||
req.PageSize,
|
||||
)
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get workspace subscription list: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate pagination info
|
||||
totalPages := int(total) / req.PageSize
|
||||
if int(total)%req.PageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
// Format response
|
||||
type SubscriptionInfo struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
RegionDomain string `json:"regionDomain"`
|
||||
UserUID uuid.UUID `json:"userUID"`
|
||||
PlanName string `json:"planName"`
|
||||
Status string `json:"status"`
|
||||
PayStatus string `json:"payStatus"`
|
||||
PayMethod string `json:"payMethod"`
|
||||
CurrentPeriodStartAt time.Time `json:"currentPeriodStartAt"`
|
||||
CurrentPeriodEndAt time.Time `json:"currentPeriodEndAt"`
|
||||
CreateAt time.Time `json:"createAt"`
|
||||
ExpireAt *time.Time `json:"expireAt"`
|
||||
}
|
||||
|
||||
subscriptionInfos := make([]SubscriptionInfo, len(subscriptions))
|
||||
for i, sub := range subscriptions {
|
||||
subscriptionInfos[i] = SubscriptionInfo{
|
||||
ID: sub.ID,
|
||||
Workspace: sub.Workspace,
|
||||
RegionDomain: sub.RegionDomain,
|
||||
UserUID: sub.UserUID,
|
||||
PlanName: sub.PlanName,
|
||||
Status: string(sub.Status),
|
||||
PayStatus: string(sub.PayStatus),
|
||||
PayMethod: string(sub.PayMethod),
|
||||
CurrentPeriodStartAt: sub.CurrentPeriodStartAt,
|
||||
CurrentPeriodEndAt: sub.CurrentPeriodEndAt,
|
||||
CreateAt: sub.CreateAt,
|
||||
ExpireAt: sub.ExpireAt,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"subscriptions": subscriptionInfos,
|
||||
"pagination": gin.H{
|
||||
"pageIndex": req.PageIndex,
|
||||
"pageSize": req.PageSize,
|
||||
"totalRecords": total,
|
||||
"totalPages": totalPages,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// AdminSubscriptionPlansGET
|
||||
// @Summary Admin get subscription plans (GET)
|
||||
// @Description Admin interface to get all available subscription plans using GET method
|
||||
// @Tags WorkspaceSubscription
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param includeInactive query bool false "Include inactive plans" example(false)
|
||||
// @Param planType query string false "Filter by plan type" example("workspace")
|
||||
// @Success 200 {object} AdminSubscriptionPlansResp
|
||||
// @Router /admin/v1alpha1/subscription-plans [get]
|
||||
func AdminSubscriptionPlansGET(c *gin.Context) {
|
||||
// Authenticate admin request
|
||||
if err := authenticateAdminRequest(c); err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusForbidden,
|
||||
gin.H{"error": fmt.Sprintf("admin authenticate error: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse query parameters
|
||||
req := &helper.AdminSubscriptionPlansReq{}
|
||||
|
||||
if includeInactiveStr := c.Query("includeInactive"); includeInactiveStr != "" {
|
||||
if includeInactive, err := strconv.ParseBool(includeInactiveStr); err == nil {
|
||||
req.IncludeInactive = includeInactive
|
||||
}
|
||||
}
|
||||
|
||||
req.PlanType = c.Query("planType")
|
||||
|
||||
logrus.Infof("Admin getting subscription plans (GET): includeInactive=%v, planType=%s",
|
||||
req.IncludeInactive, req.PlanType)
|
||||
|
||||
// Get subscription plans
|
||||
plans, err := dao.DBClient.GetWorkspaceSubscriptionPlanList()
|
||||
if err != nil {
|
||||
SetErrorResp(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
gin.H{"error": fmt.Sprintf("failed to get subscription plans: %v", err)},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter plans based on request parameters
|
||||
filteredPlans := []types.WorkspaceSubscriptionPlan{}
|
||||
for _, plan := range plans {
|
||||
// Filter by plan type if specified (check tags for type classification)
|
||||
if req.PlanType != "" {
|
||||
hasType := false
|
||||
for _, tag := range plan.Tags {
|
||||
if tag == req.PlanType {
|
||||
hasType = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasType {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// For now, include all plans since there's no explicit status field
|
||||
// The includeInactive parameter can be used later if status tracking is added
|
||||
filteredPlans = append(filteredPlans, plan)
|
||||
}
|
||||
|
||||
// Format response with pricing information
|
||||
type PlanPriceInfo struct {
|
||||
BillingCycle string `json:"billingCycle"`
|
||||
Price int64 `json:"price"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
type PlanInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Order int `json:"order"`
|
||||
Tags []string `json:"tags"`
|
||||
Prices []PlanPriceInfo `json:"prices"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
AIQuota int64 `json:"aiQuota"`
|
||||
MaxResources string `json:"maxResources"`
|
||||
MaxSeats int `json:"maxSeats"`
|
||||
}
|
||||
|
||||
planInfos := make([]PlanInfo, len(filteredPlans))
|
||||
for i, plan := range filteredPlans {
|
||||
prices := make([]PlanPriceInfo, len(plan.Prices))
|
||||
for j, price := range plan.Prices {
|
||||
prices[j] = PlanPriceInfo{
|
||||
BillingCycle: string(price.BillingCycle),
|
||||
Price: price.Price,
|
||||
Currency: "USD", // Default currency
|
||||
}
|
||||
}
|
||||
|
||||
planInfos[i] = PlanInfo{
|
||||
ID: plan.ID.String(),
|
||||
Name: plan.Name,
|
||||
Description: plan.Description,
|
||||
Order: plan.Order,
|
||||
Tags: plan.Tags,
|
||||
Prices: prices,
|
||||
Traffic: plan.Traffic,
|
||||
AIQuota: plan.AIQuota,
|
||||
MaxResources: plan.MaxResources,
|
||||
MaxSeats: plan.MaxSeats,
|
||||
}
|
||||
}
|
||||
|
||||
resp := gin.H{
|
||||
"plans": planInfos,
|
||||
"total": len(planInfos),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -610,37 +610,46 @@ func CheckQuota(ctx context.Context, workspace, planName string) (bool, error) {
|
||||
}
|
||||
|
||||
// processExpiredBalanceSubscriptions 处理已到期的余额支付订阅
|
||||
// 1. The current cycle is about to end (within 20 minutes) and the expiration time is longer than the end time of the current cycle - renewal is required
|
||||
// 2. The situation where the current cycle has ended and needs to be renewed
|
||||
func (wsp *WorkspaceSubscriptionProcessor) processExpiredBalanceSubscriptions(
|
||||
ctx context.Context,
|
||||
) (int, error) {
|
||||
var expiredSubscriptions []types.WorkspaceSubscription
|
||||
var subscriptionsToProcess []types.WorkspaceSubscription
|
||||
now := time.Now().UTC()
|
||||
renewalThreshold := now.Add(20 * time.Minute)
|
||||
localDomain := dao.DBClient.GetLocalRegion().Domain
|
||||
normalStatus := types.SubscriptionStatusNormal
|
||||
canceledStatus := types.SubscriptionPayStatusCanceled
|
||||
balancePayMethod := types.PaymentMethodBalance
|
||||
|
||||
// 查询已到期且支付方式为余额的订阅
|
||||
err := dao.DBClient.GetGlobalDB().WithContext(ctx).Model(&types.WorkspaceSubscription{}).
|
||||
Where("current_period_end_at <= ? AND pay_method = ? AND status = ? AND region_domain = ? AND pay_status NOT IN (?, ?)",
|
||||
now.Add(20*time.Minute),
|
||||
types.PaymentMethodBalance,
|
||||
types.SubscriptionStatusNormal,
|
||||
dao.DBClient.GetLocalRegion().Domain,
|
||||
types.SubscriptionPayStatusCanceled,
|
||||
types.SubscriptionPayStatusNoNeed).
|
||||
Find(&expiredSubscriptions).Error
|
||||
err := dao.DBClient.GetGlobalDB().WithContext(ctx).
|
||||
Model(&types.WorkspaceSubscription{}).
|
||||
Where("current_period_end_at <= ? AND status = ? AND region_domain = ? AND pay_status != ? AND (pay_method = ? OR (expire_at > current_period_end_at))",
|
||||
renewalThreshold,
|
||||
normalStatus,
|
||||
localDomain,
|
||||
canceledStatus,
|
||||
balancePayMethod).
|
||||
Find(&subscriptionsToProcess).Error
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to query expired balance subscriptions: %w", err)
|
||||
return 0, fmt.Errorf("failed to query subscriptions needing period renewal: %w", err)
|
||||
}
|
||||
|
||||
if len(expiredSubscriptions) == 0 {
|
||||
if len(subscriptionsToProcess) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
logrus.Infof("Found %d expired balance subscriptions to process", len(expiredSubscriptions))
|
||||
logrus.Infof(
|
||||
"Found %d subscriptions needing period renewal (includes expired and expiring subscriptions with remaining prepaid time)",
|
||||
len(subscriptionsToProcess),
|
||||
)
|
||||
|
||||
processedCount := 0
|
||||
for i := range expiredSubscriptions {
|
||||
if err := wsp.processExpiredBalanceSubscription(ctx, &expiredSubscriptions[i]); err != nil {
|
||||
for i := range subscriptionsToProcess {
|
||||
if err := wsp.processPeriodRenewal(ctx, &subscriptionsToProcess[i]); err != nil {
|
||||
dao.Logger.Errorf(
|
||||
"Failed to process expired balance subscription %s: %v",
|
||||
expiredSubscriptions[i].ID,
|
||||
"Failed to process period renewal for subscription %s: %v",
|
||||
subscriptionsToProcess[i].ID,
|
||||
err,
|
||||
)
|
||||
} else {
|
||||
@@ -651,39 +660,38 @@ func (wsp *WorkspaceSubscriptionProcessor) processExpiredBalanceSubscriptions(
|
||||
return processedCount, nil
|
||||
}
|
||||
|
||||
// processExpiredBalanceSubscription 处理单个到期的余额支付订阅
|
||||
func (wsp *WorkspaceSubscriptionProcessor) processExpiredBalanceSubscription(
|
||||
// processPeriodRenewal 处理订阅的周期续期
|
||||
func (wsp *WorkspaceSubscriptionProcessor) processPeriodRenewal(
|
||||
ctx context.Context,
|
||||
sub *types.WorkspaceSubscription,
|
||||
) error {
|
||||
return dao.DBClient.GetGlobalDB().Transaction(func(dbTx *gorm.DB) error {
|
||||
// 重新获取最新的订阅状态,防止并发问题
|
||||
// Re-obtain the latest subscription status to prevent concurrent issues
|
||||
var latestSub types.WorkspaceSubscription
|
||||
if err := dbTx.Where("id = ?", sub.ID).First(&latestSub).Error; err != nil {
|
||||
return fmt.Errorf("failed to get latest subscription: %w", err)
|
||||
}
|
||||
|
||||
// 再次检查是否需要处理
|
||||
// check again to see if any processing is needed
|
||||
now := time.Now().UTC()
|
||||
if latestSub.CurrentPeriodEndAt.After(now) ||
|
||||
latestSub.PayMethod != types.PaymentMethodBalance ||
|
||||
latestSub.Status != types.SubscriptionStatusNormal {
|
||||
logrus.Infof("Subscription %s no longer needs processing, skipping", latestSub.ID)
|
||||
|
||||
// 检查订阅状态
|
||||
if latestSub.Status != types.SubscriptionStatusNormal {
|
||||
logrus.Infof("Subscription %s status is not normal, skipping", latestSub.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.Infof(
|
||||
"Processing expired balance subscription: workspace=%s, region=%s, plan=%s, expired_at=%s",
|
||||
latestSub.Workspace,
|
||||
latestSub.RegionDomain,
|
||||
latestSub.PlanName,
|
||||
latestSub.CurrentPeriodEndAt.Format(time.RFC3339),
|
||||
)
|
||||
|
||||
account, err := dao.DBClient.GetAccount(types.UserQueryOpts{UID: latestSub.UserUID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account for user %s: %w", latestSub.UserUID, err)
|
||||
// Check if the current cycle has not yet reached the renewal time (leave a 20-minute buffer)
|
||||
if latestSub.CurrentPeriodEndAt.After(now.Add(20 * time.Minute)) {
|
||||
logrus.Infof("Subscription %s current period not yet ending (ends at %s), skipping",
|
||||
latestSub.ID, latestSub.CurrentPeriodEndAt.Format(time.RFC3339))
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine whether the subscription has expired
|
||||
isExpired := latestSub.CurrentPeriodEndAt.Before(now)
|
||||
|
||||
// obtain plan information
|
||||
plan, err := dao.DBClient.GetWorkspaceSubscriptionPlan(latestSub.PlanName)
|
||||
if err != nil {
|
||||
return fmt.Errorf(
|
||||
@@ -693,29 +701,158 @@ func (wsp *WorkspaceSubscriptionProcessor) processExpiredBalanceSubscription(
|
||||
)
|
||||
}
|
||||
|
||||
price, err := dao.DBClient.GetWorkspaceSubscriptionPlanPrice(
|
||||
latestSub.PlanName,
|
||||
types.SubscriptionPeriodMonthly,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get plan price for %s: %w", latestSub.PlanName, err)
|
||||
// Calculate the new cycle time
|
||||
// For expired subscriptions, the new cycle begins now; For subscriptions that are about to expire, start from the end time of the current cycle
|
||||
var newPeriodStart time.Time
|
||||
if isExpired {
|
||||
newPeriodStart = now
|
||||
} else {
|
||||
newPeriodStart = latestSub.CurrentPeriodEndAt
|
||||
}
|
||||
|
||||
// 检查余额是否足够
|
||||
availableBalance := account.Balance - account.DeductionBalance
|
||||
if availableBalance < price.Price {
|
||||
// 余额不足,更新订阅状态为欠费
|
||||
return wsp.handleInsufficientBalance(
|
||||
ctx,
|
||||
dbTx,
|
||||
&latestSub,
|
||||
price.Price,
|
||||
availableBalance,
|
||||
monthlyPeriod, _ := types.ParsePeriod(types.SubscriptionPeriodMonthly)
|
||||
potentialPeriodEnd := newPeriodStart.Add(monthlyPeriod)
|
||||
|
||||
newPeriodEnd := potentialPeriodEnd
|
||||
needsPayment := false
|
||||
var paymentID string
|
||||
var paymentAmount int64 = 0
|
||||
|
||||
// 检查是否还有剩余订阅时长
|
||||
if potentialPeriodEnd.After(*latestSub.ExpireAt) {
|
||||
if latestSub.CurrentPeriodEndAt.Before(*latestSub.ExpireAt) {
|
||||
newPeriodEnd = *latestSub.ExpireAt
|
||||
} else {
|
||||
needsPayment = true
|
||||
}
|
||||
}
|
||||
|
||||
if needsPayment {
|
||||
// Only subscriptions with balance payments are processed here. Other payment methods such as stripe are not handled
|
||||
if latestSub.PayMethod != types.PaymentMethodBalance {
|
||||
return nil
|
||||
}
|
||||
price, err := dao.DBClient.GetWorkspaceSubscriptionPlanPrice(
|
||||
latestSub.PlanName,
|
||||
types.SubscriptionPeriodMonthly,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get plan price for %s: %w", latestSub.PlanName, err)
|
||||
}
|
||||
|
||||
account, err := dao.DBClient.GetAccount(types.UserQueryOpts{UID: latestSub.UserUID})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get account for user %s: %w", latestSub.UserUID, err)
|
||||
}
|
||||
|
||||
availableBalance := account.Balance - account.DeductionBalance
|
||||
if availableBalance < price.Price {
|
||||
return wsp.handleInsufficientBalance(
|
||||
ctx,
|
||||
dbTx,
|
||||
&latestSub,
|
||||
price.Price,
|
||||
availableBalance,
|
||||
)
|
||||
}
|
||||
_payID, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create payment id: %w", err)
|
||||
}
|
||||
paymentID = _payID
|
||||
paymentAmount = price.Price
|
||||
if err := cockroach.AddDeductionAccount(dbTx, latestSub.UserUID, price.Price); err != nil {
|
||||
return fmt.Errorf("failed to deduct balance: %w", err)
|
||||
}
|
||||
|
||||
payment := types.Payment{
|
||||
ID: paymentID,
|
||||
PaymentRaw: types.PaymentRaw{
|
||||
UserUID: latestSub.UserUID,
|
||||
RegionUID: dao.DBClient.GetLocalRegion().UID,
|
||||
CreatedAt: now,
|
||||
Method: types.PaymentMethodBalance,
|
||||
Amount: price.Price,
|
||||
TradeNO: paymentID,
|
||||
Type: types.PaymentTypeSubscription,
|
||||
ChargeSource: types.ChargeSourceBalance,
|
||||
Status: types.PaymentStatusPAID,
|
||||
WorkspaceSubscriptionID: &latestSub.ID,
|
||||
Message: fmt.Sprintf(
|
||||
"Period renewal payment for workspace %s/%s",
|
||||
latestSub.Workspace,
|
||||
latestSub.RegionDomain,
|
||||
),
|
||||
},
|
||||
}
|
||||
if err := dbTx.Create(&payment).Error; err != nil {
|
||||
return fmt.Errorf("failed to create payment record: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 余额充足,执行续费
|
||||
return wsp.handleSuccessfulBalanceRenewal(ctx, dbTx, &latestSub, plan, price)
|
||||
latestSub.CurrentPeriodStartAt = newPeriodStart
|
||||
latestSub.CurrentPeriodEndAt = newPeriodEnd
|
||||
latestSub.Status = types.SubscriptionStatusNormal
|
||||
latestSub.TrafficStatus = types.WorkspaceTrafficStatusActive
|
||||
latestSub.UpdateAt = now
|
||||
|
||||
if err := dbTx.Save(&latestSub).Error; err != nil {
|
||||
return fmt.Errorf("failed to update subscription: %w", err)
|
||||
}
|
||||
|
||||
var payStatus types.SubscriptionPayStatus
|
||||
var statusDesc string
|
||||
|
||||
if needsPayment {
|
||||
payStatus = types.SubscriptionPayStatusPaid
|
||||
statusDesc = "Auto period renewal with payment by processor"
|
||||
} else {
|
||||
payStatus = types.SubscriptionPayStatusNoNeed
|
||||
statusDesc = "Auto period renewal using prepaid time (no payment needed)"
|
||||
}
|
||||
|
||||
transaction := types.WorkspaceSubscriptionTransaction{
|
||||
ID: uuid.New(),
|
||||
From: types.TransactionFromSystem,
|
||||
Workspace: latestSub.Workspace,
|
||||
RegionDomain: latestSub.RegionDomain,
|
||||
UserUID: latestSub.UserUID,
|
||||
OldPlanName: latestSub.PlanName,
|
||||
NewPlanName: latestSub.PlanName,
|
||||
OldPlanStatus: types.SubscriptionStatusNormal,
|
||||
Operator: types.SubscriptionTransactionTypeRenewed,
|
||||
StartAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: types.SubscriptionTransactionStatusCompleted,
|
||||
StatusDesc: statusDesc,
|
||||
PayStatus: payStatus,
|
||||
PayID: paymentID,
|
||||
Period: types.SubscriptionPeriodMonthly,
|
||||
Amount: paymentAmount,
|
||||
}
|
||||
|
||||
if err := dbTx.Create(&transaction).Error; err != nil {
|
||||
return fmt.Errorf("failed to create renewal transaction: %w", err)
|
||||
}
|
||||
|
||||
if err = helper.AddTrafficPackage(dbTx, dao.K8sManager.GetClient(), &latestSub, plan, newPeriodEnd, types.WorkspaceTrafficFromWorkspaceSubscription, transaction.ID.String()); err != nil {
|
||||
return fmt.Errorf("failed to add traffic package: %w", err)
|
||||
}
|
||||
|
||||
if err = cockroach.AddWorkspaceSubscriptionAIQuotaPackage(dbTx, latestSub.ID, plan.AIQuota, newPeriodEnd, types.PKGFromWorkspaceSubscription, transaction.ID.String()); err != nil {
|
||||
return fmt.Errorf("failed to create AI quota package: %w", err)
|
||||
}
|
||||
|
||||
logrus.Infof(
|
||||
"Successfully processed period renewal for workspace %s, new period: %s to %s, expire_at: %s",
|
||||
latestSub.Workspace,
|
||||
latestSub.CurrentPeriodStartAt.Format(time.RFC3339),
|
||||
latestSub.CurrentPeriodEndAt.Format(time.RFC3339),
|
||||
latestSub.ExpireAt.Format(time.RFC3339),
|
||||
)
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -827,116 +964,3 @@ func (wsp *WorkspaceSubscriptionProcessor) handleInsufficientBalance(
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSuccessfulBalanceRenewal 处理成功的余额续费
|
||||
func (wsp *WorkspaceSubscriptionProcessor) handleSuccessfulBalanceRenewal(
|
||||
_ context.Context,
|
||||
dbTx *gorm.DB,
|
||||
sub *types.WorkspaceSubscription,
|
||||
plan *types.WorkspaceSubscriptionPlan,
|
||||
price *types.ProductPrice,
|
||||
) error {
|
||||
now := time.Now().UTC()
|
||||
|
||||
paymentID, err := gonanoid.New(12)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create payment id: %w", err)
|
||||
}
|
||||
|
||||
logrus.Infof(
|
||||
"Processing successful balance renewal for workspace %s, amount: %d",
|
||||
sub.Workspace,
|
||||
price.Price,
|
||||
)
|
||||
|
||||
periodDuration, err := types.ParsePeriod(types.SubscriptionPeriodMonthly)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse period: %w", err)
|
||||
}
|
||||
|
||||
// 设置新的周期时间
|
||||
if sub.CurrentPeriodEndAt.Before(now) {
|
||||
sub.CurrentPeriodStartAt = now
|
||||
sub.CurrentPeriodEndAt = now.Add(periodDuration)
|
||||
} else {
|
||||
sub.CurrentPeriodStartAt = sub.CurrentPeriodEndAt
|
||||
sub.CurrentPeriodEndAt = sub.CurrentPeriodEndAt.Add(periodDuration)
|
||||
}
|
||||
|
||||
sub.Status = types.SubscriptionStatusNormal
|
||||
sub.PayStatus = types.SubscriptionPayStatusPaid
|
||||
sub.TrafficStatus = types.WorkspaceTrafficStatusActive
|
||||
sub.UpdateAt = now
|
||||
|
||||
if err := dbTx.Save(sub).Error; err != nil {
|
||||
return fmt.Errorf("failed to update subscription: %w", err)
|
||||
}
|
||||
|
||||
// 创建成功的续费事务记录
|
||||
successTransaction := types.WorkspaceSubscriptionTransaction{
|
||||
ID: uuid.New(),
|
||||
From: types.TransactionFromSystem,
|
||||
Workspace: sub.Workspace,
|
||||
RegionDomain: sub.RegionDomain,
|
||||
UserUID: sub.UserUID,
|
||||
OldPlanName: sub.PlanName,
|
||||
NewPlanName: sub.PlanName,
|
||||
OldPlanStatus: types.SubscriptionStatusNormal,
|
||||
Operator: types.SubscriptionTransactionTypeRenewed,
|
||||
StartAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Status: types.SubscriptionTransactionStatusCompleted,
|
||||
StatusDesc: "Auto renewal successful",
|
||||
PayStatus: types.SubscriptionPayStatusPaid,
|
||||
PayID: paymentID,
|
||||
Period: types.SubscriptionPeriodMonthly,
|
||||
Amount: price.Price,
|
||||
}
|
||||
|
||||
if err := dbTx.Create(&successTransaction).Error; err != nil {
|
||||
return fmt.Errorf("failed to create successful renewal transaction: %w", err)
|
||||
}
|
||||
if err := cockroach.AddDeductionAccount(dbTx, sub.UserUID, price.Price); err != nil {
|
||||
return fmt.Errorf("failed to deduct balance: %w", err)
|
||||
}
|
||||
|
||||
payment := types.Payment{
|
||||
ID: paymentID,
|
||||
PaymentRaw: types.PaymentRaw{
|
||||
UserUID: sub.UserUID,
|
||||
RegionUID: dao.DBClient.GetLocalRegion().UID,
|
||||
CreatedAt: now,
|
||||
Method: types.PaymentMethodBalance,
|
||||
Amount: price.Price,
|
||||
TradeNO: successTransaction.ID.String(),
|
||||
Type: types.PaymentTypeSubscription,
|
||||
ChargeSource: types.ChargeSourceBalance,
|
||||
Status: types.PaymentStatusPAID,
|
||||
WorkspaceSubscriptionID: &sub.ID,
|
||||
Message: fmt.Sprintf(
|
||||
"Auto renewal for workspace %s/%s",
|
||||
sub.Workspace,
|
||||
sub.RegionDomain,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
if err := dbTx.Create(&payment).Error; err != nil {
|
||||
return fmt.Errorf("failed to create payment record: %w", err)
|
||||
}
|
||||
if err = helper.AddTrafficPackage(dbTx, dao.K8sManager.GetClient(), sub, plan, sub.CurrentPeriodEndAt, types.WorkspaceTrafficFromWorkspaceSubscription, successTransaction.ID.String()); err != nil {
|
||||
return fmt.Errorf("failed to add traffic package: %w", err)
|
||||
}
|
||||
if err = cockroach.AddWorkspaceSubscriptionAIQuotaPackage(dbTx, sub.ID, plan.AIQuota, sub.CurrentPeriodEndAt, types.PKGFromWorkspaceSubscription, successTransaction.ID.String()); err != nil {
|
||||
return fmt.Errorf("failed to create AI quota package: %w", err)
|
||||
}
|
||||
logrus.Infof(
|
||||
"Successfully processed balance renewal for workspace %s, new period: %s to %s",
|
||||
sub.Workspace,
|
||||
sub.CurrentPeriodStartAt.Format(time.RFC3339),
|
||||
sub.CurrentPeriodEndAt.Format(time.RFC3339),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1185,7 +1185,7 @@ func (m *MongoDB) GetAppCosts(req *helper.AppCostsReq) (results *common.AppCosts
|
||||
}
|
||||
|
||||
if strings.ToUpper(req.AppType) != resources.AppStore {
|
||||
var match = make(bson.D, len(matchConditions))
|
||||
match := make(bson.D, len(matchConditions))
|
||||
copy(match, matchConditions[:])
|
||||
if req.AppType != "" {
|
||||
match = append(
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
// @host localhost:2333
|
||||
// @BasePath
|
||||
func main() {
|
||||
go pprof.RunPprofServer(10000)
|
||||
go func() {
|
||||
_ = pprof.RunPprofServer(10000)
|
||||
}()
|
||||
router.RegisterPayRouter()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package pprof
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
//nolint:gosec
|
||||
_ "net/http/pprof"
|
||||
"strconv"
|
||||
|
||||
Reference in New Issue
Block a user