Initial commit

This commit is contained in:
2025-11-18 03:36:49 +08:00
commit d17c7efb3c
7078 changed files with 831480 additions and 0 deletions

184
billing/views.py Normal file
View File

@@ -0,0 +1,184 @@
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__)