forked from wangziqi/gongxue-base
feat: add platform tenant detail audit console
This commit is contained in:
@@ -3,10 +3,15 @@ import Taro from '@tarojs/taro';
|
||||
import { Button, Input, Text, View } from '@tarojs/components';
|
||||
import {
|
||||
createPlatformTenant,
|
||||
loadPlatformAuditLogs,
|
||||
loadPlatformPlans,
|
||||
loadPlatformTenantDetail,
|
||||
loadPlatformTenants,
|
||||
updatePlatformTenantStatus,
|
||||
upsertPlatformTenantBillingProfile,
|
||||
type PlatformAuditLogItem,
|
||||
type PlatformSaasPlan,
|
||||
type PlatformTenantDetail,
|
||||
type PlatformTenantItem,
|
||||
} from '@/services/platformAdmin';
|
||||
import '../platform.css';
|
||||
@@ -21,11 +26,41 @@ function centsFromYuan(value: string) {
|
||||
return Math.round(amount * 100);
|
||||
}
|
||||
|
||||
function dateText(value?: string | null) {
|
||||
return value ? String(value).slice(0, 10) : '-';
|
||||
}
|
||||
|
||||
function auditDetailsText(item: PlatformAuditLogItem) {
|
||||
try {
|
||||
const text = JSON.stringify(item.details || {});
|
||||
return text.length > 160 ? `${text.slice(0, 160)}...` : text;
|
||||
} catch {
|
||||
return '{}';
|
||||
}
|
||||
}
|
||||
|
||||
const emptyBillingForm = {
|
||||
tenantId: '',
|
||||
billingName: '',
|
||||
taxId: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
billingAddress: '',
|
||||
invoiceTitle: '',
|
||||
invoiceType: 'none',
|
||||
bankName: '',
|
||||
bankAccountMasked: '',
|
||||
};
|
||||
|
||||
export default function PlatformTenantsPage() {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
const [plans, setPlans] = useState<PlatformSaasPlan[]>([]);
|
||||
const [tenants, setTenants] = useState<PlatformTenantItem[]>([]);
|
||||
const [selectedTenantId, setSelectedTenantId] = useState('');
|
||||
const [detail, setDetail] = useState<PlatformTenantDetail | null>(null);
|
||||
const [auditLogs, setAuditLogs] = useState<PlatformAuditLogItem[]>([]);
|
||||
const [tenantForm, setTenantForm] = useState({
|
||||
slug: '',
|
||||
name: '',
|
||||
@@ -42,6 +77,7 @@ export default function PlatformTenantsPage() {
|
||||
billingStatus: '',
|
||||
reason: '',
|
||||
});
|
||||
const [billingForm, setBillingForm] = useState(emptyBillingForm);
|
||||
const [busy, setBusy] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
@@ -51,6 +87,43 @@ export default function PlatformTenantsPage() {
|
||||
.catch(nextError => setError(nextError instanceof Error ? nextError.message : '租户列表加载失败'));
|
||||
}
|
||||
|
||||
function fillBillingForm(nextDetail: PlatformTenantDetail | null, fallbackTenantId = '') {
|
||||
const tenant = nextDetail?.tenant;
|
||||
setBillingForm({
|
||||
tenantId: tenant?.id || fallbackTenantId,
|
||||
billingName: tenant?.billingName || '',
|
||||
taxId: tenant?.taxId || '',
|
||||
contactName: tenant?.contactName || '',
|
||||
contactPhone: tenant?.contactPhone || '',
|
||||
contactEmail: tenant?.contactEmail || '',
|
||||
billingAddress: tenant?.billingAddress || '',
|
||||
invoiceTitle: tenant?.invoiceTitle || '',
|
||||
invoiceType: tenant?.invoiceType || 'none',
|
||||
bankName: tenant?.bankName || '',
|
||||
bankAccountMasked: tenant?.bankAccountMasked || '',
|
||||
});
|
||||
}
|
||||
|
||||
async function loadDetail(tenantId: string) {
|
||||
if (!tenantId) return;
|
||||
setError('');
|
||||
setBusy('detail');
|
||||
try {
|
||||
const [detailPayload, auditPayload] = await Promise.all([
|
||||
loadPlatformTenantDetail(tenantId),
|
||||
loadPlatformAuditLogs({ tenantId, limit: 30 }).catch(() => ({ items: [] })),
|
||||
]);
|
||||
const nextDetail = detailPayload.item || null;
|
||||
setDetail(nextDetail);
|
||||
fillBillingForm(nextDetail, tenantId);
|
||||
setAuditLogs(auditPayload.items || []);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '租户详情加载失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
reload('', '');
|
||||
loadPlatformPlans().then(payload => {
|
||||
@@ -73,6 +146,22 @@ export default function PlatformTenantsPage() {
|
||||
setStatusForm(current => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
function updateBillingForm(key: keyof typeof billingForm, value: string) {
|
||||
setBillingForm(current => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
function chooseTenant(item: PlatformTenantItem) {
|
||||
setSelectedTenantId(item.id);
|
||||
setStatusForm(current => ({
|
||||
...current,
|
||||
tenantId: item.id,
|
||||
status: item.status || 'active',
|
||||
billingStatus: item.billingStatus || '',
|
||||
}));
|
||||
fillBillingForm(null, item.id);
|
||||
loadDetail(item.id);
|
||||
}
|
||||
|
||||
async function confirm(title: string, content: string) {
|
||||
const result = await Taro.showModal({ title, content, confirmText: '确认', cancelText: '取消' });
|
||||
return result.confirm;
|
||||
@@ -90,7 +179,7 @@ export default function PlatformTenantsPage() {
|
||||
if (!ok) return;
|
||||
setBusy('create');
|
||||
try {
|
||||
await createPlatformTenant({
|
||||
const payload = await createPlatformTenant({
|
||||
slug,
|
||||
name,
|
||||
legalName: tenantForm.legalName.trim() || undefined,
|
||||
@@ -112,6 +201,10 @@ export default function PlatformTenantsPage() {
|
||||
amountYuan: '',
|
||||
}));
|
||||
reload(status, keyword);
|
||||
if (payload.item?.id) {
|
||||
setSelectedTenantId(payload.item.id);
|
||||
await loadDetail(payload.item.id);
|
||||
}
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '创建租户失败');
|
||||
} finally {
|
||||
@@ -141,6 +234,7 @@ export default function PlatformTenantsPage() {
|
||||
});
|
||||
Taro.showToast({ title: '已更新', icon: 'success' });
|
||||
reload(status, keyword);
|
||||
await loadDetail(statusForm.tenantId);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '状态更新失败');
|
||||
} finally {
|
||||
@@ -148,13 +242,47 @@ export default function PlatformTenantsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBillingProfile() {
|
||||
setError('');
|
||||
if (!billingForm.tenantId) {
|
||||
setError('请先选择租户,再维护账务资料。');
|
||||
return;
|
||||
}
|
||||
const ok = await confirm('保存账务资料', '确认更新该租户的开票和收款联系资料?');
|
||||
if (!ok) return;
|
||||
setBusy('billing');
|
||||
try {
|
||||
await upsertPlatformTenantBillingProfile({
|
||||
tenantId: billingForm.tenantId,
|
||||
billingName: billingForm.billingName.trim() || undefined,
|
||||
taxId: billingForm.taxId.trim() || undefined,
|
||||
contactName: billingForm.contactName.trim() || undefined,
|
||||
contactPhone: billingForm.contactPhone.trim() || undefined,
|
||||
contactEmail: billingForm.contactEmail.trim() || undefined,
|
||||
billingAddress: billingForm.billingAddress.trim() || undefined,
|
||||
invoiceTitle: billingForm.invoiceTitle.trim() || undefined,
|
||||
invoiceType: billingForm.invoiceType.trim() || 'none',
|
||||
bankName: billingForm.bankName.trim() || undefined,
|
||||
bankAccountMasked: billingForm.bankAccountMasked.trim() || undefined,
|
||||
});
|
||||
Taro.showToast({ title: '已保存', icon: 'success' });
|
||||
await loadDetail(billingForm.tenantId);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError.message : '账务资料保存失败');
|
||||
} finally {
|
||||
setBusy('');
|
||||
}
|
||||
}
|
||||
|
||||
const selectedTenant = detail?.tenant;
|
||||
|
||||
return (
|
||||
<View className='platform-page'>
|
||||
<View className='platform-shell'>
|
||||
<View className='platform-header'>
|
||||
<Text className='platform-kicker'>Tenants</Text>
|
||||
<Text className='platform-title'>租户管理</Text>
|
||||
<Text className='platform-subtitle'>查看 SaaS 租户的品牌、订阅、状态和欠费情况;创建和状态变更表单后续接入同一组平台 API。</Text>
|
||||
<Text className='platform-subtitle'>创建合作商租户、维护 SaaS 状态和账务资料,并追踪租户级平台审计证据。</Text>
|
||||
</View>
|
||||
|
||||
<View className='platform-actions'>
|
||||
@@ -176,7 +304,7 @@ export default function PlatformTenantsPage() {
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>当前列表</Text><Text className='platform-metric-value'>{String(tenants.length)}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>欠费租户</Text><Text className='platform-metric-value'>{String(tenants.filter(item => Number(item.openBalanceCents || 0) > 0).length)}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>试用租户</Text><Text className='platform-metric-value'>{String(tenants.filter(item => item.billingStatus === 'trial').length)}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>暂停租户</Text><Text className='platform-metric-value'>{String(tenants.filter(item => item.status === 'suspended').length)}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>已选租户</Text><Text className='platform-metric-value'>{selectedTenant ? selectedTenant.slug : '-'}</Text></View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
@@ -196,19 +324,6 @@ export default function PlatformTenantsPage() {
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>状态变更</Text>
|
||||
<View className='platform-form compact'>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>租户 ID</Text><Input className='platform-input' placeholder='从列表选择或粘贴 tenantId' value={statusForm.tenantId} onInput={event => updateStatusForm('tenantId', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>租户状态</Text><Input className='platform-input' placeholder='active / suspended' value={statusForm.status} onInput={event => updateStatusForm('status', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>账务状态</Text><Input className='platform-input' placeholder='trial / active / past_due' value={statusForm.billingStatus} onInput={event => updateStatusForm('billingStatus', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>原因</Text><Input className='platform-input' placeholder='内部审计备注' value={statusForm.reason} onInput={event => updateStatusForm('reason', String(event.detail.value || ''))} /></View>
|
||||
</View>
|
||||
<View className='platform-actions'>
|
||||
<Button className='platform-button primary' loading={busy === 'status'} onClick={submitStatusChange}>提交状态变更</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>租户列表</Text>
|
||||
<View className='platform-list'>
|
||||
@@ -216,17 +331,114 @@ export default function PlatformTenantsPage() {
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.brandName || item.name}</Text>
|
||||
<Text className='platform-row-meta'>{item.slug} · {item.legalName || '未填公司'} · {item.status || '-'} · {item.billingStatus || '-'}</Text>
|
||||
<Text className='platform-row-meta'>套餐 {item.planCode || '未订阅'} · 订阅 {item.subscriptionStatus || '-'} · 到期 {item.subscriptionExpiresAt ? String(item.subscriptionExpiresAt).slice(0, 10) : '-'}</Text>
|
||||
<Text className='platform-row-meta'>套餐 {item.planCode || '未订阅'} · 订阅 {item.subscriptionStatus || '-'} · 到期 {dateText(item.subscriptionExpiresAt)}</Text>
|
||||
<Text className='platform-row-meta'>未收余额 {money(item.openBalanceCents)}</Text>
|
||||
<View className='platform-row-actions'>
|
||||
<Button className='platform-mini-button' onClick={() => setStatusForm(current => ({ ...current, tenantId: item.id, status: item.status || 'active', billingStatus: item.billingStatus || '' }))}>选择</Button>
|
||||
<Button className='platform-mini-button danger' onClick={() => setStatusForm({ tenantId: item.id, status: 'suspended', billingStatus: item.billingStatus || '', reason: 'platform manual suspend' })}>准备暂停</Button>
|
||||
<Button className='platform-mini-button' loading={busy === 'detail' && selectedTenantId === item.id} onClick={() => chooseTenant(item)}>详情</Button>
|
||||
<Button className='platform-mini-button danger' onClick={() => {
|
||||
setSelectedTenantId(item.id);
|
||||
setStatusForm({ tenantId: item.id, status: 'suspended', billingStatus: item.billingStatus || '', reason: 'platform manual suspend' });
|
||||
fillBillingForm(null, item.id);
|
||||
}}>准备暂停</Button>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!tenants.length ? <View className='platform-empty'>暂无租户,或当前平台管理员权限未通过。</View> : null}
|
||||
</View>
|
||||
|
||||
{selectedTenant ? (
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>租户详情</Text>
|
||||
<View className='platform-grid'>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>品牌</Text><Text className='platform-metric-value'>{selectedTenant.brandName || selectedTenant.name}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>租户状态</Text><Text className='platform-metric-value'>{selectedTenant.status || '-'}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>账务状态</Text><Text className='platform-metric-value'>{selectedTenant.billingStatus || '-'}</Text></View>
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>主体</Text><Text className='platform-metric-value'>{selectedTenant.legalName || '-'}</Text></View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>状态变更</Text>
|
||||
<View className='platform-form compact'>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>租户 ID</Text><Input className='platform-input' placeholder='tenantId' value={statusForm.tenantId} onInput={event => updateStatusForm('tenantId', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>租户状态</Text><Input className='platform-input' placeholder='active / suspended' value={statusForm.status} onInput={event => updateStatusForm('status', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>账务状态</Text><Input className='platform-input' placeholder='trial / active / past_due' value={statusForm.billingStatus} onInput={event => updateStatusForm('billingStatus', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>原因</Text><Input className='platform-input' placeholder='内部审计备注' value={statusForm.reason} onInput={event => updateStatusForm('reason', String(event.detail.value || ''))} /></View>
|
||||
</View>
|
||||
<View className='platform-actions'>
|
||||
<Button className='platform-button primary' loading={busy === 'status'} onClick={submitStatusChange}>提交状态变更</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>账务资料</Text>
|
||||
<View className='platform-form'>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>租户 ID</Text><Input className='platform-input' value={billingForm.tenantId} onInput={event => updateBillingForm('tenantId', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>开票名称</Text><Input className='platform-input' placeholder='公司或个人抬头' value={billingForm.billingName} onInput={event => updateBillingForm('billingName', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>税号</Text><Input className='platform-input' placeholder='可选' value={billingForm.taxId} onInput={event => updateBillingForm('taxId', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>联系人</Text><Input className='platform-input' value={billingForm.contactName} onInput={event => updateBillingForm('contactName', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>联系电话</Text><Input className='platform-input' value={billingForm.contactPhone} onInput={event => updateBillingForm('contactPhone', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>联系邮箱</Text><Input className='platform-input' value={billingForm.contactEmail} onInput={event => updateBillingForm('contactEmail', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>发票类型</Text><Input className='platform-input' placeholder='none / normal_vat / special_vat' value={billingForm.invoiceType} onInput={event => updateBillingForm('invoiceType', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>发票抬头</Text><Input className='platform-input' value={billingForm.invoiceTitle} onInput={event => updateBillingForm('invoiceTitle', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field wide'><Text className='platform-field-label'>账单地址</Text><Input className='platform-input' value={billingForm.billingAddress} onInput={event => updateBillingForm('billingAddress', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>开户行</Text><Input className='platform-input' value={billingForm.bankName} onInput={event => updateBillingForm('bankName', String(event.detail.value || ''))} /></View>
|
||||
<View className='platform-field'><Text className='platform-field-label'>银行账号掩码</Text><Input className='platform-input' placeholder='仅保存掩码' value={billingForm.bankAccountMasked} onInput={event => updateBillingForm('bankAccountMasked', String(event.detail.value || ''))} /></View>
|
||||
</View>
|
||||
<View className='platform-actions'>
|
||||
<Button className='platform-button primary' loading={busy === 'billing'} onClick={submitBillingProfile}>保存账务资料</Button>
|
||||
<Button className='platform-button' onClick={() => Taro.navigateTo({ url: '/pages/platform-admin/billing/index' })}>去账务中心</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>域名 / 订阅 / 账单 / 用量</Text>
|
||||
<View className='platform-list'>
|
||||
{(detail?.domains || []).map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.host || item.id}</Text>
|
||||
<Text className='platform-row-meta'>{item.domainType || 'domain'} · {item.status || '-'} · {item.isPrimary ? 'primary' : 'secondary'} · 验证 {dateText(item.verifiedAt)}</Text>
|
||||
</View>
|
||||
))}
|
||||
{(detail?.subscriptions || []).slice(0, 3).map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.planCode || item.id}</Text>
|
||||
<Text className='platform-row-meta'>{item.status || '-'} · {item.billingCycle || '-'} · {money(item.amountCents)} · {dateText(item.startsAt)} 至 {dateText(item.expiresAt)}</Text>
|
||||
</View>
|
||||
))}
|
||||
{(detail?.invoices || []).slice(0, 4).map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.invoiceNo || item.id}</Text>
|
||||
<Text className='platform-row-meta'>{item.status || '-'} · 总额 {money(item.totalCents)} · 已收 {money(item.paidCents)} · 余额 {money(item.balanceCents)} · 到期 {dateText(item.dueDate)}</Text>
|
||||
</View>
|
||||
))}
|
||||
{(detail?.usage || []).slice(0, 6).map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.metricKey || 'metric'}</Text>
|
||||
<Text className='platform-row-meta'>{String(item.metricValue || 0)} · {dateText(item.periodStart)} 至 {dateText(item.periodEnd)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>最近平台审计</Text>
|
||||
<View className='platform-list'>
|
||||
{auditLogs.map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.action || '-'}</Text>
|
||||
<Text className='platform-row-meta'>{dateText(item.createdAt)} · {item.actorName || item.actorUsername || item.actorPhone || 'system'} · {item.targetType || '-'} · {item.targetId || '-'}</Text>
|
||||
<Text className='platform-row-meta'>{auditDetailsText(item)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!auditLogs.length ? <View className='platform-empty'>暂无平台审计记录。</View> : null}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className='platform-empty'>从租户列表点击“详情”后,可以查看域名、订阅、账单、用量、账务资料和平台审计。</View>
|
||||
)}
|
||||
|
||||
{error ? <Text className='platform-error'>{error}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,11 +2,13 @@ import { useEffect, useState } from 'react';
|
||||
import Taro from '@tarojs/taro';
|
||||
import { Button, Text, View } from '@tarojs/components';
|
||||
import {
|
||||
loadPlatformAuditLogs,
|
||||
loadPlatformInvoices,
|
||||
loadPlatformOverview,
|
||||
loadPlatformQuestionBankGrants,
|
||||
loadPlatformQuestionBanks,
|
||||
loadPlatformTenants,
|
||||
type PlatformAuditLogItem,
|
||||
type PlatformInvoiceItem,
|
||||
type PlatformOverview,
|
||||
type PlatformQuestionBankGrant,
|
||||
@@ -23,6 +25,7 @@ export default function PlatformWorkbenchPage() {
|
||||
const [overview, setOverview] = useState<PlatformOverview | null>(null);
|
||||
const [tenants, setTenants] = useState<PlatformTenantItem[]>([]);
|
||||
const [invoices, setInvoices] = useState<PlatformInvoiceItem[]>([]);
|
||||
const [auditLogs, setAuditLogs] = useState<PlatformAuditLogItem[]>([]);
|
||||
const [banks, setBanks] = useState<PlatformQuestionBankItem[]>([]);
|
||||
const [grants, setGrants] = useState<PlatformQuestionBankGrant[]>([]);
|
||||
const [error, setError] = useState('');
|
||||
@@ -34,12 +37,14 @@ export default function PlatformWorkbenchPage() {
|
||||
loadPlatformInvoices({ limit: 6 }).catch(() => ({ items: [] })),
|
||||
loadPlatformQuestionBanks({ limit: 6 }).catch(() => ({ items: [] })),
|
||||
loadPlatformQuestionBankGrants({ limit: 6 }).catch(() => ({ items: [] })),
|
||||
]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload]) => {
|
||||
loadPlatformAuditLogs({ limit: 6 }).catch(() => ({ items: [] })),
|
||||
]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload, auditPayload]) => {
|
||||
setOverview(overviewPayload.item || null);
|
||||
setTenants(tenantPayload.items || []);
|
||||
setInvoices(invoicePayload.items || []);
|
||||
setBanks(bankPayload.items || []);
|
||||
setGrants(grantPayload.items || []);
|
||||
setAuditLogs(auditPayload.items || []);
|
||||
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '平台后台加载失败'));
|
||||
}, []);
|
||||
|
||||
@@ -105,6 +110,18 @@ export default function PlatformWorkbenchPage() {
|
||||
<View className='platform-metric'><Text className='platform-metric-label'>逾期账单</Text><Text className='platform-metric-value'>{String(overview?.billing?.overdueInvoices || 0)}</Text></View>
|
||||
</View>
|
||||
</View>
|
||||
<View className='platform-section'>
|
||||
<Text className='platform-section-title'>最近平台审计</Text>
|
||||
<View className='platform-list'>
|
||||
{auditLogs.map(item => (
|
||||
<View className='platform-row' key={item.id}>
|
||||
<Text className='platform-row-main'>{item.action || '-'}</Text>
|
||||
<Text className='platform-row-meta'>{item.tenantName || item.tenantSlug || '平台'} · {item.actorName || item.actorUsername || 'system'} · {item.targetType || '-'} · {String(item.createdAt || '').slice(0, 19).replace('T', ' ')}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
{!auditLogs.length ? <View className='platform-empty'>暂无审计记录。</View> : null}
|
||||
</View>
|
||||
{error ? <Text className='platform-error'>{error}</Text> : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -54,6 +54,60 @@ export interface PlatformTenantItem {
|
||||
openBalanceCents?: number | string | null;
|
||||
}
|
||||
|
||||
export interface PlatformTenantBillingProfile {
|
||||
tenantId?: string | null;
|
||||
billingName?: string | null;
|
||||
taxId?: string | null;
|
||||
contactName?: string | null;
|
||||
contactPhone?: string | null;
|
||||
contactEmail?: string | null;
|
||||
billingAddress?: string | null;
|
||||
invoiceTitle?: string | null;
|
||||
invoiceType?: string | null;
|
||||
bankName?: string | null;
|
||||
bankAccountMasked?: string | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface PlatformTenantDetailTenant extends PlatformTenantItem, PlatformTenantBillingProfile {
|
||||
shortName?: string | null;
|
||||
serviceWechat?: string | null;
|
||||
ownerUserId?: string | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
createdAt?: string | null;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformTenantDomain {
|
||||
id: string;
|
||||
host?: string | null;
|
||||
domainType?: string | null;
|
||||
status?: string | null;
|
||||
isPrimary?: boolean | null;
|
||||
verifiedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformTenantSubscription {
|
||||
id: string;
|
||||
planCode?: string | null;
|
||||
status?: string | null;
|
||||
startsAt?: string | null;
|
||||
expiresAt?: string | null;
|
||||
billingCycle?: string | null;
|
||||
amountCents?: number | string | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformTenantDetail {
|
||||
tenant?: PlatformTenantDetailTenant | null;
|
||||
domains?: PlatformTenantDomain[];
|
||||
subscriptions?: PlatformTenantSubscription[];
|
||||
invoices?: PlatformInvoiceItem[];
|
||||
usage?: PlatformUsageItem[];
|
||||
}
|
||||
|
||||
export interface PlatformInvoiceItem {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
@@ -113,6 +167,24 @@ export interface PlatformQuestionBankGrant {
|
||||
expiresAt?: string | null;
|
||||
}
|
||||
|
||||
export interface PlatformAuditLogItem {
|
||||
id: string;
|
||||
tenantId?: string | null;
|
||||
tenantSlug?: string | null;
|
||||
tenantName?: string | null;
|
||||
actorUserId?: string | null;
|
||||
actorUsername?: string | null;
|
||||
actorName?: string | null;
|
||||
actorPhone?: string | null;
|
||||
action?: string | null;
|
||||
targetType?: string | null;
|
||||
targetId?: string | null;
|
||||
details?: Record<string, unknown> | null;
|
||||
ipAddress?: string | null;
|
||||
userAgent?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
|
||||
export interface CreatePlatformTenantInput {
|
||||
slug: string;
|
||||
name: string;
|
||||
@@ -133,6 +205,10 @@ export interface UpdatePlatformTenantStatusInput {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface UpsertPlatformTenantBillingProfileInput extends PlatformTenantBillingProfile {
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
export interface CreatePlatformSubscriptionInput {
|
||||
tenantId: string;
|
||||
planCode: string;
|
||||
@@ -198,6 +274,20 @@ export async function loadPlatformTenants(query: { q?: string; status?: string;
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadPlatformTenantDetail(tenantId: string) {
|
||||
return apiRequest<{ item?: PlatformTenantDetail }>('/api/platform-admin/tenants/detail', {
|
||||
query: { tenantId },
|
||||
tenantId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadPlatformAuditLogs(query: { tenantId?: string; action?: string; targetType?: string; actorUserId?: string; q?: string; limit?: number } = {}) {
|
||||
return apiRequest<{ items?: PlatformAuditLogItem[] }>('/api/platform-admin/audit-logs', {
|
||||
query: { ...query, limit: query.limit || 100 },
|
||||
tenantId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadPlatformInvoices(query: { tenantId?: string; status?: string; limit?: number } = {}) {
|
||||
return apiRequest<{ items?: PlatformInvoiceItem[] }>('/api/platform-admin/invoices', {
|
||||
query: { ...query, limit: query.limit || 80 },
|
||||
@@ -242,6 +332,14 @@ export async function updatePlatformTenantStatus(input: UpdatePlatformTenantStat
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertPlatformTenantBillingProfile(input: UpsertPlatformTenantBillingProfileInput) {
|
||||
return apiRequest<{ item?: PlatformTenantBillingProfile }>('/api/platform-admin/tenants/billing-profile', {
|
||||
method: 'PUT',
|
||||
body: input,
|
||||
tenantId: null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPlatformSubscription(input: CreatePlatformSubscriptionInput) {
|
||||
return apiRequest<{ item?: Record<string, unknown> }>('/api/platform-admin/subscriptions', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user