159 lines
5.4 KiB
Python
159 lines
5.4 KiB
Python
"""ICCID 19/20 位拆分与校验工具。
|
|
|
|
移植自 pkg/utils/iccid.go,并按运营商类型增加长度合规校验。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
|
|
# 运营商标准 ICCID 长度
|
|
CARRIER_ICCID_LENGTH = {
|
|
"CTCC": 19, # 电信
|
|
"CMCC": 20, # 移动
|
|
"CUCC": 20, # 联通
|
|
"CBN": 20, # 广电
|
|
}
|
|
|
|
VALID_CARRIER_TYPES = set(CARRIER_ICCID_LENGTH.keys())
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SplitResult:
|
|
"""ICCID 拆分结果。"""
|
|
|
|
iccid: str # 原始 ICCID(去空格)
|
|
iccid_19: str # 前 19 位(所有卡必填)
|
|
iccid_20: Optional[str] # 完整 20 位(仅 20 位运营商卡有值,19 位卡为 None → SQL NULL)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ValidationError:
|
|
"""ICCID 校验失败信息。"""
|
|
|
|
code: str # 错误码:length_invalid / length_mismatch
|
|
message: str # 中文描述
|
|
|
|
|
|
def split_iccid(iccid: str) -> Optional[SplitResult]:
|
|
"""按 ICCID 长度拆分双列存储值。
|
|
|
|
与 Go 版本一致:
|
|
19 位 → (iccid, iccid, None)
|
|
20 位 → (iccid, iccid[:19], iccid)
|
|
其他 → None(调用方按异常处理)
|
|
"""
|
|
iccid = iccid.strip()
|
|
if len(iccid) == 19:
|
|
return SplitResult(iccid=iccid, iccid_19=iccid, iccid_20=None)
|
|
if len(iccid) == 20:
|
|
return SplitResult(iccid=iccid, iccid_19=iccid[:19], iccid_20=iccid)
|
|
return None
|
|
|
|
|
|
def validate_and_split(iccid: str, carrier_type: str) -> tuple[Optional[SplitResult], Optional[ValidationError]]:
|
|
"""按运营商类型校验 ICCID 长度并拆分。
|
|
|
|
返回 (拆分结果, 错误信息) 二元组:
|
|
- 长度不是 19/20:返回 (None, length_invalid 错误)
|
|
- 长度与 carrier_type 不匹配:返回 (拆分结果, length_mismatch 警告) → 调用方决定要不要放行
|
|
- 合法:返回 (拆分结果, None)
|
|
|
|
设计说明:不直接拒绝长度不匹配的情况(电信卡居然是 20 位 / 非电信卡是 19 位),
|
|
保留拆分结果让脚本写入 errors.csv 让业务方确认。
|
|
"""
|
|
iccid = iccid.strip()
|
|
if carrier_type not in VALID_CARRIER_TYPES:
|
|
return None, ValidationError(
|
|
code="carrier_type_invalid",
|
|
message=f"无效的运营商类型 {carrier_type!r},应为 {sorted(VALID_CARRIER_TYPES)} 之一",
|
|
)
|
|
|
|
result = split_iccid(iccid)
|
|
if result is None:
|
|
return None, ValidationError(
|
|
code="length_invalid",
|
|
message=f"ICCID 长度 {len(iccid)} 不合法,只支持 19/20 位",
|
|
)
|
|
|
|
expected_len = CARRIER_ICCID_LENGTH[carrier_type]
|
|
if len(iccid) != expected_len:
|
|
return result, ValidationError(
|
|
code="length_mismatch",
|
|
message=f"{carrier_type} 应为 {expected_len} 位但 ICCID 是 {len(iccid)} 位,请人工确认",
|
|
)
|
|
|
|
return result, None
|
|
|
|
|
|
def prefix_19(iccid: str) -> str:
|
|
"""ICCID 前 19 位(稳定 ID,无校验位)。
|
|
|
|
用于 devices.csv 引用 cards 时的兼容查找:业务方在 devices 里填 19 还是 20 位都接受,
|
|
内部统一截取前 19 位回查 cards.iccid_19。
|
|
"""
|
|
s = (iccid or "").strip()
|
|
if len(s) >= 19:
|
|
return s[:19]
|
|
return s
|
|
|
|
|
|
def validate_iccid_19(iccid_19: str) -> Optional[ValidationError]:
|
|
"""iccid_19 必须 19 位纯数字。"""
|
|
if not iccid_19:
|
|
return ValidationError(code="iccid_19_required", message="iccid_19 不能为空")
|
|
if len(iccid_19) != 19:
|
|
return ValidationError(
|
|
code="iccid_19_length",
|
|
message=f"iccid_19 必须是 19 位,当前 {len(iccid_19)} 位",
|
|
)
|
|
if not iccid_19.isdigit():
|
|
return ValidationError(code="iccid_19_format", message="iccid_19 必须是纯数字")
|
|
return None
|
|
|
|
|
|
def validate_iccid_20(iccid_20: str, iccid_19: str) -> Optional[ValidationError]:
|
|
"""iccid_20 可空(CTCC 卡);非空时必须 20 位纯数字且前 19 位 == iccid_19。"""
|
|
if not iccid_20:
|
|
return None
|
|
if len(iccid_20) != 20:
|
|
return ValidationError(
|
|
code="iccid_20_length",
|
|
message=f"iccid_20 必须是 20 位,当前 {len(iccid_20)} 位",
|
|
)
|
|
if not iccid_20.isdigit():
|
|
return ValidationError(code="iccid_20_format", message="iccid_20 必须是纯数字")
|
|
if iccid_20[:19] != iccid_19:
|
|
return ValidationError(
|
|
code="iccid_20_prefix_mismatch",
|
|
message=f"iccid_20 前 19 位 ({iccid_20[:19]!r}) 与 iccid_19 ({iccid_19!r}) 不一致",
|
|
)
|
|
return None
|
|
|
|
|
|
def validate_carrier_iccid_match(
|
|
iccid_20: str, carrier_type: str
|
|
) -> Optional[ValidationError]:
|
|
"""业务方填写的 iccid_20 与奇成查到的 carrier_type 是否匹配。
|
|
|
|
- CTCC(19 位运营商):iccid_20 必须为空
|
|
- CMCC/CUCC/CBN(20 位运营商):iccid_20 必填
|
|
"""
|
|
if carrier_type not in VALID_CARRIER_TYPES:
|
|
return ValidationError(
|
|
code="carrier_type_invalid",
|
|
message=f"未知运营商类型 {carrier_type!r}",
|
|
)
|
|
expected_len = CARRIER_ICCID_LENGTH[carrier_type]
|
|
if expected_len == 19 and iccid_20:
|
|
return ValidationError(
|
|
code="carrier_length_conflict",
|
|
message=f"{carrier_type} 是 19 位运营商,iccid_20 应留空,实际填了 {iccid_20!r}",
|
|
)
|
|
if expected_len == 20 and not iccid_20:
|
|
return ValidationError(
|
|
code="carrier_length_conflict",
|
|
message=f"{carrier_type} 是 {expected_len} 位运营商,iccid_20 必填",
|
|
)
|
|
return None
|