迁移脚本的一些修复

This commit is contained in:
Break
2026-06-09 11:29:40 +08:00
parent 6a70713b30
commit 283e3e3eb3
8 changed files with 183 additions and 121 deletions

View File

@@ -54,7 +54,7 @@ class DeviceRow:
line_no: int
imei: str
virtual_no: str # 留空时用 imei 兜底,见 _load_devices
sim_iccids: dict[int, str] # slot → iccid(只保留非空)
sim_iccids: dict[int, str] # slot → 完整 ICCID(只保留非空)
device_name: str = ""
device_model: str = ""
device_type: str = ""
@@ -159,7 +159,7 @@ def _load_cards(path: Path, result: LoadResult) -> None:
if "iccid_20" not in header:
raise ValueError(f"{path} 必须包含 iccid_20 列(20 位运营商卡填写,CTCC 留空)")
seen_iccid_19: dict[str, int] = {}
seen_iccid_full: dict[str, int] = {}
for idx, raw in enumerate(reader, start=2):
iccid_19 = _normalize_cell(raw.get("iccid_19", ""))
iccid_20 = _normalize_cell(raw.get("iccid_20", ""))
@@ -184,13 +184,14 @@ def _load_cards(path: Path, result: LoadResult) -> None:
result.errors.append(ErrorRow("cards.csv", idx, "iccid_20", iccid_20, err.code, err.message))
continue
if iccid_19 in seen_iccid_19:
iccid_full = iccid_20 or iccid_19
if iccid_full in seen_iccid_full:
result.errors.append(
ErrorRow("cards.csv", idx, "iccid_19", iccid_19, "duplicate",
f"与第 {seen_iccid_19[iccid_19]}iccid_19 重复")
ErrorRow("cards.csv", idx, "iccid", iccid_full, "duplicate",
f"与第 {seen_iccid_full[iccid_full]}ICCID 重复")
)
continue
seen_iccid_19[iccid_19] = idx
seen_iccid_full[iccid_full] = idx
is_industry = _parse_bool(raw.get("is_industry", ""))
if is_industry is None:
@@ -292,27 +293,59 @@ def _load_devices(path: Path, result: LoadResult) -> None:
def _cross_check(result: LoadResult) -> None:
"""校验设备与卡之间的一致性,并把 bound_device_virtual_no 回填到 CardRow。
devices.csv 里 sim_iccid_X 业务方可能填 19 位也可能填 20 位,
统一截取前 19 位回查 cards.iccid_19
devices.csv 里 sim_iccid_X 业务方可能填 19 位也可能填 20 位
20 位按完整 ICCID 精确匹配;19 位只有在 cards.csv 中唯一对应一张卡时才允许
"""
from . import iccid_utils
cards_by_iccid_19: dict[str, CardRow] = {c.iccid_19: c for c in result.cards}
cards_by_full: dict[str, CardRow] = {c.iccid_full: c for c in result.cards}
cards_by_i19: dict[str, list[CardRow]] = {}
for card in result.cards:
cards_by_i19.setdefault(card.iccid_19, []).append(card)
device_card_refs: dict[str, tuple[str, int]] = {}
for device in result.devices:
for slot, raw_iccid in device.sim_iccids.items():
key19 = iccid_utils.prefix_19(raw_iccid)
if key19 not in cards_by_iccid_19:
resolved_card: Optional[CardRow] = None
if len(raw_iccid) == 20:
resolved_card = cards_by_full.get(raw_iccid)
if resolved_card is None:
prefix = iccid_utils.prefix_19(raw_iccid)
candidates = cards_by_i19.get(prefix, [])
if len(candidates) == 1 and not candidates[0].iccid_20:
resolved_card = candidates[0]
elif len(raw_iccid) == 19:
candidates = cards_by_i19.get(raw_iccid, [])
if len(candidates) > 1:
result.errors.append(
ErrorRow(
"devices.csv", device.line_no, f"sim_iccid_{slot}", raw_iccid,
"ambiguous_iccid_19", "该 19 位前缀对应多张 20 位卡,请填写完整 20 位 ICCID",
)
)
continue
if candidates:
resolved_card = candidates[0]
else:
result.errors.append(
ErrorRow(
"devices.csv", device.line_no, f"sim_iccid_{slot}", raw_iccid,
"card_not_found", "引用的 ICCID(前 19 位)在 cards.csv 中不存在",
"sim_iccid_length", "设备槽位 ICCID 必须是 19 或 20 位",
)
)
continue
if key19 in device_card_refs:
prev_vno, prev_line = device_card_refs[key19]
if resolved_card is None:
result.errors.append(
ErrorRow(
"devices.csv", device.line_no, f"sim_iccid_{slot}", raw_iccid,
"card_not_found", "引用的 ICCID 在 cards.csv 中不存在",
)
)
continue
card_key = resolved_card.iccid_full
if card_key in device_card_refs:
prev_vno, prev_line = device_card_refs[card_key]
result.errors.append(
ErrorRow(
"devices.csv", device.line_no, f"sim_iccid_{slot}", raw_iccid,
@@ -320,7 +353,7 @@ def _cross_check(result: LoadResult) -> None:
)
)
continue
device_card_refs[key19] = (device.virtual_no, device.line_no)
cards_by_iccid_19[key19].bound_device_virtual_no = device.virtual_no
# 标准化:slot 里的值统一改写为 iccid_19,sql_builder 直接用它查 cards / card_metas
device.sim_iccids[slot] = key19
device_card_refs[card_key] = (device.virtual_no, device.line_no)
resolved_card.bound_device_virtual_no = device.virtual_no
# 标准化:slot 里的值统一改写为完整 ICCID,sql_builder 直接用它查 cards / card_metas
device.sim_iccids[slot] = card_key