feat(subscription): 支持恢复已撤销订阅

This commit is contained in:
deqiying
2026-07-01 22:19:21 +08:00
parent 7dc7cfce1d
commit b26dcc3da2
17 changed files with 382 additions and 19 deletions
@@ -268,6 +268,24 @@ func (h *SubscriptionHandler) Revoke(c *gin.Context) {
response.Success(c, gin.H{"message": "Subscription revoked successfully"})
}
// Restore handles restoring a revoked subscription.
// POST /api/v1/admin/subscriptions/:id/restore
func (h *SubscriptionHandler) Restore(c *gin.Context) {
subscriptionID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
response.BadRequest(c, "Invalid subscription ID")
return
}
subscription, err := h.subscriptionService.RestoreSubscription(c.Request.Context(), subscriptionID)
if err != nil {
response.ErrorFrom(c, err)
return
}
response.Success(c, dto.UserSubscriptionFromServiceAdmin(subscription))
}
// ListByGroup handles listing subscriptions for a specific group
// GET /api/v1/admin/groups/:id/subscriptions
func (h *SubscriptionHandler) ListByGroup(c *gin.Context) {
@@ -75,6 +75,21 @@ func (r *userSubscriptionRepository) GetByID(ctx context.Context, id int64) (*se
return userSubscriptionEntityToService(m), nil
}
func (r *userSubscriptionRepository) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
client := clientFromContext(ctx, r.client)
queryCtx := mixins.SkipSoftDelete(ctx)
m, err := client.UserSubscription.Query().
Where(usersubscription.IDEQ(id)).
WithUser().
WithGroup().
WithAssignedByUser().
Only(queryCtx)
if err != nil {
return nil, translatePersistenceError(err, service.ErrSubscriptionNotFound, nil)
}
return userSubscriptionEntityToServicePreserveStatus(m), nil
}
func (r *userSubscriptionRepository) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
client := clientFromContext(ctx, r.client)
m, err := client.UserSubscription.Query().
@@ -141,6 +156,20 @@ func (r *userSubscriptionRepository) Delete(ctx context.Context, id int64) error
return err
}
func (r *userSubscriptionRepository) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
client := clientFromContext(ctx, r.client)
queryCtx := mixins.SkipSoftDelete(ctx)
_, err := client.UserSubscription.UpdateOneID(subscriptionID).
SetStatus(restoredStatus).
ClearDeletedAt().
SetUpdatedAt(time.Now()).
Save(queryCtx)
if err != nil {
return nil, translatePersistenceError(err, service.ErrSubscriptionNotFound, service.ErrSubscriptionRestoreConflict)
}
return r.GetByID(ctx, subscriptionID)
}
func (r *userSubscriptionRepository) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
client := clientFromContext(ctx, r.client)
subs, err := client.UserSubscription.Query().
@@ -299,6 +328,10 @@ func (r *userSubscriptionRepository) ExistsByUserIDAndGroupID(ctx context.Contex
Exist(ctx)
}
func (r *userSubscriptionRepository) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return r.ExistsByUserIDAndGroupID(ctx, userID, groupID)
}
func (r *userSubscriptionRepository) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
client := clientFromContext(ctx, r.client)
_, err := client.UserSubscription.UpdateOneID(subscriptionID).
@@ -520,11 +553,19 @@ func uniqueInt64s(values []int64) []int64 {
}
func userSubscriptionEntityToService(m *dbent.UserSubscription) *service.UserSubscription {
return userSubscriptionEntityToServiceWithStatusMapping(m, true)
}
func userSubscriptionEntityToServicePreserveStatus(m *dbent.UserSubscription) *service.UserSubscription {
return userSubscriptionEntityToServiceWithStatusMapping(m, false)
}
func userSubscriptionEntityToServiceWithStatusMapping(m *dbent.UserSubscription, mapDeletedToRevoked bool) *service.UserSubscription {
if m == nil {
return nil
}
status := m.Status
if m.DeletedAt != nil {
if mapDeletedToRevoked && m.DeletedAt != nil {
status = service.SubscriptionStatusRevoked
}
out := &service.UserSubscription{
@@ -157,6 +157,41 @@ func (s *UserSubscriptionRepoSuite) TestDelete() {
s.Require().Error(err, "expected error after delete")
}
func (s *UserSubscriptionRepoSuite) TestGetByIDIncludeDeleted_PreservesPersistedStatus() {
user := s.mustCreateUser("include-deleted@test.com", service.RoleUser)
group := s.mustCreateGroup("g-include-deleted")
sub := s.mustCreateSubscription(user.ID, group.ID, func(c *dbent.UserSubscriptionCreate) {
c.SetStatus(service.SubscriptionStatusActive)
})
s.Require().NoError(s.repo.Delete(s.ctx, sub.ID), "Delete")
got, err := s.repo.GetByIDIncludeDeleted(s.ctx, sub.ID)
s.Require().NoError(err, "GetByIDIncludeDeleted")
s.Require().Equal(service.SubscriptionStatusActive, got.Status)
s.Require().NotNil(got.DeletedAt)
s.Require().NotNil(got.User)
s.Require().NotNil(got.Group)
}
func (s *UserSubscriptionRepoSuite) TestRestore() {
user := s.mustCreateUser("restore@test.com", service.RoleUser)
group := s.mustCreateGroup("g-restore")
sub := s.mustCreateSubscription(user.ID, group.ID, nil)
s.Require().NoError(s.repo.Delete(s.ctx, sub.ID), "Delete")
restored, err := s.repo.Restore(s.ctx, sub.ID, service.SubscriptionStatusExpired)
s.Require().NoError(err, "Restore")
s.Require().Equal(service.SubscriptionStatusExpired, restored.Status)
s.Require().Nil(restored.DeletedAt)
got, err := s.repo.GetByID(s.ctx, sub.ID)
s.Require().NoError(err, "GetByID after restore")
s.Require().Nil(got.DeletedAt)
s.Require().Equal(service.SubscriptionStatusExpired, got.Status)
}
func (s *UserSubscriptionRepoSuite) TestDelete_Idempotent() {
s.Require().NoError(s.repo.Delete(s.ctx, 42424242), "Delete should be idempotent")
}
@@ -587,6 +622,22 @@ func (s *UserSubscriptionRepoSuite) TestExistsByUserIDAndGroupID() {
s.Require().False(notExists)
}
func (s *UserSubscriptionRepoSuite) TestExistsActiveByUserIDAndGroupID_IgnoresSoftDeletedRows() {
user := s.mustCreateUser("exists-active@test.com", service.RoleUser)
group := s.mustCreateGroup("g-exists-active")
sub := s.mustCreateSubscription(user.ID, group.ID, nil)
exists, err := s.repo.ExistsActiveByUserIDAndGroupID(s.ctx, user.ID, group.ID)
s.Require().NoError(err, "ExistsActiveByUserIDAndGroupID")
s.Require().True(exists)
s.Require().NoError(s.repo.Delete(s.ctx, sub.ID), "Delete")
exists, err = s.repo.ExistsActiveByUserIDAndGroupID(s.ctx, user.ID, group.ID)
s.Require().NoError(err, "ExistsActiveByUserIDAndGroupID after delete")
s.Require().False(exists)
}
// --- CountByGroupID / CountActiveByGroupID ---
func (s *UserSubscriptionRepoSuite) TestCountByGroupID() {
@@ -1999,6 +1999,9 @@ func (stubUserSubscriptionRepo) Create(ctx context.Context, sub *service.UserSub
func (stubUserSubscriptionRepo) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
@@ -2011,6 +2014,9 @@ func (stubUserSubscriptionRepo) Update(ctx context.Context, sub *service.UserSub
func (stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (stubUserSubscriptionRepo) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
if r.byUser == nil {
return nil, nil
@@ -2032,6 +2038,9 @@ func (stubUserSubscriptionRepo) List(ctx context.Context, params pagination.Pagi
func (stubUserSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (stubUserSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
return errors.New("not implemented")
}
@@ -117,6 +117,9 @@ func (f fakeGoogleSubscriptionRepo) Create(ctx context.Context, sub *service.Use
func (f fakeGoogleSubscriptionRepo) GetByID(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (f fakeGoogleSubscriptionRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (f fakeGoogleSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
@@ -132,6 +135,9 @@ func (f fakeGoogleSubscriptionRepo) Update(ctx context.Context, sub *service.Use
func (f fakeGoogleSubscriptionRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (f fakeGoogleSubscriptionRepo) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (f fakeGoogleSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
@@ -147,6 +153,9 @@ func (f fakeGoogleSubscriptionRepo) List(ctx context.Context, params pagination.
func (f fakeGoogleSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (f fakeGoogleSubscriptionRepo) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (f fakeGoogleSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
return errors.New("not implemented")
}
@@ -1175,6 +1175,10 @@ func (r *stubUserSubscriptionRepo) GetByID(ctx context.Context, id int64) (*serv
return nil, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) GetByIDIncludeDeleted(ctx context.Context, id int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
@@ -1194,6 +1198,10 @@ func (r *stubUserSubscriptionRepo) Delete(ctx context.Context, id int64) error {
return errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) ListByUserID(ctx context.Context, userID int64) ([]service.UserSubscription, error) {
return nil, errors.New("not implemented")
}
@@ -1214,6 +1222,10 @@ func (r *stubUserSubscriptionRepo) ExistsByUserIDAndGroupID(ctx context.Context,
return false, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error) {
return false, errors.New("not implemented")
}
func (r *stubUserSubscriptionRepo) ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error {
return errors.New("not implemented")
}
+1
View File
@@ -562,6 +562,7 @@ func registerSubscriptionRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
subscriptions.POST("/:id/extend", h.Admin.Subscription.Extend)
subscriptions.POST("/:id/reset-quota", h.Admin.Subscription.ResetQuota)
subscriptions.POST("/:id/revoke", h.Admin.Subscription.Revoke)
subscriptions.POST("/:id/restore", h.Admin.Subscription.Restore)
subscriptions.DELETE("/:id", h.Admin.Subscription.Revoke)
}
@@ -73,6 +73,9 @@ func (userSubRepoNoop) Create(context.Context, *UserSubscription) error {
func (userSubRepoNoop) GetByID(context.Context, int64) (*UserSubscription, error) {
panic("unexpected GetByID call")
}
func (userSubRepoNoop) GetByIDIncludeDeleted(context.Context, int64) (*UserSubscription, error) {
panic("unexpected GetByIDIncludeDeleted call")
}
func (userSubRepoNoop) GetByUserIDAndGroupID(context.Context, int64, int64) (*UserSubscription, error) {
panic("unexpected GetByUserIDAndGroupID call")
}
@@ -83,6 +86,9 @@ func (userSubRepoNoop) Update(context.Context, *UserSubscription) error {
panic("unexpected Update call")
}
func (userSubRepoNoop) Delete(context.Context, int64) error { panic("unexpected Delete call") }
func (userSubRepoNoop) Restore(context.Context, int64, string) (*UserSubscription, error) {
panic("unexpected Restore call")
}
func (userSubRepoNoop) ListByUserID(context.Context, int64) ([]UserSubscription, error) {
panic("unexpected ListByUserID call")
}
@@ -98,6 +104,9 @@ func (userSubRepoNoop) List(context.Context, pagination.PaginationParams, *int64
func (userSubRepoNoop) ExistsByUserIDAndGroupID(context.Context, int64, int64) (bool, error) {
panic("unexpected ExistsByUserIDAndGroupID call")
}
func (userSubRepoNoop) ExistsActiveByUserIDAndGroupID(context.Context, int64, int64) (bool, error) {
panic("unexpected ExistsActiveByUserIDAndGroupID call")
}
func (userSubRepoNoop) ExtendExpiry(context.Context, int64, time.Time) error {
panic("unexpected ExtendExpiry call")
}
@@ -22,6 +22,10 @@ func (r *subscriptionExpiryRepoStub) GetByID(context.Context, int64) (*UserSubsc
return nil, ErrSubscriptionNotFound
}
func (r *subscriptionExpiryRepoStub) GetByIDIncludeDeleted(context.Context, int64) (*UserSubscription, error) {
return nil, ErrSubscriptionNotFound
}
func (r *subscriptionExpiryRepoStub) GetByUserIDAndGroupID(context.Context, int64, int64) (*UserSubscription, error) {
return nil, ErrSubscriptionNotFound
}
@@ -38,6 +42,10 @@ func (r *subscriptionExpiryRepoStub) Delete(context.Context, int64) error {
return nil
}
func (r *subscriptionExpiryRepoStub) Restore(context.Context, int64, string) (*UserSubscription, error) {
return nil, ErrSubscriptionNotFound
}
func (r *subscriptionExpiryRepoStub) ListByUserID(context.Context, int64) ([]UserSubscription, error) {
return nil, nil
}
@@ -59,6 +67,10 @@ func (r *subscriptionExpiryRepoStub) ExistsByUserIDAndGroupID(context.Context, i
return false, nil
}
func (r *subscriptionExpiryRepoStub) ExistsActiveByUserIDAndGroupID(context.Context, int64, int64) (bool, error) {
return false, nil
}
func (r *subscriptionExpiryRepoStub) ExtendExpiry(context.Context, int64, time.Time) error {
return nil
}
@@ -74,3 +74,99 @@ func TestRevokeSubscription_InvalidatesL1CacheSynchronously(t *testing.T) {
require.ErrorIs(t, err, ErrSubscriptionNotFound)
require.Equal(t, 2, repo.getActiveCalls, "撤销后应回源确认订阅已不存在,不能命中旧 L1")
}
type restoreUserSubRepoStub struct {
userSubRepoNoop
sub *UserSubscription
existsActive bool
restoreCalls int
restoredStatus string
}
func (r *restoreUserSubRepoStub) GetByIDIncludeDeleted(_ context.Context, id int64) (*UserSubscription, error) {
if r.sub == nil || r.sub.ID != id {
return nil, ErrSubscriptionNotFound
}
cp := *r.sub
return &cp, nil
}
func (r *restoreUserSubRepoStub) ExistsActiveByUserIDAndGroupID(context.Context, int64, int64) (bool, error) {
return r.existsActive, nil
}
func (r *restoreUserSubRepoStub) Restore(_ context.Context, id int64, restoredStatus string) (*UserSubscription, error) {
if r.sub == nil || r.sub.ID != id {
return nil, ErrSubscriptionNotFound
}
r.restoreCalls++
r.restoredStatus = restoredStatus
cp := *r.sub
cp.Status = restoredStatus
cp.DeletedAt = nil
r.sub = &cp
return &cp, nil
}
func TestRestoreSubscription_ExpiredActiveRestoresAsExpired(t *testing.T) {
deletedAt := time.Now().Add(-time.Hour)
repo := &restoreUserSubRepoStub{
sub: &UserSubscription{
ID: 1,
UserID: 10,
GroupID: 20,
Status: SubscriptionStatusActive,
ExpiresAt: time.Now().Add(-time.Minute),
DeletedAt: &deletedAt,
},
}
svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
t.Cleanup(svc.Stop)
restored, err := svc.RestoreSubscription(context.Background(), 1)
require.NoError(t, err)
require.Equal(t, 1, repo.restoreCalls)
require.Equal(t, SubscriptionStatusExpired, repo.restoredStatus)
require.Equal(t, SubscriptionStatusExpired, restored.Status)
require.Nil(t, restored.DeletedAt)
}
func TestRestoreSubscription_NotRevokedReturnsConflict(t *testing.T) {
repo := &restoreUserSubRepoStub{
sub: &UserSubscription{
ID: 1,
UserID: 10,
GroupID: 20,
Status: SubscriptionStatusActive,
ExpiresAt: time.Now().Add(time.Hour),
},
}
svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
t.Cleanup(svc.Stop)
_, err := svc.RestoreSubscription(context.Background(), 1)
require.ErrorIs(t, err, ErrSubscriptionNotRevoked)
require.Zero(t, repo.restoreCalls)
}
func TestRestoreSubscription_LiveSubscriptionConflict(t *testing.T) {
deletedAt := time.Now().Add(-time.Hour)
repo := &restoreUserSubRepoStub{
existsActive: true,
sub: &UserSubscription{
ID: 1,
UserID: 10,
GroupID: 20,
Status: SubscriptionStatusExpired,
ExpiresAt: time.Now().Add(-time.Hour),
DeletedAt: &deletedAt,
},
}
svc := NewSubscriptionService(groupRepoNoop{}, repo, nil, nil, nil)
t.Cleanup(svc.Stop)
_, err := svc.RestoreSubscription(context.Background(), 1)
require.ErrorIs(t, err, ErrSubscriptionRestoreConflict)
require.Zero(t, repo.restoreCalls)
}
@@ -25,18 +25,20 @@ var MaxExpiresAt = time.Date(2099, 12, 31, 23, 59, 59, 0, time.UTC)
const MaxValidityDays = 36500
var (
ErrSubscriptionNotFound = infraerrors.NotFound("SUBSCRIPTION_NOT_FOUND", "subscription not found")
ErrSubscriptionExpired = infraerrors.Forbidden("SUBSCRIPTION_EXPIRED", "subscription has expired")
ErrSubscriptionSuspended = infraerrors.Forbidden("SUBSCRIPTION_SUSPENDED", "subscription is suspended")
ErrSubscriptionAlreadyExists = infraerrors.Conflict("SUBSCRIPTION_ALREADY_EXISTS", "subscription already exists for this user and group")
ErrSubscriptionAssignConflict = infraerrors.Conflict("SUBSCRIPTION_ASSIGN_CONFLICT", "subscription exists but request conflicts with existing assignment semantics")
ErrGroupNotSubscriptionType = infraerrors.BadRequest("GROUP_NOT_SUBSCRIPTION_TYPE", "group is not a subscription type")
ErrInvalidInput = infraerrors.BadRequest("INVALID_INPUT", "at least one of resetDaily, resetWeekly, or resetMonthly must be true")
ErrDailyLimitExceeded = infraerrors.TooManyRequests("DAILY_LIMIT_EXCEEDED", "daily usage limit exceeded")
ErrWeeklyLimitExceeded = infraerrors.TooManyRequests("WEEKLY_LIMIT_EXCEEDED", "weekly usage limit exceeded")
ErrMonthlyLimitExceeded = infraerrors.TooManyRequests("MONTHLY_LIMIT_EXCEEDED", "monthly usage limit exceeded")
ErrSubscriptionNilInput = infraerrors.BadRequest("SUBSCRIPTION_NIL_INPUT", "subscription input cannot be nil")
ErrAdjustWouldExpire = infraerrors.BadRequest("ADJUST_WOULD_EXPIRE", "adjustment would result in expired subscription (remaining days must be > 0)")
ErrSubscriptionNotFound = infraerrors.NotFound("SUBSCRIPTION_NOT_FOUND", "subscription not found")
ErrSubscriptionExpired = infraerrors.Forbidden("SUBSCRIPTION_EXPIRED", "subscription has expired")
ErrSubscriptionSuspended = infraerrors.Forbidden("SUBSCRIPTION_SUSPENDED", "subscription is suspended")
ErrSubscriptionAlreadyExists = infraerrors.Conflict("SUBSCRIPTION_ALREADY_EXISTS", "subscription already exists for this user and group")
ErrSubscriptionAssignConflict = infraerrors.Conflict("SUBSCRIPTION_ASSIGN_CONFLICT", "subscription exists but request conflicts with existing assignment semantics")
ErrSubscriptionNotRevoked = infraerrors.Conflict("SUBSCRIPTION_NOT_REVOKED", "subscription is not revoked")
ErrSubscriptionRestoreConflict = infraerrors.Conflict("SUBSCRIPTION_RESTORE_CONFLICT", "subscription already exists for this user and group")
ErrGroupNotSubscriptionType = infraerrors.BadRequest("GROUP_NOT_SUBSCRIPTION_TYPE", "group is not a subscription type")
ErrInvalidInput = infraerrors.BadRequest("INVALID_INPUT", "at least one of resetDaily, resetWeekly, or resetMonthly must be true")
ErrDailyLimitExceeded = infraerrors.TooManyRequests("DAILY_LIMIT_EXCEEDED", "daily usage limit exceeded")
ErrWeeklyLimitExceeded = infraerrors.TooManyRequests("WEEKLY_LIMIT_EXCEEDED", "weekly usage limit exceeded")
ErrMonthlyLimitExceeded = infraerrors.TooManyRequests("MONTHLY_LIMIT_EXCEEDED", "monthly usage limit exceeded")
ErrSubscriptionNilInput = infraerrors.BadRequest("SUBSCRIPTION_NIL_INPUT", "subscription input cannot be nil")
ErrAdjustWouldExpire = infraerrors.BadRequest("ADJUST_WOULD_EXPIRE", "adjustment would result in expired subscription (remaining days must be > 0)")
)
// SubscriptionService 订阅服务
@@ -578,6 +580,41 @@ func (s *SubscriptionService) RevokeSubscription(ctx context.Context, subscripti
return nil
}
// RestoreSubscription 恢复已撤销订阅
func (s *SubscriptionService) RestoreSubscription(ctx context.Context, subscriptionID int64) (*UserSubscription, error) {
sub, err := s.userSubRepo.GetByIDIncludeDeleted(ctx, subscriptionID)
if err != nil {
return nil, err
}
if sub.DeletedAt == nil {
return nil, ErrSubscriptionNotRevoked
}
exists, err := s.userSubRepo.ExistsActiveByUserIDAndGroupID(ctx, sub.UserID, sub.GroupID)
if err != nil {
return nil, err
}
if exists {
return nil, ErrSubscriptionRestoreConflict
}
restoredStatus := sub.Status
now := time.Now()
if restoredStatus == SubscriptionStatusActive && !sub.ExpiresAt.After(now) {
restoredStatus = SubscriptionStatusExpired
}
restored, err := s.userSubRepo.Restore(ctx, subscriptionID, restoredStatus)
if err != nil {
return nil, err
}
if err := s.invalidateSubscriptionCaches(restored.UserID, restored.GroupID); err != nil {
return nil, err
}
return restored, nil
}
// ExtendSubscription 调整订阅时长(正数延长,负数缩短)
func (s *SubscriptionService) ExtendSubscription(ctx context.Context, subscriptionID int64, days int) (*UserSubscription, error) {
sub, err := s.userSubRepo.GetByID(ctx, subscriptionID)
@@ -10,10 +10,12 @@ import (
type UserSubscriptionRepository interface {
Create(ctx context.Context, sub *UserSubscription) error
GetByID(ctx context.Context, id int64) (*UserSubscription, error)
GetByIDIncludeDeleted(ctx context.Context, id int64) (*UserSubscription, error)
GetByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*UserSubscription, error)
GetActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (*UserSubscription, error)
Update(ctx context.Context, sub *UserSubscription) error
Delete(ctx context.Context, id int64) error
Restore(ctx context.Context, subscriptionID int64, restoredStatus string) (*UserSubscription, error)
ListByUserID(ctx context.Context, userID int64) ([]UserSubscription, error)
ListActiveByUserID(ctx context.Context, userID int64) ([]UserSubscription, error)
@@ -21,6 +23,7 @@ type UserSubscriptionRepository interface {
List(ctx context.Context, params pagination.PaginationParams, userID, groupID *int64, status, platform, sortBy, sortOrder string) ([]UserSubscription, *pagination.PaginationResult, error)
ExistsByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error)
ExistsActiveByUserIDAndGroupID(ctx context.Context, userID, groupID int64) (bool, error)
ExtendExpiry(ctx context.Context, subscriptionID int64, newExpiresAt time.Time) error
UpdateStatus(ctx context.Context, subscriptionID int64, status string) error
UpdateNotes(ctx context.Context, subscriptionID int64, notes string) error