mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(subscription): 支持恢复已撤销订阅
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user