222 lines
6.4 KiB
Python
222 lines
6.4 KiB
Python
from flask import (
|
|
Blueprint,
|
|
flash,
|
|
jsonify,
|
|
redirect,
|
|
render_template,
|
|
request,
|
|
session,
|
|
url_for,
|
|
)
|
|
from web.config import Config
|
|
from web.auth import admin_required, csrf_protected, validate_csrf
|
|
from web.database import query_db
|
|
from web.services.admin_service import AdminService
|
|
from web.services.etl_service import EtlService
|
|
import hmac
|
|
import time
|
|
|
|
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
|
|
|
@bp.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
if request.method == 'POST':
|
|
validate_csrf()
|
|
token = request.form.get('token') or ''
|
|
if hmac.compare_digest(token, Config.ADMIN_TOKEN):
|
|
session['is_admin'] = True
|
|
return redirect(url_for('admin.dashboard'))
|
|
else:
|
|
flash('Invalid Token', 'error')
|
|
return render_template('admin/login.html')
|
|
|
|
@bp.route('/logout')
|
|
def logout():
|
|
session.pop('is_admin', None)
|
|
return redirect(url_for('main.index'))
|
|
|
|
@bp.route('/')
|
|
@admin_required
|
|
def dashboard():
|
|
return render_template(
|
|
'admin/dashboard.html',
|
|
**AdminService.get_overview(),
|
|
)
|
|
|
|
@bp.route('/data-integrity')
|
|
@admin_required
|
|
def data_integrity():
|
|
from web.services.integrity_service import IntegrityService
|
|
|
|
report = IntegrityService.build_report()
|
|
if request.args.get('format') == 'json':
|
|
return jsonify(report)
|
|
return render_template('admin/data_integrity.html', report=report)
|
|
|
|
@bp.route('/trigger_etl', methods=['POST'])
|
|
@admin_required
|
|
@csrf_protected
|
|
def trigger_etl():
|
|
from database.job_store import JobStore
|
|
|
|
store = JobStore(Config.DB_WEB_PATH)
|
|
job_id = store.create_job(
|
|
'manual_pipeline',
|
|
created_by='admin',
|
|
)
|
|
try:
|
|
pid = EtlService.start_pipeline(job_id)
|
|
except Exception as exc:
|
|
store.finish_job(job_id, False, str(exc), 0)
|
|
return jsonify({'success': False, 'error': str(exc)}), 500
|
|
return jsonify({'success': True, 'job_id': job_id, 'pid': pid}), 202
|
|
|
|
|
|
@bp.route('/import-match', methods=['GET', 'POST'])
|
|
@admin_required
|
|
def import_match():
|
|
from database.job_store import JobStore
|
|
from web.services.import_service import (
|
|
DuplicateMatchError,
|
|
ImportValidationError,
|
|
MatchImportService,
|
|
)
|
|
|
|
store = JobStore(Config.DB_WEB_PATH)
|
|
if request.method == 'POST':
|
|
validate_csrf()
|
|
upload = request.files.get('capture')
|
|
if not upload or not upload.filename:
|
|
flash('请选择 iframe_network.json 文件。', 'error')
|
|
return redirect(url_for('admin.import_match'))
|
|
|
|
prepared = None
|
|
try:
|
|
prepared = MatchImportService.prepare_import(
|
|
upload.read(),
|
|
upload.filename,
|
|
created_by='admin',
|
|
replace=request.form.get('replace') == '1',
|
|
)
|
|
EtlService.start_pipeline(
|
|
prepared['job_id'],
|
|
match_id=prepared['match_id'],
|
|
replace=prepared['replace'],
|
|
)
|
|
flash(
|
|
f"比赛 {prepared['match_id']} 已进入导入队列。",
|
|
'success',
|
|
)
|
|
return redirect(url_for(
|
|
'admin.jobs',
|
|
job_id=prepared['job_id'],
|
|
))
|
|
except (DuplicateMatchError, ImportValidationError) as exc:
|
|
flash(str(exc), 'warning')
|
|
except Exception as exc:
|
|
if prepared:
|
|
store.finish_job(
|
|
prepared['job_id'],
|
|
False,
|
|
f'Failed to start pipeline: {exc}',
|
|
0,
|
|
)
|
|
flash(f'启动导入失败:{exc}', 'error')
|
|
|
|
return render_template(
|
|
'admin/import_match.html',
|
|
jobs=store.list_jobs(5),
|
|
)
|
|
|
|
|
|
@bp.route('/jobs')
|
|
@admin_required
|
|
def jobs():
|
|
from database.job_store import JobStore
|
|
|
|
status = request.args.get('status') or None
|
|
job_type = request.args.get('type') or None
|
|
selected_job_id = request.args.get('job_id', type=int)
|
|
data = AdminService.get_jobs(status, job_type)
|
|
data['selected_job'] = (
|
|
JobStore(Config.DB_WEB_PATH).get_job(selected_job_id)
|
|
if selected_job_id else None
|
|
)
|
|
return render_template('admin/jobs.html', **data)
|
|
|
|
|
|
@bp.route('/system')
|
|
@admin_required
|
|
def system():
|
|
return render_template(
|
|
'admin/system.html',
|
|
**AdminService.get_system_status(),
|
|
)
|
|
|
|
|
|
@bp.route('/api/jobs/<int:job_id>')
|
|
@admin_required
|
|
def api_job(job_id):
|
|
from database.job_store import JobStore
|
|
|
|
job = JobStore(Config.DB_WEB_PATH).get_job(job_id)
|
|
if not job:
|
|
return jsonify({'error': 'Job not found'}), 404
|
|
return jsonify(job)
|
|
|
|
@bp.route('/sql', methods=['GET', 'POST'])
|
|
@admin_required
|
|
def sql_runner():
|
|
result = None
|
|
error = None
|
|
query = ""
|
|
db_name = request.args.get('db_name', 'l2')
|
|
if db_name not in {'l2', 'l3', 'web'}:
|
|
db_name = 'l2'
|
|
duration_ms = None
|
|
row_count = None
|
|
|
|
if request.method == 'POST':
|
|
validate_csrf()
|
|
query = (request.form.get('query') or '').strip()
|
|
db_name = request.form.get('db_name', 'l2')
|
|
|
|
statement = query.rstrip(';').strip()
|
|
if db_name not in {'l2', 'l3', 'web'}:
|
|
error = "Unknown database."
|
|
elif not statement.upper().startswith('SELECT '):
|
|
error = "Only SELECT queries are allowed."
|
|
elif ';' in statement:
|
|
error = "Only one SQL statement is allowed."
|
|
else:
|
|
try:
|
|
query = statement
|
|
if 'LIMIT' not in statement.upper():
|
|
query = f"{statement} LIMIT 50"
|
|
started = time.perf_counter()
|
|
rows = query_db(db_name, query)
|
|
duration_ms = (time.perf_counter() - started) * 1000
|
|
row_count = len(rows)
|
|
if rows:
|
|
columns = rows[0].keys()
|
|
result = {'columns': columns, 'rows': rows}
|
|
else:
|
|
result = {'columns': [], 'rows': []}
|
|
except Exception as e:
|
|
error = str(e)
|
|
|
|
try:
|
|
catalog = AdminService.get_database_catalog(db_name)
|
|
except ValueError:
|
|
catalog = []
|
|
return render_template(
|
|
'admin/sql.html',
|
|
result=result,
|
|
error=error,
|
|
query=query,
|
|
db_name=db_name,
|
|
catalog=catalog,
|
|
duration_ms=duration_ms,
|
|
row_count=row_count,
|
|
)
|