Files
wecom_it_smart_desk/backend/app/services/automation/snapshot_service.py
T

211 lines
6.8 KiB
Python
Raw Normal View History

"""
快照管理服务 — P3 核心组件。
在每次更正发生前创建信息项快照,支持更正撤销(undo)。
撤销限制:最多撤销最近5次。
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.models.automation import InformationItem, InformationSnapshot
logger = logging.getLogger(__name__)
MAX_UNDO_COUNT = getattr(settings, "max_undo_count", 5)
class SnapshotService:
"""信息项快照管理服务。"""
def __init__(self, db: AsyncSession):
self.db = db
async def create_snapshot(
self,
session_id: str,
trigger_item_key: str,
correction_ids: List[str],
) -> InformationSnapshot:
"""在更正前创建快照,记录当前全部信息项状态。
Args:
session_id: 会话ID
trigger_item_key: 触发更正的信息项 key
correction_ids: 本次更正涉及的信息项 ID 列表
Returns:
InformationSnapshot: 创建的快照记录
"""
# 获取当前所有信息项
items = await self._get_items(session_id)
# 构建快照数据
snapshot_data = {}
for item in items:
snapshot_data[item.name] = {
"value": item.value,
"version": item.version,
"id": item.id,
}
snapshot = InformationSnapshot(
session_id=session_id,
trigger_item_key=trigger_item_key,
snapshot_data=snapshot_data,
correction_ids=correction_ids,
is_undone=False,
)
self.db.add(snapshot)
await self.db.flush()
logger.info(
f"创建快照: session={session_id} trigger={trigger_item_key} "
f"items={len(snapshot_data)}"
)
return snapshot
async def undo_correction(self, session_id: str) -> dict:
"""撤销最近一次更正。
Returns:
dict: {
"undone_items": List[str], # 被回滚的信息项名称
"restored_values": Dict[str, str], # 恢复的值
"snapshot_id": int,
}
Raises:
ValueError: 无可撤销快照 或 撤销次数超限
"""
# 检查撤销次数
undone_count = await self._count_undone(session_id)
if undone_count >= MAX_UNDO_COUNT:
raise ValueError(
f"撤销次数超限,最多可撤销{MAX_UNDO_COUNT}次更正"
)
# 获取最近一条未撤销的快照
snapshot = await self.get_latest_snapshot(session_id)
if snapshot is None:
raise ValueError("无可撤销的更正")
# 回滚信息项
undone_items = []
restored_values = {}
for item_name, item_data in snapshot.snapshot_data.items():
item = await self._get_item(session_id, item_name)
if item is not None:
old_value = item.value
item.value = item_data["value"]
item.version = item_data["version"]
undone_items.append(item_name)
restored_values[item_name] = item_data["value"]
logger.info(
f"撤销回滚: session={session_id} item={item_name} "
f"value={old_value}->{item.value}"
)
# 标记快照为已撤销
snapshot.is_undone = True
await self.db.flush()
logger.info(
f"撤销完成: session={session_id} snapshot={snapshot.id} "
f"items={undone_items}"
)
return {
"undone_items": undone_items,
"restored_values": restored_values,
"snapshot_id": snapshot.id,
}
async def get_latest_snapshot(
self, session_id: str
) -> Optional[InformationSnapshot]:
"""获取最近一条未撤销的快照。"""
stmt = (
select(InformationSnapshot)
.where(
InformationSnapshot.session_id == session_id,
InformationSnapshot.is_undone == False, # noqa: E712
)
.order_by(InformationSnapshot.created_at.desc())
.limit(1)
)
return (await self.db.execute(stmt)).scalar_one_or_none()
async def get_snapshot_history(
self, session_id: str
) -> List[InformationSnapshot]:
"""获取快照历史列表。"""
stmt = (
select(InformationSnapshot)
.where(InformationSnapshot.session_id == session_id)
.order_by(InformationSnapshot.created_at.desc())
)
return list((await self.db.execute(stmt)).scalars().all())
async def get_version_diff(
self, session_id: str, item_name: str, v1: int, v2: int
) -> dict:
"""对比某个信息项的两个版本。
从 update_history 中提取指定版本号的值进行对比。
"""
item = await self._get_item(session_id, item_name)
if item is None:
raise ValueError(f"信息项 {item_name} 不存在")
# 从 update_history 中找到对应版本
history = {h["version"]: h for h in (item.update_history or [])}
v1_data = history.get(v1, {})
v2_data = history.get(v2, {})
v1_value = v1_data.get("new_value", item.value if v1 == item.version else "")
v2_value = v2_data.get("new_value", item.value if v2 == item.version else "")
return {
"item_key": item_name,
"v1": v1,
"v1_value": v1_value,
"v2": v2,
"v2_value": v2_value,
"changed": v1_value != v2_value,
}
async def _get_items(self, session_id: str) -> List[InformationItem]:
"""获取会话下所有信息项。"""
stmt = select(InformationItem).where(
InformationItem.session_id == session_id
)
return list((await self.db.execute(stmt)).scalars().all())
async def _get_item(
self, session_id: str, name: str
) -> Optional[InformationItem]:
"""按名称获取单个信息项。"""
stmt = select(InformationItem).where(
InformationItem.session_id == session_id,
InformationItem.name == name,
)
return (await self.db.execute(stmt)).scalar_one_or_none()
async def _count_undone(self, session_id: str) -> int:
"""统计已撤销的快照数量。"""
stmt = (
select(func.count(InformationSnapshot.id))
.where(
InformationSnapshot.session_id == session_id,
InformationSnapshot.is_undone == True, # noqa: E712
)
)
result = await self.db.execute(stmt)
return result.scalar() or 0