package order import ( "context" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/notification" "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 } checkoutToUserID := order.OwnerID if isPlatformSettlementOrder(order) && platformManagedAdminID(order) > 0 { checkoutToUserID = platformManagedAdminID(order) } 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 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 } 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 return err }) 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 } 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 } 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 } 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 return err }) 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 }