Parcourir la source

✨ feat(logs): implement structured operation log system

- add OperationLog entity with complete data model for detailed operation tracking
- create OperationLogService with methods for logging and querying operations
- implement enhanced permission middleware with automatic log recording
- add API routes for operation log management
- integrate new entity into data source configuration

📝 docs(logs): add structured operation log documentation

- document system architecture, components and implementation steps
- provide data structure examples and query capabilities
- include security considerations and performance optimization strategies
- add deployment checklist and future iteration plans
yourname il y a 7 mois
Parent
commit
c36e12ec4d

+ 217 - 0
docs/结构化操作日志扩展方案.md

@@ -0,0 +1,217 @@
+# 结构化操作日志扩展方案
+
+## 📋 项目概述
+
+基于现有日志系统,创建一个新的结构化操作日志系统,提供更详细、可查询的操作追踪能力。
+
+## 🎯 核心目标
+
+1. **结构化的数据存储**:支持JSON格式存储详细操作数据
+2. **完整的操作上下文**:记录请求、响应、用户信息、网络环境
+3. **性能监控**:记录操作耗时和错误详情
+4. **数据对比**:支持操作前后数据对比
+5. **向后兼容**:保持现有Logfile系统不变
+
+## 🏗️ 技术架构
+
+### 数据模型设计
+
+#### OperationLog实体结构
+```typescript
+- id: number (主键)
+- requestId: string (请求唯一标识)
+- userId: number (操作用户ID)
+- username: string (用户名)
+- resourceType: string (资源类型)
+- resourceId: string (资源ID)
+- action: string (操作动作)
+- method: string (HTTP方法)
+- endpoint: string (API端点)
+- ipAddress: string (IP地址)
+- userAgent: string (用户代理)
+- requestData: JSON (请求数据)
+- responseData: JSON (响应数据)
+- beforeData: JSON (操作前数据)
+- afterData: JSON (操作后数据)
+- duration: number (耗时ms)
+- status: enum (成功/失败/权限拒绝)
+- errorMessage: text (错误信息)
+- stackTrace: text (错误堆栈)
+- createdAt: timestamp (创建时间)
+```
+
+### 系统组件
+
+1. **OperationLog实体** (`src/server/modules/logs/operation-log.entity.ts`)
+2. **OperationLog服务** (`src/server/modules/logs/operation-log.service.ts`)
+3. **增强日志中间件** (`src/server/middleware/operation-log.middleware.ts`)
+4. **数据库迁移脚本** (`src/server/migrations/CreateOperationLogTable.ts`)
+5. **API路由** (`src/server/api/operation-logs/index.ts`)
+
+## 📁 文件结构
+
+```
+src/server/modules/logs/
+├── operation-log.entity.ts     # 操作日志实体
+├── operation-log.service.ts    # 操作日志服务
+├── operation-log.dto.ts        # 请求/响应DTO
+└── operation-log.middleware.ts # 增强中间件
+
+src/server/api/operation-logs/
+├── index.ts                    # 路由聚合
+├── get.ts                      # 列表查询
+├── [id]/
+│   ├── get.ts                  # 单个详情
+│   └── delete.ts               # 删除日志
+```
+
+## 🚀 实施步骤
+
+### 第一阶段:创建核心实体
+1. 创建OperationLog实体
+2. 创建相关DTO和Schema
+3. 注册到数据源
+
+### 第二阶段:实现服务层
+1. 创建OperationLogService
+2. 实现日志记录方法
+3. 添加数据脱敏功能
+
+### 第三阶段:增强中间件
+1. 创建operationLogMiddleware
+2. 集成到权限系统
+3. 添加性能监控
+
+### 第四阶段:API接口
+1. 创建CRUD路由
+2. 添加筛选和分页
+3. 实现导出功能
+
+## 🔧 数据结构示例
+
+### 日志记录示例
+```json
+{
+  "id": 1,
+  "requestId": "req_20231215120000_abc123",
+  "userId": 1001,
+  "username": "admin",
+  "resourceType": "client",
+  "resourceId": "C20230001",
+  "action": "update",
+  "method": "PUT",
+  "endpoint": "/api/v1/clients/C20230001",
+  "ipAddress": "192.168.1.100",
+  "userAgent": "Mozilla/5.0...",
+  "requestData": {
+    "name": "新客户名称",
+    "phone": "13800138000"
+  },
+  "responseData": {
+    "id": "C20230001",
+    "name": "新客户名称",
+    "updatedAt": "2023-12-15T12:00:00Z"
+  },
+  "beforeData": {
+    "name": "旧客户名称",
+    "phone": "13900139000"
+  },
+  "afterData": {
+    "name": "新客户名称",
+    "phone": "13800138000"
+  },
+  "duration": 150,
+  "status": "success",
+  "createdAt": "2023-12-15T12:00:00Z"
+}
+```
+
+## 📊 查询能力
+
+### 支持的筛选条件
+- 按用户ID/用户名筛选
+- 按资源类型/ID筛选
+- 按操作类型筛选
+- 按时间范围筛选
+- 按状态筛选
+- 按IP地址筛选
+
+### 排序和分页
+- 按时间倒序排序
+- 按操作耗时排序
+- 支持自定义分页大小
+- 支持游标分页
+
+## 🔒 安全考虑
+
+### 数据脱敏
+- 自动过滤敏感字段(password, token, secret等)
+- 支持自定义脱敏规则
+- 记录脱敏日志
+
+### 权限控制
+- 只有管理员角色可查看操作日志
+- 支持基于用户的数据权限控制
+- 审计日志访问记录
+
+## 📈 性能优化
+
+### 数据库优化
+- 为常用查询字段添加索引
+- 使用分区表按时间分片
+- 定期归档旧数据
+
+### 中间件优化
+- 异步写入日志(可选)
+- 批量处理减少数据库压力
+- 缓存热点数据
+
+## 🧪 测试计划
+
+### 单元测试
+- 实体字段验证测试
+- 服务层方法测试
+- 数据脱敏逻辑测试
+
+### 集成测试
+- 中间件链路测试
+- API接口测试
+- 权限控制测试
+
+### 性能测试
+- 高并发日志写入测试
+- 大数据量查询测试
+- 内存使用测试
+
+## 📋 部署清单
+
+### 数据库迁移
+1. 执行创建表SQL
+2. 验证表结构
+3. 添加必要的索引
+
+### 代码部署
+1. 部署新实体和服务
+2. 更新数据源配置
+3. 启用新中间件
+
+### 监控设置
+1. 日志写入监控
+2. 查询性能监控
+3. 错误率监控
+
+## 🔄 后续迭代
+
+### 功能增强
+- 操作日志分析报表
+- 异常操作告警
+- 用户行为画像
+- 合规审计报告
+
+### 技术优化
+- 引入Elasticsearch存储
+- 实现日志聚合分析
+- 支持实时告警
+- 添加数据可视化
+
+这个方案提供了一个完整的结构化操作日志系统,既保持了向后兼容性,又大大增强了日志的功能和可查询性。

+ 3 - 0
src/server/api.ts

@@ -12,6 +12,7 @@ import hetongRoutes from './api/contracts/index'
 import hetongRenewRoutes from './api/contracts-renew/index'
 import linkmanRoutes from './api/contacts/index'
 import logfileRoutes from './api/logs/index'
+import operationLogRoutes from './api/operation-logs/index'
 import orderRecordRoutes from './api/order-records/index'
 import followUpRecordRoutes from './api/follow-up-records/index'
 import departmentsRoute from './api/departments/index'
@@ -77,6 +78,7 @@ const hetongApiRoutes = api.route('/api/v1/contracts', hetongRoutes)
 const hetongRenewApiRoutes = api.route('/api/v1/contracts-renew', hetongRenewRoutes)
 const linkmanApiRoutes = api.route('/api/v1/contacts', linkmanRoutes)
 const logfileApiRoutes = api.route('/api/v1/logs', logfileRoutes)
+const operationLogApiRoutes = api.route('/api/v1/operation-logs', operationLogRoutes)
 const orderRecordApiRoutes = api.route('/api/v1/order-records', orderRecordRoutes)
 const followUpRecordApiRoutes = api.route('/api/v1/follow-up-records', followUpRecordRoutes)
 const departmentsApiRoutes = api.route('/api/v1/departments', departmentsRoute)
@@ -96,6 +98,7 @@ export type HetongRoutes = typeof hetongApiRoutes
 export type HetongRenewRoutes = typeof hetongRenewApiRoutes
 export type LinkmanRoutes = typeof linkmanApiRoutes
 export type LogfileRoutes = typeof logfileApiRoutes
+export type OperationLogRoutes = typeof operationLogApiRoutes
 export type OrderRecordRoutes = typeof orderRecordApiRoutes
 export type FollowUpRecordRoutes = typeof followUpRecordApiRoutes
 export type DepartmentRoutes = typeof departmentsApiRoutes

+ 16 - 0
src/server/api/operation-logs/index.ts

@@ -0,0 +1,16 @@
+import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
+import { OperationLog } from '@/server/modules/logs/operation-log.entity';
+import { OperationLogSchema, CreateOperationLogDto, UpdateOperationLogDto } from '@/server/modules/logs/operation-log.entity';
+import { authMiddleware } from '@/server/middleware/auth.middleware';
+
+const operationLogRoutes = createCrudRoutes({
+  entity: OperationLog,
+  createSchema: CreateOperationLogDto,
+  updateSchema: UpdateOperationLogDto,
+  getSchema: OperationLogSchema,
+  listSchema: OperationLogSchema,
+  searchFields: ['resourceType', 'action', 'endpoint', 'username', 'ipAddress'],
+  middleware: [authMiddleware]
+});
+
+export default operationLogRoutes;

+ 2 - 1
src/server/data-source.ts

@@ -14,6 +14,7 @@ import { Hetong } from "./modules/contracts/hetong.entity"
 import { HetongRenew } from "./modules/contracts/hetong-renew.entity"
 import { Linkman } from "./modules/contacts/linkman.entity"
 import { Logfile } from "./modules/logs/logfile.entity"
+import { OperationLog } from "./modules/logs/operation-log.entity"
 import { OrderRecord } from "./modules/orders/order-record.entity"
 import { FollowUpRecord } from "./modules/follow-ups/follow-up-record.entity"
 import { Department, UserDepartment } from "./modules/departments/department.entity"
@@ -28,7 +29,7 @@ export const AppDataSource = new DataSource({
   database: process.env.DB_DATABASE || "d8dai",
   entities: [
     User, Role,
-    AreaData, Client, Expense, File, Hetong, HetongRenew, Linkman, Logfile,
+    AreaData, Client, Expense, File, Hetong, HetongRenew, Linkman, Logfile, OperationLog,
     OrderRecord, FollowUpRecord, Department, UserDepartment, Permission, RolePermission
   ],
   migrations: [],

+ 256 - 0
src/server/middleware/enhanced-permission-log.middleware.ts

@@ -0,0 +1,256 @@
+import { Context, Next } from 'hono';
+import { checkPermission } from './permission.middleware';
+import { OperationLogService } from '@/server/modules/logs/operation-log.service';
+import { AppDataSource } from '@/server/data-source';
+
+/**
+ * 增强版权限日志中间件 - 集成结构化操作日志
+ * 自动记录详细的操作信息,包括请求数据、响应数据、性能等
+ */
+export function enhancedPermissionWithLog(requiredPermissions: string[]) {
+  const operationLogService = new OperationLogService(AppDataSource);
+  const permissionChecker = checkPermission(requiredPermissions);
+  
+  return async (c: Context, next: Next) => {
+    const user = c.get('user');
+    const method = c.req.method;
+    const url = new URL(c.req.url);
+    const path = url.pathname;
+    const params = c.req.param();
+    
+    // 生成请求唯一标识
+    const requestId = generateRequestId();
+    
+    // 自动检测资源类型
+    const resourceType = detectResourceType(requiredPermissions, path);
+    
+    // 获取相关ID
+    const resourceId = params.id || null;
+    
+    // 获取客户端信息
+    const ipAddress = c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || c.req.header('cf-connecting-ip') || 'unknown';
+    const userAgent = c.req.header('user-agent') || '';
+    
+    // 记录操作开始时间
+    const startTime = Date.now();
+    
+    try {
+      // 执行权限检查
+      const hasPermission = await permissionChecker(user);
+      
+      if (!hasPermission) {
+        // 记录权限拒绝
+        await operationLogService.quickLog({
+          requestId,
+          userId: user?.id,
+          username: user?.username,
+          resourceType,
+          resourceId: resourceId ? String(resourceId) : undefined,
+          action: 'permission_denied',
+          method,
+          endpoint: path,
+          ipAddress,
+          userAgent,
+          status: 'permission_denied',
+          duration: 0,
+          permissionRequired: requiredPermissions.join(', ')
+        });
+        
+        return c.json({ message: '没有权限访问该资源', code: 403 }, 403);
+      }
+      
+      // 解析请求数据(非GET请求)
+      let requestData = null;
+      if (method !== 'GET') {
+        try {
+          requestData = await c.req.json();
+        } catch (e) {
+          // 如果JSON解析失败,尝试获取原始文本
+          try {
+            requestData = await c.req.text();
+            requestData = requestData ? { raw: requestData } : null;
+          } catch (textError) {
+            requestData = null;
+          }
+        }
+      }
+      
+      // 记录操作开始
+      await operationLogService.quickLog({
+        requestId,
+        userId: user?.id,
+        username: user?.username,
+        resourceType,
+        resourceId: resourceId ? String(resourceId) : undefined,
+        action: determineAction(method, path),
+        method,
+        endpoint: path,
+        ipAddress,
+        userAgent,
+        requestData: sanitizeData(requestData),
+        status: 'success',
+        duration: 0,
+        permissionRequired: requiredPermissions.join(', ')
+      });
+      
+      try {
+        // 继续执行后续处理
+        await next();
+        
+        // 计算执行时间
+        const duration = Date.now() - startTime;
+        
+        // 根据响应状态更新日志
+        const status = c.res.status >= 400 ? 'failed' : 'success';
+        
+        // 更新操作完成信息
+        await operationLogService.updateOperationComplete(requestId, {
+          duration,
+          status,
+          errorMessage: status === 'failed' ? `HTTP ${c.res.status}` : undefined
+        });
+        
+      } catch (error) {
+        // 计算执行时间
+        const duration = Date.now() - startTime;
+        
+        // 记录操作失败
+        await operationLogService.updateOperationComplete(requestId, {
+          duration,
+          status: 'failed',
+          errorMessage: error instanceof Error ? error.message : 'Unknown error',
+          stackTrace: error instanceof Error ? error.stack : undefined
+        });
+        
+        throw error;
+      }
+      
+    } catch (error) {
+      // 处理中间件本身的错误
+      console.error('Enhanced permission log middleware error:', error);
+      
+      // 仍然执行原始权限检查
+      const hasPermission = await permissionChecker(user);
+      if (!hasPermission) {
+        return c.json({ message: '没有权限访问该资源', code: 403 }, 403);
+      }
+      
+      await next();
+    }
+  };
+}
+
+// 智能资源类型检测(增强版)
+function detectResourceType(permissions: string[], path: string): string {
+  if (!permissions || permissions.length === 0) {
+    return extractFromPath(path);
+  }
+  
+  // 从权限字符串提取
+  const permission = permissions[0];
+  const resource = extractFromPermission(permission);
+  if (resource !== 'unknown') {
+    return resource;
+  }
+  
+  // 从路径提取
+  return extractFromPath(path);
+}
+
+// 从权限字符串提取资源类型
+function extractFromPermission(permission: string): string {
+  const parts = permission.split(':');
+  
+  if (parts.length >= 2) {
+    // 处理 system:user:create 格式
+    if (parts[0] === 'system' && parts[1]) {
+      return parts[1];
+    }
+    // 处理 client:view 或 client:view:all 格式
+    return parts[0];
+  }
+  
+  return parts[0] || 'unknown';
+}
+
+// 从URL路径提取资源类型
+function extractFromPath(path: string): string {
+  // 匹配 /api/v1/users 或 /api/v1/users/123
+  const match = path.match(/\/api\/v\d+\/([^/]+)/);
+  if (match && match[1]) {
+    let resource = match[1];
+    // 转换为单数形式
+    resource = resource.replace(/s$/, '');
+    return resource;
+  }
+  
+  // 匹配 /api/v1/contract-renews -> contract_renew
+  const hyphenMatch = path.match(/\/api\/v\d+\/([^/]+)/);
+  if (hyphenMatch && hyphenMatch[1]) {
+    return hyphenMatch[1].replace(/-/g, '_');
+  }
+  
+  return 'unknown';
+}
+
+// 确定操作动作
+function determineAction(method: string, path: string): string {
+  const parts = path.split('/');
+  const lastPart = parts[parts.length - 1];
+  const isIdPath = /^\d+$/.test(lastPart);
+  
+  switch (method.toUpperCase()) {
+    case 'GET':
+      return isIdPath ? 'view' : 'list';
+    case 'POST':
+      return 'create';
+    case 'PUT':
+    case 'PATCH':
+      return 'update';
+    case 'DELETE':
+      return 'delete';
+    default:
+      return 'unknown';
+  }
+}
+
+// 生成请求唯一标识
+function generateRequestId(): string {
+  const timestamp = Date.now();
+  const random = Math.random().toString(36).substring(2, 8);
+  return `req_${timestamp}_${random}`;
+}
+
+// 数据脱敏处理
+function sanitizeData(data: any): any {
+  if (!data) return data;
+  
+  const sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential'];
+  const sanitized = Array.isArray(data) ? [...data] : { ...data };
+  
+  const sanitizeRecursive = (obj: any) => {
+    if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
+      for (const field of sensitiveFields) {
+        if (obj[field]) {
+          obj[field] = '***';
+        }
+      }
+      // 递归处理嵌套对象
+      for (const key in obj) {
+        if (obj[key] && typeof obj[key] === 'object') {
+          sanitizeRecursive(obj[key]);
+        }
+      }
+    }
+  };
+  
+  sanitizeRecursive(sanitized);
+  return sanitized;
+}
+
+// 简化使用方式
+export const permissionWithEnhancedLog = enhancedPermissionWithLog;
+
+// 使用示例:
+// app.get('/api/users', authMiddleware, permissionWithEnhancedLog(['system:user:view']), handler);
+// app.post('/api/clients', authMiddleware, permissionWithEnhancedLog(['client:create']), handler);

+ 271 - 0
src/server/modules/logs/operation-log.entity.ts

@@ -0,0 +1,271 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
+import { z } from '@hono/zod-openapi';
+
+@Entity('operation_logs')
+export class OperationLog {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({ name: 'request_id', type: 'varchar', length: 50, comment: '请求唯一标识' })
+  requestId!: string;
+
+  @Column({ name: 'user_id', type: 'int', nullable: true, comment: '操作用户ID' })
+  userId?: number;
+
+  @Column({ name: 'username', type: 'varchar', length: 50, nullable: true, comment: '用户名' })
+  username?: string;
+
+  @Column({ name: 'resource_type', type: 'varchar', length: 50, comment: '资源类型' })
+  resourceType!: string;
+
+  @Column({ name: 'resource_id', type: 'varchar', length: 50, nullable: true, comment: '资源ID' })
+  resourceId?: string;
+
+  @Column({ name: 'action', type: 'varchar', length: 50, comment: '操作动作' })
+  action!: string;
+
+  @Column({ name: 'method', type: 'varchar', length: 10, comment: 'HTTP方法' })
+  method!: string;
+
+  @Column({ name: 'endpoint', type: 'varchar', length: 255, comment: 'API端点' })
+  endpoint!: string;
+
+  @Column({ name: 'ip_address', type: 'varchar', length: 45, nullable: true, comment: 'IP地址' })
+  ipAddress?: string;
+
+  @Column({ name: 'user_agent', type: 'varchar', length: 500, nullable: true, comment: '用户代理' })
+  userAgent?: string;
+
+  @Column({ name: 'request_data', type: 'json', nullable: true, comment: '请求数据(JSON)' })
+  requestData?: any;
+
+  @Column({ name: 'response_data', type: 'json', nullable: true, comment: '响应数据(JSON)' })
+  responseData?: any;
+
+  @Column({ name: 'before_data', type: 'json', nullable: true, comment: '操作前数据(JSON)' })
+  beforeData?: any;
+
+  @Column({ name: 'after_data', type: 'json', nullable: true, comment: '操作后数据(JSON)' })
+  afterData?: any;
+
+  @Column({ name: 'duration', type: 'int', default: 0, comment: '操作耗时(ms)' })
+  duration!: number;
+
+  @Column({ name: 'status', type: 'enum', enum: ['success', 'failed', 'permission_denied'], comment: '操作状态' })
+  status!: 'success' | 'failed' | 'permission_denied';
+
+  @Column({ name: 'error_message', type: 'text', nullable: true, comment: '错误信息' })
+  errorMessage?: string;
+
+  @Column({ name: 'stack_trace', type: 'text', nullable: true, comment: '错误堆栈' })
+  stackTrace?: string;
+
+  @Column({ name: 'permission_required', type: 'varchar', length: 255, nullable: true, comment: '所需权限' })
+  permissionRequired?: string;
+
+  @CreateDateColumn({ name: 'created_at' })
+  createdAt!: Date;
+
+  @UpdateDateColumn({ name: 'updated_at' })
+  updatedAt!: Date;
+}
+
+// Zod Schema定义
+export const OperationLogSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '操作日志ID',
+    example: 1
+  }),
+  requestId: z.string().max(50).openapi({
+    description: '请求唯一标识',
+    example: 'req_20231215120000_abc123'
+  }),
+  userId: z.number().int().positive().nullable().openapi({
+    description: '操作用户ID',
+    example: 1001
+  }),
+  username: z.string().max(50).nullable().openapi({
+    description: '用户名',
+    example: 'admin'
+  }),
+  resourceType: z.string().max(50).openapi({
+    description: '资源类型',
+    example: 'client'
+  }),
+  resourceId: z.string().max(50).nullable().openapi({
+    description: '资源ID',
+    example: 'C20230001'
+  }),
+  action: z.string().max(50).openapi({
+    description: '操作动作',
+    example: 'update'
+  }),
+  method: z.string().max(10).openapi({
+    description: 'HTTP方法',
+    example: 'PUT'
+  }),
+  endpoint: z.string().max(255).openapi({
+    description: 'API端点',
+    example: '/api/v1/clients/C20230001'
+  }),
+  ipAddress: z.string().max(45).nullable().openapi({
+    description: 'IP地址',
+    example: '192.168.1.100'
+  }),
+  userAgent: z.string().max(500).nullable().openapi({
+    description: '用户代理',
+    example: 'Mozilla/5.0...'
+  }),
+  requestData: z.any().nullable().openapi({
+    description: '请求数据',
+    example: { name: '新客户名称' }
+  }),
+  responseData: z.any().nullable().openapi({
+    description: '响应数据',
+    example: { id: 'C20230001', name: '新客户名称' }
+  }),
+  beforeData: z.any().nullable().openapi({
+    description: '操作前数据',
+    example: { name: '旧客户名称' }
+  }),
+  afterData: z.any().nullable().openapi({
+    description: '操作后数据',
+    example: { name: '新客户名称' }
+  }),
+  duration: z.number().int().nonnegative().openapi({
+    description: '操作耗时(ms)',
+    example: 150
+  }),
+  status: z.enum(['success', 'failed', 'permission_denied']).openapi({
+    description: '操作状态',
+    example: 'success'
+  }),
+  errorMessage: z.string().nullable().openapi({
+    description: '错误信息',
+    example: '权限不足'
+  }),
+  stackTrace: z.string().nullable().openapi({
+    description: '错误堆栈',
+    example: 'Error: ...'
+  }),
+  permissionRequired: z.string().max(255).nullable().openapi({
+    description: '所需权限',
+    example: 'system:client:update'
+  }),
+  createdAt: z.date().openapi({
+    description: '创建时间',
+    example: '2023-12-15T12:00:00Z'
+  }),
+  updatedAt: z.date().openapi({
+    description: '更新时间',
+    example: '2023-12-15T12:00:00Z'
+  })
+});
+
+// 创建DTO
+export const CreateOperationLogDto = z.object({
+  requestId: z.string().max(50).openapi({
+    description: '请求唯一标识',
+    example: 'req_20231215120000_abc123'
+  }),
+  userId: z.number().int().positive().nullable().optional().openapi({
+    description: '操作用户ID',
+    example: 1001
+  }),
+  username: z.string().max(50).nullable().optional().openapi({
+    description: '用户名',
+    example: 'admin'
+  }),
+  resourceType: z.string().max(50).openapi({
+    description: '资源类型',
+    example: 'client'
+  }),
+  resourceId: z.string().max(50).nullable().optional().openapi({
+    description: '资源ID',
+    example: 'C20230001'
+  }),
+  action: z.string().max(50).openapi({
+    description: '操作动作',
+    example: 'update'
+  }),
+  method: z.string().max(10).openapi({
+    description: 'HTTP方法',
+    example: 'PUT'
+  }),
+  endpoint: z.string().max(255).openapi({
+    description: 'API端点',
+    example: '/api/v1/clients/C20230001'
+  }),
+  ipAddress: z.string().max(45).nullable().optional().openapi({
+    description: 'IP地址',
+    example: '192.168.1.100'
+  }),
+  userAgent: z.string().max(500).nullable().optional().openapi({
+    description: '用户代理',
+    example: 'Mozilla/5.0...'
+  }),
+  requestData: z.any().nullable().optional().openapi({
+    description: '请求数据',
+    example: { name: '新客户名称' }
+  }),
+  responseData: z.any().nullable().optional().openapi({
+    description: '响应数据',
+    example: { id: 'C20230001', name: '新客户名称' }
+  }),
+  beforeData: z.any().nullable().optional().openapi({
+    description: '操作前数据',
+    example: { name: '旧客户名称' }
+  }),
+  afterData: z.any().nullable().optional().openapi({
+    description: '操作后数据',
+    example: { name: '新客户名称' }
+  }),
+  duration: z.coerce.number().int().nonnegative().optional().openapi({
+    description: '操作耗时(ms)',
+    example: 150
+  }),
+  status: z.enum(['success', 'failed', 'permission_denied']).optional().openapi({
+    description: '操作状态',
+    example: 'success'
+  }),
+  errorMessage: z.string().nullable().optional().openapi({
+    description: '错误信息',
+    example: '权限不足'
+  }),
+  stackTrace: z.string().nullable().optional().openapi({
+    description: '错误堆栈',
+    example: 'Error: ...'
+  }),
+  permissionRequired: z.string().max(255).nullable().optional().openapi({
+    description: '所需权限',
+    example: 'system:client:update'
+  })
+});
+
+// 更新DTO
+export const UpdateOperationLogDto = z.object({
+  responseData: z.any().nullable().optional().openapi({
+    description: '响应数据',
+    example: { id: 'C20230001', name: '新客户名称' }
+  }),
+  afterData: z.any().nullable().optional().openapi({
+    description: '操作后数据',
+    example: { name: '新客户名称' }
+  }),
+  duration: z.coerce.number().int().nonnegative().optional().openapi({
+    description: '操作耗时(ms)',
+    example: 150
+  }),
+  status: z.enum(['success', 'failed', 'permission_denied']).optional().openapi({
+    description: '操作状态',
+    example: 'success'
+  }),
+  errorMessage: z.string().nullable().optional().openapi({
+    description: '错误信息',
+    example: '权限不足'
+  }),
+  stackTrace: z.string().nullable().optional().openapi({
+    description: '错误堆栈',
+    example: 'Error: ...'
+  })
+});

+ 121 - 0
src/server/modules/logs/operation-log.service.ts

@@ -0,0 +1,121 @@
+import { GenericCrudService } from '@/server/utils/generic-crud.service';
+import { DataSource } from 'typeorm';
+import { OperationLog } from './operation-log.entity';
+
+export class OperationLogService extends GenericCrudService<OperationLog> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, OperationLog);
+  }
+
+  /**
+   * 快速记录操作日志(简化版本)
+   */
+  async quickLog(data: {
+    requestId: string;
+    userId?: number;
+    username?: string;
+    resourceType: string;
+    resourceId?: string;
+    action: string;
+    method: string;
+    endpoint: string;
+    ipAddress?: string;
+    userAgent?: string;
+    status: 'success' | 'failed' | 'permission_denied';
+    duration?: number;
+    errorMessage?: string;
+    permissionRequired?: string;
+  }): Promise<OperationLog> {
+    return this.create({
+      ...data,
+      duration: data.duration || 0
+    });
+  }
+
+  /**
+   * 更新操作完成信息
+   */
+  async updateOperationComplete(
+    requestId: string,
+    updateData: {
+      responseData?: any;
+      afterData?: any;
+      duration: number;
+      status: 'success' | 'failed';
+      errorMessage?: string;
+      stackTrace?: string;
+    }
+  ): Promise<void> {
+    const log = await this.repository.findOne({ where: { requestId } });
+    if (log) {
+      Object.assign(log, updateData);
+      await this.repository.save(log);
+    }
+  }
+
+  /**
+   * 根据请求ID查找日志
+   */
+  async findByRequestId(requestId: string): Promise<OperationLog | null> {
+    return this.repository.findOne({ where: { requestId } });
+  }
+
+  /**
+   * 获取用户操作历史
+   */
+  async getUserOperationHistory(
+    userId: number,
+    options?: {
+      limit?: number;
+      offset?: number;
+      startDate?: Date;
+      endDate?: Date;
+    }
+  ): Promise<[OperationLog[], number]> {
+    const query = this.repository.createQueryBuilder('log')
+      .where('log.userId = :userId', { userId })
+      .orderBy('log.createdAt', 'DESC');
+
+    if (options?.startDate) {
+      query.andWhere('log.createdAt >= :startDate', { startDate: options.startDate });
+    }
+
+    if (options?.endDate) {
+      query.andWhere('log.createdAt <= :endDate', { endDate: options.endDate });
+    }
+
+    const limit = options?.limit || 20;
+    const offset = options?.offset || 0;
+
+    query.skip(offset).take(limit);
+
+    return query.getManyAndCount();
+  }
+
+  /**
+   * 获取资源操作历史
+   */
+  async getResourceOperationHistory(
+    resourceType: string,
+    resourceId?: string,
+    options?: {
+      limit?: number;
+      offset?: number;
+    }
+  ): Promise<[OperationLog[], number]> {
+    const query = this.repository.createQueryBuilder('log')
+      .where('log.resourceType = :resourceType', { resourceType })
+      .orderBy('log.createdAt', 'DESC');
+
+    if (resourceId) {
+      query.andWhere('log.resourceId = :resourceId', { resourceId });
+    }
+
+    const limit = options?.limit || 20;
+    const offset = options?.offset || 0;
+
+    query.skip(offset).take(limit);
+
+    return query.getManyAndCount();
+  }
+}