refactor: rename expected_undelivered to compare
git mv expected_undelivered.py -> compare.py; update the 3 importers (store/runtime/router) to import compare. All public names (main, write_site_file, _read_business_dates) unchanged. Also add the Tier 2 plan doc. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
403
docs/superpowers/plans/2026-07-23-tier2-domain-rename-dedup.md
Normal file
403
docs/superpowers/plans/2026-07-23-tier2-domain-rename-dedup.md
Normal file
@@ -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 一致;站点下载落在 <root>/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 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
@@ -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()
|
||||
|
||||
|
||||
# ====================================================================
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Reference in New Issue
Block a user