From fbdff4f34fb19d4fc7b2df195b8ddb43b8486a61 Mon Sep 17 00:00:00 2001 From: shaw Date: Sun, 28 Dec 2025 11:45:41 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=98=B2=E6=AD=A2=E8=AE=A2=E9=98=85?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E6=97=B6=E9=97=B4=E8=B6=85=E5=87=BA=20JSON?= =?UTF-8?q?=20=E5=BA=8F=E5=88=97=E5=8C=96=E8=8C=83=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:当分配订阅天数过大时,expires_at 年份可能超过 9999, 导致 time.Time JSON 序列化失败(RFC 3339 要求年份 <= 9999), 使后台无法显示和删除异常数据。 修复: - handler 层添加 validity_days 最大值验证(max=36500,即100年) - service 层添加 MaxValidityDays 和 MaxExpiresAt 双重保护 - 启动时自动修复已存在的异常数据(expires_at > 2099年) --- .../internal/handler/admin/redeem_handler.go | 4 +-- .../handler/admin/subscription_handler.go | 6 ++-- backend/internal/repository/auto_migrate.go | 33 ++++++++++++++++-- .../internal/service/subscription_service.go | 34 ++++++++++++++++++- 4 files changed, 69 insertions(+), 8 deletions(-) diff --git a/backend/internal/handler/admin/redeem_handler.go b/backend/internal/handler/admin/redeem_handler.go index 4e8e48e6..45fae43a 100644 --- a/backend/internal/handler/admin/redeem_handler.go +++ b/backend/internal/handler/admin/redeem_handler.go @@ -30,8 +30,8 @@ type GenerateRedeemCodesRequest struct { Count int `json:"count" binding:"required,min=1,max=100"` Type string `json:"type" binding:"required,oneof=balance concurrency subscription"` Value float64 `json:"value" binding:"min=0"` - GroupID *int64 `json:"group_id"` // 订阅类型必填 - ValidityDays int `json:"validity_days"` // 订阅类型使用,默认30天 + GroupID *int64 `json:"group_id"` // 订阅类型必填 + ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // 订阅类型使用,默认30天,最大100年 } // List handles listing all redeem codes with pagination diff --git a/backend/internal/handler/admin/subscription_handler.go b/backend/internal/handler/admin/subscription_handler.go index ca69eb05..08db999a 100644 --- a/backend/internal/handler/admin/subscription_handler.go +++ b/backend/internal/handler/admin/subscription_handler.go @@ -41,7 +41,7 @@ func NewSubscriptionHandler(subscriptionService *service.SubscriptionService) *S type AssignSubscriptionRequest struct { UserID int64 `json:"user_id" binding:"required"` GroupID int64 `json:"group_id" binding:"required"` - ValidityDays int `json:"validity_days"` + ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years Notes string `json:"notes"` } @@ -49,13 +49,13 @@ type AssignSubscriptionRequest struct { type BulkAssignSubscriptionRequest struct { UserIDs []int64 `json:"user_ids" binding:"required,min=1"` GroupID int64 `json:"group_id" binding:"required"` - ValidityDays int `json:"validity_days"` + ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // max 100 years Notes string `json:"notes"` } // ExtendSubscriptionRequest represents extend subscription request type ExtendSubscriptionRequest struct { - Days int `json:"days" binding:"required,min=1"` + Days int `json:"days" binding:"required,min=1,max=36500"` // max 100 years } // List handles listing all subscriptions with pagination and filters diff --git a/backend/internal/repository/auto_migrate.go b/backend/internal/repository/auto_migrate.go index 6ca28036..9127eeb9 100644 --- a/backend/internal/repository/auto_migrate.go +++ b/backend/internal/repository/auto_migrate.go @@ -1,11 +1,20 @@ package repository -import "gorm.io/gorm" +import ( + "log" + "time" + + "gorm.io/gorm" +) + +// MaxExpiresAt is the maximum allowed expiration date for subscriptions (year 2099) +// This prevents time.Time JSON serialization errors (RFC 3339 requires year <= 9999) +var maxExpiresAt = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC) // AutoMigrate runs schema migrations for all repository persistence models. // Persistence models are defined within individual `*_repo.go` files. func AutoMigrate(db *gorm.DB) error { - return db.AutoMigrate( + err := db.AutoMigrate( &userModel{}, &apiKeyModel{}, &groupModel{}, @@ -17,4 +26,24 @@ func AutoMigrate(db *gorm.DB) error { &settingModel{}, &userSubscriptionModel{}, ) + if err != nil { + return err + } + + // 修复无效的过期时间(年份超过 2099 会导致 JSON 序列化失败) + return fixInvalidExpiresAt(db) +} + +// fixInvalidExpiresAt 修复 user_subscriptions 表中无效的过期时间 +func fixInvalidExpiresAt(db *gorm.DB) error { + result := db.Model(&userSubscriptionModel{}). + Where("expires_at > ?", maxExpiresAt). + Update("expires_at", maxExpiresAt) + if result.Error != nil { + return result.Error + } + if result.RowsAffected > 0 { + log.Printf("[AutoMigrate] Fixed %d subscriptions with invalid expires_at (year > 2099)", result.RowsAffected) + } + return nil } diff --git a/backend/internal/service/subscription_service.go b/backend/internal/service/subscription_service.go index 5b957094..fec6c147 100644 --- a/backend/internal/service/subscription_service.go +++ b/backend/internal/service/subscription_service.go @@ -10,6 +10,13 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" ) +// MaxExpiresAt is the maximum allowed expiration date (year 2099) +// This prevents time.Time JSON serialization errors (RFC 3339 requires year <= 9999) +var MaxExpiresAt = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC) + +// MaxValidityDays is the maximum allowed validity days for subscriptions (100 years) +const MaxValidityDays = 36500 + var ( ErrSubscriptionNotFound = infraerrors.NotFound("SUBSCRIPTION_NOT_FOUND", "subscription not found") ErrSubscriptionExpired = infraerrors.Forbidden("SUBSCRIPTION_EXPIRED", "subscription has expired") @@ -111,6 +118,9 @@ func (s *SubscriptionService) AssignOrExtendSubscription(ctx context.Context, in if validityDays <= 0 { validityDays = 30 } + if validityDays > MaxValidityDays { + validityDays = MaxValidityDays + } // 已有订阅,执行续期 if existingSub != nil { @@ -125,6 +135,11 @@ func (s *SubscriptionService) AssignOrExtendSubscription(ctx context.Context, in newExpiresAt = now.AddDate(0, 0, validityDays) } + // 确保不超过最大过期时间 + if newExpiresAt.After(MaxExpiresAt) { + newExpiresAt = MaxExpiresAt + } + // 更新过期时间 if err := s.userSubRepo.ExtendExpiry(ctx, existingSub.ID, newExpiresAt); err != nil { return nil, false, fmt.Errorf("extend subscription: %w", err) @@ -189,13 +204,21 @@ func (s *SubscriptionService) createSubscription(ctx context.Context, input *Ass if validityDays <= 0 { validityDays = 30 } + if validityDays > MaxValidityDays { + validityDays = MaxValidityDays + } now := time.Now() + expiresAt := now.AddDate(0, 0, validityDays) + if expiresAt.After(MaxExpiresAt) { + expiresAt = MaxExpiresAt + } + sub := &UserSubscription{ UserID: input.UserID, GroupID: input.GroupID, StartsAt: now, - ExpiresAt: now.AddDate(0, 0, validityDays), + ExpiresAt: expiresAt, Status: SubscriptionStatusActive, AssignedAt: now, Notes: input.Notes, @@ -291,8 +314,17 @@ func (s *SubscriptionService) ExtendSubscription(ctx context.Context, subscripti return nil, ErrSubscriptionNotFound } + // 限制延长天数 + if days > MaxValidityDays { + days = MaxValidityDays + } + // 计算新的过期时间 newExpiresAt := sub.ExpiresAt.AddDate(0, 0, days) + if newExpiresAt.After(MaxExpiresAt) { + newExpiresAt = MaxExpiresAt + } + if err := s.userSubRepo.ExtendExpiry(ctx, subscriptionID, newExpiresAt); err != nil { return nil, err }