fix: align retention cleanup across notifications logs and share telemetry

This commit is contained in:
Hermes Agent
2026-06-18 15:20:16 +08:00
parent 9a8ad91216
commit a82c5b3cca
7 changed files with 287 additions and 18 deletions

View File

@@ -125,6 +125,11 @@ class OrderDeletionService:
"UPDATE order_intakes SET payload_json='{}', updated_at=? WHERE order_id=?",
(now, order_id),
)
if self._table_exists("delivery_notifications"):
self._conn.execute(
"UPDATE delivery_notifications SET payload_json='{}' WHERE order_id=?",
(order_id,),
)
self._insert_audit(
order_id=order_id,
action="anonymize",

View File

@@ -4,6 +4,7 @@ import argparse
import json
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from data.orders.dao import OrdersDAO
from data.orders.deletion_service import OrderDeletionService
@@ -16,6 +17,8 @@ class RetentionCleanupResult:
scanned: int = 0
candidates: int = 0
anonymized: int = 0
deletion_logs_pruned: int = 0
share_events_pruned: int = 0
_TERMINAL_STATUSES = {"completed", "refunded"}
@@ -55,6 +58,54 @@ def _iter_candidates(db_path: str, cutoff_iso: str) -> tuple[int, list[str]]:
return scanned, candidates
def _prune_deletion_request_log(log_path: str, candidate_ids: list[str]) -> int:
path = Path(log_path)
if not path.exists() or not candidate_ids:
return 0
keep: list[str] = []
pruned = 0
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip():
continue
try:
item = json.loads(line)
except json.JSONDecodeError:
keep.append(line)
continue
if str(item.get("order_id") or "") in candidate_ids:
pruned += 1
continue
keep.append(json.dumps(item, ensure_ascii=False))
path.write_text("\n".join(keep) + ("\n" if keep else ""), encoding="utf-8")
return pruned
def _prune_share_access_events(share_db_path: str, candidate_ids: list[str]) -> int:
if not candidate_ids:
return 0
from data.share.short_link import ShortLinkService
service = ShortLinkService(db_path=share_db_path)
with service._connect() as conn:
rows = conn.execute(
"SELECT code FROM share_links WHERE report_id IN ({})".format(
",".join("?" for _ in candidate_ids)
),
tuple(candidate_ids),
).fetchall()
codes = [str(row[0]) for row in rows]
if not codes:
return 0
conn.execute(
"DELETE FROM share_link_access_events WHERE code IN ({})".format(
",".join("?" for _ in codes)
),
tuple(codes),
)
conn.commit()
return int(conn.total_changes)
def resolve_cutoff_iso(
*, cutoff_iso: str | None = None, retention_days: int | None = None
) -> str:
@@ -68,8 +119,24 @@ def resolve_cutoff_iso(
def run_cleanup(
db_path: str, *, cutoff_iso: str, apply: bool
db_path: str,
*,
cutoff_iso: str,
apply: bool,
deletion_request_log_path: str | None = None,
share_db_path: str | None = None,
) -> RetentionCleanupResult:
if deletion_request_log_path is None or share_db_path is None:
try:
from admin.config import load_settings
settings = load_settings()
except Exception:
settings = None
if deletion_request_log_path is None and settings is not None:
deletion_request_log_path = settings.deletion_request_log_path
if share_db_path is None and settings is not None:
share_db_path = settings.share_db_path
scanned, candidate_ids = _iter_candidates(db_path, cutoff_iso)
result = RetentionCleanupResult(
cutoff_iso=cutoff_iso,
@@ -88,6 +155,14 @@ def run_cleanup(
result.anonymized += 1
finally:
service.close()
if deletion_request_log_path:
result.deletion_logs_pruned = _prune_deletion_request_log(
deletion_request_log_path, candidate_ids
)
if share_db_path:
result.share_events_pruned = _prune_share_access_events(
share_db_path, candidate_ids
)
return result
@@ -110,10 +185,11 @@ def build_parser() -> argparse.ArgumentParser:
def main(argv: list[str] | None = None, *, db_path: str | None = None) -> int:
if db_path is None:
from admin.config import load_settings
from admin.config import load_settings
db_path = load_settings().orders_db_path
settings = load_settings()
if db_path is None:
db_path = settings.orders_db_path
parser = build_parser()
args = parser.parse_args(argv)
try:
@@ -123,7 +199,13 @@ def main(argv: list[str] | None = None, *, db_path: str | None = None) -> int:
)
except ValueError as exc:
parser.error(str(exc))
result = run_cleanup(db_path, cutoff_iso=cutoff_iso, apply=not args.dry_run)
result = run_cleanup(
db_path,
cutoff_iso=cutoff_iso,
apply=not args.dry_run,
deletion_request_log_path=getattr(settings, "deletion_request_log_path", None),
share_db_path=getattr(settings, "share_db_path", None),
)
print(json.dumps(result.__dict__, ensure_ascii=False, indent=2))
return 0