feat(distribution): 分站可下调收号比例且不超过主站上限

让分销站能按自身能力下调收号比例扩大利润,同时把主站回收比例作为硬边界,避免分站把比例加得比主站更高。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-15 20:35:30 +08:00
co-authored by Cursor
parent 1c86bef765
commit c9d9160ff4
27 changed files with 1159 additions and 106 deletions
@@ -12,6 +12,7 @@ export interface DistributionSite {
tax_rate_bps: number
settlement_hold_days: number
listing_qrcode_url?: string
receive_ratio_delta?: number
created_at: string
}
@@ -152,6 +153,7 @@ export async function updateDistributionSite(
| 'tax_rate_bps'
| 'settlement_hold_days'
| 'listing_qrcode_url'
| 'receive_ratio_delta'
>
>
) {
@@ -10,6 +10,7 @@ import {
Clock,
Picture,
Upload,
Coin,
} from '@element-plus/icons-vue'
import {
fetchDistributionSites,
@@ -45,6 +46,14 @@ const settlementForm = reactive({
settlement_hold_days: defaultSettlementHoldDays.value,
})
const receiveRatioDialogVisible = ref(false)
const receiveRatioSubmitting = ref(false)
const receiveRatioForm = reactive({
site_id: 0,
site_name: '',
receive_ratio_delta: 0,
})
const qrDialogVisible = ref(false)
const qrSubmitting = ref(false)
const qrUploading = ref(false)
@@ -77,6 +86,29 @@ async function handleSettlementSubmit() {
}
}
function handleOpenReceiveRatio(row: DistributionSite) {
receiveRatioForm.site_id = row.id
receiveRatioForm.site_name = row.site_name
receiveRatioForm.receive_ratio_delta = Number(row.receive_ratio_delta || 0)
receiveRatioDialogVisible.value = true
}
async function handleReceiveRatioSubmit() {
receiveRatioSubmitting.value = true
try {
await updateDistributionSite(receiveRatioForm.site_id, {
receive_ratio_delta: Number(receiveRatioForm.receive_ratio_delta),
})
ElMessage.success(`【${receiveRatioForm.site_name}】收号比例下调已更新`)
receiveRatioDialogVisible.value = false
await loadSites()
} catch (err) {
ElMessage.error(readError(err, '更新收号比例失败'))
} finally {
receiveRatioSubmitting.value = false
}
}
function handleOpenQrcode(row: DistributionSite) {
qrForm.site_id = row.id
qrForm.site_name = row.site_name
@@ -257,8 +289,8 @@ onMounted(() => {
<div>
<h2 class="page-title">分销站点管理</h2>
<p class="page-subtitle">
开通分销站、配置结算冷却期与发布群固定二维码。利润按 35% 收号 / 40% 卖出 / 25%
服务费切分。
开通分销站、配置结算冷却期、收号比例上限与发布群固定二维码。利润按 35% 收号 / 40% 卖出 /
25% 服务费切分。
</p>
</div>
<div class="header-actions">
@@ -314,6 +346,17 @@ onMounted(() => {
</el-tag>
</template>
</el-table-column>
<el-table-column label="收号下调" width="110">
<template #default="{ row }">
<el-tag :type="Number(row.receive_ratio_delta || 0) > 0 ? 'warning' : 'info'" size="small">
{{
Number(row.receive_ratio_delta || 0) > 0
? `-${Number(row.receive_ratio_delta).toFixed(1)} 点`
: '与主站一致'
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="发布群二维码" width="120" align="center">
<template #default="{ row }">
<AuthImage
@@ -331,7 +374,7 @@ onMounted(() => {
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="280" fixed="right">
<el-table-column label="操作" width="340" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
@@ -345,6 +388,9 @@ onMounted(() => {
<el-button type="primary" link :icon="Clock" @click="handleOpenSettlement(row)">
结算设置
</el-button>
<el-button type="primary" link :icon="Coin" @click="handleOpenReceiveRatio(row)">
收号比例
</el-button>
<el-button type="primary" link :icon="Picture" @click="handleOpenQrcode(row)">
二维码
</el-button>
@@ -522,6 +568,43 @@ onMounted(() => {
</template>
</el-dialog>
<!-- 分站收号比例(主站为上限) -->
<el-dialog
v-model="receiveRatioDialogVisible"
title="分站收号比例"
width="520px"
destroy-on-close
>
<el-alert
type="info"
:closable="false"
show-icon
title="主站回收比例是上限。下调点数越大,号主底价越高、分站利润空间越大,但不能高于主站比例。0 表示与主站一致。"
style="margin-bottom: 16px"
/>
<el-form label-width="110px">
<el-form-item label="分销站点">
<el-input :model-value="receiveRatioForm.site_name" disabled />
</el-form-item>
<el-form-item label="相对主站下调">
<el-input-number
v-model="receiveRatioForm.receive_ratio_delta"
:min="0"
:max="20"
:step="0.1"
:precision="1"
/>
<span class="settlement-hint">点(0-20)</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="receiveRatioDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="receiveRatioSubmitting" @click="handleReceiveRatioSubmit">
保存设置
</el-button>
</template>
</el-dialog>
<!-- 发布群固定二维码 -->
<el-dialog v-model="qrDialogVisible" title="发布群固定二维码" width="520px" destroy-on-close>
<el-alert
@@ -34,6 +34,7 @@ import {
import type { PublishForm } from '@/shared/types/publish'
import { yuanToCent } from '@/shared/utils/money'
import { dailyLossOptions, formatNumber, roundRatio } from '@/shared/utils/pricing'
import { useDistributionSiteStore } from '@/stores/distributionSite'
const draftSaveDelay = 400
const multiScreenshotLimit = 3
@@ -54,6 +55,9 @@ interface UsePublishFormOptions {
export function usePublishForm(options: UsePublishFormOptions) {
const router = useRouter()
const route = useRoute()
const siteStore = useDistributionSiteStore()
const isDistribution = computed(() => siteStore.isDistribution)
const receiveRatioDelta = computed(() => Number(siteStore.receiveRatioDelta || 0))
const loading = ref(false)
const uploading = ref(false)
const suppressDraftSave = ref(false)
@@ -89,6 +93,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
quantityModes,
screenshotFiles,
selectedSkins,
isDistribution,
receiveRatioDelta,
})
// ========== 上号方式互斥 ==========
@@ -911,6 +917,7 @@ export function usePublishForm(options: UsePublishFormOptions) {
return {
...pricing,
isDistribution,
dailyLossOptions,
formatNumber,
router,
@@ -692,6 +692,14 @@
line-height: 1.5;
}
.ratio-inherit-hint {
margin: 0;
color: #1677ff;
font-size: 11px;
font-weight: 700;
line-height: 1.5;
}
.ratio-mode-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -49,6 +49,7 @@ const {
priceConfig,
fireLevelPlaceholder,
dailyLossMAmount,
isDistribution,
calculatedDefaultSaleRatio,
maxAcceleratedSaleRatio,
calculatedCoinBasePrice,
@@ -694,11 +695,16 @@ const screenshotGuides: Record<string, string> = {
</p>
<div class="ratio-panel">
<div class="ratio-reference">
<span>参考比例</span>
<span>{{ isDistribution ? '主站收号上限' : '参考比例' }}</span>
<strong>{{ calculatedDefaultSaleRatioText }}</strong>
</div>
<p class="ratio-range">{{ saleRatioRangeText }},比例越高出租速度越快,自行选择。</p>
<div class="ratio-mode-grid">
<p v-if="isDistribution" class="ratio-inherit-hint">
本站可下调收号比例换取更高利润,但不能高于主站比例。当前按站点配置计算。
</p>
<p class="ratio-range">
{{ saleRatioRangeText }}{{ isDistribution ? '。' : ',比例越高出租速度越快,自行选择。' }}
</p>
<div v-if="!isDistribution" class="ratio-mode-grid">
<button
type="button"
class="ratio-mode-btn"
@@ -675,6 +675,12 @@
line-height: 1.5;
}
.ratio-inherit-hint {
margin: 8px 0 0 !important;
color: #1677ff !important;
font-weight: 600;
}
.ratio-reference {
display: flex;
align-items: center;
@@ -67,6 +67,7 @@ const {
recommendedDepositAmount,
depositBreakdownItems,
publishTitle,
isDistribution,
hasAcceleratedSaleRatioInput,
isQuantityItemDisabled,
handleSaveDraft,
@@ -557,11 +558,14 @@ function selectDailyLoss(value: string | number) {
<div class="ratio-panel">
<div class="ratio-reference">
<span>参考比例</span>
<span>{{ isDistribution ? '主站收号上限' : '参考比例' }}</span>
<strong>{{ calculatedDefaultSaleRatioText }}</strong>
</div>
<p>{{ saleRatioRangeText }},比例越高出租速度越快。</p>
<div class="ratio-mode-grid">
<p v-if="isDistribution" class="ratio-inherit-hint">
本站可下调收号比例换取更高利润,但不能高于主站比例。当前按站点配置计算。
</p>
<p>{{ saleRatioRangeText }}{{ isDistribution ? '。' : ',比例越高出租速度越快。' }}</p>
<div v-if="!isDistribution" class="ratio-mode-grid">
<button
type="button"
class="ratio-mode-btn"
@@ -19,6 +19,7 @@ import {
hasAcceleratedSaleRatioInput as hasAcceleratedSaleRatioValue,
isQuantityItemDisabledForInsurance,
readFinalSaleRatio,
applySiteReceiveRatioDelta,
roundMoney,
roundRatio,
} from '@/shared/utils/pricing'
@@ -31,6 +32,8 @@ export function usePricingCalculator(options: {
quantityModes: Record<QuantityKey, ChargeMode>
screenshotFiles: Record<ScreenshotKey, string[]>
selectedSkins: Ref<string[]>
isDistribution?: Ref<boolean>
receiveRatioDelta?: Ref<number>
}) {
const serverOptions = computed(() => options.publishOptions.value.server_options)
const faceOptions = computed(() => options.publishOptions.value.face_options)
@@ -85,13 +88,20 @@ export function usePricingCalculator(options: {
const maxAcceleratedSaleRatio = computed(() =>
calculatedDefaultSaleRatio.value > 0 ? roundRatio(calculatedDefaultSaleRatio.value + 10) : 0
)
const calculatedRatio = computed(() =>
readFinalSaleRatio(
const siteReceiveRatioDelta = computed(() =>
Math.max(0, Number(options.receiveRatioDelta?.value || 0))
)
const calculatedRatio = computed(() => {
if (calculatedDefaultSaleRatio.value <= 0) return 0
if (options.isDistribution?.value) {
return applySiteReceiveRatioDelta(calculatedDefaultSaleRatio.value, siteReceiveRatioDelta.value)
}
return readFinalSaleRatio(
calculatedDefaultSaleRatio.value,
options.form.accelerated_sale_ratio,
maxAcceleratedSaleRatio.value
)
)
})
const calculatedCoinBasePrice = computed(() => {
if (calculatedRatio.value <= 0) return 0
return roundMoney(coinWanAmount.value / calculatedRatio.value)
@@ -131,6 +141,9 @@ export function usePricingCalculator(options: {
)
const saleRatioRangeText = computed(() => {
if (calculatedDefaultSaleRatio.value <= 0) return '完成基础信息后自动计算参考比例'
if (options.isDistribution?.value) {
return `主站上限 1:${formatNumber(calculatedDefaultSaleRatio.value)},本站收号 1:${formatNumber(calculatedRatio.value)}`
}
return `可设置 1:${formatNumber(calculatedDefaultSaleRatio.value)} ~ 1:${formatNumber(maxAcceleratedSaleRatio.value)}`
})
const acceleratedSaleRatioPlaceholder = computed(() => {
@@ -195,6 +208,7 @@ export function usePricingCalculator(options: {
calculatedSellerReferenceRatio,
calculatedDefaultSaleRatio,
maxAcceleratedSaleRatio,
siteReceiveRatioDelta,
calculatedRatio,
calculatedCoinBasePrice,
calculatedConsumablePrice,
+6
View File
@@ -159,6 +159,12 @@ export function readFinalSaleRatio(
return roundRatio(Math.min(Math.max(ratio, defaultRatio), maxAcceleratedSaleRatio))
}
export function applySiteReceiveRatioDelta(referenceRatio: number, delta: number) {
if (referenceRatio <= 0) return 0
const safeDelta = Math.min(20, Math.max(0, Number(delta) || 0))
return roundRatio(Math.max(referenceRatio - safeDelta, 0.01))
}
export function hasAcceleratedSaleRatioInput(value: number | '' | null | undefined) {
const ratio = Number(value)
return (
+5
View File
@@ -27,6 +27,7 @@ export interface DistributionSiteInfo {
banners_json?: string
announcements_json?: string
footer_info_json?: string
receive_ratio_delta?: number
}
/**
@@ -72,6 +73,7 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
banners: [] as DistributionBanner[],
announcements: [] as string[],
footerInfo: null as DistributionFooterInfo | null,
receiveRatioDelta: 0,
initialized: false,
}
},
@@ -175,6 +177,8 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
this.footerInfo = null
}
this.receiveRatioDelta = Math.max(0, Number(info.receive_ratio_delta || 0))
sessionStorage.setItem('hfb_site_code', this.siteCode)
sessionStorage.setItem('hfb_site_name', this.siteName)
@@ -212,6 +216,7 @@ export const useDistributionSiteStore = defineStore('distributionSite', {
this.banners = []
this.announcements = []
this.footerInfo = null
this.receiveRatioDelta = 0
sessionStorage.removeItem('hfb_site_code')
sessionStorage.removeItem('hfb_site_name')
localStorage.removeItem('hfb_site_code')