feat(finance): include pickup profit in dashboard
This commit is contained in:
@@ -11,11 +11,15 @@ func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*Dash
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
summary, err := r.summary(ctx, query)
|
pickup, err := r.pickupSummary(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
pickup, err := r.pickupSummary(ctx, query)
|
summary, err := r.summary(ctx, query, pickup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mohong, err := r.mohongSummary(ctx, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -27,6 +31,7 @@ func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*Dash
|
|||||||
Summary: *summary,
|
Summary: *summary,
|
||||||
DailyItems: dailyItems,
|
DailyItems: dailyItems,
|
||||||
PickupSummary: *pickup,
|
PickupSummary: *pickup,
|
||||||
|
MohongSummary: *mohong,
|
||||||
DisbursementSummary: *disbursement,
|
DisbursementSummary: *disbursement,
|
||||||
GeneratedAt: timeutil.ShanghaiNow(),
|
GeneratedAt: timeutil.ShanghaiNow(),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -59,7 +64,31 @@ func (r *Repository) pickupSummary(ctx context.Context, query DashboardQuery) (*
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*FinanceSummaryDTO, error) {
|
// mohongSummary 撞车商城按支付订单统计交易流水,不计入平台利润。
|
||||||
|
func (r *Repository) mohongSummary(ctx context.Context, query DashboardQuery) (*MohongSummaryDTO, error) {
|
||||||
|
db := r.db.WithContext(ctx)
|
||||||
|
var row struct {
|
||||||
|
FlowAmountCent int64
|
||||||
|
RefundAmountCent int64
|
||||||
|
PaidOrderCount int64
|
||||||
|
}
|
||||||
|
if err := db.Table("payment_orders").
|
||||||
|
Select(`COALESCE(SUM(CASE WHEN biz_type = 'mohong_pay' AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS flow_amount_cent,
|
||||||
|
COALESCE(SUM(CASE WHEN biz_type = 'mohong_refund' AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS refund_amount_cent,
|
||||||
|
COALESCE(SUM(CASE WHEN biz_type = 'mohong_pay' AND status = 'paid' THEN 1 ELSE 0 END), 0) AS paid_order_count`).
|
||||||
|
Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Scan(&row).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &MohongSummaryDTO{
|
||||||
|
FlowAmountCent: row.FlowAmountCent,
|
||||||
|
RefundAmountCent: row.RefundAmountCent,
|
||||||
|
NetAmountCent: row.FlowAmountCent - row.RefundAmountCent,
|
||||||
|
PaidOrderCount: row.PaidOrderCount,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) summary(ctx context.Context, query DashboardQuery, pickup *PickupSummaryDTO) (*FinanceSummaryDTO, error) {
|
||||||
db := r.db.WithContext(ctx)
|
db := r.db.WithContext(ctx)
|
||||||
var payment paymentSummaryRow
|
var payment paymentSummaryRow
|
||||||
if err := db.Table("payment_orders AS po").
|
if err := db.Table("payment_orders AS po").
|
||||||
@@ -138,12 +167,18 @@ func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*Financ
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pickupProfitAmountCent := int64(0)
|
||||||
|
if pickup != nil {
|
||||||
|
pickupProfitAmountCent = pickup.ProfitAmountCent
|
||||||
|
}
|
||||||
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 + pickupProfitAmountCent,
|
||||||
|
NormalOrderProfitAmountCent: settlement.PlatformIncomeAmountCent,
|
||||||
|
PickupProfitAmountCent: pickupProfitAmountCent,
|
||||||
OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent,
|
OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent,
|
||||||
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
|
OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent,
|
||||||
OwnerEffectiveSettlementAmountCent: settlement.OwnerEffectiveSettlementAmountCent,
|
OwnerEffectiveSettlementAmountCent: settlement.OwnerEffectiveSettlementAmountCent,
|
||||||
@@ -222,6 +257,17 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pickupProfits := make([]dailyPickupProfitRow, 0)
|
||||||
|
if err := db.Table("admin_pickups").
|
||||||
|
Select(`DATE(completed_at) AS date,
|
||||||
|
COALESCE(SUM(profit_amount_cent), 0) AS profit_amount_cent`).
|
||||||
|
Where("status = ?", "completed").
|
||||||
|
Where("completed_at >= ? AND completed_at <= ?", query.StartDate, query.EndDate).
|
||||||
|
Group("DATE(completed_at)").
|
||||||
|
Scan(&pickupProfits).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// 每日预计收入:尚未结算订单的下单预估平台手续费,按下单日期归集,口径与 summary 一致。
|
// 每日预计收入:尚未结算订单的下单预估平台手续费,按下单日期归集,口径与 summary 一致。
|
||||||
estimates := make([]dailyEstimatedRow, 0)
|
estimates := make([]dailyEstimatedRow, 0)
|
||||||
if err := db.Table("rental_orders").
|
if err := db.Table("rental_orders").
|
||||||
@@ -275,6 +321,13 @@ func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]Fi
|
|||||||
item.OfflineSettlementPendingCount = row.OfflineSettlementPendingCount
|
item.OfflineSettlementPendingCount = row.OfflineSettlementPendingCount
|
||||||
itemsByDate[date] = item
|
itemsByDate[date] = item
|
||||||
}
|
}
|
||||||
|
for _, row := range pickupProfits {
|
||||||
|
date := dailyDateKey(row.Date)
|
||||||
|
item := itemsByDate[date]
|
||||||
|
item.Date = date
|
||||||
|
item.PlatformIncomeAmountCent += row.ProfitAmountCent
|
||||||
|
itemsByDate[date] = item
|
||||||
|
}
|
||||||
for _, row := range estimates {
|
for _, row := range estimates {
|
||||||
date := dailyDateKey(row.Date)
|
date := dailyDateKey(row.Date)
|
||||||
item := itemsByDate[date]
|
item := itemsByDate[date]
|
||||||
@@ -353,6 +406,11 @@ type dailySettlementRow struct {
|
|||||||
SettledOrderCount int64
|
SettledOrderCount int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dailyPickupProfitRow struct {
|
||||||
|
Date string
|
||||||
|
ProfitAmountCent int64
|
||||||
|
}
|
||||||
|
|
||||||
type dailyEstimatedRow struct {
|
type dailyEstimatedRow struct {
|
||||||
Date string
|
Date string
|
||||||
EstimatedIncomeAmountCent int64
|
EstimatedIncomeAmountCent int64
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type DashboardDTO struct {
|
|||||||
Summary FinanceSummaryDTO `json:"summary"`
|
Summary FinanceSummaryDTO `json:"summary"`
|
||||||
DailyItems []FinanceDailyDTO `json:"daily_items"`
|
DailyItems []FinanceDailyDTO `json:"daily_items"`
|
||||||
PickupSummary PickupSummaryDTO `json:"pickup_summary"`
|
PickupSummary PickupSummaryDTO `json:"pickup_summary"`
|
||||||
|
MohongSummary MohongSummaryDTO `json:"mohong_summary"`
|
||||||
DisbursementSummary DisbursementSummaryDTO `json:"disbursement_summary"`
|
DisbursementSummary DisbursementSummaryDTO `json:"disbursement_summary"`
|
||||||
GeneratedAt time.Time `json:"generated_at"`
|
GeneratedAt time.Time `json:"generated_at"`
|
||||||
}
|
}
|
||||||
@@ -50,6 +51,14 @@ type PickupSummaryDTO struct {
|
|||||||
InProgressCount int64 `json:"in_progress_count"` // 当前提号中笔数(不按时间)
|
InProgressCount int64 `json:"in_progress_count"` // 当前提号中笔数(不按时间)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MohongSummaryDTO 撞车商城的交易流水,单独展示,不计入平台利润。
|
||||||
|
type MohongSummaryDTO struct {
|
||||||
|
FlowAmountCent int64 `json:"flow_amount_cent"`
|
||||||
|
RefundAmountCent int64 `json:"refund_amount_cent"`
|
||||||
|
NetAmountCent int64 `json:"net_amount_cent"`
|
||||||
|
PaidOrderCount int64 `json:"paid_order_count"`
|
||||||
|
}
|
||||||
|
|
||||||
// DisbursementSummaryDTO 统计平台实际线下出款及当前待处理金额。
|
// DisbursementSummaryDTO 统计平台实际线下出款及当前待处理金额。
|
||||||
// 已打款按确认打款时间归入查询区间,待处理不受日期范围限制,反映当前资金待办。
|
// 已打款按确认打款时间归入查询区间,待处理不受日期范围限制,反映当前资金待办。
|
||||||
type DisbursementSummaryDTO struct {
|
type DisbursementSummaryDTO struct {
|
||||||
@@ -158,11 +167,15 @@ type ManualDisbursementDTO 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 为正常订单最终结账利润与已完成线下提号利润之和。
|
||||||
|
// 保留原 JSON 字段名,避免影响已接入的管理端客户端。
|
||||||
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
|
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
|
||||||
|
NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"`
|
||||||
|
PickupProfitAmountCent int64 `json:"pickup_profit_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"`
|
||||||
OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
|
OwnerEffectiveSettlementAmountCent int64 `json:"owner_effective_settlement_amount_cent"`
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface FinanceSummary {
|
|||||||
pending_refund_amount_cent: number
|
pending_refund_amount_cent: number
|
||||||
channel_net_amount_cent: number
|
channel_net_amount_cent: number
|
||||||
platform_income_amount_cent: number
|
platform_income_amount_cent: number
|
||||||
|
normal_order_profit_amount_cent: number
|
||||||
|
pickup_profit_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
|
owner_effective_settlement_amount_cent: number
|
||||||
@@ -67,6 +69,13 @@ export interface FinancePickupSummary {
|
|||||||
in_progress_count: number
|
in_progress_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FinanceMohongSummary {
|
||||||
|
flow_amount_cent: number
|
||||||
|
refund_amount_cent: number
|
||||||
|
net_amount_cent: number
|
||||||
|
paid_order_count: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface FinanceDisbursementSummary {
|
export interface FinanceDisbursementSummary {
|
||||||
paid_amount_cent: number
|
paid_amount_cent: number
|
||||||
paid_count: number
|
paid_count: number
|
||||||
@@ -183,6 +192,7 @@ export interface FinanceDashboard {
|
|||||||
summary: FinanceSummary
|
summary: FinanceSummary
|
||||||
daily_items: FinanceDailyItem[]
|
daily_items: FinanceDailyItem[]
|
||||||
pickup_summary: FinancePickupSummary
|
pickup_summary: FinancePickupSummary
|
||||||
|
mohong_summary: FinanceMohongSummary
|
||||||
disbursement_summary: FinanceDisbursementSummary
|
disbursement_summary: FinanceDisbursementSummary
|
||||||
generated_at: string
|
generated_at: string
|
||||||
}
|
}
|
||||||
@@ -260,6 +270,12 @@ export async function fetchFinanceDashboard(query: FinanceDateQuery = {}) {
|
|||||||
completed_count: 0,
|
completed_count: 0,
|
||||||
in_progress_count: 0,
|
in_progress_count: 0,
|
||||||
},
|
},
|
||||||
|
mohong_summary: data.data?.mohong_summary ?? {
|
||||||
|
flow_amount_cent: 0,
|
||||||
|
refund_amount_cent: 0,
|
||||||
|
net_amount_cent: 0,
|
||||||
|
paid_order_count: 0,
|
||||||
|
},
|
||||||
disbursement_summary: data.data?.disbursement_summary ?? {
|
disbursement_summary: data.data?.disbursement_summary ?? {
|
||||||
paid_amount_cent: 0,
|
paid_amount_cent: 0,
|
||||||
paid_count: 0,
|
paid_count: 0,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ function rowOfflinePendingClass(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
|
||||||
@@ -108,9 +108,12 @@ function rowOfflinePendingClass(row: FinanceDailyItem) {
|
|||||||
<small>成功收款 - 成功退款</small>
|
<small>成功收款 - 成功退款</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>平台收入</span>
|
<span>平台利润</span>
|
||||||
<strong>{{ moneyCent(dashboard.summary.platform_income_amount_cent) }}</strong>
|
<strong>{{ moneyCent(dashboard.summary.platform_income_amount_cent) }}</strong>
|
||||||
<small>{{ dashboard.summary.settled_order_count }} 个已结算订单</small>
|
<small>
|
||||||
|
普通订单 {{ moneyCent(dashboard.summary.normal_order_profit_amount_cent) }} + 线下提号
|
||||||
|
{{ moneyCent(dashboard.summary.pickup_profit_amount_cent) }}
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>号主应得</span>
|
<span>号主应得</span>
|
||||||
@@ -142,12 +145,37 @@ function rowOfflinePendingClass(row: FinanceDailyItem) {
|
|||||||
<small>{{ dashboard.summary.financial_exception_count }} 个异常订单</small>
|
<small>{{ dashboard.summary.financial_exception_count }} 个异常订单</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span>预计收入</span>
|
<span>预计利润</span>
|
||||||
<strong>{{ moneyCent(dashboard.summary.estimated_income_amount_cent) }}</strong>
|
<strong>{{ moneyCent(dashboard.summary.estimated_income_amount_cent) }}</strong>
|
||||||
<small>{{ dashboard.summary.pending_settle_order_count }} 个待结算订单</small>
|
<small>{{ dashboard.summary.pending_settle_order_count }} 个待结算订单</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dashboard?.mohong_summary" class="section-heading">
|
||||||
|
<div>
|
||||||
|
<h2>撞车商城</h2>
|
||||||
|
<p>商城交易流水单独统计,不计入平台利润。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="dashboard?.mohong_summary" class="metric-grid mohong-grid">
|
||||||
|
<div class="metric-card mohong-card">
|
||||||
|
<span>商城成交额</span>
|
||||||
|
<strong>{{ moneyCent(dashboard.mohong_summary.flow_amount_cent) }}</strong>
|
||||||
|
<small>{{ dashboard.mohong_summary.paid_order_count }} 笔成功订单</small>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card mohong-card">
|
||||||
|
<span>商城退款</span>
|
||||||
|
<strong>{{ moneyCent(dashboard.mohong_summary.refund_amount_cent) }}</strong>
|
||||||
|
<small>已完成退款</small>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card mohong-card">
|
||||||
|
<span>商城渠道净流入</span>
|
||||||
|
<strong>{{ moneyCent(dashboard.mohong_summary.net_amount_cent) }}</strong>
|
||||||
|
<small>成交额 - 已退款</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="dashboard?.disbursement_summary" class="section-heading">
|
<div v-if="dashboard?.disbursement_summary" class="section-heading">
|
||||||
<div>
|
<div>
|
||||||
<h2>资金出款</h2>
|
<h2>资金出款</h2>
|
||||||
@@ -241,7 +269,7 @@ function rowOfflinePendingClass(row: FinanceDailyItem) {
|
|||||||
<el-table-column label="渠道净流入" width="140">
|
<el-table-column label="渠道净流入" width="140">
|
||||||
<template #default="{ row }">{{ moneyCent(row.channel_net_amount_cent) }}</template>
|
<template #default="{ row }">{{ moneyCent(row.channel_net_amount_cent) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="平台收入" width="130">
|
<el-table-column label="平台利润" width="130">
|
||||||
<template #default="{ row }">{{ moneyCent(row.platform_income_amount_cent) }}</template>
|
<template #default="{ row }">{{ moneyCent(row.platform_income_amount_cent) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="号主应得" width="130">
|
<el-table-column label="号主应得" width="130">
|
||||||
@@ -322,6 +350,10 @@ function rowOfflinePendingClass(row: FinanceDailyItem) {
|
|||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mohong-grid {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
@@ -362,6 +394,10 @@ function rowOfflinePendingClass(row: FinanceDailyItem) {
|
|||||||
border-left: 3px solid #6366f1;
|
border-left: 3px solid #6366f1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mohong-card {
|
||||||
|
border-left: 3px solid #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
.generated-at {
|
.generated-at {
|
||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
|||||||
Reference in New Issue
Block a user