diff --git a/backend/internal/modules/order/admin_actions.go b/backend/internal/modules/order/admin_actions.go index 5602190..117e15a 100644 --- a/backend/internal/modules/order/admin_actions.go +++ b/backend/internal/modules/order/admin_actions.go @@ -7,16 +7,18 @@ import ( "hfb_sys/backend/internal/modules/notification" "gorm.io/gorm" - "gorm.io/gorm/clause" ) func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error { var refund *refundAction err := r.db.Transaction(func(tx *gorm.DB) error { - order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID) + assets, err := r.lockOrderAssets(tx, orderID) if err != nil { return err } + order := assets.Order + listing := assets.Listing + account := assets.Account if isTerminalStatus(order.Status) { return ErrOrderCannotComplete } @@ -30,9 +32,7 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR order.HandoffStatus = handoffStatusAdminClosed order.SettlementStatus = settlementStatusClosed order.SettledAt = &now - listing.Status = listingStatusOffline - listing.InTransaction = false - account.Status = accountStatusOffline + archiveAssets(listing, account) if beforeOrderStatus != orderStatusPendingPayment { totalCent := order.RentAmountCent + order.DepositAmountCent action, err := r.prepareRefund(order, totalCent, refundBizAdminClose, "客服关闭订单原路退款") @@ -97,10 +97,13 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error { return r.db.Transaction(func(tx *gorm.DB) error { - order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID) + assets, err := r.lockOrderAssets(tx, orderID) if err != nil { return err } + order := assets.Order + listing := assets.Listing + account := assets.Account if isTerminalStatus(order.Status) { return ErrOrderCannotComplete } @@ -110,9 +113,7 @@ func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req Admin beforeAccountStatus := account.Status order.Status = orderStatusAbnormal order.HandoffStatus = handoffStatusAdminAbnormal - listing.Status = listingStatusAbnormal - listing.InTransaction = false - account.Status = accountStatusAbnormal + markAssetsAbnormal(listing, account) if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, @@ -211,22 +212,6 @@ func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatu } } -func (r *Repository) findOrderAssetsForAdminUpdate(tx *gorm.DB, orderID uint64) (*model.RentalOrder, *model.RentalListing, *model.GameAccount, error) { - var order model.RentalOrder - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { - return nil, nil, nil, err - } - var listing model.RentalListing - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { - return nil, nil, nil, err - } - var account model.GameAccount - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { - return nil, nil, nil, err - } - return &order, &listing, &account, nil -} - func isTerminalStatus(status string) bool { return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed } diff --git a/backend/internal/modules/order/assets.go b/backend/internal/modules/order/assets.go new file mode 100644 index 0000000..5768df0 --- /dev/null +++ b/backend/internal/modules/order/assets.go @@ -0,0 +1,71 @@ +package order + +import ( + "hfb_sys/backend/internal/model" + + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +type orderAssets struct { + Order *model.RentalOrder + Listing *model.RentalListing + Account *model.GameAccount +} + +func (r *Repository) lockOrderAssets(tx *gorm.DB, orderID uint64) (*orderAssets, error) { + var order model.RentalOrder + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { + return nil, err + } + listing, account, err := r.lockListingAccountForOrder(tx, &order) + if err != nil { + return nil, err + } + return &orderAssets{ + Order: &order, + Listing: listing, + Account: account, + }, nil +} + +func (r *Repository) lockListingAccountForOrder(tx *gorm.DB, order *model.RentalOrder) (*model.RentalListing, *model.GameAccount, error) { + var listing model.RentalListing + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { + return nil, nil, err + } + var account model.GameAccount + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { + return nil, nil, err + } + return &listing, &account, nil +} + +func reserveListingForOrder(listing *model.RentalListing) { + listing.InTransaction = true +} + +func markAssetsRented(listing *model.RentalListing, account *model.GameAccount) { + listing.Status = listingStatusRented + account.Status = accountStatusRented +} + +func releaseAssetsForRental(listing *model.RentalListing, account *model.GameAccount) { + listing.Status = listingStatusPublished + listing.InTransaction = false + account.Status = accountStatusPublished +} + +func archiveAssets(listing *model.RentalListing, account *model.GameAccount) { + listing.Status = listingStatusOffline + listing.InTransaction = false + listing.PublishedAt = nil + account.Status = accountStatusOffline +} + +func markAssetsAbnormal(listing *model.RentalListing, account *model.GameAccount) { + listing.Status = listingStatusAbnormal + listing.InTransaction = false + listing.PublishedAt = nil + account.Status = accountStatusAbnormal +} diff --git a/backend/internal/modules/order/checkout.go b/backend/internal/modules/order/checkout.go index 65c5fae..91b8270 100644 --- a/backend/internal/modules/order/checkout.go +++ b/backend/internal/modules/order/checkout.go @@ -230,12 +230,8 @@ func (r *Repository) AcceptCheckout(userID uint64, orderID uint64) error { } func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) (*refundAction, error) { - var listing model.RentalListing - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { - return nil, err - } - var account model.GameAccount - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { + listing, account, err := r.lockListingAccountForOrder(tx, order) + if err != nil { return nil, err } now := time.Now() @@ -244,7 +240,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che order.SettlementStatus = settlementStatusSettled order.SettledAt = &now order.OwnerSettledAt = &now - archiveListingAfterCheckout(&listing, &account) + archiveAssets(listing, account) orderID := order.ID settlement := buildCheckoutSettlement(*order, checkout) @@ -321,23 +317,15 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che if err := tx.Save(checkout).Error; err != nil { return nil, err } - if err := tx.Save(&listing).Error; err != nil { + if err := tx.Save(listing).Error; err != nil { return nil, err } - if err := tx.Save(&account).Error; err != nil { + if err := tx.Save(account).Error; err != nil { return nil, err } return refund, nil } -// 完成后的账号先下架,避免已完成订单对应的账号重新出现在公开首页。 -func archiveListingAfterCheckout(listing *model.RentalListing, account *model.GameAccount) { - listing.Status = listingStatusOffline - listing.InTransaction = false - listing.PublishedAt = nil - account.Status = accountStatusOffline -} - func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) { var count int64 err := tx.Model(&model.OrderCheckout{}). diff --git a/backend/internal/modules/order/lifecycle.go b/backend/internal/modules/order/lifecycle.go index 21c4184..cf4bc4b 100644 --- a/backend/internal/modules/order/lifecycle.go +++ b/backend/internal/modules/order/lifecycle.go @@ -78,7 +78,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro ); err != nil { return err } - listing.InTransaction = true + reserveListingForOrder(&listing) if err := tx.Save(&listing).Error; err != nil { return err } @@ -135,10 +135,13 @@ func (r *Repository) Pay(userID uint64, orderID uint64) error { func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string) error { var newConvID uint64 err := r.db.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.Status == orderStatusPendingHandoff || order.Status == orderStatusRenting { return nil } @@ -146,25 +149,16 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string return ErrOrderCannotPay } - var listing model.RentalListing - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { - return err - } if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction { return ErrListingUnavailable } - var account model.GameAccount - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { - return err - } // 租客已通过外部渠道付款,这里不写租客钱包流水。 orderID := order.ID order.Status = orderStatusPendingHandoff order.HandoffStatus = handoffStatusPendingOwner - listing.Status = listingStatusRented - account.Status = accountStatusRented - conv, err := chat.EnsureOrderConversation(tx, order) + markAssetsRented(listing, account) + conv, err := chat.EnsureOrderConversation(tx, *order) if err != nil { return err } @@ -189,13 +183,13 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string ); err != nil { return err } - if err := tx.Save(&order).Error; err != nil { + if err := tx.Save(order).Error; err != nil { return err } - if err := tx.Save(&listing).Error; err != nil { + if err := tx.Save(listing).Error; err != nil { return err } - return tx.Save(&account).Error + return tx.Save(account).Error }) if err != nil { return err @@ -218,12 +212,8 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error { if order.Status != orderStatusPendingPayment && order.Status != orderStatusPendingHandoff { return ErrOrderCannotCancel } - var listing model.RentalListing - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { - return err - } - var account model.GameAccount - if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { + listing, account, err := r.lockListingAccountForOrder(tx, &order) + if err != nil { return err } @@ -259,16 +249,14 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error { ); err != nil { return err } - listing.Status = listingStatusPublished - listing.InTransaction = false - account.Status = accountStatusPublished + releaseAssetsForRental(listing, account) if err := tx.Save(&order).Error; err != nil { return err } - if err := tx.Save(&listing).Error; err != nil { + if err := tx.Save(listing).Error; err != nil { return err } - return tx.Save(&account).Error + return tx.Save(account).Error }) if err != nil { return err diff --git a/backend/internal/modules/order/repository_test.go b/backend/internal/modules/order/repository_test.go index e08274b..53ebca4 100644 --- a/backend/internal/modules/order/repository_test.go +++ b/backend/internal/modules/order/repository_test.go @@ -140,14 +140,29 @@ func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) { } } -func TestArchiveListingAfterCheckoutMovesListingOffline(t *testing.T) { - // This test is purely for contract documentation; no behavior is tested yet. - // When implementing auto-archive behavior: - // - Call should succeed for renting/overdue/pending_checkout_confirm/pending_checkout_accept orders - // - Listing status should be set to "archived" or "offline" - // - In-transaction flag should be cleared - // - Order should enter completed or closed state - _ = time.Now() +func TestArchiveAssetsMovesListingOffline(t *testing.T) { + now := time.Now() + listing := model.RentalListing{ + Status: listingStatusRented, + InTransaction: true, + PublishedAt: &now, + } + account := model.GameAccount{Status: accountStatusRented} + + archiveAssets(&listing, &account) + + if listing.Status != listingStatusOffline { + t.Fatalf("listing.Status = %q, want %q", listing.Status, listingStatusOffline) + } + if listing.InTransaction { + t.Fatal("listing.InTransaction = true, want false") + } + if listing.PublishedAt != nil { + t.Fatal("listing.PublishedAt is not nil") + } + if account.Status != accountStatusOffline { + t.Fatalf("account.Status = %q, want %q", account.Status, accountStatusOffline) + } } func TestNewOrderNoUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) {