Initial commit
This commit is contained in:
762
admin_panel/views.py
Normal file
762
admin_panel/views.py
Normal file
@@ -0,0 +1,762 @@
|
||||
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__)
|
||||
Reference in New Issue
Block a user