package order import ( "context" "encoding/json" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/chat" "hfb_sys/backend/internal/modules/notification" "gorm.io/datatypes" "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 := estimateOrderDurationHours(snapshot) 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 { convID, err := r.ConfirmPaidFromChannelTx(tx, orderID) if err != nil { return err } newConvID = convID return nil }) if err != nil { return err } if newConvID > 0 && r.chatNotifier != nil { r.chatNotifier.NotifyNewConversation(newConvID) } return nil } // ConfirmPaidFromChannelTx 在外部事务内推进支付成功后的订单状态。 // 调用方负责提交事务,并在提交成功后按返回的会话 ID 触发异步通知。 func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint64, error) { assets, err := r.lockOrderAssets(tx, orderID) if err != nil { return 0, err } order := assets.Order listing := assets.Listing account := assets.Account if order.Status == orderStatusPendingHandoff || order.Status == orderStatusRenting { return 0, nil } if order.Status != orderStatusPendingPayment { return 0, ErrOrderCannotPay } if listing.Status != listingStatusPublished || listing.ReviewStatus != listingReviewStatusApproved || !listing.InTransaction { return 0, ErrListingUnavailable } // 租客已通过外部渠道付款,这里不写租客钱包流水。 orderID = order.ID now := time.Now() order.Status = orderStatusPendingHandoff order.HandoffStatus = handoffStatusPendingOwner order.HandoffStartedAt = &now if err := markAssetsRented(tx, listing, account); err != nil { return 0, err } // 拉租客进发布群(替代原来的建订单群) conversationID := uint64(0) if err := chat.AddRenterToListingConversation(tx, listing.ID, order.RenterID, order.OrderNo); err != nil { return 0, err } // 查询发布群ID用于返回 var listingConv model.ChatConversation if err := tx.Where("listing_id = ?", listing.ID).First(&listingConv).Error; err == nil { conversationID = listingConv.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 0, err } if err := tx.Save(order).Error; err != nil { return 0, err } if err := tx.Save(listing).Error; err != nil { return 0, err } if err := tx.Save(account).Error; err != nil { return 0, err } return conversationID, nil } func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64) error { 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 now := time.Now() order.Status = orderStatusCancelled order.SettledAt = &now orderID := order.ID if beforeStatus == orderStatusPendingHandoff { totalCent := order.RentAmountCent + order.DepositAmountCent order.RefundStatus = refundStatusPendingReview order.RefundAmountCent = totalCent order.RefundedAt = nil } else { order.HandoffStatus = handoffStatusCancelled } 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 := releaseAssetsForRental(tx, listing, account); err != nil { return err } if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); 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 } return nil } // closePendingOrderPayments 在订单取消时关闭仍未完成的下单支付流水,避免后台一直显示“支付中”。 func closePendingOrderPayments(tx *gorm.DB, orderID uint64, source string) error { raw, err := json.Marshal(map[string]string{ "source": source, "reason": "订单已取消,关闭未完成支付单", }) if err != nil { return err } return tx.Model(&model.PaymentOrder{}). Where("order_id = ? AND biz_type = ? AND status IN ?", orderID, "order_pay", []string{"created", "paying"}). Updates(map[string]any{ "status": "closed", "raw_response": datatypes.JSON(raw), }).Error } 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 order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot) 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 }) }