From c0a0db41421213aeb97f05b3a1aede59b1c610ef Mon Sep 17 00:00:00 2001 From: 24024 <240241002@qq.com> Date: Sat, 2 May 2026 11:45:44 +0800 Subject: [PATCH] Add admin usage audit for generation records --- .codex/environments/environment.toml | 14 ++ blueprints/admin.py | 139 +++++++++++++++- blueprints/auth.py | 7 + templates/usage.html | 236 +++++++++++++++++++++++++++ 4 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 .codex/environments/environment.toml create mode 100644 templates/usage.html diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 0000000..4f3f69e --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,14 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "google" + +[setup] +script = "" + +[[actions]] +name = "运行" +icon = "run" +command = ''' +.venv\Scripts\Activate.ps1 +python app.py +''' diff --git a/blueprints/admin.py b/blueprints/admin.py index 9f9d7b4..5deca5a 100644 --- a/blueprints/admin.py +++ b/blueprints/admin.py @@ -2,7 +2,7 @@ from flask import Blueprint, request, jsonify, g from datetime import timedelta import json from extensions import db -from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, to_bj_time, get_bj_now +from models import User, Role, Permission, SystemDict, SystemNotification, Order, PointsGrant, InviteReward, GenerationRecord, to_bj_time, get_bj_now from middlewares.auth import permission_required, login_required from services.logger import system_logger @@ -343,6 +343,143 @@ def get_order_detail(order_id): "current_is_admin": g.user.has_permission('manage_system') }) +# --- 用户积分消耗与生成内容管理 --- +def _fix_minio_url(url): + if url and ":9000/images/" in url: + return "/files/" + url.split(":9000/images/")[-1] + return url + + +def _normalize_generation_outputs(raw_image_urls): + """兼容历史字符串 URL、新对象格式、文本与视频生成记录。""" + if not raw_image_urls: + return [] + + try: + raw_outputs = json.loads(raw_image_urls) + except (TypeError, ValueError): + return [] + + if not isinstance(raw_outputs, list): + raw_outputs = [raw_outputs] + + outputs = [] + for item in raw_outputs: + if isinstance(item, str): + outputs.append({ + "type": "image", + "url": _fix_minio_url(item), + "thumb": _fix_minio_url(item) + }) + continue + + if not isinstance(item, dict): + continue + + output_type = item.get("type") or "image" + output = {"type": output_type} + + if output_type == "text": + output["content"] = item.get("content", "") + else: + url = _fix_minio_url(item.get("url")) + thumb = _fix_minio_url(item.get("thumb") or item.get("thumbnail") or url) + output["url"] = url + output["thumb"] = thumb + if output_type == "video" and not thumb: + output["thumb"] = "https://img.icons8.com/flat-round/64/000000/play--v1.png" + + outputs.append(output) + + return outputs + + +def _detect_generation_type(outputs): + output_types = {item.get("type") for item in outputs} + if "video" in output_types: + return "video" + if "text" in output_types: + return "text" + return "image" + + +@admin_bp.route('/usage/stats', methods=['GET']) +@permission_required('manage_system') +def get_usage_stats(): + """获取全站积分消耗与生成内容统计。""" + today = get_bj_now().date() + + total_cost = db.session.query(db.func.sum(GenerationRecord.cost)).scalar() or 0 + today_cost = db.session.query(db.func.sum(GenerationRecord.cost)).filter( + db.func.date(GenerationRecord.created_at) == today + ).scalar() or 0 + + return jsonify({ + "total_records": GenerationRecord.query.count(), + "total_cost": int(total_cost), + "today_records": GenerationRecord.query.filter( + db.func.date(GenerationRecord.created_at) == today + ).count(), + "today_cost": int(today_cost), + "active_users": db.session.query(db.func.count(db.func.distinct(GenerationRecord.user_id))).scalar() or 0 + }) + + +@admin_bp.route('/usage/records', methods=['GET']) +@permission_required('manage_system') +def get_usage_records(): + """分页获取用户积分消耗和生成内容。""" + page = request.args.get('page', 1, type=int) + per_page = min(request.args.get('per_page', 20, type=int), 100) + search = request.args.get('q', '').strip() + content_type = request.args.get('type', 'all') + + query = db.session.query(GenerationRecord).outerjoin(User, GenerationRecord.user_id == User.id) + + if search: + keyword = f"%{search}%" + query = query.filter(db.or_( + User.phone.like(keyword), + GenerationRecord.prompt.like(keyword), + GenerationRecord.model.like(keyword) + )) + + if content_type == 'video': + query = query.filter(GenerationRecord.image_urls.like('%"type": "video"%')) + elif content_type == 'text': + query = query.filter(GenerationRecord.image_urls.like('%"type": "text"%')) + elif content_type == 'image': + query = query.filter( + ~GenerationRecord.image_urls.like('%"type": "video"%'), + ~GenerationRecord.image_urls.like('%"type": "text"%') + ) + + pagination = query.order_by(GenerationRecord.created_at.desc()).paginate( + page=page, per_page=per_page, error_out=False + ) + + records = [] + for record in pagination.items: + outputs = _normalize_generation_outputs(record.image_urls) + records.append({ + "id": record.id, + "user_id": record.user_id, + "user_phone": record.user.phone if record.user else "未知", + "prompt": record.prompt or "", + "model": record.model or "Default", + "cost": record.cost or 0, + "type": _detect_generation_type(outputs), + "outputs": outputs, + "created_at": record.created_at_bj.strftime('%Y-%m-%d %H:%M:%S') + }) + + return jsonify({ + "records": records, + "total": pagination.total, + "pages": pagination.pages, + "current_page": pagination.page + }) + # --- 积分发放管理 --- @admin_bp.route('/points/grant', methods=['POST']) @permission_required('manage_system') diff --git a/blueprints/auth.py b/blueprints/auth.py index 5ab478e..e2d07da 100644 --- a/blueprints/auth.py +++ b/blueprints/auth.py @@ -53,6 +53,12 @@ def admin_points_page(): """积分发放管理页面""" return render_template('points.html') +@auth_bp.route('/admin/usage') +@admin_required +def admin_usage_page(): + """全站积分消耗与生成内容审计页面""" + return render_template('usage.html') + @auth_bp.route('/buy') def buy_page(): """购买积分页面""" @@ -617,6 +623,7 @@ def get_user_menu(user): {"name": "验光单助手", "icon": "scan-eye", "url": "/ocr", "perm": None}, {"name": "购买积分", "icon": "shopping-cart", "url": "/buy", "perm": None}, {"name": "积分发放管理", "icon": "gift", "url": "/admin/points", "perm": "manage_system"}, + {"name": "生成内容审计", "icon": "shield-alert", "url": "/admin/usage", "perm": "manage_system"}, {"name": "权限管理中心", "icon": "shield-check", "url": "/rbac", "perm": "manage_rbac"}, {"name": "系统字典管理", "icon": "book-open", "url": "/dicts", "perm": "manage_dicts"}, {"name": "系统通知管理", "icon": "megaphone", "url": "/notifications", "perm": "manage_notifications"}, diff --git a/templates/usage.html b/templates/usage.html new file mode 100644 index 0000000..ba26fb1 --- /dev/null +++ b/templates/usage.html @@ -0,0 +1,236 @@ +{% extends "base.html" %} + +{% block title %}生成内容审计 - AI 视界{% endblock %} + +{% block content %} +
+
+
+
+
+ +
+
+

生成内容审计

+

全站用户积分消耗与生成内容留痕

+
+
+ +
+ +
+
+

生成记录总数

+

0

+
+
+

累计消耗积分

+

0

+
+
+

今日生成 / 消耗

+

0/0

+
+
+

生成用户数

+

0

+
+
+ +
+
+ + +
+
+ + + + +
+
+ +
+
+ + + + + + + + + + + + + + + +
用户内容输出预览消耗时间
正在加载审计记录...
+
+
+ 共 0 条记录 +
+ + +
+
+
+
+
+ + +{% endblock %} + +{% block scripts %} + +{% endblock %}