更新一版

This commit is contained in:
Break
2026-06-12 18:10:22 +08:00
parent e139f5e227
commit 1c492fe8db
10 changed files with 1735 additions and 258 deletions

View File

@@ -17,6 +17,7 @@ from __future__ import annotations
import argparse
import csv
import sys
from dataclasses import dataclass
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -31,15 +32,30 @@ from lib.mapping_loader import ( # noqa: E402
)
def _read_iccid_pairs(cards_csv: Path) -> list[tuple[str, str, bool]]:
@dataclass(frozen=True)
class ICCIDInput:
"""业务方提供的单行 ICCID 输入。"""
line_no: int
iccid_19: str
iccid_20: str
allow_iccid_19_lookup: bool
@property
def card_key(self) -> str:
"""奇成扫描结果使用的业务方完整键。"""
return self.iccid_20 or self.iccid_19
def _read_iccid_pairs(cards_csv: Path) -> list[ICCIDInput]:
"""从 cards.csv 读 iccid_19 + iccid_20 列(只关心这两列,其他列忽略)。
返回 (iccid_19, iccid_20, allow_iccid_19_lookup) 列表。
返回带行号的 ICCID 输入列表。
只填 iccid_20 时,iccid_19 从前 19 位派生,但不用于奇成主表兜底查询。
"""
if not cards_csv.exists():
raise FileNotFoundError(f"找不到 {cards_csv},请按 resources/README.md 准备")
pairs: dict[str, tuple[str, str, bool]] = {}
pairs: dict[str, ICCIDInput] = {}
first_seen_line: dict[str, int] = {}
errors: list[str] = []
with cards_csv.open("r", encoding="utf-8-sig", newline="") as f:
@@ -75,7 +91,12 @@ def _read_iccid_pairs(cards_csv: Path) -> list[tuple[str, str, bool]]:
errors.append(f"{line_no}{'iccid_20' if i20 else 'iccid_19'} 与第 {first_seen_line[dedup_key]} 行重复: {dedup_key!r}")
continue
first_seen_line[dedup_key] = line_no
pairs[dedup_key] = (i19, i20, allow_i19_lookup)
pairs[dedup_key] = ICCIDInput(
line_no=line_no,
iccid_19=i19,
iccid_20=i20,
allow_iccid_19_lookup=allow_i19_lookup,
)
if errors:
preview = "\n".join(f" - {msg}" for msg in errors[:20])
if len(errors) > 20:
@@ -106,7 +127,10 @@ def main() -> int:
dsn = mapping_loader.load_legacy_dsn(config_dir)
with legacy_query.connect_readonly(dsn) as conn:
card_metas, dup_iccids = legacy_query.fetch_card_meta(conn, iccid_pairs)
card_metas, dup_iccids = legacy_query.fetch_card_meta(
conn,
[(x.iccid_19, x.iccid_20, x.allow_iccid_19_lookup) for x in iccid_pairs],
)
meal_ids = {m.current_meal_id for m in card_metas.values() if m.current_meal_id}
meal_metas = legacy_query.fetch_meal_meta(conn, meal_ids)
@@ -120,8 +144,11 @@ def main() -> int:
merged = _merge(existing, card_metas, meal_metas)
out_path = mapping_loader.merge_and_save(config_dir, merged)
todo = _collect_todo(merged)
unmatched = _collect_unmatched_iccids(iccid_pairs, card_metas, dup_iccids)
_write_todo(config_dir / "mapping_todo.txt", todo)
_print_summary(out_path, card_metas, meal_metas, todo)
unmatched_path = config_dir / "unmatched_iccids.csv"
_write_unmatched_iccids(unmatched_path, unmatched)
_print_summary(out_path, card_metas, meal_metas, todo, unmatched_path, len(unmatched))
return 0
@@ -134,6 +161,9 @@ def _merge(
merged = Mapping(
migration_user_id=existing.migration_user_id,
migration_batch_no=existing.migration_batch_no,
ownership_rules=existing.ownership_rules,
package_rules=existing.package_rules,
overrides=existing.overrides,
carriers=dict(existing.carriers),
packages=dict(existing.packages),
series=dict(existing.series),
@@ -210,6 +240,19 @@ def _collect_todo(mapping: Mapping) -> dict[str, list[str]]:
return todo
def _collect_unmatched_iccids(
iccid_pairs: list[ICCIDInput],
card_metas: dict[str, "legacy_query.LegacyCardMeta"],
duplicate_iccids: set[str],
) -> list[ICCIDInput]:
"""收集未在奇成 tbl_card 命中的 ICCID。
重复命中的卡属于脏数据异常,不是未命中,因此从未命中清单中排除。
"""
resolved_keys = set(card_metas) | set(duplicate_iccids)
return [item for item in iccid_pairs if item.card_key not in resolved_keys]
def _write_todo(path: Path, todo: dict[str, list[str]]) -> None:
lines = ["奇成迁移映射待填写清单", "=" * 40, ""]
for kind in ("carriers", "packages", "agents"):
@@ -220,13 +263,47 @@ def _write_todo(path: Path, todo: dict[str, list[str]]) -> None:
path.write_text("\n".join(lines), encoding="utf-8")
def _print_summary(out_path: Path, card_metas, meal_metas, todo: dict[str, list[str]]) -> None:
def _write_unmatched_iccids(path: Path, unmatched: list[ICCIDInput]) -> None:
"""把未命中的 ICCID 写成 CSV,便于转给业务方核对。"""
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8-sig", newline="") as f:
writer = csv.DictWriter(
f,
fieldnames=["line_no", "iccid_19", "iccid_20", "lookup_keys", "note"],
)
writer.writeheader()
for item in unmatched:
lookup_keys = [item.iccid_20 or item.iccid_19]
if item.iccid_20 and item.allow_iccid_19_lookup:
lookup_keys.append(item.iccid_19)
writer.writerow(
{
"line_no": item.line_no,
"iccid_19": item.iccid_19,
"iccid_20": item.iccid_20,
"lookup_keys": ";".join(lookup_keys),
"note": "奇成 tbl_card 未命中",
}
)
def _print_summary(
out_path: Path,
card_metas,
meal_metas,
todo: dict[str, list[str]],
unmatched_path: Path,
unmatched_count: int,
) -> None:
print(f"扫描完成,mapping 写入: {out_path}")
print(f" 奇成卡命中 : {len(card_metas)}")
print(f" 奇成卡未命中 : {unmatched_count}")
print(f" 奇成套餐命中 : {len(meal_metas)}")
print(f" 待填 carriers : {len(todo['carriers'])}")
print(f" 待填 packages : {len(todo['packages'])}")
print(f" 待填 agents : {len(todo['agents'])}")
if unmatched_count:
print(f"未命中清单: {unmatched_path}")
if any(todo.values()):
print(f"详见: {out_path.parent / 'mapping_todo.txt'}")