diff --git a/backend/internal/model/listing.go b/backend/internal/model/listing.go index 8ebaabe..bf25749 100644 --- a/backend/internal/model/listing.go +++ b/backend/internal/model/listing.go @@ -32,6 +32,7 @@ type RentalListing struct { ID uint64 `gorm:"primaryKey" json:"id"` AccountID uint64 `gorm:"not null;index" json:"account_id"` OwnerID uint64 `gorm:"not null;index" json:"owner_id"` + Price float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price"` PriceHourly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_hourly"` PriceDaily float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_daily"` PriceWeekly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_weekly"` diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index 8b1b918..9fdd3b4 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -18,11 +18,13 @@ type ListingDTO struct { AssetSummary map[string]any `json:"asset_summary,omitempty"` ScreenshotURLS []string `json:"screenshot_urls"` CoverURL string `json:"cover_url"` + Price float64 `json:"price"` PriceHourly float64 `json:"price_hourly"` PriceDaily float64 `json:"price_daily"` PriceWeekly float64 `json:"price_weekly"` DepositAmount float64 `json:"deposit_amount"` IsAccelerated bool `json:"is_accelerated_sale"` + InTransaction bool `json:"in_transaction"` Status string `json:"status"` ReviewStatus string `json:"review_status"` ReviewReason string `json:"review_reason"` @@ -40,6 +42,7 @@ type CreateRequest struct { HafCoinAmount int64 `json:"haf_coin_amount"` AssetSummary map[string]any `json:"asset_summary"` ScreenshotURLS []string `json:"screenshot_urls"` + Price float64 `json:"price"` PriceHourly float64 `json:"price_hourly"` PriceDaily float64 `json:"price_daily"` PriceWeekly float64 `json:"price_weekly"` diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go index 027afe0..cf17f2d 100644 --- a/backend/internal/modules/listing/repository.go +++ b/backend/internal/modules/listing/repository.go @@ -60,12 +60,14 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo if err := tx.Create(&account).Error; err != nil { return err } + price := normalizedListingPrice(req) listing := model.RentalListing{ AccountID: account.ID, OwnerID: ownerID, - PriceHourly: req.PriceHourly, - PriceDaily: req.PriceDaily, - PriceWeekly: req.PriceWeekly, + Price: price, + PriceHourly: listingHourlyPrice(req, price), + PriceDaily: listingDailyPrice(req, price), + PriceWeekly: listingWeeklyPrice(req, price), DepositAmount: req.DepositAmount, Status: listingStatus, ReviewStatus: reviewStatus, @@ -87,7 +89,7 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest, if err != nil { return err } - if listing.Status == "rented" { + if listing.Status == "rented" || listing.InTransaction { return ErrListingLocked } @@ -108,9 +110,11 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest, } account.ScreenshotURLS = screenshots - listing.PriceHourly = req.PriceHourly - listing.PriceDaily = req.PriceDaily - listing.PriceWeekly = req.PriceWeekly + price := normalizedListingPrice(req) + listing.Price = price + listing.PriceHourly = listingHourlyPrice(req, price) + listing.PriceDaily = listingDailyPrice(req, price) + listing.PriceWeekly = listingWeeklyPrice(req, price) listing.DepositAmount = req.DepositAmount listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) listing.Status = listingStatus @@ -137,7 +141,7 @@ func (r *Repository) SubmitReview(ownerID uint64, listingID uint64, reviewRequir if err != nil { return err } - if listing.Status == "rented" { + if listing.Status == "rented" || listing.InTransaction { return ErrListingLocked } listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) @@ -215,7 +219,7 @@ func (r *Repository) adminUpdateStatus(adminID uint64, listingID uint64, req Adm if err != nil { return err } - if listing.Status == "rented" { + if listing.Status == "rented" || listing.InTransaction { return ErrListingLocked } beforeListingStatus := listing.Status @@ -270,7 +274,7 @@ func (r *Repository) Approve(listingID uint64) (*ListingDTO, error) { if err != nil { return err } - if listing.Status == "rented" { + if listing.Status == "rented" || listing.InTransaction { return ErrListingLocked } now := time.Now() @@ -309,7 +313,7 @@ func (r *Repository) Reject(listingID uint64, req ReviewRequest) (*ListingDTO, e if err != nil { return err } - if listing.Status == "rented" { + if listing.Status == "rented" || listing.InTransaction { return ErrListingLocked } listing.Status = "draft" @@ -346,7 +350,7 @@ func (r *Repository) Offline(ownerID uint64, listingID uint64) error { if err != nil { return err } - if listing.Status == "rented" { + if listing.Status == "rented" || listing.InTransaction { return ErrListingLocked } listing.Status = "offline" @@ -484,6 +488,66 @@ func rowsToDTO(rows []listingRow) []ListingDTO { return items } +func normalizedListingPrice(req CreateRequest) float64 { + if req.Price > 0 { + return req.Price + } + if req.PriceDaily > 0 { + return req.PriceDaily + } + if req.PriceHourly > 0 { + return req.PriceHourly * 24 + } + if req.PriceWeekly > 0 { + return req.PriceWeekly / 7 + } + return 0 +} + +func listingHourlyPrice(req CreateRequest, price float64) float64 { + if req.PriceHourly > 0 { + return req.PriceHourly + } + return price / 24 +} + +func listingDailyPrice(req CreateRequest, price float64) float64 { + if req.PriceDaily > 0 { + return req.PriceDaily + } + return price +} + +func listingWeeklyPrice(req CreateRequest, price float64) float64 { + if req.PriceWeekly > 0 { + return req.PriceWeekly + } + return price * 7 +} + +func listingDisplayPrices(price float64, hourly float64, daily float64, weekly float64) (float64, float64, float64, float64) { + if price <= 0 { + switch { + case daily > 0: + price = daily + case hourly > 0: + price = hourly * 24 + case weekly > 0: + price = weekly / 7 + } + } + if daily <= 0 { + daily = price + } + if hourly <= 0 && price > 0 { + hourly = price / 24 + } + if weekly <= 0 && price > 0 { + weekly = price * 7 + } + return price, hourly, daily, weekly +} + func publicListings(items []ListingDTO) []ListingDTO { for index := range items { applyPublicListingURLs(&items[index]) @@ -501,6 +565,7 @@ func applyPublicListingURLs(item *ListingDTO) { func (row listingRow) toDTO() ListingDTO { assetSummary := decodeAssetSummary(row.AssetSummary) screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS)) + price, priceHourly, priceDaily, priceWeekly := listingDisplayPrices(row.Price, row.PriceHourly, row.PriceDaily, row.PriceWeekly) return ListingDTO{ ID: row.ID, AccountID: row.AccountID, @@ -517,11 +582,13 @@ func (row listingRow) toDTO() ListingDTO { AssetSummary: assetSummary, ScreenshotURLS: screenshotURLS, CoverURL: publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus), - PriceHourly: row.PriceHourly, - PriceDaily: row.PriceDaily, - PriceWeekly: row.PriceWeekly, + Price: price, + PriceHourly: priceHourly, + PriceDaily: priceDaily, + PriceWeekly: priceWeekly, DepositAmount: row.DepositAmount, IsAccelerated: isAcceleratedSale(assetSummary), + InTransaction: row.InTransaction, Status: row.Status, ReviewStatus: row.ReviewStatus, ReviewReason: row.ReviewReason, @@ -534,6 +601,7 @@ func (row listingRow) toDTO() ListingDTO { func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO { assetSummary := decodeAssetSummary(account.AssetSummary) screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS)) + price, priceHourly, priceDaily, priceWeekly := listingDisplayPrices(listing.Price, listing.PriceHourly, listing.PriceDaily, listing.PriceWeekly) return &ListingDTO{ ID: listing.ID, AccountID: account.ID, @@ -548,11 +616,13 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO { AssetSummary: assetSummary, ScreenshotURLS: screenshotURLS, CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus), - PriceHourly: listing.PriceHourly, - PriceDaily: listing.PriceDaily, - PriceWeekly: listing.PriceWeekly, + Price: price, + PriceHourly: priceHourly, + PriceDaily: priceDaily, + PriceWeekly: priceWeekly, DepositAmount: listing.DepositAmount, IsAccelerated: isAcceleratedSale(assetSummary), + InTransaction: listing.InTransaction, Status: listing.Status, ReviewStatus: listing.ReviewStatus, ReviewReason: listing.ReviewReason, diff --git a/backend/internal/modules/listing/service.go b/backend/internal/modules/listing/service.go index 6091080..0eb364a 100644 --- a/backend/internal/modules/listing/service.go +++ b/backend/internal/modules/listing/service.go @@ -211,7 +211,7 @@ func validateRequest(req CreateRequest, rules publishRules) error { if strings.TrimSpace(req.ServerRegion) == "" { return ErrMissingServerRegion } - if req.PriceHourly <= 0 { + if normalizedListingPrice(req) <= 0 { return ErrInvalidPrice } if req.DepositAmount < 0 { diff --git a/backend/internal/modules/order/repository.go b/backend/internal/modules/order/repository.go index f8ec860..11a79b0 100644 --- a/backend/internal/modules/order/repository.go +++ b/backend/internal/modules/order/repository.go @@ -28,6 +28,21 @@ 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 { @@ -62,7 +77,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro OwnerID: listing.OwnerID, RenterID: renterID, RentHours: rentHours, - RentAmount: listing.PriceHourly * float64(rentHours), + RentAmount: listingOrderPrice(listing), DepositAmount: listing.DepositAmount, PlatformFee: 0, AccountSnapshot: snapshot, diff --git a/backend/migrations/000001_init.sql b/backend/migrations/000001_init.sql index 4f5a298..3ae33ff 100644 --- a/backend/migrations/000001_init.sql +++ b/backend/migrations/000001_init.sql @@ -53,6 +53,7 @@ CREATE TABLE rental_listings ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, account_id BIGINT UNSIGNED NOT NULL, owner_id BIGINT UNSIGNED NOT NULL, + price DECIMAL(12,2) NOT NULL DEFAULT 0.00, price_hourly DECIMAL(12,2) NOT NULL DEFAULT 0.00, price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00, price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00, @@ -66,7 +67,8 @@ CREATE TABLE rental_listings ( updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_rental_listings_account_id (account_id), KEY idx_rental_listings_owner_id (owner_id), - KEY idx_rental_listings_filter (status, review_status, price_hourly) + KEY idx_rental_listings_filter (status, review_status, price), + KEY idx_rental_listings_legacy_filter (status, review_status, price_hourly) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE TABLE rental_orders ( diff --git a/backend/migrations/000002_order_payment_stepwise.sql b/backend/migrations/000002_order_payment_stepwise.sql index faff9f3..d708c55 100644 --- a/backend/migrations/000002_order_payment_stepwise.sql +++ b/backend/migrations/000002_order_payment_stepwise.sql @@ -15,6 +15,32 @@ PREPARE add_listing_in_transaction_stmt FROM @add_listing_in_transaction_sql; EXECUTE add_listing_in_transaction_stmt; DEALLOCATE PREPARE add_listing_in_transaction_stmt; +SET @listing_price_exists := ( + SELECT COUNT(*) + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'rental_listings' + AND column_name = 'price' +); + +SET @add_listing_price_sql := IF( + @listing_price_exists = 0, + 'ALTER TABLE rental_listings ADD COLUMN price DECIMAL(12,2) NOT NULL DEFAULT 0.00 AFTER owner_id', + 'SELECT 1' +); +PREPARE add_listing_price_stmt FROM @add_listing_price_sql; +EXECUTE add_listing_price_stmt; +DEALLOCATE PREPARE add_listing_price_stmt; + +UPDATE rental_listings +SET price = CASE + WHEN price_daily > 0 THEN price_daily + WHEN price_hourly > 0 THEN price_hourly * 24 + WHEN price_weekly > 0 THEN price_weekly / 7 + ELSE price +END +WHERE price <= 0; + 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/listings.ts b/frontend/src/api/listings.ts index 19705c0..e995773 100644 --- a/frontend/src/api/listings.ts +++ b/frontend/src/api/listings.ts @@ -16,11 +16,13 @@ export interface Listing { asset_summary?: Record screenshot_urls: string[] cover_url: string + price: number price_hourly: number price_daily: number price_weekly: number deposit_amount: number is_accelerated_sale?: boolean + in_transaction: boolean status: string review_status: string review_reason: string @@ -38,6 +40,7 @@ export interface ListingPayload { haf_coin_amount: number asset_summary?: Record screenshot_urls: string[] + price: number price_hourly: number price_daily: number price_weekly: number diff --git a/frontend/src/views/admin/AdminListingDetailView.vue b/frontend/src/views/admin/AdminListingDetailView.vue index 4017eb8..e552b74 100644 --- a/frontend/src/views/admin/AdminListingDetailView.vue +++ b/frontend/src/views/admin/AdminListingDetailView.vue @@ -58,7 +58,7 @@ function money(value: number) { } function listingPrice(row: Listing) { - return money(row.price_daily || row.price_hourly) + return money(row.price || row.price_daily || row.price_hourly) } function extractObjectKey(url: string) { diff --git a/frontend/src/views/admin/AdminListingReviewView.vue b/frontend/src/views/admin/AdminListingReviewView.vue index 67511c0..63019cc 100644 --- a/frontend/src/views/admin/AdminListingReviewView.vue +++ b/frontend/src/views/admin/AdminListingReviewView.vue @@ -63,7 +63,7 @@ function openEvidence(row: Listing) { } function listingPrice(row: Listing) { - return `¥${Number(row.price_daily || row.price_hourly || 0).toFixed(2)}` + return `¥${Number(row.price || row.price_daily || row.price_hourly || 0).toFixed(2)}` } function extractObjectKey(url: string) { diff --git a/frontend/src/views/admin/AdminListingsView.vue b/frontend/src/views/admin/AdminListingsView.vue index cb805a7..e144a47 100644 --- a/frontend/src/views/admin/AdminListingsView.vue +++ b/frontend/src/views/admin/AdminListingsView.vue @@ -43,7 +43,7 @@ function money(value: number) { } function listingPrice(row: Listing) { - return money(row.price_daily || row.price_hourly) + return money(row.price || row.price_daily || row.price_hourly) } function ownerName(row: Listing) { diff --git a/frontend/src/views/mobile/MobileSellerListingCreateView.vue b/frontend/src/views/mobile/MobileSellerListingCreateView.vue index 6c41882..595d584 100644 --- a/frontend/src/views/mobile/MobileSellerListingCreateView.vue +++ b/frontend/src/views/mobile/MobileSellerListingCreateView.vue @@ -439,6 +439,7 @@ async function handleSubmit() { haf_coin_amount: coinMAmount.value * 1000000, asset_summary: buildAssetSummary(), screenshot_urls: screenshotUrls.value, + price: dailyPrice, price_hourly: hourlyPrice, price_daily: dailyPrice, price_weekly: roundMoney(dailyPrice * 7), diff --git a/frontend/src/views/public/ListingDetailView.vue b/frontend/src/views/public/ListingDetailView.vue index bb38915..f3e0ab2 100644 --- a/frontend/src/views/public/ListingDetailView.vue +++ b/frontend/src/views/public/ListingDetailView.vue @@ -45,7 +45,7 @@ function readError(error: unknown, fallback: string) { } function listingPrice(item: Listing) { - return Number(item.price_daily || item.price_hourly || 0).toFixed(2); + return Number(item.price || item.price_daily || item.price_hourly || 0).toFixed(2); } @@ -104,10 +104,11 @@ function listingPrice(item: Listing) { type="warning" size="large" :loading="ordering" + :disabled="listing.in_transaction" class="full-control" @click="handleCreateOrder" > - 立即下单 + {{ listing.in_transaction ? "交易中" : "立即下单" }} diff --git a/frontend/src/views/seller/SellerListingCreateView.vue b/frontend/src/views/seller/SellerListingCreateView.vue index be6181d..8875f61 100644 --- a/frontend/src/views/seller/SellerListingCreateView.vue +++ b/frontend/src/views/seller/SellerListingCreateView.vue @@ -17,18 +17,17 @@ const form = reactive({ login_platform: 'QQ', rank_level: '', haf_coin_amount: 0, - price_hourly: 5, - price_daily: 50, - price_weekly: 300, + price: 50, deposit_amount: 100, }) async function handleSubmit() { loading.value = true try { - const price = Number(form.price_daily || 0) + const price = Number(form.price || 0) const listing = await createListing({ ...form, + price, price_hourly: Math.max(Math.round((price / 24) * 100) / 100, 0.01), price_daily: price, price_weekly: Math.round(price * 7 * 100) / 100, @@ -125,7 +124,7 @@ function readError(error: unknown, fallback: string) { - + diff --git a/frontend/src/views/seller/SellerListingsView.vue b/frontend/src/views/seller/SellerListingsView.vue index 07a4120..eae7c23 100644 --- a/frontend/src/views/seller/SellerListingsView.vue +++ b/frontend/src/views/seller/SellerListingsView.vue @@ -45,7 +45,7 @@ function readSellerPrice(row: Listing) { const price = Number((breakdown as Record).seller_total_price) if (Number.isFinite(price) && price > 0) return price } - return Number(row.price_daily || row.price_hourly || 0) + return Number(row.price || row.price_daily || row.price_hourly || 0) }