from django.shortcuts import render import logging from django.contrib.auth.decorators import login_required from django.db.models import Sum from django.shortcuts import get_object_or_404, redirect from django.utils import timezone from django.contrib import messages from .models import Invoice from domains.models import DomainTrafficDaily from decimal import Decimal from core.models import OperationLog from core.models import SystemSettings from django.http import HttpResponse import hashlib import urllib.parse @login_required def list_billing(request): status = request.GET.get('status') invoices_qs = Invoice.objects.filter(user=request.user) if status: invoices_qs = invoices_qs.filter(status=status) invoices = invoices_qs.order_by('-period_end') totals = invoices_qs.aggregate(total_amount=Sum('amount_total')) return render(request, 'billing/list.html', { 'invoices': invoices, 'total_amount': totals.get('total_amount') or 0, 'status': status or '', 'status_choices': getattr(Invoice, 'STATUS_CHOICES', []), }) @login_required def invoice_detail(request, invoice_id: int): invoice = get_object_or_404(Invoice, id=invoice_id) # 权限:仅本人或staff可查看 if not (invoice.user_id == request.user.id or request.user.is_staff): return redirect('billing:list') 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', 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') messages.success(request, '账单已标记为已支付。') return redirect('billing:detail', invoice_id=invoice.id) items = invoice.items.select_related('domain').all() plan_total = invoice.amount_plan_total overage_total = invoice.amount_overage_total adjustment = invoice.amount_adjustment amount_total = invoice.amount_total # 聚合账单周期内的域名用量(GB) traffic_by_domain = {} domain_stats = [] period_start = invoice.period_start period_end = invoice.period_end for it in items: if it.domain_id and it.domain_id not in traffic_by_domain: bytes_sum = DomainTrafficDaily.objects.filter(domain_id=it.domain_id, day__gte=period_start, day__lte=period_end).aggregate(b=Sum('bytes'))['b'] or 0 gb = round(Decimal(str(bytes_sum)) / Decimal(str(1024 ** 3)), 3) traffic_by_domain[it.domain_id] = gb domain_stats.append({'domain': it.domain, 'gb': gb}) return render(request, 'billing/detail.html', { 'invoice': invoice, 'items': items, 'plan_total': plan_total, 'overage_total': overage_total, 'adjustment': adjustment, 'amount_total': amount_total, 'traffic_by_domain': traffic_by_domain, 'domain_stats': domain_stats, 'period_start': period_start, 'period_end': period_end, }) @login_required def invoice_pay(request, invoice_id: int): invoice = get_object_or_404(Invoice, id=invoice_id, user=request.user) if invoice.status != Invoice.STATUS_UNPAID: return redirect('billing:detail', invoice_id=invoice.id) settings_obj = SystemSettings.objects.order_by('id').first() api = (settings_obj.epay_api_base_url or '').rstrip('/') pid = settings_obj.epay_pid or '' key = settings_obj.epay_key or '' if not (api and pid and key): messages.error(request, '支付未配置') return redirect('billing:detail', invoice_id=invoice.id) pay_type = request.GET.get('type') or 'alipay' params = { 'pid': pid, 'type': pay_type, 'out_trade_no': f'INV{invoice.id}', 'notify_url': request.build_absolute_uri(reverse('billing:notify')), 'return_url': request.build_absolute_uri(reverse('billing:return')), 'name': f'Invoice#{invoice.id}', 'money': str(invoice.amount_total), 'sitename': 'PyGoEdge', 'sign_type': 'MD5', } sign_src = '&'.join(f"{k}={params[k]}" for k in sorted(params) if k not in ['sign', 'sign_type'] and params[k] != '') + '&key=' + key sign = hashlib.md5(sign_src.encode('utf-8')).hexdigest().upper() params['sign'] = sign url = api + '/submit.php?' + urllib.parse.urlencode(params) try: OperationLog.objects.create(actor=request.user, action='invoice_pay_create', target=f'Invoice#{invoice.id}', detail=url) except Exception: pass return redirect(url) def _verify_epay_sign(params: dict, key: str) -> bool: p = {k: v for k, v in params.items() if k not in ['sign', 'sign_type'] and v is not None and v != ''} src = '&'.join(f"{k}={p[k]}" for k in sorted(p)) + '&key=' + key calc = hashlib.md5(src.encode('utf-8')).hexdigest().upper() return calc == (params.get('sign') or '').upper() def payment_notify(request): settings_obj = SystemSettings.objects.order_by('id').first() key = settings_obj.epay_key or '' params = dict(request.GET.items()) ok = (_verify_epay_sign(params, key) if key else False) out_trade_no = params.get('out_trade_no') or '' trade_status = params.get('trade_status') or '' money = params.get('money') or '' if ok and out_trade_no.startswith('INV') and trade_status: inv_id = int(out_trade_no.replace('INV', '')) inv = Invoice.objects.filter(id=inv_id).first() if inv and inv.status == Invoice.STATUS_UNPAID: if str(inv.amount_total) == str(money): inv.status = Invoice.STATUS_PAID inv.paid_at = timezone.now() inv.save(update_fields=['status', 'paid_at']) try: OperationLog.objects.create(actor=None, action='invoice_paid_notify', target=f'Invoice#{inv.id}', detail=out_trade_no) except Exception: pass return HttpResponse('SUCCESS') return HttpResponse('FAIL') @login_required def payment_return(request): messages.info(request, '支付流程已完成,如账单仍显示未支付,请稍后或刷新页面。') return redirect('billing:list') @login_required def invoice_detail_csv(request, invoice_id: int): invoice = get_object_or_404(Invoice, id=invoice_id) if not (invoice.user_id == request.user.id or request.user.is_staff): return redirect('billing:list') import csv from django.http import HttpResponse response = HttpResponse(content_type='text/csv; charset=utf-8') response['Content-Disposition'] = f'attachment; filename="invoice_{invoice.id}.csv"' writer = csv.writer(response) writer.writerow(['Domain', 'Description', 'Quantity(GB)', 'Unit Price', 'Amount']) for it in invoice.items.select_related('domain').all(): writer.writerow([ it.domain.name if it.domain else '', it.description, str(it.quantity), str(it.unit_price), str(it.amount), ]) return response # Create your views here. logger = logging.getLogger(__name__)