诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""P1-3 · 审计日志保留策略:purge_audit_logs。
|
||||
|
||||
`--days N` 保留窗口(默认 180);`--dry-run` 只报数不删;
|
||||
不传 `--yes` 不删(防手滑);`--batch SIZE` 分批 delete;
|
||||
`--export path.jsonl` 删除前先落 NDJSON 归档。
|
||||
"""
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "按保留窗口删除过期审计日志(默认保留 180 天)。"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("--days", type=int, default=180,
|
||||
help="保留最近 N 天(默认 180)")
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="只统计,不删除")
|
||||
parser.add_argument("--yes", action="store_true",
|
||||
help="确认执行删除(不传则不删)")
|
||||
parser.add_argument("--batch", type=int, default=1000,
|
||||
help="每批删除条数(默认 1000,避免长事务锁表)")
|
||||
parser.add_argument("--export", type=str, default="",
|
||||
help="删除前把待删记录导出为 NDJSON 到该路径")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
from datetime import timedelta
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from apps.core.models import AuditLog
|
||||
|
||||
days = options["days"]
|
||||
if days is None or days < 1:
|
||||
raise CommandError("--days 必须 >= 1")
|
||||
batch = options["batch"] or 1000
|
||||
if batch < 1:
|
||||
raise CommandError("--batch 必须 >= 1")
|
||||
|
||||
cutoff = timezone.now() - timedelta(days=days)
|
||||
base_qs = AuditLog.objects.filter(created_at__lt=cutoff)
|
||||
total = base_qs.count()
|
||||
self.stdout.write(f"保留窗口:{days} 天(早于 {cutoff.isoformat()} 的 {total} 条待处理)")
|
||||
|
||||
export_path = options["export"]
|
||||
if export_path and total:
|
||||
import json
|
||||
|
||||
count = 0
|
||||
with open(export_path, "w", encoding="utf-8") as fh:
|
||||
for row in base_qs.order_by("id").iterator(chunk_size=batch):
|
||||
fh.write(json.dumps({
|
||||
"id": row.id,
|
||||
"tenant_id": row.tenant_id,
|
||||
"user_id": row.user_id,
|
||||
"action": row.action,
|
||||
"target_type": row.target_type,
|
||||
"target_id": row.target_id,
|
||||
"detail": row.detail,
|
||||
"ip": str(row.ip) if row.ip else None,
|
||||
"user_agent": row.user_agent,
|
||||
"created_at": row.created_at.isoformat(),
|
||||
}, ensure_ascii=False) + "\n")
|
||||
count += 1
|
||||
self.stdout.write(f"已归档 {count} 条 → {export_path}")
|
||||
|
||||
if options["dry_run"]:
|
||||
self.stdout.write("dry-run:未删除任何记录。")
|
||||
return f"dry-run: {total} matched"
|
||||
if not options["yes"]:
|
||||
self.stdout.write("未传 --yes:未删除任何记录(防手滑)。")
|
||||
return f"needs --yes: {total} matched"
|
||||
|
||||
deleted = 0
|
||||
while True:
|
||||
ids = list(
|
||||
AuditLog.objects.filter(created_at__lt=cutoff)
|
||||
.order_by("id")
|
||||
.values_list("id", flat=True)[:batch]
|
||||
)
|
||||
if not ids:
|
||||
break
|
||||
n, _ = AuditLog.objects.filter(pk__in=ids).delete()
|
||||
deleted += n
|
||||
self.stdout.write(self.style.SUCCESS(f"已删除 {deleted} 条过期审计日志。"))
|
||||
return f"deleted: {deleted}"
|
||||
@@ -21,7 +21,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
|
||||
from apps.core.models import Org, Tenant
|
||||
from apps.core.models import Org, Tenant, TenantMembership
|
||||
from apps.catalog.models import Brand, Category, Product, Unit, UnitConversion
|
||||
from apps.partner.models import Customer, Supplier
|
||||
from apps.inventory.models import Warehouse
|
||||
@@ -131,6 +131,12 @@ class Command(BaseCommand):
|
||||
if created or not user.check_password(DEMO_PASSWORD):
|
||||
user.set_password(DEMO_PASSWORD)
|
||||
user.save()
|
||||
# The demo token flow authenticates this account, and the membership
|
||||
# permission requires an explicit relation for it.
|
||||
TenantMembership.objects.get_or_create(
|
||||
user=user, tenant=tenant,
|
||||
defaults={"role": "owner", "is_active": True},
|
||||
)
|
||||
self.stdout.write(f" {'✓ 创建' if created else '· 复用'} 演示账号 {DEMO_USER}")
|
||||
return user
|
||||
|
||||
|
||||
Reference in New Issue
Block a user