Initial commit
This commit is contained in:
0
domains/__init__.py
Normal file
0
domains/__init__.py
Normal file
BIN
domains/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
domains/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/admin.cpython-312.pyc
Normal file
BIN
domains/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/apps.cpython-312.pyc
Normal file
BIN
domains/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/forms.cpython-312.pyc
Normal file
BIN
domains/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/models.cpython-312.pyc
Normal file
BIN
domains/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/tests.cpython-312.pyc
Normal file
BIN
domains/__pycache__/tests.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/urls.cpython-312.pyc
Normal file
BIN
domains/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/__pycache__/views.cpython-312.pyc
Normal file
BIN
domains/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
19
domains/admin.py
Normal file
19
domains/admin.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.contrib import admin
|
||||
from .models import Domain, DomainTrafficDaily
|
||||
|
||||
|
||||
@admin.register(Domain)
|
||||
class DomainAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'name', 'user', 'status', 'current_plan', 'edge_server_id', 'updated_at'
|
||||
)
|
||||
list_filter = ('status', 'current_plan')
|
||||
search_fields = ('name', 'user__username')
|
||||
|
||||
|
||||
@admin.register(DomainTrafficDaily)
|
||||
class DomainTrafficDailyAdmin(admin.ModelAdmin):
|
||||
list_display = ('domain', 'day', 'bytes', 'peak_bandwidth_mbps')
|
||||
list_filter = ('day',)
|
||||
|
||||
# Register your models here.
|
||||
6
domains/apps.py
Normal file
6
domains/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DomainsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'domains'
|
||||
66
domains/forms.py
Normal file
66
domains/forms.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from django import forms
|
||||
from plans.models import Plan
|
||||
|
||||
|
||||
class AddDomainForm(forms.Form):
|
||||
name = forms.CharField(label='主域名', max_length=253)
|
||||
subdomains = forms.CharField(label='接入子域名(逗号分隔)', required=False, help_text='例如:www,static')
|
||||
origin_host = forms.CharField(label='源站地址(域名或IP)', max_length=253)
|
||||
origin_protocol = forms.ChoiceField(label='回源协议', choices=[('http', 'HTTP'), ('https', 'HTTPS')], initial='http')
|
||||
origin_port = forms.IntegerField(label='回源端口', initial=80, min_value=1, max_value=65535)
|
||||
plan = forms.ModelChoiceField(
|
||||
label='套餐',
|
||||
queryset=Plan.objects.filter(is_active=True, is_public=True, allow_new_purchase=True),
|
||||
required=False
|
||||
)
|
||||
enable_websocket = forms.BooleanField(label='启用WebSocket', required=False, initial=False)
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
# 规范化子域名列表
|
||||
subs_raw = cleaned.get('subdomains') or ''
|
||||
subs = [s.strip() for s in subs_raw.split(',') if s.strip()]
|
||||
cleaned['subdomains'] = subs
|
||||
return cleaned
|
||||
|
||||
|
||||
class DomainSettingsForm(forms.Form):
|
||||
waf_enabled = forms.BooleanField(label='启用 WAF', required=False)
|
||||
http3_enabled = forms.BooleanField(label='启用 HTTP/3', required=False)
|
||||
logs_enabled = forms.BooleanField(label='启用实时日志', required=False)
|
||||
websocket_enabled = forms.BooleanField(label='启用 WebSocket', required=False)
|
||||
redirect_https_enabled = forms.BooleanField(label='强制 HTTP→HTTPS 跳转', required=False)
|
||||
cache_rules_json = forms.CharField(label='缓存规则(JSON)', required=False, widget=forms.Textarea(attrs={'rows': 6}))
|
||||
ip_whitelist = forms.CharField(label='IP 白名单(逗号分隔)', required=False, widget=forms.Textarea(attrs={'rows': 3}))
|
||||
ip_blacklist = forms.CharField(label='IP 黑名单(逗号分隔)', required=False, widget=forms.Textarea(attrs={'rows': 3}))
|
||||
page_rules_json = forms.CharField(label='页面规则(JSON)', required=False, widget=forms.Textarea(attrs={'rows': 6}))
|
||||
|
||||
def clean_cache_rules_json(self):
|
||||
val = self.cleaned_data.get('cache_rules_json') or ''
|
||||
if not val.strip():
|
||||
return {}
|
||||
import json
|
||||
try:
|
||||
return json.loads(val)
|
||||
except Exception:
|
||||
raise forms.ValidationError('缓存规则需为合法 JSON')
|
||||
|
||||
def clean_page_rules_json(self):
|
||||
val = self.cleaned_data.get('page_rules_json') or ''
|
||||
if not val.strip():
|
||||
return {}
|
||||
import json
|
||||
try:
|
||||
return json.loads(val)
|
||||
except Exception:
|
||||
raise forms.ValidationError('页面规则需为合法 JSON')
|
||||
|
||||
def clean_ip_whitelist(self):
|
||||
val = self.cleaned_data.get('ip_whitelist') or ''
|
||||
ips = [s.strip() for s in val.split(',') if s.strip()]
|
||||
return ips
|
||||
|
||||
def clean_ip_blacklist(self):
|
||||
val = self.cleaned_data.get('ip_blacklist') or ''
|
||||
ips = [s.strip() for s in val.split(',') if s.strip()]
|
||||
return ips
|
||||
1
domains/management/__init__.py
Normal file
1
domains/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Django management package for domains app."""
|
||||
BIN
domains/management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
domains/management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
67
domains/management/commands/pull_daily_stats.py
Normal file
67
domains/management/commands/pull_daily_stats.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from domains.models import Domain, DomainTrafficDaily
|
||||
from core.goedge_client import GoEdgeClient
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '从 GoEdge 拉取每日流量统计并写入 DomainTrafficDaily(默认处理昨天的数据)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--days', type=int, default=1, help='拉取最近N天(默认1)')
|
||||
parser.add_argument('--server-id', type=int, default=0, help='仅处理指定的 serverId')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
days: int = options['days']
|
||||
server_id_filter: int = options['server_id']
|
||||
client = GoEdgeClient()
|
||||
|
||||
qs = Domain.objects.exclude(edge_server_id__isnull=True).exclude(edge_server_id__exact='')
|
||||
if server_id_filter:
|
||||
qs = qs.filter(edge_server_id=server_id_filter)
|
||||
|
||||
processed = 0
|
||||
for domain in qs:
|
||||
server_id = int(domain.edge_server_id)
|
||||
try:
|
||||
daily_stats = client.find_latest_server_daily_stats(server_id=server_id, days=days)
|
||||
bw_stats = client.find_daily_bandwidth_stats(server_id=server_id, days=days, algo='avg')
|
||||
except Exception as e:
|
||||
self.stderr.write(self.style.ERROR(f'域名 {domain.name} 拉取失败: {e}'))
|
||||
continue
|
||||
|
||||
# 将带宽按day映射
|
||||
bw_by_day = {}
|
||||
for s in bw_stats or []:
|
||||
day = s.get('day')
|
||||
bytes_per_sec = s.get('bytes') or 0
|
||||
# 转换为 Mbps:bytes/sec * 8 / 1_000_000
|
||||
mbps = (bytes_per_sec * 8) / 1_000_000 if bytes_per_sec else 0.0
|
||||
bw_by_day[day] = mbps
|
||||
|
||||
for s in daily_stats or []:
|
||||
day_str: Optional[str] = s.get('day') # YYYYMMDD
|
||||
if not day_str:
|
||||
continue
|
||||
try:
|
||||
day_date = datetime.datetime.strptime(day_str, '%Y%m%d').date()
|
||||
except Exception:
|
||||
continue
|
||||
bytes_used = int(s.get('bytes') or 0)
|
||||
peak_mbps = float(bw_by_day.get(day_str) or 0.0)
|
||||
|
||||
obj, created = DomainTrafficDaily.objects.update_or_create(
|
||||
domain=domain,
|
||||
day=day_date,
|
||||
defaults={
|
||||
'bytes': bytes_used,
|
||||
'peak_bandwidth_mbps': peak_mbps,
|
||||
}
|
||||
)
|
||||
processed += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'完成,写入/更新 {processed} 条日流量记录。'))
|
||||
74
domains/management/commands/pull_traffic.py
Normal file
74
domains/management/commands/pull_traffic.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import datetime
|
||||
from typing import Dict
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from domains.models import Domain, DomainTrafficDaily
|
||||
from core.goedge_client import GoEdgeClient
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '从 GoEdge 读取各域名(serverId)的最近每日流量并写入 DomainTrafficDaily'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--days', type=int, default=35, help='拉取最近N天数据(默认35)')
|
||||
parser.add_argument('--only-active', action='store_true', dest='only_active', default=False, help='仅拉取状态为active的域名')
|
||||
# 兼容文档中的别名参数
|
||||
parser.add_argument('--active-only', action='store_true', dest='only_active', help='参数别名,等同 --only-active')
|
||||
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
days: int = options['days']
|
||||
only_active: bool = options['only_active']
|
||||
|
||||
qs = Domain.objects.exclude(edge_server_id__isnull=True)
|
||||
if only_active:
|
||||
qs = qs.filter(status=Domain.STATUS_ACTIVE)
|
||||
|
||||
domains = list(qs)
|
||||
if not domains:
|
||||
self.stdout.write(self.style.WARNING('没有可拉取的域名(缺少 edge_server_id)。'))
|
||||
return
|
||||
|
||||
client = GoEdgeClient()
|
||||
total_written = 0
|
||||
|
||||
for d in domains:
|
||||
server_id = int(d.edge_server_id)
|
||||
try:
|
||||
stats = client.find_latest_server_daily_stats(server_id=server_id, days=days)
|
||||
except Exception as e:
|
||||
self.stderr.write(self.style.ERROR(f'[domain={d.name}] 拉取失败:{e}'))
|
||||
continue
|
||||
|
||||
# 解析并入库
|
||||
for s in stats:
|
||||
day_str = s.get('day')
|
||||
bytes_val = int(s.get('bytes') or 0)
|
||||
peak_mbps = float(s.get('peakBandwidthMbps') or 0.0)
|
||||
# 将 day 格式 YYYYMMDD 转为 date
|
||||
try:
|
||||
day_date = datetime.datetime.strptime(day_str, '%Y%m%d').date()
|
||||
except Exception:
|
||||
# 某些版本可能返回 YYYY-MM-DD
|
||||
try:
|
||||
day_date = datetime.datetime.strptime(day_str, '%Y-%m-%d').date()
|
||||
except Exception:
|
||||
self.stderr.write(self.style.WARNING(f'无法解析日期:{day_str}'))
|
||||
continue
|
||||
|
||||
obj, created = DomainTrafficDaily.objects.update_or_create(
|
||||
domain=d,
|
||||
day=day_date,
|
||||
defaults={
|
||||
'bytes': bytes_val,
|
||||
'peak_bandwidth_mbps': peak_mbps,
|
||||
}
|
||||
)
|
||||
total_written += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'[domain={d.name}] 写入 {len(stats)} 条'))
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f'完成,总写入记录:{total_written}'))
|
||||
73
domains/migrations/0001_initial.py
Normal file
73
domains/migrations/0001_initial.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Generated by Django 5.2.8 on 2025-11-07 07:39
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('plans', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=253, unique=True)),
|
||||
('status', models.CharField(choices=[('pending_dns', '等待DNS配置'), ('active', '正常'), ('suspended', '暂停'), ('deleted', '已删除')], default='pending_dns', max_length=20)),
|
||||
('current_cycle_start', models.DateField(blank=True, null=True)),
|
||||
('current_cycle_end', models.DateField(blank=True, null=True)),
|
||||
('cname_targets', models.JSONField(blank=True, default=dict)),
|
||||
('origin_config', models.JSONField(blank=True, default=dict)),
|
||||
('edge_server_id', models.BigIntegerField(blank=True, null=True)),
|
||||
('extra_free_traffic_gb_current_cycle', models.PositiveIntegerField(default=0)),
|
||||
('custom_overage_price_per_gb', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
|
||||
('custom_features', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('current_plan', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='plans.plan')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '域名',
|
||||
'verbose_name_plural': '域名',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DomainTrafficDaily',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('day', models.DateField()),
|
||||
('bytes', models.BigIntegerField(default=0)),
|
||||
('peak_bandwidth_mbps', models.FloatField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('domain', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='daily_traffic', to='domains.domain')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '域名日流量',
|
||||
'verbose_name_plural': '域名日流量',
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='domain',
|
||||
index=models.Index(fields=['user', 'name'], name='domains_dom_user_id_840f80_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='domain',
|
||||
index=models.Index(fields=['status'], name='domains_dom_status_e71c1c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='domaintrafficdaily',
|
||||
index=models.Index(fields=['domain', 'day'], name='domains_dom_domain__a38816_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='domaintrafficdaily',
|
||||
unique_together={('domain', 'day')},
|
||||
),
|
||||
]
|
||||
0
domains/migrations/__init__.py
Normal file
0
domains/migrations/__init__.py
Normal file
BIN
domains/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
domains/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
domains/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
domains/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
66
domains/models.py
Normal file
66
domains/models.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from plans.models import Plan
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Domain(models.Model):
|
||||
STATUS_PENDING = 'pending_dns'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_SUSPENDED = 'suspended'
|
||||
STATUS_DELETED = 'deleted'
|
||||
STATUS_CHOICES = [
|
||||
(STATUS_PENDING, '等待DNS配置'),
|
||||
(STATUS_ACTIVE, '正常'),
|
||||
(STATUS_SUSPENDED, '暂停'),
|
||||
(STATUS_DELETED, '已删除'),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='domains')
|
||||
name = models.CharField(max_length=253, unique=True)
|
||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
|
||||
current_plan = models.ForeignKey(Plan, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
current_cycle_start = models.DateField(null=True, blank=True)
|
||||
current_cycle_end = models.DateField(null=True, blank=True)
|
||||
cname_targets = models.JSONField(default=dict, blank=True)
|
||||
origin_config = models.JSONField(default=dict, blank=True)
|
||||
edge_server_id = models.BigIntegerField(null=True, blank=True)
|
||||
|
||||
# overrides (optional)
|
||||
extra_free_traffic_gb_current_cycle = models.PositiveIntegerField(default=0)
|
||||
custom_overage_price_per_gb = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
|
||||
custom_features = models.JSONField(default=dict, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '域名'
|
||||
verbose_name_plural = '域名'
|
||||
indexes = [
|
||||
models.Index(fields=['user', 'name']),
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class DomainTrafficDaily(models.Model):
|
||||
domain = models.ForeignKey(Domain, on_delete=models.CASCADE, related_name='daily_traffic')
|
||||
day = models.DateField()
|
||||
bytes = models.BigIntegerField(default=0)
|
||||
peak_bandwidth_mbps = models.FloatField(default=0)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '域名日流量'
|
||||
verbose_name_plural = '域名日流量'
|
||||
unique_together = ('domain', 'day')
|
||||
indexes = [
|
||||
models.Index(fields=['domain', 'day']),
|
||||
]
|
||||
|
||||
# Create your models here.
|
||||
31
domains/tests.py
Normal file
31
domains/tests.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from unittest.mock import patch
|
||||
|
||||
from domains.models import Domain
|
||||
from core.models import SystemSettings
|
||||
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class DNSCheckTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username='u1', email='u1@example.com', password='p')
|
||||
SystemSettings.objects.create(cname_template='{sub}.cdn.example.com')
|
||||
self.domain = Domain.objects.create(
|
||||
user=self.user,
|
||||
name='example.com',
|
||||
status=Domain.STATUS_PENDING,
|
||||
cname_targets={'www.example.com': 'www.cdn.example.com'},
|
||||
)
|
||||
|
||||
def test_dns_check_updates_status(self):
|
||||
c = Client()
|
||||
c.login(username='u1', password='p')
|
||||
with patch('core.utils.resolve_cname', return_value=['www.cdn.example.com']):
|
||||
resp = c.post(reverse('domains:check_dns', kwargs={'domain_id': self.domain.id}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.domain.refresh_from_db()
|
||||
self.assertEqual(self.domain.status, Domain.STATUS_ACTIVE)
|
||||
13
domains/urls.py
Normal file
13
domains/urls.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.list_domains, name='list'),
|
||||
path('add/', views.add_domain, name='add'),
|
||||
path('<int:domain_id>/', views.domain_detail, name='detail'),
|
||||
path('<int:domain_id>/check-dns/', views.check_dns, name='check_dns'),
|
||||
path('<int:domain_id>/upgrade/', views.upgrade_plan, name='upgrade'),
|
||||
path('<int:domain_id>/settings/', views.domain_settings, name='settings'),
|
||||
path('<int:domain_id>/logs/', views.domain_logs, name='logs'),
|
||||
]
|
||||
565
domains/views.py
Normal file
565
domains/views.py
Normal file
@@ -0,0 +1,565 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
import json
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib import messages
|
||||
from django.urls import reverse
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Domain, DomainTrafficDaily
|
||||
from .forms import AddDomainForm, DomainSettingsForm
|
||||
from core.goedge_client import GoEdgeClient
|
||||
from core.models import SystemSettings, OperationLog
|
||||
from plans.models import Plan
|
||||
from core.utils import check_cname_map, bytes_to_gb
|
||||
|
||||
|
||||
@login_required
|
||||
def list_domains(request):
|
||||
domains = Domain.objects.filter(user=request.user).order_by('-created_at')
|
||||
# 计算当月用量与配额
|
||||
today = timezone.now().date()
|
||||
month_start = today.replace(day=1)
|
||||
sys = SystemSettings.objects.order_by('id').first()
|
||||
for d in domains:
|
||||
traffic_qs = DomainTrafficDaily.objects.filter(domain=d, day__gte=month_start)
|
||||
total_bytes = sum(t.bytes for t in traffic_qs)
|
||||
used_gb = bytes_to_gb(total_bytes)
|
||||
base_quota_gb = 0
|
||||
if d.current_plan and d.current_plan.included_traffic_gb_per_domain:
|
||||
base_quota_gb = d.current_plan.included_traffic_gb_per_domain
|
||||
elif sys:
|
||||
base_quota_gb = sys.default_free_traffic_gb_per_domain
|
||||
total_quota_gb = (base_quota_gb or 0) + (d.extra_free_traffic_gb_current_cycle or 0)
|
||||
# 动态属性用于模板展示
|
||||
d.used_gb = used_gb
|
||||
d.total_quota_gb = total_quota_gb
|
||||
return render(request, 'domains/list.html', {
|
||||
'domains': domains,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@transaction.atomic
|
||||
def add_domain(request):
|
||||
if request.method == 'POST':
|
||||
form = AddDomainForm(request.POST)
|
||||
if form.is_valid():
|
||||
name = form.cleaned_data['name'].strip()
|
||||
subdomains = form.cleaned_data['subdomains']
|
||||
origin_host = form.cleaned_data['origin_host'].strip()
|
||||
origin_protocol = form.cleaned_data['origin_protocol']
|
||||
origin_port = form.cleaned_data['origin_port']
|
||||
plan = form.cleaned_data.get('plan')
|
||||
enable_websocket = form.cleaned_data.get('enable_websocket') or False
|
||||
|
||||
# 构造域名列表:至少包含一个主域名或子域名
|
||||
domains_list = []
|
||||
if subdomains:
|
||||
domains_list.extend([f"{sub}.{name}" for sub in subdomains])
|
||||
else:
|
||||
domains_list.append(name)
|
||||
|
||||
# 构造源站地址列表
|
||||
origin_addr = f"{origin_protocol}://{origin_host}:{origin_port}"
|
||||
origin_addrs = [origin_addr]
|
||||
|
||||
# 初始化 GoEdge 客户端并创建服务
|
||||
try:
|
||||
client = GoEdgeClient()
|
||||
server_id = client.create_basic_http_server(
|
||||
domains=domains_list,
|
||||
origin_addrs=origin_addrs,
|
||||
user_id=0, # 管理员创建,不指定用户
|
||||
enable_websocket=enable_websocket,
|
||||
)
|
||||
except Exception as e:
|
||||
messages.error(request, f'创建加速服务失败:{e}')
|
||||
return render(request, 'domains/add.html', {'form': form})
|
||||
|
||||
# 保存域名记录
|
||||
sys = SystemSettings.objects.order_by('id').first()
|
||||
cname_template = (sys.cname_template if sys else '{sub}.cdn.example.com')
|
||||
cname_map = {}
|
||||
for sub in subdomains or []:
|
||||
cname_map[f"{sub}.{name}"] = cname_template.replace('{sub}', sub)
|
||||
if not cname_map:
|
||||
# 主域名CNAME场景(可选)
|
||||
cname_map[name] = cname_template.replace('{sub}', 'www')
|
||||
|
||||
# 计费周期:设置为当前自然月
|
||||
today = timezone.now().date()
|
||||
month_start = today.replace(day=1)
|
||||
# 下个月第一天
|
||||
if month_start.month == 12:
|
||||
next_month_start = month_start.replace(year=month_start.year + 1, month=1, day=1)
|
||||
else:
|
||||
next_month_start = month_start.replace(month=month_start.month + 1, day=1)
|
||||
current_cycle_end = next_month_start - timezone.timedelta(days=1)
|
||||
|
||||
domain = Domain.objects.create(
|
||||
user=request.user,
|
||||
name=name,
|
||||
status=Domain.STATUS_PENDING,
|
||||
current_plan=plan,
|
||||
current_cycle_start=month_start,
|
||||
current_cycle_end=current_cycle_end,
|
||||
cname_targets=cname_map,
|
||||
origin_config={
|
||||
'host': origin_host,
|
||||
'protocol': origin_protocol,
|
||||
'port': origin_port,
|
||||
},
|
||||
edge_server_id=server_id,
|
||||
)
|
||||
|
||||
# 记录操作日志
|
||||
try:
|
||||
OperationLog.objects.create(
|
||||
actor=request.user,
|
||||
action='create_domain',
|
||||
target=name,
|
||||
detail=f"server_id={server_id}; origin={origin_protocol}://{origin_host}:{origin_port}; subdomains={','.join(subdomains or [])}",
|
||||
)
|
||||
except Exception:
|
||||
# 日志写入失败不阻断主流程
|
||||
pass
|
||||
|
||||
messages.success(request, '域名已创建,请按提示配置DNS CNAME记录并等待生效。')
|
||||
return redirect(reverse('domains:detail', kwargs={'domain_id': domain.id}))
|
||||
else:
|
||||
form = AddDomainForm()
|
||||
|
||||
return render(request, 'domains/add.html', {'form': form})
|
||||
|
||||
@login_required
|
||||
def domain_detail(request, domain_id: int):
|
||||
domain = get_object_or_404(Domain, id=domain_id, user=request.user)
|
||||
|
||||
# 流量统计:当月(按自然月)
|
||||
today = timezone.now().date()
|
||||
month_start = today.replace(day=1)
|
||||
traffic_qs = DomainTrafficDaily.objects.filter(domain=domain, day__gte=month_start)
|
||||
total_bytes = sum(t.bytes for t in traffic_qs)
|
||||
used_gb = bytes_to_gb(total_bytes)
|
||||
|
||||
# 配额计算:Plan 或系统默认 + 域名当期额外
|
||||
sys = SystemSettings.objects.order_by('id').first()
|
||||
base_quota_gb = 0
|
||||
if domain.current_plan and domain.current_plan.included_traffic_gb_per_domain:
|
||||
base_quota_gb = domain.current_plan.included_traffic_gb_per_domain
|
||||
elif sys:
|
||||
base_quota_gb = sys.default_free_traffic_gb_per_domain
|
||||
total_quota_gb = (base_quota_gb or 0) + (domain.extra_free_traffic_gb_current_cycle or 0)
|
||||
progress_pct = 0
|
||||
if total_quota_gb > 0:
|
||||
progress_pct = int(min(100, (used_gb / total_quota_gb) * 100))
|
||||
|
||||
# 近7/30天统计与峰值(跨自然月)
|
||||
seven_start = today - timezone.timedelta(days=6)
|
||||
thirty_start = today - timezone.timedelta(days=29)
|
||||
last7_qs = DomainTrafficDaily.objects.filter(domain=domain, day__gte=seven_start, day__lte=today)
|
||||
last30_qs = DomainTrafficDaily.objects.filter(domain=domain, day__gte=thirty_start, day__lte=today)
|
||||
|
||||
last7_total_gb = bytes_to_gb(sum(t.bytes for t in last7_qs))
|
||||
last30_total_gb = bytes_to_gb(sum(t.bytes for t in last30_qs))
|
||||
# 平均值(GB/日)
|
||||
last7_count = last7_qs.count()
|
||||
last30_count = last30_qs.count()
|
||||
last7_avg_gb = round((last7_total_gb / last7_count), 2) if last7_count else 0
|
||||
last30_avg_gb = round((last30_total_gb / last30_count), 2) if last30_count else 0
|
||||
|
||||
peak_volume_record = last30_qs.order_by('-bytes').first()
|
||||
peak_bandwidth_record = last30_qs.order_by('-peak_bandwidth_mbps').first()
|
||||
peak_volume_day = None
|
||||
peak_bandwidth_day = None
|
||||
if peak_volume_record:
|
||||
peak_volume_day = {
|
||||
'day': peak_volume_record.day,
|
||||
'gb': bytes_to_gb(peak_volume_record.bytes),
|
||||
}
|
||||
if peak_bandwidth_record:
|
||||
peak_bandwidth_day = {
|
||||
'day': peak_bandwidth_record.day,
|
||||
'mbps': peak_bandwidth_record.peak_bandwidth_mbps,
|
||||
}
|
||||
|
||||
# 近30天每日统计(GB 与峰值带宽)
|
||||
last_30 = list(last30_qs.order_by('-day'))
|
||||
traffic_rows = []
|
||||
for t in last_30:
|
||||
gb = bytes_to_gb(t.bytes)
|
||||
traffic_rows.append({'day': t.day, 'gb': gb, 'peak_mbps': t.peak_bandwidth_mbps})
|
||||
|
||||
# GoEdge 同步状态
|
||||
goedge_status = None
|
||||
if domain.edge_server_id:
|
||||
try:
|
||||
client = GoEdgeClient()
|
||||
goedge_status = client.get_server_feature_status(domain.edge_server_id)
|
||||
except Exception as e:
|
||||
goedge_status = {'error': str(e)}
|
||||
|
||||
requests_24h_total = None
|
||||
status_bins = None
|
||||
status_top = None
|
||||
if domain.edge_server_id:
|
||||
try:
|
||||
client = GoEdgeClient()
|
||||
hs = client.find_latest_server_hourly_stats(int(domain.edge_server_id), hours=24)
|
||||
requests_24h_total = sum(int(x.get('countRequests') or 0) for x in hs)
|
||||
agg = client.aggregate_status_codes(int(domain.edge_server_id))
|
||||
status_bins = agg.get('bins')
|
||||
status_top = agg.get('top')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = {
|
||||
'domain': domain,
|
||||
'used_gb': used_gb,
|
||||
'total_quota_gb': total_quota_gb,
|
||||
'progress_pct': progress_pct,
|
||||
'traffic_rows': traffic_rows,
|
||||
'last7_total_gb': last7_total_gb,
|
||||
'last30_total_gb': last30_total_gb,
|
||||
'last7_avg_gb': last7_avg_gb,
|
||||
'last30_avg_gb': last30_avg_gb,
|
||||
'peak_volume_day': peak_volume_day,
|
||||
'peak_bandwidth_day': peak_bandwidth_day,
|
||||
'cname_results': None,
|
||||
'goedge_status': goedge_status,
|
||||
'requests_24h_total': requests_24h_total,
|
||||
'status_bins': status_bins,
|
||||
'status_top': status_top,
|
||||
}
|
||||
|
||||
return render(request, 'domains/detail.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
@transaction.atomic
|
||||
def check_dns(request, domain_id: int):
|
||||
domain = get_object_or_404(Domain, id=domain_id, user=request.user)
|
||||
if request.method != 'POST':
|
||||
return redirect(reverse('domains:detail', kwargs={'domain_id': domain.id}))
|
||||
|
||||
cname_map = domain.cname_targets or {}
|
||||
results = check_cname_map(cname_map)
|
||||
|
||||
ok_count = sum(1 for r in results if r.get('ok'))
|
||||
total = len(results)
|
||||
if ok_count == total and total > 0:
|
||||
messages.success(request, 'DNS 已生效,所有 CNAME 指向正确。')
|
||||
# 如果当前为等待DNS,可切换为 active
|
||||
if domain.status == Domain.STATUS_PENDING:
|
||||
domain.status = Domain.STATUS_ACTIVE
|
||||
domain.save(update_fields=['status'])
|
||||
else:
|
||||
messages.warning(request, f'DNS 检测完成:{ok_count}/{total} 条记录正确。请检查未生效的记录。')
|
||||
|
||||
# 重渲染详情页并显示检测结果
|
||||
today = timezone.now().date()
|
||||
month_start = today.replace(day=1)
|
||||
traffic_qs = DomainTrafficDaily.objects.filter(domain=domain, day__gte=month_start)
|
||||
total_bytes = sum(t.bytes for t in traffic_qs)
|
||||
used_gb = bytes_to_gb(total_bytes)
|
||||
sys = SystemSettings.objects.order_by('id').first()
|
||||
base_quota_gb = 0
|
||||
if domain.current_plan and domain.current_plan.included_traffic_gb_per_domain:
|
||||
base_quota_gb = domain.current_plan.included_traffic_gb_per_domain
|
||||
elif sys:
|
||||
base_quota_gb = sys.default_free_traffic_gb_per_domain
|
||||
total_quota_gb = (base_quota_gb or 0) + (domain.extra_free_traffic_gb_current_cycle or 0)
|
||||
progress_pct = 0
|
||||
if total_quota_gb > 0:
|
||||
progress_pct = int(min(100, (used_gb / total_quota_gb) * 100))
|
||||
|
||||
seven_start = today - timezone.timedelta(days=6)
|
||||
thirty_start = today - timezone.timedelta(days=29)
|
||||
last30_qs = DomainTrafficDaily.objects.filter(domain=domain, day__gte=thirty_start, day__lte=today)
|
||||
last7_qs = DomainTrafficDaily.objects.filter(domain=domain, day__gte=seven_start, day__lte=today)
|
||||
peak_volume_record = last30_qs.order_by('-bytes').first()
|
||||
peak_bandwidth_record = last30_qs.order_by('-peak_bandwidth_mbps').first()
|
||||
return render(request, 'domains/detail.html', {
|
||||
'domain': domain,
|
||||
'used_gb': used_gb,
|
||||
'total_quota_gb': total_quota_gb,
|
||||
'progress_pct': progress_pct,
|
||||
'traffic_rows': [{
|
||||
'day': t.day,
|
||||
'gb': bytes_to_gb(t.bytes),
|
||||
'peak_mbps': t.peak_bandwidth_mbps,
|
||||
} for t in last30_qs.order_by('-day')],
|
||||
'last7_total_gb': bytes_to_gb(sum(t.bytes for t in last7_qs)),
|
||||
'last30_total_gb': bytes_to_gb(sum(t.bytes for t in last30_qs)),
|
||||
'last7_avg_gb': (round(bytes_to_gb(sum(t.bytes for t in last7_qs)) / last7_qs.count(), 2) if last7_qs.count() else 0),
|
||||
'last30_avg_gb': (round(bytes_to_gb(sum(t.bytes for t in last30_qs)) / last30_qs.count(), 2) if last30_qs.count() else 0),
|
||||
'peak_volume_day': ({
|
||||
'day': peak_volume_record.day,
|
||||
'gb': bytes_to_gb(peak_volume_record.bytes),
|
||||
} if peak_volume_record else None),
|
||||
'peak_bandwidth_day': ({
|
||||
'day': peak_bandwidth_record.day,
|
||||
'mbps': peak_bandwidth_record.peak_bandwidth_mbps,
|
||||
} if peak_bandwidth_record else None),
|
||||
'cname_results': results,
|
||||
})
|
||||
|
||||
# Create your views here.
|
||||
|
||||
|
||||
@login_required
|
||||
@transaction.atomic
|
||||
def upgrade_plan(request, domain_id: int):
|
||||
"""最小套餐升级流程:展示公开套餐并允许用户选择变更当前域名的套餐。"""
|
||||
domain = get_object_or_404(Domain, id=domain_id, user=request.user)
|
||||
|
||||
# 可选套餐列表(公开且激活)
|
||||
available_plans = Plan.objects.filter(is_active=True, is_public=True).order_by('base_price_per_domain')
|
||||
|
||||
if request.method == 'POST':
|
||||
plan_id = request.POST.get('plan_id')
|
||||
try:
|
||||
new_plan = available_plans.get(id=plan_id)
|
||||
except Plan.DoesNotExist:
|
||||
messages.error(request, '选择的套餐不可用或不存在。')
|
||||
return render(request, 'domains/upgrade.html', {
|
||||
'domain': domain,
|
||||
'plans': available_plans,
|
||||
})
|
||||
|
||||
old_plan = domain.current_plan
|
||||
domain.current_plan = new_plan
|
||||
domain.save(update_fields=['current_plan', 'updated_at'])
|
||||
|
||||
# 操作日志
|
||||
try:
|
||||
OperationLog.objects.create(
|
||||
actor=request.user,
|
||||
action='upgrade_plan',
|
||||
target=domain.name,
|
||||
detail=f"from={old_plan.name if old_plan else '-'} to={new_plan.name}",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
messages.success(request, f'套餐已升级为:{new_plan.name}')
|
||||
return redirect(reverse('domains:detail', kwargs={'domain_id': domain.id}))
|
||||
|
||||
return render(request, 'domains/upgrade.html', {
|
||||
'domain': domain,
|
||||
'plans': available_plans,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@transaction.atomic
|
||||
def domain_settings(request, domain_id: int):
|
||||
"""域名功能设置页:保存到 Domain.custom_features,后续接入 GoEdge 配置更新。"""
|
||||
domain = get_object_or_404(Domain, id=domain_id)
|
||||
# 权限:仅域名所有者或staff
|
||||
if not (domain.user_id == request.user.id or request.user.is_staff):
|
||||
messages.warning(request, '无权编辑该域名设置。')
|
||||
return redirect('domains:detail', domain_id=domain.id)
|
||||
|
||||
plan_features = domain.current_plan.features if domain.current_plan else {}
|
||||
import json
|
||||
initial = {
|
||||
'waf_enabled': bool(domain.custom_features.get('waf_enabled', plan_features.get('waf_enabled', False))),
|
||||
'http3_enabled': bool(domain.custom_features.get('http3_enabled', plan_features.get('http3_enabled', False))),
|
||||
'logs_enabled': bool(domain.custom_features.get('logs_enabled', plan_features.get('logs_enabled', False))),
|
||||
'websocket_enabled': bool(domain.custom_features.get('websocket_enabled', plan_features.get('websocket_enabled', False))),
|
||||
'redirect_https_enabled': bool(domain.custom_features.get('redirect_https_enabled', False)),
|
||||
'cache_rules_json': json.dumps(domain.custom_features.get('cache_rules_json', {}), ensure_ascii=False),
|
||||
'page_rules_json': json.dumps(domain.custom_features.get('page_rules_json', {}), ensure_ascii=False),
|
||||
'ip_whitelist': ','.join(domain.custom_features.get('ip_whitelist', [])),
|
||||
'ip_blacklist': ','.join(domain.custom_features.get('ip_blacklist', [])),
|
||||
}
|
||||
|
||||
if request.method == 'POST':
|
||||
form = DomainSettingsForm(request.POST)
|
||||
if form.is_valid():
|
||||
data = form.cleaned_data
|
||||
domain.custom_features = {
|
||||
'waf_enabled': bool(data.get('waf_enabled')),
|
||||
'http3_enabled': bool(data.get('http3_enabled')),
|
||||
'logs_enabled': bool(data.get('logs_enabled')),
|
||||
'websocket_enabled': bool(data.get('websocket_enabled')),
|
||||
'redirect_https_enabled': bool(data.get('redirect_https_enabled')),
|
||||
'cache_rules_json': data.get('cache_rules_json') or {},
|
||||
'page_rules_json': data.get('page_rules_json') or {},
|
||||
'ip_whitelist': data.get('ip_whitelist') or [],
|
||||
'ip_blacklist': data.get('ip_blacklist') or [],
|
||||
}
|
||||
domain.save(update_fields=['custom_features', 'updated_at'])
|
||||
# 记录操作日志(含策略ID与同步结果)
|
||||
log_detail = {
|
||||
'requested': {
|
||||
'waf_enabled': bool(data.get('waf_enabled')),
|
||||
'http3_enabled': bool(data.get('http3_enabled')),
|
||||
'logs_enabled': bool(data.get('logs_enabled')),
|
||||
'websocket_enabled': bool(data.get('websocket_enabled')),
|
||||
},
|
||||
'sync': {
|
||||
'accessLog': None,
|
||||
'websocket': None,
|
||||
'firewall': None,
|
||||
'ssl_http3': None,
|
||||
}
|
||||
}
|
||||
# 尝试同步到 GoEdge(根据可用策略与配置)
|
||||
try:
|
||||
client = GoEdgeClient()
|
||||
web_id = client.find_server_web_id(domain.edge_server_id)
|
||||
if web_id:
|
||||
sys = SystemSettings.objects.order_by('id').first()
|
||||
# 访问日志
|
||||
client.update_http_web_access_log(
|
||||
http_web_id=web_id,
|
||||
is_on=bool(data.get('logs_enabled')),
|
||||
policy_id=(sys.default_http_access_log_policy_id if sys else None)
|
||||
)
|
||||
log_detail['sync']['accessLog'] = {
|
||||
'webId': web_id,
|
||||
'isOn': bool(data.get('logs_enabled')),
|
||||
'policyId': (sys.default_http_access_log_policy_id if sys else None)
|
||||
}
|
||||
# WebSocket
|
||||
client.update_http_web_websocket(http_web_id=web_id, is_on=bool(data.get('websocket_enabled')))
|
||||
log_detail['sync']['websocket'] = {'webId': web_id, 'isOn': bool(data.get('websocket_enabled'))}
|
||||
# WAF(如有默认策略则引用)
|
||||
client.update_http_web_firewall(
|
||||
http_web_id=web_id,
|
||||
is_on=bool(data.get('waf_enabled')),
|
||||
policy_id=(sys.default_http_firewall_policy_id if sys else None)
|
||||
)
|
||||
log_detail['sync']['firewall'] = {
|
||||
'webId': web_id,
|
||||
'isOn': bool(data.get('waf_enabled')),
|
||||
'policyId': (sys.default_http_firewall_policy_id if sys else None)
|
||||
}
|
||||
cache_conf = data.get('cache_rules_json') or {}
|
||||
if isinstance(cache_conf, dict) and cache_conf:
|
||||
client.update_http_web_cache(http_web_id=web_id, cache_conf=cache_conf)
|
||||
log_detail['sync']['cache'] = {'webId': web_id, 'applied': True}
|
||||
page_rules = data.get('page_rules_json') or {}
|
||||
if isinstance(page_rules, dict) and page_rules:
|
||||
locations_conf = page_rules.get('locations') or page_rules.get('Locations') or None
|
||||
rewrite_conf = page_rules.get('rewriteRules') or page_rules.get('RewriteRules') or None
|
||||
if locations_conf:
|
||||
client.update_http_web_locations(http_web_id=web_id, locations_conf=locations_conf)
|
||||
log_detail['sync']['locations'] = {'webId': web_id, 'count': len(locations_conf) if isinstance(locations_conf, list) else 1}
|
||||
if rewrite_conf:
|
||||
client.update_http_web_rewrite_rules(http_web_id=web_id, rewrite_conf=rewrite_conf)
|
||||
log_detail['sync']['rewrite'] = {'webId': web_id, 'count': len(rewrite_conf) if isinstance(rewrite_conf, list) else 1}
|
||||
# 强制 HTTPS 跳转
|
||||
if bool(data.get('redirect_https_enabled')):
|
||||
client.update_http_web_redirect_to_https(http_web_id=web_id, redirect_conf={'isOn': True})
|
||||
log_detail['sync']['redirectToHTTPS'] = {'webId': web_id, 'isOn': True}
|
||||
else:
|
||||
client.update_http_web_redirect_to_https(http_web_id=web_id, redirect_conf={'isOn': False})
|
||||
log_detail['sync']['redirectToHTTPS'] = {'webId': web_id, 'isOn': False}
|
||||
# HTTP/3 通过 SSL 策略更新
|
||||
ssl_policy_id = client.find_server_ssl_policy_id(domain.edge_server_id)
|
||||
if ssl_policy_id is not None:
|
||||
client.update_ssl_policy_http3(ssl_policy_id, bool(data.get('http3_enabled')))
|
||||
log_detail['sync']['ssl_http3'] = {'sslPolicyId': ssl_policy_id, 'http3Enabled': bool(data.get('http3_enabled'))}
|
||||
except Exception as e:
|
||||
messages.warning(request, f'部分功能未同步到 GoEdge:{e}')
|
||||
log_detail['sync']['error'] = str(e)
|
||||
# 写操作日志
|
||||
try:
|
||||
OperationLog.objects.create(
|
||||
actor=request.user,
|
||||
action='domain_settings_update',
|
||||
target=domain.name,
|
||||
detail=json.dumps(log_detail, ensure_ascii=False)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
messages.success(request, '设置已保存。部分功能需对应套餐支持方可生效。')
|
||||
return redirect('domains:settings', domain_id=domain.id)
|
||||
else:
|
||||
messages.error(request, '保存失败,请检查表单字段。')
|
||||
else:
|
||||
form = DomainSettingsForm(initial=initial)
|
||||
|
||||
return render(request, 'domains/settings.html', {
|
||||
'domain': domain,
|
||||
'form': form,
|
||||
'plan_features': plan_features,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def domain_logs(request, domain_id: int):
|
||||
domain = get_object_or_404(Domain, id=domain_id, user=request.user)
|
||||
server_id = int(domain.edge_server_id or 0)
|
||||
logs = []
|
||||
request_id = request.GET.get('request_id') or None
|
||||
day = request.GET.get('day') or None
|
||||
hour_from = request.GET.get('hour_from') or None
|
||||
hour_to = request.GET.get('hour_to') or None
|
||||
ip = request.GET.get('ip') or None
|
||||
keyword = request.GET.get('keyword') or None
|
||||
size = int(request.GET.get('size') or 50)
|
||||
status_code = request.GET.get('status_code') or ''
|
||||
export = request.GET.get('export') or ''
|
||||
reverse = bool(request.GET.get('reverse'))
|
||||
has_more = False
|
||||
if server_id:
|
||||
try:
|
||||
client = GoEdgeClient()
|
||||
res = client.list_http_access_logs(
|
||||
server_id=server_id,
|
||||
day=day,
|
||||
size=size,
|
||||
hour_from=hour_from,
|
||||
hour_to=hour_to,
|
||||
reverse=reverse,
|
||||
ip=ip,
|
||||
keyword=keyword,
|
||||
request_id=request_id,
|
||||
)
|
||||
logs = res.get('logs') or []
|
||||
request_id = res.get('requestId') or None
|
||||
has_more = bool(res.get('hasMore'))
|
||||
except Exception as e:
|
||||
messages.warning(request, f'访问日志查询失败:{e}')
|
||||
# 过滤状态码
|
||||
if status_code:
|
||||
try:
|
||||
sc = int(status_code)
|
||||
logs = [l for l in (logs or []) if int(l.get('status') or 0) == sc]
|
||||
except Exception:
|
||||
pass
|
||||
# 导出
|
||||
if export == 'csv':
|
||||
import csv
|
||||
from django.http import HttpResponse
|
||||
resp = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||
resp['Content-Disposition'] = f'attachment; filename="{domain.name}_access_logs.csv"'
|
||||
writer = csv.writer(resp)
|
||||
writer.writerow(['timeLocal', 'host', 'remoteAddr', 'method', 'requestURI', 'status', 'bytesSent', 'userAgent'])
|
||||
for l in logs:
|
||||
writer.writerow([
|
||||
l.get('timeLocal'), l.get('host'), l.get('remoteAddr'), l.get('method'), l.get('requestURI'), l.get('status'), l.get('bytesSent'), l.get('userAgent'),
|
||||
])
|
||||
return resp
|
||||
if export == 'json':
|
||||
from django.http import JsonResponse
|
||||
return JsonResponse({'domain': domain.name, 'logs': logs}, json_dumps_params={'ensure_ascii': False})
|
||||
return render(request, 'domains/logs.html', {
|
||||
'domain': domain,
|
||||
'logs': logs,
|
||||
'day': day or '',
|
||||
'hour_from': hour_from or '',
|
||||
'hour_to': hour_to or '',
|
||||
'ip': ip or '',
|
||||
'keyword': keyword or '',
|
||||
'size': size,
|
||||
'reverse': reverse,
|
||||
'request_id': request_id or '',
|
||||
'has_more': has_more,
|
||||
'status_code': status_code or '',
|
||||
})
|
||||
Reference in New Issue
Block a user