迁移脚本的修复
This commit is contained in:
@@ -237,10 +237,10 @@ packages: # 同理
|
|||||||
legacy_meal_name: "三网充电宝全国畅玩年卡套餐..."
|
legacy_meal_name: "三网充电宝全国畅玩年卡套餐..."
|
||||||
target_package_id: 2 ← 必填:新库 tb_package.id
|
target_package_id: 2 ← 必填:新库 tb_package.id
|
||||||
|
|
||||||
series: # 仅保留奇成套餐系列快照,生成器不要求填写
|
series: # 套餐系列兜底映射;有 target_package_id 时优先用 tb_package.series_id
|
||||||
- legacy_series_id: "..."
|
- legacy_series_id: "..."
|
||||||
legacy_series_name: "国内卡 无预存30天"
|
legacy_series_name: "国内卡 无预存30天"
|
||||||
target_series_id: null
|
target_series_id: 5 ← 可选:无当前套餐时作为资产 series_id 兜底
|
||||||
|
|
||||||
agents: # legacy_agent 归属模式 + 代理钱包初始化
|
agents: # legacy_agent 归属模式 + 代理钱包初始化
|
||||||
- legacy_agent_id: "D15EC779767B412A82B8448AC2324F04"
|
- legacy_agent_id: "D15EC779767B412A82B8448AC2324F04"
|
||||||
@@ -287,7 +287,7 @@ agents:
|
|||||||
`ownership_rules.device.mode: legacy_agent`,脚本会用设备 `package_source_slot` 那张绑定卡的 `agent_id`
|
`ownership_rules.device.mode: legacy_agent`,脚本会用设备 `package_source_slot` 那张绑定卡的 `agent_id`
|
||||||
决定设备归属。
|
决定设备归属。
|
||||||
|
|
||||||
设备槽位解析优先级是 `devices.csv` 行级配置 > `overrides.devices` > `ownership_rules.device`。`current_slot` 对应 `tb_device_sim_binding.is_current`;`package_source_slot` 只决定从哪张绑定卡读取奇成旧套餐,新系统套餐仍写到设备资产。
|
设备槽位解析优先级是 `devices.csv` 行级配置 > `overrides.devices` > `ownership_rules.device`。`current_slot` 是默认当前卡槽,对应 `tb_device_sim_binding.is_current`;`package_source_slot` 是默认旧套餐读取卡槽,只决定从哪张绑定卡读取奇成旧套餐,新系统套餐仍写到设备资产。若 CSV 行级和配置都不填 `package_source_slot`,会回退到 `current_slot`。
|
||||||
|
|
||||||
## 5. 识别迁移数据(后台报表过滤用)
|
## 5. 识别迁移数据(后台报表过滤用)
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ real_used_mb = total_bytes_cnt / (1 + flow_add_discount / 100)
|
|||||||
|
|
||||||
设备上的卡在新系统运行态会按设备维度判断有效套餐和复机条件,因此迁移时只用设备 `package_source_slot` 对应绑定卡读取奇成旧套餐生命周期。生成的新系统套餐使用记录仍然是 `usage_type=device`、`device_id=设备ID`,不会挂到这张卡资产上。其他槽位绑定卡不读取旧套餐,避免同一设备重复迁入多份主套餐。
|
设备上的卡在新系统运行态会按设备维度判断有效套餐和复机条件,因此迁移时只用设备 `package_source_slot` 对应绑定卡读取奇成旧套餐生命周期。生成的新系统套餐使用记录仍然是 `usage_type=device`、`device_id=设备ID`,不会挂到这张卡资产上。其他槽位绑定卡不读取旧套餐,避免同一设备重复迁入多份主套餐。
|
||||||
|
|
||||||
设备和卡的 `series_id` 以新系统目标套餐为准:脚本通过 `mapping.packages[].target_package_id` 对应的 `tb_package.series_id` 写入资产,并额外生成 `step2_06_asset_series_update.sql`,用于修复已经导入但因 `ON CONFLICT DO NOTHING` 没有被 step1 更新到的资产。
|
设备和卡的 `series_id` 以新系统目标套餐为准:脚本优先通过 `mapping.packages[].target_package_id` 对应的 `tb_package.series_id` 写入资产。若旧卡没有当前正式套餐、但本批 `series[].target_series_id` 只配置了一个目标系列,脚本会把它作为资产系列兜底。`step1` 对已存在资产也会更新 `series_id`;`step2_06_asset_series_update.sql` 继续用于按已生成套餐使用记录回填系列。
|
||||||
|
|
||||||
## 7. 重要约束
|
## 7. 重要约束
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ ownership_rules:
|
|||||||
# default_shop 模式的批量目标店铺。legacy_agent/none 模式可保持 null。
|
# default_shop 模式的批量目标店铺。legacy_agent/none 模式可保持 null。
|
||||||
default_target_shop_code: KWTX
|
default_target_shop_code: KWTX
|
||||||
device:
|
device:
|
||||||
# default_shop=默认店铺;legacy_agent=按旧套餐读取槽位的绑定卡 agent_id 分店铺;none=进平台库存
|
# default_shop=默认店铺;legacy_agent=按默认/行级旧套餐读取槽位的绑定卡 agent_id 分店铺;none=进平台库存
|
||||||
mode: default_shop
|
mode: default_shop
|
||||||
# 批量默认当前槽位,用于 tb_device_sim_binding.is_current
|
# 默认当前卡槽。devices.csv 未填 current_slot 时使用这里;用于 tb_device_sim_binding.is_current
|
||||||
current_slot: 2
|
current_slot: 2
|
||||||
# 批量默认旧套餐读取槽位。新系统套餐挂设备,这里只决定从奇成哪张绑定卡读套餐生命周期
|
# 默认旧套餐读取卡槽。devices.csv 未填 package_source_slot 时使用这里;新系统套餐仍挂设备;不填则回退 current_slot
|
||||||
package_source_slot: 2
|
package_source_slot: 2
|
||||||
standalone_card:
|
standalone_card:
|
||||||
# default_shop=默认店铺;legacy_agent=按卡 agent_id 分店铺;none=进平台库存
|
# default_shop=默认店铺;legacy_agent=按卡 agent_id 分店铺;none=进平台库存
|
||||||
@@ -72,11 +72,11 @@ packages:
|
|||||||
target_package_id: null # 必填:新库 tb_package.id
|
target_package_id: null # 必填:新库 tb_package.id
|
||||||
|
|
||||||
# ============== 套餐系列快照(可选) ==============
|
# ============== 套餐系列快照(可选) ==============
|
||||||
# 仅保留奇成套餐系列供人工参考;资产 series_id 会从 target_package_id 对应的 tb_package.series_id 反查
|
# 资产 series_id 优先从 target_package_id 对应的 tb_package.series_id 反查;target_series_id 可作无套餐时的兜底
|
||||||
series:
|
series:
|
||||||
- legacy_series_id: "95A038B9D3014F9DA1539EC092D9DBAA"
|
- legacy_series_id: "95A038B9D3014F9DA1539EC092D9DBAA"
|
||||||
legacy_series_name: "国内卡 无预存30天"
|
legacy_series_name: "国内卡 无预存30天"
|
||||||
target_series_id: null # 可选参考字段,生成 SQL 不依赖这里
|
target_series_id: null # 可选:新库套餐系列 ID;无当前套餐时可作为资产系列兜底
|
||||||
|
|
||||||
# ============== 代理-店铺映射(legacy_agent 归属 + 代理钱包) ==============
|
# ============== 代理-店铺映射(legacy_agent 归属 + 代理钱包) ==============
|
||||||
# scan_legacy 会按"奇成 tbl_card.agent_id"分组写入。
|
# scan_legacy 会按"奇成 tbl_card.agent_id"分组写入。
|
||||||
|
|||||||
@@ -159,6 +159,8 @@ class LegacyCardMeta:
|
|||||||
current_meal_name: str
|
current_meal_name: str
|
||||||
current_meal_type: str # tbl_card_life.type,1=叠加包/加油包,2=月卡,3=季卡,4=半年卡,5=年卡
|
current_meal_type: str # tbl_card_life.type,1=叠加包/加油包,2=月卡,3=季卡,4=半年卡,5=年卡
|
||||||
current_expire_date: Optional[str] # ISO 字符串
|
current_expire_date: Optional[str] # ISO 字符串
|
||||||
|
current_series_id: str # 当前生效正式套餐所属奇成套餐系列 ID
|
||||||
|
current_series_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -629,7 +631,9 @@ def fetch_card_meta(
|
|||||||
COALESCE(cl.meal_id, '') AS meal_id,
|
COALESCE(cl.meal_id, '') AS meal_id,
|
||||||
COALESCE(cl.meal_name, '') AS meal_name,
|
COALESCE(cl.meal_name, '') AS meal_name,
|
||||||
COALESCE(CAST(cl.meal_type AS CHAR), '') AS meal_type,
|
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
|
FROM tbl_card c
|
||||||
LEFT JOIN tbl_vendor_category vc ON vc.category = c.category
|
LEFT JOIN tbl_vendor_category vc ON vc.category = c.category
|
||||||
LEFT JOIN tbl_virtual_number vn ON (
|
LEFT JOIN tbl_virtual_number vn ON (
|
||||||
@@ -655,6 +659,8 @@ def fetch_card_meta(
|
|||||||
cl.iccid_mark = c.iccid_mark
|
cl.iccid_mark = c.iccid_mark
|
||||||
OR (CHAR_LENGTH(c.iccid_mark) = 19 AND LEFT(cl.iccid_mark, 19) = 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
|
WHERE c.iccid_mark IN %s
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -705,6 +711,8 @@ def fetch_card_meta(
|
|||||||
current_meal_name=row.get("meal_name") or "",
|
current_meal_name=row.get("meal_name") or "",
|
||||||
current_meal_type=str(row.get("meal_type") or ""),
|
current_meal_type=str(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_name=row.get("series_name") or "",
|
||||||
)
|
)
|
||||||
return metas, duplicates
|
return metas, duplicates
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class PackageMapping:
|
|||||||
class SeriesMapping:
|
class SeriesMapping:
|
||||||
legacy_series_id: str
|
legacy_series_id: str
|
||||||
legacy_series_name: str
|
legacy_series_name: str
|
||||||
target_series_id: Optional[int] # 允许 null,表示不写入新库 series_id
|
target_series_id: Optional[int] # 允许 null;填了可作为资产 series_id 兜底
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@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)}",
|
f" default_target_shop_code: {_yaml_scalar(mapping.ownership_rules.default_target_shop_code)}",
|
||||||
" # 设备规则只在存在 resources/devices.csv 时生效。",
|
" # 设备规则只在存在 resources/devices.csv 时生效。",
|
||||||
" device:",
|
" device:",
|
||||||
" # default_shop=默认店铺;legacy_agent=按旧套餐读取槽位绑定卡的奇成 agent_id 映射店铺;none=平台库存。",
|
" # default_shop=默认店铺;legacy_agent=按默认/行级旧套餐读取槽位绑定卡的奇成 agent_id 映射店铺;none=平台库存。",
|
||||||
f" mode: {_yaml_scalar(mapping.ownership_rules.device.mode)}",
|
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)}",
|
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)}",
|
f" package_source_slot: {_yaml_scalar(mapping.ownership_rules.device.package_source_slot)}",
|
||||||
" standalone_card:",
|
" standalone_card:",
|
||||||
" # default_shop=默认店铺;legacy_agent=按奇成 agent_id 映射店铺;none=平台库存。",
|
" # default_shop=默认店铺;legacy_agent=按奇成 agent_id 映射店铺;none=平台库存。",
|
||||||
@@ -442,7 +442,7 @@ def _render_commented_mapping_yaml(mapping: Mapping) -> str:
|
|||||||
lines.extend([
|
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)
|
_append_series(lines, mapping)
|
||||||
|
|
||||||
|
|||||||
@@ -338,19 +338,34 @@ def _target_series_id_expr_for_iccid(
|
|||||||
"""按卡当前套餐映射生成新库套餐系列 SQL 表达式。
|
"""按卡当前套餐映射生成新库套餐系列 SQL 表达式。
|
||||||
|
|
||||||
系列归属以新系统目标套餐为准,避免奇成套餐系列与新库系列二次映射不一致。
|
系列归属以新系统目标套餐为准,避免奇成套餐系列与新库系列二次映射不一致。
|
||||||
|
如果旧卡没有当前正式套餐,但本批 mapping 只指定了一个 target_series_id,则按批量兜底写入。
|
||||||
"""
|
"""
|
||||||
if not iccid:
|
if not iccid:
|
||||||
return "NULL"
|
return "NULL"
|
||||||
meta = card_metas.get(iccid)
|
meta = card_metas.get(iccid)
|
||||||
if meta is None or not meta.current_meal_id:
|
if meta is None:
|
||||||
return "NULL"
|
return "NULL"
|
||||||
pkg_map = mapping.lookup_package(meta.current_meal_id)
|
series_candidates: list[str] = []
|
||||||
if pkg_map is None or not pkg_map.target_package_id:
|
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 "NULL"
|
||||||
return (
|
return f"COALESCE({', '.join(series_candidates)})"
|
||||||
"(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)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _write_formal_package_guard(f, target_pkg_id: int, legacy_meal_name: str) -> None:
|
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"'迁移套餐映射禁止指向加油包: legacy_meal_name=' || {_sql_str(legacy_meal_name)} || "
|
||||||
f"', target_package_id={target_pkg_id}, package_type=' || v_package_type;\n"
|
f"', target_package_id={target_pkg_id}, package_type=' || v_package_type;\n"
|
||||||
" END IF;\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 = "
|
" RAISE EXCEPTION USING MESSAGE = "
|
||||||
f"'迁移目标店铺不存在或已删除: shop_code={shop_code}';\n"
|
f"'迁移目标店铺不存在或已删除: shop_code={shop_code}';\n"
|
||||||
" END IF;\n"
|
" END IF;\n"
|
||||||
"END $$;\n\n"
|
"END;\n"
|
||||||
|
"$$;\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -635,6 +652,7 @@ def _write_iot_cards(
|
|||||||
"SET\n"
|
"SET\n"
|
||||||
f" status = {card_status},\n"
|
f" status = {card_status},\n"
|
||||||
f" shop_id = {shop_id_expr},\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" virtual_no = {_sql_str(card_virtual_no) if card_virtual_no else 'NULL'},\n"
|
||||||
f" is_standalone = {is_standalone},\n"
|
f" is_standalone = {is_standalone},\n"
|
||||||
f" device_virtual_no = {_sql_str(device_virtual_no)},\n"
|
f" device_virtual_no = {_sql_str(device_virtual_no)},\n"
|
||||||
@@ -643,6 +661,7 @@ def _write_iot_cards(
|
|||||||
" AND (\n"
|
" AND (\n"
|
||||||
f" status IS DISTINCT FROM {card_status}\n"
|
f" status IS DISTINCT FROM {card_status}\n"
|
||||||
f" OR shop_id IS DISTINCT FROM {shop_id_expr}\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 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 is_standalone IS DISTINCT FROM {is_standalone}\n"
|
||||||
f" OR device_virtual_no IS DISTINCT FROM {_sql_str(device_virtual_no)}\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)),
|
("max_sim_slots", str(d.max_sim_slots)),
|
||||||
("status", str(device_status)),
|
("status", str(device_status)),
|
||||||
("shop_id", shop_id_expr),
|
("shop_id", shop_id_expr),
|
||||||
|
("series_id", series_id_expr),
|
||||||
("device_name", d.device_name),
|
("device_name", d.device_name),
|
||||||
("device_model", d.device_model),
|
("device_model", d.device_model),
|
||||||
("device_type", d.device_type),
|
("device_type", d.device_type),
|
||||||
@@ -712,13 +732,16 @@ def _write_devices(
|
|||||||
if value or column in {"max_sim_slots", "status", "shop_id"}
|
if value or column in {"max_sim_slots", "status", "shop_id"}
|
||||||
]
|
]
|
||||||
if metadata_updates:
|
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 = [
|
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
|
for column, value in metadata_updates
|
||||||
]
|
]
|
||||||
diff_checks = [
|
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
|
if column in sql_literal_columns
|
||||||
else f"{column} IS DISTINCT FROM {_sql_str(value)}"
|
else f"{column} IS DISTINCT FROM {_sql_str(value)}"
|
||||||
for column, value in metadata_updates
|
for column, value in metadata_updates
|
||||||
|
|||||||
@@ -47,8 +47,8 @@
|
|||||||
| `device_type` | 否 | 设备类型,写入 `tb_device.device_type` |
|
| `device_type` | 否 | 设备类型,写入 `tb_device.device_type` |
|
||||||
| `max_sim_slots` | 否 | 最大卡槽数,范围 1-4。不填时按非空 `sim_iccid_*` 的最大槽位推导 |
|
| `max_sim_slots` | 否 | 最大卡槽数,范围 1-4。不填时按非空 `sim_iccid_*` 的最大槽位推导 |
|
||||||
| `sim_iccid_1` ~ `sim_iccid_4` | 否 | 各插槽 ICCID,引用 `cards.csv` 的卡。可填 19 或 20 位;20 位精确匹配完整 ICCID,19 位仅在该前缀唯一对应一张卡时允许(至少要有 1 个) |
|
| `sim_iccid_1` ~ `sim_iccid_4` | 否 | 各插槽 ICCID,引用 `cards.csv` 的卡。可填 19 或 20 位;20 位精确匹配完整 ICCID,19 位仅在该前缀唯一对应一张卡时允许(至少要有 1 个) |
|
||||||
| `current_slot` | 否 | 当前槽位,范围 1-4。写入 `tb_device_sim_binding.is_current`;空值用批量规则或覆盖项 |
|
| `current_slot` | 否 | 行级当前槽位,范围 1-4。写入 `tb_device_sim_binding.is_current`;空值用覆盖项或批量默认值 |
|
||||||
| `package_source_slot` | 否 | 设备旧套餐读取槽位,范围 1-4。新系统套餐仍归属设备;该字段只决定从奇成哪张绑定卡读取正式套餐生命周期 |
|
| `package_source_slot` | 否 | 行级旧套餐读取槽位,范围 1-4。新系统套餐仍归属设备;空值用覆盖项或批量默认值,都不填则回退 `current_slot` |
|
||||||
|
|
||||||
**槽位优先级**:
|
**槽位优先级**:
|
||||||
1. `devices.csv` 行级 `current_slot` / `package_source_slot`
|
1. `devices.csv` 行级 `current_slot` / `package_source_slot`
|
||||||
|
|||||||
@@ -204,6 +204,12 @@ def _merge(
|
|||||||
legacy_agent_name=meta.agent_name,
|
legacy_agent_name=meta.agent_name,
|
||||||
target_shop_code=None,
|
target_shop_code=None,
|
||||||
)
|
)
|
||||||
|
if meta.current_series_id and meta.current_series_id not in merged.series:
|
||||||
|
merged.series[meta.current_series_id] = SeriesMapping(
|
||||||
|
legacy_series_id=meta.current_series_id,
|
||||||
|
legacy_series_name=meta.current_series_name,
|
||||||
|
target_series_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
for mm in meal_metas.values():
|
for mm in meal_metas.values():
|
||||||
if mm.meal_id not in merged.packages:
|
if mm.meal_id not in merged.packages:
|
||||||
|
|||||||
Reference in New Issue
Block a user