Manage daily reports by date and refresh monthly reports after deletion
This commit is contained in:
1 parent
976a4fa7b2
commit
2a54838d59
41 files changed
+2757
-89
No files matched your search
+179
-8
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user