feat: sync latest ARR implementation
This commit is contained in:
722
booking_ingestion/excel_postgres.py
Normal file
722
booking_ingestion/excel_postgres.py
Normal file
@@ -0,0 +1,722 @@
|
||||
"""Atomic PostgreSQL activation for deterministic Booking Excel imports."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Mapping, Optional
|
||||
|
||||
from arr_ingestion.contracts import ArtifactRef
|
||||
|
||||
from .excel import (
|
||||
PROCESSOR_NAME,
|
||||
PROCESSOR_VERSION,
|
||||
RESULT_SCHEMA_VERSION,
|
||||
XLSX_MIME,
|
||||
ExcelDocument,
|
||||
build_excel_parse_result,
|
||||
canonical_json_bytes,
|
||||
rule_set_sha256,
|
||||
)
|
||||
|
||||
|
||||
TARGET_DATABASE = "booking_test"
|
||||
DATABASE_ENV = "ARR_DATABASE_URL"
|
||||
ARTIFACT_STORAGE_PROVIDER = "oss"
|
||||
ARTIFACT_BUCKET_ALIAS = "arr-private"
|
||||
|
||||
|
||||
class BookingExcelRepositoryError(RuntimeError):
|
||||
def __init__(self, code: str, safe_message: str, status: int = 503):
|
||||
super().__init__(safe_message)
|
||||
self.code = code
|
||||
self.safe_message = safe_message
|
||||
self.status = status
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DatabaseConfig:
|
||||
dsn: str
|
||||
|
||||
@classmethod
|
||||
def from_environment(cls) -> "DatabaseConfig":
|
||||
dsn = os.environ.get(DATABASE_ENV, "").strip()
|
||||
if not dsn:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_CONFIG_MISSING",
|
||||
"Booking Excel 数据库配置缺失",
|
||||
)
|
||||
return cls(dsn=dsn)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BookingSourceSummary:
|
||||
source_batch_id: int
|
||||
source_kind: str
|
||||
filename: Optional[str]
|
||||
source_rows: int
|
||||
worksheet_count: int
|
||||
distinct_group_codes: int
|
||||
room_quantity: int
|
||||
activated_at: Optional[datetime]
|
||||
disposition: str = "current"
|
||||
|
||||
def to_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"source_batch_id": self.source_batch_id,
|
||||
"source_type": (
|
||||
"excel" if self.source_kind == "booking_excel" else "historical"
|
||||
),
|
||||
"filename": self.filename if self.source_kind == "booking_excel" else None,
|
||||
"source_rows": self.source_rows,
|
||||
"worksheet_count": self.worksheet_count,
|
||||
"distinct_group_codes": self.distinct_group_codes,
|
||||
"room_quantity": self.room_quantity,
|
||||
"activated_at": (
|
||||
self.activated_at.isoformat() if self.activated_at is not None else None
|
||||
),
|
||||
"disposition": self.disposition,
|
||||
}
|
||||
|
||||
|
||||
CURRENT_SOURCE_SQL = """
|
||||
SELECT
|
||||
batch.id,
|
||||
batch.source_kind,
|
||||
CASE
|
||||
WHEN batch.source_kind = 'booking_excel' THEN artifact.original_filename
|
||||
ELSE NULL
|
||||
END AS uploaded_filename,
|
||||
count(source.id)::bigint AS source_rows,
|
||||
count(DISTINCT source.source_worksheet)::bigint AS worksheet_count,
|
||||
count(DISTINCT source.group_code_key)::bigint AS distinct_group_codes,
|
||||
COALESCE(sum(source.no_of_rooms), 0)::bigint AS room_quantity,
|
||||
active_source.activated_at
|
||||
FROM booking.current_source_batch AS active_source
|
||||
JOIN booking.source_batches AS batch
|
||||
ON batch.id = active_source.source_batch_id
|
||||
JOIN ingestion.artifacts AS artifact
|
||||
ON artifact.id = batch.source_artifact_id
|
||||
LEFT JOIN booking.source_rows AS source
|
||||
ON source.source_batch_id = batch.id
|
||||
WHERE active_source.singleton
|
||||
AND batch.batch_status = 'accepted'
|
||||
GROUP BY
|
||||
batch.id,
|
||||
batch.source_kind,
|
||||
artifact.original_filename,
|
||||
active_source.activated_at
|
||||
""".strip()
|
||||
|
||||
|
||||
BATCH_SUMMARY_SQL = """
|
||||
SELECT
|
||||
batch.id,
|
||||
batch.source_kind,
|
||||
CASE
|
||||
WHEN batch.source_kind = 'booking_excel' THEN artifact.original_filename
|
||||
ELSE NULL
|
||||
END AS uploaded_filename,
|
||||
count(source.id)::bigint AS source_rows,
|
||||
count(DISTINCT source.source_worksheet)::bigint AS worksheet_count,
|
||||
count(DISTINCT source.group_code_key)::bigint AS distinct_group_codes,
|
||||
COALESCE(sum(source.no_of_rooms), 0)::bigint AS room_quantity,
|
||||
active_source.activated_at
|
||||
FROM booking.source_batches AS batch
|
||||
JOIN ingestion.artifacts AS artifact
|
||||
ON artifact.id = batch.source_artifact_id
|
||||
LEFT JOIN booking.source_rows AS source
|
||||
ON source.source_batch_id = batch.id
|
||||
LEFT JOIN booking.current_source_batch AS active_source
|
||||
ON active_source.source_batch_id = batch.id
|
||||
AND active_source.singleton
|
||||
WHERE batch.id = %s
|
||||
AND batch.batch_status = 'accepted'
|
||||
GROUP BY
|
||||
batch.id,
|
||||
batch.source_kind,
|
||||
artifact.original_filename,
|
||||
active_source.activated_at
|
||||
""".strip()
|
||||
|
||||
|
||||
def _default_connect(dsn: str) -> Any:
|
||||
try:
|
||||
import psycopg # type: ignore[import-not-found]
|
||||
except ImportError:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_DRIVER_UNAVAILABLE",
|
||||
"Booking Excel 数据库驱动不可用",
|
||||
) from None
|
||||
try:
|
||||
return psycopg.connect(dsn, autocommit=False)
|
||||
except Exception:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_UNAVAILABLE",
|
||||
"Booking Excel 数据库暂不可用",
|
||||
) from None
|
||||
|
||||
|
||||
class PostgresBookingExcelRepository:
|
||||
def __init__(
|
||||
self,
|
||||
config: DatabaseConfig,
|
||||
connect: Optional[Callable[[str], Any]] = None,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._connect = connect or _default_connect
|
||||
|
||||
def _open(self) -> Any:
|
||||
try:
|
||||
return self._connect(self._config.dsn)
|
||||
except BookingExcelRepositoryError:
|
||||
raise
|
||||
except Exception:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_UNAVAILABLE",
|
||||
"Booking Excel 数据库暂不可用",
|
||||
) from None
|
||||
|
||||
@staticmethod
|
||||
def _begin(cursor: Any, *, read_only: bool = False) -> None:
|
||||
mode = "REPEATABLE READ READ ONLY" if read_only else "SERIALIZABLE"
|
||||
cursor.execute(f"SET TRANSACTION ISOLATION LEVEL {mode}")
|
||||
cursor.execute("SET LOCAL lock_timeout = '10s'")
|
||||
cursor.execute("SET LOCAL statement_timeout = '120s'")
|
||||
cursor.execute(
|
||||
"SELECT current_database(), to_regclass('booking.current_source_batch')"
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row or row[0] != TARGET_DATABASE or row[1] is None:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_NOT_READY",
|
||||
"Booking Excel 数据库迁移尚未就绪",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _summary_from_row(
|
||||
row: object,
|
||||
*,
|
||||
disposition: str = "current",
|
||||
) -> Optional[BookingSourceSummary]:
|
||||
if row is None:
|
||||
return None
|
||||
values = tuple(row) # type: ignore[arg-type]
|
||||
return BookingSourceSummary(
|
||||
source_batch_id=int(values[0]),
|
||||
source_kind=str(values[1]),
|
||||
filename=str(values[2]) if values[2] else None,
|
||||
source_rows=int(values[3]),
|
||||
worksheet_count=int(values[4]),
|
||||
distinct_group_codes=int(values[5]),
|
||||
room_quantity=int(values[6]),
|
||||
activated_at=values[7] if isinstance(values[7], datetime) else None,
|
||||
disposition=disposition,
|
||||
)
|
||||
|
||||
def current_source(self) -> Optional[BookingSourceSummary]:
|
||||
connection = self._open()
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
self._begin(cursor, read_only=True)
|
||||
cursor.execute(CURRENT_SOURCE_SQL)
|
||||
return self._summary_from_row(cursor.fetchone())
|
||||
except BookingExcelRepositoryError:
|
||||
raise
|
||||
except Exception:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_UNAVAILABLE",
|
||||
"当前 Excel 数据源暂时无法读取",
|
||||
) from None
|
||||
finally:
|
||||
try:
|
||||
connection.rollback()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
@staticmethod
|
||||
def _find_existing_batch(cursor: Any, source_sha256: str) -> Optional[int]:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT batch.id
|
||||
FROM booking.source_batches AS batch
|
||||
JOIN ingestion.artifacts AS artifact
|
||||
ON artifact.id = batch.source_artifact_id
|
||||
WHERE batch.source_kind = 'booking_excel'
|
||||
AND batch.batch_status = 'accepted'
|
||||
AND artifact.artifact_kind = 'booking_excel'
|
||||
AND artifact.sha256 = %s
|
||||
ORDER BY batch.id
|
||||
LIMIT 1
|
||||
FOR UPDATE OF batch
|
||||
""",
|
||||
(source_sha256,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
return int(row[0]) if row is not None else None
|
||||
|
||||
@staticmethod
|
||||
def _activate(cursor: Any, source_batch_id: int) -> bool:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT source_batch_id
|
||||
FROM booking.current_source_batch
|
||||
WHERE singleton
|
||||
FOR UPDATE
|
||||
"""
|
||||
)
|
||||
current = cursor.fetchone()
|
||||
already_active = current is not None and int(current[0]) == source_batch_id
|
||||
if not already_active:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO booking.current_source_batch (
|
||||
singleton,
|
||||
source_batch_id,
|
||||
activated_at
|
||||
)
|
||||
VALUES (true, %s, now())
|
||||
ON CONFLICT (singleton) DO UPDATE
|
||||
SET source_batch_id = EXCLUDED.source_batch_id,
|
||||
activated_at = EXCLUDED.activated_at
|
||||
""",
|
||||
(source_batch_id,),
|
||||
)
|
||||
return already_active
|
||||
|
||||
@staticmethod
|
||||
def _batch_summary(
|
||||
cursor: Any,
|
||||
source_batch_id: int,
|
||||
disposition: str,
|
||||
) -> BookingSourceSummary:
|
||||
cursor.execute(BATCH_SUMMARY_SQL, (source_batch_id,))
|
||||
summary = PostgresBookingExcelRepository._summary_from_row(
|
||||
cursor.fetchone(),
|
||||
disposition=disposition,
|
||||
)
|
||||
if summary is None:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_STATE_INVALID",
|
||||
"Booking Excel 数据源状态无效",
|
||||
)
|
||||
return summary
|
||||
|
||||
def activate_existing(
|
||||
self,
|
||||
source_sha256: str,
|
||||
) -> Optional[BookingSourceSummary]:
|
||||
connection = self._open()
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
self._begin(cursor)
|
||||
cursor.execute(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))",
|
||||
("arr_booking_excel_import",),
|
||||
)
|
||||
source_batch_id = self._find_existing_batch(cursor, source_sha256)
|
||||
if source_batch_id is None:
|
||||
connection.rollback()
|
||||
return None
|
||||
already_active = self._activate(cursor, source_batch_id)
|
||||
summary = self._batch_summary(
|
||||
cursor,
|
||||
source_batch_id,
|
||||
"already_active" if already_active else "reactivated",
|
||||
)
|
||||
connection.commit()
|
||||
return summary
|
||||
except BookingExcelRepositoryError:
|
||||
connection.rollback()
|
||||
raise
|
||||
except Exception:
|
||||
connection.rollback()
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_WRITE_FAILED",
|
||||
"Excel 已校验,但数据源未能完成入库",
|
||||
) from None
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
@staticmethod
|
||||
def _ensure_artifact(cursor: Any, reference: ArtifactRef) -> int:
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
id,
|
||||
artifact_kind,
|
||||
original_filename,
|
||||
sha256,
|
||||
byte_size,
|
||||
mime_type
|
||||
FROM ingestion.artifacts
|
||||
WHERE storage_provider = %s
|
||||
AND bucket_alias = %s
|
||||
AND object_key = %s
|
||||
AND object_version_id IS NULL
|
||||
FOR SHARE
|
||||
""",
|
||||
(
|
||||
ARTIFACT_STORAGE_PROVIDER,
|
||||
ARTIFACT_BUCKET_ALIAS,
|
||||
reference.object_key,
|
||||
),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if row is not None:
|
||||
if (
|
||||
str(row[1]) != reference.file_kind
|
||||
or str(row[2]) != reference.original_filename
|
||||
or str(row[3]) != reference.sha256
|
||||
or int(row[4]) != reference.byte_size
|
||||
or str(row[5] or "") != reference.mime_type
|
||||
):
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_ARTIFACT_CONFLICT",
|
||||
"Excel 工件身份冲突",
|
||||
409,
|
||||
)
|
||||
return int(row[0])
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ingestion.artifacts (
|
||||
artifact_kind,
|
||||
storage_provider,
|
||||
bucket_alias,
|
||||
object_key,
|
||||
original_filename,
|
||||
sha256,
|
||||
byte_size,
|
||||
mime_type
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
reference.file_kind,
|
||||
ARTIFACT_STORAGE_PROVIDER,
|
||||
ARTIFACT_BUCKET_ALIAS,
|
||||
reference.object_key,
|
||||
reference.original_filename,
|
||||
reference.sha256,
|
||||
reference.byte_size,
|
||||
reference.mime_type,
|
||||
),
|
||||
)
|
||||
return int(cursor.fetchone()[0])
|
||||
|
||||
def import_document(
|
||||
self,
|
||||
job_id: str,
|
||||
source: ArtifactRef,
|
||||
document: ExcelDocument,
|
||||
) -> BookingSourceSummary:
|
||||
if (
|
||||
source.role != "booking_source"
|
||||
or source.file_kind != "booking_excel"
|
||||
or source.mime_type != XLSX_MIME
|
||||
or source.sha256 != document.source_sha256
|
||||
or source.byte_size != document.source_byte_size
|
||||
):
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_ARTIFACT_INVALID",
|
||||
"Excel 工件身份无效",
|
||||
422,
|
||||
)
|
||||
if document.pending_item_count:
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_REVIEW_REQUIRED",
|
||||
"仍有房型需要人工确认或删除",
|
||||
409,
|
||||
)
|
||||
|
||||
connection = self._open()
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
self._begin(cursor)
|
||||
cursor.execute(
|
||||
"SELECT pg_advisory_xact_lock(hashtextextended(%s, 0))",
|
||||
("arr_booking_excel_import",),
|
||||
)
|
||||
existing_batch_id = self._find_existing_batch(
|
||||
cursor,
|
||||
document.source_sha256,
|
||||
)
|
||||
if existing_batch_id is not None:
|
||||
already_active = self._activate(cursor, existing_batch_id)
|
||||
summary = self._batch_summary(
|
||||
cursor,
|
||||
existing_batch_id,
|
||||
"already_active" if already_active else "reactivated",
|
||||
)
|
||||
connection.commit()
|
||||
return summary
|
||||
|
||||
artifact_id = self._ensure_artifact(cursor, source)
|
||||
delivery = {
|
||||
"source_sha256": document.source_sha256,
|
||||
"source_rows": len(document.rows),
|
||||
"worksheet_count": document.worksheet_count,
|
||||
"distinct_group_codes": document.distinct_group_code_count,
|
||||
"room_quantity": document.room_quantity,
|
||||
}
|
||||
delivery_bytes = canonical_json_bytes(delivery)
|
||||
delivery_sha256 = hashlib.sha256(delivery_bytes).hexdigest()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ingestion.processing_runs (
|
||||
run_key,
|
||||
pipeline_type,
|
||||
source_artifact_id,
|
||||
run_status,
|
||||
requested_processor_version,
|
||||
requested_rule_set_sha256,
|
||||
delivered_processor_version,
|
||||
delivered_rule_set_sha256,
|
||||
result_schema_version,
|
||||
delivery_sha256,
|
||||
delivery_json
|
||||
)
|
||||
VALUES (
|
||||
%s,
|
||||
'booking_source_import',
|
||||
%s,
|
||||
'validating',
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s::jsonb
|
||||
)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
job_id,
|
||||
artifact_id,
|
||||
PROCESSOR_VERSION,
|
||||
rule_set_sha256(),
|
||||
PROCESSOR_VERSION,
|
||||
rule_set_sha256(),
|
||||
RESULT_SCHEMA_VERSION,
|
||||
delivery_sha256,
|
||||
delivery_bytes.decode("utf-8"),
|
||||
),
|
||||
)
|
||||
processing_run_id = int(cursor.fetchone()[0])
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO booking.source_batches (
|
||||
source_artifact_id,
|
||||
source_kind,
|
||||
source_format_version,
|
||||
batch_status,
|
||||
source_rows,
|
||||
accepted_rows,
|
||||
failed_rows
|
||||
)
|
||||
VALUES (
|
||||
%s,
|
||||
'booking_excel',
|
||||
'xlsx/1.0',
|
||||
'processing',
|
||||
%s,
|
||||
0,
|
||||
0
|
||||
)
|
||||
RETURNING id
|
||||
""",
|
||||
(artifact_id, len(document.rows)),
|
||||
)
|
||||
source_batch_id = int(cursor.fetchone()[0])
|
||||
|
||||
for row in document.rows:
|
||||
row_sha256 = row.sha256()
|
||||
result = build_excel_parse_result(row)
|
||||
result_bytes = canonical_json_bytes(result)
|
||||
result_sha256 = hashlib.sha256(result_bytes).hexdigest()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO booking.source_rows (
|
||||
source_batch_id,
|
||||
source_worksheet,
|
||||
source_row_no,
|
||||
group_code_raw,
|
||||
type_of_room_raw,
|
||||
no_of_rooms,
|
||||
source_row_sha256
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
source_batch_id,
|
||||
row.worksheet,
|
||||
row.row_no,
|
||||
row.group_code_raw,
|
||||
row.type_of_room_raw,
|
||||
row.no_of_rooms,
|
||||
row_sha256,
|
||||
),
|
||||
)
|
||||
source_row_id = int(cursor.fetchone()[0])
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO booking.parse_versions (
|
||||
source_row_id,
|
||||
version_no,
|
||||
parse_status,
|
||||
result_schema_version,
|
||||
processor_name,
|
||||
processor_version,
|
||||
rule_set_sha256,
|
||||
input_row_sha256,
|
||||
result_sha256,
|
||||
result_json,
|
||||
validated_at
|
||||
)
|
||||
VALUES (
|
||||
%s,
|
||||
1,
|
||||
'accepted',
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s,
|
||||
%s::jsonb,
|
||||
now()
|
||||
)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
source_row_id,
|
||||
RESULT_SCHEMA_VERSION,
|
||||
PROCESSOR_NAME,
|
||||
PROCESSOR_VERSION,
|
||||
rule_set_sha256(),
|
||||
row_sha256,
|
||||
result_sha256,
|
||||
result_bytes.decode("utf-8"),
|
||||
),
|
||||
)
|
||||
parse_version_id = int(cursor.fetchone()[0])
|
||||
for item in row.room_items:
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO booking.room_items (
|
||||
parse_version_id,
|
||||
item_no,
|
||||
room_type_raw,
|
||||
room_type_code,
|
||||
quantity,
|
||||
unit_price,
|
||||
currency_code,
|
||||
price_token_raw,
|
||||
source_fragment
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, NULL, NULL, NULL, %s)
|
||||
""",
|
||||
(
|
||||
parse_version_id,
|
||||
item.item_no,
|
||||
item.room_type_raw,
|
||||
item.room_type_code,
|
||||
item.quantity,
|
||||
item.source_fragment,
|
||||
),
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE booking.source_batches
|
||||
SET batch_status = 'accepted',
|
||||
accepted_rows = source_rows,
|
||||
failed_rows = 0,
|
||||
validated_at = now(),
|
||||
finished_at = now()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(source_batch_id,),
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO booking.current_row_parses (
|
||||
source_row_id,
|
||||
parse_version_id
|
||||
)
|
||||
SELECT source.id, parsed.id
|
||||
FROM booking.source_rows AS source
|
||||
JOIN booking.parse_versions AS parsed
|
||||
ON parsed.source_row_id = source.id
|
||||
AND parsed.version_no = 1
|
||||
AND parsed.parse_status = 'accepted'
|
||||
WHERE source.source_batch_id = %s
|
||||
""",
|
||||
(source_batch_id,),
|
||||
)
|
||||
self._activate(cursor, source_batch_id)
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE ingestion.processing_runs
|
||||
SET run_status = 'accepted',
|
||||
updated_at = now(),
|
||||
validated_at = now(),
|
||||
finished_at = now()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(processing_run_id,),
|
||||
)
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO ingestion.outbox_events (
|
||||
event_key,
|
||||
aggregate_type,
|
||||
aggregate_id,
|
||||
event_type,
|
||||
payload
|
||||
)
|
||||
VALUES (
|
||||
%s,
|
||||
'booking_source_batch',
|
||||
%s,
|
||||
'booking.source_batch.accepted',
|
||||
%s::jsonb
|
||||
)
|
||||
""",
|
||||
(
|
||||
f"booking-source-batch:{source_batch_id}:accepted",
|
||||
source_batch_id,
|
||||
json.dumps(
|
||||
{
|
||||
"source_batch_id": source_batch_id,
|
||||
"source_rows": len(document.rows),
|
||||
"distinct_group_codes": document.distinct_group_code_count,
|
||||
},
|
||||
separators=(",", ":"),
|
||||
),
|
||||
),
|
||||
)
|
||||
summary = self._batch_summary(
|
||||
cursor,
|
||||
source_batch_id,
|
||||
"imported_and_activated",
|
||||
)
|
||||
connection.commit()
|
||||
return summary
|
||||
except BookingExcelRepositoryError:
|
||||
connection.rollback()
|
||||
raise
|
||||
except Exception:
|
||||
connection.rollback()
|
||||
raise BookingExcelRepositoryError(
|
||||
"BOOKING_EXCEL_DATABASE_WRITE_FAILED",
|
||||
"Excel 已校验,但数据源未能完成入库",
|
||||
) from None
|
||||
finally:
|
||||
connection.close()
|
||||
Reference in New Issue
Block a user