统一金额分制重构

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"` ListingNo string `gorm:"column:listing_no;size:20;not null;uniqueIndex" json:"listing_no"`
AccountID uint64 `gorm:"not null;index" json:"account_id"` AccountID uint64 `gorm:"not null;index" json:"account_id"`
OwnerID uint64 `gorm:"not null;index" json:"owner_id"` OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
Price float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
PriceCent int64 `gorm:"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:"-"` DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"` InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
Status string `gorm:"size:32;not null;default:'draft'" json:"status"` Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
-6
View File
@@ -15,17 +15,11 @@ type RentalOrder struct {
RenterID uint64 `gorm:"not null;index" json:"renter_id"` RenterID uint64 `gorm:"not null;index" json:"renter_id"`
RentedAt *time.Time `json:"rented_at"` RentedAt *time.Time `json:"rented_at"`
EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"` EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"`
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
RentAmountCent int64 `gorm:"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:"-"` 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:"-"` 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:"-"` 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:"-"` 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:"-"` PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
AccountSnapshot datatypes.JSON `json:"account_snapshot"` AccountSnapshot datatypes.JSON `json:"account_snapshot"`
Status string `gorm:"size:32;not null;default:'pending_payment'" json:"status"` 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"` OrderID uint64 `gorm:"not null;index" json:"order_id"`
InitiatedBy uint64 `gorm:"not null" json:"initiated_by"` InitiatedBy uint64 `gorm:"not null" json:"initiated_by"`
Status string `gorm:"size:32;not null;default:'submitted'" json:"status"` Status string `gorm:"size:32;not null;default:'submitted'" json:"status"`
RentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
RentAmountCent int64 `gorm:"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:"-"` 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:"-"` 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:"-"` 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:"-"` ConsumableAmountCent int64 `gorm:"not null;default:0" json:"-"`
CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"` CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"`
OtherAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
OtherAmountCent int64 `gorm:"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:"-"` 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:"-"` 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:"-"` OwnerIncomeAmountCent int64 `gorm:"not null;default:0" json:"-"`
Content string `json:"content"` Content string `json:"content"`
EvidenceURLS datatypes.JSON `gorm:"column:evidence_urls" json:"evidence_urls"` 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"` RealnameStatus string `gorm:"size:32;not null;default:'unverified'" json:"realname_status"`
RiskStatus string `gorm:"size:32;not null;default:'normal'" json:"risk_status"` RiskStatus string `gorm:"size:32;not null;default:'normal'" json:"risk_status"`
CreditScore int `gorm:"not null;default:100" json:"credit_score"` 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:"-"` DepositFreeQuotaCent int64 `gorm:"not null;default:0" json:"-"`
Status string `gorm:"size:32;not null;default:'active'" json:"status"` Status string `gorm:"size:32;not null;default:'active'" json:"status"`
LastLoginAt *time.Time `json:"last_login_at"` LastLoginAt *time.Time `json:"last_login_at"`
-4
View File
@@ -5,9 +5,7 @@ import "time"
type WalletAccount struct { type WalletAccount struct {
ID uint64 `gorm:"primaryKey" json:"id"` ID uint64 `gorm:"primaryKey" json:"id"`
UserID uint64 `gorm:"not null;uniqueIndex" json:"user_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:"-"` 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:"-"` FrozenBalanceCent int64 `gorm:"not null;default:0" json:"-"`
Status string `gorm:"size:32;not null;default:'active'" json:"status"` Status string `gorm:"size:32;not null;default:'active'" json:"status"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -24,9 +22,7 @@ type WalletLedger struct {
UserID uint64 `gorm:"not null;index" json:"user_id"` UserID uint64 `gorm:"not null;index" json:"user_id"`
OrderID *uint64 `json:"order_id"` OrderID *uint64 `json:"order_id"`
Direction string `gorm:"size:16;not null" json:"direction"` 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:"-"` 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:"-"` BalanceAfterCent int64 `gorm:"not null;default:0" json:"-"`
BalanceType string `gorm:"size:32;not null" json:"balance_type"` BalanceType string `gorm:"size:32;not null" json:"balance_type"`
BizType string `gorm:"size:32;not null" json:"biz_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"` ID uint64 `gorm:"primaryKey" json:"id"`
WithdrawNo string `gorm:"size:64;not null;uniqueIndex" json:"withdraw_no"` WithdrawNo string `gorm:"size:64;not null;uniqueIndex" json:"withdraw_no"`
UserID uint64 `gorm:"not null;index" json:"user_id"` 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:"-"` 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:"-"` 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:"-"` ActualAmountCent int64 `gorm:"not null;default:0" json:"-"`
PaymentAccountID *uint64 `json:"payment_account_id"` PaymentAccountID *uint64 `json:"payment_account_id"`
AccountType string `gorm:"size:32;not null" json:"account_type"` AccountType string `gorm:"size:32;not null" json:"account_type"`
@@ -18,7 +18,7 @@ type MetricsDTO struct {
TotalOrders int64 `json:"total_orders"` TotalOrders int64 `json:"total_orders"`
RentingOrders int64 `json:"renting_orders"` RentingOrders int64 `json:"renting_orders"`
TodayOrders int64 `json:"today_orders"` TodayOrders int64 `json:"today_orders"`
TodayLedgerAmount float64 `json:"today_ledger_amount"` TodayLedgerAmountCent int64 `json:"today_ledger_amount_cent"`
} }
type PendingDTO struct { type PendingDTO struct {
@@ -35,8 +35,8 @@ type RecentOrderDTO struct {
RenterID uint64 `json:"renter_id"` RenterID uint64 `json:"renter_id"`
OwnerID uint64 `json:"owner_id"` OwnerID uint64 `json:"owner_id"`
Status string `json:"status"` Status string `json:"status"`
RentAmount float64 `json:"rent_amount"` RentAmountCent int64 `json:"rent_amount_cent"`
DepositAmount float64 `json:"deposit_amount"` DepositAmountCent int64 `json:"deposit_amount_cent"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }
@@ -44,9 +44,9 @@ func (r *Repository) Summary() (*DashboardDTO, error) {
return nil, err return nil, err
} }
if err := r.db.Model(&model.WalletLedger{}). if err := r.db.Model(&model.WalletLedger{}).
Select("COALESCE(SUM(amount), 0)"). Select("COALESCE(SUM(amount_cent), 0)").
Where("created_at >= ?", today). Where("created_at >= ?", today).
Scan(&metrics.TodayLedgerAmount).Error; err != nil { Scan(&metrics.TodayLedgerAmountCent).Error; err != nil {
return nil, err return nil, err
} }
if err := r.db.Model(&model.RentalListing{}).Where("review_status = ?", "pending").Count(&pending.ListingReviews).Error; err != nil { 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) { func (r *Repository) recentOrders() ([]RecentOrderDTO, error) {
rows := make([]RecentOrderDTO, 0) rows := make([]RecentOrderDTO, 0)
err := r.db.Table("rental_orders AS o"). 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"). Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Order("o.id DESC"). Order("o.id DESC").
Limit(8). Limit(8).
+17 -17
View File
@@ -30,10 +30,10 @@ type FinanceSummaryDTO struct {
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"` TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"` PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmount float64 `json:"platform_income_amount"` PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"` OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"` OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"` SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
SuccessfulPayCount int64 `json:"successful_pay_count"` SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"` SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"` PendingRefundCount int64 `json:"pending_refund_count"`
@@ -47,10 +47,10 @@ type FinanceDailyDTO struct {
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"` TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"` PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmount float64 `json:"platform_income_amount"` PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"` OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"` OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"` SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
SuccessfulPayCount int64 `json:"successful_pay_count"` SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"` SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"` PendingRefundCount int64 `json:"pending_refund_count"`
@@ -76,20 +76,20 @@ type FinanceDetailDTO struct {
OwnerID uint64 `json:"owner_id"` OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone"` OwnerPhone string `json:"owner_phone"`
OwnerNickname string `json:"owner_nickname"` OwnerNickname string `json:"owner_nickname"`
OrderRentAmount float64 `json:"order_rent_amount"` OrderRentAmountCent int64 `json:"order_rent_amount_cent"`
OrderDepositAmount float64 `json:"order_deposit_amount"` OrderDepositAmountCent int64 `json:"order_deposit_amount_cent"`
CheckoutRentAmount float64 `json:"checkout_rent_amount"` CheckoutRentAmountCent int64 `json:"checkout_rent_amount_cent"`
CheckoutRenterRefund float64 `json:"checkout_renter_refund"` CheckoutRenterRefundCent int64 `json:"checkout_renter_refund_cent"`
CheckoutOwnerIncome float64 `json:"checkout_owner_income"` CheckoutOwnerIncomeCent int64 `json:"checkout_owner_income_cent"`
CheckoutPlatformFee float64 `json:"checkout_platform_fee"` CheckoutPlatformFeeCent int64 `json:"checkout_platform_fee_cent"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"` OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
PaidAmountCent int64 `json:"paid_amount_cent"` PaidAmountCent int64 `json:"paid_amount_cent"`
RefundedAmountCent int64 `json:"refunded_amount_cent"` RefundedAmountCent int64 `json:"refunded_amount_cent"`
RefundingAmountCent int64 `json:"refunding_amount_cent"` RefundingAmountCent int64 `json:"refunding_amount_cent"`
FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"` FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformNetAmount float64 `json:"platform_net_amount"` PlatformNetAmountCent int64 `json:"platform_net_amount_cent"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"` SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
FinanceStatus string `json:"finance_status"` FinanceStatus string `json:"finance_status"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
SettledAt *time.Time `json:"settled_at,omitempty"` SettledAt *time.Time `json:"settled_at,omitempty"`
@@ -1,11 +1,9 @@
package adminfinance package adminfinance
import ( import (
"math"
"time" "time"
"hfb_sys/backend/internal/timeutil" "hfb_sys/backend/internal/timeutil"
"hfb_sys/backend/pkg/money"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -71,9 +69,9 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) {
var settlement settlementSummaryRow var settlement settlementSummaryRow
if err := r.db.Table("rental_orders AS ro"). if err := r.db.Table("rental_orders AS ro").
Select(`COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount, Select(`COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent,
COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount, COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent,
COUNT(ro.id) AS settled_order_count`). COUNT(ro.id) AS settled_order_count`).
Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). 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)). 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, TotalRefundAmountCent: payment.TotalRefundAmountCent,
PendingRefundAmountCent: payment.PendingRefundAmountCent, PendingRefundAmountCent: payment.PendingRefundAmountCent,
ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent, ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent,
PlatformIncomeAmount: money.Round(settlement.PlatformIncomeAmount), PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent,
OwnerShouldIncomeAmount: money.Round(settlement.OwnerShouldIncomeAmount), OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent,
OwnerWalletIncomeAmount: money.Round(settlement.OwnerWalletIncomeAmount), OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
SettlementDiffAmount: money.Round(settlement.OwnerShouldIncomeAmount - settlement.OwnerWalletIncomeAmount), SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent,
SuccessfulPayCount: payment.SuccessfulPayCount, SuccessfulPayCount: payment.SuccessfulPayCount,
SuccessfulRefundCount: payment.SuccessfulRefundCount, SuccessfulRefundCount: payment.SuccessfulRefundCount,
PendingRefundCount: payment.PendingRefundCount, PendingRefundCount: payment.PendingRefundCount,
@@ -130,9 +128,9 @@ func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error)
settlements := make([]dailySettlementRow, 0) settlements := make([]dailySettlementRow, 0)
if err := r.db.Table("rental_orders AS ro"). if err := r.db.Table("rental_orders AS ro").
Select(`DATE(ro.settled_at) AS date, Select(`DATE(ro.settled_at) AS date,
COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount, COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent,
COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount, COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent,
COUNT(ro.id) AS settled_order_count`). COUNT(ro.id) AS settled_order_count`).
Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). 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)). 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 { for _, row := range settlements {
item := itemsByDate[row.Date] item := itemsByDate[row.Date]
item.Date = row.Date item.Date = row.Date
item.PlatformIncomeAmount = money.Round(row.PlatformIncomeAmount) item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent
item.OwnerShouldIncomeAmount = money.Round(row.OwnerShouldIncomeAmount) item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent
item.OwnerWalletIncomeAmount = money.Round(row.OwnerWalletIncomeAmount) item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent
item.SettlementDiffAmount = money.Round(row.OwnerShouldIncomeAmount - row.OwnerWalletIncomeAmount) item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent
item.SettledOrderCount = row.SettledOrderCount item.SettledOrderCount = row.SettledOrderCount
itemsByDate[row.Date] = item 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, 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.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.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, ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent,
COALESCE(oc.rent_amount, 0) AS checkout_rent_amount, COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent,
COALESCE(oc.renter_refund_amount, 0) AS checkout_renter_refund, COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent,
COALESCE(oc.owner_income_amount, 0) AS checkout_owner_income, COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent,
COALESCE(oc.platform_fee, 0) AS checkout_platform_fee, COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent,
COALESCE(w.owner_wallet_income_amount, 0) AS owner_wallet_income_amount, 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.paid_amount_cent, 0) AS paid_amount_cent,
COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent,
COALESCE(p.refunding_amount_cent, 0) AS refunding_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.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(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.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, 0) - COALESCE(w.owner_wallet_income_amount, 0) AS settlement_diff_amount, COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent,
CASE CASE
WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' 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 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' ELSE 'normal'
END AS finance_status, END AS finance_status,
ro.created_at, ro.settled_at`). ro.created_at, ro.settled_at`).
@@ -250,7 +248,7 @@ func orderPaymentSubquery(db *gorm.DB) *gorm.DB {
func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB { func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB {
return db.Table("wallet_ledger"). 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"}). Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation"}).
Group("order_id") Group("order_id")
} }
@@ -283,9 +281,9 @@ type paymentSummaryRow struct {
} }
type settlementSummaryRow struct { type settlementSummaryRow struct {
PlatformIncomeAmount float64 PlatformIncomeAmountCent int64
OwnerShouldIncomeAmount float64 OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmount float64 OwnerWalletIncomeAmountCent int64
SettledOrderCount int64 SettledOrderCount int64
} }
@@ -301,9 +299,9 @@ type dailyPaymentRow struct {
type dailySettlementRow struct { type dailySettlementRow struct {
Date string Date string
PlatformIncomeAmount float64 PlatformIncomeAmountCent int64
OwnerShouldIncomeAmount float64 OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmount float64 OwnerWalletIncomeAmountCent int64
SettledOrderCount int64 SettledOrderCount int64
} }
@@ -319,32 +317,32 @@ type financeDetailRow struct {
OwnerID uint64 OwnerID uint64
OwnerPhone string OwnerPhone string
OwnerNickname string OwnerNickname string
OrderRentAmount float64 OrderRentAmountCent int64
OrderDepositAmount float64 OrderDepositAmountCent int64
CheckoutRentAmount float64 CheckoutRentAmountCent int64
CheckoutRenterRefund float64 CheckoutRenterRefundCent int64
CheckoutOwnerIncome float64 CheckoutOwnerIncomeCent int64
CheckoutPlatformFee float64 CheckoutPlatformFeeCent int64
OwnerWalletIncomeAmount float64 OwnerWalletIncomeAmountCent int64
PaidAmountCent int64 PaidAmountCent int64
RefundedAmountCent int64 RefundedAmountCent int64
RefundingAmountCent int64 RefundingAmountCent int64
FailedRefundAmountCent int64 FailedRefundAmountCent int64
ChannelNetAmountCent int64 ChannelNetAmountCent int64
PlatformNetAmount float64 PlatformNetAmountCent int64
SettlementDiffAmount float64 SettlementDiffAmountCent int64
FinanceStatus string FinanceStatus string
CreatedAt time.Time CreatedAt time.Time
SettledAt *time.Time SettledAt *time.Time
} }
func (r financeDetailRow) toDTO() FinanceDetailDTO { func (r financeDetailRow) toDTO() FinanceDetailDTO {
diff := money.Round(r.SettlementDiffAmount) diff := r.SettlementDiffAmountCent
status := r.FinanceStatus status := r.FinanceStatus
if status == "" { if status == "" {
status = "normal" status = "normal"
} }
if math.Abs(diff) < 0.05 && status == "settlement_diff" { if absCent(diff) < 5 && status == "settlement_diff" {
status = "normal" status = "normal"
} }
return FinanceDetailDTO{ return FinanceDetailDTO{
@@ -359,22 +357,29 @@ func (r financeDetailRow) toDTO() FinanceDetailDTO {
OwnerID: r.OwnerID, OwnerID: r.OwnerID,
OwnerPhone: r.OwnerPhone, OwnerPhone: r.OwnerPhone,
OwnerNickname: r.OwnerNickname, OwnerNickname: r.OwnerNickname,
OrderRentAmount: money.Round(r.OrderRentAmount), OrderRentAmountCent: r.OrderRentAmountCent,
OrderDepositAmount: money.Round(r.OrderDepositAmount), OrderDepositAmountCent: r.OrderDepositAmountCent,
CheckoutRentAmount: money.Round(r.CheckoutRentAmount), CheckoutRentAmountCent: r.CheckoutRentAmountCent,
CheckoutRenterRefund: money.Round(r.CheckoutRenterRefund), CheckoutRenterRefundCent: r.CheckoutRenterRefundCent,
CheckoutOwnerIncome: money.Round(r.CheckoutOwnerIncome), CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent,
CheckoutPlatformFee: money.Round(r.CheckoutPlatformFee), CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent,
OwnerWalletIncomeAmount: money.Round(r.OwnerWalletIncomeAmount), OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent,
PaidAmountCent: r.PaidAmountCent, PaidAmountCent: r.PaidAmountCent,
RefundedAmountCent: r.RefundedAmountCent, RefundedAmountCent: r.RefundedAmountCent,
RefundingAmountCent: r.RefundingAmountCent, RefundingAmountCent: r.RefundingAmountCent,
FailedRefundAmountCent: r.FailedRefundAmountCent, FailedRefundAmountCent: r.FailedRefundAmountCent,
ChannelNetAmountCent: r.ChannelNetAmountCent, ChannelNetAmountCent: r.ChannelNetAmountCent,
PlatformNetAmount: money.Round(r.PlatformNetAmount), PlatformNetAmountCent: r.PlatformNetAmountCent,
SettlementDiffAmount: diff, SettlementDiffAmountCent: diff,
FinanceStatus: status, FinanceStatus: status,
CreatedAt: r.CreatedAt, CreatedAt: r.CreatedAt,
SettledAt: r.SettledAt, 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"` RealnameStatus string `json:"realname_status"`
RiskStatus string `json:"risk_status"` RiskStatus string `json:"risk_status"`
CreditScore int `json:"credit_score"` CreditScore int `json:"credit_score"`
DepositFreeQuota float64 `json:"deposit_free_quota"` DepositFreeQuotaCent int64 `json:"deposit_free_quota_cent"`
DepositFreeUsed float64 `json:"deposit_free_used"` DepositFreeUsedCent int64 `json:"deposit_free_used_cent"`
DepositFreeRemaining float64 `json:"deposit_free_remaining"` DepositFreeRemainingCent int64 `json:"deposit_free_remaining_cent"`
Status string `json:"status"` Status string `json:"status"`
OrderCount int64 `json:"order_count"` OrderCount int64 `json:"order_count"`
ListingCount int64 `json:"listing_count"` ListingCount int64 `json:"listing_count"`
@@ -26,7 +26,6 @@ type FreezeRequest struct {
type DepositFreeQuotaRequest struct { type DepositFreeQuotaRequest struct {
AmountCent int64 `json:"amount_cent"` AmountCent int64 `json:"amount_cent"`
Amount float64 `json:"amount"`
} }
type PaginatedResult struct { type PaginatedResult struct {
@@ -2,11 +2,9 @@ package adminuser
import ( import (
"errors" "errors"
"math"
"hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/model" "hfb_sys/backend/internal/model"
"hfb_sys/backend/pkg/money"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "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(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count, COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_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 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 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 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"). Order("u.id DESC").
Offset(offset).Limit(pageSize). Offset(offset).Limit(pageSize).
Scan(&rows).Error Scan(&rows).Error
@@ -65,23 +63,18 @@ func (r *Repository) SetDepositFreeQuota(adminID uint64, userID uint64, req Depo
if amountCent < 0 { if amountCent < 0 {
return nil, ErrInvalidUser return nil, ErrInvalidUser
} }
amount := float64(amountCent) / 100
err := r.db.Transaction(func(tx *gorm.DB) error { err := r.db.Transaction(func(tx *gorm.DB) error {
var user model.User var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return err return err
} }
beforeAmount := user.DepositFreeQuota
beforeAmountCent := user.DepositFreeQuotaCent beforeAmountCent := user.DepositFreeQuotaCent
user.DepositFreeQuota = amount
user.DepositFreeQuotaCent = amountCent user.DepositFreeQuotaCent = amountCent
if err := tx.Save(&user).Error; err != nil { if err := tx.Save(&user).Error; err != nil {
return err return err
} }
return appendAuditLog(tx, adminID, "admin_user.set_deposit_free_quota", user.ID, meta, map[string]any{ return appendAuditLog(tx, adminID, "admin_user.set_deposit_free_quota", user.ID, meta, map[string]any{
"user_id": user.ID, "user_id": user.ID,
"before_amount": beforeAmount,
"after_amount": amount,
"before_amount_cent": beforeAmountCent, "before_amount_cent": beforeAmountCent,
"after_amount_cent": amountCent, "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(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count, COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_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 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 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 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). Where("u.id = ?", userID).
First(&row).Error First(&row).Error
if err != nil { if err != nil {
@@ -146,11 +139,11 @@ type userRow struct {
OrderCount int64 OrderCount int64
ListingCount int64 ListingCount int64
DisputeCount int64 DisputeCount int64
DepositFreeUsed float64 DepositFreeUsedCent int64
} }
func (row userRow) toDTO() UserDTO { func (row userRow) toDTO() UserDTO {
remaining := roundMoney(row.DepositFreeQuota - row.DepositFreeUsed) remaining := row.DepositFreeQuotaCent - row.DepositFreeUsedCent
if remaining < 0 { if remaining < 0 {
remaining = 0 remaining = 0
} }
@@ -161,9 +154,9 @@ func (row userRow) toDTO() UserDTO {
RealnameStatus: row.RealnameStatus, RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus, RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore, CreditScore: row.CreditScore,
DepositFreeQuota: row.DepositFreeQuota, DepositFreeQuotaCent: row.DepositFreeQuotaCent,
DepositFreeUsed: roundMoney(row.DepositFreeUsed), DepositFreeUsedCent: row.DepositFreeUsedCent,
DepositFreeRemaining: remaining, DepositFreeRemainingCent: remaining,
Status: row.Status, Status: row.Status,
OrderCount: row.OrderCount, OrderCount: row.OrderCount,
ListingCount: row.ListingCount, ListingCount: row.ListingCount,
@@ -174,16 +167,9 @@ func (row userRow) toDTO() UserDTO {
} }
} }
func roundMoney(value float64) float64 {
return money.Round(value)
}
func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 { func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 {
if req.AmountCent != 0 {
return req.AmountCent 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 { func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
return auditlog.Append(tx, auditlog.Entry{ return auditlog.Append(tx, auditlog.Entry{
@@ -9,18 +9,13 @@ func TestDepositFreeQuotaAmountCent(t *testing.T) {
want int64 want int64
}{ }{
{ {
name: "优先使用分字段", name: "使用分字段",
req: DepositFreeQuotaRequest{AmountCent: 1234, Amount: 99}, req: DepositFreeQuotaRequest{AmountCent: 1234},
want: 1234,
},
{
name: "缺少分字段时回退元字段",
req: DepositFreeQuotaRequest{Amount: 12.34},
want: 1234, want: 1234,
}, },
{ {
name: "负分拒绝", name: "负分拒绝",
req: DepositFreeQuotaRequest{AmountCent: -1, Amount: 12.34}, req: DepositFreeQuotaRequest{AmountCent: -1},
want: -1, want: -1,
}, },
} }
@@ -46,7 +46,7 @@ func (s *Service) SetDepositFreeQuota(adminID uint64, userID uint64, req Deposit
if s.repo == nil { if s.repo == nil {
return nil, ErrDependencyUnavailable return nil, ErrDependencyUnavailable
} }
if userID == 0 || req.Amount < 0 { if userID == 0 || req.AmountCent < 0 {
return nil, ErrInvalidUser return nil, ErrInvalidUser
} }
return s.repo.SetDepositFreeQuota(adminID, userID, req, meta) return s.repo.SetDepositFreeQuota(adminID, userID, req, meta)
+1 -1
View File
@@ -37,7 +37,7 @@ type CreateRequest struct {
type ArbitrateRequest struct { type ArbitrateRequest struct {
Result string `json:"result" binding:"required"` Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"` Remark string `json:"remark" binding:"required"`
Amount float64 `json:"amount"` AmountCent int64 `json:"amount_cent"`
} }
type AuditMeta = auditlog.Meta type AuditMeta = auditlog.Meta
+47 -67
View File
@@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"time" "time"
"hfb_sys/backend/internal/auditlog" "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 { if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
return err return err
} }
if settlement.RenterRefundAmount > 0 { if settlement.RenterRefundAmountCent > 0 {
refundCent := int64(math.Round(settlement.RenterRefundAmount * 100)) action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还")
action, err := r.prepareRefund(&order, refundCent, "arbitration_refund", "仲裁退款原路退还")
if err != nil { if err != nil {
return err return err
} }
@@ -286,10 +284,10 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
"order_no": order.OrderNo, "order_no": order.OrderNo,
"result": req.Result, "result": req.Result,
"remark": req.Remark, "remark": req.Remark,
"input_amount": req.Amount, "input_amount_cent": req.AmountCent,
"renter_refund_amount": settlement.RenterRefundAmount, "renter_refund_amount_cent": settlement.RenterRefundAmountCent,
"owner_income_amount": settlement.OwnerIncomeAmount, "owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
"deposit_deduct_amount": settlement.DepositDeductAmount, "deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
"before_order_status": beforeOrderStatus, "before_order_status": beforeOrderStatus,
"after_order_status": order.Status, "after_order_status": order.Status,
"before_handoff_status": beforeHandoffStatus, "before_handoff_status": beforeHandoffStatus,
@@ -339,35 +337,28 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
type arbitrationSettlement struct { type arbitrationSettlement struct {
Entries []wallet.Entry Entries []wallet.Entry
RenterRefundAmount float64 RenterRefundAmountCent int64
OwnerIncomeAmount float64 OwnerIncomeAmountCent int64
DepositDeductAmount float64 DepositDeductAmountCent int64
} }
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalance float64) (arbitrationSettlement, error) { func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
rentAmount := float64(order.RentAmountCent) / 100 rentAmountCent := order.RentAmountCent
if rentAmount <= 0 { depositAmountCent := order.DepositAmountCent
rentAmount = order.RentAmount ownerRentAmountCent := order.OwnerRentAmountCent
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
ownerRentAmountCent = rentAmountCent
} }
depositAmount := float64(order.DepositAmountCent) / 100 totalCent := rentAmountCent + depositAmountCent
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)
settlement := arbitrationSettlement{} settlement := arbitrationSettlement{}
orderID := order.ID orderID := order.ID
releaseFrozenAmount := minMoney(total, roundMoney(renterFrozenBalance)) releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
if releaseFrozenAmount > 0 { if releaseFrozenAmountCent > 0 {
settlement.Entries = append(settlement.Entries, wallet.Entry{ settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.RenterID, UserID: order.RenterID,
OrderID: &orderID, OrderID: &orderID,
Direction: "out", Direction: "out",
AmountCent: int64(math.Round(releaseFrozenAmount * 100)), AmountCent: releaseFrozenAmountCent,
BalanceType: "frozen", BalanceType: "frozen",
BizType: "arbitration_release_frozen", BizType: "arbitration_release_frozen",
BizNo: order.OrderNo, BizNo: order.OrderNo,
@@ -375,22 +366,22 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
}) })
} }
addRenterRefund := func(amount float64, remark string) { addRenterRefund := func(amountCent int64, remark string) {
if amount <= 0 { if amountCent <= 0 {
return return
} }
settlement.RenterRefundAmount += amount settlement.RenterRefundAmountCent += amountCent
} }
addOwnerIncome := func(amount float64, remark string) { addOwnerIncome := func(amountCent int64, remark string) {
if amount <= 0 { if amountCent <= 0 {
return return
} }
settlement.OwnerIncomeAmount += amount settlement.OwnerIncomeAmountCent += amountCent
settlement.Entries = append(settlement.Entries, wallet.Entry{ settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.OwnerID, UserID: order.OwnerID,
OrderID: &orderID, OrderID: &orderID,
Direction: "in", Direction: "in",
AmountCent: int64(math.Round(amount * 100)), AmountCent: amountCent,
BalanceType: "available", BalanceType: "available",
BizType: "arbitration_owner_income", BizType: "arbitration_owner_income",
BizNo: order.OrderNo, BizNo: order.OrderNo,
@@ -400,28 +391,27 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
switch req.Result { switch req.Result {
case "full_refund": case "full_refund":
addRenterRefund(total, "仲裁全额退款") addRenterRefund(totalCent, "仲裁全额退款")
case "partial_refund": case "partial_refund":
req.Amount = roundMoney(req.Amount) if req.AmountCent <= 0 || req.AmountCent > totalCent {
if req.Amount <= 0 || req.Amount > total {
return settlement, ErrInvalidDispute return settlement, ErrInvalidDispute
} }
addRenterRefund(req.Amount, "仲裁部分退款") addRenterRefund(req.AmountCent, "仲裁部分退款")
addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+depositAmount), "仲裁剩余金额结算给号主") addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主")
case "release_deposit": case "release_deposit":
addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主") addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
addRenterRefund(depositAmount, "仲裁释放押金给租客") addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
case "deduct_deposit", "compensate_owner": case "deduct_deposit", "compensate_owner":
deductAmount := roundMoney(req.Amount) deductAmountCent := req.AmountCent
if deductAmount <= 0 { if deductAmountCent <= 0 {
deductAmount = depositAmount deductAmountCent = depositAmountCent
} }
if deductAmount > depositAmount { if deductAmountCent > depositAmountCent {
return settlement, ErrInvalidDispute return settlement, ErrInvalidDispute
} }
settlement.DepositDeductAmount = deductAmount settlement.DepositDeductAmountCent = deductAmountCent
addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主") addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
addRenterRefund(depositAmount-deductAmount, "仲裁退回剩余押金给租客") addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
case "order_close": case "order_close":
// Only release frozen funds. No available-balance settlement happens in development mode. // Only release frozen funds. No available-balance settlement happens in development mode.
case "mark_abnormal": case "mark_abnormal":
@@ -457,7 +447,7 @@ func (r *Repository) startRefundBestEffort(action *refundAction) {
_, _ = r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark) _, _ = 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 var account model.WalletAccount
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("user_id = ?", renterID). Where("user_id = ?", renterID).
@@ -468,17 +458,7 @@ func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
return account.FrozenBalance, nil return account.FrozenBalanceCent, 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)
} }
func (r *Repository) baseQuery() *gorm.DB { func (r *Repository) baseQuery() *gorm.DB {
@@ -550,14 +530,14 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration
if req.Remark != "" { if req.Remark != "" {
content += "\n处理说明:" + req.Remark content += "\n处理说明:" + req.Remark
} }
if settlement.RenterRefundAmount > 0 { if settlement.RenterRefundAmountCent > 0 {
content += fmt.Sprintf("\n退款给租客:¥%.2f", settlement.RenterRefundAmount) content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent)
} }
if settlement.OwnerIncomeAmount > 0 { if settlement.OwnerIncomeAmountCent > 0 {
content += fmt.Sprintf("\n结算给号主:¥%.2f", settlement.OwnerIncomeAmount) content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent)
} }
if settlement.DepositDeductAmount > 0 { if settlement.DepositDeductAmountCent > 0 {
content += fmt.Sprintf("\n押金扣除:¥%.2f", settlement.DepositDeductAmount) content += "\n押金扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
} }
return content return content
} }
@@ -31,11 +31,11 @@ func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *test
t.Fatalf("不应在无冻结余额时生成解冻流水: %+v", entry) t.Fatalf("不应在无冻结余额时生成解冻流水: %+v", entry)
} }
} }
if settlement.OwnerIncomeAmount != 180 { if settlement.OwnerIncomeAmountCent != 18000 {
t.Fatalf("OwnerIncomeAmount = %.1f, want 180.0", settlement.OwnerIncomeAmount) t.Fatalf("OwnerIncomeAmountCent = %d, want 18000", settlement.OwnerIncomeAmountCent)
} }
if settlement.RenterRefundAmount != 100 { if settlement.RenterRefundAmountCent != 10000 {
t.Fatalf("RenterRefundAmount = %.1f, want 100.0", settlement.RenterRefundAmount) t.Fatalf("RenterRefundAmountCent = %d, want 10000", settlement.RenterRefundAmountCent)
} }
for _, entry := range settlement.Entries { for _, entry := range settlement.Entries {
if entry.UserID == order.RenterID && entry.BizType == "arbitration_renter_refund" { if entry.UserID == order.RenterID && entry.BizType == "arbitration_renter_refund" {
@@ -58,7 +58,7 @@ func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testi
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
Result: "order_close", Result: "order_close",
Remark: "测试裁决", Remark: "测试裁决",
}, 120) }, 12000)
if err != nil { if err != nil {
t.Fatalf("buildArbitrationSettlement() error = %v", err) t.Fatalf("buildArbitrationSettlement() error = %v", err)
} }
@@ -83,15 +83,15 @@ func TestBuildArbitrationHandoffContent(t *testing.T) {
Result: "partial_refund", Result: "partial_refund",
Remark: "账号异常,退还部分租金。", Remark: "账号异常,退还部分租金。",
}, arbitrationSettlement{ }, arbitrationSettlement{
RenterRefundAmount: 80, RenterRefundAmountCent: 8000,
OwnerIncomeAmount: 120, OwnerIncomeAmountCent: 12000,
}) })
wantParts := []string{ wantParts := []string{
"客服仲裁结果:部分退款", "客服仲裁结果:部分退款",
"处理说明:账号异常,退还部分租金。", "处理说明:账号异常,退还部分租金。",
"退款给租客:¥80.00", "退款给租客:¥80.0",
"结算给号主:¥120.00", "结算给号主:¥120.0",
} }
for _, part := range wantParts { for _, part := range wantParts {
if !strings.Contains(content, part) { if !strings.Contains(content, part) {
+3 -3
View File
@@ -46,8 +46,8 @@ type CreateRequest struct {
HafCoinAmount int64 `json:"haf_coin_amount"` HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary"` AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"` ScreenshotURLS []string `json:"screenshot_urls"`
Price float64 `json:"price"` PriceCent int64 `json:"price_cent"`
DepositAmount float64 `json:"deposit_amount"` DepositAmountCent int64 `json:"deposit_amount_cent"`
AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"` AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"`
AgreedSellerAgreement bool `json:"agreed_seller_agreement"` AgreedSellerAgreement bool `json:"agreed_seller_agreement"`
@@ -61,7 +61,7 @@ type ReviewRequest struct {
type AdminPriceAdjustRequest struct { type AdminPriceAdjustRequest struct {
BuyerRatio float64 `json:"buyer_ratio"` BuyerRatio float64 `json:"buyer_ratio"`
BuyerTotalPrice float64 `json:"buyer_total_price"` BuyerTotalPriceCent int64 `json:"buyer_total_price_cent"`
Reason string `json:"reason"` 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 { if err := tx.Create(&account).Error; err != nil {
return err return err
} }
price := normalizedListingPrice(req) priceCent := normalizedListingPriceCent(req)
depositAmount := roundMoney(req.DepositAmount)
listing := model.RentalListing{ listing := model.RentalListing{
ListingNo: listingNo, ListingNo: listingNo,
AccountID: account.ID, AccountID: account.ID,
OwnerID: ownerID, OwnerID: ownerID,
Price: price, PriceCent: priceCent,
PriceCent: int64(math.Round(price * 100)), DepositAmountCent: req.DepositAmountCent,
DepositAmount: depositAmount,
DepositAmountCent: int64(math.Round(depositAmount * 100)),
Status: listingStatus, Status: listingStatus,
ReviewStatus: reviewStatus, ReviewStatus: reviewStatus,
PublishedAt: publishedAt, PublishedAt: publishedAt,
@@ -149,16 +146,13 @@ func (r *Repository) CreateFromExternalUpload(upload externalUploadCreate, req C
if err := tx.Create(&account).Error; err != nil { if err := tx.Create(&account).Error; err != nil {
return err return err
} }
price := normalizedListingPrice(req) priceCent := normalizedListingPriceCent(req)
depositAmount := roundMoney(req.DepositAmount)
listing := model.RentalListing{ listing := model.RentalListing{
ListingNo: listingNo, ListingNo: listingNo,
AccountID: account.ID, AccountID: account.ID,
OwnerID: owner.ID, OwnerID: owner.ID,
Price: price, PriceCent: priceCent,
PriceCent: int64(math.Round(price * 100)), DepositAmountCent: req.DepositAmountCent,
DepositAmount: depositAmount,
DepositAmountCent: int64(math.Round(depositAmount * 100)),
Status: "draft", Status: "draft",
ReviewStatus: "pending", ReviewStatus: "pending",
} }
@@ -216,9 +210,8 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest,
} }
account.ScreenshotURLS = screenshots account.ScreenshotURLS = screenshots
price := normalizedListingPrice(req) listing.PriceCent = normalizedListingPriceCent(req)
listing.Price = price listing.DepositAmountCent = req.DepositAmountCent
listing.DepositAmount = roundMoney(req.DepositAmount)
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus listing.Status = listingStatus
listing.ReviewStatus = reviewStatus listing.ReviewStatus = reviewStatus
@@ -521,7 +514,7 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
} }
sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"]) sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"])
if sellerTotalPrice <= 0 { 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"]) sellerCoinBasePrice := readSummaryNumber(breakdown["seller_coin_base_price"])
if sellerCoinBasePrice <= 0 { if sellerCoinBasePrice <= 0 {
@@ -536,13 +529,13 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
return ErrInvalidPrice return ErrInvalidPrice
} }
beforePrice := listing.Price beforePriceCent := listing.PriceCent
beforeRatio := readSummaryNumber(breakdown["buyer_ratio"]) beforeRatio := readSummaryNumber(breakdown["buyer_ratio"])
if beforeRatio <= 0 && listing.Price > consumablePrice { if beforeRatio <= 0 && centToYuan(listing.PriceCent) > consumablePrice {
beforeRatio = roundRatio(coinWan / (listing.Price - consumablePrice)) beforeRatio = roundRatio(coinWan / (centToYuan(listing.PriceCent) - consumablePrice))
} }
listing.Price = buyerTotalPrice listing.PriceCent = yuanToCent(buyerTotalPrice)
summary["publish_ratio"] = buyerRatio summary["publish_ratio"] = buyerRatio
breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice) breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice)
breakdown["seller_total_price"] = roundMoney(sellerTotalPrice) breakdown["seller_total_price"] = roundMoney(sellerTotalPrice)
@@ -572,8 +565,8 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
"listing_id": listing.ID, "listing_id": listing.ID,
"account_id": account.ID, "account_id": account.ID,
"owner_id": listing.OwnerID, "owner_id": listing.OwnerID,
"before_price": beforePrice, "before_price_cent": beforePriceCent,
"after_price": listing.Price, "after_price_cent": listing.PriceCent,
"before_buyer_ratio": beforeRatio, "before_buyer_ratio": beforeRatio,
"after_buyer_ratio": buyerRatio, "after_buyer_ratio": buyerRatio,
"platform_markup": breakdown["platform_markup_amount"], "platform_markup": breakdown["platform_markup_amount"],
@@ -783,9 +776,9 @@ func canListPublicWithSQL(query PublicListQuery) bool {
func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB { func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB {
switch sortKey { switch sortKey {
case "priceAsc": 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": 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": case "coinDesc":
return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC") return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC")
default: default:
@@ -1384,16 +1377,16 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
return items return items
} }
func normalizedListingPrice(req CreateRequest) float64 { func normalizedListingPriceCent(req CreateRequest) int64 {
if req.AssetSummary != nil { if req.AssetSummary != nil {
if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok { if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok {
buyerPrice := readSummaryNumber(breakdown["buyer_total_price"]) buyerPrice := readSummaryNumber(breakdown["buyer_total_price"])
if buyerPrice > 0 { if buyerPrice > 0 {
return roundMoney(buyerPrice) return yuanToCent(buyerPrice)
} }
} }
} }
return roundMoney(req.Price) return req.PriceCent
} }
func publicListings(items []ListingDTO) []ListingDTO { 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) { func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) {
if req.BuyerTotalPrice > 0 { if req.BuyerTotalPriceCent > 0 {
buyerTotalPrice := roundMoney(req.BuyerTotalPrice) buyerTotalPrice := roundMoney(centToYuan(req.BuyerTotalPriceCent))
buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice) buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice)
if buyerCoinBasePrice <= 0 || coinWan <= 0 { if buyerCoinBasePrice <= 0 || coinWan <= 0 {
return 0, 0, 0 return 0, 0, 0
@@ -1643,6 +1636,14 @@ func roundRatio(value float64) float64 {
return math.Round(value*10) / 10 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 { func cleanScreenshotURLs(urls []string) []string {
cleaned := make([]string, 0, len(urls)) cleaned := make([]string, 0, len(urls))
seen := make(map[string]struct{}, 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 { if s.repo == nil {
return nil, ErrDependencyUnavailable return nil, ErrDependencyUnavailable
} }
if req.BuyerRatio <= 0 && req.BuyerTotalPrice <= 0 { if req.BuyerRatio <= 0 && req.BuyerTotalPriceCent <= 0 {
return nil, ErrInvalidPrice return nil, ErrInvalidPrice
} }
return s.repo.AdjustReviewPrice(adminID, id, req, meta) return s.repo.AdjustReviewPrice(adminID, id, req, meta)
@@ -328,13 +328,13 @@ func validateRequest(req CreateRequest, rules publishRules) error {
if strings.TrimSpace(req.ServerRegion) == "" { if strings.TrimSpace(req.ServerRegion) == "" {
return ErrMissingServerRegion return ErrMissingServerRegion
} }
if normalizedListingPrice(req) <= 0 { if normalizedListingPriceCent(req) <= 0 {
return ErrInvalidPrice return ErrInvalidPrice
} }
if req.DepositAmount < 0 { if req.DepositAmountCent < 0 {
return ErrInvalidDeposit 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 return ErrDepositTooLow
} }
if req.HafCoinAmount < 0 { if req.HafCoinAmount < 0 {
@@ -533,8 +533,8 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)), HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary, AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot}, ScreenshotURLS: []string{defaultUploadScreenshot},
Price: price, PriceCent: yuanToCent(price),
DepositAmount: item.Deposit, DepositAmountCent: yuanToCent(item.Deposit),
} }
} }
@@ -27,7 +27,7 @@ func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) {
} }
func TestCalculateAdminAdjustedPriceByTotalPrice(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 { if base != 50 || total != 70 || ratio != 20 {
t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio) 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{ req := CreateRequest{
Title: "测试账号", Title: "测试账号",
ServerRegion: "烽火地带", ServerRegion: "烽火地带",
Price: 100, PriceCent: 10000,
DepositAmount: 2, DepositAmountCent: 200,
HafCoinAmount: 1000000, HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"}, ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{ AssetSummary: map[string]any{
@@ -52,7 +52,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
t.Fatalf("expected ErrDepositTooLow, got %v", err) t.Fatalf("expected ErrDepositTooLow, got %v", err)
} }
req.DepositAmount = 3 req.DepositAmountCent = 300
if err := validateRequest(req, publishRules{}); err != nil { if err := validateRequest(req, publishRules{}); err != nil {
t.Fatalf("expected valid request, got %v", err) t.Fatalf("expected valid request, got %v", err)
} }
@@ -159,8 +159,8 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
if req.HafCoinAmount != 197100000 { if req.HafCoinAmount != 197100000 {
t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount) t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount)
} }
if req.Price != 458 || req.DepositAmount != 400 { if req.PriceCent != 45800 || req.DepositAmountCent != 40000 {
t.Fatalf("unexpected price/deposit %.2f/%.2f", req.Price, req.DepositAmount) t.Fatalf("unexpected price/deposit cent %d/%d", req.PriceCent, req.DepositAmountCent)
} }
if req.AssetSummary["season_insurance"] != "3*3" { if req.AssetSummary["season_insurance"] != "3*3" {
t.Fatalf("expected 3*3 insurance, got %#v", req.AssetSummary["season_insurance"]) 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 { type SubmitCheckoutRequest struct {
Content string `json:"content" binding:"required"` Content string `json:"content" binding:"required"`
ConsumableAmount float64 `json:"consumable_amount"` ConsumableAmountCent int64 `json:"consumable_amount_cent"`
CoinConsumedM float64 `json:"coin_consumed_m"` CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"` OtherAmountCent int64 `json:"other_amount_cent"`
EvidenceURLS []string `json:"evidence_urls"` EvidenceURLS []string `json:"evidence_urls"`
} }
type CounterCheckoutRequest struct { type CounterCheckoutRequest struct {
ConsumableAmount float64 `json:"consumable_amount"` ConsumableAmountCent int64 `json:"consumable_amount_cent"`
CoinConsumedM float64 `json:"coin_consumed_m"` CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"` OtherAmountCent int64 `json:"other_amount_cent"`
DepositDeductAmount float64 `json:"deposit_deduct_amount"` DepositDeductAmountCent int64 `json:"deposit_deduct_amount_cent"`
Reason string `json:"reason" binding:"required"` Reason string `json:"reason" binding:"required"`
EvidenceURLS []string `json:"evidence_urls"` 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 { func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
rentAmount := roundMoney(listing.Price) rentAmountCent := listing.PriceCent
ownerRentAmount := readSnapshotPrice(account.AssetSummary, "seller_total_price") ownerRentAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100))
if ownerRentAmount <= 0 || ownerRentAmount > rentAmount { if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
ownerRentAmount = rentAmount ownerRentAmountCent = rentAmountCent
} }
platformFee := readSnapshotPrice(account.AssetSummary, "platform_markup_amount") platformFeeCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "platform_markup_amount") * 100))
if platformFee <= 0 || roundMoney(ownerRentAmount+platformFee) != rentAmount { if platformFeeCent <= 0 || ownerRentAmountCent+platformFeeCent != rentAmountCent {
platformFee = roundMoney(rentAmount - ownerRentAmount) platformFeeCent = rentAmountCent - ownerRentAmountCent
} }
if platformFee < 0 { if platformFeeCent < 0 {
platformFee = 0 platformFeeCent = 0
} }
return orderPricing{ return orderPricing{
RentAmountCent: int64(math.Round(rentAmount * 100)), RentAmountCent: rentAmountCent,
OwnerRentAmountCent: int64(math.Round(ownerRentAmount * 100)), OwnerRentAmountCent: ownerRentAmountCent,
PlatformFeeCent: int64(math.Round(platformFee * 100)), PlatformFeeCent: platformFeeCent,
} }
} }
@@ -173,8 +173,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
} }
rentHours := internalOrderHours rentHours := internalOrderHours
pricing := buildOrderPricing(listing, account) pricing := buildOrderPricing(listing, account)
depositOriginalAmount := roundMoney(listing.DepositAmount) depositOriginalAmountCent := listing.DepositAmountCent
paidDepositAmount, waivedDepositAmount, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmount) paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent)
if err != nil { if err != nil {
return err return err
} }
@@ -185,17 +185,11 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
OwnerID: listing.OwnerID, OwnerID: listing.OwnerID,
RenterID: renterID, RenterID: renterID,
EstimatedDurationHours: rentHours, EstimatedDurationHours: rentHours,
RentAmount: float64(pricing.RentAmountCent) / 100,
RentAmountCent: pricing.RentAmountCent, RentAmountCent: pricing.RentAmountCent,
OwnerRentAmount: float64(pricing.OwnerRentAmountCent) / 100,
OwnerRentAmountCent: pricing.OwnerRentAmountCent, OwnerRentAmountCent: pricing.OwnerRentAmountCent,
DepositAmount: paidDepositAmount, DepositAmountCent: paidDepositAmountCent,
DepositAmountCent: int64(math.Round(paidDepositAmount * 100)), DepositOriginalAmountCent: depositOriginalAmountCent,
DepositOriginalAmount: depositOriginalAmount, DepositWaivedAmountCent: waivedDepositAmountCent,
DepositOriginalAmountCent: int64(math.Round(depositOriginalAmount * 100)),
DepositWaivedAmount: waivedDepositAmount,
DepositWaivedAmountCent: int64(math.Round(waivedDepositAmount * 100)),
PlatformFee: float64(pricing.PlatformFeeCent) / 100,
PlatformFeeCent: pricing.PlatformFeeCent, PlatformFeeCent: pricing.PlatformFeeCent,
AccountSnapshot: snapshot, AccountSnapshot: snapshot,
Status: "pending_payment", Status: "pending_payment",
@@ -231,9 +225,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
return r.FindForUser(renterID, createdID) return r.FindForUser(renterID, createdID)
} }
func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDeposit float64) (float64, float64, error) { func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64) (int64, int64, error) {
originalDeposit = roundMoney(originalDeposit) if originalDepositCent <= 0 {
if originalDeposit <= 0 {
return 0, 0, nil return 0, 0, nil
} }
var user model.User var user model.User
@@ -244,28 +237,27 @@ func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, origin
if err != nil { if err != nil {
return 0, 0, err return 0, 0, err
} }
paidDeposit, waivedDeposit := calculateDepositWaiver(originalDeposit, user.DepositFreeQuota, used) paidDeposit, waivedDeposit := calculateDepositWaiver(originalDepositCent, user.DepositFreeQuotaCent, used)
return paidDeposit, waivedDeposit, nil return paidDeposit, waivedDeposit, nil
} }
func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (float64, error) { func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (int64, error) {
var used float64 var used int64
err := tx.Model(&model.RentalOrder{}). err := tx.Model(&model.RentalOrder{}).
Where("renter_id = ? AND status NOT IN ?", Where("renter_id = ? AND status NOT IN ?",
renterID, renterID,
[]string{"completed", "cancelled", "closed"}, []string{"completed", "cancelled", "closed"},
). ).
Select("COALESCE(SUM(deposit_waived_amount), 0)"). Select("COALESCE(SUM(deposit_waived_amount_cent), 0)").
Scan(&used).Error Scan(&used).Error
return roundMoney(used), err return used, err
} }
func calculateDepositWaiver(originalDeposit float64, quota float64, used float64) (float64, float64) { func calculateDepositWaiver(originalDepositCent int64, quotaCent int64, usedCent int64) (int64, int64) {
originalDeposit = roundMoney(originalDeposit) remaining := maxCent(quotaCent-usedCent, 0)
remaining := maxMoney(roundMoney(quota)-roundMoney(used), 0) waived := minCent(originalDepositCent, remaining)
waived := minMoney(originalDeposit, remaining) paid := maxCent(originalDepositCent-waived, 0)
paid := maxMoney(originalDeposit-waived, 0) return paid, waived
return roundMoney(paid), roundMoney(waived)
} }
// Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。 // Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。
@@ -554,7 +546,7 @@ func (r *Repository) SubmitCheckout(userID uint64, orderID uint64, req SubmitChe
if err := tx.Create(&record).Error; err != nil { if err := tx.Create(&record).Error; err != nil {
return err 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 { if err != nil {
return err return err
} }
@@ -650,18 +642,22 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC
First(&checkout).Error; err != nil { First(&checkout).Error; err != nil {
return err 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 { if err != nil {
return err return err
} }
now := time.Now() now := time.Now()
checkout.Status = "countered" 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.CoinConsumedM = next.CoinConsumedM
checkout.OtherAmount = next.OtherAmount checkout.OtherAmountCent = next.OtherAmountCent
checkout.DepositDeductAmount = next.DepositDeductAmount checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
checkout.RenterRefundAmount = next.RenterRefundAmount checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
checkout.OwnerIncomeAmount = next.OwnerIncomeAmount checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
checkout.OwnerAdjustmentReason = req.Reason checkout.OwnerAdjustmentReason = req.Reason
checkout.OwnerAdjustedAt = &now checkout.OwnerAdjustedAt = &now
checkout.EvidenceURLS = next.EvidenceURLS checkout.EvidenceURLS = next.EvidenceURLS
@@ -697,9 +693,9 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC
} }
dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{ dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{
OwnerID: userID, OwnerID: userID,
RentAmountCent: int64(math.Round(checkout.RentAmount * 100)), RentAmountCent: checkout.RentAmountCent,
OwnerRentAmountCent: int64(math.Round(checkout.OwnerRentAmount * 100)), OwnerRentAmountCent: checkout.OwnerRentAmountCent,
DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)), DepositAmountCent: checkout.DepositAmountCent,
}) })
return &dto, nil return &dto, nil
} }
@@ -1095,11 +1091,11 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
refund = action refund = action
} }
checkout.RentAmount = float64(settlement.ActualRentAmountCent) / 100 checkout.RentAmountCent = settlement.ActualRentAmountCent
checkout.OwnerRentAmount = float64(settlement.OwnerRentIncomeCent) / 100 checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent
checkout.PlatformFee = float64(settlement.PlatformFeeCent) / 100 checkout.PlatformFeeCent = settlement.PlatformFeeCent
checkout.RenterRefundAmount = float64(settlement.RenterRefundCent) / 100 checkout.RenterRefundAmountCent = settlement.RenterRefundCent
checkout.OwnerIncomeAmount = float64(settlement.OwnerIncomeCent) / 100 checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent
if err := notification.Append(tx, if err := notification.Append(tx,
notification.Entry{ notification.Entry{
UserID: order.RenterID, UserID: order.RenterID,
@@ -1229,22 +1225,18 @@ func applyPaymentDeadline(dto *OrderDTO, order model.RentalOrder, timeoutMinutes
dto.PaymentDeadlineAt = &deadline 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) { 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 consumableAmount < 0 || coinConsumedM < 0 || otherAmount < 0 || explicitDeduct < 0 { if consumableAmountCent < 0 || coinConsumedM < 0 || otherAmountCent < 0 || explicitDeductCent < 0 {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount return model.OrderCheckout{}, ErrInvalidCheckoutAmount
} }
consumableAmount = roundMoney(consumableAmount) deductAmountCent := otherAmountCent
otherAmount = roundMoney(otherAmount)
explicitDeduct = roundMoney(explicitDeduct)
deductAmount := otherAmount
if useExplicitDeduct { if useExplicitDeduct {
deductAmount = explicitDeduct deductAmountCent = explicitDeductCent
} }
depositAmountFromCent := float64(order.DepositAmountCent) / 100 if deductAmountCent > order.DepositAmountCent {
if deductAmount > depositAmountFromCent {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount return model.OrderCheckout{}, ErrInvalidCheckoutAmount
} }
settlement := calculateCheckoutSettlement(order, consumableAmount, roundQuantity(coinConsumedM), deductAmount) settlement := calculateCheckoutSettlement(order, consumableAmountCent, roundQuantity(coinConsumedM), deductAmountCent)
evidence, err := marshalStringList(evidenceURLS) evidence, err := marshalStringList(evidenceURLS)
if err != nil { if err != nil {
return model.OrderCheckout{}, err return model.OrderCheckout{}, err
@@ -1253,24 +1245,15 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
OrderID: order.ID, OrderID: order.ID,
InitiatedBy: initiatedBy, InitiatedBy: initiatedBy,
Status: status, Status: status,
RentAmount: float64(settlement.ActualRentAmountCent) / 100,
RentAmountCent: settlement.ActualRentAmountCent, RentAmountCent: settlement.ActualRentAmountCent,
OwnerRentAmount: float64(settlement.OwnerRentIncomeCent) / 100,
OwnerRentAmountCent: settlement.OwnerRentIncomeCent, OwnerRentAmountCent: settlement.OwnerRentIncomeCent,
PlatformFee: float64(settlement.PlatformFeeCent) / 100,
PlatformFeeCent: settlement.PlatformFeeCent, PlatformFeeCent: settlement.PlatformFeeCent,
DepositAmount: depositAmountFromCent,
DepositAmountCent: order.DepositAmountCent, DepositAmountCent: order.DepositAmountCent,
ConsumableAmount: consumableAmount, ConsumableAmountCent: consumableAmountCent,
ConsumableAmountCent: int64(math.Round(consumableAmount * 100)),
CoinConsumedM: roundQuantity(coinConsumedM), CoinConsumedM: roundQuantity(coinConsumedM),
OtherAmount: otherAmount, OtherAmountCent: otherAmountCent,
OtherAmountCent: int64(math.Round(otherAmount * 100)),
DepositDeductAmount: roundMoney(deductAmount),
DepositDeductAmountCent: settlement.DepositCompensationCent, DepositDeductAmountCent: settlement.DepositCompensationCent,
RenterRefundAmount: float64(settlement.RenterRefundCent) / 100,
RenterRefundAmountCent: settlement.RenterRefundCent, RenterRefundAmountCent: settlement.RenterRefundCent,
OwnerIncomeAmount: float64(settlement.OwnerIncomeCent) / 100,
OwnerIncomeAmountCent: settlement.OwnerIncomeCent, OwnerIncomeAmountCent: settlement.OwnerIncomeCent,
Content: content, Content: content,
EvidenceURLS: evidence, 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 { 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 { func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
// 从分字段读取,转为元进行计算(保持现有逻辑兼容性) orderRentAmountCent := order.RentAmountCent
orderRentAmount := float64(order.RentAmountCent) / 100 orderOwnerRentAmountCent := order.OwnerRentAmountCent
orderOwnerRentAmount := float64(order.OwnerRentAmountCent) / 100 orderDepositAmountCent := order.DepositAmountCent
orderDepositAmount := float64(order.DepositAmountCent) / 100
buyerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") buyerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") * 100))
if buyerCoinBasePrice <= 0 || buyerCoinBasePrice > orderRentAmount { if buyerCoinBasePriceCent <= 0 || buyerCoinBasePriceCent > orderRentAmountCent {
buyerCoinBasePrice = orderRentAmount buyerCoinBasePriceCent = orderRentAmountCent
} }
sellerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") sellerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") * 100))
if sellerCoinBasePrice <= 0 || sellerCoinBasePrice > orderOwnerRentAmount { if sellerCoinBasePriceCent <= 0 || sellerCoinBasePriceCent > orderOwnerRentAmountCent {
sellerCoinBasePrice = orderOwnerRentAmount sellerCoinBasePriceCent = orderOwnerRentAmountCent
} }
prepaidConsumablePrice := readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") prepaidConsumablePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") * 100))
if prepaidConsumablePrice <= 0 || prepaidConsumablePrice > orderRentAmount-buyerCoinBasePrice { if prepaidConsumablePriceCent <= 0 || prepaidConsumablePriceCent > orderRentAmountCent-buyerCoinBasePriceCent {
prepaidConsumablePrice = maxMoney(orderRentAmount-buyerCoinBasePrice, 0) prepaidConsumablePriceCent = maxCent(orderRentAmountCent-buyerCoinBasePriceCent, 0)
} }
prepaidOwnerConsumablePrice := maxMoney(orderOwnerRentAmount-sellerCoinBasePrice, 0) prepaidOwnerConsumablePriceCent := maxCent(orderOwnerRentAmountCent-sellerCoinBasePriceCent, 0)
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot) totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
coinUseRatio := 1.0 coinUseRatio := 1.0
if totalCoinM > 0 { if totalCoinM > 0 {
coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1) coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1)
} }
usedBuyerCoinPrice := roundMoney(buyerCoinBasePrice * coinUseRatio) usedBuyerCoinPriceCent := int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
usedOwnerCoinPrice := roundMoney(sellerCoinBasePrice * coinUseRatio) usedOwnerCoinPriceCent := int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
usedBuyerConsumablePrice := minMoney(roundMoney(consumableAmount), prepaidConsumablePrice) usedBuyerConsumablePriceCent := minCent(consumableAmountCent, prepaidConsumablePriceCent)
consumableUseRatio := 1.0 consumableUseRatio := 1.0
if prepaidConsumablePrice > 0 { if prepaidConsumablePriceCent > 0 {
consumableUseRatio = minRatio(maxRatio(usedBuyerConsumablePrice/prepaidConsumablePrice, 0), 1) consumableUseRatio = minRatio(maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0), 1)
} }
usedOwnerConsumablePrice := roundMoney(prepaidOwnerConsumablePrice * consumableUseRatio) usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
actualRentAmount := minMoney(roundMoney(usedBuyerCoinPrice+usedBuyerConsumablePrice), orderRentAmount) actualRentAmountCent := minCent(usedBuyerCoinPriceCent+usedBuyerConsumablePriceCent, orderRentAmountCent)
ownerRentIncome := minMoney(roundMoney(usedOwnerCoinPrice+usedOwnerConsumablePrice), orderOwnerRentAmount) ownerRentIncomeCent := minCent(usedOwnerCoinPriceCent+usedOwnerConsumablePriceCent, orderOwnerRentAmountCent)
depositCompensation := minMoney(roundMoney(depositDeductAmount), orderDepositAmount) depositCompensationCent := minCent(depositDeductAmountCent, orderDepositAmountCent)
rentRefund := maxMoney(orderRentAmount-actualRentAmount, 0) rentRefundCent := maxCent(orderRentAmountCent-actualRentAmountCent, 0)
depositRefund := maxMoney(orderDepositAmount-depositCompensation, 0) depositRefundCent := maxCent(orderDepositAmountCent-depositCompensationCent, 0)
// 最后转换为分返回
return checkoutSettlement{ return checkoutSettlement{
OwnerRentIncomeCent: int64(math.Round(ownerRentIncome * 100)), OwnerRentIncomeCent: ownerRentIncomeCent,
DepositCompensationCent: int64(math.Round(depositCompensation * 100)), DepositCompensationCent: depositCompensationCent,
OwnerIncomeCent: int64(math.Round((ownerRentIncome + depositCompensation) * 100)), OwnerIncomeCent: ownerRentIncomeCent + depositCompensationCent,
RentRefundCent: int64(math.Round(rentRefund * 100)), RentRefundCent: rentRefundCent,
DepositRefundCent: int64(math.Round(depositRefund * 100)), DepositRefundCent: depositRefundCent,
RenterRefundCent: int64(math.Round((rentRefund + depositRefund) * 100)), RenterRefundCent: rentRefundCent + depositRefundCent,
PlatformFeeCent: int64(math.Round(maxMoney(actualRentAmount-ownerRentIncome, 0) * 100)), PlatformFeeCent: maxCent(actualRentAmountCent-ownerRentIncomeCent, 0),
ActualRentAmountCent: int64(math.Round(actualRentAmount * 100)), ActualRentAmountCent: actualRentAmountCent,
} }
} }
@@ -1381,6 +1362,20 @@ func maxMoney(a float64, b float64) float64 {
return money.Max(a, b) 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 { func minRatio(a float64, b float64) float64 {
if a < b { if a < b {
return a 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 { func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 {
if order.DepositOriginalAmountCent > 0 { if order.DepositOriginalAmountCent > 0 {
return order.DepositOriginalAmountCent return order.DepositOriginalAmountCent
@@ -1537,11 +1525,11 @@ func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 {
} }
func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO { func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO {
rentAmountCent := int64(math.Round(checkout.RentAmount * 100)) rentAmountCent := checkout.RentAmountCent
ownerRentAmountCent := int64(math.Round(checkout.OwnerRentAmount * 100)) ownerRentAmountCent := checkout.OwnerRentAmountCent
platformFeeCent := int64(math.Round(checkout.PlatformFee * 100)) platformFeeCent := checkout.PlatformFeeCent
renterRefundAmountCent := int64(math.Round(checkout.RenterRefundAmount * 100)) renterRefundAmountCent := checkout.RenterRefundAmountCent
ownerIncomeAmountCent := int64(math.Round(checkout.OwnerIncomeAmount * 100)) ownerIncomeAmountCent := checkout.OwnerIncomeAmountCent
return CheckoutDTO{ return CheckoutDTO{
ID: checkout.ID, ID: checkout.ID,
OrderID: checkout.OrderID, OrderID: checkout.OrderID,
@@ -1552,11 +1540,11 @@ func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO {
RentAmountCent: &rentAmountCent, RentAmountCent: &rentAmountCent,
OwnerRentAmountCent: &ownerRentAmountCent, OwnerRentAmountCent: &ownerRentAmountCent,
PlatformFeeCent: &platformFeeCent, PlatformFeeCent: &platformFeeCent,
DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)), DepositAmountCent: checkout.DepositAmountCent,
ConsumableAmountCent: int64(math.Round(checkout.ConsumableAmount * 100)), ConsumableAmountCent: checkout.ConsumableAmountCent,
CoinConsumedM: checkout.CoinConsumedM, CoinConsumedM: checkout.CoinConsumedM,
OtherAmountCent: int64(math.Round(checkout.OtherAmount * 100)), OtherAmountCent: checkout.OtherAmountCent,
DepositDeductAmountCent: int64(math.Round(checkout.DepositDeductAmount * 100)), DepositDeductAmountCent: checkout.DepositDeductAmountCent,
RenterRefundAmountCent: &renterRefundAmountCent, RenterRefundAmountCent: &renterRefundAmountCent,
OwnerIncomeAmountCent: &ownerIncomeAmountCent, OwnerIncomeAmountCent: &ownerIncomeAmountCent,
Content: checkout.Content, 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) // 角精度:243.7 = roundMoney(236.7 + 7)
if settlement.ActualRentAmountCent != 24370 { 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 { if settlement.ActualRentAmountCent != 38300 {
t.Fatalf("ActualRentAmountCent = %d, want 38300", settlement.ActualRentAmountCent) t.Fatalf("ActualRentAmountCent = %d, want 38300", settlement.ActualRentAmountCent)
@@ -126,17 +126,17 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) {
func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) { func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) {
paid, waived := calculateDepositWaiver(500, 300, 0) paid, waived := calculateDepositWaiver(500, 300, 0)
if paid != 200 || waived != 300 { 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) paid, waived = calculateDepositWaiver(500, 300, 200)
if paid != 400 || waived != 100 { 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) paid, waived = calculateDepositWaiver(500, 300, 300)
if paid != 500 || waived != 0 { 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 { type WalletRechargePaymentRequest struct {
Amount float64 `json:"amount"` AmountCent int64 `json:"amount_cent"`
PayWay string `json:"pay_way"` PayWay string `json:"pay_way"`
JSPayFlag string `json:"jspay_flag"` 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) { func (r *Repository) StartWalletRecharge(userID uint64, req WalletRechargePaymentRequest, clientIP string) (*PaymentDTO, error) {
amountCent := moneyCent(req.Amount) amountCent := req.AmountCent
if userID == 0 || req.Amount < MinWalletRechargeAmount || amountCent <= 0 { if userID == 0 || amountCent < moneyCent(MinWalletRechargeAmount) {
return nil, ErrPaymentCannotStart return nil, ErrPaymentCannotStart
} }
runtimeConfig, err := r.defaultRuntimeConfig() runtimeConfig, err := r.defaultRuntimeConfig()
@@ -764,7 +764,7 @@ func (r *Repository) preparePayment(userID uint64, orderID uint64, req StartPaym
if row.Status != "pending_payment" { if row.Status != "pending_payment" {
return ErrPaymentCannotStart return ErrPaymentCannotStart
} }
amountCent := moneyCent(row.RentAmount + row.DepositAmount) amountCent := row.RentAmountCent + row.DepositAmountCent
if amountCent <= 0 { if amountCent <= 0 {
return ErrPaymentCannotStart return ErrPaymentCannotStart
} }
@@ -58,11 +58,8 @@ func (r *Repository) Create(userID uint64, req CreateWithdrawalRequest) (*Withdr
withdrawal := model.WithdrawalRequest{ withdrawal := model.WithdrawalRequest{
WithdrawNo: withdrawNo, WithdrawNo: withdrawNo,
UserID: userID, UserID: userID,
Amount: float64(req.AmountCent) / 100,
AmountCent: req.AmountCent, AmountCent: req.AmountCent,
Fee: float64(feeCent) / 100,
FeeCent: feeCent, FeeCent: feeCent,
ActualAmount: float64(actualAmountCent) / 100,
ActualAmountCent: actualAmountCent, ActualAmountCent: actualAmountCent,
PaymentAccountID: &req.PaymentAccountID, PaymentAccountID: &req.PaymentAccountID,
AccountType: paymentAccount.AccountType, 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失败', 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冻结', risk_status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '风控状态: normal正常, warning警告, frozen冻结',
credit_score INT NOT NULL DEFAULT 100 COMMENT '信用分', 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 '免押总额度(分)', deposit_free_quota_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押总额度(分)',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '账号状态: active活跃, inactive停用, banned封禁', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '账号状态: active活跃, inactive停用, banned封禁',
last_login_at DATETIME NULL COMMENT '最后登录时间', 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', listing_no VARCHAR(20) NOT NULL COMMENT '商品编号,格式yyyyMMddNNNN',
account_id BIGINT UNSIGNED NOT NULL COMMENT '关联的游戏账号ID', account_id BIGINT UNSIGNED NOT NULL COMMENT '关联的游戏账号ID',
owner_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 '租金(分/小时)', 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 '押金金额(分)', deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)',
in_transaction TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否正在交易中: 0否, 1是', in_transaction TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否正在交易中: 0否, 1是',
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '商品状态: draft草稿, active上架, offline下架, deleted删除', 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), UNIQUE KEY uk_rental_listings_listing_no (listing_no),
KEY idx_rental_listings_account_id (account_id), KEY idx_rental_listings_account_id (account_id),
KEY idx_rental_listings_owner_id (owner_id), KEY idx_rental_listings_owner_id (owner_id),
KEY idx_rental_listings_filter (status, review_status, 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) KEY idx_rental_listings_published (status, review_status, published_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租号商品列表'; ) 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', renter_id BIGINT UNSIGNED NOT NULL COMMENT '租客ID',
rented_at DATETIME NULL COMMENT '租用开始时间', rented_at DATETIME NULL COMMENT '租用开始时间',
estimated_duration_hours INT NOT NULL DEFAULT 24 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 '租金总额(分)', 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 '号主实得租金(分)', 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_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_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 '免押抵扣金额(分)', 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 '平台手续费(分)', platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)',
account_snapshot JSON NULL COMMENT '账号快照(下单时的账号状态)', account_snapshot JSON NULL COMMENT '账号快照(下单时的账号状态)',
status VARCHAR(32) NOT NULL DEFAULT 'pending_payment' COMMENT '订单状态: pending_payment待支付, active进行中, completed已完成, cancelled已取消, closed已关闭', 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', order_id BIGINT UNSIGNED NOT NULL COMMENT '订单ID',
initiated_by 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完成', 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 '租金(分)', 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 '号主实得租金(分)', 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 '平台手续费(分)', 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 '押金(分)', 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 '消耗品扣费(分)', consumable_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '消耗品扣费(分)',
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00 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 '其他费用(分)', 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 '押金扣除金额(分)', 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 '租客退款金额(分)', 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 '号主收入金额(分)', owner_income_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主收入金额(分)',
content TEXT NULL COMMENT '结算说明', content TEXT NULL COMMENT '结算说明',
evidence_urls JSON NULL COMMENT '证据截图URL列表', evidence_urls JSON NULL COMMENT '证据截图URL列表',
@@ -215,9 +197,7 @@ CREATE TABLE IF NOT EXISTS handoff_records (
CREATE TABLE IF NOT EXISTS wallet_accounts ( CREATE TABLE IF NOT EXISTS wallet_accounts (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', 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 '可用余额(分)', 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 '冻结余额(分)', frozen_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额(分)',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '钱包状态: active正常, frozen冻结, closed关闭', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '钱包状态: active正常, frozen冻结, closed关闭',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 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', user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
order_id BIGINT UNSIGNED NULL COMMENT '关联订单ID', order_id BIGINT UNSIGNED NULL COMMENT '关联订单ID',
direction VARCHAR(16) NOT NULL COMMENT '方向: in收入, out支出', direction VARCHAR(16) NOT NULL COMMENT '方向: in收入, out支出',
amount DECIMAL(12,2) NOT NULL COMMENT '金额',
amount_cent BIGINT NOT NULL DEFAULT 0 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_after_cent BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额(分)',
balance_type VARCHAR(32) NOT NULL COMMENT '余额类型: available可用, frozen冻结', balance_type VARCHAR(32) NOT NULL COMMENT '余额类型: available可用, frozen冻结',
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型: rent_payment租金支付, deposit_freeze押金冻结, settlement结算, refund退款等', 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, id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
withdraw_no VARCHAR(64) NOT NULL COMMENT '提现单号', withdraw_no VARCHAR(64) NOT NULL COMMENT '提现单号',
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID', user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
amount DECIMAL(12,2) NOT NULL COMMENT '提现金额',
amount_cent BIGINT NOT NULL DEFAULT 0 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 '手续费(分)', 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 '实际到账(分)', 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 return math.Round(value*10) / 10
} }
// ToCent 将转换为分(整数),用于存储 // ToCent 将转换为分(整数),用于存储
// 12.3 -> 123分 // 12.34 元 -> 1234
// 12.34角 -> 123分(自动舍入到角) func ToCent(yuan float64) int64 {
func ToCent(jiao float64) int64 { return int64(math.Round(yuan * 100))
return int64(math.Round(jiao * 10))
} }
// ToJiao 将分转换为角(0.1元精度),用于API响应 // ToDisplayYuan 将分转换为按角精度展示的元值。
// 123分 -> 12.3 // 1234 分 -> 12.3
// 1234分 -> 123.4角 func ToDisplayYuan(cent int64) float64 {
func ToJiao(cent int64) float64 { return Round(float64(cent) / 100)
return float64(cent) / 10.0
} }
// Format 格式化分为字符串(角精度,保留1位小数) // Format 格式化分为字符串(角精度,保留1位小数)
// 123分 -> "12.3" // 1234 分 -> "12.3"
// 1230分 -> "123.0" // 1230 分 -> "12.3"
func Format(cent int64) string { func Format(cent int64) string {
jiao := ToJiao(cent) return fmt.Sprintf("%.1f", ToDisplayYuan(cent))
return fmt.Sprintf("%.1f", jiao)
} }
// FormatWithSymbol 格式化分为带符号的字符串 // FormatWithSymbol 格式化分为带符号的字符串
+34 -36
View File
@@ -5,46 +5,44 @@ import "testing"
func TestToCent(t *testing.T) { func TestToCent(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
jiao float64 yuan float64
want int64 want int64
}{ }{
{"12.3转123分", 12.3, 123}, {"12.3转1230分", 12.3, 1230},
{"12.34角舍入到123分", 12.34, 123}, {"12.34元转1234分", 12.34, 1234},
{"12.36角舍入到124分", 12.36, 124}, {"12.36元转1236分", 12.36, 1236},
{"0.1转1分", 0.1, 1}, {"0.1转10分", 0.1, 10},
{"0.05角舍入到1分", 0.05, 1}, {"100元转10000分", 100.0, 10000},
{"0.04角舍入到0分", 0.04, 0},
{"100角转1000分", 100.0, 1000},
{"零值", 0.0, 0}, {"零值", 0.0, 0},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := ToCent(tt.jiao) got := ToCent(tt.yuan)
if got != tt.want { 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 { tests := []struct {
name string name string
cent int64 cent int64
want float64 want float64
}{ }{
{"123分转12.3角", 123, 12.3}, {"123分展示1.2元", 123, 1.2},
{"1234分转123.4角", 1234, 123.4}, {"1234分展示12.3元", 1234, 12.3},
{"1分转0.1角", 1, 0.1}, {"1236分展示12.4元", 1236, 12.4},
{"10分转1.0角", 10, 1.0}, {"10分展示0.1元", 10, 0.1},
{"1000分100.0", 1000, 100.0}, {"1000分展示10.0", 1000, 10.0},
{"零值", 0, 0.0}, {"零值", 0, 0.0},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got := ToJiao(tt.cent) got := ToDisplayYuan(tt.cent)
if got != tt.want { 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 cent int64
want string want string
}{ }{
{"123分格式化", 123, "12.3"}, {"123分格式化", 123, "1.2"},
{"1234分格式化", 1234, "123.4"}, {"1234分格式化", 1234, "12.3"},
{"10分格式化", 10, "1.0"}, {"1236分格式化", 1236, "12.4"},
{"1分格式化", 1, "0.1"}, {"10分格式化", 10, "0.1"},
{"1000分格式化", 1000, "100.0"}, {"1000分格式化", 1000, "10.0"},
{"零值格式化", 0, "0.0"}, {"零值格式化", 0, "0.0"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -79,8 +77,8 @@ func TestFormatWithSymbol(t *testing.T) {
cent int64 cent int64
want string want string
}{ }{
{"123分带符号", 123, "¥12.3"}, {"123分带符号", 123, "¥1.2"},
{"1000分带符号", 1000, "¥100.0"}, {"1000分带符号", 1000, "¥10.0"},
{"零值带符号", 0, "¥0.0"}, {"零值带符号", 0, "¥0.0"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -139,21 +137,21 @@ func TestMinMax(t *testing.T) {
func TestRoundTrip(t *testing.T) { func TestRoundTrip(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
originalJiao float64 originalYuan float64
expectJiao float64 // 因为角精度,可能会有舍入 expectDisplay float64
}{ }{
{"12.3往返", 12.3, 12.3}, {"12.3往返", 12.3, 12.3},
{"12.34往返(舍入", 12.34, 12.3}, {"12.34往返(展示到角", 12.34, 12.3},
{"100.0往返", 100.0, 100.0}, {"100.0往返", 100.0, 100.0},
{"0.1往返", 0.1, 0.1}, {"0.1往返", 0.1, 0.1},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
cent := ToCent(tt.originalJiao) cent := ToCent(tt.originalYuan)
gotJiao := ToJiao(cent) got := ToDisplayYuan(cent)
if gotJiao != tt.expectJiao { if got != tt.expectDisplay {
t.Errorf("往返转换:%v -> %v分 -> %v, 期望 %v", t.Errorf("往返转换:%v -> %v分 -> %v, 期望 %v",
tt.originalJiao, cent, gotJiao, tt.expectJiao) tt.originalYuan, cent, got, tt.expectDisplay)
} }
}) })
} }
@@ -10,7 +10,7 @@ export interface DashboardMetrics {
total_orders: number total_orders: number
renting_orders: number renting_orders: number
today_orders: number today_orders: number
today_ledger_amount: number today_ledger_amount_cent: number
} }
export interface DashboardPending { export interface DashboardPending {
@@ -27,8 +27,8 @@ export interface DashboardRecentOrder {
renter_id: number renter_id: number
owner_id: number owner_id: number
status: OrderStatus status: OrderStatus
rent_amount: number rent_amount_cent: number
deposit_amount: number deposit_amount_cent: number
created_at: string created_at: string
} }
+17 -17
View File
@@ -6,10 +6,10 @@ export interface FinanceSummary {
total_refund_amount_cent: number total_refund_amount_cent: number
pending_refund_amount_cent: number pending_refund_amount_cent: number
channel_net_amount_cent: number channel_net_amount_cent: number
platform_income_amount: number platform_income_amount_cent: number
owner_should_income_amount: number owner_should_income_amount_cent: number
owner_wallet_income_amount: number owner_wallet_income_amount_cent: number
settlement_diff_amount: number settlement_diff_amount_cent: number
successful_pay_count: number successful_pay_count: number
successful_refund_count: number successful_refund_count: number
pending_refund_count: number pending_refund_count: number
@@ -23,10 +23,10 @@ export interface FinanceDailyItem {
total_refund_amount_cent: number total_refund_amount_cent: number
pending_refund_amount_cent: number pending_refund_amount_cent: number
channel_net_amount_cent: number channel_net_amount_cent: number
platform_income_amount: number platform_income_amount_cent: number
owner_should_income_amount: number owner_should_income_amount_cent: number
owner_wallet_income_amount: number owner_wallet_income_amount_cent: number
settlement_diff_amount: number settlement_diff_amount_cent: number
successful_pay_count: number successful_pay_count: number
successful_refund_count: number successful_refund_count: number
pending_refund_count: number pending_refund_count: number
@@ -51,20 +51,20 @@ export interface FinanceDetail {
owner_id: number owner_id: number
owner_phone: string owner_phone: string
owner_nickname: string owner_nickname: string
order_rent_amount: number order_rent_amount_cent: number
order_deposit_amount: number order_deposit_amount_cent: number
checkout_rent_amount: number checkout_rent_amount_cent: number
checkout_renter_refund: number checkout_renter_refund_cent: number
checkout_owner_income: number checkout_owner_income_cent: number
checkout_platform_fee: number checkout_platform_fee_cent: number
owner_wallet_income_amount: number owner_wallet_income_amount_cent: number
paid_amount_cent: number paid_amount_cent: number
refunded_amount_cent: number refunded_amount_cent: number
refunding_amount_cent: number refunding_amount_cent: number
failed_refund_amount_cent: number failed_refund_amount_cent: number
channel_net_amount_cent: number channel_net_amount_cent: number
platform_net_amount: number platform_net_amount_cent: number
settlement_diff_amount: number settlement_diff_amount_cent: number
finance_status: string finance_status: string
created_at: string created_at: string
settled_at?: string settled_at?: string
@@ -10,9 +10,9 @@ export interface AdminUserItem {
realname_status: RealnameStatusValue realname_status: RealnameStatusValue
risk_status: RiskStatus risk_status: RiskStatus
credit_score: number credit_score: number
deposit_free_quota: number deposit_free_quota_cent: number
deposit_free_used: number deposit_free_used_cent: number
deposit_free_remaining: number deposit_free_remaining_cent: number
status: UserStatus status: UserStatus
order_count: number order_count: number
listing_count: number listing_count: number
@@ -44,10 +44,10 @@ export async function unfreezeAdminUser(id: number) {
return data.data 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>>( const { data } = await apiClient.post<ApiResponse<AdminUserItem>>(
`/admin/users/${id}/deposit-free-quota`, `/admin/users/${id}/deposit-free-quota`,
{ amount } { amount_cent: amountCent }
) )
return data.data return data.data
} }
@@ -12,9 +12,6 @@ export interface WithdrawalDetail {
amount_cent: number amount_cent: number
fee_cent: number fee_cent: number
actual_amount_cent: number actual_amount_cent: number
amount?: number
fee?: number
actual_amount?: number
payment_account_id: number | null payment_account_id: number | null
account_type: string account_type: string
account_name: string account_name: string
@@ -17,12 +17,10 @@ import {
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard' import { fetchAdminDashboard, type AdminDashboard } from '@/features/admin/api/adminDashboard'
import { useAdminTable } from '@/features/admin/composables/useAdminTable' 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 { disputeStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
const money = useMoney()
const { const {
loading, loading,
error, error,
@@ -98,7 +96,7 @@ const {
</div> </div>
<div class="metric-content"> <div class="metric-content">
<span>今日流水</span> <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> <small>今日订单 {{ dashboard.metrics.today_orders }} </small>
</div> </div>
</div> </div>
@@ -224,9 +222,9 @@ const {
</el-tag> </el-tag>
</template> </template>
</el-table-column> </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 }"> <template #default="{ row }">
<span class="amount">{{ money(row.rent_amount) }}</span> <span class="amount">{{ formatCentWithSymbol(row.rent_amount_cent) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="创建时间" min-width="180"> <el-table-column label="创建时间" min-width="180">
@@ -7,6 +7,7 @@ import { fetchAdminFileBlob } from '@/shared/api/files'
import { disputeStatusLabel } from '@/utils/statusLabels' import { disputeStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay' import { formatListingNo } from '@/utils/listingDisplay'
import { yuanToCent } from '@/shared/utils/money'
import AdminTablePagination from '../components/AdminTablePagination.vue' import AdminTablePagination from '../components/AdminTablePagination.vue'
const loading = ref(false) const loading = ref(false)
@@ -100,7 +101,7 @@ async function handleArbitrate() {
await arbitrateDispute(activeDispute.value.id, { await arbitrateDispute(activeDispute.value.id, {
result: result.value, result: result.value,
remark: remark.value.trim(), remark: remark.value.trim(),
amount: amount.value, amount_cent: amount.value ? yuanToCent(amount.value) : undefined,
}) })
ElMessage.success('仲裁结果已保存,双方已收到通知') ElMessage.success('仲裁结果已保存,双方已收到通知')
activeDispute.value = null activeDispute.value = null
@@ -7,7 +7,7 @@ import {
type FinanceDashboard, type FinanceDashboard,
type FinanceDailyItem, type FinanceDailyItem,
} from '@/features/admin/api/adminFinance' } from '@/features/admin/api/adminFinance'
import { formatMoneyWithSymbol } from '@/shared/utils/money' import { formatCentWithSymbol } from '@/shared/utils/money'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
const loading = ref(false) const loading = ref(false)
@@ -31,11 +31,7 @@ async function loadDashboard() {
} }
function moneyCent(value: number) { function moneyCent(value: number) {
return formatMoneyWithSymbol(Number(value || 0) / 100) return formatCentWithSymbol(value)
}
function money(value: number) {
return formatMoneyWithSymbol(value)
} }
function defaultStartDate() { function defaultStartDate() {
@@ -53,11 +49,11 @@ function formatInputDate(date: Date) {
} }
function diffType(value: number) { 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) { 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> </script>
@@ -107,17 +103,17 @@ function rowDiffClass(row: FinanceDailyItem) {
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>平台收入</span> <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> <small>{{ dashboard.summary.settled_order_count }} 个已结算订单</small>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>号主应得</span> <span>号主应得</span>
<strong>{{ money(dashboard.summary.owner_should_income_amount) }}</strong> <strong>{{ moneyCent(dashboard.summary.owner_should_income_amount_cent) }}</strong>
<small>结账单口径</small> <small>结账单口径</small>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>号主实际入账</span> <span>号主实际入账</span>
<strong>{{ money(dashboard.summary.owner_wallet_income_amount) }}</strong> <strong>{{ moneyCent(dashboard.summary.owner_wallet_income_amount_cent) }}</strong>
<small>钱包流水口径</small> <small>钱包流水口径</small>
</div> </div>
<div class="metric-card"> <div class="metric-card">
@@ -128,8 +124,8 @@ function rowDiffClass(row: FinanceDailyItem) {
<div class="metric-card"> <div class="metric-card">
<span>结算差异</span> <span>结算差异</span>
<strong> <strong>
<el-tag :type="diffType(dashboard.summary.settlement_diff_amount)"> <el-tag :type="diffType(dashboard.summary.settlement_diff_amount_cent)">
{{ money(dashboard.summary.settlement_diff_amount) }} {{ moneyCent(dashboard.summary.settlement_diff_amount_cent) }}
</el-tag> </el-tag>
</strong> </strong>
<small>{{ dashboard.summary.financial_exception_count }} 个异常订单</small> <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> <template #default="{ row }">{{ moneyCent(row.channel_net_amount_cent) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="平台收入" width="130"> <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>
<el-table-column label="号主应得" width="130"> <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>
<el-table-column label="号主入账" width="130"> <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>
<el-table-column label="结算差异" width="130"> <el-table-column label="结算差异" width="130">
<template #default="{ row }"> <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> </template>
</el-table-column> </el-table-column>
<el-table-column label="收款/退款/结算" min-width="170"> <el-table-column label="收款/退款/结算" min-width="170">
@@ -6,7 +6,7 @@ import {
fetchFinanceDetails, fetchFinanceDetails,
type FinanceDetail, type FinanceDetail,
} from '@/features/admin/api/adminFinance' } from '@/features/admin/api/adminFinance'
import { formatMoneyWithSymbol } from '@/shared/utils/money' import { formatCentWithSymbol } from '@/shared/utils/money'
import { orderStatusLabel } from '@/utils/statusLabels' import { orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue' import AdminTablePagination from '../components/AdminTablePagination.vue'
@@ -59,12 +59,8 @@ async function handlePageChange() {
await loadDetails() await loadDetails()
} }
function money(value: number) {
return formatMoneyWithSymbol(value)
}
function moneyCent(value: number) { function moneyCent(value: number) {
return formatMoneyWithSymbol(Number(value || 0) / 100) return formatCentWithSymbol(value)
} }
function financeStatusLabel(status: string) { function financeStatusLabel(status: string) {
@@ -226,14 +222,14 @@ function formatInputDate(date: Date) {
{{ moneyCent(row.refunding_amount_cent) }} {{ moneyCent(row.refunding_amount_cent) }}
</span> </span>
<span class="amount-text amount-cell">{{ moneyCent(row.channel_net_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">{{ moneyCent(row.checkout_platform_fee_cent) }}</span>
<span class="amount-text amount-cell">{{ money(row.checkout_owner_income) }}</span> <span class="amount-text amount-cell">{{ moneyCent(row.checkout_owner_income_cent) }}</span>
<span class="amount-text amount-cell">{{ money(row.owner_wallet_income_amount) }}</span> <span class="amount-text amount-cell">{{ moneyCent(row.owner_wallet_income_amount_cent) }}</span>
<span <span
class="amount-text amount-cell" 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> </span>
</div> </div>
</div> </div>
@@ -5,7 +5,7 @@ import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { fetchAdminFileBlob } from '@/shared/api/files' import { fetchAdminFileBlob } from '@/shared/api/files'
import { formatMoneyWithSymbol } from '@/shared/utils/money' import { formatCentWithSymbol } from '@/shared/utils/money'
import { import {
adminMarkListingAbnormal, adminMarkListingAbnormal,
adminOfflineListing, adminOfflineListing,
@@ -78,12 +78,12 @@ async function submitAction() {
} }
} }
function money(value: number) { function moneyCent(value: number) {
return formatMoneyWithSymbol(value) return formatCentWithSymbol(value)
} }
function listingPrice(row: Listing) { function listingPrice(row: Listing) {
return money(row.price) return moneyCent(row.price_cent)
} }
function extractObjectKey(url: string) { function extractObjectKey(url: string) {
@@ -150,7 +150,7 @@ function readError(error: unknown, fallback: string) {
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>押金</span> <span>押金</span>
<strong>{{ money(listing.deposit_amount) }}</strong> <strong>{{ moneyCent(listing.deposit_amount_cent) }}</strong>
</div> </div>
</div> </div>
@@ -172,7 +172,7 @@ function readError(error: unknown, fallback: string) {
<p>号主{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p> <p>号主{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p>
<p>号主 ID{{ listing.owner_id }}</p> <p>号主 ID{{ listing.owner_id }}</p>
<p>价格{{ listingPrice(listing) }}</p> <p>价格{{ listingPrice(listing) }}</p>
<p>押金{{ money(listing.deposit_amount) }}</p> <p>押金{{ moneyCent(listing.deposit_amount_cent) }}</p>
</div> </div>
</div> </div>
@@ -4,7 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue' import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
import { fetchAdminFileBlob } from '@/shared/api/files' import { fetchAdminFileBlob } from '@/shared/api/files'
import { formatMoneyWithSymbol } from '@/shared/utils/money' import { centToYuan, formatCent, formatMoneyWithSymbol, yuanToCent } from '@/shared/utils/money'
import { import {
adjustListingReviewPrice, adjustListingReviewPrice,
approveListing, approveListing,
@@ -185,10 +185,10 @@ async function handleSavePriceAdjust() {
reason: priceAdjustForm.reason.trim(), 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(), 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('请填写有效的加价后比例或价格') ElMessage.warning('请填写有效的加价后比例或价格')
return return
} }
@@ -298,8 +298,9 @@ function roundPreviewRatio(value: number) {
function sellerTotalPrice(row: Listing) { function sellerTotalPrice(row: Listing) {
const value = breakdownNumber(row, 'seller_total_price') const value = breakdownNumber(row, 'seller_total_price')
if (value > 0) return value if (value > 0) return value
const fallback = Number(row.price || 0) - getListingConsumablePrice(row) const price = centToYuan(row.price_cent)
return fallback > 0 ? fallback : Number(row.price || 0) const fallback = price - getListingConsumablePrice(row)
return fallback > 0 ? fallback : price
} }
function sellerCoinBasePrice(row: Listing) { function sellerCoinBasePrice(row: Listing) {
@@ -318,7 +319,7 @@ function sellerRatio(row: Listing) {
function buyerTotalPrice(row: Listing) { function buyerTotalPrice(row: Listing) {
const value = breakdownNumber(row, 'buyer_total_price') 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) { function buyerCoinBasePrice(row: Listing) {
@@ -463,7 +464,7 @@ function riskItems(row: Listing): RiskItem[] {
if (hasBanRecord(row)) items.push({ label: `封禁记录:${banRecordText(row)}`, level: 'danger' }) if (hasBanRecord(row)) items.push({ label: `封禁记录:${banRecordText(row)}`, level: 'danger' })
if ( if (
getListingConsumablePrice(row) > 0 && getListingConsumablePrice(row) > 0 &&
Number(row.deposit_amount || 0) <= getListingConsumablePrice(row) centToYuan(row.deposit_amount_cent) <= getListingConsumablePrice(row)
) { ) {
items.push({ label: '押金不高于消耗品价值', level: 'danger' }) items.push({ label: '押金不高于消耗品价值', level: 'danger' })
} }
@@ -646,7 +647,7 @@ function readError(error: unknown, fallback: string) {
<span>KD {{ assetNumberText(item, 'secret_kd') }}</span> <span>KD {{ assetNumberText(item, 'secret_kd') }}</span>
</div> </div>
<div class="queue-footer"> <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">{{ <el-tag v-if="isExternalUpload(item)" size="small" type="info">{{
uploaderName(item) uploaderName(item)
}}</el-tag> }}</el-tag>
@@ -733,7 +734,7 @@ function readError(error: unknown, fallback: string) {
</div> </div>
<div class="price-decision-card deposit"> <div class="price-decision-card deposit">
<span>押金与损耗</span> <span>押金与损耗</span>
<strong>{{ money(selectedListing.deposit_amount) }}</strong> <strong>¥{{ formatCent(selectedListing.deposit_amount_cent) }}</strong>
<p>每日损耗 {{ dailyLossText(selectedListing) }}</p> <p>每日损耗 {{ dailyLossText(selectedListing) }}</p>
<small>哈夫币 {{ formatHafCoinM(getCoinWan(selectedListing)) }}</small> <small>哈夫币 {{ formatHafCoinM(getCoinWan(selectedListing)) }}</small>
</div> </div>
@@ -10,7 +10,7 @@ import {
type Listing, type Listing,
} from '@/features/listings' } from '@/features/listings'
import { useAdminTable } from '@/features/admin/composables/useAdminTable' import { useAdminTable } from '@/features/admin/composables/useAdminTable'
import { useMoney } from '@/shared/composables/useMoney' import { formatCentWithSymbol } from '@/shared/utils/money'
import { import {
assetRegions, assetRegions,
formatEstimatedRentalDuration, formatEstimatedRentalDuration,
@@ -23,8 +23,6 @@ import {
getSkinGroup, getSkinGroup,
} from '@/utils/listingDisplay' } from '@/utils/listingDisplay'
const money = useMoney()
const filters = reactive<AdminListingQuery>({ const filters = reactive<AdminListingQuery>({
owner_id: '', owner_id: '',
status: '', status: '',
@@ -322,7 +320,7 @@ function readScreenshotError(error: unknown, fallback: string) {
} }
function listingPrice(row: Listing) { function listingPrice(row: Listing) {
return money(row.price) return formatCentWithSymbol(row.price_cent)
} }
function listingCoinM(row: Listing) { function listingCoinM(row: Listing) {
@@ -372,7 +370,7 @@ function characterAndWeaponSkinText(row: Listing) {
} }
function rentAndDepositText(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) { function estimateTitle(row: Listing) {
@@ -15,7 +15,7 @@ import {
type RefundStatus, type RefundStatus,
} from '@/features/orders' } from '@/features/orders'
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments' 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 { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
import { formatListingNo } from '@/utils/listingDisplay' import { formatListingNo } from '@/utils/listingDisplay'
@@ -111,9 +111,9 @@ function money(value: unknown) {
return formatMoney(Number(value || 0)) 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)) if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
return Number(legacyYuan || 0) return 0
} }
async function handleRefund() { async function handleRefund() {
@@ -173,7 +173,7 @@ function paymentBizTypeLabel(type: string) {
} }
function moneyCent(value: number) { function moneyCent(value: number) {
return formatMoneyWithSymbol(Number(value || 0) / 100) return formatCentWithSymbol(value)
} }
function formatHandoffRecordType(type: string) { function formatHandoffRecordType(type: string) {
@@ -232,15 +232,15 @@ function formatHandoffRecordType(type: string) {
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>订单金额</span> <span>订单金额</span>
<strong>¥{{ money(amountYuan(order.rent_amount_cent, order.rent_amount)) }}</strong> <strong>¥{{ money(amountYuan(order.rent_amount_cent)) }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>平台费用</span> <span>平台费用</span>
<strong>¥{{ money(amountYuan(order.platform_fee_cent, order.platform_fee)) }}</strong> <strong>¥{{ money(amountYuan(order.platform_fee_cent)) }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>押金</span> <span>押金</span>
<strong>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong> <strong>¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong>
</div> </div>
<div v-if="refundStatus" class="metric-card"> <div v-if="refundStatus" class="metric-card">
<span>退款状态</span> <span>退款状态</span>
@@ -37,9 +37,9 @@ const filteredOrders = computed(() => {
return orders.value.filter(item => item.status === status.value) 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)) if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
return Number(legacyYuan || 0) return 0
} }
function money(value: unknown) { function money(value: unknown) {
@@ -95,10 +95,10 @@ function money(value: unknown) {
<template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template> <template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="订单金额" width="100"> <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>
<el-table-column label="押金" width="100"> <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>
<el-table-column label="创建时间" min-width="180"> <el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template> <template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
@@ -11,7 +11,7 @@ import {
type PaymentConfig, type PaymentConfig,
} from '@/features/admin/api/paymentConfig' } from '@/features/admin/api/paymentConfig'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
import { formatMoney } from '@/shared/utils/money' import { formatCent } from '@/shared/utils/money'
import { readError } from '@/utils/error' import { readError } from '@/utils/error'
import PaymentConfigDialog from '../components/PaymentConfigDialog.vue' import PaymentConfigDialog from '../components/PaymentConfigDialog.vue'
@@ -231,7 +231,7 @@ function formatEnvironment(env: string) {
} }
function formatAmount(amountCent: number) { function formatAmount(amountCent: number) {
return formatMoney(amountCent / 100) return formatCent(amountCent)
} }
function getStatusType(status: string) { function getStatusType(status: string) {
@@ -3,7 +3,7 @@ import { Document, Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { fetchAdminPayments, type AdminPayment } from '@/features/admin/api/adminPayments' 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 { formatDateTime } from '@/utils/time'
import AdminTablePagination from '../components/AdminTablePagination.vue' import AdminTablePagination from '../components/AdminTablePagination.vue'
@@ -70,7 +70,7 @@ async function handlePageChange() {
} }
function moneyCent(value: number) { function moneyCent(value: number) {
return formatMoneyWithSymbol(Number(value || 0) / 100) return formatCentWithSymbol(value)
} }
function paymentStatusType(status: string) { function paymentStatusType(status: string) {
@@ -9,7 +9,7 @@ import {
unfreezeAdminUser, unfreezeAdminUser,
type AdminUserItem, type AdminUserItem,
} from '@/features/admin/api/adminUsers' } 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 { useAdminPaginatedTable } from '@/features/admin/composables/useAdminPaginatedTable'
import { userStatusLabel } from '@/utils/statusLabels' import { userStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
@@ -39,14 +39,14 @@ function openFreeze(row: AdminUserItem) {
function openDepositQuota(row: AdminUserItem) { function openDepositQuota(row: AdminUserItem) {
quotaUser.value = row quotaUser.value = row
quotaAmount.value = Number(row.deposit_free_quota || 0) quotaAmount.value = centToYuan(row.deposit_free_quota_cent)
} }
async function handleSetDepositQuota() { async function handleSetDepositQuota() {
if (!quotaUser.value) return if (!quotaUser.value) return
submitting.value = true submitting.value = true
try { try {
await setAdminUserDepositFreeQuota(quotaUser.value.id, quotaAmount.value) await setAdminUserDepositFreeQuota(quotaUser.value.id, yuanToCent(quotaAmount.value))
ElMessage.success('免押额度已更新') ElMessage.success('免押额度已更新')
quotaUser.value = null quotaUser.value = null
await loadUsers() await loadUsers()
@@ -93,8 +93,8 @@ function readError(error: unknown, fallback: string) {
return fallback return fallback
} }
function money(value: number | string | undefined) { function moneyCent(value: number | string | undefined) {
return formatMoney(Number(value || 0)) return formatCent(Number(value || 0))
} }
</script> </script>
@@ -117,13 +117,13 @@ function money(value: number | string | undefined) {
<template #default="{ row }">{{ userStatusLabel(row.status) }}</template> <template #default="{ row }">{{ userStatusLabel(row.status) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="免押额度" width="130"> <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>
<el-table-column label="已占用" width="120"> <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>
<el-table-column label="剩余免押" width="120"> <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>
<el-table-column label="注册时间" min-width="180"> <el-table-column label="注册时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template> <template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
@@ -195,8 +195,8 @@ function money(value: number | string | undefined) {
<strong>{{ quotaUser.phone }}</strong> · {{ quotaUser.nickname }} <strong>{{ quotaUser.phone }}</strong> · {{ quotaUser.nickname }}
</p> </p>
<p> <p>
已占用 ¥{{ money(quotaUser.deposit_free_used) }}剩余 ¥{{ 已占用 ¥{{ moneyCent(quotaUser.deposit_free_used_cent) }}剩余 ¥{{
money(quotaUser.deposit_free_remaining) moneyCent(quotaUser.deposit_free_remaining_cent)
}} }}
</p> </p>
<el-input-number <el-input-number
@@ -47,7 +47,7 @@ export async function fetchAdminDisputes(page = 1, pageSize = 20) {
export async function arbitrateDispute( export async function arbitrateDispute(
id: number, id: number,
payload: { result: string; remark: string; amount?: number } payload: { result: string; remark: string; amount_cent?: number }
) { ) {
const { data } = await apiClient.post<ApiResponse<Dispute>>( const { data } = await apiClient.post<ApiResponse<Dispute>>(
`/admin/disputes/${id}/arbitrate`, `/admin/disputes/${id}/arbitrate`,
@@ -19,8 +19,8 @@ export interface Listing {
asset_summary?: Record<string, unknown> asset_summary?: Record<string, unknown>
screenshot_urls: string[] screenshot_urls: string[]
cover_url: string cover_url: string
price: number price_cent: number
deposit_amount: number deposit_amount_cent: number
is_accelerated_sale?: boolean is_accelerated_sale?: boolean
in_transaction: boolean in_transaction: boolean
status: ListingStatus status: ListingStatus
@@ -40,8 +40,8 @@ export interface ListingPayload {
haf_coin_amount: number haf_coin_amount: number
asset_summary?: Record<string, unknown> asset_summary?: Record<string, unknown>
screenshot_urls: string[] screenshot_urls: string[]
price: number price_cent: number
deposit_amount: number deposit_amount_cent: number
agreed_virtual_asset_sale: boolean agreed_virtual_asset_sale: boolean
agreed_seller_agreement: boolean agreed_seller_agreement: boolean
} }
@@ -214,7 +214,7 @@ export async function approveListing(id: number) {
export interface AdminListingPriceAdjustPayload { export interface AdminListingPriceAdjustPayload {
buyer_ratio?: number buyer_ratio?: number
buyer_total_price?: number buyer_total_price_cent?: number
reason?: string reason?: string
} }
@@ -2,7 +2,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import type { Listing } from '@/features/listings' import type { Listing } from '@/features/listings'
import { formatMoney } from '@/shared/utils/money' import { formatCent, formatMoney } from '@/shared/utils/money'
import { import {
formatHafCoinM, formatHafCoinM,
getCoinWan, getCoinWan,
@@ -140,7 +140,7 @@ function formatStatNumber(value: number) {
</div> </div>
<div class="price-item deposit"> <div class="price-item deposit">
<small>押金</small> <small>押金</small>
<span>¥{{ formatMoney(listing.deposit_amount) }}</span> <span>¥{{ formatCent(listing.deposit_amount_cent) }}</span>
</div> </div>
<div class="price-action"> <div class="price-action">
<button class="rent-btn">立即租用</button> <button class="rent-btn">立即租用</button>
@@ -11,7 +11,7 @@ import {
} from '@/features/orders/api/orders' } from '@/features/orders/api/orders'
import { useSessionStore } from '@/stores/session' import { useSessionStore } from '@/stores/session'
import AuthImage from '@/shared/components/business/AuthImage.vue' import AuthImage from '@/shared/components/business/AuthImage.vue'
import { roundMoney, formatMoney } from '@/shared/utils/money' import { roundMoney, formatMoney, formatCent } from '@/shared/utils/money'
import { import {
assetRegions, assetRegions,
formatEstimatedRentalDuration, formatEstimatedRentalDuration,
@@ -102,7 +102,7 @@ const detailMetrics = computed(() => {
tone: 'coin', tone: 'coin',
}, },
{ label: '价格', value: `¥${orderTotal.value}`, tone: 'price' }, { 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> <strong>¥{{ formatMoney(orderPriceBreakdown.consumable) }}</strong>
</div> </div>
</div> </div>
<em>押金另付 ¥{{ formatMoney(listing.deposit_amount) }}</em> <em>押金另付 ¥{{ formatCent(listing.deposit_amount_cent) }}</em>
</div> </div>
<dl class="order-check-list"> <dl class="order-check-list">
<div> <div>
@@ -6,7 +6,7 @@ import {
type ListingPublishOptions, type ListingPublishOptions,
} from '@/features/listings/api/listingOptions' } from '@/features/listings/api/listingOptions'
import { fetchListings, type Listing } from '@/features/listings/api/listings' import { fetchListings, type Listing } from '@/features/listings/api/listings'
import { formatMoney } from '@/shared/utils/money' import { centToYuan, formatCent, formatMoney } from '@/shared/utils/money'
import { import {
defaultHomeAnnouncements, defaultHomeAnnouncements,
defaultHomeBanners, defaultHomeBanners,
@@ -347,7 +347,7 @@ function matchesFilters(item: Listing) {
if (key === 'price') value = getListingDisplayPrice(item) if (key === 'price') value = getListingDisplayPrice(item)
if (key === 'coin') value = getCoinM(item) if (key === 'coin') value = getCoinM(item)
if (key === 'secretKd') value = readAssetNumber(item, 'secret_kd') 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_')) { if (key.startsWith('resource_')) {
value = getResourceQuantity(item, key.replace('resource_', '')) value = getResourceQuantity(item, key.replace('resource_', ''))
} }
@@ -622,7 +622,7 @@ function parseQuantityUnit(price: string) {
</div> </div>
<div class="resource-price-box"> <div class="resource-price-box">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong> <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>
</RouterLink> </RouterLink>
</div> </div>
@@ -5,7 +5,7 @@ import { showToast } from 'vant'
import MobileBottomNav from '@/components/MobileBottomNav.vue' import MobileBottomNav from '@/components/MobileBottomNav.vue'
import { ensureSupportChat } from '@/features/chats/api/chats' import { ensureSupportChat } from '@/features/chats/api/chats'
import { formatMoney } from '@/shared/utils/money' import { formatCent, formatMoney } from '@/shared/utils/money'
import { import {
emptyListingPublishOptions, emptyListingPublishOptions,
type ListingPublishOptions, type ListingPublishOptions,
@@ -697,7 +697,7 @@ function chipTone(label: string) {
<div class="card-footer"> <div class="card-footer">
<div class="price-col"> <div class="price-col">
<strong>¥{{ formatMoney(getListingDisplayPrice(item)) }}</strong> <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> </div>
</div> </div>
@@ -11,7 +11,7 @@ import {
} from '@/features/orders/api/orders' } from '@/features/orders/api/orders'
import { useSessionStore } from '@/stores/session' import { useSessionStore } from '@/stores/session'
import AuthImage from '@/shared/components/business/AuthImage.vue' import AuthImage from '@/shared/components/business/AuthImage.vue'
import { formatMoney } from '@/shared/utils/money' import { formatCent, formatMoney } from '@/shared/utils/money'
import { import {
assetRegions, assetRegions,
formatHafCoinM, formatHafCoinM,
@@ -95,7 +95,7 @@ const detailMetrics = computed(() => {
tone: 'coin', tone: 'coin',
}, },
{ label: '价格', value: `¥${formatMoney(getListingDisplayPrice(listing.value))}`, tone: 'price' }, { 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_original_amount_cent: number
deposit_waived_amount_cent: number deposit_waived_amount_cent: number
platform_fee_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> account_snapshot?: Record<string, unknown>
listing_snapshot?: string listing_snapshot?: string
checkout_info?: string checkout_info?: string
@@ -64,16 +56,6 @@ export interface Checkout {
deposit_deduct_amount_cent: number deposit_deduct_amount_cent: number
renter_refund_amount_cent?: number renter_refund_amount_cent?: number
owner_income_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 content: string
evidence_urls: string[] evidence_urls: string[]
owner_adjustment_reason: 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`) const { data } = await apiClient.post<ApiResponse<RefundStatus>>(`/admin/orders/${id}/refund`)
return data.data return data.data
} }
@@ -557,26 +557,24 @@ function formatHandoffRecordType(type: string) {
return typeMap[type] || type return typeMap[type] || type
} }
function amountYuan(cent: unknown, legacyYuan?: unknown) { function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent)) if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent))
return readNumber(legacyYuan) return 0
} }
function orderRentAmount(item: Order) { function orderRentAmount(item: Order) {
if (item.owner_id === session.userId) 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) if (item.renter_id === session.userId)
return amountYuan(item.rent_amount_cent, item.rent_amount ?? item.display_amount) return amountYuan(item.rent_amount_cent)
return amountYuan(item.display_amount_cent, item.display_amount) return amountYuan(item.display_amount_cent)
} }
function ownerActualIncome(item: Order) { function ownerActualIncome(item: Order) {
if (item.owner_id !== session.userId) return null if (item.owner_id !== session.userId) return null
const value = item.checkout?.owner_income_amount_cent const value = item.checkout?.owner_income_amount_cent
if (typeof value === 'number') return centToYuan(value) if (typeof value === 'number') return centToYuan(value)
return typeof item.checkout?.owner_income_amount === 'number' return null
? item.checkout.owner_income_amount
: null
} }
function quantity(value: unknown) { function quantity(value: unknown) {
@@ -596,16 +594,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
function hydrateCounterForm() { function hydrateCounterForm() {
if (!order.value?.checkout) return if (!order.value?.checkout) return
const checkout = order.value.checkout const checkout = order.value.checkout
counterForm.value.consumable_amount = amountYuan( counterForm.value.consumable_amount = amountYuan(checkout.consumable_amount_cent)
checkout.consumable_amount_cent,
checkout.consumable_amount
)
counterForm.value.coin_consumed_m = checkout.coin_consumed_m counterForm.value.coin_consumed_m = checkout.coin_consumed_m
counterForm.value.other_amount = amountYuan(checkout.other_amount_cent, checkout.other_amount) counterForm.value.other_amount = amountYuan(checkout.other_amount_cent)
counterForm.value.deposit_deduct_amount = amountYuan( counterForm.value.deposit_deduct_amount = amountYuan(checkout.deposit_deduct_amount_cent)
checkout.deposit_deduct_amount_cent,
checkout.deposit_deduct_amount
)
} }
function linesToList(value: string) { function linesToList(value: string) {
@@ -696,14 +688,12 @@ async function copyListingCode() {
<div class="meta-item"> <div class="meta-item">
<span class="meta-label">押金</span> <span class="meta-label">押金</span>
<strong class="meta-value" <strong class="meta-value"
>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong >¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong
> >
<span <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" class="meta-note"
>已免押 ¥{{ >已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</span
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount))
}}</span
> >
</div> </div>
<div class="meta-item"> <div class="meta-item">
@@ -956,59 +946,36 @@ async function copyListingCode() {
<van-cell-group inset :border="false"> <van-cell-group inset :border="false">
<van-cell <van-cell
title="实际结算租金" title="实际结算租金"
:value="`¥${money( :value="`¥${money(amountYuan(order.checkout.display_amount_cent))}`"
amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount)
)}`"
/> />
<van-cell <van-cell
title="押金总额" title="押金总额"
:label=" :label="
amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0 amountYuan(order.deposit_waived_amount_cent) > 0
? `已免押 ¥${money( ? `已免押 ¥${money(amountYuan(order.deposit_waived_amount_cent))}`
amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)
)}`
: '' : ''
" "
:value="`¥${money( :value="`¥${money(amountYuan(order.checkout.deposit_amount_cent))}`"
amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount)
)}`"
/> />
<van-cell <van-cell
title="额外消耗品已用" title="额外消耗品已用"
:value="`¥${money( :value="`¥${money(amountYuan(order.checkout.consumable_amount_cent))}`"
amountYuan(order.checkout.consumable_amount_cent, order.checkout.consumable_amount)
)}`"
/> />
<van-cell <van-cell
title="押金赔付扣除" title="押金赔付扣除"
:value="`¥${money( :value="`¥${money(amountYuan(order.checkout.deposit_deduct_amount_cent))}`"
amountYuan(
order.checkout.deposit_deduct_amount_cent,
order.checkout.deposit_deduct_amount
)
)}`"
value-class="red-text" value-class="red-text"
/> />
<van-cell <van-cell
v-if="isRenter" v-if="isRenter"
title="退还租客" title="退还租客"
:value="`¥${money( :value="`¥${money(amountYuan(order.checkout.renter_refund_amount_cent))}`"
amountYuan(
order.checkout.renter_refund_amount_cent,
order.checkout.renter_refund_amount
)
)}`"
value-class="green-text" value-class="green-text"
/> />
<van-cell <van-cell
v-if="isOwner" v-if="isOwner"
title="号主最终收入" title="号主最终收入"
:value="`¥${money( :value="`¥${money(amountYuan(order.checkout.owner_income_amount_cent))}`"
amountYuan(
order.checkout.owner_income_amount_cent,
order.checkout.owner_income_amount
)
)}`"
value-class="green-text" value-class="green-text"
/> />
<van-cell <van-cell
@@ -131,21 +131,21 @@ function amountLabel(order: Order) {
return isOwner(order) ? '预计租金' : '支付租金' return isOwner(order) ? '预计租金' : '支付租金'
} }
function amountYuan(cent: unknown, legacyYuan?: unknown) { function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0)) if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
return Number(legacyYuan || 0) return 0
} }
function orderRentAmount(order: Order) { function orderRentAmount(order: Order) {
if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent, order.owner_rent_amount ?? order.display_amount) if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent)
return amountYuan(order.rent_amount_cent, order.rent_amount ?? order.display_amount) return amountYuan(order.rent_amount_cent)
} }
function ownerActualIncome(order: Order) { function ownerActualIncome(order: Order) {
if (!isOwner(order)) return null if (!isOwner(order)) return null
const value = order.checkout?.owner_income_amount_cent const value = order.checkout?.owner_income_amount_cent
if (typeof value === 'number') return centToYuan(value) 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) { function formatListingCode(order: Order) {
@@ -238,9 +238,9 @@ async function copyListingCode(order: Order) {
<div class="price-item"> <div class="price-item">
<span class="price-label">押金金额</span> <span class="price-label">押金金额</span>
<span class="price-val deposit"> <span class="price-val deposit">
¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }} ¥{{ money(amountYuan(order.deposit_amount_cent)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0" <em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
> ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}</em > ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
> >
</span> </span>
</div> </div>
@@ -8,7 +8,7 @@ import QRCode from 'qrcode'
import { fetchOrderChat } from '@/features/chats/api/chats' import { fetchOrderChat } from '@/features/chats/api/chats'
import { createDispute } from '@/features/disputes' import { createDispute } from '@/features/disputes'
import { uploadFile } from '@/shared/api/files' import { uploadFile } from '@/shared/api/files'
import { centToYuan, formatMoney } from '@/shared/utils/money' import { centToYuan, formatCent, formatMoney } from '@/shared/utils/money'
import { import {
acceptCheckout, acceptCheckout,
cancelOrder, cancelOrder,
@@ -606,26 +606,24 @@ function money(value: unknown) {
return formatMoney(readNumber(value)) return formatMoney(readNumber(value))
} }
function amountYuan(cent: unknown, legacyYuan?: unknown) { function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent)) if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent))
return readNumber(legacyYuan) return 0
} }
function orderRentAmount(item: Order) { function orderRentAmount(item: Order) {
if (item.owner_id === session.userId) 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) if (item.renter_id === session.userId)
return amountYuan(item.rent_amount_cent, item.rent_amount ?? item.display_amount) return amountYuan(item.rent_amount_cent)
return amountYuan(item.display_amount_cent, item.display_amount) return amountYuan(item.display_amount_cent)
} }
function ownerActualIncome(item: Order) { function ownerActualIncome(item: Order) {
if (item.owner_id !== session.userId) return null if (item.owner_id !== session.userId) return null
const value = item.checkout?.owner_income_amount_cent const value = item.checkout?.owner_income_amount_cent
if (typeof value === 'number') return centToYuan(value) if (typeof value === 'number') return centToYuan(value)
return typeof item.checkout?.owner_income_amount === 'number' return null
? item.checkout.owner_income_amount
: null
} }
function quantity(value: unknown) { function quantity(value: unknown) {
@@ -645,16 +643,10 @@ function isRecord(value: unknown): value is Record<string, unknown> {
function hydrateCounterForm() { function hydrateCounterForm() {
if (!order.value?.checkout) return if (!order.value?.checkout) return
const checkout = order.value.checkout const checkout = order.value.checkout
counterForm.value.consumable_amount = amountYuan( counterForm.value.consumable_amount = amountYuan(checkout.consumable_amount_cent)
checkout.consumable_amount_cent,
checkout.consumable_amount
)
counterForm.value.coin_consumed_m = checkout.coin_consumed_m counterForm.value.coin_consumed_m = checkout.coin_consumed_m
counterForm.value.other_amount = amountYuan(checkout.other_amount_cent, checkout.other_amount) counterForm.value.other_amount = amountYuan(checkout.other_amount_cent)
counterForm.value.deposit_deduct_amount = amountYuan( counterForm.value.deposit_deduct_amount = amountYuan(checkout.deposit_deduct_amount_cent)
checkout.deposit_deduct_amount_cent,
checkout.deposit_deduct_amount
)
} }
function linesToList(value: string) { function linesToList(value: string) {
@@ -789,13 +781,13 @@ async function copyListingCode() {
<div class="metric-card"> <div class="metric-card">
<span class="metric-label">押金</span> <span class="metric-label">押金</span>
<strong class="metric-value" <strong class="metric-value"
>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong >¥{{ money(amountYuan(order.deposit_amount_cent)) }}</strong
> >
<span <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" class="metric-note"
>已免押 ¥{{ >已免押 ¥{{
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) money(amountYuan(order.deposit_waived_amount_cent))
}}</span }}</span
> >
</div> </div>
@@ -979,81 +971,40 @@ async function copyListingCode() {
<div class="summary-row"> <div class="summary-row">
<span>实际结算租金</span> <span>实际结算租金</span>
<strong <strong
>¥{{ >¥{{ money(amountYuan(order.checkout.display_amount_cent)) }}</strong
money(
amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount)
)
}}</strong
> >
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>预收押金</span> <span>预收押金</span>
<strong> <strong>
¥{{ ¥{{ money(amountYuan(order.checkout.deposit_amount_cent)) }}
money( <em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount) >已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
)
}}
<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
> >
</strong> </strong>
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>额外消耗品已用</span> <span>额外消耗品已用</span>
<strong class="warning" <strong class="warning"
>¥{{ >¥{{ money(amountYuan(order.checkout.consumable_amount_cent)) }}</strong
money(
amountYuan(
order.checkout.consumable_amount_cent,
order.checkout.consumable_amount
)
)
}}</strong
> >
</div> </div>
<div class="summary-row"> <div class="summary-row">
<span>押金赔付扣除</span> <span>押金赔付扣除</span>
<strong class="warning" <strong class="warning"
>¥{{ >¥{{ money(amountYuan(order.checkout.deposit_deduct_amount_cent)) }}</strong
money(
amountYuan(
order.checkout.deposit_deduct_amount_cent,
order.checkout.deposit_deduct_amount
)
)
}}</strong
> >
</div> </div>
<div v-if="isRenter" class="summary-row highlight"> <div v-if="isRenter" class="summary-row highlight">
<span>退还租客(未使用租金 + 剩余押金)</span> <span>退还租客(未使用租金 + 剩余押金)</span>
<strong class="amount" <strong class="amount"
>¥{{ >¥{{ money(amountYuan(order.checkout.renter_refund_amount_cent)) }}</strong
money(
amountYuan(
order.checkout.renter_refund_amount_cent,
order.checkout.renter_refund_amount
)
)
}}</strong
> >
</div> </div>
<div v-if="isOwner" class="summary-row highlight"> <div v-if="isOwner" class="summary-row highlight">
<span>号主最终收入(租金 + 押金赔付)</span> <span>号主最终收入(租金 + 押金赔付)</span>
<strong class="amount" <strong class="amount"
>¥{{ >¥{{ money(amountYuan(order.checkout.owner_income_amount_cent)) }}</strong
money(
amountYuan(
order.checkout.owner_income_amount_cent,
order.checkout.owner_income_amount
)
)
}}</strong
> >
</div> </div>
<div v-if="order.checkout.content" class="summary-note"> <div v-if="order.checkout.content" class="summary-note">
@@ -1109,7 +1060,7 @@ async function copyListingCode() {
v-model="counterForm.deposit_deduct_amount" v-model="counterForm.deposit_deduct_amount"
class="full-control" class="full-control"
:min="0" :min="0"
:max="amountYuan(order.deposit_amount_cent, order.deposit_amount)" :max="amountYuan(order.deposit_amount_cent)"
:precision="0" :precision="0"
controls-position="right" controls-position="right"
/> />
@@ -1331,7 +1282,7 @@ async function copyListingCode() {
<div class="pay-summary"> <div class="pay-summary">
<div class="pay-summary-row"> <div class="pay-summary-row">
<span>支付金额</span> <span>支付金额</span>
<strong>¥{{ formatMoney(activePayment.amount_cent / 100) }}</strong> <strong>¥{{ formatCent(activePayment.amount_cent) }}</strong>
</div> </div>
</div> </div>
<div v-if="paymentPayURL()" class="pay-qr-section"> <div v-if="paymentPayURL()" class="pay-qr-section">
@@ -127,22 +127,22 @@ function amountLabel(order: Order) {
return isRenter(order) ? '支付租金' : '预计租金' return isRenter(order) ? '支付租金' : '预计租金'
} }
function amountYuan(cent: unknown, legacyYuan?: unknown) { function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0)) if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
return Number(legacyYuan || 0) return 0
} }
function orderRentAmount(order: Order) { function orderRentAmount(order: Order) {
if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent, order.owner_rent_amount ?? order.display_amount) if (isOwner(order)) return amountYuan(order.owner_rent_amount_cent)
if (isRenter(order)) return amountYuan(order.rent_amount_cent, order.rent_amount ?? order.display_amount) if (isRenter(order)) return amountYuan(order.rent_amount_cent)
return amountYuan(order.display_amount_cent, order.display_amount) return amountYuan(order.display_amount_cent)
} }
function ownerActualIncome(order: Order) { function ownerActualIncome(order: Order) {
if (!isOwner(order)) return null if (!isOwner(order)) return null
const value = order.checkout?.owner_income_amount_cent const value = order.checkout?.owner_income_amount_cent
if (typeof value === 'number') return centToYuan(value) 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) { function money(value: unknown) {
@@ -269,9 +269,9 @@ function getCountdownMinutes(order: Order) {
<el-table-column label="押金" width="100"> <el-table-column label="押金" width="100">
<template #default="{ row }"> <template #default="{ row }">
<div class="amount-cell"> <div class="amount-cell">
<span class="amount-value">¥{{ money(row.deposit_amount) }}</span> <span class="amount-value">¥{{ money(amountYuan(row.deposit_amount_cent)) }}</span>
<span v-if="row.deposit_waived_amount > 0" class="amount-label" <span v-if="amountYuan(row.deposit_waived_amount_cent) > 0" class="amount-label"
> ¥{{ money(row.deposit_waived_amount) }}</span > ¥{{ money(amountYuan(row.deposit_waived_amount_cent)) }}</span
> >
</div> </div>
</template> </template>
@@ -357,9 +357,9 @@ function getCountdownMinutes(order: Order) {
<div class="meta-row"> <div class="meta-row">
<span class="meta-label">押金</span> <span class="meta-label">押金</span>
<span class="meta-value"> <span class="meta-value">
¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }} ¥{{ money(amountYuan(order.deposit_amount_cent)) }}
<em v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0" <em v-if="amountYuan(order.deposit_waived_amount_cent) > 0"
> ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}</em > ¥{{ money(amountYuan(order.deposit_waived_amount_cent)) }}</em
> >
</span> </span>
</div> </div>
@@ -26,6 +26,7 @@ import {
writePublishDraft, writePublishDraft,
} from '@/features/seller/composables/usePublishDraft' } from '@/features/seller/composables/usePublishDraft'
import type { PublishForm } from '@/types/publish' import type { PublishForm } from '@/types/publish'
import { yuanToCent } from '@/shared/utils/money'
import { commonOnlineTimes, dailyLossOptions, formatNumber, roundRatio } from '@/utils/pricing' import { commonOnlineTimes, dailyLossOptions, formatNumber, roundRatio } from '@/utils/pricing'
const draftSaveDelay = 400 const draftSaveDelay = 400
@@ -567,8 +568,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
haf_coin_amount: pricing.coinMAmount.value * 1000000, haf_coin_amount: pricing.coinMAmount.value * 1000000,
asset_summary: buildAssetSummary(), asset_summary: buildAssetSummary(),
screenshot_urls: pricing.screenshotUrls.value, screenshot_urls: pricing.screenshotUrls.value,
price: pricing.calculatedFinalPrice.value, price_cent: yuanToCent(pricing.calculatedFinalPrice.value),
deposit_amount: Number(form.deposit_amount), deposit_amount_cent: yuanToCent(Number(form.deposit_amount)),
agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value, agreed_virtual_asset_sale: virtualAssetSaleAgreementChecked.value,
agreed_seller_agreement: sellerAgreementChecked.value, agreed_seller_agreement: sellerAgreementChecked.value,
}) })
@@ -5,7 +5,7 @@ import { fetchOrders, type Order } from '@/features/orders'
import { useSessionStore } from '@/stores/session' import { useSessionStore } from '@/stores/session'
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels' import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time' import { formatDateTime } from '@/utils/time'
import { formatMoney } from '@/shared/utils/money' import { centToYuan, formatMoney } from '@/shared/utils/money'
import { formatListingNo } from '@/utils/listingDisplay' import { formatListingNo } from '@/utils/listingDisplay'
const session = useSessionStore() const session = useSessionStore()
@@ -73,6 +73,10 @@ function actionText(order: Order) {
function money(value: unknown) { function money(value: unknown) {
return formatMoney(Number(value || 0)) return formatMoney(Number(value || 0))
} }
function sellerAmount(order: Order) {
return centToYuan(order.owner_rent_amount_cent ?? order.display_amount_cent)
}
</script> </script>
<template> <template>
@@ -122,7 +126,7 @@ function money(value: unknown) {
<el-table-column prop="order_no" label="订单号" min-width="220" /> <el-table-column prop="order_no" label="订单号" min-width="220" />
<el-table-column prop="title" label="账号" min-width="180" /> <el-table-column prop="title" label="账号" min-width="180" />
<el-table-column label="金额" width="120"> <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>
<el-table-column label="订单状态" width="150"> <el-table-column label="订单状态" width="150">
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template> <template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
@@ -9,7 +9,7 @@ import {
submitListingReview, submitListingReview,
type Listing, type Listing,
} from '@/features/listings' } 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 { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
import { formatListingCode, getListingSellerPrice } from '@/utils/listingDisplay' import { formatListingCode, getListingSellerPrice } from '@/utils/listingDisplay'
@@ -227,7 +227,7 @@ function isPendingReview(row: Listing) {
</div> </div>
<div> <div>
<span>押金</span> <span>押金</span>
<strong>¥{{ formatMoney(item.deposit_amount) }}</strong> <strong>¥{{ formatCent(item.deposit_amount_cent) }}</strong>
</div> </div>
</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 { ApiResponse, PaginatedResult } from '@/shared/types/types'
import type { BalanceType, LedgerDirection, WalletStatus } from '@/shared/types/status' import type { BalanceType, LedgerDirection, WalletStatus } from '@/shared/types/status'
import type { PaymentOrder } from '@/features/orders/api/orders' import type { PaymentOrder } from '@/features/orders/api/orders'
import { yuanToCent } from '@/shared/utils/money'
export interface WalletAccount { export interface WalletAccount {
user_id: number user_id: number
@@ -42,13 +43,13 @@ export async function fetchWalletLedger(page = 1, pageSize = 20) {
} }
export async function rechargeWallet(amountYuan: number) { 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 }) const { data } = await apiClient.post<ApiResponse<WalletAccount>>('/wallet/recharge', { amount_cent })
return data.data return data.data
} }
export async function startWalletRechargePayment(amountYuan: number) { 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', { const { data } = await apiClient.post<ApiResponse<PaymentOrder>>('/wallet/recharge/pay', {
amount_cent, amount_cent,
}) })
@@ -402,8 +402,8 @@ function amountPrefix(direction: string) {
<span>支付金额</span> <span>支付金额</span>
<strong>{{ <strong>{{
activeRechargePayment activeRechargePayment
? formatMoney(activeRechargePayment.amount_cent / 100) ? formatCentWithSymbol(activeRechargePayment.amount_cent)
: formatMoney(devRechargeAmount) : `¥${formatMoney(devRechargeAmount)}`
}}</strong> }}</strong>
</div> </div>
<div v-if="rechargePayURL()" class="cashier-qr"> <div v-if="rechargePayURL()" class="cashier-qr">
+2 -1
View File
@@ -1,4 +1,5 @@
import type { Listing } from '@/features/listings/api/listings' import type { Listing } from '@/features/listings/api/listings'
import { centToYuan } from '@/shared/utils/money'
export interface ListingDisplayChip { export interface ListingDisplayChip {
label: string label: string
@@ -41,7 +42,7 @@ export function formatHafCoinM(amountWan: number) {
} }
export function getListingDisplayPrice(item: Listing) { export function getListingDisplayPrice(item: Listing) {
return Number(item.price || 0) return centToYuan(item.price_cent)
} }
export function getListingRentPrice(item: Listing) { export function getListingRentPrice(item: Listing) {
+12 -5
View File
@@ -40,12 +40,19 @@ export function centToYuan(cent: number | undefined | null): number {
} }
/** /**
* 0.1 * 0.1
* @example centToJiao(12345) -> 123.5 * @example centToDisplayYuan(12345) -> 123.5
* @example centToJiao(12344) -> 123.4 * @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 { 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" * @example formatCent(12344) -> "123.4"
*/ */
export function formatCent(cent: number | undefined | null): string { 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 type { Listing } from '@/features/listings/api/listings'
import { centToYuan } from '@/shared/utils/money'
export interface ListingDisplayChip { export interface ListingDisplayChip {
label: string label: string
@@ -41,7 +42,7 @@ export function formatHafCoinM(amountWan: number) {
} }
export function getListingDisplayPrice(item: Listing) { export function getListingDisplayPrice(item: Listing) {
return Number(item.price || 0) return centToYuan(item.price_cent)
} }
export function getListingRentPrice(item: Listing) { export function getListingRentPrice(item: Listing) {
+22 -21
View File
@@ -53,11 +53,11 @@ BEGIN
END IF; 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 ( VALUES (
LAST_INSERT_ID(), LAST_INSERT_ID(),
(i % 10) * 100.00, (i % 10) * 10000,
(i % 5) * 50.00, (i % 5) * 5000,
'active' 'active'
) ON DUPLICATE KEY UPDATE user_id=user_id; ) ON DUPLICATE KEY UPDATE user_id=user_id;
@@ -83,8 +83,8 @@ BEGIN
DECLARE i INT DEFAULT 1; DECLARE i INT DEFAULT 1;
DECLARE user_id_val BIGINT; DECLARE user_id_val BIGINT;
DECLARE account_id_val BIGINT; DECLARE account_id_val BIGINT;
DECLARE price_val DECIMAL(12,2); DECLARE price_val BIGINT;
DECLARE deposit_val DECIMAL(12,2); DECLARE deposit_val BIGINT;
DECLARE verified_user_min BIGINT; DECLARE verified_user_min BIGINT;
DECLARE verified_user_max BIGINT; DECLARE verified_user_max BIGINT;
DECLARE user_pick BIGINT; DECLARE user_pick BIGINT;
@@ -146,11 +146,11 @@ BEGIN
SET account_id_val = LAST_INSERT_ID(); SET account_id_val = LAST_INSERT_ID();
-- 创建租号商品 -- 创建租号商品
SET price_val = 5.00 + (i % 20) * 0.5; SET price_val = 500 + (i % 20) * 50;
SET deposit_val = 100.00 + (i % 10) * 50.00; SET deposit_val = 10000 + (i % 10) * 5000;
INSERT INTO rental_listings ( 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 in_transaction, status, review_status, published_at
) VALUES ( ) VALUES (
CONCAT( CONCAT(
@@ -205,8 +205,8 @@ BEGIN
DECLARE owner_id_val BIGINT; DECLARE owner_id_val BIGINT;
DECLARE renter_id_val BIGINT; DECLARE renter_id_val BIGINT;
DECLARE order_no_val VARCHAR(64); DECLARE order_no_val VARCHAR(64);
DECLARE rent_amount_val DECIMAL(12,2); DECLARE rent_amount_val BIGINT;
DECLARE deposit_val DECIMAL(12,2); DECLARE deposit_val BIGINT;
DECLARE listing_min BIGINT; DECLARE listing_min BIGINT;
DECLARE listing_max BIGINT; DECLARE listing_max BIGINT;
DECLARE listing_pick BIGINT; DECLARE listing_pick BIGINT;
@@ -235,7 +235,7 @@ BEGIN
-- 近似随机选择一个上架商品,避免 ORDER BY RAND() 全表排序。 -- 近似随机选择一个上架商品,避免 ORDER BY RAND() 全表排序。
SET listing_id_val = NULL; SET listing_id_val = NULL;
SET listing_pick = listing_min + FLOOR(RAND() * (listing_max - listing_min + 1)); 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 INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
FROM rental_listings rl FROM rental_listings rl
WHERE rl.id >= listing_pick WHERE rl.id >= listing_pick
@@ -244,7 +244,7 @@ BEGIN
ORDER BY rl.id LIMIT 1; ORDER BY rl.id LIMIT 1;
IF listing_id_val IS NULL THEN 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 INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
FROM rental_listings rl FROM rental_listings rl
WHERE rl.status IN ('published', 'active') WHERE rl.status IN ('published', 'active')
@@ -274,8 +274,8 @@ BEGIN
INSERT INTO rental_orders ( INSERT INTO rental_orders (
order_no, listing_id, account_id, owner_id, renter_id, order_no, listing_id, account_id, owner_id, renter_id,
estimated_duration_hours, rent_amount, owner_rent_amount, estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent,
deposit_amount, platform_fee, status, handoff_status, deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status,
settlement_status, rented_at, created_at settlement_status, rented_at, created_at
) VALUES ( ) VALUES (
order_no_val, order_no_val,
@@ -285,9 +285,10 @@ BEGIN
renter_id_val, renter_id_val,
24, 24,
rent_amount_val, rent_amount_val,
rent_amount_val * 0.95, -- 号主实得95% ROUND(rent_amount_val * 0.95), -- 号主实得95%
deposit_val, deposit_val,
rent_amount_val * 0.05, -- 平台5%手续费 deposit_val,
ROUND(rent_amount_val * 0.05), -- 平台5%手续费
CASE (i % 10) CASE (i % 10)
WHEN 0 THEN 'pending_payment' WHEN 0 THEN 'pending_payment'
WHEN 1 THEN 'cancelled' WHEN 1 THEN 'cancelled'
@@ -331,7 +332,7 @@ BEGIN
DECLARE user_id_val BIGINT; DECLARE user_id_val BIGINT;
DECLARE order_id_val BIGINT; DECLARE order_id_val BIGINT;
DECLARE ledger_no_val VARCHAR(64); DECLARE ledger_no_val VARCHAR(64);
DECLARE amount_val DECIMAL(12,2); DECLARE amount_val BIGINT;
DECLARE user_min BIGINT; DECLARE user_min BIGINT;
DECLARE user_max BIGINT; DECLARE user_max BIGINT;
DECLARE order_min BIGINT; DECLARE order_min BIGINT;
@@ -374,18 +375,18 @@ BEGIN
END IF; END IF;
SET ledger_no_val = CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(i, 6, '0')); 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 ( INSERT INTO wallet_ledger (
ledger_no, user_id, order_id, direction, amount, ledger_no, user_id, order_id, direction, amount_cent,
balance_after, balance_type, biz_type, biz_no, remark, created_at balance_after_cent, balance_type, biz_type, biz_no, remark, created_at
) VALUES ( ) VALUES (
ledger_no_val, ledger_no_val,
user_id_val, user_id_val,
order_id_val, order_id_val,
CASE WHEN i % 2 = 0 THEN 'in' ELSE 'out' END, CASE WHEN i % 2 = 0 THEN 'in' ELSE 'out' END,
amount_val, amount_val,
1000.00 + (i % 1000), 100000 + (i % 1000) * 100,
CASE WHEN i % 5 = 0 THEN 'frozen' ELSE 'available' END, CASE WHEN i % 5 = 0 THEN 'frozen' ELSE 'available' END,
CASE (i % 6) CASE (i % 6)
WHEN 0 THEN 'rent_payment' 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); 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 SELECT
u.id, u.id,
(u.id % 10) * 100.00, (u.id % 10) * 10000,
(u.id % 5) * 50.00, (u.id % 5) * 5000,
'active' 'active'
FROM users u FROM users u
WHERE NOT EXISTS (SELECT 1 FROM wallet_accounts WHERE user_id = u.id); WHERE NOT EXISTS (SELECT 1 FROM wallet_accounts WHERE user_id = u.id);
@@ -159,7 +159,7 @@ BEGIN
-- 批量生成租号商品 -- 批量生成租号商品
INSERT INTO rental_listings ( 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 in_transaction, status, review_status, published_at
) )
SELECT SELECT
@@ -169,8 +169,8 @@ BEGIN
) as listing_no, ) as listing_no,
ga.id as account_id, ga.id as account_id,
ga.owner_id, ga.owner_id,
5.00 + ((ga.id % 20) * 0.5) as price, 500 + ((ga.id % 20) * 50) as price_cent,
100.00 + ((ga.id % 10) * 50.00) as deposit_amount, 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 % 10 = 0 THEN 1 ELSE 0 END as in_transaction,
CASE CASE
WHEN ga.id % 20 = 0 THEN 'offline' WHEN ga.id % 20 = 0 THEN 'offline'
@@ -222,13 +222,13 @@ BEGIN
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
account_id BIGINT, account_id BIGINT,
owner_id BIGINT, owner_id BIGINT,
price DECIMAL(12,2), price_cent BIGINT,
deposit_amount DECIMAL(12,2), deposit_amount_cent BIGINT,
row_num INT row_num INT
); );
INSERT INTO tmp_available_listings (id, account_id, owner_id, price, deposit_amount, row_num) INSERT INTO tmp_available_listings (id, account_id, owner_id, price_cent, deposit_amount_cent, row_num)
SELECT id, account_id, owner_id, price, deposit_amount, (@rn := @rn + 1) SELECT id, account_id, owner_id, price_cent, deposit_amount_cent, (@rn := @rn + 1)
FROM rental_listings, (SELECT @rn := 0) init FROM rental_listings, (SELECT @rn := 0) init
WHERE status IN ('published', 'active') AND review_status = 'approved' WHERE status IN ('published', 'active') AND review_status = 'approved'
ORDER BY id; ORDER BY id;
@@ -255,8 +255,8 @@ BEGIN
WHILE current_batch < batches DO WHILE current_batch < batches DO
INSERT INTO rental_orders ( INSERT INTO rental_orders (
order_no, listing_id, account_id, owner_id, renter_id, order_no, listing_id, account_id, owner_id, renter_id,
estimated_duration_hours, rent_amount, owner_rent_amount, estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent,
deposit_amount, platform_fee, status, handoff_status, deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status,
settlement_status, rented_at, created_at settlement_status, rented_at, created_at
) )
SELECT SELECT
@@ -266,10 +266,11 @@ BEGIN
l.owner_id, l.owner_id,
r.id as renter_id, r.id as renter_id,
24 as estimated_duration_hours, 24 as estimated_duration_hours,
l.price * 24 as rent_amount, l.price_cent * 24 as rent_amount_cent,
l.price * 24 * 0.95 as owner_rent_amount, ROUND(l.price_cent * 24 * 0.95) as owner_rent_amount_cent,
l.deposit_amount, l.deposit_amount_cent,
l.price * 24 * 0.05 as platform_fee, l.deposit_amount_cent,
ROUND(l.price_cent * 24 * 0.05) as platform_fee_cent,
CASE (seq % 10) CASE (seq % 10)
WHEN 0 THEN 'pending_payment' WHEN 0 THEN 'pending_payment'
WHEN 1 THEN 'cancelled' WHEN 1 THEN 'cancelled'
@@ -360,16 +361,16 @@ BEGIN
WHILE current_batch < batches DO WHILE current_batch < batches DO
INSERT INTO wallet_ledger ( INSERT INTO wallet_ledger (
ledger_no, user_id, order_id, direction, amount, ledger_no, user_id, order_id, direction, amount_cent,
balance_after, balance_type, biz_type, biz_no, remark, created_at balance_after_cent, balance_type, biz_type, biz_no, remark, created_at
) )
SELECT SELECT
CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(seq, 6, '0')) as ledger_no, CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(seq, 6, '0')) as ledger_no,
u.id as user_id, u.id as user_id,
IF(seq % 2 = 0, o.id, NULL) as order_id, IF(seq % 2 = 0, o.id, NULL) as order_id,
CASE WHEN seq % 2 = 0 THEN 'in' ELSE 'out' END as direction, CASE WHEN seq % 2 = 0 THEN 'in' ELSE 'out' END as direction,
(seq % 500) + (seq * 0.01) as amount, ((seq % 500) * 100) + seq as amount_cent,
1000.00 + (seq % 1000) as balance_after, 100000 + ((seq % 1000) * 100) as balance_after_cent,
CASE WHEN seq % 5 = 0 THEN 'frozen' ELSE 'available' END as balance_type, CASE WHEN seq % 5 = 0 THEN 'frozen' ELSE 'available' END as balance_type,
CASE (seq % 6) CASE (seq % 6)
WHEN 0 THEN 'rent_payment' WHEN 0 THEN 'rent_payment'