删除
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 53s

This commit is contained in:
2026-07-11 12:28:38 +09:00
parent 31232ea899
commit 026d4908d8

View 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())