Files
hfb_sys/backend/internal/modules/order/checkout.go
T
2026-06-09 20:11:28 +08:00

336 lines
11 KiB
Go

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 != orderStatusRenting && order.Status != orderStatusOverdue) || (order.HandoffStatus != handoffStatusReceived && order.HandoffStatus != handoffStatusReturnOverdue) {
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, checkoutStatusSubmitted, 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 = orderStatusPendingCheckoutConfirm
order.HandoffStatus = handoffStatusPendingOwnerCheckout
order.SettlementStatus = settlementStatusPending
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 != orderStatusPendingCheckoutConfirm || order.HandoffStatus != handoffStatusPendingOwnerCheckout {
return ErrCheckoutCannotConfirm
}
var checkout model.OrderCheckout
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND status = ?", order.ID, checkoutStatusSubmitted).
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 = checkoutStatusAccepted
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 != orderStatusPendingCheckoutConfirm || order.HandoffStatus != handoffStatusPendingOwnerCheckout {
return ErrCheckoutCannotCounter
}
var checkout model.OrderCheckout
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND status = ?", order.ID, checkoutStatusSubmitted).
Order("id DESC").
First(&checkout).Error; err != nil {
return err
}
next, err := buildCheckout(order, checkout.InitiatedBy, checkoutStatusCountered, checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.DepositDeductAmountCent, true)
if err != nil {
return err
}
now := time.Now()
checkout.Status = checkoutStatusCountered
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 = orderStatusPendingCheckoutAccept
order.HandoffStatus = handoffStatusPendingRenterCheckout
order.SettlementStatus = settlementStatusPending
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 != orderStatusPendingCheckoutAccept || order.HandoffStatus != handoffStatusPendingRenterCheckout {
return ErrCheckoutCannotConfirm
}
var checkout model.OrderCheckout
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND status = ?", order.ID, checkoutStatusCountered).
Order("id DESC").
First(&checkout).Error; err != nil {
return err
}
now := time.Now()
checkout.Status = checkoutStatusAccepted
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) {
listing, account, err := r.lockListingAccountForOrder(tx, order)
if err != nil {
return nil, err
}
now := time.Now()
order.Status = orderStatusCompleted
order.HandoffStatus = handoffStatusReturned
order.SettlementStatus = settlementStatusSettled
order.SettledAt = &now
order.OwnerSettledAt = &now
archiveAssets(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: walletBizOwnerIncome,
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: walletBizDepositCompensation,
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, refundBizCheckout, "结账退款原路退还")
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 hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
var count int64
err := tx.Model(&model.OrderCheckout{}).
Where("order_id = ? AND status IN ?", orderID, []string{checkoutStatusSubmitted, checkoutStatusCountered, checkoutStatusAccepted, checkoutStatusDisputed}).
Count(&count).Error
return count > 0, err
}