日志管理增强:登录补全角色、IP兜底、记录业务对象与决策

- 登录日志从用户表补全 actorId/actorName/role
- IP 兜底:X-Forwarded-For/X-Real-IP → 否则用连接信息(getConnInfo)
- 业务动作记录目标项目名(target_name)与决策(风控审核·通过/管理层审批·驳回/红线裁定·命中)
- system_logs 增加 target_name 列;前端日志页显示项目名
This commit is contained in:
freedakgmail
2026-06-14 09:02:05 +08:00
parent 1a37daea68
commit a9c41bba57
5 changed files with 65 additions and 18 deletions
+6 -4
View File
@@ -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<void> {
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,
+44 -13
View File
@@ -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<string, string> = { 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' }));