diff --git a/backend/internal/model/listing.go b/backend/internal/model/listing.go index 4dfac0d..0b5a5cc 100644 --- a/backend/internal/model/listing.go +++ b/backend/internal/model/listing.go @@ -29,21 +29,19 @@ func (GameAccount) TableName() string { } type RentalListing struct { - ID uint64 `gorm:"primaryKey" json:"id"` - ListingNo string `gorm:"column:listing_no;size:20;not null;uniqueIndex" json:"listing_no"` - 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:"-"` - PriceCent int64 `gorm:"not null;default:0" json:"-"` - DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - DepositAmountCent int64 `gorm:"not null;default:0" json:"-"` - InTransaction bool `gorm:"not null;default:false" json:"in_transaction"` - Status string `gorm:"size:32;not null;default:'draft'" json:"status"` - ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"` - ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"` - PublishedAt *time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `gorm:"primaryKey" json:"id"` + ListingNo string `gorm:"column:listing_no;size:20;not null;uniqueIndex" json:"listing_no"` + AccountID uint64 `gorm:"not null;index" json:"account_id"` + OwnerID uint64 `gorm:"not null;index" json:"owner_id"` + PriceCent int64 `gorm:"not null;default:0" json:"-"` + DepositAmountCent int64 `gorm:"not null;default:0" json:"-"` + InTransaction bool `gorm:"not null;default:false" json:"in_transaction"` + Status string `gorm:"size:32;not null;default:'draft'" json:"status"` + ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"` + ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"` + PublishedAt *time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (RentalListing) TableName() string { diff --git a/backend/internal/model/order.go b/backend/internal/model/order.go index dc3a498..76355f6 100644 --- a/backend/internal/model/order.go +++ b/backend/internal/model/order.go @@ -15,17 +15,11 @@ 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"` - RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` RentAmountCent int64 `gorm:"not null;default:0" json:"-"` - OwnerRentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"` - DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` DepositAmountCent int64 `gorm:"not null;default:0" json:"-"` - DepositOriginalAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` DepositOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"` - DepositWaivedAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` DepositWaivedAmountCent int64 `gorm:"not null;default:0" json:"-"` - PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"` AccountSnapshot datatypes.JSON `json:"account_snapshot"` Status string `gorm:"size:32;not null;default:'pending_payment'" json:"status"` diff --git a/backend/internal/model/order_checkout.go b/backend/internal/model/order_checkout.go index cbcd2eb..601bfeb 100644 --- a/backend/internal/model/order_checkout.go +++ b/backend/internal/model/order_checkout.go @@ -7,37 +7,28 @@ import ( ) type OrderCheckout struct { - ID uint64 `gorm:"primaryKey" json:"id"` - OrderID uint64 `gorm:"not null;index" json:"order_id"` - 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:"-"` - RentAmountCent int64 `gorm:"not null;default:0" json:"-"` - OwnerRentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"` - PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"` - DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - DepositAmountCent int64 `gorm:"not null;default:0" json:"-"` - ConsumableAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - ConsumableAmountCent int64 `gorm:"not null;default:0" json:"-"` - CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"` - OtherAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - OtherAmountCent int64 `gorm:"not null;default:0" json:"-"` - DepositDeductAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - DepositDeductAmountCent int64 `gorm:"not null;default:0" json:"-"` - RenterRefundAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - RenterRefundAmountCent int64 `gorm:"not null;default:0" json:"-"` - OwnerIncomeAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - OwnerIncomeAmountCent int64 `gorm:"not null;default:0" json:"-"` - Content string `json:"content"` - 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"` - RenterRejectedAt *time.Time `json:"renter_rejected_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `gorm:"primaryKey" json:"id"` + OrderID uint64 `gorm:"not null;index" json:"order_id"` + InitiatedBy uint64 `gorm:"not null" json:"initiated_by"` + Status string `gorm:"size:32;not null;default:'submitted'" json:"status"` + RentAmountCent int64 `gorm:"not null;default:0" json:"-"` + OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"` + PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"` + DepositAmountCent int64 `gorm:"not null;default:0" json:"-"` + ConsumableAmountCent int64 `gorm:"not null;default:0" json:"-"` + CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"` + OtherAmountCent int64 `gorm:"not null;default:0" json:"-"` + DepositDeductAmountCent int64 `gorm:"not null;default:0" json:"-"` + RenterRefundAmountCent int64 `gorm:"not null;default:0" json:"-"` + OwnerIncomeAmountCent int64 `gorm:"not null;default:0" json:"-"` + Content string `json:"content"` + 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"` + RenterRejectedAt *time.Time `json:"renter_rejected_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (OrderCheckout) TableName() string { diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index 8c970a7..3a9da6d 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -3,19 +3,18 @@ package model import "time" type User struct { - ID uint64 `gorm:"primaryKey" json:"id"` - Phone string `gorm:"size:32;not null;uniqueIndex" json:"phone"` - Nickname string `gorm:"size:64;not null;default:''" json:"nickname"` - AvatarURL string `gorm:"size:512;not null;default:''" json:"avatar_url"` - RealnameStatus string `gorm:"size:32;not null;default:'unverified'" json:"realname_status"` - RiskStatus string `gorm:"size:32;not null;default:'normal'" json:"risk_status"` - CreditScore int `gorm:"not null;default:100" json:"credit_score"` - DepositFreeQuota float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` - DepositFreeQuotaCent int64 `gorm:"not null;default:0" json:"-"` - Status string `gorm:"size:32;not null;default:'active'" json:"status"` - LastLoginAt *time.Time `json:"last_login_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `gorm:"primaryKey" json:"id"` + Phone string `gorm:"size:32;not null;uniqueIndex" json:"phone"` + Nickname string `gorm:"size:64;not null;default:''" json:"nickname"` + AvatarURL string `gorm:"size:512;not null;default:''" json:"avatar_url"` + RealnameStatus string `gorm:"size:32;not null;default:'unverified'" json:"realname_status"` + RiskStatus string `gorm:"size:32;not null;default:'normal'" json:"risk_status"` + CreditScore int `gorm:"not null;default:100" json:"credit_score"` + DepositFreeQuotaCent int64 `gorm:"not null;default:0" json:"-"` + Status string `gorm:"size:32;not null;default:'active'" json:"status"` + LastLoginAt *time.Time `json:"last_login_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (User) TableName() string { diff --git a/backend/internal/model/wallet.go b/backend/internal/model/wallet.go index 3f2951f..869e5e2 100644 --- a/backend/internal/model/wallet.go +++ b/backend/internal/model/wallet.go @@ -5,9 +5,7 @@ import "time" type WalletAccount struct { ID uint64 `gorm:"primaryKey" json:"id"` UserID uint64 `gorm:"not null;uniqueIndex" json:"user_id"` - AvailableBalance float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` AvailableBalanceCent int64 `gorm:"not null;default:0" json:"-"` - FrozenBalance float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"` FrozenBalanceCent int64 `gorm:"not null;default:0" json:"-"` Status string `gorm:"size:32;not null;default:'active'" json:"status"` CreatedAt time.Time `json:"created_at"` @@ -24,9 +22,7 @@ type WalletLedger struct { UserID uint64 `gorm:"not null;index" json:"user_id"` OrderID *uint64 `json:"order_id"` Direction string `gorm:"size:16;not null" json:"direction"` - Amount float64 `gorm:"type:decimal(12,2);not null" json:"-"` AmountCent int64 `gorm:"not null;default:0" json:"-"` - BalanceAfter float64 `gorm:"type:decimal(12,2);not null" json:"-"` BalanceAfterCent int64 `gorm:"not null;default:0" json:"-"` BalanceType string `gorm:"size:32;not null" json:"balance_type"` BizType string `gorm:"size:32;not null" json:"biz_type"` diff --git a/backend/internal/model/withdrawal.go b/backend/internal/model/withdrawal.go index c869469..3af5010 100644 --- a/backend/internal/model/withdrawal.go +++ b/backend/internal/model/withdrawal.go @@ -6,11 +6,8 @@ type WithdrawalRequest struct { ID uint64 `gorm:"primaryKey" json:"id"` WithdrawNo string `gorm:"size:64;not null;uniqueIndex" json:"withdraw_no"` UserID uint64 `gorm:"not null;index" json:"user_id"` - Amount float64 `gorm:"type:decimal(12,2);not null" json:"-"` AmountCent int64 `gorm:"not null;default:0" json:"-"` - Fee float64 `gorm:"type:decimal(12,2);not null;default:0.00" json:"-"` FeeCent int64 `gorm:"not null;default:0" json:"-"` - ActualAmount float64 `gorm:"type:decimal(12,2);not null" json:"-"` ActualAmountCent int64 `gorm:"not null;default:0" json:"-"` PaymentAccountID *uint64 `json:"payment_account_id"` AccountType string `gorm:"size:32;not null" json:"account_type"` diff --git a/backend/internal/modules/admindashboard/dto.go b/backend/internal/modules/admindashboard/dto.go index c8584a7..77906a8 100644 --- a/backend/internal/modules/admindashboard/dto.go +++ b/backend/internal/modules/admindashboard/dto.go @@ -11,14 +11,14 @@ type DashboardDTO struct { } type MetricsDTO struct { - TotalUsers int64 `json:"total_users"` - VerifiedUsers int64 `json:"verified_users"` - TotalListings int64 `json:"total_listings"` - PublishedListings int64 `json:"published_listings"` - TotalOrders int64 `json:"total_orders"` - RentingOrders int64 `json:"renting_orders"` - TodayOrders int64 `json:"today_orders"` - TodayLedgerAmount float64 `json:"today_ledger_amount"` + TotalUsers int64 `json:"total_users"` + VerifiedUsers int64 `json:"verified_users"` + TotalListings int64 `json:"total_listings"` + PublishedListings int64 `json:"published_listings"` + TotalOrders int64 `json:"total_orders"` + RentingOrders int64 `json:"renting_orders"` + TodayOrders int64 `json:"today_orders"` + TodayLedgerAmountCent int64 `json:"today_ledger_amount_cent"` } type PendingDTO struct { @@ -29,15 +29,15 @@ type PendingDTO struct { } type RecentOrderDTO struct { - ID uint64 `json:"id"` - OrderNo string `json:"order_no"` - Title string `json:"title"` - RenterID uint64 `json:"renter_id"` - OwnerID uint64 `json:"owner_id"` - Status string `json:"status"` - RentAmount float64 `json:"rent_amount"` - DepositAmount float64 `json:"deposit_amount"` - CreatedAt time.Time `json:"created_at"` + ID uint64 `json:"id"` + OrderNo string `json:"order_no"` + Title string `json:"title"` + RenterID uint64 `json:"renter_id"` + OwnerID uint64 `json:"owner_id"` + Status string `json:"status"` + RentAmountCent int64 `json:"rent_amount_cent"` + DepositAmountCent int64 `json:"deposit_amount_cent"` + CreatedAt time.Time `json:"created_at"` } type RecentDisputeDTO struct { diff --git a/backend/internal/modules/admindashboard/repository.go b/backend/internal/modules/admindashboard/repository.go index 472cc49..231d164 100644 --- a/backend/internal/modules/admindashboard/repository.go +++ b/backend/internal/modules/admindashboard/repository.go @@ -44,9 +44,9 @@ func (r *Repository) Summary() (*DashboardDTO, error) { return nil, err } if err := r.db.Model(&model.WalletLedger{}). - Select("COALESCE(SUM(amount), 0)"). + Select("COALESCE(SUM(amount_cent), 0)"). Where("created_at >= ?", today). - Scan(&metrics.TodayLedgerAmount).Error; err != nil { + Scan(&metrics.TodayLedgerAmountCent).Error; err != nil { return nil, err } if err := r.db.Model(&model.RentalListing{}).Where("review_status = ?", "pending").Count(&pending.ListingReviews).Error; err != nil { @@ -81,7 +81,7 @@ func (r *Repository) Summary() (*DashboardDTO, error) { func (r *Repository) recentOrders() ([]RecentOrderDTO, error) { rows := make([]RecentOrderDTO, 0) err := r.db.Table("rental_orders AS o"). - Select("o.id, o.order_no, a.title, o.renter_id, o.owner_id, o.status, o.rent_amount, o.deposit_amount, o.created_at"). + Select("o.id, o.order_no, a.title, o.renter_id, o.owner_id, o.status, o.rent_amount_cent, o.deposit_amount_cent, o.created_at"). Joins("JOIN game_accounts AS a ON a.id = o.account_id"). Order("o.id DESC"). Limit(8). diff --git a/backend/internal/modules/adminfinance/dto.go b/backend/internal/modules/adminfinance/dto.go index 3ea79b0..d5381f6 100644 --- a/backend/internal/modules/adminfinance/dto.go +++ b/backend/internal/modules/adminfinance/dto.go @@ -26,35 +26,35 @@ type DashboardDTO struct { } type FinanceSummaryDTO struct { - TotalFlowAmountCent int64 `json:"total_flow_amount_cent"` - TotalRefundAmountCent int64 `json:"total_refund_amount_cent"` - PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"` - ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` - PlatformIncomeAmount float64 `json:"platform_income_amount"` - OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"` - OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"` - SettlementDiffAmount float64 `json:"settlement_diff_amount"` - SuccessfulPayCount int64 `json:"successful_pay_count"` - SuccessfulRefundCount int64 `json:"successful_refund_count"` - PendingRefundCount int64 `json:"pending_refund_count"` - SettledOrderCount int64 `json:"settled_order_count"` - FinancialExceptionCount int64 `json:"financial_exception_count"` + TotalFlowAmountCent int64 `json:"total_flow_amount_cent"` + TotalRefundAmountCent int64 `json:"total_refund_amount_cent"` + PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"` + ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` + PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"` + OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"` + OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"` + SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"` + SuccessfulPayCount int64 `json:"successful_pay_count"` + SuccessfulRefundCount int64 `json:"successful_refund_count"` + PendingRefundCount int64 `json:"pending_refund_count"` + SettledOrderCount int64 `json:"settled_order_count"` + FinancialExceptionCount int64 `json:"financial_exception_count"` } type FinanceDailyDTO struct { - Date string `json:"date"` - TotalFlowAmountCent int64 `json:"total_flow_amount_cent"` - TotalRefundAmountCent int64 `json:"total_refund_amount_cent"` - PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"` - ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` - PlatformIncomeAmount float64 `json:"platform_income_amount"` - OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"` - OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"` - SettlementDiffAmount float64 `json:"settlement_diff_amount"` - SuccessfulPayCount int64 `json:"successful_pay_count"` - SuccessfulRefundCount int64 `json:"successful_refund_count"` - PendingRefundCount int64 `json:"pending_refund_count"` - SettledOrderCount int64 `json:"settled_order_count"` + Date string `json:"date"` + TotalFlowAmountCent int64 `json:"total_flow_amount_cent"` + TotalRefundAmountCent int64 `json:"total_refund_amount_cent"` + PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"` + ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` + PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"` + OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"` + OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"` + SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"` + SuccessfulPayCount int64 `json:"successful_pay_count"` + SuccessfulRefundCount int64 `json:"successful_refund_count"` + PendingRefundCount int64 `json:"pending_refund_count"` + SettledOrderCount int64 `json:"settled_order_count"` } type PaginatedResult struct { @@ -65,32 +65,32 @@ type PaginatedResult struct { } type FinanceDetailDTO struct { - OrderID uint64 `json:"order_id"` - OrderNo string `json:"order_no"` - OrderStatus string `json:"order_status"` - SettlementStatus string `json:"settlement_status"` - RefundStatus string `json:"refund_status"` - RenterID uint64 `json:"renter_id"` - RenterPhone string `json:"renter_phone"` - RenterNickname string `json:"renter_nickname"` - OwnerID uint64 `json:"owner_id"` - OwnerPhone string `json:"owner_phone"` - OwnerNickname string `json:"owner_nickname"` - OrderRentAmount float64 `json:"order_rent_amount"` - OrderDepositAmount float64 `json:"order_deposit_amount"` - CheckoutRentAmount float64 `json:"checkout_rent_amount"` - CheckoutRenterRefund float64 `json:"checkout_renter_refund"` - CheckoutOwnerIncome float64 `json:"checkout_owner_income"` - CheckoutPlatformFee float64 `json:"checkout_platform_fee"` - OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"` - PaidAmountCent int64 `json:"paid_amount_cent"` - RefundedAmountCent int64 `json:"refunded_amount_cent"` - RefundingAmountCent int64 `json:"refunding_amount_cent"` - FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"` - ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` - PlatformNetAmount float64 `json:"platform_net_amount"` - SettlementDiffAmount float64 `json:"settlement_diff_amount"` - FinanceStatus string `json:"finance_status"` - CreatedAt time.Time `json:"created_at"` - SettledAt *time.Time `json:"settled_at,omitempty"` + OrderID uint64 `json:"order_id"` + OrderNo string `json:"order_no"` + OrderStatus string `json:"order_status"` + SettlementStatus string `json:"settlement_status"` + RefundStatus string `json:"refund_status"` + RenterID uint64 `json:"renter_id"` + RenterPhone string `json:"renter_phone"` + RenterNickname string `json:"renter_nickname"` + OwnerID uint64 `json:"owner_id"` + OwnerPhone string `json:"owner_phone"` + OwnerNickname string `json:"owner_nickname"` + OrderRentAmountCent int64 `json:"order_rent_amount_cent"` + OrderDepositAmountCent int64 `json:"order_deposit_amount_cent"` + CheckoutRentAmountCent int64 `json:"checkout_rent_amount_cent"` + CheckoutRenterRefundCent int64 `json:"checkout_renter_refund_cent"` + CheckoutOwnerIncomeCent int64 `json:"checkout_owner_income_cent"` + CheckoutPlatformFeeCent int64 `json:"checkout_platform_fee_cent"` + OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"` + PaidAmountCent int64 `json:"paid_amount_cent"` + RefundedAmountCent int64 `json:"refunded_amount_cent"` + RefundingAmountCent int64 `json:"refunding_amount_cent"` + FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"` + ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` + PlatformNetAmountCent int64 `json:"platform_net_amount_cent"` + SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"` + FinanceStatus string `json:"finance_status"` + CreatedAt time.Time `json:"created_at"` + SettledAt *time.Time `json:"settled_at,omitempty"` } diff --git a/backend/internal/modules/adminfinance/repository.go b/backend/internal/modules/adminfinance/repository.go index 69677c3..5f645c3 100644 --- a/backend/internal/modules/adminfinance/repository.go +++ b/backend/internal/modules/adminfinance/repository.go @@ -1,11 +1,9 @@ package adminfinance import ( - "math" "time" "hfb_sys/backend/internal/timeutil" - "hfb_sys/backend/pkg/money" "gorm.io/gorm" ) @@ -71,10 +69,10 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) { var settlement settlementSummaryRow if err := r.db.Table("rental_orders AS ro"). - Select(`COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount, - COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount, - COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount, - COUNT(ro.id) AS settled_order_count`). + Select(`COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, + COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, + COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, + COUNT(ro.id) AS settled_order_count`). Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)). Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). @@ -94,19 +92,19 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) { } return &FinanceSummaryDTO{ - TotalFlowAmountCent: payment.TotalFlowAmountCent, - TotalRefundAmountCent: payment.TotalRefundAmountCent, - PendingRefundAmountCent: payment.PendingRefundAmountCent, - ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent, - PlatformIncomeAmount: money.Round(settlement.PlatformIncomeAmount), - OwnerShouldIncomeAmount: money.Round(settlement.OwnerShouldIncomeAmount), - OwnerWalletIncomeAmount: money.Round(settlement.OwnerWalletIncomeAmount), - SettlementDiffAmount: money.Round(settlement.OwnerShouldIncomeAmount - settlement.OwnerWalletIncomeAmount), - SuccessfulPayCount: payment.SuccessfulPayCount, - SuccessfulRefundCount: payment.SuccessfulRefundCount, - PendingRefundCount: payment.PendingRefundCount, - SettledOrderCount: settlement.SettledOrderCount, - FinancialExceptionCount: exceptionCount, + TotalFlowAmountCent: payment.TotalFlowAmountCent, + TotalRefundAmountCent: payment.TotalRefundAmountCent, + PendingRefundAmountCent: payment.PendingRefundAmountCent, + ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent, + PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent, + OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent, + OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent, + SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent, + SuccessfulPayCount: payment.SuccessfulPayCount, + SuccessfulRefundCount: payment.SuccessfulRefundCount, + PendingRefundCount: payment.PendingRefundCount, + SettledOrderCount: settlement.SettledOrderCount, + FinancialExceptionCount: exceptionCount, }, nil } @@ -130,10 +128,10 @@ func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error) settlements := make([]dailySettlementRow, 0) if err := r.db.Table("rental_orders AS ro"). Select(`DATE(ro.settled_at) AS date, - COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount, - COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount, - COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount, - COUNT(ro.id) AS settled_order_count`). + COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, + COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, + COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, + COUNT(ro.id) AS settled_order_count`). Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)). Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). @@ -162,10 +160,10 @@ func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error) for _, row := range settlements { item := itemsByDate[row.Date] item.Date = row.Date - item.PlatformIncomeAmount = money.Round(row.PlatformIncomeAmount) - item.OwnerShouldIncomeAmount = money.Round(row.OwnerShouldIncomeAmount) - item.OwnerWalletIncomeAmount = money.Round(row.OwnerWalletIncomeAmount) - item.SettlementDiffAmount = money.Round(row.OwnerShouldIncomeAmount - row.OwnerWalletIncomeAmount) + item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent + item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent + item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent + item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent item.SettledOrderCount = row.SettledOrderCount itemsByDate[row.Date] = item } @@ -182,23 +180,23 @@ func (r *Repository) financeDetailBaseQuery(query DetailQuery) *gorm.DB { Select(`ro.id AS order_id, ro.order_no, ro.status AS order_status, ro.settlement_status, ro.refund_status, ro.renter_id, COALESCE(ru.phone, '') AS renter_phone, COALESCE(ru.nickname, '') AS renter_nickname, ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname, - ro.rent_amount AS order_rent_amount, ro.deposit_amount AS order_deposit_amount, - COALESCE(oc.rent_amount, 0) AS checkout_rent_amount, - COALESCE(oc.renter_refund_amount, 0) AS checkout_renter_refund, - COALESCE(oc.owner_income_amount, 0) AS checkout_owner_income, - COALESCE(oc.platform_fee, 0) AS checkout_platform_fee, - COALESCE(w.owner_wallet_income_amount, 0) AS owner_wallet_income_amount, - COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent, - COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, - COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent, - COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent, - COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent, - COALESCE(oc.platform_fee, 0) + ((COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0))) AS platform_net_amount, - COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0) AS settlement_diff_amount, - CASE - WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' - WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending' - WHEN ABS(COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0)) >= 0.05 THEN 'settlement_diff' + ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent, + COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent, + COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent, + COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent, + COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent, + COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent, + COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent, + COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, + COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent, + COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent, + COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent, + COALESCE(oc.platform_fee_cent, 0) + ((COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0))) AS platform_net_amount_cent, + COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent, + CASE + WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' + WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending' + WHEN ABS(COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0)) >= 5 THEN 'settlement_diff' ELSE 'normal' END AS finance_status, ro.created_at, ro.settled_at`). @@ -250,7 +248,7 @@ func orderPaymentSubquery(db *gorm.DB) *gorm.DB { func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB { return db.Table("wallet_ledger"). - Select("order_id, COALESCE(SUM(amount), 0) AS owner_wallet_income_amount"). + Select("order_id, COALESCE(SUM(amount_cent), 0) AS owner_wallet_income_amount_cent"). Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation"}). Group("order_id") } @@ -283,10 +281,10 @@ type paymentSummaryRow struct { } type settlementSummaryRow struct { - PlatformIncomeAmount float64 - OwnerShouldIncomeAmount float64 - OwnerWalletIncomeAmount float64 - SettledOrderCount int64 + PlatformIncomeAmountCent int64 + OwnerShouldIncomeAmountCent int64 + OwnerWalletIncomeAmountCent int64 + SettledOrderCount int64 } type dailyPaymentRow struct { @@ -300,81 +298,88 @@ type dailyPaymentRow struct { } type dailySettlementRow struct { - Date string - PlatformIncomeAmount float64 - OwnerShouldIncomeAmount float64 - OwnerWalletIncomeAmount float64 - SettledOrderCount int64 + Date string + PlatformIncomeAmountCent int64 + OwnerShouldIncomeAmountCent int64 + OwnerWalletIncomeAmountCent int64 + SettledOrderCount int64 } type financeDetailRow struct { - OrderID uint64 - OrderNo string - OrderStatus string - SettlementStatus string - RefundStatus string - RenterID uint64 - RenterPhone string - RenterNickname string - OwnerID uint64 - OwnerPhone string - OwnerNickname string - OrderRentAmount float64 - OrderDepositAmount float64 - CheckoutRentAmount float64 - CheckoutRenterRefund float64 - CheckoutOwnerIncome float64 - CheckoutPlatformFee float64 - OwnerWalletIncomeAmount float64 - PaidAmountCent int64 - RefundedAmountCent int64 - RefundingAmountCent int64 - FailedRefundAmountCent int64 - ChannelNetAmountCent int64 - PlatformNetAmount float64 - SettlementDiffAmount float64 - FinanceStatus string - CreatedAt time.Time - SettledAt *time.Time + OrderID uint64 + OrderNo string + OrderStatus string + SettlementStatus string + RefundStatus string + RenterID uint64 + RenterPhone string + RenterNickname string + OwnerID uint64 + OwnerPhone string + OwnerNickname string + OrderRentAmountCent int64 + OrderDepositAmountCent int64 + CheckoutRentAmountCent int64 + CheckoutRenterRefundCent int64 + CheckoutOwnerIncomeCent int64 + CheckoutPlatformFeeCent int64 + OwnerWalletIncomeAmountCent int64 + PaidAmountCent int64 + RefundedAmountCent int64 + RefundingAmountCent int64 + FailedRefundAmountCent int64 + ChannelNetAmountCent int64 + PlatformNetAmountCent int64 + SettlementDiffAmountCent int64 + FinanceStatus string + CreatedAt time.Time + SettledAt *time.Time } func (r financeDetailRow) toDTO() FinanceDetailDTO { - diff := money.Round(r.SettlementDiffAmount) + diff := r.SettlementDiffAmountCent status := r.FinanceStatus if status == "" { status = "normal" } - if math.Abs(diff) < 0.05 && status == "settlement_diff" { + if absCent(diff) < 5 && status == "settlement_diff" { status = "normal" } return FinanceDetailDTO{ - OrderID: r.OrderID, - OrderNo: r.OrderNo, - OrderStatus: r.OrderStatus, - SettlementStatus: r.SettlementStatus, - RefundStatus: r.RefundStatus, - RenterID: r.RenterID, - RenterPhone: r.RenterPhone, - RenterNickname: r.RenterNickname, - OwnerID: r.OwnerID, - OwnerPhone: r.OwnerPhone, - OwnerNickname: r.OwnerNickname, - OrderRentAmount: money.Round(r.OrderRentAmount), - OrderDepositAmount: money.Round(r.OrderDepositAmount), - CheckoutRentAmount: money.Round(r.CheckoutRentAmount), - CheckoutRenterRefund: money.Round(r.CheckoutRenterRefund), - CheckoutOwnerIncome: money.Round(r.CheckoutOwnerIncome), - CheckoutPlatformFee: money.Round(r.CheckoutPlatformFee), - OwnerWalletIncomeAmount: money.Round(r.OwnerWalletIncomeAmount), - PaidAmountCent: r.PaidAmountCent, - RefundedAmountCent: r.RefundedAmountCent, - RefundingAmountCent: r.RefundingAmountCent, - FailedRefundAmountCent: r.FailedRefundAmountCent, - ChannelNetAmountCent: r.ChannelNetAmountCent, - PlatformNetAmount: money.Round(r.PlatformNetAmount), - SettlementDiffAmount: diff, - FinanceStatus: status, - CreatedAt: r.CreatedAt, - SettledAt: r.SettledAt, + OrderID: r.OrderID, + OrderNo: r.OrderNo, + OrderStatus: r.OrderStatus, + SettlementStatus: r.SettlementStatus, + RefundStatus: r.RefundStatus, + RenterID: r.RenterID, + RenterPhone: r.RenterPhone, + RenterNickname: r.RenterNickname, + OwnerID: r.OwnerID, + OwnerPhone: r.OwnerPhone, + OwnerNickname: r.OwnerNickname, + OrderRentAmountCent: r.OrderRentAmountCent, + OrderDepositAmountCent: r.OrderDepositAmountCent, + CheckoutRentAmountCent: r.CheckoutRentAmountCent, + CheckoutRenterRefundCent: r.CheckoutRenterRefundCent, + CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent, + CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent, + OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent, + PaidAmountCent: r.PaidAmountCent, + RefundedAmountCent: r.RefundedAmountCent, + RefundingAmountCent: r.RefundingAmountCent, + FailedRefundAmountCent: r.FailedRefundAmountCent, + ChannelNetAmountCent: r.ChannelNetAmountCent, + PlatformNetAmountCent: r.PlatformNetAmountCent, + SettlementDiffAmountCent: diff, + FinanceStatus: status, + CreatedAt: r.CreatedAt, + SettledAt: r.SettledAt, } } + +func absCent(value int64) int64 { + if value < 0 { + return -value + } + return value +} diff --git a/backend/internal/modules/adminuser/dto.go b/backend/internal/modules/adminuser/dto.go index 034392a..559d58b 100644 --- a/backend/internal/modules/adminuser/dto.go +++ b/backend/internal/modules/adminuser/dto.go @@ -3,30 +3,29 @@ package adminuser import "time" type UserDTO struct { - ID uint64 `json:"id"` - Phone string `json:"phone"` - Nickname string `json:"nickname"` - RealnameStatus string `json:"realname_status"` - RiskStatus string `json:"risk_status"` - CreditScore int `json:"credit_score"` - DepositFreeQuota float64 `json:"deposit_free_quota"` - DepositFreeUsed float64 `json:"deposit_free_used"` - DepositFreeRemaining float64 `json:"deposit_free_remaining"` - Status string `json:"status"` - OrderCount int64 `json:"order_count"` - ListingCount int64 `json:"listing_count"` - DisputeCount int64 `json:"dispute_count"` - LastLoginAt *time.Time `json:"last_login_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `json:"id"` + Phone string `json:"phone"` + Nickname string `json:"nickname"` + RealnameStatus string `json:"realname_status"` + RiskStatus string `json:"risk_status"` + CreditScore int `json:"credit_score"` + DepositFreeQuotaCent int64 `json:"deposit_free_quota_cent"` + DepositFreeUsedCent int64 `json:"deposit_free_used_cent"` + DepositFreeRemainingCent int64 `json:"deposit_free_remaining_cent"` + Status string `json:"status"` + OrderCount int64 `json:"order_count"` + ListingCount int64 `json:"listing_count"` + DisputeCount int64 `json:"dispute_count"` + LastLoginAt *time.Time `json:"last_login_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type FreezeRequest struct { Reason string `json:"reason"` } type DepositFreeQuotaRequest struct { - AmountCent int64 `json:"amount_cent"` - Amount float64 `json:"amount"` + AmountCent int64 `json:"amount_cent"` } type PaginatedResult struct { diff --git a/backend/internal/modules/adminuser/repository.go b/backend/internal/modules/adminuser/repository.go index 804e0bb..857476e 100644 --- a/backend/internal/modules/adminuser/repository.go +++ b/backend/internal/modules/adminuser/repository.go @@ -2,11 +2,9 @@ package adminuser import ( "errors" - "math" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" - "hfb_sys/backend/pkg/money" "gorm.io/gorm" "gorm.io/gorm/clause" @@ -34,11 +32,11 @@ func (r *Repository) List(page, pageSize int) (*PaginatedResult, error) { COALESCE(o.order_count, 0) AS order_count, COALESCE(l.listing_count, 0) AS listing_count, COALESCE(d.dispute_count, 0) AS dispute_count, - COALESCE(df.deposit_free_used, 0) AS deposit_free_used`). + COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent`). Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id"). Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id"). Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id"). - Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount) AS deposit_free_used FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id"). + Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id"). Order("u.id DESC"). Offset(offset).Limit(pageSize). Scan(&rows).Error @@ -65,23 +63,18 @@ func (r *Repository) SetDepositFreeQuota(adminID uint64, userID uint64, req Depo if amountCent < 0 { return nil, ErrInvalidUser } - amount := float64(amountCent) / 100 err := r.db.Transaction(func(tx *gorm.DB) error { var user model.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil { return err } - beforeAmount := user.DepositFreeQuota beforeAmountCent := user.DepositFreeQuotaCent - user.DepositFreeQuota = amount user.DepositFreeQuotaCent = amountCent if err := tx.Save(&user).Error; err != nil { return err } return appendAuditLog(tx, adminID, "admin_user.set_deposit_free_quota", user.ID, meta, map[string]any{ "user_id": user.ID, - "before_amount": beforeAmount, - "after_amount": amount, "before_amount_cent": beforeAmountCent, "after_amount_cent": amountCent, }) @@ -127,11 +120,11 @@ func (r *Repository) Find(userID uint64) (*UserDTO, error) { COALESCE(o.order_count, 0) AS order_count, COALESCE(l.listing_count, 0) AS listing_count, COALESCE(d.dispute_count, 0) AS dispute_count, - COALESCE(df.deposit_free_used, 0) AS deposit_free_used`). + COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent`). Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id"). Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id"). Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id"). - Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount) AS deposit_free_used FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id"). + Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id"). Where("u.id = ?", userID). First(&row).Error if err != nil { @@ -143,46 +136,39 @@ func (r *Repository) Find(userID uint64) (*UserDTO, error) { type userRow struct { model.User - OrderCount int64 - ListingCount int64 - DisputeCount int64 - DepositFreeUsed float64 + OrderCount int64 + ListingCount int64 + DisputeCount int64 + DepositFreeUsedCent int64 } func (row userRow) toDTO() UserDTO { - remaining := roundMoney(row.DepositFreeQuota - row.DepositFreeUsed) + remaining := row.DepositFreeQuotaCent - row.DepositFreeUsedCent if remaining < 0 { remaining = 0 } return UserDTO{ - ID: row.ID, - Phone: row.Phone, - Nickname: row.Nickname, - RealnameStatus: row.RealnameStatus, - RiskStatus: row.RiskStatus, - CreditScore: row.CreditScore, - DepositFreeQuota: row.DepositFreeQuota, - DepositFreeUsed: roundMoney(row.DepositFreeUsed), - DepositFreeRemaining: remaining, - Status: row.Status, - OrderCount: row.OrderCount, - ListingCount: row.ListingCount, - DisputeCount: row.DisputeCount, - LastLoginAt: row.LastLoginAt, - CreatedAt: row.CreatedAt, - UpdatedAt: row.UpdatedAt, + ID: row.ID, + Phone: row.Phone, + Nickname: row.Nickname, + RealnameStatus: row.RealnameStatus, + RiskStatus: row.RiskStatus, + CreditScore: row.CreditScore, + DepositFreeQuotaCent: row.DepositFreeQuotaCent, + DepositFreeUsedCent: row.DepositFreeUsedCent, + DepositFreeRemainingCent: remaining, + Status: row.Status, + OrderCount: row.OrderCount, + ListingCount: row.ListingCount, + DisputeCount: row.DisputeCount, + LastLoginAt: row.LastLoginAt, + CreatedAt: row.CreatedAt, + UpdatedAt: row.UpdatedAt, } } -func roundMoney(value float64) float64 { - return money.Round(value) -} - func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 { - if req.AmountCent != 0 { - return req.AmountCent - } - return int64(math.Round(req.Amount * 100)) + return req.AmountCent } func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error { diff --git a/backend/internal/modules/adminuser/repository_test.go b/backend/internal/modules/adminuser/repository_test.go index 1b20a4b..1dafee9 100644 --- a/backend/internal/modules/adminuser/repository_test.go +++ b/backend/internal/modules/adminuser/repository_test.go @@ -9,18 +9,13 @@ func TestDepositFreeQuotaAmountCent(t *testing.T) { want int64 }{ { - name: "优先使用分字段", - req: DepositFreeQuotaRequest{AmountCent: 1234, Amount: 99}, - want: 1234, - }, - { - name: "缺少分字段时回退元字段", - req: DepositFreeQuotaRequest{Amount: 12.34}, + name: "使用分字段", + req: DepositFreeQuotaRequest{AmountCent: 1234}, want: 1234, }, { name: "负分拒绝", - req: DepositFreeQuotaRequest{AmountCent: -1, Amount: 12.34}, + req: DepositFreeQuotaRequest{AmountCent: -1}, want: -1, }, } diff --git a/backend/internal/modules/adminuser/service.go b/backend/internal/modules/adminuser/service.go index 093688b..921b354 100644 --- a/backend/internal/modules/adminuser/service.go +++ b/backend/internal/modules/adminuser/service.go @@ -46,7 +46,7 @@ func (s *Service) SetDepositFreeQuota(adminID uint64, userID uint64, req Deposit if s.repo == nil { return nil, ErrDependencyUnavailable } - if userID == 0 || req.Amount < 0 { + if userID == 0 || req.AmountCent < 0 { return nil, ErrInvalidUser } return s.repo.SetDepositFreeQuota(adminID, userID, req, meta) diff --git a/backend/internal/modules/dispute/dto.go b/backend/internal/modules/dispute/dto.go index 0f4ab8a..5af13b4 100644 --- a/backend/internal/modules/dispute/dto.go +++ b/backend/internal/modules/dispute/dto.go @@ -35,9 +35,9 @@ type CreateRequest struct { } type ArbitrateRequest struct { - Result string `json:"result" binding:"required"` - Remark string `json:"remark" binding:"required"` - Amount float64 `json:"amount"` + Result string `json:"result" binding:"required"` + Remark string `json:"remark" binding:"required"` + AmountCent int64 `json:"amount_cent"` } type AuditMeta = auditlog.Meta diff --git a/backend/internal/modules/dispute/repository.go b/backend/internal/modules/dispute/repository.go index c64c6fc..281c218 100644 --- a/backend/internal/modules/dispute/repository.go +++ b/backend/internal/modules/dispute/repository.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "math" "time" "hfb_sys/backend/internal/auditlog" @@ -249,9 +248,8 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil { return err } - if settlement.RenterRefundAmount > 0 { - refundCent := int64(math.Round(settlement.RenterRefundAmount * 100)) - action, err := r.prepareRefund(&order, refundCent, "arbitration_refund", "仲裁退款原路退还") + if settlement.RenterRefundAmountCent > 0 { + action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还") if err != nil { return err } @@ -281,25 +279,25 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, } disputeID := row.ID if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{ - "dispute_id": row.ID, - "order_id": order.ID, - "order_no": order.OrderNo, - "result": req.Result, - "remark": req.Remark, - "input_amount": req.Amount, - "renter_refund_amount": settlement.RenterRefundAmount, - "owner_income_amount": settlement.OwnerIncomeAmount, - "deposit_deduct_amount": settlement.DepositDeductAmount, - "before_order_status": beforeOrderStatus, - "after_order_status": order.Status, - "before_handoff_status": beforeHandoffStatus, - "after_handoff_status": order.HandoffStatus, - "before_settlement_status": beforeSettlementStatus, - "after_settlement_status": order.SettlementStatus, - "before_listing_status": beforeListingStatus, - "after_listing_status": listing.Status, - "before_account_status": beforeAccountStatus, - "after_account_status": account.Status, + "dispute_id": row.ID, + "order_id": order.ID, + "order_no": order.OrderNo, + "result": req.Result, + "remark": req.Remark, + "input_amount_cent": req.AmountCent, + "renter_refund_amount_cent": settlement.RenterRefundAmountCent, + "owner_income_amount_cent": settlement.OwnerIncomeAmountCent, + "deposit_deduct_amount_cent": settlement.DepositDeductAmountCent, + "before_order_status": beforeOrderStatus, + "after_order_status": order.Status, + "before_handoff_status": beforeHandoffStatus, + "after_handoff_status": order.HandoffStatus, + "before_settlement_status": beforeSettlementStatus, + "after_settlement_status": order.SettlementStatus, + "before_listing_status": beforeListingStatus, + "after_listing_status": listing.Status, + "before_account_status": beforeAccountStatus, + "after_account_status": account.Status, }); err != nil { return err } @@ -338,36 +336,29 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, } type arbitrationSettlement struct { - Entries []wallet.Entry - RenterRefundAmount float64 - OwnerIncomeAmount float64 - DepositDeductAmount float64 + Entries []wallet.Entry + RenterRefundAmountCent int64 + OwnerIncomeAmountCent int64 + DepositDeductAmountCent int64 } -func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalance float64) (arbitrationSettlement, error) { - rentAmount := float64(order.RentAmountCent) / 100 - if rentAmount <= 0 { - rentAmount = order.RentAmount +func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) { + rentAmountCent := order.RentAmountCent + depositAmountCent := order.DepositAmountCent + ownerRentAmountCent := order.OwnerRentAmountCent + if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent { + ownerRentAmountCent = rentAmountCent } - depositAmount := float64(order.DepositAmountCent) / 100 - if depositAmount <= 0 { - depositAmount = order.DepositAmount - } - ownerRentAmount := float64(order.OwnerRentAmountCent) / 100 - if ownerRentAmount <= 0 || ownerRentAmount > rentAmount { - ownerRentAmount = rentAmount - } - total := roundMoney(rentAmount + depositAmount) - ownerRentAmount = roundMoney(ownerRentAmount) + totalCent := rentAmountCent + depositAmountCent settlement := arbitrationSettlement{} orderID := order.ID - releaseFrozenAmount := minMoney(total, roundMoney(renterFrozenBalance)) - if releaseFrozenAmount > 0 { + releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent) + if releaseFrozenAmountCent > 0 { settlement.Entries = append(settlement.Entries, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "out", - AmountCent: int64(math.Round(releaseFrozenAmount * 100)), + AmountCent: releaseFrozenAmountCent, BalanceType: "frozen", BizType: "arbitration_release_frozen", BizNo: order.OrderNo, @@ -375,22 +366,22 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r }) } - addRenterRefund := func(amount float64, remark string) { - if amount <= 0 { + addRenterRefund := func(amountCent int64, remark string) { + if amountCent <= 0 { return } - settlement.RenterRefundAmount += amount + settlement.RenterRefundAmountCent += amountCent } - addOwnerIncome := func(amount float64, remark string) { - if amount <= 0 { + addOwnerIncome := func(amountCent int64, remark string) { + if amountCent <= 0 { return } - settlement.OwnerIncomeAmount += amount + settlement.OwnerIncomeAmountCent += amountCent settlement.Entries = append(settlement.Entries, wallet.Entry{ UserID: order.OwnerID, OrderID: &orderID, Direction: "in", - AmountCent: int64(math.Round(amount * 100)), + AmountCent: amountCent, BalanceType: "available", BizType: "arbitration_owner_income", BizNo: order.OrderNo, @@ -400,28 +391,27 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r switch req.Result { case "full_refund": - addRenterRefund(total, "仲裁全额退款") + addRenterRefund(totalCent, "仲裁全额退款") case "partial_refund": - req.Amount = roundMoney(req.Amount) - if req.Amount <= 0 || req.Amount > total { + if req.AmountCent <= 0 || req.AmountCent > totalCent { return settlement, ErrInvalidDispute } - addRenterRefund(req.Amount, "仲裁部分退款") - addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+depositAmount), "仲裁剩余金额结算给号主") + addRenterRefund(req.AmountCent, "仲裁部分退款") + addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主") case "release_deposit": - addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主") - addRenterRefund(depositAmount, "仲裁释放押金给租客") + addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主") + addRenterRefund(depositAmountCent, "仲裁释放押金给租客") case "deduct_deposit", "compensate_owner": - deductAmount := roundMoney(req.Amount) - if deductAmount <= 0 { - deductAmount = depositAmount + deductAmountCent := req.AmountCent + if deductAmountCent <= 0 { + deductAmountCent = depositAmountCent } - if deductAmount > depositAmount { + if deductAmountCent > depositAmountCent { return settlement, ErrInvalidDispute } - settlement.DepositDeductAmount = deductAmount - addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主") - addRenterRefund(depositAmount-deductAmount, "仲裁退回剩余押金给租客") + settlement.DepositDeductAmountCent = deductAmountCent + addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主") + addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客") case "order_close": // Only release frozen funds. No available-balance settlement happens in development mode. case "mark_abnormal": @@ -457,7 +447,7 @@ func (r *Repository) startRefundBestEffort(action *refundAction) { _, _ = r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark) } -func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) { +func renterFrozenBalance(tx *gorm.DB, renterID uint64) (int64, error) { var account model.WalletAccount err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("user_id = ?", renterID). @@ -468,17 +458,7 @@ func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) { if err != nil { return 0, err } - return account.FrozenBalance, nil -} - -// minMoney 返回较小金额(角精度) -func minMoney(a float64, b float64) float64 { - return money.Min(a, b) -} - -// roundMoney 使用统一的角精度(0.1元) -func roundMoney(value float64) float64 { - return money.Round(value) + return account.FrozenBalanceCent, nil } func (r *Repository) baseQuery() *gorm.DB { @@ -550,14 +530,14 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration if req.Remark != "" { content += "\n处理说明:" + req.Remark } - if settlement.RenterRefundAmount > 0 { - content += fmt.Sprintf("\n退款给租客:¥%.2f", settlement.RenterRefundAmount) + if settlement.RenterRefundAmountCent > 0 { + content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent) } - if settlement.OwnerIncomeAmount > 0 { - content += fmt.Sprintf("\n结算给号主:¥%.2f", settlement.OwnerIncomeAmount) + if settlement.OwnerIncomeAmountCent > 0 { + content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent) } - if settlement.DepositDeductAmount > 0 { - content += fmt.Sprintf("\n押金扣除:¥%.2f", settlement.DepositDeductAmount) + if settlement.DepositDeductAmountCent > 0 { + content += "\n押金扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent) } return content } diff --git a/backend/internal/modules/dispute/repository_test.go b/backend/internal/modules/dispute/repository_test.go index 94b73ef..583d470 100644 --- a/backend/internal/modules/dispute/repository_test.go +++ b/backend/internal/modules/dispute/repository_test.go @@ -31,11 +31,11 @@ func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *test t.Fatalf("不应在无冻结余额时生成解冻流水: %+v", entry) } } - if settlement.OwnerIncomeAmount != 180 { - t.Fatalf("OwnerIncomeAmount = %.1f, want 180.0", settlement.OwnerIncomeAmount) + if settlement.OwnerIncomeAmountCent != 18000 { + t.Fatalf("OwnerIncomeAmountCent = %d, want 18000", settlement.OwnerIncomeAmountCent) } - if settlement.RenterRefundAmount != 100 { - t.Fatalf("RenterRefundAmount = %.1f, want 100.0", settlement.RenterRefundAmount) + if settlement.RenterRefundAmountCent != 10000 { + t.Fatalf("RenterRefundAmountCent = %d, want 10000", settlement.RenterRefundAmountCent) } for _, entry := range settlement.Entries { if entry.UserID == order.RenterID && entry.BizType == "arbitration_renter_refund" { @@ -58,7 +58,7 @@ func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testi settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ Result: "order_close", Remark: "测试裁决", - }, 120) + }, 12000) if err != nil { t.Fatalf("buildArbitrationSettlement() error = %v", err) } @@ -83,15 +83,15 @@ func TestBuildArbitrationHandoffContent(t *testing.T) { Result: "partial_refund", Remark: "账号异常,退还部分租金。", }, arbitrationSettlement{ - RenterRefundAmount: 80, - OwnerIncomeAmount: 120, + RenterRefundAmountCent: 8000, + OwnerIncomeAmountCent: 12000, }) wantParts := []string{ "客服仲裁结果:部分退款", "处理说明:账号异常,退还部分租金。", - "退款给租客:¥80.00", - "结算给号主:¥120.00", + "退款给租客:¥80.0", + "结算给号主:¥120.0", } for _, part := range wantParts { if !strings.Contains(content, part) { diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index 1aa0367..69a2007 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -9,45 +9,45 @@ import ( ) type ListingDTO struct { - ID uint64 `json:"id"` - ListingNo string `json:"listing_no"` - AccountID uint64 `json:"account_id"` - OwnerID uint64 `json:"owner_id"` - OwnerPhone string `json:"owner_phone,omitempty"` - OwnerNickname string `json:"owner_nickname,omitempty"` - Title string `json:"title"` - Description string `json:"description"` - GameName string `json:"game_name"` - ServerRegion string `json:"server_region"` - LoginPlatform string `json:"login_platform"` - RankLevel string `json:"rank_level"` - HafCoinAmount int64 `json:"haf_coin_amount"` - AssetSummary map[string]any `json:"asset_summary,omitempty"` - ScreenshotURLS []string `json:"screenshot_urls"` - CoverURL string `json:"cover_url"` - PriceCent int64 `json:"price_cent"` - DepositAmountCent int64 `json:"deposit_amount_cent"` - IsAccelerated bool `json:"is_accelerated_sale"` - InTransaction bool `json:"in_transaction"` - Status string `json:"status"` - ReviewStatus string `json:"review_status"` - ReviewReason string `json:"review_reason"` - PublishedAt *time.Time `json:"published_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint64 `json:"id"` + ListingNo string `json:"listing_no"` + AccountID uint64 `json:"account_id"` + OwnerID uint64 `json:"owner_id"` + OwnerPhone string `json:"owner_phone,omitempty"` + OwnerNickname string `json:"owner_nickname,omitempty"` + Title string `json:"title"` + Description string `json:"description"` + GameName string `json:"game_name"` + ServerRegion string `json:"server_region"` + LoginPlatform string `json:"login_platform"` + RankLevel string `json:"rank_level"` + HafCoinAmount int64 `json:"haf_coin_amount"` + AssetSummary map[string]any `json:"asset_summary,omitempty"` + ScreenshotURLS []string `json:"screenshot_urls"` + CoverURL string `json:"cover_url"` + PriceCent int64 `json:"price_cent"` + DepositAmountCent int64 `json:"deposit_amount_cent"` + IsAccelerated bool `json:"is_accelerated_sale"` + InTransaction bool `json:"in_transaction"` + Status string `json:"status"` + ReviewStatus string `json:"review_status"` + ReviewReason string `json:"review_reason"` + PublishedAt *time.Time `json:"published_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type CreateRequest struct { - Title string `json:"title" binding:"required"` - Description string `json:"description"` - ServerRegion string `json:"server_region" binding:"required"` - LoginPlatform string `json:"login_platform"` - RankLevel string `json:"rank_level"` - HafCoinAmount int64 `json:"haf_coin_amount"` - AssetSummary map[string]any `json:"asset_summary"` - ScreenshotURLS []string `json:"screenshot_urls"` - Price float64 `json:"price"` - DepositAmount float64 `json:"deposit_amount"` + Title string `json:"title" binding:"required"` + Description string `json:"description"` + ServerRegion string `json:"server_region" binding:"required"` + LoginPlatform string `json:"login_platform"` + RankLevel string `json:"rank_level"` + HafCoinAmount int64 `json:"haf_coin_amount"` + AssetSummary map[string]any `json:"asset_summary"` + ScreenshotURLS []string `json:"screenshot_urls"` + PriceCent int64 `json:"price_cent"` + DepositAmountCent int64 `json:"deposit_amount_cent"` AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"` AgreedSellerAgreement bool `json:"agreed_seller_agreement"` @@ -60,9 +60,9 @@ type ReviewRequest struct { } type AdminPriceAdjustRequest struct { - BuyerRatio float64 `json:"buyer_ratio"` - BuyerTotalPrice float64 `json:"buyer_total_price"` - Reason string `json:"reason"` + BuyerRatio float64 `json:"buyer_ratio"` + BuyerTotalPriceCent int64 `json:"buyer_total_price_cent"` + Reason string `json:"reason"` } type AdminListQuery struct { diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go index 2fe9914..a33fd27 100644 --- a/backend/internal/modules/listing/repository.go +++ b/backend/internal/modules/listing/repository.go @@ -79,16 +79,13 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo if err := tx.Create(&account).Error; err != nil { return err } - price := normalizedListingPrice(req) - depositAmount := roundMoney(req.DepositAmount) + priceCent := normalizedListingPriceCent(req) listing := model.RentalListing{ ListingNo: listingNo, AccountID: account.ID, OwnerID: ownerID, - Price: price, - PriceCent: int64(math.Round(price * 100)), - DepositAmount: depositAmount, - DepositAmountCent: int64(math.Round(depositAmount * 100)), + PriceCent: priceCent, + DepositAmountCent: req.DepositAmountCent, Status: listingStatus, ReviewStatus: reviewStatus, PublishedAt: publishedAt, @@ -149,16 +146,13 @@ func (r *Repository) CreateFromExternalUpload(upload externalUploadCreate, req C if err := tx.Create(&account).Error; err != nil { return err } - price := normalizedListingPrice(req) - depositAmount := roundMoney(req.DepositAmount) + priceCent := normalizedListingPriceCent(req) listing := model.RentalListing{ ListingNo: listingNo, AccountID: account.ID, OwnerID: owner.ID, - Price: price, - PriceCent: int64(math.Round(price * 100)), - DepositAmount: depositAmount, - DepositAmountCent: int64(math.Round(depositAmount * 100)), + PriceCent: priceCent, + DepositAmountCent: req.DepositAmountCent, Status: "draft", ReviewStatus: "pending", } @@ -216,9 +210,8 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest, } account.ScreenshotURLS = screenshots - price := normalizedListingPrice(req) - listing.Price = price - listing.DepositAmount = roundMoney(req.DepositAmount) + listing.PriceCent = normalizedListingPriceCent(req) + listing.DepositAmountCent = req.DepositAmountCent listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) listing.Status = listingStatus listing.ReviewStatus = reviewStatus @@ -521,7 +514,7 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm } sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"]) if sellerTotalPrice <= 0 { - sellerTotalPrice = math.Max(0, listing.Price-consumablePrice) + sellerTotalPrice = math.Max(0, centToYuan(listing.PriceCent)-consumablePrice) } sellerCoinBasePrice := readSummaryNumber(breakdown["seller_coin_base_price"]) if sellerCoinBasePrice <= 0 { @@ -536,13 +529,13 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm return ErrInvalidPrice } - beforePrice := listing.Price + beforePriceCent := listing.PriceCent beforeRatio := readSummaryNumber(breakdown["buyer_ratio"]) - if beforeRatio <= 0 && listing.Price > consumablePrice { - beforeRatio = roundRatio(coinWan / (listing.Price - consumablePrice)) + if beforeRatio <= 0 && centToYuan(listing.PriceCent) > consumablePrice { + beforeRatio = roundRatio(coinWan / (centToYuan(listing.PriceCent) - consumablePrice)) } - listing.Price = buyerTotalPrice + listing.PriceCent = yuanToCent(buyerTotalPrice) summary["publish_ratio"] = buyerRatio breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice) breakdown["seller_total_price"] = roundMoney(sellerTotalPrice) @@ -572,8 +565,8 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm "listing_id": listing.ID, "account_id": account.ID, "owner_id": listing.OwnerID, - "before_price": beforePrice, - "after_price": listing.Price, + "before_price_cent": beforePriceCent, + "after_price_cent": listing.PriceCent, "before_buyer_ratio": beforeRatio, "after_buyer_ratio": buyerRatio, "platform_markup": breakdown["platform_markup_amount"], @@ -783,9 +776,9 @@ func canListPublicWithSQL(query PublicListQuery) bool { func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB { switch sortKey { case "priceAsc": - return db.Order("l.price ASC, l.published_at DESC, l.id DESC") + return db.Order("l.price_cent ASC, l.published_at DESC, l.id DESC") case "priceDesc": - return db.Order("l.price DESC, l.published_at DESC, l.id DESC") + return db.Order("l.price_cent DESC, l.published_at DESC, l.id DESC") case "coinDesc": return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC") default: @@ -1384,16 +1377,16 @@ func rowsToDTO(rows []listingRow) []ListingDTO { return items } -func normalizedListingPrice(req CreateRequest) float64 { +func normalizedListingPriceCent(req CreateRequest) int64 { if req.AssetSummary != nil { if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok { buyerPrice := readSummaryNumber(breakdown["buyer_total_price"]) if buyerPrice > 0 { - return roundMoney(buyerPrice) + return yuanToCent(buyerPrice) } } } - return roundMoney(req.Price) + return req.PriceCent } func publicListings(items []ListingDTO) []ListingDTO { @@ -1620,8 +1613,8 @@ func ensurePriceBreakdown(summary map[string]any) map[string]any { } func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) { - if req.BuyerTotalPrice > 0 { - buyerTotalPrice := roundMoney(req.BuyerTotalPrice) + if req.BuyerTotalPriceCent > 0 { + buyerTotalPrice := roundMoney(centToYuan(req.BuyerTotalPriceCent)) buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice) if buyerCoinBasePrice <= 0 || coinWan <= 0 { return 0, 0, 0 @@ -1643,6 +1636,14 @@ func roundRatio(value float64) float64 { return math.Round(value*10) / 10 } +func yuanToCent(value float64) int64 { + return int64(math.Round(roundMoney(value) * 100)) +} + +func centToYuan(value int64) float64 { + return float64(value) / 100 +} + func cleanScreenshotURLs(urls []string) []string { cleaned := make([]string, 0, len(urls)) seen := make(map[string]struct{}, len(urls)) diff --git a/backend/internal/modules/listing/service.go b/backend/internal/modules/listing/service.go index 79813e9..ce29c4a 100644 --- a/backend/internal/modules/listing/service.go +++ b/backend/internal/modules/listing/service.go @@ -252,7 +252,7 @@ func (s *Service) AdjustReviewPrice(adminID uint64, id uint64, req AdminPriceAdj if s.repo == nil { return nil, ErrDependencyUnavailable } - if req.BuyerRatio <= 0 && req.BuyerTotalPrice <= 0 { + if req.BuyerRatio <= 0 && req.BuyerTotalPriceCent <= 0 { return nil, ErrInvalidPrice } return s.repo.AdjustReviewPrice(adminID, id, req, meta) @@ -328,13 +328,13 @@ func validateRequest(req CreateRequest, rules publishRules) error { if strings.TrimSpace(req.ServerRegion) == "" { return ErrMissingServerRegion } - if normalizedListingPrice(req) <= 0 { + if normalizedListingPriceCent(req) <= 0 { return ErrInvalidPrice } - if req.DepositAmount < 0 { + if req.DepositAmountCent < 0 { return ErrInvalidDeposit } - if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmount <= consumables { + if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmountCent <= yuanToCent(consumables) { return ErrDepositTooLow } if req.HafCoinAmount < 0 { @@ -525,16 +525,16 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item }, } return CreateRequest{ - Title: externalUploadTitle(item, insurance, hafCoinM), - Description: "开放接口自动上传,等待后台审核。", - ServerRegion: serverRegionFromLoginMethod(item.LoginMethod), - LoginPlatform: strings.TrimSpace(item.LoginMethod), - RankLevel: strings.TrimSpace(item.Rank), - HafCoinAmount: int64(math.Round(hafCoinM * 1000000)), - AssetSummary: assetSummary, - ScreenshotURLS: []string{defaultUploadScreenshot}, - Price: price, - DepositAmount: item.Deposit, + Title: externalUploadTitle(item, insurance, hafCoinM), + Description: "开放接口自动上传,等待后台审核。", + ServerRegion: serverRegionFromLoginMethod(item.LoginMethod), + LoginPlatform: strings.TrimSpace(item.LoginMethod), + RankLevel: strings.TrimSpace(item.Rank), + HafCoinAmount: int64(math.Round(hafCoinM * 1000000)), + AssetSummary: assetSummary, + ScreenshotURLS: []string{defaultUploadScreenshot}, + PriceCent: yuanToCent(price), + DepositAmountCent: yuanToCent(item.Deposit), } } diff --git a/backend/internal/modules/listing/service_test.go b/backend/internal/modules/listing/service_test.go index 0355ae9..972e34f 100644 --- a/backend/internal/modules/listing/service_test.go +++ b/backend/internal/modules/listing/service_test.go @@ -27,7 +27,7 @@ func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) { } func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) { - base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPrice: 70}, 1000, 20) + base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPriceCent: 7000}, 1000, 20) if base != 50 || total != 70 || ratio != 20 { t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio) } @@ -35,12 +35,12 @@ func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) { func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) { req := CreateRequest{ - Title: "测试账号", - ServerRegion: "烽火地带", - Price: 100, - DepositAmount: 2, - HafCoinAmount: 1000000, - ScreenshotURLS: []string{"https://example.com/a.png"}, + Title: "测试账号", + ServerRegion: "烽火地带", + PriceCent: 10000, + DepositAmountCent: 200, + HafCoinAmount: 1000000, + ScreenshotURLS: []string{"https://example.com/a.png"}, AssetSummary: map[string]any{ "resources": []any{ map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"}, @@ -52,7 +52,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) { t.Fatalf("expected ErrDepositTooLow, got %v", err) } - req.DepositAmount = 3 + req.DepositAmountCent = 300 if err := validateRequest(req, publishRules{}); err != nil { t.Fatalf("expected valid request, got %v", err) } @@ -159,8 +159,8 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) { if req.HafCoinAmount != 197100000 { t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount) } - if req.Price != 458 || req.DepositAmount != 400 { - t.Fatalf("unexpected price/deposit %.2f/%.2f", req.Price, req.DepositAmount) + if req.PriceCent != 45800 || req.DepositAmountCent != 40000 { + t.Fatalf("unexpected price/deposit cent %d/%d", req.PriceCent, req.DepositAmountCent) } if req.AssetSummary["season_insurance"] != "3*3" { t.Fatalf("expected 3*3 insurance, got %#v", req.AssetSummary["season_insurance"]) diff --git a/backend/internal/modules/order/dto.go b/backend/internal/modules/order/dto.go index 42f25b5..1a90418 100644 --- a/backend/internal/modules/order/dto.go +++ b/backend/internal/modules/order/dto.go @@ -54,20 +54,20 @@ type SubmitReturnRequest struct { } type SubmitCheckoutRequest struct { - Content string `json:"content" binding:"required"` - ConsumableAmount float64 `json:"consumable_amount"` - CoinConsumedM float64 `json:"coin_consumed_m"` - OtherAmount float64 `json:"other_amount"` - EvidenceURLS []string `json:"evidence_urls"` + Content string `json:"content" binding:"required"` + ConsumableAmountCent int64 `json:"consumable_amount_cent"` + CoinConsumedM float64 `json:"coin_consumed_m"` + OtherAmountCent int64 `json:"other_amount_cent"` + EvidenceURLS []string `json:"evidence_urls"` } type CounterCheckoutRequest struct { - ConsumableAmount float64 `json:"consumable_amount"` - CoinConsumedM float64 `json:"coin_consumed_m"` - OtherAmount float64 `json:"other_amount"` - DepositDeductAmount float64 `json:"deposit_deduct_amount"` - Reason string `json:"reason" binding:"required"` - EvidenceURLS []string `json:"evidence_urls"` + ConsumableAmountCent int64 `json:"consumable_amount_cent"` + CoinConsumedM float64 `json:"coin_consumed_m"` + OtherAmountCent int64 `json:"other_amount_cent"` + DepositDeductAmountCent int64 `json:"deposit_deduct_amount_cent"` + Reason string `json:"reason" binding:"required"` + EvidenceURLS []string `json:"evidence_urls"` } type AdminActionRequest struct { diff --git a/backend/internal/modules/order/repository.go b/backend/internal/modules/order/repository.go index 4cf48f1..1c3fa05 100644 --- a/backend/internal/modules/order/repository.go +++ b/backend/internal/modules/order/repository.go @@ -77,22 +77,22 @@ func orderDurationHours(order model.RentalOrder) int { } 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 + rentAmountCent := listing.PriceCent + ownerRentAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100)) + if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent { + ownerRentAmountCent = rentAmountCent } - platformFee := readSnapshotPrice(account.AssetSummary, "platform_markup_amount") - if platformFee <= 0 || roundMoney(ownerRentAmount+platformFee) != rentAmount { - platformFee = roundMoney(rentAmount - ownerRentAmount) + platformFeeCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "platform_markup_amount") * 100)) + if platformFeeCent <= 0 || ownerRentAmountCent+platformFeeCent != rentAmountCent { + platformFeeCent = rentAmountCent - ownerRentAmountCent } - if platformFee < 0 { - platformFee = 0 + if platformFeeCent < 0 { + platformFeeCent = 0 } return orderPricing{ - RentAmountCent: int64(math.Round(rentAmount * 100)), - OwnerRentAmountCent: int64(math.Round(ownerRentAmount * 100)), - PlatformFeeCent: int64(math.Round(platformFee * 100)), + RentAmountCent: rentAmountCent, + OwnerRentAmountCent: ownerRentAmountCent, + PlatformFeeCent: platformFeeCent, } } @@ -173,8 +173,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro } rentHours := internalOrderHours pricing := buildOrderPricing(listing, account) - depositOriginalAmount := roundMoney(listing.DepositAmount) - paidDepositAmount, waivedDepositAmount, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmount) + depositOriginalAmountCent := listing.DepositAmountCent + paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent) if err != nil { return err } @@ -185,17 +185,11 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro OwnerID: listing.OwnerID, RenterID: renterID, EstimatedDurationHours: rentHours, - RentAmount: float64(pricing.RentAmountCent) / 100, RentAmountCent: pricing.RentAmountCent, - OwnerRentAmount: float64(pricing.OwnerRentAmountCent) / 100, OwnerRentAmountCent: pricing.OwnerRentAmountCent, - DepositAmount: paidDepositAmount, - DepositAmountCent: int64(math.Round(paidDepositAmount * 100)), - DepositOriginalAmount: depositOriginalAmount, - DepositOriginalAmountCent: int64(math.Round(depositOriginalAmount * 100)), - DepositWaivedAmount: waivedDepositAmount, - DepositWaivedAmountCent: int64(math.Round(waivedDepositAmount * 100)), - PlatformFee: float64(pricing.PlatformFeeCent) / 100, + DepositAmountCent: paidDepositAmountCent, + DepositOriginalAmountCent: depositOriginalAmountCent, + DepositWaivedAmountCent: waivedDepositAmountCent, PlatformFeeCent: pricing.PlatformFeeCent, AccountSnapshot: snapshot, Status: "pending_payment", @@ -231,9 +225,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro return r.FindForUser(renterID, createdID) } -func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDeposit float64) (float64, float64, error) { - originalDeposit = roundMoney(originalDeposit) - if originalDeposit <= 0 { +func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64) (int64, int64, error) { + if originalDepositCent <= 0 { return 0, 0, nil } var user model.User @@ -244,28 +237,27 @@ func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, origin if err != nil { return 0, 0, err } - paidDeposit, waivedDeposit := calculateDepositWaiver(originalDeposit, user.DepositFreeQuota, used) + paidDeposit, waivedDeposit := calculateDepositWaiver(originalDepositCent, user.DepositFreeQuotaCent, used) return paidDeposit, waivedDeposit, nil } -func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (float64, error) { - var used float64 +func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (int64, error) { + var used int64 err := tx.Model(&model.RentalOrder{}). Where("renter_id = ? AND status NOT IN ?", renterID, []string{"completed", "cancelled", "closed"}, ). - Select("COALESCE(SUM(deposit_waived_amount), 0)"). + Select("COALESCE(SUM(deposit_waived_amount_cent), 0)"). Scan(&used).Error - return roundMoney(used), err + return used, err } -func calculateDepositWaiver(originalDeposit float64, quota float64, used float64) (float64, float64) { - originalDeposit = roundMoney(originalDeposit) - remaining := maxMoney(roundMoney(quota)-roundMoney(used), 0) - waived := minMoney(originalDeposit, remaining) - paid := maxMoney(originalDeposit-waived, 0) - return roundMoney(paid), roundMoney(waived) +func calculateDepositWaiver(originalDepositCent int64, quotaCent int64, usedCent int64) (int64, int64) { + remaining := maxCent(quotaCent-usedCent, 0) + waived := minCent(originalDepositCent, remaining) + paid := maxCent(originalDepositCent-waived, 0) + return paid, waived } // Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。 @@ -554,7 +546,7 @@ func (r *Repository) SubmitCheckout(userID uint64, orderID uint64, req SubmitChe if err := tx.Create(&record).Error; err != nil { return err } - checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, 0, false) + checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, 0, false) if err != nil { return err } @@ -650,18 +642,22 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC First(&checkout).Error; err != nil { return err } - next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, req.DepositDeductAmount, true) + next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.DepositDeductAmountCent, true) if err != nil { return err } now := time.Now() checkout.Status = "countered" - checkout.ConsumableAmount = next.ConsumableAmount + checkout.RentAmountCent = next.RentAmountCent + checkout.OwnerRentAmountCent = next.OwnerRentAmountCent + checkout.PlatformFeeCent = next.PlatformFeeCent + checkout.DepositAmountCent = next.DepositAmountCent + checkout.ConsumableAmountCent = next.ConsumableAmountCent checkout.CoinConsumedM = next.CoinConsumedM - checkout.OtherAmount = next.OtherAmount - checkout.DepositDeductAmount = next.DepositDeductAmount - checkout.RenterRefundAmount = next.RenterRefundAmount - checkout.OwnerIncomeAmount = next.OwnerIncomeAmount + checkout.OtherAmountCent = next.OtherAmountCent + checkout.DepositDeductAmountCent = next.DepositDeductAmountCent + checkout.RenterRefundAmountCent = next.RenterRefundAmountCent + checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent checkout.OwnerAdjustmentReason = req.Reason checkout.OwnerAdjustedAt = &now checkout.EvidenceURLS = next.EvidenceURLS @@ -697,9 +693,9 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC } dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{ OwnerID: userID, - RentAmountCent: int64(math.Round(checkout.RentAmount * 100)), - OwnerRentAmountCent: int64(math.Round(checkout.OwnerRentAmount * 100)), - DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)), + RentAmountCent: checkout.RentAmountCent, + OwnerRentAmountCent: checkout.OwnerRentAmountCent, + DepositAmountCent: checkout.DepositAmountCent, }) return &dto, nil } @@ -1095,11 +1091,11 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che refund = action } - checkout.RentAmount = float64(settlement.ActualRentAmountCent) / 100 - checkout.OwnerRentAmount = float64(settlement.OwnerRentIncomeCent) / 100 - checkout.PlatformFee = float64(settlement.PlatformFeeCent) / 100 - checkout.RenterRefundAmount = float64(settlement.RenterRefundCent) / 100 - checkout.OwnerIncomeAmount = float64(settlement.OwnerIncomeCent) / 100 + checkout.RentAmountCent = settlement.ActualRentAmountCent + checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent + checkout.PlatformFeeCent = settlement.PlatformFeeCent + checkout.RenterRefundAmountCent = settlement.RenterRefundCent + checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, @@ -1229,22 +1225,18 @@ func applyPaymentDeadline(dto *OrderDTO, order model.RentalOrder, timeoutMinutes dto.PaymentDeadlineAt = &deadline } -func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmount float64, coinConsumedM float64, otherAmount float64, explicitDeduct float64, useExplicitDeduct bool) (model.OrderCheckout, error) { - if consumableAmount < 0 || coinConsumedM < 0 || otherAmount < 0 || explicitDeduct < 0 { +func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmountCent int64, coinConsumedM float64, otherAmountCent int64, explicitDeductCent int64, useExplicitDeduct bool) (model.OrderCheckout, error) { + if consumableAmountCent < 0 || coinConsumedM < 0 || otherAmountCent < 0 || explicitDeductCent < 0 { return model.OrderCheckout{}, ErrInvalidCheckoutAmount } - consumableAmount = roundMoney(consumableAmount) - otherAmount = roundMoney(otherAmount) - explicitDeduct = roundMoney(explicitDeduct) - deductAmount := otherAmount + deductAmountCent := otherAmountCent if useExplicitDeduct { - deductAmount = explicitDeduct + deductAmountCent = explicitDeductCent } - depositAmountFromCent := float64(order.DepositAmountCent) / 100 - if deductAmount > depositAmountFromCent { + if deductAmountCent > order.DepositAmountCent { return model.OrderCheckout{}, ErrInvalidCheckoutAmount } - settlement := calculateCheckoutSettlement(order, consumableAmount, roundQuantity(coinConsumedM), deductAmount) + settlement := calculateCheckoutSettlement(order, consumableAmountCent, roundQuantity(coinConsumedM), deductAmountCent) evidence, err := marshalStringList(evidenceURLS) if err != nil { return model.OrderCheckout{}, err @@ -1253,24 +1245,15 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c OrderID: order.ID, InitiatedBy: initiatedBy, Status: status, - RentAmount: float64(settlement.ActualRentAmountCent) / 100, RentAmountCent: settlement.ActualRentAmountCent, - OwnerRentAmount: float64(settlement.OwnerRentIncomeCent) / 100, OwnerRentAmountCent: settlement.OwnerRentIncomeCent, - PlatformFee: float64(settlement.PlatformFeeCent) / 100, PlatformFeeCent: settlement.PlatformFeeCent, - DepositAmount: depositAmountFromCent, DepositAmountCent: order.DepositAmountCent, - ConsumableAmount: consumableAmount, - ConsumableAmountCent: int64(math.Round(consumableAmount * 100)), + ConsumableAmountCent: consumableAmountCent, CoinConsumedM: roundQuantity(coinConsumedM), - OtherAmount: otherAmount, - OtherAmountCent: int64(math.Round(otherAmount * 100)), - DepositDeductAmount: roundMoney(deductAmount), + OtherAmountCent: otherAmountCent, DepositDeductAmountCent: settlement.DepositCompensationCent, - RenterRefundAmount: float64(settlement.RenterRefundCent) / 100, RenterRefundAmountCent: settlement.RenterRefundCent, - OwnerIncomeAmount: float64(settlement.OwnerIncomeCent) / 100, OwnerIncomeAmountCent: settlement.OwnerIncomeCent, Content: content, EvidenceURLS: evidence, @@ -1278,57 +1261,55 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c } func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement { - return calculateCheckoutSettlement(order, checkout.ConsumableAmount, checkout.CoinConsumedM, checkout.DepositDeductAmount) + return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent) } -func calculateCheckoutSettlement(order model.RentalOrder, consumableAmount float64, coinConsumedM float64, depositDeductAmount float64) checkoutSettlement { - // 从分字段读取,转为元进行计算(保持现有逻辑兼容性) - orderRentAmount := float64(order.RentAmountCent) / 100 - orderOwnerRentAmount := float64(order.OwnerRentAmountCent) / 100 - orderDepositAmount := float64(order.DepositAmountCent) / 100 +func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement { + orderRentAmountCent := order.RentAmountCent + orderOwnerRentAmountCent := order.OwnerRentAmountCent + orderDepositAmountCent := order.DepositAmountCent - buyerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") - if buyerCoinBasePrice <= 0 || buyerCoinBasePrice > orderRentAmount { - buyerCoinBasePrice = orderRentAmount + buyerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") * 100)) + if buyerCoinBasePriceCent <= 0 || buyerCoinBasePriceCent > orderRentAmountCent { + buyerCoinBasePriceCent = orderRentAmountCent } - sellerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") - if sellerCoinBasePrice <= 0 || sellerCoinBasePrice > orderOwnerRentAmount { - sellerCoinBasePrice = orderOwnerRentAmount + sellerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") * 100)) + if sellerCoinBasePriceCent <= 0 || sellerCoinBasePriceCent > orderOwnerRentAmountCent { + sellerCoinBasePriceCent = orderOwnerRentAmountCent } - prepaidConsumablePrice := readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") - if prepaidConsumablePrice <= 0 || prepaidConsumablePrice > orderRentAmount-buyerCoinBasePrice { - prepaidConsumablePrice = maxMoney(orderRentAmount-buyerCoinBasePrice, 0) + prepaidConsumablePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") * 100)) + if prepaidConsumablePriceCent <= 0 || prepaidConsumablePriceCent > orderRentAmountCent-buyerCoinBasePriceCent { + prepaidConsumablePriceCent = maxCent(orderRentAmountCent-buyerCoinBasePriceCent, 0) } - prepaidOwnerConsumablePrice := maxMoney(orderOwnerRentAmount-sellerCoinBasePrice, 0) + prepaidOwnerConsumablePriceCent := maxCent(orderOwnerRentAmountCent-sellerCoinBasePriceCent, 0) totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot) coinUseRatio := 1.0 if totalCoinM > 0 { coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1) } - usedBuyerCoinPrice := roundMoney(buyerCoinBasePrice * coinUseRatio) - usedOwnerCoinPrice := roundMoney(sellerCoinBasePrice * coinUseRatio) - usedBuyerConsumablePrice := minMoney(roundMoney(consumableAmount), prepaidConsumablePrice) + usedBuyerCoinPriceCent := int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio)) + usedOwnerCoinPriceCent := int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio)) + usedBuyerConsumablePriceCent := minCent(consumableAmountCent, prepaidConsumablePriceCent) consumableUseRatio := 1.0 - if prepaidConsumablePrice > 0 { - consumableUseRatio = minRatio(maxRatio(usedBuyerConsumablePrice/prepaidConsumablePrice, 0), 1) + if prepaidConsumablePriceCent > 0 { + consumableUseRatio = minRatio(maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0), 1) } - usedOwnerConsumablePrice := roundMoney(prepaidOwnerConsumablePrice * consumableUseRatio) - actualRentAmount := minMoney(roundMoney(usedBuyerCoinPrice+usedBuyerConsumablePrice), orderRentAmount) - ownerRentIncome := minMoney(roundMoney(usedOwnerCoinPrice+usedOwnerConsumablePrice), orderOwnerRentAmount) - depositCompensation := minMoney(roundMoney(depositDeductAmount), orderDepositAmount) - rentRefund := maxMoney(orderRentAmount-actualRentAmount, 0) - depositRefund := maxMoney(orderDepositAmount-depositCompensation, 0) + usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio)) + actualRentAmountCent := minCent(usedBuyerCoinPriceCent+usedBuyerConsumablePriceCent, orderRentAmountCent) + ownerRentIncomeCent := minCent(usedOwnerCoinPriceCent+usedOwnerConsumablePriceCent, orderOwnerRentAmountCent) + depositCompensationCent := minCent(depositDeductAmountCent, orderDepositAmountCent) + rentRefundCent := maxCent(orderRentAmountCent-actualRentAmountCent, 0) + depositRefundCent := maxCent(orderDepositAmountCent-depositCompensationCent, 0) - // 最后转换为分返回 return checkoutSettlement{ - OwnerRentIncomeCent: int64(math.Round(ownerRentIncome * 100)), - DepositCompensationCent: int64(math.Round(depositCompensation * 100)), - OwnerIncomeCent: int64(math.Round((ownerRentIncome + depositCompensation) * 100)), - RentRefundCent: int64(math.Round(rentRefund * 100)), - DepositRefundCent: int64(math.Round(depositRefund * 100)), - RenterRefundCent: int64(math.Round((rentRefund + depositRefund) * 100)), - PlatformFeeCent: int64(math.Round(maxMoney(actualRentAmount-ownerRentIncome, 0) * 100)), - ActualRentAmountCent: int64(math.Round(actualRentAmount * 100)), + OwnerRentIncomeCent: ownerRentIncomeCent, + DepositCompensationCent: depositCompensationCent, + OwnerIncomeCent: ownerRentIncomeCent + depositCompensationCent, + RentRefundCent: rentRefundCent, + DepositRefundCent: depositRefundCent, + RenterRefundCent: rentRefundCent + depositRefundCent, + PlatformFeeCent: maxCent(actualRentAmountCent-ownerRentIncomeCent, 0), + ActualRentAmountCent: actualRentAmountCent, } } @@ -1381,6 +1362,20 @@ func maxMoney(a float64, b float64) float64 { return money.Max(a, b) } +func minCent(a int64, b int64) int64 { + if a < b { + return a + } + return b +} + +func maxCent(a int64, b int64) int64 { + if a > b { + return a + } + return b +} + func minRatio(a float64, b float64) float64 { if a < b { return a @@ -1522,13 +1517,6 @@ func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO { } } -func effectiveDepositOriginalAmount(order model.RentalOrder) float64 { - if order.DepositOriginalAmount > 0 { - return order.DepositOriginalAmount - } - return order.DepositAmount -} - func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 { if order.DepositOriginalAmountCent > 0 { return order.DepositOriginalAmountCent @@ -1537,11 +1525,11 @@ func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 { } func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO { - rentAmountCent := int64(math.Round(checkout.RentAmount * 100)) - ownerRentAmountCent := int64(math.Round(checkout.OwnerRentAmount * 100)) - platformFeeCent := int64(math.Round(checkout.PlatformFee * 100)) - renterRefundAmountCent := int64(math.Round(checkout.RenterRefundAmount * 100)) - ownerIncomeAmountCent := int64(math.Round(checkout.OwnerIncomeAmount * 100)) + rentAmountCent := checkout.RentAmountCent + ownerRentAmountCent := checkout.OwnerRentAmountCent + platformFeeCent := checkout.PlatformFeeCent + renterRefundAmountCent := checkout.RenterRefundAmountCent + ownerIncomeAmountCent := checkout.OwnerIncomeAmountCent return CheckoutDTO{ ID: checkout.ID, OrderID: checkout.OrderID, @@ -1552,11 +1540,11 @@ func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO { RentAmountCent: &rentAmountCent, OwnerRentAmountCent: &ownerRentAmountCent, PlatformFeeCent: &platformFeeCent, - DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)), - ConsumableAmountCent: int64(math.Round(checkout.ConsumableAmount * 100)), + DepositAmountCent: checkout.DepositAmountCent, + ConsumableAmountCent: checkout.ConsumableAmountCent, CoinConsumedM: checkout.CoinConsumedM, - OtherAmountCent: int64(math.Round(checkout.OtherAmount * 100)), - DepositDeductAmountCent: int64(math.Round(checkout.DepositDeductAmount * 100)), + OtherAmountCent: checkout.OtherAmountCent, + DepositDeductAmountCent: checkout.DepositDeductAmountCent, RenterRefundAmountCent: &renterRefundAmountCent, OwnerIncomeAmountCent: &ownerIncomeAmountCent, Content: checkout.Content, diff --git a/backend/internal/modules/order/repository_test.go b/backend/internal/modules/order/repository_test.go index fe2e408..e08274b 100644 --- a/backend/internal/modules/order/repository_test.go +++ b/backend/internal/modules/order/repository_test.go @@ -27,7 +27,7 @@ func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) { }`)), } - settlement := calculateCheckoutSettlement(order, 7, 90, 0) + settlement := calculateCheckoutSettlement(order, 700, 90, 0) // 角精度:243.7 = roundMoney(236.7 + 7) if settlement.ActualRentAmountCent != 24370 { @@ -104,7 +104,7 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) { }`)), } - settlement := calculateCheckoutSettlement(order, 120, 100, 30) + settlement := calculateCheckoutSettlement(order, 12000, 100, 3000) if settlement.ActualRentAmountCent != 38300 { t.Fatalf("ActualRentAmountCent = %d, want 38300", settlement.ActualRentAmountCent) @@ -126,17 +126,17 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) { func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) { paid, waived := calculateDepositWaiver(500, 300, 0) if paid != 200 || waived != 300 { - t.Fatalf("paid = %.1f, waived = %.1f, want 200.0, 300.0", paid, waived) + t.Fatalf("paid = %d, waived = %d, want 200, 300", paid, waived) } paid, waived = calculateDepositWaiver(500, 300, 200) if paid != 400 || waived != 100 { - t.Fatalf("paid = %.1f, waived = %.1f, want 400.0, 100.0", paid, waived) + t.Fatalf("paid = %d, waived = %d, want 400, 100", paid, waived) } paid, waived = calculateDepositWaiver(500, 300, 300) if paid != 500 || waived != 0 { - t.Fatalf("paid = %.1f, waived = %.1f, want 500.0, 0.0", paid, waived) + t.Fatalf("paid = %d, waived = %d, want 500, 0", paid, waived) } } diff --git a/backend/internal/modules/payment/dto.go b/backend/internal/modules/payment/dto.go index e871f88..d748909 100644 --- a/backend/internal/modules/payment/dto.go +++ b/backend/internal/modules/payment/dto.go @@ -12,9 +12,9 @@ type StartPaymentRequest struct { } type WalletRechargePaymentRequest struct { - Amount float64 `json:"amount"` - PayWay string `json:"pay_way"` - JSPayFlag string `json:"jspay_flag"` + AmountCent int64 `json:"amount_cent"` + PayWay string `json:"pay_way"` + JSPayFlag string `json:"jspay_flag"` } type PaymentDTO struct { diff --git a/backend/internal/modules/payment/repository.go b/backend/internal/modules/payment/repository.go index aa922db..c980111 100644 --- a/backend/internal/modules/payment/repository.go +++ b/backend/internal/modules/payment/repository.go @@ -229,8 +229,8 @@ func (r *Repository) Start(userID uint64, orderID uint64, req StartPaymentReques } func (r *Repository) StartWalletRecharge(userID uint64, req WalletRechargePaymentRequest, clientIP string) (*PaymentDTO, error) { - amountCent := moneyCent(req.Amount) - if userID == 0 || req.Amount < MinWalletRechargeAmount || amountCent <= 0 { + amountCent := req.AmountCent + if userID == 0 || amountCent < moneyCent(MinWalletRechargeAmount) { return nil, ErrPaymentCannotStart } runtimeConfig, err := r.defaultRuntimeConfig() @@ -764,7 +764,7 @@ func (r *Repository) preparePayment(userID uint64, orderID uint64, req StartPaym if row.Status != "pending_payment" { return ErrPaymentCannotStart } - amountCent := moneyCent(row.RentAmount + row.DepositAmount) + amountCent := row.RentAmountCent + row.DepositAmountCent if amountCent <= 0 { return ErrPaymentCannotStart } diff --git a/backend/internal/modules/withdrawal/repository.go b/backend/internal/modules/withdrawal/repository.go index 9397bf6..49b491f 100644 --- a/backend/internal/modules/withdrawal/repository.go +++ b/backend/internal/modules/withdrawal/repository.go @@ -58,11 +58,8 @@ func (r *Repository) Create(userID uint64, req CreateWithdrawalRequest) (*Withdr withdrawal := model.WithdrawalRequest{ WithdrawNo: withdrawNo, UserID: userID, - Amount: float64(req.AmountCent) / 100, AmountCent: req.AmountCent, - Fee: float64(feeCent) / 100, FeeCent: feeCent, - ActualAmount: float64(actualAmountCent) / 100, ActualAmountCent: actualAmountCent, PaymentAccountID: &req.PaymentAccountID, AccountType: paymentAccount.AccountType, diff --git a/backend/migrations/000001_init.sql b/backend/migrations/000001_init.sql index 1b1d315..ec47c5c 100644 --- a/backend/migrations/000001_init.sql +++ b/backend/migrations/000001_init.sql @@ -22,7 +22,6 @@ CREATE TABLE IF NOT EXISTS users ( realname_status VARCHAR(32) NOT NULL DEFAULT 'unverified' COMMENT '实名状态: unverified未实名, pending审核中, verified已实名, failed失败', risk_status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '风控状态: normal正常, warning警告, frozen冻结', credit_score INT NOT NULL DEFAULT 100 COMMENT '信用分', - deposit_free_quota DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '免押总额度', deposit_free_quota_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押总额度(分)', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '账号状态: active活跃, inactive停用, banned封禁', last_login_at DATETIME NULL COMMENT '最后登录时间', @@ -82,9 +81,7 @@ CREATE TABLE IF NOT EXISTS rental_listings ( listing_no VARCHAR(20) NOT NULL COMMENT '商品编号,格式yyyyMMddNNNN', account_id BIGINT UNSIGNED NOT NULL COMMENT '关联的游戏账号ID', owner_id BIGINT UNSIGNED NOT NULL COMMENT '号主ID', - price DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金(元/小时)', price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分/小时)', - deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金金额', deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)', in_transaction TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否正在交易中: 0否, 1是', status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '商品状态: draft草稿, active上架, offline下架, deleted删除', @@ -96,7 +93,7 @@ CREATE TABLE IF NOT EXISTS rental_listings ( UNIQUE KEY uk_rental_listings_listing_no (listing_no), KEY idx_rental_listings_account_id (account_id), KEY idx_rental_listings_owner_id (owner_id), - KEY idx_rental_listings_filter (status, review_status, in_transaction, price), + KEY idx_rental_listings_filter (status, review_status, in_transaction, price_cent), KEY idx_rental_listings_published (status, review_status, published_at DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租号商品列表'; @@ -122,17 +119,11 @@ CREATE TABLE IF NOT EXISTS rental_orders ( renter_id BIGINT UNSIGNED NOT NULL COMMENT '租客ID', rented_at DATETIME NULL COMMENT '租用开始时间', estimated_duration_hours INT NOT NULL DEFAULT 24 COMMENT '预计租用时长(小时)', - rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金总额', rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金总额(分)', - owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主实得租金', owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)', - deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '实际收取押金金额', deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际收取押金(分)', - deposit_original_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品原始押金金额', deposit_original_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '商品原始押金(分)', - deposit_waived_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '本单免押抵扣金额', deposit_waived_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押抵扣金额(分)', - platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '平台手续费', platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)', account_snapshot JSON NULL COMMENT '账号快照(下单时的账号状态)', status VARCHAR(32) NOT NULL DEFAULT 'pending_payment' COMMENT '订单状态: pending_payment待支付, active进行中, completed已完成, cancelled已取消, closed已关闭', @@ -161,24 +152,15 @@ CREATE TABLE IF NOT EXISTS order_checkouts ( order_id BIGINT UNSIGNED NOT NULL COMMENT '订单ID', initiated_by BIGINT UNSIGNED NOT NULL COMMENT '发起者ID', status VARCHAR(32) NOT NULL DEFAULT 'submitted' COMMENT '结算状态: submitted已提交, renter_confirmed租客确认, renter_rejected租客拒绝, completed完成', - rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金', rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分)', - owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主实得租金', owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)', - platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '平台手续费', platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)', - deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金', deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)', - consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '消耗品扣费', consumable_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '消耗品扣费(分)', coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '游戏币消耗(百万为单位)', - other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '其他费用', other_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '其他费用(分)', - deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金扣除金额', deposit_deduct_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金扣除金额(分)', - renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租客退款金额', renter_refund_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租客退款金额(分)', - owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主收入金额', owner_income_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主收入金额(分)', content TEXT NULL COMMENT '结算说明', evidence_urls JSON NULL COMMENT '证据截图URL列表', @@ -215,9 +197,7 @@ CREATE TABLE IF NOT EXISTS handoff_records ( CREATE TABLE IF NOT EXISTS wallet_accounts ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', - available_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额', available_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '可用余额(分)', - frozen_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额', frozen_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额(分)', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '钱包状态: active正常, frozen冻结, closed关闭', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -232,9 +212,7 @@ CREATE TABLE IF NOT EXISTS wallet_ledger ( user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', order_id BIGINT UNSIGNED NULL COMMENT '关联订单ID', direction VARCHAR(16) NOT NULL COMMENT '方向: in收入, out支出', - amount DECIMAL(12,2) NOT NULL COMMENT '金额', amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)', - balance_after DECIMAL(12,2) NOT NULL COMMENT '变动后余额', balance_after_cent BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额(分)', balance_type VARCHAR(32) NOT NULL COMMENT '余额类型: available可用, frozen冻结', biz_type VARCHAR(32) NOT NULL COMMENT '业务类型: rent_payment租金支付, deposit_freeze押金冻结, settlement结算, refund退款等', @@ -839,11 +817,8 @@ CREATE TABLE IF NOT EXISTS withdrawal_requests ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, withdraw_no VARCHAR(64) NOT NULL COMMENT '提现单号', user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', - amount DECIMAL(12,2) NOT NULL COMMENT '提现金额', amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '提现金额(分)', - fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '手续费', fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '手续费(分)', - actual_amount DECIMAL(12,2) NOT NULL COMMENT '实际到账金额', actual_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际到账(分)', -- 收款账号信息(快照) diff --git a/backend/migrations/20260609052734_add_amount_cent_fields.sql b/backend/migrations/20260609052734_add_amount_cent_fields.sql deleted file mode 100644 index 954841b..0000000 --- a/backend/migrations/20260609052734_add_amount_cent_fields.sql +++ /dev/null @@ -1,190 +0,0 @@ --- +goose Up --- +goose StatementBegin --- ============================================ --- 金额统一重构:新增分字段(BIGINT) --- 目标:所有金额存储使用整数分,避免浮点精度问题 --- 展示:统一到角精度(0.1元) --- ============================================ - --- 用户表:免押额度 -ALTER TABLE users - ADD COLUMN IF NOT EXISTS deposit_free_quota_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押总额度(分)' AFTER deposit_free_quota; - --- 商品表:价格和押金 -ALTER TABLE rental_listings - ADD COLUMN IF NOT EXISTS price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分/小时)' AFTER price, - ADD COLUMN IF NOT EXISTS deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)' AFTER deposit_amount; - --- 订单表:租金、押金、平台费 -ALTER TABLE rental_orders - ADD COLUMN IF NOT EXISTS rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金总额(分)' AFTER rent_amount, - ADD COLUMN IF NOT EXISTS owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)' AFTER owner_rent_amount, - ADD COLUMN IF NOT EXISTS deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际收取押金(分)' AFTER deposit_amount, - ADD COLUMN IF NOT EXISTS deposit_original_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '商品原始押金(分)' AFTER deposit_original_amount, - ADD COLUMN IF NOT EXISTS deposit_waived_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押抵扣金额(分)' AFTER deposit_waived_amount, - ADD COLUMN IF NOT EXISTS platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)' AFTER platform_fee; - --- 结算记录表:所有金额字段 -ALTER TABLE order_checkouts - ADD COLUMN IF NOT EXISTS rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分)' AFTER rent_amount, - ADD COLUMN IF NOT EXISTS owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)' AFTER owner_rent_amount, - ADD COLUMN IF NOT EXISTS platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)' AFTER platform_fee, - ADD COLUMN IF NOT EXISTS deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)' AFTER deposit_amount, - ADD COLUMN IF NOT EXISTS consumable_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '消耗品扣费(分)' AFTER consumable_amount, - ADD COLUMN IF NOT EXISTS other_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '其他费用(分)' AFTER other_amount, - ADD COLUMN IF NOT EXISTS deposit_deduct_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金扣除金额(分)' AFTER deposit_deduct_amount, - ADD COLUMN IF NOT EXISTS renter_refund_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租客退款金额(分)' AFTER renter_refund_amount, - ADD COLUMN IF NOT EXISTS owner_income_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主收入金额(分)' AFTER owner_income_amount; - --- 钱包账户表:余额 -ALTER TABLE wallet_accounts - ADD COLUMN IF NOT EXISTS available_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '可用余额(分)' AFTER available_balance, - ADD COLUMN IF NOT EXISTS frozen_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额(分)' AFTER frozen_balance; - --- 钱包流水表:金额和余额 -ALTER TABLE wallet_ledger - ADD COLUMN IF NOT EXISTS amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)' AFTER amount, - ADD COLUMN IF NOT EXISTS balance_after_cent BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额(分)' AFTER balance_after; - --- 提现表是可选模块;仅在表存在时修改,避免缺表环境迁移失败。 -SET @withdrawal_requests_exists := ( - SELECT COUNT(*) - FROM information_schema.tables - WHERE table_schema = DATABASE() - AND table_name = 'withdrawal_requests' -); -SET @sql := IF(@withdrawal_requests_exists > 0, - 'ALTER TABLE withdrawal_requests - ADD COLUMN IF NOT EXISTS amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT ''提现金额(分)'' AFTER amount, - ADD COLUMN IF NOT EXISTS fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT ''手续费(分)'' AFTER fee, - ADD COLUMN IF NOT EXISTS actual_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT ''实际到账(分)'' AFTER actual_amount', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- ============================================ --- 数据迁移:从 DECIMAL 复制到 BIGINT(分) --- 注意:ROUND(value * 100) 确保精度 --- ============================================ - --- 用户免押额度 -UPDATE users -SET deposit_free_quota_cent = ROUND(deposit_free_quota * 100) -WHERE deposit_free_quota > 0; - --- 商品价格和押金 -UPDATE rental_listings -SET price_cent = ROUND(price * 100), - deposit_amount_cent = ROUND(deposit_amount * 100) -WHERE price > 0 OR deposit_amount > 0; - --- 订单金额 -UPDATE rental_orders -SET rent_amount_cent = ROUND(rent_amount * 100), - owner_rent_amount_cent = ROUND(owner_rent_amount * 100), - deposit_amount_cent = ROUND(deposit_amount * 100), - deposit_original_amount_cent = ROUND(deposit_original_amount * 100), - deposit_waived_amount_cent = ROUND(deposit_waived_amount * 100), - platform_fee_cent = ROUND(platform_fee * 100) -WHERE rent_amount > 0 OR owner_rent_amount > 0 OR deposit_amount > 0 - OR deposit_original_amount > 0 OR deposit_waived_amount > 0 OR platform_fee > 0; - --- 结算记录 -UPDATE order_checkouts -SET rent_amount_cent = ROUND(rent_amount * 100), - owner_rent_amount_cent = ROUND(owner_rent_amount * 100), - platform_fee_cent = ROUND(platform_fee * 100), - deposit_amount_cent = ROUND(deposit_amount * 100), - consumable_amount_cent = ROUND(consumable_amount * 100), - other_amount_cent = ROUND(other_amount * 100), - deposit_deduct_amount_cent = ROUND(deposit_deduct_amount * 100), - renter_refund_amount_cent = ROUND(renter_refund_amount * 100), - owner_income_amount_cent = ROUND(owner_income_amount * 100) -WHERE rent_amount > 0 OR owner_rent_amount > 0 OR platform_fee > 0 - OR deposit_amount > 0 OR consumable_amount > 0 OR other_amount > 0 - OR deposit_deduct_amount > 0 OR renter_refund_amount > 0 OR owner_income_amount > 0; - --- 钱包余额 -UPDATE wallet_accounts -SET available_balance_cent = ROUND(available_balance * 100), - frozen_balance_cent = ROUND(frozen_balance * 100) -WHERE available_balance > 0 OR frozen_balance > 0; - --- 钱包流水 -UPDATE wallet_ledger -SET amount_cent = ROUND(amount * 100), - balance_after_cent = ROUND(balance_after * 100) -WHERE amount != 0 OR balance_after != 0; - --- 提现记录 -SET @sql := IF(@withdrawal_requests_exists > 0, - 'UPDATE withdrawal_requests - SET amount_cent = ROUND(amount * 100), - fee_cent = ROUND(fee * 100), - actual_amount_cent = ROUND(actual_amount * 100) - WHERE amount > 0 OR fee > 0 OR actual_amount > 0', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; --- +goose StatementEnd - --- +goose Down --- +goose StatementBegin --- ============================================ --- 回滚:删除所有新增的分字段 --- ============================================ - -ALTER TABLE users DROP COLUMN IF EXISTS deposit_free_quota_cent; - -ALTER TABLE rental_listings - DROP COLUMN IF EXISTS price_cent, - DROP COLUMN IF EXISTS deposit_amount_cent; - -ALTER TABLE rental_orders - DROP COLUMN IF EXISTS rent_amount_cent, - DROP COLUMN IF EXISTS owner_rent_amount_cent, - DROP COLUMN IF EXISTS deposit_amount_cent, - DROP COLUMN IF EXISTS deposit_original_amount_cent, - DROP COLUMN IF EXISTS deposit_waived_amount_cent, - DROP COLUMN IF EXISTS platform_fee_cent; - -ALTER TABLE order_checkouts - DROP COLUMN IF EXISTS rent_amount_cent, - DROP COLUMN IF EXISTS owner_rent_amount_cent, - DROP COLUMN IF EXISTS platform_fee_cent, - DROP COLUMN IF EXISTS deposit_amount_cent, - DROP COLUMN IF EXISTS consumable_amount_cent, - DROP COLUMN IF EXISTS other_amount_cent, - DROP COLUMN IF EXISTS deposit_deduct_amount_cent, - DROP COLUMN IF EXISTS renter_refund_amount_cent, - DROP COLUMN IF EXISTS owner_income_amount_cent; - -ALTER TABLE wallet_accounts - DROP COLUMN IF EXISTS available_balance_cent, - DROP COLUMN IF EXISTS frozen_balance_cent; - -ALTER TABLE wallet_ledger - DROP COLUMN IF EXISTS amount_cent, - DROP COLUMN IF EXISTS balance_after_cent; - -SET @withdrawal_requests_exists := ( - SELECT COUNT(*) - FROM information_schema.tables - WHERE table_schema = DATABASE() - AND table_name = 'withdrawal_requests' -); -SET @sql := IF(@withdrawal_requests_exists > 0, - 'ALTER TABLE withdrawal_requests - DROP COLUMN IF EXISTS amount_cent, - DROP COLUMN IF EXISTS fee_cent, - DROP COLUMN IF EXISTS actual_amount_cent', - 'SELECT 1' -); -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; --- +goose StatementEnd diff --git a/backend/pkg/money/format.go b/backend/pkg/money/format.go index e7c7dab..98e0ec8 100644 --- a/backend/pkg/money/format.go +++ b/backend/pkg/money/format.go @@ -11,26 +11,23 @@ func Round(value float64) float64 { return math.Round(value*10) / 10 } -// ToCent 将角转换为分(整数),用于存储 -// 12.3角 -> 123分 -// 12.34角 -> 123分(自动舍入到角) -func ToCent(jiao float64) int64 { - return int64(math.Round(jiao * 10)) +// ToCent 将元转换为分(整数),用于存储。 +// 12.34 元 -> 1234 分 +func ToCent(yuan float64) int64 { + return int64(math.Round(yuan * 100)) } -// ToJiao 将分转换为角(0.1元精度),用于API响应 -// 123分 -> 12.3角 -// 1234分 -> 123.4角 -func ToJiao(cent int64) float64 { - return float64(cent) / 10.0 +// ToDisplayYuan 将分转换为按角精度展示的元值。 +// 1234 分 -> 12.3 元 +func ToDisplayYuan(cent int64) float64 { + return Round(float64(cent) / 100) } // Format 格式化分为字符串(角精度,保留1位小数) -// 123分 -> "12.3" -// 1230分 -> "123.0" +// 1234 分 -> "12.3" +// 1230 分 -> "12.3" func Format(cent int64) string { - jiao := ToJiao(cent) - return fmt.Sprintf("%.1f", jiao) + return fmt.Sprintf("%.1f", ToDisplayYuan(cent)) } // FormatWithSymbol 格式化分为带符号的字符串 diff --git a/backend/pkg/money/format_test.go b/backend/pkg/money/format_test.go index 9a8e74c..4489c99 100644 --- a/backend/pkg/money/format_test.go +++ b/backend/pkg/money/format_test.go @@ -5,46 +5,44 @@ import "testing" func TestToCent(t *testing.T) { tests := []struct { name string - jiao float64 + yuan float64 want int64 }{ - {"12.3角转123分", 12.3, 123}, - {"12.34角舍入到123分", 12.34, 123}, - {"12.36角舍入到124分", 12.36, 124}, - {"0.1角转1分", 0.1, 1}, - {"0.05角舍入到1分", 0.05, 1}, - {"0.04角舍入到0分", 0.04, 0}, - {"100角转1000分", 100.0, 1000}, + {"12.3元转1230分", 12.3, 1230}, + {"12.34元转1234分", 12.34, 1234}, + {"12.36元转1236分", 12.36, 1236}, + {"0.1元转10分", 0.1, 10}, + {"100元转10000分", 100.0, 10000}, {"零值", 0.0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := ToCent(tt.jiao) + got := ToCent(tt.yuan) if got != tt.want { - t.Errorf("ToCent(%v) = %v, want %v", tt.jiao, got, tt.want) + t.Errorf("ToCent(%v) = %v, want %v", tt.yuan, got, tt.want) } }) } } -func TestToJiao(t *testing.T) { +func TestToDisplayYuan(t *testing.T) { tests := []struct { name string cent int64 want float64 }{ - {"123分转12.3角", 123, 12.3}, - {"1234分转123.4角", 1234, 123.4}, - {"1分转0.1角", 1, 0.1}, - {"10分转1.0角", 10, 1.0}, - {"1000分转100.0角", 1000, 100.0}, + {"123分展示1.2元", 123, 1.2}, + {"1234分展示12.3元", 1234, 12.3}, + {"1236分展示12.4元", 1236, 12.4}, + {"10分展示0.1元", 10, 0.1}, + {"1000分展示10.0元", 1000, 10.0}, {"零值", 0, 0.0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := ToJiao(tt.cent) + got := ToDisplayYuan(tt.cent) if got != tt.want { - t.Errorf("ToJiao(%v) = %v, want %v", tt.cent, got, tt.want) + t.Errorf("ToDisplayYuan(%v) = %v, want %v", tt.cent, got, tt.want) } }) } @@ -56,11 +54,11 @@ func TestFormat(t *testing.T) { cent int64 want string }{ - {"123分格式化", 123, "12.3"}, - {"1234分格式化", 1234, "123.4"}, - {"10分格式化", 10, "1.0"}, - {"1分格式化", 1, "0.1"}, - {"1000分格式化", 1000, "100.0"}, + {"123分格式化", 123, "1.2"}, + {"1234分格式化", 1234, "12.3"}, + {"1236分格式化", 1236, "12.4"}, + {"10分格式化", 10, "0.1"}, + {"1000分格式化", 1000, "10.0"}, {"零值格式化", 0, "0.0"}, } for _, tt := range tests { @@ -79,8 +77,8 @@ func TestFormatWithSymbol(t *testing.T) { cent int64 want string }{ - {"123分带符号", 123, "¥12.3"}, - {"1000分带符号", 1000, "¥100.0"}, + {"123分带符号", 123, "¥1.2"}, + {"1000分带符号", 1000, "¥10.0"}, {"零值带符号", 0, "¥0.0"}, } for _, tt := range tests { @@ -138,22 +136,22 @@ func TestMinMax(t *testing.T) { // 测试双向转换的精度 func TestRoundTrip(t *testing.T) { tests := []struct { - name string - originalJiao float64 - expectJiao float64 // 因为角精度,可能会有舍入 + name string + originalYuan float64 + expectDisplay float64 }{ - {"12.3角往返", 12.3, 12.3}, - {"12.34角往返(舍入)", 12.34, 12.3}, - {"100.0角往返", 100.0, 100.0}, - {"0.1角往返", 0.1, 0.1}, + {"12.3元往返", 12.3, 12.3}, + {"12.34元往返(展示到角)", 12.34, 12.3}, + {"100.0元往返", 100.0, 100.0}, + {"0.1元往返", 0.1, 0.1}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cent := ToCent(tt.originalJiao) - gotJiao := ToJiao(cent) - if gotJiao != tt.expectJiao { - t.Errorf("往返转换:%v角 -> %v分 -> %v角, 期望 %v角", - tt.originalJiao, cent, gotJiao, tt.expectJiao) + cent := ToCent(tt.originalYuan) + got := ToDisplayYuan(cent) + if got != tt.expectDisplay { + t.Errorf("往返转换:%v元 -> %v分 -> %v元, 期望 %v元", + tt.originalYuan, cent, got, tt.expectDisplay) } }) } diff --git a/frontend/src/features/admin/api/adminDashboard.ts b/frontend/src/features/admin/api/adminDashboard.ts index ca9895d..b183f09 100644 --- a/frontend/src/features/admin/api/adminDashboard.ts +++ b/frontend/src/features/admin/api/adminDashboard.ts @@ -10,7 +10,7 @@ export interface DashboardMetrics { total_orders: number renting_orders: number today_orders: number - today_ledger_amount: number + today_ledger_amount_cent: number } export interface DashboardPending { @@ -27,8 +27,8 @@ export interface DashboardRecentOrder { renter_id: number owner_id: number status: OrderStatus - rent_amount: number - deposit_amount: number + rent_amount_cent: number + deposit_amount_cent: number created_at: string } diff --git a/frontend/src/features/admin/api/adminFinance.ts b/frontend/src/features/admin/api/adminFinance.ts index 0cd2c67..c1c86f3 100644 --- a/frontend/src/features/admin/api/adminFinance.ts +++ b/frontend/src/features/admin/api/adminFinance.ts @@ -6,10 +6,10 @@ export interface FinanceSummary { total_refund_amount_cent: number pending_refund_amount_cent: number channel_net_amount_cent: number - platform_income_amount: number - owner_should_income_amount: number - owner_wallet_income_amount: number - settlement_diff_amount: number + platform_income_amount_cent: number + owner_should_income_amount_cent: number + owner_wallet_income_amount_cent: number + settlement_diff_amount_cent: number successful_pay_count: number successful_refund_count: number pending_refund_count: number @@ -23,10 +23,10 @@ export interface FinanceDailyItem { total_refund_amount_cent: number pending_refund_amount_cent: number channel_net_amount_cent: number - platform_income_amount: number - owner_should_income_amount: number - owner_wallet_income_amount: number - settlement_diff_amount: number + platform_income_amount_cent: number + owner_should_income_amount_cent: number + owner_wallet_income_amount_cent: number + settlement_diff_amount_cent: number successful_pay_count: number successful_refund_count: number pending_refund_count: number @@ -51,20 +51,20 @@ export interface FinanceDetail { owner_id: number owner_phone: string owner_nickname: string - order_rent_amount: number - order_deposit_amount: number - checkout_rent_amount: number - checkout_renter_refund: number - checkout_owner_income: number - checkout_platform_fee: number - owner_wallet_income_amount: number + order_rent_amount_cent: number + order_deposit_amount_cent: number + checkout_rent_amount_cent: number + checkout_renter_refund_cent: number + checkout_owner_income_cent: number + checkout_platform_fee_cent: number + owner_wallet_income_amount_cent: number paid_amount_cent: number refunded_amount_cent: number refunding_amount_cent: number failed_refund_amount_cent: number channel_net_amount_cent: number - platform_net_amount: number - settlement_diff_amount: number + platform_net_amount_cent: number + settlement_diff_amount_cent: number finance_status: string created_at: string settled_at?: string diff --git a/frontend/src/features/admin/api/adminUsers.ts b/frontend/src/features/admin/api/adminUsers.ts index 6138d40..ffea016 100644 --- a/frontend/src/features/admin/api/adminUsers.ts +++ b/frontend/src/features/admin/api/adminUsers.ts @@ -10,9 +10,9 @@ export interface AdminUserItem { realname_status: RealnameStatusValue risk_status: RiskStatus credit_score: number - deposit_free_quota: number - deposit_free_used: number - deposit_free_remaining: number + deposit_free_quota_cent: number + deposit_free_used_cent: number + deposit_free_remaining_cent: number status: UserStatus order_count: number listing_count: number @@ -44,10 +44,10 @@ export async function unfreezeAdminUser(id: number) { return data.data } -export async function setAdminUserDepositFreeQuota(id: number, amount: number) { +export async function setAdminUserDepositFreeQuota(id: number, amountCent: number) { const { data } = await apiClient.post>( `/admin/users/${id}/deposit-free-quota`, - { amount } + { amount_cent: amountCent } ) return data.data } diff --git a/frontend/src/features/admin/api/adminWithdrawal.ts b/frontend/src/features/admin/api/adminWithdrawal.ts index 14e72e9..5bcffef 100644 --- a/frontend/src/features/admin/api/adminWithdrawal.ts +++ b/frontend/src/features/admin/api/adminWithdrawal.ts @@ -12,9 +12,6 @@ export interface WithdrawalDetail { amount_cent: number fee_cent: number actual_amount_cent: number - amount?: number - fee?: number - actual_amount?: number payment_account_id: number | null account_type: string account_name: string diff --git a/frontend/src/features/admin/views/AdminDashboardView.vue b/frontend/src/features/admin/views/AdminDashboardView.vue index 4cb753c..54e8d8b 100644 --- a/frontend/src/features/admin/views/AdminDashboardView.vue +++ b/frontend/src/features/admin/views/AdminDashboardView.vue @@ -17,12 +17,10 @@ import { } from '@element-plus/icons-vue' import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard' import { useAdminTable } from '@/features/admin/composables/useAdminTable' -import { useMoney } from '@/shared/composables/useMoney' +import { formatCentWithSymbol } from '@/shared/utils/money' import { disputeStatusLabel, orderStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' -const money = useMoney() - const { loading, error, @@ -98,7 +96,7 @@ const {
今日流水 - {{ money(dashboard.metrics.today_ledger_amount) }} + {{ formatCentWithSymbol(dashboard.metrics.today_ledger_amount_cent) }} 今日订单 {{ dashboard.metrics.today_orders }} 个
@@ -224,9 +222,9 @@ const { - + diff --git a/frontend/src/features/admin/views/AdminDisputesView.vue b/frontend/src/features/admin/views/AdminDisputesView.vue index 504313b..1450bde 100644 --- a/frontend/src/features/admin/views/AdminDisputesView.vue +++ b/frontend/src/features/admin/views/AdminDisputesView.vue @@ -7,6 +7,7 @@ import { fetchAdminFileBlob } from '@/shared/api/files' import { disputeStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' import { formatListingNo } from '@/utils/listingDisplay' +import { yuanToCent } from '@/shared/utils/money' import AdminTablePagination from '../components/AdminTablePagination.vue' const loading = ref(false) @@ -100,7 +101,7 @@ async function handleArbitrate() { await arbitrateDispute(activeDispute.value.id, { result: result.value, remark: remark.value.trim(), - amount: amount.value, + amount_cent: amount.value ? yuanToCent(amount.value) : undefined, }) ElMessage.success('仲裁结果已保存,双方已收到通知') activeDispute.value = null diff --git a/frontend/src/features/admin/views/AdminFinanceDashboardView.vue b/frontend/src/features/admin/views/AdminFinanceDashboardView.vue index 218e228..e5fe5df 100644 --- a/frontend/src/features/admin/views/AdminFinanceDashboardView.vue +++ b/frontend/src/features/admin/views/AdminFinanceDashboardView.vue @@ -7,7 +7,7 @@ import { type FinanceDashboard, type FinanceDailyItem, } from '@/features/admin/api/adminFinance' -import { formatMoneyWithSymbol } from '@/shared/utils/money' +import { formatCentWithSymbol } from '@/shared/utils/money' import { formatDateTime } from '@/utils/time' const loading = ref(false) @@ -31,11 +31,7 @@ async function loadDashboard() { } function moneyCent(value: number) { - return formatMoneyWithSymbol(Number(value || 0) / 100) -} - -function money(value: number) { - return formatMoneyWithSymbol(value) + return formatCentWithSymbol(value) } function defaultStartDate() { @@ -53,11 +49,11 @@ function formatInputDate(date: Date) { } function diffType(value: number) { - return Math.abs(Number(value || 0)) >= 0.05 ? 'danger' : 'success' + return Math.abs(Number(value || 0)) >= 5 ? 'danger' : 'success' } function rowDiffClass(row: FinanceDailyItem) { - return Math.abs(Number(row.settlement_diff_amount || 0)) >= 0.05 ? 'amount-danger' : '' + return Math.abs(Number(row.settlement_diff_amount_cent || 0)) >= 5 ? 'amount-danger' : '' } @@ -107,17 +103,17 @@ function rowDiffClass(row: FinanceDailyItem) {
平台收入 - {{ money(dashboard.summary.platform_income_amount) }} + {{ moneyCent(dashboard.summary.platform_income_amount_cent) }} {{ dashboard.summary.settled_order_count }} 个已结算订单
号主应得 - {{ money(dashboard.summary.owner_should_income_amount) }} + {{ moneyCent(dashboard.summary.owner_should_income_amount_cent) }} 结账单口径
号主实际入账 - {{ money(dashboard.summary.owner_wallet_income_amount) }} + {{ moneyCent(dashboard.summary.owner_wallet_income_amount_cent) }} 钱包流水口径
@@ -128,8 +124,8 @@ function rowDiffClass(row: FinanceDailyItem) {
结算差异 - - {{ money(dashboard.summary.settlement_diff_amount) }} + + {{ moneyCent(dashboard.summary.settlement_diff_amount_cent) }} {{ dashboard.summary.financial_exception_count }} 个异常订单 @@ -148,17 +144,17 @@ function rowDiffClass(row: FinanceDailyItem) { - + - + - + diff --git a/frontend/src/features/admin/views/AdminFinanceDetailsView.vue b/frontend/src/features/admin/views/AdminFinanceDetailsView.vue index c745552..46035c2 100644 --- a/frontend/src/features/admin/views/AdminFinanceDetailsView.vue +++ b/frontend/src/features/admin/views/AdminFinanceDetailsView.vue @@ -6,7 +6,7 @@ import { fetchFinanceDetails, type FinanceDetail, } from '@/features/admin/api/adminFinance' -import { formatMoneyWithSymbol } from '@/shared/utils/money' +import { formatCentWithSymbol } from '@/shared/utils/money' import { orderStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' import AdminTablePagination from '../components/AdminTablePagination.vue' @@ -59,12 +59,8 @@ async function handlePageChange() { await loadDetails() } -function money(value: number) { - return formatMoneyWithSymbol(value) -} - function moneyCent(value: number) { - return formatMoneyWithSymbol(Number(value || 0) / 100) + return formatCentWithSymbol(value) } function financeStatusLabel(status: string) { @@ -226,14 +222,14 @@ function formatInputDate(date: Date) { {{ moneyCent(row.refunding_amount_cent) }} {{ moneyCent(row.channel_net_amount_cent) }} - {{ money(row.checkout_platform_fee) }} - {{ money(row.checkout_owner_income) }} - {{ money(row.owner_wallet_income_amount) }} + {{ moneyCent(row.checkout_platform_fee_cent) }} + {{ moneyCent(row.checkout_owner_income_cent) }} + {{ moneyCent(row.owner_wallet_income_amount_cent) }} - {{ money(row.settlement_diff_amount) }} + {{ moneyCent(row.settlement_diff_amount_cent) }}
diff --git a/frontend/src/features/admin/views/AdminListingDetailView.vue b/frontend/src/features/admin/views/AdminListingDetailView.vue index 43721d2..cd26358 100644 --- a/frontend/src/features/admin/views/AdminListingDetailView.vue +++ b/frontend/src/features/admin/views/AdminListingDetailView.vue @@ -5,7 +5,7 @@ import { computed, onMounted, ref } from 'vue' import { useRoute } from 'vue-router' import { fetchAdminFileBlob } from '@/shared/api/files' -import { formatMoneyWithSymbol } from '@/shared/utils/money' +import { formatCentWithSymbol } from '@/shared/utils/money' import { adminMarkListingAbnormal, adminOfflineListing, @@ -78,12 +78,12 @@ async function submitAction() { } } -function money(value: number) { - return formatMoneyWithSymbol(value) +function moneyCent(value: number) { + return formatCentWithSymbol(value) } function listingPrice(row: Listing) { - return money(row.price) + return moneyCent(row.price_cent) } function extractObjectKey(url: string) { @@ -150,7 +150,7 @@ function readError(error: unknown, fallback: string) {
押金 - {{ money(listing.deposit_amount) }} + {{ moneyCent(listing.deposit_amount_cent) }}
@@ -172,7 +172,7 @@ function readError(error: unknown, fallback: string) {

号主:{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}

号主 ID:{{ listing.owner_id }}

价格:{{ listingPrice(listing) }}

-

押金:{{ money(listing.deposit_amount) }}

+

押金:{{ moneyCent(listing.deposit_amount_cent) }}

diff --git a/frontend/src/features/admin/views/AdminListingReviewView.vue b/frontend/src/features/admin/views/AdminListingReviewView.vue index 54d8f18..81bb73b 100644 --- a/frontend/src/features/admin/views/AdminListingReviewView.vue +++ b/frontend/src/features/admin/views/AdminListingReviewView.vue @@ -4,7 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' import { fetchAdminFileBlob } from '@/shared/api/files' -import { formatMoneyWithSymbol } from '@/shared/utils/money' +import { centToYuan, formatCent, formatMoneyWithSymbol, yuanToCent } from '@/shared/utils/money' import { adjustListingReviewPrice, approveListing, @@ -185,10 +185,10 @@ async function handleSavePriceAdjust() { reason: priceAdjustForm.reason.trim(), } : { - buyer_total_price: Number(priceAdjustForm.buyer_total_price || 0), + buyer_total_price_cent: yuanToCent(Number(priceAdjustForm.buyer_total_price || 0)), reason: priceAdjustForm.reason.trim(), } - if ((payload.buyer_ratio || payload.buyer_total_price || 0) <= 0) { + if ((payload.buyer_ratio || payload.buyer_total_price_cent || 0) <= 0) { ElMessage.warning('请填写有效的加价后比例或价格') return } @@ -298,8 +298,9 @@ function roundPreviewRatio(value: number) { function sellerTotalPrice(row: Listing) { const value = breakdownNumber(row, 'seller_total_price') if (value > 0) return value - const fallback = Number(row.price || 0) - getListingConsumablePrice(row) - return fallback > 0 ? fallback : Number(row.price || 0) + const price = centToYuan(row.price_cent) + const fallback = price - getListingConsumablePrice(row) + return fallback > 0 ? fallback : price } function sellerCoinBasePrice(row: Listing) { @@ -318,7 +319,7 @@ function sellerRatio(row: Listing) { function buyerTotalPrice(row: Listing) { const value = breakdownNumber(row, 'buyer_total_price') - return value > 0 ? value : Number(row.price || 0) + return value > 0 ? value : centToYuan(row.price_cent) } function buyerCoinBasePrice(row: Listing) { @@ -463,7 +464,7 @@ function riskItems(row: Listing): RiskItem[] { if (hasBanRecord(row)) items.push({ label: `封禁记录:${banRecordText(row)}`, level: 'danger' }) if ( getListingConsumablePrice(row) > 0 && - Number(row.deposit_amount || 0) <= getListingConsumablePrice(row) + centToYuan(row.deposit_amount_cent) <= getListingConsumablePrice(row) ) { items.push({ label: '押金不高于消耗品价值', level: 'danger' }) } @@ -646,7 +647,7 @@ function readError(error: unknown, fallback: string) { KD {{ assetNumberText(item, 'secret_kd') }}
押金与损耗 - {{ money(selectedListing.deposit_amount) }} + ¥{{ formatCent(selectedListing.deposit_amount_cent) }}

每日损耗 {{ dailyLossText(selectedListing) }}

哈夫币 {{ formatHafCoinM(getCoinWan(selectedListing)) }}
diff --git a/frontend/src/features/admin/views/AdminListingsView.vue b/frontend/src/features/admin/views/AdminListingsView.vue index 5d70dce..27a5429 100644 --- a/frontend/src/features/admin/views/AdminListingsView.vue +++ b/frontend/src/features/admin/views/AdminListingsView.vue @@ -10,7 +10,7 @@ import { type Listing, } from '@/features/listings' import { useAdminTable } from '@/features/admin/composables/useAdminTable' -import { useMoney } from '@/shared/composables/useMoney' +import { formatCentWithSymbol } from '@/shared/utils/money' import { assetRegions, formatEstimatedRentalDuration, @@ -23,8 +23,6 @@ import { getSkinGroup, } from '@/utils/listingDisplay' -const money = useMoney() - const filters = reactive({ owner_id: '', status: '', @@ -322,7 +320,7 @@ function readScreenshotError(error: unknown, fallback: string) { } function listingPrice(row: Listing) { - return money(row.price) + return formatCentWithSymbol(row.price_cent) } function listingCoinM(row: Listing) { @@ -372,7 +370,7 @@ function characterAndWeaponSkinText(row: Listing) { } function rentAndDepositText(row: Listing) { - return `${listingPrice(row)}/${money(row.deposit_amount)}` + return `${listingPrice(row)}/${formatCentWithSymbol(row.deposit_amount_cent)}` } function estimateTitle(row: Listing) { diff --git a/frontend/src/features/admin/views/AdminOrderDetailView.vue b/frontend/src/features/admin/views/AdminOrderDetailView.vue index 7ce9b99..cf81059 100644 --- a/frontend/src/features/admin/views/AdminOrderDetailView.vue +++ b/frontend/src/features/admin/views/AdminOrderDetailView.vue @@ -15,7 +15,7 @@ import { type RefundStatus, } from '@/features/orders' import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments' -import { centToYuan, formatMoney, formatMoneyWithSymbol } from '@/shared/utils/money' +import { centToYuan, formatCentWithSymbol, formatMoney } from '@/shared/utils/money' import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' import { formatListingNo } from '@/utils/listingDisplay' @@ -111,9 +111,9 @@ function money(value: unknown) { return formatMoney(Number(value || 0)) } -function amountYuan(cent: unknown, legacyYuan?: unknown) { +function amountYuan(cent: unknown) { if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0)) - return Number(legacyYuan || 0) + return 0 } async function handleRefund() { @@ -173,7 +173,7 @@ function paymentBizTypeLabel(type: string) { } function moneyCent(value: number) { - return formatMoneyWithSymbol(Number(value || 0) / 100) + return formatCentWithSymbol(value) } function formatHandoffRecordType(type: string) { @@ -232,15 +232,15 @@ function formatHandoffRecordType(type: string) {
订单金额 - ¥{{ money(amountYuan(order.rent_amount_cent, order.rent_amount)) }} + ¥{{ money(amountYuan(order.rent_amount_cent)) }}
平台费用 - ¥{{ money(amountYuan(order.platform_fee_cent, order.platform_fee)) }} + ¥{{ money(amountYuan(order.platform_fee_cent)) }}
押金 - ¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }} + ¥{{ money(amountYuan(order.deposit_amount_cent)) }}
退款状态 diff --git a/frontend/src/features/admin/views/AdminOrdersView.vue b/frontend/src/features/admin/views/AdminOrdersView.vue index 2ddd0ba..5cb312c 100644 --- a/frontend/src/features/admin/views/AdminOrdersView.vue +++ b/frontend/src/features/admin/views/AdminOrdersView.vue @@ -37,9 +37,9 @@ const filteredOrders = computed(() => { return orders.value.filter(item => item.status === status.value) }) -function amountYuan(cent: unknown, legacyYuan?: unknown) { +function amountYuan(cent: unknown) { if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0)) - return Number(legacyYuan || 0) + return 0 } function money(value: unknown) { @@ -95,10 +95,10 @@ function money(value: unknown) { - + - + diff --git a/frontend/src/features/admin/views/AdminPaymentConfigsView.vue b/frontend/src/features/admin/views/AdminPaymentConfigsView.vue index bac741f..ccb96aa 100644 --- a/frontend/src/features/admin/views/AdminPaymentConfigsView.vue +++ b/frontend/src/features/admin/views/AdminPaymentConfigsView.vue @@ -11,7 +11,7 @@ import { type PaymentConfig, } from '@/features/admin/api/paymentConfig' import { formatDateTime } from '@/utils/time' -import { formatMoney } from '@/shared/utils/money' +import { formatCent } from '@/shared/utils/money' import { readError } from '@/utils/error' import PaymentConfigDialog from '../components/PaymentConfigDialog.vue' @@ -231,7 +231,7 @@ function formatEnvironment(env: string) { } function formatAmount(amountCent: number) { - return formatMoney(amountCent / 100) + return formatCent(amountCent) } function getStatusType(status: string) { diff --git a/frontend/src/features/admin/views/AdminPaymentsView.vue b/frontend/src/features/admin/views/AdminPaymentsView.vue index ddacb25..f936e21 100644 --- a/frontend/src/features/admin/views/AdminPaymentsView.vue +++ b/frontend/src/features/admin/views/AdminPaymentsView.vue @@ -3,7 +3,7 @@ import { Document, Search } from '@element-plus/icons-vue' import { computed, onMounted, reactive, ref } from 'vue' import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments' -import { formatMoneyWithSymbol } from '@/shared/utils/money' +import { formatCentWithSymbol } from '@/shared/utils/money' import { formatDateTime } from '@/utils/time' import AdminTablePagination from '../components/AdminTablePagination.vue' @@ -70,7 +70,7 @@ async function handlePageChange() { } function moneyCent(value: number) { - return formatMoneyWithSymbol(Number(value || 0) / 100) + return formatCentWithSymbol(value) } function paymentStatusType(status: string) { diff --git a/frontend/src/features/admin/views/AdminUsersView.vue b/frontend/src/features/admin/views/AdminUsersView.vue index abb1462..2ab2177 100644 --- a/frontend/src/features/admin/views/AdminUsersView.vue +++ b/frontend/src/features/admin/views/AdminUsersView.vue @@ -9,7 +9,7 @@ import { unfreezeAdminUser, type AdminUserItem, } from '@/features/admin/api/adminUsers' -import { formatMoney } from '@/shared/utils/money' +import { centToYuan, formatCent, yuanToCent } from '@/shared/utils/money' import { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable' import { userStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' @@ -39,14 +39,14 @@ function openFreeze(row: AdminUserItem) { function openDepositQuota(row: AdminUserItem) { quotaUser.value = row - quotaAmount.value = Number(row.deposit_free_quota || 0) + quotaAmount.value = centToYuan(row.deposit_free_quota_cent) } async function handleSetDepositQuota() { if (!quotaUser.value) return submitting.value = true try { - await setAdminUserDepositFreeQuota(quotaUser.value.id, quotaAmount.value) + await setAdminUserDepositFreeQuota(quotaUser.value.id, yuanToCent(quotaAmount.value)) ElMessage.success('免押额度已更新') quotaUser.value = null await loadUsers() @@ -93,8 +93,8 @@ function readError(error: unknown, fallback: string) { return fallback } -function money(value: number | string | undefined) { - return formatMoney(Number(value || 0)) +function moneyCent(value: number | string | undefined) { + return formatCent(Number(value || 0)) } @@ -117,13 +117,13 @@ function money(value: number | string | undefined) { - + - + - + @@ -195,8 +195,8 @@ function money(value: number | string | undefined) { {{ quotaUser.phone }} · {{ quotaUser.nickname }}

- 已占用 ¥{{ money(quotaUser.deposit_free_used) }},剩余 ¥{{ - money(quotaUser.deposit_free_remaining) + 已占用 ¥{{ moneyCent(quotaUser.deposit_free_used_cent) }},剩余 ¥{{ + moneyCent(quotaUser.deposit_free_remaining_cent) }}

>( `/admin/disputes/${id}/arbitrate`, diff --git a/frontend/src/features/listings/api/listings.ts b/frontend/src/features/listings/api/listings.ts index 75f4474..c10c444 100644 --- a/frontend/src/features/listings/api/listings.ts +++ b/frontend/src/features/listings/api/listings.ts @@ -19,8 +19,8 @@ export interface Listing { asset_summary?: Record screenshot_urls: string[] cover_url: string - price: number - deposit_amount: number + price_cent: number + deposit_amount_cent: number is_accelerated_sale?: boolean in_transaction: boolean status: ListingStatus @@ -40,8 +40,8 @@ export interface ListingPayload { haf_coin_amount: number asset_summary?: Record screenshot_urls: string[] - price: number - deposit_amount: number + price_cent: number + deposit_amount_cent: number agreed_virtual_asset_sale: boolean agreed_seller_agreement: boolean } @@ -214,7 +214,7 @@ export async function approveListing(id: number) { export interface AdminListingPriceAdjustPayload { buyer_ratio?: number - buyer_total_price?: number + buyer_total_price_cent?: number reason?: string } diff --git a/frontend/src/features/listings/components/ListingCard.vue b/frontend/src/features/listings/components/ListingCard.vue index cac4fb4..cbde804 100644 --- a/frontend/src/features/listings/components/ListingCard.vue +++ b/frontend/src/features/listings/components/ListingCard.vue @@ -2,7 +2,7 @@ import { computed } from 'vue' import { RouterLink } from 'vue-router' import type { Listing } from '@/features/listings' -import { formatMoney } from '@/shared/utils/money' +import { formatCent, formatMoney } from '@/shared/utils/money' import { formatHafCoinM, getCoinWan, @@ -140,7 +140,7 @@ function formatStatNumber(value: number) {
押金 - ¥{{ formatMoney(listing.deposit_amount) }} + ¥{{ formatCent(listing.deposit_amount_cent) }}
diff --git a/frontend/src/features/listings/views/ListingDetailView.vue b/frontend/src/features/listings/views/ListingDetailView.vue index 8b76acf..a750be3 100644 --- a/frontend/src/features/listings/views/ListingDetailView.vue +++ b/frontend/src/features/listings/views/ListingDetailView.vue @@ -11,7 +11,7 @@ import { } from '@/features/orders/api/orders' import { useSessionStore } from '@/stores/session' import AuthImage from '@/shared/components/business/AuthImage.vue' -import { roundMoney, formatMoney } from '@/shared/utils/money' +import { roundMoney, formatMoney, formatCent } from '@/shared/utils/money' import { assetRegions, formatEstimatedRentalDuration, @@ -102,7 +102,7 @@ const detailMetrics = computed(() => { tone: 'coin', }, { label: '价格', value: `¥${orderTotal.value}`, tone: 'price' }, - { label: '押金', value: `¥${formatMoney(listing.value.deposit_amount)}`, tone: '' }, + { label: '押金', value: `¥${formatCent(listing.value.deposit_amount_cent)}`, tone: '' }, ] }) @@ -465,7 +465,7 @@ function listingPrice(item: Listing) { ¥{{ formatMoney(orderPriceBreakdown.consumable) }}
- 押金另付 ¥{{ formatMoney(listing.deposit_amount) }} + 押金另付 ¥{{ formatCent(listing.deposit_amount_cent) }}
diff --git a/frontend/src/features/listings/views/ListingsView.vue b/frontend/src/features/listings/views/ListingsView.vue index 2576ae4..8edaf88 100644 --- a/frontend/src/features/listings/views/ListingsView.vue +++ b/frontend/src/features/listings/views/ListingsView.vue @@ -6,7 +6,7 @@ import { type ListingPublishOptions, } from '@/features/listings/api/listingOptions' import { fetchListings, type Listing } from '@/features/listings/api/listings' -import { formatMoney } from '@/shared/utils/money' +import { centToYuan, formatCent, formatMoney } from '@/shared/utils/money' import { defaultHomeAnnouncements, defaultHomeBanners, @@ -347,7 +347,7 @@ function matchesFilters(item: Listing) { if (key === 'price') value = getListingDisplayPrice(item) if (key === 'coin') value = getCoinM(item) if (key === 'secretKd') value = readAssetNumber(item, 'secret_kd') - if (key === 'deposit') value = Number(item.deposit_amount || 0) + if (key === 'deposit') value = centToYuan(item.deposit_amount_cent) if (key.startsWith('resource_')) { value = getResourceQuantity(item, key.replace('resource_', '')) } @@ -622,7 +622,7 @@ function parseQuantityUnit(price: string) {
¥{{ formatMoney(getListingDisplayPrice(item)) }} - 押金¥{{ formatMoney(item.deposit_amount) }} + 押金¥{{ formatCent(item.deposit_amount_cent) }}
diff --git a/frontend/src/features/listings/views/MobileHomeView.vue b/frontend/src/features/listings/views/MobileHomeView.vue index 5f01f42..b5004c3 100644 --- a/frontend/src/features/listings/views/MobileHomeView.vue +++ b/frontend/src/features/listings/views/MobileHomeView.vue @@ -5,7 +5,7 @@ import { showToast } from 'vant' import MobileBottomNav from '@/components/MobileBottomNav.vue' import { ensureSupportChat } from '@/features/chats/api/chats' -import { formatMoney } from '@/shared/utils/money' +import { formatCent, formatMoney } from '@/shared/utils/money' import { emptyListingPublishOptions, type ListingPublishOptions, @@ -697,7 +697,7 @@ function chipTone(label: string) { diff --git a/frontend/src/features/listings/views/MobileListingDetailView.vue b/frontend/src/features/listings/views/MobileListingDetailView.vue index e00ec97..d96454e 100644 --- a/frontend/src/features/listings/views/MobileListingDetailView.vue +++ b/frontend/src/features/listings/views/MobileListingDetailView.vue @@ -11,7 +11,7 @@ import { } from '@/features/orders/api/orders' import { useSessionStore } from '@/stores/session' import AuthImage from '@/shared/components/business/AuthImage.vue' -import { formatMoney } from '@/shared/utils/money' +import { formatCent, formatMoney } from '@/shared/utils/money' import { assetRegions, formatHafCoinM, @@ -95,7 +95,7 @@ const detailMetrics = computed(() => { tone: 'coin', }, { label: '价格', value: `¥${formatMoney(getListingDisplayPrice(listing.value))}`, tone: 'price' }, - { label: '押金', value: `¥${formatMoney(listing.value.deposit_amount)}`, tone: '' }, + { label: '押金', value: `¥${formatCent(listing.value.deposit_amount_cent)}`, tone: '' }, ] }) diff --git a/frontend/src/features/orders/api/orders.ts b/frontend/src/features/orders/api/orders.ts index b6790e7..d14a887 100644 --- a/frontend/src/features/orders/api/orders.ts +++ b/frontend/src/features/orders/api/orders.ts @@ -26,14 +26,6 @@ export interface Order { deposit_original_amount_cent: number deposit_waived_amount_cent: number platform_fee_cent?: number - // Legacy yuan fields may be present from cached/older API payloads during rollout. - display_amount?: number - rent_amount?: number - owner_rent_amount?: number - deposit_amount?: number - deposit_original_amount?: number - deposit_waived_amount?: number - platform_fee?: number account_snapshot?: Record listing_snapshot?: string checkout_info?: string @@ -64,16 +56,6 @@ export interface Checkout { deposit_deduct_amount_cent: number renter_refund_amount_cent?: number owner_income_amount_cent?: number - display_amount?: number - rent_amount?: number - owner_rent_amount?: number - platform_fee?: number - deposit_amount?: number - consumable_amount?: number - other_amount?: number - deposit_deduct_amount?: number - renter_refund_amount?: number - owner_income_amount?: number content: string evidence_urls: string[] owner_adjustment_reason: string @@ -319,4 +301,3 @@ export async function adminRefundOrder(id: number) { const { data } = await apiClient.post>(`/admin/orders/${id}/refund`) return data.data } - diff --git a/frontend/src/features/orders/views/MobileOrderDetailView.vue b/frontend/src/features/orders/views/MobileOrderDetailView.vue index 1994fe5..285ab97 100644 --- a/frontend/src/features/orders/views/MobileOrderDetailView.vue +++ b/frontend/src/features/orders/views/MobileOrderDetailView.vue @@ -557,26 +557,24 @@ function formatHandoffRecordType(type: string) { return typeMap[type] || type } -function amountYuan(cent: unknown, legacyYuan?: unknown) { +function amountYuan(cent: unknown) { if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent)) - return readNumber(legacyYuan) + return 0 } function orderRentAmount(item: Order) { if (item.owner_id === session.userId) - return amountYuan(item.owner_rent_amount_cent, item.owner_rent_amount ?? item.display_amount) + return amountYuan(item.owner_rent_amount_cent) if (item.renter_id === session.userId) - return amountYuan(item.rent_amount_cent, item.rent_amount ?? item.display_amount) - return amountYuan(item.display_amount_cent, item.display_amount) + return amountYuan(item.rent_amount_cent) + return amountYuan(item.display_amount_cent) } function ownerActualIncome(item: Order) { if (item.owner_id !== session.userId) return null const value = item.checkout?.owner_income_amount_cent if (typeof value === 'number') return centToYuan(value) - return typeof item.checkout?.owner_income_amount === 'number' - ? item.checkout.owner_income_amount - : null + return null } function quantity(value: unknown) { @@ -596,16 +594,10 @@ function isRecord(value: unknown): value is Record { function hydrateCounterForm() { if (!order.value?.checkout) return const checkout = order.value.checkout - counterForm.value.consumable_amount = amountYuan( - checkout.consumable_amount_cent, - checkout.consumable_amount - ) + counterForm.value.consumable_amount = amountYuan(checkout.consumable_amount_cent) counterForm.value.coin_consumed_m = checkout.coin_consumed_m - counterForm.value.other_amount = amountYuan(checkout.other_amount_cent, checkout.other_amount) - counterForm.value.deposit_deduct_amount = amountYuan( - checkout.deposit_deduct_amount_cent, - checkout.deposit_deduct_amount - ) + counterForm.value.other_amount = amountYuan(checkout.other_amount_cent) + counterForm.value.deposit_deduct_amount = amountYuan(checkout.deposit_deduct_amount_cent) } function linesToList(value: string) { @@ -696,14 +688,12 @@ async function copyListingCode() {
押金 ¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}¥{{ money(amountYuan(order.deposit_amount_cent)) }} 已免押 ¥{{ - money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) - }}已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}
@@ -956,59 +946,36 @@ async function copyListingCode() { 押金金额 - ¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }} - 免 ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}免 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}
diff --git a/frontend/src/features/orders/views/OrderDetailView.vue b/frontend/src/features/orders/views/OrderDetailView.vue index f03a5d1..d51920f 100644 --- a/frontend/src/features/orders/views/OrderDetailView.vue +++ b/frontend/src/features/orders/views/OrderDetailView.vue @@ -8,7 +8,7 @@ import QRCode from 'qrcode' import { fetchOrderChat } from '@/features/chats/api/chats' import { createDispute } from '@/features/disputes' import { uploadFile } from '@/shared/api/files' -import { centToYuan, formatMoney } from '@/shared/utils/money' +import { centToYuan, formatCent, formatMoney } from '@/shared/utils/money' import { acceptCheckout, cancelOrder, @@ -606,26 +606,24 @@ function money(value: unknown) { return formatMoney(readNumber(value)) } -function amountYuan(cent: unknown, legacyYuan?: unknown) { +function amountYuan(cent: unknown) { if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent)) - return readNumber(legacyYuan) + return 0 } function orderRentAmount(item: Order) { if (item.owner_id === session.userId) - return amountYuan(item.owner_rent_amount_cent, item.owner_rent_amount ?? item.display_amount) + return amountYuan(item.owner_rent_amount_cent) if (item.renter_id === session.userId) - return amountYuan(item.rent_amount_cent, item.rent_amount ?? item.display_amount) - return amountYuan(item.display_amount_cent, item.display_amount) + return amountYuan(item.rent_amount_cent) + return amountYuan(item.display_amount_cent) } function ownerActualIncome(item: Order) { if (item.owner_id !== session.userId) return null const value = item.checkout?.owner_income_amount_cent if (typeof value === 'number') return centToYuan(value) - return typeof item.checkout?.owner_income_amount === 'number' - ? item.checkout.owner_income_amount - : null + return null } function quantity(value: unknown) { @@ -645,16 +643,10 @@ function isRecord(value: unknown): value is Record { function hydrateCounterForm() { if (!order.value?.checkout) return const checkout = order.value.checkout - counterForm.value.consumable_amount = amountYuan( - checkout.consumable_amount_cent, - checkout.consumable_amount - ) + counterForm.value.consumable_amount = amountYuan(checkout.consumable_amount_cent) counterForm.value.coin_consumed_m = checkout.coin_consumed_m - counterForm.value.other_amount = amountYuan(checkout.other_amount_cent, checkout.other_amount) - counterForm.value.deposit_deduct_amount = amountYuan( - checkout.deposit_deduct_amount_cent, - checkout.deposit_deduct_amount - ) + counterForm.value.other_amount = amountYuan(checkout.other_amount_cent) + counterForm.value.deposit_deduct_amount = amountYuan(checkout.deposit_deduct_amount_cent) } function linesToList(value: string) { @@ -789,13 +781,13 @@ async function copyListingCode() {
押金 ¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}¥{{ money(amountYuan(order.deposit_amount_cent)) }} 已免押 ¥{{ - money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) + money(amountYuan(order.deposit_waived_amount_cent)) }}
@@ -979,81 +971,40 @@ async function copyListingCode() {
实际结算租金 ¥{{ - money( - amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount) - ) - }}¥{{ money(amountYuan(order.checkout.display_amount_cent)) }}
预收押金 - ¥{{ - money( - amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount) - ) - }} - 已免押 ¥{{ - money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) - }}已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}
额外消耗品已用 ¥{{ - money( - amountYuan( - order.checkout.consumable_amount_cent, - order.checkout.consumable_amount - ) - ) - }}¥{{ money(amountYuan(order.checkout.consumable_amount_cent)) }}
押金赔付扣除 ¥{{ - money( - amountYuan( - order.checkout.deposit_deduct_amount_cent, - order.checkout.deposit_deduct_amount - ) - ) - }}¥{{ money(amountYuan(order.checkout.deposit_deduct_amount_cent)) }}
退还租客(未使用租金 + 剩余押金) ¥{{ - money( - amountYuan( - order.checkout.renter_refund_amount_cent, - order.checkout.renter_refund_amount - ) - ) - }}¥{{ money(amountYuan(order.checkout.renter_refund_amount_cent)) }}
号主最终收入(租金 + 押金赔付) ¥{{ - money( - amountYuan( - order.checkout.owner_income_amount_cent, - order.checkout.owner_income_amount - ) - ) - }}¥{{ money(amountYuan(order.checkout.owner_income_amount_cent)) }}
@@ -1109,7 +1060,7 @@ async function copyListingCode() { v-model="counterForm.deposit_deduct_amount" class="full-control" :min="0" - :max="amountYuan(order.deposit_amount_cent, order.deposit_amount)" + :max="amountYuan(order.deposit_amount_cent)" :precision="0" controls-position="right" /> @@ -1331,7 +1282,7 @@ async function copyListingCode() {
支付金额 - ¥{{ formatMoney(activePayment.amount_cent / 100) }} + ¥{{ formatCent(activePayment.amount_cent) }}
diff --git a/frontend/src/features/orders/views/OrdersView.vue b/frontend/src/features/orders/views/OrdersView.vue index 20a7ce8..62baf14 100644 --- a/frontend/src/features/orders/views/OrdersView.vue +++ b/frontend/src/features/orders/views/OrdersView.vue @@ -127,22 +127,22 @@ function amountLabel(order: Order) { return isRenter(order) ? '支付租金' : '预计租金' } -function amountYuan(cent: unknown, legacyYuan?: unknown) { +function amountYuan(cent: unknown) { if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0)) - return Number(legacyYuan || 0) + return 0 } function orderRentAmount(order: Order) { - if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent, order.owner_rent_amount ?? order.display_amount) - if (isRenter(order)) return amountYuan(order.rent_amount_cent, order.rent_amount ?? order.display_amount) - return amountYuan(order.display_amount_cent, order.display_amount) + if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent) + if (isRenter(order)) return amountYuan(order.rent_amount_cent) + return amountYuan(order.display_amount_cent) } function ownerActualIncome(order: Order) { if (!isOwner(order)) return null const value = order.checkout?.owner_income_amount_cent if (typeof value === 'number') return centToYuan(value) - return typeof order.checkout?.owner_income_amount === 'number' ? order.checkout.owner_income_amount : null + return null } function money(value: unknown) { @@ -269,9 +269,9 @@ function getCountdownMinutes(order: Order) { @@ -357,9 +357,9 @@ function getCountdownMinutes(order: Order) {
押金 - ¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }} - 免 ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}免 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}
diff --git a/frontend/src/features/seller/composables/usePublishForm.ts b/frontend/src/features/seller/composables/usePublishForm.ts index a8fb76b..6121f9c 100644 --- a/frontend/src/features/seller/composables/usePublishForm.ts +++ b/frontend/src/features/seller/composables/usePublishForm.ts @@ -26,6 +26,7 @@ import { writePublishDraft, } from '@/features/seller/composables/usePublishDraft' import type { PublishForm } from '@/types/publish' +import { yuanToCent } from '@/shared/utils/money' import { commonOnlineTimes, dailyLossOptions, formatNumber, roundRatio } from '@/utils/pricing' const draftSaveDelay = 400 @@ -567,8 +568,8 @@ export function usePublishForm(options: UsePublishFormOptions) { haf_coin_amount: pricing.coinMAmount.value * 1000000, asset_summary: buildAssetSummary(), screenshot_urls: pricing.screenshotUrls.value, - price: pricing.calculatedFinalPrice.value, - deposit_amount: Number(form.deposit_amount), + price_cent: yuanToCent(pricing.calculatedFinalPrice.value), + deposit_amount_cent: yuanToCent(Number(form.deposit_amount)), agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value, agreed_seller_agreement: sellerAgreementChecked.value, }) diff --git a/frontend/src/features/seller/views/SellerHandoffsView.vue b/frontend/src/features/seller/views/SellerHandoffsView.vue index a52c5a1..b9e97bd 100644 --- a/frontend/src/features/seller/views/SellerHandoffsView.vue +++ b/frontend/src/features/seller/views/SellerHandoffsView.vue @@ -5,7 +5,7 @@ import { fetchOrders, type Order } from '@/features/orders' import { useSessionStore } from '@/stores/session' import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' -import { formatMoney } from '@/shared/utils/money' +import { centToYuan, formatMoney } from '@/shared/utils/money' import { formatListingNo } from '@/utils/listingDisplay' const session = useSessionStore() @@ -73,6 +73,10 @@ function actionText(order: Order) { function money(value: unknown) { return formatMoney(Number(value || 0)) } + +function sellerAmount(order: Order) { + return centToYuan(order.owner_rent_amount_cent ?? order.display_amount_cent) +}