#!/usr/bin/env python3 """卡删除脚本:读取 ICCID 列表,生成数据库删除 SQL 和 Redis 轮询清理命令。 用法: python3 remove_cards.py --input [--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())