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 }