feat(distribution): 补齐退款加价回退、本站自卖与开通默认规则

退款时压缩卖方加价,人工/自动批次仅完结单可入账,号主收入进主站钱包;仲裁后重算释放并支持作废未入账 DS。开通时四种模式按默认生效,对外口径合并为 35/40/25,不再单独展示通道费。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-10 18:13:46 +08:00
co-authored by Cursor
parent e621722218
commit 768902f2f7
28 changed files with 627 additions and 263 deletions
@@ -94,9 +94,6 @@ export interface OnboardDistributionSitePayload {
tax_enabled?: boolean
tax_rate_bps?: number
settlement_hold_days?: number
distributor_receive_ratio?: number
platform_sell_ratio?: number
service_fee_value?: number
}
export interface OnboardDistributionSiteResult {
@@ -254,6 +251,13 @@ export async function settleDistributionBatch(id: number) {
return data.data
}
export async function cancelDistributionBatch(id: number) {
const { data } = await apiClient.post<ApiResponse<DistributionSettlementBatch>>(
`/admin/distribution/settlement-batches/${id}/cancel`
)
return data.data
}
export async function fetchDistributionSettlementItems(batchId: number) {
const { data } = await apiClient.get<ApiResponse<{ items: DistributionSettlementItem[] }>>(
`/admin/distribution/settlement-batches/${batchId}/items`
@@ -16,6 +16,11 @@ export const DISTRIBUTION_MODES = [
label: '三方分账',
hint: '分站卖另一分站的货',
},
{
value: 'site_self_sell',
label: '本站自卖',
hint: '分站卖本站货,卖出站拿 75%',
},
] as const
const MODE_LABELS: Record<string, string> = Object.fromEntries(
@@ -46,10 +51,6 @@ export function formatRatioPercent(value?: number) {
return `${n.toFixed(n % 1 === 0 ? 0 : 1)}%`
}
export function formatPaymentFeeBps(bps?: number) {
return `${Number(bps || 0) / 100}%`
}
export function ruleSplitParts(rule: DistributionRule) {
switch (rule.mode) {
case 'two_party_distributor_receive':
@@ -67,6 +68,8 @@ export function ruleSplitParts(rule: DistributionRule) {
{ label: '收号站', value: formatRatioPercent(rule.distributor_a_ratio) },
{ label: '卖出站', value: formatRatioPercent(rule.distributor_b_ratio) },
]
case 'site_self_sell':
return [{ label: '本站卖出', value: formatRatioPercent(rule.distributor_sell_ratio) }]
default:
return []
}
@@ -90,7 +90,7 @@ onMounted(async () => {
<div>
<h2 class="page-title">分销订单</h2>
<p class="page-subtitle">
查看分销订单的利润基数、主站承担的通道费,以及冷却期释放状态。不要把订单履约结算和分销入账混为一谈。
查看分销订单的利润基数与冷却期释放状态。不要把订单履约结算和分销入账混为一谈。
</p>
</div>
</div>
@@ -171,12 +171,6 @@ onMounted(async () => {
{{ formatCent(row.split_base_amount_cent) }}
</template>
</el-table-column>
<el-table-column label="通道费" min-width="110" align="right">
<template #default="{ row }">
<div>{{ formatCent(row.payment_fee_cent) }}</div>
<small class="muted">主站承担</small>
</template>
</el-table-column>
<el-table-column label="服务费" min-width="90" align="right">
<template #default="{ row }">
{{ formatCent(row.service_fee_cent) }}
@@ -255,9 +249,4 @@ onMounted(async () => {
justify-content: flex-end;
margin-top: 16px;
}
.muted {
color: #9ca3af;
font-size: 12px;
}
</style>
@@ -14,7 +14,6 @@ import {
import {
DISTRIBUTION_MODES,
distributionModeLabel,
formatPaymentFeeBps,
formatServiceFee,
ruleSplitParts,
} from '@/features/admin/utils/distributionLabels'
@@ -94,7 +93,7 @@ function resetCreateForm() {
createForm.site_id = selectedSiteId.value
createForm.mode = selectedMode.value || 'two_party_distributor_receive'
createForm.receive_ratio = 35
createForm.sell_ratio = 40
createForm.sell_ratio = createForm.mode === 'site_self_sell' ? 75 : 40
createForm.service_fee_value = 25
createForm.payment_fee_bps = 50
createForm.tax_enabled = false
@@ -102,6 +101,12 @@ function resetCreateForm() {
createForm.activate = true
}
function handleModeChange(mode: string) {
createForm.sell_ratio = mode === 'site_self_sell' ? 75 : 40
createForm.receive_ratio = 35
createForm.service_fee_value = 25
}
function handleOpenCreate() {
resetCreateForm()
createVisible.value = true
@@ -123,6 +128,8 @@ function buildCreatePayload(): CreateRulePayload {
} else if (createForm.mode === 'two_party_platform_receive') {
payload.platform_receive_ratio = Number(createForm.receive_ratio)
payload.distributor_sell_ratio = Number(createForm.sell_ratio)
} else if (createForm.mode === 'site_self_sell') {
payload.distributor_sell_ratio = Number(createForm.sell_ratio)
} else {
payload.distributor_a_ratio = Number(createForm.receive_ratio)
payload.distributor_b_ratio = Number(createForm.sell_ratio)
@@ -132,11 +139,17 @@ function buildCreatePayload(): CreateRulePayload {
async function handleCreate() {
const total =
Number(createForm.receive_ratio) +
Number(createForm.sell_ratio) +
Number(createForm.service_fee_value)
createForm.mode === 'site_self_sell'
? Number(createForm.sell_ratio) + Number(createForm.service_fee_value)
: Number(createForm.receive_ratio) +
Number(createForm.sell_ratio) +
Number(createForm.service_fee_value)
if (Math.abs(total - 100) > 0.05) {
ElMessage.warning(`收号 + 卖出 + 服务费应为 100%,当前为 ${total}%`)
ElMessage.warning(
createForm.mode === 'site_self_sell'
? `本站分成 + 服务费应为 100%,当前为 ${total}%`
: `收号 + 卖出 + 服务费应为 100%,当前为 ${total}%`
)
return
}
creating.value = true
@@ -167,7 +180,7 @@ onMounted(async () => {
<div>
<h2 class="page-title">分销规则配置</h2>
<p class="page-subtitle">
按模式配置利润分成。利润 = 净付款 − 号主 − 加价;通道费由主站承担,只记账不进分成。
按模式配置利润分成。利润 = 净付款 − 号主 − 加价,再按收号 / 卖出 / 服务费切分。
</p>
</div>
<div class="header-filters">
@@ -208,7 +221,7 @@ onMounted(async () => {
:closable="false"
show-icon
class="rules-alert"
title="默认 35% 收号 / 40% 卖出 / 25% 服务费。站点专属规则优先于全局默认;通道费默认 0.5%,从主站服务费与角色分成中承担。"
title="默认 35% 收号 / 40% 卖出 / 25% 服务费。站点专属规则优先于全局默认。"
/>
<el-card shadow="never">
@@ -240,14 +253,6 @@ onMounted(async () => {
</div>
</template>
</el-table-column>
<el-table-column label="通道费" min-width="140">
<template #default="{ row }">
<div class="fee-cell">
<span>{{ formatPaymentFeeBps(row.payment_fee_bps) }}</span>
<small>主站承担</small>
</div>
</template>
</el-table-column>
<el-table-column label="税费" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.tax_enabled" type="warning" size="small">
@@ -287,7 +292,7 @@ onMounted(async () => {
:closable="false"
type="info"
show-icon
:title="`${createModeMeta?.label}:${createModeMeta?.hint}。分成三项合计应为 100%。`"
:title="`${createModeMeta?.label}:${createModeMeta?.hint}。${createForm.mode === 'site_self_sell' ? '本站分成与服务费合计应为 100%。' : '分成三项合计应为 100%。'}`"
style="margin-bottom: 16px"
/>
<el-form :model="createForm" label-width="120px">
@@ -307,7 +312,7 @@ onMounted(async () => {
</el-select>
</el-form-item>
<el-form-item label="分销模式">
<el-select v-model="createForm.mode" style="width: 100%">
<el-select v-model="createForm.mode" style="width: 100%" @change="handleModeChange">
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
@@ -316,7 +321,10 @@ onMounted(async () => {
/>
</el-select>
</el-form-item>
<el-form-item :label="createForm.mode === 'three_party' ? '收号站分成' : '收号分成'">
<el-form-item
v-if="createForm.mode !== 'site_self_sell'"
:label="createForm.mode === 'three_party' ? '收号站分成' : '收号分成'"
>
<el-input-number
v-model="createForm.receive_ratio"
:min="0"
@@ -325,7 +333,15 @@ onMounted(async () => {
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="createForm.mode === 'three_party' ? '卖出站分成' : '卖出分成'">
<el-form-item
:label="
createForm.mode === 'site_self_sell'
? '本站分成'
: createForm.mode === 'three_party'
? '卖出站分成'
: '卖出分成'
"
>
<el-input-number
v-model="createForm.sell_ratio"
:min="0"
@@ -343,18 +359,6 @@ onMounted(async () => {
style="width: 100%"
/>
</el-form-item>
<el-form-item label="通道费率 BPS">
<el-input-number
v-model="createForm.payment_fee_bps"
:min="0"
:max="10000"
:step="1"
style="width: 100%"
/>
<div class="form-hint">
当前 {{ formatPaymentFeeBps(createForm.payment_fee_bps) }},由主站承担,不从利润里扣。
</div>
</el-form-item>
<el-form-item label="征收税费">
<el-switch v-model="createForm.tax_enabled" />
</el-form-item>
@@ -447,22 +451,4 @@ onMounted(async () => {
background: #eff6ff;
color: #1d4ed8;
}
.fee-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.fee-cell small {
color: #9ca3af;
font-size: 12px;
}
.form-hint {
margin-top: 4px;
color: #6b7280;
font-size: 12px;
line-height: 1.4;
}
</style>
@@ -7,6 +7,7 @@ import {
fetchDistributionSettlementItems,
createDistributionSettlementBatch,
settleDistributionBatch,
cancelDistributionBatch,
fetchDistributionSites,
type DistributionSettlementBatch,
type DistributionSettlementItem,
@@ -104,6 +105,21 @@ async function handleSettle(row: DistributionSettlementBatch) {
}
}
async function handleCancel(row: DistributionSettlementBatch) {
try {
await ElMessageBox.confirm(
`作废批次 ${row.batch_no} 后,其中订单可重新进入自动释放或新的人工批次。`,
'作废未入账批次',
{ type: 'warning', confirmButtonText: '确认作废', cancelButtonText: '取消' }
)
await cancelDistributionBatch(row.id)
ElMessage.success('批次已作废')
await loadBatches()
} catch {
// cancelled or error handled
}
}
async function handleViewItems(row: DistributionSettlementBatch) {
currentBatch.value = row
itemsDialogVisible.value = true
@@ -119,6 +135,7 @@ async function handleViewItems(row: DistributionSettlementBatch) {
function batchStatus(row: DistributionSettlementBatch) {
if (row.status === 'settled') return { label: '已释放入账', type: 'success' as const }
if (row.status === 'cancelled') return { label: '已作废', type: 'info' as const }
if (row.status === 'pending' || row.status === 'draft')
return { label: '待入账', type: 'warning' as const }
return { label: row.status, type: 'info' as const }
@@ -193,17 +210,25 @@ onMounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleViewItems(row)">查看明细</el-button>
<el-button
v-if="row.status !== 'settled'"
v-if="row.status !== 'settled' && row.status !== 'cancelled'"
link
type="success"
@click="handleSettle(row)"
>
立即释放入账
</el-button>
<el-button
v-if="row.status !== 'settled' && row.status !== 'cancelled'"
link
type="danger"
@click="handleCancel(row)"
>
作废
</el-button>
</template>
</el-table-column>
</el-table>
@@ -63,9 +63,6 @@ const onboardForm = reactive({
admin_password: '',
admin_nickname: '',
member_type: 'operator',
distributor_receive_ratio: 35,
platform_sell_ratio: 40,
service_fee_value: 25,
settlement_hold_days: 7,
display_account_enabled: true,
tax_enabled: false,
@@ -105,9 +102,6 @@ function handleOpenOnboard() {
onboardForm.admin_password = ''
onboardForm.admin_nickname = ''
onboardForm.member_type = 'operator'
onboardForm.distributor_receive_ratio = 35
onboardForm.platform_sell_ratio = 40
onboardForm.service_fee_value = 25
onboardForm.settlement_hold_days = 7
onboardForm.display_account_enabled = true
onboardForm.tax_enabled = false
@@ -129,9 +123,6 @@ async function handleOnboardSubmit() {
admin_password: onboardForm.admin_password,
admin_nickname: onboardForm.admin_nickname.trim() || undefined,
member_type: onboardForm.member_type,
distributor_receive_ratio: Number(onboardForm.distributor_receive_ratio),
platform_sell_ratio: Number(onboardForm.platform_sell_ratio),
service_fee_value: Number(onboardForm.service_fee_value),
settlement_hold_days: Number(onboardForm.settlement_hold_days),
display_account_enabled: onboardForm.display_account_enabled,
tax_enabled: onboardForm.tax_enabled,
@@ -189,8 +180,7 @@ onMounted(() => {
<div>
<h2 class="page-title">分销站点管理</h2>
<p class="page-subtitle">
开通分销站、配置结算冷却期。利润按 35% 收号 / 40% 卖出 / 25%
服务费切分,通道费由主站承担。
开通分销站、配置结算冷却期。利润按 35% 收号 / 40% 卖出 / 25% 服务费切分。
</p>
</div>
<div class="header-actions">
@@ -325,50 +315,15 @@ onMounted(() => {
</el-col>
</el-row>
<div class="form-section-title">3. 分站收号规则与冷却期</div>
<div class="form-section-title">3. 结算冷却期与展示</div>
<el-alert
type="info"
:closable="false"
show-icon
title="此处只生成「两方 · 分站收号」草稿。利润 = 净付款 − 号主 − 加价;通道费 0.5% 由主站承担。另外两种模式请到规则页按全局默认启用或单独配置。"
title="开通后四种分成模式均按默认生效:收号 35% / 卖出 40% / 服务费 25%(本站自卖为 75% + 25%)。之后可在「分销规则」页单独调整。"
style="margin-bottom: 12px"
/>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="分站收号分成 (%)">
<el-input-number
v-model="onboardForm.distributor_receive_ratio"
:min="0"
:max="100"
:step="0.1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="主站卖出分成 (%)">
<el-input-number
v-model="onboardForm.platform_sell_ratio"
:min="0"
:max="100"
:step="0.1"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="平台服务费 (%)">
<el-input-number
v-model="onboardForm.service_fee_value"
:min="0"
:max="100"
:step="1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结算冷却期 (天)">
<el-input-number
@@ -380,19 +335,15 @@ onMounted(() => {
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="展示主站账号">
<el-switch v-model="onboardForm.display_account_enabled" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否征税">
<el-switch v-model="onboardForm.tax_enabled" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="是否征税">
<el-switch v-model="onboardForm.tax_enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onboardDialogVisible = false">取消</el-button>
@@ -425,10 +376,9 @@ onMounted(() => {
<el-descriptions-item label="分销后台地址">
<code>http://{{ windowHost }}:5190/login</code>
</el-descriptions-item>
<el-descriptions-item label="初始规则">
分站收号 {{ onboardResult.rule.distributor_receive_ratio }}% · 主站卖出
{{ onboardResult.rule.platform_sell_ratio }}% · 服务费
{{ onboardResult.rule.service_fee_value }}%(草稿,通道费主站承担)
<el-descriptions-item label="初始分成规则">
四种模式均已按默认生效:收号 35% / 卖出 40% / 服务费 25%(本站自卖 75% +
25%),可到规则页调整。
</el-descriptions-item>
<el-descriptions-item label="结算冷却期">
{{ onboardResult.site.settlement_hold_days }} 天