package order import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "math" "strconv" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/notification" "hfb_sys/backend/internal/modules/wallet" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } const defaultPendingPaymentTimeoutMinutes = 15 func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func listingOrderPrice(listing model.RentalListing) float64 { switch { case listing.Price > 0: return listing.Price case listing.PriceDaily > 0: return listing.PriceDaily case listing.PriceHourly > 0: return listing.PriceHourly * float64(internalOrderHours) case listing.PriceWeekly > 0: return listing.PriceWeekly / 7 default: return 0 } } 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) if err != nil { return err } orderNo, err := newOrderNo() if err != nil { return err } rentHours := internalOrderHours order := model.RentalOrder{ OrderNo: orderNo, ListingID: listing.ID, AccountID: listing.AccountID, OwnerID: listing.OwnerID, RenterID: renterID, RentHours: rentHours, RentAmount: listingOrderPrice(listing), DepositAmount: listing.DepositAmount, PlatformFee: 0, 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) Pay(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"}). Where("id = ? AND renter_id = ?", orderID, userID). First(&order).Error; err != nil { return err } if order.Status != "pending_payment" { return ErrOrderCannotPay } timeoutMinutes := pendingPaymentTimeoutMinutes(tx) if order.CreatedAt.Before(time.Now().Add(-time.Duration(timeoutMinutes) * time.Minute)) { 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 } total := order.RentAmount + order.DepositAmount if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "out", Amount: total, BalanceType: "available", BizType: "order_pay", BizNo: order.OrderNo, Remark: "订单支付扣减可用余额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "in", Amount: total, BalanceType: "frozen", BizType: "order_lock", BizNo: order.OrderNo, Remark: "订单支付冻结租金和押金", }, ); err != nil { if errors.Is(err, wallet.ErrInsufficientBalance) { return ErrInsufficientBalance } return err } order.Status = "pending_handoff" order.HandoffStatus = "pending_owner" listing.Status = "rented" account.Status = "rented" orderID := order.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 }) } func (r *Repository) Cancel(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"}). 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" { total := order.RentAmount + order.DepositAmount if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "out", Amount: total, BalanceType: "frozen", BizType: "order_cancel", BizNo: order.OrderNo, Remark: "取消订单释放冻结金额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "in", Amount: total, BalanceType: "available", BizType: "order_cancel_refund", BizNo: order.OrderNo, Remark: "取消订单退回可用余额", }, ); err != nil { return err } } 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 }) } func (r *Repository) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*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.OwnerID != userID { return ErrPermissionDenied } if order.Status != "pending_handoff" || order.HandoffStatus != "pending_owner" { return ErrOrderCannotHandoff } record := model.HandoffRecord{ OrderID: order.ID, FromUserID: order.OwnerID, ToUserID: order.RenterID, Type: "owner_handoff", Content: req.Content, } if err := tx.Create(&record).Error; err != nil { return err } order.HandoffStatus = "pending_renter_confirm" orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "handoff", 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) 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" order.RentStartAt = &now rentEnd := now.Add(time.Duration(order.RentHours) * time.Hour) order.RentEndAt = &rentEnd 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 }) } func (r *Repository) HandoffRecords(userID uint64, orderID uint64) ([]HandoffRecordDTO, error) { var order model.RentalOrder if err := r.db.Where("id = ? AND (renter_id = ? OR owner_id = ?)", orderID, userID, userID).First(&order).Error; err != nil { return nil, err } var records []model.HandoffRecord if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil { return nil, err } items := make([]HandoffRecordDTO, 0, len(records)) for _, record := range records { items = append(items, toHandoffDTO(record)) } return items, nil } func (r *Repository) SubmitReturn(userID uint64, orderID uint64, req SubmitReturnRequest) (*HandoffRecordDTO, error) { return r.SubmitCheckout(userID, orderID, SubmitCheckoutRequest{Content: req.Content}) } 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.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, 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 { 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.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 return r.finalizeCheckout(tx, &order, &checkout, "号主已确认结账,订单完成。") }) } 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.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, req.DepositDeductAmount, true) if err != nil { return err } now := time.Now() checkout.Status = "countered" checkout.ConsumableAmount = next.ConsumableAmount checkout.CoinConsumedM = next.CoinConsumedM checkout.OtherAmount = next.OtherAmount checkout.DepositDeductAmount = next.DepositDeductAmount checkout.RenterRefundAmount = next.RenterRefundAmount checkout.OwnerIncomeAmount = next.OwnerIncomeAmount 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 := toCheckoutDTO(*checkout) return &dto, nil } func (r *Repository) AcceptCheckout(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_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 return r.finalizeCheckout(tx, &order, &checkout, "租客已确认修正结账,订单完成。") }) } func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) { var rows []orderRow err := r.baseQuery(). Where("o.renter_id = ?", userID). Order("o.id DESC"). Scan(&rows).Error if err != nil { return nil, err } items := make([]OrderDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items, nil } func (r *Repository) ListAdmin() ([]OrderDTO, error) { var rows []orderRow err := r.adminQuery(). Order("o.id DESC"). Limit(200). Scan(&rows).Error if err != nil { return nil, err } items := make([]OrderDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items, nil } func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) { var row orderRow if err := r.adminQuery().Where("o.id = ?", orderID).First(&row).Error; err != nil { return nil, err } dto := row.toDTO() dto.Checkout = r.latestCheckoutDTO(orderID) return &dto, nil } func (r *Repository) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, error) { var order model.RentalOrder if err := r.db.First(&order, orderID).Error; err != nil { return nil, err } var records []model.HandoffRecord if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil { return nil, err } items := make([]HandoffRecordDTO, 0, len(records)) for _, record := range records { items = append(items, toHandoffDTO(record)) } return items, nil } func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error { return r.db.Transaction(func(tx *gorm.DB) error { order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID) if err != nil { return err } if isTerminalStatus(order.Status) { return ErrOrderCannotComplete } beforeOrderStatus := order.Status beforeHandoffStatus := order.HandoffStatus beforeSettlementStatus := order.SettlementStatus beforeListingStatus := listing.Status beforeAccountStatus := account.Status now := time.Now() order.Status = "closed" order.HandoffStatus = "admin_closed" order.SettlementStatus = "closed" order.SettledAt = &now listing.Status = "offline" listing.InTransaction = false account.Status = "offline" if beforeOrderStatus != "pending_payment" { total := order.RentAmount + order.DepositAmount if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "out", Amount: total, BalanceType: "frozen", BizType: "admin_order_close", BizNo: order.OrderNo, Remark: "后台关闭订单释放冻结金额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "in", Amount: total, BalanceType: "available", BizType: "admin_order_close_refund", BizNo: order.OrderNo, Remark: "后台关闭订单退回可用余额", }, ); err != nil { return err } } if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "order_admin", Title: "订单已由客服关闭", Content: "客服已关闭订单,模拟冻结金额已释放。原因:" + req.Reason, BizType: "order", BizID: &order.ID, }, notification.Entry{ UserID: order.OwnerID, Type: "order_admin", Title: "订单已由客服关闭", Content: "客服已关闭订单,关联商品已下架。原因:" + req.Reason, BizType: "order", BizID: &order.ID, }, ); err != nil { return err } if err := appendAuditLog(tx, adminID, "order.admin_close", "order", order.ID, meta, map[string]any{ "order_id": order.ID, "order_no": order.OrderNo, "listing_id": order.ListingID, "account_id": order.AccountID, "reason": req.Reason, "before_order_status": beforeOrderStatus, "after_order_status": order.Status, "before_handoff_status": beforeHandoffStatus, "after_handoff_status": order.HandoffStatus, "before_settlement_status": beforeSettlementStatus, "after_settlement_status": order.SettlementStatus, "before_listing_status": beforeListingStatus, "after_listing_status": listing.Status, "before_account_status": beforeAccountStatus, "after_account_status": account.Status, }); 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 }) } func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error { return r.db.Transaction(func(tx *gorm.DB) error { order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID) if err != nil { return err } if isTerminalStatus(order.Status) { return ErrOrderCannotComplete } beforeOrderStatus := order.Status beforeHandoffStatus := order.HandoffStatus beforeListingStatus := listing.Status beforeAccountStatus := account.Status order.Status = "abnormal" order.HandoffStatus = "admin_abnormal" listing.Status = "abnormal" listing.InTransaction = false account.Status = "abnormal" if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "order_admin", Title: "订单已被标记异常", Content: "客服已将订单标记为异常,请等待进一步处理。原因:" + req.Reason, BizType: "order", BizID: &order.ID, }, notification.Entry{ UserID: order.OwnerID, Type: "order_admin", Title: "订单已被标记异常", Content: "客服已将订单标记为异常,关联商品暂不可出租。原因:" + req.Reason, BizType: "order", BizID: &order.ID, }, ); err != nil { return err } if err := appendAuditLog(tx, adminID, "order.mark_abnormal", "order", order.ID, meta, map[string]any{ "order_id": order.ID, "order_no": order.OrderNo, "listing_id": order.ListingID, "account_id": order.AccountID, "reason": req.Reason, "before_order_status": beforeOrderStatus, "after_order_status": order.Status, "before_handoff_status": beforeHandoffStatus, "after_handoff_status": order.HandoffStatus, "before_listing_status": beforeListingStatus, "after_listing_status": listing.Status, "before_account_status": beforeAccountStatus, "after_account_status": account.Status, }); 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 }) } func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) { var row orderRow if err := r.baseQuery(). Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID). First(&row).Error; err != nil { return nil, err } dto := row.toDTO() dto.Checkout = r.latestCheckoutDTO(orderID) return &dto, nil } func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, checkout *model.OrderCheckout, renterContent string) error { 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 } now := time.Now() order.Status = "completed" order.HandoffStatus = "returned" order.SettlementStatus = "settled" order.SettledAt = &now order.OwnerSettledAt = &now listing.Status = "published" listing.InTransaction = false account.Status = "published" orderID := order.ID if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "out", Amount: order.RentAmount + order.DepositAmount, BalanceType: "frozen", BizType: "order_settle", BizNo: order.OrderNo, Remark: "订单结账释放冻结金额", }, wallet.Entry{ UserID: order.OwnerID, OrderID: &orderID, Direction: "in", Amount: checkout.OwnerIncomeAmount, BalanceType: "available", BizType: "owner_income", BizNo: order.OrderNo, Remark: "订单结账收入", }, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "in", Amount: checkout.RenterRefundAmount, BalanceType: "available", BizType: "deposit_release", BizNo: order.OrderNo, Remark: "订单结账退回押金", }, ); err != nil { return err } 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 err } if err := tx.Save(order).Error; err != nil { return err } if err := tx.Save(checkout).Error; err != nil { return err } if err := tx.Save(&listing).Error; err != nil { return err } return tx.Save(&account).Error } func (r *Repository) findOrderAssetsForAdminUpdate(tx *gorm.DB, orderID uint64) (*model.RentalOrder, *model.RentalListing, *model.GameAccount, error) { var order model.RentalOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil { return nil, nil, nil, err } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { return nil, nil, nil, err } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { return nil, nil, nil, err } return &order, &listing, &account, nil } func isTerminalStatus(status string) bool { return status == "completed" || status == "cancelled" || status == "closed" } func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) { var record model.HandoffRecord if err := r.db.First(&record, id).Error; err != nil { return nil, err } dto := toHandoffDTO(record) return &dto, nil } func pendingPaymentTimeoutMinutes(tx *gorm.DB) int { var row model.SystemConfig err := tx.Where("`key` = ?", "order.pending_payment_timeout_minutes").First(&row).Error if err != nil { return defaultPendingPaymentTimeoutMinutes } value, err := strconv.Atoi(row.Value) if err != nil || value <= 0 { return defaultPendingPaymentTimeoutMinutes } return value } func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmount float64, coinConsumedM float64, otherAmount float64, explicitDeduct float64, useExplicitDeduct bool) (model.OrderCheckout, error) { if consumableAmount < 0 || coinConsumedM < 0 || otherAmount < 0 || explicitDeduct < 0 { return model.OrderCheckout{}, ErrInvalidCheckoutAmount } deductAmount := consumableAmount + otherAmount if useExplicitDeduct { deductAmount = explicitDeduct } if deductAmount > order.DepositAmount { return model.OrderCheckout{}, ErrInvalidCheckoutAmount } renterRefund := order.DepositAmount - deductAmount evidence, err := marshalStringList(evidenceURLS) if err != nil { return model.OrderCheckout{}, err } return model.OrderCheckout{ OrderID: order.ID, InitiatedBy: initiatedBy, Status: status, RentAmount: order.RentAmount, DepositAmount: order.DepositAmount, ConsumableAmount: roundMoney(consumableAmount), CoinConsumedM: roundMoney(coinConsumedM), OtherAmount: roundMoney(otherAmount), DepositDeductAmount: roundMoney(deductAmount), RenterRefundAmount: roundMoney(renterRefund), OwnerIncomeAmount: roundMoney(order.RentAmount + deductAmount), Content: content, EvidenceURLS: evidence, }, nil } func marshalStringList(items []string) (datatypes.JSON, error) { if items == nil { items = []string{} } raw, err := json.Marshal(items) return datatypes.JSON(raw), err } func decodeStringList(raw datatypes.JSON) []string { if len(raw) == 0 { return []string{} } var items []string if err := json.Unmarshal(raw, &items); err != nil { return []string{} } return items } func roundMoney(value float64) float64 { return math.Round(value*100) / 100 } 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 } func (r *Repository) latestCheckoutDTO(orderID uint64) *CheckoutDTO { var checkout model.OrderCheckout if err := r.db.Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil { return nil } dto := toCheckoutDTO(checkout) return &dto } func (r *Repository) findCheckout(id uint64) (*model.OrderCheckout, error) { var checkout model.OrderCheckout if err := r.db.First(&checkout, id).Error; err != nil { return nil, err } return &checkout, nil } func (r *Repository) baseQuery() *gorm.DB { return r.db.Table("rental_orders AS o"). Select("o.*, a.title, a.server_region, a.login_platform"). Joins("JOIN game_accounts AS a ON a.id = o.account_id") } func (r *Repository) adminQuery() *gorm.DB { return r.db.Table("rental_orders AS o"). Select("o.*, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone"). Joins("JOIN game_accounts AS a ON a.id = o.account_id"). Joins("JOIN users AS owner ON owner.id = o.owner_id"). Joins("JOIN users AS renter ON renter.id = o.renter_id") } type orderRow struct { model.RentalOrder Title string ServerRegion string LoginPlatform string OwnerPhone string RenterPhone string } func (row orderRow) toDTO() OrderDTO { return OrderDTO{ ID: row.ID, OrderNo: row.OrderNo, ListingID: row.ListingID, AccountID: row.AccountID, OwnerID: row.OwnerID, RenterID: row.RenterID, OwnerPhone: row.OwnerPhone, RenterPhone: row.RenterPhone, Title: row.Title, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, RentStartAt: row.RentStartAt, RentEndAt: row.RentEndAt, RentAmount: row.RentAmount, DepositAmount: row.DepositAmount, PlatformFee: row.PlatformFee, AccountSnapshot: row.AccountSnapshot, Status: row.Status, HandoffStatus: row.HandoffStatus, SettlementStatus: row.SettlementStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO { return HandoffRecordDTO{ ID: record.ID, OrderID: record.OrderID, FromUserID: record.FromUserID, ToUserID: record.ToUserID, Type: record.Type, Content: record.Content, ConfirmedByRenterAt: record.ConfirmedByRenterAt, ConfirmedByOwnerAt: record.ConfirmedByOwnerAt, CreatedAt: record.CreatedAt, } } func toCheckoutDTO(checkout model.OrderCheckout) CheckoutDTO { return CheckoutDTO{ ID: checkout.ID, OrderID: checkout.OrderID, InitiatedBy: checkout.InitiatedBy, Status: checkout.Status, RentAmount: checkout.RentAmount, DepositAmount: checkout.DepositAmount, ConsumableAmount: checkout.ConsumableAmount, CoinConsumedM: checkout.CoinConsumedM, OtherAmount: checkout.OtherAmount, DepositDeductAmount: checkout.DepositDeductAmount, RenterRefundAmount: checkout.RenterRefundAmount, OwnerIncomeAmount: checkout.OwnerIncomeAmount, Content: checkout.Content, EvidenceURLS: decodeStringList(checkout.EvidenceURLS), OwnerAdjustmentReason: checkout.OwnerAdjustmentReason, OwnerAdjustedAt: checkout.OwnerAdjustedAt, RenterConfirmedAt: checkout.RenterConfirmedAt, RenterRejectedAt: checkout.RenterRejectedAt, CreatedAt: checkout.CreatedAt, UpdatedAt: checkout.UpdatedAt, } } func makeAccountSnapshot(account model.GameAccount) (datatypes.JSON, error) { payload := map[string]any{ "account_id": account.ID, "title": account.Title, "game_name": account.GameName, "server_region": account.ServerRegion, "login_platform": account.LoginPlatform, "rank_level": account.RankLevel, "haf_coin_amount": account.HafCoinAmount, "asset_summary": account.AssetSummary, "season_tags": account.SeasonTags, "screenshot_urls": account.ScreenshotURLS, "snapshot_version": 1, } raw, err := json.Marshal(payload) return datatypes.JSON(raw), err } func newOrderNo() (string, error) { buf := make([]byte, 4) if _, err := rand.Read(buf); err != nil { return "", err } return "RO" + strconv.FormatInt(time.Now().UnixNano(), 10) + hex.EncodeToString(buf), nil } func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error { raw, err := json.Marshal(detail) if err != nil { return err } row := model.AuditLog{ ActorType: "admin", ActorID: actorID, Action: action, BizType: bizType, BizID: &bizID, IP: meta.IP, UserAgent: meta.UserAgent, Detail: datatypes.JSON(raw), } return tx.Create(&row).Error } func IsNotFound(err error) bool { return errors.Is(err, gorm.ErrRecordNotFound) }