forked from xiongyuxing/tiku-backend.net
1319 lines
58 KiB
JavaScript
1319 lines
58 KiB
JavaScript
(() => {
|
||
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 = {}, pathParams = {}) {
|
||
const baseUrl = normalizeBaseUrl(runtime.apiBaseUrl);
|
||
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));
|
||
});
|
||
return url.toString();
|
||
}
|
||
|
||
function responseRequestId(payload, response) {
|
||
return String(
|
||
payload?.meta?.requestId
|
||
|| payload?.requestId
|
||
|| payload?.traceId
|
||
|| payload?.extensions?.traceId
|
||
|| response?.headers?.get?.('x-request-id')
|
||
|| '',
|
||
);
|
||
}
|
||
|
||
function responseErrorCode(payload) {
|
||
return String(payload?.code || payload?.extensions?.code || payload?.errorCode || 'PLATFORM_API_REQUEST_FAILED');
|
||
}
|
||
|
||
function responseErrorMessage(payload, status) {
|
||
return String(payload?.message || payload?.title || payload?.error || `请求失败(HTTP ${status})`);
|
||
}
|
||
|
||
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, options.pathParams), {
|
||
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(
|
||
responseErrorMessage(payload, response.status),
|
||
{
|
||
status: response.status,
|
||
code: responseErrorCode(payload),
|
||
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: '严重' },
|
||
providerToApi: { Webhook: 'generic', SMTP: 'generic', 邮件: 'generic', 短信: 'generic', 钉钉: 'dingtalk', 飞书: 'feishu', 企微: 'wecom' },
|
||
providerFromApi: { generic: 'Webhook', dingtalk: '钉钉', feishu: '飞书', wecom: '企微' },
|
||
eventStatusFromApi: { pending: '待处理', processing: '发送中', sent: '成功', retrying: '重试中', failed: '失败', discarded: '已丢弃' },
|
||
crmStatusFromApi: { pending: '待推送', processing: '推送中', retrying: '重试中', sent: '已推送', failed: '推送失败', discarded: '已忽略' },
|
||
providerStatusFromApi: { active: '正常', disabled: '未启用', testing: '测试中' },
|
||
providerStatusToApi: { 正常: 'active', 未启用: 'disabled', 测试中: 'testing' },
|
||
smsTemplateStatusFromApi: { active: '正常', disabled: '未启用' },
|
||
smsAuditStatusFromApi: { draft: '草稿', pending_review: '审核中', approved: '已通过', rejected: '已拒绝' },
|
||
paymentStatusFromApi: { active: '正常', disabled: '未启用', testing: '测试中' },
|
||
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;
|
||
}
|
||
|
||
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 = {
|
||
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,
|
||
status: mapValue(dictionaries.tenantStatusToApi, input.status, input.status),
|
||
billingStatus: mapValue(dictionaries.billingStatusToApi, input.billingStatus, input.billingStatus),
|
||
metadata: input.metadata || {},
|
||
ownerEmail: input.ownerEmail || undefined,
|
||
ownerPhone: input.ownerPhone || undefined,
|
||
ownerName: input.ownerName,
|
||
temporaryPassword: input.temporaryPassword,
|
||
};
|
||
},
|
||
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 {
|
||
paymentId: input.paymentId || input.backendPaymentId || input.invoiceId,
|
||
providerTradeNo: input.tradeNo || input.providerTradeNo || undefined,
|
||
paidAt: input.paidAt ? new Date(input.paidAt).toISOString() : undefined,
|
||
reason: input.reason || input.paymentNo || 'platform_manual_payment_confirm',
|
||
};
|
||
},
|
||
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' },
|
||
};
|
||
},
|
||
PlatformAdminOverviewController_upsertStaff(input) {
|
||
return {
|
||
userId: input.userId || undefined,
|
||
email: input.email || undefined,
|
||
phone: input.phone || undefined,
|
||
name: input.name,
|
||
status: input.status === '已停用' ? 'disabled' : 'active',
|
||
roleIds: input.roleIds || [],
|
||
};
|
||
},
|
||
PlatformAdminOverviewController_updateStaffStatus(input) {
|
||
return {
|
||
userId: input.userId || input.staffId,
|
||
status: input.status === '正常' || input.status === 'active' ? 'active' : 'disabled',
|
||
reason: input.reason || undefined,
|
||
};
|
||
},
|
||
PlatformAdminAuditController_updateAlert(input) {
|
||
return {
|
||
alertId: input.alertId,
|
||
status: input.status,
|
||
resolutionNote: input.resolutionNote || input.note || undefined,
|
||
};
|
||
},
|
||
PlatformAdminCrmController_upsertConfig(input) {
|
||
return {
|
||
id: input.backendId || input.id || undefined,
|
||
tenantId: input.tenantId,
|
||
enabled: input.enabled !== false && input.status !== '未启用',
|
||
url: input.endpoint || input.url || undefined,
|
||
secretRef: input.secretRef || undefined,
|
||
formName: input.formName || input.pool || undefined,
|
||
examType: input.examType || input.subject || undefined,
|
||
timeoutSeconds: Number(input.timeout || input.timeoutSeconds || 10),
|
||
delaySeconds: Number(input.delaySeconds || 0),
|
||
assignmentMode: input.assignmentMode || 'round_robin',
|
||
assignmentPool: splitList(input.pool),
|
||
assignmentConfig: { retry: Number(input.retry || 3), source: 'platform_console' },
|
||
};
|
||
},
|
||
PlatformAdminCrmController_retryLead(input) {
|
||
return { queueId: input.backendId || input.queueId || input.id, note: input.note || 'platform_console_retry' };
|
||
},
|
||
PlatformAdminSmsController_upsertChannel(input) {
|
||
return {
|
||
id: input.backendId || input.id || undefined,
|
||
tenantId: input.tenantId,
|
||
provider: input.provider || 'generic',
|
||
name: input.name || `${input.provider || 'generic'} 短信渠道`,
|
||
signature: input.signature || '',
|
||
scene: input.scene || 'login',
|
||
status: mapValue(dictionaries.providerStatusToApi, input.status, input.status || 'active'),
|
||
secretRef: input.secretRef || undefined,
|
||
priority: Number(input.priority || 100),
|
||
monthlyQuota: Number(input.quota || input.monthlyQuota || 0),
|
||
configPublic: input.configPublic || { redacted: true },
|
||
metadata: { source: 'platform_console' },
|
||
};
|
||
},
|
||
PlatformAdminSmsController_upsertTemplate(input) {
|
||
return {
|
||
id: input.backendId || input.id || undefined,
|
||
tenantId: input.tenantId,
|
||
channelId: input.channelId,
|
||
code: input.code,
|
||
name: input.name,
|
||
type: input.type || 'notification',
|
||
auditStatus: input.auditStatus || 'draft',
|
||
status: input.status || 'active',
|
||
providerTemplateCode: input.providerTemplateCode || undefined,
|
||
content: input.content || '',
|
||
remark: input.remark || undefined,
|
||
metadata: { source: 'platform_console' },
|
||
};
|
||
},
|
||
PlatformAdminPaymentSettingsController_upsertApp(input) {
|
||
return {
|
||
id: input.backendId || input.id || undefined,
|
||
appCode: input.appCode || input.code || String(input.appName || '').toLowerCase().replaceAll(' ', '_'),
|
||
appName: input.appName,
|
||
status: mapValue(dictionaries.paymentStatusToApi, input.status, input.status || 'active'),
|
||
settlementMode: input.settlement || input.settlementMode || 'PlatformCollect',
|
||
description: input.description || undefined,
|
||
metadata: { source: 'platform_console' },
|
||
};
|
||
},
|
||
PlatformAdminPaymentSettingsController_upsertChannel(input) {
|
||
return {
|
||
id: input.backendId || input.id || undefined,
|
||
appId: input.appId,
|
||
provider: input.provider || 'manual',
|
||
mode: input.mode || 'PlatformCollect',
|
||
status: mapValue(dictionaries.paymentStatusToApi, input.status, input.status || 'active'),
|
||
displayName: input.displayName || input.appName || input.provider || '支付渠道',
|
||
secretRef: input.secretRef || undefined,
|
||
callbackPath: input.callbackPath || undefined,
|
||
priority: Number(input.priority || 100),
|
||
configPublic: input.configPublic || { redacted: true },
|
||
metadata: { source: 'platform_console' },
|
||
};
|
||
},
|
||
PlatformAdminTenantPaymentSettingsController_upsertApp(input) {
|
||
return {
|
||
tenantId: input.tenantId,
|
||
provider: input.provider || 'manual',
|
||
status: mapValue(dictionaries.providerStatusToApi, input.status, input.status || 'active'),
|
||
displayName: input.appName || input.displayName || undefined,
|
||
secretRef: input.secretRef || undefined,
|
||
priority: Number(input.priority || 100),
|
||
configPublic: input.configPublic || { mode: input.mode || 'TenantCollect', redacted: true },
|
||
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) {
|
||
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 => ({
|
||
channelId: input.id || input.channelId || undefined,
|
||
channelCode: input.code || input.channelCode,
|
||
name: input.name,
|
||
description: input.description || undefined,
|
||
enabled: input.enabled !== false,
|
||
provider: mapValue(dictionaries.providerToApi, input.provider, input.provider || 'generic'),
|
||
webhookUrl: input.endpoint || input.webhookUrl,
|
||
secretRef: input.secretRef || undefined,
|
||
minReminderLevel: Number(input.minReminderLevel || 1),
|
||
reminderTypes: input.reminderTypes || ['overdue', 'final_notice'],
|
||
reminderChannels: input.reminderChannels || ['internal'],
|
||
tenantIds: input.tenantIds || [],
|
||
timeoutSeconds: Number(input.timeout || input.timeoutSeconds || 10),
|
||
metadata: { ...(input.metadata || {}), secretRedacted: Boolean(input.secretRef) },
|
||
});
|
||
|
||
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) || {};
|
||
}
|
||
|
||
function itemOf(payload) {
|
||
return payload?.item || payload || {};
|
||
}
|
||
|
||
function itemsOf(payload, key = '') {
|
||
if (Array.isArray(payload)) return payload;
|
||
if (Array.isArray(payload?.items)) return payload.items;
|
||
if (key && Array.isArray(payload?.[key])) return payload[key];
|
||
return [];
|
||
}
|
||
|
||
function lowerEnum(value) {
|
||
return String(value || '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
||
}
|
||
|
||
function catalogPlans(payload) {
|
||
const offerings = itemsOf(payload, 'offerings');
|
||
const offeringsById = new Map(offerings.map(item => [item.id, item]));
|
||
return itemsOf(payload, 'versions')
|
||
.map(version => {
|
||
const offering = offeringsById.get(version.offeringId) || {};
|
||
return {
|
||
...version,
|
||
code: offering.code || version.offeringCode,
|
||
name: offering.name || version.offeringName,
|
||
description: offering.description || version.description,
|
||
status: lowerEnum(version.status),
|
||
billingCycle: lowerEnum(version.billingCycle),
|
||
baseAmountCents: version.amountCents ?? version.baseAmountCents,
|
||
includedQuotas: version.limits,
|
||
offeringType: lowerEnum(offering.type || version.offeringType),
|
||
};
|
||
})
|
||
.filter(item => item.offeringType !== 'add_on' && item.status !== 'retired');
|
||
}
|
||
|
||
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 || item.baseOfferingVersionId || '',
|
||
status: mapValue(dictionaries.subscriptionStatusFromApi, lowerEnum(item.status), item.status || '-'),
|
||
cycle: ({ yearly: '年度', monthly: '月度', quarterly: '季度', trial: '试用' })[lowerEnum(item.billingCycle)] || item.billingCycle || '-',
|
||
amount: centsToYuan(item.amountCents ?? item.totalAmountCents),
|
||
start: dateOnly(item.startsAt),
|
||
expires: dateOnly(item.expiresAt || item.currentPeriodEnd),
|
||
autoRenew: Boolean(item.metadata?.autoRenew),
|
||
};
|
||
},
|
||
invoice(item) {
|
||
const status = lowerEnum(item.status);
|
||
const totalCents = item.totalCents ?? item.totalAmountCents;
|
||
const paidCents = item.paidCents ?? (status === 'paid' ? totalCents : 0);
|
||
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),
|
||
total: centsToYuan(totalCents),
|
||
paid: centsToYuan(paidCents),
|
||
balance: centsToYuan(item.balanceCents ?? Math.max(0, Number(totalCents || 0) - Number(paidCents || 0))),
|
||
due: dateOnly(item.dueDate),
|
||
status: mapValue(dictionaries.invoiceStatusFromApi, status, item.status || '-'),
|
||
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,
|
||
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,
|
||
contentEntryId: item.contentEntryId || '',
|
||
catalog: '平台公共题库',
|
||
source: '平台自建',
|
||
nodes: Number(item.nodeCount || 0),
|
||
questions: Number(item.questionCount || 0),
|
||
version: item.metadata?.version || item.metadata?.contentVersion || '-',
|
||
status: lowerEnum(item.status) === 'active' ? '正常' : lowerEnum(item.status) === 'archived' ? '已归档' : item.status || '-',
|
||
};
|
||
},
|
||
staff(item) {
|
||
const roleCodes = item.roleCodes || [];
|
||
const permissions = roleCodes.includes('platform_super_admin') ? ['*'] : [];
|
||
return {
|
||
id: item.userId || item.id,
|
||
userId: item.userId || item.id,
|
||
name: item.name || item.email || '平台员工',
|
||
email: item.email || '',
|
||
phone: item.phoneMasked || item.phone || '-',
|
||
roleCodes,
|
||
role: permissions.includes('*') ? '超级管理员' : roleCodes.join('、') || '未分配角色',
|
||
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 || '',
|
||
};
|
||
},
|
||
crmConfig(item) {
|
||
const assignmentPool = Array.isArray(item.assignmentPool) ? item.assignmentPool.join('、') : jsonSummary(item.assignmentPool);
|
||
return {
|
||
id: item.id,
|
||
backendId: item.id,
|
||
tenantId: item.tenantId,
|
||
provider: item.url ? 'Webhook' : 'generic',
|
||
endpoint: item.url || '',
|
||
assignmentMode: ({ round_robin: '按地区轮转', direct: '手工分配', referrer: '来源归属', none: '不分配' })[lowerEnum(item.assignmentMode)] || item.assignmentMode || '-',
|
||
pool: assignmentPool || item.formName || '-',
|
||
timeout: Number(item.timeoutSeconds || 0),
|
||
retry: Number(item.assignmentConfig?.retry || 0),
|
||
enabled: item.enabled !== false,
|
||
status: item.enabled === false ? '未启用' : '正常',
|
||
secretRef: item.secretRef || '',
|
||
formName: item.formName || '',
|
||
examType: item.examType || '',
|
||
};
|
||
},
|
||
crmLead(item) {
|
||
return {
|
||
id: item.id,
|
||
backendId: item.id,
|
||
tenantId: item.tenantId,
|
||
student: item.payload?.student || item.leadId || item.recordId || 'CRM 线索',
|
||
phone: item.payload?.phoneMasked || '-',
|
||
source: item.source || '-',
|
||
subject: item.payload?.subject || item.provider || '-',
|
||
owner: item.payload?.owner || '-',
|
||
attempts: Number(item.attempts || 0),
|
||
status: mapValue(dictionaries.crmStatusFromApi, lowerEnum(item.status), item.status || '-'),
|
||
lastError: item.lastError || '',
|
||
};
|
||
},
|
||
smsChannel(item) {
|
||
const status = mapValue(dictionaries.providerStatusFromApi, lowerEnum(item.status), item.status || '-');
|
||
return {
|
||
id: item.id,
|
||
backendId: item.id,
|
||
tenantId: item.tenantId,
|
||
provider: item.provider || '-',
|
||
name: item.name || item.displayName || '',
|
||
signature: item.signature || item.name || '-',
|
||
scene: item.scene || '-',
|
||
secretRef: item.secretRef || '',
|
||
quota: Number(item.monthlyQuota || 0),
|
||
used: Number(item.sentThisMonth || 0),
|
||
status,
|
||
enabled: status === '正常',
|
||
priority: Number(item.priority || 100),
|
||
};
|
||
},
|
||
smsTemplate(item) {
|
||
const sent = Number(item.sentCount || 0);
|
||
const failed = Number(item.failedCount || 0);
|
||
return {
|
||
id: item.id,
|
||
backendId: item.id,
|
||
tenantId: item.tenantId,
|
||
channelId: item.channelId,
|
||
name: item.name || '-',
|
||
code: item.code || '-',
|
||
type: lowerEnum(item.type),
|
||
status: mapValue(dictionaries.smsAuditStatusFromApi, lowerEnum(item.auditStatus), item.auditStatus || '-'),
|
||
templateStatus: mapValue(dictionaries.smsTemplateStatusFromApi, lowerEnum(item.status), item.status || '-'),
|
||
lastSend: dateTime(item.updatedAt),
|
||
successRate: sent + failed ? `${Math.round(sent / (sent + failed) * 100)}%` : '-',
|
||
content: item.content || '',
|
||
};
|
||
},
|
||
smsLog(item) {
|
||
return {
|
||
id: item.id,
|
||
tenantId: item.tenantId,
|
||
templateId: item.templateId,
|
||
channelId: item.channelId,
|
||
phone: item.phoneMasked || '-',
|
||
provider: item.provider || '-',
|
||
updatedAt: dateTime(item.sentAt || item.createdAt),
|
||
status: lowerEnum(item.status) === 'sent' ? '发送成功' : lowerEnum(item.status) === 'failed' ? '发送失败' : '待发送',
|
||
};
|
||
},
|
||
platformPaymentApp(item) {
|
||
const status = mapValue(dictionaries.paymentStatusFromApi, lowerEnum(item.status), item.status || '-');
|
||
return {
|
||
id: item.id,
|
||
backendId: item.id,
|
||
tenantId: '',
|
||
appCode: item.appCode || '',
|
||
appName: item.appName || '-',
|
||
provider: 'platform',
|
||
mode: '平台收款',
|
||
settlement: item.settlementMode || 'PlatformCollect',
|
||
secretRef: '',
|
||
status,
|
||
appScope: 'platform',
|
||
};
|
||
},
|
||
tenantPaymentApp(item) {
|
||
const status = mapValue(dictionaries.providerStatusFromApi, lowerEnum(item.status), item.status || '-');
|
||
return {
|
||
id: item.id,
|
||
backendId: item.id,
|
||
tenantId: item.tenantId || '',
|
||
appName: item.displayName || item.provider || '-',
|
||
provider: item.provider || '-',
|
||
mode: item.configPublic?.mode || 'TenantCollect',
|
||
settlement: '租户收款',
|
||
secretRef: item.secretRef || '',
|
||
status,
|
||
appScope: 'tenant',
|
||
};
|
||
},
|
||
platformPaymentEvent(item) {
|
||
return {
|
||
id: item.id,
|
||
appId: item.appId || '',
|
||
orderNo: item.providerEventId || item.eventId || item.paymentId || '-',
|
||
amount: centsToYuan(item.payload?.amountCents || item.amountCents || 0),
|
||
channel: item.provider || '-',
|
||
notified: item.eventType || '-',
|
||
updatedAt: dateTime(item.createdAt || item.processedAt),
|
||
status: item.error ? '回调失败' : '成功',
|
||
};
|
||
},
|
||
};
|
||
|
||
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 = [
|
||
['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', 'PlatformQuestionBanksController_getBanks', { query: { status: 'all' } }],
|
||
['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 } }],
|
||
['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 } }],
|
||
];
|
||
|
||
async function bootstrap(options = {}) {
|
||
const descriptors = bootstrapDescriptors.map(([key, operationId, requestOptions]) => {
|
||
const nextOptions = { ...requestOptions, query: { ...(requestOptions.query || {}) } };
|
||
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 = {};
|
||
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) {
|
||
patch.plans = planPayload.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]));
|
||
const tenants = itemsOf(payloads.tenants);
|
||
if (tenants.length) {
|
||
patch.tenants = tenants.map(item => adapters.tenant(item, currentById(currentData.tenants, item.id), planMap));
|
||
}
|
||
const staff = itemsOf(payloads.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) {
|
||
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);
|
||
});
|
||
}
|
||
const reminders = itemsOf(payloads.reminders);
|
||
if (reminders.length) patch.reminders = reminders.map(adapters.reminder);
|
||
const dunningChannels = itemsOf(payloads.dunningChannels);
|
||
if (dunningChannels.length) patch.dunningChannels = dunningChannels.map(adapters.channel);
|
||
const dunningEvents = itemsOf(payloads.dunningEvents);
|
||
if (dunningEvents.length) patch.dunningEvents = dunningEvents.map(item => adapters.event(item, 'dunning'));
|
||
const banks = itemsOf(payloads.banks);
|
||
if (banks.length) {
|
||
patch.banks = banks.map(item => adapters.bank(item, currentById(currentData.banks, item.id)));
|
||
}
|
||
const auditLogs = itemsOf(payloads.auditLogs);
|
||
if (auditLogs.length) patch.auditLogs = auditLogs.map(adapters.audit);
|
||
const alertRules = itemsOf(payloads.alertRules);
|
||
if (alertRules.length) patch.alertRules = alertRules.map(adapters.rule);
|
||
const alerts = itemsOf(payloads.alerts);
|
||
if (alerts.length) patch.alerts = alerts.map(adapters.alert);
|
||
const crmConfigs = itemsOf(payloads.crmConfigs);
|
||
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);
|
||
if (smsTemplates.length) patch.smsTemplates = smsTemplates.map(adapters.smsTemplate);
|
||
const smsLogs = itemsOf(payloads.smsLogs);
|
||
if (smsLogs.length) patch.smsLogs = smsLogs.map(adapters.smsLog);
|
||
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),
|
||
];
|
||
if (paymentEvents.length) patch.paymentEvents = paymentEvents;
|
||
const auditChannels = itemsOf(payloads.auditChannels);
|
||
if (auditChannels.length) patch.auditChannels = auditChannels.map(adapters.channel);
|
||
const notificationEvents = itemsOf(payloads.notificationEvents);
|
||
if (notificationEvents.length) {
|
||
patch.notificationEvents = notificationEvents.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,
|
||
});
|
||
})();
|