Files
junhong_cmp_fiber/scripts/migration/remove_cards.py
break 026d4908d8
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s
删除
2026-07-11 12:28:38 +09:00

187 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""卡删除脚本:读取 ICCID 列表,生成数据库删除 SQL 和 Redis 轮询清理命令。
用法:
python3 remove_cards.py --input <csv文件> [--output-dir output]
CSV 格式: 纯文本,每行一个 ICCID首行可有 "iccid" 表头(自动跳过)
输出:
output/remove_cards_db.sql -- 数据库软删 + 标识符硬删 SQL事务封装
output/remove_cards_redis_gen.sql -- 在数据库执行后把输出通过 redis-cli 清理轮询队列
"""
from __future__ import annotations
import argparse
import sys
from datetime import datetime
from pathlib import Path
def load_iccids(csv_path: Path) -> list[str]:
"""读取 CSV 文件中的 ICCID 列表,自动跳过空行和表头。"""
iccids: list[str] = []
with csv_path.open(encoding="utf-8") as f:
for lineno, raw in enumerate(f, 1):
line = raw.strip()
if not line:
continue
# 跳过纯字母表头(如 "iccid"
if lineno == 1 and not line.isdigit() and not line.startswith("8"):
print(f"[remove] 跳过表头: {line!r}", file=sys.stderr)
continue
iccids.append(line)
return iccids
def sql_in_list(iccids: list[str]) -> str:
"""生成 SQL IN 列表字符串,如 '898...', '898...'"""
return ", ".join(f"'{i}'" for i in iccids)
def generate_db_sql(iccids: list[str], generated_at: str) -> str:
in_list = sql_in_list(iccids)
count = len(iccids)
return f"""\
-- ============================================================
-- 迁移卡删除脚本 (数据库部分)
-- 生成时间: {generated_at}
-- 卡数量 : {count}
-- 执行前请先用 remove_cards_redis_gen.sql 生成 Redis 清理命令并执行
-- 建议执行: psql ... -1 -f <此文件>
-- ============================================================
BEGIN;
-- 临时表:本次要删除的卡
CREATE TEMP TABLE tmp_remove_cards AS
SELECT id, iccid
FROM tb_iot_card
WHERE iccid IN ({in_list});
DO $$ BEGIN
RAISE NOTICE '本次将删除 % 张卡(共输入 {count} 个 ICCID', (SELECT COUNT(*) FROM tmp_remove_cards);
END $$;
-- 1. 删除套餐使用记录
DELETE FROM tb_package_usage
WHERE iot_card_id IN (SELECT id FROM tmp_remove_cards);
-- 2. 删除订单
DELETE FROM tb_order
WHERE iot_card_id IN (SELECT id FROM tmp_remove_cards);
-- 3. 删除分配记录
DELETE FROM tb_asset_allocation_record
WHERE asset_type = 'iot_card'
AND asset_id IN (SELECT id FROM tmp_remove_cards);
-- 4. 删除资产钱包
DELETE FROM tb_asset_wallet
WHERE resource_type = 'iot_card'
AND resource_id IN (SELECT id FROM tmp_remove_cards);
-- 5. 删除设备绑卡关系
DELETE FROM tb_device_sim_binding
WHERE iot_card_id IN (SELECT id FROM tmp_remove_cards);
-- 6. 删除资产标识符
DELETE FROM tb_asset_identifier
WHERE asset_type = 'iot_card'
AND asset_id IN (SELECT id FROM tmp_remove_cards);
-- 7. 删除卡本体(最后执行)
DELETE FROM tb_iot_card
WHERE id IN (SELECT id FROM tmp_remove_cards);
COMMIT;
"""
def generate_redis_gen_sql(iccids: list[str], generated_at: str) -> str:
in_list = sql_in_list(iccids)
count = len(iccids)
return f"""\
-- ============================================================
-- 轮询队列清理命令生成 SQL
-- 生成时间: {generated_at}
-- 卡数量 : {count}
-- 执行方式:
-- psql ... -t -A -c "$(cat remove_cards_redis_gen.sql)" | redis-cli --pipe
-- 或:
-- psql ... -t -A -f remove_cards_redis_gen.sql | redis-cli
-- ============================================================
WITH cards AS (
SELECT id FROM tb_iot_card
WHERE iccid IN ({in_list})
),
task_types(task_type) AS (
VALUES
('polling:realname'),
('polling:carddata'),
('polling:package'),
('polling:protect'),
('polling:card_status')
)
SELECT format('ZREM polling:shard:%s:queue:%s %s', id % 16, task_type, id)
FROM cards CROSS JOIN task_types
UNION ALL
SELECT format('DEL polling:card:%s', id)
FROM cards
UNION ALL
SELECT format('LREM polling:manual:%s 0 %s', task_type, id)
FROM cards CROSS JOIN task_types
UNION ALL
SELECT format('SREM polling:manual:dedupe:%s %s', task_type, id)
FROM cards CROSS JOIN task_types;
"""
def main() -> int:
parser = argparse.ArgumentParser(description="迁移卡删除脚本:生成删除 SQL 和 Redis 清理命令")
parser.add_argument("--input", required=True, help="ICCID 列表 CSV 文件路径")
parser.add_argument("--output-dir", default="output", help="输出目录(默认 output")
args = parser.parse_args()
base = Path(__file__).resolve().parent
input_path = Path(args.input)
if not input_path.is_absolute():
input_path = base / input_path
output_dir = (base / args.output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
if not input_path.exists():
print(f"[remove] 错误: 找不到输入文件 {input_path}", file=sys.stderr)
return 1
iccids = load_iccids(input_path)
if not iccids:
print("[remove] 错误: CSV 中没有找到有效的 ICCID", file=sys.stderr)
return 1
print(f"[remove] 读取到 {len(iccids)} 个 ICCID开始生成 SQL...")
generated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
db_sql_path = output_dir / "remove_cards_db.sql"
redis_sql_path = output_dir / "remove_cards_redis_gen.sql"
db_sql_path.write_text(generate_db_sql(iccids, generated_at), encoding="utf-8")
redis_sql_path.write_text(generate_redis_gen_sql(iccids, generated_at), encoding="utf-8")
print(f"[remove] 生成完成:")
print(f" 数据库删除 SQL : {db_sql_path}")
print(f" Redis 清理 SQL : {redis_sql_path}")
print()
print("执行步骤:")
print(" 1. 先执行 Redis 清理(在删除卡数据之前,以便 SQL 还能查到卡 ID:")
print(f" psql <连接参数> -t -A -f {redis_sql_path} | redis-cli")
print(" 2. 再执行数据库删除:")
print(f" psql <连接参数> -1 -f {db_sql_path}")
return 0
if __name__ == "__main__":
sys.exit(main())