fix(redemption): enhance redemption code flow with transaction safety and idempotency
This commit addresses critical issues in the redemption code activation flow
to ensure data consistency, prevent duplicate redemptions, and improve user
experience.
Key improvements:
1. Transaction Safety (P0)
- Wrap subscription creation, used count update, and record insertion in
a single database transaction
- Ensure atomicity: all operations succeed or all rollback
- Prevent orphaned records and data inconsistencies
2. Idempotency Protection (P0)
- Add redemption record check before processing to prevent duplicate
operations on queue task retries
- Maintain idempotency at multiple layers: interface, order, and record
3. Distributed Lock (P1)
- Implement Redis-based distributed lock (10s timeout) to prevent
concurrent duplicate redemptions
- Lock key format: redemption_lock:{user_id}:{code}
4. IsNew Field Correction (P2)
- Fix IsNew field to correctly determine first-time purchases using
IsUserEligibleForNewOrder method
- Ensure accurate statistics and future commission calculations
5. Quota Pre-check (P2)
- Add quota validation at interface layer for immediate user feedback
- Prevent "processing" status followed by eventual failure
6. Extended Cache TTL (P2)
- Increase Redis cache expiration from 30 minutes to 2 hours
- Ensure queue tasks can retrieve redemption data even with delays
7. Error Handling (P2)
- Clean up Order records when Redis cache or queue enqueue fails
- Prevent orphaned Order records in the database
8. Cache Clearing Optimization
- Add user subscription cache clearing after activation
- Ensure both node-side and user-side display latest subscription info
Technical details:
- Modified: internal/logic/public/redemption/redeemCodeLogic.go
- Modified: queue/logic/order/activateOrderLogic.go
- Modified: internal/model/redemption/default.go (transaction support)
Testing:
- All changes compiled successfully
- Comprehensive flow verification completed
- Ready for production deployment
BREAKING CHANGE: None
This commit is contained in:
@@ -24,14 +24,14 @@ type (
|
||||
Insert(ctx context.Context, data *RedemptionCode) error
|
||||
FindOne(ctx context.Context, id int64) (*RedemptionCode, error)
|
||||
FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error)
|
||||
Update(ctx context.Context, data *RedemptionCode) error
|
||||
Update(ctx context.Context, data *RedemptionCode, tx ...*gorm.DB) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
|
||||
customRedemptionCodeLogicModel
|
||||
}
|
||||
|
||||
RedemptionRecordModel interface {
|
||||
Insert(ctx context.Context, data *RedemptionRecord) error
|
||||
Insert(ctx context.Context, data *RedemptionRecord, tx ...*gorm.DB) error
|
||||
FindOne(ctx context.Context, id int64) (*RedemptionRecord, error)
|
||||
Update(ctx context.Context, data *RedemptionRecord) error
|
||||
Delete(ctx context.Context, id int64) error
|
||||
@@ -41,7 +41,7 @@ type (
|
||||
customRedemptionCodeLogicModel interface {
|
||||
QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error)
|
||||
BatchDelete(ctx context.Context, ids []int64) error
|
||||
IncrementUsedCount(ctx context.Context, id int64) error
|
||||
IncrementUsedCount(ctx context.Context, id int64, tx ...*gorm.DB) error
|
||||
}
|
||||
|
||||
customRedemptionRecordLogicModel interface {
|
||||
@@ -130,13 +130,16 @@ func (m *defaultRedemptionCodeModel) FindOneByCode(ctx context.Context, code str
|
||||
}
|
||||
}
|
||||
|
||||
func (m *defaultRedemptionCodeModel) Update(ctx context.Context, data *RedemptionCode) error {
|
||||
func (m *defaultRedemptionCodeModel) Update(ctx context.Context, data *RedemptionCode, tx ...*gorm.DB) error {
|
||||
old, err := m.FindOne(ctx, data.Id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
db := conn
|
||||
if len(tx) > 0 {
|
||||
db = tx[0]
|
||||
}
|
||||
return db.Save(data).Error
|
||||
}, m.getCacheKeys(old)...)
|
||||
return err
|
||||
@@ -189,12 +192,15 @@ func (m *customRedemptionCodeModel) BatchDelete(ctx context.Context, ids []int64
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *customRedemptionCodeModel) IncrementUsedCount(ctx context.Context, id int64) error {
|
||||
func (m *customRedemptionCodeModel) IncrementUsedCount(ctx context.Context, id int64, tx ...*gorm.DB) error {
|
||||
data, err := m.FindOne(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.UsedCount++
|
||||
if len(tx) > 0 {
|
||||
return m.Update(ctx, data, tx[0])
|
||||
}
|
||||
return m.Update(ctx, data)
|
||||
}
|
||||
|
||||
@@ -210,8 +216,11 @@ func (m *defaultRedemptionRecordModel) getCacheKeys(data *RedemptionRecord) []st
|
||||
return cacheKeys
|
||||
}
|
||||
|
||||
func (m *defaultRedemptionRecordModel) Insert(ctx context.Context, data *RedemptionRecord) error {
|
||||
func (m *defaultRedemptionRecordModel) Insert(ctx context.Context, data *RedemptionRecord, tx ...*gorm.DB) error {
|
||||
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return conn.Create(data).Error
|
||||
}, m.getCacheKeys(data)...)
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user