Files
hfb_sys/backend/internal/modules/order/checkout.go
T

373 lines
13 KiB
Go

package order
import (
"context"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/notification"
"hfb_sys/backend/internal/processlog"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID uint64, req SubmitCheckoutRequest) (*HandoffRecordDTO, error) {
var recordID uint64
err := r.db.WithContext(ctx).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
}
before := orderState(order)
checkoutToUserID := order.OwnerID
checkoutTargetType := processlog.ActorUser
if isPlatformSettlementOrder(order) && platformManagedAdminID(order) > 0 {
checkoutToUserID = platformManagedAdminID(order)
checkoutTargetType = processlog.ActorAdmin
}
record := model.HandoffRecord{
OrderID: order.ID,
FromUserID: order.RenterID,
ToUserID: checkoutToUserID,
Type: "renter_checkout",
Content: req.Content,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
// 首轮:租客提案,轮到号主确认/还价;押金赔付可由租客申报 other
checkout, err := buildCheckout(order, order.RenterID, checkoutStatusSubmitted, req.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.OtherAmountCent, true)
if err != nil {
return err
}
checkout.RoundCount = 1
checkout.Turn = checkoutTurnOwner
checkout.ProposedBy = order.RenterID
if err := tx.Create(&checkout).Error; err != nil {
return err
}
order.Status = orderStatusPendingCheckoutConfirm
order.HandoffStatus = handoffStatusPendingOwnerCheckout
order.SettlementStatus = settlementStatusPending
now := time.Now()
order.HandoffStartedAt = &now
if err := appendOrderEvent(tx, order, "checkout", "checkout_submitted", processlog.ActorUser, userID, checkoutTargetType, &checkoutToUserID, req.Content, "", checkoutEventPayload(checkout), before, req.EvidenceURLS); err != nil {
return err
}
orderID := order.ID
if isPlatformSettlementOrder(order) {
if err := appendManagedAdminNotification(tx, order, "checkout", "代管订单待确认结账", "租客已发起结账,请检查账号状态和消耗明细后确认。"); err != nil {
return err
}
} else {
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(ctx, recordID)
}
func (r *Repository) ConfirmReturn(ctx context.Context, userID uint64, orderID uint64) error {
return r.ConfirmCheckout(ctx, userID, orderID)
}
// ConfirmCheckout 号主同意当前提案并完结(轮到号主时)
func (r *Repository) ConfirmCheckout(ctx context.Context, userID uint64, orderID uint64) error {
var refund *refundAction
err := r.db.WithContext(ctx).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
}
before := orderState(order)
checkout, err := lockOpenCheckout(tx, order.ID)
if err != nil {
return err
}
if normalizeCheckoutTurn(checkout) != checkoutTurnOwner {
return ErrCheckoutCannotConfirm
}
if err := ensureCheckoutCompletable(order, checkout); 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
if err != nil {
return err
}
return appendOrderEvent(tx, order, "checkout", "checkout_confirmed", processlog.ActorUser, userID, processlog.ActorUser, &order.RenterID, "号主确认当前结账方案,订单完成。", "", checkoutEventPayload(*checkout), before, decodeStringList(checkout.EvidenceURLS))
})
if err != nil {
return err
}
r.startRefundBestEffort(ctx, refund)
return nil
}
// CounterCheckout 任一方在轮到自己时修改结账提案(最多 checkoutMaxRounds 轮)
func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID uint64, req CounterCheckoutRequest) (*CheckoutDTO, error) {
var checkoutID uint64
var orderSnapshot model.RentalOrder
err := r.db.WithContext(ctx).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
}
isOwner := order.OwnerID == userID
isRenter := order.RenterID == userID
if !isOwner && !isRenter {
return ErrPermissionDenied
}
if order.Status != orderStatusPendingCheckoutConfirm && order.Status != orderStatusPendingCheckoutAccept {
return ErrCheckoutCannotCounter
}
before := orderState(order)
checkout, err := lockOpenCheckout(tx, order.ID)
if err != nil {
return err
}
turn := normalizeCheckoutTurn(checkout)
if isOwner && (turn != checkoutTurnOwner || order.Status != orderStatusPendingCheckoutConfirm) {
return ErrCheckoutCannotCounter
}
if isRenter && (turn != checkoutTurnRenter || order.Status != orderStatusPendingCheckoutAccept) {
return ErrCheckoutCannotCounter
}
round := checkout.RoundCount
if round <= 0 {
round = 1
}
if round >= checkoutMaxRounds {
return ErrCheckoutMaxRounds
}
depositDeductCent := checkoutDepositDeductCent(req)
next, err := buildCheckout(order, checkout.InitiatedBy, checkoutStatusCountered, checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, depositDeductCent, depositDeductCent, true)
if err != nil {
return err
}
now := time.Now()
applyCounterCheckoutUpdate(checkout, next, req.Reason, userID, round+1, now)
notifyUserID := order.RenterID
notifyTitle := "号主已修改结账金额"
notifyContent := "请核对对方修正的消耗和结算金额。可同意完结、继续还价(最多 6 轮),或发起争议。"
notifyManagedAdmin := false
if isOwner {
checkout.Turn = checkoutTurnRenter
order.Status = orderStatusPendingCheckoutAccept
order.HandoffStatus = handoffStatusPendingRenterCheckout
} else {
checkout.Turn = checkoutTurnOwner
order.Status = orderStatusPendingCheckoutConfirm
order.HandoffStatus = handoffStatusPendingOwnerCheckout
notifyTitle = "租客已修改结账金额"
if isPlatformSettlementOrder(order) {
notifyManagedAdmin = true
} else {
notifyUserID = order.OwnerID
}
}
order.SettlementStatus = settlementStatusPending
orderID := order.ID
if notifyManagedAdmin {
if err := appendManagedAdminNotification(tx, order, "checkout", notifyTitle, notifyContent); err != nil {
return err
}
} else {
if err := notification.Append(tx, notification.Entry{
UserID: notifyUserID,
Type: "checkout",
Title: notifyTitle,
Content: notifyContent,
BizType: "order",
BizID: &orderID,
}); err != nil {
return err
}
}
if err := tx.Save(&order).Error; err != nil {
return err
}
targetID := order.OwnerID
if isOwner {
targetID = order.RenterID
}
if err := appendOrderEvent(tx, order, "checkout", "checkout_countered", processlog.ActorUser, userID, processlog.ActorUser, &targetID, "", req.Reason, checkoutEventPayload(*checkout), before, req.EvidenceURLS); err != nil {
return err
}
if err := tx.Save(checkout).Error; err != nil {
return err
}
checkoutID = checkout.ID
orderSnapshot = order
return nil
})
if err != nil {
return nil, err
}
checkout, err := r.findCheckout(ctx, checkoutID)
if err != nil {
return nil, err
}
dto := toCheckoutDTOForUser(*checkout, userID, orderSnapshot)
return &dto, nil
}
func checkoutDepositDeductCent(req CounterCheckoutRequest) int64 {
depositDeductCent := req.DepositDeductAmountCent
if depositDeductCent <= 0 && req.OtherAmountCent > 0 {
depositDeductCent = req.OtherAmountCent
}
return depositDeductCent
}
func applyCounterCheckoutUpdate(checkout *model.OrderCheckout, next model.OrderCheckout, reason string, proposedBy uint64, roundCount int, now time.Time) {
checkout.Status = checkoutStatusCountered
checkout.RoundCount = roundCount
checkout.ProposedBy = proposedBy
checkout.RentAmountCent = next.RentAmountCent
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
checkout.PlatformFeeCent = next.PlatformFeeCent
checkout.DepositAmountCent = next.DepositAmountCent
checkout.PureCoinAmountCent = next.PureCoinAmountCent
checkout.PureCoinDiscountCent = next.PureCoinDiscountCent
checkout.PureCoinPayableCent = next.PureCoinPayableCent
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.ShortfallCent = next.ShortfallCent
checkout.OvershootAmountCent = next.OvershootAmountCent
checkout.OwnerAdjustmentReason = reason
checkout.OwnerAdjustedAt = &now
checkout.EvidenceURLS = next.EvidenceURLS
}
// AcceptCheckout 租客同意当前提案并完结(轮到租客时)
func (r *Repository) AcceptCheckout(ctx context.Context, userID uint64, orderID uint64) error {
var refund *refundAction
err := r.db.WithContext(ctx).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
}
before := orderState(order)
checkout, err := lockOpenCheckout(tx, order.ID)
if err != nil {
return err
}
if normalizeCheckoutTurn(checkout) != checkoutTurnRenter {
return ErrCheckoutCannotConfirm
}
if err := ensureCheckoutCompletable(order, checkout); err != nil {
return err
}
now := time.Now()
checkout.Status = checkoutStatusAccepted
checkout.RenterConfirmedAt = &now
action, err := r.finalizeCheckout(tx, &order, checkout, "租客已确认结账协商,订单完成。")
refund = action
if err != nil {
return err
}
return appendOrderEvent(tx, order, "checkout", "checkout_accepted", processlog.ActorUser, userID, processlog.ActorUser, &order.OwnerID, "租客接受当前结账方案,订单完成。", "", checkoutEventPayload(*checkout), before, decodeStringList(checkout.EvidenceURLS))
})
if err != nil {
return err
}
r.startRefundBestEffort(ctx, refund)
return nil
}
func lockOpenCheckout(tx *gorm.DB, orderID uint64) (*model.OrderCheckout, error) {
var checkout model.OrderCheckout
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND status IN ?", orderID, []string{checkoutStatusSubmitted, checkoutStatusCountered}).
Order("id DESC").
First(&checkout).Error
if err != nil {
return nil, err
}
return &checkout, nil
}
func normalizeCheckoutTurn(checkout *model.OrderCheckout) string {
if checkout == nil {
return checkoutTurnOwner
}
switch checkout.Turn {
case checkoutTurnOwner, checkoutTurnRenter:
return checkout.Turn
default:
// 兼容旧数据:已反价默认等租客,否则等号主
if checkout.Status == checkoutStatusCountered {
return checkoutTurnRenter
}
return checkoutTurnOwner
}
}
func ensureCheckoutCompletable(order model.RentalOrder, checkout *model.OrderCheckout) error {
if checkout == nil {
return ErrCheckoutCannotConfirm
}
settlement := buildCheckoutSettlement(order, checkout)
if settlement.ShortfallCent > 0 {
return ErrCheckoutDepositShortfall
}
return nil
}