package order import ( "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/chat" "hfb_sys/backend/internal/modules/notification" "gorm.io/gorm" "gorm.io/gorm/clause" ) func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) { var createdID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil { return err } if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction { return ErrListingUnavailable } if listing.OwnerID == renterID { return ErrCannotRentOwnListing } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil { return err } snapshot, err := makeAccountSnapshot(account, listing) if err != nil { return err } orderNo, err := newOrderNo() if err != nil { return err } rentHours := internalOrderHours pricing := buildOrderPricing(listing, account) depositOriginalAmountCent := listing.DepositAmountCent paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent) if err != nil { return err } order := model.RentalOrder{ OrderNo: orderNo, ListingID: listing.ID, AccountID: listing.AccountID, OwnerID: listing.OwnerID, RenterID: renterID, EstimatedDurationHours: rentHours, RentAmountCent: pricing.RentAmountCent, OwnerRentAmountCent: pricing.OwnerRentAmountCent, DepositAmountCent: paidDepositAmountCent, DepositOriginalAmountCent: depositOriginalAmountCent, DepositWaivedAmountCent: waivedDepositAmountCent, PlatformFeeCent: pricing.PlatformFeeCent, AccountSnapshot: snapshot, Status: "pending_payment", HandoffStatus: "none", SettlementStatus: "unsettled", } if err := tx.Create(&order).Error; err != nil { return err } orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "order", Title: "订单已创建", Content: "订单已创建,请在有效时间内完成支付。", BizType: "order", BizID: &orderID, }, ); err != nil { return err } listing.InTransaction = true if err := tx.Save(&listing).Error; err != nil { return err } createdID = order.ID return nil }) if err != nil { return nil, err } return r.FindForUser(renterID, createdID) } func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64) (int64, int64, error) { if originalDepositCent <= 0 { return 0, 0, nil } var user model.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, renterID).Error; err != nil { return 0, 0, err } used, err := activeDepositFreeUsed(tx, renterID) if err != nil { return 0, 0, err } paidDeposit, waivedDeposit := calculateDepositWaiver(originalDepositCent, user.DepositFreeQuotaCent, used) return paidDeposit, waivedDeposit, nil } func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (int64, error) { var used int64 err := tx.Model(&model.RentalOrder{}). Where("renter_id = ? AND status NOT IN ?", renterID, []string{"completed", "cancelled", "closed"}, ). Select("COALESCE(SUM(deposit_waived_amount_cent), 0)"). Scan(&used).Error return used, err } func calculateDepositWaiver(originalDepositCent int64, quotaCent int64, usedCent int64) (int64, int64) { remaining := maxCent(quotaCent-usedCent, 0) waived := minCent(originalDepositCent, remaining) paid := maxCent(originalDepositCent-waived, 0) return paid, waived } // Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。 func (r *Repository) Pay(userID uint64, orderID uint64) error { return ErrChannelPaymentRequired } // ConfirmPaidFromChannel 在乐刷确认支付后推进订单状态;租客资金不进入站内钱包。 func (r *Repository) ConfirmPaidFromChannel(orderID uint64, providerBizNo string) error { var newConvID 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.Status == "pending_handoff" || order.Status == "renting" { return nil } if order.Status != "pending_payment" { return ErrOrderCannotPay } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { return err } if listing.Status != "published" || listing.ReviewStatus != "approved" || !listing.InTransaction { return ErrListingUnavailable } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { return err } // 租客已通过外部渠道付款,这里不写租客钱包流水。 orderID := order.ID order.Status = "pending_handoff" order.HandoffStatus = "pending_owner" listing.Status = "rented" account.Status = "rented" conv, err := chat.EnsureOrderConversation(tx, order) if err != nil { return err } newConvID = conv.ID if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "order", Title: "收到新的租号订单", Content: "租客已完成支付,请尽快提交交接说明。", BizType: "order", BizID: &orderID, }, notification.Entry{ UserID: order.RenterID, Type: "order", Title: "订单支付成功", Content: "支付已完成,等待号主提交交接说明。", BizType: "order", BizID: &orderID, }, ); err != nil { return err } if err := tx.Save(&order).Error; err != nil { return err } if err := tx.Save(&listing).Error; err != nil { return err } return tx.Save(&account).Error }) if err != nil { return err } if newConvID > 0 && r.chatRepo != nil { r.chatRepo.NotifyNewConversation(newConvID) } return nil } func (r *Repository) Cancel(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"}). Where("id = ? AND renter_id = ?", orderID, userID). First(&order).Error; err != nil { return err } if order.Status != "pending_payment" && order.Status != "pending_handoff" { return ErrOrderCannotCancel } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { return err } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { return err } beforeStatus := order.Status order.Status = "cancelled" order.HandoffStatus = "cancelled" orderID := order.ID if beforeStatus == "pending_handoff" { totalCent := order.RentAmountCent + order.DepositAmountCent action, err := r.prepareRefund(&order, totalCent, "cancel_refund", "取消订单原路退款") if err != nil { return err } refund = action } if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "order", Title: "订单已取消", Content: "租客已取消订单,账号已重新释放。", BizType: "order", BizID: &orderID, }, notification.Entry{ UserID: order.RenterID, Type: "order", Title: "订单取消成功", Content: "订单已取消,退款将原路退回您的支付账户。", BizType: "order", BizID: &orderID, }, ); err != nil { return err } listing.Status = "published" listing.InTransaction = false account.Status = "published" if err := tx.Save(&order).Error; err != nil { return err } if err := tx.Save(&listing).Error; err != nil { return err } return tx.Save(&account).Error }) if err != nil { return err } r.startRefundBestEffort(refund) return nil } func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error { return 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_handoff" || order.HandoffStatus != "pending_renter_confirm" { return ErrOrderCannotReceive } now := time.Now() if err := tx.Model(&model.HandoffRecord{}). Where("order_id = ? AND type = ?", order.ID, "owner_handoff"). Update("confirmed_by_renter_at", now).Error; err != nil { return err } order.Status = "renting" order.HandoffStatus = "received" durationHours := orderDurationHours(order) order.EstimatedDurationHours = durationHours order.RentedAt = &now orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "handoff", Title: "租客已确认收号", Content: "订单已进入使用中。", BizType: "order", BizID: &orderID, }); err != nil { return err } return tx.Save(&order).Error }) }