from django import forms import logging from core.models import SystemSettings from plans.models import Plan from domains.models import Domain from billing.models import Invoice from decimal import Decimal class SystemSettingsForm(forms.ModelForm): overage_action = forms.ChoiceField(choices=[('shutdown', '停服'), ('limit', '限速')], required=False, widget=forms.Select(attrs={'class': 'form-select'})) overage_limit_bps = forms.IntegerField(required=False, min_value=0, widget=forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '字节/秒'})) class Meta: model = SystemSettings fields = [ 'goedge_base_url', 'admin_access_key_id', 'admin_access_key', 'default_node_cluster_id', 'default_free_traffic_gb_per_domain', 'default_http_access_log_policy_id', 'default_http_firewall_policy_id', 'cname_template', 'anomaly_detection_enabled', 'anomaly_threshold_multiplier', 'anomaly_window_days', 'anomaly_min_gb', 'captcha_enabled', 'epay_api_base_url', 'epay_pid', 'epay_key', ] widgets = { 'goedge_base_url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'https://backend.example.com'}), 'admin_access_key_id': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'AKID...'}), 'admin_access_key': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'AK...'}), 'default_node_cluster_id': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '集群ID'}), 'default_free_traffic_gb_per_domain': forms.NumberInput(attrs={'class': 'form-control'}), 'default_http_access_log_policy_id': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'HTTPAccessLogPolicyId'}), 'default_http_firewall_policy_id': forms.NumberInput(attrs={'class': 'form-control', 'placeholder': 'HTTPFirewallPolicyId'}), 'cname_template': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '{sub}.cdn.example.com'}), 'anomaly_detection_enabled': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'anomaly_threshold_multiplier': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '1'}), 'anomaly_window_days': forms.NumberInput(attrs={'class': 'form-control', 'min': '3'}), 'anomaly_min_gb': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0'}), 'captcha_enabled': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'epay_api_base_url': forms.URLInput(attrs={'class': 'form-control', 'placeholder': 'https://api.example.com'}), 'epay_pid': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '商户ID'}), 'epay_key': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '商户密钥'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) try: policy = dict(getattr(self.instance, 'default_overage_policy', {}) or {}) except Exception: policy = {} self.fields['overage_action'].initial = (policy.get('action') or 'shutdown') self.fields['overage_limit_bps'].initial = int(policy.get('limit_bps') or 0) def save(self, commit=True): obj = super().save(commit=False) act = (self.cleaned_data.get('overage_action') or 'shutdown').lower() limit = int(self.cleaned_data.get('overage_limit_bps') or 0) obj.default_overage_policy = {'action': act, 'limit_bps': limit} if commit: obj.save() return obj class PlanForm(forms.ModelForm): # 使用 JSONField 保证 features 验证 features = forms.JSONField(required=False, widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 4, 'placeholder': '{"waf_enabled": true, "http3_enabled": false}' })) # 可视化常用功能开关(与 JSON 保持兼容) waf_enabled = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})) http3_enabled = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})) websocket_enabled = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})) realtime_logs_enabled = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})) custom_ssl_enabled = forms.BooleanField(required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})) page_rules_limit = forms.IntegerField(required=False, min_value=0, widget=forms.NumberInput(attrs={'class': 'form-control', 'placeholder': '页面规则数量上限(可选)'})) class Meta: model = Plan fields = [ 'name', 'description', 'billing_mode', 'base_price_per_domain', 'included_traffic_gb_per_domain', 'overage_price_per_gb', 'allow_overage', 'is_active', 'is_public', 'allow_new_purchase', 'allow_renew', 'features', ] widgets = { 'name': forms.TextInput(attrs={'class': 'form-control'}), 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), 'billing_mode': forms.TextInput(attrs={'class': 'form-control'}), 'base_price_per_domain': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), 'included_traffic_gb_per_domain': forms.NumberInput(attrs={'class': 'form-control'}), 'overage_price_per_gb': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'}), 'allow_overage': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_public': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'allow_new_purchase': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'allow_renew': forms.CheckboxInput(attrs={'class': 'form-check-input'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 初始化可视化字段的初始值来源于现有 features JSON features_dict = {} try: if getattr(self.instance, 'features', None): features_dict = dict(self.instance.features) except Exception: features_dict = {} self.fields['waf_enabled'].initial = features_dict.get('waf_enabled', False) self.fields['http3_enabled'].initial = features_dict.get('http3_enabled', False) self.fields['websocket_enabled'].initial = features_dict.get('websocket_enabled', False) self.fields['realtime_logs_enabled'].initial = features_dict.get('realtime_logs_enabled', False) self.fields['custom_ssl_enabled'].initial = features_dict.get('custom_ssl_enabled', False) self.fields['page_rules_limit'].initial = features_dict.get('page_rules_limit', None) def save(self, commit=True): plan = super().save(commit=False) # 合并 JSON 字段与可视化字段 base_features = {} try: if getattr(self.instance, 'features', None): base_features = dict(self.instance.features) except Exception: base_features = {} cleaned_features = self.cleaned_data.get('features') or {} if not isinstance(cleaned_features, dict): cleaned_features = {} # 先合并自由 JSON merged = {**base_features, **cleaned_features} # 再覆盖常用可视化选项 merged['waf_enabled'] = bool(self.cleaned_data.get('waf_enabled')) merged['http3_enabled'] = bool(self.cleaned_data.get('http3_enabled')) merged['websocket_enabled'] = bool(self.cleaned_data.get('websocket_enabled')) merged['realtime_logs_enabled'] = bool(self.cleaned_data.get('realtime_logs_enabled')) merged['custom_ssl_enabled'] = bool(self.cleaned_data.get('custom_ssl_enabled')) page_rules_limit = self.cleaned_data.get('page_rules_limit') if page_rules_limit is not None: merged['page_rules_limit'] = int(page_rules_limit) else: # 若用户未设置,上次值保留(已在 merged 中) pass plan.features = merged if commit: plan.save() return plan class DomainPlanSwitchForm(forms.ModelForm): class Meta: model = Domain fields = ['current_plan'] widgets = { 'current_plan': forms.Select(attrs={'class': 'form-select'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 仅允许选择激活的套餐(运营可见不强制) self.fields['current_plan'].queryset = Plan.objects.filter(is_active=True).order_by('base_price_per_domain') class DomainGrantTrafficForm(forms.ModelForm): class Meta: model = Domain fields = ['extra_free_traffic_gb_current_cycle'] widgets = { 'extra_free_traffic_gb_current_cycle': forms.NumberInput(attrs={'class': 'form-control', 'min': '0'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['extra_free_traffic_gb_current_cycle'].label = '本周期额外赠送流量(GB)' class InvoiceAdjustmentForm(forms.Form): description = forms.CharField(label='调整说明', max_length=200, required=False, initial='人工调整', widget=forms.TextInput(attrs={'class': 'form-control'})) amount = forms.DecimalField(label='调整金额(¥)', max_digits=12, decimal_places=2, widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01'})) is_increase = forms.BooleanField(required=False, initial=True, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})) def apply(self, invoice: Invoice) -> Invoice: amt = Decimal(str(self.cleaned_data['amount'])) if not self.cleaned_data.get('is_increase'): amt = -amt desc = self.cleaned_data.get('description') or '人工调整' # 更新账单金额 invoice.amount_adjustment = (Decimal(str(invoice.amount_adjustment)) + amt).quantize(Decimal('0.01')) invoice.amount_total = (Decimal(str(invoice.amount_total)) + amt).quantize(Decimal('0.01')) invoice.save(update_fields=['amount_adjustment', 'amount_total']) # 记录为一条账单项 try: from billing.models import InvoiceItem InvoiceItem.objects.create( invoice=invoice, domain=None, description=desc, quantity=Decimal('0.000'), unit_price=Decimal('0.00'), amount=amt, ) except Exception: logger.exception('invoice item write failed') return invoice logger = logging.getLogger(__name__)