feat(finance): 增加保险统计与订单保险明细

This commit is contained in:
yml
2026-11-14 17:04:24 +08:00
parent 50e3ce00f2
commit 6a2652f69d
11 changed files with 288 additions and 7 deletions
@@ -406,6 +406,18 @@ func (r *Repository) dailySettlements(ctx context.Context, query DashboardQuery)
err := db.Table("rental_orders AS ro USE INDEX (idx_rental_orders_dashboard_settled)").
Select(`DATE_FORMAT(ro.settled_at, '%Y-%m-%d') AS date,
COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent,
COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0
THEN 1 ELSE 0 END), 0) AS insurance_order_count,
COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0
THEN COALESCE(oc.insurance_fee_cent, 0) ELSE 0 END), 0) AS insurance_profit_amount_cent,
COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0
AND ro.is_premium = 0 THEN 1 ELSE 0 END), 0) AS normal_insurance_order_count,
COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0
AND ro.is_premium = 0 THEN COALESCE(oc.insurance_fee_cent, 0) ELSE 0 END), 0) AS normal_insurance_profit_amount_cent,
COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0
AND ro.is_premium = 1 THEN 1 ELSE 0 END), 0) AS premium_insurance_order_count,
COALESCE(SUM(CASE WHEN ro.compensation_plan = 'insurance' AND ro.insurance_fee_bps > 0
AND ro.is_premium = 1 THEN COALESCE(oc.insurance_fee_cent, 0) ELSE 0 END), 0) AS premium_insurance_profit_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(CASE WHEN ro.settlement_mode = 'platform_managed'
@@ -466,6 +478,12 @@ func summaryFromDailyRows(payments []dailyPaymentRow, settlements []dailySettlem
}
for _, row := range settlements {
summary.NormalOrderProfitAmountCent += row.PlatformIncomeAmountCent
summary.InsuranceOrderCount += row.InsuranceOrderCount
summary.InsuranceProfitAmountCent += row.InsuranceProfitAmountCent
summary.NormalInsuranceOrderCount += row.NormalInsuranceOrderCount
summary.NormalInsuranceProfitAmountCent += row.NormalInsuranceProfitAmountCent
summary.PremiumInsuranceOrderCount += row.PremiumInsuranceOrderCount
summary.PremiumInsuranceProfitAmountCent += row.PremiumInsuranceProfitAmountCent
summary.OwnerShouldIncomeAmountCent += row.OwnerShouldIncomeAmountCent
summary.OwnerWalletIncomeAmountCent += row.OwnerWalletIncomeAmountCent
summary.OwnerEffectiveSettlementAmountCent += row.OwnerEffectiveSettlementAmountCent
@@ -604,6 +622,12 @@ type dailyPaidOrderRow struct {
type dailySettlementRow struct {
Date string
PlatformIncomeAmountCent int64
InsuranceOrderCount int64
InsuranceProfitAmountCent int64
NormalInsuranceOrderCount int64
NormalInsuranceProfitAmountCent int64
PremiumInsuranceOrderCount int64
PremiumInsuranceProfitAmountCent int64
OwnerShouldIncomeAmountCent int64
OwnerWalletIncomeAmountCent int64
OwnerEffectiveSettlementAmountCent int64
@@ -10,7 +10,7 @@ import (
const dashboardCacheTTL = 5 * time.Minute
func (r *Repository) dashboardCacheKey(query DashboardQuery) string {
return fmt.Sprintf("admin-finance:dashboard:v1:%d:%d", query.StartDate.Unix(), query.EndDate.Unix())
return fmt.Sprintf("admin-finance:dashboard:v2:%d:%d", query.StartDate.Unix(), query.EndDate.Unix())
}
func (r *Repository) loadDashboardCache(ctx context.Context, query DashboardQuery) *DashboardDTO {
@@ -257,8 +257,8 @@ func TestSummaryFromDailyRowsPreservesDashboardTotals(t *testing.T) {
{TotalFlowAmountCent: 2500, TotalRefundAmountCent: 400, PendingRefundAmountCent: 70, SuccessfulPayCount: 5, PartialRefundCount: 2, PendingRefundCount: 1},
}
settlementRows := []dailySettlementRow{
{PlatformIncomeAmountCent: 300, OwnerShouldIncomeAmountCent: 900, OwnerWalletIncomeAmountCent: 850, OwnerEffectiveSettlementAmountCent: 850, OfflineSettlementAmountCent: 100, OfflineSettlementPendingAmountCent: 20, OfflineSettlementPendingCount: 1, FinancialExceptionCount: 2, SettledOrderCount: 3},
{PlatformIncomeAmountCent: 200, OwnerShouldIncomeAmountCent: 600, OwnerWalletIncomeAmountCent: 500, OwnerEffectiveSettlementAmountCent: 550, OfflineSettlementAmountCent: 200, OfflineSettlementPendingAmountCent: 40, OfflineSettlementPendingCount: 2, FinancialExceptionCount: 1, SettledOrderCount: 2},
{PlatformIncomeAmountCent: 300, InsuranceOrderCount: 2, InsuranceProfitAmountCent: 80, NormalInsuranceOrderCount: 1, NormalInsuranceProfitAmountCent: 50, PremiumInsuranceOrderCount: 1, PremiumInsuranceProfitAmountCent: 30, OwnerShouldIncomeAmountCent: 900, OwnerWalletIncomeAmountCent: 850, OwnerEffectiveSettlementAmountCent: 850, OfflineSettlementAmountCent: 100, OfflineSettlementPendingAmountCent: 20, OfflineSettlementPendingCount: 1, FinancialExceptionCount: 2, SettledOrderCount: 3},
{PlatformIncomeAmountCent: 200, InsuranceOrderCount: 1, InsuranceProfitAmountCent: 20, NormalInsuranceOrderCount: 1, NormalInsuranceProfitAmountCent: 20, OwnerShouldIncomeAmountCent: 600, OwnerWalletIncomeAmountCent: 500, OwnerEffectiveSettlementAmountCent: 550, OfflineSettlementAmountCent: 200, OfflineSettlementPendingAmountCent: 40, OfflineSettlementPendingCount: 2, FinancialExceptionCount: 1, SettledOrderCount: 2},
}
estimateRows := []dailyEstimatedRow{
{EstimatedIncomeAmountCent: 120, PendingSettleOrderCount: 4},
@@ -276,6 +276,11 @@ func TestSummaryFromDailyRowsPreservesDashboardTotals(t *testing.T) {
if summary.NormalOrderProfitAmountCent != 500 || summary.PickupProfitAmountCent != 90 || summary.PlatformIncomeAmountCent != 590 || summary.PickupCompletedCount != 2 {
t.Fatalf("平台利润汇总错误: %+v", summary)
}
if summary.InsuranceOrderCount != 3 || summary.InsuranceProfitAmountCent != 100 ||
summary.NormalInsuranceOrderCount != 2 || summary.NormalInsuranceProfitAmountCent != 70 ||
summary.PremiumInsuranceOrderCount != 1 || summary.PremiumInsuranceProfitAmountCent != 30 {
t.Fatalf("保险汇总错误: %+v", summary)
}
if summary.OwnerShouldIncomeAmountCent != 1500 || summary.OwnerWalletIncomeAmountCent != 1350 || summary.OwnerEffectiveSettlementAmountCent != 1400 || summary.SettlementDiffAmountCent != 100 {
t.Fatalf("货主结算汇总错误: %+v", summary)
}
@@ -287,6 +292,54 @@ func TestSummaryFromDailyRowsPreservesDashboardTotals(t *testing.T) {
}
}
func TestInsuranceSummaryUsesSettlementDateAndPremiumSnapshot(t *testing.T) {
db := database.NewTestDB()
loc := timeutil.ShanghaiLocation()
settledAt := time.Date(2026, 8, 2, 10, 0, 0, 0, loc)
for _, row := range [][]any{
{1, "INS-NORMAL", 1, 1, 1, 1, settledAt, false, "insurance", 800},
{2, "INS-PREMIUM", 2, 2, 2, 2, settledAt, true, "insurance", 400},
{3, "NO-INSURANCE", 3, 3, 3, 3, settledAt, false, "normal", 0},
{4, "OUTSIDE-RANGE", 4, 4, 4, 4, settledAt.AddDate(0, 0, 1), true, "insurance", 400},
} {
if err := db.Exec(`INSERT INTO rental_orders
(id, order_no, listing_id, account_id, owner_id, renter_id, settled_at,
is_premium, compensation_plan, insurance_fee_bps)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, row...).Error; err != nil {
t.Fatalf("写入订单失败: %v", err)
}
}
for _, row := range [][]any{
{1, 1, 1, "accepted", 800},
{2, 2, 2, "accepted", 400},
{3, 3, 3, "accepted", 0},
{4, 4, 4, "accepted", 400},
} {
if err := db.Exec(`INSERT INTO order_checkouts
(id, order_id, initiated_by, status, insurance_fee_cent)
VALUES (?, ?, ?, ?, ?)`, row...).Error; err != nil {
t.Fatalf("写入结账单失败: %v", err)
}
}
repo := NewRepository(db)
query := DashboardQuery{
StartDate: time.Date(2026, 8, 2, 0, 0, 0, 0, loc),
EndDate: time.Date(2026, 8, 2, 23, 59, 59, 0, loc),
}
settlements, err := repo.dailySettlements(t.Context(), query)
if err != nil {
t.Fatalf("读取每日结算汇总失败: %v", err)
}
summary := summaryFromDailyRows(nil, settlements, nil)
if summary.InsuranceOrderCount != 2 || summary.InsuranceProfitAmountCent != 1200 ||
summary.NormalInsuranceOrderCount != 1 || summary.NormalInsuranceProfitAmountCent != 800 ||
summary.PremiumInsuranceOrderCount != 1 || summary.PremiumInsuranceProfitAmountCent != 400 {
t.Fatalf("保险汇总错误: %+v", summary)
}
}
func TestDashboardCacheKeyAndSingleflight(t *testing.T) {
repo := NewRepository(nil)
q := DashboardQuery{
@@ -246,6 +246,13 @@ type FinanceSummaryDTO struct {
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"`
PickupProfitAmountCent int64 `json:"pickup_profit_amount_cent"`
// 无忧保险统计与平台净利润同口径,按结算日归集。
InsuranceOrderCount int64 `json:"insurance_order_count"`
InsuranceProfitAmountCent int64 `json:"insurance_profit_amount_cent"`
NormalInsuranceOrderCount int64 `json:"normal_insurance_order_count"`
NormalInsuranceProfitAmountCent int64 `json:"normal_insurance_profit_amount_cent"`
PremiumInsuranceOrderCount int64 `json:"premium_insurance_order_count"`
PremiumInsuranceProfitAmountCent int64 `json:"premium_insurance_profit_amount_cent"`
// PickupCompletedCount 线下提号利润对应的已完成提号笔数。
PickupCompletedCount int64 `json:"pickup_completed_count"`
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`