统一金额分制重构

This commit is contained in:
yml
2026-06-09 19:04:11 +08:00
parent 87673288ef
commit 5cd3ead4bd
69 changed files with 886 additions and 1277 deletions
-2
View File
@@ -33,9 +33,7 @@ type RentalListing struct {
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"`
-6
View File
@@ -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"`
-9
View File
@@ -11,24 +11,15 @@ type OrderCheckout struct {
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"`
-1
View File
@@ -10,7 +10,6 @@ type User struct {
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"`
-4
View File
@@ -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"`
-3
View File
@@ -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"`
@@ -18,7 +18,7 @@ type MetricsDTO struct {
TotalOrders int64 `json:"total_orders"`
RentingOrders int64 `json:"renting_orders"`
TodayOrders int64 `json:"today_orders"`
TodayLedgerAmount float64 `json:"today_ledger_amount"`
TodayLedgerAmountCent int64 `json:"today_ledger_amount_cent"`
}
type PendingDTO struct {
@@ -35,8 +35,8 @@ type RecentOrderDTO struct {
RenterID uint64 `json:"renter_id"`
OwnerID uint64 `json:"owner_id"`
Status string `json:"status"`
RentAmount float64 `json:"rent_amount"`
DepositAmount float64 `json:"deposit_amount"`
RentAmountCent int64 `json:"rent_amount_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
CreatedAt time.Time `json:"created_at"`
}
@@ -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).
+17 -17
View File
@@ -30,10 +30,10 @@ type FinanceSummaryDTO struct {
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"`
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"`
@@ -47,10 +47,10 @@ type FinanceDailyDTO struct {
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"`
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"`
@@ -76,20 +76,20 @@ type FinanceDetailDTO struct {
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"`
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"`
PlatformNetAmount float64 `json:"platform_net_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
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"`
@@ -1,11 +1,9 @@
package adminfinance
import (
"math"
"time"
"hfb_sys/backend/internal/timeutil"
"hfb_sys/backend/pkg/money"
"gorm.io/gorm"
)
@@ -71,9 +69,9 @@ 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,
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)).
@@ -98,10 +96,10 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) {
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),
PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent,
OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent,
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent,
SuccessfulPayCount: payment.SuccessfulPayCount,
SuccessfulRefundCount: payment.SuccessfulRefundCount,
PendingRefundCount: payment.PendingRefundCount,
@@ -130,9 +128,9 @@ 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,
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)).
@@ -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,
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, 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,
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, 0) - COALESCE(w.owner_wallet_income_amount, 0)) >= 0.05 THEN 'settlement_diff'
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,9 +281,9 @@ type paymentSummaryRow struct {
}
type settlementSummaryRow struct {
PlatformIncomeAmount float64
OwnerShouldIncomeAmount float64
OwnerWalletIncomeAmount float64
PlatformIncomeAmountCent int64
OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64
SettledOrderCount int64
}
@@ -301,9 +299,9 @@ type dailyPaymentRow struct {
type dailySettlementRow struct {
Date string
PlatformIncomeAmount float64
OwnerShouldIncomeAmount float64
OwnerWalletIncomeAmount float64
PlatformIncomeAmountCent int64
OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64
SettledOrderCount int64
}
@@ -319,32 +317,32 @@ type financeDetailRow struct {
OwnerID uint64
OwnerPhone string
OwnerNickname string
OrderRentAmount float64
OrderDepositAmount float64
CheckoutRentAmount float64
CheckoutRenterRefund float64
CheckoutOwnerIncome float64
CheckoutPlatformFee float64
OwnerWalletIncomeAmount float64
OrderRentAmountCent int64
OrderDepositAmountCent int64
CheckoutRentAmountCent int64
CheckoutRenterRefundCent int64
CheckoutOwnerIncomeCent int64
CheckoutPlatformFeeCent int64
OwnerWalletIncomeAmountCent int64
PaidAmountCent int64
RefundedAmountCent int64
RefundingAmountCent int64
FailedRefundAmountCent int64
ChannelNetAmountCent int64
PlatformNetAmount float64
SettlementDiffAmount float64
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{
@@ -359,22 +357,29 @@ func (r financeDetailRow) toDTO() FinanceDetailDTO {
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),
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,
PlatformNetAmount: money.Round(r.PlatformNetAmount),
SettlementDiffAmount: diff,
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
}
+3 -4
View File
@@ -9,9 +9,9 @@ type UserDTO struct {
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"`
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"`
@@ -26,7 +26,6 @@ type FreezeRequest struct {
type DepositFreeQuotaRequest struct {
AmountCent int64 `json:"amount_cent"`
Amount float64 `json:"amount"`
}
type PaginatedResult struct {
@@ -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 {
@@ -146,11 +139,11 @@ type userRow struct {
OrderCount int64
ListingCount int64
DisputeCount int64
DepositFreeUsed float64
DepositFreeUsedCent int64
}
func (row userRow) toDTO() UserDTO {
remaining := roundMoney(row.DepositFreeQuota - row.DepositFreeUsed)
remaining := row.DepositFreeQuotaCent - row.DepositFreeUsedCent
if remaining < 0 {
remaining = 0
}
@@ -161,9 +154,9 @@ func (row userRow) toDTO() UserDTO {
RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore,
DepositFreeQuota: row.DepositFreeQuota,
DepositFreeUsed: roundMoney(row.DepositFreeUsed),
DepositFreeRemaining: remaining,
DepositFreeQuotaCent: row.DepositFreeQuotaCent,
DepositFreeUsedCent: row.DepositFreeUsedCent,
DepositFreeRemainingCent: remaining,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,
@@ -174,15 +167,8 @@ func (row userRow) toDTO() UserDTO {
}
}
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))
}
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
@@ -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,
},
}
@@ -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)
+1 -1
View File
@@ -37,7 +37,7 @@ type CreateRequest struct {
type ArbitrateRequest struct {
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
Amount float64 `json:"amount"`
AmountCent int64 `json:"amount_cent"`
}
type AuditMeta = auditlog.Meta
+47 -67
View File
@@ -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
}
@@ -286,10 +284,10 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
"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,
"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,
@@ -339,35 +337,28 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
type arbitrationSettlement struct {
Entries []wallet.Entry
RenterRefundAmount float64
OwnerIncomeAmount float64
DepositDeductAmount float64
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
}
@@ -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) {
+3 -3
View File
@@ -46,8 +46,8 @@ type CreateRequest struct {
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"`
Price float64 `json:"price"`
DepositAmount float64 `json:"deposit_amount"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"`
AgreedSellerAgreement bool `json:"agreed_seller_agreement"`
@@ -61,7 +61,7 @@ type ReviewRequest struct {
type AdminPriceAdjustRequest struct {
BuyerRatio float64 `json:"buyer_ratio"`
BuyerTotalPrice float64 `json:"buyer_total_price"`
BuyerTotalPriceCent int64 `json:"buyer_total_price_cent"`
Reason string `json:"reason"`
}
+30 -29
View File
@@ -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))
+6 -6
View File
@@ -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 {
@@ -533,8 +533,8 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot},
Price: price,
DepositAmount: item.Deposit,
PriceCent: yuanToCent(price),
DepositAmountCent: yuanToCent(item.Deposit),
}
}
@@ -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)
}
@@ -37,8 +37,8 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
req := CreateRequest{
Title: "测试账号",
ServerRegion: "烽火地带",
Price: 100,
DepositAmount: 2,
PriceCent: 10000,
DepositAmountCent: 200,
HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{
@@ -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"])
+5 -5
View File
@@ -55,17 +55,17 @@ type SubmitReturnRequest struct {
type SubmitCheckoutRequest struct {
Content string `json:"content" binding:"required"`
ConsumableAmount float64 `json:"consumable_amount"`
ConsumableAmountCent int64 `json:"consumable_amount_cent"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"`
OtherAmountCent int64 `json:"other_amount_cent"`
EvidenceURLS []string `json:"evidence_urls"`
}
type CounterCheckoutRequest struct {
ConsumableAmount float64 `json:"consumable_amount"`
ConsumableAmountCent int64 `json:"consumable_amount_cent"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"`
DepositDeductAmount float64 `json:"deposit_deduct_amount"`
OtherAmountCent int64 `json:"other_amount_cent"`
DepositDeductAmountCent int64 `json:"deposit_deduct_amount_cent"`
Reason string `json:"reason" binding:"required"`
EvidenceURLS []string `json:"evidence_urls"`
}
+113 -125
View File
@@ -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,
@@ -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)
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ type StartPaymentRequest struct {
}
type WalletRechargePaymentRequest struct {
Amount float64 `json:"amount"`
AmountCent int64 `json:"amount_cent"`
PayWay string `json:"pay_way"`
JSPayFlag string `json:"jspay_flag"`
}
@@ -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
}
@@ -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,
+1 -26
View File
@@ -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 '实际到账(分)',
-- 收款账号信息(快照)
@@ -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
+11 -14
View File
@@ -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 格式化分为带符号的字符串
+34 -36
View File
@@ -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 {
@@ -139,21 +137,21 @@ func TestMinMax(t *testing.T) {
func TestRoundTrip(t *testing.T) {
tests := []struct {
name string
originalJiao float64
expectJiao float64 // 因为角精度,可能会有舍入
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)
}
})
}
@@ -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
}
+17 -17
View File
@@ -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
@@ -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<ApiResponse<AdminUserItem>>(
`/admin/users/${id}/deposit-free-quota`,
{ amount }
{ amount_cent: amountCent }
)
return data.data
}
@@ -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
@@ -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 {
</div>
<div class="metric-content">
<span>今日流水</span>
<strong>{{ money(dashboard.metrics.today_ledger_amount) }}</strong>
<strong>{{ formatCentWithSymbol(dashboard.metrics.today_ledger_amount_cent) }}</strong>
<small>今日订单 {{ dashboard.metrics.today_orders }} </small>
</div>
</div>
@@ -224,9 +222,9 @@ const {
</el-tag>
</template>
</el-table-column>
<el-table-column prop="rent_amount" label="金额" width="100">
<el-table-column prop="rent_amount_cent" label="金额" width="100">
<template #default="{ row }">
<span class="amount">{{ money(row.rent_amount) }}</span>
<span class="amount">{{ formatCentWithSymbol(row.rent_amount_cent) }}</span>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="180">
@@ -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
@@ -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' : ''
}
</script>
@@ -107,17 +103,17 @@ function rowDiffClass(row: FinanceDailyItem) {
</div>
<div class="metric-card">
<span>平台收入</span>
<strong>{{ money(dashboard.summary.platform_income_amount) }}</strong>
<strong>{{ moneyCent(dashboard.summary.platform_income_amount_cent) }}</strong>
<small>{{ dashboard.summary.settled_order_count }} 个已结算订单</small>
</div>
<div class="metric-card">
<span>号主应得</span>
<strong>{{ money(dashboard.summary.owner_should_income_amount) }}</strong>
<strong>{{ moneyCent(dashboard.summary.owner_should_income_amount_cent) }}</strong>
<small>结账单口径</small>
</div>
<div class="metric-card">
<span>号主实际入账</span>
<strong>{{ money(dashboard.summary.owner_wallet_income_amount) }}</strong>
<strong>{{ moneyCent(dashboard.summary.owner_wallet_income_amount_cent) }}</strong>
<small>钱包流水口径</small>
</div>
<div class="metric-card">
@@ -128,8 +124,8 @@ function rowDiffClass(row: FinanceDailyItem) {
<div class="metric-card">
<span>结算差异</span>
<strong>
<el-tag :type="diffType(dashboard.summary.settlement_diff_amount)">
{{ money(dashboard.summary.settlement_diff_amount) }}
<el-tag :type="diffType(dashboard.summary.settlement_diff_amount_cent)">
{{ moneyCent(dashboard.summary.settlement_diff_amount_cent) }}
</el-tag>
</strong>
<small>{{ dashboard.summary.financial_exception_count }} 个异常订单</small>
@@ -148,17 +144,17 @@ function rowDiffClass(row: FinanceDailyItem) {
<template #default="{ row }">{{ moneyCent(row.channel_net_amount_cent) }}</template>
</el-table-column>
<el-table-column label="平台收入" width="130">
<template #default="{ row }">{{ money(row.platform_income_amount) }}</template>
<template #default="{ row }">{{ moneyCent(row.platform_income_amount_cent) }}</template>
</el-table-column>
<el-table-column label="号主应得" width="130">
<template #default="{ row }">{{ money(row.owner_should_income_amount) }}</template>
<template #default="{ row }">{{ moneyCent(row.owner_should_income_amount_cent) }}</template>
</el-table-column>
<el-table-column label="号主入账" width="130">
<template #default="{ row }">{{ money(row.owner_wallet_income_amount) }}</template>
<template #default="{ row }">{{ moneyCent(row.owner_wallet_income_amount_cent) }}</template>
</el-table-column>
<el-table-column label="结算差异" width="130">
<template #default="{ row }">
<span :class="rowDiffClass(row)">{{ money(row.settlement_diff_amount) }}</span>
<span :class="rowDiffClass(row)">{{ moneyCent(row.settlement_diff_amount_cent) }}</span>
</template>
</el-table-column>
<el-table-column label="收款/退款/结算" min-width="170">
@@ -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) }}
</span>
<span class="amount-text amount-cell">{{ moneyCent(row.channel_net_amount_cent) }}</span>
<span class="amount-text amount-cell">{{ money(row.checkout_platform_fee) }}</span>
<span class="amount-text amount-cell">{{ money(row.checkout_owner_income) }}</span>
<span class="amount-text amount-cell">{{ money(row.owner_wallet_income_amount) }}</span>
<span class="amount-text amount-cell">{{ moneyCent(row.checkout_platform_fee_cent) }}</span>
<span class="amount-text amount-cell">{{ moneyCent(row.checkout_owner_income_cent) }}</span>
<span class="amount-text amount-cell">{{ moneyCent(row.owner_wallet_income_amount_cent) }}</span>
<span
class="amount-text amount-cell"
:class="{ 'amount-danger': Math.abs(row.settlement_diff_amount) >= 0.05 }"
:class="{ 'amount-danger': Math.abs(row.settlement_diff_amount_cent) >= 5 }"
>
{{ money(row.settlement_diff_amount) }}
{{ moneyCent(row.settlement_diff_amount_cent) }}
</span>
</div>
</div>
@@ -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) {
</div>
<div class="metric-card">
<span>押金</span>
<strong>{{ money(listing.deposit_amount) }}</strong>
<strong>{{ moneyCent(listing.deposit_amount_cent) }}</strong>
</div>
</div>
@@ -172,7 +172,7 @@ function readError(error: unknown, fallback: string) {
<p>号主{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p>
<p>号主 ID{{ listing.owner_id }}</p>
<p>价格{{ listingPrice(listing) }}</p>
<p>押金{{ money(listing.deposit_amount) }}</p>
<p>押金{{ moneyCent(listing.deposit_amount_cent) }}</p>
</div>
</div>
@@ -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) {
<span>KD {{ assetNumberText(item, 'secret_kd') }}</span>
</div>
<div class="queue-footer">
<span>{{ money(item.price) }} / {{ money(item.deposit_amount) }}</span>
<span>¥{{ formatCent(item.price_cent) }} / ¥{{ formatCent(item.deposit_amount_cent) }}</span>
<el-tag v-if="isExternalUpload(item)" size="small" type="info">{{
uploaderName(item)
}}</el-tag>
@@ -733,7 +734,7 @@ function readError(error: unknown, fallback: string) {
</div>
<div class="price-decision-card deposit">
<span>押金与损耗</span>
<strong>{{ money(selectedListing.deposit_amount) }}</strong>
<strong>¥{{ formatCent(selectedListing.deposit_amount_cent) }}</strong>
<p>每日损耗 {{ dailyLossText(selectedListing) }}</p>
<small>哈夫币 {{ formatHafCoinM(getCoinWan(selectedListing)) }}</small>
</div>
@@ -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<AdminListingQuery>({
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) {
@@ -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) {
</div>
<div class="metric-card">
<span>订单金额</span>
<strong>¥{{ money(amountYuan(order.rent_amount_cent, order.rent_amount)) }}</strong>
<strong>¥{{ money(amountYuan(order.rent_amount_cent)) }}</strong>
</div>
<div class="metric-card">
<span>平台费用</span>
<strong>¥{{ money(amountYuan(order.platform_fee_cent, order.platform_fee)) }}</strong>
<strong>¥{{ money(amountYuan(order.platform_fee_cent)) }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong>
<strong>¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong>
</div>
<div v-if="refundStatus" class="metric-card">
<span>退款状态</span>
@@ -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) {
<template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template>
</el-table-column>
<el-table-column label="订单金额" width="100">
<template #default="{ row }">¥{{ money(amountYuan(row.rent_amount_cent, row.rent_amount)) }}</template>
<template #default="{ row }">¥{{ money(amountYuan(row.rent_amount_cent)) }}</template>
</el-table-column>
<el-table-column label="押金" width="100">
<template #default="{ row }">¥{{ money(amountYuan(row.deposit_amount_cent, row.deposit_amount)) }}</template>
<template #default="{ row }">¥{{ money(amountYuan(row.deposit_amount_cent)) }}</template>
</el-table-column>
<el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
@@ -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) {
@@ -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) {
@@ -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))
}
</script>
@@ -117,13 +117,13 @@ function money(value: number | string | undefined) {
<template #default="{ row }">{{ userStatusLabel(row.status) }}</template>
</el-table-column>
<el-table-column label="免押额度" width="130">
<template #default="{ row }">¥{{ money(row.deposit_free_quota) }}</template>
<template #default="{ row }">¥{{ moneyCent(row.deposit_free_quota_cent) }}</template>
</el-table-column>
<el-table-column label="已占用" width="120">
<template #default="{ row }">¥{{ money(row.deposit_free_used) }}</template>
<template #default="{ row }">¥{{ moneyCent(row.deposit_free_used_cent) }}</template>
</el-table-column>
<el-table-column label="剩余免押" width="120">
<template #default="{ row }">¥{{ money(row.deposit_free_remaining) }}</template>
<template #default="{ row }">¥{{ moneyCent(row.deposit_free_remaining_cent) }}</template>
</el-table-column>
<el-table-column label="注册时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
@@ -195,8 +195,8 @@ function money(value: number | string | undefined) {
<strong>{{ quotaUser.phone }}</strong> · {{ quotaUser.nickname }}
</p>
<p>
已占用 ¥{{ money(quotaUser.deposit_free_used) }}剩余 ¥{{
money(quotaUser.deposit_free_remaining)
已占用 ¥{{ moneyCent(quotaUser.deposit_free_used_cent) }}剩余 ¥{{
moneyCent(quotaUser.deposit_free_remaining_cent)
}}
</p>
<el-input-number
@@ -47,7 +47,7 @@ export async function fetchAdminDisputes(page = 1, pageSize = 20) {
export async function arbitrateDispute(
id: number,
payload: { result: string; remark: string; amount?: number }
payload: { result: string; remark: string; amount_cent?: number }
) {
const { data } = await apiClient.post<ApiResponse<Dispute>>(
`/admin/disputes/${id}/arbitrate`,
@@ -19,8 +19,8 @@ export interface Listing {
asset_summary?: Record<string, unknown>
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<string, unknown>
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
}
@@ -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) {
</div>
<div class="price-item deposit">
<small>押金</small>
<span>¥{{ formatMoney(listing.deposit_amount) }}</span>
<span>¥{{ formatCent(listing.deposit_amount_cent) }}</span>
</div>
<div class="price-action">
<button class="rent-btn">立即租用</button>
@@ -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) {
<strong>¥{{ formatMoney(orderPriceBreakdown.consumable) }}</strong>
</div>
</div>
<em>押金另付 ¥{{ formatMoney(listing.deposit_amount) }}</em>
<em>押金另付 ¥{{ formatCent(listing.deposit_amount_cent) }}</em>
</div>
<dl class="order-check-list">
<div>
@@ -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) {
</div>
<div class="resource-price-box">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
<span class="rent-sub">押金¥{{ formatMoney(item.deposit_amount) }}</span>
<span class="rent-sub">押金¥{{ formatCent(item.deposit_amount_cent) }}</span>
</div>
</RouterLink>
</div>
@@ -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) {
<div class="card-footer">
<div class="price-col">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong>
<span class="rent-sub">押金 ¥{{ formatMoney(item.deposit_amount) }}</span>
<span class="rent-sub">押金 ¥{{ formatCent(item.deposit_amount_cent) }}</span>
</div>
</div>
</div>
@@ -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: '' },
]
})
@@ -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<string, unknown>
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<ApiResponse<RefundStatus>>(`/admin/orders/${id}/refund`)
return data.data
}
@@ -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<string, unknown> {
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() {
<div class="meta-item">
<span class="meta-label">押金</span>
<strong class="meta-value"
>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong
>¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong
>
<span
v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0"
v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
class="meta-note"
>已免押 ¥{{
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount))
}}</span
>已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</span
>
</div>
<div class="meta-item">
@@ -956,59 +946,36 @@ async function copyListingCode() {
<van-cell-group inset :border="false">
<van-cell
title="实际结算租金"
:value="`¥${money(
amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount)
)}`"
:value="`¥${money(amountYuan(order.checkout.display_amount_cent))}`"
/>
<van-cell
title="押金总额"
:label="
amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0
? `已免押 ¥${money(
amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)
)}`
amountYuan(order.deposit_waived_amount_cent) > 0
? `已免押 ¥${money(amountYuan(order.deposit_waived_amount_cent))}`
: ''
"
:value="`¥${money(
amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount)
)}`"
:value="`¥${money(amountYuan(order.checkout.deposit_amount_cent))}`"
/>
<van-cell
title="额外消耗品已用"
:value="`¥${money(
amountYuan(order.checkout.consumable_amount_cent, order.checkout.consumable_amount)
)}`"
:value="`¥${money(amountYuan(order.checkout.consumable_amount_cent))}`"
/>
<van-cell
title="押金赔付扣除"
:value="`¥${money(
amountYuan(
order.checkout.deposit_deduct_amount_cent,
order.checkout.deposit_deduct_amount
)
)}`"
:value="`¥${money(amountYuan(order.checkout.deposit_deduct_amount_cent))}`"
value-class="red-text"
/>
<van-cell
v-if="isRenter"
title="退还租客"
:value="`¥${money(
amountYuan(
order.checkout.renter_refund_amount_cent,
order.checkout.renter_refund_amount
)
)}`"
:value="`¥${money(amountYuan(order.checkout.renter_refund_amount_cent))}`"
value-class="green-text"
/>
<van-cell
v-if="isOwner"
title="号主最终收入"
:value="`¥${money(
amountYuan(
order.checkout.owner_income_amount_cent,
order.checkout.owner_income_amount
)
)}`"
:value="`¥${money(amountYuan(order.checkout.owner_income_amount_cent))}`"
value-class="green-text"
/>
<van-cell
@@ -131,21 +131,21 @@ function amountLabel(order: Order) {
return isOwner(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)
return amountYuan(order.rent_amount_cent, order.rent_amount ?? order.display_amount)
if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent)
return amountYuan(order.rent_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 formatListingCode(order: Order) {
@@ -238,9 +238,9 @@ async function copyListingCode(order: Order) {
<div class="price-item">
<span class="price-label">押金金额</span>
<span class="price-val deposit">
¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0"
> ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}</em
¥{{ money(amountYuan(order.deposit_amount_cent)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
> ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
>
</span>
</div>
@@ -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<string, unknown> {
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() {
<div class="metric-card">
<span class="metric-label">押金</span>
<strong class="metric-value"
>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong
>¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong
>
<span
v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0"
v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
class="metric-note"
>已免押 ¥{{
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount))
money(amountYuan(order.deposit_waived_amount_cent))
}}</span
>
</div>
@@ -979,81 +971,40 @@ async function copyListingCode() {
<div class="summary-row">
<span>实际结算租金</span>
<strong
>¥{{
money(
amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount)
)
}}</strong
>¥{{ money(amountYuan(order.checkout.display_amount_cent)) }}</strong
>
</div>
<div class="summary-row">
<span>预收押金</span>
<strong>
¥{{
money(
amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount)
)
}}
<em
v-if="
amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0
"
>已免押 ¥{{
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount))
}}</em
¥{{ money(amountYuan(order.checkout.deposit_amount_cent)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
>已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
>
</strong>
</div>
<div class="summary-row">
<span>额外消耗品已用</span>
<strong class="warning"
>¥{{
money(
amountYuan(
order.checkout.consumable_amount_cent,
order.checkout.consumable_amount
)
)
}}</strong
>¥{{ money(amountYuan(order.checkout.consumable_amount_cent)) }}</strong
>
</div>
<div class="summary-row">
<span>押金赔付扣除</span>
<strong class="warning"
>¥{{
money(
amountYuan(
order.checkout.deposit_deduct_amount_cent,
order.checkout.deposit_deduct_amount
)
)
}}</strong
>¥{{ money(amountYuan(order.checkout.deposit_deduct_amount_cent)) }}</strong
>
</div>
<div v-if="isRenter" class="summary-row highlight">
<span>退还租客(未使用租金 + 剩余押金)</span>
<strong class="amount"
>¥{{
money(
amountYuan(
order.checkout.renter_refund_amount_cent,
order.checkout.renter_refund_amount
)
)
}}</strong
>¥{{ money(amountYuan(order.checkout.renter_refund_amount_cent)) }}</strong
>
</div>
<div v-if="isOwner" class="summary-row highlight">
<span>号主最终收入(租金 + 押金赔付)</span>
<strong class="amount"
>¥{{
money(
amountYuan(
order.checkout.owner_income_amount_cent,
order.checkout.owner_income_amount
)
)
}}</strong
>¥{{ money(amountYuan(order.checkout.owner_income_amount_cent)) }}</strong
>
</div>
<div v-if="order.checkout.content" class="summary-note">
@@ -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() {
<div class="pay-summary">
<div class="pay-summary-row">
<span>支付金额</span>
<strong>¥{{ formatMoney(activePayment.amount_cent / 100) }}</strong>
<strong>¥{{ formatCent(activePayment.amount_cent) }}</strong>
</div>
</div>
<div v-if="paymentPayURL()" class="pay-qr-section">
@@ -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) {
<el-table-column label="押金" width="100">
<template #default="{ row }">
<div class="amount-cell">
<span class="amount-value">¥{{ money(row.deposit_amount) }}</span>
<span v-if="row.deposit_waived_amount > 0" class="amount-label"
> ¥{{ money(row.deposit_waived_amount) }}</span
<span class="amount-value">¥{{ money(amountYuan(row.deposit_amount_cent)) }}</span>
<span v-if="amountYuan(row.deposit_waived_amount_cent) > 0" class="amount-label"
> ¥{{ money(amountYuan(row.deposit_waived_amount_cent)) }}</span
>
</div>
</template>
@@ -357,9 +357,9 @@ function getCountdownMinutes(order: Order) {
<div class="meta-row">
<span class="meta-label">押金</span>
<span class="meta-value">
¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0"
> ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}</em
¥{{ money(amountYuan(order.deposit_amount_cent)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
> ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
>
</span>
</div>
@@ -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,
})
@@ -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)
}
</script>
<template>
@@ -122,7 +126,7 @@ function money(value: unknown) {
<el-table-column prop="order_no" label="订单号" min-width="220" />
<el-table-column prop="title" label="账号" min-width="180" />
<el-table-column label="金额" width="120">
<template #default="{ row }">¥{{ money(row.display_amount) }}</template>
<template #default="{ row }">¥{{ money(sellerAmount(row)) }}</template>
</el-table-column>
<el-table-column label="订单状态" width="150">
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
@@ -9,7 +9,7 @@ import {
submitListingReview,
type Listing,
} from '@/features/listings'
import { formatMoney, formatMoneyWithSymbol } from '@/shared/utils/money'
import { formatCent, formatMoney, formatMoneyWithSymbol } from '@/shared/utils/money'
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { formatListingCode, getListingSellerPrice } from '@/utils/listingDisplay'
@@ -227,7 +227,7 @@ function isPendingReview(row: Listing) {
</div>
<div>
<span>押金</span>
<strong>¥{{ formatMoney(item.deposit_amount) }}</strong>
<strong>¥{{ formatCent(item.deposit_amount_cent) }}</strong>
</div>
</div>
+3 -2
View File
@@ -3,6 +3,7 @@ import { apiClient } from '@/shared/api/client'
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
import type { BalanceType, LedgerDirection, WalletStatus } from '@/shared/types/status'
import type { PaymentOrder } from '@/features/orders/api/orders'
import { yuanToCent } from '@/shared/utils/money'
export interface WalletAccount {
user_id: number
@@ -42,13 +43,13 @@ export async function fetchWalletLedger(page = 1, pageSize = 20) {
}
export async function rechargeWallet(amountYuan: number) {
const amount_cent = Math.round(amountYuan * 100)
const amount_cent = yuanToCent(amountYuan)
const { data } = await apiClient.post<ApiResponse<WalletAccount>>('/wallet/recharge', { amount_cent })
return data.data
}
export async function startWalletRechargePayment(amountYuan: number) {
const amount_cent = Math.round(amountYuan * 100)
const amount_cent = yuanToCent(amountYuan)
const { data } = await apiClient.post<ApiResponse<PaymentOrder>>('/wallet/recharge/pay', {
amount_cent,
})
@@ -402,8 +402,8 @@ function amountPrefix(direction: string) {
<span>支付金额</span>
<strong>{{
activeRechargePayment
? formatMoney(activeRechargePayment.amount_cent / 100)
: formatMoney(devRechargeAmount)
? formatCentWithSymbol(activeRechargePayment.amount_cent)
: `¥${formatMoney(devRechargeAmount)}`
}}</strong>
</div>
<div v-if="rechargePayURL()" class="cashier-qr">
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Listing } from '@/features/listings/api/listings'
import { centToYuan } from '@/shared/utils/money'
export interface ListingDisplayChip {
label: string
@@ -41,7 +42,7 @@ export function formatHafCoinM(amountWan: number) {
}
export function getListingDisplayPrice(item: Listing) {
return Number(item.price || 0)
return centToYuan(item.price_cent)
}
export function getListingRentPrice(item: Listing) {
+12 -5
View File
@@ -40,12 +40,19 @@ export function centToYuan(cent: number | undefined | null): number {
}
/**
* 0.1
* @example centToJiao(12345) -> 123.5
* @example centToJiao(12344) -> 123.4
* 0.1
* @example centToDisplayYuan(12345) -> 123.5
* @example centToDisplayYuan(12344) -> 123.4
*/
export function centToDisplayYuan(cent: number | undefined | null): number {
return roundMoney(centToYuan(cent))
}
/**
* @deprecated 使 centToDisplayYuan
*/
export function centToJiao(cent: number | undefined | null): number {
return Math.round(Number(cent || 0) / 10) / 10
return centToDisplayYuan(cent)
}
/**
@@ -54,7 +61,7 @@ export function centToJiao(cent: number | undefined | null): number {
* @example formatCent(12344) -> "123.4"
*/
export function formatCent(cent: number | undefined | null): string {
return centToJiao(cent).toFixed(1)
return centToDisplayYuan(cent).toFixed(1)
}
/**
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Listing } from '@/features/listings/api/listings'
import { centToYuan } from '@/shared/utils/money'
export interface ListingDisplayChip {
label: string
@@ -41,7 +42,7 @@ export function formatHafCoinM(amountWan: number) {
}
export function getListingDisplayPrice(item: Listing) {
return Number(item.price || 0)
return centToYuan(item.price_cent)
}
export function getListingRentPrice(item: Listing) {
+22 -21
View File
@@ -53,11 +53,11 @@ BEGIN
END IF;
-- 为用户创建钱包
INSERT INTO wallet_accounts (user_id, available_balance, frozen_balance, status)
INSERT INTO wallet_accounts (user_id, available_balance_cent, frozen_balance_cent, status)
VALUES (
LAST_INSERT_ID(),
(i % 10) * 100.00,
(i % 5) * 50.00,
(i % 10) * 10000,
(i % 5) * 5000,
'active'
) ON DUPLICATE KEY UPDATE user_id=user_id;
@@ -83,8 +83,8 @@ BEGIN
DECLARE i INT DEFAULT 1;
DECLARE user_id_val BIGINT;
DECLARE account_id_val BIGINT;
DECLARE price_val DECIMAL(12,2);
DECLARE deposit_val DECIMAL(12,2);
DECLARE price_val BIGINT;
DECLARE deposit_val BIGINT;
DECLARE verified_user_min BIGINT;
DECLARE verified_user_max BIGINT;
DECLARE user_pick BIGINT;
@@ -146,11 +146,11 @@ BEGIN
SET account_id_val = LAST_INSERT_ID();
-- 创建租号商品
SET price_val = 5.00 + (i % 20) * 0.5;
SET deposit_val = 100.00 + (i % 10) * 50.00;
SET price_val = 500 + (i % 20) * 50;
SET deposit_val = 10000 + (i % 10) * 5000;
INSERT INTO rental_listings (
listing_no, account_id, owner_id, price, deposit_amount,
listing_no, account_id, owner_id, price_cent, deposit_amount_cent,
in_transaction, status, review_status, published_at
) VALUES (
CONCAT(
@@ -205,8 +205,8 @@ BEGIN
DECLARE owner_id_val BIGINT;
DECLARE renter_id_val BIGINT;
DECLARE order_no_val VARCHAR(64);
DECLARE rent_amount_val DECIMAL(12,2);
DECLARE deposit_val DECIMAL(12,2);
DECLARE rent_amount_val BIGINT;
DECLARE deposit_val BIGINT;
DECLARE listing_min BIGINT;
DECLARE listing_max BIGINT;
DECLARE listing_pick BIGINT;
@@ -235,7 +235,7 @@ BEGIN
-- 近似随机选择一个上架商品,避免 ORDER BY RAND() 全表排序。
SET listing_id_val = NULL;
SET listing_pick = listing_min + FLOOR(RAND() * (listing_max - listing_min + 1));
SELECT rl.id, rl.account_id, rl.owner_id, rl.price, rl.deposit_amount
SELECT rl.id, rl.account_id, rl.owner_id, rl.price_cent, rl.deposit_amount_cent
INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
FROM rental_listings rl
WHERE rl.id >= listing_pick
@@ -244,7 +244,7 @@ BEGIN
ORDER BY rl.id LIMIT 1;
IF listing_id_val IS NULL THEN
SELECT rl.id, rl.account_id, rl.owner_id, rl.price, rl.deposit_amount
SELECT rl.id, rl.account_id, rl.owner_id, rl.price_cent, rl.deposit_amount_cent
INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
FROM rental_listings rl
WHERE rl.status IN ('published', 'active')
@@ -274,8 +274,8 @@ BEGIN
INSERT INTO rental_orders (
order_no, listing_id, account_id, owner_id, renter_id,
estimated_duration_hours, rent_amount, owner_rent_amount,
deposit_amount, platform_fee, status, handoff_status,
estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent,
deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status,
settlement_status, rented_at, created_at
) VALUES (
order_no_val,
@@ -285,9 +285,10 @@ BEGIN
renter_id_val,
24,
rent_amount_val,
rent_amount_val * 0.95, -- 号主实得95%
ROUND(rent_amount_val * 0.95), -- 号主实得95%
deposit_val,
rent_amount_val * 0.05, -- 平台5%手续费
deposit_val,
ROUND(rent_amount_val * 0.05), -- 平台5%手续费
CASE (i % 10)
WHEN 0 THEN 'pending_payment'
WHEN 1 THEN 'cancelled'
@@ -331,7 +332,7 @@ BEGIN
DECLARE user_id_val BIGINT;
DECLARE order_id_val BIGINT;
DECLARE ledger_no_val VARCHAR(64);
DECLARE amount_val DECIMAL(12,2);
DECLARE amount_val BIGINT;
DECLARE user_min BIGINT;
DECLARE user_max BIGINT;
DECLARE order_min BIGINT;
@@ -374,18 +375,18 @@ BEGIN
END IF;
SET ledger_no_val = CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(i, 6, '0'));
SET amount_val = (i % 500) + RAND() * 100;
SET amount_val = ((i % 500) * 100) + FLOOR(RAND() * 10000);
INSERT INTO wallet_ledger (
ledger_no, user_id, order_id, direction, amount,
balance_after, balance_type, biz_type, biz_no, remark, created_at
ledger_no, user_id, order_id, direction, amount_cent,
balance_after_cent, balance_type, biz_type, biz_no, remark, created_at
) VALUES (
ledger_no_val,
user_id_val,
order_id_val,
CASE WHEN i % 2 = 0 THEN 'in' ELSE 'out' END,
amount_val,
1000.00 + (i % 1000),
100000 + (i % 1000) * 100,
CASE WHEN i % 5 = 0 THEN 'frozen' ELSE 'available' END,
CASE (i % 6)
WHEN 0 THEN 'rent_payment'
+21 -20
View File
@@ -69,11 +69,11 @@ BEGIN
AND NOT EXISTS (SELECT 1 FROM user_realname WHERE user_id = u.id);
-- 批量生成钱包账户
INSERT INTO wallet_accounts (user_id, available_balance, frozen_balance, status)
INSERT INTO wallet_accounts (user_id, available_balance_cent, frozen_balance_cent, status)
SELECT
u.id,
(u.id % 10) * 100.00,
(u.id % 5) * 50.00,
(u.id % 10) * 10000,
(u.id % 5) * 5000,
'active'
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM wallet_accounts WHERE user_id = u.id);
@@ -159,7 +159,7 @@ BEGIN
-- 批量生成租号商品
INSERT INTO rental_listings (
listing_no, account_id, owner_id, price, deposit_amount,
listing_no, account_id, owner_id, price_cent, deposit_amount_cent,
in_transaction, status, review_status, published_at
)
SELECT
@@ -169,8 +169,8 @@ BEGIN
) as listing_no,
ga.id as account_id,
ga.owner_id,
5.00 + ((ga.id % 20) * 0.5) as price,
100.00 + ((ga.id % 10) * 50.00) as deposit_amount,
500 + ((ga.id % 20) * 50) as price_cent,
10000 + ((ga.id % 10) * 5000) as deposit_amount_cent,
CASE WHEN ga.id % 10 = 0 THEN 1 ELSE 0 END as in_transaction,
CASE
WHEN ga.id % 20 = 0 THEN 'offline'
@@ -222,13 +222,13 @@ BEGIN
id BIGINT PRIMARY KEY,
account_id BIGINT,
owner_id BIGINT,
price DECIMAL(12,2),
deposit_amount DECIMAL(12,2),
price_cent BIGINT,
deposit_amount_cent BIGINT,
row_num INT
);
INSERT INTO tmp_available_listings (id, account_id, owner_id, price, deposit_amount, row_num)
SELECT id, account_id, owner_id, price, deposit_amount, (@rn := @rn + 1)
INSERT INTO tmp_available_listings (id, account_id, owner_id, price_cent, deposit_amount_cent, row_num)
SELECT id, account_id, owner_id, price_cent, deposit_amount_cent, (@rn := @rn + 1)
FROM rental_listings, (SELECT @rn := 0) init
WHERE status IN ('published', 'active') AND review_status = 'approved'
ORDER BY id;
@@ -255,8 +255,8 @@ BEGIN
WHILE current_batch < batches DO
INSERT INTO rental_orders (
order_no, listing_id, account_id, owner_id, renter_id,
estimated_duration_hours, rent_amount, owner_rent_amount,
deposit_amount, platform_fee, status, handoff_status,
estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent,
deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status,
settlement_status, rented_at, created_at
)
SELECT
@@ -266,10 +266,11 @@ BEGIN
l.owner_id,
r.id as renter_id,
24 as estimated_duration_hours,
l.price * 24 as rent_amount,
l.price * 24 * 0.95 as owner_rent_amount,
l.deposit_amount,
l.price * 24 * 0.05 as platform_fee,
l.price_cent * 24 as rent_amount_cent,
ROUND(l.price_cent * 24 * 0.95) as owner_rent_amount_cent,
l.deposit_amount_cent,
l.deposit_amount_cent,
ROUND(l.price_cent * 24 * 0.05) as platform_fee_cent,
CASE (seq % 10)
WHEN 0 THEN 'pending_payment'
WHEN 1 THEN 'cancelled'
@@ -360,16 +361,16 @@ BEGIN
WHILE current_batch < batches DO
INSERT INTO wallet_ledger (
ledger_no, user_id, order_id, direction, amount,
balance_after, balance_type, biz_type, biz_no, remark, created_at
ledger_no, user_id, order_id, direction, amount_cent,
balance_after_cent, balance_type, biz_type, biz_no, remark, created_at
)
SELECT
CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(seq, 6, '0')) as ledger_no,
u.id as user_id,
IF(seq % 2 = 0, o.id, NULL) as order_id,
CASE WHEN seq % 2 = 0 THEN 'in' ELSE 'out' END as direction,
(seq % 500) + (seq * 0.01) as amount,
1000.00 + (seq % 1000) as balance_after,
((seq % 500) * 100) + seq as amount_cent,
100000 + ((seq % 1000) * 100) as balance_after_cent,
CASE WHEN seq % 5 = 0 THEN 'frozen' ELSE 'available' END as balance_type,
CASE (seq % 6)
WHEN 0 THEN 'rent_payment'