统一金额分制重构
This commit is contained in:
@@ -29,21 +29,19 @@ func (GameAccount) TableName() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RentalListing struct {
|
type RentalListing struct {
|
||||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
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:"-"`
|
DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
|
||||||
DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
||||||
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
|
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
|
||||||
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"`
|
||||||
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
|
PublishedAt *time.Time `json:"published_at"`
|
||||||
ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
PublishedAt *time.Time `json:"published_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (RentalListing) TableName() string {
|
func (RentalListing) TableName() string {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -7,37 +7,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type OrderCheckout struct {
|
type OrderCheckout struct {
|
||||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
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:"-"`
|
OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
OwnerRentAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
PlatformFee float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
ConsumableAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
|
CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"`
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
OtherAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
DepositDeductAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
ConsumableAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
RenterRefundAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
ConsumableAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
OwnerIncomeAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
CoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"coin_consumed_m"`
|
Content string `json:"content"`
|
||||||
OtherAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
EvidenceURLS datatypes.JSON `gorm:"column:evidence_urls" json:"evidence_urls"`
|
||||||
OtherAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
|
||||||
DepositDeductAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
OwnerAdjustedAt *time.Time `json:"owner_adjusted_at"`
|
||||||
DepositDeductAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
RenterConfirmedAt *time.Time `json:"renter_confirmed_at"`
|
||||||
RenterRefundAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
RenterRejectedAt *time.Time `json:"renter_rejected_at"`
|
||||||
RenterRefundAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
OwnerIncomeAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"-"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
OwnerIncomeAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
|
||||||
Content string `json:"content"`
|
|
||||||
EvidenceURLS datatypes.JSON `gorm:"column:evidence_urls" json:"evidence_urls"`
|
|
||||||
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
|
|
||||||
OwnerAdjustedAt *time.Time `json:"owner_adjusted_at"`
|
|
||||||
RenterConfirmedAt *time.Time `json:"renter_confirmed_at"`
|
|
||||||
RenterRejectedAt *time.Time `json:"renter_rejected_at"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (OrderCheckout) TableName() string {
|
func (OrderCheckout) TableName() string {
|
||||||
|
|||||||
@@ -3,19 +3,18 @@ package model
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||||
Phone string `gorm:"size:32;not null;uniqueIndex" json:"phone"`
|
Phone string `gorm:"size:32;not null;uniqueIndex" json:"phone"`
|
||||||
Nickname string `gorm:"size:64;not null;default:''" json:"nickname"`
|
Nickname string `gorm:"size:64;not null;default:''" json:"nickname"`
|
||||||
AvatarURL string `gorm:"size:512;not null;default:''" json:"avatar_url"`
|
AvatarURL string `gorm:"size:512;not null;default:''" json:"avatar_url"`
|
||||||
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"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (User) TableName() string {
|
func (User) TableName() string {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ type DashboardDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MetricsDTO struct {
|
type MetricsDTO struct {
|
||||||
TotalUsers int64 `json:"total_users"`
|
TotalUsers int64 `json:"total_users"`
|
||||||
VerifiedUsers int64 `json:"verified_users"`
|
VerifiedUsers int64 `json:"verified_users"`
|
||||||
TotalListings int64 `json:"total_listings"`
|
TotalListings int64 `json:"total_listings"`
|
||||||
PublishedListings int64 `json:"published_listings"`
|
PublishedListings int64 `json:"published_listings"`
|
||||||
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 {
|
||||||
@@ -29,15 +29,15 @@ type PendingDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RecentOrderDTO struct {
|
type RecentOrderDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecentDisputeDTO struct {
|
type RecentDisputeDTO struct {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -26,35 +26,35 @@ type DashboardDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FinanceSummaryDTO struct {
|
type FinanceSummaryDTO struct {
|
||||||
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
|
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
|
||||||
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"`
|
||||||
SettledOrderCount int64 `json:"settled_order_count"`
|
SettledOrderCount int64 `json:"settled_order_count"`
|
||||||
FinancialExceptionCount int64 `json:"financial_exception_count"`
|
FinancialExceptionCount int64 `json:"financial_exception_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FinanceDailyDTO struct {
|
type FinanceDailyDTO struct {
|
||||||
Date string `json:"date"`
|
Date string `json:"date"`
|
||||||
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
|
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
|
||||||
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"`
|
||||||
SettledOrderCount int64 `json:"settled_order_count"`
|
SettledOrderCount int64 `json:"settled_order_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginatedResult struct {
|
type PaginatedResult struct {
|
||||||
@@ -65,32 +65,32 @@ type PaginatedResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type FinanceDetailDTO struct {
|
type FinanceDetailDTO struct {
|
||||||
OrderID uint64 `json:"order_id"`
|
OrderID uint64 `json:"order_id"`
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
OrderStatus string `json:"order_status"`
|
OrderStatus string `json:"order_status"`
|
||||||
SettlementStatus string `json:"settlement_status"`
|
SettlementStatus string `json:"settlement_status"`
|
||||||
RefundStatus string `json:"refund_status"`
|
RefundStatus string `json:"refund_status"`
|
||||||
RenterID uint64 `json:"renter_id"`
|
RenterID uint64 `json:"renter_id"`
|
||||||
RenterPhone string `json:"renter_phone"`
|
RenterPhone string `json:"renter_phone"`
|
||||||
RenterNickname string `json:"renter_nickname"`
|
RenterNickname string `json:"renter_nickname"`
|
||||||
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,10 +69,10 @@ 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)).
|
||||||
Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate).
|
Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
@@ -94,19 +92,19 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &FinanceSummaryDTO{
|
return &FinanceSummaryDTO{
|
||||||
TotalFlowAmountCent: payment.TotalFlowAmountCent,
|
TotalFlowAmountCent: payment.TotalFlowAmountCent,
|
||||||
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,
|
||||||
SettledOrderCount: settlement.SettledOrderCount,
|
SettledOrderCount: settlement.SettledOrderCount,
|
||||||
FinancialExceptionCount: exceptionCount,
|
FinancialExceptionCount: exceptionCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +128,10 @@ 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)).
|
||||||
Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate).
|
Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate).
|
||||||
@@ -162,10 +160,10 @@ func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error)
|
|||||||
for _, row := range settlements {
|
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,10 +281,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type dailyPaymentRow struct {
|
type dailyPaymentRow struct {
|
||||||
@@ -300,81 +298,88 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
type financeDetailRow struct {
|
type financeDetailRow struct {
|
||||||
OrderID uint64
|
OrderID uint64
|
||||||
OrderNo string
|
OrderNo string
|
||||||
OrderStatus string
|
OrderStatus string
|
||||||
SettlementStatus string
|
SettlementStatus string
|
||||||
RefundStatus string
|
RefundStatus string
|
||||||
RenterID uint64
|
RenterID uint64
|
||||||
RenterPhone string
|
RenterPhone string
|
||||||
RenterNickname string
|
RenterNickname string
|
||||||
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{
|
||||||
OrderID: r.OrderID,
|
OrderID: r.OrderID,
|
||||||
OrderNo: r.OrderNo,
|
OrderNo: r.OrderNo,
|
||||||
OrderStatus: r.OrderStatus,
|
OrderStatus: r.OrderStatus,
|
||||||
SettlementStatus: r.SettlementStatus,
|
SettlementStatus: r.SettlementStatus,
|
||||||
RefundStatus: r.RefundStatus,
|
RefundStatus: r.RefundStatus,
|
||||||
RenterID: r.RenterID,
|
RenterID: r.RenterID,
|
||||||
RenterPhone: r.RenterPhone,
|
RenterPhone: r.RenterPhone,
|
||||||
RenterNickname: r.RenterNickname,
|
RenterNickname: r.RenterNickname,
|
||||||
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,30 +3,29 @@ package adminuser
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type UserDTO struct {
|
type UserDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
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"`
|
||||||
DisputeCount int64 `json:"dispute_count"`
|
DisputeCount int64 `json:"dispute_count"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at"`
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
type FreezeRequest struct {
|
type FreezeRequest struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -143,46 +136,39 @@ func (r *Repository) Find(userID uint64) (*UserDTO, error) {
|
|||||||
|
|
||||||
type userRow struct {
|
type userRow struct {
|
||||||
model.User
|
model.User
|
||||||
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
|
||||||
}
|
}
|
||||||
return UserDTO{
|
return UserDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
Phone: row.Phone,
|
Phone: row.Phone,
|
||||||
Nickname: row.Nickname,
|
Nickname: row.Nickname,
|
||||||
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,
|
||||||
DisputeCount: row.DisputeCount,
|
DisputeCount: row.DisputeCount,
|
||||||
LastLoginAt: row.LastLoginAt,
|
LastLoginAt: row.LastLoginAt,
|
||||||
CreatedAt: row.CreatedAt,
|
CreatedAt: row.CreatedAt,
|
||||||
UpdatedAt: row.UpdatedAt,
|
UpdatedAt: row.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
@@ -281,25 +279,25 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
|||||||
}
|
}
|
||||||
disputeID := row.ID
|
disputeID := row.ID
|
||||||
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
|
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
|
||||||
"dispute_id": row.ID,
|
"dispute_id": row.ID,
|
||||||
"order_id": order.ID,
|
"order_id": order.ID,
|
||||||
"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,
|
||||||
"after_handoff_status": order.HandoffStatus,
|
"after_handoff_status": order.HandoffStatus,
|
||||||
"before_settlement_status": beforeSettlementStatus,
|
"before_settlement_status": beforeSettlementStatus,
|
||||||
"after_settlement_status": order.SettlementStatus,
|
"after_settlement_status": order.SettlementStatus,
|
||||||
"before_listing_status": beforeListingStatus,
|
"before_listing_status": beforeListingStatus,
|
||||||
"after_listing_status": listing.Status,
|
"after_listing_status": listing.Status,
|
||||||
"before_account_status": beforeAccountStatus,
|
"before_account_status": beforeAccountStatus,
|
||||||
"after_account_status": account.Status,
|
"after_account_status": account.Status,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -338,36 +336,29 @@ 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) {
|
||||||
|
|||||||
@@ -9,45 +9,45 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ListingDTO struct {
|
type ListingDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
ListingNo string `json:"listing_no"`
|
ListingNo string `json:"listing_no"`
|
||||||
AccountID uint64 `json:"account_id"`
|
AccountID uint64 `json:"account_id"`
|
||||||
OwnerID uint64 `json:"owner_id"`
|
OwnerID uint64 `json:"owner_id"`
|
||||||
OwnerPhone string `json:"owner_phone,omitempty"`
|
OwnerPhone string `json:"owner_phone,omitempty"`
|
||||||
OwnerNickname string `json:"owner_nickname,omitempty"`
|
OwnerNickname string `json:"owner_nickname,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
GameName string `json:"game_name"`
|
GameName string `json:"game_name"`
|
||||||
ServerRegion string `json:"server_region"`
|
ServerRegion string `json:"server_region"`
|
||||||
LoginPlatform string `json:"login_platform"`
|
LoginPlatform string `json:"login_platform"`
|
||||||
RankLevel string `json:"rank_level"`
|
RankLevel string `json:"rank_level"`
|
||||||
HafCoinAmount int64 `json:"haf_coin_amount"`
|
HafCoinAmount int64 `json:"haf_coin_amount"`
|
||||||
AssetSummary map[string]any `json:"asset_summary,omitempty"`
|
AssetSummary map[string]any `json:"asset_summary,omitempty"`
|
||||||
ScreenshotURLS []string `json:"screenshot_urls"`
|
ScreenshotURLS []string `json:"screenshot_urls"`
|
||||||
CoverURL string `json:"cover_url"`
|
CoverURL string `json:"cover_url"`
|
||||||
PriceCent int64 `json:"price_cent"`
|
PriceCent int64 `json:"price_cent"`
|
||||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||||
IsAccelerated bool `json:"is_accelerated_sale"`
|
IsAccelerated bool `json:"is_accelerated_sale"`
|
||||||
InTransaction bool `json:"in_transaction"`
|
InTransaction bool `json:"in_transaction"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
ReviewStatus string `json:"review_status"`
|
ReviewStatus string `json:"review_status"`
|
||||||
ReviewReason string `json:"review_reason"`
|
ReviewReason string `json:"review_reason"`
|
||||||
PublishedAt *time.Time `json:"published_at"`
|
PublishedAt *time.Time `json:"published_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
type CreateRequest struct {
|
||||||
Title string `json:"title" binding:"required"`
|
Title string `json:"title" binding:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ServerRegion string `json:"server_region" binding:"required"`
|
ServerRegion string `json:"server_region" binding:"required"`
|
||||||
LoginPlatform string `json:"login_platform"`
|
LoginPlatform string `json:"login_platform"`
|
||||||
RankLevel string `json:"rank_level"`
|
RankLevel string `json:"rank_level"`
|
||||||
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"`
|
||||||
@@ -60,9 +60,9 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminListQuery struct {
|
type AdminListQuery struct {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -525,16 +525,16 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
return CreateRequest{
|
return CreateRequest{
|
||||||
Title: externalUploadTitle(item, insurance, hafCoinM),
|
Title: externalUploadTitle(item, insurance, hafCoinM),
|
||||||
Description: "开放接口自动上传,等待后台审核。",
|
Description: "开放接口自动上传,等待后台审核。",
|
||||||
ServerRegion: serverRegionFromLoginMethod(item.LoginMethod),
|
ServerRegion: serverRegionFromLoginMethod(item.LoginMethod),
|
||||||
LoginPlatform: strings.TrimSpace(item.LoginMethod),
|
LoginPlatform: strings.TrimSpace(item.LoginMethod),
|
||||||
RankLevel: strings.TrimSpace(item.Rank),
|
RankLevel: strings.TrimSpace(item.Rank),
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -35,12 +35,12 @@ func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
|
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{
|
||||||
"resources": []any{
|
"resources": []any{
|
||||||
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"},
|
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"},
|
||||||
@@ -52,7 +52,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
|
|||||||
t.Fatalf("expected ErrDepositTooLow, got %v", err)
|
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"])
|
||||||
|
|||||||
@@ -54,20 +54,20 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminActionRequest struct {
|
type AdminActionRequest struct {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ 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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaymentDTO struct {
|
type PaymentDTO struct {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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 格式化分为带符号的字符串
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -138,22 +136,22 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user