1800 lines
66 KiB
Python
1800 lines
66 KiB
Python
"""
|
||
管理员 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
|
||
|
||
|
||
ALLOWED_DEDUPE_TOTAL_DATA_ADMIN_USERNAMES = {'刘丽泓'}
|
||
|
||
|
||
def _can_access_dedupe_total_data(role, current_row):
|
||
_, _, denied = _ensure_admin_menu_access('dedupe-total-data')
|
||
return denied is None
|
||
if role == 'super_admin':
|
||
return True
|
||
if role != 'admin' or not current_row:
|
||
return False
|
||
return (current_row.get('username') or '').strip() in ALLOWED_DEDUPE_TOTAL_DATA_ADMIN_USERNAMES
|
||
|
||
|
||
def _ensure_dedupe_total_data_access():
|
||
return _ensure_admin_menu_access('dedupe-total-data')
|
||
role, current_row = get_current_admin_role()
|
||
if not _can_access_dedupe_total_data(role, current_row):
|
||
return None, None, (jsonify({'success': False, 'error': '无权访问数据去重汇总数据'}), 403)
|
||
return role, current_row, None
|
||
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')
|
||
|
||
ADMIN_MENU_ACCESS_CONFIG = {
|
||
'dedupe-total-data': {
|
||
'column_key': 'admin_dedupe_total_data',
|
||
'route_path': 'dedupe-total-data',
|
||
'error': '无权访问数据去重汇总数据',
|
||
},
|
||
'shop-manage': {
|
||
'column_key': 'admin_shop_manage',
|
||
'route_path': 'shop-manage',
|
||
'error': '无权访问店铺管理模块',
|
||
},
|
||
'skip-price-asin': {
|
||
'column_key': 'admin_skip_price_asin',
|
||
'route_path': 'skip-price-asin',
|
||
'error': '无权访问跳过跟价 ASIN 模块',
|
||
},
|
||
}
|
||
|
||
ADMIN_MENU_ACCESS_CONFIG.update({
|
||
'users': {
|
||
'column_key': 'admin_users',
|
||
'route_path': 'users',
|
||
'error': '无权访问用户管理模块',
|
||
},
|
||
'columns': {
|
||
'column_key': 'admin_columns',
|
||
'route_path': 'columns',
|
||
'error': '无权访问栏目权限配置模块',
|
||
},
|
||
'shop-keys': {
|
||
'column_key': 'admin_shop_keys',
|
||
'route_path': 'shop-keys',
|
||
'error': '无权访问店铺密钥管理模块',
|
||
},
|
||
'history': {
|
||
'column_key': 'admin_history',
|
||
'route_path': 'history',
|
||
'error': '无权访问查看生成记录模块',
|
||
},
|
||
'version': {
|
||
'column_key': 'admin_version',
|
||
'route_path': 'version',
|
||
'error': '无权访问版本管理模块',
|
||
},
|
||
})
|
||
|
||
|
||
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
|
||
|
||
|
||
def _format_permission_item(item):
|
||
return {
|
||
'id': item.get('id'),
|
||
'name': item.get('name') or '',
|
||
'column_key': item.get('columnKey') or '',
|
||
'menu_type': item.get('menuType') or 'app',
|
||
'route_path': item.get('routePath') or '',
|
||
'sort_order': item.get('sortOrder') if item.get('sortOrder') is not None else 0,
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
}
|
||
|
||
|
||
def _parse_optional_int(value):
|
||
if value is None:
|
||
return None
|
||
text = str(value).strip()
|
||
if text == '':
|
||
return None
|
||
return int(text)
|
||
|
||
|
||
def _ensure_column_sort_schema():
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
try:
|
||
cur.execute("ALTER TABLE columns ADD COLUMN sort_order INT NOT NULL DEFAULT 0 COMMENT '菜单排序' AFTER route_path")
|
||
except Exception:
|
||
pass
|
||
try:
|
||
cur.execute("UPDATE columns SET sort_order = id WHERE sort_order IS NULL OR sort_order = 0")
|
||
except Exception:
|
||
pass
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _load_local_column_sort_map():
|
||
_ensure_column_sort_schema()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute("SELECT id, sort_order FROM columns")
|
||
rows = cur.fetchall()
|
||
return {int(row['id']): int(row.get('sort_order') or 0) for row in rows if row.get('id') is not None}
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _load_local_columns(menu_type=None):
|
||
_ensure_column_sort_schema()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
sql = """
|
||
SELECT id, name, column_key, menu_type, route_path, sort_order, created_at
|
||
FROM columns
|
||
"""
|
||
params = []
|
||
if menu_type:
|
||
sql += " WHERE menu_type = %s"
|
||
params.append(menu_type)
|
||
sql += " ORDER BY sort_order ASC, id ASC"
|
||
cur.execute(sql, params)
|
||
return cur.fetchall()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _sync_local_columns_from_backend(items):
|
||
if not items:
|
||
return
|
||
_ensure_column_sort_schema()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
for item in items:
|
||
item_id = item.get('id')
|
||
if item_id is None:
|
||
continue
|
||
sort_order = item.get('sort_order')
|
||
cur.execute(
|
||
"""
|
||
INSERT INTO columns (id, name, column_key, menu_type, route_path, sort_order, created_at)
|
||
VALUES (%s, %s, %s, %s, %s, %s, COALESCE(%s, NOW()))
|
||
ON DUPLICATE KEY UPDATE
|
||
name = VALUES(name),
|
||
column_key = VALUES(column_key),
|
||
menu_type = VALUES(menu_type),
|
||
route_path = VALUES(route_path),
|
||
sort_order = CASE
|
||
WHEN columns.sort_order IS NULL OR columns.sort_order = 0 THEN VALUES(sort_order)
|
||
ELSE columns.sort_order
|
||
END
|
||
""",
|
||
(
|
||
int(item_id),
|
||
item.get('name') or '',
|
||
item.get('column_key') or '',
|
||
item.get('menu_type') or 'app',
|
||
item.get('route_path') or '',
|
||
int(sort_order if sort_order not in (None, '') else item_id),
|
||
item.get('created_at') or None,
|
||
),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _set_local_column_sort_order(column_id, sort_order):
|
||
_ensure_column_sort_schema()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"UPDATE columns SET sort_order = %s WHERE id = %s",
|
||
(int(sort_order), int(column_id)),
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _get_next_local_column_sort_order():
|
||
_ensure_column_sort_schema()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute("SELECT COALESCE(MAX(sort_order), 0) AS max_sort FROM columns")
|
||
row = cur.fetchone() or {}
|
||
return int(row.get('max_sort') or 0) + 1
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _swap_local_column_sort_order(column_id, target_id):
|
||
_ensure_column_sort_schema()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute("SELECT id, sort_order FROM columns WHERE id IN (%s, %s) ORDER BY id", (int(column_id), int(target_id)))
|
||
rows = cur.fetchall()
|
||
if len(rows) != 2:
|
||
raise ValueError('菜单不存在')
|
||
by_id = {int(row['id']): int(row.get('sort_order') or 0) for row in rows}
|
||
left_sort = by_id.get(int(column_id), 0) or int(column_id)
|
||
right_sort = by_id.get(int(target_id), 0) or int(target_id)
|
||
cur.execute("UPDATE columns SET sort_order = %s WHERE id = %s", (right_sort, int(column_id)))
|
||
cur.execute("UPDATE columns SET sort_order = %s WHERE id = %s", (left_sort, int(target_id)))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
|
||
|
||
def _sync_user_column_permissions(user_id, column_ids):
|
||
_, error_response, status = _proxy_backend_java(
|
||
'PUT',
|
||
f'/api/admin/permission-users/{user_id}/columns',
|
||
json_data={'columnIds': [int(x) for x in (column_ids or []) if str(x).isdigit()]},
|
||
)
|
||
return error_response, status
|
||
|
||
|
||
def _get_current_admin_id(current_row=None):
|
||
if current_row and current_row.get('id'):
|
||
return current_row.get('id')
|
||
return session.get('user_id')
|
||
|
||
|
||
def _get_current_admin_permission_sets():
|
||
user_id = _get_current_admin_id()
|
||
if not user_id:
|
||
return set(), set()
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute(
|
||
"""SELECT c.column_key, c.route_path
|
||
FROM user_column_permission ucp
|
||
INNER JOIN columns c ON c.id = ucp.column_id
|
||
WHERE ucp.user_id = %s AND c.menu_type = 'admin'""",
|
||
(user_id,),
|
||
)
|
||
rows = cur.fetchall()
|
||
finally:
|
||
conn.close()
|
||
key_set = {(row.get('column_key') or '').strip() for row in rows if (row.get('column_key') or '').strip()}
|
||
route_set = {(row.get('route_path') or '').strip() for row in rows if (row.get('route_path') or '').strip()}
|
||
return key_set, route_set
|
||
|
||
|
||
def _ensure_admin_menu_access(*menu_names):
|
||
role, current_row = get_current_admin_role()
|
||
if role == 'super_admin':
|
||
return role, current_row, None
|
||
if role != 'admin' or not current_row:
|
||
return role, current_row, (jsonify({'success': False, 'error': '需要管理员权限'}), 403)
|
||
key_set, route_set = _get_current_admin_permission_sets()
|
||
for menu_name in menu_names:
|
||
config = ADMIN_MENU_ACCESS_CONFIG.get(menu_name) or {}
|
||
column_key = (config.get('column_key') or '').strip()
|
||
route_path = (config.get('route_path') or '').strip()
|
||
if (column_key and column_key in key_set) or (route_path and route_path in route_set):
|
||
return role, current_row, None
|
||
fallback_menu = menu_names[0] if menu_names else ''
|
||
fallback_config = ADMIN_MENU_ACCESS_CONFIG.get(fallback_menu) or {}
|
||
error_message = fallback_config.get('error') or '无权访问当前模块'
|
||
return role, current_row, (jsonify({'success': False, 'error': error_message}), 403)
|
||
|
||
|
||
def _ensure_dedupe_total_data_access():
|
||
return _ensure_admin_menu_access('dedupe-total-data')
|
||
|
||
|
||
def _load_current_admin_menu_items():
|
||
role, current_row = get_current_admin_role()
|
||
if not role or not current_row:
|
||
return None, None, None, (jsonify({'success': False, 'error': '需要管理员权限'}), 403)
|
||
user_id = _get_current_admin_id(current_row)
|
||
if not user_id:
|
||
return role, current_row, None, (jsonify({'success': False, 'error': '当前登录用户缺少有效ID'}), 400)
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
f'/api/admin/permission-users/{user_id}/column-permissions',
|
||
params={'menuType': 'admin'},
|
||
)
|
||
if error_response is not None:
|
||
return role, current_row, None, (error_response, status)
|
||
items = [_format_permission_item(item) for item in (result.get('data') or [])]
|
||
return role, current_row, items, None
|
||
|
||
|
||
@admin_api.route('/current-user')
|
||
@admin_required
|
||
def get_admin_current_user():
|
||
role, current_row = get_current_admin_role()
|
||
if not role or not current_row:
|
||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||
return jsonify({
|
||
'success': True,
|
||
'item': {
|
||
'id': current_row.get('id'),
|
||
'username': current_row.get('username') or session.get('username') or '',
|
||
'role': role,
|
||
}
|
||
})
|
||
|
||
|
||
@admin_api.route('/current-user/menus')
|
||
@admin_required
|
||
def get_admin_current_user_menus():
|
||
_, _, items, denied = _load_current_admin_menu_items()
|
||
if denied:
|
||
return denied
|
||
return jsonify({'success': True, 'items': items})
|
||
|
||
|
||
@admin_api.route('/logout', methods=['POST'])
|
||
@admin_required
|
||
def admin_logout():
|
||
session.clear()
|
||
return jsonify({'success': True, 'msg': '退出成功', 'redirect': '/login'})
|
||
|
||
|
||
# ---------- 用户管理 ----------
|
||
|
||
@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)))
|
||
_, _, denied = _ensure_admin_menu_access('users', 'history', 'shop-manage', 'skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
_, _, denied = _ensure_admin_menu_access('users', 'history', 'shop-manage', 'skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
page_size = min(999, 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_id': session.get('user_id'),
|
||
'current_user_role': role,
|
||
'current_user_username': current_row.get('username') or '',
|
||
'admins': admins,
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'success': False, 'error': str(e)})
|
||
|
||
|
||
@admin_api.route('/user', methods=['POST'])
|
||
@admin_required
|
||
def create_user():
|
||
_, _, denied = _ensure_admin_menu_access('users')
|
||
if denied:
|
||
return denied
|
||
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') or []
|
||
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
|
||
conn.commit()
|
||
conn.close()
|
||
error_response, status = _sync_user_column_permissions(new_uid, column_ids)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
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 {}
|
||
_, _, denied = _ensure_admin_menu_access('users')
|
||
if denied:
|
||
return denied
|
||
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')
|
||
conn.commit()
|
||
conn.close()
|
||
if column_ids is not None:
|
||
error_response, status = _sync_user_column_permissions(uid, column_ids)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
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):
|
||
"""删除用户"""
|
||
_, _, denied = _ensure_admin_menu_access('users')
|
||
if denied:
|
||
return denied
|
||
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():
|
||
"""管理员分页获取所有生成记录"""
|
||
_, _, denied = _ensure_admin_menu_access('history')
|
||
if denied:
|
||
return denied
|
||
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():
|
||
_, _, denied = _ensure_admin_menu_access('columns', 'users')
|
||
if denied:
|
||
return denied
|
||
menu_type = (request.args.get('menu_type') or '').strip()
|
||
params = {'menuType': menu_type} if menu_type else None
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
'/api/admin/permission-menus',
|
||
params=params,
|
||
)
|
||
if error_response is None:
|
||
_sync_local_columns_from_backend([_format_permission_item(item) for item in (result.get('data') or [])])
|
||
local_rows = _load_local_columns(menu_type or None)
|
||
items = [
|
||
{
|
||
'id': row.get('id'),
|
||
'name': row.get('name') or '',
|
||
'column_key': row.get('column_key') or '',
|
||
'menu_type': row.get('menu_type') or 'app',
|
||
'route_path': row.get('route_path') or '',
|
||
'sort_order': int(row.get('sort_order') or 0),
|
||
'created_at': str(row.get('created_at') or '')[:16].replace('T', ' '),
|
||
}
|
||
for row in local_rows
|
||
]
|
||
return jsonify({'success': True, 'items': items})
|
||
|
||
|
||
@admin_api.route('/column', methods=['POST'])
|
||
@admin_required
|
||
def create_column():
|
||
_, _, denied = _ensure_admin_menu_access('columns')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
name = (data.get('name') or '').strip()
|
||
column_key = (data.get('column_key') or '').strip()
|
||
menu_type = (data.get('menu_type') or '').strip() or 'admin'
|
||
route_path = (data.get('route_path') or '').strip()
|
||
try:
|
||
sort_order = _parse_optional_int(data.get('sort_order'))
|
||
except (TypeError, ValueError):
|
||
return jsonify({'success': False, 'error': '排序必须是数字'})
|
||
if not name:
|
||
return jsonify({'success': False, 'error': '栏目名不能为空'})
|
||
if not column_key:
|
||
return jsonify({'success': False, 'error': '栏目标识不能为空'})
|
||
if not route_path:
|
||
return jsonify({'success': False, 'error': '菜单路由不能为空'})
|
||
result, error_response, status = _proxy_backend_java(
|
||
'POST',
|
||
'/api/admin/permission-menus',
|
||
json_data={
|
||
'name': name,
|
||
'columnKey': column_key,
|
||
'menuType': menu_type,
|
||
'routePath': route_path,
|
||
},
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
item = result.get('data') or {}
|
||
_sync_local_columns_from_backend([_format_permission_item(item)])
|
||
if item.get('id') is not None:
|
||
_set_local_column_sort_order(item.get('id'), sort_order if sort_order is not None else _get_next_local_column_sort_order())
|
||
return jsonify({'success': True, 'msg': result.get('message') or '创建成功', 'id': item.get('id')})
|
||
|
||
|
||
@admin_api.route('/column/<int:cid>', methods=['PUT'])
|
||
@admin_required
|
||
def update_column(cid):
|
||
_, _, denied = _ensure_admin_menu_access('columns')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
name = (data.get('name') or '').strip()
|
||
column_key = (data.get('column_key') or '').strip()
|
||
menu_type = (data.get('menu_type') or '').strip() or 'admin'
|
||
route_path = (data.get('route_path') or '').strip()
|
||
try:
|
||
sort_order = _parse_optional_int(data.get('sort_order'))
|
||
except (TypeError, ValueError):
|
||
return jsonify({'success': False, 'error': '排序必须是数字'})
|
||
if not name:
|
||
return jsonify({'success': False, 'error': '栏目名不能为空'})
|
||
if not column_key:
|
||
return jsonify({'success': False, 'error': '栏目标识不能为空'})
|
||
if not route_path:
|
||
return jsonify({'success': False, 'error': '菜单路由不能为空'})
|
||
result, error_response, status = _proxy_backend_java(
|
||
'PUT',
|
||
f'/api/admin/permission-menus/{cid}',
|
||
json_data={
|
||
'name': name,
|
||
'columnKey': column_key,
|
||
'menuType': menu_type,
|
||
'routePath': route_path,
|
||
},
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
_sync_local_columns_from_backend([{
|
||
'id': cid,
|
||
'name': name,
|
||
'column_key': column_key,
|
||
'menu_type': menu_type,
|
||
'route_path': route_path,
|
||
'sort_order': sort_order if sort_order is not None else cid,
|
||
'created_at': None,
|
||
}])
|
||
if sort_order is not None:
|
||
_set_local_column_sort_order(cid, sort_order)
|
||
return jsonify({'success': True, 'msg': result.get('message') or '更新成功'})
|
||
|
||
|
||
@admin_api.route('/column/reorder', methods=['POST'])
|
||
@admin_required
|
||
def reorder_column():
|
||
_, _, denied = _ensure_admin_menu_access('columns')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
try:
|
||
column_id = int(data.get('column_id'))
|
||
target_id = int(data.get('target_id'))
|
||
except (TypeError, ValueError):
|
||
return jsonify({'success': False, 'error': '菜单重排参数无效'}), 400
|
||
if column_id == target_id:
|
||
return jsonify({'success': True, 'msg': '排序未变化'})
|
||
try:
|
||
_swap_local_column_sort_order(column_id, target_id)
|
||
except ValueError as exc:
|
||
return jsonify({'success': False, 'error': str(exc)}), 400
|
||
except Exception as exc:
|
||
return jsonify({'success': False, 'error': str(exc)}), 500
|
||
return jsonify({'success': True, 'msg': '排序更新成功'})
|
||
|
||
|
||
@admin_api.route('/column/<int:cid>', methods=['DELETE'])
|
||
@admin_required
|
||
def delete_column(cid):
|
||
_, _, denied = _ensure_admin_menu_access('columns')
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'DELETE',
|
||
f'/api/admin/permission-menus/{cid}',
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
cur.execute("DELETE FROM columns WHERE id = %s", (cid,))
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
return jsonify({'success': True, 'msg': result.get('message') or '删除成功'})
|
||
|
||
|
||
@admin_api.route('/user/<int:uid>/columns')
|
||
@admin_required
|
||
def get_user_columns(uid):
|
||
_, _, denied = _ensure_admin_menu_access('users')
|
||
if denied:
|
||
return denied
|
||
menu_type = (request.args.get('menu_type') or '').strip()
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
f'/api/admin/permission-users/{uid}/columns',
|
||
params={'menuType': menu_type} if menu_type else None,
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
payload = result.get('data') or {}
|
||
return jsonify({'success': True, 'column_ids': payload.get('columnIds') or []})
|
||
|
||
|
||
@admin_api.route('/user/<int:uid>/column-permissions')
|
||
@admin_required
|
||
def get_user_column_permissions(uid):
|
||
_, _, denied = _ensure_admin_menu_access('users')
|
||
if denied:
|
||
return denied
|
||
menu_type = (request.args.get('menu_type') or '').strip()
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
f'/api/admin/permission-users/{uid}/column-permissions',
|
||
params={'menuType': menu_type} if menu_type else None,
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
return jsonify({'success': True, 'items': [_format_permission_item(item) for item in (result.get('data') or [])]})
|
||
|
||
|
||
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():
|
||
_, _, denied = _ensure_admin_menu_access('version')
|
||
if denied:
|
||
return denied
|
||
"""获取版本列表"""
|
||
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"""
|
||
_, _, denied = _ensure_admin_menu_access('version')
|
||
if denied:
|
||
return denied
|
||
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('/shop-keys')
|
||
@admin_required
|
||
def list_shop_keys():
|
||
_, _, denied = _ensure_admin_menu_access('shop-keys')
|
||
if denied:
|
||
return denied
|
||
page = max(1, int(request.args.get('page', 1)))
|
||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
'/api/admin/shop-keys',
|
||
params={'page': page, 'pageSize': page_size},
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
payload = result.get('data') or {}
|
||
items = [
|
||
{
|
||
'id': item.get('id'),
|
||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||
'ziniao_token': item.get('ziniaoToken') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') 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('/shop-key', methods=['POST'])
|
||
@admin_required
|
||
def create_shop_key():
|
||
_, _, denied = _ensure_admin_menu_access('shop-keys')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {
|
||
'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(),
|
||
'ziniaoToken': (data.get('ziniao_token') or '').strip(),
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'POST',
|
||
'/api/admin/shop-keys',
|
||
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'),
|
||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||
'ziniao_token': item.get('ziniaoToken') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/shop-key/<int:item_id>', methods=['PUT'])
|
||
@admin_required
|
||
def update_shop_key(item_id):
|
||
_, _, denied = _ensure_admin_menu_access('shop-keys')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {
|
||
'ziniaoAccountName': (data.get('ziniao_account_name') or '').strip(),
|
||
'ziniaoToken': (data.get('ziniao_token') or '').strip(),
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'PUT',
|
||
f'/api/admin/shop-keys/{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'),
|
||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||
'ziniao_token': item.get('ziniaoToken') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/shop-key/<int:item_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def delete_shop_key(item_id):
|
||
_, _, denied = _ensure_admin_menu_access('shop-keys')
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'DELETE',
|
||
f'/api/admin/shop-keys/{item_id}',
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
return jsonify({
|
||
'success': True,
|
||
'msg': result.get('message') or '删除成功',
|
||
})
|
||
|
||
|
||
# ---------- 数据去重总数据 ----------
|
||
|
||
@admin_api.route('/dedupe-total-data')
|
||
@admin_required
|
||
def list_dedupe_total_data():
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
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/<import_id>')
|
||
@admin_required
|
||
def dedupe_total_data_import_progress(import_id):
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
f'/api/admin/dedupe-total-data/import/{import_id}',
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
progress = result.get('data') or {}
|
||
return jsonify({
|
||
'success': True,
|
||
'progress': {
|
||
'status': progress.get('status') or 'pending',
|
||
'total_rows': progress.get('totalRows') or 0,
|
||
'processed_rows': progress.get('processedRows') or 0,
|
||
'asin_count': progress.get('asinCount') or 0,
|
||
'inserted_count': progress.get('insertedCount') or 0,
|
||
'skipped_count': progress.get('skippedCount') or 0,
|
||
'error_message': progress.get('errorMessage') or '',
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/dedupe-total-data/import', methods=['POST'])
|
||
@admin_required
|
||
def import_dedupe_total_data():
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
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 '开始导入',
|
||
'import_id': summary.get('importId') or '',
|
||
})
|
||
|
||
|
||
@admin_api.route('/dedupe-total-data/delete-import/<import_id>')
|
||
@admin_required
|
||
def dedupe_total_data_delete_import_progress(import_id):
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
f'/api/admin/dedupe-total-data/delete-import/{import_id}',
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
progress = result.get('data') or {}
|
||
return jsonify({
|
||
'success': True,
|
||
'progress': {
|
||
'status': progress.get('status') or 'pending',
|
||
'total_rows': progress.get('totalRows') or 0,
|
||
'processed_rows': progress.get('processedRows') or 0,
|
||
'asin_count': progress.get('asinCount') or 0,
|
||
'deleted_count': progress.get('insertedCount') or 0,
|
||
'skipped_count': progress.get('skippedCount') or 0,
|
||
'error_message': progress.get('errorMessage') or '',
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/dedupe-total-data/delete-import', methods=['POST'])
|
||
@admin_required
|
||
def delete_import_dedupe_total_data():
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
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/delete-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 '开始删除',
|
||
'import_id': summary.get('importId') or '',
|
||
})
|
||
|
||
|
||
@admin_api.route('/dedupe-total-data', methods=['POST'])
|
||
@admin_required
|
||
def create_dedupe_total_data():
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
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):
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
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):
|
||
_, _, denied = _ensure_dedupe_total_data_access()
|
||
if denied:
|
||
return denied
|
||
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 '删除成功',
|
||
})
|
||
|
||
|
||
# ---------- 店铺管理 ----------
|
||
|
||
@admin_api.route('/shop-manages')
|
||
@admin_required
|
||
def list_shop_manages():
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||
if denied:
|
||
return denied
|
||
page = max(1, int(request.args.get('page', 1)))
|
||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||
group_id_raw = (request.args.get('group_id') or '').strip()
|
||
shop_name = (request.args.get('shop_name') or '').strip()
|
||
|
||
params = {'page': page, 'pageSize': page_size}
|
||
if current_row and current_row.get('id'):
|
||
params['operatorId'] = current_row.get('id')
|
||
params['superAdmin'] = 'true' if role == 'super_admin' else 'false'
|
||
if group_id_raw:
|
||
try:
|
||
group_id = int(group_id_raw)
|
||
if group_id > 0:
|
||
params['groupId'] = group_id
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if shop_name:
|
||
params['shopName'] = shop_name
|
||
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
'/api/admin/shop-manages',
|
||
params=params,
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
payload = result.get('data') or {}
|
||
items = [
|
||
{
|
||
'id': item.get('id'),
|
||
'group_id': item.get('groupId'),
|
||
'group_name': item.get('groupName') or '',
|
||
'shop_name': item.get('shopName') or '',
|
||
'mall_name': item.get('mallName') or '',
|
||
'account': item.get('account') or '',
|
||
'password': item.get('passwordMasked') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') 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('/shop-manage', methods=['POST'])
|
||
@admin_required
|
||
def create_shop_manage():
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {
|
||
'groupId': data.get('group_id'),
|
||
'shopName': (data.get('shop_name') or '').strip(),
|
||
'mallName': (data.get('mall_name') or '').strip(),
|
||
'account': (data.get('account') or '').strip(),
|
||
'password': (data.get('password') or '').strip(),
|
||
'createdById': current_row.get('id') if current_row else None,
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'POST',
|
||
'/api/admin/shop-manages',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
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'),
|
||
'group_id': item.get('groupId'),
|
||
'group_name': item.get('groupName') or '',
|
||
'shop_name': item.get('shopName') or '',
|
||
'mall_name': item.get('mallName') or '',
|
||
'account': item.get('account') or '',
|
||
'password': item.get('password') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/shop-manage/<int:item_id>', methods=['PUT'])
|
||
@admin_required
|
||
def update_shop_manage(item_id):
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {
|
||
'groupId': data.get('group_id'),
|
||
'shopName': (data.get('shop_name') or '').strip(),
|
||
'mallName': (data.get('mall_name') or '').strip(),
|
||
'account': (data.get('account') or '').strip(),
|
||
'password': (data.get('password') or '').strip(),
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'PUT',
|
||
f'/api/admin/shop-manages/{item_id}',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
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'),
|
||
'group_name': item.get('groupName') or '',
|
||
'shop_name': item.get('shopName') or '',
|
||
'mall_name': item.get('mallName') or '',
|
||
'account': item.get('account') or '',
|
||
'password': item.get('password') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/shop-manage/<int:item_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def delete_shop_manage(item_id):
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'DELETE',
|
||
f'/api/admin/shop-manages/{item_id}',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
return jsonify({
|
||
'success': True,
|
||
'msg': result.get('message') or '删除成功',
|
||
})
|
||
|
||
|
||
@admin_api.route('/shop-manage-groups')
|
||
@admin_required
|
||
def list_shop_manage_groups():
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
params = {}
|
||
if current_row and current_row.get('id'):
|
||
params['operatorId'] = current_row.get('id')
|
||
params['superAdmin'] = 'true' if role == 'super_admin' else 'false'
|
||
if role != 'super_admin' and current_row and current_row.get('id'):
|
||
params['createdById'] = current_row.get('id')
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
'/api/admin/shop-manages/groups',
|
||
params=params,
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
items = [
|
||
{
|
||
'id': item.get('id'),
|
||
'group_name': item.get('groupName') or '',
|
||
'user_id': item.get('userId'),
|
||
'username': item.get('username') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
}
|
||
for item in (result.get('data') or [])
|
||
]
|
||
return jsonify({'success': True, 'items': items})
|
||
|
||
|
||
@admin_api.route('/shop-manage-group', methods=['POST'])
|
||
@admin_required
|
||
def create_shop_manage_group():
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {
|
||
'groupName': (data.get('group_name') or '').strip(),
|
||
'userId': data.get('user_id'),
|
||
'createdById': current_row.get('id') if current_row else None,
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'POST',
|
||
'/api/admin/shop-manages/groups',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
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'),
|
||
'group_name': item.get('groupName') or '',
|
||
'user_id': item.get('userId'),
|
||
'username': item.get('username') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['PUT'])
|
||
@admin_required
|
||
def update_shop_manage_group(item_id):
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {
|
||
'groupName': (data.get('group_name') or '').strip(),
|
||
'userId': data.get('user_id'),
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'PUT',
|
||
f'/api/admin/shop-manages/groups/{item_id}',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
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'),
|
||
'group_name': item.get('groupName') or '',
|
||
'user_id': item.get('userId'),
|
||
'username': item.get('username') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/skip-price-asins')
|
||
@admin_required
|
||
def list_skip_price_asins():
|
||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
page = max(1, int(request.args.get('page', 1)))
|
||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||
group_id_raw = (request.args.get('group_id') or '').strip()
|
||
shop_name = (request.args.get('shop_name') or '').strip()
|
||
asin = (request.args.get('asin') or '').strip()
|
||
params = {'page': page, 'pageSize': page_size}
|
||
if current_row and current_row.get('id'):
|
||
params['operatorId'] = current_row.get('id')
|
||
params['superAdmin'] = 'true' if role == 'super_admin' else 'false'
|
||
if group_id_raw:
|
||
try:
|
||
group_id = int(group_id_raw)
|
||
if group_id > 0:
|
||
params['groupId'] = group_id
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if shop_name:
|
||
params['shopName'] = shop_name
|
||
if asin:
|
||
params['asin'] = asin
|
||
result, error_response, status = _proxy_backend_java(
|
||
'GET',
|
||
'/api/admin/skip-price-asins',
|
||
params=params,
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
payload = result.get('data') or {}
|
||
items = [
|
||
{
|
||
'id': item.get('id'),
|
||
'group_id': item.get('groupId'),
|
||
'group_name': item.get('groupName') or '',
|
||
'shop_name': item.get('shopName') or '',
|
||
'asin_de': item.get('asinDe') or '',
|
||
'asin_uk': item.get('asinUk') or '',
|
||
'asin_fr': item.get('asinFr') or '',
|
||
'asin_it': item.get('asinIt') or '',
|
||
'asin_es': item.get('asinEs') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') 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('/skip-price-asin', methods=['POST'])
|
||
@admin_required
|
||
def create_skip_price_asin():
|
||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
asin_mappings = data.get('asin_mappings') or {}
|
||
fallback_asin = (data.get('asin') or '').strip()
|
||
if not fallback_asin and isinstance(asin_mappings, dict):
|
||
for value in asin_mappings.values():
|
||
fallback_asin = (value or '').strip()
|
||
if fallback_asin:
|
||
break
|
||
payload = {
|
||
'groupId': data.get('group_id'),
|
||
'shopName': (data.get('shop_name') or '').strip(),
|
||
'countries': data.get('countries') or [],
|
||
'asin': fallback_asin,
|
||
'asinMappings': asin_mappings,
|
||
}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'POST',
|
||
'/api/admin/skip-price-asins',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
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'),
|
||
'group_id': item.get('groupId'),
|
||
'group_name': item.get('groupName') or '',
|
||
'shop_name': item.get('shopName') or '',
|
||
'asin_de': item.get('asinDe') or '',
|
||
'asin_uk': item.get('asinUk') or '',
|
||
'asin_fr': item.get('asinFr') or '',
|
||
'asin_it': item.get('asinIt') or '',
|
||
'asin_es': item.get('asinEs') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|
||
|
||
|
||
@admin_api.route('/skip-price-asin/<int:item_id>/country/<country>', methods=['DELETE'])
|
||
@admin_required
|
||
def delete_skip_price_asin_country(item_id, country):
|
||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'DELETE',
|
||
f'/api/admin/skip-price-asins/{item_id}/countries/{country}',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
return jsonify({'success': True, 'msg': result.get('message') or '删除成功'})
|
||
|
||
|
||
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['DELETE'])
|
||
@admin_required
|
||
def delete_shop_manage_group(item_id):
|
||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
result, error_response, status = _proxy_backend_java(
|
||
'DELETE',
|
||
f'/api/admin/shop-manages/groups/{item_id}',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
)
|
||
if error_response is not None:
|
||
return error_response, status
|
||
return jsonify({'success': True, 'msg': result.get('message') or '删除成功'})
|
||
|
||
|
||
@admin_api.route('/skip-price-asin/<int:item_id>/country/<country>', methods=['PUT'])
|
||
@admin_required
|
||
def update_skip_price_asin_country(item_id, country):
|
||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||
if denied:
|
||
return denied
|
||
data = request.get_json() or {}
|
||
payload = {'asin': (data.get('asin') or '').strip()}
|
||
result, error_response, status = _proxy_backend_java(
|
||
'PUT',
|
||
f'/api/admin/skip-price-asins/{item_id}/countries/{country}',
|
||
params={
|
||
'operatorId': current_row.get('id') if current_row else None,
|
||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||
},
|
||
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'),
|
||
'group_id': item.get('groupId'),
|
||
'group_name': item.get('groupName') or '',
|
||
'shop_name': item.get('shopName') or '',
|
||
'asin_de': item.get('asinDe') or '',
|
||
'asin_uk': item.get('asinUk') or '',
|
||
'asin_fr': item.get('asinFr') or '',
|
||
'asin_it': item.get('asinIt') or '',
|
||
'asin_es': item.get('asinEs') or '',
|
||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||
},
|
||
})
|