更新一版
This commit is contained in:
@@ -43,23 +43,43 @@ def _import_pymysql():
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LegacyPackage:
|
||||
"""奇成当前生效套餐快照(取自 tbl_card_life)。"""
|
||||
"""奇成当前生效正式套餐快照(取自 tbl_card_life)。"""
|
||||
|
||||
iccid: str
|
||||
meal_id: str
|
||||
meal_name: str
|
||||
meal_type: str
|
||||
start_date: Optional[str]
|
||||
expire_date: Optional[str]
|
||||
flow_size_mb: int # 套餐总量,单位 MB
|
||||
status: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LegacyPackageLifecycle:
|
||||
"""奇成正式套餐生命周期记录。"""
|
||||
|
||||
iccid: str
|
||||
source_table: str
|
||||
life_id: str
|
||||
meal_id: str
|
||||
meal_name: str
|
||||
meal_type: str
|
||||
status: int
|
||||
start_date: Optional[str]
|
||||
expire_date: Optional[str]
|
||||
flow_size_mb: int
|
||||
stable_sort_key: str
|
||||
migration_status: str
|
||||
skip_reason: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LegacyCardUsage:
|
||||
"""奇成卡累计流量快照。
|
||||
|
||||
奇成 ``tbl_card.total_bytes_cnt`` 存的是经过当前套餐 ``flow_add_discount``
|
||||
加成后的"虚用量",我们这里同时拉取当前生效套餐(``tbl_card_life`` status=1
|
||||
加成后的"虚用量",我们这里同时拉取当前生效正式套餐(``tbl_card_life`` status=1,type!=1
|
||||
且 ``expire_date`` 最大)的 ``flow_add_discount``,反算回新系统期望的"真用量"。
|
||||
|
||||
精度说明:跨多个套餐周期累计时,``total_bytes_cnt`` 是按各时段对应套餐
|
||||
@@ -70,7 +90,7 @@ class LegacyCardUsage:
|
||||
iccid: str # 业务方视角的完整 ICCID
|
||||
virtual_used_mb: Decimal # 奇成 total_bytes_cnt 原始值(虚 MB)
|
||||
flow_add_discount_pct: Decimal # 当前生效套餐虚比百分比(0~100)
|
||||
has_active_package: bool # 是否在 tbl_card_life 中找到当前生效套餐
|
||||
has_active_package: bool # 是否在 tbl_card_life 中找到当前生效正式套餐
|
||||
|
||||
@property
|
||||
def real_used_mb(self) -> Decimal:
|
||||
@@ -135,8 +155,9 @@ class LegacyCardMeta:
|
||||
agent_name: str
|
||||
msisdn: str # tbl_card.phone
|
||||
virtual_no: str # tbl_virtual_number.vcode(如有)
|
||||
current_meal_id: str # 当前生效套餐 ID(可空,取自 tbl_card_life status=1 expire_date 最大)
|
||||
current_meal_id: str # 当前生效正式套餐 ID(可空,取自 tbl_card_life status=1,type!=1,expire_date 最大)
|
||||
current_meal_name: str
|
||||
current_meal_type: str # tbl_card_life.type,1=叠加包/加油包,2=月卡,3=季卡,4=半年卡,5=年卡
|
||||
current_expire_date: Optional[str] # ISO 字符串
|
||||
|
||||
|
||||
@@ -217,7 +238,7 @@ def _to_mb_decimal(value) -> Decimal:
|
||||
|
||||
|
||||
def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPackage]:
|
||||
"""查每张卡的当前生效套餐:status=1 且 expire_date 最大。
|
||||
"""查每张卡的当前生效主套餐:status=1、type!=1 且 expire_date 最大。
|
||||
|
||||
返回: iccid → LegacyPackage(找不到的 iccid 不在结果中)
|
||||
"""
|
||||
@@ -231,6 +252,7 @@ def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPacka
|
||||
iccid_mark,
|
||||
meal_id,
|
||||
meal_name,
|
||||
COALESCE(CAST(type AS CHAR), '') AS meal_type,
|
||||
start_date,
|
||||
expire_date,
|
||||
COALESCE(flow_size, 0) AS flow_size,
|
||||
@@ -238,6 +260,7 @@ def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPacka
|
||||
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
|
||||
"""
|
||||
seen: set[str] = set()
|
||||
@@ -253,6 +276,7 @@ def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPacka
|
||||
iccid=iccid,
|
||||
meal_id=row.get("meal_id") or "",
|
||||
meal_name=row.get("meal_name") or "",
|
||||
meal_type=str(row.get("meal_type") or ""),
|
||||
start_date=row["start_date"].strftime("%Y-%m-%d %H:%M:%S") if row.get("start_date") else None,
|
||||
expire_date=row["expire_date"].strftime("%Y-%m-%d %H:%M:%S") if row.get("expire_date") else None,
|
||||
flow_size_mb=int(row.get("flow_size") or 0),
|
||||
@@ -261,11 +285,172 @@ def fetch_current_packages(conn, iccids: Iterable[str]) -> dict[str, LegacyPacka
|
||||
return out
|
||||
|
||||
|
||||
def fetch_package_lifecycles(conn, iccid_pairs: Iterable[tuple]) -> dict[str, list[LegacyPackageLifecycle]]:
|
||||
"""查当前生效和未生效待生效正式套餐生命周期。
|
||||
|
||||
只返回可迁移范围(active/pending)和需要在审核文件中说明的跳过记录。
|
||||
加油包(type=1)和已过期记录不生成套餐 SQL,但保留 skip_reason 供审核产物输出。
|
||||
"""
|
||||
pairs = _normalize_iccid_pairs(iccid_pairs)
|
||||
out: dict[str, list[LegacyPackageLifecycle]] = {}
|
||||
if not pairs:
|
||||
return out
|
||||
|
||||
query_to_full: dict[str, str] = {}
|
||||
for i19, i20, allow_i19_lookup in pairs:
|
||||
card_key = i20 or i19
|
||||
query_to_full[card_key] = card_key
|
||||
if i20 and allow_i19_lookup:
|
||||
query_to_full[i19] = card_key
|
||||
if not i20:
|
||||
query_to_full[i19] = card_key
|
||||
iccid_list = sorted(query_to_full.keys())
|
||||
|
||||
sql = """
|
||||
SELECT
|
||||
'tbl_card_life' AS source_table,
|
||||
COALESCE(CAST(id AS CHAR), '') AS life_id,
|
||||
iccid_mark,
|
||||
COALESCE(meal_id, '') AS meal_id,
|
||||
COALESCE(meal_name, '') AS meal_name,
|
||||
COALESCE(CAST(type AS CHAR), '') AS meal_type,
|
||||
status,
|
||||
start_date,
|
||||
expire_date,
|
||||
COALESCE(flow_size, 0) AS flow_size
|
||||
FROM tbl_card_life
|
||||
WHERE iccid_mark IN %s
|
||||
ORDER BY
|
||||
iccid_mark ASC,
|
||||
COALESCE(start_date, '1970-01-01') ASC,
|
||||
COALESCE(expire_date, '9999-12-31') ASC,
|
||||
id ASC
|
||||
"""
|
||||
with conn.cursor() as cur:
|
||||
for batch in _chunks(iccid_list):
|
||||
cur.execute(sql, (tuple(batch),))
|
||||
for row in cur.fetchall():
|
||||
legacy_iccid = row.get("iccid_mark") or ""
|
||||
if not legacy_iccid:
|
||||
continue
|
||||
iccid = query_to_full.get(legacy_iccid)
|
||||
if not iccid:
|
||||
continue
|
||||
start = row.get("start_date")
|
||||
expire = row.get("expire_date")
|
||||
start_str = start.strftime("%Y-%m-%d %H:%M:%S") if start else None
|
||||
expire_str = expire.strftime("%Y-%m-%d %H:%M:%S") if expire else None
|
||||
meal_type = str(row.get("meal_type") or "")
|
||||
raw_status = int(row.get("status") or 0)
|
||||
migration_status, skip_reason = _classify_package_lifecycle(meal_type, raw_status, start, expire)
|
||||
life_id = str(row.get("life_id") or "")
|
||||
stable_sort_key = "|".join([start_str or "", expire_str or "", life_id])
|
||||
item = LegacyPackageLifecycle(
|
||||
iccid=iccid,
|
||||
source_table=row.get("source_table") or "tbl_card_life",
|
||||
life_id=life_id,
|
||||
meal_id=row.get("meal_id") or "",
|
||||
meal_name=row.get("meal_name") or "",
|
||||
meal_type=meal_type,
|
||||
status=raw_status,
|
||||
start_date=start_str,
|
||||
expire_date=expire_str,
|
||||
flow_size_mb=int(row.get("flow_size") or 0),
|
||||
stable_sort_key=stable_sort_key,
|
||||
migration_status=migration_status,
|
||||
skip_reason=skip_reason,
|
||||
)
|
||||
out.setdefault(iccid, []).append(item)
|
||||
|
||||
next_sql = """
|
||||
SELECT
|
||||
'tbl_next_month_card_life' AS source_table,
|
||||
COALESCE(CAST(id AS CHAR), '') AS life_id,
|
||||
iccid_mark,
|
||||
COALESCE(meal_id, '') AS meal_id,
|
||||
COALESCE(meal_name, '') AS meal_name,
|
||||
COALESCE(CAST(order_type AS CHAR), '') AS meal_type,
|
||||
status,
|
||||
effect_begin_time AS start_date,
|
||||
effect_complete_time AS expire_date,
|
||||
COALESCE(flow_size, 0) AS flow_size
|
||||
FROM tbl_next_month_card_life
|
||||
WHERE iccid_mark IN %s
|
||||
ORDER BY
|
||||
iccid_mark ASC,
|
||||
COALESCE(effect_begin_time, '1970-01-01') ASC,
|
||||
COALESCE(effect_complete_time, '9999-12-31') ASC,
|
||||
id ASC
|
||||
"""
|
||||
with conn.cursor() as cur:
|
||||
for batch in _chunks(iccid_list):
|
||||
cur.execute(next_sql, (tuple(batch),))
|
||||
for row in cur.fetchall():
|
||||
legacy_iccid = row.get("iccid_mark") or ""
|
||||
if not legacy_iccid:
|
||||
continue
|
||||
iccid = query_to_full.get(legacy_iccid)
|
||||
if not iccid:
|
||||
continue
|
||||
start = row.get("start_date")
|
||||
expire = row.get("expire_date")
|
||||
start_str = start.strftime("%Y-%m-%d %H:%M:%S") if start else None
|
||||
expire_str = expire.strftime("%Y-%m-%d %H:%M:%S") if expire else None
|
||||
raw_status = int(row.get("status") or 0)
|
||||
migration_status, skip_reason = _classify_next_month_lifecycle(raw_status)
|
||||
life_id = str(row.get("life_id") or "")
|
||||
stable_sort_key = "|".join([start_str or "", expire_str or "", f"next:{life_id}"])
|
||||
out.setdefault(iccid, []).append(LegacyPackageLifecycle(
|
||||
iccid=iccid,
|
||||
source_table=row.get("source_table") or "tbl_next_month_card_life",
|
||||
life_id=life_id,
|
||||
meal_id=row.get("meal_id") or "",
|
||||
meal_name=row.get("meal_name") or "",
|
||||
meal_type=str(row.get("meal_type") or ""),
|
||||
status=raw_status,
|
||||
start_date=start_str,
|
||||
expire_date=expire_str,
|
||||
flow_size_mb=int(row.get("flow_size") or 0),
|
||||
stable_sort_key=stable_sort_key,
|
||||
migration_status=migration_status,
|
||||
skip_reason=skip_reason,
|
||||
))
|
||||
return out
|
||||
|
||||
|
||||
def _classify_package_lifecycle(meal_type: str, status: int, start_date, expire_date) -> tuple[str, str]:
|
||||
"""把奇成生命周期记录分类为 active/pending/skipped。"""
|
||||
if meal_type == "1":
|
||||
return "skipped", "加油包不迁移"
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
now = datetime.now()
|
||||
if expire_date and expire_date <= now:
|
||||
return "skipped", "已过期套餐不迁移"
|
||||
if status == 1:
|
||||
if start_date and start_date > now:
|
||||
return "pending", ""
|
||||
return "active", ""
|
||||
if start_date and start_date > now:
|
||||
return "pending", ""
|
||||
return "skipped", f"奇成状态 {status} 不在迁移范围"
|
||||
|
||||
|
||||
def _classify_next_month_lifecycle(status: int) -> tuple[str, str]:
|
||||
"""把奇成次月待生效套餐记录分类为 pending/skipped。"""
|
||||
if status == 1:
|
||||
return "pending", ""
|
||||
if status == 2:
|
||||
return "skipped", "次月待生效记录已插入 tbl_card_life,避免重复迁移"
|
||||
return "skipped", f"奇成次月待生效状态 {status} 不在迁移范围"
|
||||
|
||||
|
||||
def fetch_card_usage(conn, iccid_pairs: Iterable[tuple]) -> dict[str, LegacyCardUsage]:
|
||||
"""查每张卡的累计已用流量,并同步取当前生效套餐的 flow_add_discount。
|
||||
"""查每张卡的累计已用流量,并同步取当前生效正式套餐的 flow_add_discount。
|
||||
|
||||
奇成 ``tbl_card.total_bytes_cnt`` 是虚用量(已被套餐虚比加成)。
|
||||
这里 JOIN 当前生效套餐(``tbl_card_life`` status=1 且 ``expire_date`` 最大)
|
||||
这里 JOIN 当前生效正式套餐(``tbl_card_life`` status=1,type!=1 且 ``expire_date`` 最大)
|
||||
+ ``tbl_set_meal`` 取 ``flow_add_discount``,放进 ``LegacyCardUsage``;
|
||||
上层通过 ``real_used_mb_floor`` 反算真用量写入新系统。
|
||||
|
||||
@@ -312,12 +497,15 @@ def fetch_card_usage(conn, iccid_pairs: Iterable[tuple]) -> dict[str, LegacyCard
|
||||
INNER JOIN (
|
||||
SELECT iccid_mark, MAX(expire_date) AS max_expire
|
||||
FROM tbl_card_life
|
||||
WHERE status = 1 AND iccid_mark IN %s
|
||||
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)
|
||||
@@ -440,6 +628,7 @@ def fetch_card_meta(
|
||||
COALESCE(vn.vcode, '') AS vcode,
|
||||
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
|
||||
FROM tbl_card c
|
||||
LEFT JOIN tbl_vendor_category vc ON vc.category = c.category
|
||||
@@ -448,17 +637,20 @@ def fetch_card_meta(
|
||||
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.expire_date
|
||||
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 iccid_mark IN %s
|
||||
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)
|
||||
@@ -511,6 +703,7 @@ def fetch_card_meta(
|
||||
virtual_no=row.get("vcode") or "",
|
||||
current_meal_id=row.get("meal_id") or "",
|
||||
current_meal_name=row.get("meal_name") or "",
|
||||
current_meal_type=str(row.get("meal_type") or ""),
|
||||
current_expire_date=expire_str,
|
||||
)
|
||||
return metas, duplicates
|
||||
|
||||
Reference in New Issue
Block a user