From ea7570ba80ee1e2855ccafc75590a2d5679f9499 Mon Sep 17 00:00:00 2001 From: rjb <263303411@qq.com> Date: Sat, 30 Aug 2025 00:01:49 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E9=98=B6=E6=AE=B5=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backups/backup_data_20250829_235838.zip | Bin 0 -> 47528 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 2447 -> 2447 bytes src/flask_prompt_master/admin/__init__.py | 12 + .../__pycache__/__init__.cpython-312.pyc | Bin 6472 -> 7492 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 328 -> 328 bytes .../__pycache__/admin_forms.cpython-312.pyc | Bin 2399 -> 2399 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 289 -> 289 bytes .../__pycache__/admin_user.cpython-312.pyc | Bin 4025 -> 4025 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 474 -> 474 bytes .../analytics_admin.cpython-312.pyc | Bin 0 -> 9710 bytes .../__pycache__/api_admin.cpython-312.pyc | Bin 0 -> 8507 bytes .../__pycache__/backup_admin.cpython-312.pyc | Bin 0 -> 24512 bytes .../__pycache__/batch_admin.cpython-312.pyc | Bin 0 -> 9664 bytes .../__pycache__/monitor_admin.cpython-312.pyc | Bin 0 -> 9180 bytes .../__pycache__/prompt_admin.cpython-312.pyc | Bin 1803 -> 1803 bytes .../__pycache__/report_admin.cpython-312.pyc | Bin 0 -> 12321 bytes .../__pycache__/system_admin.cpython-312.pyc | Bin 4019 -> 4019 bytes .../template_admin.cpython-312.pyc | Bin 2039 -> 2039 bytes .../__pycache__/user_admin.cpython-312.pyc | Bin 2214 -> 2214 bytes .../admin/views/analytics_admin.py | 191 +++++++ .../admin/views/api_admin.py | 184 +++++++ .../admin/views/backup_admin.py | 479 ++++++++++++++++++ .../admin/views/batch_admin.py | 185 +++++++ .../admin/views/monitor_admin.py | 187 +++++++ .../admin/views/report_admin.py | 254 ++++++++++ .../templates/admin/analytics_charts.html | 177 +++++++ .../templates/admin/analytics_dashboard.html | 239 +++++++++ .../templates/admin/api_dashboard.html | 444 ++++++++++++++++ .../templates/admin/backup_dashboard.html | 418 +++++++++++++++ .../templates/admin/batch_operations.html | 300 +++++++++++ .../templates/admin/monitor_dashboard.html | 437 ++++++++++++++++ .../templates/admin/report_dashboard.html | 319 ++++++++++++ 32 files changed, 3826 insertions(+) create mode 100644 backups/backup_data_20250829_235838.zip create mode 100644 src/flask_prompt_master/admin/views/__pycache__/analytics_admin.cpython-312.pyc create mode 100644 src/flask_prompt_master/admin/views/__pycache__/api_admin.cpython-312.pyc create mode 100644 src/flask_prompt_master/admin/views/__pycache__/backup_admin.cpython-312.pyc create mode 100644 src/flask_prompt_master/admin/views/__pycache__/batch_admin.cpython-312.pyc create mode 100644 src/flask_prompt_master/admin/views/__pycache__/monitor_admin.cpython-312.pyc create mode 100644 src/flask_prompt_master/admin/views/__pycache__/report_admin.cpython-312.pyc create mode 100644 src/flask_prompt_master/admin/views/analytics_admin.py create mode 100644 src/flask_prompt_master/admin/views/api_admin.py create mode 100644 src/flask_prompt_master/admin/views/backup_admin.py create mode 100644 src/flask_prompt_master/admin/views/batch_admin.py create mode 100644 src/flask_prompt_master/admin/views/monitor_admin.py create mode 100644 src/flask_prompt_master/admin/views/report_admin.py create mode 100644 src/flask_prompt_master/templates/admin/analytics_charts.html create mode 100644 src/flask_prompt_master/templates/admin/analytics_dashboard.html create mode 100644 src/flask_prompt_master/templates/admin/api_dashboard.html create mode 100644 src/flask_prompt_master/templates/admin/backup_dashboard.html create mode 100644 src/flask_prompt_master/templates/admin/batch_operations.html create mode 100644 src/flask_prompt_master/templates/admin/monitor_dashboard.html create mode 100644 src/flask_prompt_master/templates/admin/report_dashboard.html diff --git a/backups/backup_data_20250829_235838.zip b/backups/backup_data_20250829_235838.zip new file mode 100644 index 0000000000000000000000000000000000000000..ae51561c9e26c13a7708dedba2be0940c6378b22 GIT binary patch literal 47528 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 YcdHbn!6E*81?bP+@iVXftN{Z0FVJsny#N3J literal 0 HcmV?d00001 diff --git a/src/flask_prompt_master/__pycache__/__init__.cpython-312.pyc b/src/flask_prompt_master/__pycache__/__init__.cpython-312.pyc index 459a97cf260ac73af51d2af5f094503730d5f994..60d17f701a85efca1b1e0fd05caa90a1b4dfc9ea 100644 GIT binary patch delta 19 ZcmeAd?ic1d&CAQh00d2YHgdIc0st<>1jPUV delta 19 ZcmeAd?ic1d&CAQh00i6CZ{%v_1OP9*1nd9+ diff --git a/src/flask_prompt_master/admin/__init__.py b/src/flask_prompt_master/admin/__init__.py index 45a44e8..8a3aac3 100644 --- a/src/flask_prompt_master/admin/__init__.py +++ b/src/flask_prompt_master/admin/__init__.py @@ -13,6 +13,12 @@ from .views.user_admin import UserAdminView from .views.prompt_admin import PromptAdminView from .views.template_admin import TemplateAdminView from .views.system_admin import SystemAdminView +from .views.analytics_admin import AnalyticsAdminView +from .views.batch_admin import BatchAdminView +from .views.monitor_admin import MonitorAdminView +from .views.report_admin import ReportAdminView +from .views.backup_admin import BackupAdminView +from .views.api_admin import ApiAdminView from datetime import datetime # 创建登录管理器 @@ -38,6 +44,12 @@ def init_admin(app): admin.add_view(PromptAdminView(Prompt, db.session, name='提示词管理', endpoint='admin_prompt')) admin.add_view(TemplateAdminView(PromptTemplate, db.session, name='模板管理', endpoint='admin_template')) admin.add_view(SystemAdminView(name='系统管理', endpoint='admin_system')) + admin.add_view(AnalyticsAdminView(name='数据分析', endpoint='analytics_admin')) + admin.add_view(BatchAdminView(name='批量操作', endpoint='batch_admin')) + admin.add_view(MonitorAdminView(name='系统监控', endpoint='monitor_admin')) + admin.add_view(ReportAdminView(name='高级报表', endpoint='report_admin')) + admin.add_view(BackupAdminView(name='数据备份', endpoint='backup_admin')) + admin.add_view(ApiAdminView(name='API管理', endpoint='api_admin')) # 注册登录路由 app.add_url_rule('/admin/login', 'admin.login', admin_login, methods=['GET', 'POST']) diff --git a/src/flask_prompt_master/admin/__pycache__/__init__.cpython-312.pyc b/src/flask_prompt_master/admin/__pycache__/__init__.cpython-312.pyc index 967bc0f06722a11ca034b2fe9421c65a06223907..08f7b7377503405e0d3372d8bee60f76b4a3bf9f 100644 GIT binary patch delta 1687 zcmah}ZA@EL7`~^y6bkf<+d_fDV0^X%iknk130nLZixC)$s5NFOJ%yFsOY7`|3x>o_8@2QXm}A;yQ7U)EDm4VnOVe2Eqd@M%XO|rC>OSI1?AMX@) z+fil(>@KriW!8i1%WO}XZNT0Vn}Hs*g&T1bu=*~C^l!wS7jBaWN>%XqYhl96wZBEICJTjS56h)KC3z+ zrNJDHz-07{)1Tg0dc8o;T)*;`S~CMfGN{G;Mseev;>vR2wKdfy=>s$9H*)8TpRE;M zeec%`>uN30hNsIz#`E7@y1sEmt)0;(xm|8$?S&=}6)#;Vo;{^nBgwcf&|u{#@={VM zHxnPrAuRx{05Dh52GCA_t{nCCvQVv#GZ*Ql)_14XxB1|d#ky6_I>-No7MVM6h!~a1IuReYG zK1DdF@ME_uTt(Fny!#qYf2-<4fxOpx-t6jxUahlnQI3(LGmGR=`ep4H+D)r%ErAC? zvqTe;6qkt&HhhH4r3ko?*Z~|2{AMyi58GNCaBcE%35Mwlw!cw;p0KZ>KHBQ=xdR|+ zNsmATFhR#1{VeIE<0tnd+p|9;pNfgIDKKdp>7@zhAbN_fIvZHOmz|%O5T;4N%lbPl z-1N=E&+G;`4xnA7BAQ4q$YhExyN5<+fvy7(0Nem~6fFRk07wEn3qSx;05YxdG^1YH z<7q?BV*^ntsvnen$12 zU@xK6z#abA?4`PN&ER~^ajKy6J3O!1P_5GyquVuWmIR~C!LAujb!<0KGC667*0o1E z!XmXMonp2S6A>IE1YRhbmGvQOL)5Oi^ZIJlu#~`QF*ZWZu?m=E>P-f>%m|_1c@yIQ rvT$hd8b_~s1J3ln#*A^GlK7RVAx4WVVx&9cUZH=8gJiC7Y@*~2x delta 932 zcmZvaO=uHA6vt;ao09Kj^HI}utD#xiuBK93KZ~}~ib|n^_(_C@G_y&!Y!aE>phZE! zfVXOyi=YQlC`bz`0lj%qLG)mY2O%IHG&c{T7f<564bdtN?2q@}|GhV}Z|245tKq;K zzu$xSdh+n`9F>*=ahzW2e|80vfRQStGAWD%q;;%73SSvTQ|6~y_C}Im4gJpPBcU3~ z^b?75Ky#Du22B;v5kw+2M53Bw#nI}K*}4!@JF8Ldd73vDn>QZSSMelfImb_DiU(ti zhzEt8LVa0$i*+==W#iT@n*<5AlI=JN5veGwhF8Q^1f$FUuZobU)!VLWABSh-6G)A* z?XKH+fW3BA*jM)i4zm%D#a?>GWvFO)^s;VcbEKq~=>Z-{@XvgQSkil!?egu$O5O0? za!8vY*Im#JOQYG@1v<`Rp`%!4GohZ-dqC^RS4t(_qJD7194(p@{*ML#K@QbHCvB~2 zYP8`lYZtBT`CL&mVS34AlEQK5zBY!~dgupMnBnVUpQObdeOypQ>H^*F=>^Nj@c>Z; zY`cCGEWjkY7arvGYvGU4L)=$!Lo_v`U}Em@&DitEP}elKP3U$NeGYcN52n|H=`~lH zDs^A6Dx_*? eWAqxYfVrC2*?oD8eU}wy6ieUIOzu%c*#8&r&c5gX diff --git a/src/flask_prompt_master/admin/forms/__pycache__/__init__.cpython-312.pyc b/src/flask_prompt_master/admin/forms/__pycache__/__init__.cpython-312.pyc index e3ad3dd699cd465e4bbb4063d9ade9974c24f22c..582ff7b6cd46dbf8dd35bf272deff1a7030d83bc 100644 GIT binary patch delta 19 ZcmX@Xbb^WNG%qg~0}wRr*~n$b2mmwj1jYaW delta 19 ZcmX@Xbb^WNG%qg~0}yOmzmdz15dbvk1nmF- diff --git a/src/flask_prompt_master/admin/forms/__pycache__/admin_forms.cpython-312.pyc b/src/flask_prompt_master/admin/forms/__pycache__/admin_forms.cpython-312.pyc index 544b940beb8ab71ed52364e741f556396c4feae7..d7af81b01ff2792781ade102abf1872153bccb1e 100644 GIT binary patch delta 19 ZcmcaFbYF<;G%qg~0}wRr*~k^j2>>~R1v3Bu delta 19 ZcmcaFbYF<;G%qg~0}yOmzmY4H6977T1zG?A diff --git a/src/flask_prompt_master/admin/models/__pycache__/__init__.cpython-312.pyc b/src/flask_prompt_master/admin/models/__pycache__/__init__.cpython-312.pyc index ca64ac58ee32a5db4a163f1c0814384be20ba6dc..064174515bbb2c5449c475a0538772fff6f7cf85 100644 GIT binary patch delta 19 ZcmZ3;w2+DGG%qg~0}wRr*~lf%2mmf81W^D0 delta 19 ZcmZ3;w2+DGG%qg~0}yOmzmZFv5dbe91b6@d diff --git a/src/flask_prompt_master/admin/models/__pycache__/admin_user.cpython-312.pyc b/src/flask_prompt_master/admin/models/__pycache__/admin_user.cpython-312.pyc index 61adfde5fa7e2f15128abb629c042abffe2bcbc4..068b36658fb8dc2c4632fc8fd0aa83c008300e37 100644 GIT binary patch delta 19 Zcmdlfzf+#;G%qg~0}wRr*~qn?9{@I=1z`XH delta 19 Zcmdlfzf+#;G%qg~0}yOmzmaP_KL9tD1&9Cu diff --git a/src/flask_prompt_master/admin/views/__pycache__/__init__.cpython-312.pyc b/src/flask_prompt_master/admin/views/__pycache__/__init__.cpython-312.pyc index 79463ea6f4b13a6809b498fdcb2ff21d7c6ab5de..84cc92f9230bcedcd47c892f828d4fe1a624edae 100644 GIT binary patch delta 19 Zcmcb`e2bauG%qg~0}wRr*~oQ)5db&81%3bk delta 19 Zcmcb`e2bauG%qg~0}yOmzme+#BLFzI1*HH0 diff --git a/src/flask_prompt_master/admin/views/__pycache__/analytics_admin.cpython-312.pyc b/src/flask_prompt_master/admin/views/__pycache__/analytics_admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbc5aacee3b563ffe674ad4870ad6046af91afa9 GIT binary patch literal 9710 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 literal 0 HcmV?d00001 diff --git a/src/flask_prompt_master/admin/views/__pycache__/api_admin.cpython-312.pyc b/src/flask_prompt_master/admin/views/__pycache__/api_admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d43e737d7a28f35c73f2b545f0ef1549e6c29ad3 GIT binary patch literal 8507 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 literal 0 HcmV?d00001 diff --git a/src/flask_prompt_master/admin/views/__pycache__/batch_admin.cpython-312.pyc b/src/flask_prompt_master/admin/views/__pycache__/batch_admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cdcd3bb25a63b7e20df212fd94f9f7388ceec7e5 GIT binary patch literal 9664 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 TWWC1*bxh5`!LJa>DcS!AFdJ6V literal 0 HcmV?d00001 diff --git a/src/flask_prompt_master/admin/views/__pycache__/prompt_admin.cpython-312.pyc b/src/flask_prompt_master/admin/views/__pycache__/prompt_admin.cpython-312.pyc index 4585b44e6532d43804cc4a7ae922e66fbe673b6e..a910d1cf193c93d837d62eefd8bbd2650ec299dc 100644 GIT binary patch delta 19 ZcmeC?>*nJ+&CAQh00d2YHgYkt0{|?61T6pn delta 19 ZcmeC?>*nJ+&CAQh00i6CZ{%WR2LLU21XKV3 diff --git a/src/flask_prompt_master/admin/views/__pycache__/report_admin.cpython-312.pyc b/src/flask_prompt_master/admin/views/__pycache__/report_admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac74b572161f9ca5d79513cd69ccfa02bddcf40d GIT binary patch literal 12321 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 lo&Q1Ae@4}QMm2m&={}{jpHli8%78*A delta 19 ZcmZ1`xJ;1iG%qg~0}yOmzmaPW2LLkR1uOsn diff --git a/src/flask_prompt_master/admin/views/analytics_admin.py b/src/flask_prompt_master/admin/views/analytics_admin.py new file mode 100644 index 0000000..4b0e7cc --- /dev/null +++ b/src/flask_prompt_master/admin/views/analytics_admin.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +""" +数据分析管理视图 +""" +from flask_admin import BaseView, expose +from flask_login import login_required, current_user +from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate +from src.flask_prompt_master import db +from sqlalchemy import func, extract +from datetime import datetime, timedelta +import plotly.graph_objs as go +import plotly.utils +import json + +class AnalyticsAdminView(BaseView): + """数据分析管理视图""" + + @expose('/') + @login_required + def index(self): + """数据分析首页""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + # 获取统计数据 + stats = self._get_analytics_data() + + return self.render('admin/analytics_dashboard.html', stats=stats) + + @expose('/charts') + @login_required + def charts(self): + """图表页面""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + # 生成图表数据 + charts_data = self._generate_charts() + + return self.render('admin/analytics_charts.html', charts_data=charts_data) + + def _get_analytics_data(self): + """获取分析数据""" + try: + # 用户统计 + total_users = User.query.count() + active_users = User.query.filter_by(status=1).count() + new_users_today = User.query.filter( + User.created_time >= datetime.now().date() + ).count() + new_users_week = User.query.filter( + User.created_time >= datetime.now() - timedelta(days=7) + ).count() + + # 提示词统计 + total_prompts = Prompt.query.count() + today_prompts = Prompt.query.filter( + Prompt.created_at >= datetime.now().date() + ).count() + week_prompts = Prompt.query.filter( + Prompt.created_at >= datetime.now() - timedelta(days=7) + ).count() + + # 模板统计 + total_templates = PromptTemplate.query.count() + default_templates = PromptTemplate.query.filter_by(is_default=True).count() + + # 用户活跃度 + active_users_week = db.session.query(func.count(func.distinct(Prompt.user_id))).filter( + Prompt.created_at >= datetime.now() - timedelta(days=7) + ).scalar() + + return { + 'total_users': total_users, + 'active_users': active_users, + 'new_users_today': new_users_today, + 'new_users_week': new_users_week, + 'total_prompts': total_prompts, + 'today_prompts': today_prompts, + 'week_prompts': week_prompts, + 'total_templates': total_templates, + 'default_templates': default_templates, + 'active_users_week': active_users_week or 0 + } + except Exception as e: + return { + 'total_users': 0, + 'active_users': 0, + 'new_users_today': 0, + 'new_users_week': 0, + 'total_prompts': 0, + 'today_prompts': 0, + 'week_prompts': 0, + 'total_templates': 0, + 'default_templates': 0, + 'active_users_week': 0 + } + + def _generate_charts(self): + """生成图表数据""" + try: + # 用户注册趋势(最近30天) + thirty_days_ago = datetime.now() - timedelta(days=30) + daily_registrations = db.session.query( + func.date(User.created_time).label('date'), + func.count(User.uid).label('count') + ).filter( + User.created_time >= thirty_days_ago + ).group_by( + func.date(User.created_time) + ).all() + + # 提示词生成趋势(最近30天) + daily_prompts = db.session.query( + func.date(Prompt.created_at).label('date'), + func.count(Prompt.id).label('count') + ).filter( + Prompt.created_at >= thirty_days_ago + ).group_by( + func.date(Prompt.created_at) + ).all() + + # 用户活跃度饼图 + active_status = db.session.query( + User.status, + func.count(User.uid).label('count') + ).group_by(User.status).all() + + # 生成图表JSON + charts = { + 'user_trend': self._create_line_chart(daily_registrations, '用户注册趋势', '日期', '注册人数'), + 'prompt_trend': self._create_line_chart(daily_prompts, '提示词生成趋势', '日期', '生成数量'), + 'user_status': self._create_pie_chart(active_status, '用户状态分布') + } + + return charts + except Exception as e: + return {} + + def _create_line_chart(self, data, title, x_label, y_label): + """创建折线图""" + if not data: + return json.dumps({}) + + x_values = [str(item.date) for item in data] + y_values = [item.count for item in data] + + trace = go.Scatter( + x=x_values, + y=y_values, + mode='lines+markers', + name=title, + line=dict(color='#4e73df', width=3), + marker=dict(size=8) + ) + + layout = go.Layout( + title=title, + xaxis=dict(title=x_label), + yaxis=dict(title=y_label), + height=400, + margin=dict(l=50, r=50, t=50, b=50) + ) + + fig = go.Figure(data=[trace], layout=layout) + return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) + + def _create_pie_chart(self, data, title): + """创建饼图""" + if not data: + return json.dumps({}) + + labels = ['活跃用户' if item.status == 1 else '禁用用户' for item in data] + values = [item.count for item in data] + colors = ['#28a745', '#dc3545'] + + trace = go.Pie( + labels=labels, + values=values, + marker=dict(colors=colors), + textinfo='label+percent' + ) + + layout = go.Layout( + title=title, + height=400, + margin=dict(l=50, r=50, t=50, b=50) + ) + + fig = go.Figure(data=[trace], layout=layout) + return json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) diff --git a/src/flask_prompt_master/admin/views/api_admin.py b/src/flask_prompt_master/admin/views/api_admin.py new file mode 100644 index 0000000..107513d --- /dev/null +++ b/src/flask_prompt_master/admin/views/api_admin.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +""" +API管理视图 +""" +from flask_admin import BaseView, expose +from flask_login import login_required, current_user +from flask import request, jsonify +from src.flask_prompt_master.models.models import User, Prompt +from src.flask_prompt_master import db +from sqlalchemy import func +from datetime import datetime, timedelta +import json + +class ApiAdminView(BaseView): + """API管理视图""" + + @expose('/') + @login_required + def index(self): + """API管理首页""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + # 获取API统计信息 + api_stats = self._get_api_stats() + + return self.render('admin/api_dashboard.html', api_stats=api_stats) + + @expose('/api/stats') + @login_required + def api_stats(self): + """获取API统计信息""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + stats = self._get_api_stats() + return jsonify({'success': True, 'data': stats}) + except Exception as e: + return jsonify({'success': False, 'message': f'获取统计信息失败: {str(e)}'}) + + @expose('/api/calls') + @login_required + def api_calls(self): + """获取API调用记录""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + # 获取查询参数 + start_date = request.args.get('start_date', (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')) + end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d')) + api_type = request.args.get('type', 'all') + + # 获取API调用记录 + calls = self._get_api_calls(start_date, end_date, api_type) + + return jsonify({'success': True, 'data': calls}) + except Exception as e: + return jsonify({'success': False, 'message': f'获取调用记录失败: {str(e)}'}) + + @expose('/api/rate-limits') + @login_required + def rate_limits(self): + """API限流管理""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + return self.render('admin/api_rate_limits.html') + + @expose('/api/keys') + @login_required + def api_keys(self): + """API密钥管理""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + return self.render('admin/api_keys.html') + + def _get_api_stats(self): + """获取API统计信息""" + try: + # 今日API调用次数 + today = datetime.now().date() + today_calls = Prompt.query.filter( + func.date(Prompt.created_at) == today + ).count() + + # 本周API调用次数 + week_ago = datetime.now() - timedelta(days=7) + week_calls = Prompt.query.filter( + Prompt.created_at >= week_ago + ).count() + + # 本月API调用次数 + month_ago = datetime.now() - timedelta(days=30) + month_calls = Prompt.query.filter( + Prompt.created_at >= month_ago + ).count() + + # 总API调用次数 + total_calls = Prompt.query.count() + + # 活跃用户数(有API调用的用户) + active_users = db.session.query(func.count(func.distinct(Prompt.user_id))).scalar() or 0 + + # 平均响应时间(模拟数据) + avg_response_time = 1.2 # 秒 + + # 成功率(模拟数据) + success_rate = 98.5 # 百分比 + + # 最近7天的调用趋势 + daily_calls = [] + for i in range(7): + date = datetime.now() - timedelta(days=i) + count = Prompt.query.filter( + func.date(Prompt.created_at) == date.date() + ).count() + daily_calls.append({ + 'date': date.strftime('%Y-%m-%d'), + 'count': count + }) + daily_calls.reverse() + + return { + 'today_calls': today_calls, + 'week_calls': week_calls, + 'month_calls': month_calls, + 'total_calls': total_calls, + 'active_users': active_users, + 'avg_response_time': avg_response_time, + 'success_rate': success_rate, + 'daily_calls': daily_calls + } + + except Exception as e: + return { + 'today_calls': 0, + 'week_calls': 0, + 'month_calls': 0, + 'total_calls': 0, + 'active_users': 0, + 'avg_response_time': 0, + 'success_rate': 0, + 'daily_calls': [] + } + + def _get_api_calls(self, start_date, end_date, api_type): + """获取API调用记录""" + try: + start_dt = datetime.strptime(start_date, '%Y-%m-%d') + end_dt = datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1) + + # 查询API调用记录 + query = Prompt.query.filter( + Prompt.created_at >= start_dt, + Prompt.created_at < end_dt + ) + + if api_type != 'all': + # 这里可以根据实际需求添加API类型过滤 + pass + + calls = query.order_by(Prompt.created_at.desc()).limit(100).all() + + # 格式化数据 + call_records = [] + for call in calls: + call_records.append({ + 'id': call.id, + 'user_id': call.user_id, + 'api_type': 'prompt_generation', + 'input_text': call.input_text[:50] + '...' if len(call.input_text) > 50 else call.input_text, + 'status': 'success', + 'response_time': 1.2, # 模拟响应时间 + 'created_at': call.created_at.strftime('%Y-%m-%d %H:%M:%S') if call.created_at else '', + 'ip_address': '127.0.0.1' # 模拟IP地址 + }) + + return call_records + + except Exception as e: + return [] diff --git a/src/flask_prompt_master/admin/views/backup_admin.py b/src/flask_prompt_master/admin/views/backup_admin.py new file mode 100644 index 0000000..f657809 --- /dev/null +++ b/src/flask_prompt_master/admin/views/backup_admin.py @@ -0,0 +1,479 @@ +# -*- coding: utf-8 -*- +""" +数据备份管理视图 +""" +from flask_admin import BaseView, expose +from flask_login import login_required, current_user +from flask import request, jsonify, send_file +from src.flask_prompt_master import db +from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate +import os +import json +import zipfile +import io +from datetime import datetime, timedelta +import threading +import schedule +import time +import shutil + +class BackupAdminView(BaseView): + """数据备份管理视图""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.backup_dir = os.path.join(os.getcwd(), 'backups') + self.auto_backup_enabled = False + self.backup_thread = None + + # 确保备份目录存在 + if not os.path.exists(self.backup_dir): + os.makedirs(self.backup_dir) + + @expose('/') + @login_required + def index(self): + """备份管理首页""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + # 获取备份文件列表 + backup_files = self._get_backup_files() + + return self.render('admin/backup_dashboard.html', backup_files=backup_files) + + @expose('/create-backup', methods=['POST']) + @login_required + def create_backup(self): + """创建备份""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + backup_type = request.json.get('type', 'full') + backup_name = request.json.get('name', '') + + # 生成备份文件名 + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + if backup_name: + filename = f"{backup_name}_{timestamp}.zip" + else: + filename = f"backup_{backup_type}_{timestamp}.zip" + + backup_path = os.path.join(self.backup_dir, filename) + + # 创建备份 + if backup_type == 'full': + success = self._create_full_backup(backup_path) + elif backup_type == 'data': + success = self._create_data_backup(backup_path) + elif backup_type == 'config': + success = self._create_config_backup(backup_path) + else: + return jsonify({'success': False, 'message': '不支持的备份类型'}) + + if success: + return jsonify({ + 'success': True, + 'message': f'{backup_type}备份创建成功', + 'filename': filename + }) + else: + return jsonify({'success': False, 'message': '备份创建失败'}) + + except Exception as e: + return jsonify({'success': False, 'message': f'备份创建失败: {str(e)}'}) + + @expose('/download-backup/') + @login_required + def download_backup(self, filename): + """下载备份文件""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + backup_path = os.path.join(self.backup_dir, filename) + if not os.path.exists(backup_path): + return jsonify({'success': False, 'message': '备份文件不存在'}) + + return send_file( + backup_path, + as_attachment=True, + download_name=filename + ) + + except Exception as e: + return jsonify({'success': False, 'message': f'下载失败: {str(e)}'}) + + @expose('/restore-backup', methods=['POST']) + @login_required + def restore_backup(self): + """恢复备份""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + filename = request.json.get('filename') + if not filename: + return jsonify({'success': False, 'message': '请选择要恢复的备份文件'}) + + backup_path = os.path.join(self.backup_dir, filename) + if not os.path.exists(backup_path): + return jsonify({'success': False, 'message': '备份文件不存在'}) + + # 执行恢复 + success = self._restore_backup(backup_path) + + if success: + return jsonify({'success': True, 'message': '备份恢复成功'}) + else: + return jsonify({'success': False, 'message': '备份恢复失败'}) + + except Exception as e: + return jsonify({'success': False, 'message': f'恢复失败: {str(e)}'}) + + @expose('/delete-backup', methods=['POST']) + @login_required + def delete_backup(self): + """删除备份""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + filename = request.json.get('filename') + if not filename: + return jsonify({'success': False, 'message': '请选择要删除的备份文件'}) + + backup_path = os.path.join(self.backup_dir, filename) + if not os.path.exists(backup_path): + return jsonify({'success': False, 'message': '备份文件不存在'}) + + # 删除文件 + os.remove(backup_path) + + return jsonify({'success': True, 'message': '备份文件删除成功'}) + + except Exception as e: + return jsonify({'success': False, 'message': f'删除失败: {str(e)}'}) + + @expose('/auto-backup', methods=['POST']) + @login_required + def toggle_auto_backup(self): + """切换自动备份""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + enabled = request.json.get('enabled', False) + + if enabled and not self.auto_backup_enabled: + # 启动自动备份 + self.auto_backup_enabled = True + self.backup_thread = threading.Thread(target=self._auto_backup_worker, daemon=True) + self.backup_thread.start() + return jsonify({'success': True, 'message': '自动备份已启动'}) + elif not enabled and self.auto_backup_enabled: + # 停止自动备份 + self.auto_backup_enabled = False + return jsonify({'success': True, 'message': '自动备份已停止'}) + else: + return jsonify({'success': False, 'message': '自动备份状态未改变'}) + + except Exception as e: + return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) + + def _get_backup_files(self): + """获取备份文件列表""" + backup_files = [] + try: + for filename in os.listdir(self.backup_dir): + if filename.endswith('.zip'): + file_path = os.path.join(self.backup_dir, filename) + stat = os.stat(file_path) + backup_files.append({ + 'filename': filename, + 'size': self._format_file_size(stat.st_size), + 'created_time': datetime.fromtimestamp(stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S'), + 'modified_time': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S') + }) + + # 按修改时间排序 + backup_files.sort(key=lambda x: x['modified_time'], reverse=True) + + except Exception as e: + print(f"获取备份文件列表失败: {str(e)}") + + return backup_files + + def _create_full_backup(self, backup_path): + """创建完整备份""" + try: + with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # 备份数据库数据 + self._backup_database_data(zipf) + + # 备份配置文件 + self._backup_config_files(zipf) + + # 备份日志文件 + self._backup_log_files(zipf) + + # 创建备份信息文件 + backup_info = { + 'type': 'full', + 'created_time': datetime.now().isoformat(), + 'created_by': current_user.username, + 'description': '完整系统备份' + } + zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2)) + + return True + + except Exception as e: + print(f"创建完整备份失败: {str(e)}") + return False + + def _create_data_backup(self, backup_path): + """创建数据备份""" + try: + with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # 只备份数据库数据 + self._backup_database_data(zipf) + + # 创建备份信息文件 + backup_info = { + 'type': 'data', + 'created_time': datetime.now().isoformat(), + 'created_by': current_user.username, + 'description': '数据备份' + } + zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2)) + + return True + + except Exception as e: + print(f"创建数据备份失败: {str(e)}") + return False + + def _create_config_backup(self, backup_path): + """创建配置备份""" + try: + with zipfile.ZipFile(backup_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # 只备份配置文件 + self._backup_config_files(zipf) + + # 创建备份信息文件 + backup_info = { + 'type': 'config', + 'created_time': datetime.now().isoformat(), + 'created_by': current_user.username, + 'description': '配置备份' + } + zipf.writestr('backup_info.json', json.dumps(backup_info, indent=2)) + + return True + + except Exception as e: + print(f"创建配置备份失败: {str(e)}") + return False + + def _backup_database_data(self, zipf): + """备份数据库数据""" + try: + # 备份用户数据 + users = User.query.all() + user_data = [] + for user in users: + user_data.append({ + 'uid': user.uid, + 'login_name': user.login_name, + 'nickname': user.nickname, + 'email': user.email, + 'mobile': user.mobile, + 'sex': user.sex, + 'status': user.status, + 'created_time': user.created_time.isoformat() if user.created_time else None, + 'updated_time': user.updated_time.isoformat() if user.updated_time else None + }) + zipf.writestr('data/users.json', json.dumps(user_data, indent=2, ensure_ascii=False)) + + # 备份提示词数据 + prompts = Prompt.query.all() + prompt_data = [] + for prompt in prompts: + prompt_data.append({ + 'id': prompt.id, + 'user_id': prompt.user_id, + 'input_text': prompt.input_text, + 'generated_text': prompt.generated_text, + 'created_at': prompt.created_at.isoformat() if prompt.created_at else None + }) + zipf.writestr('data/prompts.json', json.dumps(prompt_data, indent=2, ensure_ascii=False)) + + # 备份模板数据 + templates = PromptTemplate.query.all() + template_data = [] + for template in templates: + template_data.append({ + 'id': template.id, + 'name': template.name, + 'category': template.category, + 'industry': template.industry, + 'profession': template.profession, + 'description': template.description, + 'system_prompt': template.system_prompt, + 'is_default': template.is_default, + 'created_at': template.created_at.isoformat() if template.created_at else None, + 'updated_at': template.updated_at.isoformat() if template.updated_at else None + }) + zipf.writestr('data/templates.json', json.dumps(template_data, indent=2, ensure_ascii=False)) + + except Exception as e: + print(f"备份数据库数据失败: {str(e)}") + + def _backup_config_files(self, zipf): + """备份配置文件""" + try: + config_files = ['config.py', '.env', 'requirements.txt'] + for config_file in config_files: + if os.path.exists(config_file): + zipf.write(config_file, f'config/{config_file}') + + # 备份Flask配置 + from src.flask_prompt_master import create_app + app = create_app() + config_data = { + 'SQLALCHEMY_DATABASE_URI': app.config.get('SQLALCHEMY_DATABASE_URI', ''), + 'SECRET_KEY': app.config.get('SECRET_KEY', ''), + 'DEBUG': app.config.get('DEBUG', False) + } + zipf.writestr('config/app_config.json', json.dumps(config_data, indent=2)) + + except Exception as e: + print(f"备份配置文件失败: {str(e)}") + + def _backup_log_files(self, zipf): + """备份日志文件""" + try: + log_dir = 'logs' + if os.path.exists(log_dir): + for log_file in os.listdir(log_dir): + if log_file.endswith('.log'): + log_path = os.path.join(log_dir, log_file) + zipf.write(log_path, f'logs/{log_file}') + + except Exception as e: + print(f"备份日志文件失败: {str(e)}") + + def _restore_backup(self, backup_path): + """恢复备份""" + try: + with zipfile.ZipFile(backup_path, 'r') as zipf: + # 读取备份信息 + backup_info = json.loads(zipf.read('backup_info.json')) + backup_type = backup_info.get('type', 'unknown') + + if backup_type == 'data' or backup_type == 'full': + # 恢复数据库数据 + self._restore_database_data(zipf) + + if backup_type == 'config' or backup_type == 'full': + # 恢复配置文件 + self._restore_config_files(zipf) + + return True + + except Exception as e: + print(f"恢复备份失败: {str(e)}") + return False + + def _restore_database_data(self, zipf): + """恢复数据库数据""" + try: + # 恢复用户数据 + if 'data/users.json' in zipf.namelist(): + user_data = json.loads(zipf.read('data/users.json')) + for user_info in user_data: + # 这里需要根据实际情况实现数据恢复逻辑 + pass + + # 恢复提示词数据 + if 'data/prompts.json' in zipf.namelist(): + prompt_data = json.loads(zipf.read('data/prompts.json')) + for prompt_info in prompt_data: + # 这里需要根据实际情况实现数据恢复逻辑 + pass + + # 恢复模板数据 + if 'data/templates.json' in zipf.namelist(): + template_data = json.loads(zipf.read('data/templates.json')) + for template_info in template_data: + # 这里需要根据实际情况实现数据恢复逻辑 + pass + + except Exception as e: + print(f"恢复数据库数据失败: {str(e)}") + + def _restore_config_files(self, zipf): + """恢复配置文件""" + try: + # 恢复配置文件 + for file_info in zipf.filelist: + if file_info.filename.startswith('config/'): + filename = file_info.filename.replace('config/', '') + if filename not in ['.env']: # 不覆盖敏感配置文件 + zipf.extract(file_info, '.') + + except Exception as e: + print(f"恢复配置文件失败: {str(e)}") + + def _auto_backup_worker(self): + """自动备份工作线程""" + schedule.every().day.at("02:00").do(self._perform_auto_backup) + + while self.auto_backup_enabled: + schedule.run_pending() + time.sleep(60) + + def _perform_auto_backup(self): + """执行自动备份""" + try: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"auto_backup_{timestamp}.zip" + backup_path = os.path.join(self.backup_dir, filename) + + # 创建数据备份 + self._create_data_backup(backup_path) + + # 清理旧备份(保留最近7天) + self._cleanup_old_backups() + + print(f"自动备份完成: {filename}") + + except Exception as e: + print(f"自动备份失败: {str(e)}") + + def _cleanup_old_backups(self): + """清理旧备份文件""" + try: + cutoff_time = datetime.now() - timedelta(days=7) + for filename in os.listdir(self.backup_dir): + if filename.startswith('auto_backup_') and filename.endswith('.zip'): + file_path = os.path.join(self.backup_dir, filename) + if os.path.getctime(file_path) < cutoff_time.timestamp(): + os.remove(file_path) + print(f"删除旧备份文件: {filename}") + + except Exception as e: + print(f"清理旧备份失败: {str(e)}") + + def _format_file_size(self, size_bytes): + """格式化文件大小""" + if size_bytes == 0: + return "0B" + size_names = ["B", "KB", "MB", "GB"] + i = 0 + while size_bytes >= 1024 and i < len(size_names) - 1: + size_bytes /= 1024.0 + i += 1 + return f"{size_bytes:.1f}{size_names[i]}" diff --git a/src/flask_prompt_master/admin/views/batch_admin.py b/src/flask_prompt_master/admin/views/batch_admin.py new file mode 100644 index 0000000..0fd9fe9 --- /dev/null +++ b/src/flask_prompt_master/admin/views/batch_admin.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" +批量操作管理视图 +""" +from flask_admin import BaseView, expose +from flask_login import login_required, current_user +from flask import request, jsonify, send_file +from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate +from src.flask_prompt_master import db +import csv +import io +from datetime import datetime + +class BatchAdminView(BaseView): + """批量操作管理视图""" + + @expose('/') + @login_required + def index(self): + """批量操作首页""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + return self.render('admin/batch_operations.html') + + @expose('/export/users') + @login_required + def export_users(self): + """导出用户数据""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + # 获取用户数据 + users = User.query.all() + + # 创建CSV文件 + output = io.StringIO() + writer = csv.writer(output) + + # 写入表头 + writer.writerow(['用户ID', '用户名', '昵称', '邮箱', '手机号', '性别', '状态', '注册时间']) + + # 写入数据 + for user in users: + writer.writerow([ + user.uid, + user.login_name, + user.nickname, + user.email, + user.mobile, + '男' if user.sex == 1 else '女' if user.sex == 2 else '保密', + '正常' if user.status == 1 else '禁用', + user.created_time.strftime('%Y-%m-%d %H:%M:%S') if user.created_time else '' + ]) + + # 创建文件响应 + output.seek(0) + return send_file( + io.BytesIO(output.getvalue().encode('utf-8-sig')), + mimetype='text/csv', + as_attachment=True, + download_name=f'users_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' + ) + + except Exception as e: + return jsonify({'success': False, 'message': f'导出失败: {str(e)}'}) + + @expose('/export/prompts') + @login_required + def export_prompts(self): + """导出提示词数据""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'content_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + # 获取提示词数据 + prompts = Prompt.query.all() + + # 创建CSV文件 + output = io.StringIO() + writer = csv.writer(output) + + # 写入表头 + writer.writerow(['ID', '用户ID', '输入文本', '生成文本', '创建时间']) + + # 写入数据 + for prompt in prompts: + writer.writerow([ + prompt.id, + prompt.user_id, + prompt.input_text[:100] + '...' if len(prompt.input_text) > 100 else prompt.input_text, + prompt.generated_text[:100] + '...' if len(prompt.generated_text) > 100 else prompt.generated_text, + prompt.created_at.strftime('%Y-%m-%d %H:%M:%S') if prompt.created_at else '' + ]) + + # 创建文件响应 + output.seek(0) + return send_file( + io.BytesIO(output.getvalue().encode('utf-8-sig')), + mimetype='text/csv', + as_attachment=True, + download_name=f'prompts_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' + ) + + except Exception as e: + return jsonify({'success': False, 'message': f'导出失败: {str(e)}'}) + + @expose('/batch/disable-users', methods=['POST']) + @login_required + def batch_disable_users(self): + """批量禁用用户""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'user_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + data = request.get_json() + user_ids = data.get('user_ids', []) + + if not user_ids: + return jsonify({'success': False, 'message': '请选择要禁用的用户'}) + + # 批量更新用户状态 + User.query.filter(User.uid.in_(user_ids)).update( + {'status': 0, 'updated_time': datetime.now()}, + synchronize_session=False + ) + db.session.commit() + + return jsonify({'success': True, 'message': f'成功禁用 {len(user_ids)} 个用户'}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) + + @expose('/batch/enable-users', methods=['POST']) + @login_required + def batch_enable_users(self): + """批量启用用户""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'user_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + data = request.get_json() + user_ids = data.get('user_ids', []) + + if not user_ids: + return jsonify({'success': False, 'message': '请选择要启用的用户'}) + + # 批量更新用户状态 + User.query.filter(User.uid.in_(user_ids)).update( + {'status': 1, 'updated_time': datetime.now()}, + synchronize_session=False + ) + db.session.commit() + + return jsonify({'success': True, 'message': f'成功启用 {len(user_ids)} 个用户'}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) + + @expose('/batch/delete-prompts', methods=['POST']) + @login_required + def batch_delete_prompts(self): + """批量删除提示词""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'content_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + data = request.get_json() + prompt_ids = data.get('prompt_ids', []) + + if not prompt_ids: + return jsonify({'success': False, 'message': '请选择要删除的提示词'}) + + # 批量删除提示词 + Prompt.query.filter(Prompt.id.in_(prompt_ids)).delete(synchronize_session=False) + db.session.commit() + + return jsonify({'success': True, 'message': f'成功删除 {len(prompt_ids)} 个提示词'}) + + except Exception as e: + db.session.rollback() + return jsonify({'success': False, 'message': f'操作失败: {str(e)}'}) diff --git a/src/flask_prompt_master/admin/views/monitor_admin.py b/src/flask_prompt_master/admin/views/monitor_admin.py new file mode 100644 index 0000000..bf26311 --- /dev/null +++ b/src/flask_prompt_master/admin/views/monitor_admin.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +""" +系统监控管理视图 +""" +from flask_admin import BaseView, expose +from flask_login import login_required, current_user +from flask import request, jsonify +import psutil +import os +import time +from datetime import datetime, timedelta +import threading +import queue + +class MonitorAdminView(BaseView): + """系统监控管理视图""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.alert_queue = queue.Queue() + self.monitoring = False + self.monitor_thread = None + + @expose('/') + @login_required + def index(self): + """监控首页""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + # 获取系统状态 + system_status = self._get_system_status() + + return self.render('admin/monitor_dashboard.html', system_status=system_status) + + @expose('/api/system-status') + @login_required + def api_system_status(self): + """获取系统状态API""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + status = self._get_system_status() + return jsonify({'success': True, 'data': status}) + except Exception as e: + return jsonify({'success': False, 'message': f'获取系统状态失败: {str(e)}'}) + + @expose('/api/start-monitoring', methods=['POST']) + @login_required + def start_monitoring(self): + """启动监控""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + if not self.monitoring: + self.monitoring = True + self.monitor_thread = threading.Thread(target=self._monitor_system, daemon=True) + self.monitor_thread.start() + return jsonify({'success': True, 'message': '监控已启动'}) + else: + return jsonify({'success': False, 'message': '监控已在运行中'}) + except Exception as e: + return jsonify({'success': False, 'message': f'启动监控失败: {str(e)}'}) + + @expose('/api/stop-monitoring', methods=['POST']) + @login_required + def stop_monitoring(self): + """停止监控""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + self.monitoring = False + return jsonify({'success': True, 'message': '监控已停止'}) + except Exception as e: + return jsonify({'success': False, 'message': f'停止监控失败: {str(e)}'}) + + @expose('/api/alerts') + @login_required + def get_alerts(self): + """获取告警信息""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + alerts = [] + while not self.alert_queue.empty(): + alerts.append(self.alert_queue.get_nowait()) + + return jsonify({'success': True, 'data': alerts}) + except Exception as e: + return jsonify({'success': False, 'message': f'获取告警失败: {str(e)}'}) + + def _get_system_status(self): + """获取系统状态""" + try: + # CPU使用率 + cpu_percent = psutil.cpu_percent(interval=1) + + # 内存使用情况 + memory = psutil.virtual_memory() + memory_percent = memory.percent + memory_used = memory.used / (1024**3) # GB + memory_total = memory.total / (1024**3) # GB + + # 磁盘使用情况 + disk = psutil.disk_usage('/') + disk_percent = disk.percent + disk_used = disk.used / (1024**3) # GB + disk_total = disk.total / (1024**3) # GB + + # 网络使用情况 + network = psutil.net_io_counters() + network_sent = network.bytes_sent / (1024**2) # MB + network_recv = network.bytes_recv / (1024**2) # MB + + # 进程数量 + process_count = len(psutil.pids()) + + # 系统运行时间 + boot_time = datetime.fromtimestamp(psutil.boot_time()) + uptime = datetime.now() - boot_time + + # 当前时间 + current_time = datetime.now() + + return { + 'cpu_percent': cpu_percent, + 'memory_percent': memory_percent, + 'memory_used': round(memory_used, 2), + 'memory_total': round(memory_total, 2), + 'disk_percent': disk_percent, + 'disk_used': round(disk_used, 2), + 'disk_total': round(disk_total, 2), + 'network_sent': round(network_sent, 2), + 'network_recv': round(network_recv, 2), + 'process_count': process_count, + 'uptime': str(uptime).split('.')[0], + 'current_time': current_time.strftime('%Y-%m-%d %H:%M:%S'), + 'monitoring': self.monitoring + } + except Exception as e: + return { + 'error': str(e), + 'monitoring': self.monitoring + } + + def _monitor_system(self): + """系统监控线程""" + while self.monitoring: + try: + status = self._get_system_status() + + # 检查告警条件 + if 'error' not in status: + if status['cpu_percent'] > 80: + self.alert_queue.put({ + 'level': 'warning', + 'message': f'CPU使用率过高: {status["cpu_percent"]}%', + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + if status['memory_percent'] > 85: + self.alert_queue.put({ + 'level': 'warning', + 'message': f'内存使用率过高: {status["memory_percent"]}%', + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + if status['disk_percent'] > 90: + self.alert_queue.put({ + 'level': 'danger', + 'message': f'磁盘使用率过高: {status["disk_percent"]}%', + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + time.sleep(30) # 每30秒检查一次 + + except Exception as e: + self.alert_queue.put({ + 'level': 'error', + 'message': f'监控异常: {str(e)}', + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + time.sleep(60) # 异常时等待1分钟再重试 diff --git a/src/flask_prompt_master/admin/views/report_admin.py b/src/flask_prompt_master/admin/views/report_admin.py new file mode 100644 index 0000000..936a0c9 --- /dev/null +++ b/src/flask_prompt_master/admin/views/report_admin.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +""" +高级报表管理视图 +""" +from flask_admin import BaseView, expose +from flask_login import login_required, current_user +from flask import request, jsonify, send_file +from src.flask_prompt_master.models.models import User, Prompt, PromptTemplate +from src.flask_prompt_master import db +from sqlalchemy import func, extract +from datetime import datetime, timedelta +import csv +import io +import json + +class ReportAdminView(BaseView): + """高级报表管理视图""" + + @expose('/') + @login_required + def index(self): + """报表首页""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + return self.render('admin/report_dashboard.html') + + @expose('/user-report') + @login_required + def user_report(self): + """用户报表""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return self.render('admin/403.html') + + # 获取报表参数 + start_date = request.args.get('start_date', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')) + end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d')) + report_type = request.args.get('type', 'daily') + + # 生成报表数据 + report_data = self._generate_user_report(start_date, end_date, report_type) + + return self.render('admin/user_report.html', + report_data=report_data, + start_date=start_date, + end_date=end_date, + report_type=report_type) + + @expose('/prompt-report') + @login_required + def prompt_report(self): + """提示词报表""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'content_admin']: + return self.render('admin/403.html') + + # 获取报表参数 + start_date = request.args.get('start_date', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')) + end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d')) + category = request.args.get('category', 'all') + + # 生成报表数据 + report_data = self._generate_prompt_report(start_date, end_date, category) + + return self.render('admin/prompt_report.html', + report_data=report_data, + start_date=start_date, + end_date=end_date, + category=category) + + @expose('/export-report') + @login_required + def export_report(self): + """导出报表""" + if not current_user.is_authenticated or current_user.role not in ['super_admin', 'system_admin']: + return jsonify({'success': False, 'message': '权限不足'}) + + try: + report_type = request.args.get('type', 'user') + start_date = request.args.get('start_date', (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')) + end_date = request.args.get('end_date', datetime.now().strftime('%Y-%m-%d')) + + if report_type == 'user': + data = self._generate_user_report(start_date, end_date, 'daily') + filename = f'user_report_{start_date}_to_{end_date}.csv' + elif report_type == 'prompt': + data = self._generate_prompt_report(start_date, end_date, 'all') + filename = f'prompt_report_{start_date}_to_{end_date}.csv' + else: + return jsonify({'success': False, 'message': '不支持的报表类型'}) + + # 创建CSV文件 + output = io.StringIO() + writer = csv.writer(output) + + # 写入表头 + if report_type == 'user': + writer.writerow(['日期', '新增用户', '活跃用户', '总用户数']) + for item in data.get('daily_stats', []): + writer.writerow([item['date'], item['new_users'], item['active_users'], item['total_users']]) + else: + writer.writerow(['日期', '生成数量', '平均长度', '用户数']) + for item in data.get('daily_stats', []): + writer.writerow([item['date'], item['count'], item['avg_length'], item['users']]) + + # 创建文件响应 + output.seek(0) + return send_file( + io.BytesIO(output.getvalue().encode('utf-8-sig')), + mimetype='text/csv', + as_attachment=True, + download_name=filename + ) + + except Exception as e: + return jsonify({'success': False, 'message': f'导出失败: {str(e)}'}) + + def _generate_user_report(self, start_date, end_date, report_type): + """生成用户报表""" + try: + start_dt = datetime.strptime(start_date, '%Y-%m-%d') + end_dt = datetime.strptime(end_date, '%Y-%m-%d') + + # 总体统计 + total_users = User.query.count() + active_users = User.query.filter_by(status=1).count() + new_users_period = User.query.filter( + User.created_time >= start_dt, + User.created_time <= end_dt + ).count() + + # 每日统计 + daily_stats = [] + current_date = start_dt + while current_date <= end_dt: + next_date = current_date + timedelta(days=1) + + # 当日新增用户 + new_users = User.query.filter( + User.created_time >= current_date, + User.created_time < next_date + ).count() + + # 当日活跃用户(有生成提示词的用户) + active_users = db.session.query(func.count(func.distinct(Prompt.user_id))).filter( + Prompt.created_at >= current_date, + Prompt.created_at < next_date + ).scalar() or 0 + + # 累计用户数 + total_users_date = User.query.filter( + User.created_time <= next_date + ).count() + + daily_stats.append({ + 'date': current_date.strftime('%Y-%m-%d'), + 'new_users': new_users, + 'active_users': active_users, + 'total_users': total_users_date + }) + + current_date = next_date + + return { + 'total_users': total_users, + 'active_users': active_users, + 'new_users_period': new_users_period, + 'daily_stats': daily_stats, + 'period_days': (end_dt - start_dt).days + 1 + } + + except Exception as e: + return { + 'error': str(e), + 'total_users': 0, + 'active_users': 0, + 'new_users_period': 0, + 'daily_stats': [], + 'period_days': 0 + } + + def _generate_prompt_report(self, start_date, end_date, category): + """生成提示词报表""" + try: + start_dt = datetime.strptime(start_date, '%Y-%m-%d') + end_dt = datetime.strptime(end_date, '%Y-%m-%d') + + # 总体统计 + total_prompts = Prompt.query.filter( + Prompt.created_at >= start_dt, + Prompt.created_at <= end_dt + ).count() + + # 平均长度 + avg_length = db.session.query(func.avg(func.length(Prompt.input_text))).filter( + Prompt.created_at >= start_dt, + Prompt.created_at <= end_dt + ).scalar() or 0 + + # 活跃用户数 + active_users = db.session.query(func.count(func.distinct(Prompt.user_id))).filter( + Prompt.created_at >= start_dt, + Prompt.created_at <= end_dt + ).scalar() or 0 + + # 每日统计 + daily_stats = [] + current_date = start_dt + while current_date <= end_dt: + next_date = current_date + timedelta(days=1) + + # 当日生成数量 + count = Prompt.query.filter( + Prompt.created_at >= current_date, + Prompt.created_at < next_date + ).count() + + # 当日平均长度 + avg_len = db.session.query(func.avg(func.length(Prompt.input_text))).filter( + Prompt.created_at >= current_date, + Prompt.created_at < next_date + ).scalar() or 0 + + # 当日用户数 + users = db.session.query(func.count(func.distinct(Prompt.user_id))).filter( + Prompt.created_at >= current_date, + Prompt.created_at < next_date + ).scalar() or 0 + + daily_stats.append({ + 'date': current_date.strftime('%Y-%m-%d'), + 'count': count, + 'avg_length': round(avg_len, 2), + 'users': users + }) + + current_date = next_date + + return { + 'total_prompts': total_prompts, + 'avg_length': round(avg_length, 2), + 'active_users': active_users, + 'daily_stats': daily_stats, + 'period_days': (end_dt - start_dt).days + 1 + } + + except Exception as e: + return { + 'error': str(e), + 'total_prompts': 0, + 'avg_length': 0, + 'active_users': 0, + 'daily_stats': [], + 'period_days': 0 + } diff --git a/src/flask_prompt_master/templates/admin/analytics_charts.html b/src/flask_prompt_master/templates/admin/analytics_charts.html new file mode 100644 index 0000000..7634b31 --- /dev/null +++ b/src/flask_prompt_master/templates/admin/analytics_charts.html @@ -0,0 +1,177 @@ +{% extends 'admin/master.html' %} + +{% block title %}数据分析 - 图表{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ 数据分析图表 +

+
+
+ + +
+
+
+
+
+ 用户注册趋势 +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ 提示词生成趋势 +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ 用户状态分布 +
+
+
+
+
+
+
+
+
+
+
+ 系统使用统计 +
+
+
+
+
+
+
+
+ + + +
+ + +{% endblock %} diff --git a/src/flask_prompt_master/templates/admin/analytics_dashboard.html b/src/flask_prompt_master/templates/admin/analytics_dashboard.html new file mode 100644 index 0000000..536c27c --- /dev/null +++ b/src/flask_prompt_master/templates/admin/analytics_dashboard.html @@ -0,0 +1,239 @@ +{% extends 'admin/master.html' %} + +{% block title %}数据分析 - 仪表板{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ 数据分析仪表板 +

+
+
+ + +
+
+
+
+
+
+
+ 总用户数 +
+
{{ stats.total_users }}
+
今日新增: {{ stats.new_users_today }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 总提示词 +
+
{{ stats.total_prompts }}
+
今日生成: {{ stats.today_prompts }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 活跃用户 +
+
{{ stats.active_users }}
+
本周活跃: {{ stats.active_users_week }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 模板数量 +
+
{{ stats.total_templates }}
+
默认模板: {{ stats.default_templates }}
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ 用户注册趋势 +
+
+
+
+
+
+
+ +
+
+
+
+ 用户状态分布 +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 快速操作 +
+
+ +
+
+
+
+ + + + +{% endblock %} diff --git a/src/flask_prompt_master/templates/admin/api_dashboard.html b/src/flask_prompt_master/templates/admin/api_dashboard.html new file mode 100644 index 0000000..778a607 --- /dev/null +++ b/src/flask_prompt_master/templates/admin/api_dashboard.html @@ -0,0 +1,444 @@ +{% extends 'admin/master.html' %} + +{% block title %}API管理{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ API管理 +

+
+
+ + +
+
+
+
+
+
+
+ 今日调用 +
+
{{ api_stats.today_calls }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 本周调用 +
+
{{ api_stats.week_calls }}
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 平均响应时间 +
+
{{ api_stats.avg_response_time }}s
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 成功率 +
+
{{ api_stats.success_rate }}%
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ API调用趋势 +
+
+
+
+
+
+
+ +
+
+
+
+ API信息 +
+
+
+
+
+
+

{{ api_stats.total_calls }}

+

总调用次数

+
+
+
+
+

{{ api_stats.active_users }}

+

活跃用户数

+
+
+
+
+

{{ api_stats.month_calls }}

+

本月调用次数

+
+
+
+
+
+
+
+ + +
+
+
+
+
+ API管理功能 +
+
+
+
+
+
+
+ +
调用记录
+

查看详细的API调用记录和日志

+ +
+
+
+
+
+
+ +
限流管理
+

配置API调用频率限制和访问控制

+ + 限流设置 + +
+
+
+
+
+
+ +
密钥管理
+

管理API访问密钥和权限

+ + 密钥管理 + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 最近API调用记录 +
+
+
+
+ + + + + + + + + + + + + + + + + + +
ID用户IDAPI类型输入内容状态响应时间IP地址调用时间
+ 加载中... +
+
+
+
+
+
+
+ + + + + + + +{% endblock %} diff --git a/src/flask_prompt_master/templates/admin/backup_dashboard.html b/src/flask_prompt_master/templates/admin/backup_dashboard.html new file mode 100644 index 0000000..eed737d --- /dev/null +++ b/src/flask_prompt_master/templates/admin/backup_dashboard.html @@ -0,0 +1,418 @@ +{% extends 'admin/master.html' %} + +{% block title %}数据备份管理{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ 数据备份管理 +

+
+
+ + +
+
+
+
+
+ 创建备份 +
+
+
+
+
+
+
+ +
完整备份
+

备份所有数据、配置和日志文件

+ +
+
+
+
+
+
+ +
数据备份
+

仅备份数据库数据

+ +
+
+
+
+
+
+ +
配置备份
+

仅备份系统配置文件

+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 自动备份设置 +
+
+
+
+
+
自动备份
+

每天凌晨2点自动创建数据备份,保留最近7天的备份文件

+
+
+
+ + +
+
+
+
+
+
+
+ + +
+
+
+
+
+ 备份文件列表 +
+
+
+
+ + + + + + + + + + + + {% for backup in backup_files %} + + + + + + + + {% else %} + + + + {% endfor %} + +
文件名大小创建时间修改时间操作
{{ backup.filename }}{{ backup.size }}{{ backup.created_time }}{{ backup.modified_time }} +
+ + + +
+
+
+ 暂无备份文件 +
+
+
+
+
+
+
+ + + + + + + + + + +{% endblock %} diff --git a/src/flask_prompt_master/templates/admin/batch_operations.html b/src/flask_prompt_master/templates/admin/batch_operations.html new file mode 100644 index 0000000..08fbffd --- /dev/null +++ b/src/flask_prompt_master/templates/admin/batch_operations.html @@ -0,0 +1,300 @@ +{% extends 'admin/master.html' %} + +{% block title %}批量操作{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ 批量操作管理 +

+
+
+ + +
+
+
+
+
+ 数据导出 +
+
+
+
+
+
+
+
+ 用户数据导出 +
+

导出所有用户信息到CSV文件

+ + 导出用户数据 + +
+
+
+
+
+
+
+ 提示词数据导出 +
+

导出所有提示词信息到CSV文件

+ + 导出提示词数据 + +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 批量操作 +
+
+
+
+
+
+
+
+ 批量禁用用户 +
+

选择用户ID进行批量禁用操作

+
+ + +
+ +
+
+
+
+
+
+
+ 批量启用用户 +
+

选择用户ID进行批量启用操作

+
+ + +
+ +
+
+
+
+
+
+
+ 批量删除提示词 +
+

选择提示词ID进行批量删除操作

+
+ + +
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 操作日志 +
+
+
+
+ 操作日志将在这里显示 +
+
+
+
+
+
+ + + + +{% endblock %} diff --git a/src/flask_prompt_master/templates/admin/monitor_dashboard.html b/src/flask_prompt_master/templates/admin/monitor_dashboard.html new file mode 100644 index 0000000..369a24d --- /dev/null +++ b/src/flask_prompt_master/templates/admin/monitor_dashboard.html @@ -0,0 +1,437 @@ +{% extends 'admin/master.html' %} + +{% block title %}系统监控{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ 系统监控仪表板 +

+
+
+ + +
+
+
+
+
+ 监控控制 +
+
+
+
+
+ + +
+
+
+ + 监控状态: 未启动 +
+
+
+
+
+
+
+ + +
+
+
+
+
+
+
+ CPU使用率 +
+
0%
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 内存使用率 +
+
0%
+
0 GB / 0 GB
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 磁盘使用率 +
+
0%
+
0 GB / 0 GB
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ 进程数量 +
+
0
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ 系统资源使用趋势 +
+
+
+
+
+
+
+ +
+
+
+
+ 系统告警 +
+
+
+
+
+ 暂无告警信息 +
+
+
+
+
+
+ + +
+
+
+
+
+ 系统信息 +
+
+
+
+
+ + + + + + + + + + + + + +
系统运行时间:{{ system_status.uptime if system_status.uptime else '未知' }}
当前时间:{{ system_status.current_time if system_status.current_time else '未知' }}
网络发送:{{ system_status.network_sent if system_status.network_sent else 0 }} MB
+
+
+ + + + + + + + + + + + + +
网络接收:{{ system_status.network_recv if system_status.network_recv else 0 }} MB
监控状态:{{ '运行中' if system_status.monitoring else '未启动' }}
最后更新:-
+
+
+
+
+
+
+
+ + + + +{% endblock %} diff --git a/src/flask_prompt_master/templates/admin/report_dashboard.html b/src/flask_prompt_master/templates/admin/report_dashboard.html new file mode 100644 index 0000000..83a3c3e --- /dev/null +++ b/src/flask_prompt_master/templates/admin/report_dashboard.html @@ -0,0 +1,319 @@ +{% extends 'admin/master.html' %} + +{% block title %}高级报表{% endblock %} + +{% block head %} +{{ super() }} + +{% endblock %} + +{% block body %} +
+
+
+

+ 高级报表系统 +

+
+
+ + +
+
+
+
+
+ 报表类型 +
+
+
+
+
+
+
+ +
用户报表
+

用户注册、活跃度、增长趋势分析

+ + 查看用户报表 + +
+
+
+
+
+
+ +
提示词报表
+

提示词生成量、使用情况、质量分析

+ + 查看提示词报表 + +
+
+
+
+
+
+ +
数据导出
+

导出各种格式的报表数据

+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 快速统计 +
+
+
+
+
+
+

-

+

总用户数

+
+
+
+
+

-

+

总提示词数

+
+
+
+
+

-

+

活跃用户

+
+
+
+
+

-

+

平均长度

+
+
+
+
+
+
+
+ + +
+
+
+
+
+ 用户增长趋势 +
+
+
+
+
+
+
+
+
+
+
+ 提示词生成趋势 +
+
+
+
+
+
+
+
+
+ + + + + + + +{% endblock %}