统一订单资产状态变更

This commit is contained in:
yml
2026-06-09 20:11:28 +08:00
parent 0990652f8b
commit 4c2199df58
5 changed files with 125 additions and 78 deletions
+10 -25
View File
@@ -7,16 +7,18 @@ import (
"hfb_sys/backend/internal/modules/notification" "hfb_sys/backend/internal/modules/notification"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error { func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
var refund *refundAction var refund *refundAction
err := r.db.Transaction(func(tx *gorm.DB) error { 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 { if err != nil {
return err return err
} }
order := assets.Order
listing := assets.Listing
account := assets.Account
if isTerminalStatus(order.Status) { if isTerminalStatus(order.Status) {
return ErrOrderCannotComplete return ErrOrderCannotComplete
} }
@@ -30,9 +32,7 @@ func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionR
order.HandoffStatus = handoffStatusAdminClosed order.HandoffStatus = handoffStatusAdminClosed
order.SettlementStatus = settlementStatusClosed order.SettlementStatus = settlementStatusClosed
order.SettledAt = &now order.SettledAt = &now
listing.Status = listingStatusOffline archiveAssets(listing, account)
listing.InTransaction = false
account.Status = accountStatusOffline
if beforeOrderStatus != orderStatusPendingPayment { if beforeOrderStatus != orderStatusPendingPayment {
totalCent := order.RentAmountCent + order.DepositAmountCent totalCent := order.RentAmountCent + order.DepositAmountCent
action, err := r.prepareRefund(order, totalCent, refundBizAdminClose, "客服关闭订单原路退款") 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 { func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
return r.db.Transaction(func(tx *gorm.DB) 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 { if err != nil {
return err return err
} }
order := assets.Order
listing := assets.Listing
account := assets.Account
if isTerminalStatus(order.Status) { if isTerminalStatus(order.Status) {
return ErrOrderCannotComplete return ErrOrderCannotComplete
} }
@@ -110,9 +113,7 @@ func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req Admin
beforeAccountStatus := account.Status beforeAccountStatus := account.Status
order.Status = orderStatusAbnormal order.Status = orderStatusAbnormal
order.HandoffStatus = handoffStatusAdminAbnormal order.HandoffStatus = handoffStatusAdminAbnormal
listing.Status = listingStatusAbnormal markAssetsAbnormal(listing, account)
listing.InTransaction = false
account.Status = accountStatusAbnormal
if err := notification.Append(tx, if err := notification.Append(tx,
notification.Entry{ notification.Entry{
UserID: order.RenterID, 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 { func isTerminalStatus(status string) bool {
return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed
} }
+71
View File
@@ -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
}
+5 -17
View File
@@ -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) { func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) (*refundAction, error) {
var listing model.RentalListing listing, account, err := r.lockListingAccountForOrder(tx, order)
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { if err != nil {
return nil, err
}
var account model.GameAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
return nil, err return nil, err
} }
now := time.Now() now := time.Now()
@@ -244,7 +240,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
order.SettlementStatus = settlementStatusSettled order.SettlementStatus = settlementStatusSettled
order.SettledAt = &now order.SettledAt = &now
order.OwnerSettledAt = &now order.OwnerSettledAt = &now
archiveListingAfterCheckout(&listing, &account) archiveAssets(listing, account)
orderID := order.ID orderID := order.ID
settlement := buildCheckoutSettlement(*order, checkout) 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 { if err := tx.Save(checkout).Error; err != nil {
return nil, err return nil, err
} }
if err := tx.Save(&listing).Error; err != nil { if err := tx.Save(listing).Error; err != nil {
return nil, err return nil, err
} }
if err := tx.Save(&account).Error; err != nil { if err := tx.Save(account).Error; err != nil {
return nil, err return nil, err
} }
return refund, nil 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) { func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
var count int64 var count int64
err := tx.Model(&model.OrderCheckout{}). err := tx.Model(&model.OrderCheckout{}).
+16 -28
View File
@@ -78,7 +78,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
); err != nil { ); err != nil {
return err return err
} }
listing.InTransaction = true reserveListingForOrder(&listing)
if err := tx.Save(&listing).Error; err != nil { if err := tx.Save(&listing).Error; err != nil {
return err return err
} }
@@ -135,10 +135,13 @@ func (r *Repository) Pay(userID uint64, orderID uint64) error {
func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string) error { func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string) error {
var newConvID uint64 var newConvID uint64
err := r.db.Transaction(func(tx *gorm.DB) error { err := r.db.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.Status == orderStatusPendingHandoff || order.Status == orderStatusRenting { if order.Status == orderStatusPendingHandoff || order.Status == orderStatusRenting {
return nil return nil
} }
@@ -146,25 +149,16 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string
return ErrOrderCannotPay 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 { if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction {
return ErrListingUnavailable 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 orderID := order.ID
order.Status = orderStatusPendingHandoff order.Status = orderStatusPendingHandoff
order.HandoffStatus = handoffStatusPendingOwner order.HandoffStatus = handoffStatusPendingOwner
listing.Status = listingStatusRented markAssetsRented(listing, account)
account.Status = accountStatusRented conv, err := chat.EnsureOrderConversation(tx, *order)
conv, err := chat.EnsureOrderConversation(tx, order)
if err != nil { if err != nil {
return err return err
} }
@@ -189,13 +183,13 @@ func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string
); err != nil { ); err != nil {
return err return err
} }
if err := tx.Save(&order).Error; err != nil { if err := tx.Save(order).Error; err != nil {
return err return err
} }
if err := tx.Save(&listing).Error; err != nil { if err := tx.Save(listing).Error; err != nil {
return err return err
} }
return tx.Save(&account).Error return tx.Save(account).Error
}) })
if err != nil { if err != nil {
return err return err
@@ -218,12 +212,8 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
if order.Status != orderStatusPendingPayment && order.Status != orderStatusPendingHandoff { if order.Status != orderStatusPendingPayment && order.Status != orderStatusPendingHandoff {
return ErrOrderCannotCancel return ErrOrderCannotCancel
} }
var listing model.RentalListing listing, account, err := r.lockListingAccountForOrder(tx, &order)
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { if err != nil {
return err
}
var account model.GameAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
return err return err
} }
@@ -259,16 +249,14 @@ func (r *Repository) Cancel(userID uint64, orderID uint64) error {
); err != nil { ); err != nil {
return err return err
} }
listing.Status = listingStatusPublished releaseAssetsForRental(listing, account)
listing.InTransaction = false
account.Status = accountStatusPublished
if err := tx.Save(&order).Error; err != nil { if err := tx.Save(&order).Error; err != nil {
return err return err
} }
if err := tx.Save(&listing).Error; err != nil { if err := tx.Save(listing).Error; err != nil {
return err return err
} }
return tx.Save(&account).Error return tx.Save(account).Error
}) })
if err != nil { if err != nil {
return err return err
@@ -140,14 +140,29 @@ func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) {
} }
} }
func TestArchiveListingAfterCheckoutMovesListingOffline(t *testing.T) { func TestArchiveAssetsMovesListingOffline(t *testing.T) {
// This test is purely for contract documentation; no behavior is tested yet. now := time.Now()
// When implementing auto-archive behavior: listing := model.RentalListing{
// - Call should succeed for renting/overdue/pending_checkout_confirm/pending_checkout_accept orders Status: listingStatusRented,
// - Listing status should be set to "archived" or "offline" InTransaction: true,
// - In-transaction flag should be cleared PublishedAt: &now,
// - Order should enter completed or closed state }
_ = time.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) { func TestNewOrderNoUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) {