XuqmGroup-Web/tenant-platform/src/api/system.ts
XuqmGroup 4b13f64966 feat(log): 优化操作日志记录和展示功能
- 在OperationLogEntity实体中新增summary和ipAddress字段存储摘要和IP信息
- 修改operationLogService.record方法支持传入操作摘要信息
- 实现客户端IP地址解析功能,支持X-Forwarded-For和X-Real-IP头
- 更新系统更新服务中的数据库表结构迁移逻辑,增加NOT NULL列处理
- 优化前端操作日志页面展示,添加标签分类和详情弹窗功能
- 在系统更新流式响应中增加网络连接异常处理机制
- 添加Nginx代理配置中的缓冲区设置以支持实时日志流式传输
2026-05-27 12:27:43 +08:00

153 行
4.8 KiB
TypeScript

const BASE = import.meta.env.VITE_API_BASE_URL ?? '/api'
export interface DeploymentStatus {
mode: 'PUBLIC' | 'PRIVATE'
tenantRegisterEnabled: boolean
services: Record<string, { enabled: boolean; baseUrl: string | null }>
}
export async function getDeploymentStatus(): Promise<DeploymentStatus> {
const res = await fetch(`${BASE}/private/deployment/status`)
const json = await res.json()
return json.data as DeploymentStatus
}
async function streamOperation(
path: string,
onLine: (line: string) => void,
signal?: AbortSignal,
): Promise<void> {
const token = localStorage.getItem('token') ?? ''
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
signal,
})
if (!res.ok) {
const text = await res.text()
throw new Error(text || `HTTP ${res.status}`)
}
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buf = ''
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
buf += decoder.decode(value, { stream: true })
const lines = buf.split('\n')
buf = lines.pop() ?? ''
for (const line of lines) {
onLine(line)
}
}
if (buf) onLine(buf)
} catch (e: any) {
// Connection drop during RESTART_SELF is expected - don't throw
if (e?.name === 'TypeError' || e?.message?.includes('network') || e?.message?.includes('fetch')) {
onLine('>>> 连接已中断(服务正在重启)')
return
}
throw e
}
}
export async function getRunningServices(): Promise<string[]> {
const token = localStorage.getItem('token') ?? ''
const res = await fetch(`${BASE}/system/services`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
return json.data as string[]
}
export async function fetchServiceLogs(service: string, lines: number): Promise<string> {
const token = localStorage.getItem('token') ?? ''
const res = await fetch(
`${BASE}/system/logs/${encodeURIComponent(service)}?lines=${lines}`,
{ headers: { Authorization: `Bearer ${token}` } },
)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.text()
}
export async function getSystemVersion(): Promise<{ currentVersion: string }> {
const token = localStorage.getItem('token') ?? ''
const res = await fetch(`${BASE}/system/version`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
return json.data as { currentVersion: string }
}
export function streamSystemUpdate(onLine: (line: string) => void, signal?: AbortSignal) {
return streamOperation('/system/update', onLine, signal)
}
export function streamSystemReset(onLine: (line: string) => void, signal?: AbortSignal) {
return streamOperation('/system/reset', onLine, signal)
}
// ── Database Management (PRIVATE mode only) ──────────────────────────────
export interface TableInfo {
name: string
comment: string
}
export interface ColumnInfo {
name: string
type: string
size: number
nullable: boolean
comment: string
}
export interface TableDataPage {
columns: string[]
rows: Record<string, any>[]
total: number
totalPages: number
page: number
size: number
}
async function authFetch(path: string): Promise<any> {
const token = localStorage.getItem('token') ?? ''
const res = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) {
const json = await res.json().catch(() => null)
throw new Error(json?.message ?? `HTTP ${res.status}`)
}
return res.json()
}
export async function listDatabaseTables(): Promise<TableInfo[]> {
const json = await authFetch('/system/database/tables')
return json.data as TableInfo[]
}
export async function getTableColumns(tableName: string): Promise<ColumnInfo[]> {
const json = await authFetch(`/system/database/tables/${encodeURIComponent(tableName)}/columns`)
return json.data as ColumnInfo[]
}
export async function getTableData(
tableName: string,
params: { page?: number; size?: number; keyword?: string; sortColumn?: string; sortDirection?: string },
): Promise<TableDataPage> {
const query = new URLSearchParams()
if (params.page != null) query.set('page', String(params.page))
if (params.size != null) query.set('size', String(params.size))
if (params.keyword) query.set('keyword', params.keyword)
if (params.sortColumn) query.set('sortColumn', params.sortColumn)
if (params.sortDirection) query.set('sortDirection', params.sortDirection)
const qs = query.toString()
const json = await authFetch(`/system/database/tables/${encodeURIComponent(tableName)}/data${qs ? '?' + qs : ''}`)
return json.data as TableDataPage
}