更新新增三个模块
This commit is contained in:
@@ -0,0 +1 @@
|
||||
8cecee3bc02a178bf372ca1c3d02fc5cae3c0a51c8346f6e6a710988599c08be
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
import argparse
|
||||
import base64
|
||||
import re
|
||||
import time
|
||||
import alibabacloud_oss_v2 as oss
|
||||
import requests
|
||||
|
||||
from config import region, endpoint, bucket, file_url_pre, bucket_path
|
||||
|
||||
|
||||
def upload_file(file_content: bytes, key: str):
|
||||
credentials_provider = oss.credentials.EnvironmentVariableCredentialsProvider()
|
||||
cfg = oss.config.load_default()
|
||||
cfg.credentials_provider = credentials_provider
|
||||
cfg.region = region
|
||||
cfg.endpoint = endpoint
|
||||
client = oss.Client(cfg)
|
||||
|
||||
result = client.put_object(
|
||||
oss.PutObjectRequest(
|
||||
bucket=bucket, # 存储空间名称
|
||||
key=key, # 对象名称
|
||||
body=file_content # 读取文件内容
|
||||
)
|
||||
)
|
||||
# print(result)
|
||||
return file_url_pre + key
|
||||
|
||||
|
||||
|
||||
|
||||
# 脚本入口,当文件被直接运行时调用main函数
|
||||
if __name__ == "__main__":
|
||||
with open("测试图片数据/IMG_2685.JPG", "rb") as f:
|
||||
file_content = f.read()
|
||||
upload_file(file_content,key=bucket_path+"test.png")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
卖相AI - Flask 后端
|
||||
按功能拆分为蓝图:认证(auth)、主页面(main)、管理员API(admin_api)、版本(version)
|
||||
"""
|
||||
import os
|
||||
import secrets
|
||||
from datetime import timedelta
|
||||
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
|
||||
from utils.db import init_db
|
||||
from blueprints.auth import auth
|
||||
from blueprints.main import main
|
||||
from blueprints.admin_api import admin_api
|
||||
from blueprints.version import version_bp
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
app = Flask(__name__, template_folder=BASE_DIR, static_folder=BASE_DIR)
|
||||
|
||||
CORS(app)
|
||||
app.secret_key = os.environ.get('SECRET_KEY', secrets.token_hex(32))
|
||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7)
|
||||
|
||||
# 注册蓝图
|
||||
app.register_blueprint(auth)
|
||||
app.register_blueprint(main)
|
||||
app.register_blueprint(admin_api)
|
||||
app.register_blueprint(version_bp)
|
||||
|
||||
|
||||
def run_app(host='0.0.0.0', port=15124):
|
||||
init_db()
|
||||
app.run(host=host, port=port, threaded=True, use_reloader=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_app()
|
||||
@@ -0,0 +1 @@
|
||||
# Blueprints 包
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,723 @@
|
||||
"""
|
||||
管理员 API 蓝图:用户管理、生成历史、版本管理(后台)
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import requests
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
|
||||
import pymysql
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from utils.db import get_db
|
||||
from utils.auth import admin_required, get_current_admin_role
|
||||
from ali_oss import upload_file as oss_upload_file
|
||||
|
||||
try:
|
||||
from config import bucket_path, backend_java_base_url
|
||||
except ImportError:
|
||||
bucket_path = os.environ.get('BUCKET_PATH', 'nanri-image/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
|
||||
admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin')
|
||||
|
||||
|
||||
def _safe_version_key(version):
|
||||
"""将版本号转为安全的 OSS 对象名部分"""
|
||||
s = (version or '').strip()
|
||||
s = re.sub(r'[^\w.\-]', '_', s)
|
||||
return s or 'unknown'
|
||||
|
||||
|
||||
def _proxy_backend_java(method, path, *, params=None, json_data=None, files=None, data=None):
|
||||
url = f"{backend_java_base_url}{path}"
|
||||
try:
|
||||
resp = requests.request(method=method, url=url, params=params, json=json_data, files=files, data=data, timeout=10)
|
||||
except requests.RequestException:
|
||||
return None, jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError:
|
||||
data = None
|
||||
if resp.status_code >= 400:
|
||||
if isinstance(data, dict):
|
||||
return data, jsonify({'success': False, 'error': data.get('message') or data.get('error') or 'backend-java 请求失败'}), resp.status_code
|
||||
return None, jsonify({'success': False, 'error': 'backend-java 请求失败'}), resp.status_code
|
||||
if not isinstance(data, dict):
|
||||
return None, jsonify({'success': False, 'error': 'backend-java 返回格式错误'}), 502
|
||||
if not data.get('success'):
|
||||
return data, jsonify({'success': False, 'error': data.get('message') or '操作失败'}), 200
|
||||
return data, None, 200
|
||||
|
||||
|
||||
# ---------- 用户管理 ----------
|
||||
|
||||
@admin_api.route('/users')
|
||||
@admin_required
|
||||
def list_users():
|
||||
"""分页获取用户列表;支持用户名模糊搜索、指定管理员所属普通用户筛选"""
|
||||
role, current_row = get_current_admin_role()
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
page_size = min(50, max(5, int(request.args.get('page_size', 15))))
|
||||
offset = (page - 1) * page_size
|
||||
search_username = (request.args.get('username') or request.args.get('search') or '').strip()
|
||||
created_by_id_arg = request.args.get('created_by_id') or request.args.get('admin_id')
|
||||
created_by_id = int(created_by_id_arg) if created_by_id_arg and str(created_by_id_arg).isdigit() else None
|
||||
if role != 'super_admin':
|
||||
created_by_id = None
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
if role == 'super_admin':
|
||||
where_parts = ["1=1"]
|
||||
params = []
|
||||
if search_username:
|
||||
where_parts.append("u.username LIKE %s")
|
||||
params.append("%" + search_username + "%")
|
||||
if created_by_id is not None:
|
||||
where_parts.append("u.created_by_id = %s")
|
||||
params.append(created_by_id)
|
||||
where_sql = " AND ".join(where_parts)
|
||||
cur.execute(
|
||||
"""SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id,
|
||||
creator.username AS creator_username
|
||||
FROM users u
|
||||
LEFT JOIN users creator ON creator.id = u.created_by_id
|
||||
WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""",
|
||||
tuple(params) + (page_size, offset),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.execute("SELECT COUNT(*) as total FROM users u WHERE " + where_sql, tuple(params))
|
||||
total = cur.fetchone()['total']
|
||||
cur.execute("SELECT id, username FROM users WHERE role = 'admin' ORDER BY id")
|
||||
admins = [{'id': r['id'], 'username': r['username']} for r in cur.fetchall()]
|
||||
else:
|
||||
admin_id = current_row['id']
|
||||
where_parts = ["(u.id = %s OR (u.role = 'normal' AND u.created_by_id = %s))"]
|
||||
params = [admin_id, admin_id]
|
||||
if search_username:
|
||||
where_parts.append("u.username LIKE %s")
|
||||
params.append("%" + search_username + "%")
|
||||
where_sql = " AND ".join(where_parts)
|
||||
cur.execute(
|
||||
"""SELECT u.id, u.username, u.is_admin, u.role, u.created_at, u.created_by_id,
|
||||
creator.username AS creator_username
|
||||
FROM users u
|
||||
LEFT JOIN users creator ON creator.id = u.created_by_id
|
||||
WHERE """ + where_sql + """ ORDER BY u.id LIMIT %s OFFSET %s""",
|
||||
tuple(params) + (page_size, offset),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) as total FROM users u WHERE " + where_sql,
|
||||
tuple(params),
|
||||
)
|
||||
total = cur.fetchone()['total']
|
||||
admins = []
|
||||
items = [
|
||||
{
|
||||
'id': r['id'],
|
||||
'username': r['username'],
|
||||
'is_admin': bool(r.get('is_admin')),
|
||||
'role': r.get('role') or 'normal',
|
||||
'created_by_id': r.get('created_by_id'),
|
||||
'creator_username': r.get('creator_username') or '',
|
||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
conn.close()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items,
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'current_user_role': role,
|
||||
'admins': admins,
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/user', methods=['POST'])
|
||||
@admin_required
|
||||
def create_user():
|
||||
data = request.get_json() or {}
|
||||
username = (data.get('username') or '').strip()
|
||||
password = data.get('password') or ''
|
||||
role, current_row = get_current_admin_role()
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
want_role = (data.get('role') or 'normal').strip() or 'normal'
|
||||
if want_role not in ('admin', 'normal'):
|
||||
want_role = 'normal'
|
||||
if role == 'admin' and want_role == 'admin':
|
||||
return jsonify({'success': False, 'error': '仅超级管理员可创建管理员'})
|
||||
if want_role == 'admin':
|
||||
want_created_by = current_row['id']
|
||||
elif role == 'super_admin':
|
||||
want_created_by = data.get('created_by_id')
|
||||
else:
|
||||
want_created_by = current_row['id']
|
||||
if not username or not password:
|
||||
return jsonify({'success': False, 'error': '用户名和密码不能为空'})
|
||||
if len(username) < 2:
|
||||
return jsonify({'success': False, 'error': '用户名至少2个字符'})
|
||||
if len(password) < 6:
|
||||
return jsonify({'success': False, 'error': '密码至少6个字符'})
|
||||
if want_role == 'normal' and role == 'super_admin' and want_created_by is None:
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id FROM users WHERE role = 'admin' ORDER BY id LIMIT 1")
|
||||
r = cur.fetchone()
|
||||
conn.close()
|
||||
want_created_by = r['id'] if r else current_row['id']
|
||||
except Exception:
|
||||
want_created_by = current_row['id']
|
||||
if want_role == 'normal' and want_created_by is None:
|
||||
want_created_by = current_row['id']
|
||||
is_admin = 1 if want_role in ('super_admin', 'admin') else 0
|
||||
pwd_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
column_ids = data.get('column_ids')
|
||||
if column_ids is None:
|
||||
column_ids = []
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO users (username, password_hash, is_admin, role, created_by_id) VALUES (%s, %s, %s, %s, %s)",
|
||||
(username, pwd_hash, is_admin, want_role, want_created_by),
|
||||
)
|
||||
new_uid = cur.lastrowid
|
||||
_set_user_column_permissions(cur, new_uid, column_ids)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'msg': '用户创建成功'})
|
||||
except pymysql.IntegrityError:
|
||||
return jsonify({'success': False, 'error': '用户名已存在'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/user/<int:uid>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_user(uid):
|
||||
"""更新用户(密码、角色)"""
|
||||
data = request.get_json() or {}
|
||||
password = data.get('password')
|
||||
want_role = (data.get('role') or '').strip() or data.get('role')
|
||||
role, current_row = get_current_admin_role()
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
if want_role is None and not password:
|
||||
return jsonify({'success': False, 'error': '请提供要修改的内容'})
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,))
|
||||
target = cur.fetchone()
|
||||
if not target:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '用户不存在'})
|
||||
if role == 'admin':
|
||||
if target['role'] != 'normal' or target.get('created_by_id') != current_row['id']:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '只能编辑自己创建的普通用户'}), 403
|
||||
want_role = None
|
||||
else:
|
||||
if target.get('role') == 'super_admin':
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '不能修改超级管理员'})
|
||||
if want_role == 'super_admin':
|
||||
return jsonify({'success': False, 'error': '不能将用户设为超级管理员'})
|
||||
if want_role not in ('admin', 'normal', None, ''):
|
||||
want_role = None
|
||||
if password:
|
||||
if len(password) < 6:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '密码至少6个字符'})
|
||||
pwd_hash = generate_password_hash(password, method='pbkdf2:sha256')
|
||||
cur.execute("UPDATE users SET password_hash = %s WHERE id = %s", (pwd_hash, uid))
|
||||
if want_role is not None and want_role != '':
|
||||
is_admin = 1 if want_role == 'admin' else 0
|
||||
cur.execute(
|
||||
"UPDATE users SET is_admin = %s, role = %s WHERE id = %s",
|
||||
(is_admin, want_role, uid),
|
||||
)
|
||||
column_ids = data.get('column_ids')
|
||||
if column_ids is not None:
|
||||
_set_user_column_permissions(cur, uid, column_ids)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'msg': '更新成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/user/<int:uid>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_user(uid):
|
||||
"""删除用户"""
|
||||
if session.get('user_id') == uid:
|
||||
return jsonify({'success': False, 'error': '不能删除当前登录账号'})
|
||||
role, current_row = get_current_admin_role()
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id, role, created_by_id FROM users WHERE id = %s", (uid,))
|
||||
target = cur.fetchone()
|
||||
if not target:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '用户不存在'})
|
||||
if target.get('role') == 'super_admin':
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '不能删除超级管理员'})
|
||||
if role == 'admin':
|
||||
if target.get('role') != 'normal' or target.get('created_by_id') != current_row['id']:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '只能删除自己创建的普通用户'}), 403
|
||||
cur.execute("DELETE FROM users WHERE id = %s", (uid,))
|
||||
affected = cur.rowcount
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if affected == 0:
|
||||
return jsonify({'success': False, 'error': '用户不存在'})
|
||||
return jsonify({'success': True, 'msg': '删除成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
# ---------- 生成历史 ----------
|
||||
|
||||
@admin_api.route('/history')
|
||||
@admin_required
|
||||
def history():
|
||||
"""管理员分页获取所有生成记录"""
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
page_size = min(50, max(10, int(request.args.get('page_size', 15))))
|
||||
offset = (page - 1) * page_size
|
||||
user_id = request.args.get('user_id', type=int)
|
||||
time_start = (request.args.get('time_start') or '').strip()
|
||||
time_end = (request.args.get('time_end') or '').strip()
|
||||
conditions, params = [], []
|
||||
if user_id:
|
||||
conditions.append("h.user_id = %s")
|
||||
params.append(user_id)
|
||||
if time_start:
|
||||
conditions.append("h.created_at >= %s")
|
||||
params.append(time_start)
|
||||
if time_end:
|
||||
conditions.append("h.created_at <= %s")
|
||||
params.append(time_end + ' 23:59:59' if len(time_end) <= 10 else time_end)
|
||||
where_clause = " AND ".join(conditions) if conditions else "1=1"
|
||||
params_count = params[:]
|
||||
params.extend([page_size, offset])
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT h.id, h.user_id, h.created_at, h.panel_type, h.original_urls, h.params, h.result_urls,
|
||||
h.long_image_url, u.username
|
||||
FROM image_history h
|
||||
LEFT JOIN users u ON h.user_id = u.id
|
||||
WHERE """ + where_clause + """ ORDER BY h.created_at DESC LIMIT %s OFFSET %s""",
|
||||
params,
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.execute("SELECT COUNT(*) as total FROM image_history h WHERE " + where_clause, params_count)
|
||||
total = cur.fetchone()['total']
|
||||
conn.close()
|
||||
|
||||
def _parse_json(val, default=None):
|
||||
if val is None:
|
||||
return default if default is not None else []
|
||||
if isinstance(val, (list, dict)):
|
||||
return val
|
||||
try:
|
||||
return json.loads(val)
|
||||
except Exception:
|
||||
return default if default is not None else []
|
||||
|
||||
items = []
|
||||
for r in rows:
|
||||
items.append({
|
||||
'id': r['id'],
|
||||
'user_id': r['user_id'],
|
||||
'username': r.get('username') or '-',
|
||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r['created_at'] else '',
|
||||
'panel_type': r['panel_type'] or '',
|
||||
'original_urls': _parse_json(r['original_urls'], []),
|
||||
'params': _parse_json(r['params'], {}),
|
||||
'result_urls': _parse_json(r['result_urls'], []),
|
||||
'long_image_url': (r.get('long_image_url') or '').strip() or None,
|
||||
})
|
||||
return jsonify({'success': True, 'items': items, 'total': total, 'page': page, 'page_size': page_size})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
# ---------- 栏目权限配置 ----------
|
||||
|
||||
@admin_api.route('/columns')
|
||||
@admin_required
|
||||
def list_columns():
|
||||
"""获取栏目列表(用于栏目配置与用户权限选择)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, name, column_key, created_at FROM columns ORDER BY id"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
items = [
|
||||
{
|
||||
'id': r['id'],
|
||||
'name': r['name'] or '',
|
||||
'column_key': r['column_key'] or '',
|
||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return jsonify({'success': True, 'items': items})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/column', methods=['POST'])
|
||||
@admin_required
|
||||
def create_column():
|
||||
"""新增栏目"""
|
||||
data = request.get_json() or {}
|
||||
name = (data.get('name') or '').strip()
|
||||
column_key = (data.get('column_key') or '').strip()
|
||||
if not name:
|
||||
return jsonify({'success': False, 'error': '栏目名不能为空'})
|
||||
if not column_key:
|
||||
return jsonify({'success': False, 'error': '栏目标识不能为空'})
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO columns (name, column_key) VALUES (%s, %s)",
|
||||
(name, column_key),
|
||||
)
|
||||
cid = cur.lastrowid
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'msg': '创建成功', 'id': cid})
|
||||
except pymysql.IntegrityError:
|
||||
return jsonify({'success': False, 'error': '栏目标识已存在'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/column/<int:cid>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_column(cid):
|
||||
"""更新栏目"""
|
||||
data = request.get_json() or {}
|
||||
name = (data.get('name') or '').strip()
|
||||
column_key = (data.get('column_key') or '').strip()
|
||||
if not name:
|
||||
return jsonify({'success': False, 'error': '栏目名不能为空'})
|
||||
if not column_key:
|
||||
return jsonify({'success': False, 'error': '栏目标识不能为空'})
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE columns SET name = %s, column_key = %s WHERE id = %s",
|
||||
(name, column_key, cid),
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '栏目不存在'})
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'msg': '更新成功'})
|
||||
except pymysql.IntegrityError:
|
||||
return jsonify({'success': False, 'error': '栏目标识已存在'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/column/<int:cid>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_column(cid):
|
||||
"""删除栏目(会同步删除用户栏目权限关联)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("DELETE FROM user_column_permission WHERE column_id = %s", (cid,))
|
||||
cur.execute("DELETE FROM columns WHERE id = %s", (cid,))
|
||||
if cur.rowcount == 0:
|
||||
conn.close()
|
||||
return jsonify({'success': False, 'error': '栏目不存在'})
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({'success': True, 'msg': '删除成功'})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/user/<int:uid>/columns')
|
||||
@admin_required
|
||||
def get_user_columns(uid):
|
||||
"""获取某用户的栏目权限 ID 列表(编辑用户时回显)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT column_id FROM user_column_permission WHERE user_id = %s",
|
||||
(uid,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
column_ids = [r['column_id'] for r in rows]
|
||||
return jsonify({'success': True, 'column_ids': column_ids})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/user/<int:uid>/column-permissions')
|
||||
@admin_required
|
||||
def get_user_column_permissions(uid):
|
||||
"""根据用户获取其拥有的栏目权限详细信息"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT c.id, c.name, c.column_key, c.created_at
|
||||
FROM user_column_permission ucp
|
||||
JOIN columns c ON c.id = ucp.column_id
|
||||
WHERE ucp.user_id = %s
|
||||
ORDER BY c.id
|
||||
""",
|
||||
(uid,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
items = [
|
||||
{
|
||||
'id': r['id'],
|
||||
'name': r['name'] or '',
|
||||
'column_key': r['column_key'] or '',
|
||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return jsonify({'success': True, 'items': items})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
def _set_user_column_permissions(cur, user_id, column_ids):
|
||||
"""设置用户栏目权限:先删后插。cur 为已打开的游标。"""
|
||||
cur.execute("DELETE FROM user_column_permission WHERE user_id = %s", (user_id,))
|
||||
if column_ids:
|
||||
column_ids = [int(x) for x in column_ids if x]
|
||||
for cid in column_ids:
|
||||
cur.execute(
|
||||
"INSERT INTO user_column_permission (user_id, column_id) VALUES (%s, %s)",
|
||||
(user_id, cid),
|
||||
)
|
||||
|
||||
|
||||
# ---------- 版本管理(web_config) ----------
|
||||
|
||||
@admin_api.route('/versions')
|
||||
@admin_required
|
||||
def list_versions():
|
||||
"""获取版本列表"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, version, file_url, created_at FROM web_config ORDER BY created_at DESC"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
items = [
|
||||
{
|
||||
'id': r['id'],
|
||||
'version': r['version'] or '',
|
||||
'file_url': r['file_url'] or '',
|
||||
'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '',
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return jsonify({'success': True, 'items': items})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/version', methods=['POST'])
|
||||
@admin_required
|
||||
def upload_version():
|
||||
"""接收版本号 + zip 文件,上传到 OSS,写入 web_config"""
|
||||
version = (request.form.get('version') or '').strip()
|
||||
if not version:
|
||||
return jsonify({'success': False, 'error': '请填写版本号'})
|
||||
file_storage = request.files.get('file')
|
||||
if not file_storage or file_storage.filename == '':
|
||||
return jsonify({'success': False, 'error': '请选择要上传的 zip 压缩包'})
|
||||
if not (file_storage.filename or '').lower().endswith('.zip'):
|
||||
return jsonify({'success': False, 'error': '仅支持 .zip 格式'})
|
||||
try:
|
||||
file_content = file_storage.read()
|
||||
if not file_content:
|
||||
return jsonify({'success': False, 'error': '文件为空'})
|
||||
safe_key = _safe_version_key(version)
|
||||
key = f"{bucket_path}versions/{safe_key}.zip"
|
||||
file_url = oss_upload_file(file_content, key)
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"INSERT INTO web_config (version, file_url) VALUES (%s, %s)",
|
||||
(version, file_url)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'version': version,
|
||||
'file_url': file_url,
|
||||
'msg': '上传成功',
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
# ---------- 数据去重总数据 ----------
|
||||
|
||||
@admin_api.route('/dedupe-total-data')
|
||||
@admin_required
|
||||
def list_dedupe_total_data():
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||||
keyword = (request.args.get('keyword') or '').strip()
|
||||
data, error_response, status = _proxy_backend_java(
|
||||
'GET',
|
||||
'/api/admin/dedupe-total-data',
|
||||
params={'page': page, 'pageSize': page_size, 'keyword': keyword},
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
payload = data.get('data') or {}
|
||||
items = [
|
||||
{
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
}
|
||||
for item in (payload.get('items') or [])
|
||||
]
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items,
|
||||
'total': payload.get('total') or 0,
|
||||
'page': payload.get('page') or page,
|
||||
'page_size': payload.get('pageSize') or page_size,
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/dedupe-total-data/import', methods=['POST'])
|
||||
@admin_required
|
||||
def import_dedupe_total_data():
|
||||
file_storage = request.files.get('file')
|
||||
if not file_storage or file_storage.filename == '':
|
||||
return jsonify({'success': False, 'error': '请选择 Excel 文件'})
|
||||
files = {
|
||||
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
|
||||
}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
'POST',
|
||||
'/api/admin/dedupe-total-data/import',
|
||||
files=files,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
summary = result.get('data') or {}
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'msg': result.get('message') or '导入成功',
|
||||
'summary': {
|
||||
'total_rows': summary.get('totalRows') or 0,
|
||||
'asin_count': summary.get('asinCount') or 0,
|
||||
'inserted_count': summary.get('insertedCount') or 0,
|
||||
'skipped_count': summary.get('skippedCount') or 0,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/dedupe-total-data', methods=['POST'])
|
||||
@admin_required
|
||||
def create_dedupe_total_data():
|
||||
data = request.get_json() or {}
|
||||
payload = {'dataValue': (data.get('data_value') or '').strip()}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
'POST',
|
||||
'/api/admin/dedupe-total-data',
|
||||
json_data=payload,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
item = result.get('data') or {}
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'msg': result.get('message') or '创建成功',
|
||||
'item': {
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/dedupe-total-data/<int:item_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
def update_dedupe_total_data(item_id):
|
||||
data = request.get_json() or {}
|
||||
payload = {'dataValue': (data.get('data_value') or '').strip()}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
'PUT',
|
||||
f'/api/admin/dedupe-total-data/{item_id}',
|
||||
json_data=payload,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
item = result.get('data') or {}
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'msg': result.get('message') or '更新成功',
|
||||
'item': {
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/dedupe-total-data/<int:item_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_dedupe_total_data(item_id):
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
'DELETE',
|
||||
f'/api/admin/dedupe-total-data/{item_id}',
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'msg': result.get('message') or '删除成功',
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
认证蓝图:登录、登出、登录状态校验
|
||||
"""
|
||||
from flask import Blueprint, request, redirect, url_for, session, jsonify
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from utils.db import get_db
|
||||
from utils.auth import login_required, is_session_user_valid
|
||||
from utils.render import render_html
|
||||
|
||||
auth = Blueprint('auth', __name__, url_prefix='')
|
||||
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if session.get('user_id') and is_session_user_valid():
|
||||
return redirect(url_for('main.admin_page'))
|
||||
if request.method == 'POST':
|
||||
data = request.get_json() if request.is_json else request.form
|
||||
username = (data.get('username') or '').strip()
|
||||
password = data.get('password') or ''
|
||||
if not username or not password:
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': '请输入用户名和密码'})
|
||||
return render_html('login.html', error='请输入用户名和密码')
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, password_hash, machine, is_admin FROM users WHERE username = %s",
|
||||
(username,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row and check_password_hash(row['password_hash'], password):
|
||||
session.permanent = True
|
||||
session['user_id'] = row['id']
|
||||
session['username'] = username
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'redirect': url_for('main.admin_page')})
|
||||
return redirect(url_for('main.admin_page'))
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
return render_html('login.html', error='登录失败,请稍后重试')
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': '用户名或密码错误'})
|
||||
return render_html('login.html', error='用户名或密码错误')
|
||||
return render_html('login.html')
|
||||
|
||||
|
||||
@auth.route('/api/auth/check')
|
||||
@login_required
|
||||
def api_auth_check():
|
||||
"""校验登录状态,用于页面加载时判断是否已登录"""
|
||||
if not session.get('user_id'):
|
||||
return jsonify({'logged_in': False})
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT machine, is_admin FROM users WHERE id = %s", (session['user_id'],))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return jsonify({'logged_in': False})
|
||||
except Exception:
|
||||
return jsonify({'logged_in': False})
|
||||
return jsonify({'logged_in': True, 'redirect': url_for('main.admin_page')})
|
||||
|
||||
|
||||
@auth.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('auth.login'))
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
主页面蓝图:首页、管理后台页、静态文件
|
||||
"""
|
||||
import os
|
||||
from flask import Blueprint, redirect, url_for, send_file, session
|
||||
|
||||
from utils.auth import login_required, admin_required, is_session_user_valid
|
||||
from utils.render import render_html
|
||||
|
||||
main = Blueprint('main', __name__, url_prefix='')
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STATIC_DIR = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
|
||||
@main.route('/')
|
||||
def index():
|
||||
if session.get('user_id') and is_session_user_valid():
|
||||
return redirect(url_for('main.admin_page'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@main.route('/admin')
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_page():
|
||||
return render_html('admin.html')
|
||||
|
||||
|
||||
@main.route('/static/<path:filename>')
|
||||
def serve_static(filename):
|
||||
"""提供 static 目录及子目录下的静态文件访问"""
|
||||
filepath = os.path.normpath(os.path.join(STATIC_DIR, filename))
|
||||
static_abs = os.path.abspath(STATIC_DIR)
|
||||
file_abs = os.path.abspath(filepath)
|
||||
if not file_abs.startswith(static_abs) or not os.path.isfile(file_abs):
|
||||
return '', 404
|
||||
return send_file(file_abs, as_attachment=False)
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
版本公开 API 蓝图:当前版本、最新版本下载链接
|
||||
"""
|
||||
import os
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
from utils.db import get_db
|
||||
|
||||
version_bp = Blueprint('version', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@version_bp.route('/version')
|
||||
def api_version():
|
||||
"""检测更新:返回当前版本及可选的最新版本信息"""
|
||||
return jsonify({
|
||||
'version': os.environ.get('APP_VERSION', '1.0.0'),
|
||||
'desc': '',
|
||||
'url': os.environ.get('APP_UPDATE_URL', ''),
|
||||
})
|
||||
|
||||
|
||||
@version_bp.route('/version/latest')
|
||||
def api_version_latest():
|
||||
"""GET 获取最新版本的下载链接(按创建时间取最新一条)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT version, file_url FROM web_config ORDER BY created_at DESC LIMIT 1"
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
return jsonify({'version': None, 'file_url': None})
|
||||
return jsonify({
|
||||
'version': row['version'] or '',
|
||||
'file_url': row['file_url'] or '',
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -0,0 +1,38 @@
|
||||
base_url = "https://api.coze.cn/v1"
|
||||
coze_token = "sat_12nW40INoJxArrDXbY4lSCoudbqkOYTcphC99BP2efWyzxmsk4q81WDX3ezWgqZ5"
|
||||
workflow_id = "7608812635877900322"
|
||||
STITCH_WORKFLOW_ID = "7608813873483300907"
|
||||
|
||||
|
||||
# MySQL 配置
|
||||
mysql_host = "8.136.19.173"
|
||||
mysql_user = "aiimage"
|
||||
mysql_password = "WTFrb5y6hNLz6hNy" # 请修改为您的数据库密码
|
||||
mysql_database = "aiimage"
|
||||
|
||||
|
||||
cache_path = "./user_data"
|
||||
|
||||
region = "cn-hangzhou"
|
||||
endpoint = "oss-cn-hangzhou.aliyuncs.com"
|
||||
bucket = "nanri-ai-images"
|
||||
accessKeyId = "LTAI5tNpyvzMNz9f2dHarsm8"
|
||||
accessKeySecret = "bQSZnFH455i8tzyOgeahJmUzwmhynz"
|
||||
bucket_path = "nanri-image/"
|
||||
|
||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||
|
||||
import os
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||
|
||||
|
||||
debug = True
|
||||
version = "1.0.0"
|
||||
APP_UPDATE_URL = ""
|
||||
os.environ['APP_VERSION'] = version
|
||||
os.environ['APP_UPDATE_URL'] = version
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
alibabacloud-oss-v2==1.2.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
Authlib==1.6.8
|
||||
blinker==1.9.0
|
||||
bottle==0.13.4
|
||||
certifi==2026.1.4
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.1.8
|
||||
clr_loader==0.2.10
|
||||
colorama==0.4.6
|
||||
cozepy==0.20.0
|
||||
crcmod-plus==2.3.1
|
||||
cryptography==41.0.0
|
||||
distro==1.9.0
|
||||
et_xmlfile==2.0.0
|
||||
exceptiongroup==1.3.1
|
||||
Flask==3.1.3
|
||||
flask-cors==6.0.2
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.11
|
||||
importlib_metadata==8.7.1
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.3
|
||||
Nuitka==2.8.6
|
||||
numpy==1.25.2
|
||||
openpyxl==3.1.5
|
||||
ordered-set==4.1.0
|
||||
pandas==2.3.3
|
||||
pillow==11.3.0
|
||||
pip==26.0.1
|
||||
proxy_tools==0.1.0
|
||||
psutil==7.2.2
|
||||
pycparser==2.23
|
||||
pycryptodome==3.23.0
|
||||
pydantic==2.12.5
|
||||
pydantic_core==2.41.5
|
||||
PyMySQL==1.1.2
|
||||
PyQt5==5.15.11
|
||||
PyQt5-Qt5==5.15.2
|
||||
PyQt5_sip==12.17.1
|
||||
python-dateutil==2.9.0.post0
|
||||
pythonnet==3.0.5
|
||||
pytz==2026.1.post1
|
||||
pywebview==6.1
|
||||
requests==2.32.5
|
||||
setuptools==80.9.0
|
||||
six==1.17.0
|
||||
typing_extensions==4.15.0
|
||||
typing-inspection==0.4.2
|
||||
tzdata==2025.3
|
||||
urllib3==2.6.3
|
||||
websockets==14.2
|
||||
Werkzeug==3.1.6
|
||||
wheel==0.45.1
|
||||
zipp==3.23.0
|
||||
zstandard==0.25.0
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
Binary file not shown.
@@ -0,0 +1,249 @@
|
||||
import hashlib
|
||||
import platform
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class DeviceIDGenerator:
|
||||
"""
|
||||
Windows设备唯一ID生成器
|
||||
通过收集多个硬件特征来生成稳定的设备唯一标识符
|
||||
"""
|
||||
|
||||
def __init__(self, use_cache: bool = True, cache_file: str = ".device_id"):
|
||||
"""
|
||||
初始化设备ID生成器
|
||||
|
||||
Args:
|
||||
use_cache: 是否使用本地缓存
|
||||
cache_file: 缓存文件名
|
||||
"""
|
||||
self.use_cache = use_cache
|
||||
self.cache_file = cache_file
|
||||
|
||||
def _run_wmic_command(self, command: str) -> Optional[str]:
|
||||
"""
|
||||
执行WMIC命令并返回结果
|
||||
|
||||
Args:
|
||||
command: WMIC命令
|
||||
|
||||
Returns:
|
||||
命令执行结果,失败则返回None
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return result.stdout.strip()
|
||||
except (subprocess.TimeoutExpired, Exception):
|
||||
pass
|
||||
return None
|
||||
|
||||
def _get_motherboard_serial(self) -> Optional[str]:
|
||||
"""获取主板序列号"""
|
||||
return self._run_wmic_command("wmic baseboard get serialnumber /value")
|
||||
|
||||
def _get_cpu_id(self) -> Optional[str]:
|
||||
"""获取CPU ID"""
|
||||
return self._run_wmic_command("wmic cpu get processorid /value")
|
||||
|
||||
def _get_bios_serial(self) -> Optional[str]:
|
||||
"""获取BIOS序列号"""
|
||||
return self._run_wmic_command("wmic bios get serialnumber /value")
|
||||
|
||||
def _get_disk_serial(self) -> Optional[str]:
|
||||
"""获取系统盘序列号"""
|
||||
return self._run_wmic_command("wmic diskdrive get serialnumber /value")
|
||||
|
||||
def _get_machine_guid(self) -> Optional[str]:
|
||||
"""获取Windows机器GUID"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography" /v MachineGuid',
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
for line in result.stdout.split('\n'):
|
||||
if 'MachineGuid' in line:
|
||||
return line.split()[-1]
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _extract_value(self, wmic_output: str) -> str:
|
||||
"""从WMIC输出中提取实际值"""
|
||||
if not wmic_output:
|
||||
return ""
|
||||
|
||||
lines = wmic_output.split('\n')
|
||||
for line in lines:
|
||||
if '=' in line and not line.strip().endswith('='):
|
||||
return line.split('=', 1)[1].strip()
|
||||
return ""
|
||||
|
||||
def _collect_hardware_info(self) -> dict:
|
||||
"""
|
||||
收集硬件信息
|
||||
|
||||
Returns:
|
||||
包含各种硬件信息的字典
|
||||
"""
|
||||
hardware_info = {}
|
||||
|
||||
# 主板序列号
|
||||
motherboard = self._get_motherboard_serial()
|
||||
hardware_info['motherboard'] = self._extract_value(motherboard) if motherboard else ""
|
||||
|
||||
# CPU ID
|
||||
cpu_id = self._get_cpu_id()
|
||||
hardware_info['cpu'] = self._extract_value(cpu_id) if cpu_id else ""
|
||||
|
||||
# BIOS序列号
|
||||
bios = self._get_bios_serial()
|
||||
hardware_info['bios'] = self._extract_value(bios) if bios else ""
|
||||
|
||||
# 硬盘序列号
|
||||
disk = self._get_disk_serial()
|
||||
hardware_info['disk'] = self._extract_value(disk) if disk else ""
|
||||
|
||||
# Windows机器GUID
|
||||
machine_guid = self._get_machine_guid()
|
||||
hardware_info['machine_guid'] = machine_guid if machine_guid else ""
|
||||
|
||||
# 计算机名称
|
||||
hardware_info['computer_name'] = platform.node()
|
||||
|
||||
# MAC地址(作为备用)
|
||||
hardware_info['mac_address'] = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff)
|
||||
for elements in range(0, 2*6, 2)][::-1])
|
||||
|
||||
return hardware_info
|
||||
|
||||
def _generate_device_id(self, hardware_info: dict) -> str:
|
||||
"""
|
||||
基于硬件信息生成设备ID
|
||||
|
||||
Args:
|
||||
hardware_info: 硬件信息字典
|
||||
|
||||
Returns:
|
||||
32位十六进制设备ID
|
||||
"""
|
||||
# 过滤掉空值,并按键排序确保一致性
|
||||
filtered_info = {k: v for k, v in hardware_info.items() if v and v.strip()}
|
||||
|
||||
# 如果没有任何硬件信息,使用MAC地址作为后备方案
|
||||
if not filtered_info:
|
||||
filtered_info = {'mac_address': hardware_info.get('mac_address', str(uuid.getnode()))}
|
||||
|
||||
# 将所有信息连接成字符串
|
||||
info_string = '|'.join(f"{k}:{v}" for k, v in sorted(filtered_info.items()))
|
||||
|
||||
# 使用SHA256生成哈希值
|
||||
hash_object = hashlib.sha256(info_string.encode('utf-8'))
|
||||
device_id = hash_object.hexdigest()
|
||||
|
||||
return device_id
|
||||
|
||||
def _load_cached_device_id(self) -> Optional[str]:
|
||||
"""从缓存文件加载设备ID"""
|
||||
try:
|
||||
if os.path.exists(self.cache_file):
|
||||
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
||||
cached_id = f.read().strip()
|
||||
if len(cached_id) == 64: # SHA256哈希长度
|
||||
return cached_id
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _save_device_id_to_cache(self, device_id: str) -> None:
|
||||
"""将设备ID保存到缓存文件"""
|
||||
try:
|
||||
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
||||
f.write(device_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_device_id(self) -> str:
|
||||
"""
|
||||
获取设备唯一ID
|
||||
|
||||
Returns:
|
||||
64字符的十六进制设备ID
|
||||
"""
|
||||
# 如果启用缓存,先尝试从缓存加载
|
||||
if self.use_cache:
|
||||
cached_id = self._load_cached_device_id()
|
||||
if cached_id:
|
||||
return cached_id
|
||||
|
||||
# 收集硬件信息
|
||||
hardware_info = self._collect_hardware_info()
|
||||
|
||||
# 生成设备ID
|
||||
device_id = self._generate_device_id(hardware_info)
|
||||
|
||||
# 保存到缓存
|
||||
if self.use_cache:
|
||||
self._save_device_id_to_cache(device_id)
|
||||
|
||||
return device_id
|
||||
|
||||
def get_device_id_short(self, length: int = 16) -> str:
|
||||
"""
|
||||
获取短版本的设备ID
|
||||
|
||||
Args:
|
||||
length: 返回ID的长度
|
||||
|
||||
Returns:
|
||||
指定长度的设备ID
|
||||
"""
|
||||
full_id = self.get_device_id()
|
||||
return full_id[:length]
|
||||
|
||||
def get_hardware_info(self) -> dict:
|
||||
"""
|
||||
获取硬件信息(用于调试)
|
||||
|
||||
Returns:
|
||||
硬件信息字典
|
||||
"""
|
||||
return self._collect_hardware_info()
|
||||
|
||||
|
||||
# 使用示例
|
||||
def main():
|
||||
"""使用示例"""
|
||||
# 创建设备ID生成器实例
|
||||
device_generator = DeviceIDGenerator()
|
||||
|
||||
# 获取完整设备ID(64字符)
|
||||
device_id = device_generator.get_device_id()
|
||||
print(f"完整设备ID: {device_id}")
|
||||
|
||||
# 获取短版本设备ID(16字符)
|
||||
short_id = device_generator.get_device_id_short(16)
|
||||
print(f"短设备ID: {short_id}")
|
||||
|
||||
# 查看硬件信息(调试用)
|
||||
hardware_info = device_generator.get_hardware_info()
|
||||
print("\n硬件信息:")
|
||||
for key, value in hardware_info.items():
|
||||
print(f" {key}: {value}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1 @@
|
||||
# Utils 包:数据库、认证装饰器、模板渲染等
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,81 @@
|
||||
"""
|
||||
认证装饰器与 session 校验
|
||||
"""
|
||||
from functools import wraps
|
||||
from flask import request, redirect, url_for, session, jsonify
|
||||
|
||||
from utils.db import get_db
|
||||
|
||||
|
||||
def is_session_user_valid():
|
||||
"""校验 session 中的 user_id 是否在数据库中仍存在;不存在则清除 session 并返回 False"""
|
||||
uid = session.get('user_id')
|
||||
if not uid:
|
||||
return False
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id FROM users WHERE id = %s", (uid,))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row:
|
||||
session.clear()
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
session.clear()
|
||||
return False
|
||||
|
||||
|
||||
def get_current_admin_role():
|
||||
"""获取当前登录用户的管理角色:super_admin / admin / None(非管理员)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"SELECT id, username, is_admin, role, created_by_id FROM users WHERE id = %s",
|
||||
(session['user_id'],)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row or not row.get('is_admin'):
|
||||
return None, None
|
||||
return row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin'), row
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
def login_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get('user_id') or not is_session_user_valid():
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
if not session.get('user_id'):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({'success': False, 'error': '未登录'}), 401
|
||||
return redirect(url_for('auth.login'))
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT is_admin, role FROM users WHERE id = %s", (session['user_id'],))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row or not row.get('is_admin'):
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
return redirect(url_for('main.admin_page'))
|
||||
except Exception as e:
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
return redirect(url_for('main.admin_page'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated
|
||||
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
数据库连接与初始化
|
||||
"""
|
||||
import os
|
||||
import pymysql
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
try:
|
||||
from config import mysql_host, mysql_user, mysql_password, mysql_database
|
||||
except ImportError:
|
||||
mysql_host = os.environ.get('MYSQL_HOST', 'localhost')
|
||||
mysql_user = os.environ.get('MYSQL_USER', 'root')
|
||||
mysql_password = os.environ.get('MYSQL_PASSWORD', '')
|
||||
mysql_database = os.environ.get('MYSQL_DATABASE', 'maixiang_ai')
|
||||
|
||||
|
||||
def get_db():
|
||||
return pymysql.connect(
|
||||
host=mysql_host,
|
||||
user=mysql_user,
|
||||
password=mysql_password,
|
||||
database=mysql_database,
|
||||
charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor
|
||||
)
|
||||
|
||||
|
||||
def _create_initial_admin():
|
||||
"""若没有任何管理员,则创建默认超级管理员(首次启动时,仅一个)"""
|
||||
try:
|
||||
conn = get_db()
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT id FROM users WHERE role = 'super_admin' LIMIT 1")
|
||||
if cur.fetchone():
|
||||
conn.close()
|
||||
return
|
||||
admin_user = os.environ.get('ADMIN_USER', 'admin')
|
||||
admin_pwd = os.environ.get('ADMIN_PASSWORD', 'admin123')
|
||||
pwd_hash = generate_password_hash(admin_pwd, method='pbkdf2:sha256')
|
||||
cur.execute(
|
||||
"INSERT INTO users (username, password_hash, is_admin, role) VALUES (%s, %s, 1, 'super_admin')",
|
||||
(admin_user, pwd_hash)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def init_db():
|
||||
"""初始化数据库表,若不存在则创建"""
|
||||
conn = pymysql.connect(
|
||||
host=mysql_host,
|
||||
user=mysql_user,
|
||||
password=mysql_password,
|
||||
charset='utf8mb4'
|
||||
)
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(f"CREATE DATABASE IF NOT EXISTS `{mysql_database}` DEFAULT CHARSET utf8mb4")
|
||||
cur.execute(f"USE `{mysql_database}`")
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(64) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(256) NOT NULL,
|
||||
is_admin TINYINT(1) DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS image_history (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
panel_type VARCHAR(64) DEFAULT '',
|
||||
original_urls JSON,
|
||||
params JSON,
|
||||
result_urls JSON,
|
||||
long_image_url VARCHAR(1024) DEFAULT NULL,
|
||||
INDEX idx_user_created (user_id, created_at DESC)
|
||||
)
|
||||
""")
|
||||
try:
|
||||
cur.execute("ALTER TABLE image_history ADD COLUMN long_image_url VARCHAR(1024) DEFAULT NULL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cur.execute("ALTER TABLE users ADD COLUMN machine VARCHAR(64) DEFAULT NULL")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cur.execute("ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'normal'")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cur.execute("ALTER TABLE users ADD COLUMN created_by_id INT NULL")
|
||||
except Exception:
|
||||
pass
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS web_config (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
version VARCHAR(64) NOT NULL,
|
||||
file_url VARCHAR(1024) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS columns (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL COMMENT '栏目名',
|
||||
column_key VARCHAR(64) NOT NULL COMMENT '栏目标识',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_column_key (column_key)
|
||||
)
|
||||
""")
|
||||
cur.execute("""
|
||||
CREATE TABLE IF NOT EXISTS user_column_permission (
|
||||
user_id INT NOT NULL,
|
||||
column_id INT NOT NULL,
|
||||
PRIMARY KEY (user_id, column_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (column_id) REFERENCES columns(id) ON DELETE CASCADE
|
||||
)
|
||||
""")
|
||||
try:
|
||||
cur.execute("UPDATE users SET role = 'normal' WHERE (role IS NULL OR role = '') AND (is_admin = 0 OR is_admin IS NULL)")
|
||||
cur.execute("SELECT MIN(id) AS mid FROM users WHERE is_admin = 1")
|
||||
row = cur.fetchone()
|
||||
if row and row.get('mid'):
|
||||
mid = row['mid']
|
||||
cur.execute("UPDATE users SET role = 'super_admin' WHERE id = %s", (mid,))
|
||||
cur.execute("UPDATE users SET role = 'admin' WHERE is_admin = 1 AND id != %s", (mid,))
|
||||
cur.execute("UPDATE users SET created_by_id = %s WHERE role = 'admin' AND (created_by_id IS NULL)", (mid,))
|
||||
except Exception:
|
||||
pass
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
_create_initial_admin()
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
模板渲染:支持加密 HTML 解密后渲染
|
||||
"""
|
||||
import os
|
||||
from flask import render_template, render_template_string
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def render_html(template_name: str, **context):
|
||||
"""读取 HTML 模板:若为加密文件则先解密,再渲染。未加密或解密失败时按明文渲染。"""
|
||||
path = os.path.join(BASE_DIR, "web_source", template_name)
|
||||
if not os.path.isfile(path):
|
||||
return render_template(template_name, **context)
|
||||
with open(path, "rb") as f:
|
||||
raw = f.read()
|
||||
try:
|
||||
from html_crypto import decrypt
|
||||
content = decrypt(raw).decode("utf-8")
|
||||
except Exception:
|
||||
content = raw.decode("utf-8", errors="replace")
|
||||
return render_template_string(content, **context)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - 南日AI</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
background: #d8e4ec;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.header-title { font-size: 18px; font-weight: 600; color: #333; }
|
||||
.login-box {
|
||||
background: #fff;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
border: 1px solid #b8d4e3;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
background: #fff;
|
||||
}
|
||||
.form-group input:focus {
|
||||
border-color: #3498db;
|
||||
}
|
||||
.error-msg {
|
||||
color: #e74c3c;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-login:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.btn-login:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<span class="header-title">南日AI</span>
|
||||
</header>
|
||||
|
||||
<div class="login-box">
|
||||
<h1 class="login-title">登录</h1>
|
||||
{% if error %}
|
||||
<p class="error-msg">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form id="loginForm" method="POST" action="/login">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" name="username" placeholder="请输入用户名" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" name="password" placeholder="请输入密码" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-login" id="btnLogin">登录</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
(function() {
|
||||
fetch('/api/auth/check', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.logged_in && res.redirect) {
|
||||
window.location.href = res.redirect;
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
})();
|
||||
document.getElementById('loginForm').onsubmit = function(e) {
|
||||
e.preventDefault();
|
||||
var btn = document.getElementById('btnLogin');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '登录中...';
|
||||
var form = e.target;
|
||||
var fd = new FormData(form);
|
||||
fetch(form.action, {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(res) {
|
||||
if (res.success) {
|
||||
window.location.href = res.redirect || '/admin';
|
||||
} else {
|
||||
var errEl = document.querySelector('.error-msg');
|
||||
if (!errEl) {
|
||||
errEl = document.createElement('p');
|
||||
errEl.className = 'error-msg';
|
||||
form.insertBefore(errEl, form.firstChild);
|
||||
}
|
||||
errEl.textContent = res.error || '登录失败';
|
||||
btn.disabled = false;
|
||||
btn.textContent = '登录';
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
form.submit();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user