拆分订单仓储模块
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) SubmitCheckout(userID uint64, orderID uint64, req SubmitCheckoutRequest) (*HandoffRecordDTO, error) {
|
||||
var recordID 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 {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if (order.Status != "renting" && order.Status != "overdue") || (order.HandoffStatus != "received" && order.HandoffStatus != "return_overdue") {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
hasOpen, err := hasOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasOpen {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: order.RenterID,
|
||||
ToUserID: order.OwnerID,
|
||||
Type: "renter_checkout",
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, 0, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = "pending_checkout_confirm"
|
||||
order.HandoffStatus = "pending_owner_checkout"
|
||||
order.SettlementStatus = "pending"
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "checkout",
|
||||
Title: "租客已发起结账",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
recordID = record.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.findHandoffRecord(recordID)
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
|
||||
return r.ConfirmCheckout(userID, orderID)
|
||||
}
|
||||
|
||||
func (r *Repository) ConfirmCheckout(userID uint64, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND status = ?", order.ID, "submitted").
|
||||
Order("id DESC").
|
||||
First(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "renter_checkout").
|
||||
Update("confirmed_by_owner_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkout.Status = "accepted"
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, &checkout, "号主已确认结账,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterCheckoutRequest) (*CheckoutDTO, error) {
|
||||
var checkoutID 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 {
|
||||
return err
|
||||
}
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND status = ?", order.ID, "submitted").
|
||||
Order("id DESC").
|
||||
First(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.DepositDeductAmountCent, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = "countered"
|
||||
checkout.RentAmountCent = next.RentAmountCent
|
||||
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
|
||||
checkout.PlatformFeeCent = next.PlatformFeeCent
|
||||
checkout.DepositAmountCent = next.DepositAmountCent
|
||||
checkout.ConsumableAmountCent = next.ConsumableAmountCent
|
||||
checkout.CoinConsumedM = next.CoinConsumedM
|
||||
checkout.OtherAmountCent = next.OtherAmountCent
|
||||
checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
|
||||
checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
|
||||
checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
|
||||
checkout.OwnerAdjustmentReason = req.Reason
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
checkout.EvidenceURLS = next.EvidenceURLS
|
||||
order.Status = "pending_checkout_accept"
|
||||
order.HandoffStatus = "pending_renter_checkout"
|
||||
order.SettlementStatus = "pending"
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "checkout",
|
||||
Title: "号主已修改结账金额",
|
||||
Content: "请核对号主修正的消耗和结算金额。同意后订单完成;不同意可发起争议。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkoutID = checkout.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkout, err := r.findCheckout(checkoutID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{
|
||||
OwnerID: userID,
|
||||
RentAmountCent: checkout.RentAmountCent,
|
||||
OwnerRentAmountCent: checkout.OwnerRentAmountCent,
|
||||
DepositAmountCent: checkout.DepositAmountCent,
|
||||
})
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AcceptCheckout(userID uint64, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != "pending_checkout_accept" || order.HandoffStatus != "pending_renter_checkout" {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
var checkout model.OrderCheckout
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("order_id = ? AND status = ?", order.ID, "countered").
|
||||
Order("id DESC").
|
||||
First(&checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = "accepted"
|
||||
checkout.RenterConfirmedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, &checkout, "租客已确认修正结账,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
order.Status = "completed"
|
||||
order.HandoffStatus = "returned"
|
||||
order.SettlementStatus = "settled"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
archiveListingAfterCheckout(&listing, &account)
|
||||
orderID := order.ID
|
||||
settlement := buildCheckoutSettlement(*order, checkout)
|
||||
|
||||
// 卖家收入进入站内钱包;租客资金不进入站内钱包。
|
||||
var ownerEntries []wallet.Entry
|
||||
if settlement.OwnerRentIncomeCent > 0 {
|
||||
ownerEntries = append(ownerEntries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: settlement.OwnerRentIncomeCent,
|
||||
BalanceType: "available",
|
||||
BizType: "owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账租金收入",
|
||||
})
|
||||
}
|
||||
if settlement.DepositCompensationCent > 0 {
|
||||
ownerEntries = append(ownerEntries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: settlement.DepositCompensationCent,
|
||||
BalanceType: "available",
|
||||
BizType: "deposit_compensation",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "订单结账押金赔付",
|
||||
})
|
||||
}
|
||||
if len(ownerEntries) > 0 {
|
||||
if err := wallet.AppendEntries(tx, ownerEntries...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var refund *refundAction
|
||||
renterRefundTotalCent := settlement.RentRefundCent + settlement.DepositRefundCent
|
||||
if renterRefundTotalCent > 0 {
|
||||
action, err := r.prepareRefund(order, renterRefundTotalCent, "checkout_refund", "结账退款原路退还")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
|
||||
checkout.RentAmountCent = settlement.ActualRentAmountCent
|
||||
checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent
|
||||
checkout.PlatformFeeCent = settlement.PlatformFeeCent
|
||||
checkout.RenterRefundAmountCent = settlement.RenterRefundCent
|
||||
checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: renterContent,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: "订单已完成,结账金额已入账。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(checkout).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return refund, nil
|
||||
}
|
||||
|
||||
// 完成后的账号先下架,避免已完成订单对应的账号重新出现在公开首页。
|
||||
func archiveListingAfterCheckout(listing *model.RentalListing, account *model.GameAccount) {
|
||||
listing.Status = "offline"
|
||||
listing.InTransaction = false
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "offline"
|
||||
}
|
||||
|
||||
func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
|
||||
var count int64
|
||||
err := tx.Model(&model.OrderCheckout{}).
|
||||
Where("order_id = ? AND status IN ?", orderID, []string{"submitted", "countered", "accepted", "disputed"}).
|
||||
Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
Reference in New Issue
Block a user