@@ -4,11 +4,13 @@ import (
44 "bytes"
55 "context"
66 "encoding/csv"
7+ "errors"
78 "fmt"
89 "strconv"
910 "strings"
1011
1112 "github.com/Wei-Shaw/sub2api/internal/handler/dto"
13+ infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
1214 "github.com/Wei-Shaw/sub2api/internal/pkg/response"
1315 "github.com/Wei-Shaw/sub2api/internal/service"
1416
@@ -17,13 +19,15 @@ import (
1719
1820// RedeemHandler handles admin redeem code management
1921type RedeemHandler struct {
20- adminService service.AdminService
22+ adminService service.AdminService
23+ redeemService * service.RedeemService
2124}
2225
2326// NewRedeemHandler creates a new admin redeem handler
24- func NewRedeemHandler (adminService service.AdminService ) * RedeemHandler {
27+ func NewRedeemHandler (adminService service.AdminService , redeemService * service. RedeemService ) * RedeemHandler {
2528 return & RedeemHandler {
26- adminService : adminService ,
29+ adminService : adminService ,
30+ redeemService : redeemService ,
2731 }
2832}
2933
@@ -36,6 +40,15 @@ type GenerateRedeemCodesRequest struct {
3640 ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // 订阅类型使用,默认30天,最大100年
3741}
3842
43+ // CreateAndRedeemCodeRequest represents creating a fixed code and redeeming it for a target user.
44+ type CreateAndRedeemCodeRequest struct {
45+ Code string `json:"code" binding:"required,min=3,max=128"`
46+ Type string `json:"type" binding:"required,oneof=balance concurrency subscription invitation"`
47+ Value float64 `json:"value" binding:"required,gt=0"`
48+ UserID int64 `json:"user_id" binding:"required,gt=0"`
49+ Notes string `json:"notes"`
50+ }
51+
3952// List handles listing all redeem codes with pagination
4053// GET /api/v1/admin/redeem-codes
4154func (h * RedeemHandler ) List (c * gin.Context ) {
@@ -109,6 +122,81 @@ func (h *RedeemHandler) Generate(c *gin.Context) {
109122 })
110123}
111124
125+ // CreateAndRedeem creates a fixed redeem code and redeems it for a target user in one step.
126+ // POST /api/v1/admin/redeem-codes/create-and-redeem
127+ func (h * RedeemHandler ) CreateAndRedeem (c * gin.Context ) {
128+ if h .redeemService == nil {
129+ response .InternalError (c , "redeem service not configured" )
130+ return
131+ }
132+
133+ var req CreateAndRedeemCodeRequest
134+ if err := c .ShouldBindJSON (& req ); err != nil {
135+ response .BadRequest (c , "Invalid request: " + err .Error ())
136+ return
137+ }
138+ req .Code = strings .TrimSpace (req .Code )
139+
140+ executeAdminIdempotentJSON (c , "admin.redeem_codes.create_and_redeem" , req , service .DefaultWriteIdempotencyTTL (), func (ctx context.Context ) (any , error ) {
141+ existing , err := h .redeemService .GetByCode (ctx , req .Code )
142+ if err == nil {
143+ return h .resolveCreateAndRedeemExisting (ctx , existing , req .UserID )
144+ }
145+ if ! errors .Is (err , service .ErrRedeemCodeNotFound ) {
146+ return nil , err
147+ }
148+
149+ createErr := h .redeemService .CreateCode (ctx , & service.RedeemCode {
150+ Code : req .Code ,
151+ Type : req .Type ,
152+ Value : req .Value ,
153+ Status : service .StatusUnused ,
154+ Notes : req .Notes ,
155+ })
156+ if createErr != nil {
157+ // Unique code race: if code now exists, use idempotent semantics by used_by.
158+ existingAfterCreateErr , getErr := h .redeemService .GetByCode (ctx , req .Code )
159+ if getErr == nil {
160+ return h .resolveCreateAndRedeemExisting (ctx , existingAfterCreateErr , req .UserID )
161+ }
162+ return nil , createErr
163+ }
164+
165+ redeemed , redeemErr := h .redeemService .Redeem (ctx , req .UserID , req .Code )
166+ if redeemErr != nil {
167+ return nil , redeemErr
168+ }
169+ return gin.H {"redeem_code" : dto .RedeemCodeFromServiceAdmin (redeemed )}, nil
170+ })
171+ }
172+
173+ func (h * RedeemHandler ) resolveCreateAndRedeemExisting (ctx context.Context , existing * service.RedeemCode , userID int64 ) (any , error ) {
174+ if existing == nil {
175+ return nil , infraerrors .Conflict ("REDEEM_CODE_CONFLICT" , "redeem code conflict" )
176+ }
177+
178+ // If previous run created the code but crashed before redeem, redeem it now.
179+ if existing .CanUse () {
180+ redeemed , err := h .redeemService .Redeem (ctx , userID , existing .Code )
181+ if err == nil {
182+ return gin.H {"redeem_code" : dto .RedeemCodeFromServiceAdmin (redeemed )}, nil
183+ }
184+ if ! errors .Is (err , service .ErrRedeemCodeUsed ) {
185+ return nil , err
186+ }
187+ latest , getErr := h .redeemService .GetByCode (ctx , existing .Code )
188+ if getErr == nil {
189+ existing = latest
190+ }
191+ }
192+
193+ if existing .UsedBy != nil && * existing .UsedBy == userID {
194+ return gin.H {"redeem_code" : dto .RedeemCodeFromServiceAdmin (existing )}, nil
195+ }
196+
197+ return nil , infraerrors .Conflict ("REDEEM_CODE_CONFLICT" , "redeem code already used by another user" )
198+ }
199+
112200// Delete handles deleting a redeem code
113201// DELETE /api/v1/admin/redeem-codes/:id
114202func (h * RedeemHandler ) Delete (c * gin.Context ) {
0 commit comments