feat(finance): 增加保险统计与订单保险明细
This commit is contained in:
@@ -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)").
|
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,
|
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(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(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,
|
||||||
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
|
COALESCE(SUM(CASE WHEN ro.settlement_mode = 'platform_managed'
|
||||||
@@ -466,6 +478,12 @@ func summaryFromDailyRows(payments []dailyPaymentRow, settlements []dailySettlem
|
|||||||
}
|
}
|
||||||
for _, row := range settlements {
|
for _, row := range settlements {
|
||||||
summary.NormalOrderProfitAmountCent += row.PlatformIncomeAmountCent
|
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.OwnerShouldIncomeAmountCent += row.OwnerShouldIncomeAmountCent
|
||||||
summary.OwnerWalletIncomeAmountCent += row.OwnerWalletIncomeAmountCent
|
summary.OwnerWalletIncomeAmountCent += row.OwnerWalletIncomeAmountCent
|
||||||
summary.OwnerEffectiveSettlementAmountCent += row.OwnerEffectiveSettlementAmountCent
|
summary.OwnerEffectiveSettlementAmountCent += row.OwnerEffectiveSettlementAmountCent
|
||||||
@@ -604,6 +622,12 @@ type dailyPaidOrderRow struct {
|
|||||||
type dailySettlementRow struct {
|
type dailySettlementRow struct {
|
||||||
Date string
|
Date string
|
||||||
PlatformIncomeAmountCent int64
|
PlatformIncomeAmountCent int64
|
||||||
|
InsuranceOrderCount int64
|
||||||
|
InsuranceProfitAmountCent int64
|
||||||
|
NormalInsuranceOrderCount int64
|
||||||
|
NormalInsuranceProfitAmountCent int64
|
||||||
|
PremiumInsuranceOrderCount int64
|
||||||
|
PremiumInsuranceProfitAmountCent int64
|
||||||
OwnerShouldIncomeAmountCent int64
|
OwnerShouldIncomeAmountCent int64
|
||||||
OwnerWalletIncomeAmountCent int64
|
OwnerWalletIncomeAmountCent int64
|
||||||
OwnerEffectiveSettlementAmountCent int64
|
OwnerEffectiveSettlementAmountCent int64
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
const dashboardCacheTTL = 5 * time.Minute
|
const dashboardCacheTTL = 5 * time.Minute
|
||||||
|
|
||||||
func (r *Repository) dashboardCacheKey(query DashboardQuery) string {
|
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 {
|
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},
|
{TotalFlowAmountCent: 2500, TotalRefundAmountCent: 400, PendingRefundAmountCent: 70, SuccessfulPayCount: 5, PartialRefundCount: 2, PendingRefundCount: 1},
|
||||||
}
|
}
|
||||||
settlementRows := []dailySettlementRow{
|
settlementRows := []dailySettlementRow{
|
||||||
{PlatformIncomeAmountCent: 300, OwnerShouldIncomeAmountCent: 900, OwnerWalletIncomeAmountCent: 850, OwnerEffectiveSettlementAmountCent: 850, OfflineSettlementAmountCent: 100, OfflineSettlementPendingAmountCent: 20, OfflineSettlementPendingCount: 1, FinancialExceptionCount: 2, SettledOrderCount: 3},
|
{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, OwnerShouldIncomeAmountCent: 600, OwnerWalletIncomeAmountCent: 500, OwnerEffectiveSettlementAmountCent: 550, OfflineSettlementAmountCent: 200, OfflineSettlementPendingAmountCent: 40, OfflineSettlementPendingCount: 2, FinancialExceptionCount: 1, SettledOrderCount: 2},
|
{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{
|
estimateRows := []dailyEstimatedRow{
|
||||||
{EstimatedIncomeAmountCent: 120, PendingSettleOrderCount: 4},
|
{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 {
|
if summary.NormalOrderProfitAmountCent != 500 || summary.PickupProfitAmountCent != 90 || summary.PlatformIncomeAmountCent != 590 || summary.PickupCompletedCount != 2 {
|
||||||
t.Fatalf("平台利润汇总错误: %+v", summary)
|
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 {
|
if summary.OwnerShouldIncomeAmountCent != 1500 || summary.OwnerWalletIncomeAmountCent != 1350 || summary.OwnerEffectiveSettlementAmountCent != 1400 || summary.SettlementDiffAmountCent != 100 {
|
||||||
t.Fatalf("货主结算汇总错误: %+v", summary)
|
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) {
|
func TestDashboardCacheKeyAndSingleflight(t *testing.T) {
|
||||||
repo := NewRepository(nil)
|
repo := NewRepository(nil)
|
||||||
q := DashboardQuery{
|
q := DashboardQuery{
|
||||||
|
|||||||
@@ -246,6 +246,13 @@ type FinanceSummaryDTO struct {
|
|||||||
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
|
PlatformIncomeAmountCent int64 `json:"platform_income_amount_cent"`
|
||||||
NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"`
|
NormalOrderProfitAmountCent int64 `json:"normal_order_profit_amount_cent"`
|
||||||
PickupProfitAmountCent int64 `json:"pickup_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 线下提号利润对应的已完成提号笔数。
|
||||||
PickupCompletedCount int64 `json:"pickup_completed_count"`
|
PickupCompletedCount int64 `json:"pickup_completed_count"`
|
||||||
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
|
OwnerShouldIncomeAmountCent int64 `json:"owner_should_income_amount_cent"`
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export interface FinanceSummary {
|
|||||||
platform_income_amount_cent: number
|
platform_income_amount_cent: number
|
||||||
normal_order_profit_amount_cent: number
|
normal_order_profit_amount_cent: number
|
||||||
pickup_profit_amount_cent: number
|
pickup_profit_amount_cent: number
|
||||||
|
insurance_order_count: number
|
||||||
|
insurance_profit_amount_cent: number
|
||||||
|
normal_insurance_order_count: number
|
||||||
|
normal_insurance_profit_amount_cent: number
|
||||||
|
premium_insurance_order_count: number
|
||||||
|
premium_insurance_profit_amount_cent: number
|
||||||
/** 普通(租赁)订单利润对应的已结算订单数量 */
|
/** 普通(租赁)订单利润对应的已结算订单数量 */
|
||||||
settled_order_count: number
|
settled_order_count: number
|
||||||
/** 线下提号利润对应的已完成提号笔数 */
|
/** 线下提号利润对应的已完成提号笔数 */
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
estimateOrderEndAt,
|
estimateOrderEndAt,
|
||||||
|
orderPlatformFeeBreakdown,
|
||||||
paymentBizTypeLabel,
|
paymentBizTypeLabel,
|
||||||
paymentPaidAt,
|
paymentPaidAt,
|
||||||
paymentStatusLabel,
|
paymentStatusLabel,
|
||||||
@@ -39,4 +40,45 @@ describe('admin order presentation', () => {
|
|||||||
).toBe('paid')
|
).toBe('paid')
|
||||||
expect(paymentPaidAt({ updated_at: 'updated', created_at: 'created' })).toBe('updated')
|
expect(paymentPaidAt({ updated_at: 'updated', created_at: 'created' })).toBe('updated')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('splits insurance from the final checkout platform income', () => {
|
||||||
|
expect(
|
||||||
|
orderPlatformFeeBreakdown({
|
||||||
|
status: 'completed',
|
||||||
|
platform_fee_cent: 3260,
|
||||||
|
insurance_fee_cent: 1739,
|
||||||
|
insurance_fee_bps: 800,
|
||||||
|
checkout: {
|
||||||
|
status: 'accepted',
|
||||||
|
platform_fee_cent: 4999,
|
||||||
|
insurance_fee_cent: 1739,
|
||||||
|
insurance_fee_bps: 800,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
baseFeeCent: 3260,
|
||||||
|
insuranceFeeCent: 1739,
|
||||||
|
insuranceFeeBps: 800,
|
||||||
|
totalFeeCent: 4999,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the order snapshot before settlement and when there is no insurance', () => {
|
||||||
|
expect(
|
||||||
|
orderPlatformFeeBreakdown({
|
||||||
|
status: 'renting',
|
||||||
|
platform_fee_cent: 4000,
|
||||||
|
insurance_fee_cent: 0,
|
||||||
|
checkout: {
|
||||||
|
status: 'submitted',
|
||||||
|
platform_fee_cent: 5000,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
baseFeeCent: 4000,
|
||||||
|
insuranceFeeCent: 0,
|
||||||
|
insuranceFeeBps: 0,
|
||||||
|
totalFeeCent: 4000,
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
import type { AdminPayment } from '@/features/admin/api/adminPayments'
|
import type { AdminPayment } from '@/features/admin/api/adminPayments'
|
||||||
|
|
||||||
|
type OrderPlatformFeeSource = {
|
||||||
|
status?: string
|
||||||
|
platform_fee_cent?: number
|
||||||
|
insurance_fee_cent?: number
|
||||||
|
insurance_fee_bps?: number
|
||||||
|
checkout?: {
|
||||||
|
status?: string
|
||||||
|
platform_fee_cent?: number
|
||||||
|
insurance_fee_cent?: number
|
||||||
|
insurance_fee_bps?: number
|
||||||
|
} | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OrderPlatformFeeBreakdown = {
|
||||||
|
baseFeeCent: number
|
||||||
|
insuranceFeeCent: number
|
||||||
|
insuranceFeeBps: number
|
||||||
|
totalFeeCent: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonNegativeCent(value: number | undefined) {
|
||||||
|
const amount = Number(value || 0)
|
||||||
|
return Number.isFinite(amount) ? Math.max(amount, 0) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单主表保存下单时的平台基础费用,结账单保存结算后的平台收入合计。
|
||||||
|
* 已完成订单优先采用结账单,并从合计中拆出无忧保险,避免客服看到旧快照。
|
||||||
|
*/
|
||||||
|
export function orderPlatformFeeBreakdown(
|
||||||
|
order: OrderPlatformFeeSource | null | undefined
|
||||||
|
): OrderPlatformFeeBreakdown {
|
||||||
|
if (!order) {
|
||||||
|
return { baseFeeCent: 0, insuranceFeeCent: 0, insuranceFeeBps: 0, totalFeeCent: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderFeeCent = nonNegativeCent(order.platform_fee_cent)
|
||||||
|
const checkoutFeeCent = nonNegativeCent(order.checkout?.platform_fee_cent)
|
||||||
|
const totalFeeCent =
|
||||||
|
order.status === 'completed' ? Math.max(orderFeeCent, checkoutFeeCent) : orderFeeCent
|
||||||
|
const orderInsuranceFeeCent = nonNegativeCent(order.insurance_fee_cent)
|
||||||
|
const checkoutInsuranceFeeCent = nonNegativeCent(order.checkout?.insurance_fee_cent)
|
||||||
|
const insuranceFeeCent =
|
||||||
|
orderInsuranceFeeCent > 0 ? orderInsuranceFeeCent : checkoutInsuranceFeeCent
|
||||||
|
const insuranceFeeBps = Math.max(
|
||||||
|
Number(order.insurance_fee_bps || 0),
|
||||||
|
insuranceFeeCent > 0 ? Number(order.checkout?.insurance_fee_bps || 0) : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseFeeCent: Math.max(totalFeeCent - insuranceFeeCent, 0),
|
||||||
|
insuranceFeeCent: Math.min(insuranceFeeCent, totalFeeCent),
|
||||||
|
insuranceFeeBps,
|
||||||
|
totalFeeCent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function estimateOrderEndAt(
|
export function estimateOrderEndAt(
|
||||||
estimatedEndAt?: string,
|
estimatedEndAt?: string,
|
||||||
rentedAt?: string,
|
rentedAt?: string,
|
||||||
|
|||||||
@@ -386,6 +386,11 @@
|
|||||||
color: #4f46e5;
|
color: #4f46e5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-badge.amber {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
@@ -457,6 +462,20 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.insurance-type-card .sub-card-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurance-type-card--normal {
|
||||||
|
border-left: 3px solid #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.insurance-type-card--premium {
|
||||||
|
border-left: 3px solid #8b5cf6;
|
||||||
|
}
|
||||||
|
|
||||||
.dot-sep {
|
.dot-sep {
|
||||||
color: #cbd5e1;
|
color: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import {
|
|||||||
Coin,
|
Coin,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
DocumentChecked,
|
DocumentChecked,
|
||||||
|
Medal,
|
||||||
Money,
|
Money,
|
||||||
Refresh,
|
Refresh,
|
||||||
Search,
|
Search,
|
||||||
ShoppingCart,
|
ShoppingCart,
|
||||||
Timer,
|
Timer,
|
||||||
TrendCharts,
|
TrendCharts,
|
||||||
|
Umbrella,
|
||||||
Van,
|
Van,
|
||||||
Wallet,
|
Wallet,
|
||||||
WarningFilled,
|
WarningFilled,
|
||||||
@@ -370,6 +372,60 @@ function diffType(value: number | undefined | null) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 无忧保险 -->
|
||||||
|
<div class="section-container">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title-wrap">
|
||||||
|
<div class="section-badge amber">
|
||||||
|
<el-icon><Umbrella /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="section-title">无忧保险</h2>
|
||||||
|
<p class="section-desc">按结算日统计投保订单数量与保险服务费利润</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sub-metric-grid cols-3">
|
||||||
|
<div class="sub-card">
|
||||||
|
<div class="sub-card-label">保险利润合计</div>
|
||||||
|
<div class="sub-card-value emerald-text">
|
||||||
|
{{ moneyCent(dashboard.summary.insurance_profit_amount_cent) }}
|
||||||
|
</div>
|
||||||
|
<div class="sub-card-sub">
|
||||||
|
<span>{{ dashboard.summary.insurance_order_count }} 笔已结算投保订单</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sub-card insurance-type-card insurance-type-card--normal">
|
||||||
|
<div class="sub-card-label">普通无忧保险</div>
|
||||||
|
<div class="sub-card-value">
|
||||||
|
{{ moneyCent(dashboard.summary.normal_insurance_profit_amount_cent) }}
|
||||||
|
</div>
|
||||||
|
<div class="sub-card-sub">
|
||||||
|
<span>{{ dashboard.summary.normal_insurance_order_count }} 笔投保订单</span>
|
||||||
|
<span class="dot-sep">·</span>
|
||||||
|
<span>保险利润</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sub-card insurance-type-card insurance-type-card--premium">
|
||||||
|
<div class="sub-card-label">
|
||||||
|
<el-icon><Medal /></el-icon>
|
||||||
|
极品专区保险
|
||||||
|
</div>
|
||||||
|
<div class="sub-card-value">
|
||||||
|
{{ moneyCent(dashboard.summary.premium_insurance_profit_amount_cent) }}
|
||||||
|
</div>
|
||||||
|
<div class="sub-card-sub">
|
||||||
|
<span>{{ dashboard.summary.premium_insurance_order_count }} 笔投保订单</span>
|
||||||
|
<span class="dot-sep">·</span>
|
||||||
|
<span>保险利润</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 资金出款与待办审批 -->
|
<!-- 资金出款与待办审批 -->
|
||||||
<div v-if="dashboard.disbursement_summary" class="section-container">
|
<div v-if="dashboard.disbursement_summary" class="section-container">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ import { formatGameName, formatListingNo } from '@/shared/utils/listingDisplay'
|
|||||||
import AdminOrderMetrics, { type AdminOrderMetric } from '../components/AdminOrderMetrics.vue'
|
import AdminOrderMetrics, { type AdminOrderMetric } from '../components/AdminOrderMetrics.vue'
|
||||||
import AdminOrderPaymentRecords from '../components/AdminOrderPaymentRecords.vue'
|
import AdminOrderPaymentRecords from '../components/AdminOrderPaymentRecords.vue'
|
||||||
import ProcessTimeline from '../components/ProcessTimeline.vue'
|
import ProcessTimeline from '../components/ProcessTimeline.vue'
|
||||||
import { estimateOrderEndAt, paymentStatusLabel } from '../utils/adminOrderPresentation'
|
import {
|
||||||
|
estimateOrderEndAt,
|
||||||
|
orderPlatformFeeBreakdown,
|
||||||
|
paymentStatusLabel,
|
||||||
|
} from '../utils/adminOrderPresentation'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -362,6 +366,7 @@ const ownerLossPriceCent = computed(() => {
|
|||||||
return Math.max(Number(order.value.owner_rent_amount_cent) - ownerCoinBasePriceCent.value, 0)
|
return Math.max(Number(order.value.owner_rent_amount_cent) - ownerCoinBasePriceCent.value, 0)
|
||||||
})
|
})
|
||||||
const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null)
|
const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null)
|
||||||
|
const platformFeeBreakdown = computed(() => orderPlatformFeeBreakdown(order.value))
|
||||||
const fundSplitRows = computed(() => {
|
const fundSplitRows = computed(() => {
|
||||||
if (!order.value) return []
|
if (!order.value) return []
|
||||||
const rows: FundSplitRow[] = []
|
const rows: FundSplitRow[] = []
|
||||||
@@ -398,8 +403,20 @@ const fundSplitRows = computed(() => {
|
|||||||
} else {
|
} else {
|
||||||
rows.push({ label: '号主租金', amountCent: order.value.owner_rent_amount_cent })
|
rows.push({ label: '号主租金', amountCent: order.value.owner_rent_amount_cent })
|
||||||
}
|
}
|
||||||
|
const platformFee = platformFeeBreakdown.value
|
||||||
|
if (platformFee.insuranceFeeCent > 0) {
|
||||||
|
rows.push(
|
||||||
|
{ label: '平台基础费用', amountCent: platformFee.baseFeeCent },
|
||||||
|
{
|
||||||
|
label: `无忧租号保险服务费(${(platformFee.insuranceFeeBps / 100).toFixed(1)}%)`,
|
||||||
|
amountCent: platformFee.insuranceFeeCent,
|
||||||
|
},
|
||||||
|
{ label: '平台收入合计', amountCent: platformFee.totalFeeCent }
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rows.push({ label: '平台费用', amountCent: platformFee.totalFeeCent })
|
||||||
|
}
|
||||||
rows.push(
|
rows.push(
|
||||||
{ label: '平台费用', amountCent: order.value.platform_fee_cent },
|
|
||||||
{ label: '实付押金', amountCent: order.value.deposit_amount_cent },
|
{ label: '实付押金', amountCent: order.value.deposit_amount_cent },
|
||||||
{ label: '原始押金', amountCent: order.value.deposit_original_amount_cent },
|
{ label: '原始押金', amountCent: order.value.deposit_original_amount_cent },
|
||||||
{ label: '免押额度', amountCent: order.value.deposit_waived_amount_cent }
|
{ label: '免押额度', amountCent: order.value.deposit_waived_amount_cent }
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ container="${TEST_MYSQL_CONTAINER:-hfb-mysql}"
|
|||||||
|
|
||||||
[[ "${base_database}" =~ ^[A-Za-z0-9_]+$ ]] || { printf '测试库基础名不合法: %s\n' "${base_database}" >&2; exit 2; }
|
[[ "${base_database}" =~ ^[A-Za-z0-9_]+$ ]] || { printf '测试库基础名不合法: %s\n' "${base_database}" >&2; exit 2; }
|
||||||
|
|
||||||
# MySQL 模式串里下划线是通配符,需要转义成字面量。
|
# MySQL 模式串里下划线是通配符,需要转义成字面量;只保留一层反斜杠。
|
||||||
pattern="$(printf '%s' "${base_database}" | sed 's/_/\\\\_/g')"
|
pattern="$(printf '%s' "${base_database}" | sed 's/_/\\_/g')"
|
||||||
|
|
||||||
run_admin_sql() {
|
run_admin_sql() {
|
||||||
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "${container}"; then
|
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "${container}"; then
|
||||||
|
|||||||
Reference in New Issue
Block a user