统一订单资产状态变更

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"
"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
}
+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) {
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{}).
+16 -28
View File
@@ -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
@@ -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) {