import { CallHandler, ExecutionContext, Injectable, NestInterceptor, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { Observable, catchError, tap, throwError } from 'rxjs'; import type { AuthUser } from '../guards/jwt-auth.guard'; import type { RequestWithId } from '../logging/request-id.middleware'; import { HQ_OPERATION_KEY, type HqOperationMeta } from './hq-operation.decorator'; import { HqOperationLogService } from './hq-operation-log.service'; function pickRefId(value: unknown): bigint | null { if (value == null || value === '') return null; try { return BigInt(String(value)); } catch { return null; } } function sanitizeBody(body: unknown): unknown { if (!body || typeof body !== 'object') return body; const copy = { ...(body as Record) }; for (const key of Object.keys(copy)) { if (/password|secret|token/i.test(key)) { copy[key] = '***'; } } return copy; } function summarizeResponse(data: unknown): unknown { if (data == null) return null; if (typeof data !== 'object') return data; const obj = data as Record; const summary: Record = {}; for (const key of ['id', 'ok', 'deleted', 'orderNo', 'userNo', 'redeemNo', 'message', 'status']) { if (obj[key] !== undefined) summary[key] = obj[key]; } if (Array.isArray(obj.items)) { summary.itemCount = obj.items.length; } if (Array.isArray(obj.orderNos)) { summary.orderNos = obj.orderNos; } return Object.keys(summary).length ? summary : obj; } @Injectable() export class HqOperationInterceptor implements NestInterceptor { constructor( private readonly reflector: Reflector, private readonly logService: HqOperationLogService, ) {} intercept(context: ExecutionContext, next: CallHandler): Observable { const meta = this.reflector.get( HQ_OPERATION_KEY, context.getHandler(), ); if (!meta) return next.handle(); const req = context.switchToHttp().getRequest(); const user = req.user as AuthUser | undefined; if (!user || user.actorType !== 'HQ') { return next.handle(); } const writeLog = (status: 'SUCCESS' | 'FAILED', data?: unknown, errorMessage?: string) => { const refId = meta.batch ? 0n : pickRefId(meta.refIdParam ? req.params?.[meta.refIdParam] : null) ?? pickRefId( meta.refIdField ? (data as Record | null)?.[meta.refIdField] : (data as Record | null)?.id, ) ?? 0n; const detail: Record = { method: req.method, path: req.originalUrl ?? req.url, requestId: req.requestId, }; if (meta.includeBody && req.body) { detail.requestBody = sanitizeBody(req.body); } if (status === 'SUCCESS' && meta.includeResponse !== false && data != null) { detail.response = summarizeResponse(data); } if (errorMessage) detail.error = errorMessage; this.logService.logSafe({ hqAccountId: user.actorId, action: meta.action, refType: meta.refType, refId, status, detail, }); }; return next.handle().pipe( tap((data) => writeLog('SUCCESS', data)), catchError((err: { message?: string }) => { writeLog('FAILED', undefined, err?.message ?? '操作失败'); return throwError(() => err); }), ); } }