mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 13:40:44 +08:00
feat(affiliate): add feature toggle and per-user custom invite settings
- 在系统设置「功能开关」中新增邀请返利总开关,默认关闭;
关闭态:菜单隐藏、注册忽略 aff、新充值不返利,但已有 quota 仍可转余额
- 支持管理员为指定用户设置专属邀请码(覆盖随机码,全局唯一)
- 支持管理员为指定用户设置专属返利比例(覆盖全局比例,可单条/批量调整)
- 在系统设置邀请返利卡片内嵌入专属用户管理表格(搜索/编辑/批量/删除),
删除采用项目通用 ConfirmDialog,会同时清除专属比例并把邀请码重置为系统随机码
- /affiliate 用户页新增「我的返利比例」卡片与动态使用说明,让用户直观看到
分享后能拿到多少(同源 resolveRebateRatePercent 计算,与实际充值一致)
- 新增数据库迁移 132 添加 aff_rebate_rate_percent 与 aff_code_custom 列
- 新增 admin 路由组 /api/v1/admin/affiliates/users/* 共 5 个端点
- AffiliateService 改为只依赖 *SettingService,去除冗余的 SettingRepository
- 邀请码格式校验放宽到 [A-Z0-9_-]{4,32},兼容旧 12 位系统码与新自定义码
- 补充单元测试与集成测试覆盖新方法、冲突路径与边界值
This commit is contained in:
@@ -8,7 +8,23 @@
|
||||
</div>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<!-- 返利比例:用主色突出,让用户一眼看到「能拿多少」 -->
|
||||
<div class="card relative overflow-hidden p-5">
|
||||
<div class="absolute -right-6 -top-6 h-24 w-24 rounded-full bg-primary-500/10"></div>
|
||||
<div class="relative">
|
||||
<p class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-dark-400">
|
||||
<Icon name="dollar" size="sm" class="text-primary-500" />
|
||||
{{ t('affiliate.stats.rebateRate') }}
|
||||
</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-primary-600 dark:text-primary-400">
|
||||
{{ formattedRebateRate }}<span class="ml-0.5 text-base font-medium">%</span>
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-400 dark:text-dark-500">
|
||||
{{ t('affiliate.stats.rebateRateHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-5">
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400">{{ t('affiliate.stats.invitedUsers') }}</p>
|
||||
<p class="mt-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
@@ -61,7 +77,7 @@
|
||||
<p class="text-sm font-medium text-primary-800 dark:text-primary-200">{{ t('affiliate.tips.title') }}</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-primary-700 dark:text-primary-300">
|
||||
<li>1. {{ t('affiliate.tips.line1') }}</li>
|
||||
<li>2. {{ t('affiliate.tips.line2') }}</li>
|
||||
<li>2. {{ t('affiliate.tips.line2', { rate: `${formattedRebateRate}%` }) }}</li>
|
||||
<li>3. {{ t('affiliate.tips.line3') }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -149,6 +165,14 @@ const inviteLink = computed(() => {
|
||||
return `${window.location.origin}/register?aff=${encodeURIComponent(detail.value.aff_code)}`
|
||||
})
|
||||
|
||||
// Rebate rate is a percentage in the range [0, 100]; backend already clamps it.
|
||||
// We trim trailing zeros (e.g. 20.00 → "20", 12.50 → "12.5") for a cleaner UI.
|
||||
const formattedRebateRate = computed(() => {
|
||||
const v = detail.value?.effective_rebate_rate_percent ?? 0
|
||||
const rounded = Math.round(v * 100) / 100
|
||||
return Number.isInteger(rounded) ? String(rounded) : rounded.toString()
|
||||
})
|
||||
|
||||
function formatCount(value: number): string {
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user