From a9c41bba5799f15baffc6c4fe00caba8a69180ca Mon Sep 17 00:00:00 2001 From: freedakgmail Date: Sun, 14 Jun 2026 09:02:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=AE=A1=E7=90=86=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=EF=BC=9A=E7=99=BB=E5=BD=95=E8=A1=A5=E5=85=A8=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E3=80=81IP=E5=85=9C=E5=BA=95=E3=80=81=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E4=B8=9A=E5=8A=A1=E5=AF=B9=E8=B1=A1=E4=B8=8E=E5=86=B3?= =?UTF-8?q?=E7=AD=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 登录日志从用户表补全 actorId/actorName/role - IP 兜底:X-Forwarded-For/X-Real-IP → 否则用连接信息(getConnInfo) - 业务动作记录目标项目名(target_name)与决策(风控审核·通过/管理层审批·驳回/红线裁定·命中) - system_logs 增加 target_name 列;前端日志页显示项目名 --- .../1730000028000_system_logs_target_name.cjs | 12 ++++ src/persistence/systemLogs.ts | 10 ++-- src/server/index.ts | 57 ++++++++++++++----- web/src/api/client.ts | 1 + web/src/pages/SystemLogs.tsx | 3 +- 5 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 migrations/1730000028000_system_logs_target_name.cjs diff --git a/migrations/1730000028000_system_logs_target_name.cjs b/migrations/1730000028000_system_logs_target_name.cjs new file mode 100644 index 0000000..439f504 --- /dev/null +++ b/migrations/1730000028000_system_logs_target_name.cjs @@ -0,0 +1,12 @@ +/* eslint-disable */ +/** + * 系统日志增加 target_name(业务对象名称,如项目名),让审计可读:"谁 对 哪个项目 做了什么"。 + */ + +exports.up = (pgm) => { + pgm.sql(`ALTER TABLE system_logs ADD COLUMN IF NOT EXISTS target_name TEXT;`); +}; + +exports.down = (pgm) => { + pgm.sql(`ALTER TABLE system_logs DROP COLUMN IF EXISTS target_name;`); +}; diff --git a/src/persistence/systemLogs.ts b/src/persistence/systemLogs.ts index d791010..0102818 100644 --- a/src/persistence/systemLogs.ts +++ b/src/persistence/systemLogs.ts @@ -11,6 +11,7 @@ export interface SystemLogEntry { method: string; path: string; targetId: string | null; + targetName?: string | null; status: number | null; success: boolean | null; durationMs: number | null; @@ -27,10 +28,10 @@ export interface SystemLogRow extends SystemLogEntry { /** 写入一条系统审计日志。 */ export async function insertSystemLog(pool: pg.Pool, e: SystemLogEntry): Promise { await pool.query( - `INSERT INTO system_logs(actor_id, actor_name, role, action, method, path, target_id, status, success, duration_ms, ip, query, detail) - VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`, + `INSERT INTO system_logs(actor_id, actor_name, role, action, method, path, target_id, target_name, status, success, duration_ms, ip, query, detail) + VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, [ - e.actorId, e.actorName, e.role, e.action, e.method, e.path, e.targetId, + e.actorId, e.actorName, e.role, e.action, e.method, e.path, e.targetId, e.targetName ?? null, e.status, e.success, e.durationMs, e.ip, e.query, e.detail !== undefined ? JSON.stringify(e.detail) : null, ], @@ -72,7 +73,7 @@ export async function querySystemLogs( const dataParams = [...params, opts.limit, opts.offset]; const res = await pool.query( - `SELECT id, ts, actor_id, actor_name, role, action, method, path, target_id, status, success, duration_ms, ip, query, detail + `SELECT id, ts, actor_id, actor_name, role, action, method, path, target_id, target_name, status, success, duration_ms, ip, query, detail FROM system_logs ${whereSql} ORDER BY id DESC LIMIT $${dataParams.length - 1} OFFSET $${dataParams.length}`, @@ -88,6 +89,7 @@ export async function querySystemLogs( method: String(r.method), path: String(r.path), targetId: r.target_id != null ? String(r.target_id) : null, + targetName: r.target_name != null ? String(r.target_name) : null, status: r.status != null ? Number(r.status) : null, success: r.success != null ? Boolean(r.success) : null, durationMs: r.duration_ms != null ? Number(r.duration_ms) : null, diff --git a/src/server/index.ts b/src/server/index.ts index 0792225..83960e9 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,6 +8,7 @@ import { Hono } from 'hono'; import { cors } from 'hono/cors'; import { logger } from 'hono/logger'; +import { getConnInfo } from '@hono/node-server/conninfo'; import { classify, isBusinessType, @@ -199,31 +200,61 @@ app.use('/api/*', async (c, next) => { const path = c.req.path; if (path === '/api/system-logs') return; // 不记录查询日志自身 const payload = (c as import('hono').Context).get('user') as AuthPayload | undefined; - const actorId = payload?.uid ?? null; + let actorId = payload?.uid ?? null; let actorName = payload?.username ?? null; - const role = payload?.role ?? null; - // 登录成功:从响应体补充操作人(此时尚无 JWT)。 - if (path === '/api/auth/login' && actorName === null) { - actorName = (await c.req.json<{ username?: string }>().catch(() => ({} as { username?: string }))).username ?? null; - } + let role = payload?.role ?? null; const status = c.res.status; - const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.header('x-real-ip') ?? null; + + // 登录:此时无 JWT,从请求体用户名补全操作人与角色(成功时)。 + if (path === '/api/auth/login') { + const lb = await c.req.json<{ username?: string }>().catch(() => ({} as { username?: string })); + if (lb.username) { + actorName = lb.username; + const u = await getUserByUsername(pool, lb.username).catch(() => null); + if (u) { actorId = u.id; role = u.role; } + } + } + + // 业务动作增强:解析目标项目名 + 决策(通过/驳回等)。 + let action = deriveActionLabel(method, path); + let targetName: string | null = null; + const targetId = deriveTargetId(path); + const isAssessmentPath = /^\/api\/assessments\/[^/]+/.test(path) && targetId !== null && targetId !== 'run' && targetId !== 'profitability' && targetId !== 'summary' && targetId !== 'expiring' && targetId !== 'overdue'; + if (isAssessmentPath && targetId) { + const rec = store.get(targetId); + const desc = rec ? String((rec.assessment as unknown as { projectDescription?: string }).projectDescription ?? '') : ''; + const m = desc.match(/【项目】([^||\n]+)/); + targetName = m?.[1]?.trim() ?? null; + } + // 含决策的动作:读已缓存的请求体补充"通过/驳回/放弃"。 + if (/\/(review|approve)$/.test(path)) { + const b = await c.req.json<{ action?: string }>().catch(() => ({} as { action?: string })); + const map: Record = { approve: '通过', reject: '驳回', abandon: '放弃' }; + if (b.action && map[b.action]) action = `${action}·${map[b.action]}`; + } else if (/\/redline-verdict$/.test(path)) { + const b = await c.req.json<{ status?: string }>().catch(() => ({} as { status?: string })); + if (b.status) action = `${action}·${b.status}`; + } + + const ip = c.req.header('x-forwarded-for')?.split(',')[0]?.trim() + ?? c.req.header('x-real-ip') + ?? (() => { try { return getConnInfo(c).remote.address ?? null; } catch { return null; } })(); const queryStr = (() => { const i = c.req.url.indexOf('?'); return i >= 0 ? c.req.url.slice(i + 1) : null; })(); - const entry = { + void insertSystemLog(pool, { actorId, actorName, role, - action: deriveActionLabel(method, path), + action, method, path, - targetId: deriveTargetId(path), + targetId, + targetName, status, success: status < 400, durationMs: Date.now() - start, - ip, + ip: ip ?? null, query: queryStr, - }; - void insertSystemLog(pool, entry).catch(() => undefined); + }).catch(() => undefined); }); app.get('/api/health', (c) => c.json({ status: 'ok', version: '0.1.0' })); diff --git a/web/src/api/client.ts b/web/src/api/client.ts index f9991a7..ef78e06 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -993,6 +993,7 @@ export interface SystemLogItem { method: string; path: string; targetId: string | null; + targetName: string | null; status: number | null; success: boolean | null; durationMs: number | null; diff --git a/web/src/pages/SystemLogs.tsx b/web/src/pages/SystemLogs.tsx index d746c8e..008c75c 100644 --- a/web/src/pages/SystemLogs.tsx +++ b/web/src/pages/SystemLogs.tsx @@ -116,7 +116,8 @@ export function SystemLogs(): JSX.Element { {it.action} {it.method} - {it.targetId && {it.targetId}} + {it.targetName &&
{it.targetName}
} + {it.targetId && !it.targetName && {it.targetId}}
{it.path}