2026-04-20 15:02:17 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import csv
|
|
|
|
|
import io
|
2026-05-12 10:55:19 +08:00
|
|
|
import logging
|
|
|
|
|
import time
|
2026-04-20 15:02:17 +08:00
|
|
|
from typing import Any
|
|
|
|
|
|
2026-05-12 10:15:46 +08:00
|
|
|
import psycopg2
|
|
|
|
|
|
2026-05-12 10:55:19 +08:00
|
|
|
from inventory_analytics.logging_utils import log_event as emit_log_event
|
|
|
|
|
|
2026-04-20 15:02:17 +08:00
|
|
|
from .settings import AppConfig
|
2026-04-23 16:40:45 +08:00
|
|
|
from .specs import BrandSyncTarget
|
|
|
|
|
from .sql import DEFAULT_SYNC_TARGET, POSTGRES_QUERY, build_postgres_query
|
2026-04-20 15:02:17 +08:00
|
|
|
|
|
|
|
|
|
2026-05-12 10:55:19 +08:00
|
|
|
logger = logging.getLogger("inventory_sync.postgres")
|
|
|
|
|
|
|
|
|
|
LOG_FIELD_LABELS = {
|
|
|
|
|
"elapsed_ms": "耗时毫秒",
|
|
|
|
|
"row_count": "行数",
|
|
|
|
|
"stage": "阶段",
|
|
|
|
|
"statement_timeout_ms": "查询超时毫秒",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def log_event(level: int, event: str, **fields: Any) -> None:
|
|
|
|
|
emit_log_event(logger, LOG_FIELD_LABELS, level, event, **fields)
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 16:40:45 +08:00
|
|
|
def query_postgres(
|
|
|
|
|
config: AppConfig,
|
|
|
|
|
brand_name: str | None = None,
|
|
|
|
|
warehouse_keys: tuple[int | str, ...] | None = None,
|
|
|
|
|
target: BrandSyncTarget | None = None,
|
|
|
|
|
) -> dict[str, Any]:
|
2026-05-12 10:55:19 +08:00
|
|
|
started_at = time.monotonic()
|
|
|
|
|
statement_timeout_ms = 300_000
|
2026-04-23 16:40:45 +08:00
|
|
|
active_brand_name = brand_name or (target.brand_name if target else DEFAULT_SYNC_TARGET.brand_name)
|
|
|
|
|
active_warehouse_keys = warehouse_keys or (target.warehouse_keys if target else DEFAULT_SYNC_TARGET.warehouse_keys)
|
|
|
|
|
query = (
|
|
|
|
|
POSTGRES_QUERY
|
|
|
|
|
if brand_name is None and warehouse_keys is None and target is None
|
|
|
|
|
else build_postgres_query(
|
|
|
|
|
brand_name=active_brand_name,
|
|
|
|
|
warehouse_keys=active_warehouse_keys,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-05-12 10:15:46 +08:00
|
|
|
output = io.StringIO()
|
2026-04-20 15:02:17 +08:00
|
|
|
try:
|
2026-05-12 10:55:19 +08:00
|
|
|
log_event(logging.INFO, "开始连接PostgreSQL", stage="database_connect")
|
2026-05-12 10:15:46 +08:00
|
|
|
with psycopg2.connect(
|
|
|
|
|
host=config.analytics_db_host,
|
|
|
|
|
port=config.analytics_db_port,
|
|
|
|
|
user=config.analytics_db_user,
|
|
|
|
|
password=config.analytics_db_password,
|
|
|
|
|
dbname=config.analytics_db_name,
|
2026-05-12 10:55:19 +08:00
|
|
|
connect_timeout=10,
|
2026-05-12 10:15:46 +08:00
|
|
|
) as conn:
|
2026-05-12 10:55:19 +08:00
|
|
|
log_event(
|
|
|
|
|
logging.INFO,
|
|
|
|
|
"PostgreSQL连接完成",
|
|
|
|
|
stage="database_connect",
|
|
|
|
|
elapsed_ms=int((time.monotonic() - started_at) * 1000),
|
|
|
|
|
)
|
2026-05-12 10:15:46 +08:00
|
|
|
with conn.cursor() as cur:
|
|
|
|
|
cur.execute("SET TIME ZONE 'Asia/Shanghai';")
|
2026-05-12 10:55:19 +08:00
|
|
|
cur.execute("SET statement_timeout = %s;", (statement_timeout_ms,))
|
|
|
|
|
copy_started_at = time.monotonic()
|
|
|
|
|
log_event(
|
|
|
|
|
logging.INFO,
|
|
|
|
|
"开始执行渠道库存SQL",
|
|
|
|
|
stage="database_query",
|
|
|
|
|
statement_timeout_ms=statement_timeout_ms,
|
|
|
|
|
)
|
2026-05-12 10:15:46 +08:00
|
|
|
cur.copy_expert(f"COPY ({query}) TO STDOUT WITH CSV HEADER", output)
|
2026-05-12 10:55:19 +08:00
|
|
|
log_event(
|
|
|
|
|
logging.INFO,
|
|
|
|
|
"渠道库存SQL执行完成",
|
|
|
|
|
stage="database_query",
|
|
|
|
|
elapsed_ms=int((time.monotonic() - copy_started_at) * 1000),
|
|
|
|
|
)
|
2026-05-12 10:15:46 +08:00
|
|
|
except psycopg2.Error as exc:
|
|
|
|
|
raise RuntimeError(f"PostgreSQL 查询失败: {exc}") from exc
|
2026-04-20 15:02:17 +08:00
|
|
|
|
2026-05-12 10:55:19 +08:00
|
|
|
parse_started_at = time.monotonic()
|
2026-05-12 10:15:46 +08:00
|
|
|
output.seek(0)
|
|
|
|
|
rows = list(csv.DictReader(output))
|
2026-05-12 10:55:19 +08:00
|
|
|
log_event(
|
|
|
|
|
logging.INFO,
|
|
|
|
|
"渠道库存查询结果解析完成",
|
|
|
|
|
stage="database_parse",
|
|
|
|
|
row_count=len(rows),
|
|
|
|
|
elapsed_ms=int((time.monotonic() - parse_started_at) * 1000),
|
|
|
|
|
)
|
2026-04-20 15:02:17 +08:00
|
|
|
return {"row_count": len(rows), "rows": rows}
|