This commit is contained in:
@@ -43,7 +43,7 @@ scripts/migration/
|
||||
把两个 CSV 放到 `resources/` 下(命名为 `cards.csv` / `devices.csv`):
|
||||
|
||||
- **cards.csv**:`iccid_19`/`iccid_20` 至少填一个;仅填 `iccid_20` 时脚本自动派生前 19 位,但奇成主表只按 20 位精确查询;`is_industry` 可选
|
||||
- **devices.csv**:`virtual_no` 必填,`sim_iccid_*` 引用 `cards.csv` 中的 ICCID(可填 19 或 20 位)
|
||||
- **devices.csv**:`imei` / `virtual_no` 至少填一个;`device_name` / `device_model` / `device_type` / `max_sim_slots` 可选;`sim_iccid_*` 引用 `cards.csv` 中的 ICCID(可填 19 或 20 位)
|
||||
|
||||
字段说明详见 [`resources/README.md`](resources/README.md)。
|
||||
|
||||
@@ -145,7 +145,7 @@ CSV 里**没有** `target_shop_code` 列,所有归属都从 `mapping.agents` 推
|
||||
|------|---------|
|
||||
| 独立卡 | 奇成 `tbl_card.agent_id` → `mapping.agents[].target_shop_code` |
|
||||
| 设备 | 主卡(`sim_iccid_1`)的 agent_id → 同上 |
|
||||
| 设备上的卡 | 跟随设备(`shop_id=NULL`,由触发器+绑定关系维护) |
|
||||
| 设备上的卡 | 跟随设备所属店铺(`shop_id` 与设备一致,设备分配时卡状态同步为已分销) |
|
||||
|
||||
如果 agent 在 `mapping.agents` 里 `target_shop_code` 留空 → 该资产进平台库存。
|
||||
|
||||
|
||||
@@ -46,16 +46,19 @@ class CardRow:
|
||||
class DeviceRow:
|
||||
"""devices.csv 单行的解析结果。
|
||||
|
||||
简化为业务方核心关心的列:imei / virtual_no / sim_iccid_*。
|
||||
简化为业务方核心关心的列:imei / virtual_no / device_* / max_sim_slots / sim_iccid_*。
|
||||
店铺归属由"主卡 sim_iccid_1 的 agent_id → mapping.agents.target_shop_code"推导。
|
||||
其他字段(device_name/device_model/manufacturer 等)直接用默认值,不暴露给业务方。
|
||||
其他字段(manufacturer 等)直接用默认值,不暴露给业务方。
|
||||
"""
|
||||
|
||||
line_no: int
|
||||
imei: str
|
||||
virtual_no: str # 留空时用 imei 兜底,见 _load_devices
|
||||
sim_iccids: dict[int, str] # slot → iccid(只保留非空)
|
||||
max_sim_slots: int = 4 # 默认 4 槽位
|
||||
device_name: str = ""
|
||||
device_model: str = ""
|
||||
device_type: str = ""
|
||||
max_sim_slots: int = 1 # 不填时按非空 sim_iccid_* 的最大槽位推导
|
||||
current_slot: int = 1 # 默认 slot 1 为当前生效
|
||||
|
||||
|
||||
@@ -220,6 +223,9 @@ def _load_devices(path: Path, result: LoadResult) -> None:
|
||||
for idx, raw in enumerate(reader, start=2):
|
||||
imei = _normalize_cell(raw.get("imei", ""))
|
||||
virtual_no = _normalize_cell(raw.get("virtual_no", ""))
|
||||
device_name = _normalize_cell(raw.get("device_name", ""))
|
||||
device_model = _normalize_cell(raw.get("device_model", ""))
|
||||
device_type = _normalize_cell(raw.get("device_type", ""))
|
||||
# virtual_no 留空时用 imei 兜底(业务方说"虚拟号一般会提供,没有就用 imei")
|
||||
effective_vno = virtual_no or imei
|
||||
if not effective_vno:
|
||||
@@ -242,12 +248,43 @@ def _load_devices(path: Path, result: LoadResult) -> None:
|
||||
if v:
|
||||
sim_iccids[slot] = v
|
||||
|
||||
max_sim_slots_raw = _normalize_cell(raw.get("max_sim_slots", ""))
|
||||
if max_sim_slots_raw:
|
||||
try:
|
||||
max_sim_slots = int(max_sim_slots_raw)
|
||||
except ValueError:
|
||||
result.errors.append(
|
||||
ErrorRow("devices.csv", idx, "max_sim_slots", max_sim_slots_raw, "invalid",
|
||||
"最大卡槽数必须是 1-4 的整数")
|
||||
)
|
||||
continue
|
||||
if max_sim_slots < 1 or max_sim_slots > 4:
|
||||
result.errors.append(
|
||||
ErrorRow("devices.csv", idx, "max_sim_slots", max_sim_slots_raw, "invalid",
|
||||
"最大卡槽数必须在 1-4 之间")
|
||||
)
|
||||
continue
|
||||
else:
|
||||
max_sim_slots = max(sim_iccids.keys(), default=1)
|
||||
|
||||
overflow_slots = [slot for slot in sim_iccids if slot > max_sim_slots]
|
||||
if overflow_slots:
|
||||
result.errors.append(
|
||||
ErrorRow("devices.csv", idx, "max_sim_slots", str(max_sim_slots), "slot_overflow",
|
||||
f"已填写的卡槽 {max(overflow_slots)} 超出最大卡槽数")
|
||||
)
|
||||
continue
|
||||
|
||||
result.devices.append(
|
||||
DeviceRow(
|
||||
line_no=idx,
|
||||
imei=imei,
|
||||
virtual_no=effective_vno,
|
||||
device_name=device_name,
|
||||
device_model=device_model,
|
||||
device_type=device_type,
|
||||
sim_iccids=sim_iccids,
|
||||
max_sim_slots=max_sim_slots,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -171,6 +171,23 @@ def _device_by_virtual_no(devices: list[DeviceRow]) -> dict[str, DeviceRow]:
|
||||
return {d.virtual_no: d for d in devices}
|
||||
|
||||
|
||||
def _device_virtual_no_set(devices: list[DeviceRow]) -> set[str]:
|
||||
return {d.virtual_no for d in devices if d.virtual_no}
|
||||
|
||||
|
||||
def _card_virtual_no_for_import(meta: LegacyCardMeta, device_virtual_nos: set[str]) -> str:
|
||||
"""返回可安全写入卡表的 virtual_no。
|
||||
|
||||
奇成里绑定设备的主卡可能把设备号放在卡 virtual_no 字段。新系统全局标识符要求
|
||||
设备号归属 device,不能再注册成 iot_card,否则资产解析会变成卡视角。
|
||||
"""
|
||||
if not meta.virtual_no:
|
||||
return ""
|
||||
if meta.virtual_no in device_virtual_nos:
|
||||
return ""
|
||||
return meta.virtual_no
|
||||
|
||||
|
||||
def _runtime_asset_for_card(card: CardRow, devices_by_virtual_no: dict[str, DeviceRow]) -> tuple[str, str] | None:
|
||||
"""返回运行态套餐/订单归属:(asset_type, asset_identifier)。
|
||||
|
||||
@@ -218,7 +235,7 @@ def write_step1(
|
||||
|
||||
cards_by_iccid_19 = {c.iccid_19: c for c in valid_cards}
|
||||
|
||||
_write_iot_cards(output_dir / "step1_01_iot_cards.sql", valid_cards, card_metas, mapping, user_id)
|
||||
_write_iot_cards(output_dir / "step1_01_iot_cards.sql", valid_cards, devices, card_metas, mapping, user_id)
|
||||
_write_devices(output_dir / "step1_02_devices.sql", devices, card_metas, mapping, user_id)
|
||||
_write_asset_identifiers(output_dir / "step1_03_asset_identifiers.sql", valid_cards, devices, card_metas)
|
||||
_write_asset_wallets(output_dir / "step1_04_asset_wallets.sql", valid_cards, devices)
|
||||
@@ -292,11 +309,14 @@ def _filter_cards_by_carrier(
|
||||
def _write_iot_cards(
|
||||
path: Path,
|
||||
cards: list[CardRow],
|
||||
devices: list[DeviceRow],
|
||||
card_metas: dict[str, LegacyCardMeta],
|
||||
mapping: Mapping,
|
||||
user_id: int,
|
||||
) -> None:
|
||||
"""写 tb_iot_card。元数据从奇成 card_metas + mapping 推导,iccid_19/iccid_20 用业务方两列。"""
|
||||
device_virtual_nos = _device_virtual_no_set(devices)
|
||||
devices_by_virtual_no = _device_by_virtual_no(devices)
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
f.write(_file_header("step1_01 卡资产导入 (tb_iot_card)"))
|
||||
for c in cards:
|
||||
@@ -309,7 +329,15 @@ def _write_iot_cards(
|
||||
|
||||
card_category = "industry" if c.is_industry else "normal"
|
||||
shop_code = _resolve_card_shop_code(c, meta, mapping)
|
||||
if c.bound_device_virtual_no:
|
||||
device = devices_by_virtual_no.get(c.bound_device_virtual_no)
|
||||
shop_code = _resolve_device_shop_code(device, card_metas, mapping) if device else ""
|
||||
series_id_expr = _target_series_id_expr_for_iccid_19(c.iccid_19, card_metas, mapping)
|
||||
card_virtual_no = _card_virtual_no_for_import(meta, device_virtual_nos)
|
||||
is_standalone = "FALSE" if c.bound_device_virtual_no else "TRUE"
|
||||
device_virtual_no = c.bound_device_virtual_no or ""
|
||||
card_status = 2 if shop_code else 1
|
||||
shop_id_expr = _shop_id_sub(shop_code)
|
||||
|
||||
f.write(
|
||||
"INSERT INTO tb_iot_card (\n"
|
||||
@@ -326,17 +354,35 @@ def _write_iot_cards(
|
||||
f" {_sql_str(c.iccid_full)}, {_sql_str(c.iccid_19)}, {iccid_20_expr},\n"
|
||||
f" {carrier.target_carrier_id}, {_sql_str(carrier.target_carrier_type)}, {_sql_str(carrier.target_carrier_name)},\n"
|
||||
f" {_sql_str(card_category)}, {_sql_str(mapping.migration_batch_no)},\n"
|
||||
" 1, 0, 0, 0,\n"
|
||||
" 1, 1, TRUE,\n"
|
||||
f" {card_status}, 0, 0, 0,\n"
|
||||
f" 1, 1, {is_standalone},\n"
|
||||
f" 'after_order', {_sql_str(_initial_polling_stop_reason(c))},\n"
|
||||
f" {_sql_str(meta.msisdn) if meta.msisdn else 'NULL'}, "
|
||||
f"{_sql_str(meta.virtual_no) if meta.virtual_no else 'NULL'}, "
|
||||
f"{_shop_id_sub(shop_code)}, {series_id_expr},\n"
|
||||
" '', '',\n"
|
||||
f"{_sql_str(card_virtual_no) if card_virtual_no else 'NULL'}, "
|
||||
f"{shop_id_expr}, {series_id_expr},\n"
|
||||
f" {_sql_str(device_virtual_no)}, '',\n"
|
||||
f" {user_id}, {user_id}, NOW(), NOW()\n"
|
||||
")\n"
|
||||
"ON CONFLICT (iccid) WHERE deleted_at IS NULL DO NOTHING;\n\n"
|
||||
)
|
||||
f.write(
|
||||
"UPDATE tb_iot_card\n"
|
||||
"SET\n"
|
||||
f" status = {card_status},\n"
|
||||
f" shop_id = {shop_id_expr},\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"
|
||||
" updated_at = NOW()\n"
|
||||
f"WHERE iccid = {_sql_str(c.iccid_full)} AND deleted_at IS NULL\n"
|
||||
" AND (\n"
|
||||
f" status IS DISTINCT FROM {card_status}\n"
|
||||
f" OR shop_id IS DISTINCT FROM {shop_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"
|
||||
" );\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _write_devices(
|
||||
@@ -349,9 +395,9 @@ def _write_devices(
|
||||
"""设备表 INSERT。
|
||||
|
||||
业务方不关心的字段都写默认值:
|
||||
device_name/device_model/manufacturer = NULL(可后台编辑)
|
||||
manufacturer = NULL(可后台编辑)
|
||||
max_sim_slots = 4
|
||||
status / asset_status / online_status = 1 / 1 / 0
|
||||
status = 有店铺归属则已分销,否则在库;asset_status / online_status = 1 / 0
|
||||
enable_polling = TRUE,realname_policy = 'after_order'
|
||||
software_version = '',switch_mode = '0'(自动切卡)
|
||||
"""
|
||||
@@ -359,11 +405,14 @@ def _write_devices(
|
||||
f.write(_file_header("step1_02 设备资产导入 (tb_device)"))
|
||||
for d in devices:
|
||||
shop_code = _resolve_device_shop_code(d, card_metas, mapping)
|
||||
device_status = 2 if shop_code else 1
|
||||
shop_id_expr = _shop_id_sub(shop_code)
|
||||
source_iccid = _device_package_source_iccid(d)
|
||||
series_id_expr = _target_series_id_expr_for_iccid_19(source_iccid, card_metas, mapping)
|
||||
f.write(
|
||||
"INSERT INTO tb_device (\n"
|
||||
" virtual_no, max_sim_slots, imei,\n"
|
||||
" device_name, device_model, device_type,\n"
|
||||
" status, asset_status, generation, online_status,\n"
|
||||
" enable_polling, realname_policy,\n"
|
||||
" software_version, switch_mode,\n"
|
||||
@@ -372,15 +421,53 @@ def _write_devices(
|
||||
") VALUES (\n"
|
||||
f" {_sql_str(d.virtual_no)}, {d.max_sim_slots}, "
|
||||
f"{_sql_str(d.imei) if d.imei else 'NULL'},\n"
|
||||
" 1, 1, 1, 0,\n"
|
||||
f" {_sql_str(d.device_name) if d.device_name else 'NULL'}, "
|
||||
f"{_sql_str(d.device_model) if d.device_model else 'NULL'}, "
|
||||
f"{_sql_str(d.device_type) if d.device_type else 'NULL'},\n"
|
||||
f" {device_status}, 1, 1, 0,\n"
|
||||
" TRUE, 'after_order',\n"
|
||||
" '', '0',\n"
|
||||
f" {_sql_str(mapping.migration_batch_no)}, {_shop_id_sub(shop_code)}, {series_id_expr},\n"
|
||||
f" {_sql_str(mapping.migration_batch_no)}, {shop_id_expr}, {series_id_expr},\n"
|
||||
f" {user_id}, {user_id}, NOW(), NOW()\n"
|
||||
")\n"
|
||||
"ON CONFLICT (virtual_no) WHERE deleted_at IS NULL DO NOTHING;\n\n"
|
||||
)
|
||||
|
||||
metadata_updates = [
|
||||
(column, value)
|
||||
for column, value in (
|
||||
("max_sim_slots", str(d.max_sim_slots)),
|
||||
("status", str(device_status)),
|
||||
("shop_id", shop_id_expr),
|
||||
("device_name", d.device_name),
|
||||
("device_model", d.device_model),
|
||||
("device_type", d.device_type),
|
||||
)
|
||||
if value or column in {"max_sim_slots", "status", "shop_id"}
|
||||
]
|
||||
if metadata_updates:
|
||||
sql_literal_columns = {"max_sim_slots", "status", "shop_id"}
|
||||
set_lines = [
|
||||
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}"
|
||||
if column in sql_literal_columns
|
||||
else f"{column} IS DISTINCT FROM {_sql_str(value)}"
|
||||
for column, value in metadata_updates
|
||||
]
|
||||
set_sql = ",\n ".join(set_lines)
|
||||
diff_sql = " OR ".join(diff_checks)
|
||||
f.write(
|
||||
"UPDATE tb_device\n"
|
||||
"SET\n"
|
||||
f" {set_sql},\n"
|
||||
" updated_at = NOW()\n"
|
||||
f"WHERE virtual_no = {_sql_str(d.virtual_no)} AND deleted_at IS NULL\n"
|
||||
f" AND ({diff_sql});\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _write_asset_identifiers(
|
||||
path: Path,
|
||||
@@ -388,21 +475,23 @@ def _write_asset_identifiers(
|
||||
devices: list[DeviceRow],
|
||||
card_metas: dict[str, LegacyCardMeta],
|
||||
) -> None:
|
||||
"""卡的 iccid + 虚拟号(来自奇成)和设备的 virtual_no 都注册。"""
|
||||
"""卡的 iccid + 安全卡虚拟号和设备 virtual_no 都注册。"""
|
||||
device_virtual_nos = _device_virtual_no_set(devices)
|
||||
with path.open("w", encoding="utf-8") as f:
|
||||
f.write(_file_header("step1_03 资产标识符注册 (tb_asset_identifier)"))
|
||||
for c in cards:
|
||||
meta = card_metas[c.iccid_19]
|
||||
card_virtual_no = _card_virtual_no_for_import(meta, device_virtual_nos)
|
||||
f.write(
|
||||
"INSERT INTO tb_asset_identifier (identifier, asset_type, asset_id)\n"
|
||||
f"SELECT {_sql_str(c.iccid_full)}, 'iot_card', id FROM tb_iot_card\n"
|
||||
f"WHERE iccid = {_sql_str(c.iccid_full)} AND deleted_at IS NULL\n"
|
||||
"ON CONFLICT (identifier) DO NOTHING;\n\n"
|
||||
)
|
||||
if meta.virtual_no:
|
||||
if card_virtual_no:
|
||||
f.write(
|
||||
"INSERT INTO tb_asset_identifier (identifier, asset_type, asset_id)\n"
|
||||
f"SELECT {_sql_str(meta.virtual_no)}, 'iot_card', id FROM tb_iot_card\n"
|
||||
f"SELECT {_sql_str(card_virtual_no)}, 'iot_card', id FROM tb_iot_card\n"
|
||||
f"WHERE iccid = {_sql_str(c.iccid_full)} AND deleted_at IS NULL\n"
|
||||
"ON CONFLICT (identifier) DO NOTHING;\n\n"
|
||||
)
|
||||
@@ -433,6 +522,17 @@ def _write_asset_wallets(path: Path, cards: list[CardRow], devices: list[DeviceR
|
||||
f"WHERE iccid = {_sql_str(c.iccid_full)} AND deleted_at IS NULL\n"
|
||||
"ON CONFLICT (resource_type, resource_id) WHERE deleted_at IS NULL DO NOTHING;\n\n"
|
||||
)
|
||||
f.write(
|
||||
"UPDATE tb_asset_wallet w\n"
|
||||
"SET shop_id_tag = COALESCE(c.shop_id, 0),\n"
|
||||
" updated_at = NOW()\n"
|
||||
"FROM tb_iot_card c\n"
|
||||
"WHERE w.resource_type = 'iot_card'\n"
|
||||
" AND w.resource_id = c.id\n"
|
||||
" AND w.deleted_at IS NULL\n"
|
||||
f" AND c.iccid = {_sql_str(c.iccid_full)} AND c.deleted_at IS NULL\n"
|
||||
" AND w.shop_id_tag IS DISTINCT FROM COALESCE(c.shop_id, 0);\n\n"
|
||||
)
|
||||
for d in devices:
|
||||
f.write(
|
||||
"INSERT INTO tb_asset_wallet (\n"
|
||||
@@ -447,6 +547,17 @@ def _write_asset_wallets(path: Path, cards: list[CardRow], devices: list[DeviceR
|
||||
f"WHERE virtual_no = {_sql_str(d.virtual_no)} AND deleted_at IS NULL\n"
|
||||
"ON CONFLICT (resource_type, resource_id) WHERE deleted_at IS NULL DO NOTHING;\n\n"
|
||||
)
|
||||
f.write(
|
||||
"UPDATE tb_asset_wallet w\n"
|
||||
"SET shop_id_tag = COALESCE(d.shop_id, 0),\n"
|
||||
" updated_at = NOW()\n"
|
||||
"FROM tb_device d\n"
|
||||
"WHERE w.resource_type = 'device'\n"
|
||||
" AND w.resource_id = d.id\n"
|
||||
" AND w.deleted_at IS NULL\n"
|
||||
f" AND d.virtual_no = {_sql_str(d.virtual_no)} AND d.deleted_at IS NULL\n"
|
||||
" AND w.shop_id_tag IS DISTINCT FROM COALESCE(d.shop_id, 0);\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _write_sim_bindings(
|
||||
@@ -475,6 +586,34 @@ def _write_sim_bindings(
|
||||
f" AND c.iccid = {_sql_str(card.iccid_full)} AND c.deleted_at IS NULL\n"
|
||||
"ON CONFLICT (device_id, slot_position) WHERE bind_status = 1 AND deleted_at IS NULL DO NOTHING;\n\n"
|
||||
)
|
||||
f.write(
|
||||
"UPDATE tb_iot_card c\n"
|
||||
"SET is_standalone = FALSE,\n"
|
||||
" device_virtual_no = d.virtual_no,\n"
|
||||
" shop_id = d.shop_id,\n"
|
||||
" status = CASE WHEN d.shop_id IS NULL THEN 1 ELSE 2 END,\n"
|
||||
" updated_at = NOW()\n"
|
||||
"FROM tb_device d\n"
|
||||
f"WHERE d.virtual_no = {_sql_str(d.virtual_no)} AND d.deleted_at IS NULL\n"
|
||||
f" AND c.iccid = {_sql_str(card.iccid_full)} AND c.deleted_at IS NULL\n"
|
||||
" AND (\n"
|
||||
" c.is_standalone IS DISTINCT FROM FALSE\n"
|
||||
" OR c.device_virtual_no IS DISTINCT FROM d.virtual_no\n"
|
||||
" OR c.shop_id IS DISTINCT FROM d.shop_id\n"
|
||||
" OR c.status IS DISTINCT FROM CASE WHEN d.shop_id IS NULL THEN 1 ELSE 2 END\n"
|
||||
" );\n\n"
|
||||
)
|
||||
f.write(
|
||||
"UPDATE tb_asset_wallet w\n"
|
||||
"SET shop_id_tag = COALESCE(c.shop_id, 0),\n"
|
||||
" updated_at = NOW()\n"
|
||||
"FROM tb_iot_card c\n"
|
||||
f"WHERE c.iccid = {_sql_str(card.iccid_full)} AND c.deleted_at IS NULL\n"
|
||||
" AND w.resource_type = 'iot_card'\n"
|
||||
" AND w.resource_id = c.id\n"
|
||||
" AND w.deleted_at IS NULL\n"
|
||||
" AND w.shop_id_tag IS DISTINCT FROM COALESCE(c.shop_id, 0);\n\n"
|
||||
)
|
||||
|
||||
|
||||
def _write_shop_allocations(
|
||||
|
||||
@@ -36,13 +36,17 @@
|
||||
|------|------|------|
|
||||
| `imei` | 否 | 设备 IMEI(非蜂窝设备可留空) |
|
||||
| `virtual_no` | 否 | 设备虚拟号/设备号。留空时用 `imei` 兜底;两者都为空则该行报错 |
|
||||
| `device_name` | 否 | 设备名称,写入 `tb_device.device_name` |
|
||||
| `device_model` | 否 | 设备型号,写入 `tb_device.device_model` |
|
||||
| `device_type` | 否 | 设备类型,写入 `tb_device.device_type` |
|
||||
| `max_sim_slots` | 否 | 最大卡槽数,范围 1-4。不填时按非空 `sim_iccid_*` 的最大槽位推导 |
|
||||
| `sim_iccid_1` ~ `sim_iccid_4` | 否 | 各插槽 ICCID,引用 `cards.csv` 的卡。可填 19 或 20 位,内部统一按前 19 位回查 `cards.iccid_19`(至少要有 1 个) |
|
||||
|
||||
**店铺归属规则**:用 `sim_iccid_1`(主卡)的 agent_id 走 `mapping.agents`
|
||||
推导,与卡归属同口径。
|
||||
|
||||
**其他字段自动取默认值**:`device_name` / `device_model` / `manufacturer` 留空,
|
||||
`max_sim_slots=4`,`current_slot=1`(以 `sim_iccid_1` 为当前生效卡)。
|
||||
**其他字段自动取默认值**:`manufacturer` 留空,`max_sim_slots` 按已填写卡槽推导,
|
||||
`current_slot=1`(以 `sim_iccid_1` 为当前生效卡)。
|
||||
|
||||
## 一致性约束
|
||||
|
||||
|
||||
Reference in New Issue
Block a user