From 4b13f6496634eacb813f99fb3e539840551cc725 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Wed, 27 May 2026 12:27:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(log):=20=E4=BC=98=E5=8C=96=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=E5=92=8C=E5=B1=95?= =?UTF-8?q?=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在OperationLogEntity实体中新增summary和ipAddress字段存储摘要和IP信息 - 修改operationLogService.record方法支持传入操作摘要信息 - 实现客户端IP地址解析功能,支持X-Forwarded-For和X-Real-IP头 - 更新系统更新服务中的数据库表结构迁移逻辑,增加NOT NULL列处理 - 优化前端操作日志页面展示,添加标签分类和详情弹窗功能 - 在系统更新流式响应中增加网络连接异常处理机制 - 添加Nginx代理配置中的缓冲区设置以支持实时日志流式传输 --- ops-platform/src/api/ops.ts | 2 + .../src/views/logs/OperationLogView.vue | 78 ++++-- tenant-platform/src/api/operationLog.ts | 2 + tenant-platform/src/api/system.ts | 27 ++- .../src/views/logs/OperationLogView.vue | 226 ++++++++++++++++-- .../src/views/security/SecurityCenterView.vue | 43 +++- 6 files changed, 315 insertions(+), 63 deletions(-) diff --git a/ops-platform/src/api/ops.ts b/ops-platform/src/api/ops.ts index fa94929..05ea286 100644 --- a/ops-platform/src/api/ops.ts +++ b/ops-platform/src/api/ops.ts @@ -120,6 +120,8 @@ export interface OpsLogItem { action: string operator: string detailJson: string + summary?: string + ipAddress?: string createdAt: string } diff --git a/ops-platform/src/views/logs/OperationLogView.vue b/ops-platform/src/views/logs/OperationLogView.vue index a2c374d..d5a8b81 100644 --- a/ops-platform/src/views/logs/OperationLogView.vue +++ b/ops-platform/src/views/logs/OperationLogView.vue @@ -6,22 +6,44 @@ - + - + - - - - - + + + + + + + + @@ -43,14 +65,30 @@ const page = ref(0) const size = ref(20) const total = ref(0) -function formatDetail(json: string) { - if (!json) return '-' +function moduleTagType(m: string): '' | 'success' | 'warning' | 'info' | 'danger' { + return { CONSOLE: 'info', APP: '', SUB_ACCOUNT: 'warning', SERVICE: 'success', APP_SECRET: 'danger', EMAIL_VERIFY: 'info' }[m] as any ?? '' +} + +function actionTagType(a: string): '' | 'success' | 'warning' | 'info' | 'danger' { + if (a.startsWith('CREATE')) return 'success' + if (a.startsWith('DELETE') || a.startsWith('DISABLE')) return 'danger' + if (a.startsWith('RESET') || a.startsWith('REGENERATE')) return 'warning' + if (a.startsWith('VIEW')) return 'info' + return '' +} + +function hasDetail(json: string) { + if (!json) return false + try { const o = JSON.parse(json); return o && typeof o === 'object' && Object.keys(o).length > 0 } catch { return false } +} + +function parseDetail(json: string): { key: string; value: string }[] { + if (!json) return [] try { - const obj = JSON.parse(json) - return Object.entries(obj).map(([k, v]) => `${k}: ${v}`).join(', ') - } catch { - return json - } + const o = JSON.parse(json) + if (!o || typeof o !== 'object') return [] + return Object.entries(o).map(([k, v]) => ({ key: k, value: typeof v === 'object' ? JSON.stringify(v) : String(v ?? '') })) + } catch { return [{ key: 'raw', value: json }] } } async function loadLogs() { @@ -68,11 +106,3 @@ async function loadLogs() { onMounted(loadLogs) - - diff --git a/tenant-platform/src/api/operationLog.ts b/tenant-platform/src/api/operationLog.ts index 0cc1303..551f670 100644 --- a/tenant-platform/src/api/operationLog.ts +++ b/tenant-platform/src/api/operationLog.ts @@ -9,6 +9,8 @@ export interface TenantOperationLog { action: string operator?: string detailJson?: string + summary?: string + ipAddress?: string createdAt: string } diff --git a/tenant-platform/src/api/system.ts b/tenant-platform/src/api/system.ts index 0f62575..8adb803 100644 --- a/tenant-platform/src/api/system.ts +++ b/tenant-platform/src/api/system.ts @@ -30,17 +30,26 @@ async function streamOperation( const reader = res.body!.getReader() const decoder = new TextDecoder() let buf = '' - 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) + 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 } - if (buf) onLine(buf) } export async function getRunningServices(): Promise { diff --git a/tenant-platform/src/views/logs/OperationLogView.vue b/tenant-platform/src/views/logs/OperationLogView.vue index f227f00..d775271 100644 --- a/tenant-platform/src/views/logs/OperationLogView.vue +++ b/tenant-platform/src/views/logs/OperationLogView.vue @@ -26,21 +26,62 @@
- - - - + - - - - - + + + + + + + + + + + @@ -78,7 +119,7 @@
- + @@ -153,7 +194,7 @@ async function loadApps() { updateAppKey.value = apps.value[0].appKey } } catch { - // ignore; empty state will be shown in the selector + // ignore } } @@ -201,17 +242,109 @@ function handleUpdateAppChange() { loadUpdateLogs() } +// ── Label mappings ────────────────────────────────────────────────────────── + function tenantModuleLabel(moduleType: string) { return { CONSOLE: '控制台', APP: '应用管理', - SUB_ACCOUNT: '子账号管理', + SUB_ACCOUNT: '子账号', SERVICE: '服务管理', - APP_SECRET: '应用密钥', + APP_SECRET: '密钥管理', EMAIL_VERIFY: '邮箱验证', }[moduleType] ?? moduleType } +function moduleTagType(moduleType: string): '' | 'success' | 'warning' | 'info' | 'danger' { + return { + CONSOLE: 'info', + APP: '', + SUB_ACCOUNT: 'warning', + SERVICE: 'success', + APP_SECRET: 'danger', + EMAIL_VERIFY: 'info', + }[moduleType] as any ?? '' +} + +function actionLabel(action: string): string { + return { + CREATE_APP: '创建', + UPDATE_APP: '编辑', + DELETE_APP: '删除', + RESET_APP_SECRET: '重置密钥', + REQUEST_SECRET_VERIFY: '验证', + REVEAL_APP_SECRET: '查看密钥', + CREATE_SUB_ACCOUNT: '创建', + DISABLE_SUB_ACCOUNT: '禁用', + SEND_VERIFY_CODE: '发送验证码', + VERIFY_EMAIL: '验证邮箱', + DISABLE_SERVICE: '停用', + UPDATE_SERVICE_CONFIG: '更新配置', + REQUEST_SERVICE_ACTIVATION: '申请开通', + REGENERATE_KEY: '重新生成密钥', + VIEW_DASHBOARD: '查看', + }[action] ?? action +} + +function actionTagType(action: string): '' | 'success' | 'warning' | 'info' | 'danger' { + if (action.startsWith('CREATE')) return 'success' + if (action.startsWith('DELETE')) return 'danger' + if (action.startsWith('DISABLE')) return 'danger' + if (action.startsWith('RESET') || action.startsWith('REGENERATE')) return 'warning' + if (action.startsWith('UPDATE') || action.startsWith('REVEAL')) return '' + if (action.startsWith('VIEW')) return 'info' + if (action.startsWith('SEND') || action.startsWith('VERIFY') || action.startsWith('REQUEST')) return 'info' + return '' +} + +function generateSummary(row: TenantOperationLog): string { + const action = actionLabel(row.action) + const resource = row.resourceId || '' + return `${action} ${resource}` +} + +function hasDetail(detailJson?: string): boolean { + if (!detailJson) return false + try { + const parsed = JSON.parse(detailJson) + return parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0 + } catch { + return false + } +} + +const DETAIL_KEY_LABELS: Record = { + name: '应用名称', + packageName: '包名', + appKey: 'AppKey', + username: '用户名', + nickname: '昵称', + email: '邮箱', + platform: '平台', + serviceType: '服务类型', + purpose: '用途', + applyReason: '申请原因', + before: '变更前', + after: '变更后', + appCount: '应用数', + serviceCount: '服务数', + subAccountCount: '子账号数', +} + +function parseDetail(detailJson?: string): { key: string; value: string }[] { + if (!detailJson) return [] + try { + const parsed = JSON.parse(detailJson) + if (!parsed || typeof parsed !== 'object') return [] + return Object.entries(parsed).map(([k, v]) => ({ + key: DETAIL_KEY_LABELS[k] ?? k, + value: typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v ?? ''), + })) + } catch { + return [{ key: '原始数据', value: detailJson }] + } +} + function updateResourceLabel(resourceType: string) { return { APP_VERSION: '应用版本', @@ -255,7 +388,6 @@ function formatDetail(detailJson?: string) { return detailJson } } -