From 778e08a261f470f15064a5d3ee76d3b77cc05aa4 Mon Sep 17 00:00:00 2001 From: yml Date: Sun, 24 May 2026 18:24:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=BC=E5=AE=B9=E6=8E=A5=E5=85=A5=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E7=A7=9F=E6=9C=9F=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/jobs/ordertimeout/job.go | 6 +- backend/internal/model/order.go | 42 ++++---- backend/internal/modules/order/dto.go | 48 ++++----- backend/internal/modules/order/repository.go | 98 ++++++++++++------- backend/migrations/000001_init.sql | 5 +- .../000002_order_payment_stepwise.sql | 61 ++++++++++++ frontend/src/api/orders.ts | 2 + .../src/views/account/OrderDetailView.vue | 17 +++- .../src/views/admin/AdminOrderDetailView.vue | 17 +++- 9 files changed, 211 insertions(+), 85 deletions(-) diff --git a/backend/internal/jobs/ordertimeout/job.go b/backend/internal/jobs/ordertimeout/job.go index ef23723..cceb01b 100644 --- a/backend/internal/jobs/ordertimeout/job.go +++ b/backend/internal/jobs/ordertimeout/job.go @@ -296,8 +296,12 @@ func (j *Job) handleRenterConfirmTimeout(ctx context.Context, now time.Time, cfg func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresholds) (int, error) { var rows []model.RentalOrder + overdueBefore := now.Add(-time.Duration(cfg.ReturnOverdueGraceMinutes) * time.Minute) err := j.db.WithContext(ctx). - Where("status = ? AND rent_end_at IS NOT NULL AND rent_end_at <= ?", "renting", now.Add(-time.Duration(cfg.ReturnOverdueGraceMinutes)*time.Minute)). + Where(`status = ? AND ( + (rented_at IS NOT NULL AND TIMESTAMPADD(HOUR, COALESCE(NULLIF(estimated_duration_hours, 0), 24), rented_at) <= ?) + OR (rented_at IS NULL AND rent_end_at IS NOT NULL AND rent_end_at <= ?) + )`, "renting", overdueBefore, overdueBefore). Order("id ASC"). Limit(100). Find(&rows).Error diff --git a/backend/internal/model/order.go b/backend/internal/model/order.go index 0f900b2..4822c65 100644 --- a/backend/internal/model/order.go +++ b/backend/internal/model/order.go @@ -7,26 +7,28 @@ import ( ) type RentalOrder struct { - ID uint64 `gorm:"primaryKey" json:"id"` - OrderNo string `gorm:"size:64;not null;uniqueIndex" json:"order_no"` - ListingID uint64 `gorm:"not null;index" json:"listing_id"` - AccountID uint64 `gorm:"not null;index" json:"account_id"` - OwnerID uint64 `gorm:"not null;index" json:"owner_id"` - RenterID uint64 `gorm:"not null;index" json:"renter_id"` - RentStartAt *time.Time `json:"rent_start_at"` - RentEndAt *time.Time `json:"rent_end_at"` - RentHours int `gorm:"not null" json:"rent_hours"` - RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"` - DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"` - PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"platform_fee"` - AccountSnapshot datatypes.JSON `json:"account_snapshot"` - Status string `gorm:"size:32;not null;default:'pending_confirm'" json:"status"` - HandoffStatus string `gorm:"size:32;not null;default:'none'" json:"handoff_status"` - SettlementStatus string `gorm:"size:32;not null;default:'unsettled'" json:"settlement_status"` - OwnerSettledAt *time.Time `json:"owner_settled_at"` - SettledAt *time.Time `json:"settled_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `gorm:"primaryKey" json:"id"` + OrderNo string `gorm:"size:64;not null;uniqueIndex" json:"order_no"` + ListingID uint64 `gorm:"not null;index" json:"listing_id"` + AccountID uint64 `gorm:"not null;index" json:"account_id"` + OwnerID uint64 `gorm:"not null;index" json:"owner_id"` + RenterID uint64 `gorm:"not null;index" json:"renter_id"` + RentedAt *time.Time `json:"rented_at"` + EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"` + RentStartAt *time.Time `json:"rent_start_at"` + RentEndAt *time.Time `json:"rent_end_at"` + RentHours int `gorm:"not null" json:"rent_hours"` + RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"` + DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"` + PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"platform_fee"` + AccountSnapshot datatypes.JSON `json:"account_snapshot"` + Status string `gorm:"size:32;not null;default:'pending_confirm'" json:"status"` + HandoffStatus string `gorm:"size:32;not null;default:'none'" json:"handoff_status"` + SettlementStatus string `gorm:"size:32;not null;default:'unsettled'" json:"settlement_status"` + OwnerSettledAt *time.Time `json:"owner_settled_at"` + SettledAt *time.Time `json:"settled_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (RentalOrder) TableName() string { diff --git a/backend/internal/modules/order/dto.go b/backend/internal/modules/order/dto.go index 486fd2f..e434330 100644 --- a/backend/internal/modules/order/dto.go +++ b/backend/internal/modules/order/dto.go @@ -7,29 +7,31 @@ import ( ) type OrderDTO struct { - ID uint64 `json:"id"` - OrderNo string `json:"order_no"` - ListingID uint64 `json:"listing_id"` - AccountID uint64 `json:"account_id"` - OwnerID uint64 `json:"owner_id"` - RenterID uint64 `json:"renter_id"` - OwnerPhone string `json:"owner_phone,omitempty"` - RenterPhone string `json:"renter_phone,omitempty"` - Title string `json:"title"` - ServerRegion string `json:"server_region"` - LoginPlatform string `json:"login_platform"` - RentStartAt *time.Time `json:"rent_start_at"` - RentEndAt *time.Time `json:"rent_end_at"` - RentAmount float64 `json:"rent_amount"` - DepositAmount float64 `json:"deposit_amount"` - PlatformFee float64 `json:"platform_fee"` - AccountSnapshot datatypes.JSON `json:"account_snapshot"` - Status string `json:"status"` - HandoffStatus string `json:"handoff_status"` - SettlementStatus string `json:"settlement_status"` - Checkout *CheckoutDTO `json:"checkout,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `json:"id"` + OrderNo string `json:"order_no"` + ListingID uint64 `json:"listing_id"` + AccountID uint64 `json:"account_id"` + OwnerID uint64 `json:"owner_id"` + RenterID uint64 `json:"renter_id"` + OwnerPhone string `json:"owner_phone,omitempty"` + RenterPhone string `json:"renter_phone,omitempty"` + Title string `json:"title"` + ServerRegion string `json:"server_region"` + LoginPlatform string `json:"login_platform"` + RentedAt *time.Time `json:"rented_at"` + EstimatedDurationHours int `json:"estimated_duration_hours"` + RentStartAt *time.Time `json:"rent_start_at"` + RentEndAt *time.Time `json:"rent_end_at"` + RentAmount float64 `json:"rent_amount"` + DepositAmount float64 `json:"deposit_amount"` + PlatformFee float64 `json:"platform_fee"` + AccountSnapshot datatypes.JSON `json:"account_snapshot"` + Status string `json:"status"` + HandoffStatus string `json:"handoff_status"` + SettlementStatus string `json:"settlement_status"` + Checkout *CheckoutDTO `json:"checkout,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateRequest struct { diff --git a/backend/internal/modules/order/repository.go b/backend/internal/modules/order/repository.go index 11a79b0..40814e5 100644 --- a/backend/internal/modules/order/repository.go +++ b/backend/internal/modules/order/repository.go @@ -43,6 +43,16 @@ func listingOrderPrice(listing model.RentalListing) float64 { } } +func orderDurationHours(order model.RentalOrder) int { + if order.EstimatedDurationHours > 0 { + return order.EstimatedDurationHours + } + if order.RentHours > 0 { + return order.RentHours + } + return internalOrderHours +} + func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) { var createdID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { @@ -71,19 +81,20 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro } 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", + OrderNo: orderNo, + ListingID: listing.ID, + AccountID: listing.AccountID, + OwnerID: listing.OwnerID, + RenterID: renterID, + EstimatedDurationHours: rentHours, + 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 @@ -357,8 +368,11 @@ func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error { } order.Status = "renting" order.HandoffStatus = "received" + durationHours := orderDurationHours(order) + order.EstimatedDurationHours = durationHours order.RentStartAt = &now - rentEnd := now.Add(time.Duration(order.RentHours) * time.Hour) + order.RentedAt = &now + rentEnd := now.Add(time.Duration(durationHours) * time.Hour) order.RentEndAt = &rentEnd orderID := order.ID if err := notification.Append(tx, notification.Entry{ @@ -1053,29 +1067,41 @@ type orderRow struct { } func (row orderRow) toDTO() OrderDTO { + rentedAt := row.RentedAt + if rentedAt == nil { + rentedAt = row.RentStartAt + } + durationHours := orderDurationHours(row.RentalOrder) + rentEndAt := row.RentEndAt + if rentEndAt == nil && rentedAt != nil && durationHours > 0 { + calculatedEnd := rentedAt.Add(time.Duration(durationHours) * time.Hour) + rentEndAt = &calculatedEnd + } 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, + 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, + RentedAt: rentedAt, + EstimatedDurationHours: durationHours, + RentStartAt: row.RentStartAt, + RentEndAt: 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, } } diff --git a/backend/migrations/000001_init.sql b/backend/migrations/000001_init.sql index 3ae33ff..be0fb99 100644 --- a/backend/migrations/000001_init.sql +++ b/backend/migrations/000001_init.sql @@ -78,6 +78,8 @@ CREATE TABLE rental_orders ( account_id BIGINT UNSIGNED NOT NULL, owner_id BIGINT UNSIGNED NOT NULL, renter_id BIGINT UNSIGNED NOT NULL, + rented_at DATETIME NULL, + estimated_duration_hours INT NOT NULL DEFAULT 24, rent_start_at DATETIME NULL, rent_end_at DATETIME NULL, rent_hours INT NOT NULL, @@ -95,7 +97,8 @@ CREATE TABLE rental_orders ( UNIQUE KEY uk_rental_orders_order_no (order_no), KEY idx_rental_orders_renter_status (renter_id, status), KEY idx_rental_orders_owner_status (owner_id, status), - KEY idx_rental_orders_listing_id (listing_id) + KEY idx_rental_orders_listing_id (listing_id), + KEY idx_rental_orders_rented_at (status, rented_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE handoff_records ( diff --git a/backend/migrations/000002_order_payment_stepwise.sql b/backend/migrations/000002_order_payment_stepwise.sql index d708c55..a24b80d 100644 --- a/backend/migrations/000002_order_payment_stepwise.sql +++ b/backend/migrations/000002_order_payment_stepwise.sql @@ -41,6 +41,67 @@ SET price = CASE END WHERE price <= 0; +SET @order_rented_at_exists := ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'rental_orders' + AND column_name = 'rented_at' +); + +SET @add_order_rented_at_sql := IF( + @order_rented_at_exists = 0, + 'ALTER TABLE rental_orders ADD COLUMN rented_at DATETIME NULL AFTER renter_id', + 'SELECT 1' +); +PREPARE add_order_rented_at_stmt FROM @add_order_rented_at_sql; +EXECUTE add_order_rented_at_stmt; +DEALLOCATE PREPARE add_order_rented_at_stmt; + +SET @order_estimated_duration_exists := ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'rental_orders' + AND column_name = 'estimated_duration_hours' +); + +SET @add_order_estimated_duration_sql := IF( + @order_estimated_duration_exists = 0, + 'ALTER TABLE rental_orders ADD COLUMN estimated_duration_hours INT NOT NULL DEFAULT 24 AFTER rented_at', + 'SELECT 1' +); +PREPARE add_order_estimated_duration_stmt FROM @add_order_estimated_duration_sql; +EXECUTE add_order_estimated_duration_stmt; +DEALLOCATE PREPARE add_order_estimated_duration_stmt; + +UPDATE rental_orders +SET + rented_at = COALESCE(rented_at, rent_start_at), + estimated_duration_hours = CASE + WHEN estimated_duration_hours > 0 THEN estimated_duration_hours + WHEN rent_hours > 0 THEN rent_hours + ELSE 24 + END +WHERE rented_at IS NULL OR estimated_duration_hours <= 0; + +SET @order_rented_at_index_exists := ( + SELECT COUNT(*) + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = 'rental_orders' + AND index_name = 'idx_rental_orders_rented_at' +); + +SET @add_order_rented_at_index_sql := IF( + @order_rented_at_index_exists = 0, + 'ALTER TABLE rental_orders ADD KEY idx_rental_orders_rented_at (status, rented_at)', + 'SELECT 1' +); +PREPARE add_order_rented_at_index_stmt FROM @add_order_rented_at_index_sql; +EXECUTE add_order_rented_at_index_stmt; +DEALLOCATE PREPARE add_order_rented_at_index_stmt; + INSERT INTO system_configs (`key`, `value`, description) VALUES ('order.pending_payment_timeout_minutes', '15', '订单待支付超时取消分钟数') ON DUPLICATE KEY UPDATE `key` = VALUES(`key`); diff --git a/frontend/src/api/orders.ts b/frontend/src/api/orders.ts index f9ad03c..2ed2e30 100644 --- a/frontend/src/api/orders.ts +++ b/frontend/src/api/orders.ts @@ -12,6 +12,8 @@ export interface Order { title: string server_region: string login_platform: string + rented_at?: string + estimated_duration_hours: number rent_start_at?: string rent_end_at?: string rent_amount: number diff --git a/frontend/src/views/account/OrderDetailView.vue b/frontend/src/views/account/OrderDetailView.vue index 443a597..b5957c5 100644 --- a/frontend/src/views/account/OrderDetailView.vue +++ b/frontend/src/views/account/OrderDetailView.vue @@ -282,6 +282,19 @@ function readError(error: unknown, fallback: string) { return fallback } +function orderRentedAt() { + return order.value?.rented_at || order.value?.rent_start_at +} + +function orderEstimatedEndAt() { + if (!order.value) return undefined + if (order.value.rent_end_at) return order.value.rent_end_at + const rentedAt = orderRentedAt() + const durationHours = Number(order.value.estimated_duration_hours || 0) + if (!rentedAt || durationHours <= 0) return undefined + return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString() +} + function hydrateCounterForm() { if (!order.value?.checkout) return const checkout = order.value.checkout @@ -327,8 +340,8 @@ function linesToList(value: string) {
-

开始:{{ formatDateTime(order.rent_start_at, '未开始') }}

-

预计截止:{{ formatDateTime(order.rent_end_at, '未设置') }}

+

开始:{{ formatDateTime(orderRentedAt(), '未开始') }}

+

预计截止:{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}

订单待支付,支付后账号进入交接流程。

支付订单 diff --git a/frontend/src/views/admin/AdminOrderDetailView.vue b/frontend/src/views/admin/AdminOrderDetailView.vue index 48f89e1..659daae 100644 --- a/frontend/src/views/admin/AdminOrderDetailView.vue +++ b/frontend/src/views/admin/AdminOrderDetailView.vue @@ -63,6 +63,19 @@ function readError(error: unknown, fallback: string) { } return fallback } + +function orderRentedAt() { + return order.value?.rented_at || order.value?.rent_start_at +} + +function orderEstimatedEndAt() { + if (!order.value) return undefined + if (order.value.rent_end_at) return order.value.rent_end_at + const rentedAt = orderRentedAt() + const durationHours = Number(order.value.estimated_duration_hours || 0) + if (!rentedAt || durationHours <= 0) return undefined + return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString() +}