Files
pyGoEdge-UserPanel/admin_panel/views.py

762 lines
32 KiB
Python
Raw Normal View History

2025-11-18 03:36:49 +08:00
from django.shortcuts import render, redirect
import json
import logging
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from django.contrib import messages
from django.db import models
from django.db.models import Count, Sum
from django.views.decorators.http import require_POST
from django.contrib.auth import get_user_model
from django.db import transaction
from django.urls import reverse
from core.models import SystemSettings
from plans.models import Plan
from domains.models import Domain
from billing.models import Invoice
from .forms import SystemSettingsForm, PlanForm, DomainPlanSwitchForm, DomainGrantTrafficForm, InvoiceAdjustmentForm
from billing.models import Invoice, InvoiceItem
from decimal import Decimal
from django.utils import timezone
from core.goedge_client import GoEdgeClient
from core.models import OperationLog
def _staff_only(request):
if not request.user.is_authenticated:
return False
return request.user.is_staff
@login_required
def dashboard(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
users_count = request.user.__class__.objects.count()
domains_count = Domain.objects.count()
plans_count = Plan.objects.count()
unpaid_invoices = Invoice.objects.filter(status='unpaid').count() if hasattr(Invoice, 'status') else 0
return render(request, 'admin_panel/dashboard.html', {
'users_count': users_count,
'domains_count': domains_count,
'plans_count': plans_count,
'unpaid_invoices': unpaid_invoices,
})
@login_required
def operation_logs(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
q_actor = request.GET.get('actor', '').strip()
q_action = request.GET.get('action', '').strip()
q_target = request.GET.get('target', '').strip()
start = request.GET.get('start') or ''
end = request.GET.get('end') or ''
export = request.GET.get('export') or ''
qs = OperationLog.objects.select_related('actor').all()
if q_actor:
qs = qs.filter(actor__username__icontains=q_actor)
if q_action:
qs = qs.filter(action__icontains=q_action)
if q_target:
qs = qs.filter(target__icontains=q_target)
if start:
try:
from django.utils.dateparse import parse_datetime, parse_date
dt = parse_datetime(start) or parse_date(start)
if dt:
qs = qs.filter(created_at__gte=dt)
except Exception:
pass
if end:
try:
from django.utils.dateparse import parse_datetime, parse_date
dt = parse_datetime(end) or parse_date(end)
if dt:
qs = qs.filter(created_at__lte=dt)
except Exception:
pass
qs = qs.order_by('-created_at')
if export == 'csv':
import csv
from django.http import HttpResponse
resp = HttpResponse(content_type='text/csv; charset=utf-8')
resp['Content-Disposition'] = 'attachment; filename="operation_logs.csv"'
writer = csv.writer(resp)
writer.writerow(['时间', '操作人', '动作', '目标', '详情'])
for row in qs[:5000]:
writer.writerow([
row.created_at, (row.actor.username if row.actor else ''), row.action, row.target, row.detail,
])
return resp
from django.core.paginator import Paginator
paginator = Paginator(qs, 50)
page = request.GET.get('page') or 1
try:
page_obj = paginator.get_page(page)
except Exception:
page_obj = paginator.get_page(1)
return render(request, 'admin_panel/operation_logs.html', {
'page_obj': page_obj,
'actor': q_actor,
'action': q_action,
'target': q_target,
'start': start,
'end': end,
})
@login_required
def settings(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
settings_obj = SystemSettings.objects.order_by('id').first()
if not settings_obj:
settings_obj = SystemSettings.objects.create()
if request.method == 'POST':
action = request.POST.get('action') or 'save'
if action == 'save':
form = SystemSettingsForm(request.POST, instance=settings_obj)
if form.is_valid():
form.save()
messages.success(request, '系统设置已保存')
return redirect('admin_panel:settings')
else:
messages.error(request, '保存失败,请检查表单字段')
elif action == 'get_token':
form = SystemSettingsForm(instance=settings_obj)
try:
client = GoEdgeClient()
token = client._ensure_token() # 触发获取/续期
messages.success(request, f'已获取 AccessToken已缓存{token[:8]}...')
except Exception as e:
messages.error(request, f'获取 AccessToken 失败:{e}')
elif action == 'test_connection':
form = SystemSettingsForm(instance=settings_obj)
try:
client = GoEdgeClient() # 构造时即验证 base_url
_ = client._headers() # 触发令牌确保
messages.success(request, '与 GoEdge API 连接正常(令牌有效)')
except Exception as e:
messages.error(request, f'连接测试失败:{e}')
elif action == 'validate_access_log_policy':
form = SystemSettingsForm(request.POST, instance=settings_obj)
if form.is_valid():
pid = form.cleaned_data.get('default_http_access_log_policy_id')
if not pid:
messages.error(request, '请先填写访问日志策略ID')
else:
try:
client = GoEdgeClient()
exists = client.check_access_log_policy_exists(int(pid))
messages.success(request, f'访问日志策略ID {pid} 校验结果:{"存在" if exists else "不存在"}')
# 操作日志
try:
OperationLog.objects.create(
actor=request.user,
action='validate_policy_id',
target='SystemSettings',
detail=json.dumps({'type': 'access_log', 'policyId': int(pid), 'exists': bool(exists)}, ensure_ascii=False)
)
except Exception:
logger.exception('operation log write failed')
except Exception as e:
messages.error(request, f'访问日志策略校验失败:{e}')
else:
messages.error(request, '策略ID格式不正确请检查输入')
elif action == 'validate_firewall_policy':
form = SystemSettingsForm(request.POST, instance=settings_obj)
if form.is_valid():
pid = form.cleaned_data.get('default_http_firewall_policy_id')
if not pid:
messages.error(request, '请先填写WAF策略ID')
else:
try:
client = GoEdgeClient()
exists = client.check_firewall_policy_exists(int(pid))
messages.success(request, f'WAF策略ID {pid} 校验结果:{"存在" if exists else "不存在"}')
# 操作日志
try:
OperationLog.objects.create(
actor=request.user,
action='validate_policy_id',
target='SystemSettings',
detail=json.dumps({'type': 'firewall', 'policyId': int(pid), 'exists': bool(exists)}, ensure_ascii=False)
)
except Exception:
logger.exception('operation log write failed')
except Exception as e:
messages.error(request, f'WAF策略校验失败{e}')
else:
messages.error(request, '策略ID格式不正确请检查输入')
else:
form = SystemSettingsForm(instance=settings_obj)
messages.error(request, '未知操作')
else:
form = SystemSettingsForm(instance=settings_obj)
return render(request, 'admin_panel/settings.html', {
'form': form,
'settings_obj': settings_obj,
})
@login_required
def plans_list(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
plans = Plan.objects.all().order_by('-is_active', 'base_price_per_domain')
return render(request, 'admin_panel/plans_list.html', {
'plans': plans,
})
@login_required
def plan_create(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
if request.method == 'POST':
form = PlanForm(request.POST)
if form.is_valid():
plan = form.save()
messages.success(request, f'套餐已创建:{plan.name}')
return redirect('admin_panel:plans')
else:
messages.error(request, '创建失败,请检查表单字段')
else:
form = PlanForm(initial={'billing_mode': 'per_domain_monthly'})
return render(request, 'admin_panel/plan_form.html', {
'form': form,
'is_edit': False,
})
@login_required
def plan_edit(request, plan_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
plan = Plan.objects.filter(id=plan_id).first()
if not plan:
messages.error(request, '套餐不存在')
return redirect('admin_panel:plans')
if request.method == 'POST':
form = PlanForm(request.POST, instance=plan)
if form.is_valid():
form.save()
messages.success(request, f'套餐已更新:{plan.name}')
return redirect('admin_panel:plans')
else:
messages.error(request, '更新失败,请检查表单字段')
else:
form = PlanForm(instance=plan)
return render(request, 'admin_panel/plan_form.html', {
'form': form,
'is_edit': True,
'plan': plan,
})
@login_required
@require_POST
def plan_toggle_active(request, plan_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
plan = Plan.objects.filter(id=plan_id).first()
if not plan:
messages.error(request, '套餐不存在')
return redirect('admin_panel:plans')
plan.is_active = not plan.is_active
plan.save(update_fields=['is_active', 'updated_at'])
messages.success(request, f'{"启用" if plan.is_active else "禁用"}套餐:{plan.name}')
return redirect('admin_panel:plans')
@login_required
@require_POST
def plan_toggle_public(request, plan_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
plan = Plan.objects.filter(id=plan_id).first()
if not plan:
messages.error(request, '套餐不存在')
return redirect('admin_panel:plans')
plan.is_public = not plan.is_public
plan.save(update_fields=['is_public', 'updated_at'])
messages.success(request, f'已设置公开状态({"公开" if plan.is_public else "隐藏"}{plan.name}')
return redirect('admin_panel:plans')
@login_required
@require_POST
def plan_toggle_allow_new(request, plan_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
plan = Plan.objects.filter(id=plan_id).first()
if not plan:
messages.error(request, '套餐不存在')
return redirect('admin_panel:plans')
plan.allow_new_purchase = not plan.allow_new_purchase
plan.save(update_fields=['allow_new_purchase', 'updated_at'])
messages.success(request, f'已切换允许新购:{plan.name}')
return redirect('admin_panel:plans')
@login_required
@require_POST
def plan_toggle_allow_renew(request, plan_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
plan = Plan.objects.filter(id=plan_id).first()
if not plan:
messages.error(request, '套餐不存在')
return redirect('admin_panel:plans')
plan.allow_renew = not plan.allow_renew
plan.save(update_fields=['allow_renew', 'updated_at'])
messages.success(request, f'已切换允许续费:{plan.name}')
return redirect('admin_panel:plans')
# Users management
@login_required
def users_list(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
User = get_user_model()
q = request.GET.get('q', '').strip()
users_qs = User.objects.all()
if q:
users_qs = users_qs.filter(models.Q(username__icontains=q) | models.Q(email__icontains=q))
users = users_qs.annotate(domain_count=Count('domains')).order_by('-is_staff', '-domain_count', '-date_joined')
return render(request, 'admin_panel/users_list.html', {
'users': users,
'q': q,
})
# Domains management
@login_required
def domains_list(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
q = request.GET.get('q', '').strip()
user_id = request.GET.get('user_id')
status = request.GET.get('status')
domains = Domain.objects.select_related('user', 'current_plan').all()
if q:
domains = domains.filter(name__icontains=q)
if user_id:
domains = domains.filter(user_id=user_id)
if status:
domains = domains.filter(status=status)
domains = domains.order_by('-updated_at')
# 汇总用于筛选的状态列表
status_choices = Domain.STATUS_CHOICES
return render(request, 'admin_panel/domains_list.html', {
'domains': domains,
'q': q,
'user_id': user_id,
'status': status,
'status_choices': status_choices,
})
@login_required
@require_POST
@transaction.atomic
def domain_toggle_suspend(request, domain_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
domain = Domain.objects.filter(id=domain_id).first()
if not domain:
messages.error(request, '域名不存在')
return redirect('admin_panel:domains')
if (request.POST.get('confirm') or '').strip().upper() != 'CONFIRM':
messages.error(request, '请在确认框中输入 CONFIRM 以继续')
return redirect('admin_panel:domains')
if domain.status == Domain.STATUS_ACTIVE:
domain.status = Domain.STATUS_SUSPENDED
elif domain.status == Domain.STATUS_SUSPENDED:
domain.status = Domain.STATUS_ACTIVE
else:
messages.warning(request, '当前状态不支持暂停/恢复切换')
return redirect('admin_panel:domains')
domain.save(update_fields=['status', 'updated_at'])
try:
OperationLog.objects.create(
actor=request.user,
action='domain_toggle_suspend',
target=domain.name,
detail=f"new_status={domain.status}"
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, f'域名状态已切换为:{dict(Domain.STATUS_CHOICES).get(domain.status, domain.status)}')
return redirect('admin_panel:domains')
@login_required
@transaction.atomic
def domain_switch_plan(request, domain_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
domain = Domain.objects.filter(id=domain_id).select_related('current_plan').first()
if not domain:
messages.error(request, '域名不存在')
return redirect('admin_panel:domains')
if request.method == 'POST':
form = DomainPlanSwitchForm(request.POST, instance=domain)
if form.is_valid():
old_plan = domain.current_plan
form.save()
try:
OperationLog.objects.create(
actor=request.user,
action='domain_switch_plan',
target=domain.name,
detail=f"{old_plan.name if old_plan else '-'}{domain.current_plan.name if domain.current_plan else '-'}"
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, f'已切换套餐:{old_plan.name if old_plan else "-"}{domain.current_plan.name if domain.current_plan else "-"}')
return redirect('admin_panel:domains')
else:
messages.error(request, '提交失败,请检查表单字段')
else:
form = DomainPlanSwitchForm(instance=domain)
return render(request, 'admin_panel/domain_plan_switch.html', {
'domain': domain,
'form': form,
})
# Domain: grant extra free traffic for current cycle
@login_required
@transaction.atomic
def domain_grant_traffic(request, domain_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
domain = Domain.objects.filter(id=domain_id).first()
if not domain:
messages.error(request, '域名不存在')
return redirect('admin_panel:domains')
if request.method == 'POST':
form = DomainGrantTrafficForm(request.POST, instance=domain)
if form.is_valid():
form.save()
try:
OperationLog.objects.create(
actor=request.user,
action='domain_grant_traffic',
target=domain.name,
detail=f"extra_free_traffic_gb_current_cycle={domain.extra_free_traffic_gb_current_cycle}"
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, f'已更新本周期额外赠送流量GB{domain.extra_free_traffic_gb_current_cycle}')
return redirect('admin_panel:domains')
else:
messages.error(request, '提交失败,请检查表单字段')
else:
form = DomainGrantTrafficForm(instance=domain)
return render(request, 'admin_panel/domain_grant_traffic.html', {
'domain': domain,
'form': form,
})
@login_required
@require_POST
@transaction.atomic
def domain_delete(request, domain_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
domain = Domain.objects.filter(id=domain_id).first()
if not domain:
messages.error(request, '域名不存在')
return redirect('admin_panel:domains')
if (request.POST.get('confirm') or '').strip().upper() != 'DELETE':
messages.error(request, '请在确认框中输入 DELETE 以继续')
return redirect('admin_panel:domains')
domain.status = Domain.STATUS_DELETED if hasattr(Domain, 'STATUS_DELETED') else 'deleted'
domain.save(update_fields=['status', 'updated_at'])
try:
OperationLog.objects.create(
actor=request.user,
action='domain_delete',
target=domain.name,
detail='soft-delete'
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, '域名已标记为删除')
return redirect('admin_panel:domains')
# Billing management (admin)
@login_required
def invoices_list(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
status = request.GET.get('status')
user_id = request.GET.get('user_id')
qs = Invoice.objects.select_related('user').all()
if status:
qs = qs.filter(status=status)
if user_id:
qs = qs.filter(user_id=user_id)
qs = qs.order_by('-period_end')
totals = qs.aggregate(total_amount=Sum('amount_total'))
return render(request, 'admin_panel/invoices_list.html', {
'invoices': qs,
'status': status,
'user_id': user_id,
'total_amount': totals.get('total_amount') or Decimal('0.00'),
})
@login_required
@transaction.atomic
def invoice_detail_admin(request, invoice_id: int):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
invoice = Invoice.objects.select_related('user').filter(id=invoice_id).first()
if not invoice:
messages.error(request, '账单不存在')
return redirect('admin_panel:billing_list')
adj_form = InvoiceAdjustmentForm()
if request.method == 'POST':
action = request.POST.get('action')
if action == 'mark_paid' and invoice.status == Invoice.STATUS_UNPAID:
invoice.status = Invoice.STATUS_PAID
invoice.paid_at = timezone.now()
invoice.save(update_fields=['status', 'paid_at'])
try:
OperationLog.objects.create(
actor=request.user,
action='invoice_mark_paid_admin',
target=f"Invoice#{invoice.id}",
detail=f"period={invoice.period_start}{invoice.period_end}; amount_total={invoice.amount_total}"
)
except Exception:
logger.exception('operation log write failed')
try:
client = GoEdgeClient()
for it in invoice.items.select_related('domain').all():
if not it.domain or not it.domain.edge_server_id:
continue
web_id = client.find_server_web_id(int(it.domain.edge_server_id))
if web_id:
# 关闭停服与限速
client.update_http_web_shutdown(web_id, {'isOn': False})
client.update_http_web_request_limit(web_id, {'isOn': False})
# 如域名被暂停,恢复为 active
if it.domain.status == 'suspended':
it.domain.status = 'active'
it.domain.save(update_fields=['status', 'updated_at'])
except Exception:
logger.exception('auto recover after paid failed')
messages.success(request, '账单已标记为已支付')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
elif action == 'add_adjustment':
if (request.POST.get('confirm') or '').strip().upper() != 'CONFIRM':
messages.error(request, '请在确认框中输入 CONFIRM 以继续')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
adj_form = InvoiceAdjustmentForm(request.POST)
if adj_form.is_valid():
adj_form.apply(invoice)
try:
OperationLog.objects.create(
actor=request.user,
action='invoice_add_adjustment',
target=f"Invoice#{invoice.id}",
detail=f"adjustment={invoice.amount_adjustment}; total={invoice.amount_total}"
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, '已添加人工调整并更新总金额')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
else:
messages.error(request, '调整提交失败,请检查表单字段')
elif action == 'cancel' and invoice.status != Invoice.STATUS_CANCELLED:
if (request.POST.get('confirm') or '').strip().upper() != 'CONFIRM':
messages.error(request, '请在确认框中输入 CONFIRM 以继续')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
invoice.status = Invoice.STATUS_CANCELLED
invoice.save(update_fields=['status'])
try:
OperationLog.objects.create(
actor=request.user,
action='invoice_cancel',
target=f"Invoice#{invoice.id}",
detail=f"period={invoice.period_start}{invoice.period_end}"
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, '账单已取消')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
elif action == 'apply_overage_policy' and invoice.status == Invoice.STATUS_UNPAID:
if (request.POST.get('confirm') or '').strip().upper() != 'CONFIRM':
messages.error(request, '请在确认框中输入 CONFIRM 以继续')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
sys = SystemSettings.objects.order_by('id').first()
policy = getattr(sys, 'default_overage_policy', {}) or {}
action_type = (policy.get('action') or '').lower()
limit_bps = int(policy.get('limit_bps') or 0)
applied = []
try:
client = GoEdgeClient()
for it in invoice.items.select_related('domain').all():
d = it.domain
if not d or not d.edge_server_id:
continue
web_id = client.find_server_web_id(int(d.edge_server_id))
if not web_id:
continue
if action_type == 'limit' and limit_bps > 0:
# 最小限速配置(简化形态):开启限速并设定速率
client.update_http_web_request_limit(web_id, {'isOn': True, 'rateBytes': int(limit_bps)})
applied.append({'domain': d.name, 'action': 'limit', 'rateBytes': limit_bps})
else:
# 默认执行停服
client.update_http_web_shutdown(web_id, {'isOn': True})
# 域名状态置为暂停
if d.status != 'suspended':
d.status = 'suspended'
d.save(update_fields=['status', 'updated_at'])
applied.append({'domain': d.name, 'action': 'shutdown'})
try:
OperationLog.objects.create(
actor=request.user,
action='apply_overage_policy',
target=f"Invoice#{invoice.id}",
detail=json.dumps({'applied': applied}, ensure_ascii=False)
)
except Exception:
logger.exception('operation log write failed')
messages.success(request, '已执行未支付策略(参考系统默认策略配置)')
except Exception as e:
messages.error(request, f'策略执行失败:{e}')
return redirect('admin_panel:billing_detail', invoice_id=invoice.id)
items = invoice.items.select_related('domain').all()
return render(request, 'admin_panel/invoice_detail.html', {
'invoice': invoice,
'items': items,
'adj_form': adj_form,
})
# Quotas view: show composed quotas (global/plan/user override/domain extra)
@login_required
def quotas_view(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
user_id = request.GET.get('user_id')
domains = Domain.objects.select_related('user', 'current_plan').all()
if user_id:
domains = domains.filter(user_id=user_id)
sys = SystemSettings.objects.order_by('id').first()
rows = []
for d in domains:
plan_quota = int(getattr(d.current_plan, 'included_traffic_gb_per_domain', 0) or 0)
user_override = 0
profile = getattr(d.user, 'profile', None)
if profile and profile.default_free_traffic_gb_per_domain_override is not None:
user_override = int(profile.default_free_traffic_gb_per_domain_override)
global_default = int(sys.default_free_traffic_gb_per_domain if sys else 0)
base_quota = plan_quota if plan_quota > 0 else (user_override if user_override > 0 else global_default)
domain_extra = int(d.extra_free_traffic_gb_current_cycle or 0)
total_quota = base_quota + domain_extra
rows.append({
'domain': d,
'plan_quota': plan_quota,
'user_override': user_override,
'global_default': global_default,
'domain_extra': domain_extra,
'total_quota': total_quota,
})
rows = sorted(rows, key=lambda r: (-(r['total_quota']), r['domain'].name))
return render(request, 'admin_panel/quotas.html', {
'rows': rows,
'user_id': user_id,
})
# Monitoring: platform daily totals and top-N domains
@login_required
def monitoring_view(request):
if not _staff_only(request):
return HttpResponseForbidden('需要运营权限staff')
from domains.models import DomainTrafficDaily
today = timezone.now().date()
days = int(request.GET.get('days') or 14)
start = today - timezone.timedelta(days=days-1)
sys = SystemSettings.objects.order_by('id').first()
anomaly_enabled = bool(getattr(sys, 'anomaly_detection_enabled', True))
threshold_mult = float(getattr(sys, 'anomaly_threshold_multiplier', 3.0) or 3.0)
window_days = int(getattr(sys, 'anomaly_window_days', 7) or 7)
min_gb = float(getattr(sys, 'anomaly_min_gb', 1.0) or 1.0)
# 平台每日总流量
daily = (DomainTrafficDaily.objects
.filter(day__gte=start, day__lte=today)
.values('day')
.annotate(total_bytes=Sum('bytes'))
.order_by('day'))
daily_rows = []
for row in daily:
gb = round((row['total_bytes'] or 0) / (1024 ** 3), 3)
daily_rows.append({'day': row['day'], 'gb': gb})
# Top N 域名(本月总量)
month_start = today.replace(day=1)
domains = Domain.objects.all()
top = []
anomalies = []
for d in domains:
bytes_sum = DomainTrafficDaily.objects.filter(domain=d, day__gte=month_start, day__lte=today).aggregate(b=Sum('bytes'))['b'] or 0
gb = round(bytes_sum / (1024 ** 3), 3)
top.append({'domain': d, 'gb': gb})
# 异常检测:今日 vs 过去窗口均值
if anomaly_enabled:
window_start = today - timezone.timedelta(days=window_days)
past = (DomainTrafficDaily.objects
.filter(domain=d, day__gte=window_start, day__lt=today)
.aggregate(b=Sum('bytes'))['b'] or 0)
past_days_count = DomainTrafficDaily.objects.filter(domain=d, day__gte=window_start, day__lt=today).count()
past_avg_gb = 0.0
if past_days_count > 0:
past_avg_gb = round((past / (1024 ** 3)) / past_days_count, 3)
today_bytes = DomainTrafficDaily.objects.filter(domain=d, day=today).aggregate(b=Sum('bytes'))['b'] or 0
today_gb = round(today_bytes / (1024 ** 3), 3)
trigger = False
if past_avg_gb > 0:
trigger = today_gb >= max(min_gb, past_avg_gb * threshold_mult)
else:
trigger = today_gb >= min_gb
if trigger and today_gb > 0:
anomalies.append({
'domain': d,
'today_gb': today_gb,
'past_avg_gb': past_avg_gb,
'ratio': round((today_gb / past_avg_gb), 2) if past_avg_gb > 0 else None,
})
top_sorted = sorted(top, key=lambda x: -x['gb'])[:20]
return render(request, 'admin_panel/monitoring.html', {
'daily_rows': daily_rows,
'top_domains': top_sorted,
'days': days,
'anomalies': anomalies,
'anomaly_config': {
'enabled': anomaly_enabled,
'multiplier': threshold_mult,
'window_days': window_days,
'min_gb': min_gb,
},
})
logger = logging.getLogger(__name__)