支持结账多轮协商与按实际消耗双比例结算
允许双方最多 6 轮修改结账方案;哈夫币按实际消耗与 buyer/seller 比例计价,打超从押金扣,押金不足禁止自动完结并引导人工争议。同步收紧结账明细展示密度。
This commit is contained in:
@@ -41,10 +41,14 @@ func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID
|
||||
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)
|
||||
// 首轮:租客提案,轮到号主确认/还价;押金赔付可由租客申报 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
|
||||
}
|
||||
@@ -58,7 +62,7 @@ func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID
|
||||
UserID: order.OwnerID,
|
||||
Type: "checkout",
|
||||
Title: "租客已发起结账",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算。",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算;也可修改后交由租客确认。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
@@ -80,6 +84,7 @@ func (r *Repository) ConfirmReturn(ctx context.Context, userID uint64, orderID u
|
||||
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 {
|
||||
@@ -93,11 +98,14 @@ func (r *Repository) ConfirmCheckout(ctx context.Context, userID uint64, orderID
|
||||
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 {
|
||||
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()
|
||||
@@ -108,7 +116,7 @@ func (r *Repository) ConfirmCheckout(ctx context.Context, userID uint64, orderID
|
||||
}
|
||||
checkout.Status = checkoutStatusAccepted
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, &checkout, "号主已确认结账,订单完成。")
|
||||
action, err := r.finalizeCheckout(tx, &order, checkout, "号主已确认结账,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
})
|
||||
@@ -119,27 +127,42 @@ func (r *Repository) ConfirmCheckout(ctx context.Context, userID uint64, orderID
|
||||
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
|
||||
}
|
||||
if order.OwnerID != userID {
|
||||
isOwner := order.OwnerID == userID
|
||||
isRenter := order.RenterID == userID
|
||||
if !isOwner && !isRenter {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status != orderStatusPendingCheckoutConfirm || order.HandoffStatus != handoffStatusPendingOwnerCheckout {
|
||||
if order.Status != orderStatusPendingCheckoutConfirm && order.Status != orderStatusPendingCheckoutAccept {
|
||||
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 {
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 号主修正只认押金赔付扣除;other 与 deduct 对齐,避免双字段语义分裂
|
||||
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 := req.DepositDeductAmountCent
|
||||
if depositDeductCent <= 0 && req.OtherAmountCent > 0 {
|
||||
depositDeductCent = req.OtherAmountCent
|
||||
@@ -150,6 +173,8 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = checkoutStatusCountered
|
||||
checkout.RoundCount = round + 1
|
||||
checkout.ProposedBy = userID
|
||||
checkout.RentAmountCent = next.RentAmountCent
|
||||
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
|
||||
checkout.PlatformFeeCent = next.PlatformFeeCent
|
||||
@@ -160,18 +185,33 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
|
||||
checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
|
||||
checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
|
||||
checkout.ShortfallCent = next.ShortfallCent
|
||||
checkout.OvershootAmountCent = next.OvershootAmountCent
|
||||
checkout.OwnerAdjustmentReason = req.Reason
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
checkout.EvidenceURLS = next.EvidenceURLS
|
||||
order.Status = orderStatusPendingCheckoutAccept
|
||||
order.HandoffStatus = handoffStatusPendingRenterCheckout
|
||||
|
||||
notifyUserID := order.RenterID
|
||||
notifyTitle := "号主已修改结账金额"
|
||||
notifyContent := "请核对对方修正的消耗和结算金额。可同意完结、继续还价(最多 6 轮),或发起争议。"
|
||||
if isOwner {
|
||||
checkout.Turn = checkoutTurnRenter
|
||||
order.Status = orderStatusPendingCheckoutAccept
|
||||
order.HandoffStatus = handoffStatusPendingRenterCheckout
|
||||
} else {
|
||||
checkout.Turn = checkoutTurnOwner
|
||||
order.Status = orderStatusPendingCheckoutConfirm
|
||||
order.HandoffStatus = handoffStatusPendingOwnerCheckout
|
||||
notifyUserID = order.OwnerID
|
||||
notifyTitle = "租客已修改结账金额"
|
||||
}
|
||||
order.SettlementStatus = settlementStatusPending
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
UserID: notifyUserID,
|
||||
Type: "checkout",
|
||||
Title: "号主已修改结账金额",
|
||||
Content: "请核对号主修正的消耗和结算金额。同意后订单完成;不同意可发起争议。",
|
||||
Title: notifyTitle,
|
||||
Content: notifyContent,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
@@ -180,10 +220,11 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&checkout).Error; err != nil {
|
||||
if err := tx.Save(checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkoutID = checkout.ID
|
||||
orderSnapshot = order
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -193,15 +234,11 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{
|
||||
OwnerID: userID,
|
||||
RentAmountCent: checkout.RentAmountCent,
|
||||
OwnerRentAmountCent: checkout.OwnerRentAmountCent,
|
||||
DepositAmountCent: checkout.DepositAmountCent,
|
||||
})
|
||||
dto := toCheckoutDTOForUser(*checkout, userID, orderSnapshot)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -215,17 +252,20 @@ func (r *Repository) AcceptCheckout(ctx context.Context, userID uint64, orderID
|
||||
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 {
|
||||
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, "租客已确认修正结账,订单完成。")
|
||||
action, err := r.finalizeCheckout(tx, &order, checkout, "租客已确认结账协商,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
})
|
||||
@@ -235,3 +275,42 @@ func (r *Repository) AcceptCheckout(ctx context.Context, userID uint64, orderID
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user