diff --git a/backend/app/http/v1/dto/order.go b/backend/app/http/v1/dto/order.go index c4990e9..ec0bcbd 100644 --- a/backend/app/http/v1/dto/order.go +++ b/backend/app/http/v1/dto/order.go @@ -9,6 +9,8 @@ type OrderCreateForm struct { Quantity int `json:"quantity"` // UserCouponID 用户券ID(可选)。 UserCouponID int64 `json:"user_coupon_id"` + // IdempotencyKey 幂等键(同一业务请求需保持一致)。 + IdempotencyKey *string `json:"idempotency_key"` } type OrderCreateResponse struct { diff --git a/backend/app/services/order.go b/backend/app/services/order.go index 8a38acf..63d5a4a 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "strings" "time" "quyun/v2/app/errorx" @@ -78,6 +79,22 @@ func (s *order) Create( uid := userID cid := form.ContentID + // 幂等控制:相同幂等键直接返回已创建的订单。 + idempotencyKey := "" + if form.IdempotencyKey != nil { + idempotencyKey = strings.TrimSpace(*form.IdempotencyKey) + } + if idempotencyKey != "" { + tbl, q := models.OrderQuery.QueryContext(ctx) + existing, err := q.Where(tbl.UserID.Eq(uid), tbl.IdempotencyKey.Eq(idempotencyKey)).First() + if err == nil { + return &transaction_dto.OrderCreateResponse{OrderID: existing.ID}, nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + } + // 1. Fetch Content & Price content, err := models.ContentQuery.WithContext(ctx).Where(models.ContentQuery.ID.Eq(cid)).First() if err != nil { @@ -123,9 +140,12 @@ func (s *order) Create( AmountDiscount: amountDiscount, AmountPaid: amountPaid, CouponID: couponID, - IdempotencyKey: uuid.NewString(), + IdempotencyKey: idempotencyKey, Snapshot: types.NewJSONType(fields.OrdersSnapshot{}), } + if order.IdempotencyKey == "" { + order.IdempotencyKey = uuid.NewString() + } err = models.Q.Transaction(func(tx *models.Query) error { if err := tx.Order.WithContext(ctx).Create(order); err != nil {