SkillSpace/archives/dev-pipeline-universal/references/operation-log-pattern.md
sinohqb 8dafa5bc52 Add archives, import dev-pipeline skill, and complete Hermes Agent spec
- Add archives/ directory for immutable external skill snapshots
  - Strict immutability rule: only README/CHANGELOG can be modified
  - First import: dev-pipeline-universal v1.0.0 (Hermes Agent)
- Import dev-pipeline-universal into hermes-agent/skills/ with CHANGELOG
- Update hermes-agent/publish/registry.json with imported skill
- Rewrite meta-skills/create-hermes-agent-skill/ with full spec:
  - SKILL.md frontmatter fields and validator constraints
  - Recommended body structure (Overview → When to Use → Pitfalls → Verification)
  - Progressive disclosure (3-level loading)
  - 8 writing quality principles
  - Official reference skills from GitHub repo
  - Tool chain docs (skill_manage, /learn, skill_view)
- Update README.md with archives/ in directory structure
2026-07-02 00:46:13 +08:00

5.9 KiB
Raw Blame History

操作日志系统(审计追踪模式)

适用场景: 任何需要审计追踪的 CRUD 管理系统 创建日期: 2026-06-29


四层架构

层次 文件 职责
模型 models/operation_log.py 定义数据库表结构
服务 services/operation_log.py 工具函数 log_operation()
API routers/logs.py GET /api/logs 查询接口
埋点 各 CRUD 路由中 在 create/update/delete 后调用

模型定义

from sqlalchemy import Column, Integer, String, DateTime, Text
from sqlalchemy.sql import func
from app.database import Base


class OperationLog(Base):
    __tablename__ = "operation_logs"

    id = Column(Integer, primary_key=True, index=True)
    user = Column(String(64), default="admin", comment="操作人")
    action = Column(String(32), nullable=False, comment="操作类型: create/update/delete")
    entity_type = Column(String(32), nullable=False, comment="实体类型")
    entity_id = Column(Integer, nullable=True, comment="实体ID")
    entity_name = Column(String(128), nullable=True, comment="实体名称(冗余,方便展示)")
    detail = Column(Text, nullable=True, comment="操作详情")
    created_at = Column(DateTime(timezone=True), server_default=func.now(), comment="操作时间")

服务层

from sqlalchemy.orm import Session
from app.models.operation_log import OperationLog


def log_operation(
    db: Session,
    action: str,
    entity_type: str,
    entity_id: int = None,
    entity_name: str = None,
    detail: str = None,
    user: str = "admin",
):
    log = OperationLog(
        user=user,
        action=action,
        entity_type=entity_type,
        entity_id=entity_id,
        entity_name=entity_name,
        detail=detail,
    )
    db.add(log)
    db.commit()

API 路由

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.database import get_db
from app.models.operation_log import OperationLog
from typing import Optional

router = APIRouter(prefix="/api/logs", tags=["操作日志"])


@router.get("")
def list_logs(
    entity_type: Optional[str] = Query(None),
    action: Optional[str] = Query(None),
    limit: int = Query(100, ge=1, le=500),
    offset: int = Query(0, ge=0),
    db: Session = Depends(get_db),
):
    q = db.query(OperationLog).order_by(OperationLog.created_at.desc())
    if entity_type:
        q = q.filter(OperationLog.entity_type == entity_type)
    if action:
        q = q.filter(OperationLog.action == action)

    total = q.count()
    logs = q.offset(offset).limit(limit).all()

    return {
        "total": total,
        "logs": [
            {
                "id": log.id,
                "user": log.user,
                "action": log.action,
                "entity_type": log.entity_type,
                "entity_id": log.entity_id,
                "entity_name": log.entity_name,
                "detail": log.detail,
                "created_at": log.created_at.isoformat() if log.created_at else None,
            }
            for log in logs
        ],
    }

埋点注入模式

创建后

log_operation(db, "create", "project", p.id, p.project_name, f"创建项目: {p.project_code}")

更新后

log_operation(db, "update", "project", p.id, p.project_name, f"更新项目: {p.project_code}")

删除前(需要 entity_name 做记录)

log_operation(db, "delete", "project", project_id, p.project_name if p else None, f"删除项目: ID={project_id}")

批量导入

log_operation(db, "create", "import", None, filename, f"Excel导入: {stats}")

注册路由

main.py 中:

from app.routers import auth, groups, personnel, projects, settings, dashboard, reports, search, finance, logs

# 受保护路由
app.include_router(logs.router, dependencies=[Depends(verify_token)])

前端页面

<template>
  <el-card>
    <template #header>
      <span>📝 操作日志</span>
      <!-- 筛选实体类型 + 操作类型 -->
    </template>
    <el-table :data="logs" stripe>
      <el-table-column prop="created_at" label="时间" />
      <el-table-column prop="user" label="操作人" />
      <el-table-column label="操作">
        <el-tag :type="actionTagType(row.action)" size="small">
          {{ row.action === 'create' ? '创建' : row.action === 'update' ? '更新' : '删除' }}
        </el-tag>
      </el-table-column>
      <el-table-column label="实体" />
      <el-table-column prop="entity_name" label="实体名称" />
      <el-table-column prop="detail" label="操作详情" />
    </el-table>
  </el-card>
</template>

路由注册 + 侧边栏

// router/index.js
{
  path: 'logs',
  name: 'Logs',
  component: () => import('../views/logs/Index.vue'),
}

// Layout.vue 侧边栏
<el-menu-item index="/logs">
  <el-icon><Tickets /></el-icon>
  <span>操作日志</span>
</el-menu-item>

适用实体类型

entity_type 含义 埋点位置
project 项目 projects.py CRUD
task WBS 任务 projects.py task CRUD
milestone 里程碑 projects.py milestone CRUD
income 月度收益 projects.py income CRUD
personnel 人员 personnel.py CRUD
group 组别 groups.py CRUD
setting 系统设置 settings.py CRUD
import 数据导入 import_data.py

注意事项

  1. 删除操作需在 delete 前记录db.delete() 后对象属性仍可访问,但 db.commit() 后不可用
  2. SQLAlchemy Column 类型警告Pyright 会报 Column[int] 不能赋值给 int 的参数类型错误,这是已知假阳性,不影响运行
  3. 不要过度埋点GET 查询操作不需要记录,只记录 create/update/delete
  4. entity_name 冗余存储:即使关联实体被删除,日志中仍保留名称用于展示