feat(db_compare): DB-based full summary report + wire 跑比对 to it
新增 build_full_report:4 站走 compare_site_date、百世走 baishi_daily_stats(基数) + undelivered_record(按 ingested_at 日期过滤),复用 compare.build_summary 渲染 KPI/柱状图/口径说明,产 output/应到未到数据.xlsx。跑比对入口(__compare__)从 compare.main() Excel 路径切换到 build_full_report。百世未到件用基数差(undelivered_pieces)与应到/已到自洽。 集成验证:前端跑比对 -> build_full_report -> /report 下载,各站未到件 顺心8/中通10/韵达2/安能0/百世8。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,7 @@ COUNT(DISTINCT piece_no) 去重计数,改为 COUNT(*) 行计数。
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
import psycopg
|
import psycopg
|
||||||
import yaml
|
import yaml
|
||||||
@@ -33,7 +33,7 @@ from openpyxl import Workbook
|
|||||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||||
|
|
||||||
from inbound_verify.paths import CONFIG_PATH, OUTPUT_DIR, DOWNLOAD_DIR
|
from inbound_verify.paths import CONFIG_PATH, OUTPUT_DIR, DOWNLOAD_DIR
|
||||||
from inbound_verify.domain import _site_cfg, ALL_REPORT_SITES
|
from inbound_verify.domain import _site_cfg, ALL_REPORT_SITES, BAISHI_COLUMNS
|
||||||
|
|
||||||
# ============================== 结果类型 ==============================
|
# ============================== 结果类型 ==============================
|
||||||
|
|
||||||
@@ -483,6 +483,130 @@ def _write_sheet(ws, result: CompareResult):
|
|||||||
ws.freeze_panes = "A2"
|
ws.freeze_panes = "A2"
|
||||||
|
|
||||||
|
|
||||||
|
# ============================== 全站汇总报表(DB 版)=============================
|
||||||
|
|
||||||
|
|
||||||
|
def _stats_to_dict(s: CompareStats) -> dict:
|
||||||
|
"""CompareStats -> build_summary 要的中文键 stats dict。"""
|
||||||
|
return {
|
||||||
|
"运单数": s.waybill_count,
|
||||||
|
"应到件": s.expected_pieces,
|
||||||
|
"已到件": s.arrived_pieces,
|
||||||
|
"未到件": s.undelivered_pieces,
|
||||||
|
"完全未到": s.full_miss,
|
||||||
|
"部分未到": s.part_miss,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _target_date_for(site: str) -> str:
|
||||||
|
"""4 站比对锚点:today - expected_offset(与 _site_undelivered_handler 一致)。"""
|
||||||
|
from inbound_verify import state_store # 懒导入,避免成环
|
||||||
|
|
||||||
|
offset = state_store.get_offset(site, "expected")
|
||||||
|
return (date.today() - timedelta(days=offset)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def _baishi_from_pg(cur, target: str):
|
||||||
|
"""查百世当日基数(baishi_daily_stats)+ 当天未到明细(undelivered_record 按 ingested_at 过滤)。
|
||||||
|
返回 (stats_dict_or_None, rows_or_None);基数与明细均无 → (None, None)。
|
||||||
|
|
||||||
|
undelivered_record 是 UPSERT 累积表;按 ingested_at::date = target 取当天入库的未到快照
|
||||||
|
(= 当天下载的当前未到,站点已剔除已到),避免累积偏大。
|
||||||
|
"""
|
||||||
|
cur.execute(
|
||||||
|
"SELECT expected_pieces, arrived_pieces, undelivered_pieces "
|
||||||
|
"FROM baishi_daily_stats WHERE site = %s AND business_date = %s",
|
||||||
|
("百世", target),
|
||||||
|
)
|
||||||
|
basis = cur.fetchone()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT waybill_no, piece_no, biz_type, last_scan FROM undelivered_record "
|
||||||
|
"WHERE site = %s AND ingested_at::date = %s",
|
||||||
|
("百世", target),
|
||||||
|
)
|
||||||
|
detail = cur.fetchall()
|
||||||
|
if basis is None and not detail:
|
||||||
|
return (None, None)
|
||||||
|
exp = basis[0] if basis else None
|
||||||
|
arr = basis[1] if basis else None
|
||||||
|
# 未到件优先取基数差(baishi_daily_stats.undelivered_pieces,与应到/已到同源自洽);
|
||||||
|
# 基数缺失时退回明细行数。
|
||||||
|
undel = basis[2] if (basis and basis[2] is not None) else len(detail)
|
||||||
|
wb_count = len({r[0] for r in detail if r[0]}) # 运单号去重
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"类型": r[2] or "",
|
||||||
|
"子单号": r[1] or "",
|
||||||
|
"运单号": r[0] or "",
|
||||||
|
"最新扫描记录": r[3] or "",
|
||||||
|
}
|
||||||
|
for r in detail
|
||||||
|
]
|
||||||
|
stats = {
|
||||||
|
"运单数": wb_count,
|
||||||
|
"应到件": exp,
|
||||||
|
"已到件": arr,
|
||||||
|
"未到件": undel,
|
||||||
|
"完全未到": None,
|
||||||
|
"部分未到": None,
|
||||||
|
}
|
||||||
|
return (stats, rows)
|
||||||
|
|
||||||
|
|
||||||
|
def build_full_report(date=None) -> str:
|
||||||
|
"""DB 版全站汇总报表:4 站走 DB 比对、百世走 PG,复用 compare.build_summary 渲染。
|
||||||
|
产出 output/应到未到数据.xlsx(/report 下载)。date=None 时各站按 expected_offset 算锚点。
|
||||||
|
返回输出路径。"""
|
||||||
|
from inbound_verify import compare # 复用 build_summary / write_station / OUTFILE
|
||||||
|
|
||||||
|
print("[db_compare] 开始生成全站汇总报表 ...")
|
||||||
|
wb = Workbook()
|
||||||
|
wb.remove(wb.active)
|
||||||
|
summary_ws = wb.create_sheet("汇总报表")
|
||||||
|
|
||||||
|
results = [] # [(name, stats_dict_or_None)],顺序 ALL_REPORT_SITES
|
||||||
|
site_targets = {} # name -> target_date(汇总表"数据日期"列)
|
||||||
|
|
||||||
|
conn = _connect()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
for name in ALL_REPORT_SITES:
|
||||||
|
if name == "百世":
|
||||||
|
target = date or datetime.now().strftime("%Y-%m-%d")
|
||||||
|
site_targets[name] = target
|
||||||
|
stats, rows = _baishi_from_pg(cur, target)
|
||||||
|
results.append((name, stats))
|
||||||
|
if rows is not None:
|
||||||
|
compare.write_station(wb.create_sheet(name), BAISHI_COLUMNS, rows)
|
||||||
|
continue
|
||||||
|
if name not in SITE_COMPARE_CONFIG:
|
||||||
|
results.append((name, None))
|
||||||
|
continue
|
||||||
|
target = date or _target_date_for(name)
|
||||||
|
site_targets[name] = target
|
||||||
|
result = compare_site_date(name, target)
|
||||||
|
if result is not None:
|
||||||
|
results.append((name, _stats_to_dict(result.stats)))
|
||||||
|
_write_sheet(wb.create_sheet(name), result)
|
||||||
|
else:
|
||||||
|
results.append((name, None))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
compare.build_summary(
|
||||||
|
summary_ws,
|
||||||
|
results,
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||||
|
dates=site_targets,
|
||||||
|
)
|
||||||
|
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||||
|
wb.save(compare.OUTFILE)
|
||||||
|
print(f"[db_compare] 全站汇总已输出: {compare.OUTFILE}")
|
||||||
|
for name, s in results:
|
||||||
|
print(f" {name}:未到 {s['未到件']} 件" if s else f" {name}:无数据,跳过")
|
||||||
|
return compare.OUTFILE
|
||||||
|
|
||||||
|
|
||||||
# ============================== 终端验证入口 ==============================
|
# ============================== 终端验证入口 ==============================
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -535,7 +535,19 @@ def _site_undelivered_handler(site):
|
|||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
|
||||||
# 「跑比对」= 纯离线比对(用 downloads/ 现有文件生成全站汇总;下载交由各站定时/手动)。
|
# 「跑比对」= DB 版全站汇总报表(替代旧 compare.main Excel 路径;下载交由各站定时/手动)。
|
||||||
|
|
||||||
|
|
||||||
|
def _run_db_full_report(date=None):
|
||||||
|
"""生成 DB 版全站汇总报表(output/应到未到数据.xlsx)。
|
||||||
|
懒导入 db_compare,best-effort:失败只告警,返回 True(与旧 lambda 契约一致)。"""
|
||||||
|
try:
|
||||||
|
from inbound_verify import db_compare
|
||||||
|
|
||||||
|
db_compare.build_full_report(date)
|
||||||
|
except Exception as e:
|
||||||
|
print(f">> [跑比对] DB 汇总报表生成失败: {e}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
TASK_HANDLERS = {
|
TASK_HANDLERS = {
|
||||||
@@ -564,8 +576,8 @@ TASK_HANDLERS = {
|
|||||||
force=force, date=date
|
force=force, date=date
|
||||||
),
|
),
|
||||||
("安能", "undelivered"): _site_undelivered_handler("安能"),
|
("安能", "undelivered"): _site_undelivered_handler("安能"),
|
||||||
("__compare__", "compare"): lambda ctx, force=False, date=None: (
|
("__compare__", "compare"): lambda ctx, force=False, date=None: _run_db_full_report(
|
||||||
compare.main() or True
|
date
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user