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
@@ -13,6 +13,12 @@ export interface FinanceSummary {
platform_income_amount_cent: number
normal_order_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
/** 线下提号利润对应的已完成提号笔数 */
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
estimateOrderEndAt,
orderPlatformFeeBreakdown,
paymentBizTypeLabel,
paymentPaidAt,
paymentStatusLabel,
@@ -39,4 +40,45 @@ describe('admin order presentation', () => {
).toBe('paid')
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'
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(
estimatedEndAt?: string,
rentedAt?: string,
@@ -386,6 +386,11 @@
color: #4f46e5;
}
.section-badge.amber {
background: #fffbeb;
color: #d97706;
}
.section-title {
margin: 0;
font-size: 17px;
@@ -457,6 +462,20 @@
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 {
color: #cbd5e1;
}
@@ -4,12 +4,14 @@ import {
Coin,
CreditCard,
DocumentChecked,
Medal,
Money,
Refresh,
Search,
ShoppingCart,
Timer,
TrendCharts,
Umbrella,
Van,
Wallet,
WarningFilled,
@@ -370,6 +372,60 @@ function diffType(value: number | undefined | null) {
</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 class="section-header">
@@ -51,7 +51,11 @@ import { formatGameName, formatListingNo } from '@/shared/utils/listingDisplay'
import AdminOrderMetrics, { type AdminOrderMetric } from '../components/AdminOrderMetrics.vue'
import AdminOrderPaymentRecords from '../components/AdminOrderPaymentRecords.vue'
import ProcessTimeline from '../components/ProcessTimeline.vue'
import { estimateOrderEndAt, paymentStatusLabel } from '../utils/adminOrderPresentation'
import {
estimateOrderEndAt,
orderPlatformFeeBreakdown,
paymentStatusLabel,
} from '../utils/adminOrderPresentation'
const route = useRoute()
const router = useRouter()
@@ -362,6 +366,7 @@ const ownerLossPriceCent = computed(() => {
return Math.max(Number(order.value.owner_rent_amount_cent) - ownerCoinBasePriceCent.value, 0)
})
const hasOwnerRentBreakdown = computed(() => ownerCoinBasePriceCent.value !== null)
const platformFeeBreakdown = computed(() => orderPlatformFeeBreakdown(order.value))
const fundSplitRows = computed(() => {
if (!order.value) return []
const rows: FundSplitRow[] = []
@@ -398,8 +403,20 @@ const fundSplitRows = computed(() => {
} else {
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(
{ label: '平台费用', amountCent: order.value.platform_fee_cent },
{ label: '实付押金', amountCent: order.value.deposit_amount_cent },
{ label: '原始押金', amountCent: order.value.deposit_original_amount_cent },
{ label: '免押额度', amountCent: order.value.deposit_waived_amount_cent }