彻底优化数据库字段
This commit is contained in:
@@ -298,10 +298,7 @@ func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresh
|
|||||||
var rows []model.RentalOrder
|
var rows []model.RentalOrder
|
||||||
overdueBefore := now.Add(-time.Duration(cfg.ReturnOverdueGraceMinutes) * time.Minute)
|
overdueBefore := now.Add(-time.Duration(cfg.ReturnOverdueGraceMinutes) * time.Minute)
|
||||||
err := j.db.WithContext(ctx).
|
err := j.db.WithContext(ctx).
|
||||||
Where(`status = ? AND (
|
Where(`status = ? AND rented_at IS NOT NULL AND TIMESTAMPADD(HOUR, COALESCE(NULLIF(estimated_duration_hours, 0), 24), rented_at) <= ?`, "renting", overdueBefore).
|
||||||
(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").
|
Order("id ASC").
|
||||||
Limit(100).
|
Limit(100).
|
||||||
Find(&rows).Error
|
Find(&rows).Error
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Dispute struct {
|
|||||||
Type string `gorm:"size:32;not null" json:"type"`
|
Type string `gorm:"size:32;not null" json:"type"`
|
||||||
Status string `gorm:"size:32;not null;default:'open'" json:"status"`
|
Status string `gorm:"size:32;not null;default:'open'" json:"status"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
|
EvidenceURLS datatypes.JSON `gorm:"column:evidence_urls" json:"evidence_urls"`
|
||||||
ArbitrationResult string `gorm:"size:32;not null;default:''" json:"arbitration_result"`
|
ArbitrationResult string `gorm:"size:32;not null;default:''" json:"arbitration_result"`
|
||||||
ArbitrationRemark string `json:"arbitration_remark"`
|
ArbitrationRemark string `json:"arbitration_remark"`
|
||||||
HandledBy *uint64 `json:"handled_by"`
|
HandledBy *uint64 `json:"handled_by"`
|
||||||
|
|||||||
@@ -33,9 +33,6 @@ type RentalListing struct {
|
|||||||
AccountID uint64 `gorm:"not null;index" json:"account_id"`
|
AccountID uint64 `gorm:"not null;index" json:"account_id"`
|
||||||
OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
|
OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
|
||||||
Price float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price"`
|
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"`
|
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
||||||
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
|
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
|
||||||
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ type RentalOrder struct {
|
|||||||
RenterID uint64 `gorm:"not null;index" json:"renter_id"`
|
RenterID uint64 `gorm:"not null;index" json:"renter_id"`
|
||||||
RentedAt *time.Time `json:"rented_at"`
|
RentedAt *time.Time `json:"rented_at"`
|
||||||
EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"`
|
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"`
|
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"`
|
||||||
|
OwnerRentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"owner_rent_amount"`
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_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"`
|
PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"platform_fee"`
|
||||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||||
Status string `gorm:"size:32;not null;default:'pending_confirm'" json:"status"`
|
Status string `gorm:"size:32;not null;default:'pending_payment'" json:"status"`
|
||||||
HandoffStatus string `gorm:"size:32;not null;default:'none'" json:"handoff_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"`
|
SettlementStatus string `gorm:"size:32;not null;default:'unsettled'" json:"settlement_status"`
|
||||||
OwnerSettledAt *time.Time `json:"owner_settled_at"`
|
OwnerSettledAt *time.Time `json:"owner_settled_at"`
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ type OrderCheckout struct {
|
|||||||
InitiatedBy uint64 `gorm:"not null" json:"initiated_by"`
|
InitiatedBy uint64 `gorm:"not null" json:"initiated_by"`
|
||||||
Status string `gorm:"size:32;not null;default:'submitted'" json:"status"`
|
Status string `gorm:"size:32;not null;default:'submitted'" json:"status"`
|
||||||
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"`
|
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"`
|
||||||
|
OwnerRentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"owner_rent_amount"`
|
||||||
|
PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"platform_fee"`
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
|
||||||
ConsumableAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"consumable_amount"`
|
ConsumableAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"consumable_amount"`
|
||||||
CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"`
|
CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"`
|
||||||
@@ -20,7 +22,7 @@ type OrderCheckout struct {
|
|||||||
RenterRefundAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"renter_refund_amount"`
|
RenterRefundAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"renter_refund_amount"`
|
||||||
OwnerIncomeAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"owner_income_amount"`
|
OwnerIncomeAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"owner_income_amount"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
|
EvidenceURLS datatypes.JSON `gorm:"column:evidence_urls" json:"evidence_urls"`
|
||||||
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
|
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
|
||||||
OwnerAdjustedAt *time.Time `json:"owner_adjusted_at"`
|
OwnerAdjustedAt *time.Time `json:"owner_adjusted_at"`
|
||||||
RenterConfirmedAt *time.Time `json:"renter_confirmed_at"`
|
RenterConfirmedAt *time.Time `json:"renter_confirmed_at"`
|
||||||
|
|||||||
@@ -303,6 +303,10 @@ type arbitrationSettlement struct {
|
|||||||
|
|
||||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (arbitrationSettlement, error) {
|
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (arbitrationSettlement, error) {
|
||||||
total := order.RentAmount + order.DepositAmount
|
total := order.RentAmount + order.DepositAmount
|
||||||
|
ownerRentAmount := order.OwnerRentAmount
|
||||||
|
if ownerRentAmount <= 0 || ownerRentAmount > order.RentAmount {
|
||||||
|
ownerRentAmount = order.RentAmount
|
||||||
|
}
|
||||||
settlement := arbitrationSettlement{}
|
settlement := arbitrationSettlement{}
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
@@ -359,9 +363,9 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
|||||||
return settlement, ErrInvalidDispute
|
return settlement, ErrInvalidDispute
|
||||||
}
|
}
|
||||||
addRenterRefund(req.Amount, "仲裁部分退款")
|
addRenterRefund(req.Amount, "仲裁部分退款")
|
||||||
addOwnerIncome(total-req.Amount, "仲裁剩余金额结算给号主")
|
addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+order.DepositAmount), "仲裁剩余金额结算给号主")
|
||||||
case "release_deposit":
|
case "release_deposit":
|
||||||
addOwnerIncome(order.RentAmount, "仲裁确认订单金额结算给号主")
|
addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主")
|
||||||
addRenterRefund(order.DepositAmount, "仲裁释放押金给租客")
|
addRenterRefund(order.DepositAmount, "仲裁释放押金给租客")
|
||||||
case "deduct_deposit", "compensate_owner":
|
case "deduct_deposit", "compensate_owner":
|
||||||
deductAmount := req.Amount
|
deductAmount := req.Amount
|
||||||
@@ -372,7 +376,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
|||||||
return settlement, ErrInvalidDispute
|
return settlement, ErrInvalidDispute
|
||||||
}
|
}
|
||||||
settlement.DepositDeductAmount = deductAmount
|
settlement.DepositDeductAmount = deductAmount
|
||||||
addOwnerIncome(order.RentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
|
addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
|
||||||
addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客")
|
addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客")
|
||||||
case "order_close":
|
case "order_close":
|
||||||
// Only release frozen funds. No available-balance settlement happens in development mode.
|
// Only release frozen funds. No available-balance settlement happens in development mode.
|
||||||
@@ -384,6 +388,13 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
|
|||||||
return settlement, nil
|
return settlement, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func minMoney(a float64, b float64) float64 {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) baseQuery() *gorm.DB {
|
func (r *Repository) baseQuery() *gorm.DB {
|
||||||
return r.db.Table("disputes AS d").
|
return r.db.Table("disputes AS d").
|
||||||
Select("d.*, o.order_no, a.title").
|
Select("d.*, o.order_no, a.title").
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ type ListingDTO struct {
|
|||||||
ScreenshotURLS []string `json:"screenshot_urls"`
|
ScreenshotURLS []string `json:"screenshot_urls"`
|
||||||
CoverURL string `json:"cover_url"`
|
CoverURL string `json:"cover_url"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
PriceHourly float64 `json:"price_hourly"`
|
|
||||||
PriceDaily float64 `json:"price_daily"`
|
|
||||||
PriceWeekly float64 `json:"price_weekly"`
|
|
||||||
DepositAmount float64 `json:"deposit_amount"`
|
DepositAmount float64 `json:"deposit_amount"`
|
||||||
IsAccelerated bool `json:"is_accelerated_sale"`
|
IsAccelerated bool `json:"is_accelerated_sale"`
|
||||||
InTransaction bool `json:"in_transaction"`
|
InTransaction bool `json:"in_transaction"`
|
||||||
@@ -43,9 +40,6 @@ type CreateRequest struct {
|
|||||||
AssetSummary map[string]any `json:"asset_summary"`
|
AssetSummary map[string]any `json:"asset_summary"`
|
||||||
ScreenshotURLS []string `json:"screenshot_urls"`
|
ScreenshotURLS []string `json:"screenshot_urls"`
|
||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
PriceHourly float64 `json:"price_hourly"`
|
|
||||||
PriceDaily float64 `json:"price_daily"`
|
|
||||||
PriceWeekly float64 `json:"price_weekly"`
|
|
||||||
DepositAmount float64 `json:"deposit_amount"`
|
DepositAmount float64 `json:"deposit_amount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -364,6 +364,8 @@ func writeListingError(c *gin.Context, err error) {
|
|||||||
response.BadRequest(c, "发布价格不正确")
|
response.BadRequest(c, "发布价格不正确")
|
||||||
case errors.Is(err, ErrInvalidDeposit):
|
case errors.Is(err, ErrInvalidDeposit):
|
||||||
response.BadRequest(c, "押金不能小于 0")
|
response.BadRequest(c, "押金不能小于 0")
|
||||||
|
case errors.Is(err, ErrDepositTooLow):
|
||||||
|
response.BadRequest(c, "押金必须大于额外消耗品总价值")
|
||||||
case errors.Is(err, ErrInvalidHafCoin):
|
case errors.Is(err, ErrInvalidHafCoin):
|
||||||
response.BadRequest(c, "哈夫币数量不正确")
|
response.BadRequest(c, "哈夫币数量不正确")
|
||||||
case errors.Is(err, ErrMissingScreenshot):
|
case errors.Is(err, ErrMissingScreenshot):
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo
|
|||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
OwnerID: ownerID,
|
OwnerID: ownerID,
|
||||||
Price: price,
|
Price: price,
|
||||||
PriceHourly: listingHourlyPrice(req, price),
|
|
||||||
PriceDaily: listingDailyPrice(req, price),
|
|
||||||
PriceWeekly: listingWeeklyPrice(req, price),
|
|
||||||
DepositAmount: req.DepositAmount,
|
DepositAmount: req.DepositAmount,
|
||||||
Status: listingStatus,
|
Status: listingStatus,
|
||||||
ReviewStatus: reviewStatus,
|
ReviewStatus: reviewStatus,
|
||||||
@@ -112,9 +109,6 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest,
|
|||||||
|
|
||||||
price := normalizedListingPrice(req)
|
price := normalizedListingPrice(req)
|
||||||
listing.Price = price
|
listing.Price = price
|
||||||
listing.PriceHourly = listingHourlyPrice(req, price)
|
|
||||||
listing.PriceDaily = listingDailyPrice(req, price)
|
|
||||||
listing.PriceWeekly = listingWeeklyPrice(req, price)
|
|
||||||
listing.DepositAmount = req.DepositAmount
|
listing.DepositAmount = req.DepositAmount
|
||||||
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
|
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
|
||||||
listing.Status = listingStatus
|
listing.Status = listingStatus
|
||||||
@@ -489,63 +483,7 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizedListingPrice(req CreateRequest) float64 {
|
func normalizedListingPrice(req CreateRequest) float64 {
|
||||||
if req.Price > 0 {
|
return req.Price
|
||||||
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 {
|
func publicListings(items []ListingDTO) []ListingDTO {
|
||||||
@@ -565,7 +503,6 @@ func applyPublicListingURLs(item *ListingDTO) {
|
|||||||
func (row listingRow) toDTO() ListingDTO {
|
func (row listingRow) toDTO() ListingDTO {
|
||||||
assetSummary := decodeAssetSummary(row.AssetSummary)
|
assetSummary := decodeAssetSummary(row.AssetSummary)
|
||||||
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
|
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
|
||||||
price, priceHourly, priceDaily, priceWeekly := listingDisplayPrices(row.Price, row.PriceHourly, row.PriceDaily, row.PriceWeekly)
|
|
||||||
return ListingDTO{
|
return ListingDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
AccountID: row.AccountID,
|
AccountID: row.AccountID,
|
||||||
@@ -582,10 +519,7 @@ func (row listingRow) toDTO() ListingDTO {
|
|||||||
AssetSummary: assetSummary,
|
AssetSummary: assetSummary,
|
||||||
ScreenshotURLS: screenshotURLS,
|
ScreenshotURLS: screenshotURLS,
|
||||||
CoverURL: publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus),
|
CoverURL: publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus),
|
||||||
Price: price,
|
Price: row.Price,
|
||||||
PriceHourly: priceHourly,
|
|
||||||
PriceDaily: priceDaily,
|
|
||||||
PriceWeekly: priceWeekly,
|
|
||||||
DepositAmount: row.DepositAmount,
|
DepositAmount: row.DepositAmount,
|
||||||
IsAccelerated: isAcceleratedSale(assetSummary),
|
IsAccelerated: isAcceleratedSale(assetSummary),
|
||||||
InTransaction: row.InTransaction,
|
InTransaction: row.InTransaction,
|
||||||
@@ -601,7 +535,6 @@ func (row listingRow) toDTO() ListingDTO {
|
|||||||
func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
||||||
assetSummary := decodeAssetSummary(account.AssetSummary)
|
assetSummary := decodeAssetSummary(account.AssetSummary)
|
||||||
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
|
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
|
||||||
price, priceHourly, priceDaily, priceWeekly := listingDisplayPrices(listing.Price, listing.PriceHourly, listing.PriceDaily, listing.PriceWeekly)
|
|
||||||
return &ListingDTO{
|
return &ListingDTO{
|
||||||
ID: listing.ID,
|
ID: listing.ID,
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
@@ -616,10 +549,7 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
|||||||
AssetSummary: assetSummary,
|
AssetSummary: assetSummary,
|
||||||
ScreenshotURLS: screenshotURLS,
|
ScreenshotURLS: screenshotURLS,
|
||||||
CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus),
|
CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus),
|
||||||
Price: price,
|
Price: listing.Price,
|
||||||
PriceHourly: priceHourly,
|
|
||||||
PriceDaily: priceDaily,
|
|
||||||
PriceWeekly: priceWeekly,
|
|
||||||
DepositAmount: listing.DepositAmount,
|
DepositAmount: listing.DepositAmount,
|
||||||
IsAccelerated: isAcceleratedSale(assetSummary),
|
IsAccelerated: isAcceleratedSale(assetSummary),
|
||||||
InTransaction: listing.InTransaction,
|
InTransaction: listing.InTransaction,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package listing
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -15,6 +17,7 @@ var (
|
|||||||
ErrMissingServerRegion = errors.New("missing server region")
|
ErrMissingServerRegion = errors.New("missing server region")
|
||||||
ErrInvalidPrice = errors.New("invalid listing price")
|
ErrInvalidPrice = errors.New("invalid listing price")
|
||||||
ErrInvalidDeposit = errors.New("invalid listing deposit")
|
ErrInvalidDeposit = errors.New("invalid listing deposit")
|
||||||
|
ErrDepositTooLow = errors.New("listing deposit too low")
|
||||||
ErrInvalidHafCoin = errors.New("invalid haf coin amount")
|
ErrInvalidHafCoin = errors.New("invalid haf coin amount")
|
||||||
ErrMissingScreenshot = errors.New("missing screenshot")
|
ErrMissingScreenshot = errors.New("missing screenshot")
|
||||||
)
|
)
|
||||||
@@ -34,6 +37,8 @@ const (
|
|||||||
defaultFireLevelMin = 38
|
defaultFireLevelMin = 38
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var priceNumberPattern = regexp.MustCompile(`(\d+(?:\.\d+)?)`)
|
||||||
|
|
||||||
type FireLevelTooLowError struct {
|
type FireLevelTooLowError struct {
|
||||||
Min int
|
Min int
|
||||||
}
|
}
|
||||||
@@ -217,6 +222,9 @@ func validateRequest(req CreateRequest, rules publishRules) error {
|
|||||||
if req.DepositAmount < 0 {
|
if req.DepositAmount < 0 {
|
||||||
return ErrInvalidDeposit
|
return ErrInvalidDeposit
|
||||||
}
|
}
|
||||||
|
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmount <= consumables {
|
||||||
|
return ErrDepositTooLow
|
||||||
|
}
|
||||||
if req.HafCoinAmount < 0 {
|
if req.HafCoinAmount < 0 {
|
||||||
return ErrInvalidHafCoin
|
return ErrInvalidHafCoin
|
||||||
}
|
}
|
||||||
@@ -229,6 +237,85 @@ func validateRequest(req CreateRequest, rules publishRules) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func consumableValue(summary map[string]any) float64 {
|
||||||
|
if summary == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
rawResources, ok := summary["resources"]
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
resources, ok := rawResources.([]any)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
total := 0.0
|
||||||
|
for _, raw := range resources {
|
||||||
|
resource, ok := raw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mode, _ := resource["mode"].(string)
|
||||||
|
if strings.TrimSpace(mode) != "收费" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
quantity := readSummaryFloat(resource["quantity"])
|
||||||
|
if quantity <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
priceText, _ := resource["price"].(string)
|
||||||
|
total += quantity * readUnitPrice(priceText)
|
||||||
|
}
|
||||||
|
return roundMoney(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSummaryFloat(value any) float64 {
|
||||||
|
switch current := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return current
|
||||||
|
case int:
|
||||||
|
return float64(current)
|
||||||
|
case int64:
|
||||||
|
return float64(current)
|
||||||
|
case json.Number:
|
||||||
|
parsed, err := current.Float64()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
case string:
|
||||||
|
parsed, err := strconv.ParseFloat(strings.TrimSpace(current), 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readUnitPrice(priceText string) float64 {
|
||||||
|
numbers := priceNumberPattern.FindAllString(priceText, -1)
|
||||||
|
if len(numbers) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
amount, err := strconv.ParseFloat(numbers[0], 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if len(numbers) >= 2 {
|
||||||
|
count, err := strconv.ParseFloat(numbers[1], 64)
|
||||||
|
if err == nil && count > 0 {
|
||||||
|
return amount / count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return amount
|
||||||
|
}
|
||||||
|
|
||||||
|
func roundMoney(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
func readFireLevel(summary map[string]any) (int, bool) {
|
func readFireLevel(summary map[string]any) (int, bool) {
|
||||||
if summary == nil {
|
if summary == nil {
|
||||||
return 0, false
|
return 0, false
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package listing
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
|
||||||
|
value := consumableValue(map[string]any{
|
||||||
|
"resources": []any{
|
||||||
|
map[string]any{"mode": "收费", "quantity": float64(5), "price": "0.1元/个"},
|
||||||
|
map[string]any{"mode": "赠送", "quantity": float64(3), "price": "0.6元/个"},
|
||||||
|
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/2个"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if value != 1.5 {
|
||||||
|
t.Fatalf("expected 1.5, got %.2f", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
|
||||||
|
req := CreateRequest{
|
||||||
|
Title: "测试账号",
|
||||||
|
ServerRegion: "烽火地带",
|
||||||
|
Price: 100,
|
||||||
|
DepositAmount: 1.5,
|
||||||
|
HafCoinAmount: 1000000,
|
||||||
|
ScreenshotURLS: []string{"https://example.com/a.png"},
|
||||||
|
AssetSummary: map[string]any{
|
||||||
|
"resources": []any{
|
||||||
|
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateRequest(req, publishRules{}); err != ErrDepositTooLow {
|
||||||
|
t.Fatalf("expected ErrDepositTooLow, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.DepositAmount = 2.01
|
||||||
|
if err := validateRequest(req, publishRules{}); err != nil {
|
||||||
|
t.Fatalf("expected valid request, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,9 +20,8 @@ type OrderDTO struct {
|
|||||||
LoginPlatform string `json:"login_platform"`
|
LoginPlatform string `json:"login_platform"`
|
||||||
RentedAt *time.Time `json:"rented_at"`
|
RentedAt *time.Time `json:"rented_at"`
|
||||||
EstimatedDurationHours int `json:"estimated_duration_hours"`
|
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"`
|
RentAmount float64 `json:"rent_amount"`
|
||||||
|
OwnerRentAmount float64 `json:"owner_rent_amount"`
|
||||||
DepositAmount float64 `json:"deposit_amount"`
|
DepositAmount float64 `json:"deposit_amount"`
|
||||||
PlatformFee float64 `json:"platform_fee"`
|
PlatformFee float64 `json:"platform_fee"`
|
||||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||||
@@ -90,6 +89,8 @@ type CheckoutDTO struct {
|
|||||||
InitiatedBy uint64 `json:"initiated_by"`
|
InitiatedBy uint64 `json:"initiated_by"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RentAmount float64 `json:"rent_amount"`
|
RentAmount float64 `json:"rent_amount"`
|
||||||
|
OwnerRentAmount float64 `json:"owner_rent_amount"`
|
||||||
|
PlatformFee float64 `json:"platform_fee"`
|
||||||
DepositAmount float64 `json:"deposit_amount"`
|
DepositAmount float64 `json:"deposit_amount"`
|
||||||
ConsumableAmount float64 `json:"consumable_amount"`
|
ConsumableAmount float64 `json:"consumable_amount"`
|
||||||
CoinConsumedM float64 `json:"coin_consumed_m"`
|
CoinConsumedM float64 `json:"coin_consumed_m"`
|
||||||
|
|||||||
@@ -28,31 +28,81 @@ func NewRepository(db *gorm.DB) *Repository {
|
|||||||
return &Repository{db: db}
|
return &Repository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
func listingOrderPrice(listing model.RentalListing) float64 {
|
type orderPricing struct {
|
||||||
switch {
|
RentAmount float64
|
||||||
case listing.Price > 0:
|
OwnerRentAmount float64
|
||||||
return listing.Price
|
PlatformFee float64
|
||||||
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 orderDurationHours(order model.RentalOrder) int {
|
func orderDurationHours(order model.RentalOrder) int {
|
||||||
if order.EstimatedDurationHours > 0 {
|
if order.EstimatedDurationHours > 0 {
|
||||||
return order.EstimatedDurationHours
|
return order.EstimatedDurationHours
|
||||||
}
|
}
|
||||||
if order.RentHours > 0 {
|
|
||||||
return order.RentHours
|
|
||||||
}
|
|
||||||
return internalOrderHours
|
return internalOrderHours
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
|
||||||
|
rentAmount := roundMoney(listing.Price)
|
||||||
|
ownerRentAmount := readSnapshotPrice(account.AssetSummary, "seller_total_price")
|
||||||
|
if ownerRentAmount <= 0 || ownerRentAmount > rentAmount {
|
||||||
|
ownerRentAmount = rentAmount
|
||||||
|
}
|
||||||
|
platformFee := readSnapshotPrice(account.AssetSummary, "platform_markup_amount")
|
||||||
|
if platformFee <= 0 || roundMoney(ownerRentAmount+platformFee) != rentAmount {
|
||||||
|
platformFee = roundMoney(rentAmount - ownerRentAmount)
|
||||||
|
}
|
||||||
|
if platformFee < 0 {
|
||||||
|
platformFee = 0
|
||||||
|
}
|
||||||
|
return orderPricing{
|
||||||
|
RentAmount: rentAmount,
|
||||||
|
OwnerRentAmount: roundMoney(ownerRentAmount),
|
||||||
|
PlatformFee: roundMoney(platformFee),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSnapshotPrice(raw datatypes.JSON, key string) float64 {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var summary map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &summary); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
breakdown, ok := summary["price_breakdown"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return readJSONNumber(breakdown[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func readJSONNumber(value any) float64 {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return typed
|
||||||
|
case float32:
|
||||||
|
return float64(typed)
|
||||||
|
case int:
|
||||||
|
return float64(typed)
|
||||||
|
case int64:
|
||||||
|
return float64(typed)
|
||||||
|
case json.Number:
|
||||||
|
number, err := typed.Float64()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return number
|
||||||
|
case string:
|
||||||
|
number, err := strconv.ParseFloat(typed, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return number
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) {
|
func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) {
|
||||||
var createdID uint64
|
var createdID uint64
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
@@ -80,6 +130,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rentHours := internalOrderHours
|
rentHours := internalOrderHours
|
||||||
|
pricing := buildOrderPricing(listing, account)
|
||||||
order := model.RentalOrder{
|
order := model.RentalOrder{
|
||||||
OrderNo: orderNo,
|
OrderNo: orderNo,
|
||||||
ListingID: listing.ID,
|
ListingID: listing.ID,
|
||||||
@@ -87,10 +138,10 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
|
|||||||
OwnerID: listing.OwnerID,
|
OwnerID: listing.OwnerID,
|
||||||
RenterID: renterID,
|
RenterID: renterID,
|
||||||
EstimatedDurationHours: rentHours,
|
EstimatedDurationHours: rentHours,
|
||||||
RentHours: rentHours,
|
RentAmount: pricing.RentAmount,
|
||||||
RentAmount: listingOrderPrice(listing),
|
OwnerRentAmount: pricing.OwnerRentAmount,
|
||||||
DepositAmount: listing.DepositAmount,
|
DepositAmount: listing.DepositAmount,
|
||||||
PlatformFee: 0,
|
PlatformFee: pricing.PlatformFee,
|
||||||
AccountSnapshot: snapshot,
|
AccountSnapshot: snapshot,
|
||||||
Status: "pending_payment",
|
Status: "pending_payment",
|
||||||
HandoffStatus: "none",
|
HandoffStatus: "none",
|
||||||
@@ -370,10 +421,7 @@ func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error {
|
|||||||
order.HandoffStatus = "received"
|
order.HandoffStatus = "received"
|
||||||
durationHours := orderDurationHours(order)
|
durationHours := orderDurationHours(order)
|
||||||
order.EstimatedDurationHours = durationHours
|
order.EstimatedDurationHours = durationHours
|
||||||
order.RentStartAt = &now
|
|
||||||
order.RentedAt = &now
|
order.RentedAt = &now
|
||||||
rentEnd := now.Add(time.Duration(durationHours) * time.Hour)
|
|
||||||
order.RentEndAt = &rentEnd
|
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
if err := notification.Append(tx, notification.Entry{
|
if err := notification.Append(tx, notification.Entry{
|
||||||
UserID: order.OwnerID,
|
UserID: order.OwnerID,
|
||||||
@@ -983,13 +1031,15 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
|
|||||||
InitiatedBy: initiatedBy,
|
InitiatedBy: initiatedBy,
|
||||||
Status: status,
|
Status: status,
|
||||||
RentAmount: order.RentAmount,
|
RentAmount: order.RentAmount,
|
||||||
|
OwnerRentAmount: order.OwnerRentAmount,
|
||||||
|
PlatformFee: order.PlatformFee,
|
||||||
DepositAmount: order.DepositAmount,
|
DepositAmount: order.DepositAmount,
|
||||||
ConsumableAmount: roundMoney(consumableAmount),
|
ConsumableAmount: roundMoney(consumableAmount),
|
||||||
CoinConsumedM: roundMoney(coinConsumedM),
|
CoinConsumedM: roundMoney(coinConsumedM),
|
||||||
OtherAmount: roundMoney(otherAmount),
|
OtherAmount: roundMoney(otherAmount),
|
||||||
DepositDeductAmount: roundMoney(deductAmount),
|
DepositDeductAmount: roundMoney(deductAmount),
|
||||||
RenterRefundAmount: roundMoney(renterRefund),
|
RenterRefundAmount: roundMoney(renterRefund),
|
||||||
OwnerIncomeAmount: roundMoney(order.RentAmount + deductAmount),
|
OwnerIncomeAmount: roundMoney(order.OwnerRentAmount + deductAmount),
|
||||||
Content: content,
|
Content: content,
|
||||||
EvidenceURLS: evidence,
|
EvidenceURLS: evidence,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -1068,15 +1118,7 @@ type orderRow struct {
|
|||||||
|
|
||||||
func (row orderRow) toDTO() OrderDTO {
|
func (row orderRow) toDTO() OrderDTO {
|
||||||
rentedAt := row.RentedAt
|
rentedAt := row.RentedAt
|
||||||
if rentedAt == nil {
|
|
||||||
rentedAt = row.RentStartAt
|
|
||||||
}
|
|
||||||
durationHours := orderDurationHours(row.RentalOrder)
|
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{
|
return OrderDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
OrderNo: row.OrderNo,
|
OrderNo: row.OrderNo,
|
||||||
@@ -1091,9 +1133,8 @@ func (row orderRow) toDTO() OrderDTO {
|
|||||||
LoginPlatform: row.LoginPlatform,
|
LoginPlatform: row.LoginPlatform,
|
||||||
RentedAt: rentedAt,
|
RentedAt: rentedAt,
|
||||||
EstimatedDurationHours: durationHours,
|
EstimatedDurationHours: durationHours,
|
||||||
RentStartAt: row.RentStartAt,
|
|
||||||
RentEndAt: rentEndAt,
|
|
||||||
RentAmount: row.RentAmount,
|
RentAmount: row.RentAmount,
|
||||||
|
OwnerRentAmount: row.OwnerRentAmount,
|
||||||
DepositAmount: row.DepositAmount,
|
DepositAmount: row.DepositAmount,
|
||||||
PlatformFee: row.PlatformFee,
|
PlatformFee: row.PlatformFee,
|
||||||
AccountSnapshot: row.AccountSnapshot,
|
AccountSnapshot: row.AccountSnapshot,
|
||||||
@@ -1126,6 +1167,8 @@ func toCheckoutDTO(checkout model.OrderCheckout) CheckoutDTO {
|
|||||||
InitiatedBy: checkout.InitiatedBy,
|
InitiatedBy: checkout.InitiatedBy,
|
||||||
Status: checkout.Status,
|
Status: checkout.Status,
|
||||||
RentAmount: checkout.RentAmount,
|
RentAmount: checkout.RentAmount,
|
||||||
|
OwnerRentAmount: checkout.OwnerRentAmount,
|
||||||
|
PlatformFee: checkout.PlatformFee,
|
||||||
DepositAmount: checkout.DepositAmount,
|
DepositAmount: checkout.DepositAmount,
|
||||||
ConsumableAmount: checkout.ConsumableAmount,
|
ConsumableAmount: checkout.ConsumableAmount,
|
||||||
CoinConsumedM: checkout.CoinConsumedM,
|
CoinConsumedM: checkout.CoinConsumedM,
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ CREATE TABLE rental_listings (
|
|||||||
account_id BIGINT UNSIGNED NOT NULL,
|
account_id BIGINT UNSIGNED NOT NULL,
|
||||||
owner_id BIGINT UNSIGNED NOT NULL,
|
owner_id BIGINT UNSIGNED NOT NULL,
|
||||||
price DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
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,
|
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
in_transaction TINYINT(1) NOT NULL DEFAULT 0,
|
in_transaction TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
status VARCHAR(32) NOT NULL DEFAULT 'draft',
|
||||||
@@ -67,8 +64,7 @@ CREATE TABLE rental_listings (
|
|||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
KEY idx_rental_listings_account_id (account_id),
|
KEY idx_rental_listings_account_id (account_id),
|
||||||
KEY idx_rental_listings_owner_id (owner_id),
|
KEY idx_rental_listings_owner_id (owner_id),
|
||||||
KEY idx_rental_listings_filter (status, review_status, price),
|
KEY idx_rental_listings_filter (status, review_status, in_transaction, price)
|
||||||
KEY idx_rental_listings_legacy_filter (status, review_status, price_hourly)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE rental_orders (
|
CREATE TABLE rental_orders (
|
||||||
@@ -80,14 +76,12 @@ CREATE TABLE rental_orders (
|
|||||||
renter_id BIGINT UNSIGNED NOT NULL,
|
renter_id BIGINT UNSIGNED NOT NULL,
|
||||||
rented_at DATETIME NULL,
|
rented_at DATETIME NULL,
|
||||||
estimated_duration_hours INT NOT NULL DEFAULT 24,
|
estimated_duration_hours INT NOT NULL DEFAULT 24,
|
||||||
rent_start_at DATETIME NULL,
|
|
||||||
rent_end_at DATETIME NULL,
|
|
||||||
rent_hours INT NOT NULL,
|
|
||||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
account_snapshot JSON NULL,
|
account_snapshot JSON NULL,
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'pending_confirm',
|
status VARCHAR(32) NOT NULL DEFAULT 'pending_payment',
|
||||||
handoff_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
handoff_status VARCHAR(32) NOT NULL DEFAULT 'none',
|
||||||
settlement_status VARCHAR(32) NOT NULL DEFAULT 'unsettled',
|
settlement_status VARCHAR(32) NOT NULL DEFAULT 'unsettled',
|
||||||
owner_settled_at DATETIME NULL,
|
owner_settled_at DATETIME NULL,
|
||||||
@@ -101,6 +95,33 @@ CREATE TABLE rental_orders (
|
|||||||
KEY idx_rental_orders_rented_at (status, rented_at)
|
KEY idx_rental_orders_rented_at (status, rented_at)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE order_checkouts (
|
||||||
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
initiated_by BIGINT UNSIGNED NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'submitted',
|
||||||
|
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||||
|
content TEXT NULL,
|
||||||
|
evidence_urls JSON NULL,
|
||||||
|
owner_adjustment_reason TEXT NULL,
|
||||||
|
owner_adjusted_at DATETIME NULL,
|
||||||
|
renter_confirmed_at DATETIME NULL,
|
||||||
|
renter_rejected_at DATETIME NULL,
|
||||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
KEY idx_order_checkouts_order_id (order_id),
|
||||||
|
KEY idx_order_checkouts_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
CREATE TABLE handoff_records (
|
CREATE TABLE handoff_records (
|
||||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
order_id BIGINT UNSIGNED NOT NULL,
|
order_id BIGINT UNSIGNED NOT NULL,
|
||||||
@@ -141,6 +162,7 @@ CREATE TABLE wallet_ledger (
|
|||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
UNIQUE KEY uk_wallet_ledger_ledger_no (ledger_no),
|
UNIQUE KEY uk_wallet_ledger_ledger_no (ledger_no),
|
||||||
KEY idx_wallet_ledger_user_created (user_id, created_at),
|
KEY idx_wallet_ledger_user_created (user_id, created_at),
|
||||||
|
KEY idx_wallet_ledger_order_id (order_id),
|
||||||
KEY idx_wallet_ledger_biz (biz_type, biz_no)
|
KEY idx_wallet_ledger_biz (biz_type, biz_no)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
SET @listing_in_transaction_exists := (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE table_schema = DATABASE()
|
|
||||||
AND table_name = 'rental_listings'
|
|
||||||
AND column_name = 'in_transaction'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @add_listing_in_transaction_sql := IF(
|
|
||||||
@listing_in_transaction_exists = 0,
|
|
||||||
'ALTER TABLE rental_listings ADD COLUMN in_transaction TINYINT(1) NOT NULL DEFAULT 0 AFTER deposit_amount',
|
|
||||||
'SELECT 1'
|
|
||||||
);
|
|
||||||
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;
|
|
||||||
|
|
||||||
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`);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS order_checkouts (
|
|
||||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
|
||||||
order_id BIGINT UNSIGNED NOT NULL,
|
|
||||||
initiated_by BIGINT UNSIGNED NOT NULL,
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'submitted',
|
|
||||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
|
||||||
content TEXT NULL,
|
|
||||||
evidence_urls JSON NULL,
|
|
||||||
owner_adjustment_reason TEXT NULL,
|
|
||||||
owner_adjusted_at DATETIME NULL,
|
|
||||||
renter_confirmed_at DATETIME NULL,
|
|
||||||
renter_rejected_at DATETIME NULL,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
KEY idx_order_checkouts_order_id (order_id),
|
|
||||||
KEY idx_order_checkouts_status (status)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
@@ -17,9 +17,6 @@ export interface Listing {
|
|||||||
screenshot_urls: string[]
|
screenshot_urls: string[]
|
||||||
cover_url: string
|
cover_url: string
|
||||||
price: number
|
price: number
|
||||||
price_hourly: number
|
|
||||||
price_daily: number
|
|
||||||
price_weekly: number
|
|
||||||
deposit_amount: number
|
deposit_amount: number
|
||||||
is_accelerated_sale?: boolean
|
is_accelerated_sale?: boolean
|
||||||
in_transaction: boolean
|
in_transaction: boolean
|
||||||
@@ -41,9 +38,6 @@ export interface ListingPayload {
|
|||||||
asset_summary?: Record<string, unknown>
|
asset_summary?: Record<string, unknown>
|
||||||
screenshot_urls: string[]
|
screenshot_urls: string[]
|
||||||
price: number
|
price: number
|
||||||
price_hourly: number
|
|
||||||
price_daily: number
|
|
||||||
price_weekly: number
|
|
||||||
deposit_amount: number
|
deposit_amount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ export interface Order {
|
|||||||
login_platform: string
|
login_platform: string
|
||||||
rented_at?: string
|
rented_at?: string
|
||||||
estimated_duration_hours: number
|
estimated_duration_hours: number
|
||||||
rent_start_at?: string
|
|
||||||
rent_end_at?: string
|
|
||||||
rent_amount: number
|
rent_amount: number
|
||||||
|
owner_rent_amount: number
|
||||||
deposit_amount: number
|
deposit_amount: number
|
||||||
platform_fee: number
|
platform_fee: number
|
||||||
account_snapshot?: Record<string, unknown>
|
account_snapshot?: Record<string, unknown>
|
||||||
@@ -34,6 +33,8 @@ export interface Checkout {
|
|||||||
initiated_by: number
|
initiated_by: number
|
||||||
status: string
|
status: string
|
||||||
rent_amount: number
|
rent_amount: number
|
||||||
|
owner_rent_amount: number
|
||||||
|
platform_fee: number
|
||||||
deposit_amount: number
|
deposit_amount: number
|
||||||
consumable_amount: number
|
consumable_amount: number
|
||||||
coin_consumed_m: number
|
coin_consumed_m: number
|
||||||
|
|||||||
@@ -308,12 +308,11 @@ function readError(error: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function orderRentedAt() {
|
function orderRentedAt() {
|
||||||
return order.value?.rented_at || order.value?.rent_start_at
|
return order.value?.rented_at
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderEstimatedEndAt() {
|
function orderEstimatedEndAt() {
|
||||||
if (!order.value) return undefined
|
if (!order.value) return undefined
|
||||||
if (order.value.rent_end_at) return order.value.rent_end_at
|
|
||||||
const rentedAt = orderRentedAt()
|
const rentedAt = orderRentedAt()
|
||||||
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
||||||
if (!rentedAt || durationHours <= 0) return undefined
|
if (!rentedAt || durationHours <= 0) return undefined
|
||||||
@@ -466,6 +465,10 @@ function linesToList(value: string) {
|
|||||||
<span>订单金额</span>
|
<span>订单金额</span>
|
||||||
<strong>¥{{ order.rent_amount }}</strong>
|
<strong>¥{{ order.rent_amount }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span>平台费用</span>
|
||||||
|
<strong>¥{{ order.platform_fee }}</strong>
|
||||||
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>押金</span>
|
<span>押金</span>
|
||||||
<strong>¥{{ order.deposit_amount }}</strong>
|
<strong>¥{{ order.deposit_amount }}</strong>
|
||||||
@@ -554,7 +557,8 @@ function linesToList(value: string) {
|
|||||||
|
|
||||||
<div v-if="order && order.checkout && ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.status)" class="order-panel">
|
<div v-if="order && order.checkout && ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.status)" class="order-panel">
|
||||||
<h2>结账明细</h2>
|
<h2>结账明细</h2>
|
||||||
<p>租金:¥{{ order.checkout.rent_amount }},押金:¥{{ order.checkout.deposit_amount }}</p>
|
<p>买家租金:¥{{ order.checkout.rent_amount }},号主租金:¥{{ order.checkout.owner_rent_amount }},平台费用:¥{{ order.checkout.platform_fee }}</p>
|
||||||
|
<p>押金:¥{{ order.checkout.deposit_amount }}</p>
|
||||||
<p>扣除:¥{{ order.checkout.deposit_deduct_amount }},退还租客:¥{{ order.checkout.renter_refund_amount }},号主收入:¥{{ order.checkout.owner_income_amount }}</p>
|
<p>扣除:¥{{ order.checkout.deposit_deduct_amount }},退还租客:¥{{ order.checkout.renter_refund_amount }},号主收入:¥{{ order.checkout.owner_income_amount }}</p>
|
||||||
<p v-if="order.checkout.content">说明:{{ order.checkout.content }}</p>
|
<p v-if="order.checkout.content">说明:{{ order.checkout.content }}</p>
|
||||||
<p v-if="order.checkout.owner_adjustment_reason">修正原因:{{ order.checkout.owner_adjustment_reason }}</p>
|
<p v-if="order.checkout.owner_adjustment_reason">修正原因:{{ order.checkout.owner_adjustment_reason }}</p>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function money(value: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listingPrice(row: Listing) {
|
function listingPrice(row: Listing) {
|
||||||
return money(row.price || row.price_daily || row.price_hourly)
|
return money(row.price)
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractObjectKey(url: string) {
|
function extractObjectKey(url: string) {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function openEvidence(row: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listingPrice(row: Listing) {
|
function listingPrice(row: Listing) {
|
||||||
return `¥${Number(row.price || row.price_daily || row.price_hourly || 0).toFixed(2)}`
|
return `¥${Number(row.price || 0).toFixed(2)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractObjectKey(url: string) {
|
function extractObjectKey(url: string) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function money(value: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listingPrice(row: Listing) {
|
function listingPrice(row: Listing) {
|
||||||
return money(row.price || row.price_daily || row.price_hourly)
|
return money(row.price)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ownerName(row: Listing) {
|
function ownerName(row: Listing) {
|
||||||
|
|||||||
@@ -65,12 +65,11 @@ function readError(error: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function orderRentedAt() {
|
function orderRentedAt() {
|
||||||
return order.value?.rented_at || order.value?.rent_start_at
|
return order.value?.rented_at
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderEstimatedEndAt() {
|
function orderEstimatedEndAt() {
|
||||||
if (!order.value) return undefined
|
if (!order.value) return undefined
|
||||||
if (order.value.rent_end_at) return order.value.rent_end_at
|
|
||||||
const rentedAt = orderRentedAt()
|
const rentedAt = orderRentedAt()
|
||||||
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
const durationHours = Number(order.value.estimated_duration_hours || 0)
|
||||||
if (!rentedAt || durationHours <= 0) return undefined
|
if (!rentedAt || durationHours <= 0) return undefined
|
||||||
@@ -108,6 +107,10 @@ function orderEstimatedEndAt() {
|
|||||||
<span>订单金额</span>
|
<span>订单金额</span>
|
||||||
<strong>¥{{ order.rent_amount }}</strong>
|
<strong>¥{{ order.rent_amount }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-card">
|
||||||
|
<span>平台费用</span>
|
||||||
|
<strong>¥{{ order.platform_fee }}</strong>
|
||||||
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>押金</span>
|
<span>押金</span>
|
||||||
<strong>¥{{ order.deposit_amount }}</strong>
|
<strong>¥{{ order.deposit_amount }}</strong>
|
||||||
|
|||||||
@@ -428,7 +428,6 @@ async function handleSubmit() {
|
|||||||
try {
|
try {
|
||||||
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}M哈夫币`;
|
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}M哈夫币`;
|
||||||
const dailyPrice = calculatedFinalPrice.value;
|
const dailyPrice = calculatedFinalPrice.value;
|
||||||
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
|
|
||||||
|
|
||||||
const listing = await createListing({
|
const listing = await createListing({
|
||||||
title,
|
title,
|
||||||
@@ -440,9 +439,6 @@ async function handleSubmit() {
|
|||||||
asset_summary: buildAssetSummary(),
|
asset_summary: buildAssetSummary(),
|
||||||
screenshot_urls: screenshotUrls.value,
|
screenshot_urls: screenshotUrls.value,
|
||||||
price: dailyPrice,
|
price: dailyPrice,
|
||||||
price_hourly: hourlyPrice,
|
|
||||||
price_daily: dailyPrice,
|
|
||||||
price_weekly: roundMoney(dailyPrice * 7),
|
|
||||||
deposit_amount: Number(form.deposit_amount),
|
deposit_amount: Number(form.deposit_amount),
|
||||||
});
|
});
|
||||||
localStorage.removeItem(draftKey);
|
localStorage.removeItem(draftKey);
|
||||||
@@ -493,6 +489,9 @@ function validateForm() {
|
|||||||
if (!Number.isFinite(Number(form.deposit_amount))) {
|
if (!Number.isFinite(Number(form.deposit_amount))) {
|
||||||
return "押金格式不正确";
|
return "押金格式不正确";
|
||||||
}
|
}
|
||||||
|
if (calculatedConsumablePrice.value > 0 && Number(form.deposit_amount) <= calculatedConsumablePrice.value) {
|
||||||
|
return `押金必须大于额外消耗品总价值 ¥${calculatedConsumablePrice.value}`;
|
||||||
|
}
|
||||||
if (!calculatedFinalPrice.value) {
|
if (!calculatedFinalPrice.value) {
|
||||||
return "请完善币数、保险、体力和负重后再发布";
|
return "请完善币数、保险、体力和负重后再发布";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function formatHafCoinM(amountWan: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getListingDisplayPrice(item: Listing) {
|
export function getListingDisplayPrice(item: Listing) {
|
||||||
return Number(item.price_daily || item.price_hourly || 0);
|
return Number(item.price || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRatioValue(item: Listing) {
|
export function getRatioValue(item: Listing) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function readError(error: unknown, fallback: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function listingPrice(item: Listing) {
|
function listingPrice(item: Listing) {
|
||||||
return Number(item.price || item.price_daily || item.price_hourly || 0).toFixed(2);
|
return Number(item.price || 0).toFixed(2);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ async function handleSubmit() {
|
|||||||
const listing = await createListing({
|
const listing = await createListing({
|
||||||
...form,
|
...form,
|
||||||
price,
|
price,
|
||||||
price_hourly: Math.max(Math.round((price / 24) * 100) / 100, 0.01),
|
|
||||||
price_daily: price,
|
|
||||||
price_weekly: Math.round(price * 7 * 100) / 100,
|
|
||||||
screenshot_urls: screenshotUrls.value,
|
screenshot_urls: screenshotUrls.value,
|
||||||
})
|
})
|
||||||
ElMessage.success(
|
ElMessage.success(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function readSellerPrice(row: Listing) {
|
|||||||
const price = Number((breakdown as Record<string, unknown>).seller_total_price)
|
const price = Number((breakdown as Record<string, unknown>).seller_total_price)
|
||||||
if (Number.isFinite(price) && price > 0) return price
|
if (Number.isFinite(price) && price > 0) return price
|
||||||
}
|
}
|
||||||
return Number(row.price || row.price_daily || row.price_hourly || 0)
|
return Number(row.price || 0)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -57,6 +57,12 @@ wait_container_healthy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init_database() {
|
init_database() {
|
||||||
|
if [[ "${DEV_RESET_DB:-0}" == "1" ]]; then
|
||||||
|
log "DEV_RESET_DB=1,重建开发数据库..."
|
||||||
|
docker exec hfb-mysql mysql -uroot -prootsecret -e \
|
||||||
|
"DROP DATABASE IF EXISTS hfb_sys; CREATE DATABASE hfb_sys CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||||
|
fi
|
||||||
|
|
||||||
local table_count
|
local table_count
|
||||||
table_count="$(
|
table_count="$(
|
||||||
docker exec hfb-mysql mysql -uhfb -psecret -N -s -e \
|
docker exec hfb-mysql mysql -uhfb -psecret -N -s -e \
|
||||||
@@ -69,11 +75,6 @@ init_database() {
|
|||||||
else
|
else
|
||||||
log "数据库结构已存在,跳过迁移"
|
log "数据库结构已存在,跳过迁移"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -f "${ROOT_DIR}/backend/migrations/000002_order_payment_stepwise.sql" ]]; then
|
|
||||||
log "应用订单支付增量迁移..."
|
|
||||||
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "${ROOT_DIR}/backend/migrations/000002_order_payment_stepwise.sql"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load_env_file() {
|
load_env_file() {
|
||||||
|
|||||||
Reference in New Issue
Block a user