统一订单资产状态变更
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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{}).
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user