refactor: extract domain.py (shared site/file/colmap config)

Move ALL_REPORT_SITES / SITE_UNDELIVERED_FILE / BAISHI_FILE / BAISHI_COLUMNS / arrived_pieces_* / STATIONS / _site_cfg out of expected_undelivered into a new leaf module inbound_verify/domain.py. store.py and runtime.py now read site/file config from domain directly instead of through the compare engine (store keeps eu only for _read_business_dates). Behavior identical.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-07-24 08:38:18 +08:00
parent bd0df8909f
commit 91ef8970b8
4 changed files with 114 additions and 94 deletions

92
inbound_verify/domain.py Normal file
View File

@@ -0,0 +1,92 @@
# -*- 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)

View File

@@ -41,96 +41,21 @@ from openpyxl.worksheet.page import PageMargins
from openpyxl.worksheet.properties import PageSetupProperties from openpyxl.worksheet.properties import PageSetupProperties
from inbound_verify.paths import DOWNLOAD_DIR, OUTPUT_DIR from inbound_verify.paths import DOWNLOAD_DIR, OUTPUT_DIR
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,
)
# 比对报表输出文件(路径锚定统一走 paths.py # 比对报表输出文件(路径锚定统一走 paths.py
OUTFILE = os.path.join(OUTPUT_DIR, "应到未到数据.xlsx") OUTFILE = os.path.join(OUTPUT_DIR, "应到未到数据.xlsx")
# 汇总报表覆盖的全部站点4 站在前、百世在末;汇总页图表只取 4 站) # 站点 / 文件名 / 列映射配置ALL_REPORT_SITES / STATIONS / _site_cfg / BAISHI_FILE 等)见 domain.py。
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)
def process(name): def process(name):

View File

@@ -22,6 +22,7 @@ import yaml
from playwright.sync_api import sync_playwright from playwright.sync_api import sync_playwright
from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH from inbound_verify.paths import DOWNLOAD_DIR, CONFIG_PATH
from inbound_verify.domain import SITE_UNDELIVERED_FILE
from inbound_verify import state_store from inbound_verify import state_store
from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng from inbound_verify.sites import shunxin, baishi, zto, yunda, anneng
@@ -444,9 +445,7 @@ def _site_undelivered_handler(site):
) )
if exp_ok and act_ok: if exp_ok and act_ok:
return expected_undelivered.write_site_file(site) return expected_undelivered.write_site_file(site)
stale = os.path.join( stale = os.path.join(DOWNLOAD_DIR, SITE_UNDELIVERED_FILE.format(name=site))
DOWNLOAD_DIR, expected_undelivered.SITE_UNDELIVERED_FILE.format(name=site)
)
if os.path.exists(stale): if os.path.exists(stale):
os.remove(stale) os.remove(stale)
return False return False

View File

@@ -29,10 +29,14 @@ import yaml
from psycopg.types.json import Jsonb from psycopg.types.json import Jsonb
from inbound_verify.paths import BASE_DIR, CONFIG_PATH, DOWNLOAD_DIR from inbound_verify.paths import BASE_DIR, CONFIG_PATH, DOWNLOAD_DIR
from inbound_verify.domain import (
BAISHI_FILE,
_site_cfg,
) # 站点 / 文件名配置(单一来源)
from inbound_verify import ( from inbound_verify import (
expected_undelivered as eu, expected_undelivered as eu,
) # 复用站点 / 文件名 / 列映射 / 基号口径(单一来源 ) # _read_business_dates比对侧业务日期读取
SCHEMA_PATH = os.path.join(BASE_DIR, "schema.sql") SCHEMA_PATH = os.path.join(BASE_DIR, "schema.sql")
@@ -261,7 +265,7 @@ _SQL_UNDELIVERED = """
def _ingest_expected(cur, site, business_date): def _ingest_expected(cur, site, business_date):
"""入库单站应到(运单级,按 waybill_no 去重 keep-first 后 UPSERT""" """入库单站应到(运单级,按 waybill_no 去重 keep-first 后 UPSERT"""
cfg = eu._site_cfg(site) cfg = _site_cfg(site)
path = os.path.join(DOWNLOAD_DIR, cfg["exp"]) path = os.path.join(DOWNLOAD_DIR, cfg["exp"])
if not os.path.exists(path): if not os.path.exists(path):
print(f" [跳过] {site} 应到:文件不存在 {cfg['exp']}") print(f" [跳过] {site} 应到:文件不存在 {cfg['exp']}")
@@ -293,7 +297,7 @@ def _ingest_expected(cur, site, business_date):
def _ingest_actual(cur, site): def _ingest_actual(cur, site):
"""入库单站实到(扫描件级,按 piece_no UPSERT""" """入库单站实到(扫描件级,按 piece_no UPSERT"""
cfg = eu._site_cfg(site) cfg = _site_cfg(site)
path = os.path.join(DOWNLOAD_DIR, cfg["act"]) path = os.path.join(DOWNLOAD_DIR, cfg["act"])
if not os.path.exists(path): if not os.path.exists(path):
print(f" [跳过] {site} 实到:文件不存在 {cfg['act']}") print(f" [跳过] {site} 实到:文件不存在 {cfg['act']}")
@@ -332,9 +336,9 @@ def _ingest_actual(cur, site):
def _ingest_undelivered_baishi(cur): def _ingest_undelivered_baishi(cur):
"""入库百世应到未到明细(子单级,按 (site, piece_no) UPSERT""" """入库百世应到未到明细(子单级,按 (site, piece_no) UPSERT"""
path = os.path.join(DOWNLOAD_DIR, eu.BAISHI_FILE) path = os.path.join(DOWNLOAD_DIR, BAISHI_FILE)
if not os.path.exists(path): if not os.path.exists(path):
print(f" [跳过] 百世 未到:文件不存在 {eu.BAISHI_FILE}") print(f" [跳过] 百世 未到:文件不存在 {BAISHI_FILE}")
return 0 return 0
df = pd.read_excel(path, dtype=str).fillna("") df = pd.read_excel(path, dtype=str).fillna("")
rows = [] rows = []