This commit is contained in:
186
scripts/migration/remove_cards.py
Normal file
186
scripts/migration/remove_cards.py
Normal file
@@ -0,0 +1,186 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user