SkillSpace/hermes-agent/skills/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

213 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 操作日志系统(审计追踪模式)
> **适用场景**: 任何需要审计追踪的 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 后调用 |
---
## 模型定义
```python
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="操作时间")
```
## 服务层
```python
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 路由
```python
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
],
}
```
## 埋点注入模式
### 创建后
```python
log_operation(db, "create", "project", p.id, p.project_name, f"创建项目: {p.project_code}")
```
### 更新后
```python
log_operation(db, "update", "project", p.id, p.project_name, f"更新项目: {p.project_code}")
```
### 删除前(需要 entity_name 做记录)
```python
log_operation(db, "delete", "project", project_id, p.project_name if p else None, f"删除项目: ID={project_id}")
```
### 批量导入
```python
log_operation(db, "create", "import", None, filename, f"Excel导入: {stats}")
```
## 注册路由
`main.py` 中:
```python
from app.routers import auth, groups, personnel, projects, settings, dashboard, reports, search, finance, logs
# 受保护路由
app.include_router(logs.router, dependencies=[Depends(verify_token)])
```
## 前端页面
```vue
<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>
```
## 路由注册 + 侧边栏
```javascript
// 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 冗余存储**:即使关联实体被删除,日志中仍保留名称用于展示