feat: 前台匿名浏览、登录引导、客服入口与返水增强

前台:
- 未登录可浏览首页/赛事/赔率,下注等操作弹出登录引导(去登录/继续浏览)
- 顶部新增客服入口与 iframe 弹窗
- 登录页支持暂不登录返回浏览

API:
- 首页/赛事/冠军盘接口改为公开访问,支持 X-Locale 头
- JWT 守卫支持可选认证

返水:
- 注单新增 is_cashbacked 字段,发放时自动标记
- 预览展示玩家余额,明确平台直发不从代理扣款
- 后台注单列表与玩家历史展示回水状态

其他:
- 串关禁止同场重复选号(SAME_MATCH)
- 补充结算资金流分析文档

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 09:36:44 +08:00
parent 785fa4416d
commit 844727c82e
35 changed files with 1007 additions and 49 deletions

View File

@@ -288,6 +288,7 @@ export const adminPagesMs: Record<string, string> = {
'bet.col.odds': 'Odds',
'bet.col.payout': 'Bayaran',
'bet.col.placed_at': 'Masa pertaruhan',
'bet.col.cashbacked': 'Rebat dibayar',
'bet.dialog.detail': 'Butiran pertaruhan',
'bet.field.total_odds': 'Jumlah odds',
'bet.field.currency': 'Mata wang',
@@ -358,20 +359,22 @@ export const adminPagesMs: Record<string, string> = {
'cashback.col.index': '#',
'cashback.col.player': 'Pemain',
'cashback.col.agent': 'Ejen',
'cashback.col.balance': 'Baki semasa',
'cashback.col.effective_stake': 'Stake berkesan',
'cashback.col.rate': 'Kadar',
'cashback.col.amount': 'Rebat',
'cashback.confirm_issue': 'Sahkan bayaran',
'cashback.cancel_issue': 'Batalkan',
'cashback.confirm_prompt': 'Bayar rebat kelompok ini ke dompet pemain? Tindakan ini tidak boleh dibatalkan.',
'cashback.confirm_prompt': 'Bayar rebat kelompok ini ke dompet pemain? Rebat dikreditkan terus oleh platform dan tidak ditolak daripada ejen. Tindakan ini tidak boleh dibatalkan.',
'cashback.cancel_prompt': 'Batalkan kelompok menunggu ini? Tiada kredit dompet; boleh pratonton semula.',
'cashback.status.CANCELLED': 'Dibatalkan',
'cashback.rules_title': 'Peraturan rebat',
'cashback.rule_period': 'Pilih julat tarikh. Taruhan dikira mengikut masa penyelesaian dalam tempoh tersebut.',
'cashback.rule_eligible': 'Termasuk: taruhan selesai WON/LOST (tunggal ikut stake; parlay sekali ikut stake parlay). Tidak termasuk: belum selesai, dibatalkan, batal, push, dan kadar 0.',
'cashback.rule_eligible': 'Termasuk: taruhan selesai WON/LOST (tunggal ikut stake; parlay sekali ikut stake parlay). Tidak termasuk: belum selesai, dibatalkan, batal, push, kadar 0, dan taruhan yang sudah dibayar rebat.',
'cashback.rule_formula': 'Setiap taruhan: stake × kadar rebat. Jumlah diagregat mengikut pemain.',
'cashback.rule_rate': 'Keutamaan kadar: pemain > ejen > global > kadar lalai ejen (cth. 0.01 = 1%).',
'cashback.rule_flow': 'Aliran: pratonton (satu menunggu setiap tempoh) → semak → sahkan bayaran; batalkan jika tidak perlu. Tempoh dibayar tidak boleh pratonton semula.',
'cashback.rule_platform': 'Bayaran: rebat dikreditkan ke baki tunai pemain oleh platform; tidak ditolak daripada kredit atau baki ejen.',
'cashback.rule_note_zero': 'Jika 0, semak taruhan WON/LOST dalam tempoh dan kadar rebat > 0.',
'user.field.player_id': 'ID pemain',

View File

@@ -303,6 +303,7 @@ export const adminPagesZh: Record<string, string> = {
'bet.col.odds': '赔率',
'bet.col.payout': '派彩',
'bet.col.placed_at': '投注时间',
'bet.col.cashbacked': '已回水',
'bet.dialog.detail': '注单详情',
'bet.field.total_odds': '总赔率',
'bet.field.currency': '币种',
@@ -373,20 +374,22 @@ export const adminPagesZh: Record<string, string> = {
'cashback.col.index': '#',
'cashback.col.player': '玩家',
'cashback.col.agent': '所属代理',
'cashback.col.balance': '当前余额',
'cashback.col.effective_stake': '有效投注',
'cashback.col.rate': '返水比例',
'cashback.col.amount': '返水金额',
'cashback.confirm_issue': '确认发放',
'cashback.cancel_issue': '作废',
'cashback.confirm_prompt': '确认向玩家钱包发放本批次返水?此操作不可撤销。',
'cashback.confirm_prompt': '确认向玩家钱包发放本批次返水?返水由平台直接入账,不从代理扣款。此操作不可撤销。',
'cashback.cancel_prompt': '确认作废该待发放批次?作废后不会入账,可重新生成预览。',
'cashback.status.CANCELLED': '已作废',
'cashback.rules_title': '返水规则说明',
'cashback.rule_period': '选择开始/结束日期,统计该周期内、按注单结算时间落在区间内的有效投注。',
'cashback.rule_eligible': '计入:已结算且结果为「赢」或「输」的注单(单关按本金,串关按整单本金计一次)。不计入:未结算、已取消、作废、走水,以及返水比例为 0 的注单。',
'cashback.rule_eligible': '计入:已结算且结果为「赢」或「输」的注单(单关按本金,串关按整单本金计一次)。不计入:未结算、已取消、作废、走水,以及返水比例为 0 的注单;已返水过的注单不会重复计入。',
'cashback.rule_formula': '单笔返水 = 投注本金 × 适用返水比例;同一玩家多笔注单汇总后生成一条返水明细。',
'cashback.rule_rate': '返水比例优先级:玩家专属规则 > 代理线规则 > 全局规则 > 所属代理默认返水率(在代理/玩家管理中配置,如 0.01 表示 1%)。',
'cashback.rule_flow': '操作流程:生成预览(同周期仅保留一条待发放)→ 核对明细 → 确认发放;不需要的可作废。已发放周期不可重复预览。',
'cashback.rule_platform': '发放方式:返水由平台直接打入玩家现金余额,不从代理信用或余额中扣除。',
'cashback.rule_note_zero': '预览为 0 时,请检查:周期内是否有已结算输赢注单、代理/玩家是否配置了大于 0 的返水率。',
'user.field.player_id': '玩家 ID',
@@ -1174,6 +1177,7 @@ export const adminPagesEn: Record<string, string> = {
'bet.col.odds': 'Odds',
'bet.col.payout': 'Payout',
'bet.col.placed_at': 'Placed at',
'bet.col.cashbacked': 'Cashbacked',
'bet.dialog.detail': 'Bet details',
'bet.field.total_odds': 'Total odds',
'bet.field.currency': 'Currency',
@@ -1244,20 +1248,22 @@ export const adminPagesEn: Record<string, string> = {
'cashback.col.index': '#',
'cashback.col.player': 'Player',
'cashback.col.agent': 'Agent',
'cashback.col.balance': 'Balance',
'cashback.col.effective_stake': 'Effective stake',
'cashback.col.rate': 'Rate',
'cashback.col.amount': 'Cashback',
'cashback.confirm_issue': 'Confirm payout',
'cashback.cancel_issue': 'Void',
'cashback.confirm_prompt': 'Pay out this cashback batch to player wallets? This cannot be undone.',
'cashback.confirm_prompt': 'Pay out this cashback batch to player wallets? Cashback is credited by the platform directly and is not deducted from agents. This cannot be undone.',
'cashback.cancel_prompt': 'Void this pending batch? No wallet credit will be made; you can preview again.',
'cashback.status.CANCELLED': 'Voided',
'cashback.rules_title': 'Cashback rules',
'cashback.rule_period': 'Pick a date range. Bets are included by settlement time within that period.',
'cashback.rule_eligible': 'Included: settled bets with result WON or LOST (singles by stake; parlays counted once by parlay stake). Excluded: pending, cancelled, void, push, and zero-rate bets.',
'cashback.rule_eligible': 'Included: settled bets with result WON or LOST (singles by stake; parlays counted once by parlay stake). Excluded: pending, cancelled, void, push, zero-rate bets, and bets already paid cashback.',
'cashback.rule_formula': 'Per bet: stake × applicable cashback rate. Amounts are summed per player into one line item.',
'cashback.rule_rate': 'Rate priority: player rule > agent rule > global rule > agent default rate (set under Agents/Players, e.g. 0.01 = 1%).',
'cashback.rule_flow': 'Flow: preview (one pending batch per period) → review → confirm payout; void if not needed. Paid periods cannot be previewed again.',
'cashback.rule_platform': 'Payout: cashback is credited to player cash balance by the platform; it is not deducted from agent credit or balance.',
'cashback.rule_note_zero': 'If preview is 0, check for settled WON/LOST bets in the period and a cashback rate above 0.',
'user.field.player_id': 'Player ID',

View File

@@ -229,6 +229,12 @@ async function openDetail(row: BetListRow) {
</el-tag>
</template>
</el-table-column>
<el-table-column :label="t('bet.col.cashbacked')" width="88" align="center">
<template #default="{ row }">
<el-tag v-if="row.isCashbacked" type="success" size="small" effect="plain"></el-tag>
<span v-else class="bet-content-empty"></span>
</template>
</el-table-column>
<el-table-column :label="t('bet.col.placed_at')" min-width="168">
<template #default="{ row }">{{ formatTime(row.placedAt) }}</template>
</el-table-column>
@@ -284,6 +290,9 @@ async function openDetail(row: BetListRow) {
</el-descriptions-item>
<el-descriptions-item :label="t('bet.col.placed_at')">{{ formatTime(detail.placedAt) }}</el-descriptions-item>
<el-descriptions-item :label="t('bet.field.settled_at')">{{ formatTime(detail.settledAt) }}</el-descriptions-item>
<el-descriptions-item :label="t('bet.col.cashbacked')">
{{ detail.isCashbacked ? t('common.yes') : t('common.no') }}
</el-descriptions-item>
<el-descriptions-item :label="t('bet.field.request_id')" :span="2">{{ detail.requestId }}</el-descriptions-item>
</el-descriptions>

View File

@@ -27,6 +27,7 @@ interface CashbackPreviewItem {
userId: string;
username: string;
agentUsername: string | null;
availableBalance: string;
effectiveStake: string;
betCount: number;
rate: string;
@@ -288,6 +289,7 @@ onMounted(loadHistory);
<li>{{ t('cashback.rule_formula') }}</li>
<li>{{ t('cashback.rule_rate') }}</li>
<li>{{ t('cashback.rule_flow') }}</li>
<li>{{ t('cashback.rule_platform') }}</li>
<li class="rules-note">{{ t('cashback.rule_note_zero') }}</li>
</ul>
</el-dialog>
@@ -374,6 +376,18 @@ onMounted(loadHistory);
>
<template #default="{ row }">{{ row.agentUsername || '—' }}</template>
</el-table-column>
<el-table-column
prop="availableBalance"
:label="t('cashback.col.balance')"
min-width="110"
align="right"
>
<template #default="{ row }">
<el-tooltip :content="formatAmountFull(row.availableBalance)" placement="top">
<span>{{ formatAmount(row.availableBalance) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
prop="betCount"
:label="t('cashback.col.bet_count')"
@@ -547,6 +561,9 @@ onMounted(loadHistory);
<el-table-column prop="agentUsername" :label="t('cashback.col.agent')" min-width="100">
<template #default="{ row }">{{ row.agentUsername || '—' }}</template>
</el-table-column>
<el-table-column prop="availableBalance" :label="t('cashback.col.balance')" min-width="100" align="right">
<template #default="{ row }">{{ formatAmount(row.availableBalance) }}</template>
</el-table-column>
<el-table-column prop="betCount" :label="t('cashback.col.bet_count')" width="88" align="right" />
<el-table-column prop="effectiveStake" :label="t('cashback.col.effective_stake')" min-width="110" align="right">
<template #default="{ row }">{{ formatAmount(row.effectiveStake) }}</template>

View File

@@ -25,6 +25,7 @@ export interface BetListRow {
currency: string;
placedAt: string;
settledAt: string | null;
isCashbacked: boolean;
selectionCount: number;
selectionSummary: string;
selectionPreviews: BetSelectionPreview[];