feat(admin+api): 代理停用默认、结算加固与冒烟配置探针

- 代理层级默认授信比例与停用冻结/禁登全局默认

- 结算预览去重、比分校验、串关当场判负与市场类型校验

- 站内信 Banner/公告自动通知开关;开发环境动态 API 端口

- 扩充 RBAC/结算/认证/返现单元测试与 agent skills
This commit is contained in:
2026-06-23 11:08:41 +08:00
parent fa06fee64c
commit ce84226219
47 changed files with 9284 additions and 118 deletions

View File

@@ -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;

View File

@@ -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>

View File

@@ -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">