feat: 初始化项目结构并添加核心功能模块
- 新增文档模板和导航结构 - 实现服务器基础API路由和控制器 - 添加扩展插件配置和前端框架 - 引入多租户和权限管理模块 - 集成日志和数据库配置 - 添加核心业务模型和类型定义
This commit is contained in:
275
server/src/services/AuditService.ts
Normal file
275
server/src/services/AuditService.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { Queue } from 'bullmq';
|
||||
import db from '../config/database';
|
||||
import { logger } from '../utils/logger';
|
||||
import { DomainEventBus, DomainEvent } from '../core/runtime/DomainEventBus';
|
||||
|
||||
export interface AuditLogEntry {
|
||||
tenantId: string;
|
||||
shopId?: string;
|
||||
taskId?: string;
|
||||
traceId: string;
|
||||
userId: string;
|
||||
roleCode?: string;
|
||||
module: string;
|
||||
action: string;
|
||||
resourceType: string;
|
||||
resourceId?: string;
|
||||
beforeSnapshot?: any;
|
||||
afterSnapshot?: any;
|
||||
result: 'success' | 'failed';
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
clientIp?: string;
|
||||
userAgent?: string;
|
||||
source: 'console' | 'extension' | 'node';
|
||||
metadata?: Record<string, any>;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 跨境风控审计内核 (CORE_DEV_04 / BIZ_GOV_20)
|
||||
* 实现基于 BullMQ 的异步审计日志系统,并对接 DomainEventBus 实现全量业务事件审计。
|
||||
*/
|
||||
export class AuditService {
|
||||
/**
|
||||
* 初始化数据库表并启动事件监听 (BIZ_GOV_20)
|
||||
*/
|
||||
static async initTable() {
|
||||
// 启动全量事件监听 (BIZ_GOV_20)
|
||||
this.startEventListener();
|
||||
|
||||
// 1. Audit Log 表
|
||||
const hasAuditLogTable = await db.schema.hasTable('cf_audit_log');
|
||||
if (!hasAuditLogTable) {
|
||||
console.log('📦 Creating cf_audit_log table...');
|
||||
await db.schema.createTable('cf_audit_log', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('user_id', 100);
|
||||
table.string('action', 100).notNullable();
|
||||
table.string('target_type', 100).notNullable();
|
||||
table.string('target_id', 100).notNullable();
|
||||
table.string('tenant_id', 100).index();
|
||||
table.string('shop_id', 100).index();
|
||||
table.string('trace_id', 100).index();
|
||||
table.text('old_data');
|
||||
table.text('new_data');
|
||||
table.text('metadata');
|
||||
table.timestamp('created_at').defaultTo(db.fn.now());
|
||||
table.index(['action', 'target_type', 'target_id'], 'idx_audit_action_target');
|
||||
table.index(['tenant_id', 'shop_id'], 'idx_audit_tenant_shop');
|
||||
});
|
||||
console.log('✅ Table cf_audit_log created');
|
||||
}
|
||||
|
||||
// 2. Operation Log 表
|
||||
const hasOpLogTable = await db.schema.hasTable('cf_operation_log');
|
||||
if (!hasOpLogTable) {
|
||||
console.log('📦 Creating cf_operation_log table...');
|
||||
await db.schema.createTable('cf_operation_log', (table) => {
|
||||
table.bigIncrements('id').primary();
|
||||
table.string('tenant_id', 64).notNullable();
|
||||
table.string('shop_id', 64);
|
||||
table.string('task_id', 64);
|
||||
table.string('trace_id', 128).notNullable();
|
||||
table.string('user_id', 64).notNullable();
|
||||
table.string('role_code', 32);
|
||||
table.string('module', 64).notNullable();
|
||||
table.string('action', 64).notNullable();
|
||||
table.string('resource_type', 64).notNullable();
|
||||
table.string('resource_id', 128);
|
||||
table.json('before_snapshot');
|
||||
table.json('after_snapshot');
|
||||
table.string('result', 16).notNullable();
|
||||
table.string('error_code', 64);
|
||||
table.string('error_message', 512);
|
||||
table.string('client_ip', 64);
|
||||
table.string('user_agent', 512);
|
||||
table.string('source', 16).notNullable();
|
||||
table.timestamp('created_at').defaultTo(db.fn.now());
|
||||
|
||||
table.index(['tenant_id', 'created_at'], 'idx_oplog_tenant_time');
|
||||
table.index(['trace_id'], 'idx_oplog_trace');
|
||||
table.index(['user_id', 'created_at'], 'idx_oplog_user_time');
|
||||
table.index(['task_id'], 'idx_oplog_task');
|
||||
table.index(['module', 'action', 'created_at'], 'idx_oplog_module_action_time');
|
||||
});
|
||||
console.log('✅ Table cf_operation_log created');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听 DomainEventBus 上的所有事件并转化为审计日志
|
||||
*/
|
||||
private static startEventListener() {
|
||||
logger.info('[AuditService] Starting DomainEventBus listener for full-pipeline auditing...');
|
||||
|
||||
DomainEventBus.getInstance().subscribeAll(async (event: DomainEvent) => {
|
||||
try {
|
||||
await this.log({
|
||||
tenantId: event.tenantId,
|
||||
traceId: event.traceId || `bus-${Date.now()}`,
|
||||
userId: event.userId || 'EVENT_BUS',
|
||||
module: event.module,
|
||||
action: event.action,
|
||||
resourceType: event.resourceType,
|
||||
resourceId: event.resourceId,
|
||||
afterSnapshot: event.data,
|
||||
result: 'success',
|
||||
source: 'node',
|
||||
metadata: { eventTimestamp: event.timestamp }
|
||||
});
|
||||
} catch (err: any) {
|
||||
logger.error(`[AuditService] Failed to process bus event: ${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static auditQueue = new Queue('audit-log', {
|
||||
connection: {
|
||||
host: process.env.REDIS_HOST || '127.0.0.1',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* [ERP_MST_02] 记录配置项变更审计 (Granular Config Audit)
|
||||
* @description 针对 ERP 核心配置(如税码、财务科目、费率模板)的颗粒度变更进行审计溯源。
|
||||
*/
|
||||
static async logConfigChange(params: {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
traceId: string;
|
||||
configKey: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
changeReason?: string;
|
||||
metadata?: any;
|
||||
}) {
|
||||
logger.info(`[AuditService] Recording config change for ${params.configKey} (Tenant: ${params.tenantId})`);
|
||||
|
||||
await this.log({
|
||||
tenantId: params.tenantId,
|
||||
userId: params.userId,
|
||||
traceId: params.traceId,
|
||||
module: 'ERP_CONFIG',
|
||||
action: 'CONFIG_CHANGE',
|
||||
resourceType: 'SYSTEM_CONFIG',
|
||||
resourceId: params.configKey,
|
||||
beforeSnapshot: params.oldValue,
|
||||
afterSnapshot: params.newValue,
|
||||
result: 'success',
|
||||
source: 'node',
|
||||
metadata: {
|
||||
...params.metadata,
|
||||
changeReason: params.changeReason
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 异步记录审计日志
|
||||
*/
|
||||
static async log(entry: Omit<AuditLogEntry, 'timestamp'>) {
|
||||
try {
|
||||
const fullEntry: AuditLogEntry = {
|
||||
...entry,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// 1. 自动敏感数据脱敏
|
||||
this.maskSensitiveData(fullEntry);
|
||||
|
||||
// 2. 加入队列异步处理
|
||||
await this.auditQueue.add('log', fullEntry, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 1000,
|
||||
});
|
||||
|
||||
logger.info(`[Audit] Job added to queue: ${entry.action} on ${entry.resourceType}:${entry.resourceId}`);
|
||||
} catch (err: any) {
|
||||
logger.error(`[Audit] Failed to queue audit log: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 系统后台任务审计日志 (无 Request 上下文)
|
||||
*/
|
||||
static async logSystem(params: {
|
||||
action: string;
|
||||
resourceType: string;
|
||||
resourceId: string;
|
||||
beforeSnapshot?: any;
|
||||
afterSnapshot?: any;
|
||||
metadata?: any;
|
||||
}) {
|
||||
await this.log({
|
||||
tenantId: 'SYSTEM',
|
||||
traceId: `sys-${Date.now()}`,
|
||||
userId: 'SYSTEM_BOT',
|
||||
module: 'SYSTEM',
|
||||
source: 'node',
|
||||
result: 'success',
|
||||
...params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 对比两个对象并记录差异
|
||||
*/
|
||||
static async logDiff(
|
||||
params: {
|
||||
tenantId: string;
|
||||
shopId?: string;
|
||||
taskId?: string;
|
||||
traceId: string;
|
||||
userId: string;
|
||||
module: string;
|
||||
action: string;
|
||||
resourceType: string;
|
||||
resourceId: string;
|
||||
beforeSnapshot: any;
|
||||
afterSnapshot: any;
|
||||
source: 'console' | 'extension' | 'node';
|
||||
}
|
||||
) {
|
||||
const { beforeSnapshot, afterSnapshot, ...rest } = params;
|
||||
const diff = this.getDiff(beforeSnapshot, afterSnapshot);
|
||||
if (Object.keys(diff.changed).length === 0) return;
|
||||
|
||||
await this.log({
|
||||
...rest,
|
||||
beforeSnapshot: diff.old,
|
||||
afterSnapshot: diff.new,
|
||||
result: 'success',
|
||||
metadata: { diffKeys: Object.keys(diff.changed) }
|
||||
});
|
||||
}
|
||||
|
||||
private static maskSensitiveData(entry: any) {
|
||||
const sensitiveKeys = ['password', 'token', 'secret', 'costPrice', 'apiKey', 'encryptedData', 'iv', 'tag'];
|
||||
const mask = (obj: any) => {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
for (const key in obj) {
|
||||
if (sensitiveKeys.some(sk => key.toLowerCase().includes(sk.toLowerCase()))) {
|
||||
obj[key] = '***MASKED***';
|
||||
} else if (typeof obj[key] === 'object') {
|
||||
mask(obj[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
mask(entry.beforeSnapshot);
|
||||
mask(entry.afterSnapshot);
|
||||
}
|
||||
|
||||
private static getDiff(oldData: any, newData: any) {
|
||||
const diff: { old: any; new: any; changed: any } = { old: {}, new: {}, changed: {} };
|
||||
for (const key in newData) {
|
||||
if (JSON.stringify(oldData[key]) !== JSON.stringify(newData[key])) {
|
||||
diff.old[key] = oldData[key];
|
||||
diff.new[key] = newData[key];
|
||||
diff.changed[key] = true;
|
||||
}
|
||||
}
|
||||
return diff;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user