修复平台代管财务差异

This commit is contained in:
yml2213
2026-07-26 10:09:37 +08:00
parent 9ccc16e068
commit 3107c54b65
8 changed files with 358 additions and 189 deletions
@@ -80,9 +80,28 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*Financ
var settlement settlementSummaryRow var settlement settlementSummaryRow
if err := db.Table("rental_orders AS ro"). if err := db.Table("rental_orders AS ro").
Select(`COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, 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(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, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent,
COUNT(ro.id) AS settled_order_count`). COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE COALESCE(w.owner_wallet_income_amount_cent, 0)
END), 0) AS owner_effective_settlement_amount_cent,
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE 0
END), 0) AS offline_settlement_amount_cent,
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE 0
END), 0) AS offline_settlement_pending_amount_cent,
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending'
AND COALESCE(ro.offline_settlement_amount_cent, 0) > 0
THEN 1
ELSE 0
END), 0) AS offline_settlement_pending_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(db)). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(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).
@@ -96,7 +115,8 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*Financ
StartDate: query.StartDate, StartDate: query.StartDate,
EndDate: query.EndDate, EndDate: query.EndDate,
})). })).
Where("finance_status <> ?", "normal"). Where("finance_status <> ?", financeStatusNormal).
Where("finance_status <> ?", financeStatusOfflineSettlementPending).
Count(&exceptionCount).Error; err != nil { Count(&exceptionCount).Error; err != nil {
return nil, err return nil, err
} }
@@ -114,23 +134,27 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*Financ
} }
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,
PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent, PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent,
OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent, OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent,
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent, OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent, OwnerEffectiveSettlementAmountCent: settlement.OwnerEffectiveSettlementAmountCent,
EstimatedIncomeAmountCent: estimated.EstimatedIncomeAmountCent, OfflineSettlementAmountCent: settlement.OfflineSettlementAmountCent,
SuccessfulPayCount: payment.SuccessfulPayCount, OfflineSettlementPendingAmountCent: settlement.OfflineSettlementPendingAmountCent,
SuccessfulRefundCount: payment.FullRefundCount + payment.PartialRefundCount, SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerEffectiveSettlementAmountCent,
FullRefundCount: payment.FullRefundCount, EstimatedIncomeAmountCent: estimated.EstimatedIncomeAmountCent,
PartialRefundCount: payment.PartialRefundCount, SuccessfulPayCount: payment.SuccessfulPayCount,
PendingRefundCount: payment.PendingRefundCount, SuccessfulRefundCount: payment.FullRefundCount + payment.PartialRefundCount,
SettledOrderCount: settlement.SettledOrderCount, FullRefundCount: payment.FullRefundCount,
PendingSettleOrderCount: estimated.PendingSettleOrderCount, PartialRefundCount: payment.PartialRefundCount,
FinancialExceptionCount: exceptionCount, PendingRefundCount: payment.PendingRefundCount,
SettledOrderCount: settlement.SettledOrderCount,
PendingSettleOrderCount: estimated.PendingSettleOrderCount,
OfflineSettlementPendingCount: settlement.OfflineSettlementPendingCount,
FinancialExceptionCount: exceptionCount,
}, nil }, nil
} }
@@ -162,10 +186,29 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
settlements := make([]dailySettlementRow, 0) settlements := make([]dailySettlementRow, 0)
if err := db.Table("rental_orders AS ro"). if err := 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_cent), 0) AS platform_income_amount_cent, 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(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, COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent,
COUNT(ro.id) AS settled_order_count`). COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE COALESCE(w.owner_wallet_income_amount_cent, 0)
END), 0) AS owner_effective_settlement_amount_cent,
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE 0
END), 0) AS offline_settlement_amount_cent,
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE 0
END), 0) AS offline_settlement_pending_amount_cent,
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending'
AND COALESCE(ro.offline_settlement_amount_cent, 0) > 0
THEN 1
ELSE 0
END), 0) AS offline_settlement_pending_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(db)). Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(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).
@@ -214,8 +257,12 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent
item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent
item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent
item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent item.OwnerEffectiveSettlementAmountCent = row.OwnerEffectiveSettlementAmountCent
item.OfflineSettlementAmountCent = row.OfflineSettlementAmountCent
item.OfflineSettlementPendingAmountCent = row.OfflineSettlementPendingAmountCent
item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerEffectiveSettlementAmountCent
item.SettledOrderCount = row.SettledOrderCount item.SettledOrderCount = row.SettledOrderCount
item.OfflineSettlementPendingCount = row.OfflineSettlementPendingCount
itemsByDate[date] = item itemsByDate[date] = item
} }
for _, row := range estimates { for _, row := range estimates {
@@ -244,10 +291,14 @@ type paymentSummaryRow struct {
} }
type settlementSummaryRow struct { type settlementSummaryRow struct {
PlatformIncomeAmountCent int64 PlatformIncomeAmountCent int64
OwnerShouldIncomeAmountCent int64 OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64 OwnerWalletIncomeAmountCent int64
SettledOrderCount int64 OwnerEffectiveSettlementAmountCent int64
OfflineSettlementAmountCent int64
OfflineSettlementPendingAmountCent int64
OfflineSettlementPendingCount int64
SettledOrderCount int64
} }
type estimatedIncomeRow struct { type estimatedIncomeRow struct {
@@ -267,11 +318,15 @@ type dailyPaymentRow struct {
} }
type dailySettlementRow struct { type dailySettlementRow struct {
Date string Date string
PlatformIncomeAmountCent int64 PlatformIncomeAmountCent int64
OwnerShouldIncomeAmountCent int64 OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64 OwnerWalletIncomeAmountCent int64
SettledOrderCount int64 OwnerEffectiveSettlementAmountCent int64
OfflineSettlementAmountCent int64
OfflineSettlementPendingAmountCent int64
OfflineSettlementPendingCount int64
SettledOrderCount int64
} }
type dailyEstimatedRow struct { type dailyEstimatedRow struct {
+41 -22
View File
@@ -61,28 +61,47 @@ func (r *Repository) applyDetailFilters(db *gorm.DB, query DetailQuery) *gorm.DB
func (r *Repository) financeDetailBaseQuery(ctx context.Context, query DetailQuery) *gorm.DB { func (r *Repository) financeDetailBaseQuery(ctx context.Context, query DetailQuery) *gorm.DB {
db := r.db.WithContext(ctx).Table("rental_orders AS ro"). db := r.db.WithContext(ctx).Table("rental_orders AS ro").
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.handoff_mode, ro.settlement_mode,
ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname, COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') AS offline_settlement_status,
ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent, COALESCE(ro.offline_settlement_amount_cent, 0) AS offline_settlement_amount_cent,
COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent, ro.renter_id, COALESCE(ru.phone, '') AS renter_phone, COALESCE(ru.nickname, '') AS renter_nickname,
COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent, ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname,
COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent, ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent,
COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent, COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent,
COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent, COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent,
COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent, COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent,
COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent,
COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent, COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent,
COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent, CASE WHEN ro.settlement_mode = 'platform_managed'
COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent, THEN COALESCE(ro.offline_settlement_amount_cent, 0)
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, ELSE COALESCE(w.owner_wallet_income_amount_cent, 0)
COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent, END AS owner_effective_settlement_amount_cent,
CASE COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent,
WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent,
WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending' COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent,
WHEN ABS(COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0)) >= ? THEN 'settlement_diff' COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent,
ELSE 'normal' COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent,
END AS finance_status, COALESCE(oc.platform_fee_cent, 0) + (COALESCE(oc.owner_income_amount_cent, 0) - CASE WHEN ro.settlement_mode = 'platform_managed'
ro.created_at, ro.settled_at`, settlementDiffThresholdCent). THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE COALESCE(w.owner_wallet_income_amount_cent, 0)
END) AS platform_net_amount_cent,
COALESCE(oc.owner_income_amount_cent, 0) - CASE WHEN ro.settlement_mode = 'platform_managed'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE COALESCE(w.owner_wallet_income_amount_cent, 0)
END 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) - CASE WHEN ro.settlement_mode = 'platform_managed'
THEN COALESCE(ro.offline_settlement_amount_cent, 0)
ELSE COALESCE(w.owner_wallet_income_amount_cent, 0)
END) >= ? THEN 'settlement_diff'
WHEN ro.settlement_mode = 'platform_managed'
AND COALESCE(ro.offline_settlement_amount_cent, 0) > 0
AND COALESCE(NULLIF(ro.offline_settlement_status, ''), 'none') = 'pending' THEN 'offline_settlement_pending'
ELSE 'normal'
END AS finance_status,
ro.created_at, ro.settled_at, ro.offline_settled_at`, settlementDiffThresholdCent).
Joins("LEFT JOIN users AS ru ON ru.id = ro.renter_id"). Joins("LEFT JOIN users AS ru ON ru.id = ro.renter_id").
Joins("LEFT JOIN users AS ou ON ou.id = ro.owner_id"). Joins("LEFT JOIN users AS ou ON ou.id = ro.owner_id").
Joins("LEFT JOIN (?) AS oc ON oc.order_id = ro.id", acceptedCheckoutSubquery(r.db.WithContext(ctx))). Joins("LEFT JOIN (?) AS oc ON oc.order_id = ro.id", acceptedCheckoutSubquery(r.db.WithContext(ctx))).
+75 -61
View File
@@ -35,42 +35,50 @@ type PickupSummaryDTO 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"`
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"` PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"` OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"` OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"` OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
EstimatedIncomeAmountCent int64 `json:"estimated_income_amount_cent"` OfflineSettlementAmountCent int64 `json:"offline_settlement_amount_cent"`
SuccessfulPayCount int64 `json:"successful_pay_count"` OfflineSettlementPendingAmountCent int64 `json:"offline_settlement_pending_amount_cent"`
SuccessfulRefundCount int64 `json:"successful_refund_count"` SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
FullRefundCount int64 `json:"full_refund_count"` EstimatedIncomeAmountCent int64 `json:"estimated_income_amount_cent"`
PartialRefundCount int64 `json:"partial_refund_count"` SuccessfulPayCount int64 `json:"successful_pay_count"`
PendingRefundCount int64 `json:"pending_refund_count"` SuccessfulRefundCount int64 `json:"successful_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"` FullRefundCount int64 `json:"full_refund_count"`
PendingSettleOrderCount int64 `json:"pending_settle_order_count"` PartialRefundCount int64 `json:"partial_refund_count"`
FinancialExceptionCount int64 `json:"financial_exception_count"` PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
PendingSettleOrderCount int64 `json:"pending_settle_order_count"`
OfflineSettlementPendingCount int64 `json:"offline_settlement_pending_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"`
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"` PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"` OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"` OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"` OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
EstimatedIncomeAmountCent int64 `json:"estimated_income_amount_cent"` OfflineSettlementAmountCent int64 `json:"offline_settlement_amount_cent"`
SuccessfulPayCount int64 `json:"successful_pay_count"` OfflineSettlementPendingAmountCent int64 `json:"offline_settlement_pending_amount_cent"`
SuccessfulRefundCount int64 `json:"successful_refund_count"` SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"`
FullRefundCount int64 `json:"full_refund_count"` EstimatedIncomeAmountCent int64 `json:"estimated_income_amount_cent"`
PartialRefundCount int64 `json:"partial_refund_count"` SuccessfulPayCount int64 `json:"successful_pay_count"`
PendingRefundCount int64 `json:"pending_refund_count"` SuccessfulRefundCount int64 `json:"successful_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"` FullRefundCount int64 `json:"full_refund_count"`
PartialRefundCount int64 `json:"partial_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
OfflineSettlementPendingCount int64 `json:"offline_settlement_pending_count"`
} }
type PaginatedResult struct { type PaginatedResult struct {
@@ -81,32 +89,38 @@ 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"` HandoffMode string `json:"handoff_mode"`
RenterPhone string `json:"renter_phone"` SettlementMode string `json:"settlement_mode"`
RenterNickname string `json:"renter_nickname"` OfflineSettlementStatus string `json:"offline_settlement_status"`
OwnerID uint64 `json:"owner_id"` OfflineSettlementAmountCent int64 `json:"offline_settlement_amount_cent"`
OwnerPhone string `json:"owner_phone"` RenterID uint64 `json:"renter_id"`
OwnerNickname string `json:"owner_nickname"` RenterPhone string `json:"renter_phone"`
OrderRentAmountCent int64 `json:"order_rent_amount_cent"` RenterNickname string `json:"renter_nickname"`
OrderDepositAmountCent int64 `json:"order_deposit_amount_cent"` OwnerID uint64 `json:"owner_id"`
CheckoutRentAmountCent int64 `json:"checkout_rent_amount_cent"` OwnerPhone string `json:"owner_phone"`
CheckoutRenterRefundCent int64 `json:"checkout_renter_refund_cent"` OwnerNickname string `json:"owner_nickname"`
CheckoutOwnerIncomeCent int64 `json:"checkout_owner_income_cent"` OrderRentAmountCent int64 `json:"order_rent_amount_cent"`
CheckoutPlatformFeeCent int64 `json:"checkout_platform_fee_cent"` OrderDepositAmountCent int64 `json:"order_deposit_amount_cent"`
OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"` CheckoutRentAmountCent int64 `json:"checkout_rent_amount_cent"`
PaidAmountCent int64 `json:"paid_amount_cent"` CheckoutRenterRefundCent int64 `json:"checkout_renter_refund_cent"`
RefundedAmountCent int64 `json:"refunded_amount_cent"` CheckoutOwnerIncomeCent int64 `json:"checkout_owner_income_cent"`
RefundingAmountCent int64 `json:"refunding_amount_cent"` CheckoutPlatformFeeCent int64 `json:"checkout_platform_fee_cent"`
FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"` OwnerWalletIncomeAmountCent int64 `json:"owner_wallet_income_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"` OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
PlatformNetAmountCent int64 `json:"platform_net_amount_cent"` PaidAmountCent int64 `json:"paid_amount_cent"`
SettlementDiffAmountCent int64 `json:"settlement_diff_amount_cent"` RefundedAmountCent int64 `json:"refunded_amount_cent"`
FinanceStatus string `json:"finance_status"` RefundingAmountCent int64 `json:"refunding_amount_cent"`
CreatedAt time.Time `json:"created_at"` FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"`
SettledAt *time.Time `json:"settled_at,omitempty"` 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"`
OfflineSettledAt *time.Time `json:"offline_settled_at,omitempty"`
} }
@@ -12,6 +12,14 @@ import (
// SQL、presenter、前端展示统一引用此口径,避免多处硬编码不一致。 // SQL、presenter、前端展示统一引用此口径,避免多处硬编码不一致。
const settlementDiffThresholdCent = 5 const settlementDiffThresholdCent = 5
const (
financeStatusNormal = "normal"
financeStatusRefundPending = "refund_pending"
financeStatusRefundFailed = "refund_failed"
financeStatusSettlementDiff = "settlement_diff"
financeStatusOfflineSettlementPending = "offline_settlement_pending"
)
func refundBizTypes() []string { func refundBizTypes() []string {
return []string{ return []string{
"cancel_refund", "cancel_refund",
@@ -5,73 +5,85 @@ import (
) )
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 HandoffMode string
RenterPhone string SettlementMode string
RenterNickname string OfflineSettlementStatus string
OwnerID uint64 OfflineSettlementAmountCent int64
OwnerPhone string RenterID uint64
OwnerNickname string RenterPhone string
OrderRentAmountCent int64 RenterNickname string
OrderDepositAmountCent int64 OwnerID uint64
CheckoutRentAmountCent int64 OwnerPhone string
CheckoutRenterRefundCent int64 OwnerNickname string
CheckoutOwnerIncomeCent int64 OrderRentAmountCent int64
CheckoutPlatformFeeCent int64 OrderDepositAmountCent int64
OwnerWalletIncomeAmountCent int64 CheckoutRentAmountCent int64
PaidAmountCent int64 CheckoutRenterRefundCent int64
RefundedAmountCent int64 CheckoutOwnerIncomeCent int64
RefundingAmountCent int64 CheckoutPlatformFeeCent int64
FailedRefundAmountCent int64 OwnerWalletIncomeAmountCent int64
ChannelNetAmountCent int64 OwnerEffectiveSettlementAmountCent int64
PlatformNetAmountCent int64 PaidAmountCent int64
SettlementDiffAmountCent int64 RefundedAmountCent int64
FinanceStatus string RefundingAmountCent int64
CreatedAt time.Time FailedRefundAmountCent int64
SettledAt *time.Time ChannelNetAmountCent int64
PlatformNetAmountCent int64
SettlementDiffAmountCent int64
FinanceStatus string
CreatedAt time.Time
SettledAt *time.Time
OfflineSettledAt *time.Time
} }
func (r financeDetailRow) toDTO() FinanceDetailDTO { func (r financeDetailRow) toDTO() FinanceDetailDTO {
diff := r.SettlementDiffAmountCent diff := r.SettlementDiffAmountCent
status := r.FinanceStatus status := r.FinanceStatus
if status == "" { if status == "" {
status = "normal" status = financeStatusNormal
} }
if absCent(diff) < settlementDiffThresholdCent && status == "settlement_diff" { if absCent(diff) < settlementDiffThresholdCent && status == financeStatusSettlementDiff {
status = "normal" status = financeStatusNormal
} }
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, HandoffMode: r.HandoffMode,
RenterPhone: r.RenterPhone, SettlementMode: r.SettlementMode,
RenterNickname: r.RenterNickname, OfflineSettlementStatus: r.OfflineSettlementStatus,
OwnerID: r.OwnerID, OfflineSettlementAmountCent: r.OfflineSettlementAmountCent,
OwnerPhone: r.OwnerPhone, RenterID: r.RenterID,
OwnerNickname: r.OwnerNickname, RenterPhone: r.RenterPhone,
OrderRentAmountCent: r.OrderRentAmountCent, RenterNickname: r.RenterNickname,
OrderDepositAmountCent: r.OrderDepositAmountCent, OwnerID: r.OwnerID,
CheckoutRentAmountCent: r.CheckoutRentAmountCent, OwnerPhone: r.OwnerPhone,
CheckoutRenterRefundCent: r.CheckoutRenterRefundCent, OwnerNickname: r.OwnerNickname,
CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent, OrderRentAmountCent: r.OrderRentAmountCent,
CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent, OrderDepositAmountCent: r.OrderDepositAmountCent,
OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent, CheckoutRentAmountCent: r.CheckoutRentAmountCent,
PaidAmountCent: r.PaidAmountCent, CheckoutRenterRefundCent: r.CheckoutRenterRefundCent,
RefundedAmountCent: r.RefundedAmountCent, CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent,
RefundingAmountCent: r.RefundingAmountCent, CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent,
FailedRefundAmountCent: r.FailedRefundAmountCent, OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent,
ChannelNetAmountCent: r.ChannelNetAmountCent, OwnerEffectiveSettlementAmountCent: r.OwnerEffectiveSettlementAmountCent,
PlatformNetAmountCent: r.PlatformNetAmountCent, PaidAmountCent: r.PaidAmountCent,
SettlementDiffAmountCent: diff, RefundedAmountCent: r.RefundedAmountCent,
FinanceStatus: status, RefundingAmountCent: r.RefundingAmountCent,
CreatedAt: r.CreatedAt, FailedRefundAmountCent: r.FailedRefundAmountCent,
SettledAt: r.SettledAt, ChannelNetAmountCent: r.ChannelNetAmountCent,
PlatformNetAmountCent: r.PlatformNetAmountCent,
SettlementDiffAmountCent: diff,
FinanceStatus: status,
CreatedAt: r.CreatedAt,
SettledAt: r.SettledAt,
OfflineSettledAt: r.OfflineSettledAt,
} }
} }
@@ -1,7 +1,7 @@
import { apiClient } from '@/shared/api/client' import { apiClient } from '@/shared/api/client'
import type { ApiResponse, PaginatedResult } from '@/shared/types/types' import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
// 结算差异判定阈值(分)。号主应得与实际入账差额绝对值达到此值即标红。 // 结算差异判定阈值(分)。号主应得与钱包入账/线下结算差额绝对值达到此值即标红。
// 与后端 settlementDiffThresholdCent 保持一致,改口径时两端同步。 // 与后端 settlementDiffThresholdCent 保持一致,改口径时两端同步。
export const SETTLEMENT_DIFF_THRESHOLD_CENT = 5 export const SETTLEMENT_DIFF_THRESHOLD_CENT = 5
@@ -13,6 +13,9 @@ export interface FinanceSummary {
platform_income_amount_cent: number platform_income_amount_cent: number
owner_should_income_amount_cent: number owner_should_income_amount_cent: number
owner_wallet_income_amount_cent: number owner_wallet_income_amount_cent: number
owner_effective_settlement_amount_cent: number
offline_settlement_amount_cent: number
offline_settlement_pending_amount_cent: number
settlement_diff_amount_cent: number settlement_diff_amount_cent: number
estimated_income_amount_cent: number estimated_income_amount_cent: number
successful_pay_count: number successful_pay_count: number
@@ -22,6 +25,7 @@ export interface FinanceSummary {
pending_refund_count: number pending_refund_count: number
settled_order_count: number settled_order_count: number
pending_settle_order_count: number pending_settle_order_count: number
offline_settlement_pending_count: number
financial_exception_count: number financial_exception_count: number
} }
@@ -34,6 +38,9 @@ export interface FinanceDailyItem {
platform_income_amount_cent: number platform_income_amount_cent: number
owner_should_income_amount_cent: number owner_should_income_amount_cent: number
owner_wallet_income_amount_cent: number owner_wallet_income_amount_cent: number
owner_effective_settlement_amount_cent: number
offline_settlement_amount_cent: number
offline_settlement_pending_amount_cent: number
settlement_diff_amount_cent: number settlement_diff_amount_cent: number
estimated_income_amount_cent: number estimated_income_amount_cent: number
successful_pay_count: number successful_pay_count: number
@@ -42,6 +49,7 @@ export interface FinanceDailyItem {
partial_refund_count: number partial_refund_count: number
pending_refund_count: number pending_refund_count: number
settled_order_count: number settled_order_count: number
offline_settlement_pending_count: number
} }
export interface FinancePickupSummary { export interface FinancePickupSummary {
@@ -64,6 +72,10 @@ export interface FinanceDetail {
order_status: string order_status: string
settlement_status: string settlement_status: string
refund_status: string refund_status: string
handoff_mode: string
settlement_mode: string
offline_settlement_status: string
offline_settlement_amount_cent: number
renter_id: number renter_id: number
renter_phone: string renter_phone: string
renter_nickname: string renter_nickname: string
@@ -77,6 +89,7 @@ export interface FinanceDetail {
checkout_owner_income_cent: number checkout_owner_income_cent: number
checkout_platform_fee_cent: number checkout_platform_fee_cent: number
owner_wallet_income_amount_cent: number owner_wallet_income_amount_cent: number
owner_effective_settlement_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
@@ -87,6 +100,7 @@ export interface FinanceDetail {
finance_status: string finance_status: string
created_at: string created_at: string
settled_at?: string settled_at?: string
offline_settled_at?: string
} }
export interface FinanceDateQuery { export interface FinanceDateQuery {
@@ -54,6 +54,10 @@ function rowDiffClass(row: FinanceDailyItem) {
? 'amount-danger' ? 'amount-danger'
: '' : ''
} }
function rowOfflinePendingClass(row: FinanceDailyItem) {
return Number(row.offline_settlement_pending_amount_cent || 0) > 0 ? 'amount-warning' : ''
}
</script> </script>
<template> <template>
@@ -62,7 +66,7 @@ function rowDiffClass(row: FinanceDailyItem) {
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Finance Dashboard</p> <p class="eyebrow">Finance Dashboard</p>
<h1>财务仪表盘</h1> <h1>财务仪表盘</h1>
<p>按天查看第三方收款退款平台收入号主入账和结算差异</p> <p>按天查看第三方收款退款平台收入号主钱包入账/线下结算和结算差异</p>
</div> </div>
<div class="toolbar-actions"> <div class="toolbar-actions">
<el-date-picker <el-date-picker
@@ -114,9 +118,14 @@ function rowDiffClass(row: FinanceDailyItem) {
<small>结账单口径</small> <small>结账单口径</small>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>号主实际入账</span> <span>号主结算金额</span>
<strong>{{ moneyCent(dashboard.summary.owner_wallet_income_amount_cent) }}</strong> <strong>{{ moneyCent(dashboard.summary.owner_effective_settlement_amount_cent) }}</strong>
<small>钱包流水口径</small> <small>钱包入账 + 平台代管线下结算</small>
</div>
<div class="metric-card">
<span>待线下结算</span>
<strong>{{ moneyCent(dashboard.summary.offline_settlement_pending_amount_cent) }}</strong>
<small>{{ dashboard.summary.offline_settlement_pending_count }} 笔平台代管待处理</small>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>退款中</span> <span>退款中</span>
@@ -174,8 +183,17 @@ function rowDiffClass(row: FinanceDailyItem) {
<el-table-column label="号主应得" width="130"> <el-table-column label="号主应得" width="130">
<template #default="{ row }">{{ moneyCent(row.owner_should_income_amount_cent) }}</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 }">{{ moneyCent(row.owner_wallet_income_amount_cent) }}</template> <template #default="{ row }">{{
moneyCent(row.owner_effective_settlement_amount_cent)
}}</template>
</el-table-column>
<el-table-column label="待线下" width="130">
<template #default="{ row }">
<span :class="rowOfflinePendingClass(row)">
{{ moneyCent(row.offline_settlement_pending_amount_cent) }}
</span>
</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 }">
@@ -211,6 +229,11 @@ function rowDiffClass(row: FinanceDailyItem) {
font-weight: 700; font-weight: 700;
} }
.amount-warning {
color: #d97706;
font-weight: 700;
}
.pickup-grid { .pickup-grid {
margin-top: 12px; margin-top: 12px;
} }
@@ -71,6 +71,7 @@ function financeStatusLabel(status: string) {
refund_pending: '退款中', refund_pending: '退款中',
refund_failed: '退款失败', refund_failed: '退款失败',
settlement_diff: '结算差异', settlement_diff: '结算差异',
offline_settlement_pending: '待线下结算',
} }
return map[status] || status return map[status] || status
} }
@@ -78,7 +79,7 @@ function financeStatusLabel(status: string) {
function financeStatusType(status: string) { function financeStatusType(status: string) {
if (status === 'normal') return 'success' if (status === 'normal') return 'success'
if (status === 'settlement_diff' || status === 'refund_failed') return 'danger' if (status === 'settlement_diff' || status === 'refund_failed') return 'danger'
if (status === 'refund_pending') return 'warning' if (status === 'refund_pending' || status === 'offline_settlement_pending') return 'warning'
return 'info' return 'info'
} }
@@ -91,6 +92,23 @@ function settlementStatusLabel(status: string) {
return map[status] || status || '-' return map[status] || status || '-'
} }
function isPlatformManaged(row: FinanceDetail) {
return row.handoff_mode === 'platform' || row.settlement_mode === 'platform_managed'
}
function settlementModeLabel(row: FinanceDetail) {
return isPlatformManaged(row) ? '平台代管' : '号主钱包'
}
function offlineSettlementStatusLabel(status: string) {
const map: Record<string, string> = {
none: '无需线下结算',
pending: '待线下结算',
settled: '已线下结算',
}
return map[status] || status || '-'
}
function shortDateTime(value?: string) { function shortDateTime(value?: string) {
if (!value) return '-' if (!value) return '-'
const text = formatDateTime(value) const text = formatDateTime(value)
@@ -114,7 +132,7 @@ function defaultEndDate() {
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Finance Details</p> <p class="eyebrow">Finance Details</p>
<h1>财务明细</h1> <h1>财务明细</h1>
<p>按订单核对收款退款平台收入号主应得与钱包实际入账</p> <p>按订单核对收款退款平台收入号主应得与钱包入账/线下结算</p>
</div> </div>
<div class="toolbar-actions"> <div class="toolbar-actions">
<el-button @click="resetFilters">重置</el-button> <el-button @click="resetFilters">重置</el-button>
@@ -191,7 +209,7 @@ function defaultEndDate() {
<div class="amount-cell">净入</div> <div class="amount-cell">净入</div>
<div class="amount-cell">平台</div> <div class="amount-cell">平台</div>
<div class="amount-cell">应得</div> <div class="amount-cell">应得</div>
<div class="amount-cell">入账</div> <div class="amount-cell">入账/线下</div>
<div class="amount-cell">差异</div> <div class="amount-cell">差异</div>
</div> </div>
@@ -206,7 +224,13 @@ function defaultEndDate() {
<div class="status-stack"> <div class="status-stack">
<span>{{ orderStatusLabel(row.order_status) }}</span> <span>{{ orderStatusLabel(row.order_status) }}</span>
<small>{{ settlementStatusLabel(row.settlement_status) }}</small> <small
>{{ settlementStatusLabel(row.settlement_status) }} ·
{{ settlementModeLabel(row) }}</small
>
<small v-if="isPlatformManaged(row)">
{{ offlineSettlementStatusLabel(row.offline_settlement_status) }}
</small>
<el-tag size="small" :type="financeStatusType(row.finance_status)"> <el-tag size="small" :type="financeStatusType(row.finance_status)">
{{ financeStatusLabel(row.finance_status) }} {{ financeStatusLabel(row.finance_status) }}
</el-tag> </el-tag>
@@ -232,7 +256,7 @@ function defaultEndDate() {
moneyCent(row.checkout_owner_income_cent) moneyCent(row.checkout_owner_income_cent)
}}</span> }}</span>
<span class="amount-text amount-cell">{{ <span class="amount-text amount-cell">{{
moneyCent(row.owner_wallet_income_amount_cent) moneyCent(row.owner_effective_settlement_amount_cent)
}}</span> }}</span>
<span <span
class="amount-text amount-cell" class="amount-text amount-cell"