mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
fix: serialize subscription extensions
This commit is contained in:
@@ -63,6 +63,10 @@ func (r *bulkActionHandlerSubscriptionRepo) GetByID(_ context.Context, id int64)
|
||||
return ©, nil
|
||||
}
|
||||
|
||||
func (r *bulkActionHandlerSubscriptionRepo) GetByIDForUpdate(ctx context.Context, id int64) (*service.UserSubscription, error) {
|
||||
return r.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (r *bulkActionHandlerSubscriptionRepo) ExtendExpiry(_ context.Context, _ int64, expiry time.Time) error {
|
||||
r.extendCalls++
|
||||
r.sub.ExpiresAt = expiry
|
||||
|
||||
@@ -38,6 +38,10 @@ func (r *bulkActionSubscriptionRepo) GetByID(ctx context.Context, id int64) (*Us
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (r *bulkActionSubscriptionRepo) GetByIDForUpdate(ctx context.Context, id int64) (*UserSubscription, error) {
|
||||
return r.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (r *bulkActionSubscriptionRepo) mutated(id int64) {
|
||||
r.mutations = append(r.mutations, id)
|
||||
if r.afterMutation != nil {
|
||||
|
||||
@@ -50,6 +50,10 @@ func (r *transactionalBulkSubscriptionRepo) GetByID(ctx context.Context, _ int64
|
||||
return ©, nil
|
||||
}
|
||||
|
||||
func (r *transactionalBulkSubscriptionRepo) GetByIDForUpdate(ctx context.Context, id int64) (*UserSubscription, error) {
|
||||
return r.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (r *transactionalBulkSubscriptionRepo) ExtendExpiry(ctx context.Context, _ int64, expiry time.Time) error {
|
||||
r.pending[dbent.TxFromContext(ctx)].ExpiresAt = expiry
|
||||
return nil
|
||||
|
||||
@@ -115,6 +115,22 @@ func TestAssignOrExtendSubscriptionSerializedRenewalsAccumulateDays(t *testing.T
|
||||
require.Equal(t, initialExpiry.AddDate(0, 0, 14), second.ExpiresAt)
|
||||
}
|
||||
|
||||
func TestExtendSubscriptionUsesLockedCurrentRow(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
initialExpiry := now.AddDate(0, 0, 10)
|
||||
repo := &lockingRenewalRepo{current: UserSubscription{
|
||||
ID: 37, UserID: 41, GroupID: 43, ExpiresAt: initialExpiry, Status: SubscriptionStatusActive,
|
||||
}}
|
||||
svc := NewSubscriptionService(nil, repo, nil, nil, nil)
|
||||
svc.now = func() time.Time { return now }
|
||||
|
||||
updated, err := svc.ExtendSubscription(context.Background(), 7, 5)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, repo.lockReads)
|
||||
require.Equal(t, initialExpiry.AddDate(0, 0, 5), updated.ExpiresAt)
|
||||
}
|
||||
|
||||
func TestAssignSubscriptionDoesNotReactivateRowSuspendedAfterStaleRead(t *testing.T) {
|
||||
now := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||||
windowStart := now.Add(-24 * time.Hour)
|
||||
|
||||
@@ -652,55 +652,72 @@ func (s *SubscriptionService) RestoreSubscription(ctx context.Context, subscript
|
||||
|
||||
// ExtendSubscription 调整订阅时长(正数延长,负数缩短)
|
||||
func (s *SubscriptionService) ExtendSubscription(ctx context.Context, subscriptionID int64, days int) (*UserSubscription, error) {
|
||||
sub, err := s.userSubRepo.GetByID(ctx, subscriptionID)
|
||||
err := s.withSubscriptionUpdateTx(ctx, func(txCtx context.Context) error {
|
||||
// Lock the row before reading its expiry. Without this lock, concurrent
|
||||
// adjustments can both calculate from the same stale expiry and lose one
|
||||
// of the updates when they write the absolute timestamp.
|
||||
sub, err := s.userSubRepo.GetByIDForUpdate(txCtx, subscriptionID)
|
||||
if err != nil {
|
||||
return ErrSubscriptionNotFound
|
||||
}
|
||||
|
||||
// 限制调整天数范围
|
||||
if days > MaxValidityDays {
|
||||
days = MaxValidityDays
|
||||
}
|
||||
if days < -MaxValidityDays {
|
||||
days = -MaxValidityDays
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if s.now != nil {
|
||||
now = s.now()
|
||||
}
|
||||
isExpired := !sub.ExpiresAt.After(now)
|
||||
|
||||
// 如果订阅已过期,不允许负向调整
|
||||
if isExpired && days < 0 {
|
||||
return infraerrors.BadRequest("CANNOT_SHORTEN_EXPIRED", "cannot shorten an expired subscription")
|
||||
}
|
||||
|
||||
// 计算新的过期时间
|
||||
var newExpiresAt time.Time
|
||||
if isExpired {
|
||||
// 已过期:从当前时间开始增加天数
|
||||
newExpiresAt = now.AddDate(0, 0, days)
|
||||
} else {
|
||||
// 未过期:从原过期时间增加/减少天数
|
||||
newExpiresAt = sub.ExpiresAt.AddDate(0, 0, days)
|
||||
}
|
||||
|
||||
if newExpiresAt.After(MaxExpiresAt) {
|
||||
newExpiresAt = MaxExpiresAt
|
||||
}
|
||||
|
||||
// 检查新的过期时间必须大于当前时间
|
||||
if !newExpiresAt.After(now) {
|
||||
return ErrAdjustWouldExpire
|
||||
}
|
||||
|
||||
if err := s.userSubRepo.ExtendExpiry(txCtx, subscriptionID, newExpiresAt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果订阅已过期,恢复为active状态
|
||||
if sub.Status == SubscriptionStatusExpired {
|
||||
if err := s.userSubRepo.UpdateStatus(txCtx, subscriptionID, SubscriptionStatusActive); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ErrSubscriptionNotFound
|
||||
}
|
||||
|
||||
// 限制调整天数范围
|
||||
if days > MaxValidityDays {
|
||||
days = MaxValidityDays
|
||||
}
|
||||
if days < -MaxValidityDays {
|
||||
days = -MaxValidityDays
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
isExpired := !sub.ExpiresAt.After(now)
|
||||
|
||||
// 如果订阅已过期,不允许负向调整
|
||||
if isExpired && days < 0 {
|
||||
return nil, infraerrors.BadRequest("CANNOT_SHORTEN_EXPIRED", "cannot shorten an expired subscription")
|
||||
}
|
||||
|
||||
// 计算新的过期时间
|
||||
var newExpiresAt time.Time
|
||||
if isExpired {
|
||||
// 已过期:从当前时间开始增加天数
|
||||
newExpiresAt = now.AddDate(0, 0, days)
|
||||
} else {
|
||||
// 未过期:从原过期时间增加/减少天数
|
||||
newExpiresAt = sub.ExpiresAt.AddDate(0, 0, days)
|
||||
}
|
||||
|
||||
if newExpiresAt.After(MaxExpiresAt) {
|
||||
newExpiresAt = MaxExpiresAt
|
||||
}
|
||||
|
||||
// 检查新的过期时间必须大于当前时间
|
||||
if !newExpiresAt.After(now) {
|
||||
return nil, ErrAdjustWouldExpire
|
||||
}
|
||||
|
||||
if err := s.userSubRepo.ExtendExpiry(ctx, subscriptionID, newExpiresAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果订阅已过期,恢复为active状态
|
||||
if sub.Status == SubscriptionStatusExpired {
|
||||
if err := s.userSubRepo.UpdateStatus(ctx, subscriptionID, SubscriptionStatusActive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub, err := s.userSubRepo.GetByID(ctx, subscriptionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 失效订阅缓存
|
||||
@@ -714,7 +731,7 @@ func (s *SubscriptionService) ExtendSubscription(ctx context.Context, subscripti
|
||||
}()
|
||||
}
|
||||
|
||||
return s.userSubRepo.GetByID(ctx, subscriptionID)
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// GetByID 根据ID获取订阅
|
||||
|
||||
Reference in New Issue
Block a user