fix(invitation): 线下提号接入邀请返利并补发历史漏发,返利比例文案改读配置
- 线下提号(admin_pickups)不走订单结账流程,此前完全不发邀请返利;现于 Complete() 内按号主利润对半基数发放卖家侧返利 - 迁移 000086:invitation_rebate_ledgers 增加 source 列,唯一键调整为 (source, order_id, trade_role),支持提号来源且保持幂等 - 新增 cmd/rebate-backfill 幂等补发脚本:默认预演,加 -apply 才写库 - 返利阶段文案不再写死 60%/30%/15%,改为按本次实际生效比例生成(钱包流水备注同步修正) - 前台邀请页阶段标签/横幅/空态文案、移动端个人中心入口文案均改读接口配置,并修掉合法 0% 比例被 || 覆盖成 60% 的问题 - 新增单测:提号返利发放与幂等、cycleLabel 跟随后台配置
This commit is contained in:
@@ -23,6 +23,8 @@ export interface AdminRebateRow {
|
||||
id: number
|
||||
order_id: number
|
||||
order_no: string
|
||||
/** 来源单据类型:rental_order=租赁订单,pickup=线下提号 */
|
||||
source?: string
|
||||
distribution_site_id: number
|
||||
inviter_id: number
|
||||
inviter_phone: string
|
||||
|
||||
@@ -367,6 +367,13 @@ function cycleStageLabel(stage: string) {
|
||||
return labels[stage] || stage || '-'
|
||||
}
|
||||
|
||||
/** 返利来源单据类型文案(提号单不走正常订单流程,需要单独标识)。 */
|
||||
function rebateSourceLabel(source?: string) {
|
||||
if (source === 'pickup') return '提号'
|
||||
if (source === 'rental_order') return '订单'
|
||||
return source || '订单'
|
||||
}
|
||||
|
||||
function paymentStatusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
created: '已创建',
|
||||
@@ -652,7 +659,18 @@ function goPayments() {
|
||||
<el-table-column label="入账时间" width="160">
|
||||
<template #default="{ row }">{{ formatDateTime(row.settled_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_no" label="关联订单" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="来源" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="row.source === 'pickup' ? 'warning' : 'info'"
|
||||
>
|
||||
{{ rebateSourceLabel(row.source) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_no" label="关联单据" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="交易人(被邀请人)" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<code class="cell-sub">{{ row.invitee_phone || '-' }}</code>
|
||||
|
||||
@@ -401,7 +401,18 @@ function cycleLabel(cycle: string) {
|
||||
style="width: 100%; margin-top: 16px"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="order_no" label="关联订单号" min-width="160">
|
||||
<el-table-column label="来源" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="row.source === 'pickup' ? 'warning' : 'info'"
|
||||
>
|
||||
{{ row.source === 'pickup' ? '提号' : '订单' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_no" label="关联单据号" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<code>{{ row.order_no }}</code>
|
||||
</template>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { formatDateMinute } from '@/shared/utils/time'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { formatCent } from '@/shared/utils/money'
|
||||
import { setPassword } from '@/features/auth/api/auth'
|
||||
import { fetchUserInvitationSummary } from '@/features/invitation/api/invitation'
|
||||
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
@@ -24,6 +25,9 @@ const session = useSessionStore()
|
||||
const siteStore = useDistributionSiteStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
/** 邀请返利入口右侧比例文案,取自后台返利配置。 */
|
||||
const inviteRateText = ref('')
|
||||
const { unreadCount: notificationUnreadCount, unreadLabel: notificationUnreadLabel } =
|
||||
useNotificationUnreadCount(route)
|
||||
|
||||
@@ -117,6 +121,7 @@ onMounted(async () => {
|
||||
// 保留本地会话展示,避免个人中心因网络短暂异常整页不可用。
|
||||
}
|
||||
void loadBalance()
|
||||
void loadInviteRate()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -129,6 +134,17 @@ async function loadBalance() {
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请返利入口右侧文案取后台实时配置的首月比例,不能写死。
|
||||
async function loadInviteRate() {
|
||||
try {
|
||||
const summary = await fetchUserInvitationSummary()
|
||||
inviteRateText.value = `首月 ${Math.round(summary.first_month_rate * 100)}% 收益`
|
||||
} catch {
|
||||
// 取不到配置时不展示数字,避免与后台实际比例不一致。
|
||||
inviteRateText.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function openLedgers() {
|
||||
showLedgers.value = true
|
||||
loadingLedgers.value = true
|
||||
@@ -472,7 +488,7 @@ function resolveAvatarURL(url: string | undefined | null) {
|
||||
icon="gold-coin-o"
|
||||
is-link
|
||||
to="/m/invitations"
|
||||
value="首月60%收益"
|
||||
:value="inviteRateText || '阶梯返利'"
|
||||
/>
|
||||
<van-cell title="租后须知" icon="info-o" is-link @click="openPostRentalNotice" />
|
||||
<van-cell title="系统设置" icon="setting-o" is-link @click="showSettings = true" />
|
||||
|
||||
@@ -143,16 +143,43 @@ function formatRate(rate: number) {
|
||||
return `${Math.round(rate * 100)}%`
|
||||
}
|
||||
|
||||
function cycleLabel(cycle: string) {
|
||||
/** 配置未加载时用占位符,避免展示与后台配置不符的假比例。 */
|
||||
function formatRateOrPlaceholder(rate: number | undefined | null) {
|
||||
return rate === undefined || rate === null ? '-' : formatRate(rate)
|
||||
}
|
||||
|
||||
/** 返利阶段文案:比例取该笔流水实际生效的 rate_applied,不能写死 60/30/15。 */
|
||||
function cycleLabel(cycle: string, rate: number) {
|
||||
const percent = formatRate(rate)
|
||||
switch (cycle) {
|
||||
case 'first_month':
|
||||
return '首月高额 (60%)'
|
||||
return `首月奖励 ${percent}`
|
||||
case 'second_month':
|
||||
return '次月奖励 (30%)'
|
||||
return `次月奖励 ${percent}`
|
||||
case 'subsequent':
|
||||
return `长期奖励 ${percent}`
|
||||
default:
|
||||
return '长期分成 (15%)'
|
||||
return `返利 ${percent}`
|
||||
}
|
||||
}
|
||||
|
||||
/** 顶部横幅文案按后台实时配置比例生成。 */
|
||||
const heroSubtitle = computed(() => {
|
||||
const current = summary.value
|
||||
if (!current) return '买卖双边分成按实名生效周期阶梯计算 · 多劳多得零门槛'
|
||||
return `买卖双边分成:首月单边 ${formatRate(current.first_month_rate)},次月 ${formatRate(
|
||||
current.second_month_rate
|
||||
)},长期 ${formatRate(current.subsequent_rate)} · 多劳多得零门槛`
|
||||
})
|
||||
|
||||
/** 好友空态文案按首月配置比例生成。 */
|
||||
const emptyFriendsTip = computed(() => {
|
||||
const rate = summary.value?.first_month_rate
|
||||
if (rate === undefined || rate === null) {
|
||||
return '您尚未邀请好友,快去分享专属链接赚取返利吧!'
|
||||
}
|
||||
return `您尚未邀请好友,快去分享专属链接赚取首月 ${formatRate(rate)} 返利吧!`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -166,7 +193,7 @@ function cycleLabel(cycle: string) {
|
||||
</div>
|
||||
<h1 class="hero-title">邀请好友赚丰厚返利</h1>
|
||||
<p class="hero-subtitle">
|
||||
买卖双边分成:首月单边高达 60%,次月 30%,永久 15% · 多劳多得零门槛
|
||||
{{ heroSubtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -279,21 +306,21 @@ function cycleLabel(cycle: string) {
|
||||
<div class="ladder-grid">
|
||||
<div class="ladder-item ladder-first">
|
||||
<div class="ladder-tag">阶梯一 · 首月</div>
|
||||
<div class="ladder-rate">{{ formatRate(summary?.first_month_rate || 0.6) }}</div>
|
||||
<div class="ladder-rate">{{ formatRateOrPlaceholder(summary?.first_month_rate) }}</div>
|
||||
<div class="ladder-time">实名生效 1 ~ 30 天</div>
|
||||
<div class="ladder-desc">每笔订单单边收益高比例奖励</div>
|
||||
</div>
|
||||
|
||||
<div class="ladder-item ladder-second">
|
||||
<div class="ladder-tag">阶梯二 · 次月</div>
|
||||
<div class="ladder-rate">{{ formatRate(summary?.second_month_rate || 0.3) }}</div>
|
||||
<div class="ladder-rate">{{ formatRateOrPlaceholder(summary?.second_month_rate) }}</div>
|
||||
<div class="ladder-time">实名生效 31 ~ 60 天</div>
|
||||
<div class="ladder-desc">持续稳定鼓励买卖活跃交易</div>
|
||||
</div>
|
||||
|
||||
<div class="ladder-item ladder-subsequent">
|
||||
<div class="ladder-tag">阶梯三 · 长期永久</div>
|
||||
<div class="ladder-rate">{{ formatRate(summary?.subsequent_rate || 0.15) }}</div>
|
||||
<div class="ladder-rate">{{ formatRateOrPlaceholder(summary?.subsequent_rate) }}</div>
|
||||
<div class="ladder-time">实名生效 60 天后永久</div>
|
||||
<div class="ladder-desc">终身绑定,笔笔成交享分成收益</div>
|
||||
</div>
|
||||
@@ -339,7 +366,7 @@ function cycleLabel(cycle: string) {
|
||||
<div v-if="loadingFriends" class="loading-state">加载好友列表中...</div>
|
||||
<div v-else-if="friends.length === 0" class="empty-state">
|
||||
<el-icon :size="40" color="#cbd5e1"><User /></el-icon>
|
||||
<p>您尚未邀请好友,快去分享专属链接赚取首月60%返利吧!</p>
|
||||
<p>{{ emptyFriendsTip }}</p>
|
||||
</div>
|
||||
<div v-else class="table-responsive">
|
||||
<table class="data-table">
|
||||
@@ -414,7 +441,7 @@ function cycleLabel(cycle: string) {
|
||||
</td>
|
||||
<td>¥{{ formatCent(r.single_side_basis_cent) }}</td>
|
||||
<td>
|
||||
<span class="cycle-pill">{{ cycleLabel(r.cycle_stage) }}</span>
|
||||
<span class="cycle-pill">{{ cycleLabel(r.cycle_stage, r.rate_applied) }}</span>
|
||||
</td>
|
||||
<td class="rebate-amount">+¥{{ formatCent(r.rebate_amount_cent) }}</td>
|
||||
<td>{{ r.settled_at ? r.settled_at.slice(0, 16) : '-' }}</td>
|
||||
|
||||
Reference in New Issue
Block a user