Files
tiku-backend.net/Tiku.Api/wwwroot/platform-admin/platform-api.js

887 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
const defaults = {
mode: 'mock',
apiBaseUrl: '',
fallbackToMock: true,
timeoutMs: 10000,
getAccessToken: null,
};
const runtime = { ...defaults, ...(window.GONGXUE_PLATFORM_RUNTIME_CONFIG || {}) };
const operationCatalog = Object.fromEntries(
(window.GONGXUE_PLATFORM_SPEC?.operations || []).map(operation => [operation.operationId, operation]),
);
const trace = [];
class PlatformApiError extends Error {
constructor(message, options = {}) {
super(message);
this.name = 'PlatformApiError';
this.status = Number(options.status || 0);
this.code = options.code || 'PLATFORM_API_ERROR';
this.requestId = options.requestId || '';
this.operationId = options.operationId || '';
this.details = options.details;
}
}
function normalizeBaseUrl(value) {
return String(value || '').trim().replace(/\/+$/, '');
}
function isEnabled() {
return runtime.mode === 'api' || (runtime.mode === 'auto' && Boolean(normalizeBaseUrl(runtime.apiBaseUrl)));
}
function publicConfig() {
return {
mode: runtime.mode,
apiBaseUrl: normalizeBaseUrl(runtime.apiBaseUrl),
fallbackToMock: runtime.fallbackToMock !== false,
timeoutMs: Number(runtime.timeoutMs) || defaults.timeoutMs,
configured: isEnabled(),
};
}
async function accessToken() {
const provider = typeof runtime.getAccessToken === 'function'
? runtime.getAccessToken
: typeof window.GONGXUE_PLATFORM_ACCESS_TOKEN_PROVIDER === 'function'
? window.GONGXUE_PLATFORM_ACCESS_TOKEN_PROVIDER
: null;
const token = provider ? await provider() : '';
return String(token || '').trim();
}
function buildUrl(path, query = {}) {
const baseUrl = normalizeBaseUrl(runtime.apiBaseUrl);
const url = new URL(`${baseUrl}${path.startsWith('/') ? path : `/${path}`}`, 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));
});
return url.toString();
}
function responseRequestId(payload, response) {
return String(
payload?.meta?.requestId
|| payload?.requestId
|| response?.headers?.get?.('x-request-id')
|| '',
);
}
async function request(operationId, options = {}) {
const operation = operationCatalog[operationId];
if (!operation) {
throw new PlatformApiError(`未知平台接口:${operationId}`, {
code: 'PLATFORM_OPERATION_UNKNOWN',
operationId,
});
}
if (!isEnabled()) {
throw new PlatformApiError('平台 API 未启用', {
code: 'PLATFORM_API_DISABLED',
operationId,
});
}
const token = await accessToken();
if (!token) {
throw new PlatformApiError('缺少平台管理员 access token', {
status: 401,
code: 'PLATFORM_ACCESS_TOKEN_MISSING',
operationId,
});
}
const extraHeaders = Object.fromEntries(Object.entries(options.headers || {}).map(([key, value]) => [key, String(value)]));
const forbiddenHeader = Object.keys(extraHeaders).find((key) => {
const normalized = key.toLowerCase();
return ['authorization', 'x-user-id', 'x-platform-admin-key'].includes(normalized)
|| normalized.startsWith('x-tenant-');
});
if (forbiddenHeader) {
throw new PlatformApiError(`禁止页面覆盖身份头:${forbiddenHeader}`, {
code: 'PLATFORM_IDENTITY_HEADER_FORBIDDEN',
operationId,
});
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), Number(runtime.timeoutMs) || defaults.timeoutMs);
const startedAt = Date.now();
if (options.signal) {
if (options.signal.aborted) controller.abort();
else options.signal.addEventListener('abort', () => controller.abort(), { once: true });
}
try {
const response = await fetch(buildUrl(operation.path, options.query), {
method: operation.method,
headers: {
...extraHeaders,
Accept: 'application/json',
Authorization: `Bearer ${token}`,
...(operation.method === 'GET' ? {} : { 'Content-Type': 'application/json' }),
},
body: operation.method === 'GET' || options.body === undefined
? undefined
: JSON.stringify(options.body),
credentials: 'omit',
signal: controller.signal,
});
const text = await response.text();
let payload = {};
if (text) {
try {
payload = JSON.parse(text);
} catch {
payload = { error: text.slice(0, 500) };
}
}
const requestId = responseRequestId(payload, response);
trace.push({
operationId,
method: operation.method,
path: operation.path,
status: response.status,
requestId,
durationMs: Date.now() - startedAt,
at: new Date().toISOString(),
});
if (trace.length > 120) trace.splice(0, trace.length - 120);
if (!response.ok) {
throw new PlatformApiError(
String(payload.message || payload.error || `请求失败HTTP ${response.status}`),
{
status: response.status,
code: String(payload.code || 'PLATFORM_API_REQUEST_FAILED'),
requestId,
operationId,
details: payload,
},
);
}
return payload;
} catch (error) {
if (error instanceof PlatformApiError) throw error;
const aborted = error?.name === 'AbortError';
throw new PlatformApiError(aborted ? '平台接口请求超时' : (error?.message || '平台接口请求失败'), {
code: aborted ? 'PLATFORM_API_TIMEOUT' : 'PLATFORM_API_NETWORK_ERROR',
operationId,
details: error,
});
} finally {
clearTimeout(timeout);
}
}
const dictionaries = {
tenantStatusToApi: { 待配置: 'draft', 试用: 'active', 正常: 'active', 暂停: 'suspended', 已归档: 'archived' },
tenantStatusFromApi: { draft: '待配置', active: '正常', suspended: '暂停', archived: '已归档' },
billingStatusToApi: { 试用: 'trial', 正常: 'active', 待付款: 'active', 逾期: 'past_due', 暂停: 'suspended', 已取消: 'cancelled' },
billingStatusFromApi: { trial: '试用', active: '正常', past_due: '逾期', suspended: '暂停', cancelled: '已取消' },
subscriptionStatusToApi: { 试用中: 'trial', 活跃: 'active', 暂停: 'suspended', 已取消: 'cancelled', 已到期: 'expired' },
subscriptionStatusFromApi: { trial: '试用中', active: '活跃', suspended: '暂停', cancelled: '已取消', expired: '已到期' },
invoiceStatusFromApi: { draft: '草稿', issued: '待付款', paid: '已支付', void: '已作废', overdue: '已逾期' },
invoiceTypeToApi: { 订阅服务费: 'subscription', 用量超额费: 'usage', 人工调整: 'adjustment', 首期服务费: 'manual' },
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: '已丢弃' },
};
function mapValue(map, value, fallback = value) {
return Object.prototype.hasOwnProperty.call(map, value) ? map[value] : fallback;
}
function centsToYuan(value) {
const number = Number(value || 0);
return Number.isFinite(number) ? number / 100 : 0;
}
function yuanToCents(value) {
const number = Number(value || 0);
return Number.isFinite(number) ? Math.round(number * 100) : 0;
}
function dateOnly(value) {
return value ? String(value).slice(0, 10) : '';
}
function monthOnly(value) {
return value ? String(value).slice(0, 7) : '';
}
function dateTime(value) {
if (!value) return '-';
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return String(value);
return new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(parsed).replaceAll('/', '-');
}
function isoStartOfDay(value) {
if (!value) return undefined;
return `${String(value).slice(0, 10)}T00:00:00+08:00`;
}
function splitList(value) {
if (Array.isArray(value)) return value.map(String).map(item => item.trim()).filter(Boolean);
return String(value || '').split(/[、,]/).map(item => item.trim()).filter(Boolean);
}
function periodBounds(period) {
const match = String(period || '').match(/^(\d{4})-(\d{2})$/);
if (!match) return { periodStart: '', periodEnd: '' };
const year = Number(match[1]);
const month = Number(match[2]);
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
periodStart: `${match[1]}-${match[2]}-01`,
periodEnd: `${match[1]}-${match[2]}-${String(lastDay).padStart(2, '0')}`,
};
}
function invoicePeriodBounds(label) {
const match = String(label || '').match(/^(\d{4})年(\d{1,2})月$/);
return match ? periodBounds(`${match[1]}-${String(match[2]).padStart(2, '0')}`) : {};
}
const bodyBuilders = {
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),
};
},
PlatformAdminTenantsController_status(input) {
return {
tenantId: input.tenantId,
status: mapValue(dictionaries.tenantStatusToApi, input.status, input.status),
billingStatus: mapValue(dictionaries.billingStatusToApi, input.billingStatus, input.billingStatus),
reason: input.reason,
};
},
PlatformAdminTenantsController_billingProfile(input) {
return {
tenantId: input.tenantId,
billingName: input.billingName || undefined,
taxId: input.taxId || undefined,
contactName: input.contactName || undefined,
contactPhone: input.phone || input.contactPhone || undefined,
contactEmail: input.email || input.contactEmail || undefined,
billingAddress: input.billingAddress || undefined,
invoiceTitle: input.invoiceTitle || undefined,
invoiceType: ({ 增值税专用发票: 'special_vat', 增值税普通发票: 'normal_vat', 不开票: 'none' })[input.type || input.invoiceType] || input.invoiceType || 'none',
bankName: input.bankName || undefined,
bankAccountMasked: input.bankAccountMasked || undefined,
};
},
PlatformAdminBillingController_createSubscription(input) {
return {
tenantId: input.tenantId,
planCode: input.planCode,
status: mapValue(dictionaries.subscriptionStatusToApi, input.status, input.status || 'active'),
billingCycle: ({ 年度: 'yearly', 月度: 'monthly', 试用: 'trial' })[input.cycle] || input.billingCycle,
amountCents: yuanToCents(input.amount),
startsAt: isoStartOfDay(input.start || input.startsAt),
expiresAt: isoStartOfDay(input.expires || input.expiresAt),
metadata: { autoRenew: Boolean(input.autoRenew), source: 'static-platform-console' },
};
},
PlatformAdminBillingController_createInvoice(input) {
const bounds = invoicePeriodBounds(input.period);
return {
tenantId: input.tenantId,
invoiceType: mapValue(dictionaries.invoiceTypeToApi, input.type, input.invoiceType || 'manual'),
status: input.status || 'draft',
dueDate: input.due || input.dueDate,
billingPeriodStart: input.billingPeriodStart || bounds.periodStart,
billingPeriodEnd: input.billingPeriodEnd || bounds.periodEnd,
items: input.items || [{
itemType: mapValue(dictionaries.invoiceTypeToApi, input.type, 'manual'),
description: input.note || input.type || '平台手工账单',
quantity: 1,
unitAmountCents: yuanToCents(input.amount),
metadata: { periodLabel: input.period || '' },
}],
note: input.note || undefined,
};
},
PlatformAdminBillingController_fromSubscription(input) {
return {
tenantId: input.tenantId,
subscriptionId: input.subscriptionId || undefined,
status: input.status || 'issued',
dueDate: input.dueDate || undefined,
note: input.note || undefined,
};
},
PlatformAdminBillingController_fromSubscriptionsBatch(input) {
return {
tenantIds: input.tenantIds || undefined,
subscriptionIds: input.subscriptionIds || undefined,
daysAhead: input.daysAhead ?? 45,
status: input.status || 'issued',
dueDate: input.dueDate || undefined,
note: input.note || undefined,
dryRun: Boolean(input.dryRun),
};
},
PlatformAdminBillingController_fromUsage(input) {
const bounds = input.periodStart && input.periodEnd ? input : { ...input, ...periodBounds(input.period) };
return {
tenantIds: bounds.tenantIds || undefined,
periodStart: bounds.periodStart,
periodEnd: bounds.periodEnd,
dueDate: bounds.dueDate || undefined,
note: bounds.note || undefined,
status: bounds.status || 'issued',
dryRun: Boolean(bounds.dryRun),
};
},
PlatformAdminBillingController_processOverdue(input) {
return { dryRun: Boolean(input.dryRun), channel: input.channel || 'internal', limit: Number(input.limit || 100) };
},
PlatformAdminBillingController_confirmPayment(input) {
return {
tenantId: input.tenantId,
invoiceId: input.invoiceId,
amountCents: yuanToCents(input.amount),
paymentNo: input.paymentNo || undefined,
provider: input.provider || 'manual',
method: input.method || 'manual',
paidAt: input.paidAt ? new Date(input.paidAt).toISOString() : undefined,
providerTradeNo: input.tradeNo || input.providerTradeNo || undefined,
};
},
PlatformAdminBillingController_recordUsage(input) {
const bounds = input.periodStart && input.periodEnd ? input : { ...input, ...periodBounds(input.period) };
return {
tenantId: bounds.tenantId,
metricKey: bounds.metricKey,
metricValue: Number(bounds.metricValue || 0),
periodStart: bounds.periodStart,
periodEnd: bounds.periodEnd,
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,
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 || '平台管理员' },
};
},
PlatformAdminOverviewController_updateStaffStatus(input) {
return {
staffId: 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,
};
},
};
function notificationBody(input) {
const maxAttempts = Number(input.maxAttempts || input.retry || 3);
return {
id: input.id || undefined,
channelCode: input.code || input.channelCode,
name: input.name,
enabled: input.enabled !== false,
provider: mapValue(dictionaries.providerToApi, input.provider, input.provider || 'generic'),
webhookUrl: input.endpoint || input.webhookUrl,
secret: input.secret || undefined,
secretRef: input.secretRef || undefined,
minSeverity: mapValue(dictionaries.severityToApi, input.minSeverity, input.minSeverity || 'medium'),
statusFilter: splitList(input.statusFilter || 'open,acknowledged'),
timeoutSec: Number(input.timeout || input.timeoutSec || 10),
maxAttempts,
metadata: { ...(input.metadata || {}), maxAttempts },
};
}
bodyBuilders.PlatformAdminAuditController_upsertChannel = notificationBody;
bodyBuilders.PlatformAdminDunningChannelsController_upsertChannel = input => ({
...notificationBody(input),
minReminderLevel: Number(input.minReminderLevel || 1),
reminderTypes: input.reminderTypes || ['overdue', 'final_notice'],
reminderChannels: input.reminderChannels || ['internal'],
});
function buildBody(operationId, input = {}) {
const builder = bodyBuilders[operationId];
return builder ? builder(input) : input;
}
function jsonSummary(value) {
if (!value) return '';
if (typeof value === 'string') return value;
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function safeEndpoint(item) {
if (!item?.webhook) return '';
const protocol = item.webhook.protocol ? `${item.webhook.protocol}://` : '';
return `${protocol}${item.webhook.host || ''}${item.webhook.pathname || ''}`;
}
function currentById(items, id) {
return (items || []).find(item => item.id === id) || {};
}
const adapters = {
plan(item, current = {}) {
const quotas = item.includedQuotas && typeof item.includedQuotas === 'object'
? Object.entries(item.includedQuotas).map(([key, value]) => `${key}: ${value}`)
: current.quotas || [];
return {
...current,
code: item.code,
name: item.name,
description: item.description || '',
cycle: ({ yearly: '年度', monthly: '月度' })[item.billingCycle] || item.billingCycle || '-',
amount: centsToYuan(item.baseAmountCents),
quotas,
tenantCount: Number(current.tenantCount || 0),
status: item.status,
};
},
tenant(item, current = {}, planMap = {}) {
const plan = planMap[item.planCode];
return {
students: 0,
questions: 0,
storageGB: 0,
trafficTB: 0,
balance: 0,
host: '-',
owner: '待配置',
ownerPhone: '-',
...current,
id: item.id,
slug: item.slug,
name: item.name,
legal: item.legalName || current.legal || item.name,
brand: item.brandName || current.brand || item.name,
planCode: item.planCode || '',
plan: plan?.name || current.plan || item.planCode || '未订阅',
status: mapValue(dictionaries.tenantStatusFromApi, item.status, item.status || '-'),
billing: mapValue(dictionaries.billingStatusFromApi, item.billingStatus, item.billingStatus || '-'),
mode: item.mode === 'saas' ? '正式' : item.mode || current.mode || '-',
expires: dateOnly(item.subscriptionExpiresAt),
balance: centsToYuan(item.openBalanceCents),
createdAt: dateTime(item.createdAt),
};
},
subscription(item) {
return {
id: item.subscriptionId || item.id,
tenantId: item.tenantId,
planCode: item.planCode || '',
status: mapValue(dictionaries.subscriptionStatusFromApi, item.status, item.status || '-'),
cycle: ({ yearly: '年度', monthly: '月度', trial: '试用' })[item.billingCycle] || item.billingCycle || '-',
amount: centsToYuan(item.amountCents),
start: dateOnly(item.startsAt),
expires: dateOnly(item.expiresAt),
autoRenew: Boolean(item.metadata?.autoRenew),
};
},
invoice(item) {
return {
id: item.invoiceNo || item.id,
backendId: item.id,
tenantId: item.tenantId,
type: mapValue(dictionaries.invoiceTypeFromApi, item.invoiceType, item.invoiceType || '-'),
period: item.billingPeriodStart ? monthOnly(item.billingPeriodStart) : monthOnly(item.issuedAt || item.dueDate),
total: centsToYuan(item.totalCents),
paid: centsToYuan(item.paidCents),
balance: centsToYuan(item.balanceCents),
due: dateOnly(item.dueDate),
status: mapValue(dictionaries.invoiceStatusFromApi, item.status, item.status || '-'),
note: item.note || '',
};
},
reminder(item) {
return {
id: item.id,
invoiceId: item.invoiceNo || item.invoiceId,
backendInvoiceId: item.invoiceId,
tenantId: item.tenantId,
level: Number(item.reminderLevel || 1),
channel: item.channel || '-',
status: mapValue(dictionaries.eventStatusFromApi, item.status, item.status || '-'),
date: dateTime(item.reminderDate || item.createdAt),
message: item.message || '',
};
},
bank(item, current = {}) {
return {
...current,
id: item.id,
name: item.name,
catalog: item.regionName || item.tenantName || '公共题库',
source: item.sourceScope === 'platform' ? '平台公共题库' : '租户自建',
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 || '-',
};
},
staff(item) {
const permissions = item.roleCodes?.includes('platform_super_admin') ? ['*'] : [];
return {
id: item.userId || item.id,
authUserId: item.userId || item.authUserId || '',
username: item.email || '',
name: item.name || item.email || '平台员工',
email: item.email || '',
phone: item.phoneMasked || item.phone || '-',
role: permissions.includes('*') ? '超级管理员' : item.rawProfile?.displayRole || '平台管理员',
permissions,
status: item.status === 'active' ? '正常' : '已停用',
lastActive: dateTime(item.updatedAt),
};
},
audit(item) {
const details = jsonSummary(item.details);
return {
id: item.id,
time: `${dateTime(item.createdAt)} CST`,
actor: item.actorName || item.actorUsername || item.actorUserId || 'system',
role: item.actorUserId ? '平台管理员' : '系统任务',
action: item.action || '-',
targetType: item.targetType || '-',
targetId: item.targetId || '',
target: item.tenantName || item.targetId || item.targetType || '-',
severity: mapValue(dictionaries.severityFromApi, item.details?.severity, item.details?.severity || '中'),
result: item.details?.result === 'failed' ? '失败' : '成功',
ip: item.ipAddress || '-',
details,
};
},
alert(item) {
const evidence = Array.isArray(item.details?.evidence)
? item.details.evidence.map(String)
: Object.entries(item.details || {}).slice(0, 8).map(([key, value]) => `${key}: ${jsonSummary(value)}`);
return {
id: item.id,
title: item.title || item.ruleName || '平台审计告警',
summary: item.summary || item.action || '',
severity: mapValue(dictionaries.severityFromApi, item.severity, item.severity || '-'),
status: item.status || 'open',
owner: item.acknowledgedByName || item.resolvedByName || item.tenantName || item.ruleName || '待认领',
targetType: item.targetType || '-',
targetId: item.targetId || '',
evidence,
updatedAt: dateTime(item.updatedAt || item.createdAt),
note: item.resolutionNote || '',
};
},
rule(item) {
return {
id: item.id,
code: item.code,
name: item.name,
severity: mapValue(dictionaries.severityFromApi, item.severity, item.severity || '-'),
enabled: item.enabled !== false,
window: item.conditions?.window || item.metadata?.window || '-',
threshold: item.conditions?.threshold || item.metadata?.threshold || '-',
target: item.description || (item.targetTypes || []).join('、') || '-',
};
},
channel(item) {
return {
id: item.id,
code: item.channelCode,
name: item.name,
provider: mapValue(dictionaries.providerFromApi, item.provider, item.provider || 'Webhook'),
endpoint: safeEndpoint(item),
enabled: item.enabled !== false,
retry: Number(item.maxAttempts || item.metadata?.maxAttempts || 3),
minSeverity: mapValue(dictionaries.severityFromApi, item.minSeverity, item.minSeverity || '中'),
statusFilter: (item.statusFilter || []).join(','),
timeout: Number(item.timeoutSec || 10),
maxAttempts: Number(item.maxAttempts || item.metadata?.maxAttempts || 3),
lastStatus: '正常',
secretRef: item.secretRef || '',
endpointRedacted: true,
reminderTypes: item.reminderTypes || [],
reminderChannels: item.reminderChannels || [],
minReminderLevel: Number(item.minReminderLevel || 1),
};
},
event(item, type = 'audit') {
return {
id: item.id,
channelId: item.channelId,
subject: type === 'dunning'
? item.reminderMessage || item.invoiceNo || '催缴通知'
: item.alertTitle || item.alertAction || '审计告警通知',
status: mapValue(dictionaries.eventStatusFromApi, item.status, item.status || '-'),
attempts: Number(item.attempts || 0),
updatedAt: dateTime(item.updatedAt || item.sentAt || item.createdAt),
error: item.lastError || '',
};
},
};
function aggregateUsage(items) {
const records = new Map();
items.forEach(item => {
const period = monthOnly(item.periodStart || item.periodEnd);
const key = `${item.tenantId}:${period}`;
const record = records.get(key) || {
tenantId: item.tenantId,
period,
source: item.metadata?.source === 'platform_usage_worker' ? 'Worker 自动采集' : '人工补录',
students: 0,
questions: 0,
storageGB: 0,
trafficTB: 0,
overage: [],
estimated: 0,
};
const metricMap = {
students: 'students',
questions: 'questions',
storage_gb: 'storageGB',
traffic_tb: 'trafficTB',
};
const field = metricMap[item.metricKey];
if (field) record[field] = Number(item.metricValue || 0);
records.set(key, record);
});
return Array.from(records.values());
}
function currentPeriod() {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
}).formatToParts(new Date());
const values = Object.fromEntries(parts.map(part => [part.type, part.value]));
return `${values.year}-${values.month}`;
}
const bootstrapDescriptors = [
['overview', 'PlatformAdminOverviewController_overview', {}],
['plans', 'PlatformAdminOverviewController_plans', { query: { includeArchived: false } }],
['tenants', 'PlatformAdminTenantsController_list', { query: { limit: 200 } }],
['staff', 'PlatformAdminOverviewController_staff', { query: { limit: 200 } }],
['dunningChannels', 'PlatformAdminDunningChannelsController_channels', { query: { limit: 100 } }],
['dunningEvents', 'PlatformAdminDunningEventsController_events', { query: { limit: 100 } }],
['auditLogs', 'PlatformAdminAuditController_logs', { query: { limit: 200 } }],
['alerts', 'PlatformAdminAuditController_alerts', { query: { limit: 100 } }],
];
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(
descriptors.map(([, operationId, requestOptions]) => request(operationId, requestOptions)),
);
const payloads = {};
const errors = {};
results.forEach((result, index) => {
const [key] = descriptors[index];
if (result.status === 'fulfilled') payloads[key] = result.value;
else errors[key] = result.reason;
});
return { payloads, errors, requested: descriptors.length };
}
function adaptBootstrap(payloads, currentData = {}) {
const patch = {};
if (payloads.overview) patch.apiOverview = payloads.overview.item || payloads.overview;
if (payloads.permissions?.item) patch.apiPermissions = payloads.permissions.item;
if (Array.isArray(payloads.plans?.items)) {
patch.plans = payloads.plans.items.map(item => adapters.plan(item, currentById(currentData.plans, item.code)));
}
const planItems = patch.plans || currentData.plans || [];
const planMap = Object.fromEntries(planItems.map(item => [item.code, item]));
if (Array.isArray(payloads.tenants?.items)) {
patch.tenants = payloads.tenants.items.map(item => adapters.tenant(item, currentById(currentData.tenants, item.id), planMap));
}
if (Array.isArray(payloads.staff?.items)) patch.staff = payloads.staff.items.map(adapters.staff);
if (Array.isArray(payloads.invoices?.items)) patch.invoices = payloads.invoices.items.map(adapters.invoice);
if (Array.isArray(payloads.subscriptions?.items)) {
patch.subscriptions = payloads.subscriptions.items
.filter(item => item.subscriptionId)
.map(adapters.subscription);
}
if (Array.isArray(payloads.usage?.items)) patch.usage = aggregateUsage(payloads.usage.items);
if (Array.isArray(payloads.usageCandidates?.items) && patch.usage) {
const usageByKey = new Map(patch.usage.map(item => [`${item.tenantId}:${item.period}`, item]));
payloads.usageCandidates.items.forEach(candidate => {
const record = usageByKey.get(`${candidate.tenantId}:${monthOnly(candidate.periodStart)}`);
if (!record) return;
record.estimated = centsToYuan(candidate.totalCents);
record.overage = (candidate.items || []).map(item => item.description || item.itemType).filter(Boolean);
});
}
if (Array.isArray(payloads.reminders?.items)) patch.reminders = payloads.reminders.items.map(adapters.reminder);
if (Array.isArray(payloads.dunningChannels?.items)) patch.dunningChannels = payloads.dunningChannels.items.map(adapters.channel);
if (Array.isArray(payloads.dunningEvents?.items)) patch.dunningEvents = payloads.dunningEvents.items.map(item => adapters.event(item, 'dunning'));
if (Array.isArray(payloads.banks?.items)) {
patch.banks = payloads.banks.items.map(item => adapters.bank(item, currentById(currentData.banks, item.id)));
}
if (Array.isArray(payloads.grants?.items)) patch.grants = payloads.grants.items.map(adapters.grant);
if (Array.isArray(payloads.bankSync?.items)) patch.bankSync = payloads.bankSync.items.map(adapters.sync);
if (Array.isArray(payloads.auditLogs?.items)) patch.auditLogs = payloads.auditLogs.items.map(adapters.audit);
if (Array.isArray(payloads.alertRules?.items)) patch.alertRules = payloads.alertRules.items.map(adapters.rule);
if (Array.isArray(payloads.alerts?.items)) patch.alerts = payloads.alerts.items.map(adapters.alert);
if (Array.isArray(payloads.auditChannels?.items)) patch.auditChannels = payloads.auditChannels.items.map(adapters.channel);
if (Array.isArray(payloads.notificationEvents?.items)) {
patch.notificationEvents = payloads.notificationEvents.items.map(item => adapters.event(item, 'audit'));
}
return patch;
}
async function tenantDetail(tenantId) {
const payload = await request('PlatformAdminTenantsController_detail', { query: { tenantId } });
const item = payload.item || {};
return {
tenant: item.tenant || null,
domains: item.domains || [],
subscriptions: (item.subscriptions || []).map(adapters.subscription),
invoices: (item.invoices || []).map(adapters.invoice),
usage: aggregateUsage(item.usage || []),
billingProfile: item.tenant ? {
billingName: item.tenant.billingName || '',
taxId: item.tenant.taxId || '',
contactName: item.tenant.contactName || '',
phone: item.tenant.contactPhone || '',
email: item.tenant.contactEmail || '',
billingAddress: item.tenant.billingAddress || '',
invoiceTitle: item.tenant.invoiceTitle || '',
type: ({ special_vat: '增值税专用发票', normal_vat: '增值税普通发票', none: '不开票' })[item.tenant.invoiceType] || item.tenant.invoiceType || '',
bankName: item.tenant.bankName || '',
bankAccountMasked: item.tenant.bankAccountMasked || '',
} : null,
};
}
function downloadExport(payload) {
const item = payload?.item;
if (!item?.contentBase64) {
throw new PlatformApiError('审计导出响应缺少文件内容', { code: 'AUDIT_EXPORT_INVALID' });
}
const bytes = Uint8Array.from(atob(item.contentBase64), character => character.charCodeAt(0));
const blob = new Blob([bytes], { type: item.mimeType || 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = item.filename || `platform-audit.${item.format || 'csv'}`;
anchor.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
return item;
}
window.GongxuePlatformApi = Object.freeze({
PlatformApiError,
adapters,
adaptBootstrap,
bootstrap,
buildBody,
dictionaries,
downloadExport,
isEnabled,
operationCatalog,
periodBounds,
publicConfig,
request,
tenantDetail,
trace,
});
})();