彻底优化数据库字段

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
overdueBefore := now.Add(-time.Duration(cfg.ReturnOverdueGraceMinutes) * time.Minute)
err := j.db.WithContext(ctx).
Where(`status = ? AND (
(rented_at IS NOT NULL AND TIMESTAMPADD(HOUR, COALESCE(NULLIF(estimated_duration_hours, 0), 24), rented_at) <= ?)
OR (rented_at IS NULL AND rent_end_at IS NOT NULL AND rent_end_at <= ?)
)`, "renting", overdueBefore, overdueBefore).
Where(`status = ? AND rented_at IS NOT NULL AND TIMESTAMPADD(HOUR, COALESCE(NULLIF(estimated_duration_hours, 0), 24), rented_at) <= ?`, "renting", overdueBefore).
Order("id ASC").
Limit(100).
Find(&rows).Error
+1 -1
View File
@@ -14,7 +14,7 @@ type Dispute struct {
Type string `gorm:"size:32;not null" json:"type"`
Status string `gorm:"size:32;not null;default:'open'" json:"status"`
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"`
ArbitrationRemark string `json:"arbitration_remark"`
HandledBy *uint64 `json:"handled_by"`
-3
View File
@@ -33,9 +33,6 @@ type RentalListing struct {
AccountID uint64 `gorm:"not null;index" json:"account_id"`
OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
Price float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price"`
PriceHourly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_hourly"`
PriceDaily float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_daily"`
PriceWeekly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_weekly"`
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
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"`
RentedAt *time.Time `json:"rented_at"`
EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"`
RentStartAt *time.Time `json:"rent_start_at"`
RentEndAt *time.Time `json:"rent_end_at"`
RentHours int `gorm:"not null" json:"rent_hours"`
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"rent_amount"`
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"`
PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"platform_fee"`
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
Status string `gorm:"size:32;not null;default:'pending_confirm'" json:"status"`
Status string `gorm:"size:32;not null;default:'pending_payment'" json:"status"`
HandoffStatus string `gorm:"size:32;not null;default:'none'" json:"handoff_status"`
SettlementStatus string `gorm:"size:32;not null;default:'unsettled'" json:"settlement_status"`
OwnerSettledAt *time.Time `json:"owner_settled_at"`
+3 -1
View File
@@ -12,6 +12,8 @@ type OrderCheckout struct {
InitiatedBy uint64 `gorm:"not null" json:"initiated_by"`
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"`
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"`
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"`
@@ -20,7 +22,7 @@ type OrderCheckout struct {
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"`
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"`
OwnerAdjustedAt *time.Time `json:"owner_adjusted_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) {
total := order.RentAmount + order.DepositAmount
ownerRentAmount := order.OwnerRentAmount
if ownerRentAmount <= 0 || ownerRentAmount > order.RentAmount {
ownerRentAmount = order.RentAmount
}
settlement := arbitrationSettlement{}
orderID := order.ID
if total > 0 {
@@ -359,9 +363,9 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
return settlement, ErrInvalidDispute
}
addRenterRefund(req.Amount, "仲裁部分退款")
addOwnerIncome(total-req.Amount, "仲裁剩余金额结算给号主")
addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+order.DepositAmount), "仲裁剩余金额结算给号主")
case "release_deposit":
addOwnerIncome(order.RentAmount, "仲裁确认订单金额结算给号主")
addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主")
addRenterRefund(order.DepositAmount, "仲裁释放押金给租客")
case "deduct_deposit", "compensate_owner":
deductAmount := req.Amount
@@ -372,7 +376,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
return settlement, ErrInvalidDispute
}
settlement.DepositDeductAmount = deductAmount
addOwnerIncome(order.RentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客")
case "order_close":
// 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
}
func minMoney(a float64, b float64) float64 {
if a < b {
return a
}
return b
}
func (r *Repository) baseQuery() *gorm.DB {
return r.db.Table("disputes AS d").
Select("d.*, o.order_no, a.title").
-6
View File
@@ -19,9 +19,6 @@ type ListingDTO struct {
ScreenshotURLS []string `json:"screenshot_urls"`
CoverURL string `json:"cover_url"`
Price float64 `json:"price"`
PriceHourly float64 `json:"price_hourly"`
PriceDaily float64 `json:"price_daily"`
PriceWeekly float64 `json:"price_weekly"`
DepositAmount float64 `json:"deposit_amount"`
IsAccelerated bool `json:"is_accelerated_sale"`
InTransaction bool `json:"in_transaction"`
@@ -43,9 +40,6 @@ type CreateRequest struct {
AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"`
Price float64 `json:"price"`
PriceHourly float64 `json:"price_hourly"`
PriceDaily float64 `json:"price_daily"`
PriceWeekly float64 `json:"price_weekly"`
DepositAmount float64 `json:"deposit_amount"`
}
@@ -364,6 +364,8 @@ func writeListingError(c *gin.Context, err error) {
response.BadRequest(c, "发布价格不正确")
case errors.Is(err, ErrInvalidDeposit):
response.BadRequest(c, "押金不能小于 0")
case errors.Is(err, ErrDepositTooLow):
response.BadRequest(c, "押金必须大于额外消耗品总价值")
case errors.Is(err, ErrInvalidHafCoin):
response.BadRequest(c, "哈夫币数量不正确")
case errors.Is(err, ErrMissingScreenshot):
+3 -73
View File
@@ -65,9 +65,6 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo
AccountID: account.ID,
OwnerID: ownerID,
Price: price,
PriceHourly: listingHourlyPrice(req, price),
PriceDaily: listingDailyPrice(req, price),
PriceWeekly: listingWeeklyPrice(req, price),
DepositAmount: req.DepositAmount,
Status: listingStatus,
ReviewStatus: reviewStatus,
@@ -112,9 +109,6 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest,
price := normalizedListingPrice(req)
listing.Price = price
listing.PriceHourly = listingHourlyPrice(req, price)
listing.PriceDaily = listingDailyPrice(req, price)
listing.PriceWeekly = listingWeeklyPrice(req, price)
listing.DepositAmount = req.DepositAmount
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus
@@ -489,63 +483,7 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
}
func normalizedListingPrice(req CreateRequest) float64 {
if req.Price > 0 {
return req.Price
}
if req.PriceDaily > 0 {
return req.PriceDaily
}
if req.PriceHourly > 0 {
return req.PriceHourly * 24
}
if req.PriceWeekly > 0 {
return req.PriceWeekly / 7
}
return 0
}
func listingHourlyPrice(req CreateRequest, price float64) float64 {
if req.PriceHourly > 0 {
return req.PriceHourly
}
return price / 24
}
func listingDailyPrice(req CreateRequest, price float64) float64 {
if req.PriceDaily > 0 {
return req.PriceDaily
}
return price
}
func listingWeeklyPrice(req CreateRequest, price float64) float64 {
if req.PriceWeekly > 0 {
return req.PriceWeekly
}
return price * 7
}
func listingDisplayPrices(price float64, hourly float64, daily float64, weekly float64) (float64, float64, float64, float64) {
if price <= 0 {
switch {
case daily > 0:
price = daily
case hourly > 0:
price = hourly * 24
case weekly > 0:
price = weekly / 7
}
}
if daily <= 0 {
daily = price
}
if hourly <= 0 && price > 0 {
hourly = price / 24
}
if weekly <= 0 && price > 0 {
weekly = price * 7
}
return price, hourly, daily, weekly
return req.Price
}
func publicListings(items []ListingDTO) []ListingDTO {
@@ -565,7 +503,6 @@ func applyPublicListingURLs(item *ListingDTO) {
func (row listingRow) toDTO() ListingDTO {
assetSummary := decodeAssetSummary(row.AssetSummary)
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS))
price, priceHourly, priceDaily, priceWeekly := listingDisplayPrices(row.Price, row.PriceHourly, row.PriceDaily, row.PriceWeekly)
return ListingDTO{
ID: row.ID,
AccountID: row.AccountID,
@@ -582,10 +519,7 @@ func (row listingRow) toDTO() ListingDTO {
AssetSummary: assetSummary,
ScreenshotURLS: screenshotURLS,
CoverURL: publicCoverURL(row.ID, screenshotURLS, row.Status, row.ReviewStatus),
Price: price,
PriceHourly: priceHourly,
PriceDaily: priceDaily,
PriceWeekly: priceWeekly,
Price: row.Price,
DepositAmount: row.DepositAmount,
IsAccelerated: isAcceleratedSale(assetSummary),
InTransaction: row.InTransaction,
@@ -601,7 +535,6 @@ func (row listingRow) toDTO() ListingDTO {
func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
assetSummary := decodeAssetSummary(account.AssetSummary)
screenshotURLS := cleanScreenshotURLs(decodeScreenshots(account.ScreenshotURLS))
price, priceHourly, priceDaily, priceWeekly := listingDisplayPrices(listing.Price, listing.PriceHourly, listing.PriceDaily, listing.PriceWeekly)
return &ListingDTO{
ID: listing.ID,
AccountID: account.ID,
@@ -616,10 +549,7 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
AssetSummary: assetSummary,
ScreenshotURLS: screenshotURLS,
CoverURL: publicCoverURL(listing.ID, screenshotURLS, listing.Status, listing.ReviewStatus),
Price: price,
PriceHourly: priceHourly,
PriceDaily: priceDaily,
PriceWeekly: priceWeekly,
Price: listing.Price,
DepositAmount: listing.DepositAmount,
IsAccelerated: isAcceleratedSale(assetSummary),
InTransaction: listing.InTransaction,
@@ -3,6 +3,8 @@ package listing
import (
"encoding/json"
"errors"
"math"
"regexp"
"strconv"
"strings"
)
@@ -15,6 +17,7 @@ var (
ErrMissingServerRegion = errors.New("missing server region")
ErrInvalidPrice = errors.New("invalid listing price")
ErrInvalidDeposit = errors.New("invalid listing deposit")
ErrDepositTooLow = errors.New("listing deposit too low")
ErrInvalidHafCoin = errors.New("invalid haf coin amount")
ErrMissingScreenshot = errors.New("missing screenshot")
)
@@ -34,6 +37,8 @@ const (
defaultFireLevelMin = 38
)
var priceNumberPattern = regexp.MustCompile(`(\d+(?:\.\d+)?)`)
type FireLevelTooLowError struct {
Min int
}
@@ -217,6 +222,9 @@ func validateRequest(req CreateRequest, rules publishRules) error {
if req.DepositAmount < 0 {
return ErrInvalidDeposit
}
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmount <= consumables {
return ErrDepositTooLow
}
if req.HafCoinAmount < 0 {
return ErrInvalidHafCoin
}
@@ -229,6 +237,85 @@ func validateRequest(req CreateRequest, rules publishRules) error {
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) {
if summary == nil {
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"`
RentedAt *time.Time `json:"rented_at"`
EstimatedDurationHours int `json:"estimated_duration_hours"`
RentStartAt *time.Time `json:"rent_start_at"`
RentEndAt *time.Time `json:"rent_end_at"`
RentAmount float64 `json:"rent_amount"`
OwnerRentAmount float64 `json:"owner_rent_amount"`
DepositAmount float64 `json:"deposit_amount"`
PlatformFee float64 `json:"platform_fee"`
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
@@ -90,6 +89,8 @@ type CheckoutDTO struct {
InitiatedBy uint64 `json:"initiated_by"`
Status string `json:"status"`
RentAmount float64 `json:"rent_amount"`
OwnerRentAmount float64 `json:"owner_rent_amount"`
PlatformFee float64 `json:"platform_fee"`
DepositAmount float64 `json:"deposit_amount"`
ConsumableAmount float64 `json:"consumable_amount"`
CoinConsumedM float64 `json:"coin_consumed_m"`
+76 -33
View File
@@ -28,31 +28,81 @@ func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func listingOrderPrice(listing model.RentalListing) float64 {
switch {
case listing.Price > 0:
return listing.Price
case listing.PriceDaily > 0:
return listing.PriceDaily
case listing.PriceHourly > 0:
return listing.PriceHourly * float64(internalOrderHours)
case listing.PriceWeekly > 0:
return listing.PriceWeekly / 7
default:
return 0
}
type orderPricing struct {
RentAmount float64
OwnerRentAmount float64
PlatformFee float64
}
func orderDurationHours(order model.RentalOrder) int {
if order.EstimatedDurationHours > 0 {
return order.EstimatedDurationHours
}
if order.RentHours > 0 {
return order.RentHours
}
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) {
var createdID uint64
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
}
rentHours := internalOrderHours
pricing := buildOrderPricing(listing, account)
order := model.RentalOrder{
OrderNo: orderNo,
ListingID: listing.ID,
@@ -87,10 +138,10 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
OwnerID: listing.OwnerID,
RenterID: renterID,
EstimatedDurationHours: rentHours,
RentHours: rentHours,
RentAmount: listingOrderPrice(listing),
RentAmount: pricing.RentAmount,
OwnerRentAmount: pricing.OwnerRentAmount,
DepositAmount: listing.DepositAmount,
PlatformFee: 0,
PlatformFee: pricing.PlatformFee,
AccountSnapshot: snapshot,
Status: "pending_payment",
HandoffStatus: "none",
@@ -370,10 +421,7 @@ func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error {
order.HandoffStatus = "received"
durationHours := orderDurationHours(order)
order.EstimatedDurationHours = durationHours
order.RentStartAt = &now
order.RentedAt = &now
rentEnd := now.Add(time.Duration(durationHours) * time.Hour)
order.RentEndAt = &rentEnd
orderID := order.ID
if err := notification.Append(tx, notification.Entry{
UserID: order.OwnerID,
@@ -983,13 +1031,15 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
InitiatedBy: initiatedBy,
Status: status,
RentAmount: order.RentAmount,
OwnerRentAmount: order.OwnerRentAmount,
PlatformFee: order.PlatformFee,
DepositAmount: order.DepositAmount,
ConsumableAmount: roundMoney(consumableAmount),
CoinConsumedM: roundMoney(coinConsumedM),
OtherAmount: roundMoney(otherAmount),
DepositDeductAmount: roundMoney(deductAmount),
RenterRefundAmount: roundMoney(renterRefund),
OwnerIncomeAmount: roundMoney(order.RentAmount + deductAmount),
OwnerIncomeAmount: roundMoney(order.OwnerRentAmount + deductAmount),
Content: content,
EvidenceURLS: evidence,
}, nil
@@ -1068,15 +1118,7 @@ type orderRow struct {
func (row orderRow) toDTO() OrderDTO {
rentedAt := row.RentedAt
if rentedAt == nil {
rentedAt = row.RentStartAt
}
durationHours := orderDurationHours(row.RentalOrder)
rentEndAt := row.RentEndAt
if rentEndAt == nil && rentedAt != nil && durationHours > 0 {
calculatedEnd := rentedAt.Add(time.Duration(durationHours) * time.Hour)
rentEndAt = &calculatedEnd
}
return OrderDTO{
ID: row.ID,
OrderNo: row.OrderNo,
@@ -1091,9 +1133,8 @@ func (row orderRow) toDTO() OrderDTO {
LoginPlatform: row.LoginPlatform,
RentedAt: rentedAt,
EstimatedDurationHours: durationHours,
RentStartAt: row.RentStartAt,
RentEndAt: rentEndAt,
RentAmount: row.RentAmount,
OwnerRentAmount: row.OwnerRentAmount,
DepositAmount: row.DepositAmount,
PlatformFee: row.PlatformFee,
AccountSnapshot: row.AccountSnapshot,
@@ -1126,6 +1167,8 @@ func toCheckoutDTO(checkout model.OrderCheckout) CheckoutDTO {
InitiatedBy: checkout.InitiatedBy,
Status: checkout.Status,
RentAmount: checkout.RentAmount,
OwnerRentAmount: checkout.OwnerRentAmount,
PlatformFee: checkout.PlatformFee,
DepositAmount: checkout.DepositAmount,
ConsumableAmount: checkout.ConsumableAmount,
CoinConsumedM: checkout.CoinConsumedM,
+31 -9
View File
@@ -54,9 +54,6 @@ CREATE TABLE rental_listings (
account_id BIGINT UNSIGNED NOT NULL,
owner_id BIGINT UNSIGNED NOT NULL,
price DECIMAL(12,2) NOT NULL DEFAULT 0.00,
price_hourly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00,
price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
in_transaction TINYINT(1) NOT NULL DEFAULT 0,
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,
KEY idx_rental_listings_account_id (account_id),
KEY idx_rental_listings_owner_id (owner_id),
KEY idx_rental_listings_filter (status, review_status, price),
KEY idx_rental_listings_legacy_filter (status, review_status, price_hourly)
KEY idx_rental_listings_filter (status, review_status, in_transaction, price)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE rental_orders (
@@ -80,14 +76,12 @@ CREATE TABLE rental_orders (
renter_id BIGINT UNSIGNED NOT NULL,
rented_at DATETIME NULL,
estimated_duration_hours INT NOT NULL DEFAULT 24,
rent_start_at DATETIME NULL,
rent_end_at DATETIME NULL,
rent_hours INT NOT NULL,
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,
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00,
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',
settlement_status VARCHAR(32) NOT NULL DEFAULT 'unsettled',
owner_settled_at DATETIME NULL,
@@ -101,6 +95,33 @@ CREATE TABLE rental_orders (
KEY idx_rental_orders_rented_at (status, rented_at)
) 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 (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL,
@@ -141,6 +162,7 @@ CREATE TABLE wallet_ledger (
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_wallet_ledger_ledger_no (ledger_no),
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)
) 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;