彻底优化数据库字段

This commit is contained in:
yml
2026-05-24 20:49:37 +08:00
parent 80e01dc6d9
commit 5f6a4afcbb
28 changed files with 295 additions and 302 deletions
+1 -4
View File
@@ -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
+1 -1
View File
@@ -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"`
-3
View File
@@ -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"`
+2 -4
View File
@@ -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"`
+3 -1
View File
@@ -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"`
+14 -3
View File
@@ -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").
-6
View File
@@ -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):
+2 -72
View File
@@ -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,64 +483,8 @@ 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 {
for index := range items { for index := range items {
@@ -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)
}
}
+3 -2
View File
@@ -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"`
+76 -33
View File
@@ -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,
+31 -9
View File
@@ -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;
-6
View File
@@ -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
} }
+3 -2
View File
@@ -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 "请完善币数、保险、体力和负重后再发布";
} }
+1 -1
View File
@@ -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
View File
@@ -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() {