feat: add platform public question bank workspace

This commit is contained in:
2026-07-30 12:18:14 +08:00
parent bf370605d4
commit 96c20a3b1d
17 changed files with 22100 additions and 212 deletions

View File

@@ -52,9 +52,10 @@
return String(token || '').trim();
}
function buildUrl(path, query = {}) {
function buildUrl(path, query = {}, pathParams = {}) {
const baseUrl = normalizeBaseUrl(runtime.apiBaseUrl);
const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`, window.location.href);
const resolvedPath = Object.entries(pathParams).reduce((value, [key, replacement]) => value.replace(`{${key}}`, encodeURIComponent(String(replacement))), path);
const url = new URL(`${baseUrl}${resolvedPath.startsWith('/') ? resolvedPath : `/${resolvedPath}`}`, window.location.href);
Object.entries(query).forEach(([key, value]) => {
if (value === undefined || value === null || value === '') return;
url.searchParams.set(key, Array.isArray(value) ? value.join(',') : String(value));
@@ -124,7 +125,7 @@
else options.signal.addEventListener('abort', () => controller.abort(), { once: true });
}
try {
const response = await fetch(buildUrl(operation.path, options.query), {
const response = await fetch(buildUrl(operation.path, options.query, options.pathParams), {
method: operation.method,
headers: {
...extraHeaders,
@@ -196,9 +197,6 @@
invoiceTypeFromApi: { subscription: '订阅服务费', usage: '用量超额费', usage_overage: '用量超额费', manual: '人工账单', adjustment: '人工调整' },
severityToApi: { : 'low', : 'medium', : 'high', 严重: 'critical' },
severityFromApi: { low: '低', medium: '中', high: '高', critical: '严重' },
grantScopeToApi: { 全部活跃租户: 'all_active_tenants', 指定套餐: 'plans', 指定租户: 'tenants', '套餐 + 租户': 'mixed' },
grantScopeFromApi: { all_active_tenants: '全部活跃租户', plans: '指定套餐', tenants: '指定租户', mixed: '套餐 + 租户' },
grantStatusFromApi: { active: '启用', disabled: '已禁用', expired: '已过期' },
providerToApi: { Webhook: 'generic', SMTP: 'generic', 邮件: 'generic', 短信: 'generic', 钉钉: 'dingtalk', 飞书: 'feishu', 企微: 'wecom' },
providerFromApi: { generic: 'Webhook', dingtalk: '钉钉', feishu: '飞书', wecom: '企微' },
eventStatusFromApi: { pending: '待处理', processing: '发送中', sent: '成功', retrying: '重试中', failed: '失败', discarded: '已丢弃' },
@@ -211,6 +209,28 @@
paymentStatusToApi: { 正常: 'active', 未启用: 'disabled', 测试中: 'testing' },
};
const platformPermissionLabels = {
'platform:dashboard:view': '查看平台经营概览',
'platform:tenant:manage': '管理平台租户',
'platform:staff:manage': '管理平台员工',
'platform:role:manage': '管理平台角色与权限',
'platform:question-bank:manage': '运营平台公共题库',
'platform:audit:view': '查看平台审计与告警',
'platform:billing:notification': '管理账务催缴通知',
'platform:saas-catalog:manage': '管理 SaaS 商品与版本',
'platform:saas-billing:manage': '管理 SaaS 订单与账务',
'platform:crm:read': '查看租户 CRM 配置',
'platform:crm:write': '管理租户 CRM 配置',
'platform:sms:read': '查看租户短信服务',
'platform:sms:write': '管理租户短信服务',
'platform:payment:read': '查看平台支付设置',
'platform:payment:write': '管理平台支付设置',
};
const platformRoleLabels = {
platform_super_admin: { name: '超级管理员', description: '拥有全部平台管理权限' },
};
function mapValue(map, value, fallback = value) {
return Object.prototype.hasOwnProperty.call(map, value) ? map[value] : fallback;
}
@@ -277,18 +297,39 @@
}
const bodyBuilders = {
PlatformSaasController_upsertFeature(input) {
return { id: input.id || undefined, code: input.code, name: input.name, category: input.category, description: input.description || undefined, referencePriceCents: yuanToCents(input.referencePrice), currency: 'CNY', status: input.status || 'active', sortOrder: Number(input.sortOrder || 100) };
},
PlatformSaasController_upsertOffering(input) {
return { id: input.id || undefined, code: input.code, name: input.name, type: input.type || 'base_plan', status: input.status || 'active', description: input.description || undefined, sortOrder: Number(input.sortOrder || 100) };
},
PlatformSaasController_upsertFeatureLimit(input) {
return { id: input.id || undefined, metricCode: input.metricCode, featureCode: input.featureCode, name: input.name, unit: input.unit || 'count', kind: input.kind || 'current', warningPercent: Number(input.warningPercent || 80), isHardLimit: input.isHardLimit !== false };
},
PlatformSaasController_upsertVersion(input) {
return { id: input.id || undefined, offeringId: input.offeringId, billingCycle: input.billingCycle || 'yearly', originalAmountCents: yuanToCents(input.originalAmount), amountCents: yuanToCents(input.amount), currency: 'CNY', effectiveAt: input.effectiveAt ? new Date(input.effectiveAt).toISOString() : undefined, featureCodes: splitList(input.featureCodes), limits: {}, metadata: {} };
},
PlatformBackofficeController_upsertRole(input) {
return { id: input.id || undefined, code: input.code, name: input.name, status: input.status || 'active', description: input.description || undefined };
},
PlatformBackofficeController_replaceRoleBindings(input) {
return { permissionCodes: input.permissionCodes || [], menuCodes: input.menuCodes || [] };
},
PlatformSaasController_upsertTenantFeatureOverride(input) {
return { tenantId: input.tenantId, featureCode: input.featureCode, mode: input.mode, expiresAt: input.expiresAt ? new Date(input.expiresAt).toISOString() : undefined, reason: input.reason };
},
PlatformAdminTenantsController_create(input) {
return {
slug: input.slug,
name: input.name,
legalName: input.legalName || undefined,
brandName: input.brandName || undefined,
shortName: input.shortName || undefined,
primaryHost: input.primaryHost || undefined,
planCode: input.planCode || undefined,
status: mapValue(dictionaries.tenantStatusToApi, input.status, input.status),
billingStatus: mapValue(dictionaries.billingStatusToApi, input.billingStatus, input.billingStatus),
amountCents: yuanToCents(input.amount),
metadata: input.metadata || {},
ownerEmail: input.ownerEmail || undefined,
ownerPhone: input.ownerPhone || undefined,
ownerName: input.ownerName,
temporaryPassword: input.temporaryPassword,
};
},
PlatformAdminTenantsController_status(input) {
@@ -399,52 +440,28 @@
metadata: { source: 'platform_console_manual' },
};
},
PlatformAdminQuestionBanksController_upsertGrant(input) {
return {
id: input.id || undefined,
sourceQuestionBankId: input.bankId || input.sourceQuestionBankId,
grantScope: mapValue(dictionaries.grantScopeToApi, input.scope, input.grantScope),
allowedPlanCodes: input.allowedPlanCodes || (input.planCode ? [input.planCode] : []),
allowedTenantIds: input.allowedTenantIds || (input.tenantId ? [input.tenantId] : []),
allowedRegionIds: input.allowedRegionIds || splitList(input.regionIds),
allowedSubjectIds: input.allowedSubjectIds || splitList(input.subjectIds),
status: input.status === '已禁用' || input.status === 'disabled'
? 'disabled'
: input.status === '已过期' || input.status === 'expired'
? 'expired'
: 'active',
startsAt: isoStartOfDay(input.startsAt),
expiresAt: isoStartOfDay(input.expiresAt),
metadata: { localEdit: Boolean(input.localEdit) },
};
},
PlatformAdminOverviewController_upsertStaff(input) {
const permissionList = input.permissions || [];
return {
id: input.id || undefined,
authUserId: input.authUserId,
username: input.username,
userId: input.userId || undefined,
email: input.email || undefined,
phone: input.phone || undefined,
name: input.name,
status: input.status === '已停用' ? 'disabled' : 'active',
platformPermissions: Object.fromEntries(permissionList.map(permission => [permission, true])),
metadata: { displayRole: input.role || '平台管理员' },
roleIds: input.roleIds || [],
};
},
PlatformAdminOverviewController_updateStaffStatus(input) {
return {
staffId: input.staffId,
userId: input.userId || input.staffId,
status: input.status === '正常' || input.status === 'active' ? 'active' : 'disabled',
reason: input.reason || undefined,
revokeSessions: input.revokeSessions !== false,
};
},
PlatformAdminAuditController_updateAlert(input) {
return {
alertId: input.alertId,
status: input.status,
note: input.note || undefined,
resolutionNote: input.resolutionNote || input.note || undefined,
};
},
PlatformAdminCrmController_upsertConfig(input) {
@@ -536,6 +553,74 @@
metadata: { source: 'platform_console' },
};
},
PlatformQuestionBanksController_upsertBank(input) {
return {
id: input.id || undefined,
name: input.name,
metadata: input.metadata || {},
};
},
PlatformQuestionBanksController_upsertNode(input) {
return {
id: input.id || undefined,
questionBankId: input.questionBankId,
parentId: input.parentId || undefined,
nodeKey: input.nodeKey || undefined,
name: input.name,
nodeType: input.nodeType,
sortOrder: Number(input.sortOrder || 0),
isSelectable: input.isSelectable !== false,
metadata: input.metadata || {},
};
},
PlatformQuestionBanksController_batchCreateNodes(input) {
return {
questionBankId: input.questionBankId,
parentId: input.parentId || undefined,
nodeType: input.nodeType,
names: Array.isArray(input.names) ? input.names : splitList(input.names),
};
},
PlatformQuestionBanksController_upsertQuestion(input) {
return {
id: input.id || undefined,
questionBankId: input.questionBankId,
contentNodeId: input.contentNodeId,
legacyId: input.legacyId || undefined,
type: input.type,
typeLabel: input.typeLabel || undefined,
difficulty: input.difficulty == null ? undefined : Number(input.difficulty),
tags: input.tags || [],
content: input.content,
options: input.options || [],
correctOptionIndex: input.correctOptionIndex ?? undefined,
correctOptionIndices: input.correctOptionIndices || [],
answerText: input.answerText || undefined,
explanation: input.explanation || undefined,
subQuestions: input.subQuestions || [],
codeLang: input.codeLang || undefined,
codeTemplate: input.codeTemplate || undefined,
mediaUrl: input.mediaUrl || undefined,
status: input.status || 'published',
examMarkers: input.examMarkers || {},
sourceHash: input.sourceHash || undefined,
};
},
PlatformQuestionBanksController_archiveQuestions(input) {
return { questionIds: input.questionIds || [] };
},
PlatformQuestionBanksController_previewImport(input) {
return {
questionBankId: input.questionBankId,
contentNodeId: input.contentNodeId || undefined,
format: input.format,
sourceName: input.sourceName || undefined,
payload: input.payload,
};
},
PlatformQuestionBanksController_executeImport(input) {
return bodyBuilders.PlatformQuestionBanksController_previewImport(input);
},
};
function notificationBody(input) {
@@ -699,6 +784,7 @@
return {
id: item.invoiceNo || item.id,
backendId: item.id,
orderId: item.orderId || '',
tenantId: item.tenantId,
type: mapValue(dictionaries.invoiceTypeFromApi, item.invoiceType, item.invoiceType || '-'),
period: item.billingPeriodStart ? monthOnly(item.billingPeriodStart) : monthOnly(item.issuedAt || item.dueDate),
@@ -710,6 +796,18 @@
note: item.note || '',
};
},
billingPayment(item) {
return {
id: item.id,
orderId: item.orderId,
tenantId: item.tenantId,
paymentNo: item.paymentNo || '',
status: lowerEnum(item.status),
amount: centsToYuan(item.amountCents),
providerTradeNo: item.providerTradeNo || '',
paidAt: item.paidAt || '',
};
},
reminder(item) {
return {
id: item.id,
@@ -728,52 +826,26 @@
...current,
id: item.id,
name: item.name,
catalog: item.regionName || item.tenantName || '公共题库',
source: item.sourceScope === 'platform' ? '平台公共题库' : '租户自建',
contentEntryId: item.contentEntryId || '',
catalog: '平台公共题库',
source: '平台自建',
nodes: Number(item.nodeCount || 0),
questions: Number(item.questionCount || 0),
version: item.metadata?.version || item.metadata?.contentVersion || '-',
status: item.status === 'active' ? '稳定版' : item.status || '-',
};
},
grant(item) {
return {
id: item.id,
bankId: item.sourceQuestionBankId,
scope: mapValue(dictionaries.grantScopeFromApi, item.grantScope, item.grantScope || '-'),
allowedPlanCodes: item.allowedPlanCodes || [],
allowedTenantIds: item.allowedTenantIds || [],
allowedRegionIds: item.allowedRegionIds || [],
allowedSubjectIds: item.allowedSubjectIds || [],
status: mapValue(dictionaries.grantStatusFromApi, item.status, item.status || '-'),
startsAt: dateOnly(item.startsAt),
expiresAt: dateOnly(item.expiresAt),
localEdit: Boolean(item.metadata?.localEdit),
};
},
sync(item) {
const progress = item.syncStatus === 'synced'
? 100
: Number(item.metadata?.lastSync?.progress || item.lastSyncCounts?.progress || 0);
return {
tenantId: item.tenantId,
bankId: item.sourceQuestionBankId,
current: item.metadata?.lastSync?.sourceVersion || '-',
target: item.metadata?.lastSync?.targetVersion || '-',
progress,
conflicts: Number(item.conflictCount || 0),
status: ({ pending: '同步中', synced: '已完成', failed: '同步失败' })[item.syncStatus] || item.syncStatus || item.status || '-',
status: lowerEnum(item.status) === 'active' ? '正常' : lowerEnum(item.status) === 'archived' ? '已归档' : item.status || '-',
};
},
staff(item) {
const permissions = item.roleCodes?.includes('platform_super_admin') ? ['*'] : [];
const roleCodes = item.roleCodes || [];
const permissions = roleCodes.includes('platform_super_admin') ? ['*'] : [];
return {
id: item.userId || item.id,
authUserId: item.userId || item.authUserId || '',
username: item.email || '',
userId: item.userId || item.id,
name: item.name || item.email || '平台员工',
email: item.email || '',
phone: item.phoneMasked || item.phone || '-',
role: permissions.includes('*') ? '超级管理员' : item.rawProfile?.displayRole || '平台管理员',
roleCodes,
role: permissions.includes('*') ? '超级管理员' : roleCodes.join('、') || '未分配角色',
permissions,
status: item.status === 'active' ? '正常' : '已停用',
lastActive: dateTime(item.updatedAt),
@@ -1027,16 +1099,19 @@
}
const bootstrapDescriptors = [
['backofficeBootstrap', 'PlatformBackofficeController_bootstrap', {}],
['overview', 'PlatformAdminOverviewController_overview', {}],
['domains', 'PlatformAdminTenantsController_domains', { query: { limit: 200 } }],
['plans', 'PlatformAdminOverviewController_plans', {}],
['tenants', 'PlatformAdminTenantsController_list', { query: { limit: 200 } }],
['subscriptions', 'PlatformAdminBillingController_subscriptions', { query: { limit: 200 } }],
['saasOrders', 'PlatformAdminBillingController_orders', { query: { limit: 200 } }],
['saasRefunds', 'PlatformAdminBillingController_refunds', { query: { limit: 200 } }],
['invoices', 'PlatformAdminBillingController_invoices', { query: { limit: 200 } }],
['billingPayments', 'PlatformAdminBillingController_payments', { query: { limit: 200 } }],
['usage', 'PlatformAdminBillingController_usage', { query: { limit: 200 } }],
['reminders', 'PlatformAdminBillingController_reminders', { query: { limit: 100 } }],
['banks', 'PlatformAdminQuestionBanksController_banks', { query: { limit: 200 } }],
['grants', 'PlatformAdminQuestionBanksController_grants', { query: { limit: 200 } }],
['bankSync', 'PlatformAdminQuestionBanksController_syncStatus', { query: { limit: 200 } }],
['banks', 'PlatformQuestionBanksController_getBanks', { query: { status: 'all' } }],
['staff', 'PlatformAdminOverviewController_staff', { query: { limit: 200 } }],
['dunningChannels', 'PlatformAdminDunningChannelsController_channels', { query: { limit: 100 } }],
['dunningEvents', 'PlatformAdminDunningEventsController_events', { query: { limit: 100 } }],
@@ -1044,10 +1119,13 @@
['alerts', 'PlatformAdminAuditController_alerts', { query: { limit: 100 } }],
['crmConfigs', 'PlatformAdminCrmController_configs', { query: { limit: 200 } }],
['crmLeads', 'PlatformAdminCrmController_leads', { query: { limit: 200 } }],
['crmLogs', 'PlatformAdminCrmController_logs', { query: { limit: 200 } }],
['smsChannels', 'PlatformAdminSmsController_channels', { query: { limit: 200 } }],
['smsTemplates', 'PlatformAdminSmsController_templates', { query: { limit: 200 } }],
['smsLogs', 'PlatformAdminSmsController_logs', { query: { limit: 200 } }],
['platformPaymentApps', 'PlatformAdminPaymentSettingsController_apps', { query: { limit: 200 } }],
['paymentChannels', 'PlatformAdminPaymentSettingsController_channels', { query: { limit: 200 } }],
['rebateSummary', 'PlatformAdminPaymentSettingsController_rebateSummary', {}],
['tenantPaymentApps', 'PlatformAdminTenantPaymentSettingsController_apps', { query: { limit: 200 } }],
['platformPaymentEvents', 'PlatformAdminPaymentSettingsController_events', { query: { limit: 100 } }],
['tenantPaymentEvents', 'PlatformAdminTenantPaymentSettingsController_events', { query: { limit: 100 } }],
@@ -1056,7 +1134,6 @@
async function bootstrap(options = {}) {
const descriptors = bootstrapDescriptors.map(([key, operationId, requestOptions]) => {
const nextOptions = { ...requestOptions, query: { ...(requestOptions.query || {}) } };
if (key === 'banks') nextOptions.query.includeTenantBanks = Boolean(options.includeTenantBanks);
return [key, operationId, nextOptions];
});
const results = await Promise.allSettled(
@@ -1074,7 +1151,26 @@
function adaptBootstrap(payloads, currentData = {}) {
const patch = {};
const backoffice = payloads.backofficeBootstrap || {};
const permissionItems = Array.isArray(backoffice.permissions) ? backoffice.permissions : [];
const roleItems = Array.isArray(backoffice.roles) ? backoffice.roles : [];
if (permissionItems.length) {
patch.platformPermissions = Object.fromEntries(permissionItems.map(item => [
item.code,
platformPermissionLabels[item.code] || item.name || item.code,
]));
}
if (roleItems.length) {
patch.platformRoles = roleItems
.filter(item => String(item.status || '').toLowerCase() === 'active')
.map(item => {
const localized = platformRoleLabels[item.code] || {};
return { id: item.id, code: item.code, name: localized.name || item.name || item.code, description: localized.description || item.description || '', permissionCodes: item.permissionCodes || [], menuCodes: item.menuCodes || [] };
});
}
if (payloads.overview) patch.apiOverview = itemOf(payloads.overview);
if (payloads.domains) patch.domains = itemsOf(payloads.domains);
if (payloads.plans) patch.saasCatalog = payloads.plans;
if (payloads.permissions) patch.apiPermissions = itemOf(payloads.permissions);
const planPayload = itemsOf(payloads.plans, 'versions').length ? catalogPlans(payloads.plans) : itemsOf(payloads.plans);
if (planPayload.length) {
@@ -1087,11 +1183,25 @@
patch.tenants = tenants.map(item => adapters.tenant(item, currentById(currentData.tenants, item.id), planMap));
}
const staff = itemsOf(payloads.staff);
if (staff.length) patch.staff = staff.map(adapters.staff);
if (staff.length) {
const roles = patch.platformRoles || currentData.platformRoles || [];
patch.staff = staff.map(item => {
const adapted = adapters.staff(item);
const assignedRoles = roles.filter(role => adapted.roleCodes.includes(role.code));
adapted.roleIds = assignedRoles.map(role => role.id);
adapted.role = assignedRoles.map(role => role.name).join('、') || adapted.role;
adapted.permissions = adapted.permissions.includes('*') ? adapted.permissions : [...new Set(assignedRoles.flatMap(role => role.permissionCodes))];
return adapted;
});
}
const invoices = itemsOf(payloads.invoices);
if (invoices.length) patch.invoices = invoices.map(adapters.invoice);
const billingPayments = itemsOf(payloads.billingPayments);
if (billingPayments.length) patch.billingPayments = billingPayments.map(adapters.billingPayment);
const subscriptions = itemsOf(payloads.subscriptions);
if (subscriptions.length) patch.subscriptions = subscriptions.map(adapters.subscription);
if (payloads.saasOrders) patch.saasOrders = itemsOf(payloads.saasOrders);
if (payloads.saasRefunds) patch.saasRefunds = itemsOf(payloads.saasRefunds);
const usage = itemsOf(payloads.usage);
if (usage.length) patch.usage = aggregateUsage(usage);
if (Array.isArray(payloads.usageCandidates?.items) && patch.usage) {
@@ -1113,10 +1223,6 @@
if (banks.length) {
patch.banks = banks.map(item => adapters.bank(item, currentById(currentData.banks, item.id)));
}
const grants = itemsOf(payloads.grants);
if (grants.length) patch.grants = grants.map(adapters.grant);
const bankSync = itemsOf(payloads.bankSync);
if (bankSync.length) patch.bankSync = bankSync.map(adapters.sync);
const auditLogs = itemsOf(payloads.auditLogs);
if (auditLogs.length) patch.auditLogs = auditLogs.map(adapters.audit);
const alertRules = itemsOf(payloads.alertRules);
@@ -1127,6 +1233,7 @@
if (crmConfigs.length) patch.crmConfigs = crmConfigs.map(adapters.crmConfig);
const crmLeads = itemsOf(payloads.crmLeads);
if (crmLeads.length) patch.crmLeads = crmLeads.map(adapters.crmLead);
if (payloads.crmLogs) patch.crmLogs = itemsOf(payloads.crmLogs);
const smsChannels = itemsOf(payloads.smsChannels);
if (smsChannels.length) patch.smsChannels = smsChannels.map(adapters.smsChannel);
const smsTemplates = itemsOf(payloads.smsTemplates);
@@ -1136,6 +1243,8 @@
const platformPaymentApps = itemsOf(payloads.platformPaymentApps).map(adapters.platformPaymentApp);
const tenantPaymentApps = itemsOf(payloads.tenantPaymentApps).map(adapters.tenantPaymentApp);
if (platformPaymentApps.length || tenantPaymentApps.length) patch.paymentApps = [...platformPaymentApps, ...tenantPaymentApps];
if (payloads.paymentChannels) patch.paymentChannels = itemsOf(payloads.paymentChannels);
if (payloads.rebateSummary) patch.rebateSummary = itemOf(payloads.rebateSummary);
const paymentEvents = [
...itemsOf(payloads.platformPaymentEvents).map(adapters.platformPaymentEvent),
...itemsOf(payloads.tenantPaymentEvents).map(adapters.platformPaymentEvent),