Manage daily reports by date and refresh monthly reports after deletion

This commit is contained in:
Wyndham ARR committed 2026-10-09 11:09:33 +08:00
1 parent 976a4fa7b2
commit 2a54838d59
41 files changed
+2757 -89

No files matched your search

+179 -8
View File
@@ -27,6 +27,15 @@ class PortalDataError(RuntimeError):
class PortalRepository(Protocol):
def list_daily_reports(self, month_key: str, limit: int = 50, offset: int = 0):
...
def get_daily_report(self, business_date: str):
...
def list_daily_report_history(self, business_date: str, limit: int = 50, offset: int = 0):
...
def list_jobs(
self,
month_key: str,
@@ -78,6 +87,15 @@ class UnavailablePortalRepository:
def _raise() -> None:
raise PortalDataError("DATABASE_UNAVAILABLE", "数据库读取服务暂不可用")
def list_daily_reports(self, month_key, limit=50, offset=0):
self._raise()
def get_daily_report(self, business_date):
self._raise()
def list_daily_report_history(self, business_date, limit=50, offset=0):
self._raise()
def list_jobs(
self,
month_key: str,
@@ -225,6 +243,61 @@ WHERE run.pipeline_type = 'opera_daily'
)
""".strip()
# Date-level views deliberately leave the original task-log API intact.
DAILY_REPORT_JOBS_SQL = """
SELECT run.run_key,
CASE run.run_status WHEN 'accepted' THEN 'succeeded'
WHEN 'awaiting_review' THEN 'needs_review' WHEN 'rejected' THEN 'failed'
ELSE run.run_status END,
run.business_date, run.failure_code, run.created_at, run.updated_at,
run.finished_at, run.uploaded_filename, source.byte_size,
version.retained_rows, version.version_no, daily.sha256,
coalesce(rooms.total, 0), review.case_key, review.case_status,
review.revision, coalesce(review.completed, 0), coalesce(review.total, 0),
version.id, current_version.daily_version_id IS NOT NULL,
retirement.id IS NOT NULL
FROM ingestion.processing_runs AS run
JOIN ingestion.artifacts AS source ON source.id = run.source_artifact_id
LEFT JOIN finance.daily_versions AS version ON version.processing_run_id = run.id
LEFT JOIN finance.current_daily_versions AS current_version
ON current_version.daily_version_id = version.id
LEFT JOIN ingestion.daily_run_retirements AS retirement ON retirement.processing_run_id = run.id
LEFT JOIN ingestion.artifacts AS daily ON daily.id = version.daily_report_artifact_id
LEFT JOIN LATERAL (
SELECT sum(record.no_of_rooms) AS total FROM finance.daily_records AS record
WHERE record.daily_version_id = version.id AND record.outcome = 'retained'
) AS rooms ON true
LEFT JOIN LATERAL (
SELECT case_row.case_key, case_row.case_status, case_row.revision,
count(item.id) AS total,
count(item.id) FILTER (WHERE item.real_price IS NOT NULL) AS completed
FROM ingestion.daily_review_cases AS case_row
LEFT JOIN ingestion.daily_review_items AS item ON item.review_case_id = case_row.id
WHERE case_row.processing_run_id = run.id
GROUP BY case_row.case_key, case_row.case_status, case_row.revision
) AS review ON true
WHERE run.pipeline_type = 'opera_daily' AND run.business_date = ANY(%s)
ORDER BY run.created_at DESC, run.id DESC
""".strip()
DAILY_REPORT_DATES_SQL = """
SELECT DISTINCT run.business_date
FROM ingestion.processing_runs AS run
LEFT JOIN ingestion.daily_run_retirements AS retirement ON retirement.processing_run_id = run.id
WHERE run.pipeline_type = 'opera_daily' AND run.business_date >= %s AND run.business_date < %s
AND retirement.id IS NULL
ORDER BY run.business_date DESC
LIMIT %s OFFSET %s
""".strip()
DAILY_REPORT_COUNT_SQL = """
SELECT count(DISTINCT run.business_date)
FROM ingestion.processing_runs AS run
LEFT JOIN ingestion.daily_run_retirements AS retirement ON retirement.processing_run_id = run.id
WHERE run.pipeline_type = 'opera_daily' AND run.business_date >= %s AND run.business_date < %s
AND retirement.id IS NULL
""".strip()
JOB_TRACE_RUN_SQL = """
SELECT
@@ -439,7 +512,7 @@ SELECT
FROM reporting.monthly_runs AS run
LEFT JOIN ingestion.artifacts AS artifact
ON artifact.id = run.workbook_artifact_id
WHERE run.period_start = %s
WHERE run.period_start = %s AND run.report_status <> 'withdrawn'
ORDER BY run.version_no DESC, run.id DESC
LIMIT %s OFFSET %s
""".strip()
@@ -448,7 +521,7 @@ LIMIT %s OFFSET %s
MONTHLY_RUNS_COUNT_SQL = """
SELECT count(*)
FROM reporting.monthly_runs AS run
WHERE run.period_start = %s
WHERE run.period_start = %s AND run.report_status <> 'withdrawn'
""".strip()
@@ -471,6 +544,7 @@ WITH daily AS (
monthly AS (
SELECT run.period_start, count(*) AS record_count
FROM reporting.monthly_runs AS run
WHERE run.report_status <> 'withdrawn'
GROUP BY run.period_start
)
SELECT
@@ -594,8 +668,17 @@ class PostgresPortalRepository:
(start, end, start, end, limit, offset),
)
rows = list(cursor.fetchall())
return [
{
return [self._job_from_row(row) for row in rows], total
except PortalDataError:
raise
except Exception:
raise PortalDataError("DATABASE_QUERY_FAILED", "日报记录查询失败") from None
finally:
connection.close()
@staticmethod
def _job_from_row(row):
return {
"job_id": str(row[0]),
"status": str(row[1]),
"arrival_date": _iso(row[2]),
@@ -615,15 +698,97 @@ class PostgresPortalRepository:
"review_completed_items": int(row[16] or 0),
"review_total_items": int(row[17] or 0),
}
for row in rows
], total
@classmethod
def _report_job(cls, row):
job = cls._job_from_row(row)
retired = bool(row[20])
job.update(daily_version_id=int(row[18]) if row[18] is not None else None,
is_current=bool(row[19]) and not retired, is_retired=retired,
can_delete=not retired and job["status"] in {"succeeded", "failed", "cancelled", "needs_review"}
and job["review_status"] not in {"generating", "generation_processing"})
if retired:
job["status"] = "deleted"
job["daily_report_sha256"] = None
return job
@staticmethod
def _date_group(day, jobs):
active = [job for job in jobs if not job["is_retired"]]
return {"business_date": day.isoformat(),
"current_job": next((job for job in active if job["is_current"]), None),
"latest_job": active[0] if active else None,
"history_count": max(0, len(jobs) - 1)}
def list_daily_reports(self, month_key, limit=50, offset=0):
start, end = self._month_bounds(month_key)
connection = self._open()
try:
with connection.transaction():
with connection.cursor() as cursor:
self._begin(cursor)
cursor.execute(DAILY_REPORT_COUNT_SQL, (start, end))
total = int(cursor.fetchone()[0])
cursor.execute(DAILY_REPORT_DATES_SQL, (start, end, limit, offset))
days = [row[0] for row in cursor.fetchall()]
if not days:
return [], total
cursor.execute(DAILY_REPORT_JOBS_SQL, (days,))
jobs = [self._report_job(row) for row in cursor.fetchall()]
return [self._date_group(day, [job for job in jobs if job["arrival_date"] == day.isoformat()])
for day in days], total
except PortalDataError:
raise
except Exception:
raise PortalDataError("DATABASE_QUERY_FAILED", "日报记录查询失败") from None
raise PortalDataError("DATABASE_QUERY_FAILED", "日报日期查询失败") from None
finally:
connection.close()
def get_daily_report(self, business_date):
day = date.fromisoformat(business_date)
connection = self._open()
try:
with connection.transaction():
with connection.cursor() as cursor:
self._begin(cursor)
cursor.execute(DAILY_REPORT_JOBS_SQL, ([day],))
jobs = [self._report_job(row) for row in cursor.fetchall()]
return self._date_group(day, jobs)
except PortalDataError:
raise
except Exception:
raise PortalDataError("DATABASE_QUERY_FAILED", "日报日期查询失败") from None
finally:
connection.close()
def list_daily_report_history(self, business_date, limit=50, offset=0):
day = date.fromisoformat(business_date)
connection = self._open()
try:
with connection.transaction():
with connection.cursor() as cursor:
self._begin(cursor)
cursor.execute(DAILY_REPORT_JOBS_SQL, ([day],))
jobs = [self._report_job(row) for row in cursor.fetchall()]
return jobs[offset:offset + limit], len(jobs)
except PortalDataError:
raise
except Exception:
raise PortalDataError("DATABASE_QUERY_FAILED", "日报历史查询失败") from None
finally:
connection.close()
@staticmethod
def _is_retired(cursor, job_id):
# Old, explicitly supported read-only deployments may predate migration022.
cursor.execute("SELECT to_regclass('ingestion.daily_run_retirements')")
if cursor.fetchone()[0] is None:
return False
cursor.execute("""SELECT 1 FROM ingestion.daily_run_retirements AS retirement
JOIN ingestion.processing_runs AS run ON run.id = retirement.processing_run_id
WHERE run.run_key = %s""", (job_id,))
return cursor.fetchone() is not None
def get_job_trace(self, job_id: str) -> Dict[str, Any]:
connection = self._open()
try:
@@ -664,6 +829,7 @@ class PostgresPortalRepository:
),
"result_schema_version": str(row[19]) if row[19] else None,
}
retired = self._is_retired(cursor, job_id)
run_id = int(row[0])
cursor.execute(JOB_TRACE_ATTEMPTS_SQL, (run_id,))
@@ -815,7 +981,7 @@ class PostgresPortalRepository:
}
for item in cursor.fetchall()
]
return build_job_trace(
trace = build_job_trace(
run,
attempts=attempts,
deliveries=deliveries,
@@ -825,6 +991,9 @@ class PostgresPortalRepository:
review_case=review_case,
review_events=review_events,
)
if retired:
trace["job"].update(status="deleted", is_retired=True, active=False)
return trace
except PortalDataError:
raise
except Exception:
@@ -940,6 +1109,8 @@ class PostgresPortalRepository:
with connection.transaction():
with connection.cursor() as cursor:
self._begin(cursor)
if statement == DAILY_DOWNLOAD_SQL and self._is_retired(cursor, str(value)):
raise PortalDataError("DOWNLOAD_NOT_FOUND", "该日报已删除")
cursor.execute(statement, (value,))
rows = list(cursor.fetchall())
if len(rows) != 1: