fix: 防止订阅过期时间超出 JSON 序列化范围

问题:当分配订阅天数过大时,expires_at 年份可能超过 9999,
导致 time.Time JSON 序列化失败(RFC 3339 要求年份 <= 9999),
使后台无法显示和删除异常数据。

修复:
- handler 层添加 validity_days 最大值验证(max=36500,即100年)
- service 层添加 MaxValidityDays 和 MaxExpiresAt 双重保护
- 启动时自动修复已存在的异常数据(expires_at > 2099年)
This commit is contained in:
shaw
2025-12-28 11:45:41 +08:00
parent 0aa480283f
commit fbdff4f34f
4 changed files with 69 additions and 8 deletions

View File

@@ -31,7 +31,7 @@ type GenerateRedeemCodesRequest struct {
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天
ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // 订阅类型使用默认30天最大100年
}
// List handles listing all redeem codes with pagination

View File

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

View File

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

View File

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