Skip to content

Commit 5d7f213

Browse files
authored
Merge pull request Wei-Shaw#3657 from jianjianai/fix/redeem-invitation-bad-request
修复邀请码误走普通兑换接口时返回 500
2 parents a5638a4 + 3724363 commit 5d7f213

2 files changed

Lines changed: 119 additions & 4 deletions

File tree

backend/internal/service/redeem_service.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,13 @@ func (s *RedeemService) releaseRedeemLock(ctx context.Context, code string) {
374374
_ = s.cache.ReleaseRedeemLock(ctx, code)
375375
}
376376

377+
func unsupportedRedeemTypeError(codeType string) error {
378+
if codeType == RedeemTypeInvitation {
379+
return infraerrors.BadRequest("REDEEM_CODE_UNSUPPORTED_TYPE", "invitation codes can only be used during registration")
380+
}
381+
return infraerrors.BadRequest("REDEEM_CODE_UNSUPPORTED_TYPE", fmt.Sprintf("unsupported redeem type: %s", codeType))
382+
}
383+
377384
// Redeem 使用兑换码
378385
func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (*RedeemCode, error) {
379386
// 检查限流
@@ -407,9 +414,15 @@ func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (
407414
return nil, ErrRedeemCodeUsed
408415
}
409416

410-
// 验证兑换码类型的前置条件
411-
if redeemCode.Type == RedeemTypeSubscription && redeemCode.GroupID == nil {
412-
return nil, infraerrors.BadRequest("REDEEM_CODE_INVALID", "invalid subscription redeem code: missing group_id")
417+
// 验证兑换码类型的前置条件。邀请码属于注册流程,不能通过普通兑换接口使用。
418+
switch redeemCode.Type {
419+
case RedeemTypeBalance, RedeemTypeConcurrency:
420+
case RedeemTypeSubscription:
421+
if redeemCode.GroupID == nil {
422+
return nil, infraerrors.BadRequest("REDEEM_CODE_INVALID", "invalid subscription redeem code: missing group_id")
423+
}
424+
default:
425+
return nil, unsupportedRedeemTypeError(redeemCode.Type)
413426
}
414427

415428
// 获取用户信息
@@ -483,7 +496,7 @@ func (s *RedeemService) Redeem(ctx context.Context, userID int64, code string) (
483496
}
484497

485498
default:
486-
return nil, fmt.Errorf("unsupported redeem type: %s", redeemCode.Type)
499+
return nil, unsupportedRedeemTypeError(redeemCode.Type)
487500
}
488501

489502
// 提交事务
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package service
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
8+
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
type redeemRejectRepo struct {
13+
code RedeemCode
14+
useCalled bool
15+
}
16+
17+
func (r *redeemRejectRepo) Create(ctx context.Context, code *RedeemCode) error {
18+
panic("unexpected Create call")
19+
}
20+
21+
func (r *redeemRejectRepo) CreateBatch(ctx context.Context, codes []RedeemCode) error {
22+
panic("unexpected CreateBatch call")
23+
}
24+
25+
func (r *redeemRejectRepo) GetByID(ctx context.Context, id int64) (*RedeemCode, error) {
26+
if r.code.ID != id {
27+
return nil, ErrRedeemCodeNotFound
28+
}
29+
clone := r.code
30+
return &clone, nil
31+
}
32+
33+
func (r *redeemRejectRepo) GetByCode(ctx context.Context, code string) (*RedeemCode, error) {
34+
if r.code.Code != code {
35+
return nil, ErrRedeemCodeNotFound
36+
}
37+
clone := r.code
38+
return &clone, nil
39+
}
40+
41+
func (r *redeemRejectRepo) Update(ctx context.Context, code *RedeemCode) error {
42+
panic("unexpected Update call")
43+
}
44+
45+
func (r *redeemRejectRepo) BatchUpdate(ctx context.Context, ids []int64, fields RedeemCodeBatchUpdateFields) (int64, error) {
46+
panic("unexpected BatchUpdate call")
47+
}
48+
49+
func (r *redeemRejectRepo) Delete(ctx context.Context, id int64) error {
50+
panic("unexpected Delete call")
51+
}
52+
53+
func (r *redeemRejectRepo) Use(ctx context.Context, id, userID int64) error {
54+
r.useCalled = true
55+
r.code.Status = StatusUsed
56+
r.code.UsedBy = &userID
57+
return nil
58+
}
59+
60+
func (r *redeemRejectRepo) List(ctx context.Context, params pagination.PaginationParams) ([]RedeemCode, *pagination.PaginationResult, error) {
61+
panic("unexpected List call")
62+
}
63+
64+
func (r *redeemRejectRepo) ListWithFilters(ctx context.Context, params pagination.PaginationParams, codeType, status, search string) ([]RedeemCode, *pagination.PaginationResult, error) {
65+
panic("unexpected ListWithFilters call")
66+
}
67+
68+
func (r *redeemRejectRepo) ListByUser(ctx context.Context, userID int64, limit int) ([]RedeemCode, error) {
69+
panic("unexpected ListByUser call")
70+
}
71+
72+
func (r *redeemRejectRepo) ListByUserPaginated(ctx context.Context, userID int64, params pagination.PaginationParams, codeType string) ([]RedeemCode, *pagination.PaginationResult, error) {
73+
panic("unexpected ListByUserPaginated call")
74+
}
75+
76+
func (r *redeemRejectRepo) SumPositiveBalanceByUser(ctx context.Context, userID int64) (float64, error) {
77+
panic("unexpected SumPositiveBalanceByUser call")
78+
}
79+
80+
func TestRedeemRejectsInvitationCodeBeforeTransaction(t *testing.T) {
81+
ctx := context.Background()
82+
redeemRepo := &redeemRejectRepo{
83+
code: RedeemCode{
84+
ID: 1,
85+
Code: "INVITE-001",
86+
Type: RedeemTypeInvitation,
87+
Status: StatusUnused,
88+
},
89+
}
90+
redeemService := NewRedeemService(redeemRepo, nil, nil, nil, nil, nil, nil, nil)
91+
92+
got, err := redeemService.Redeem(ctx, 2, redeemRepo.code.Code)
93+
94+
require.Nil(t, got)
95+
require.Error(t, err)
96+
require.True(t, infraerrors.IsBadRequest(err))
97+
require.Equal(t, "REDEEM_CODE_UNSUPPORTED_TYPE", infraerrors.Reason(err))
98+
require.Equal(t, "invitation codes can only be used during registration", infraerrors.Message(err))
99+
require.False(t, redeemRepo.useCalled)
100+
require.Equal(t, StatusUnused, redeemRepo.code.Status)
101+
require.Nil(t, redeemRepo.code.UsedBy)
102+
}

0 commit comments

Comments
 (0)