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 != "renting" && order.Status != "overdue") || (order.HandoffStatus != "received" && order.HandoffStatus != "return_overdue") { 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, "submitted", 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 = "pending_checkout_confirm" order.HandoffStatus = "pending_owner_checkout" order.SettlementStatus = "pending" 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 != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" { return ErrCheckoutCannotConfirm } var checkout model.OrderCheckout if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("order_id = ? AND status = ?", order.ID, "submitted"). 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 = "accepted" 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 != "pending_checkout_confirm" || order.HandoffStatus != "pending_owner_checkout" { return ErrCheckoutCannotCounter } var checkout model.OrderCheckout if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("order_id = ? AND status = ?", order.ID, "submitted"). Order("id DESC"). First(&checkout).Error; err != nil { return err } next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.DepositDeductAmountCent, true) if err != nil { return err } now := time.Now() checkout.Status = "countered" 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 = "pending_checkout_accept" order.HandoffStatus = "pending_renter_checkout" order.SettlementStatus = "pending" 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 != "pending_checkout_accept" || order.HandoffStatus != "pending_renter_checkout" { return ErrCheckoutCannotConfirm } var checkout model.OrderCheckout if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("order_id = ? AND status = ?", order.ID, "countered"). Order("id DESC"). First(&checkout).Error; err != nil { return err } now := time.Now() checkout.Status = "accepted" 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) { var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { return nil, err } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { return nil, err } now := time.Now() order.Status = "completed" order.HandoffStatus = "returned" order.SettlementStatus = "settled" order.SettledAt = &now order.OwnerSettledAt = &now archiveListingAfterCheckout(&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: "owner_income", 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: "deposit_compensation", 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, "checkout_refund", "结账退款原路退还") 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 archiveListingAfterCheckout(listing *model.RentalListing, account *model.GameAccount) { listing.Status = "offline" listing.InTransaction = false listing.PublishedAt = nil account.Status = "offline" } 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{"submitted", "countered", "accepted", "disputed"}). Count(&count).Error return count > 0, err }