修复退款审核前账号提前释放
This commit is contained in:
@@ -391,10 +391,13 @@ func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatu
|
|||||||
func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) error {
|
func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) error {
|
||||||
var refund *refundAction
|
var refund *refundAction
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
var order model.RentalOrder
|
assets, err := r.lockOrderAssets(tx, orderID)
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
order := assets.Order
|
||||||
|
listing := assets.Listing
|
||||||
|
account := assets.Account
|
||||||
if order.RefundStatus != refundStatusPendingReview {
|
if order.RefundStatus != refundStatusPendingReview {
|
||||||
return errors.New("订单不处于待审核退款状态")
|
return errors.New("订单不处于待审核退款状态")
|
||||||
}
|
}
|
||||||
@@ -407,14 +410,23 @@ func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) err
|
|||||||
// 全部押金被暂扣且无其他可退金额,直接标记为已处理,等待后续手动退押金。
|
// 全部押金被暂扣且无其他可退金额,直接标记为已处理,等待后续手动退押金。
|
||||||
order.RefundStatus = "none"
|
order.RefundStatus = "none"
|
||||||
order.RefundAmountCent = 0
|
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 := releaseAssetsForRentalIfIdle(tx, order, listing, account); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
refund = action
|
if err := tx.Save(order).Error; err != nil {
|
||||||
return tx.Save(&order).Error
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Save(listing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Save(account).Error
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -368,8 +368,11 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
|||||||
return err
|
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 {
|
if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package order
|
package order
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestCancelDoesNotReleaseAssetsWhenSiblingOrderActive(t *testing.T) {
|
||||||
db := setupOrderTestDB(t)
|
db := setupOrderTestDB(t)
|
||||||
repo := NewRepository(db)
|
repo := NewRepository(db)
|
||||||
|
|||||||
@@ -293,17 +293,20 @@ export function useOrderActions(options: UseOrderActionsOptions) {
|
|||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
|
const needsRefundReview = order.value.status === 'pending_handoff'
|
||||||
if (needsConfirm('cancel')) {
|
if (needsConfirm('cancel')) {
|
||||||
const ok = await options.confirm({
|
const ok = await options.confirm({
|
||||||
title: '取消订单',
|
title: '取消订单',
|
||||||
message: '确定取消订单并释放账号吗?',
|
message: needsRefundReview
|
||||||
|
? '确定申请取消订单吗?账号将在客服审核通过后释放,退款将原路退回。'
|
||||||
|
: '确定取消订单并释放账号吗?',
|
||||||
})
|
})
|
||||||
if (!ok) return
|
if (!ok) return
|
||||||
}
|
}
|
||||||
cancelling.value = true
|
cancelling.value = true
|
||||||
try {
|
try {
|
||||||
await cancelOrder(order.value.id)
|
await cancelOrder(order.value.id)
|
||||||
options.notifySuccess('订单已取消,账号已释放')
|
options.notifySuccess(needsRefundReview ? '退款申请已提交,等待客服审核' : '订单已取消,账号已释放')
|
||||||
await router.push(options.ordersPath)
|
await router.push(options.ordersPath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
options.notifyError(readError(error, '取消失败'))
|
options.notifyError(readError(error, '取消失败'))
|
||||||
|
|||||||
@@ -463,7 +463,7 @@ watch([() => route.query.focus, order, loading], () => {
|
|||||||
<div v-if="order.status === 'pending_handoff'" class="secondary-action-card danger">
|
<div v-if="order.status === 'pending_handoff'" class="secondary-action-card danger">
|
||||||
<div>
|
<div>
|
||||||
<strong>不继续交接?</strong>
|
<strong>不继续交接?</strong>
|
||||||
<span>取消后将释放账号,订单会回到可租状态。</span>
|
<span>取消后将提交退款申请,客服审核通过后账号才会恢复可租。</span>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="danger" plain :loading="cancelling" @click="handleCancel">
|
<el-button type="danger" plain :loading="cancelling" @click="handleCancel">
|
||||||
取消订单
|
取消订单
|
||||||
|
|||||||
Reference in New Issue
Block a user