package order import ( "context" "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(ctx context.Context, renterID uint64, req CreateRequest) (*OrderDTO, error) { var createdID uint64 err := r.db.WithContext(ctx).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 != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || 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: orderStatusPendingPayment, HandoffStatus: handoffStatusNone, SettlementStatus: settlementStatusUnsettled, } 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 } reserveListingForOrder(&listing) if err := tx.Save(&listing).Error; err != nil { return err } createdID = order.ID return nil }) if err != nil { return nil, err } return r.FindForUser(ctx, 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{orderStatusCompleted, orderStatusCancelled, orderStatusClosed}, ). 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(ctx context.Context, userID uint64, orderID uint64) error { return ErrChannelPaymentRequired } // ConfirmPaidFromChannel 在乐刷确认支付后推进订单状态;租客资金不进入站内钱包。 func (r *Repository) ConfirmPaidFromChannel(ctx context.Context, orderID uint64, providerBizNo string) error { var newConvID uint64 err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { assets, err := r.lockOrderAssets(tx, orderID) if err != nil { return err } order := assets.Order listing := assets.Listing account := assets.Account if order.Status == orderStatusPendingHandoff || order.Status == orderStatusRenting { return nil } if order.Status != orderStatusPendingPayment { return ErrOrderCannotPay } if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction { return ErrListingUnavailable } // 租客已通过外部渠道付款,这里不写租客钱包流水。 orderID := order.ID order.Status = orderStatusPendingHandoff order.HandoffStatus = handoffStatusPendingOwner markAssetsRented(listing, account) 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.chatNotifier != nil { r.chatNotifier.NotifyNewConversation(newConvID) } return nil } func (r *Repository) Cancel(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"}). Where("id = ? AND renter_id = ?", orderID, userID). First(&order).Error; err != nil { return err } if order.Status != orderStatusPendingPayment && order.Status != orderStatusPendingHandoff { return ErrOrderCannotCancel } listing, account, err := r.lockListingAccountForOrder(tx, &order) if err != nil { return err } beforeStatus := order.Status order.Status = orderStatusCancelled order.HandoffStatus = handoffStatusCancelled orderID := order.ID if beforeStatus == orderStatusPendingHandoff { totalCent := order.RentAmountCent + order.DepositAmountCent action, err := r.prepareRefund(&order, totalCent, refundBizCancel, "取消订单原路退款") 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 } releaseAssetsForRental(listing, account) 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(ctx, refund) return nil } func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID uint64) error { return 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 != orderStatusPendingHandoff || order.HandoffStatus != handoffStatusPendingRenterConfirm { 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 = orderStatusRenting order.HandoffStatus = handoffStatusReceived 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 }) }