From 82c80fc859606cfbff9034d6811dccd562289c1e Mon Sep 17 00:00:00 2001 From: Misaka Date: Fri, 17 Jul 2026 23:19:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AA=E5=88=B0=E6=95=B0=E6=8D=AE=E6=8C=89?= =?UTF-8?q?=E7=AB=99=E7=8B=AC=E7=AB=8B=20+=20=E7=99=BE=E4=B8=96=E5=B9=B6?= =?UTF-8?q?=E5=85=A5=E6=B1=87=E6=80=BB=20+=20=E5=85=A8=E9=87=8F=E8=B7=91?= =?UTF-8?q?=E6=AF=94=E5=AF=B9=EF=BC=88=E5=A4=B1=E8=B4=A5=E5=AE=B9=E9=94=99?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - state_store:site_status 加 undelivered_ready 字段(旧库 ALTER 迁移);init_db 提早到启动最前(server lifespan + launch_and_prepare 第0步),避免 /api/status 早于迁移报错 - expected_undelivered:重构为 process(name)/process_baishi/write_site_file/build_full_report;build_summary 支持百世(仅未到件、无基数,不计入合计/图表)与失败容错(未成功站保留行无数据) - runtime:4 站 ("站","undelivered") = 下应到+实到 → 比对写 <站>-未到数据.xlsx;("__compare__","compare") 改 run_all(顺序跑5站、记成功清单 → build_full_report,未登录/失败跳过);DATA_FILENAMES 加 undelivered、心跳探测之;_site_undelivered_handler 用 is not False 与 dispatch 一致 - site_shunxin:shunxin_expected/actual_download 改为 return with_retry 结果(修复返回 None 致调用方误判失败、以及 with_retry 失败被当成功的潜在 bug) - server:lifespan 启动时 init_db Co-Authored-By: Claude --- expected_undelivered.py | 203 +++++++++++++++++++++++++++++----------- runtime.py | 83 ++++++++++++++-- server.py | 1 + site_shunxin.py | 10 +- state_store.py | 57 +++++++---- 5 files changed, 272 insertions(+), 82 deletions(-) diff --git a/expected_undelivered.py b/expected_undelivered.py index c637f52..8af5042 100644 --- a/expected_undelivered.py +++ b/expected_undelivered.py @@ -41,6 +41,13 @@ DOWNLOADS = os.path.join(BASE, "downloads") OUTPUT = os.path.join(BASE, "output") OUTFILE = os.path.join(OUTPUT, "应到未到数据.xlsx") +# 汇总报表覆盖的全部站点(4 站在前、百世在末;汇总页图表只取 4 站) +ALL_REPORT_SITES = ["顺心", "中通", "韵达", "安能", "百世"] +# 4 站单站未到明细文件名(百世未到文件由站点直接产出,名为 BAISHI_FILE) +SITE_UNDELIVERED_FILE = "{name}-未到数据.xlsx" +BAISHI_FILE = "百世-应到未到货物数据.xlsx" +BAISHI_COLUMNS = ["类型", "子单号", "运单号", "最新扫描记录"] + # ============================ 比对逻辑 ============================ @@ -134,8 +141,16 @@ STATIONS = [ ] -def process(cfg): - """返回 (列名list, 明细行list[dict], 统计dict);源文件缺失时返回 None。""" +def _site_cfg(name): + """按名称取 4 站配置(百世不在 STATIONS,返回 None)。""" + return next((c for c in STATIONS if c["name"] == name), None) + + +def process(name): + """4 站单站比对:返回 (列名list, 明细行list[dict], 统计dict);源文件缺失或非 4 站返回 None。""" + cfg = _site_cfg(name) + if cfg is None: + return None exp_path = os.path.join(DOWNLOADS, cfg["exp"]) act_path = os.path.join(DOWNLOADS, cfg["act"]) if not os.path.exists(exp_path) or not os.path.exists(act_path): @@ -268,6 +283,78 @@ def write_station(ws, columns, rows): ws.print_title_rows = "1:1" +# ============================ 单站 / 全量产出 ============================ + + +def process_baishi(): + """百世:读站点直供的未到明细,返回 (columns, rows, stats);文件缺失返回 None。 + 百世文件本身即未到结果(无应到/已到基数),统计只能给出未到件数。""" + path = os.path.join(DOWNLOADS, BAISHI_FILE) + if not os.path.exists(path): + return None + df = pd.read_excel(path, dtype=str).fillna("") + rows = df.to_dict("records") + wb_count = df["运单号"].nunique() if "运单号" in df.columns else len(rows) + stats = { + "运单数": wb_count, + "应到件": None, + "已到件": None, + "未到件": len(rows), + "涉及运单": wb_count, + "完全未到": None, + "部分未到": None, + "重复运单": 0, + } + return (BAISHI_COLUMNS, rows, stats) + + +def write_site_file(name): + """4 站:把该站未到明细写到 downloads/<站>-未到数据.xlsx。 + 应到/实到缺(process 返回 None)→ 删旧文件、返回 False;成功返回 True。""" + path = os.path.join(DOWNLOADS, SITE_UNDELIVERED_FILE.format(name=name)) + out = process(name) + if out is None: + if os.path.exists(path): + os.remove(path) + return False + columns, rows, _stats = out + wb = Workbook() + wb.remove(wb.active) + ws = wb.create_sheet(name) + write_station(ws, columns, rows) + wb.save(path) + return True + + +def build_full_report(include): + """生成全站汇总报表 output/应到未到数据.xlsx。 + include: 本次成功的站点集合;未成功站点在汇总里保留行、无数据(不影响他站)。 + 返回 {站点: 未到件或None} 供日志。""" + os.makedirs(OUTPUT, exist_ok=True) + wb = Workbook() + wb.remove(wb.active) + summary_ws = wb.create_sheet("汇总报表") # 首页占位 + + summary = [] # (name, stats_or_None),顺序:4 站 + 百世 + for name in ALL_REPORT_SITES: + if name == "百世": + out = process_baishi() if "百世" in include else None + columns = BAISHI_COLUMNS + else: + out = process(name) if name in include else None + cfg = _site_cfg(name) + columns = cfg["columns"] if cfg else [] + stats = out[2] if out is not None else None + rows = out[1] if out is not None else [] + summary.append((name, stats)) + ws = wb.create_sheet(name) + write_station(ws, columns, rows) + + build_summary(summary_ws, summary, datetime.now().strftime("%Y-%m-%d %H:%M")) + wb.save(OUTFILE) + return {n: (s["未到件"] if s else None) for (n, s) in summary} + + # ============================ 写汇总报表 ============================ @@ -275,12 +362,15 @@ def build_summary(ws, results, generated_at): center = Alignment(horizontal="center", vertical="center") left = Alignment(horizontal="left", vertical="center", indent=1) - t_wb = sum(s["运单数"] for _, s in results) - t_exp = sum(s["应到件"] for _, s in results) - t_arr = sum(s["已到件"] for _, s in results) - t_miss = sum(s["未到件"] for _, s in results) - t_full = sum(s["完全未到"] for _, s in results) - t_part = sum(s["部分未到"] for _, s in results) + # 合计/KPI 只算 4 站中本次成功的(百世无应到基数、失败站无数据,均不计入) + four = [(n, s) for (n, s) in results if n != "百世"] + ok = [s for _, s in four if s] + t_wb = sum(s["运单数"] for s in ok) + t_exp = sum(s["应到件"] for s in ok) + t_arr = sum(s["已到件"] for s in ok) + t_miss = sum(s["未到件"] for s in ok) + t_full = sum(s["完全未到"] for s in ok) + t_part = sum(s["部分未到"] for s in ok) rate = (t_miss / t_exp) if t_exp else 0 ws.sheet_view.showGridLines = False @@ -387,20 +477,27 @@ def build_summary(ws, results, generated_at): cell.border = BORDER ws.row_dimensions[9].height = 22 - # —— 各站数据行 —— + # —— 各站数据行(4 站 + 百世)—— r = 10 for idx, (name, s) in enumerate(results): - srate = (s["未到件"] / s["应到件"]) if s["应到件"] else 0 - vals = [ - name, - s["运单数"], - s["应到件"], - s["已到件"], - s["未到件"], - srate, - s["完全未到"], - s["部分未到"], - ] + is_baishi = name == "百世" + srate = 0 + if s is None: + vals = [f"{name}(无数据)", 0, 0, 0, 0, 0, 0, 0] + elif is_baishi: + vals = [name, s["运单数"], "—", "—", s["未到件"], "—", "—", "—"] + else: + srate = (s["未到件"] / s["应到件"]) if s["应到件"] else 0 + vals = [ + name, + s["运单数"], + s["应到件"], + s["已到件"], + s["未到件"], + srate, + s["完全未到"], + s["部分未到"], + ] for i, v in enumerate(vals): col = chr(ord("B") + i) cell = ws[f"{col}{r}"] @@ -408,12 +505,13 @@ def build_summary(ws, results, generated_at): cell.font = BODY_FONT cell.border = BORDER cell.alignment = left if i == 0 else center - if idx % 2 == 1 and i != 5: + if s is None: + cell.fill = PatternFill("solid", fgColor="EFEFEF") + elif not is_baishi and idx % 2 == 1 and i != 5: cell.fill = PatternFill("solid", fgColor=ZEBRA) - if i in (1, 2, 3, 4, 6, 7): - cell.number_format = "#,##0" - if i == 5: - cell.number_format = "0.0%" + if isinstance(v, (int, float)): + cell.number_format = "0.0%" if i == 5 else "#,##0" + if i == 5 and s is not None and not is_baishi: cell.fill = PatternFill("solid", fgColor=heat(srate)) ws.row_dimensions[r].height = 19 r += 1 @@ -435,7 +533,7 @@ def build_summary(ws, results, generated_at): if i == 5: cell.number_format = "0.0%" ws.row_dimensions[r].height = 20 - last_data_row = 9 + len(results) + last_data_row = 9 + len(four) # 图表只取 4 站(百世无应到/已到基数,不绘图) chart_anchor = r + 2 # —— 堆叠柱状图:各站已到 / 未到 —— @@ -464,7 +562,9 @@ def build_summary(ws, results, generated_at): note_row = chart_anchor + 19 notes = [ "指标口径:未到率 = 未到件数 ÷ 应到件数;完全未到运单 = 整单零到货;部分未到运单 = 部分到货、部分缺件。", - "明细见各站点工作表;缺件的子单号 / 扫描单号按各站编号规则生成,并非实到原始记录。", + "合计 / 图表仅含 4 站(顺心/中通/韵达/安能,应到−实到口径);百世为站点直供未到、无应到基数,单列不计入合计。", + "本次下载失败的站点标注为(无数据)并计 0,不影响其余站点统计。", + "明细见各站点工作表;4 站缺件的子单号 / 扫描单号按各站编号规则生成,并非实到原始记录。", ] for k, text in enumerate(notes): rr = note_row + k @@ -486,37 +586,32 @@ def build_summary(ws, results, generated_at): def main(): - os.makedirs(OUTPUT, exist_ok=True) - wb = Workbook() - wb.remove(wb.active) - summary_ws = wb.create_sheet("汇总报表") # 首页占位 - - print("应到未到比对结果") + """菜单 [9] / 离线入口:用 downloads/ 下现有文件生成全站汇总报告(有文件的站即纳入)。""" + print("应到未到比对(全站汇总)") print("-" * 56) - results = [] - for cfg in STATIONS: - out = process(cfg) - if out is None: - continue - columns, rows, stats = out - ws = wb.create_sheet(cfg["name"]) - write_station(ws, columns, rows) - results.append((cfg["name"], stats)) - extra = f",应到重复运单 {stats['重复运单']}" if stats["重复运单"] else "" - print( - f"{cfg['name']}:应到运单 {stats['运单数']},应到件 {stats['应到件']}," - f"已到 {stats['已到件']},未到 {stats['未到件']} 件" - f"(涉及运单 {stats['涉及运单']}:完全未到 {stats['完全未到']} / 部分未到 {stats['部分未到']}){extra}" - ) - - if not results: + include = set() + for name in ALL_REPORT_SITES: + if name == "百世": + if os.path.exists(os.path.join(DOWNLOADS, BAISHI_FILE)): + include.add(name) + else: + cfg = _site_cfg(name) + if ( + cfg + and os.path.exists(os.path.join(DOWNLOADS, cfg["exp"])) + and os.path.exists(os.path.join(DOWNLOADS, cfg["act"])) + ): + include.add(name) + if not include: print("未处理任何站点:请确认 downloads/ 下存在源数据文件。") return - - build_summary(summary_ws, results, datetime.now().strftime("%Y-%m-%d %H:%M")) - + undel = build_full_report(include) print("-" * 56) - wb.save(OUTFILE) + for name in ALL_REPORT_SITES: + if name in include: + print(f"{name}:未到 {undel.get(name)} 件") + else: + print(f"{name}:无数据,跳过") print(f"已输出:{OUTFILE}") diff --git a/runtime.py b/runtime.py index 8ad8fbe..c6c34fb 100644 --- a/runtime.py +++ b/runtime.py @@ -55,11 +55,27 @@ HEARTBEAT_INTERVAL = 30 # 各站最终数据文件名(探测"数据是否已跑出来");百世为单流程 DATA_FILENAMES = { - "顺心": {"expected": "顺心-应到货物数据.xlsx", "actual": "顺心-实到货物数据.xlsx"}, - "中通": {"expected": "中通-应到货物数据.xlsx", "actual": "中通-实到货物数据.xlsx"}, - "韵达": {"expected": "韵达-应到货物数据.xlsx", "actual": "韵达-实到货物数据.xlsx"}, - "安能": {"expected": "安能-应到货物数据.xlsx", "actual": "安能-实到货物数据.xlsx"}, - "百世": {"expected": "百世-应到未到货物数据.xlsx", "actual": ""}, + "顺心": { + "expected": "顺心-应到货物数据.xlsx", + "actual": "顺心-实到货物数据.xlsx", + "undelivered": "顺心-未到数据.xlsx", + }, + "中通": { + "expected": "中通-应到货物数据.xlsx", + "actual": "中通-实到货物数据.xlsx", + "undelivered": "中通-未到数据.xlsx", + }, + "韵达": { + "expected": "韵达-应到货物数据.xlsx", + "actual": "韵达-实到货物数据.xlsx", + "undelivered": "韵达-未到数据.xlsx", + }, + "安能": { + "expected": "安能-应到货物数据.xlsx", + "actual": "安能-实到货物数据.xlsx", + "undelivered": "安能-未到数据.xlsx", + }, + "百世": {"expected": "", "actual": "", "undelivered": "百世-应到未到货物数据.xlsx"}, } @@ -196,6 +212,9 @@ def launch_and_prepare(debug_mode=False, debug_target=""): 必须在"持有 Playwright 的线程"调用(交互模式主线程 / 服务模式 worker 线程)。 阻塞至所有站点登录就绪才返回。 """ + # 0. 状态库建表/迁移(须在 reset_login_states 等读写新字段的调用之前) + state_store.init_db() + # 1. 读 config(anneng.app_path) anneng_app_path = "" try: @@ -309,8 +328,7 @@ def launch_and_prepare(debug_mode=False, debug_target=""): if s in pages_map: print(f" ✅ 【{s}】已就绪。") - # 8. 状态库 + 心跳初值(就绪轮询刚通过 → 各站视为已登录) - state_store.init_db() + # 8. 心跳初值(就绪轮询刚通过 → 各站视为已登录;init_db 已在启动时完成) sites_to_watch = list(ready_status.keys()) for _site in sites_to_watch: state_store.set_login_state(_site, True) @@ -389,19 +407,66 @@ def _web_handler(site, download_func): return handler +def _site_undelivered_handler(site): + """4 站未到:下应到+实到 → 比对写 downloads/<站>-未到数据.xlsx。 + 任一下载失败 → 清掉旧未到文件、返回 False(前端不展示陈旧未到)。""" + + def handler(ctx): + # 各站下载入口约定返回 True/False;顺心历史返回 None(视为成功,与 dispatch 一致) + exp_ok = TASK_HANDLERS[(site, "expected")](ctx) is not False + act_ok = ( + (TASK_HANDLERS[(site, "actual")](ctx) is not False) if exp_ok else False + ) + if exp_ok and act_ok: + return expected_undelivered.write_site_file(site) + stale = os.path.join( + DOWNLOAD_DIR, expected_undelivered.SITE_UNDELIVERED_FILE.format(name=site) + ) + if os.path.exists(stale): + os.remove(stale) + return False + + return handler + + +def _run_all_compare_handler(): + """顶部「跑比对」:顺序跑 5 站未到(4 站 expected+actual→比对;百世直供), + 记录本次成功站 → build_full_report。失败/未登录站保留行、无数据,不影响他站。""" + + def handler(ctx): + include = set() + for site in ("顺心", "中通", "韵达", "安能", "百世"): + if not probe_site_login(site, ctx.pages_map): + print(f">> [跑比对] {site} 未登录,跳过其统计") + continue + h = TASK_HANDLERS.get((site, "undelivered")) + if h and h(ctx): + include.add(site) + else: + print(f">> [跑比对] {site} 未到数据获取失败,跳过其统计") + expected_undelivered.build_full_report(include) + return True + + return handler + + TASK_HANDLERS = { ("顺心", "expected"): _web_handler("顺心", site_shunxin.shunxin_expected_download), ("顺心", "actual"): _web_handler("顺心", site_shunxin.shunxin_actual_download), + ("顺心", "undelivered"): _site_undelivered_handler("顺心"), ("百世", "undelivered"): _web_handler( "百世", site_baishi.baishi_download_undelivered_data ), ("中通", "expected"): _web_handler("中通", site_zto.zto_expected_download), ("中通", "actual"): _web_handler("中通", site_zto.zto_actual_download), + ("中通", "undelivered"): _site_undelivered_handler("中通"), ("韵达", "expected"): _web_handler("韵达", site_yunda.yunda_expected_download), ("韵达", "actual"): _web_handler("韵达", site_yunda.yunda_actual_download), + ("韵达", "undelivered"): _site_undelivered_handler("韵达"), ("安能", "expected"): lambda ctx: site_anneng.anneng_expected_download(), ("安能", "actual"): lambda ctx: site_anneng.anneng_actual_download(), - ("__compare__", "compare"): lambda ctx: (expected_undelivered.main() or True), + ("安能", "undelivered"): _site_undelivered_handler("安能"), + ("__compare__", "compare"): _run_all_compare_handler(), } @@ -449,6 +514,6 @@ def run_heartbeat(ctx): now_login = state_store.LOGIN_IN if logged_in else state_store.LOGIN_OUT if prev_login and prev_login not in (now_login, state_store.LOGIN_UNKNOWN): print(f"\n ⚠️【{site_name}】登录态变化: {prev_login} → {now_login}") - for kind in ("expected", "actual"): + for kind in ("expected", "actual", "undelivered"): ready, gen_at = probe_data_file(site_name, kind) state_store.set_data_state(site_name, kind, ready, gen_at) diff --git a/server.py b/server.py index 929f52c..9f298c9 100644 --- a/server.py +++ b/server.py @@ -89,6 +89,7 @@ def _worker_loop(): @asynccontextmanager async def lifespan(_app): """服务启停:起 worker 线程 / 通知 worker 停。""" + state_store.init_db() # 先建表/迁移状态库,确保早于 worker 就绪的 /api/status 可用 t = threading.Thread(target=_worker_loop, daemon=True) worker_state["thread"] = t t.start() diff --git a/site_shunxin.py b/site_shunxin.py index 28c976d..43e25d7 100644 --- a/site_shunxin.py +++ b/site_shunxin.py @@ -171,14 +171,17 @@ def shunxin_expected_download(pages): for idx, (pg, tag) in enumerate(zip(pages, tags), start=1): pg.bring_to_front() print(f"\n========== 顺心 · 账号{idx}({tag})应到数据下载 ==========") - with_retry( + ok = with_retry( f"顺心-{tag}", "应到", lambda p=pg, t=tag: shunxin_expected_download_impl(p, out_tag=t), lambda p=pg: shunxin_reset(p), ) + if not ok: + return False # 某账号重试耗尽 → 整体失败,不融合(避免部分数据) shunxin_merge_final("应到", tags) + return True def shunxin_expected_download_impl(page, out_tag=""): @@ -493,14 +496,17 @@ def shunxin_actual_download(pages): for idx, (pg, tag) in enumerate(zip(pages, tags), start=1): pg.bring_to_front() print(f"\n========== 顺心 · 账号{idx}({tag})实到数据下载 ==========") - with_retry( + ok = with_retry( f"顺心-{tag}", "实到", lambda p=pg, t=tag: shunxin_actual_download_impl(p, out_tag=t), lambda p=pg: shunxin_reset(p), ) + if not ok: + return False shunxin_merge_final("实到", tags) + return True def shunxin_actual_download_impl(page, out_tag=""): diff --git a/state_store.py b/state_store.py index ff3aba0..90625b8 100644 --- a/state_store.py +++ b/state_store.py @@ -35,16 +35,27 @@ def init_db(): with sqlite3.connect(STATE_DB_PATH) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS site_status ( - site TEXT PRIMARY KEY, - login_state TEXT, - login_checked_at TEXT, - expected_ready INTEGER, - expected_generated_at TEXT, - actual_ready INTEGER, - actual_generated_at TEXT, - updated_at TEXT + site TEXT PRIMARY KEY, + login_state TEXT, + login_checked_at TEXT, + expected_ready INTEGER, + expected_generated_at TEXT, + actual_ready INTEGER, + actual_generated_at TEXT, + undelivered_ready INTEGER, + undelivered_generated_at TEXT, + updated_at TEXT ) """) + # 旧库迁移:补 undelivered 两列(新库已含;重复添加抛 OperationalError,忽略) + for _col, _typedef in [ + ("undelivered_ready", "INTEGER NOT NULL DEFAULT 0"), + ("undelivered_generated_at", "TEXT NOT NULL DEFAULT ''"), + ]: + try: + conn.execute(f"ALTER TABLE site_status ADD COLUMN {_col} {_typedef}") + except sqlite3.OperationalError: + pass conn.execute(""" CREATE TABLE IF NOT EXISTS task_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -70,7 +81,8 @@ def _upsert(conn, site, **fields): """更新(或插入)单站:保留未传字段,刷新 updated_at。""" row = conn.execute( "SELECT login_state, login_checked_at, expected_ready, expected_generated_at, " - "actual_ready, actual_generated_at FROM site_status WHERE site = ?", + "actual_ready, actual_generated_at, undelivered_ready, undelivered_generated_at " + "FROM site_status WHERE site = ?", (site,), ).fetchone() cur = { @@ -80,6 +92,8 @@ def _upsert(conn, site, **fields): "expected_generated_at": "", "actual_ready": 0, "actual_generated_at": "", + "undelivered_ready": 0, + "undelivered_generated_at": "", } if row: ( @@ -89,6 +103,8 @@ def _upsert(conn, site, **fields): cur["expected_generated_at"], cur["actual_ready"], cur["actual_generated_at"], + cur["undelivered_ready"], + cur["undelivered_generated_at"], ) = row cur.update(fields) cur["updated_at"] = _now() @@ -101,20 +117,24 @@ def _upsert(conn, site, **fields): cur["expected_generated_at"], cur["actual_ready"], cur["actual_generated_at"], + cur["undelivered_ready"], + cur["undelivered_generated_at"], cur["updated_at"], ) if row: conn.execute( "UPDATE site_status SET login_state=?, login_checked_at=?, expected_ready=?, " - "expected_generated_at=?, actual_ready=?, actual_generated_at=?, updated_at=? " + "expected_generated_at=?, actual_ready=?, actual_generated_at=?, " + "undelivered_ready=?, undelivered_generated_at=?, updated_at=? " "WHERE site=?", values[1:] + (site,), ) else: conn.execute( "INSERT INTO site_status (site, login_state, login_checked_at, expected_ready, " - "expected_generated_at, actual_ready, actual_generated_at, updated_at) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + "expected_generated_at, actual_ready, actual_generated_at, " + "undelivered_ready, undelivered_generated_at, updated_at) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", values, ) conn.commit() @@ -136,9 +156,9 @@ def reset_login_states(sites): def set_data_state(site, kind, ready, generated_at): - """更新单站数据态。kind: 'expected'/'actual';ready: bool;generated_at: str 或 ''。""" - key_ready = "expected_ready" if kind == "expected" else "actual_ready" - key_at = "expected_generated_at" if kind == "expected" else "actual_generated_at" + """更新单站数据态。kind: 'expected'/'actual'/'undelivered';ready: bool;generated_at: str。""" + key_ready = f"{kind}_ready" + key_at = f"{kind}_generated_at" with sqlite3.connect(STATE_DB_PATH) as conn: _upsert( conn, site, **{key_ready: 1 if ready else 0, key_at: generated_at or ""} @@ -152,7 +172,8 @@ def get_all_status(): with sqlite3.connect(STATE_DB_PATH) as conn: rows = conn.execute( "SELECT site, login_state, login_checked_at, expected_ready, " - "expected_generated_at, actual_ready, actual_generated_at, updated_at " + "expected_generated_at, actual_ready, actual_generated_at, " + "undelivered_ready, undelivered_generated_at, updated_at " "FROM site_status" ).fetchall() return { @@ -163,7 +184,9 @@ def get_all_status(): "expected_generated_at": r[4], "actual_ready": bool(r[5]), "actual_generated_at": r[6], - "updated_at": r[7], + "undelivered_ready": bool(r[7]), + "undelivered_generated_at": r[8], + "updated_at": r[9], } for r in rows }