From cf241bf9d64a386a3768174ac17d394cf6501326 Mon Sep 17 00:00:00 2001 From: ibuler Date: Tue, 24 Jun 2025 16:36:43 +0800 Subject: [PATCH] perf: basic finished --- apps/jumpserver/urls.py | 1 + apps/reports/urls/view_urls.py | 15 ++++ apps/reports/views.py | 131 ++++++++++++++++++++++++++++++++- pyproject.toml | 2 + requirements/mac_pkg.sh | 5 +- 5 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 apps/reports/urls/view_urls.py diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index c02aba2ae..fd2291534 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -43,6 +43,7 @@ api_v1 = resource_api + [ app_view_patterns = [ path('auth/', include('authentication.urls.view_urls'), name='auth'), path('ops/', include('ops.urls.view_urls'), name='ops'), + path('reports/', include('reports.urls.view_urls'), name='reports'), path('tickets/', include('tickets.urls.view_urls'), name='tickets'), path('common/', include('common.urls.view_urls'), name='common'), re_path(r'flower/(?P.*)', views.celery_flower_view, name='flower-view'), diff --git a/apps/reports/urls/view_urls.py b/apps/reports/urls/view_urls.py new file mode 100644 index 000000000..404c78802 --- /dev/null +++ b/apps/reports/urls/view_urls.py @@ -0,0 +1,15 @@ +# ~*~ coding: utf-8 ~*~ +from __future__ import unicode_literals +from django.urls import path + +from .. import views + +__all__ = ["urlpatterns"] + +app_name = "reports" + +urlpatterns = [ + # Resource Task url + path('export-pdf/', views.ExportPdfView.as_view(), name='export-pdf'), + path('send-mail/', views.SendMailView.as_view(), name='send-mail'), +] \ No newline at end of file diff --git a/apps/reports/views.py b/apps/reports/views.py index 91ea44a21..9f0fd8a80 100644 --- a/apps/reports/views.py +++ b/apps/reports/views.py @@ -1,3 +1,130 @@ -from django.shortcuts import render +import io +import urllib.parse +from urllib.parse import urlparse -# Create your views here. +from django.http import JsonResponse +from django.utils import timezone +from django.conf import settings +from django.http import FileResponse, HttpResponseBadRequest +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from playwright.sync_api import sync_playwright +from django.core.mail import EmailMultiAlternatives +from pdf2image import convert_from_bytes +import base64 +from io import BytesIO + +charts_map = { + "UserActivity": { + "title": "用户活动", + "path": "/ui/#/reports/users/user-activity" + } +} + + +def export_chart_to_pdf(chart_name, sessionid, request=None): + chart_info = charts_map.get(chart_name) + if not chart_info: + return None, None + + if request: + url = request.build_absolute_uri(urllib.parse.unquote(chart_info['path'])) + else: + url = urllib.parse.unquote(chart_info['path']) + + if settings.DEBUG_DEV: + url = url.replace(":8080", ":9528") + print("Url: ", url) + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = browser.new_context(viewport={"width": 1000, "height": 800}) + # 设置 sessionid cookie + parsed_url = urlparse(url) + context.add_cookies([ + { + 'name': settings.SESSION_COOKIE_NAME, + 'value': sessionid, + 'domain': parsed_url.hostname, + 'path': '/', + 'httpOnly': True, + 'secure': False, # 如有 https 可改 True + } + ]) + page = context.new_page() + try: + page.goto(url, wait_until='networkidle') + pdf_bytes = page.pdf(format="A4", landscape=True, + margin={"top": "35px", "bottom": "30px", "left": "20px", "right": "20px"}) + except Exception as e: + print(f'Playwright error: {e}') + pdf_bytes = None + finally: + browser.close() + return pdf_bytes, chart_info['title'] + + +@method_decorator(csrf_exempt, name='dispatch') +class ExportPdfView(View): + def get(self, request): + chart_name = request.GET.get('chart') + return self._handle_export(request, chart_name) + + def post(self, request): + chart_name = request.POST.get('chart') + return self._handle_export(request, chart_name) + + def _handle_export(self, request, chart_name): + if not chart_name: + return HttpResponseBadRequest('Missing chart parameter') + sessionid = request.COOKIES.get(settings.SESSION_COOKIE_NAME) + if not sessionid: + return HttpResponseBadRequest('No sessionid found in cookies') + + pdf_bytes, title = export_chart_to_pdf(chart_name, sessionid, request=request) + if not pdf_bytes: + return HttpResponseBadRequest('Failed to generate PDF') + filename = f"{title}-{timezone.now().strftime('%Y%m%d%H%M%S')}.pdf" + response = FileResponse(io.BytesIO(pdf_bytes), as_attachment=True, filename=filename, + content_type='application/pdf') + return response + + +class SendMailView(View): + def get(self, request): + chart_name = request.GET.get('chart') + email = "ibuler@qq.com" + if not chart_name or not email: + return HttpResponseBadRequest('Missing chart or email parameter') + sessionid = request.COOKIES.get(settings.SESSION_COOKIE_NAME) + if not sessionid: + return HttpResponseBadRequest('No sessionid found in cookies') + + # 1. 生成 PDF + pdf_bytes, title = export_chart_to_pdf(chart_name, sessionid, request=request) + if not pdf_bytes: + return HttpResponseBadRequest('Failed to generate PDF') + + # 2. PDF 转图片 + images = convert_from_bytes(pdf_bytes, dpi=200) + # 3. 图片转 base64 + img_tags = [] + for img in images: + buffer = BytesIO() + img.save(buffer, format="PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("utf-8") + img_tags.append(f'') + html_content = "
".join(img_tags) + + # 4. 发送邮件 + subject = f"{title} 报表" + from_email = settings.EMAIL_HOST_USER + to = [email] + msg = EmailMultiAlternatives(subject, '', from_email, to) + msg.attach_alternative(html_content, "text/html") + filename = f"{title}-{timezone.now().strftime('%Y%m%d%H%M%S')}.pdf" + msg.attach(filename, pdf_bytes, "application/pdf") + msg.send() + + return JsonResponse({"message": "邮件发送成功"}) diff --git a/pyproject.toml b/pyproject.toml index aa0e98bd5..080d7c3bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,6 +152,8 @@ dependencies = [ 'botocore==1.31.9', 's3transfer==0.6.1', 'xmlsec==1.3.14', + 'playwright==4.12.2', + 'pdf2image==1.17.0' ] [project.urls] diff --git a/requirements/mac_pkg.sh b/requirements/mac_pkg.sh index c70080059..3e19608d9 100644 --- a/requirements/mac_pkg.sh +++ b/requirements/mac_pkg.sh @@ -5,16 +5,13 @@ PROJECT_DIR=$(dirname "$BASE_DIR") echo "1. 安装依赖" brew install libtiff libjpeg webp little-cms2 openssl gettext git \ git-lfs libxml2 libxmlsec1 pkg-config postgresql freetds openssl \ - libffi freerdp + libffi freerdp poppler pip install daphne==4.0.0 channels channels-redis echo "2. 下载 IP 数据库" ip_db_path="${PROJECT_DIR}/apps/common/utils/geoip/GeoLite2-City.mmdb" wget "https://download.jumpserver.org/files/GeoLite2-City.mmdb" -O "${ip_db_path}" -echo "3. 安装依赖的插件" -git lfs install - if ! uname -a | grep 'ARM64' &> /dev/null;then exit 0 fi