feat(admin+api): 代理停用默认、结算加固与冒烟配置探针
- 代理层级默认授信比例与停用冻结/禁登全局默认 - 结算预览去重、比分校验、串关当场判负与市场类型校验 - 站内信 Banner/公告自动通知开关;开发环境动态 API 端口 - 扩充 RBAC/结算/认证/返现单元测试与 agent skills
This commit is contained in:
@@ -206,6 +206,8 @@ const adminPages: Record<string, string> = {
|
||||
'agent.hierarchy.max_level': 'Max agent level',
|
||||
'agent.hierarchy.default_sub_credit_ratio': 'Default sub-agent credit ratio',
|
||||
'agent.hierarchy.default_sub_credit_ratio_hint': 'When creating a sub-agent, pre-fill credit as parent available × this ratio',
|
||||
'agent.suspend.settings_title': 'Default agent suspend behavior',
|
||||
'agent.suspend.settings_hint': 'Each suspend/unfreeze action can override these; used as dialog defaults',
|
||||
'agent.hierarchy.create_credit_default_hint': 'Default {ratio}% ({amount}), capped by parent available credit; adjustable',
|
||||
'agent.hierarchy.create_credit_quick_hint': 'Parent available {amount} — click a ratio to fill',
|
||||
'agent.hierarchy.create_level_hint': 'Will be created as level {n} agent',
|
||||
@@ -885,6 +887,10 @@ const adminPages: Record<string, string> = {
|
||||
'content.inbox_notify.inbox_enabled_hint': 'When off, the player hub opens support only (no mailbox tab)',
|
||||
'content.inbox_notify.deposit': 'Deposit results',
|
||||
'content.inbox_notify.deposit_hint': 'Auto-send inbox messages on approve or reject',
|
||||
'content.inbox_notify.banner': 'Homepage promo',
|
||||
'content.inbox_notify.banner_hint': 'Auto-broadcast when publishing a banner with inbox notify checked',
|
||||
'content.inbox_notify.announcement': 'Announcements / ticker',
|
||||
'content.inbox_notify.announcement_hint': 'Auto-broadcast when publishing notice/ticker with inbox notify checked',
|
||||
'content.inbox_notify.manual_title': 'Check "Inbox notify" when creating:',
|
||||
'content.inbox_notify.banner_note': 'Homepage promo',
|
||||
'content.inbox_notify.announcement_note': 'Announcements / ticker',
|
||||
|
||||
@@ -207,6 +207,8 @@ const adminPages: Record<string, string> = {
|
||||
'agent.hierarchy.max_level': '最大代理层级',
|
||||
'agent.hierarchy.default_sub_credit_ratio': '下级默认授信比例',
|
||||
'agent.hierarchy.default_sub_credit_ratio_hint': '创建下级代理时,授信额度默认预填为上级可用授信 × 此比例',
|
||||
'agent.suspend.settings_title': '停用代理默认行为',
|
||||
'agent.suspend.settings_hint': '单次停用/解冻操作仍可单独勾选覆盖;此处为对话框默认勾选状态',
|
||||
'agent.hierarchy.create_credit_default_hint': '默认 {ratio}%({amount}),不超过上级可用授信,可手动调整',
|
||||
'agent.hierarchy.create_credit_quick_hint': '上级可用授信 {amount},点击比例快速填入',
|
||||
'agent.hierarchy.create_level_hint': '将创建为 {n} 级代理',
|
||||
@@ -882,6 +884,10 @@ const adminPages: Record<string, string> = {
|
||||
'content.inbox_notify.inbox_enabled_hint': '关闭后玩家端入口直达客服,不展示邮箱标签页',
|
||||
'content.inbox_notify.deposit': '充值结果',
|
||||
'content.inbox_notify.deposit_hint': '审核通过或拒绝时自动发送站内信',
|
||||
'content.inbox_notify.banner': '首页推广',
|
||||
'content.inbox_notify.banner_hint': '发布 Banner 且勾选邮箱通知时自动群发',
|
||||
'content.inbox_notify.announcement': '公告 / 跑马灯',
|
||||
'content.inbox_notify.announcement_hint': '发布通知或跑马灯且勾选邮箱通知时自动群发',
|
||||
'content.inbox_notify.manual_title': '以下类型在新建时勾选「邮箱通知」',
|
||||
'content.inbox_notify.banner_note': '首页推广',
|
||||
'content.inbox_notify.announcement_note': '通知公告 / 跑马灯',
|
||||
|
||||
@@ -112,7 +112,11 @@ type SubAgentLevelState = {
|
||||
|
||||
const subAgentLevelState = reactive<Record<number, SubAgentLevelState>>({});
|
||||
const agentLevelCounts = ref<Record<number, number>>({});
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0 });
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0, defaultSubAgentCreditRatio: 50 });
|
||||
const agentSuspendDefaults = ref({
|
||||
suspendFreezeDirectPlayers: false,
|
||||
suspendBlockPlayerLogin: false,
|
||||
});
|
||||
|
||||
function ensureSubAgentState(level: number): SubAgentLevelState {
|
||||
if (!subAgentLevelState[level]) {
|
||||
@@ -220,7 +224,9 @@ const creditContextLoading = ref(false);
|
||||
/* ─── Init ─── */
|
||||
let pageInitPromise: Promise<void> | null = null;
|
||||
const pageInitLoaded = ref(false);
|
||||
const DEFAULT_SUB_AGENT_CREDIT_RATIO = 50;
|
||||
const DEFAULT_SUB_AGENT_CREDIT_RATIO = computed(
|
||||
() => hierarchySettings.value.defaultSubAgentCreditRatio || 50,
|
||||
);
|
||||
const freezeAgentVisible = ref(false);
|
||||
const freezeAgentLoading = ref(false);
|
||||
const freezeAgentTarget = ref<AgentRow | null>(null);
|
||||
@@ -323,7 +329,7 @@ function computeSubAgentCreditByRatio(available: number, ratioPercent: number):
|
||||
const creditQuickRatios = [10, 15, 20, 30] as const;
|
||||
|
||||
function computeDefaultSubAgentCreditLimit(available: number): number {
|
||||
return computeSubAgentCreditByRatio(available, DEFAULT_SUB_AGENT_CREDIT_RATIO);
|
||||
return computeSubAgentCreditByRatio(available, DEFAULT_SUB_AGENT_CREDIT_RATIO.value);
|
||||
}
|
||||
|
||||
function applyCreateSubAgentCreditRatio(ratioPercent: number) {
|
||||
@@ -439,16 +445,33 @@ onActivated(() => {
|
||||
}
|
||||
});
|
||||
|
||||
async function loadAgentSuspendDefaults() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/agents/settings/suspend');
|
||||
agentSuspendDefaults.value = {
|
||||
suspendFreezeDirectPlayers: Boolean(data.data?.suspendFreezeDirectPlayers),
|
||||
suspendBlockPlayerLogin: Boolean(data.data?.suspendBlockPlayerLogin),
|
||||
};
|
||||
} catch {
|
||||
/* keep defaults */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsersPageInit() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/users/page-init');
|
||||
const [pageInitRes] = await Promise.all([
|
||||
api.get('/admin/users/page-init'),
|
||||
loadAgentSuspendDefaults(),
|
||||
]);
|
||||
const { data } = pageInitRes;
|
||||
const payload = data.data as {
|
||||
hierarchySettings?: { maxAgentLevel: number };
|
||||
hierarchySettings?: { maxAgentLevel: number; defaultSubAgentCreditRatio?: number };
|
||||
agentLevelCounts?: Record<number, number>;
|
||||
};
|
||||
if (payload.hierarchySettings) {
|
||||
hierarchySettings.value = {
|
||||
maxAgentLevel: payload.hierarchySettings.maxAgentLevel ?? 0,
|
||||
defaultSubAgentCreditRatio: payload.hierarchySettings.defaultSubAgentCreditRatio ?? 50,
|
||||
};
|
||||
}
|
||||
if (payload.agentLevelCounts) {
|
||||
@@ -1108,8 +1131,8 @@ const freezeAgentIsSuspend = computed(() => {
|
||||
function toggleFreezeAgent(row: AgentRow) {
|
||||
freezeAgentTarget.value = row;
|
||||
freezeAgentForm.value = {
|
||||
freezeDirectPlayers: false,
|
||||
blockDirectPlayerLogin: false,
|
||||
freezeDirectPlayers: agentSuspendDefaults.value.suspendFreezeDirectPlayers,
|
||||
blockDirectPlayerLogin: agentSuspendDefaults.value.suspendBlockPlayerLogin,
|
||||
unfreezeDirectPlayers: false,
|
||||
};
|
||||
freezeAgentVisible.value = true;
|
||||
|
||||
@@ -75,6 +75,8 @@ const notifyInbox = ref(false);
|
||||
interface InboxNotifySettings {
|
||||
inboxEnabled: boolean;
|
||||
deposit: boolean;
|
||||
banner: boolean;
|
||||
announcement: boolean;
|
||||
}
|
||||
|
||||
interface MessageBroadcastItem {
|
||||
@@ -100,6 +102,8 @@ interface BroadcastTranslationForm {
|
||||
const inboxNotifySettings = ref<InboxNotifySettings>({
|
||||
inboxEnabled: true,
|
||||
deposit: true,
|
||||
banner: true,
|
||||
announcement: true,
|
||||
});
|
||||
const inboxNotifySaving = ref(false);
|
||||
|
||||
@@ -218,6 +222,8 @@ async function loadInboxNotifySettings() {
|
||||
inboxNotifySettings.value = {
|
||||
inboxEnabled: data.data?.inboxEnabled !== false,
|
||||
deposit: Boolean(data.data?.deposit),
|
||||
banner: data.data?.banner !== false,
|
||||
announcement: data.data?.announcement !== false,
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
@@ -232,10 +238,14 @@ async function saveInboxNotifySettings() {
|
||||
const { data } = await api.put('/admin/contents/inbox-notify-settings', {
|
||||
inboxEnabled: inboxNotifySettings.value.inboxEnabled,
|
||||
deposit: inboxNotifySettings.value.deposit,
|
||||
banner: inboxNotifySettings.value.banner,
|
||||
announcement: inboxNotifySettings.value.announcement,
|
||||
});
|
||||
inboxNotifySettings.value = {
|
||||
inboxEnabled: data.data?.inboxEnabled !== false,
|
||||
deposit: Boolean(data.data?.deposit),
|
||||
banner: data.data?.banner !== false,
|
||||
announcement: data.data?.announcement !== false,
|
||||
};
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
@@ -785,6 +795,30 @@ void load();
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="inboxNotifySettings.inboxEnabled" class="inbox-notify-row">
|
||||
<div class="inbox-notify-row-text">
|
||||
<span class="inbox-notify-label">{{ t('content.inbox_notify.banner') }}</span>
|
||||
<span class="inbox-notify-hint">{{ t('content.inbox_notify.banner_hint') }}</span>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="inboxNotifySettings.banner"
|
||||
:disabled="!canManageContent || inboxNotifySaving"
|
||||
@change="saveInboxNotifySettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="inboxNotifySettings.inboxEnabled" class="inbox-notify-row">
|
||||
<div class="inbox-notify-row-text">
|
||||
<span class="inbox-notify-label">{{ t('content.inbox_notify.announcement') }}</span>
|
||||
<span class="inbox-notify-hint">{{ t('content.inbox_notify.announcement_hint') }}</span>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="inboxNotifySettings.announcement"
|
||||
:disabled="!canManageContent || inboxNotifySaving"
|
||||
@change="saveInboxNotifySettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-show="inboxNotifySettings.inboxEnabled" class="inbox-notify-notes-title">{{ t('content.inbox_notify.manual_title') }}</p>
|
||||
<ul v-show="inboxNotifySettings.inboxEnabled" class="inbox-notify-notes">
|
||||
<li>{{ t('content.inbox_notify.banner_note') }}</li>
|
||||
|
||||
@@ -27,7 +27,11 @@ const bettingLimits = ref({
|
||||
maxPayoutParlay: 1000000,
|
||||
dailyStakeLimit: 200000,
|
||||
});
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0 });
|
||||
const hierarchySettings = ref({ maxAgentLevel: 0, defaultSubAgentCreditRatio: 50 });
|
||||
const agentSuspendSettings = ref({
|
||||
suspendFreezeDirectPlayers: false,
|
||||
suspendBlockPlayerLogin: false,
|
||||
});
|
||||
const platformDirectRate = ref(0);
|
||||
const adminInviteRate = ref(0);
|
||||
const resetAllowed = ref(false);
|
||||
@@ -36,6 +40,7 @@ const resetConfirmPhrase = ref('');
|
||||
const settingsSaving = ref(false);
|
||||
const limitsSaving = ref(false);
|
||||
const hierarchySaving = ref(false);
|
||||
const suspendSaving = ref(false);
|
||||
const platformDirectSaving = ref(false);
|
||||
const resetLoading = ref(false);
|
||||
const loading = ref(false);
|
||||
@@ -47,7 +52,7 @@ async function loadSettings() {
|
||||
const payload = data.data as {
|
||||
playerSettings?: typeof playerSettings.value;
|
||||
bettingLimits?: typeof bettingLimits.value;
|
||||
hierarchySettings?: { maxAgentLevel: number };
|
||||
hierarchySettings?: { maxAgentLevel: number; defaultSubAgentCreditRatio?: number };
|
||||
platformDirect?: { platformDirectRate?: number | string; adminInviteRate?: number | string };
|
||||
};
|
||||
if (payload.playerSettings) playerSettings.value = payload.playerSettings;
|
||||
@@ -55,6 +60,7 @@ async function loadSettings() {
|
||||
if (payload.hierarchySettings) {
|
||||
hierarchySettings.value = {
|
||||
maxAgentLevel: payload.hierarchySettings.maxAgentLevel ?? 0,
|
||||
defaultSubAgentCreditRatio: payload.hierarchySettings.defaultSubAgentCreditRatio ?? 50,
|
||||
};
|
||||
}
|
||||
if (payload.platformDirect) {
|
||||
@@ -93,11 +99,27 @@ async function savePlayerSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAgentSuspendSettings() {
|
||||
try {
|
||||
const { data } = await api.get('/admin/agents/settings/suspend');
|
||||
agentSuspendSettings.value = {
|
||||
suspendFreezeDirectPlayers: Boolean(data.data?.suspendFreezeDirectPlayers),
|
||||
suspendBlockPlayerLogin: Boolean(data.data?.suspendBlockPlayerLogin),
|
||||
};
|
||||
} catch {
|
||||
/* keep defaults */
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHierarchySettings() {
|
||||
hierarchySaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/agents/settings/hierarchy', hierarchySettings.value);
|
||||
hierarchySettings.value = { maxAgentLevel: data.data?.maxAgentLevel ?? hierarchySettings.value.maxAgentLevel };
|
||||
hierarchySettings.value = {
|
||||
maxAgentLevel: data.data?.maxAgentLevel ?? hierarchySettings.value.maxAgentLevel,
|
||||
defaultSubAgentCreditRatio:
|
||||
data.data?.defaultSubAgentCreditRatio ?? hierarchySettings.value.defaultSubAgentCreditRatio,
|
||||
};
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
@@ -107,6 +129,23 @@ async function saveHierarchySettings() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAgentSuspendSettings() {
|
||||
suspendSaving.value = true;
|
||||
try {
|
||||
const { data } = await api.put('/admin/agents/settings/suspend', agentSuspendSettings.value);
|
||||
agentSuspendSettings.value = {
|
||||
suspendFreezeDirectPlayers: Boolean(data.data?.suspendFreezeDirectPlayers),
|
||||
suspendBlockPlayerLogin: Boolean(data.data?.suspendBlockPlayerLogin),
|
||||
};
|
||||
ElMessage.success(t('msg.saved'));
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
suspendSaving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function savePlatformDirectSettings() {
|
||||
platformDirectSaving.value = true;
|
||||
try {
|
||||
@@ -177,7 +216,7 @@ onMounted(() => {
|
||||
void router.replace('/users');
|
||||
return;
|
||||
}
|
||||
void Promise.all([loadSettings(), loadResetDatabaseStatus()]);
|
||||
void Promise.all([loadSettings(), loadAgentSuspendSettings(), loadResetDatabaseStatus()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -211,6 +250,17 @@ onMounted(() => {
|
||||
:disabled="hierarchySaving"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('agent.hierarchy.default_sub_credit_ratio')">
|
||||
<el-input-number
|
||||
v-model="hierarchySettings.defaultSubAgentCreditRatio"
|
||||
:min="1"
|
||||
:max="100"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
:disabled="hierarchySaving"
|
||||
/>
|
||||
</el-form-item>
|
||||
<p class="list-settings-hint block-hint">{{ t('agent.hierarchy.default_sub_credit_ratio_hint') }}</p>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="hierarchySaving" @click="saveHierarchySettings">
|
||||
{{ t('common.save') }}
|
||||
@@ -219,6 +269,27 @@ onMounted(() => {
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('agent.suspend.settings_title') }}</p>
|
||||
<p class="list-settings-hint">{{ t('agent.suspend.settings_hint') }}</p>
|
||||
<el-form inline size="small" class="settings-form">
|
||||
<el-form-item :label="t('agent.freeze.opt_freeze_direct_players')">
|
||||
<el-switch
|
||||
v-model="agentSuspendSettings.suspendFreezeDirectPlayers"
|
||||
:loading="suspendSaving"
|
||||
@change="saveAgentSuspendSettings"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('agent.freeze.opt_block_player_login')">
|
||||
<el-switch
|
||||
v-model="agentSuspendSettings.suspendBlockPlayerLogin"
|
||||
:loading="suspendSaving"
|
||||
@change="saveAgentSuspendSettings"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-settings-block">
|
||||
<p class="list-settings-title">{{ t('cashback.settings_title') }}</p>
|
||||
<el-form inline size="small" class="settings-form">
|
||||
|
||||
Reference in New Issue
Block a user