优化迁移脚本速度
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s

This commit is contained in:
2026-07-10 13:10:00 +09:00
parent b38df737e1
commit 31232ea899
4 changed files with 405 additions and 180 deletions

View File

@@ -6,12 +6,29 @@
from __future__ import annotations from __future__ import annotations
import contextlib import contextlib
import time
from dataclasses import dataclass from dataclasses import dataclass
from decimal import Decimal from decimal import Decimal
from typing import Any, Iterable, Iterator, Optional from typing import Any, Iterable, Iterator, Optional
BATCH_SIZE = 200 # 重构后分阶段索引查询已无大 JOIN,1000 为安全默认值
BATCH_SIZE = 1000
class _StageTimer:
"""阶段耗时计时器,记录迁移各阶段用时。"""
def __init__(self, func_name: str) -> None:
self._func = func_name
self._t = time.perf_counter()
def log(self, stage: str, **kw: Any) -> None:
elapsed = time.perf_counter() - self._t
parts = " ".join(f"{k}={v}" for k, v in kw.items())
suffix = f" {parts}" if parts else ""
print(f"[legacy] {self._func}.{stage}:{suffix} elapsed={elapsed:.1f}s")
self._t = time.perf_counter()
def _normalize_iccid_pairs(iccid_pairs: Iterable[tuple]) -> list[tuple[str, str, bool]]: def _normalize_iccid_pairs(iccid_pairs: Iterable[tuple]) -> list[tuple[str, str, bool]]:
@@ -236,14 +253,35 @@ def _to_mb_decimal(value) -> Decimal:
return Decimal("0") return Decimal("0")
def _build_card_lookup(
pairs: list[tuple[str, str, bool]],
) -> dict[str, tuple[str, int]]:
"""构建 tbl_card 查询键 → (完整 ICCID, rank) 映射。
rank=0 表示完整精确命中,rank=1 表示 19 位兜底命中。
"""
by_full: dict[str, tuple[str, int]] = {}
for i19, i20, allow_i19_lookup in pairs:
card_key = i20 or i19
by_full[card_key] = (card_key, 0)
if i20 and allow_i19_lookup:
by_full[i19] = (card_key, 1)
return by_full
# ---------------- 查询函数 ---------------- # ---------------- 查询函数 ----------------
def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPackage]: def fetch_current_packages(
conn,
iccids: Iterable[str],
batch_size: int = BATCH_SIZE,
) -> dict[str, LegacyPackage]:
"""查每张卡的当前生效主套餐:status=1、type!=1 且 expire_date 最大。 """查每张卡的当前生效主套餐:status=1、type!=1 且 expire_date 最大。
返回: iccid → LegacyPackage(找不到的 iccid 不在结果中) 返回: iccid → LegacyPackage(找不到的 iccid 不在结果中)
""" """
timer = _StageTimer("fetch_current_packages")
iccid_list = sorted({i for i in iccids if i}) iccid_list = sorted({i for i in iccids if i})
out: dict[str, LegacyPackage] = {} out: dict[str, LegacyPackage] = {}
if not iccid_list: if not iccid_list:
@@ -266,11 +304,15 @@ def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPacka
ORDER BY iccid_mark ASC, expire_date DESC ORDER BY iccid_mark ASC, expire_date DESC
""" """
seen: set[str] = set() seen: set[str] = set()
rows = 0
batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(iccid_list): for batch in _chunks(iccid_list, batch_size):
batches += 1
cur.execute(sql, (tuple(batch),)) cur.execute(sql, (tuple(batch),))
for row in cur.fetchall(): for row in cur.fetchall():
iccid = row["iccid_mark"] iccid = row["iccid_mark"]
rows += 1
if not iccid or iccid in seen: if not iccid or iccid in seen:
continue continue
seen.add(iccid) seen.add(iccid)
@@ -284,15 +326,21 @@ def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPacka
flow_size_mb=int(row.get("flow_size") or 0), flow_size_mb=int(row.get("flow_size") or 0),
status=int(row.get("status") or 0), status=int(row.get("status") or 0),
) )
timer.log("tbl_card_life", keys=len(iccid_list), batches=batches, rows=rows, found=len(out))
return out return out
def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, list[LegacyPackageLifecycle]]: def fetch_package_lifecycles(
conn,
iccid_pairs: Iterable[tuple],
batch_size: int = BATCH_SIZE,
) -> dict[str, list[LegacyPackageLifecycle]]:
"""查当前生效和未生效待生效正式套餐生命周期。 """查当前生效和未生效待生效正式套餐生命周期。
只返回可迁移范围(active/pending)和需要在审核文件中说明的跳过记录。 只返回可迁移范围(active/pending)和需要在审核文件中说明的跳过记录。
加油包(type=1)和已过期记录不生成套餐 SQL,但保留 skip_reason 供审核产物输出。 加油包(type=1)和已过期记录不生成套餐 SQL,但保留 skip_reason 供审核产物输出。
""" """
timer = _StageTimer("fetch_package_lifecycles")
pairs = _normalize_iccid_pairs(iccid_pairs) pairs = _normalize_iccid_pairs(iccid_pairs)
out: dict[str, list[LegacyPackageLifecycle]] = {} out: dict[str, list[LegacyPackageLifecycle]] = {}
if not pairs: if not pairs:
@@ -328,8 +376,11 @@ def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, li
COALESCE(expire_date, '9999-12-31') ASC, COALESCE(expire_date, '9999-12-31') ASC,
id ASC id ASC
""" """
rows = 0
batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(iccid_list): for batch in _chunks(iccid_list, batch_size):
batches += 1
cur.execute(sql, (tuple(batch),)) cur.execute(sql, (tuple(batch),))
for row in cur.fetchall(): for row in cur.fetchall():
legacy_iccid = row.get("iccid_mark") or "" legacy_iccid = row.get("iccid_mark") or ""
@@ -338,6 +389,7 @@ def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, li
iccid = query_to_full.get(legacy_iccid) iccid = query_to_full.get(legacy_iccid)
if not iccid: if not iccid:
continue continue
rows += 1
start = row.get("start_date") start = row.get("start_date")
expire = row.get("expire_date") expire = row.get("expire_date")
start_str = start.strftime("%Y-%m-%d %H:%M:%S") if start else None start_str = start.strftime("%Y-%m-%d %H:%M:%S") if start else None
@@ -363,6 +415,7 @@ def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, li
skip_reason=skip_reason, skip_reason=skip_reason,
) )
out.setdefault(iccid, []).append(item) out.setdefault(iccid, []).append(item)
timer.log("tbl_card_life", keys=len(iccid_list), batches=batches, rows=rows)
next_sql = """ next_sql = """
SELECT SELECT
@@ -384,8 +437,11 @@ def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, li
COALESCE(effect_complete_time, '9999-12-31') ASC, COALESCE(effect_complete_time, '9999-12-31') ASC,
id ASC id ASC
""" """
next_rows = 0
next_batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(iccid_list): for batch in _chunks(iccid_list, batch_size):
next_batches += 1
cur.execute(next_sql, (tuple(batch),)) cur.execute(next_sql, (tuple(batch),))
for row in cur.fetchall(): for row in cur.fetchall():
legacy_iccid = row.get("iccid_mark") or "" legacy_iccid = row.get("iccid_mark") or ""
@@ -394,6 +450,7 @@ def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, li
iccid = query_to_full.get(legacy_iccid) iccid = query_to_full.get(legacy_iccid)
if not iccid: if not iccid:
continue continue
next_rows += 1
start = row.get("start_date") start = row.get("start_date")
expire = row.get("expire_date") expire = row.get("expire_date")
start_str = start.strftime("%Y-%m-%d %H:%M:%S") if start else None start_str = start.strftime("%Y-%m-%d %H:%M:%S") if start else None
@@ -417,6 +474,7 @@ def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, li
migration_status=migration_status, migration_status=migration_status,
skip_reason=skip_reason, skip_reason=skip_reason,
)) ))
timer.log("tbl_next_month_card_life", keys=len(iccid_list), batches=next_batches, rows=next_rows)
return out return out
@@ -448,13 +506,18 @@ def _classify_next_month_lifecycle(status: int) -> tuple[str, str]:
return "skipped", f"奇成次月待生效状态 {status} 不在迁移范围" return "skipped", f"奇成次月待生效状态 {status} 不在迁移范围"
def fetch_card_usage(conn, iccid_pairs: Iterable[tuple]) -> dict[str, LegacyCardUsage]: def fetch_card_usage(
"""查每张卡的累计已用流量,并同步取当前生效正式套餐的 flow_add_discount。 conn,
iccid_pairs: Iterable[tuple],
batch_size: int = BATCH_SIZE,
) -> dict[str, LegacyCardUsage]:
"""查每张卡的累计已用流量,分阶段索引查询替代大 JOIN。
奇成 ``tbl_card.total_bytes_cnt`` 是虚用量(已被套餐虚比加成)。 阶段:
这里 JOIN 当前生效正式套餐(``tbl_card_life`` status=1,type!=1 且 ``expire_date`` 最大) 1. tbl_card — iccid_mark + total_bytes_cnt
+ ``tbl_set_meal`` 取 ``flow_add_discount``,放进 ``LegacyCardUsage``; 2. tbl_card_life — 当前生效正式套餐 meal_id(Python 选 expire_date 最大)
上层通过 ``real_used_mb_floor`` 反算真用量写入新系统。 3. tbl_set_meal — flow_add_discount(按 meal_id 精确查)
4. Python 组装 LegacyCardUsage
Args: Args:
iccid_pairs: (iccid_19, iccid_20, allow_iccid_19_lookup) 列表。 iccid_pairs: (iccid_19, iccid_20, allow_iccid_19_lookup) 列表。
@@ -462,88 +525,119 @@ def fetch_card_usage(conn, iccid_pairs: Iterable[tuple]) -> dict[str, LegacyCard
Returns: Returns:
{完整 ICCID: LegacyCardUsage}。 {完整 ICCID: LegacyCardUsage}。
""" """
timer = _StageTimer("fetch_card_usage")
pairs = _normalize_iccid_pairs(iccid_pairs) pairs = _normalize_iccid_pairs(iccid_pairs)
out: dict[str, LegacyCardUsage] = {} out: dict[str, LegacyCardUsage] = {}
if not pairs: if not pairs:
return out return out
query_to_full: dict[str, tuple[str, int]] = {} by_full = _build_card_lookup(pairs)
related_keys_by_full: dict[str, set[str]] = {} full_set = sorted(by_full.keys())
for i19, i20, allow_i19_lookup in pairs:
card_key = i20 or i19
# tbl_card 主表同时查优先键和兜底键,结果选择时按 rank:
# 0=业务方完整 ICCID 精确命中,1=20 位卡在奇成主表只存 19 位时兜底。
query_to_full[card_key] = (card_key, 0)
if i20 and allow_i19_lookup:
query_to_full[i19] = (card_key, 1)
if not i20 or allow_i19_lookup:
related_keys_by_full.setdefault(card_key, set()).add(i19)
else:
related_keys_by_full.setdefault(card_key, set())
if i20:
related_keys_by_full[card_key].add(i20)
full_set = sorted(query_to_full.keys())
# tbl_card 主表用 iccid_mark IN 全长精确匹配;从表只在主表本身是 19 位时 # ── 阶段1: tbl_card ───────────────────────────────────────────────────────
# 按前缀兜底,避免同前缀 20 位卡互相串套餐。 card_sql = """
sql = """
SELECT SELECT
c.iccid_mark, iccid_mark,
c.total_bytes_cnt, COALESCE(total_bytes_cnt, 0) AS total_bytes_cnt
COALESCE(sm.flow_add_discount, 0) AS flow_add_discount, FROM tbl_card
CASE WHEN cl.iccid_mark IS NOT NULL THEN 1 ELSE 0 END AS has_active_package WHERE iccid_mark IN %s
FROM tbl_card c
LEFT JOIN (
SELECT cl1.iccid_mark, cl1.meal_id
FROM tbl_card_life cl1
INNER JOIN (
SELECT iccid_mark, MAX(expire_date) AS max_expire
FROM tbl_card_life
WHERE status = 1
AND COALESCE(CAST(type AS CHAR), '') <> '1'
AND iccid_mark IN %s
GROUP BY iccid_mark
) latest
ON latest.iccid_mark = cl1.iccid_mark
AND latest.max_expire = cl1.expire_date
WHERE cl1.status = 1
AND COALESCE(CAST(cl1.type AS CHAR), '') <> '1'
) cl ON (
cl.iccid_mark = c.iccid_mark
OR (CHAR_LENGTH(c.iccid_mark) = 19 AND LEFT(cl.iccid_mark, 19) = c.iccid_mark)
)
LEFT JOIN tbl_set_meal sm ON sm.id = cl.meal_id
WHERE c.iccid_mark IN %s
""" """
hits: dict[str, list[tuple[int, dict]]] = {} hits: dict[str, list[tuple[int, dict]]] = {}
card_rows = 0
card_batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(full_set): for batch in _chunks(full_set, batch_size):
batch_keys = {query_to_full[x][0] for x in batch} card_batches += 1
related_batch = sorted({x for key in batch_keys for x in related_keys_by_full[key]}) cur.execute(card_sql, (tuple(batch),))
cur.execute(sql, (tuple(related_batch), tuple(batch)))
for row in cur.fetchall(): for row in cur.fetchall():
legacy_iccid = row["iccid_mark"] legacy_iccid = row["iccid_mark"]
if not legacy_iccid: if not legacy_iccid:
continue continue
matched = query_to_full.get(legacy_iccid) matched = by_full.get(legacy_iccid)
if matched is None: if matched is None:
continue continue
card_key, rank = matched card_key, rank = matched
hits.setdefault(card_key, []).append((rank, row)) hits.setdefault(card_key, []).append((rank, row))
card_rows += 1
timer.log("tbl_card", keys=len(full_set), batches=card_batches, rows=card_rows)
# 取最优命中行,构建 legacy_iccid 列表
card_data: dict[str, dict] = {} # card_key → best tbl_card row
for card_key, rows in hits.items(): for card_key, rows in hits.items():
best_rank = min(rank for rank, _row in rows) best_rank = min(r for r, _ in rows)
row = next(row for rank, row in rows if rank == best_rank) card_data[card_key] = next(row for r, row in rows if r == best_rank)
if not card_data:
return out
legacy_iccids = sorted({row["iccid_mark"] for row in card_data.values()})
# ── 阶段2: tbl_card_life 当前生效正式套餐(精确匹配,Python 选最新) ─────────
life_sql = """
SELECT
iccid_mark,
COALESCE(meal_id, '') AS meal_id
FROM tbl_card_life
WHERE iccid_mark IN %s
AND status = 1
AND COALESCE(CAST(type AS CHAR), '') <> '1'
ORDER BY iccid_mark ASC, expire_date DESC, id DESC
"""
current_meal_by_iccid: dict[str, str] = {} # legacy_iccid → meal_id
life_rows = 0
life_batches = 0
with conn.cursor() as cur:
for batch in _chunks(legacy_iccids, batch_size):
life_batches += 1
cur.execute(life_sql, (tuple(batch),))
for row in cur.fetchall():
iccid = row["iccid_mark"]
life_rows += 1
if iccid not in current_meal_by_iccid:
current_meal_by_iccid[iccid] = row.get("meal_id") or ""
timer.log("tbl_card_life", keys=len(legacy_iccids), batches=life_batches, rows=life_rows)
# ── 阶段3: tbl_set_meal flow_add_discount(按 distinct meal_id 查) ─────────
meal_ids = sorted({mid for mid in current_meal_by_iccid.values() if mid})
discount_by_meal: dict[str, Decimal] = {}
if meal_ids:
meal_sql = "SELECT id, COALESCE(flow_add_discount, 0) AS flow_add_discount FROM tbl_set_meal WHERE id IN %s"
meal_batches = 0
with conn.cursor() as cur:
for batch in _chunks(meal_ids, batch_size):
meal_batches += 1
cur.execute(meal_sql, (tuple(batch),))
for row in cur.fetchall():
mid = str(row["id"] or "")
if mid:
discount_by_meal[mid] = _to_mb_decimal(row.get("flow_add_discount"))
timer.log("tbl_set_meal", meal_ids=len(meal_ids), batches=meal_batches)
else:
timer.log("tbl_set_meal", meal_ids=0, skipped=True)
# ── 阶段4: Python 组装 ────────────────────────────────────────────────────
for card_key, base_row in card_data.items():
legacy_iccid = base_row["iccid_mark"]
meal_id = current_meal_by_iccid.get(legacy_iccid, "")
has_active = bool(meal_id or legacy_iccid in current_meal_by_iccid)
discount = discount_by_meal.get(str(meal_id), Decimal("0")) if meal_id else Decimal("0")
out[card_key] = LegacyCardUsage( out[card_key] = LegacyCardUsage(
iccid=card_key, iccid=card_key,
virtual_used_mb=_to_mb_decimal(row.get("total_bytes_cnt")), virtual_used_mb=_to_mb_decimal(base_row.get("total_bytes_cnt")),
flow_add_discount_pct=_to_mb_decimal(row.get("flow_add_discount")), flow_add_discount_pct=discount,
has_active_package=bool(row.get("has_active_package")), has_active_package=has_active,
) )
timer.log("assemble", cards=len(out))
return out return out
def fetch_commission_accounts(conn, agent_ids: Iterable[str]) -> dict[str, LegacyCommissionAccount]: def fetch_commission_accounts(
conn,
agent_ids: Iterable[str],
batch_size: int = BATCH_SIZE,
) -> dict[str, LegacyCommissionAccount]:
"""查代理佣金账户。""" """查代理佣金账户。"""
timer = _StageTimer("fetch_commission_accounts")
agent_list = sorted({a for a in agent_ids if a}) agent_list = sorted({a for a in agent_ids if a})
out: dict[str, LegacyCommissionAccount] = {} out: dict[str, LegacyCommissionAccount] = {}
if not agent_list: if not agent_list:
@@ -557,11 +651,15 @@ def fetch_commission_accounts(conn, agent_ids: Iterable[str]) -> dict[str, Legac
FROM tbl_agent_commission_account FROM tbl_agent_commission_account
WHERE agent_id IN %s WHERE agent_id IN %s
""" """
rows = 0
batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(agent_list): for batch in _chunks(agent_list, batch_size):
batches += 1
cur.execute(sql, (tuple(batch),)) cur.execute(sql, (tuple(batch),))
for row in cur.fetchall(): for row in cur.fetchall():
aid = row["agent_id"] aid = row["agent_id"]
rows += 1
if not aid: if not aid:
continue continue
out[aid] = LegacyCommissionAccount( out[aid] = LegacyCommissionAccount(
@@ -572,107 +670,69 @@ def fetch_commission_accounts(conn, agent_ids: Iterable[str]) -> dict[str, Legac
total_commission_amount_fen=_to_fen(row.get("total_commision_amount")), total_commission_amount_fen=_to_fen(row.get("total_commision_amount")),
total_draw_amount_fen=_to_fen(row.get("total_draw_amount")), total_draw_amount_fen=_to_fen(row.get("total_draw_amount")),
) )
timer.log("tbl_agent_commission_account", agents=len(agent_list), batches=batches, rows=rows)
return out return out
def fetch_card_meta( def fetch_card_meta(
conn, conn,
iccid_pairs: Iterable[tuple], iccid_pairs: Iterable[tuple],
batch_size: int = BATCH_SIZE,
) -> tuple[dict[str, LegacyCardMeta], set[str]]: ) -> tuple[dict[str, LegacyCardMeta], set[str]]:
"""查每张卡的元数据(卡本体 + 当前生效套餐 + 虚拟号) """查每张卡的元数据,分阶段索引查询替代大 JOIN
阶段:
1. tbl_card — 卡基础字段 + 去重检测
2. tbl_vendor_category — 运营商分类名称(按 distinct category 查)
3. tbl_card_life — 当前生效正式套餐(Python 选 expire_date 最大,精确匹配)
4. tbl_set_meal + tbl_set_meal_series — 套餐系列(按 distinct meal_id 查)
5. tbl_virtual_number — 虚拟号(精确匹配,不做 LEFT() 前缀扫描)
6. Python 组装 LegacyCardMeta
匹配策略: 匹配策略:
- tbl_card 主表同时查优先键和显式允许的兜底键:有 iccid_20 时优先 - tbl_card 主表同时查优先键和显式允许的兜底键;20 位精确命中优先
20 位精确命中;只有业务方也显式填了 iccid_19 时,才允许退回 19 位。 - 从表(card_life / virtual_number)用 tbl_card 实际返回的 iccid_mark 精确查询
只填 iccid_20 的固定 20 位卡绝不拿 19 位前缀查主表。 - 不再使用 LEFT(vn.iccid_mark, 19) 前缀匹配,避免全表扫描
- tbl_card_life / tbl_virtual_number 从表在主表本身是 19 位时才用
LEFT(iccid_mark, 19) 兜底,避免同前缀 20 位卡互相串数据
Args: Args:
iccid_pairs: (iccid_19, iccid_20, allow_iccid_19_lookup) 列表。 iccid_pairs: (iccid_19, iccid_20, allow_iccid_19_lookup) 列表。
Returns: Returns:
(metas, duplicate_iccids): (metas, duplicate_iccids):
- metas: {完整 ICCID: LegacyCardMeta} 单匹结果 - metas: {完整 ICCID: LegacyCardMeta}
- duplicate_iccids: 一份业务方完整 ICCID 命中奇成 tbl_card 多条记录的 set, - duplicate_iccids: 一份业务方 ICCID 命中多条 tbl_card 记录的 set
这些卡不进 metas,sql_builder 会写 errors.csv 阻断
""" """
timer = _StageTimer("fetch_card_meta")
pairs = _normalize_iccid_pairs(iccid_pairs) pairs = _normalize_iccid_pairs(iccid_pairs)
if not pairs: if not pairs:
return {}, set() return {}, set()
by_full: dict[str, tuple[str, int]] = {} # tbl_card 查询键 → (完整 ICCID, rank) by_full = _build_card_lookup(pairs)
related_keys_by_full: dict[str, set[str]] = {}
for i19, i20, allow_i19_lookup in pairs:
card_key = i20 or i19
by_full[card_key] = (card_key, 0)
if i20 and allow_i19_lookup:
by_full[i19] = (card_key, 1)
if not i20 or allow_i19_lookup:
related_keys_by_full.setdefault(card_key, set()).add(i19)
else:
related_keys_by_full.setdefault(card_key, set())
if i20:
related_keys_by_full[card_key].add(i20)
full_set = sorted(by_full.keys()) full_set = sorted(by_full.keys())
sql = """ # ── 阶段1: tbl_card ───────────────────────────────────────────────────────
card_sql = """
SELECT SELECT
c.id AS card_id, id AS card_id,
c.iccid_mark AS iccid, iccid_mark,
COALESCE(c.account_id, '') AS account_id, COALESCE(account_id, '') AS account_id,
COALESCE(c.account_name, '') AS account_name, COALESCE(account_name, '') AS account_name,
COALESCE(c.category, 0) AS category, COALESCE(category, 0) AS category,
COALESCE(vc.category_name, '') AS category_name, COALESCE(agent_id, '') AS agent_id,
COALESCE(c.agent_id, '') AS agent_id, COALESCE(agent_name, '') AS agent_name,
COALESCE(c.agent_name, '') AS agent_name, COALESCE(phone, '') AS phone
COALESCE(c.phone, '') AS phone, FROM tbl_card
COALESCE(vn.vcode, '') AS vcode, WHERE iccid_mark IN %s
COALESCE(cl.meal_id, '') AS meal_id,
COALESCE(cl.meal_name, '') AS meal_name,
COALESCE(CAST(cl.meal_type AS CHAR), '') AS meal_type,
cl.expire_date AS expire_date,
COALESCE(sm.meal_series_id, '') AS series_id,
COALESCE(sms.name, '') AS series_name
FROM tbl_card c
LEFT JOIN tbl_vendor_category vc ON vc.category = c.category
LEFT JOIN tbl_virtual_number vn ON (
vn.iccid_mark = c.iccid_mark
OR (CHAR_LENGTH(c.iccid_mark) = 19 AND LEFT(vn.iccid_mark, 19) = c.iccid_mark)
)
LEFT JOIN (
SELECT cl1.iccid_mark, cl1.meal_id, cl1.meal_name, cl1.type AS meal_type, cl1.expire_date
FROM tbl_card_life cl1
INNER JOIN (
SELECT iccid_mark, MAX(expire_date) AS max_expire
FROM tbl_card_life
WHERE status = 1
AND COALESCE(CAST(type AS CHAR), '') <> '1'
AND iccid_mark IN %s
GROUP BY iccid_mark
) latest
ON latest.iccid_mark = cl1.iccid_mark
AND latest.max_expire = cl1.expire_date
WHERE cl1.status = 1
AND COALESCE(CAST(cl1.type AS CHAR), '') <> '1'
) cl ON (
cl.iccid_mark = c.iccid_mark
OR (CHAR_LENGTH(c.iccid_mark) = 19 AND LEFT(cl.iccid_mark, 19) = c.iccid_mark)
)
LEFT JOIN tbl_set_meal sm ON sm.id = cl.meal_id
LEFT JOIN tbl_set_meal_series sms ON sms.id = sm.meal_series_id
WHERE c.iccid_mark IN %s
""" """
# 收集每个完整 ICCID 命中的奇成行(可能多条 = 脏数据)
hits: dict[str, list[tuple[int, dict]]] = {} hits: dict[str, list[tuple[int, dict]]] = {}
card_rows = 0
card_batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(full_set): for batch in _chunks(full_set, batch_size):
batch_keys = {by_full[x][0] for x in batch} card_batches += 1
related_batch = sorted({x for key in batch_keys for x in related_keys_by_full[key]}) cur.execute(card_sql, (tuple(batch),))
cur.execute(sql, (tuple(related_batch), tuple(batch)))
for row in cur.fetchall(): for row in cur.fetchall():
legacy_iccid = row["iccid"] legacy_iccid = row["iccid_mark"]
if not legacy_iccid: if not legacy_iccid:
continue continue
matched = by_full.get(legacy_iccid) matched = by_full.get(legacy_iccid)
@@ -680,45 +740,162 @@ def fetch_card_meta(
continue continue
card_key, rank = matched card_key, rank = matched
hits.setdefault(card_key, []).append((rank, row)) hits.setdefault(card_key, []).append((rank, row))
card_rows += 1
timer.log("tbl_card", keys=len(full_set), batches=card_batches, rows=card_rows)
metas: dict[str, LegacyCardMeta] = {} # 去重:同一完整 ICCID 命中多条不同 card_id → duplicates
metas_base: dict[str, dict] = {} # card_key → best tbl_card row
duplicates: set[str] = set() duplicates: set[str] = set()
categories: set[int] = set()
for card_key, rows in hits.items(): for card_key, rows in hits.items():
best_rank = min(rank for rank, _row in rows) best_rank = min(r for r, _ in rows)
best_rows = [row for rank, row in rows if rank == best_rank] best_rows = [row for r, row in rows if r == best_rank]
# 同一张 tbl_card 可能因 card_life / virtual_number 前缀兜底 JOIN 出多行,
# 这不是主表重复;只有不同 card_id 才算同一业务 ICCID 命中多张卡。
rows_by_card_id: dict[str, dict] = {} rows_by_card_id: dict[str, dict] = {}
for row in best_rows: for row in best_rows:
rows_by_card_id[str(row.get("card_id") or row.get("iccid") or "")] = row rows_by_card_id[str(row.get("card_id") or row.get("iccid_mark") or "")] = row
if len(rows_by_card_id) > 1: if len(rows_by_card_id) > 1:
duplicates.add(card_key) duplicates.add(card_key)
continue continue
row = next(iter(rows_by_card_id.values())) row = next(iter(rows_by_card_id.values()))
expire = row.get("expire_date") metas_base[card_key] = row
categories.add(int(row.get("category") or 0))
if not metas_base:
return {}, duplicates
# legacy_iccids: tbl_card 实际存储的 iccid_mark 值(用于从表精确查询)
legacy_iccids = sorted({row["iccid_mark"] for row in metas_base.values()})
# ── 阶段2: tbl_vendor_category(distinct category,通常极少几十个) ─────────
category_names: dict[int, str] = {}
cat_batches = 0
if categories:
cat_sql = """
SELECT category, COALESCE(category_name, '') AS category_name
FROM tbl_vendor_category
WHERE category IN %s
"""
cat_list = sorted(categories)
with conn.cursor() as cur:
for batch in _chunks(cat_list, batch_size):
cat_batches += 1
cur.execute(cat_sql, (tuple(batch),))
for row in cur.fetchall():
category_names[int(row["category"])] = row.get("category_name") or ""
timer.log("tbl_vendor_category", categories=len(categories), batches=cat_batches)
# ── 阶段3: tbl_card_life 当前生效正式套餐(精确匹配,Python 选最新) ─────────
# 精确用 legacy_iccid 查询;不再做 LEFT(iccid_mark, 19) 前缀扫描
life_sql = """
SELECT
iccid_mark,
COALESCE(meal_id, '') AS meal_id,
COALESCE(meal_name, '') AS meal_name,
COALESCE(CAST(type AS CHAR), '') AS meal_type,
expire_date
FROM tbl_card_life
WHERE iccid_mark IN %s
AND status = 1
AND COALESCE(CAST(type AS CHAR), '') <> '1'
ORDER BY iccid_mark ASC, expire_date DESC, id DESC
"""
current_meals: dict[str, dict] = {} # legacy_iccid → 当前生效套餐行
meal_ids: set[str] = set()
life_rows = 0
life_batches = 0
with conn.cursor() as cur:
for batch in _chunks(legacy_iccids, batch_size):
life_batches += 1
cur.execute(life_sql, (tuple(batch),))
for row in cur.fetchall():
iccid = row["iccid_mark"]
life_rows += 1
if iccid not in current_meals:
current_meals[iccid] = row
if row.get("meal_id"):
meal_ids.add(row["meal_id"])
timer.log("tbl_card_life", keys=len(legacy_iccids), batches=life_batches, rows=life_rows, found=len(current_meals))
# ── 阶段4: tbl_set_meal + tbl_set_meal_series(按 distinct meal_id) ───────
meal_series: dict[str, tuple[str, str]] = {} # meal_id → (series_id, series_name)
meal_batches = 0
if meal_ids:
meal_sql = """
SELECT
sm.id AS meal_id,
COALESCE(sm.meal_series_id, '') AS series_id,
COALESCE(sms.name, '') AS series_name
FROM tbl_set_meal sm
LEFT JOIN tbl_set_meal_series sms ON sms.id = sm.meal_series_id
WHERE sm.id IN %s
"""
meal_id_list = sorted(meal_ids)
with conn.cursor() as cur:
for batch in _chunks(meal_id_list, batch_size):
meal_batches += 1
cur.execute(meal_sql, (tuple(batch),))
for row in cur.fetchall():
mid = str(row["meal_id"] or "")
if mid:
meal_series[mid] = (row.get("series_id") or "", row.get("series_name") or "")
timer.log("tbl_set_meal", meal_ids=len(meal_ids), batches=meal_batches)
# ── 阶段5: tbl_virtual_number(精确匹配,不做前缀扫描) ─────────────────────
virtual_nos: dict[str, str] = {} # legacy_iccid → vcode
vn_rows = 0
vn_batches = 0
with conn.cursor() as cur:
for batch in _chunks(legacy_iccids, batch_size):
vn_batches += 1
cur.execute(
"SELECT iccid_mark, COALESCE(vcode, '') AS vcode FROM tbl_virtual_number WHERE iccid_mark IN %s",
(tuple(batch),),
)
for row in cur.fetchall():
iccid = row["iccid_mark"]
vn_rows += 1
if iccid:
virtual_nos[iccid] = row.get("vcode") or ""
timer.log("tbl_virtual_number", keys=len(legacy_iccids), batches=vn_batches, rows=vn_rows)
# ── 阶段6: Python 组装 ────────────────────────────────────────────────────
metas: dict[str, LegacyCardMeta] = {}
for card_key, base_row in metas_base.items():
legacy_iccid = base_row["iccid_mark"]
meal_row = current_meals.get(legacy_iccid, {})
expire = meal_row.get("expire_date")
expire_str = expire.strftime("%Y-%m-%d %H:%M:%S") if expire else None expire_str = expire.strftime("%Y-%m-%d %H:%M:%S") if expire else None
meal_id = meal_row.get("meal_id") or ""
series_id, series_name = meal_series.get(str(meal_id), ("", ""))
metas[card_key] = LegacyCardMeta( metas[card_key] = LegacyCardMeta(
legacy_raw_iccid=row["iccid"], legacy_raw_iccid=legacy_iccid,
account_id=row.get("account_id") or "", account_id=base_row.get("account_id") or "",
account_name=row.get("account_name") or "", account_name=base_row.get("account_name") or "",
category=int(row.get("category") or 0), category=int(base_row.get("category") or 0),
category_name=row.get("category_name") or "", category_name=category_names.get(int(base_row.get("category") or 0), ""),
agent_id=row.get("agent_id") or "", agent_id=base_row.get("agent_id") or "",
agent_name=row.get("agent_name") or "", agent_name=base_row.get("agent_name") or "",
msisdn=row.get("phone") or "", msisdn=base_row.get("phone") or "",
virtual_no=row.get("vcode") or "", virtual_no=virtual_nos.get(legacy_iccid, ""),
current_meal_id=row.get("meal_id") or "", current_meal_id=meal_id,
current_meal_name=row.get("meal_name") or "", current_meal_name=meal_row.get("meal_name") or "",
current_meal_type=str(row.get("meal_type") or ""), current_meal_type=str(meal_row.get("meal_type") or ""),
current_expire_date=expire_str, current_expire_date=expire_str,
current_series_id=row.get("series_id") or "", current_series_id=series_id,
current_series_name=row.get("series_name") or "", current_series_name=series_name,
) )
timer.log("assemble", cards=len(metas), duplicates=len(duplicates))
return metas, duplicates return metas, duplicates
def fetch_meal_meta(conn, meal_ids: Iterable[str]) -> dict[str, LegacyMealMeta]: def fetch_meal_meta(
conn,
meal_ids: Iterable[str],
batch_size: int = BATCH_SIZE,
) -> dict[str, LegacyMealMeta]:
"""查套餐 + 系列元数据(供 scan_legacy 收集映射 legacy_* 字段)。""" """查套餐 + 系列元数据(供 scan_legacy 收集映射 legacy_* 字段)。"""
timer = _StageTimer("fetch_meal_meta")
id_list = sorted({m for m in meal_ids if m}) id_list = sorted({m for m in meal_ids if m})
out: dict[str, LegacyMealMeta] = {} out: dict[str, LegacyMealMeta] = {}
if not id_list: if not id_list:
@@ -733,11 +910,15 @@ def fetch_meal_meta(conn, meal_ids: Iterable[str]) -> dict[str, LegacyMealMeta]:
LEFT JOIN tbl_set_meal_series sms ON sms.id = sm.meal_series_id LEFT JOIN tbl_set_meal_series sms ON sms.id = sm.meal_series_id
WHERE sm.id IN %s WHERE sm.id IN %s
""" """
rows = 0
batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(id_list): for batch in _chunks(id_list, batch_size):
batches += 1
cur.execute(sql, (tuple(batch),)) cur.execute(sql, (tuple(batch),))
for row in cur.fetchall(): for row in cur.fetchall():
mid = row["meal_id"] mid = row["meal_id"]
rows += 1
if not mid: if not mid:
continue continue
out[mid] = LegacyMealMeta( out[mid] = LegacyMealMeta(
@@ -746,11 +927,17 @@ def fetch_meal_meta(conn, meal_ids: Iterable[str]) -> dict[str, LegacyMealMeta]:
series_id=row.get("series_id") or "", series_id=row.get("series_id") or "",
series_name=row.get("series_name") or "", series_name=row.get("series_name") or "",
) )
timer.log("tbl_set_meal", meal_ids=len(id_list), batches=batches, rows=rows)
return out return out
def fetch_agent_balances(conn, agent_ids: Iterable[str]) -> dict[str, LegacyAgentBalance]: def fetch_agent_balances(
conn,
agent_ids: Iterable[str],
batch_size: int = BATCH_SIZE,
) -> dict[str, LegacyAgentBalance]:
"""查代理主钱包余额(tbl_agent.balance_money,元单位)。""" """查代理主钱包余额(tbl_agent.balance_money,元单位)。"""
timer = _StageTimer("fetch_agent_balances")
agent_list = sorted({a for a in agent_ids if a}) agent_list = sorted({a for a in agent_ids if a})
out: dict[str, LegacyAgentBalance] = {} out: dict[str, LegacyAgentBalance] = {}
if not agent_list: if not agent_list:
@@ -761,12 +948,17 @@ def fetch_agent_balances(conn, agent_ids: Iterable[str]) -> dict[str, LegacyAgen
FROM tbl_agent FROM tbl_agent
WHERE id IN %s WHERE id IN %s
""" """
rows = 0
batches = 0
with conn.cursor() as cur: with conn.cursor() as cur:
for batch in _chunks(agent_list): for batch in _chunks(agent_list, batch_size):
batches += 1
cur.execute(sql, (tuple(batch),)) cur.execute(sql, (tuple(batch),))
for row in cur.fetchall(): for row in cur.fetchall():
aid = row["id"] aid = row["id"]
rows += 1
if not aid: if not aid:
continue continue
out[aid] = LegacyAgentBalance(agent_id=aid, balance_fen=_to_fen(row.get("balance_money"))) out[aid] = LegacyAgentBalance(agent_id=aid, balance_fen=_to_fen(row.get("balance_money")))
timer.log("tbl_agent", agents=len(agent_list), batches=batches, rows=rows)
return out return out

View File

@@ -1091,7 +1091,6 @@ def write_step2(
cards: list[CardRow], cards: list[CardRow],
devices: list[DeviceRow], devices: list[DeviceRow],
mapping: Mapping, mapping: Mapping,
card_metas: dict[str, LegacyCardMeta],
usage_snapshots: dict[str, LegacyCardUsage], usage_snapshots: dict[str, LegacyCardUsage],
package_lifecycles: dict[str, list[LegacyPackageLifecycle]], package_lifecycles: dict[str, list[LegacyPackageLifecycle]],
commission_snapshots: dict[str, LegacyCommissionAccount], commission_snapshots: dict[str, LegacyCommissionAccount],

View File

@@ -4,7 +4,7 @@
工作流: 工作流:
1. 读 mapping.yaml(必须先跑过 scan_legacy.py) 1. 读 mapping.yaml(必须先跑过 scan_legacy.py)
2. 读 cards.csv / devices.csv 2. 读 cards.csv / devices.csv
3. 连奇成查每张卡的元数据(category/agent_id/msisdn/virtual_no) 3. 连奇成查每张卡的元数据(category/agent_id/msisdn/virtual_no/当前生效套餐)
4. 应用 mapping 把 legacy_* 翻译成 target_*,自动决定归属 4. 应用 mapping 把 legacy_* 翻译成 target_*,自动决定归属
5. 生成 output/step1_*.sql + errors.csv + summary.txt 5. 生成 output/step1_*.sql + errors.csv + summary.txt
@@ -14,11 +14,13 @@
用法: 用法:
python3 migrate_assets.py [--config-dir config] [--resources-dir resources] [--output-dir output] python3 migrate_assets.py [--config-dir config] [--resources-dir resources] [--output-dir output]
[--batch-size 1000]
""" """
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import sys import sys
import time
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent)) sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -31,6 +33,7 @@ def main() -> int:
parser.add_argument("--config-dir", default="config", help="yaml 配置目录") parser.add_argument("--config-dir", default="config", help="yaml 配置目录")
parser.add_argument("--resources-dir", default="resources", help="CSV 资源目录") parser.add_argument("--resources-dir", default="resources", help="CSV 资源目录")
parser.add_argument("--output-dir", default="output", help="SQL/CSV 输出目录") parser.add_argument("--output-dir", default="output", help="SQL/CSV 输出目录")
parser.add_argument("--batch-size", type=int, default=legacy_query.BATCH_SIZE, help="老库批量查询大小")
args = parser.parse_args() args = parser.parse_args()
base = Path(__file__).resolve().parent base = Path(__file__).resolve().parent
@@ -38,16 +41,26 @@ def main() -> int:
resources_dir = (base / args.resources_dir).resolve() resources_dir = (base / args.resources_dir).resolve()
output_dir = (base / args.output_dir).resolve() output_dir = (base / args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
batch_size = args.batch_size
t0 = time.perf_counter()
mapping = mapping_loader.load_mapping(config_dir) mapping = mapping_loader.load_mapping(config_dir)
cards, devices, errors = csv_loader.load_assets(resources_dir, mapping) cards, devices, errors = csv_loader.load_assets(resources_dir, mapping)
print(f"[migrate] 加载 CSV: cards={len(cards)} devices={len(devices)} elapsed={time.perf_counter()-t0:.1f}s")
# 连奇成查卡元数据(运营商分类、agent_id、msisdn、virtual_no、当前生效套餐) # 连奇成查卡元数据(运营商分类、agent_id、msisdn、virtual_no、当前生效套餐)
iccid_pairs = [(c.iccid_19, c.iccid_20, c.allow_iccid_19_lookup) for c in cards] iccid_pairs = [(c.iccid_19, c.iccid_20, c.allow_iccid_19_lookup) for c in cards]
t1 = time.perf_counter()
dsn = mapping_loader.load_legacy_dsn(config_dir) dsn = mapping_loader.load_legacy_dsn(config_dir)
with legacy_query.connect_readonly(dsn) as conn: with legacy_query.connect_readonly(dsn) as conn:
card_metas, dup_iccids = legacy_query.fetch_card_meta(conn, iccid_pairs) print(f"[migrate] 连接老库: elapsed={time.perf_counter()-t1:.1f}s")
t2 = time.perf_counter()
card_metas, dup_iccids = legacy_query.fetch_card_meta(conn, iccid_pairs, batch_size=batch_size)
print(f"[migrate] fetch_card_meta: cards={len(card_metas)} duplicates={len(dup_iccids)} elapsed={time.perf_counter()-t2:.1f}s")
t3 = time.perf_counter()
sql_builder.write_step1( sql_builder.write_step1(
output_dir, output_dir,
cards=cards, cards=cards,
@@ -57,8 +70,10 @@ def main() -> int:
duplicate_iccids=dup_iccids, duplicate_iccids=dup_iccids,
errors=errors, errors=errors,
) )
print(f"[migrate] SQL 生成: elapsed={time.perf_counter()-t3:.1f}s")
print(f"完成:卡 {len(cards)} 张,设备 {len(devices)} 台,异常 {len(errors)}") total = time.perf_counter() - t0
print(f"完成:卡 {len(cards)} 张,设备 {len(devices)} 台,异常 {len(errors)} 行,总耗时 {total:.1f}s")
print(f"SQL 输出: {output_dir}") print(f"SQL 输出: {output_dir}")
return 0 return 0

View File

@@ -5,17 +5,19 @@
工作流: 工作流:
1. 读 mapping.yaml + cards.csv 1. 读 mapping.yaml + cards.csv
2. 连奇成查:卡元数据(card_meta) + 累计流量(card_usage) + 代理佣金/主钱包余额 2. 连奇成查:累计流量(card_usage) + 套餐生命周期 + 代理佣金/主钱包余额
3. 卡 → 当前生效套餐 → mapping.packages → target_package_id 3. 卡 → 当前生效套餐 → mapping.packages → target_package_id
4. 生成 step2_*.sql 4. 生成 step2_*.sql
用法: 用法:
python3 migrate_runtime.py [--config-dir config] [--resources-dir resources] [--output-dir output] python3 migrate_runtime.py [--config-dir config] [--resources-dir resources] [--output-dir output]
[--batch-size 1000]
""" """
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import sys import sys
import time
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent)) sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -28,6 +30,7 @@ def main() -> int:
parser.add_argument("--config-dir", default="config", help="yaml 配置目录") parser.add_argument("--config-dir", default="config", help="yaml 配置目录")
parser.add_argument("--resources-dir", default="resources", help="CSV 资源目录") parser.add_argument("--resources-dir", default="resources", help="CSV 资源目录")
parser.add_argument("--output-dir", default="output", help="SQL/CSV 输出目录") parser.add_argument("--output-dir", default="output", help="SQL/CSV 输出目录")
parser.add_argument("--batch-size", type=int, default=legacy_query.BATCH_SIZE, help="老库批量查询大小")
args = parser.parse_args() args = parser.parse_args()
base = Path(__file__).resolve().parent base = Path(__file__).resolve().parent
@@ -35,9 +38,13 @@ def main() -> int:
resources_dir = (base / args.resources_dir).resolve() resources_dir = (base / args.resources_dir).resolve()
output_dir = (base / args.output_dir).resolve() output_dir = (base / args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True) output_dir.mkdir(parents=True, exist_ok=True)
batch_size = args.batch_size
t0 = time.perf_counter()
mapping = mapping_loader.load_mapping(config_dir) mapping = mapping_loader.load_mapping(config_dir)
cards, devices, errors = csv_loader.load_assets(resources_dir, mapping) cards, devices, errors = csv_loader.load_assets(resources_dir, mapping)
print(f"[migrate] 加载 CSV: cards={len(cards)} devices={len(devices)} elapsed={time.perf_counter()-t0:.1f}s")
if errors: if errors:
sql_builder.write_runtime_input_errors(output_dir, errors) sql_builder.write_runtime_input_errors(output_dir, errors)
print(f"cards.csv/devices.csv 存在 {len(errors)} 个基础错误,已写入 {output_dir / 'errors.csv'}", file=sys.stderr) print(f"cards.csv/devices.csv 存在 {len(errors)} 个基础错误,已写入 {output_dir / 'errors.csv'}", file=sys.stderr)
@@ -46,27 +53,39 @@ def main() -> int:
iccid_pairs = [(c.iccid_19, c.iccid_20, c.allow_iccid_19_lookup) for c in cards] iccid_pairs = [(c.iccid_19, c.iccid_20, c.allow_iccid_19_lookup) for c in cards]
agent_ids = [a.legacy_agent_id for a in mapping.agents.values() if (a.target_shop_code or "").strip()] agent_ids = [a.legacy_agent_id for a in mapping.agents.values() if (a.target_shop_code or "").strip()]
t1 = time.perf_counter()
dsn = mapping_loader.load_legacy_dsn(config_dir) dsn = mapping_loader.load_legacy_dsn(config_dir)
with legacy_query.connect_readonly(dsn) as conn: with legacy_query.connect_readonly(dsn) as conn:
card_metas, _dup_iccids = legacy_query.fetch_card_meta(conn, iccid_pairs) print(f"[migrate] 连接老库: elapsed={time.perf_counter()-t1:.1f}s")
usage_snapshots = legacy_query.fetch_card_usage(conn, iccid_pairs)
package_lifecycles = legacy_query.fetch_package_lifecycles(conn, iccid_pairs)
commission_snapshots = legacy_query.fetch_commission_accounts(conn, agent_ids) if agent_ids else {}
agent_balances = legacy_query.fetch_agent_balances(conn, agent_ids) if agent_ids else {}
t2 = time.perf_counter()
usage_snapshots = legacy_query.fetch_card_usage(conn, iccid_pairs, batch_size=batch_size)
print(f"[migrate] fetch_card_usage: cards={len(usage_snapshots)} elapsed={time.perf_counter()-t2:.1f}s")
t3 = time.perf_counter()
package_lifecycles = legacy_query.fetch_package_lifecycles(conn, iccid_pairs, batch_size=batch_size)
print(f"[migrate] fetch_package_lifecycles: cards={len(package_lifecycles)} elapsed={time.perf_counter()-t3:.1f}s")
t4 = time.perf_counter()
commission_snapshots = legacy_query.fetch_commission_accounts(conn, agent_ids, batch_size=batch_size) if agent_ids else {}
agent_balances = legacy_query.fetch_agent_balances(conn, agent_ids, batch_size=batch_size) if agent_ids else {}
print(f"[migrate] fetch_agent_data: agents={len(agent_ids)} elapsed={time.perf_counter()-t4:.1f}s")
t5 = time.perf_counter()
warnings = sql_builder.write_step2( warnings = sql_builder.write_step2(
output_dir, output_dir,
cards=cards, cards=cards,
devices=devices, devices=devices,
mapping=mapping, mapping=mapping,
card_metas=card_metas,
usage_snapshots=usage_snapshots, usage_snapshots=usage_snapshots,
package_lifecycles=package_lifecycles, package_lifecycles=package_lifecycles,
commission_snapshots=commission_snapshots, commission_snapshots=commission_snapshots,
agent_balances=agent_balances, agent_balances=agent_balances,
) )
print(f"[migrate] SQL 生成: elapsed={time.perf_counter()-t5:.1f}s")
print(f"完成:卡 {len(cards)} 张,代理 {len(agent_ids)} 个,警告 {len(warnings)}") total = time.perf_counter() - t0
print(f"完成:卡 {len(cards)} 张,代理 {len(agent_ids)} 个,警告 {len(warnings)} 条,总耗时 {total:.1f}s")
print(f"SQL 输出: {output_dir}") print(f"SQL 输出: {output_dir}")
return 0 return 0