feat: add daily manual price review workflow

This commit is contained in:
Wyndham ARR
2026-08-06 22:40:18 +08:00
parent aae3d8e1db
commit ca3e8e18fa
77 changed files with 7750 additions and 602 deletions

View File

@@ -6,13 +6,14 @@ from datetime import date, datetime, timezone
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
ACTIVE_RUN_STATUSES = {"received", "queued", "running", "validating"}
ACTIVE_RUN_STATUSES = {"received", "queued", "running", "validating", "awaiting_review"}
_CURRENT_STAGE = {
"received": "upload",
"queued": "processor",
"running": "processor",
"validating": "validation",
"awaiting_review": "review",
"accepted": "database",
"rejected": "validation",
"failed": "processor",
@@ -24,6 +25,7 @@ _CURRENT_COPY = {
"queued": ("当前:等待程序处理", "任务已进入 ARR 处理队列。"),
"running": ("当前ARR 正在处理", "固定处理器正在生成日报和完整结构化结果。"),
"validating": ("当前:正在独立验收", "ARR 正在复核文件身份、业务结果并准备数据库提交。"),
"awaiting_review": ("当前:等待人工定价", "请完成全部缺价项后确认生成正式日报。"),
}
_LOG_ORDER = {
@@ -33,6 +35,13 @@ _LOG_ORDER = {
"ARTIFACT_RESULT_RECEIVED": 40,
"DIRECT_RESULT_RECEIVED": 40,
"ARTIFACT_RESULT_VALIDATED": 50,
"ARTIFACT_RESULT_RECORDED_FOR_REVIEW": 55,
"PRICE_REVIEW_REQUIRED": 56,
"PRICE_REVIEW_UPDATED": 57,
"PRICE_REVIEW_FINALIZED": 58,
"PRICE_REVIEW_GENERATION_FAILED": 59,
"PRICE_REVIEW_FAILED": 60,
"PRICE_REVIEW_CANCELLED": 61,
"DIRECT_VALIDATION_STARTED": 50,
"FINANCE_VERSION_CREATED": 60,
"FINANCE_VERSION_VALIDATED": 70,
@@ -167,7 +176,11 @@ def _latest(values: Sequence[Mapping[str, Any]], key: str) -> Optional[Mapping[s
return max(values, key=lambda item: int(item.get(key) or 0))
def _attempt_logs(logs: List[Dict[str, Any]], attempts: Sequence[Mapping[str, Any]]) -> None:
def _attempt_logs(
logs: List[Dict[str, Any]],
attempts: Sequence[Mapping[str, Any]],
review_case: Optional[Mapping[str, Any]],
) -> None:
for attempt in attempts:
number = int(attempt.get("attempt_no") or 0)
details = {
@@ -199,6 +212,15 @@ def _attempt_logs(logs: List[Dict[str, Any]], attempts: Sequence[Mapping[str, An
details=details,
)
status = str(attempt.get("attempt_status") or "")
retryable_review_failure = (
status == "failed"
and str((review_case or {}).get("case_status") or "")
== "generation_failed"
)
if retryable_review_failure:
# The review audit event records this as a retryable warning. It is
# deliberately not presented as a terminal processing failure.
continue
if status in {"failed", "cancelled"} and attempt.get("finished_at"):
failure_code = attempt.get("failure_code")
_append_log(
@@ -268,22 +290,40 @@ def _delivery_logs(
)
status = str(delivery.get("delivery_status") or "")
if delivery.get("committed_at"):
review_recorded = (
status == "recorded_review"
and str(delivery.get("result_status") or "") == "review_required"
)
failed = status in {"recorded_failure", "rejected"}
failure_code = delivery.get("failure_code")
_append_log(
logs,
log_id=f"delivery.{identity}.finished",
timestamp=delivery.get("committed_at"),
stage="validation" if failed else "database",
level="error" if failed else "success",
stage="validation" if failed else "review" if review_recorded else "database",
level="error" if failed else "info" if review_recorded else "success",
code=str(
failure_code
or ("ARTIFACT_RESULT_REJECTED" if failed else "ARTIFACT_RESULT_COMMITTED")
or (
"ARTIFACT_RESULT_REJECTED"
if failed
else "ARTIFACT_RESULT_RECORDED_FOR_REVIEW"
if review_recorded
else "ARTIFACT_RESULT_COMMITTED"
)
),
title=(
"结果制品未通过验收"
if failed
else "待人工定价已登记"
if review_recorded
else "结果制品已提交数据库"
),
title="结果制品未通过验收" if failed else "结果制品已提交数据库",
message=(
_safe_text(delivery.get("failure_message"))
if failed
else "缺价项已登记,尚未创建 Finance 日报版本。"
if review_recorded
else "验收结果已原子提交到 Finance 数据库。"
)
or "结果制品未通过验收。",
@@ -379,6 +419,9 @@ def _version_logs(logs: List[Dict[str, Any]], versions: Sequence[Mapping[str, An
"duplicate_rows": version.get("duplicate_rows"),
"validation_failed_rows": version.get("validation_failed_rows"),
"price_unmatched_rows": version.get("price_unmatched_rows"),
"review_case_id": version.get("review_case_id"),
"manually_priced_rows": version.get("manually_priced_rows"),
"manual_override_sha256": version.get("manual_override_sha256"),
}
_append_log(
logs,
@@ -491,6 +534,64 @@ def _outbox_logs(logs: List[Dict[str, Any]], events: Sequence[Mapping[str, Any]]
)
def _review_logs(
logs: List[Dict[str, Any]],
events: Sequence[Mapping[str, Any]],
) -> None:
event_copy = {
"PRICE_REVIEW_REQUIRED": (
"info",
"人工价格复核已创建",
"仅缺少固定价的键已登记;正式日报和 Finance 版本尚未创建。",
),
"PRICE_REVIEW_UPDATED": (
"info",
"人工价格已暂存",
"一项缺价键已由受控会话更新,待全部完成后统一确认。",
),
"PRICE_REVIEW_FINALIZED": (
"info",
"人工价格清单已冻结",
"ARR 将用原始 XML、固定价表和同一冻结清单重新生成并独立验收日报。",
),
"PRICE_REVIEW_GENERATION_FAILED": (
"warning",
"正式日报生成可重试",
"技术生成未完成;冻结的人工价格清单已保留,可再次确认重试。",
),
"PRICE_REVIEW_FAILED": (
"error",
"人工价格复核生成失败",
"最终重放未通过确定性业务验收,任务未形成可接受的日报版本。",
),
"PRICE_REVIEW_CANCELLED": (
"warning",
"人工价格复核已取消",
"复核记录保留审计痕迹;如需更正,请重新上传原始 XML。",
),
}
for index, event in enumerate(events, 1):
code = str(event.get("event_type") or "")
level, title, message = event_copy.get(
code,
("info", "人工价格复核事件", "复核状态已更新。"),
)
_append_log(
logs,
log_id=f"review.{index}.{code or 'event'}",
timestamp=event.get("created_at"),
stage="validation" if code == "PRICE_REVIEW_FAILED" else "review",
level=level,
code=code or "PRICE_REVIEW_EVENT",
title=title,
message=message,
details={
"review_revision": event.get("revision"),
"actor_username": event.get("actor_username"),
},
)
def build_job_trace(
run: Mapping[str, Any],
*,
@@ -499,6 +600,8 @@ def build_job_trace(
submissions: Sequence[Mapping[str, Any]] = (),
versions: Sequence[Mapping[str, Any]] = (),
outbox_events: Sequence[Mapping[str, Any]] = (),
review_case: Optional[Mapping[str, Any]] = None,
review_events: Sequence[Mapping[str, Any]] = (),
refreshed_at: Optional[datetime] = None,
) -> Dict[str, Any]:
"""Build one complete trace using only explicitly allowlisted facts."""
@@ -521,10 +624,11 @@ def build_job_trace(
"source_sha256": run.get("source_sha256"),
},
)
_attempt_logs(logs, attempts)
_attempt_logs(logs, attempts, review_case)
_delivery_logs(logs, deliveries)
_submission_logs(logs, submissions)
_version_logs(logs, versions)
_review_logs(logs, review_events)
_outbox_logs(logs, outbox_events)
run_status = str(run.get("run_status") or "")
@@ -571,6 +675,33 @@ def build_job_trace(
"delivery_mode": run.get("delivery_mode"),
},
)
elif run_status == "awaiting_review":
review_status = str((review_case or {}).get("case_status") or "open")
if review_status == "processing":
title = "当前:正在生成正式日报"
message = "ARR 正在用冻结人工价格清单重放原始 XML 并进行独立验收。"
elif review_status == "generation_failed":
title = "当前:正式日报生成可重试"
message = "冻结价格清单已保留;可再次确认以重试技术生成。"
else:
title, message = _CURRENT_COPY["awaiting_review"]
_append_log(
logs,
log_id="run.current.awaiting_review",
timestamp=run.get("updated_at") or run.get("created_at"),
stage="review",
level="info",
code="CURRENT_AWAITING_REVIEW",
title=title,
message=message,
details={
"job_id": run.get("job_id"),
"review_case_id": (review_case or {}).get("case_id"),
"review_status": review_status,
"completed_items": (review_case or {}).get("completed_items"),
"total_items": (review_case or {}).get("total_items"),
},
)
elif run_status in ACTIVE_RUN_STATUSES:
title, message = _CURRENT_COPY[run_status]
_append_log(
@@ -603,7 +734,9 @@ def build_job_trace(
latest_attempt = _latest(attempts, "attempt_no")
latest_version = _latest(versions, "version_no")
current_stage = (
_failure_stage(failure_code)
"review"
if run_status == "awaiting_review"
else _failure_stage(failure_code)
if failure_code
else _CURRENT_STAGE.get(run_status, logs[-1]["stage"] if logs else "upload")
)
@@ -613,7 +746,7 @@ def build_job_trace(
resolved_refreshed_at = refreshed_at or datetime.now(timezone.utc)
return {
"trace_version": "arr-job-trace-3",
"trace_version": "arr-job-trace-4",
"job": {
"job_id": str(run.get("job_id") or ""),
"status": public_status,
@@ -667,6 +800,23 @@ def build_job_trace(
"delivered_processor_version": _safe_text(
run.get("delivered_processor_version"), 64
),
"review": (
{
"case_id": _safe_text(review_case.get("case_id"), 64),
"status": _safe_text(review_case.get("case_status"), 64),
"revision": int(review_case.get("revision") or 0),
"completed_items": int(review_case.get("completed_items") or 0),
"total_items": int(review_case.get("total_items") or 0),
"manual_override_sha256": _safe_text(
review_case.get("manual_override_sha256"), 64
),
"frozen_at": _iso(review_case.get("frozen_at")),
"completed_at": _iso(review_case.get("completed_at")),
"cancelled_at": _iso(review_case.get("cancelled_at")),
}
if review_case is not None
else None
),
},
"evidence": {
"attempts": len(attempts),
@@ -674,6 +824,7 @@ def build_job_trace(
"legacy_direct_submissions": len(submissions),
"finance_versions": len(versions),
"outbox_events": len(outbox_events),
"review_events": len(review_events),
},
"logs": logs,
"refreshed_at": resolved_refreshed_at.isoformat(),