211 lines
6.8 KiB
Python
211 lines
6.8 KiB
Python
|
|
"""
|
||
|
|
快照管理服务 — 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
|