迁移脚本的修复

This commit is contained in:
Break
2026-06-15 10:53:47 +08:00
parent e56649e5be
commit 2f0b24ce88
7 changed files with 66 additions and 29 deletions

View File

@@ -159,6 +159,8 @@ class LegacyCardMeta:
current_meal_name: str
current_meal_type: str # tbl_card_life.type,1=叠加包/加油包,2=月卡,3=季卡,4=半年卡,5=年卡
current_expire_date: Optional[str] # ISO 字符串
current_series_id: str # 当前生效正式套餐所属奇成套餐系列 ID
current_series_name: str
@dataclass(frozen=True)
@@ -629,7 +631,9 @@ def fetch_card_meta(
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
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 (
@@ -655,6 +659,8 @@ def fetch_card_meta(
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
"""
@@ -705,6 +711,8 @@ def fetch_card_meta(
current_meal_name=row.get("meal_name") or "",
current_meal_type=str(row.get("meal_type") or ""),
current_expire_date=expire_str,
current_series_id=row.get("series_id") or "",
current_series_name=row.get("series_name") or "",
)
return metas, duplicates

View File

@@ -131,7 +131,7 @@ class PackageMapping:
class SeriesMapping:
legacy_series_id: str
legacy_series_name: str
target_series_id: Optional[int] # 允许 null,表示不写入新库 series_id
target_series_id: Optional[int] # 允许 null;填了可作为资产 series_id 兜底
@dataclass(frozen=True)
@@ -395,11 +395,11 @@ def _render_commented_mapping_yaml(mapping: Mapping) -> str:
f" default_target_shop_code: {_yaml_scalar(mapping.ownership_rules.default_target_shop_code)}",
" # 设备规则只在存在 resources/devices.csv 时生效。",
" device:",
" # default_shop=默认店铺;legacy_agent=按旧套餐读取槽位绑定卡的奇成 agent_id 映射店铺;none=平台库存。",
" # default_shop=默认店铺;legacy_agent=按默认/行级旧套餐读取槽位绑定卡的奇成 agent_id 映射店铺;none=平台库存。",
f" mode: {_yaml_scalar(mapping.ownership_rules.device.mode)}",
" # 当前卡槽,写入 tb_device_sim_binding.is_current;纯卡迁移可保持 null",
" # 默认当前卡槽。devices.csv 未填 current_slot 时使用这里;写入 tb_device_sim_binding.is_current。",
f" current_slot: {_yaml_scalar(mapping.ownership_rules.device.current_slot)}",
" # 设备旧套餐读取槽位。新系统套餐挂设备,这里只决定从奇成哪张绑定卡读套餐生命周期;纯卡迁移可保持 null",
" # 默认旧套餐读取卡槽。devices.csv 未填 package_source_slot 时使用这里;新系统套餐挂设备;不填则回退 current_slot",
f" package_source_slot: {_yaml_scalar(mapping.ownership_rules.device.package_source_slot)}",
" standalone_card:",
" # default_shop=默认店铺;legacy_agent=按奇成 agent_id 映射店铺;none=平台库存。",
@@ -442,7 +442,7 @@ def _render_commented_mapping_yaml(mapping: Mapping) -> str:
lines.extend([
"",
"# ============== 套餐系列快照(可选) ==============",
"# 仅保留奇成套餐系列供人工参考;资产 series_id 从 target_package_id 对应的 tb_package.series_id 反查。",
"# 资产 series_id 优先从 target_package_id 对应的 tb_package.series_id 反查;target_series_id 可作无套餐时的兜底",
])
_append_series(lines, mapping)

View File

@@ -338,19 +338,34 @@ def _target_series_id_expr_for_iccid(
"""按卡当前套餐映射生成新库套餐系列 SQL 表达式。
系列归属以新系统目标套餐为准,避免奇成套餐系列与新库系列二次映射不一致。
如果旧卡没有当前正式套餐,但本批 mapping 只指定了一个 target_series_id,则按批量兜底写入。
"""
if not iccid:
return "NULL"
meta = card_metas.get(iccid)
if meta is None or not meta.current_meal_id:
if meta is None:
return "NULL"
pkg_map = mapping.lookup_package(meta.current_meal_id)
if pkg_map is None or not pkg_map.target_package_id:
series_candidates: list[str] = []
if meta.current_meal_id:
pkg_map = mapping.lookup_package(meta.current_meal_id)
if pkg_map is not None and pkg_map.target_package_id:
series_candidates.append(
"(SELECT series_id FROM tb_package "
f"WHERE id = {pkg_map.target_package_id} AND package_type = 'formal' AND deleted_at IS NULL LIMIT 1)"
)
series_map = mapping.lookup_series(getattr(meta, "current_series_id", ""))
if series_map is not None and series_map.target_series_id:
series_candidates.append(str(series_map.target_series_id))
default_series_ids = sorted({
item.target_series_id
for item in mapping.series.values()
if item.target_series_id
})
if len(default_series_ids) == 1 and str(default_series_ids[0]) not in series_candidates:
series_candidates.append(str(default_series_ids[0]))
if not series_candidates:
return "NULL"
return (
"(SELECT series_id FROM tb_package "
f"WHERE id = {pkg_map.target_package_id} AND package_type = 'formal' AND deleted_at IS NULL LIMIT 1)"
)
return f"COALESCE({', '.join(series_candidates)})"
def _write_formal_package_guard(f, target_pkg_id: int, legacy_meal_name: str) -> None:
@@ -373,7 +388,8 @@ def _write_formal_package_guard(f, target_pkg_id: int, legacy_meal_name: str) ->
f"'迁移套餐映射禁止指向加油包: legacy_meal_name=' || {_sql_str(legacy_meal_name)} || "
f"', target_package_id={target_pkg_id}, package_type=' || v_package_type;\n"
" END IF;\n"
"END $$;\n\n"
"END;\n"
"$$;\n\n"
)
@@ -390,7 +406,8 @@ def _write_shop_guards(f, shop_codes: set[str]) -> None:
" RAISE EXCEPTION USING MESSAGE = "
f"'迁移目标店铺不存在或已删除: shop_code={shop_code}';\n"
" END IF;\n"
"END $$;\n\n"
"END;\n"
"$$;\n\n"
)
@@ -635,6 +652,7 @@ def _write_iot_cards(
"SET\n"
f" status = {card_status},\n"
f" shop_id = {shop_id_expr},\n"
f" series_id = COALESCE({series_id_expr}, series_id),\n"
f" virtual_no = {_sql_str(card_virtual_no) if card_virtual_no else 'NULL'},\n"
f" is_standalone = {is_standalone},\n"
f" device_virtual_no = {_sql_str(device_virtual_no)},\n"
@@ -643,6 +661,7 @@ def _write_iot_cards(
" AND (\n"
f" status IS DISTINCT FROM {card_status}\n"
f" OR shop_id IS DISTINCT FROM {shop_id_expr}\n"
f" OR ({series_id_expr}) IS NOT NULL AND series_id IS DISTINCT FROM ({series_id_expr})\n"
f" OR virtual_no IS DISTINCT FROM {_sql_str(card_virtual_no) if card_virtual_no else 'NULL'}\n"
f" OR is_standalone IS DISTINCT FROM {is_standalone}\n"
f" OR device_virtual_no IS DISTINCT FROM {_sql_str(device_virtual_no)}\n"
@@ -705,6 +724,7 @@ def _write_devices(
("max_sim_slots", str(d.max_sim_slots)),
("status", str(device_status)),
("shop_id", shop_id_expr),
("series_id", series_id_expr),
("device_name", d.device_name),
("device_model", d.device_model),
("device_type", d.device_type),
@@ -712,13 +732,16 @@ def _write_devices(
if value or column in {"max_sim_slots", "status", "shop_id"}
]
if metadata_updates:
sql_literal_columns = {"max_sim_slots", "status", "shop_id"}
sql_literal_columns = {"max_sim_slots", "status", "shop_id", "series_id"}
set_lines = [
f"{column} = {value}" if column in sql_literal_columns else f"{column} = {_sql_str(value)}"
f"{column} = COALESCE({value}, {column})" if column == "series_id"
else f"{column} = {value}" if column in sql_literal_columns
else f"{column} = {_sql_str(value)}"
for column, value in metadata_updates
]
diff_checks = [
f"{column} IS DISTINCT FROM {value}"
f"({value}) IS NOT NULL AND {column} IS DISTINCT FROM ({value})" if column == "series_id"
else f"{column} IS DISTINCT FROM {value}"
if column in sql_literal_columns
else f"{column} IS DISTINCT FROM {_sql_str(value)}"
for column, value in metadata_updates