baodan/api/insurance/plan_data/routes.py
wsb1224 2422303b36 前工程开发已经推进到 Phase 6 基础能力,但正式验收还没有完成。更准确地说:Phase 0~5 的主要代码链路已经落地,Phase 6 完成了治理框架,尚缺生产化和真实数据验收。
阶段	当前状态	说明
Phase 0~3	基本完成	Document IR、证据链、人工确认、不可变 PlanData Snapshot、海报/PPT 投影已实现
Phase 4	代码完成	场景、策略、模板版本,校验/发布门禁,确定性 resolver 和严格槽位合并已实现
Phase 5	代码完成	输入冻结、PPTX/海报对账、失败关闭、幂等、心跳、重试和冻结输入重放已实现
Phase 6	基础完成	质量看板、结构化告警、Golden 审批、留存清理、孤儿检查和灰度开关已实现
正式上线	未完成	缺真实样本、生产模板、业务规则签字和灰度观察

目前验证基线:
PPT/海报专项测试:107 passed, 1 skipped
Vue TypeScript 检查:通过
前端生产构建:通过
代码变更仍在工作区,尚未提交
仓库全量测试仍有既有失败/挂起项,暂时不能宣称全仓测试完全绿色
仍未完成的代码任务主要有:
影子解析差异流水线
目前有灰度开关,但还没有完整的“新旧解析同时运行、字段差异入库、按保司/profile 聚合”的影子比较任务。

自动视觉回归
目前实现的是 DOM 模块、溢出、尺寸、文本和数值检查;还缺基于真实模板和标准图片的像素差异、字体缺失、遮挡和裁切回归。

告警通道接入
后台已经能产生结构化质量告警,但尚未自动推送到邮件、企微或其他通知通道。

留存任务生产化
dry-run、实删服务和失败审计已经具备,但尚未接入周期性 Celery/定时任务,也没有自动重试失败清理批次。

旧链路最终下线
旧 PPT 解析器和海报紧凑解析仍保留为回滚路径。需要全量灰度稳定后才能删除或彻底关闭写入口。

全仓测试收口
需要处理现有无关失败和挂起测试,建立真正全绿的 CI 基线。

仍需外部输入和生产环境完成的事项:
至少 30 份脱敏 Golden PDF,并完成双人标注和精确率验收。
业务专家确认派生公式、缺失值、可比较性和结论策略。
上传并标注真实生产 PPTX 的语义 shape、页面类型和容量。
安装生产字体并建立视觉基准图片。
实际执行数据库迁移 038~040。
完成留存 dry-run、实删演练以及 10% → 30% → 100% 灰度。
观察期通过后开启 SCENARIO_ENGINE_V2,目前它仍默认关闭;真实清理开关也默认关闭。
完整状态记录在 [PPT与海报Phase4至6补充实施记录](D:/work/code/python/coding/baodanagent/docs/PPT与海报Phase4至6补充实施记录_20260802.md)。
2026-08-02 16:34:06 +08:00

157 lines
5.5 KiB
Python

"""PlanData Snapshot 鉴权 API。"""
from flask import Blueprint, jsonify, request
from insurance.middleware.auth_middleware import jwt_required
plan_snapshot_bp = Blueprint("insurance_plan_snapshots", __name__)
def _response(data=None, message="success", code=0, status=200):
return jsonify({"code": code, "message": message, "data": data}), status
def _run(action):
from insurance.plan_data.service import SnapshotError
try:
return action()
except SnapshotError as exc:
return _response(
{"errorCode": exc.code, **exc.data},
exc.message,
4201 if exc.status < 500 else 5001,
exc.status,
)
@plan_snapshot_bp.route("", methods=["POST"])
@jwt_required
def create_plan_snapshot():
from insurance.plan_data.service import create_draft_from_legacy, create_snapshot
user_id = str(getattr(request, "user_id", "guest"))
payload = request.get_json(silent=True) or {}
def action():
document_id = int(payload.get("documentId") or 0)
if isinstance(payload.get("planData"), dict):
snapshot = create_snapshot(document_id, user_id, payload["planData"], status="draft")
else:
snapshot = create_draft_from_legacy(
document_id,
user_id,
payload.get("extraction") or {},
evidence_entries=payload.get("evidence") or [],
)
return _response(snapshot.to_public_dict(), status=201)
return _run(action)
@plan_snapshot_bp.route("/<int:snapshot_id>", methods=["GET"])
@jwt_required
def get_plan_snapshot(snapshot_id: int):
from insurance.plan_data.service import SnapshotError, get_owned_snapshot
user_id = str(getattr(request, "user_id", "guest"))
def action():
snapshot = get_owned_snapshot(snapshot_id, user_id)
if not snapshot:
raise SnapshotError("SNAPSHOT_NOT_FOUND", "快照不存在", status=404)
return _response(snapshot.to_public_dict())
return _run(action)
@plan_snapshot_bp.route("/<int:snapshot_id>/evidence", methods=["GET"])
@jwt_required
def get_snapshot_evidence(snapshot_id: int):
from insurance.plan_data.service import list_evidence
user_id = str(getattr(request, "user_id", "guest"))
field_path = str(request.args.get("fieldPath") or "").strip()
page = max(1, request.args.get("page", 1, type=int))
page_size = min(100, max(1, request.args.get("pageSize", 50, type=int)))
return _run(lambda: _response(list_evidence(snapshot_id, user_id, field_path, page, page_size)))
@plan_snapshot_bp.route("/<int:snapshot_id>/draft", methods=["PATCH"])
@jwt_required
def update_snapshot_draft(snapshot_id: int):
from insurance.plan_data.service import patch_draft
user_id = str(getattr(request, "user_id", "guest"))
payload = request.get_json(silent=True) or {}
return _run(lambda: _response(patch_draft(
snapshot_id,
user_id,
expected_hash=str(payload.get("expectedHash") or ""),
changes=payload.get("changes") or [],
).to_public_dict()))
@plan_snapshot_bp.route("/<int:snapshot_id>/validate", methods=["POST"])
@jwt_required
def validate_plan_snapshot(snapshot_id: int):
from insurance.plan_data.service import validate_snapshot
user_id = str(getattr(request, "user_id", "guest"))
return _run(lambda: _response(validate_snapshot(snapshot_id, user_id)))
@plan_snapshot_bp.route("/<int:snapshot_id>/confirm", methods=["POST"])
@jwt_required
def confirm_plan_snapshot(snapshot_id: int):
from insurance.plan_data.service import confirm_snapshot
user_id = str(getattr(request, "user_id", "guest"))
return _run(lambda: _response(confirm_snapshot(snapshot_id, user_id).to_public_dict(), status=201))
@plan_snapshot_bp.route("/<int:snapshot_id>/poster-projection", methods=["GET"])
@jwt_required
def poster_projection(snapshot_id: int):
from insurance.plan_data.projections import to_poster_projection
from insurance.plan_data.service import SnapshotError, get_owned_snapshot
user_id = str(getattr(request, "user_id", "guest"))
def action():
snapshot = get_owned_snapshot(snapshot_id, user_id)
if not snapshot:
raise SnapshotError("SNAPSHOT_NOT_FOUND", "快照不存在", status=404)
if snapshot.status != "confirmed":
raise SnapshotError("SNAPSHOT_NOT_CONFIRMED", "只有 confirmed 快照可以投影", status=422)
return _response({
"snapshotId": snapshot.id,
"snapshotHash": snapshot.snapshot_hash,
"projection": to_poster_projection(snapshot.plan_data()),
})
return _run(action)
@plan_snapshot_bp.route("/<int:snapshot_id>/deck-contract", methods=["GET"])
@jwt_required
def deck_contract(snapshot_id: int):
from insurance.plan_data.projections import to_deck_contract
from insurance.plan_data.service import SnapshotError, get_owned_snapshot
user_id = str(getattr(request, "user_id", "guest"))
def action():
snapshot = get_owned_snapshot(snapshot_id, user_id)
if not snapshot:
raise SnapshotError("SNAPSHOT_NOT_FOUND", "快照不存在", status=404)
if snapshot.status != "confirmed":
raise SnapshotError("SNAPSHOT_NOT_CONFIRMED", "只有 confirmed 快照可以投影", status=422)
return _response({
"snapshotId": snapshot.id,
"snapshotHash": snapshot.snapshot_hash,
"deckContract": to_deck_contract(snapshot.plan_data(), document_sha256=snapshot.document_sha256),
})
return _run(action)