fix(admin): 主站分销管理按最新分成与释放口径改展示

规则页按模式展示收号/卖出/服务费,通道费标明主站承担;订单改为释放状态筛选;结算区分自动与人工批次。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-10 17:36:49 +08:00
co-authored by Cursor
parent 647fdc0b4a
commit e621722218
10 changed files with 677 additions and 138 deletions
+9 -7
View File
@@ -145,13 +145,15 @@ type DistributionOrderDTO struct {
}
type DistributionOrderListQuery struct {
SiteID uint64
Kind string
Role string
Status string
Keyword string
Page int
Size int
SiteID uint64
Kind string
Role string
Status string
Mode string
FinancialStatus string
Keyword string
Page int
Size int
}
type DistributionOrderPage struct {
@@ -144,13 +144,15 @@ func (h *Handler) ListOrders(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{
SiteID: scoped,
Kind: c.Query("kind"),
Role: c.Query("role"),
Status: c.Query("status"),
Keyword: c.Query("keyword"),
Page: page,
Size: size,
SiteID: scoped,
Kind: c.Query("kind"),
Role: c.Query("role"),
Status: c.Query("status"),
Mode: c.Query("mode"),
FinancialStatus: c.Query("financial_status"),
Keyword: c.Query("keyword"),
Page: page,
Size: size,
})
if err != nil {
writeError(c, err)
@@ -331,9 +331,16 @@ func (r *Repository) UpdateSite(ctx context.Context, adminID, id uint64, req Upd
}
func (r *Repository) ListRules(ctx context.Context, adminID uint64, siteID *uint64, mode string) ([]RuleDTO, error) {
db := r.db.WithContext(ctx).Model(&model.DistributionRuleVersion{}).Order("effective_from DESC, id DESC")
db := r.db.WithContext(ctx).Model(&model.DistributionRuleVersion{}).
Order("CASE WHEN site_id IS NULL THEN 0 ELSE 1 END ASC, site_id ASC, mode ASC, version DESC, id DESC")
if siteID == nil {
db = db.Where("site_id IS NULL")
global, err := r.hasGlobalDistributionAccess(ctx, adminID)
if err != nil {
return nil, err
}
if !global {
db = db.Where("site_id IS NULL")
}
} else {
if err := r.ensureSiteAccess(ctx, adminID, *siteID); err != nil {
return nil, err
@@ -475,6 +482,12 @@ func (r *Repository) ListOrders(ctx context.Context, query DistributionOrderList
if query.Status != "" {
base = base.Where("ro.status = ?", query.Status)
}
if mode := strings.TrimSpace(query.Mode); mode != "" {
base = base.Where("ro.distribution_mode = ?", mode)
}
if financialStatus := strings.TrimSpace(query.FinancialStatus); financialStatus != "" {
base = base.Where("f.settlement_status = ?", financialStatus)
}
if strings.TrimSpace(query.Keyword) != "" {
kw := "%" + strings.TrimSpace(query.Keyword) + "%"
base = base.Where("(ro.order_no LIKE ? OR rl.listing_no LIKE ? OR ga.title LIKE ? OR u.phone LIKE ?)", kw, kw, kw, kw)
@@ -42,7 +42,11 @@ export interface DistributionOrder {
status: string
refund_status: string
settlement_status: string
distributor_settlement_status?: string
release_at?: string
distribution_mode: string
gross_paid_amount_cent: number
refund_amount_cent: number
net_paid_amount_cent: number
payment_fee_cent: number
service_fee_cent: number
@@ -194,6 +198,8 @@ export async function updateDistributionRuleStatus(id: number, status: string) {
export async function fetchDistributionOrders(params: {
kind?: string
status?: string
mode?: string
financialStatus?: string
siteID?: number
page?: number
pageSize?: number
@@ -204,6 +210,8 @@ export async function fetchDistributionOrders(params: {
params: {
kind: params.kind || undefined,
status: params.status || undefined,
mode: params.mode || undefined,
financial_status: params.financialStatus || undefined,
site_id: params.siteID || undefined,
page: params.page || 1,
page_size: params.pageSize || 20,
@@ -0,0 +1,112 @@
import type { DistributionRule } from '@/features/admin/api/adminDistribution'
export const DISTRIBUTION_MODES = [
{
value: 'two_party_distributor_receive',
label: '两方 · 分站收号',
hint: '主站下单,货在分站',
},
{
value: 'two_party_platform_receive',
label: '两方 · 分站卖出',
hint: '分站卖主站货',
},
{
value: 'three_party',
label: '三方分账',
hint: '分站卖另一分站的货',
},
] as const
const MODE_LABELS: Record<string, string> = Object.fromEntries(
DISTRIBUTION_MODES.map(item => [item.value, item.label])
)
const PARTY_ROLE_LABELS: Record<string, string> = {
distributor_receive: '分站收号',
platform_sell: '主站卖出',
platform_receive: '主站收号',
distributor_sell: '分站卖出',
distributor_a: '收号站',
distributor_b: '卖出站',
}
export function distributionModeLabel(mode?: string) {
if (!mode) return '-'
return MODE_LABELS[mode] || mode
}
export function distributionPartyRoleLabel(role?: string) {
if (!role) return '-'
return PARTY_ROLE_LABELS[role] || role
}
export function formatRatioPercent(value?: number) {
const n = Number(value || 0)
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':
return [
{ label: '分站收号', value: formatRatioPercent(rule.distributor_receive_ratio) },
{ label: '主站卖出', value: formatRatioPercent(rule.platform_sell_ratio) },
]
case 'two_party_platform_receive':
return [
{ label: '主站收号', value: formatRatioPercent(rule.platform_receive_ratio) },
{ label: '分站卖出', value: formatRatioPercent(rule.distributor_sell_ratio) },
]
case 'three_party':
return [
{ label: '收号站', value: formatRatioPercent(rule.distributor_a_ratio) },
{ label: '卖出站', value: formatRatioPercent(rule.distributor_b_ratio) },
]
default:
return []
}
}
export function formatServiceFee(
rule: Pick<DistributionRule, 'service_fee_value' | 'service_fee_unit'>
) {
if (rule.service_fee_unit === 'fixed_amount') {
return `${Number(rule.service_fee_value || 0).toFixed(2)} 元/单`
}
return formatRatioPercent(rule.service_fee_value)
}
export function settlementBatchKind(batchNo?: string) {
if (batchNo?.startsWith('DA')) {
return { label: '自动释放', type: 'success' as const }
}
if (batchNo?.startsWith('DS')) {
return { label: '人工批次', type: 'warning' as const }
}
return { label: '结算批次', type: 'info' as const }
}
export function distributorReleaseLabel(row: {
distributor_settlement_status?: string
release_at?: string
}) {
if (row.distributor_settlement_status === 'settled') {
return { label: '已释放入账', type: 'success' as const }
}
if (row.distributor_settlement_status === 'pending' && row.release_at) {
const date = new Date(row.release_at)
const text = Number.isNaN(date.getTime())
? '待释放'
: `待释放 ${date.toLocaleDateString('zh-CN', { month: 'numeric', day: 'numeric' })}`
return { label: text, type: 'warning' as const }
}
if (row.distributor_settlement_status === 'pending') {
return { label: '待释放', type: 'info' as const }
}
return { label: '未进入释放', type: 'info' as const }
}
@@ -7,8 +7,14 @@ import {
type DistributionOrder,
type DistributionSite,
} from '@/features/admin/api/adminDistribution'
import {
DISTRIBUTION_MODES,
distributionModeLabel,
distributorReleaseLabel,
} from '@/features/admin/utils/distributionLabels'
import { formatCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { orderStatusLabel, refundStatusLabel } from '@/shared/utils/statusLabels'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
@@ -18,18 +24,13 @@ const total = ref(0)
const query = reactive({
siteID: undefined as number | undefined,
kind: '' as string,
status: '' as string,
mode: '',
status: '',
financialStatus: '',
page: 1,
pageSize: 20,
})
const modeMap: Record<string, string> = {
two_party_distributor_receive: '收号',
two_party_platform_receive: '售出',
three_party: '三方',
}
async function loadSites() {
try {
sites.value = await fetchDistributionSites()
@@ -43,8 +44,9 @@ async function loadOrders() {
try {
const res = await fetchDistributionOrders({
siteID: query.siteID,
kind: query.kind || undefined,
mode: query.mode || undefined,
status: query.status || undefined,
financialStatus: query.financialStatus || undefined,
page: query.page,
pageSize: query.pageSize,
})
@@ -64,15 +66,16 @@ function handleSearch() {
function handleReset() {
query.siteID = undefined
query.kind = ''
query.mode = ''
query.status = ''
query.financialStatus = ''
query.page = 1
loadOrders()
}
function getSiteName(siteId: number) {
const site = sites.value.find(s => s.id === siteId)
return site ? `${site.site_name}` : `站点 #${siteId}`
return site ? site.site_name : `站点 #${siteId}`
}
onMounted(async () => {
@@ -85,9 +88,9 @@ onMounted(async () => {
<div class="distribution-orders-view">
<div class="page-header">
<div>
<h2 class="page-title">分销订单监控</h2>
<h2 class="page-title">分销订单</h2>
<p class="page-subtitle">
全览各合作分销站点的收号与售出订单明细、渠道手续费、单笔服务费及利润分成流水。
查看分销订单的利润基数、主站承担的通道费,以及冷却期释放状态。不要把订单履约结算和分销入账混为一谈。
</p>
</div>
</div>
@@ -104,17 +107,33 @@ onMounted(async () => {
/>
</el-select>
</el-form-item>
<el-form-item label="交易类型">
<el-select v-model="query.kind" clearable placeholder="全部模式" style="width: 140px">
<el-option label="分销收号" value="receive" />
<el-option label="分销售出" value="sold" />
<el-option label="全部" value="all" />
<el-form-item label="分销模式">
<el-select v-model="query.mode" clearable placeholder="全部模式" style="width: 180px">
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="结算状态">
<el-select v-model="query.status" clearable placeholder="全部状态" style="width: 140px">
<el-option label="待结算" value="pending" />
<el-option label="已结算" value="settled" />
<el-form-item label="订单状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width: 140px">
<el-option label="已完成" value="completed" />
<el-option label="使用中" value="renting" />
<el-option label="申诉中" value="disputing" />
<el-option label="已关闭" value="closed" />
</el-select>
</el-form-item>
<el-form-item label="释放状态">
<el-select
v-model="query.financialStatus"
clearable
placeholder="全部"
style="width: 140px"
>
<el-option label="待释放" value="pending" />
<el-option label="已释放入账" value="settled" />
</el-select>
</el-form-item>
<el-form-item>
@@ -127,53 +146,62 @@ onMounted(async () => {
<el-card shadow="never" style="margin-top: 16px">
<el-table v-loading="loading" :data="orders" stripe style="width: 100%">
<el-table-column label="订单号" prop="order_no" min-width="160" />
<el-table-column label="分销站点" min-width="140">
<el-table-column label="分销站点" min-width="130">
<template #default="{ row }">
<el-tag size="small">{{ getSiteName(row.site_id) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="模式" width="90" align="center">
<el-table-column label="模式" min-width="140">
<template #default="{ row }">
<el-tag type="info" size="small">
{{ modeMap[row.distribution_mode] || row.distribution_mode }}
</el-tag>
{{ distributionModeLabel(row.distribution_mode) }}
</template>
</el-table-column>
<el-table-column label="订单实付" min-width="110" align="right">
<el-table-column label="净付款" min-width="100" align="right">
<template #default="{ row }">
<strong>{{ formatCent(row.net_paid_amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="渠道手续费" min-width="100" align="right">
<el-table-column label="分站加价" min-width="100" align="right">
<template #default="{ row }">
{{ formatCent(row.payment_fee_cent) }}
{{ formatCent(row.markup_amount_cent) }}
</template>
</el-table-column>
<el-table-column label="固定服务费" min-width="100" align="right">
<el-table-column label="利润基数" min-width="100" align="right">
<template #default="{ row }">
{{ 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) }}
</template>
</el-table-column>
<el-table-column label="税费" min-width="90" align="right">
<template #default="{ row }">
{{ formatCent(row.tax_amount_cent) }}
</template>
</el-table-column>
<el-table-column label="订单状态" width="100" align="center">
<el-table-column label="订单状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'completed' ? 'success' : 'warning'" size="small">
{{ row.status }}
{{ orderStatusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="结算状态" width="100" align="center">
<el-table-column label="退款" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.settlement_status === 'settled' ? 'success' : 'info'" size="small">
{{ row.settlement_status === 'settled' ? '已结算' : '待结算' }}
<span>{{ refundStatusLabel(row.refund_status) }}</span>
</template>
</el-table-column>
<el-table-column label="分销入账" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="distributorReleaseLabel(row).type" size="small">
{{ distributorReleaseLabel(row).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" min-width="160">
<el-table-column label="下单时间" min-width="160">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
@@ -215,6 +243,7 @@ onMounted(async () => {
font-size: 13px;
color: #6b7280;
margin: 0;
max-width: 720px;
}
.filter-card {
@@ -226,4 +255,9 @@ onMounted(async () => {
justify-content: flex-end;
margin-top: 16px;
}
.muted {
color: #9ca3af;
font-size: 12px;
}
</style>
@@ -1,26 +1,49 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
fetchDistributionRules,
fetchDistributionSites,
updateDistributionRuleStatus,
createDistributionRule,
type DistributionRule,
type DistributionSite,
type CreateRulePayload,
} from '@/features/admin/api/adminDistribution'
import {
DISTRIBUTION_MODES,
distributionModeLabel,
formatPaymentFeeBps,
formatServiceFee,
ruleSplitParts,
} from '@/features/admin/utils/distributionLabels'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
const sites = ref<DistributionSite[]>([])
const selectedSiteId = ref<number | undefined>(undefined)
const selectedMode = ref('')
const rules = ref<DistributionRule[]>([])
const modeMap: Record<string, string> = {
two_party_distributor_receive: '两方-分销收号',
two_party_platform_receive: '两方-分销卖出',
three_party: '三方分销',
}
const createVisible = ref(false)
const creating = ref(false)
const createForm = reactive({
site_id: undefined as number | undefined,
mode: 'two_party_distributor_receive',
receive_ratio: 35,
sell_ratio: 40,
service_fee_value: 25,
payment_fee_bps: 50,
tax_enabled: false,
tax_rate_bps: 100,
activate: true,
})
const createModeMeta = computed(
() => DISTRIBUTION_MODES.find(item => item.value === createForm.mode) ?? DISTRIBUTION_MODES[0]
)
async function loadSites() {
try {
@@ -33,7 +56,10 @@ async function loadSites() {
async function loadRules() {
loading.value = true
try {
rules.value = await fetchDistributionRules(selectedSiteId.value)
rules.value = await fetchDistributionRules(
selectedSiteId.value,
selectedMode.value || undefined
)
} catch (err) {
ElMessage.error(readError(err, '获取分销规则列表失败'))
} finally {
@@ -41,6 +67,18 @@ async function loadRules() {
}
}
function getSiteName(siteId?: number) {
if (!siteId) return '全局默认'
const site = sites.value.find(s => s.id === siteId)
return site ? `${site.site_name} (${site.site_code})` : `站点 #${siteId}`
}
function statusMeta(status: string) {
if (status === 'active') return { label: '生效中', type: 'success' as const }
if (status === 'draft') return { label: '草稿', type: 'info' as const }
return { label: '已停用', type: 'danger' as const }
}
async function handleToggleStatus(row: DistributionRule) {
const next = row.status === 'active' ? 'disabled' : 'active'
try {
@@ -52,10 +90,69 @@ async function handleToggleStatus(row: DistributionRule) {
}
}
function getSiteName(siteId?: number) {
if (!siteId) return '全局默认'
const site = sites.value.find(s => s.id === siteId)
return site ? `${site.site_name} (${site.site_code})` : `站点 #${siteId}`
function resetCreateForm() {
createForm.site_id = selectedSiteId.value
createForm.mode = selectedMode.value || 'two_party_distributor_receive'
createForm.receive_ratio = 35
createForm.sell_ratio = 40
createForm.service_fee_value = 25
createForm.payment_fee_bps = 50
createForm.tax_enabled = false
createForm.tax_rate_bps = 100
createForm.activate = true
}
function handleOpenCreate() {
resetCreateForm()
createVisible.value = true
}
function buildCreatePayload(): CreateRulePayload {
const payload: CreateRulePayload = {
site_id: createForm.site_id || undefined,
mode: createForm.mode,
service_fee_value: Number(createForm.service_fee_value),
service_fee_unit: 'percent',
payment_fee_bps: Number(createForm.payment_fee_bps),
tax_enabled: createForm.tax_enabled,
tax_rate_bps: createForm.tax_enabled ? Number(createForm.tax_rate_bps) : 100,
}
if (createForm.mode === 'two_party_distributor_receive') {
payload.distributor_receive_ratio = Number(createForm.receive_ratio)
payload.platform_sell_ratio = Number(createForm.sell_ratio)
} 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 {
payload.distributor_a_ratio = Number(createForm.receive_ratio)
payload.distributor_b_ratio = Number(createForm.sell_ratio)
}
return payload
}
async function handleCreate() {
const total =
Number(createForm.receive_ratio) +
Number(createForm.sell_ratio) +
Number(createForm.service_fee_value)
if (Math.abs(total - 100) > 0.05) {
ElMessage.warning(`收号 + 卖出 + 服务费应为 100%,当前为 ${total}%`)
return
}
creating.value = true
try {
const created = await createDistributionRule(buildCreatePayload())
if (createForm.activate) {
await updateDistributionRuleStatus(created.id, 'active')
}
ElMessage.success(createForm.activate ? '规则已创建并启用' : '规则草稿已创建')
createVisible.value = false
await loadRules()
} catch (err) {
ElMessage.error(readError(err, '创建分销规则失败'))
} finally {
creating.value = false
}
}
onMounted(async () => {
@@ -70,15 +167,15 @@ onMounted(async () => {
<div>
<h2 class="page-title">分销规则配置</h2>
<p class="page-subtitle">
配置与维护各分销站点的佣金分成比例、单笔服务费、支付通道手续费与税费规则。
按模式配置利润分成。利润 = 净付款 − 号主 − 加价;通道费由主站承担,只记账不进分成。
</p>
</div>
<div class="header-filters">
<el-select
v-model="selectedSiteId"
clearable
placeholder="按分销站点筛选"
style="width: 240px"
placeholder="全部站点(含全局)"
style="width: 220px"
@change="loadRules"
>
<el-option
@@ -88,44 +185,75 @@ onMounted(async () => {
:value="site.id"
/>
</el-select>
<el-select
v-model="selectedMode"
clearable
placeholder="全部分销模式"
style="width: 200px"
@change="loadRules"
>
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-button type="primary" :icon="Plus" @click="handleOpenCreate">新建规则</el-button>
</div>
</div>
<el-alert
type="info"
:closable="false"
show-icon
class="rules-alert"
title="默认 35% 收号 / 40% 卖出 / 25% 服务费。站点专属规则优先于全局默认;通道费默认 0.5%,从主站服务费与角色分成中承担。"
/>
<el-card shadow="never">
<el-table v-loading="loading" :data="rules" stripe style="width: 100%">
<el-table-column label="适用站点" min-width="160">
<el-table-column label="适用站点" min-width="170">
<template #default="{ row }">
<el-tag :type="row.site_id ? 'primary' : 'info'" size="small">
{{ getSiteName(row.site_id) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="分销模式" prop="mode" min-width="140">
<el-table-column label="分销模式" min-width="160">
<template #default="{ row }">
{{ modeMap[row.mode] || row.mode }}
<div class="mode-cell">
<strong>{{ distributionModeLabel(row.mode) }}</strong>
<small>v{{ row.version }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="版本号" prop="version" width="80" align="center">
<template #default="{ row }"> v{{ row.version }} </template>
<el-table-column label="利润分成" min-width="280">
<template #default="{ row }">
<div class="split-cell">
<span v-for="part in ruleSplitParts(row)" :key="part.label" class="split-chip">
{{ part.label }} <strong>{{ part.value }}</strong>
</span>
<span class="split-chip service"
>服务费 <strong>{{ formatServiceFee(row) }}</strong></span
>
</div>
</template>
</el-table-column>
<el-table-column label="分销商收号分成" min-width="130" align="right">
<template #default="{ row }"> {{ row.distributor_receive_ratio }}% </template>
<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="120" align="right">
<template #default="{ row }"> {{ row.platform_sell_ratio }}% </template>
</el-table-column>
<el-table-column label="服务费" min-width="110" align="right">
<template #default="{ row }"> {{ row.service_fee_value }}% </template>
</el-table-column>
<el-table-column label="支付手续费" min-width="110" align="right">
<template #default="{ row }"> {{ row.payment_fee_bps / 100 }}% </template>
</el-table-column>
<el-table-column label="税费率" min-width="100" align="center">
<el-table-column label="税费" min-width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.tax_enabled" type="warning" size="small">
{{ row.tax_rate_bps / 100 }}%
</el-tag>
<el-tag v-else type="info" size="small">免税</el-tag>
<el-tag v-else type="info" size="small">关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="生效时间" min-width="160">
@@ -135,8 +263,8 @@ onMounted(async () => {
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'" size="small">
{{ row.status === 'active' ? '生效中' : '已停用' }}
<el-tag :type="statusMeta(row.status).type" size="small">
{{ statusMeta(row.status).label }}
</el-tag>
</template>
</el-table-column>
@@ -153,6 +281,101 @@ onMounted(async () => {
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="createVisible" title="新建分销规则" width="560px" destroy-on-close>
<el-alert
:closable="false"
type="info"
show-icon
:title="`${createModeMeta?.label}:${createModeMeta?.hint}。分成三项合计应为 100%。`"
style="margin-bottom: 16px"
/>
<el-form :model="createForm" label-width="120px">
<el-form-item label="适用站点">
<el-select
v-model="createForm.site_id"
clearable
placeholder="全局默认"
style="width: 100%"
>
<el-option
v-for="site in sites"
:key="site.id"
:label="`${site.site_name} (${site.site_code})`"
:value="site.id"
/>
</el-select>
</el-form-item>
<el-form-item label="分销模式">
<el-select v-model="createForm.mode" style="width: 100%">
<el-option
v-for="item in DISTRIBUTION_MODES"
:key="item.value"
:label="`${item.label}(${item.hint})`"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item :label="createForm.mode === 'three_party' ? '收号站分成' : '收号分成'">
<el-input-number
v-model="createForm.receive_ratio"
:min="0"
:max="100"
:step="0.5"
style="width: 100%"
/>
</el-form-item>
<el-form-item :label="createForm.mode === 'three_party' ? '卖出站分成' : '卖出分成'">
<el-input-number
v-model="createForm.sell_ratio"
:min="0"
:max="100"
:step="0.5"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="平台服务费">
<el-input-number
v-model="createForm.service_fee_value"
:min="0"
:max="100"
:step="0.5"
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>
<el-form-item v-if="createForm.tax_enabled" label="税费率 BPS">
<el-input-number
v-model="createForm.tax_rate_bps"
:min="0"
:max="10000"
:step="1"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="立即启用">
<el-switch v-model="createForm.activate" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="createVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreate">保存规则</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -165,7 +388,8 @@ onMounted(async () => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
margin-bottom: 16px;
gap: 16px;
}
.page-title {
@@ -180,4 +404,65 @@ onMounted(async () => {
color: #6b7280;
margin: 0;
}
.header-filters {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.rules-alert {
margin-bottom: 16px;
}
.mode-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.mode-cell small {
color: #9ca3af;
font-size: 12px;
}
.split-cell {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.split-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 999px;
background: #f3f4f6;
color: #374151;
font-size: 12px;
}
.split-chip.service {
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>
@@ -12,6 +12,10 @@ import {
type DistributionSettlementItem,
type DistributionSite,
} from '@/features/admin/api/adminDistribution'
import {
distributionPartyRoleLabel,
settlementBatchKind,
} from '@/features/admin/utils/distributionLabels'
import { formatCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
@@ -33,6 +37,8 @@ const itemsLoading = ref(false)
const currentBatch = ref<DistributionSettlementBatch | null>(null)
const items = ref<DistributionSettlementItem[]>([])
const selectedSite = () => sites.value.find(site => site.id === selectedSiteId.value)
async function loadSites() {
try {
const list = await fetchDistributionSites()
@@ -73,7 +79,7 @@ async function handleCreateBatch() {
creating.value = true
try {
await createDistributionSettlementBatch(createForm.site_id, start, end)
ElMessage.success('结算批次生成成功')
ElMessage.success('人工结算批次已生成,可立即释放入账')
createDialogVisible.value = false
await loadBatches()
} catch (err) {
@@ -86,12 +92,12 @@ async function handleCreateBatch() {
async function handleSettle(row: DistributionSettlementBatch) {
try {
await ElMessageBox.confirm(
`确定将结算批次 ${row.batch_no} 标记为已打款结算吗?结算总额为 ${formatCent(row.total_amount_cent)}。`,
'确认结算',
{ type: 'warning', confirmButtonText: '确认标记已结算', cancelButtonText: '取消' }
`确认将批次 ${row.batch_no} 的 ${formatCent(row.total_amount_cent)} 立即转入站点可提现余额?人工批次会绕过冷却期。`,
'立即释放入账',
{ type: 'warning', confirmButtonText: '确认入账', cancelButtonText: '取消' }
)
await settleDistributionBatch(row.id)
ElMessage.success('批次已成功标记为已结算')
ElMessage.success('批次已释放到站点可提现余额')
await loadBatches()
} catch {
// cancelled or error handled
@@ -111,6 +117,13 @@ async function handleViewItems(row: DistributionSettlementBatch) {
}
}
function batchStatus(row: DistributionSettlementBatch) {
if (row.status === 'settled') return { label: '已释放入账', type: 'success' as const }
if (row.status === 'pending' || row.status === 'draft')
return { label: '待入账', type: 'warning' as const }
return { label: row.status, type: 'info' as const }
}
onMounted(() => {
loadSites()
})
@@ -120,9 +133,9 @@ onMounted(() => {
<div class="distribution-settlements-view">
<div class="page-header">
<div>
<h2 class="page-title">分销结算管理</h2>
<h2 class="page-title">分销结算对账</h2>
<p class="page-subtitle">
按周期汇总各合作分销站点的佣金分成批次,并执行最终结算出款确认。
冷却期满后系统按站点按天自动出对账单并入账。人工批次用于立即结算,不经过冷却期。
</p>
</div>
<div class="header-actions">
@@ -139,38 +152,48 @@ onMounted(() => {
:value="site.id"
/>
</el-select>
<el-button type="primary" :icon="Plus" @click="handleOpenCreate"> 生成结算批次 </el-button>
<el-button type="primary" :icon="Plus" @click="handleOpenCreate"> 生成人工批次 </el-button>
</div>
</div>
<el-alert
type="info"
:closable="false"
show-icon
class="settlement-alert"
:title="`当前站点冷却期 ${selectedSite()?.settlement_hold_days ?? 7} 天。DA 开头为自动释放对账单,DS 开头为人工立即结算。`"
/>
<el-card shadow="never">
<el-table v-loading="loading" :data="batches" stripe style="width: 100%">
<el-table-column label="结算批次号" prop="batch_no" min-width="180" />
<el-table-column label="周期开始时间" min-width="160">
<el-table-column label="批次号" prop="batch_no" min-width="200">
<template #default="{ row }">
{{ formatDateTime(row.period_start) }}
<div class="batch-no-cell">
<code>{{ row.batch_no }}</code>
<el-tag :type="settlementBatchKind(row.batch_no).type" size="small">
{{ settlementBatchKind(row.batch_no).label }}
</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="周期结束时间" min-width="160">
<el-table-column label="对账周期" min-width="220">
<template #default="{ row }">
{{ formatDateTime(row.period_end) }}
{{ formatDateTime(row.period_start) }} ~ {{ formatDateTime(row.period_end) }}
</template>
</el-table-column>
<el-table-column label="应结总金额" min-width="140" align="right">
<el-table-column label="入账金额" min-width="130" align="right">
<template #default="{ row }">
<strong style="color: #67c23a; font-size: 15px">
{{ formatCent(row.total_amount_cent) }}
</strong>
<strong class="amount">{{ formatCent(row.total_amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="批次状态" width="120" align="center">
<el-table-column label="状态" width="120" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'settled' ? 'success' : 'warning'" size="small">
{{ row.status === 'settled' ? '已打款' : '待结算' }}
<el-tag :type="batchStatus(row).type" size="small">
{{ batchStatus(row).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160" fixed="right">
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleViewItems(row)">查看明细</el-button>
<el-button
@@ -179,20 +202,26 @@ onMounted(() => {
type="success"
@click="handleSettle(row)"
>
打款结算
立即释放入账
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 生成结算批次对话框 -->
<el-dialog
v-model="createDialogVisible"
title="生成分销结算批次"
width="500px"
title="生成人工结算批次"
width="520px"
destroy-on-close
>
<el-alert
type="warning"
:closable="false"
show-icon
title="人工批次会把周期内尚未入账的分站份额立即结算,不走冷却期。"
style="margin-bottom: 16px"
/>
<el-form label-width="100px">
<el-form-item label="分销站点">
<el-select v-model="createForm.site_id" style="width: 100%">
@@ -219,29 +248,32 @@ onMounted(() => {
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreateBatch">
立即结算汇总
生成批次
</el-button>
</template>
</el-dialog>
<!-- 结算条目明细对话框 -->
<el-dialog
v-model="itemsDialogVisible"
:title="`结算批次明细 (${currentBatch?.batch_no})`"
width="650px"
:title="`批次明细(${currentBatch?.batch_no})`"
width="680px"
>
<el-table v-loading="itemsLoading" :data="items" size="small" border>
<el-table-column label="订单 ID" prop="order_id" width="100" />
<el-table-column label="分成角色" prop="party_role" min-width="150" />
<el-table-column label="应分金额" min-width="120" align="right">
<el-table-column label="分成角色" min-width="140">
<template #default="{ row }">
{{ distributionPartyRoleLabel(row.party_role) }}
</template>
</el-table-column>
<el-table-column label="入账金额" min-width="120" align="right">
<template #default="{ row }">
<strong>{{ formatCent(row.amount_cent) }}</strong>
</template>
</el-table-column>
<el-table-column label="状态" prop="status" width="100" align="center">
<el-table-column label="状态" width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'settled' ? 'success' : 'info'" size="small">
{{ row.status }}
{{ row.status === 'settled' ? '已入账' : '待入账' }}
</el-tag>
</template>
</el-table-column>
@@ -262,7 +294,8 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
margin-bottom: 16px;
gap: 16px;
}
.page-title {
@@ -276,10 +309,31 @@ onMounted(() => {
font-size: 13px;
color: #6b7280;
margin: 0;
max-width: 640px;
}
.header-actions {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.settlement-alert {
margin-bottom: 16px;
}
.batch-no-cell {
display: flex;
align-items: center;
gap: 8px;
}
.batch-no-cell code {
font-size: 13px;
}
.amount {
color: #16a34a;
font-size: 15px;
}
</style>
@@ -66,6 +66,7 @@ const onboardForm = reactive({
distributor_receive_ratio: 35,
platform_sell_ratio: 40,
service_fee_value: 25,
settlement_hold_days: 7,
display_account_enabled: true,
tax_enabled: false,
tax_rate_bps: 100,
@@ -107,6 +108,7 @@ function handleOpenOnboard() {
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
onboardForm.tax_rate_bps = 100
@@ -130,6 +132,7 @@ async function handleOnboardSubmit() {
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,
tax_rate_bps: onboardForm.tax_enabled ? onboardForm.tax_rate_bps : 0,
@@ -186,7 +189,8 @@ onMounted(() => {
<div>
<h2 class="page-title">分销站点管理</h2>
<p class="page-subtitle">
统一管理全平台合作分销站点,支持一键开通分销商、初始化管理员账号及专属佣金费率。
开通分销站、配置结算冷却期。利润按 35% 收号 / 40% 卖出 / 25%
服务费切分,通道费由主站承担。
</p>
</div>
<div class="header-actions">
@@ -321,10 +325,17 @@ 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% 由主站承担。另外两种模式请到规则页按全局默认启用或单独配置。"
style="margin-bottom: 12px"
/>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="收号分成比例 (%)">
<el-form-item label="分站收号分成 (%)">
<el-input-number
v-model="onboardForm.distributor_receive_ratio"
:min="0"
@@ -335,7 +346,7 @@ onMounted(() => {
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="平台卖号分成 (%)">
<el-form-item label="主站卖出分成 (%)">
<el-input-number
v-model="onboardForm.platform_sell_ratio"
:min="0"
@@ -358,15 +369,30 @@ onMounted(() => {
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="结算冷却期 (天)">
<el-input-number
v-model="onboardForm.settlement_hold_days"
:min="0"
:max="365"
:step="1"
style="width: 100%"
/>
</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>
@@ -400,9 +426,12 @@ onMounted(() => {
<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 }}%
分站收号 {{ onboardResult.rule.distributor_receive_ratio }}% · 主站卖出
{{ onboardResult.rule.platform_sell_ratio }}% · 服务费
{{ onboardResult.rule.service_fee_value }}%(草稿,通道费主站承担)
</el-descriptions-item>
<el-descriptions-item label="结算冷却期">
{{ onboardResult.site.settlement_hold_days }} 天
</el-descriptions-item>
</el-descriptions>
</div>
+1 -1
View File
@@ -158,7 +158,7 @@ const allNavGroups: NavGroup[] = [
permission: 'distribution:order_view',
},
{
label: '分销结算',
label: '结算对账',
to: adminPath('distribution/settlements'),
icon: Coin,
permission: 'distribution:settlement_view',