feat: add Vite configuration for Tiku.PlatformAdmin.Web and update documentation

- Introduced Vite configuration files (vite.config.js, vite.config.ts, vite.config.d.ts) for the React frontend.
- Configured server proxy settings for API endpoints.
- Added Vitest configuration files (vitest.config.js, vitest.config.ts, vitest.config.d.ts) for testing.
- Updated architecture overview to reflect the separation of the platform admin frontend into its own React project.
- Modified quickstart documentation to guide users on starting the platform admin frontend.
This commit is contained in:
2026-07-30 14:54:53 +08:00
parent 8eaad9a358
commit 30fd159041
62 changed files with 63463 additions and 3760 deletions

View File

@@ -0,0 +1 @@

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

View File

@@ -1,35 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#111827" />
<title>恭学题库 SaaS · 平台管理</title>
<link rel="icon" href="./assets/logo.png" />
<link rel="stylesheet" href="./styles.css?v=20260728-api2" />
<script src="https://unpkg.com/lucide@0.468.0/dist/umd/lucide.min.js" defer onerror="document.documentElement.classList.add('icons-unavailable')"></script>
<script src="./runtime-config.js?v=20260730-question-bank1" defer></script>
<script src="./platform-auth.js?v=20260730-api-fix1" defer></script>
<script src="./spec-contract.js?v=20260730-question-bank1" defer></script>
<script src="./platform-api.js?v=20260730-api-fix1" defer></script>
<script src="./app.js?v=20260730-api-fix1" defer></script>
</head>
<body>
<div id="platformAuthGate" class="platform-auth-gate" hidden></div>
<div class="platform-app">
<aside class="platform-sidebar">
<button class="platform-brand" data-nav="overview"><img src="./assets/logo.png" alt="" /><span><strong>恭学 SaaS</strong><small>平台管理控制台</small></span></button>
<div class="environment"><i></i><span>生产环境</span><b>CN</b></div>
<nav id="sideNav"></nav>
<div class="platform-foot"><button data-nav="staff"><i data-lucide="shield-check"></i><span>平台权限</span></button><div><span></span><p><strong>陈浩</strong><small>超级管理员</small></p><button data-action="logout" aria-label="退出平台端"><i data-lucide="log-out"></i></button></div></div>
</aside>
<section class="platform-workspace">
<header class="platform-topbar"><button class="icon-button mobile-menu" id="menuButton"><i data-lucide="menu"></i></button><div class="page-context"><small>PLATFORM / PROD</small><strong id="pageTitle">平台经营工作台</strong></div><label class="platform-search"><i data-lucide="search"></i><input id="globalSearch" placeholder="搜索租户、账单、告警或审计事件" /><kbd>⌘ K</kbd></label><div class="prototype-controls"><label title="切换角色以验证菜单和动作权限"><span>角色</span><select id="roleSelect"></select></label><label title="演练加载、空、错误和 403 页面状态"><span>页面状态</span><select id="scenarioSelect"></select></label><label title="控制下一次 Mock 写请求结果"><span>写请求</span><select id="responseSelect"></select></label></div><div class="top-status"><span><i></i>所有系统正常</span><button class="icon-button" data-action="open-task-center" aria-label="任务中心" title="任务中心"><i data-lucide="list-checks"></i><b id="taskCount">0</b></button><button class="icon-button" data-nav="alerts" aria-label="告警与通知" title="告警与通知"><i data-lucide="bell-ring"></i><b id="alertCount">0</b></button></div></header>
<main id="mainView"></main>
</section>
</div>
<button class="sidebar-backdrop" id="sidebarBackdrop" aria-label="关闭导航菜单"></button>
<div id="overlayRoot"></div>
<div class="toast"><i data-lucide="circle-check"></i><span></span></div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,141 +0,0 @@
(() => {
const ACCESS_TOKEN_KEY = 'tiku_platform_access_token';
const REFRESH_TOKEN_KEY = 'tiku_platform_refresh_token';
const ACCESS_EXPIRES_KEY = 'tiku_platform_access_expires_at';
const USER_KEY = 'tiku_platform_user';
const runtime = window.GONGXUE_PLATFORM_RUNTIME_CONFIG || {};
let challengeToken = '';
let pendingResolve;
function apiUrl(path) {
return `${String(runtime.apiBaseUrl || '').replace(/\/+$/, '')}${path}`;
}
async function post(path, body) {
const response = await fetch(apiUrl(path), {
method: 'POST',
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
credentials: 'omit',
body: JSON.stringify(body),
});
const text = await response.text();
let payload = {};
if (text) {
try { payload = JSON.parse(text); } catch { payload = { detail: text.slice(0, 500) }; }
}
if (!response.ok) throw new Error(payload.detail || payload.title || payload.message || `请求失败HTTP ${response.status}`);
return payload;
}
function gate() { return document.querySelector('#platformAuthGate'); }
function shell(title, description, content) {
gate().innerHTML = `<section class="platform-auth-card" role="dialog" aria-modal="true"><header><img src="./assets/logo.png" alt="" /><div><h1>${title}</h1><p>${description}</p></div></header>${content}<p class="platform-auth-error" id="platformAuthError" role="alert"></p></section>`;
gate().hidden = false;
document.querySelector('.platform-app')?.setAttribute('inert', '');
}
function showError(error) {
const target = document.querySelector('#platformAuthError');
if (target) target.textContent = error?.message || '操作失败,请重试';
}
function setBusy(form, busy) {
form.querySelectorAll('button,input').forEach(control => { control.disabled = busy; });
}
function storeAuthenticated(result) {
const user = result?.user;
const tokens = user?.tokens;
if (!tokens?.accessToken || !tokens?.refreshToken) throw new Error('登录响应缺少令牌');
sessionStorage.setItem(ACCESS_TOKEN_KEY, tokens.accessToken);
sessionStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken);
sessionStorage.setItem(ACCESS_EXPIRES_KEY, tokens.accessTokenExpiresAt || '');
sessionStorage.setItem(USER_KEY, JSON.stringify({ userId: user.userId, email: user.email, name: user.name }));
}
function completeGate() {
gate().hidden = true;
gate().innerHTML = '';
document.querySelector('.platform-app')?.removeAttribute('inert');
pendingResolve?.(true);
pendingResolve = null;
}
function finishAuthentication(result) {
storeAuthenticated(result);
completeGate();
}
async function handleAuthenticationResult(result) {
challengeToken = result.challengeToken || '';
if (result.status === 'authenticated') { finishAuthentication(result); return; }
if (result.status === 'password_change_required') { renderPasswordChange(); return; }
throw new Error(`不支持的认证状态:${result.status || 'unknown'}`);
}
function renderLogin() {
shell('平台管理员登录', '连接真实 PostgreSQL 与 ASP.NET Core API。', `<form id="platformLoginForm"><label>平台账号<input name="identifier" type="email" autocomplete="username" required /></label><label>密码<input name="password" type="password" autocomplete="current-password" minlength="8" required /></label><button type="submit">登录</button></form>`);
document.querySelector('#platformLoginForm').addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget;
setBusy(form, true);
try {
const result = await post('/api/auth/login/password', { realm: 'platform', identifier: form.elements.identifier.value.trim(), password: form.elements.password.value });
await handleAuthenticationResult(result);
} catch (error) { showError(error); setBusy(form, false); }
});
}
function renderPasswordChange() {
shell('设置正式密码', '至少 8 位,且必须同时包含字母和数字。', `<form id="platformPasswordForm"><label>新密码<input name="newPassword" type="password" autocomplete="new-password" minlength="8" required /></label><label>确认新密码<input name="confirmPassword" type="password" autocomplete="new-password" minlength="8" required /></label><button type="submit">更新密码并继续</button></form>`);
document.querySelector('#platformPasswordForm').addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget;
if (form.elements.newPassword.value !== form.elements.confirmPassword.value) { showError(new Error('两次输入的密码不一致')); return; }
setBusy(form, true);
try { await handleAuthenticationResult(await post('/api/auth/password/change-required', { challengeToken, newPassword: form.elements.newPassword.value })); }
catch (error) { showError(error); setBusy(form, false); }
});
}
function clearSession() {
[ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, ACCESS_EXPIRES_KEY, USER_KEY].forEach(key => sessionStorage.removeItem(key));
}
function hasSession() {
const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
const expiresAt = Date.parse(sessionStorage.getItem(ACCESS_EXPIRES_KEY) || '');
return Boolean(token) && Number.isFinite(expiresAt) && expiresAt > Date.now() + 15_000;
}
async function requireSession() {
if (hasSession()) return true;
clearSession();
renderLogin();
return new Promise(resolve => { pendingResolve = resolve; });
}
function requireReauthentication() {
clearSession();
renderLogin();
pendingResolve = () => location.reload();
}
async function logout() {
const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_KEY);
try { if (refreshToken) await post('/api/auth/logout', { refreshToken }); } catch { /* Clear the browser session even when revocation cannot be reached. */ }
clearSession();
location.reload();
}
window.GongxuePlatformAuth = Object.freeze({
getAccessToken: () => sessionStorage.getItem(ACCESS_TOKEN_KEY) || '',
getUser: () => {
try { return JSON.parse(sessionStorage.getItem(USER_KEY) || 'null'); } catch { return null; }
},
logout,
requireReauthentication,
requireSession,
});
})();

View File

@@ -1,24 +0,0 @@
/*
* 平台端公开运行时配置。
*
* 默认连接同源后端。其他部署环境可在本文件之前注入同名对象,
* 或在部署时替换本文件。不要把 access token、service role key 或任何服务端密钥
* 写入静态文件getAccessToken 应从当前平台登录会话中按需读取短期 JWT。
*
* window.GONGXUE_PLATFORM_RUNTIME_CONFIG = {
* mode: 'api',
* apiBaseUrl: 'https://api.example.com',
* fallbackToMock: true,
* timeoutMs: 10000,
* getAccessToken: async () => {
* return sessionStorage.getItem('tiku_platform_access_token') || '';
* },
* };
*/
window.GONGXUE_PLATFORM_RUNTIME_CONFIG = window.GONGXUE_PLATFORM_RUNTIME_CONFIG || {
mode: 'api',
apiBaseUrl: '',
fallbackToMock: false,
timeoutMs: 10000,
getAccessToken: async () => window.GongxuePlatformAuth?.getAccessToken() || '',
};

View File

@@ -1,313 +0,0 @@
(() => {
const actions = [
['P-01', 'act.p-01.01', '进入租户/账务/题库/审计', 'command', 'native', 'nav', '[data-nav="tenants"],[data-nav="billing"],[data-nav="bank"],[data-nav="audit"]', 'PlatformAdminOverviewController_overview', 'allow'],
['P-01', 'act.p-01.02', '确认/解决告警', 'command', 'native', 'mutation', '[data-alert-detail],[data-action="transition-alert"]', 'PlatformAdminAuditController_alerts', 'allow'],
['P-01', 'act.p-01.03', '导出审计', 'async', 'native', 'task', '[data-action="export-audit"]', 'PlatformAdminAuditController_logs', 'allow'],
['P-01', 'act.p-01.04', '刷新', 'command', 'native', 'read', '[data-action="refresh"]', 'PlatformAdminOverviewController_overview', 'allow'],
['P-02', 'act.p-02.01', '搜索', 'command', 'native', 'read', '#tenantSearch,[data-action="apply-tenant-query"]', 'PlatformAdminTenantsController_list', 'allow'],
['P-02', 'act.p-02.02', '创建租户', 'command', 'native', 'mutation', '[data-action="create-tenant"],[form="createTenantForm"]', 'PlatformAdminTenantsController_create', 'block'],
['P-02', 'act.p-02.03', '打开详情', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminTenantsController_list', 'allow'],
['P-02', 'act.p-02.04', '准备暂停/恢复', 'destructive', 'native', 'mutation', '[data-action="prepare-tenant-status"],[form="tenantStatusForm"]', 'PlatformAdminTenantsController_status', 'block'],
['P-02', 'act.p-02.05', '去账务', 'command', 'native', 'nav', '[data-action="tenant-to-billing"]', 'PlatformAdminBillingController_invoices', 'allow'],
['P-03', 'act.p-03.01', '修改状态', 'command', 'native', 'mutation', '[data-action="prepare-tenant-status"],[form="tenantStatusForm"]', 'PlatformAdminTenantsController_status', 'block'],
['P-03', 'act.p-03.02', '编辑账务资料', 'command', 'native', 'nav', '[data-action="edit-billing-profile"]', 'PlatformAdminTenantsController_billingProfile', 'block'],
['P-03', 'act.p-03.03', '创建订阅', 'command', 'native', 'mutation', '[data-action="create-subscription-for-tenant"],[form="subscriptionForm"]', 'PlatformAdminBillingController_createSubscription', 'block'],
['P-03', 'act.p-03.04', '查看账单/用量', 'command', 'native', 'nav', '[data-action="tenant-to-billing"],[data-action="tenant-to-usage"]', 'PlatformAdminTenantsController_detail', 'allow'],
['P-03', 'act.p-03.05', '打开租户门户入口', 'command', 'native', 'nav', '[data-action="open-tenant-portal"]', 'PlatformAdminTenantsController_detail', 'allow'],
['P-04', 'act.p-04.01', '编辑', 'command', 'native', 'mutation', '#billingProfileForm', 'PlatformAdminTenantsController_billingProfile', 'block'],
['P-04', 'act.p-04.02', '保存', 'command', 'native', 'mutation', '[form="billingProfileForm"]', 'PlatformAdminTenantsController_billingProfile', 'block'],
['P-04', 'act.p-04.03', '查看关联账单', 'command', 'native', 'nav', '[data-action="billing-profile-to-invoices"]', 'PlatformAdminTenantsController_billingProfile', 'allow'],
['P-05', 'act.p-05.01', '选择套餐', 'command', 'native', 'mutation', '[data-action="select-plan"]', 'PlatformAdminOverviewController_plans', 'block'],
['P-05', 'act.p-05.02', '创建订阅', 'command', 'native', 'mutation', '[data-action="create-subscription"],[form="subscriptionForm"]', 'PlatformAdminBillingController_createSubscription', 'block'],
['P-05', 'act.p-05.03', '进入账单候选', 'command', 'native', 'nav', '[data-action="subscription-to-billing"]', 'PlatformAdminBillingController_createSubscription', 'allow'],
['P-05', 'act.p-05.04', '查看租户详情', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminBillingController_createSubscription', 'allow'],
['P-06', 'act.p-06.01', '查询', 'command', 'contract', 'read', '[data-action="apply-invoice-query"]', 'PlatformAdminBillingController_invoices', 'allow'],
['P-06', 'act.p-06.02', '手工创建', 'command', 'native', 'mutation', '[data-action="create-invoice"],[form="invoiceForm"]', 'PlatformAdminBillingController_createInvoice', 'block'],
['P-06', 'act.p-06.03', '预览候选', 'command', 'native', 'read', '[data-action="preview-invoice-candidates"]', 'PlatformAdminBillingController_subscriptionCandidates', 'block'],
['P-06', 'act.p-06.04', '单个生成', 'async', 'contract', 'task', '[data-action="generate-subscription-invoice"]', 'PlatformAdminBillingController_fromSubscription', 'block'],
['P-06', 'act.p-06.05', '批量 dry-run', 'command', 'native', 'read', '[data-action="run-invoice-dry-run"]', 'PlatformAdminBillingController_fromSubscriptionsBatch', 'block'],
['P-06', 'act.p-06.06', '确认生成', 'async', 'native', 'task', '[data-action="confirm-generate-candidates"]', 'PlatformAdminBillingController_fromSubscriptionsBatch', 'block'],
['P-06', 'act.p-06.07', '打开租户', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminBillingController_invoices', 'allow'],
['P-07', 'act.p-07.01', '记录用量', 'command', 'native', 'mutation', '[data-action="record-usage"],[form="usageForm"]', 'PlatformAdminBillingController_recordUsage', 'block'],
['P-07', 'act.p-07.02', '查询', 'command', 'native', 'read', '#usagePeriodFilter', 'PlatformAdminBillingController_usage', 'allow'],
['P-07', 'act.p-07.03', '预览超额候选', 'command', 'native', 'read', '[data-action="preview-overage"]', 'PlatformAdminBillingController_usageCandidates', 'block'],
['P-07', 'act.p-07.04', 'dry-run', 'command', 'native', 'read', '[data-action="run-overage-dry-run"]', 'PlatformAdminBillingController_fromUsage', 'block'],
['P-07', 'act.p-07.05', '生成超额账单', 'async', 'native', 'task', '[data-action="confirm-overage-invoices"]', 'PlatformAdminBillingController_fromUsage', 'block'],
['P-07', 'act.p-07.06', '跳租户详情', 'command', 'native', 'nav', '[data-tenant-detail]', 'PlatformAdminBillingController_usage', 'allow'],
['P-08', 'act.p-08.01', '确认收款', 'command', 'native', 'mutation', '[data-action="confirm-payment"],[form="paymentForm"]', 'PlatformAdminBillingController_confirmPayment', 'block'],
['P-08', 'act.p-08.02', '预览逾期', 'command', 'native', 'read', '[data-action="preview-overdue"]', 'PlatformAdminBillingController_processOverdue', 'block'],
['P-08', 'act.p-08.03', '生成催缴', 'async', 'native', 'task', '[data-action="confirm-overdue-process"],[data-action="generate-reminder"]', 'PlatformAdminBillingController_processOverdue', 'block'],
['P-08', 'act.p-08.04', '查询提醒', 'command', 'native', 'read', '[data-billing-tab="reminders"]', 'PlatformAdminBillingController_reminders', 'allow'],
['P-08', 'act.p-08.05', '新增/编辑/启停渠道', 'command', 'native', 'mutation', '[data-action="new-dunning-channel"],[data-action="edit-dunning-channel"],[data-action="toggle-dunning-channel"]', 'PlatformAdminDunningChannelsController_upsertChannel', 'block'],
['P-08', 'act.p-08.06', '查看通知事件', 'command', 'contract', 'read', '[data-notification-detail]', 'PlatformAdminDunningEventsController_events', 'allow'],
['P-09', 'act.p-09.01', '查询与选择题库', 'command', 'native', 'read', '[data-action="apply-bank-search"],[data-bank-select]', 'PlatformQuestionBanksController_getBanks', 'allow'],
['P-09', 'act.p-09.02', '维护题库与内容结构', 'command', 'native', 'mutation', '[data-action="new-bank"],[data-action="edit-bank"],[data-action="new-bank-node"],[data-action="batch-bank-nodes"]', 'PlatformQuestionBanksController_upsertNode', 'block'],
['P-09', 'act.p-09.03', '单题录入与题目管理', 'command', 'native', 'mutation', '#bankQuestionForm,[data-action="edit-bank-question"],[data-action="archive-bank-question"]', 'PlatformQuestionBanksController_upsertQuestion', 'block'],
['P-09', 'act.p-09.04', '普通题目批量导入', 'async', 'native', 'task', '#bankBatchImportForm', 'PlatformQuestionBanksController_executeImport', 'block'],
['P-09', 'act.p-09.05', '结构化题库文件导入', 'async', 'native', 'task', '#bankStructuredImportForm', 'PlatformQuestionBanksController_executeImport', 'block'],
['P-10', 'act.p-10.01', '切换 CRM 视图', 'command', 'native', 'read', '[data-crm-tab]', 'PlatformAdminCrmController_configs', 'allow'],
['P-10', 'act.p-10.02', '新建/编辑 CRM 接入', 'command', 'native', 'mutation', '[data-action="new-crm-config"],[data-action="edit-crm-config"]', 'PlatformAdminCrmController_upsertConfig', 'block'],
['P-10', 'act.p-10.03', '启停 CRM 接入', 'destructive', 'native', 'mutation', '[data-action="toggle-crm-config"]', 'PlatformAdminCrmController_upsertConfig', 'block'],
['P-10', 'act.p-10.04', '重试线索推送', 'command', 'native', 'mutation', '[data-action="retry-crm-lead"]', 'PlatformAdminCrmController_retryLead', 'block'],
['P-11', 'act.p-11.01', '切换短信视图', 'command', 'native', 'read', '[data-sms-tab]', 'PlatformAdminSmsController_channels', 'allow'],
['P-11', 'act.p-11.02', '新建/编辑短信渠道', 'command', 'native', 'mutation', '[data-action="new-sms-channel"],[data-action="edit-sms-channel"]', 'PlatformAdminSmsController_upsertChannel', 'block'],
['P-11', 'act.p-11.03', '启停短信渠道', 'destructive', 'native', 'mutation', '[data-action="toggle-sms-channel"]', 'PlatformAdminSmsController_upsertChannel', 'block'],
['P-12', 'act.p-12.01', '切换支付设置视图', 'command', 'native', 'read', '[data-payment-tab]', 'PlatformAdminPaymentSettingsController_apps', 'allow'],
['P-12', 'act.p-12.02', '新建/编辑支付应用', 'command', 'native', 'mutation', '[data-action="new-payment-app"],[data-action="edit-payment-app"]', 'PlatformAdminPaymentSettingsController_upsertApp', 'block'],
['P-12', 'act.p-12.03', '启停支付应用', 'destructive', 'native', 'mutation', '[data-action="toggle-payment-app"]', 'PlatformAdminPaymentSettingsController_upsertApp', 'block'],
['P-12', 'act.p-12.04', '查看风控规则', 'command', 'native', 'read', '[data-action="payment-rule-detail"]', 'PlatformAdminPaymentSettingsController_events', 'allow'],
['P-13', 'act.p-13.01', '新建/编辑', 'command', 'native', 'mutation', '[data-action="new-staff"],[data-action="edit-staff"]', 'PlatformAdminOverviewController_upsertStaff', 'block'],
['P-13', 'act.p-13.02', '全选/分组授权', 'command', 'native', 'mutation', '[data-action="select-permission-group"]', 'PlatformAdminOverviewController_permissions', 'block'],
['P-13', 'act.p-13.03', '启用/禁用', 'destructive', 'native', 'mutation', '[data-action="toggle-staff"]', 'PlatformAdminOverviewController_updateStaffStatus', 'block'],
['P-13', 'act.p-13.04', '禁用时撤销会话', 'destructive', 'native', 'audit', '[data-action="execute-staff-disable"]', 'PlatformAdminOverviewController_updateStaffStatus', 'block'],
['P-13', 'act.p-13.05', '清空表单', 'command', 'contract', 'mutation', '[data-action="clear-staff-form"]', 'PlatformAdminOverviewController_upsertStaff', 'block'],
['P-14', 'act.p-14.01', '筛选', 'command', 'native', 'read', '#auditSearch,#auditRangeFilter,[data-audit-severity]', 'PlatformAdminAuditController_logs', 'allow'],
['P-14', 'act.p-14.02', '查看详情', 'command', 'native', 'read', '[data-audit-detail]', 'PlatformAdminAuditController_logs', 'allow'],
['P-14', 'act.p-14.03', '导出 CSV/JSON', 'async', 'native', 'task', '[data-action="export-audit"]', 'PlatformAdminAuditController_exportLogs', 'allow'],
['P-14', 'act.p-14.04', '复制 ID', 'command', 'native', 'read', '[data-copy]', 'PlatformAdminAuditController_logs', 'allow'],
['P-14', 'act.p-14.05', '跳目标对象', 'command', 'native', 'nav', '[data-action="jump-audit-target"],[data-action="jump-related-alert"]', 'PlatformAdminAuditController_logs', 'allow'],
['P-15', 'act.p-15.01', '确认/解决告警', 'command', 'native', 'mutation', '[data-action="transition-alert"]', 'PlatformAdminAuditController_updateAlert', 'allow'],
['P-15', 'act.p-15.02', '查看证据', 'command', 'native', 'read', '[data-alert-detail]', 'PlatformAdminAuditController_alerts', 'allow'],
['P-15', 'act.p-15.03', '新建/编辑/启停通知渠道', 'command', 'native', 'mutation', '[data-action="new-audit-channel"],[data-action="edit-audit-channel"],[data-action="toggle-audit-channel"]', 'PlatformAdminAuditController_upsertChannel', 'block'],
['P-15', 'act.p-15.04', '查看失败事件', 'command', 'contract', 'read', '[data-notification-detail]', 'PlatformAdminAuditController_events', 'allow'],
].map(([pageId, id, label, kind, mode, effect, selector, operationId, mobile]) => ({ pageId, id, label, kind, mode, effect, selector, operationIds: [operationId], mobile, destructive: kind === 'destructive', implemented: true }));
const operationRows = [
['P-13', 'PlatformBackofficeController_bootstrap', 'GET', '/api/backoffice/platform/bootstrap'],
['P-01', 'PlatformAdminOverviewController_overview', 'GET', '/api/platform-admin/overview'],
['P-01', 'PlatformAdminAuditController_alerts', 'GET', '/api/platform-admin/audit-alerts'],
['P-01', 'PlatformAdminAuditController_logs', 'GET', '/api/platform-admin/audit-logs'],
['P-02', 'PlatformAdminTenantsController_list', 'GET', '/api/platform-admin/tenants'],
['P-02', 'PlatformAdminTenantsController_create', 'POST', '/api/platform-admin/tenants'],
['P-02A', 'PlatformAdminTenantsController_domains', 'GET', '/api/platform-admin/domains'],
['P-02A', 'PlatformAdminTenantsController_recheckDomain', 'POST', '/api/platform-admin/domains/{domainId}/recheck'],
['P-03', 'PlatformAdminTenantsController_detail', 'GET', '/api/platform-admin/tenants/detail'],
['P-03', 'PlatformAdminTenantsController_status', 'PATCH', '/api/platform-admin/tenants/status'],
['P-03', 'PlatformAdminTenantsController_billingProfile', 'PUT', '/api/platform-admin/tenants/billing-profile'],
['P-03', 'PlatformAdminBillingController_createSubscription', 'POST', '/api/platform-admin/saas/subscriptions'],
['P-04', 'PlatformAdminTenantsController_billingProfile', 'PUT', '/api/platform-admin/tenants/billing-profile'],
['P-05', 'PlatformAdminOverviewController_plans', 'GET', '/api/platform-admin/saas/catalog'],
['P-05A', 'PlatformSaasController_upsertFeature', 'PUT', '/api/platform-admin/saas/features'],
['P-05A', 'PlatformSaasController_upsertFeatureLimit', 'PUT', '/api/platform-admin/saas/feature-limits'],
['P-05A', 'PlatformSaasController_upsertOffering', 'PUT', '/api/platform-admin/saas/offerings'],
['P-05A', 'PlatformSaasController_upsertVersion', 'PUT', '/api/platform-admin/saas/offering-versions'],
['P-05A', 'PlatformSaasController_publishVersion', 'POST', '/api/platform-admin/saas/offering-versions/{versionId}/publish'],
['P-05A', 'PlatformSaasController_cloneVersion', 'POST', '/api/platform-admin/saas/offering-versions/{versionId}/clone'],
['P-05A', 'PlatformSaasController_retireVersion', 'POST', '/api/platform-admin/saas/offering-versions/{versionId}/retire'],
['P-05B', 'PlatformAdminBillingController_orders', 'GET', '/api/platform-admin/saas/orders'],
['P-05B', 'PlatformAdminBillingController_refunds', 'GET', '/api/platform-admin/saas/refunds'],
['P-05A', 'PlatformSaasController_upsertTenantFeatureOverride', 'PUT', '/api/platform-admin/saas/tenant-feature-overrides'],
['P-05', 'PlatformAdminBillingController_subscriptions', 'GET', '/api/platform-admin/saas/subscriptions'],
['P-05', 'PlatformAdminBillingController_createSubscription', 'POST', '/api/platform-admin/saas/subscriptions'],
['P-06', 'PlatformAdminBillingController_invoices', 'GET', '/api/platform-admin/saas/invoices'],
['P-06', 'PlatformAdminBillingController_createInvoice', 'POST', '/api/platform-admin/saas/invoices'],
['P-06', 'PlatformAdminBillingController_subscriptionCandidates', 'GET', '/api/platform-admin/saas/invoices/subscription-candidates'],
['P-06', 'PlatformAdminBillingController_fromSubscription', 'POST', '/api/platform-admin/saas/invoices/from-subscription'],
['P-06', 'PlatformAdminBillingController_fromSubscriptionsBatch', 'POST', '/api/platform-admin/saas/invoices/from-subscriptions-batch'],
['P-07', 'PlatformAdminBillingController_usage', 'GET', '/api/platform-admin/saas/usage'],
['P-07', 'PlatformAdminBillingController_recordUsage', 'POST', '/api/platform-admin/saas/usage'],
['P-07', 'PlatformAdminBillingController_usageCandidates', 'GET', '/api/platform-admin/saas/invoices/usage-overage-candidates'],
['P-07', 'PlatformAdminBillingController_fromUsage', 'POST', '/api/platform-admin/saas/invoices/from-usage-overage'],
['P-08', 'PlatformAdminBillingController_confirmPayment', 'POST', '/api/platform-admin/saas/payments/manual/confirm'],
['P-08', 'PlatformAdminBillingController_payments', 'GET', '/api/platform-admin/saas/payments'],
['P-08', 'PlatformAdminBillingController_processOverdue', 'POST', '/api/platform-admin/saas/invoices/process-overdue'],
['P-08', 'PlatformAdminBillingController_reminders', 'GET', '/api/platform-admin/saas/invoices/reminders'],
['P-08', 'PlatformAdminDunningChannelsController_channels', 'GET', '/api/platform-admin/saas/dunning/channels'],
['P-08', 'PlatformAdminDunningChannelsController_upsertChannel', 'PUT', '/api/platform-admin/saas/dunning/channels'],
['P-08', 'PlatformAdminDunningChannelsController_disableChannel', 'POST', '/api/platform-admin/saas/dunning/channels/disable'],
['P-08', 'PlatformAdminDunningEventsController_events', 'GET', '/api/platform-admin/saas/dunning/events'],
['P-08', 'PlatformAdminDunningEventsController_eventDetail', 'GET', '/api/platform-admin/saas/dunning/events/detail'],
['P-08', 'PlatformAdminDunningEventsController_retryEvent', 'POST', '/api/platform-admin/saas/dunning/events/retry'],
['P-09', 'PlatformQuestionBanksController_getBanks', 'GET', '/api/platform-admin/question-banks'],
['P-09', 'PlatformQuestionBanksController_upsertBank', 'PUT', '/api/platform-admin/question-banks'],
['P-09', 'PlatformQuestionBanksController_archiveBank', 'POST', '/api/platform-admin/question-banks/{bankId}/archive'],
['P-09', 'PlatformQuestionBanksController_getNodes', 'GET', '/api/platform-admin/question-banks/{bankId}/nodes'],
['P-09', 'PlatformQuestionBanksController_upsertNode', 'PUT', '/api/platform-admin/question-banks/nodes'],
['P-09', 'PlatformQuestionBanksController_batchCreateNodes', 'POST', '/api/platform-admin/question-banks/nodes/batch'],
['P-09', 'PlatformQuestionBanksController_archiveNode', 'POST', '/api/platform-admin/question-banks/nodes/{nodeId}/archive'],
['P-09', 'PlatformQuestionBanksController_getQuestions', 'GET', '/api/platform-admin/question-banks/questions'],
['P-09', 'PlatformQuestionBanksController_upsertQuestion', 'PUT', '/api/platform-admin/question-banks/questions'],
['P-09', 'PlatformQuestionBanksController_archiveQuestions', 'POST', '/api/platform-admin/question-banks/questions/archive'],
['P-09', 'PlatformQuestionBanksController_previewImport', 'POST', '/api/platform-admin/question-banks/imports/preview'],
['P-09', 'PlatformQuestionBanksController_executeImport', 'POST', '/api/platform-admin/question-banks/imports'],
['P-09', 'PlatformQuestionBanksController_getImport', 'GET', '/api/platform-admin/question-banks/imports/{jobId}'],
['P-09', 'PlatformQuestionBanksController_signAssetUpload', 'POST', '/api/platform-admin/question-banks/assets/upload-sign'],
['P-09', 'PlatformQuestionBanksController_confirmAssetUpload', 'POST', '/api/platform-admin/question-banks/assets/upload-confirm'],
['P-10', 'PlatformAdminCrmController_configs', 'GET', '/api/platform-admin/tenant-capabilities/crm/configs'],
['P-10', 'PlatformAdminCrmController_upsertConfig', 'PUT', '/api/platform-admin/tenant-capabilities/crm/configs'],
['P-10', 'PlatformAdminCrmController_leads', 'GET', '/api/platform-admin/tenant-capabilities/crm/leads'],
['P-10', 'PlatformAdminCrmController_retryLead', 'POST', '/api/platform-admin/tenant-capabilities/crm/leads/retry'],
['P-10', 'PlatformAdminCrmController_logs', 'GET', '/api/platform-admin/tenant-capabilities/crm/logs'],
['P-11', 'PlatformAdminSmsController_channels', 'GET', '/api/platform-admin/tenant-capabilities/sms/channels'],
['P-11', 'PlatformAdminSmsController_disableChannel', 'POST', '/api/platform-admin/tenant-capabilities/sms/channels/{channelId}/disable'],
['P-11', 'PlatformAdminSmsController_templates', 'GET', '/api/platform-admin/tenant-capabilities/sms/templates'],
['P-11', 'PlatformAdminSmsController_submitTemplateReview', 'POST', '/api/platform-admin/tenant-capabilities/sms/templates/{templateId}/submit-review'],
['P-11', 'PlatformAdminSmsController_disableTemplate', 'POST', '/api/platform-admin/tenant-capabilities/sms/templates/{templateId}/disable'],
['P-11', 'PlatformAdminSmsController_logs', 'GET', '/api/platform-admin/tenant-capabilities/sms/logs'],
['P-11', 'PlatformAdminSmsController_upsertChannel', 'PUT', '/api/platform-admin/tenant-capabilities/sms/channels'],
['P-11', 'PlatformAdminSmsController_upsertTemplate', 'PUT', '/api/platform-admin/tenant-capabilities/sms/templates'],
['P-12', 'PlatformAdminPaymentSettingsController_apps', 'GET', '/api/platform-admin/payment-settings/apps'],
['P-12', 'PlatformAdminPaymentSettingsController_channels', 'GET', '/api/platform-admin/payment-settings/channels'],
['P-12', 'PlatformAdminPaymentSettingsController_upsertChannel', 'PUT', '/api/platform-admin/payment-settings/channels'],
['P-12', 'PlatformAdminPaymentSettingsController_disableChannel', 'POST', '/api/platform-admin/payment-settings/channels/{channelId}/disable'],
['P-12', 'PlatformAdminPaymentSettingsController_rebateSummary', 'GET', '/api/platform-admin/payment-settings/rebates/summary'],
['P-12', 'PlatformAdminPaymentSettingsController_events', 'GET', '/api/platform-admin/payment-settings/events'],
['P-12', 'PlatformAdminPaymentSettingsController_rebates', 'GET', '/api/platform-admin/payment-settings/rebates/summary'],
['P-12', 'PlatformAdminPaymentSettingsController_upsertApp', 'PUT', '/api/platform-admin/payment-settings/apps'],
['P-12', 'PlatformAdminTenantPaymentSettingsController_apps', 'GET', '/api/platform-admin/tenant-capabilities/payments/apps'],
['P-12', 'PlatformAdminTenantPaymentSettingsController_events', 'GET', '/api/platform-admin/tenant-capabilities/payments/events'],
['P-12', 'PlatformAdminTenantPaymentSettingsController_upsertApp', 'PUT', '/api/platform-admin/tenant-capabilities/payments/apps'],
['P-13', 'PlatformAdminOverviewController_permissions', 'GET', '/api/platform-admin/permissions'],
['P-13', 'PlatformAdminOverviewController_staff', 'GET', '/api/platform-admin/staff'],
['P-13', 'PlatformAdminOverviewController_upsertStaff', 'PUT', '/api/platform-admin/staff'],
['P-13', 'PlatformAdminOverviewController_updateStaffStatus', 'PATCH', '/api/platform-admin/staff/status'],
['P-13A', 'PlatformBackofficeController_upsertRole', 'POST', '/api/backoffice/platform/roles'],
['P-13A', 'PlatformBackofficeController_replaceRoleBindings', 'PUT', '/api/backoffice/platform/roles/{roleId}/bindings'],
['P-14', 'PlatformAdminAuditController_logs', 'GET', '/api/platform-admin/audit-logs'],
['P-14', 'PlatformAdminAuditController_exportLogs', 'GET', '/api/platform-admin/audit-logs/export'],
['P-15', 'PlatformAdminAuditController_rules', 'GET', '/api/platform-admin/audit-alert-rules'],
['P-15', 'PlatformAdminAuditController_alerts', 'GET', '/api/platform-admin/audit-alerts'],
['P-15', 'PlatformAdminAuditController_updateAlert', 'POST', '/api/platform-admin/audit-alerts/status'],
['P-15', 'PlatformAdminAuditController_channels', 'GET', '/api/platform-admin/audit-notification-channels'],
['P-15', 'PlatformAdminAuditController_upsertChannel', 'PUT', '/api/platform-admin/audit-notification-channels'],
['P-15', 'PlatformAdminAuditController_events', 'GET', '/api/platform-admin/audit-notification-events'],
];
const operations = operationRows.map(([pageId, operationId, method, path]) => ({ pageId, operationId, method, path }));
const operationById = Object.fromEntries(operations.map(operation => [operation.operationId, operation]));
const navEdges = [
['nav.p-01.p-02', 'P-01', 'P-02', '[data-nav="tenants"]'],
['nav.p-01.p-06', 'P-01', 'P-06', '[data-nav="billing"]'],
['nav.p-01.p-10', 'P-01', 'P-10', '[data-nav="crm"]'],
['nav.p-01.p-11', 'P-01', 'P-11', '[data-nav="sms"]'],
['nav.p-01.p-12', 'P-01', 'P-12', '[data-nav="paymentSettings"]'],
['nav.p-01.p-15', 'P-01', 'P-15', '[data-nav="alerts"],[data-alert-detail]'],
['nav.p-02.p-03', 'P-02', 'P-03', '[data-tenant-detail]'],
['nav.p-03.p-04', 'P-03', 'P-04', '[data-action="edit-billing-profile"]'],
['nav.p-03.p-05', 'P-03', 'P-05', '[data-action="create-subscription-for-tenant"]'],
['nav.p-03.p-06', 'P-03', 'P-06', '[data-action="tenant-to-billing"]'],
['nav.p-05.p-06', 'P-05', 'P-06', '[data-action="subscription-to-billing"]'],
['nav.p-05.p-07', 'P-05', 'P-07', '[data-action="subscriptions-to-usage"]'],
['nav.p-06.p-08', 'P-06', 'P-08', '[data-nav="dunning"]'],
['nav.p-14.p-15', 'P-14', 'P-15', '[data-action="jump-related-alert"]'],
].map(([id, fromPageId, toPageId, selector]) => ({ id, fromPageId, toPageId, selector, implemented: true }));
const supportingReads = [];
const trace = [];
const decoratedActionIds = new Set();
const selectorCollisions = new Set();
const explicitResultActionIds = new Set(['act.p-06.01', 'act.p-06.04', 'act.p-09.01', 'act.p-09.05']);
function pushTrace(entry) {
trace.push({ at: new Date().toISOString(), ...entry });
if (trace.length > 120) trace.splice(0, trace.length - 120);
document.documentElement.dataset.platformContractEvents = String(trace.length);
}
function decorate(root = document, pageId = '') {
const shell = root.querySelector?.('.page-shell');
if (shell && pageId) {
const pageOperations = operations.filter(operation => operation.pageId === pageId);
shell.dataset.specPageId = pageId;
shell.dataset.operationIds = pageOperations.map(operation => operation.operationId).join(' ');
shell.dataset.operationContracts = pageOperations.map(operation => `${operation.method} ${operation.path}`).join(' | ');
}
actions.filter(action => action.pageId === pageId).forEach(action => {
root.querySelectorAll?.(action.selector).forEach(element => {
const existingActionId = element.dataset.specActionId;
if (existingActionId && existingActionId !== action.id) selectorCollisions.add(`${existingActionId} -> ${action.id}`);
element.dataset.specActionId = action.id;
element.dataset.specActionMode = action.mode;
element.dataset.specEffect = action.effect;
element.dataset.mobileBoundary = action.mobile;
const operation = operationById[action.operationIds[0]];
if (operation) {
element.dataset.operationId = operation.operationId;
element.dataset.operationMethod = operation.method;
element.dataset.operationPath = operation.path;
}
decoratedActionIds.add(action.id);
});
});
supportingReads.filter(read => read.pageId === pageId).forEach(read => {
const operation = operationById[read.operationId];
root.querySelectorAll?.(read.selector).forEach(element => {
element.dataset.specSupportingRead = 'true';
element.dataset.specEffect = 'read';
if (operation) {
element.dataset.operationId = operation.operationId;
element.dataset.operationMethod = operation.method;
element.dataset.operationPath = operation.path;
}
});
});
navEdges.filter(edge => edge.fromPageId === pageId).forEach(edge => {
root.querySelectorAll?.(edge.selector).forEach(element => {
element.dataset.navEdgeId = edge.id;
element.dataset.navTargetPage = edge.toPageId;
});
});
window.__PLATFORM_SPEC_COVERAGE__ = coverage();
}
function eventContract(target) {
const element = target?.closest?.('[data-spec-action-id]');
if (!element) return null;
return { element, actionId: element.dataset.specActionId, operationId: element.dataset.operationId || '', requestId: `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}` };
}
document.addEventListener('click', event => {
const contract = eventContract(event.target);
if (!contract) return;
contract.element.dataset.contractRequestId = contract.requestId;
pushTrace({ phase: 'before', actionId: contract.actionId, operationId: contract.operationId, requestId: contract.requestId });
setTimeout(() => pushTrace({ phase: 'after', actionId: contract.actionId, operationId: contract.operationId, requestId: contract.requestId, result: 'handled', pageId: document.querySelector('.page-shell')?.dataset.specPageId || '' }), 0);
}, true);
document.addEventListener('change', event => {
const contract = eventContract(event.target);
if (!contract) return;
pushTrace({ phase: 'after', actionId: contract.actionId, operationId: contract.operationId, requestId: contract.requestId, result: 'changed' });
}, true);
const coverage = () => ({
expected: 70,
total: actions.length,
unique: new Set(actions.map(action => action.id)).size,
catalogued: actions.filter(action => action.label && action.kind).length,
selectorDeclared: actions.filter(action => action.selector).length,
nativeMetadata: actions.filter(action => action.mode === 'native').length,
contractMetadata: actions.filter(action => action.mode === 'contract').length,
nativeMapped: actions.filter(action => action.mode === 'native' && decoratedActionIds.has(action.id)).length,
dedicatedExecutor: explicitResultActionIds.size,
dedicatedExecutorDefinition: 'actions with explicit success/failure result recording',
reachable: actions.filter(action => action.implemented && action.selector).length,
mountedThisSession: decoratedActionIds.size,
decoratedInSession: decoratedActionIds.size,
notYetDecorated: actions.filter(action => !decoratedActionIds.has(action.id)).map(action => action.id),
implemented: actions.filter(action => action.implemented && action.selector).length,
unimplemented: actions.filter(action => !action.implemented || !action.selector).map(action => action.id),
selectorCollisions: Array.from(selectorCollisions),
apiExact: operations.filter(operation => operation.operationId && operation.method && operation.path).length,
uiOnlyExplained: actions.filter(action => action.operationIds.length === 0).length,
effectDeclared: actions.filter(action => action.effect).length,
mobileDeclared: actions.filter(action => action.mobile).length,
actuallyExecuted: new Set(trace.filter(entry => entry.phase === 'result' && entry.result === 'success').map(entry => entry.actionId)).size,
executionNote: 'catalogue and selector metadata are not counted as successful business execution',
navigation: { expected: 14, total: navEdges.length, unique: new Set(navEdges.map(edge => edge.id)).size },
pageBindings: { expected: 63, total: operations.length, unique: new Set(operations.map(operation => `${operation.pageId}:${operation.operationId}`)).size },
});
const assertions = {
pages: new Set(actions.map(action => action.pageId)).size === 15,
actionIds: actions.every(action => action.id.startsWith(`act.${action.pageId.toLowerCase()}.`)),
duplicateActions: new Set(actions.map(action => action.id)).size === actions.length,
operationContracts: operations.every(operation => /^(GET|POST|PUT|PATCH|DELETE)$/.test(operation.method) && operation.path.startsWith('/api/')),
navTargets: navEdges.every(edge => actions.some(action => action.pageId === edge.fromPageId) && actions.some(action => action.pageId === edge.toPageId)),
};
window.GONGXUE_PLATFORM_SPEC = { actions, operations, navEdges, supportingReads };
window.__GONGXUE_ACTION_CONTRACT__ = { coverage, assertions, trace };
window.decoratePlatformSpec = decorate;
window.recordPlatformSpecResult = (actionId, result = {}) => pushTrace({ phase: 'result', actionId, ...result });
window.__PLATFORM_CONTRACT_EVENTS__ = trace;
window.__PLATFORM_SPEC_COVERAGE__ = coverage();
})();

File diff suppressed because one or more lines are too long