fix: serialize subscription extensions

This commit is contained in:
SavitarC
2026-09-13 12:33:35 +08:00
parent 3d6c207723
commit e3cce574d7
5 changed files with 91 additions and 46 deletions
@@ -63,6 +63,10 @@ func (r *bulkActionHandlerSubscriptionRepo) GetByID(_ context.Context, id int64)
return &copy, 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 &copy, 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获取订阅