From 266abb4aaaa24d809e30177c6a9fd04a66c6474b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 20 Aug 2026 16:50:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=80=80=E6=AC=BE=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E5=89=8D=E8=B4=A6=E5=8F=B7=E6=8F=90=E5=89=8D=E9=87=8A?= =?UTF-8?q?=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/modules/order/admin_actions.go | 26 +++-- backend/internal/modules/order/lifecycle.go | 7 +- .../order/repository_integration_test.go | 100 ++++++++++++++++++ .../orders/composables/useOrderActions.ts | 7 +- .../features/orders/views/OrderDetailView.vue | 2 +- 5 files changed, 130 insertions(+), 12 deletions(-) diff --git a/backend/internal/modules/order/admin_actions.go b/backend/internal/modules/order/admin_actions.go index 969b077..8227b08 100644 --- a/backend/internal/modules/order/admin_actions.go +++ b/backend/internal/modules/order/admin_actions.go @@ -391,10 +391,13 @@ func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatu func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) error { var refund *refundAction err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - var order model.RentalOrder - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { + assets, err := r.lockOrderAssets(tx, orderID) + if err != nil { return err } + order := assets.Order + listing := assets.Listing + account := assets.Account if order.RefundStatus != refundStatusPendingReview { return errors.New("订单不处于待审核退款状态") } @@ -407,14 +410,23 @@ func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) err // 全部押金被暂扣且无其他可退金额,直接标记为已处理,等待后续手动退押金。 order.RefundStatus = "none" order.RefundAmountCent = 0 - return tx.Save(&order).Error + } else { + action, err := r.prepareRefund(order, refundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)") + if err != nil { + return err + } + refund = action } - action, err := r.prepareRefund(&order, refundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)") - if err != nil { + if err := releaseAssetsForRentalIfIdle(tx, order, listing, account); err != nil { return err } - refund = action - return tx.Save(&order).Error + if err := tx.Save(order).Error; err != nil { + return err + } + if err := tx.Save(listing).Error; err != nil { + return err + } + return tx.Save(account).Error }) if err != nil { return err diff --git a/backend/internal/modules/order/lifecycle.go b/backend/internal/modules/order/lifecycle.go index 50573b5..530e6ef 100644 --- a/backend/internal/modules/order/lifecycle.go +++ b/backend/internal/modules/order/lifecycle.go @@ -368,8 +368,11 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64) return err } } - if err := releaseAssetsForRentalIfIdle(tx, &order, listing, account); err != nil { - return err + // 已付款订单取消后需等待客服审核退款,审核通过前继续占用账号。 + if beforeStatus == orderStatusPendingPayment { + if err := releaseAssetsForRentalIfIdle(tx, &order, listing, account); err != nil { + return err + } } if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil { return err diff --git a/backend/internal/modules/order/repository_integration_test.go b/backend/internal/modules/order/repository_integration_test.go index a91b03e..835e1b8 100644 --- a/backend/internal/modules/order/repository_integration_test.go +++ b/backend/internal/modules/order/repository_integration_test.go @@ -1,6 +1,7 @@ package order import ( + "context" "testing" "time" @@ -721,6 +722,105 @@ func TestCancelClosesPendingPaymentOrder(t *testing.T) { } } +func TestCancelPendingHandoffKeepsAssetsUntilRefundApproved(t *testing.T) { + db := setupOrderTestDB(t) + repo := NewRepository(db, Dependencies{ + RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) { + return "refunding", nil + }), + }) + owner := model.User{Phone: "13800001002"} + renter := model.User{Phone: "13900001002"} + if err := db.Create(&owner).Error; err != nil { + t.Fatalf("create owner failed: %v", err) + } + if err := db.Create(&renter).Error; err != nil { + t.Fatalf("create renter failed: %v", err) + } + account := model.GameAccount{ + OwnerID: owner.ID, + Status: accountStatusRented, + ServerRegion: "国服", + LoginPlatform: "steam", + Title: "待退款审核账号", + } + if err := db.Create(&account).Error; err != nil { + t.Fatalf("create account failed: %v", err) + } + listing := model.RentalListing{ + ListingNo: "LST-REFUND-REVIEW-001", + OwnerID: owner.ID, + AccountID: account.ID, + Status: listingStatusRented, + ReviewStatus: listingReviewStatusApproved, + InTransaction: true, + PriceCent: 1000, + } + if err := db.Create(&listing).Error; err != nil { + t.Fatalf("create listing failed: %v", err) + } + order := model.RentalOrder{ + OrderNo: "ORD-REFUND-REVIEW-001", + ListingID: listing.ID, + AccountID: account.ID, + OwnerID: owner.ID, + RenterID: renter.ID, + RentAmountCent: 1000, + Status: orderStatusPendingHandoff, + HandoffStatus: handoffStatusPendingOwner, + } + if err := db.Create(&order).Error; err != nil { + t.Fatalf("create order failed: %v", err) + } + + if err := repo.Cancel(t.Context(), renter.ID, order.ID); err != nil { + t.Fatalf("Cancel() error = %v", err) + } + assertAssetsRented := func() { + var savedListing model.RentalListing + if err := db.First(&savedListing, listing.ID).Error; err != nil { + t.Fatalf("find listing failed: %v", err) + } + if savedListing.Status != listingStatusRented || !savedListing.InTransaction { + t.Fatalf("listing = %s/in_transaction:%t, want rented/true", savedListing.Status, savedListing.InTransaction) + } + var savedAccount model.GameAccount + if err := db.First(&savedAccount, account.ID).Error; err != nil { + t.Fatalf("find account failed: %v", err) + } + if savedAccount.Status != accountStatusRented { + t.Fatalf("account status = %q, want %q", savedAccount.Status, accountStatusRented) + } + } + assertAssetsRented() + + var pendingReviewOrder model.RentalOrder + if err := db.First(&pendingReviewOrder, order.ID).Error; err != nil { + t.Fatalf("find cancelled order failed: %v", err) + } + if pendingReviewOrder.RefundStatus != refundStatusPendingReview { + t.Fatalf("refund status = %q, want %q", pendingReviewOrder.RefundStatus, refundStatusPendingReview) + } + if err := repo.AdminApproveRefund(t.Context(), order.ID); err != nil { + t.Fatalf("AdminApproveRefund() error = %v", err) + } + + var releasedListing model.RentalListing + if err := db.First(&releasedListing, listing.ID).Error; err != nil { + t.Fatalf("find released listing failed: %v", err) + } + if releasedListing.Status != listingStatusPublished || releasedListing.InTransaction { + t.Fatalf("listing = %s/in_transaction:%t, want published/false", releasedListing.Status, releasedListing.InTransaction) + } + var releasedAccount model.GameAccount + if err := db.First(&releasedAccount, account.ID).Error; err != nil { + t.Fatalf("find released account failed: %v", err) + } + if releasedAccount.Status != accountStatusPublished { + t.Fatalf("account status = %q, want %q", releasedAccount.Status, accountStatusPublished) + } +} + func TestCancelDoesNotReleaseAssetsWhenSiblingOrderActive(t *testing.T) { db := setupOrderTestDB(t) repo := NewRepository(db) diff --git a/frontend/src/features/orders/composables/useOrderActions.ts b/frontend/src/features/orders/composables/useOrderActions.ts index b380bbd..793eea2 100644 --- a/frontend/src/features/orders/composables/useOrderActions.ts +++ b/frontend/src/features/orders/composables/useOrderActions.ts @@ -293,17 +293,20 @@ export function useOrderActions(options: UseOrderActionsOptions) { async function handleCancel() { if (!order.value) return + const needsRefundReview = order.value.status === 'pending_handoff' if (needsConfirm('cancel')) { const ok = await options.confirm({ title: '取消订单', - message: '确定取消订单并释放账号吗?', + message: needsRefundReview + ? '确定申请取消订单吗?账号将在客服审核通过后释放,退款将原路退回。' + : '确定取消订单并释放账号吗?', }) if (!ok) return } cancelling.value = true try { await cancelOrder(order.value.id) - options.notifySuccess('订单已取消,账号已释放') + options.notifySuccess(needsRefundReview ? '退款申请已提交,等待客服审核' : '订单已取消,账号已释放') await router.push(options.ordersPath) } catch (error) { options.notifyError(readError(error, '取消失败')) diff --git a/frontend/src/features/orders/views/OrderDetailView.vue b/frontend/src/features/orders/views/OrderDetailView.vue index 149d79f..93d5c8f 100644 --- a/frontend/src/features/orders/views/OrderDetailView.vue +++ b/frontend/src/features/orders/views/OrderDetailView.vue @@ -463,7 +463,7 @@ watch([() => route.query.focus, order, loading], () => {
不继续交接? - 取消后将释放账号,订单会回到可租状态。 + 取消后将提交退款申请,客服审核通过后账号才会恢复可租。
取消订单