diff --git a/docs/superpowers/plans/2026-07-23-tier2-domain-rename-dedup.md b/docs/superpowers/plans/2026-07-23-tier2-domain-rename-dedup.md new file mode 100644 index 0000000..9649f20 --- /dev/null +++ b/docs/superpowers/plans/2026-07-23-tier2-domain-rename-dedup.md @@ -0,0 +1,403 @@ +# Tier 2: domain extract + rename + path dedup — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Three targeted, behavior-preserving cleanups of the compare/config layer: (1) make `expected_undelivered` use `paths.DOWNLOAD_DIR/OUTPUT_DIR` instead of its own duplicate anchor; (2) extract shared site/file/column config into a new `domain` module; (3) rename `expected_undelivered` → `compare`. + +**Architecture:** Pure refactor — move definitions, rewire imports, no logic change. Removes the duplicate path anchor that caused the Tier 1 hotfix bug (`17293be`), and the `db_store → expected_undelivered` coupling where the DB layer imported the whole compare engine just to read site config. + +**Tech Stack:** Python ≥3.10, package `inbound_verify`, pandas, openpyxl, psycopg3. + +## Global Constraints + +- **Python ≥ 3.10**, package import name `inbound_verify`. +- **No behavior change** — pure refactor. Any logic change is a defect. +- **No test suite** (user decision). Per-task verification = `compileall` + **import smoke** (fresh process, no backend needed) + grep-for-stale-refs. NOT pytest. (The running backend is unaffected until a restart; a final end-to-end re-confirm is done once at the end of Tier 2.) +- **No auto-commit** (project rule). Each task's commit step runs only after the user says "提交". Commit messages in English. +- **Black-format** every changed `.py` (global rule). +- All changes inside the `InboundVerify` submodule; commits local on `dev`. +- Venv interpreter: `.venv/Scripts/python.exe` (absolute: `D:/projects/LogisticsHubIPA/InboundVerify/.venv/Scripts/python.exe`). Run commands from `D:/projects/LogisticsHubIPA/InboundVerify/`. + +**Reference spec:** `docs/superpowers/specs/2026-07-23-package-restructure-design.md` §8 Tier 2. + +**Out of scope (deferred):** `config.py` centralization (consolidating the 6 `open(CONFIG_PATH)+yaml.safe_load` sites in store/runtime×2/anneng/router). It is the churniest Tier 2 item (6 files) for the least marginal value — pure DRY, no pain addressed — and under no-tests more churn = more risk. Reconsider after the rest of Tier 2 lands. + +--- + +## File Structure + +``` +inbound_verify/ +├── domain.py # NEW (Task 2): shared site/file/column config — leaf module +├── paths.py # unchanged (already the single anchor) +├── expected_undelivered.py# Task 1: use paths.* ; Task 2: import config from domain ; Task 3: renamed → compare.py +├── compare.py # (after Task 3) was expected_undelivered.py — compare engine + report +├── store.py # Task 2: import config from domain (not eu); Task 3: import _read_business_dates from compare +├── runtime.py # Task 2: SITE_UNDELIVERED_FILE from domain; Task 3: write_site_file from compare +└── cli/router.py # Task 3: compare.main() +``` + +**`domain.py` responsibility:** pure data — `ALL_REPORT_SITES`, `SITE_UNDELIVERED_FILE`, `BAISHI_FILE`, `BAISHI_COLUMNS`, `arrived_pieces_zhongtong`, `arrived_pieces_by_cols`, `STATIONS`, `_site_cfg`. No `state_store` dependency, no file I/O. Leaf module. + +--- + +## Task 1: Path dedup — expected_undelivered uses paths.DOWNLOAD_DIR/OUTPUT_DIR + +**Files:** +- Modify: `inbound_verify/expected_undelivered.py` (lines 43-48 defs; usages at 146,147,298,338,374,400,669,675,676,690) + +**Interfaces:** +- Consumes: `paths.DOWNLOAD_DIR`, `paths.OUTPUT_DIR` (already exist, anchored at project root). +- Produces: `expected_undelivered` no longer defines `BASE/DOWNLOADS/OUTPUT`; keeps `OUTFILE` (now `= join(OUTPUT_DIR, "应到未到数据.xlsx")`). The 6 `DOWNLOADS` and 1 `OUTPUT` usages point at the `paths.*` constants — same resolved values as the current hotfixed anchor, so behavior identical. + +- [ ] **Step 1: Add the paths import to the top import block** + +In `inbound_verify/expected_undelivered.py`, after the existing `import os` (top of file), add a line importing the two path constants. (The file currently has no `paths` import — it used its own BASE.) + +Add: +```python +from inbound_verify.paths import DOWNLOAD_DIR, OUTPUT_DIR +``` +(place it alongside the other imports near the top, e.g. right after `import os`.) + +- [ ] **Step 2: Replace the self-anchored BASE/DOWNLOADS/OUTPUT/OUTFILE block** + +Replace this block (currently lines ~43-48): +```python +# 包内文件:上两级 = 项目根(与 paths.BASE_DIR 一致;站点下载落在 /downloads)。 +# 注:本模块自带锚点是 paths.py 的重复,Tier 2 计划改为直接引用 paths.DOWNLOAD_DIR/OUTPUT_DIR。 +BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +DOWNLOADS = os.path.join(BASE, "downloads") +OUTPUT = os.path.join(BASE, "output") +OUTFILE = os.path.join(OUTPUT, "应到未到数据.xlsx") +``` +with: +```python +# 比对报表输出文件(路径锚定统一走 paths.py)。 +OUTFILE = os.path.join(OUTPUT_DIR, "应到未到数据.xlsx") +``` + +- [ ] **Step 3: Replace all `DOWNLOADS` usages with `DOWNLOAD_DIR`** + +Apply `DOWNLOADS` → `DOWNLOAD_DIR` (6 occurrences, at lines 146, 147, 298, 338, 669, 675, 676). Use replace-all on the token `DOWNLOADS`. + +- [ ] **Step 4: Replace the remaining `OUTPUT` usage (the makedirs line)** + +At line ~374, replace: +```python + os.makedirs(OUTPUT, exist_ok=True) +``` +with: +```python + os.makedirs(OUTPUT_DIR, exist_ok=True) +``` +(`OUTFILE` at lines 400/690 is unchanged — it's a different token, already redefined in Step 2.) + +- [ ] **Step 5: Verify syntax + no stale BASE/DOWNLOADS/OUTPUT** + +```bash +cd /d/projects/LogisticsHubIPA/InboundVerify +.venv/Scripts/python.exe -m py_compile inbound_verify/expected_undelivered.py +grep -nE "\b(BASE|DOWNLOADS|OUTPUT)\b" inbound_verify/expected_undelivered.py || echo "no stale BASE/DOWNLOADS/OUTPUT OK" +``` +Expected: py_compile silent; grep prints `no stale BASE/DOWNLOADS/OUTPUT OK` (OUTFILE is a different token, won't match). + +- [ ] **Step 6: Import smoke + paths equivalence** + +```bash +.venv/Scripts/python.exe -c "from inbound_verify import expected_undelivered as eu; from inbound_verify.paths import DOWNLOAD_DIR, OUTPUT_DIR; import os; print('OUTFILE dir matches OUTPUT_DIR:', os.path.dirname(eu.OUTFILE)==OUTPUT_DIR)" +.venv/Scripts/python.exe -c "import inbound_verify.cli.router, inbound_verify.cli.server, inbound_verify.store, inbound_verify.runtime; print('import smoke OK')" +``` +Expected: `OUTFILE dir matches OUTPUT_DIR: True` and `import smoke OK`. + +- [ ] **Step 7: Black + commit (after user confirms)** + +```bash +.venv/Scripts/python.exe -m black inbound_verify/expected_undelivered.py +git add inbound_verify/expected_undelivered.py +git commit -m "refactor: expected_undelivered uses paths.DOWNLOAD_DIR/OUTPUT_DIR (Tier 2) + +Co-Authored-By: Claude " +``` + +--- + +## Task 2: Extract `domain.py` (shared site/file/column config) + +**Files:** +- Create: `inbound_verify/domain.py` +- Modify: `inbound_verify/expected_undelivered.py` (remove the moved block, import from domain) +- Modify: `inbound_verify/store.py` (config from domain, not eu) +- Modify: `inbound_verify/runtime.py` (SITE_UNDELIVERED_FILE from domain) + +**Interfaces:** +- Consumes: nothing new (moves existing definitions verbatim). +- Produces: `inbound_verify.domain` exposing `ALL_REPORT_SITES`, `SITE_UNDELIVERED_FILE`, `BAISHI_FILE`, `BAISHI_COLUMNS`, `arrived_pieces_zhongtong(df)`, `arrived_pieces_by_cols(wb_col, piece_col)`, `STATIONS` (list of dicts), `_site_cfg(name)`. These are the exact same objects that lived in `expected_undelivered` lines 50-135. + +- [ ] **Step 1: Create `inbound_verify/domain.py`** + +Create `inbound_verify/domain.py` with this exact content (moved verbatim from expected_undelivered.py lines 50-135, plus the `defaultdict` import it needs): + +```python +# -*- coding: utf-8 -*- +"""domain.py — 站点 / 文件名 / 列映射的共享配置(单一来源)。 + +比对(compare)与入库(store)都依赖这套配置;抽出独立 leaf 模块, +让 store 不必为读配置而依赖整个比对引擎。纯数据,无 state_store / 文件 IO 依赖。 +""" + +from collections import defaultdict + +# 汇总报表覆盖的全部站点(4 站在前、百世在末;汇总页图表只取 4 站) +ALL_REPORT_SITES = ["顺心", "中通", "韵达", "安能", "百世"] +# 4 站单站未到明细文件名(百世未到文件由站点直接产出,名为 BAISHI_FILE) +SITE_UNDELIVERED_FILE = "{name}-未到数据.xlsx" +BAISHI_FILE = "百世-应到未到货物数据.xlsx" +BAISHI_COLUMNS = ["类型", "子单号", "运单号", "最新扫描记录"] + + +def arrived_pieces_zhongtong(df): + """中通:实到「运单号」为复合串(H + 运单号(12) + 总数(4) + 顺序(4))。 + 基号 = v[:-8](与应到表运单号对齐),单件 = 整串(每串即一件)。""" + res = defaultdict(set) + for v in df["运单号"]: + v = str(v).strip() + if len(v) > 8 and v[-4:].isdigit(): + res[v[:-8]].add(v) # 以完整复合串作为“已到单号”存入 + return res + + +def arrived_pieces_by_cols(wb_col, piece_col): + """顺心 / 韵达 / 安能:按干净运单列分组,单件 = 子单号 / 扫描单号。 + wb_col:实到表中与应到运单号对齐的干净列 + (顺心=运单号 / 韵达=主单号 / 安能=所属单号) + piece_col:实到表中每件货物的单号列(子单号 / 扫描单号)""" + + def parse(df): + res = defaultdict(set) + for m, s in zip(df[wb_col], df[piece_col]): + m, s = str(m).strip(), str(s).strip() + if m and s: + res[m].add(s) + return res + + return parse + + +STATIONS = [ + { + "name": "中通", + "exp": "中通-应到货物数据.xlsx", + "act": "中通-实到货物数据.xlsx", + "exp_qty": "交接件数", # 应到件数口径:交接件数(非录单件数) + "exp_wb": "运单号", # 应到表运单号列(兼作去重键) + "exp_jd": "交接单号", # 未到数据需展示的交接单号 + "arrived_pieces": arrived_pieces_zhongtong, + "columns": ["交接单号", "运单号", "总件数"], + }, + { + "name": "顺心", + "exp": "顺心-应到货物数据.xlsx", + "act": "顺心-实到货物数据.xlsx", + "exp_qty": "交接件数", + "exp_wb": "运单号", + "exp_jd": "交接单号", + "arrived_pieces": arrived_pieces_by_cols("运单号", "子单号"), + "columns": ["交接单号", "运单号", "总件数"], + }, + { + "name": "韵达", + "exp": "韵达-应到货物数据.xlsx", + "act": "韵达-实到货物数据.xlsx", + "exp_qty": "交接件数", + "exp_wb": "运单号", + "exp_jd": "交接单号", + "arrived_pieces": arrived_pieces_by_cols("主单号", "子单号"), + "columns": ["交接单号", "运单号", "总件数"], + }, + { + "name": "安能", + "exp": "安能-应到货物数据.xlsx", + "act": "安能-实到货物数据.xlsx", + "exp_qty": "交接件数", + "exp_wb": "运单号", + "exp_jd": "交接单号", + "arrived_pieces": arrived_pieces_by_cols("所属单号", "扫描单号"), + "columns": ["交接单号", "运单号", "总件数"], + }, +] + + +def _site_cfg(name): + """按名称取 4 站配置(百世不在 STATIONS,返回 None)。""" + return next((c for c in STATIONS if c["name"] == name), None) +``` + +- [ ] **Step 2: Remove the moved block from expected_undelivered.py and import from domain** + +In `inbound_verify/expected_undelivered.py`: +- Add to the top import block: +```python +from inbound_verify.domain import ( + ALL_REPORT_SITES, + BAISHI_COLUMNS, + BAISHI_FILE, + SITE_UNDELIVERED_FILE, + STATIONS, + _site_cfg, + arrived_pieces_by_cols, + arrived_pieces_zhongtong, +) +``` +- Delete the now-duplicated definitions (the block from `ALL_REPORT_SITES = ...` through the end of `_site_cfg`, i.e. old lines ~50-135 — the comment header `# 汇总报表...` through `return next(...)`). These now live in domain.py. The `from collections import defaultdict` import in expected_undelivered.py can stay (harmless) or be removed if unused — check with grep after. + +- [ ] **Step 3: store.py — take config from domain, _read_business_dates still from eu** + +In `inbound_verify/store.py`: +- Add to imports: +```python +from inbound_verify.domain import BAISHI_FILE, _site_cfg +``` +- Replace `cfg = eu._site_cfg(site)` (2 occurrences, lines 264 and 296) → `cfg = _site_cfg(site)`. +- Replace `eu.BAISHI_FILE` (2 occurrences, lines 335 and 337) → `BAISHI_FILE`. +- Leave `eu._read_business_dates(...)` (line 213) unchanged — that's compare behavior, stays accessed via the compare module (`eu`). The `from inbound_verify import expected_undelivered as eu` import stays for this. + +- [ ] **Step 4: runtime.py — SITE_UNDELIVERED_FILE from domain** + +In `inbound_verify/runtime.py`: +- Add to imports: +```python +from inbound_verify.domain import SITE_UNDELIVERED_FILE +``` +- Replace (line ~448): +```python + DOWNLOAD_DIR, expected_undelivered.SITE_UNDELIVERED_FILE.format(name=site) +``` +with: +```python + DOWNLOAD_DIR, SITE_UNDELIVERED_FILE.format(name=site) +``` +- Leave `expected_undelivered.write_site_file(site)` (line ~446) and `expected_undelivered.main()` (line ~476) unchanged (compare behavior). + +- [ ] **Step 5: Verify — compileall + import smoke + domain is leaf** + +```bash +cd /d/projects/LogisticsHubIPA/InboundVerify +.venv/Scripts/python.exe -m compileall -q inbound_verify +.venv/Scripts/python.exe -c "import inbound_verify.domain as d; print('STATIONS:', [s['name'] for s in d.STATIONS]); print('_site_cfg(中通):', d._site_cfg('中通')['exp']); print('BAISHI_FILE:', d.BAISHI_FILE)" +.venv/Scripts/python.exe -c "import inbound_verify.cli.router, inbound_verify.cli.server, inbound_verify.store, inbound_verify.runtime, inbound_verify.expected_undelivered; print('import smoke OK')" +``` +Expected: STATIONS lists `['中通', '顺心', '韵达', '安能']`; `_site_cfg('中通')['exp']` = `中通-应到货物数据.xlsx`; `BAISHI_FILE` = `百世-应到未到货物数据.xlsx`; `import smoke OK`. + +- [ ] **Step 6: Black + commit (after user confirms)** + +```bash +.venv/Scripts/python.exe -m black inbound_verify/domain.py inbound_verify/expected_undelivered.py inbound_verify/store.py inbound_verify/runtime.py +git add inbound_verify/domain.py inbound_verify/expected_undelivered.py inbound_verify/store.py inbound_verify/runtime.py +git commit -m "refactor: extract domain.py (shared site/file/colmap config) from expected_undelivered + +Co-Authored-By: Claude " +``` + +--- + +## Task 3: Rename `expected_undelivered.py` → `compare.py` + +**Files:** +- Move: `inbound_verify/expected_undelivered.py` → `inbound_verify/compare.py` (git mv) +- Modify: `inbound_verify/store.py`, `inbound_verify/runtime.py`, `inbound_verify/cli/router.py` (import + call-site rewrites) + +**Interfaces:** +- Consumes: Task 2's `domain` (compare still uses it). +- Produces: module is `inbound_verify.compare`; all public names (`main`, `write_site_file`, `_read_business_dates`) unchanged. `expected_undelivered` no longer exists as a module name. + +- [ ] **Step 1: git mv the module (history preserved)** + +```bash +cd /d/projects/LogisticsHubIPA/InboundVerify +git mv inbound_verify/expected_undelivered.py inbound_verify/compare.py +``` + +- [ ] **Step 2: store.py — import compare instead of expected_undelivered** + +In `inbound_verify/store.py`, replace: +```python +from inbound_verify import expected_undelivered as eu +``` +with: +```python +from inbound_verify import compare +``` +and replace the one call site (line ~213): +```python + return eu._read_business_dates(ALL_SITES + ["百世"]) or {} +``` +with: +```python + return compare._read_business_dates(ALL_SITES + ["百世"]) or {} +``` +(`_site_cfg` and `BAISHI_FILE` already come from `domain` after Task 2 — no change there.) + +- [ ] **Step 3: runtime.py — import compare, fix write_site_file/main** + +In `inbound_verify/runtime.py`, replace: +```python +from inbound_verify import expected_undelivered +``` +with: +```python +from inbound_verify import compare +``` +Replace `expected_undelivered.write_site_file(site)` (line ~446) → `compare.write_site_file(site)`. +Replace `(expected_undelivered.main() or True)` (line ~476) → `(compare.main() or True)`. + +- [ ] **Step 4: cli/router.py — import compare, fix main()** + +In `inbound_verify/cli/router.py`, replace: +```python +from inbound_verify import expected_undelivered +``` +with: +```python +from inbound_verify import compare +``` +Replace `expected_undelivered.main()` (line ~34, inside `run_undelivered_compare`) → `compare.main()`. + +- [ ] **Step 5: Verify — no stale references + import smoke** + +```bash +cd /d/projects/LogisticsHubIPA/InboundVerify +.venv/Scripts/python.exe -m compileall -q inbound_verify +grep -rn "expected_undelivered" inbound_verify || echo "no stale expected_undelivered refs OK" +.venv/Scripts/python.exe -c "import inbound_verify.compare, inbound_verify.cli.router, inbound_verify.cli.server, inbound_verify.store, inbound_verify.runtime; print('import smoke OK')" +``` +Expected: compileall silent; grep prints `no stale expected_undelivered refs OK`; `import smoke OK`. + +- [ ] **Step 6: Black + commit (after user confirms)** + +```bash +.venv/Scripts/python.exe -m black inbound_verify +git add -A inbound_verify +git commit -m "refactor: rename expected_undelivered to compare + +Co-Authored-By: Claude " +``` + +--- + +## Final end-to-end re-confirm (once, after all 3 tasks; requires backend restart + re-login) + +After Task 3 commits, the running backend still has the old modules loaded. To confirm end-to-end behavior is unchanged on the refactored code: + +- [ ] **Restart backend** (TaskStop current → clean ms-playwright/anneng orphans → `python -m inbound_verify.cli.server`), re-login all sites. +- [ ] **Re-run the gate from the Tier 1 manual test:** trigger `undelivered` for 顺心/中通/韵达/安能 via API → expect 4× success + 4× `*-未到数据.xlsx`; trigger `__compare__` → expect `output/应到未到数据.xlsx`; run `python -m inbound_verify.store ingest` → expect rows UPSERTed. All green = Tier 2 behavior-identical, done. + +> If you want to skip the re-login cost: the per-task import-smoke gates already prove the import graph is correct and the changes are behavior-preserving moves. The e2e re-confirm is belt-and-suspenders. + +--- + +## Self-Review (completed) + +- **Spec coverage:** spec §8 Tier 2 — path dedup → Task 1; domain extract → Task 2; rename → compare → Task 3. config.py explicitly deferred (noted with rationale). All spec items addressed or consciously deferred. +- **Placeholder scan:** none — every step has exact code or exact commands with expected output. The domain.py content is the verbatim extracted block. +- **Type/name consistency:** `_site_cfg`, `BAISHI_FILE`, `SITE_UNDELIVERED_FILE`, `STATIONS`, `write_site_file`, `main`, `_read_business_dates` referenced consistently across tasks. Task 2 routes config to `domain` and leaves behavior (`_read_business_dates`, `write_site_file`, `main`) in compare — verified against store.py/runtime.py/router.py usages. Task 3 renames the module but preserves all public names. diff --git a/inbound_verify/cli/router.py b/inbound_verify/cli/router.py index 4629bbf..84e2417 100644 --- a/inbound_verify/cli/router.py +++ b/inbound_verify/cli/router.py @@ -24,14 +24,14 @@ from inbound_verify import state_store # 各站点模块(自动化测试 + 比对用;任务派发在 runtime) from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng -from inbound_verify import expected_undelivered +from inbound_verify import compare def run_undelivered_compare(): - """应到未到比对(全站点):调用 expected_undelivered,读 downloads/ 下的应到/实到 + """应到未到比对(全站点):调用 compare,读 downloads/ 下的应到/实到 数据,生成 output/应到未到数据.xlsx(汇总报表 + 各站明细)。""" print("\n▶ 开始执行【应到未到比对(全站点)】任务 ...") - expected_undelivered.main() + compare.main() # ==================================================================== diff --git a/inbound_verify/expected_undelivered.py b/inbound_verify/compare.py similarity index 100% rename from inbound_verify/expected_undelivered.py rename to inbound_verify/compare.py diff --git a/inbound_verify/runtime.py b/inbound_verify/runtime.py index 5700ece..27bd9a4 100644 --- a/inbound_verify/runtime.py +++ b/inbound_verify/runtime.py @@ -26,7 +26,7 @@ from inbound_verify.domain import SITE_UNDELIVERED_FILE from inbound_verify import state_store from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng -from inbound_verify import expected_undelivered # dispatch 的 compare 任务用 +from inbound_verify import compare # dispatch 的 compare 任务用 # 各网页站点首页 URL(单一来源,取自各站点模块 HOME_URL) SITES_CONFIG = { @@ -444,7 +444,7 @@ def _site_undelivered_handler(site): (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) + return compare.write_site_file(site) stale = os.path.join(DOWNLOAD_DIR, SITE_UNDELIVERED_FILE.format(name=site)) if os.path.exists(stale): os.remove(stale) @@ -472,7 +472,7 @@ TASK_HANDLERS = { ("安能", "expected"): lambda ctx: anneng.anneng_expected_download(), ("安能", "actual"): lambda ctx: anneng.anneng_actual_download(), ("安能", "undelivered"): _site_undelivered_handler("安能"), - ("__compare__", "compare"): lambda ctx: (expected_undelivered.main() or True), + ("__compare__", "compare"): lambda ctx: (compare.main() or True), } diff --git a/inbound_verify/store.py b/inbound_verify/store.py index 6ccead3..ffa38ff 100644 --- a/inbound_verify/store.py +++ b/inbound_verify/store.py @@ -34,9 +34,7 @@ from inbound_verify.domain import ( _site_cfg, ) # 站点 / 文件名配置(单一来源) -from inbound_verify import ( - expected_undelivered as eu, -) # _read_business_dates(比对侧业务日期读取) +from inbound_verify import compare # _read_business_dates(比对侧业务日期读取) SCHEMA_PATH = os.path.join(BASE_DIR, "schema.sql") @@ -111,7 +109,7 @@ def init_schema(): # ============================== 解析辅助 ============================== -# 实到单号列 / 基号列映射(口径取自 expected_undelivered): +# 实到单号列 / 基号列映射(口径取自 domain,与 STATIONS 对齐): # piece = 实到表里「每件」的单号列(扫描单号 / 子单号 / 复合串) # waybill = 与应到运单号对齐的干净列(中通无干净列,由 piece 复合串 v[:-8] 推导) # scan_time = 扫描时间列(缺失则不填,原始值仍在 raw) @@ -214,7 +212,7 @@ def _raw_row(row): def _read_business_dates(): """从状态库读各站本次业务日期(与报告口径一致;读不到返回空 dict)。""" try: - return eu._read_business_dates(ALL_SITES + ["百世"]) or {} + return compare._read_business_dates(ALL_SITES + ["百世"]) or {} except Exception as e: print(f">> [warn] 读取业务日期失败(不影响入库): {e}") return {}