From 3c7e9f2522c7c5da14dfcc597364fa74ef93c3f6 Mon Sep 17 00:00:00 2001 From: Misaka Date: Fri, 31 Jul 2026 21:56:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(db=5Fcompare):=20DB-based=20full=20summary?= =?UTF-8?q?=20report=20+=20wire=20=E8=B7=91=E6=AF=94=E5=AF=B9=20to=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 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 --- inbound_verify/db_compare.py | 128 ++++++++++++++++++++++++++++++++++- inbound_verify/runtime.py | 18 ++++- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/inbound_verify/db_compare.py b/inbound_verify/db_compare.py index 0e57f36..ff93494 100644 --- a/inbound_verify/db_compare.py +++ b/inbound_verify/db_compare.py @@ -25,7 +25,7 @@ COUNT(DISTINCT piece_no) 去重计数,改为 COUNT(*) 行计数。 import os from dataclasses import dataclass, field -from datetime import date, datetime +from datetime import date, datetime, timedelta import psycopg import yaml @@ -33,7 +33,7 @@ from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side 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" +# ============================== 全站汇总报表(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 + + # ============================== 终端验证入口 ============================== diff --git a/inbound_verify/runtime.py b/inbound_verify/runtime.py index 27c7195..54546d0 100644 --- a/inbound_verify/runtime.py +++ b/inbound_verify/runtime.py @@ -535,7 +535,19 @@ def _site_undelivered_handler(site): 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 = { @@ -564,8 +576,8 @@ TASK_HANDLERS = { force=force, date=date ), ("安能", "undelivered"): _site_undelivered_handler("安能"), - ("__compare__", "compare"): lambda ctx, force=False, date=None: ( - compare.main() or True + ("__compare__", "compare"): lambda ctx, force=False, date=None: _run_db_full_report( + date ), }