fix: persist guest/redemption activation context to DB to survive Redis TTL expiry (Bug 1 + Bug 10)

- Add `activation_context` TEXT column to `order` table (migration 02150)
- purchaseLogic: write TemporaryOrderInfo JSON to order.ActivationContext in the same
  insert transaction; Redis write is now best-effort (non-fatal)
- redeemCodeLogic: write redemption {type, redemption_code_id, unit_time, quantity} JSON
  to order.ActivationContext at order creation; Redis write is now best-effort (non-fatal)
- getTempOrderInfo: on Redis miss, fall back to order.ActivationContext from DB;
  logs CRITICAL and returns error if both are missing (old orders with no DB record)
- RedemptionActivate: on Redis miss, fall back to order.ActivationContext from DB;
  same CRITICAL log path for legacy orders

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
2026-05-25 00:13:59 -07:00
parent 30221232c9
commit a0f2d8a7b8
6 changed files with 130 additions and 61 deletions
+77 -26
View File
@@ -863,26 +863,52 @@ func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *ord
return userInfo, nil
}
// getTempOrderInfo retrieves temporary order information from Redis cache
// getTempOrderInfo retrieves temporary order information from Redis cache with DB fallback.
func (l *ActivateOrderLogic) getTempOrderInfo(ctx context.Context, orderNo string) (*constant.TemporaryOrderInfo, error) {
cacheKey := fmt.Sprintf(constant.TempOrderCacheKey, orderNo)
data, err := l.svc.Redis.Get(ctx, cacheKey).Result()
if err != nil {
logger.WithContext(ctx).Error("Get temp order cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
if err == nil {
var tempOrder constant.TemporaryOrderInfo
if unmarshalErr := tempOrder.Unmarshal([]byte(data)); unmarshalErr != nil {
logger.WithContext(ctx).Error("Unmarshal temp order cache failed",
logger.Field("error", unmarshalErr.Error()),
logger.Field("cache_key", cacheKey),
logger.Field("data", data),
)
return nil, unmarshalErr
}
return &tempOrder, nil
}
// Redis miss — fall back to DB activation_context field.
logger.WithContext(ctx).Infow("Redis cache miss for temp order, falling back to DB",
logger.Field("cache_key", cacheKey),
logger.Field("error", err.Error()),
)
orderInfo, dbErr := l.svc.OrderModel.FindOneByOrderNo(ctx, orderNo)
if dbErr != nil {
logger.WithContext(ctx).Error("DB fallback for temp order failed",
logger.Field("order_no", orderNo),
logger.Field("error", dbErr.Error()),
)
return nil, err
return nil, dbErr
}
if orderInfo.ActivationContext == "" {
logger.WithContext(ctx).Error("CRITICAL: activation_context missing in DB and Redis expired; cannot recover guest order",
logger.Field("order_no", orderNo),
)
return nil, errors.New("activation context not found: Redis expired and no DB fallback available")
}
var tempOrder constant.TemporaryOrderInfo
if err = tempOrder.Unmarshal([]byte(data)); err != nil {
logger.WithContext(ctx).Error("Unmarshal temp order cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
logger.Field("data", data),
if unmarshalErr := tempOrder.Unmarshal([]byte(orderInfo.ActivationContext)); unmarshalErr != nil {
logger.WithContext(ctx).Error("Unmarshal DB activation_context failed",
logger.Field("order_no", orderNo),
logger.Field("error", unmarshalErr.Error()),
)
return nil, err
return nil, unmarshalErr
}
return &tempOrder, nil
@@ -1782,25 +1808,50 @@ func (l *ActivateOrderLogic) RedemptionActivate(ctx context.Context, orderInfo *
return err
}
// 3. 从Redis获取兑换码信息
cacheKey := fmt.Sprintf("redemption_order:%s", orderInfo.OrderNo)
data, err := l.svc.Redis.Get(ctx, cacheKey).Result()
if err != nil {
logger.WithContext(ctx).Error("Get redemption cache failed",
logger.Field("error", err.Error()),
logger.Field("cache_key", cacheKey),
)
return err
}
// 3. 从Redis获取兑换码信息Redis缺失时回查DB
var redemptionData struct {
RedemptionCodeId int64 `json:"redemption_code_id"`
UnitTime string `json:"unit_time"`
Quantity int64 `json:"quantity"`
}
if err = json.Unmarshal([]byte(data), &redemptionData); err != nil {
logger.WithContext(ctx).Error("Unmarshal redemption cache failed", logger.Field("error", err.Error()))
return err
cacheKey := fmt.Sprintf("redemption_order:%s", orderInfo.OrderNo)
data, redisErr := l.svc.Redis.Get(ctx, cacheKey).Result()
if redisErr == nil {
if err = json.Unmarshal([]byte(data), &redemptionData); err != nil {
logger.WithContext(ctx).Error("Unmarshal redemption cache failed", logger.Field("error", err.Error()))
return err
}
} else {
// Redis miss — fall back to DB activation_context field.
logger.WithContext(ctx).Infow("Redis cache miss for redemption order, falling back to DB",
logger.Field("cache_key", cacheKey),
logger.Field("error", redisErr.Error()),
)
if orderInfo.ActivationContext == "" {
logger.WithContext(ctx).Error("CRITICAL: activation_context missing in DB and Redis expired; cannot recover redemption order",
logger.Field("order_no", orderInfo.OrderNo),
)
return errors.New("activation context not found: Redis expired and no DB fallback available")
}
var fullContext struct {
Type string `json:"type"`
RedemptionCodeId int64 `json:"redemption_code_id"`
UnitTime string `json:"unit_time"`
Quantity int64 `json:"quantity"`
}
if err = json.Unmarshal([]byte(orderInfo.ActivationContext), &fullContext); err != nil {
logger.WithContext(ctx).Error("Unmarshal DB activation_context failed",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("error", err.Error()),
)
return err
}
redemptionData.RedemptionCodeId = fullContext.RedemptionCodeId
redemptionData.UnitTime = fullContext.UnitTime
redemptionData.Quantity = fullContext.Quantity
}
// 4. 幂等性检查:查询是否已有兑换记录