统一金额分制重构

This commit is contained in:
yml
2026-06-09 19:04:11 +08:00
parent 87673288ef
commit 5cd3ead4bd
69 changed files with 886 additions and 1277 deletions
+17 -17
View File
@@ -11,14 +11,14 @@ type DashboardDTO struct {
}
type MetricsDTO struct {
TotalUsers int64 `json:"total_users"`
VerifiedUsers int64 `json:"verified_users"`
TotalListings int64 `json:"total_listings"`
PublishedListings int64 `json:"published_listings"`
TotalOrders int64 `json:"total_orders"`
RentingOrders int64 `json:"renting_orders"`
TodayOrders int64 `json:"today_orders"`
TodayLedgerAmount float64 `json:"today_ledger_amount"`
TotalUsers int64 `json:"total_users"`
VerifiedUsers int64 `json:"verified_users"`
TotalListings int64 `json:"total_listings"`
PublishedListings int64 `json:"published_listings"`
TotalOrders int64 `json:"total_orders"`
RentingOrders int64 `json:"renting_orders"`
TodayOrders int64 `json:"today_orders"`
TodayLedgerAmountCent int64 `json:"today_ledger_amount_cent"`
}
type PendingDTO struct {
@@ -29,15 +29,15 @@ type PendingDTO struct {
}
type RecentOrderDTO struct {
ID uint64 `json:"id"`
OrderNo string `json:"order_no"`
Title string `json:"title"`
RenterID uint64 `json:"renter_id"`
OwnerID uint64 `json:"owner_id"`
Status string `json:"status"`
RentAmount float64 `json:"rent_amount"`
DepositAmount float64 `json:"deposit_amount"`
CreatedAt time.Time `json:"created_at"`
ID uint64 `json:"id"`
OrderNo string `json:"order_no"`
Title string `json:"title"`
RenterID uint64 `json:"renter_id"`
OwnerID uint64 `json:"owner_id"`
Status string `json:"status"`
RentAmountCent int64 `json:"rent_amount_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
CreatedAt time.Time `json:"created_at"`
}
type RecentDisputeDTO struct {
@@ -44,9 +44,9 @@ func (r *Repository) Summary() (*DashboardDTO, error) {
return nil, err
}
if err := r.db.Model(&model.WalletLedger{}).
Select("COALESCE(SUM(amount), 0)").
Select("COALESCE(SUM(amount_cent), 0)").
Where("created_at >= ?", today).
Scan(&metrics.TodayLedgerAmount).Error; err != nil {
Scan(&metrics.TodayLedgerAmountCent).Error; err != nil {
return nil, err
}
if err := r.db.Model(&model.RentalListing{}).Where("review_status = ?", "pending").Count(&pending.ListingReviews).Error; err != nil {
@@ -81,7 +81,7 @@ func (r *Repository) Summary() (*DashboardDTO, error) {
func (r *Repository) recentOrders() ([]RecentOrderDTO, error) {
rows := make([]RecentOrderDTO, 0)
err := r.db.Table("rental_orders AS o").
Select("o.id, o.order_no, a.title, o.renter_id, o.owner_id, o.status, o.rent_amount, o.deposit_amount, o.created_at").
Select("o.id, o.order_no, a.title, o.renter_id, o.owner_id, o.status, o.rent_amount_cent, o.deposit_amount_cent, o.created_at").
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Order("o.id DESC").
Limit(8).
+54 -54
View File
@@ -26,35 +26,35 @@ type DashboardDTO struct {
}
type FinanceSummaryDTO struct {
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmount float64 `json:"platform_income_amount"`
OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
FinancialExceptionCount int64 `json:"financial_exception_count"`
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
FinancialExceptionCount int64 `json:"financial_exception_count"`
}
type FinanceDailyDTO struct {
Date string `json:"date"`
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmount float64 `json:"platform_income_amount"`
OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
Date string `json:"date"`
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
}
type PaginatedResult struct {
@@ -65,32 +65,32 @@ type PaginatedResult struct {
}
type FinanceDetailDTO struct {
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"`
SettlementStatus string `json:"settlement_status"`
RefundStatus string `json:"refund_status"`
RenterID uint64 `json:"renter_id"`
RenterPhone string `json:"renter_phone"`
RenterNickname string `json:"renter_nickname"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone"`
OwnerNickname string `json:"owner_nickname"`
OrderRentAmount float64 `json:"order_rent_amount"`
OrderDepositAmount float64 `json:"order_deposit_amount"`
CheckoutRentAmount float64 `json:"checkout_rent_amount"`
CheckoutRenterRefund float64 `json:"checkout_renter_refund"`
CheckoutOwnerIncome float64 `json:"checkout_owner_income"`
CheckoutPlatformFee float64 `json:"checkout_platform_fee"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"`
PaidAmountCent int64 `json:"paid_amount_cent"`
RefundedAmountCent int64 `json:"refunded_amount_cent"`
RefundingAmountCent int64 `json:"refunding_amount_cent"`
FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformNetAmount float64 `json:"platform_net_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
FinanceStatus string `json:"finance_status"`
CreatedAt time.Time `json:"created_at"`
SettledAt *time.Time `json:"settled_at,omitempty"`
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"`
SettlementStatus string `json:"settlement_status"`
RefundStatus string `json:"refund_status"`
RenterID uint64 `json:"renter_id"`
RenterPhone string `json:"renter_phone"`
RenterNickname string `json:"renter_nickname"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone"`
OwnerNickname string `json:"owner_nickname"`
OrderRentAmountCent int64 `json:"order_rent_amount_cent"`
OrderDepositAmountCent int64 `json:"order_deposit_amount_cent"`
CheckoutRentAmountCent int64 `json:"checkout_rent_amount_cent"`
CheckoutRenterRefundCent int64 `json:"checkout_renter_refund_cent"`
CheckoutOwnerIncomeCent int64 `json:"checkout_owner_income_cent"`
CheckoutPlatformFeeCent int64 `json:"checkout_platform_fee_cent"`
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
PaidAmountCent int64 `json:"paid_amount_cent"`
RefundedAmountCent int64 `json:"refunded_amount_cent"`
RefundingAmountCent int64 `json:"refunding_amount_cent"`
FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformNetAmountCent int64 `json:"platform_net_amount_cent"`
SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
FinanceStatus string `json:"finance_status"`
CreatedAt time.Time `json:"created_at"`
SettledAt *time.Time `json:"settled_at,omitempty"`
}
@@ -1,11 +1,9 @@
package adminfinance
import (
"math"
"time"
"hfb_sys/backend/internal/timeutil"
"hfb_sys/backend/pkg/money"
"gorm.io/gorm"
)
@@ -71,10 +69,10 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) {
var settlement settlementSummaryRow
if err := r.db.Table("rental_orders AS ro").
Select(`COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount,
COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount,
COUNT(ro.id) AS settled_order_count`).
Select(`COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent,
COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent,
COUNT(ro.id) AS settled_order_count`).
Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'").
Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)).
Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate).
@@ -94,19 +92,19 @@ func (r *Repository) summary(query DashboardQuery) (*FinanceSummaryDTO, error) {
}
return &FinanceSummaryDTO{
TotalFlowAmountCent: payment.TotalFlowAmountCent,
TotalRefundAmountCent: payment.TotalRefundAmountCent,
PendingRefundAmountCent: payment.PendingRefundAmountCent,
ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent,
PlatformIncomeAmount: money.Round(settlement.PlatformIncomeAmount),
OwnerShouldIncomeAmount: money.Round(settlement.OwnerShouldIncomeAmount),
OwnerWalletIncomeAmount: money.Round(settlement.OwnerWalletIncomeAmount),
SettlementDiffAmount: money.Round(settlement.OwnerShouldIncomeAmount - settlement.OwnerWalletIncomeAmount),
SuccessfulPayCount: payment.SuccessfulPayCount,
SuccessfulRefundCount: payment.SuccessfulRefundCount,
PendingRefundCount: payment.PendingRefundCount,
SettledOrderCount: settlement.SettledOrderCount,
FinancialExceptionCount: exceptionCount,
TotalFlowAmountCent: payment.TotalFlowAmountCent,
TotalRefundAmountCent: payment.TotalRefundAmountCent,
PendingRefundAmountCent: payment.PendingRefundAmountCent,
ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent,
PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent,
OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent,
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent,
SuccessfulPayCount: payment.SuccessfulPayCount,
SuccessfulRefundCount: payment.SuccessfulRefundCount,
PendingRefundCount: payment.PendingRefundCount,
SettledOrderCount: settlement.SettledOrderCount,
FinancialExceptionCount: exceptionCount,
}, nil
}
@@ -130,10 +128,10 @@ func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error)
settlements := make([]dailySettlementRow, 0)
if err := r.db.Table("rental_orders AS ro").
Select(`DATE(ro.settled_at) AS date,
COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount,
COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount,
COUNT(ro.id) AS settled_order_count`).
COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent,
COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent,
COUNT(ro.id) AS settled_order_count`).
Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'").
Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db)).
Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate).
@@ -162,10 +160,10 @@ func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error)
for _, row := range settlements {
item := itemsByDate[row.Date]
item.Date = row.Date
item.PlatformIncomeAmount = money.Round(row.PlatformIncomeAmount)
item.OwnerShouldIncomeAmount = money.Round(row.OwnerShouldIncomeAmount)
item.OwnerWalletIncomeAmount = money.Round(row.OwnerWalletIncomeAmount)
item.SettlementDiffAmount = money.Round(row.OwnerShouldIncomeAmount - row.OwnerWalletIncomeAmount)
item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent
item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent
item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent
item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent
item.SettledOrderCount = row.SettledOrderCount
itemsByDate[row.Date] = item
}
@@ -182,23 +180,23 @@ func (r *Repository) financeDetailBaseQuery(query DetailQuery) *gorm.DB {
Select(`ro.id AS order_id, ro.order_no, ro.status AS order_status, ro.settlement_status, ro.refund_status,
ro.renter_id, COALESCE(ru.phone, '') AS renter_phone, COALESCE(ru.nickname, '') AS renter_nickname,
ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname,
ro.rent_amount AS order_rent_amount, ro.deposit_amount AS order_deposit_amount,
COALESCE(oc.rent_amount, 0) AS checkout_rent_amount,
COALESCE(oc.renter_refund_amount, 0) AS checkout_renter_refund,
COALESCE(oc.owner_income_amount, 0) AS checkout_owner_income,
COALESCE(oc.platform_fee, 0) AS checkout_platform_fee,
COALESCE(w.owner_wallet_income_amount, 0) AS owner_wallet_income_amount,
COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent,
COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent,
COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent,
COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent,
COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent,
COALESCE(oc.platform_fee, 0) + ((COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0))) AS platform_net_amount,
COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0) AS settlement_diff_amount,
CASE
WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed'
WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending'
WHEN ABS(COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0)) >= 0.05 THEN 'settlement_diff'
ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent,
COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent,
COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent,
COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent,
COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent,
COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent,
COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent,
COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent,
COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent,
COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent,
COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent,
COALESCE(oc.platform_fee_cent, 0) + ((COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0))) AS platform_net_amount_cent,
COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent,
CASE
WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed'
WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending'
WHEN ABS(COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0)) >= 5 THEN 'settlement_diff'
ELSE 'normal'
END AS finance_status,
ro.created_at, ro.settled_at`).
@@ -250,7 +248,7 @@ func orderPaymentSubquery(db *gorm.DB) *gorm.DB {
func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB {
return db.Table("wallet_ledger").
Select("order_id, COALESCE(SUM(amount), 0) AS owner_wallet_income_amount").
Select("order_id, COALESCE(SUM(amount_cent), 0) AS owner_wallet_income_amount_cent").
Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation"}).
Group("order_id")
}
@@ -283,10 +281,10 @@ type paymentSummaryRow struct {
}
type settlementSummaryRow struct {
PlatformIncomeAmount float64
OwnerShouldIncomeAmount float64
OwnerWalletIncomeAmount float64
SettledOrderCount int64
PlatformIncomeAmountCent int64
OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64
SettledOrderCount int64
}
type dailyPaymentRow struct {
@@ -300,81 +298,88 @@ type dailyPaymentRow struct {
}
type dailySettlementRow struct {
Date string
PlatformIncomeAmount float64
OwnerShouldIncomeAmount float64
OwnerWalletIncomeAmount float64
SettledOrderCount int64
Date string
PlatformIncomeAmountCent int64
OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64
SettledOrderCount int64
}
type financeDetailRow struct {
OrderID uint64
OrderNo string
OrderStatus string
SettlementStatus string
RefundStatus string
RenterID uint64
RenterPhone string
RenterNickname string
OwnerID uint64
OwnerPhone string
OwnerNickname string
OrderRentAmount float64
OrderDepositAmount float64
CheckoutRentAmount float64
CheckoutRenterRefund float64
CheckoutOwnerIncome float64
CheckoutPlatformFee float64
OwnerWalletIncomeAmount float64
PaidAmountCent int64
RefundedAmountCent int64
RefundingAmountCent int64
FailedRefundAmountCent int64
ChannelNetAmountCent int64
PlatformNetAmount float64
SettlementDiffAmount float64
FinanceStatus string
CreatedAt time.Time
SettledAt *time.Time
OrderID uint64
OrderNo string
OrderStatus string
SettlementStatus string
RefundStatus string
RenterID uint64
RenterPhone string
RenterNickname string
OwnerID uint64
OwnerPhone string
OwnerNickname string
OrderRentAmountCent int64
OrderDepositAmountCent int64
CheckoutRentAmountCent int64
CheckoutRenterRefundCent int64
CheckoutOwnerIncomeCent int64
CheckoutPlatformFeeCent int64
OwnerWalletIncomeAmountCent int64
PaidAmountCent int64
RefundedAmountCent int64
RefundingAmountCent int64
FailedRefundAmountCent int64
ChannelNetAmountCent int64
PlatformNetAmountCent int64
SettlementDiffAmountCent int64
FinanceStatus string
CreatedAt time.Time
SettledAt *time.Time
}
func (r financeDetailRow) toDTO() FinanceDetailDTO {
diff := money.Round(r.SettlementDiffAmount)
diff := r.SettlementDiffAmountCent
status := r.FinanceStatus
if status == "" {
status = "normal"
}
if math.Abs(diff) < 0.05 && status == "settlement_diff" {
if absCent(diff) < 5 && status == "settlement_diff" {
status = "normal"
}
return FinanceDetailDTO{
OrderID: r.OrderID,
OrderNo: r.OrderNo,
OrderStatus: r.OrderStatus,
SettlementStatus: r.SettlementStatus,
RefundStatus: r.RefundStatus,
RenterID: r.RenterID,
RenterPhone: r.RenterPhone,
RenterNickname: r.RenterNickname,
OwnerID: r.OwnerID,
OwnerPhone: r.OwnerPhone,
OwnerNickname: r.OwnerNickname,
OrderRentAmount: money.Round(r.OrderRentAmount),
OrderDepositAmount: money.Round(r.OrderDepositAmount),
CheckoutRentAmount: money.Round(r.CheckoutRentAmount),
CheckoutRenterRefund: money.Round(r.CheckoutRenterRefund),
CheckoutOwnerIncome: money.Round(r.CheckoutOwnerIncome),
CheckoutPlatformFee: money.Round(r.CheckoutPlatformFee),
OwnerWalletIncomeAmount: money.Round(r.OwnerWalletIncomeAmount),
PaidAmountCent: r.PaidAmountCent,
RefundedAmountCent: r.RefundedAmountCent,
RefundingAmountCent: r.RefundingAmountCent,
FailedRefundAmountCent: r.FailedRefundAmountCent,
ChannelNetAmountCent: r.ChannelNetAmountCent,
PlatformNetAmount: money.Round(r.PlatformNetAmount),
SettlementDiffAmount: diff,
FinanceStatus: status,
CreatedAt: r.CreatedAt,
SettledAt: r.SettledAt,
OrderID: r.OrderID,
OrderNo: r.OrderNo,
OrderStatus: r.OrderStatus,
SettlementStatus: r.SettlementStatus,
RefundStatus: r.RefundStatus,
RenterID: r.RenterID,
RenterPhone: r.RenterPhone,
RenterNickname: r.RenterNickname,
OwnerID: r.OwnerID,
OwnerPhone: r.OwnerPhone,
OwnerNickname: r.OwnerNickname,
OrderRentAmountCent: r.OrderRentAmountCent,
OrderDepositAmountCent: r.OrderDepositAmountCent,
CheckoutRentAmountCent: r.CheckoutRentAmountCent,
CheckoutRenterRefundCent: r.CheckoutRenterRefundCent,
CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent,
CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent,
OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent,
PaidAmountCent: r.PaidAmountCent,
RefundedAmountCent: r.RefundedAmountCent,
RefundingAmountCent: r.RefundingAmountCent,
FailedRefundAmountCent: r.FailedRefundAmountCent,
ChannelNetAmountCent: r.ChannelNetAmountCent,
PlatformNetAmountCent: r.PlatformNetAmountCent,
SettlementDiffAmountCent: diff,
FinanceStatus: status,
CreatedAt: r.CreatedAt,
SettledAt: r.SettledAt,
}
}
func absCent(value int64) int64 {
if value < 0 {
return -value
}
return value
}
+17 -18
View File
@@ -3,30 +3,29 @@ package adminuser
import "time"
type UserDTO struct {
ID uint64 `json:"id"`
Phone string `json:"phone"`
Nickname string `json:"nickname"`
RealnameStatus string `json:"realname_status"`
RiskStatus string `json:"risk_status"`
CreditScore int `json:"credit_score"`
DepositFreeQuota float64 `json:"deposit_free_quota"`
DepositFreeUsed float64 `json:"deposit_free_used"`
DepositFreeRemaining float64 `json:"deposit_free_remaining"`
Status string `json:"status"`
OrderCount int64 `json:"order_count"`
ListingCount int64 `json:"listing_count"`
DisputeCount int64 `json:"dispute_count"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint64 `json:"id"`
Phone string `json:"phone"`
Nickname string `json:"nickname"`
RealnameStatus string `json:"realname_status"`
RiskStatus string `json:"risk_status"`
CreditScore int `json:"credit_score"`
DepositFreeQuotaCent int64 `json:"deposit_free_quota_cent"`
DepositFreeUsedCent int64 `json:"deposit_free_used_cent"`
DepositFreeRemainingCent int64 `json:"deposit_free_remaining_cent"`
Status string `json:"status"`
OrderCount int64 `json:"order_count"`
ListingCount int64 `json:"listing_count"`
DisputeCount int64 `json:"dispute_count"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FreezeRequest struct {
Reason string `json:"reason"`
}
type DepositFreeQuotaRequest struct {
AmountCent int64 `json:"amount_cent"`
Amount float64 `json:"amount"`
AmountCent int64 `json:"amount_cent"`
}
type PaginatedResult struct {
@@ -2,11 +2,9 @@ package adminuser
import (
"errors"
"math"
"hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/pkg/money"
"gorm.io/gorm"
"gorm.io/gorm/clause"
@@ -34,11 +32,11 @@ func (r *Repository) List(page, pageSize int) (*PaginatedResult, error) {
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_count,
COALESCE(df.deposit_free_used, 0) AS deposit_free_used`).
COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent`).
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id").
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount) AS deposit_free_used FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Order("u.id DESC").
Offset(offset).Limit(pageSize).
Scan(&rows).Error
@@ -65,23 +63,18 @@ func (r *Repository) SetDepositFreeQuota(adminID uint64, userID uint64, req Depo
if amountCent < 0 {
return nil, ErrInvalidUser
}
amount := float64(amountCent) / 100
err := r.db.Transaction(func(tx *gorm.DB) error {
var user model.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil {
return err
}
beforeAmount := user.DepositFreeQuota
beforeAmountCent := user.DepositFreeQuotaCent
user.DepositFreeQuota = amount
user.DepositFreeQuotaCent = amountCent
if err := tx.Save(&user).Error; err != nil {
return err
}
return appendAuditLog(tx, adminID, "admin_user.set_deposit_free_quota", user.ID, meta, map[string]any{
"user_id": user.ID,
"before_amount": beforeAmount,
"after_amount": amount,
"before_amount_cent": beforeAmountCent,
"after_amount_cent": amountCent,
})
@@ -127,11 +120,11 @@ func (r *Repository) Find(userID uint64) (*UserDTO, error) {
COALESCE(o.order_count, 0) AS order_count,
COALESCE(l.listing_count, 0) AS listing_count,
COALESCE(d.dispute_count, 0) AS dispute_count,
COALESCE(df.deposit_free_used, 0) AS deposit_free_used`).
COALESCE(df.deposit_free_used_cent, 0) AS deposit_free_used_cent`).
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS order_count FROM (SELECT renter_id AS user_id FROM rental_orders UNION ALL SELECT owner_id AS user_id FROM rental_orders) AS order_users GROUP BY user_id) AS o ON o.user_id = u.id").
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount) AS deposit_free_used FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Joins("LEFT JOIN (SELECT renter_id AS user_id, SUM(deposit_waived_amount_cent) AS deposit_free_used_cent FROM rental_orders WHERE status NOT IN ('completed', 'cancelled', 'closed') GROUP BY renter_id) AS df ON df.user_id = u.id").
Where("u.id = ?", userID).
First(&row).Error
if err != nil {
@@ -143,46 +136,39 @@ func (r *Repository) Find(userID uint64) (*UserDTO, error) {
type userRow struct {
model.User
OrderCount int64
ListingCount int64
DisputeCount int64
DepositFreeUsed float64
OrderCount int64
ListingCount int64
DisputeCount int64
DepositFreeUsedCent int64
}
func (row userRow) toDTO() UserDTO {
remaining := roundMoney(row.DepositFreeQuota - row.DepositFreeUsed)
remaining := row.DepositFreeQuotaCent - row.DepositFreeUsedCent
if remaining < 0 {
remaining = 0
}
return UserDTO{
ID: row.ID,
Phone: row.Phone,
Nickname: row.Nickname,
RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore,
DepositFreeQuota: row.DepositFreeQuota,
DepositFreeUsed: roundMoney(row.DepositFreeUsed),
DepositFreeRemaining: remaining,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,
DisputeCount: row.DisputeCount,
LastLoginAt: row.LastLoginAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
ID: row.ID,
Phone: row.Phone,
Nickname: row.Nickname,
RealnameStatus: row.RealnameStatus,
RiskStatus: row.RiskStatus,
CreditScore: row.CreditScore,
DepositFreeQuotaCent: row.DepositFreeQuotaCent,
DepositFreeUsedCent: row.DepositFreeUsedCent,
DepositFreeRemainingCent: remaining,
Status: row.Status,
OrderCount: row.OrderCount,
ListingCount: row.ListingCount,
DisputeCount: row.DisputeCount,
LastLoginAt: row.LastLoginAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
func roundMoney(value float64) float64 {
return money.Round(value)
}
func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 {
if req.AmountCent != 0 {
return req.AmountCent
}
return int64(math.Round(req.Amount * 100))
return req.AmountCent
}
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
@@ -9,18 +9,13 @@ func TestDepositFreeQuotaAmountCent(t *testing.T) {
want int64
}{
{
name: "优先使用分字段",
req: DepositFreeQuotaRequest{AmountCent: 1234, Amount: 99},
want: 1234,
},
{
name: "缺少分字段时回退元字段",
req: DepositFreeQuotaRequest{Amount: 12.34},
name: "使用分字段",
req: DepositFreeQuotaRequest{AmountCent: 1234},
want: 1234,
},
{
name: "负分拒绝",
req: DepositFreeQuotaRequest{AmountCent: -1, Amount: 12.34},
req: DepositFreeQuotaRequest{AmountCent: -1},
want: -1,
},
}
@@ -46,7 +46,7 @@ func (s *Service) SetDepositFreeQuota(adminID uint64, userID uint64, req Deposit
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if userID == 0 || req.Amount < 0 {
if userID == 0 || req.AmountCent < 0 {
return nil, ErrInvalidUser
}
return s.repo.SetDepositFreeQuota(adminID, userID, req, meta)
+3 -3
View File
@@ -35,9 +35,9 @@ type CreateRequest struct {
}
type ArbitrateRequest struct {
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
Amount float64 `json:"amount"`
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
AmountCent int64 `json:"amount_cent"`
}
type AuditMeta = auditlog.Meta
+63 -83
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"math"
"time"
"hfb_sys/backend/internal/auditlog"
@@ -249,9 +248,8 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
return err
}
if settlement.RenterRefundAmount > 0 {
refundCent := int64(math.Round(settlement.RenterRefundAmount * 100))
action, err := r.prepareRefund(&order, refundCent, "arbitration_refund", "仲裁退款原路退还")
if settlement.RenterRefundAmountCent > 0 {
action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还")
if err != nil {
return err
}
@@ -281,25 +279,25 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
}
disputeID := row.ID
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
"dispute_id": row.ID,
"order_id": order.ID,
"order_no": order.OrderNo,
"result": req.Result,
"remark": req.Remark,
"input_amount": req.Amount,
"renter_refund_amount": settlement.RenterRefundAmount,
"owner_income_amount": settlement.OwnerIncomeAmount,
"deposit_deduct_amount": settlement.DepositDeductAmount,
"before_order_status": beforeOrderStatus,
"after_order_status": order.Status,
"before_handoff_status": beforeHandoffStatus,
"after_handoff_status": order.HandoffStatus,
"before_settlement_status": beforeSettlementStatus,
"after_settlement_status": order.SettlementStatus,
"before_listing_status": beforeListingStatus,
"after_listing_status": listing.Status,
"before_account_status": beforeAccountStatus,
"after_account_status": account.Status,
"dispute_id": row.ID,
"order_id": order.ID,
"order_no": order.OrderNo,
"result": req.Result,
"remark": req.Remark,
"input_amount_cent": req.AmountCent,
"renter_refund_amount_cent": settlement.RenterRefundAmountCent,
"owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
"before_order_status": beforeOrderStatus,
"after_order_status": order.Status,
"before_handoff_status": beforeHandoffStatus,
"after_handoff_status": order.HandoffStatus,
"before_settlement_status": beforeSettlementStatus,
"after_settlement_status": order.SettlementStatus,
"before_listing_status": beforeListingStatus,
"after_listing_status": listing.Status,
"before_account_status": beforeAccountStatus,
"after_account_status": account.Status,
}); err != nil {
return err
}
@@ -338,36 +336,29 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
}
type arbitrationSettlement struct {
Entries []wallet.Entry
RenterRefundAmount float64
OwnerIncomeAmount float64
DepositDeductAmount float64
Entries []wallet.Entry
RenterRefundAmountCent int64
OwnerIncomeAmountCent int64
DepositDeductAmountCent int64
}
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalance float64) (arbitrationSettlement, error) {
rentAmount := float64(order.RentAmountCent) / 100
if rentAmount <= 0 {
rentAmount = order.RentAmount
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
rentAmountCent := order.RentAmountCent
depositAmountCent := order.DepositAmountCent
ownerRentAmountCent := order.OwnerRentAmountCent
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
ownerRentAmountCent = rentAmountCent
}
depositAmount := float64(order.DepositAmountCent) / 100
if depositAmount <= 0 {
depositAmount = order.DepositAmount
}
ownerRentAmount := float64(order.OwnerRentAmountCent) / 100
if ownerRentAmount <= 0 || ownerRentAmount > rentAmount {
ownerRentAmount = rentAmount
}
total := roundMoney(rentAmount + depositAmount)
ownerRentAmount = roundMoney(ownerRentAmount)
totalCent := rentAmountCent + depositAmountCent
settlement := arbitrationSettlement{}
orderID := order.ID
releaseFrozenAmount := minMoney(total, roundMoney(renterFrozenBalance))
if releaseFrozenAmount > 0 {
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
if releaseFrozenAmountCent > 0 {
settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.RenterID,
OrderID: &orderID,
Direction: "out",
AmountCent: int64(math.Round(releaseFrozenAmount * 100)),
AmountCent: releaseFrozenAmountCent,
BalanceType: "frozen",
BizType: "arbitration_release_frozen",
BizNo: order.OrderNo,
@@ -375,22 +366,22 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
})
}
addRenterRefund := func(amount float64, remark string) {
if amount <= 0 {
addRenterRefund := func(amountCent int64, remark string) {
if amountCent <= 0 {
return
}
settlement.RenterRefundAmount += amount
settlement.RenterRefundAmountCent += amountCent
}
addOwnerIncome := func(amount float64, remark string) {
if amount <= 0 {
addOwnerIncome := func(amountCent int64, remark string) {
if amountCent <= 0 {
return
}
settlement.OwnerIncomeAmount += amount
settlement.OwnerIncomeAmountCent += amountCent
settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.OwnerID,
OrderID: &orderID,
Direction: "in",
AmountCent: int64(math.Round(amount * 100)),
AmountCent: amountCent,
BalanceType: "available",
BizType: "arbitration_owner_income",
BizNo: order.OrderNo,
@@ -400,28 +391,27 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
switch req.Result {
case "full_refund":
addRenterRefund(total, "仲裁全额退款")
addRenterRefund(totalCent, "仲裁全额退款")
case "partial_refund":
req.Amount = roundMoney(req.Amount)
if req.Amount <= 0 || req.Amount > total {
if req.AmountCent <= 0 || req.AmountCent > totalCent {
return settlement, ErrInvalidDispute
}
addRenterRefund(req.Amount, "仲裁部分退款")
addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+depositAmount), "仲裁剩余金额结算给号主")
addRenterRefund(req.AmountCent, "仲裁部分退款")
addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主")
case "release_deposit":
addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主")
addRenterRefund(depositAmount, "仲裁释放押金给租客")
addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
case "deduct_deposit", "compensate_owner":
deductAmount := roundMoney(req.Amount)
if deductAmount <= 0 {
deductAmount = depositAmount
deductAmountCent := req.AmountCent
if deductAmountCent <= 0 {
deductAmountCent = depositAmountCent
}
if deductAmount > depositAmount {
if deductAmountCent > depositAmountCent {
return settlement, ErrInvalidDispute
}
settlement.DepositDeductAmount = deductAmount
addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
addRenterRefund(depositAmount-deductAmount, "仲裁退回剩余押金给租客")
settlement.DepositDeductAmountCent = deductAmountCent
addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
case "order_close":
// Only release frozen funds. No available-balance settlement happens in development mode.
case "mark_abnormal":
@@ -457,7 +447,7 @@ func (r *Repository) startRefundBestEffort(action *refundAction) {
_, _ = r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark)
}
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) {
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (int64, error) {
var account model.WalletAccount
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("user_id = ?", renterID).
@@ -468,17 +458,7 @@ func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) {
if err != nil {
return 0, err
}
return account.FrozenBalance, nil
}
// minMoney 返回较小金额(角精度)
func minMoney(a float64, b float64) float64 {
return money.Min(a, b)
}
// roundMoney 使用统一的角精度(0.1元)
func roundMoney(value float64) float64 {
return money.Round(value)
return account.FrozenBalanceCent, nil
}
func (r *Repository) baseQuery() *gorm.DB {
@@ -550,14 +530,14 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration
if req.Remark != "" {
content += "\n处理说明:" + req.Remark
}
if settlement.RenterRefundAmount > 0 {
content += fmt.Sprintf("\n退款给租客:¥%.2f", settlement.RenterRefundAmount)
if settlement.RenterRefundAmountCent > 0 {
content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent)
}
if settlement.OwnerIncomeAmount > 0 {
content += fmt.Sprintf("\n结算给号主:¥%.2f", settlement.OwnerIncomeAmount)
if settlement.OwnerIncomeAmountCent > 0 {
content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent)
}
if settlement.DepositDeductAmount > 0 {
content += fmt.Sprintf("\n押金扣除:¥%.2f", settlement.DepositDeductAmount)
if settlement.DepositDeductAmountCent > 0 {
content += "\n押金扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
}
return content
}
@@ -31,11 +31,11 @@ func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *test
t.Fatalf("不应在无冻结余额时生成解冻流水: %+v", entry)
}
}
if settlement.OwnerIncomeAmount != 180 {
t.Fatalf("OwnerIncomeAmount = %.1f, want 180.0", settlement.OwnerIncomeAmount)
if settlement.OwnerIncomeAmountCent != 18000 {
t.Fatalf("OwnerIncomeAmountCent = %d, want 18000", settlement.OwnerIncomeAmountCent)
}
if settlement.RenterRefundAmount != 100 {
t.Fatalf("RenterRefundAmount = %.1f, want 100.0", settlement.RenterRefundAmount)
if settlement.RenterRefundAmountCent != 10000 {
t.Fatalf("RenterRefundAmountCent = %d, want 10000", settlement.RenterRefundAmountCent)
}
for _, entry := range settlement.Entries {
if entry.UserID == order.RenterID && entry.BizType == "arbitration_renter_refund" {
@@ -58,7 +58,7 @@ func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testi
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
Result: "order_close",
Remark: "测试裁决",
}, 120)
}, 12000)
if err != nil {
t.Fatalf("buildArbitrationSettlement() error = %v", err)
}
@@ -83,15 +83,15 @@ func TestBuildArbitrationHandoffContent(t *testing.T) {
Result: "partial_refund",
Remark: "账号异常,退还部分租金。",
}, arbitrationSettlement{
RenterRefundAmount: 80,
OwnerIncomeAmount: 120,
RenterRefundAmountCent: 8000,
OwnerIncomeAmountCent: 12000,
})
wantParts := []string{
"客服仲裁结果:部分退款",
"处理说明:账号异常,退还部分租金。",
"退款给租客:¥80.00",
"结算给号主:¥120.00",
"退款给租客:¥80.0",
"结算给号主:¥120.0",
}
for _, part := range wantParts {
if !strings.Contains(content, part) {
+39 -39
View File
@@ -9,45 +9,45 @@ import (
)
type ListingDTO struct {
ID uint64 `json:"id"`
ListingNo string `json:"listing_no"`
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone,omitempty"`
OwnerNickname string `json:"owner_nickname,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
GameName string `json:"game_name"`
ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary,omitempty"`
ScreenshotURLS []string `json:"screenshot_urls"`
CoverURL string `json:"cover_url"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
IsAccelerated bool `json:"is_accelerated_sale"`
InTransaction bool `json:"in_transaction"`
Status string `json:"status"`
ReviewStatus string `json:"review_status"`
ReviewReason string `json:"review_reason"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint64 `json:"id"`
ListingNo string `json:"listing_no"`
AccountID uint64 `json:"account_id"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone,omitempty"`
OwnerNickname string `json:"owner_nickname,omitempty"`
Title string `json:"title"`
Description string `json:"description"`
GameName string `json:"game_name"`
ServerRegion string `json:"server_region"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary,omitempty"`
ScreenshotURLS []string `json:"screenshot_urls"`
CoverURL string `json:"cover_url"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
IsAccelerated bool `json:"is_accelerated_sale"`
InTransaction bool `json:"in_transaction"`
Status string `json:"status"`
ReviewStatus string `json:"review_status"`
ReviewReason string `json:"review_reason"`
PublishedAt *time.Time `json:"published_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateRequest struct {
Title string `json:"title" binding:"required"`
Description string `json:"description"`
ServerRegion string `json:"server_region" binding:"required"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"`
Price float64 `json:"price"`
DepositAmount float64 `json:"deposit_amount"`
Title string `json:"title" binding:"required"`
Description string `json:"description"`
ServerRegion string `json:"server_region" binding:"required"`
LoginPlatform string `json:"login_platform"`
RankLevel string `json:"rank_level"`
HafCoinAmount int64 `json:"haf_coin_amount"`
AssetSummary map[string]any `json:"asset_summary"`
ScreenshotURLS []string `json:"screenshot_urls"`
PriceCent int64 `json:"price_cent"`
DepositAmountCent int64 `json:"deposit_amount_cent"`
AgreedVirtualAssetSale bool `json:"agreed_virtual_asset_sale"`
AgreedSellerAgreement bool `json:"agreed_seller_agreement"`
@@ -60,9 +60,9 @@ type ReviewRequest struct {
}
type AdminPriceAdjustRequest struct {
BuyerRatio float64 `json:"buyer_ratio"`
BuyerTotalPrice float64 `json:"buyer_total_price"`
Reason string `json:"reason"`
BuyerRatio float64 `json:"buyer_ratio"`
BuyerTotalPriceCent int64 `json:"buyer_total_price_cent"`
Reason string `json:"reason"`
}
type AdminListQuery struct {
+30 -29
View File
@@ -79,16 +79,13 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo
if err := tx.Create(&account).Error; err != nil {
return err
}
price := normalizedListingPrice(req)
depositAmount := roundMoney(req.DepositAmount)
priceCent := normalizedListingPriceCent(req)
listing := model.RentalListing{
ListingNo: listingNo,
AccountID: account.ID,
OwnerID: ownerID,
Price: price,
PriceCent: int64(math.Round(price * 100)),
DepositAmount: depositAmount,
DepositAmountCent: int64(math.Round(depositAmount * 100)),
PriceCent: priceCent,
DepositAmountCent: req.DepositAmountCent,
Status: listingStatus,
ReviewStatus: reviewStatus,
PublishedAt: publishedAt,
@@ -149,16 +146,13 @@ func (r *Repository) CreateFromExternalUpload(upload externalUploadCreate, req C
if err := tx.Create(&account).Error; err != nil {
return err
}
price := normalizedListingPrice(req)
depositAmount := roundMoney(req.DepositAmount)
priceCent := normalizedListingPriceCent(req)
listing := model.RentalListing{
ListingNo: listingNo,
AccountID: account.ID,
OwnerID: owner.ID,
Price: price,
PriceCent: int64(math.Round(price * 100)),
DepositAmount: depositAmount,
DepositAmountCent: int64(math.Round(depositAmount * 100)),
PriceCent: priceCent,
DepositAmountCent: req.DepositAmountCent,
Status: "draft",
ReviewStatus: "pending",
}
@@ -216,9 +210,8 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest,
}
account.ScreenshotURLS = screenshots
price := normalizedListingPrice(req)
listing.Price = price
listing.DepositAmount = roundMoney(req.DepositAmount)
listing.PriceCent = normalizedListingPriceCent(req)
listing.DepositAmountCent = req.DepositAmountCent
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus
listing.ReviewStatus = reviewStatus
@@ -521,7 +514,7 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
}
sellerTotalPrice := readSummaryNumber(breakdown["seller_total_price"])
if sellerTotalPrice <= 0 {
sellerTotalPrice = math.Max(0, listing.Price-consumablePrice)
sellerTotalPrice = math.Max(0, centToYuan(listing.PriceCent)-consumablePrice)
}
sellerCoinBasePrice := readSummaryNumber(breakdown["seller_coin_base_price"])
if sellerCoinBasePrice <= 0 {
@@ -536,13 +529,13 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
return ErrInvalidPrice
}
beforePrice := listing.Price
beforePriceCent := listing.PriceCent
beforeRatio := readSummaryNumber(breakdown["buyer_ratio"])
if beforeRatio <= 0 && listing.Price > consumablePrice {
beforeRatio = roundRatio(coinWan / (listing.Price - consumablePrice))
if beforeRatio <= 0 && centToYuan(listing.PriceCent) > consumablePrice {
beforeRatio = roundRatio(coinWan / (centToYuan(listing.PriceCent) - consumablePrice))
}
listing.Price = buyerTotalPrice
listing.PriceCent = yuanToCent(buyerTotalPrice)
summary["publish_ratio"] = buyerRatio
breakdown["seller_coin_base_price"] = roundMoney(sellerCoinBasePrice)
breakdown["seller_total_price"] = roundMoney(sellerTotalPrice)
@@ -572,8 +565,8 @@ func (r *Repository) AdjustReviewPrice(adminID uint64, listingID uint64, req Adm
"listing_id": listing.ID,
"account_id": account.ID,
"owner_id": listing.OwnerID,
"before_price": beforePrice,
"after_price": listing.Price,
"before_price_cent": beforePriceCent,
"after_price_cent": listing.PriceCent,
"before_buyer_ratio": beforeRatio,
"after_buyer_ratio": buyerRatio,
"platform_markup": breakdown["platform_markup_amount"],
@@ -783,9 +776,9 @@ func canListPublicWithSQL(query PublicListQuery) bool {
func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB {
switch sortKey {
case "priceAsc":
return db.Order("l.price ASC, l.published_at DESC, l.id DESC")
return db.Order("l.price_cent ASC, l.published_at DESC, l.id DESC")
case "priceDesc":
return db.Order("l.price DESC, l.published_at DESC, l.id DESC")
return db.Order("l.price_cent DESC, l.published_at DESC, l.id DESC")
case "coinDesc":
return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC")
default:
@@ -1384,16 +1377,16 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
return items
}
func normalizedListingPrice(req CreateRequest) float64 {
func normalizedListingPriceCent(req CreateRequest) int64 {
if req.AssetSummary != nil {
if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok {
buyerPrice := readSummaryNumber(breakdown["buyer_total_price"])
if buyerPrice > 0 {
return roundMoney(buyerPrice)
return yuanToCent(buyerPrice)
}
}
}
return roundMoney(req.Price)
return req.PriceCent
}
func publicListings(items []ListingDTO) []ListingDTO {
@@ -1620,8 +1613,8 @@ func ensurePriceBreakdown(summary map[string]any) map[string]any {
}
func calculateAdminAdjustedPrice(req AdminPriceAdjustRequest, coinWan float64, consumablePrice float64) (float64, float64, float64) {
if req.BuyerTotalPrice > 0 {
buyerTotalPrice := roundMoney(req.BuyerTotalPrice)
if req.BuyerTotalPriceCent > 0 {
buyerTotalPrice := roundMoney(centToYuan(req.BuyerTotalPriceCent))
buyerCoinBasePrice := roundMoney(buyerTotalPrice - consumablePrice)
if buyerCoinBasePrice <= 0 || coinWan <= 0 {
return 0, 0, 0
@@ -1643,6 +1636,14 @@ func roundRatio(value float64) float64 {
return math.Round(value*10) / 10
}
func yuanToCent(value float64) int64 {
return int64(math.Round(roundMoney(value) * 100))
}
func centToYuan(value int64) float64 {
return float64(value) / 100
}
func cleanScreenshotURLs(urls []string) []string {
cleaned := make([]string, 0, len(urls))
seen := make(map[string]struct{}, len(urls))
+14 -14
View File
@@ -252,7 +252,7 @@ func (s *Service) AdjustReviewPrice(adminID uint64, id uint64, req AdminPriceAdj
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if req.BuyerRatio <= 0 && req.BuyerTotalPrice <= 0 {
if req.BuyerRatio <= 0 && req.BuyerTotalPriceCent <= 0 {
return nil, ErrInvalidPrice
}
return s.repo.AdjustReviewPrice(adminID, id, req, meta)
@@ -328,13 +328,13 @@ func validateRequest(req CreateRequest, rules publishRules) error {
if strings.TrimSpace(req.ServerRegion) == "" {
return ErrMissingServerRegion
}
if normalizedListingPrice(req) <= 0 {
if normalizedListingPriceCent(req) <= 0 {
return ErrInvalidPrice
}
if req.DepositAmount < 0 {
if req.DepositAmountCent < 0 {
return ErrInvalidDeposit
}
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmount <= consumables {
if consumables := consumableValue(req.AssetSummary); consumables > 0 && req.DepositAmountCent <= yuanToCent(consumables) {
return ErrDepositTooLow
}
if req.HafCoinAmount < 0 {
@@ -525,16 +525,16 @@ func externalAccountToCreateRequest(uploaderName string, uploadTime int64, item
},
}
return CreateRequest{
Title: externalUploadTitle(item, insurance, hafCoinM),
Description: "开放接口自动上传,等待后台审核。",
ServerRegion: serverRegionFromLoginMethod(item.LoginMethod),
LoginPlatform: strings.TrimSpace(item.LoginMethod),
RankLevel: strings.TrimSpace(item.Rank),
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot},
Price: price,
DepositAmount: item.Deposit,
Title: externalUploadTitle(item, insurance, hafCoinM),
Description: "开放接口自动上传,等待后台审核。",
ServerRegion: serverRegionFromLoginMethod(item.LoginMethod),
LoginPlatform: strings.TrimSpace(item.LoginMethod),
RankLevel: strings.TrimSpace(item.Rank),
HafCoinAmount: int64(math.Round(hafCoinM * 1000000)),
AssetSummary: assetSummary,
ScreenshotURLS: []string{defaultUploadScreenshot},
PriceCent: yuanToCent(price),
DepositAmountCent: yuanToCent(item.Deposit),
}
}
@@ -27,7 +27,7 @@ func TestCalculateAdminAdjustedPriceByRatio(t *testing.T) {
}
func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPrice: 70}, 1000, 20)
base, total, ratio := calculateAdminAdjustedPrice(AdminPriceAdjustRequest{BuyerTotalPriceCent: 7000}, 1000, 20)
if base != 50 || total != 70 || ratio != 20 {
t.Fatalf("unexpected adjusted price: base=%.2f total=%.2f ratio=%.2f", base, total, ratio)
}
@@ -35,12 +35,12 @@ func TestCalculateAdminAdjustedPriceByTotalPrice(t *testing.T) {
func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
req := CreateRequest{
Title: "测试账号",
ServerRegion: "烽火地带",
Price: 100,
DepositAmount: 2,
HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"},
Title: "测试账号",
ServerRegion: "烽火地带",
PriceCent: 10000,
DepositAmountCent: 200,
HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{
"resources": []any{
map[string]any{"mode": "收费", "quantity": float64(2), "price": "1元/个"},
@@ -52,7 +52,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
t.Fatalf("expected ErrDepositTooLow, got %v", err)
}
req.DepositAmount = 3
req.DepositAmountCent = 300
if err := validateRequest(req, publishRules{}); err != nil {
t.Fatalf("expected valid request, got %v", err)
}
@@ -159,8 +159,8 @@ func TestExternalAccountToCreateRequestMapsUploadFields(t *testing.T) {
if req.HafCoinAmount != 197100000 {
t.Fatalf("expected haf coin amount 197100000, got %d", req.HafCoinAmount)
}
if req.Price != 458 || req.DepositAmount != 400 {
t.Fatalf("unexpected price/deposit %.2f/%.2f", req.Price, req.DepositAmount)
if req.PriceCent != 45800 || req.DepositAmountCent != 40000 {
t.Fatalf("unexpected price/deposit cent %d/%d", req.PriceCent, req.DepositAmountCent)
}
if req.AssetSummary["season_insurance"] != "3*3" {
t.Fatalf("expected 3*3 insurance, got %#v", req.AssetSummary["season_insurance"])
+11 -11
View File
@@ -54,20 +54,20 @@ type SubmitReturnRequest struct {
}
type SubmitCheckoutRequest struct {
Content string `json:"content" binding:"required"`
ConsumableAmount float64 `json:"consumable_amount"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"`
EvidenceURLS []string `json:"evidence_urls"`
Content string `json:"content" binding:"required"`
ConsumableAmountCent int64 `json:"consumable_amount_cent"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmountCent int64 `json:"other_amount_cent"`
EvidenceURLS []string `json:"evidence_urls"`
}
type CounterCheckoutRequest struct {
ConsumableAmount float64 `json:"consumable_amount"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"`
DepositDeductAmount float64 `json:"deposit_deduct_amount"`
Reason string `json:"reason" binding:"required"`
EvidenceURLS []string `json:"evidence_urls"`
ConsumableAmountCent int64 `json:"consumable_amount_cent"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmountCent int64 `json:"other_amount_cent"`
DepositDeductAmountCent int64 `json:"deposit_deduct_amount_cent"`
Reason string `json:"reason" binding:"required"`
EvidenceURLS []string `json:"evidence_urls"`
}
type AdminActionRequest struct {
+113 -125
View File
@@ -77,22 +77,22 @@ func orderDurationHours(order model.RentalOrder) int {
}
func buildOrderPricing(listing model.RentalListing, account model.GameAccount) orderPricing {
rentAmount := roundMoney(listing.Price)
ownerRentAmount := readSnapshotPrice(account.AssetSummary, "seller_total_price")
if ownerRentAmount <= 0 || ownerRentAmount > rentAmount {
ownerRentAmount = rentAmount
rentAmountCent := listing.PriceCent
ownerRentAmountCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "seller_total_price") * 100))
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
ownerRentAmountCent = rentAmountCent
}
platformFee := readSnapshotPrice(account.AssetSummary, "platform_markup_amount")
if platformFee <= 0 || roundMoney(ownerRentAmount+platformFee) != rentAmount {
platformFee = roundMoney(rentAmount - ownerRentAmount)
platformFeeCent := int64(math.Round(readSnapshotPrice(account.AssetSummary, "platform_markup_amount") * 100))
if platformFeeCent <= 0 || ownerRentAmountCent+platformFeeCent != rentAmountCent {
platformFeeCent = rentAmountCent - ownerRentAmountCent
}
if platformFee < 0 {
platformFee = 0
if platformFeeCent < 0 {
platformFeeCent = 0
}
return orderPricing{
RentAmountCent: int64(math.Round(rentAmount * 100)),
OwnerRentAmountCent: int64(math.Round(ownerRentAmount * 100)),
PlatformFeeCent: int64(math.Round(platformFee * 100)),
RentAmountCent: rentAmountCent,
OwnerRentAmountCent: ownerRentAmountCent,
PlatformFeeCent: platformFeeCent,
}
}
@@ -173,8 +173,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
}
rentHours := internalOrderHours
pricing := buildOrderPricing(listing, account)
depositOriginalAmount := roundMoney(listing.DepositAmount)
paidDepositAmount, waivedDepositAmount, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmount)
depositOriginalAmountCent := listing.DepositAmountCent
paidDepositAmountCent, waivedDepositAmountCent, err := r.depositAmountsForOrder(tx, renterID, depositOriginalAmountCent)
if err != nil {
return err
}
@@ -185,17 +185,11 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
OwnerID: listing.OwnerID,
RenterID: renterID,
EstimatedDurationHours: rentHours,
RentAmount: float64(pricing.RentAmountCent) / 100,
RentAmountCent: pricing.RentAmountCent,
OwnerRentAmount: float64(pricing.OwnerRentAmountCent) / 100,
OwnerRentAmountCent: pricing.OwnerRentAmountCent,
DepositAmount: paidDepositAmount,
DepositAmountCent: int64(math.Round(paidDepositAmount * 100)),
DepositOriginalAmount: depositOriginalAmount,
DepositOriginalAmountCent: int64(math.Round(depositOriginalAmount * 100)),
DepositWaivedAmount: waivedDepositAmount,
DepositWaivedAmountCent: int64(math.Round(waivedDepositAmount * 100)),
PlatformFee: float64(pricing.PlatformFeeCent) / 100,
DepositAmountCent: paidDepositAmountCent,
DepositOriginalAmountCent: depositOriginalAmountCent,
DepositWaivedAmountCent: waivedDepositAmountCent,
PlatformFeeCent: pricing.PlatformFeeCent,
AccountSnapshot: snapshot,
Status: "pending_payment",
@@ -231,9 +225,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
return r.FindForUser(renterID, createdID)
}
func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDeposit float64) (float64, float64, error) {
originalDeposit = roundMoney(originalDeposit)
if originalDeposit <= 0 {
func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64) (int64, int64, error) {
if originalDepositCent <= 0 {
return 0, 0, nil
}
var user model.User
@@ -244,28 +237,27 @@ func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, origin
if err != nil {
return 0, 0, err
}
paidDeposit, waivedDeposit := calculateDepositWaiver(originalDeposit, user.DepositFreeQuota, used)
paidDeposit, waivedDeposit := calculateDepositWaiver(originalDepositCent, user.DepositFreeQuotaCent, used)
return paidDeposit, waivedDeposit, nil
}
func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (float64, error) {
var used float64
func activeDepositFreeUsed(tx *gorm.DB, renterID uint64) (int64, error) {
var used int64
err := tx.Model(&model.RentalOrder{}).
Where("renter_id = ? AND status NOT IN ?",
renterID,
[]string{"completed", "cancelled", "closed"},
).
Select("COALESCE(SUM(deposit_waived_amount), 0)").
Select("COALESCE(SUM(deposit_waived_amount_cent), 0)").
Scan(&used).Error
return roundMoney(used), err
return used, err
}
func calculateDepositWaiver(originalDeposit float64, quota float64, used float64) (float64, float64) {
originalDeposit = roundMoney(originalDeposit)
remaining := maxMoney(roundMoney(quota)-roundMoney(used), 0)
waived := minMoney(originalDeposit, remaining)
paid := maxMoney(originalDeposit-waived, 0)
return roundMoney(paid), roundMoney(waived)
func calculateDepositWaiver(originalDepositCent int64, quotaCent int64, usedCent int64) (int64, int64) {
remaining := maxCent(quotaCent-usedCent, 0)
waived := minCent(originalDepositCent, remaining)
paid := maxCent(originalDepositCent-waived, 0)
return paid, waived
}
// Pay 保留旧接口兼容,但真实付款必须走 payment 模块的渠道支付入口。
@@ -554,7 +546,7 @@ func (r *Repository) SubmitCheckout(userID uint64, orderID uint64, req SubmitChe
if err := tx.Create(&record).Error; err != nil {
return err
}
checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, 0, false)
checkout, err := buildCheckout(order, order.RenterID, "submitted", req.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, 0, false)
if err != nil {
return err
}
@@ -650,18 +642,22 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC
First(&checkout).Error; err != nil {
return err
}
next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmount, req.CoinConsumedM, req.OtherAmount, req.DepositDeductAmount, true)
next, err := buildCheckout(order, checkout.InitiatedBy, "countered", checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, req.OtherAmountCent, req.DepositDeductAmountCent, true)
if err != nil {
return err
}
now := time.Now()
checkout.Status = "countered"
checkout.ConsumableAmount = next.ConsumableAmount
checkout.RentAmountCent = next.RentAmountCent
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
checkout.PlatformFeeCent = next.PlatformFeeCent
checkout.DepositAmountCent = next.DepositAmountCent
checkout.ConsumableAmountCent = next.ConsumableAmountCent
checkout.CoinConsumedM = next.CoinConsumedM
checkout.OtherAmount = next.OtherAmount
checkout.DepositDeductAmount = next.DepositDeductAmount
checkout.RenterRefundAmount = next.RenterRefundAmount
checkout.OwnerIncomeAmount = next.OwnerIncomeAmount
checkout.OtherAmountCent = next.OtherAmountCent
checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
checkout.OwnerAdjustmentReason = req.Reason
checkout.OwnerAdjustedAt = &now
checkout.EvidenceURLS = next.EvidenceURLS
@@ -697,9 +693,9 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC
}
dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{
OwnerID: userID,
RentAmountCent: int64(math.Round(checkout.RentAmount * 100)),
OwnerRentAmountCent: int64(math.Round(checkout.OwnerRentAmount * 100)),
DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)),
RentAmountCent: checkout.RentAmountCent,
OwnerRentAmountCent: checkout.OwnerRentAmountCent,
DepositAmountCent: checkout.DepositAmountCent,
})
return &dto, nil
}
@@ -1095,11 +1091,11 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
refund = action
}
checkout.RentAmount = float64(settlement.ActualRentAmountCent) / 100
checkout.OwnerRentAmount = float64(settlement.OwnerRentIncomeCent) / 100
checkout.PlatformFee = float64(settlement.PlatformFeeCent) / 100
checkout.RenterRefundAmount = float64(settlement.RenterRefundCent) / 100
checkout.OwnerIncomeAmount = float64(settlement.OwnerIncomeCent) / 100
checkout.RentAmountCent = settlement.ActualRentAmountCent
checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent
checkout.PlatformFeeCent = settlement.PlatformFeeCent
checkout.RenterRefundAmountCent = settlement.RenterRefundCent
checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent
if err := notification.Append(tx,
notification.Entry{
UserID: order.RenterID,
@@ -1229,22 +1225,18 @@ func applyPaymentDeadline(dto *OrderDTO, order model.RentalOrder, timeoutMinutes
dto.PaymentDeadlineAt = &deadline
}
func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmount float64, coinConsumedM float64, otherAmount float64, explicitDeduct float64, useExplicitDeduct bool) (model.OrderCheckout, error) {
if consumableAmount < 0 || coinConsumedM < 0 || otherAmount < 0 || explicitDeduct < 0 {
func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, content string, evidenceURLS []string, consumableAmountCent int64, coinConsumedM float64, otherAmountCent int64, explicitDeductCent int64, useExplicitDeduct bool) (model.OrderCheckout, error) {
if consumableAmountCent < 0 || coinConsumedM < 0 || otherAmountCent < 0 || explicitDeductCent < 0 {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
}
consumableAmount = roundMoney(consumableAmount)
otherAmount = roundMoney(otherAmount)
explicitDeduct = roundMoney(explicitDeduct)
deductAmount := otherAmount
deductAmountCent := otherAmountCent
if useExplicitDeduct {
deductAmount = explicitDeduct
deductAmountCent = explicitDeductCent
}
depositAmountFromCent := float64(order.DepositAmountCent) / 100
if deductAmount > depositAmountFromCent {
if deductAmountCent > order.DepositAmountCent {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
}
settlement := calculateCheckoutSettlement(order, consumableAmount, roundQuantity(coinConsumedM), deductAmount)
settlement := calculateCheckoutSettlement(order, consumableAmountCent, roundQuantity(coinConsumedM), deductAmountCent)
evidence, err := marshalStringList(evidenceURLS)
if err != nil {
return model.OrderCheckout{}, err
@@ -1253,24 +1245,15 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
OrderID: order.ID,
InitiatedBy: initiatedBy,
Status: status,
RentAmount: float64(settlement.ActualRentAmountCent) / 100,
RentAmountCent: settlement.ActualRentAmountCent,
OwnerRentAmount: float64(settlement.OwnerRentIncomeCent) / 100,
OwnerRentAmountCent: settlement.OwnerRentIncomeCent,
PlatformFee: float64(settlement.PlatformFeeCent) / 100,
PlatformFeeCent: settlement.PlatformFeeCent,
DepositAmount: depositAmountFromCent,
DepositAmountCent: order.DepositAmountCent,
ConsumableAmount: consumableAmount,
ConsumableAmountCent: int64(math.Round(consumableAmount * 100)),
ConsumableAmountCent: consumableAmountCent,
CoinConsumedM: roundQuantity(coinConsumedM),
OtherAmount: otherAmount,
OtherAmountCent: int64(math.Round(otherAmount * 100)),
DepositDeductAmount: roundMoney(deductAmount),
OtherAmountCent: otherAmountCent,
DepositDeductAmountCent: settlement.DepositCompensationCent,
RenterRefundAmount: float64(settlement.RenterRefundCent) / 100,
RenterRefundAmountCent: settlement.RenterRefundCent,
OwnerIncomeAmount: float64(settlement.OwnerIncomeCent) / 100,
OwnerIncomeAmountCent: settlement.OwnerIncomeCent,
Content: content,
EvidenceURLS: evidence,
@@ -1278,57 +1261,55 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
}
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
return calculateCheckoutSettlement(order, checkout.ConsumableAmount, checkout.CoinConsumedM, checkout.DepositDeductAmount)
return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent)
}
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmount float64, coinConsumedM float64, depositDeductAmount float64) checkoutSettlement {
// 从分字段读取,转为元进行计算(保持现有逻辑兼容性)
orderRentAmount := float64(order.RentAmountCent) / 100
orderOwnerRentAmount := float64(order.OwnerRentAmountCent) / 100
orderDepositAmount := float64(order.DepositAmountCent) / 100
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
orderRentAmountCent := order.RentAmountCent
orderOwnerRentAmountCent := order.OwnerRentAmountCent
orderDepositAmountCent := order.DepositAmountCent
buyerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price")
if buyerCoinBasePrice <= 0 || buyerCoinBasePrice > orderRentAmount {
buyerCoinBasePrice = orderRentAmount
buyerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "buyer_coin_base_price") * 100))
if buyerCoinBasePriceCent <= 0 || buyerCoinBasePriceCent > orderRentAmountCent {
buyerCoinBasePriceCent = orderRentAmountCent
}
sellerCoinBasePrice := readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price")
if sellerCoinBasePrice <= 0 || sellerCoinBasePrice > orderOwnerRentAmount {
sellerCoinBasePrice = orderOwnerRentAmount
sellerCoinBasePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "seller_coin_base_price") * 100))
if sellerCoinBasePriceCent <= 0 || sellerCoinBasePriceCent > orderOwnerRentAmountCent {
sellerCoinBasePriceCent = orderOwnerRentAmountCent
}
prepaidConsumablePrice := readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price")
if prepaidConsumablePrice <= 0 || prepaidConsumablePrice > orderRentAmount-buyerCoinBasePrice {
prepaidConsumablePrice = maxMoney(orderRentAmount-buyerCoinBasePrice, 0)
prepaidConsumablePriceCent := int64(math.Round(readOrderSnapshotPrice(order.AccountSnapshot, "consumable_price") * 100))
if prepaidConsumablePriceCent <= 0 || prepaidConsumablePriceCent > orderRentAmountCent-buyerCoinBasePriceCent {
prepaidConsumablePriceCent = maxCent(orderRentAmountCent-buyerCoinBasePriceCent, 0)
}
prepaidOwnerConsumablePrice := maxMoney(orderOwnerRentAmount-sellerCoinBasePrice, 0)
prepaidOwnerConsumablePriceCent := maxCent(orderOwnerRentAmountCent-sellerCoinBasePriceCent, 0)
totalCoinM := readOrderSnapshotCoinM(order.AccountSnapshot)
coinUseRatio := 1.0
if totalCoinM > 0 {
coinUseRatio = minRatio(maxRatio(roundQuantity(coinConsumedM)/totalCoinM, 0), 1)
}
usedBuyerCoinPrice := roundMoney(buyerCoinBasePrice * coinUseRatio)
usedOwnerCoinPrice := roundMoney(sellerCoinBasePrice * coinUseRatio)
usedBuyerConsumablePrice := minMoney(roundMoney(consumableAmount), prepaidConsumablePrice)
usedBuyerCoinPriceCent := int64(math.Round(float64(buyerCoinBasePriceCent) * coinUseRatio))
usedOwnerCoinPriceCent := int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
usedBuyerConsumablePriceCent := minCent(consumableAmountCent, prepaidConsumablePriceCent)
consumableUseRatio := 1.0
if prepaidConsumablePrice > 0 {
consumableUseRatio = minRatio(maxRatio(usedBuyerConsumablePrice/prepaidConsumablePrice, 0), 1)
if prepaidConsumablePriceCent > 0 {
consumableUseRatio = minRatio(maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0), 1)
}
usedOwnerConsumablePrice := roundMoney(prepaidOwnerConsumablePrice * consumableUseRatio)
actualRentAmount := minMoney(roundMoney(usedBuyerCoinPrice+usedBuyerConsumablePrice), orderRentAmount)
ownerRentIncome := minMoney(roundMoney(usedOwnerCoinPrice+usedOwnerConsumablePrice), orderOwnerRentAmount)
depositCompensation := minMoney(roundMoney(depositDeductAmount), orderDepositAmount)
rentRefund := maxMoney(orderRentAmount-actualRentAmount, 0)
depositRefund := maxMoney(orderDepositAmount-depositCompensation, 0)
usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
actualRentAmountCent := minCent(usedBuyerCoinPriceCent+usedBuyerConsumablePriceCent, orderRentAmountCent)
ownerRentIncomeCent := minCent(usedOwnerCoinPriceCent+usedOwnerConsumablePriceCent, orderOwnerRentAmountCent)
depositCompensationCent := minCent(depositDeductAmountCent, orderDepositAmountCent)
rentRefundCent := maxCent(orderRentAmountCent-actualRentAmountCent, 0)
depositRefundCent := maxCent(orderDepositAmountCent-depositCompensationCent, 0)
// 最后转换为分返回
return checkoutSettlement{
OwnerRentIncomeCent: int64(math.Round(ownerRentIncome * 100)),
DepositCompensationCent: int64(math.Round(depositCompensation * 100)),
OwnerIncomeCent: int64(math.Round((ownerRentIncome + depositCompensation) * 100)),
RentRefundCent: int64(math.Round(rentRefund * 100)),
DepositRefundCent: int64(math.Round(depositRefund * 100)),
RenterRefundCent: int64(math.Round((rentRefund + depositRefund) * 100)),
PlatformFeeCent: int64(math.Round(maxMoney(actualRentAmount-ownerRentIncome, 0) * 100)),
ActualRentAmountCent: int64(math.Round(actualRentAmount * 100)),
OwnerRentIncomeCent: ownerRentIncomeCent,
DepositCompensationCent: depositCompensationCent,
OwnerIncomeCent: ownerRentIncomeCent + depositCompensationCent,
RentRefundCent: rentRefundCent,
DepositRefundCent: depositRefundCent,
RenterRefundCent: rentRefundCent + depositRefundCent,
PlatformFeeCent: maxCent(actualRentAmountCent-ownerRentIncomeCent, 0),
ActualRentAmountCent: actualRentAmountCent,
}
}
@@ -1381,6 +1362,20 @@ func maxMoney(a float64, b float64) float64 {
return money.Max(a, b)
}
func minCent(a int64, b int64) int64 {
if a < b {
return a
}
return b
}
func maxCent(a int64, b int64) int64 {
if a > b {
return a
}
return b
}
func minRatio(a float64, b float64) float64 {
if a < b {
return a
@@ -1522,13 +1517,6 @@ func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO {
}
}
func effectiveDepositOriginalAmount(order model.RentalOrder) float64 {
if order.DepositOriginalAmount > 0 {
return order.DepositOriginalAmount
}
return order.DepositAmount
}
func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 {
if order.DepositOriginalAmountCent > 0 {
return order.DepositOriginalAmountCent
@@ -1537,11 +1525,11 @@ func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 {
}
func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO {
rentAmountCent := int64(math.Round(checkout.RentAmount * 100))
ownerRentAmountCent := int64(math.Round(checkout.OwnerRentAmount * 100))
platformFeeCent := int64(math.Round(checkout.PlatformFee * 100))
renterRefundAmountCent := int64(math.Round(checkout.RenterRefundAmount * 100))
ownerIncomeAmountCent := int64(math.Round(checkout.OwnerIncomeAmount * 100))
rentAmountCent := checkout.RentAmountCent
ownerRentAmountCent := checkout.OwnerRentAmountCent
platformFeeCent := checkout.PlatformFeeCent
renterRefundAmountCent := checkout.RenterRefundAmountCent
ownerIncomeAmountCent := checkout.OwnerIncomeAmountCent
return CheckoutDTO{
ID: checkout.ID,
OrderID: checkout.OrderID,
@@ -1552,11 +1540,11 @@ func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO {
RentAmountCent: &rentAmountCent,
OwnerRentAmountCent: &ownerRentAmountCent,
PlatformFeeCent: &platformFeeCent,
DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)),
ConsumableAmountCent: int64(math.Round(checkout.ConsumableAmount * 100)),
DepositAmountCent: checkout.DepositAmountCent,
ConsumableAmountCent: checkout.ConsumableAmountCent,
CoinConsumedM: checkout.CoinConsumedM,
OtherAmountCent: int64(math.Round(checkout.OtherAmount * 100)),
DepositDeductAmountCent: int64(math.Round(checkout.DepositDeductAmount * 100)),
OtherAmountCent: checkout.OtherAmountCent,
DepositDeductAmountCent: checkout.DepositDeductAmountCent,
RenterRefundAmountCent: &renterRefundAmountCent,
OwnerIncomeAmountCent: &ownerIncomeAmountCent,
Content: checkout.Content,
@@ -27,7 +27,7 @@ func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) {
}`)),
}
settlement := calculateCheckoutSettlement(order, 7, 90, 0)
settlement := calculateCheckoutSettlement(order, 700, 90, 0)
// 角精度:243.7 = roundMoney(236.7 + 7)
if settlement.ActualRentAmountCent != 24370 {
@@ -104,7 +104,7 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) {
}`)),
}
settlement := calculateCheckoutSettlement(order, 120, 100, 30)
settlement := calculateCheckoutSettlement(order, 12000, 100, 3000)
if settlement.ActualRentAmountCent != 38300 {
t.Fatalf("ActualRentAmountCent = %d, want 38300", settlement.ActualRentAmountCent)
@@ -126,17 +126,17 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) {
func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) {
paid, waived := calculateDepositWaiver(500, 300, 0)
if paid != 200 || waived != 300 {
t.Fatalf("paid = %.1f, waived = %.1f, want 200.0, 300.0", paid, waived)
t.Fatalf("paid = %d, waived = %d, want 200, 300", paid, waived)
}
paid, waived = calculateDepositWaiver(500, 300, 200)
if paid != 400 || waived != 100 {
t.Fatalf("paid = %.1f, waived = %.1f, want 400.0, 100.0", paid, waived)
t.Fatalf("paid = %d, waived = %d, want 400, 100", paid, waived)
}
paid, waived = calculateDepositWaiver(500, 300, 300)
if paid != 500 || waived != 0 {
t.Fatalf("paid = %.1f, waived = %.1f, want 500.0, 0.0", paid, waived)
t.Fatalf("paid = %d, waived = %d, want 500, 0", paid, waived)
}
}
+3 -3
View File
@@ -12,9 +12,9 @@ type StartPaymentRequest struct {
}
type WalletRechargePaymentRequest struct {
Amount float64 `json:"amount"`
PayWay string `json:"pay_way"`
JSPayFlag string `json:"jspay_flag"`
AmountCent int64 `json:"amount_cent"`
PayWay string `json:"pay_way"`
JSPayFlag string `json:"jspay_flag"`
}
type PaymentDTO struct {
@@ -229,8 +229,8 @@ func (r *Repository) Start(userID uint64, orderID uint64, req StartPaymentReques
}
func (r *Repository) StartWalletRecharge(userID uint64, req WalletRechargePaymentRequest, clientIP string) (*PaymentDTO, error) {
amountCent := moneyCent(req.Amount)
if userID == 0 || req.Amount < MinWalletRechargeAmount || amountCent <= 0 {
amountCent := req.AmountCent
if userID == 0 || amountCent < moneyCent(MinWalletRechargeAmount) {
return nil, ErrPaymentCannotStart
}
runtimeConfig, err := r.defaultRuntimeConfig()
@@ -764,7 +764,7 @@ func (r *Repository) preparePayment(userID uint64, orderID uint64, req StartPaym
if row.Status != "pending_payment" {
return ErrPaymentCannotStart
}
amountCent := moneyCent(row.RentAmount + row.DepositAmount)
amountCent := row.RentAmountCent + row.DepositAmountCent
if amountCent <= 0 {
return ErrPaymentCannotStart
}
@@ -58,11 +58,8 @@ func (r *Repository) Create(userID uint64, req CreateWithdrawalRequest) (*Withdr
withdrawal := model.WithdrawalRequest{
WithdrawNo: withdrawNo,
UserID: userID,
Amount: float64(req.AmountCent) / 100,
AmountCent: req.AmountCent,
Fee: float64(feeCent) / 100,
FeeCent: feeCent,
ActualAmount: float64(actualAmountCent) / 100,
ActualAmountCent: actualAmountCent,
PaymentAccountID: &req.PaymentAccountID,
AccountType: paymentAccount.AccountType,