Initial commit
This commit is contained in:
184
billing/views.py
Normal file
184
billing/views.py
Normal 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__)
|
||||
Reference in New Issue
Block a user