""" 管理员 API 蓝图:用户管理、生成历史、版本管理(后台) """ import json import os import re import threading from urllib.parse import quote import requests from requests.adapters import HTTPAdapter from flask import Blueprint, request, jsonify, session, current_app, g, Response import pymysql from werkzeug.security import generate_password_hash from utils.db import get_db from utils.auth import admin_required, login_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') _backend_java_session_local = threading.local() _column_sort_schema_checked = False _column_sort_schema_lock = threading.Lock() _product_category_schema_checked = False _product_category_schema_lock = threading.Lock() 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 模块', }, 'query-asin': { 'column_key': 'admin_query_asin', 'route_path': 'query-asin', 'error': '无权访问查询 ASIN 模块', }, 'product-categories': { 'column_key': 'admin_product_categories', 'route_path': 'product-categories', 'error': '无权访问商品类目模块', }, 'invalid-asin-data': { 'column_key': 'admin_invalid_asin_data', 'route_path': 'invalid-asin-data', '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 _get_backend_java_session(): http_session = getattr(_backend_java_session_local, 'session', None) if http_session is None: http_session = requests.Session() adapter = HTTPAdapter(pool_connections=8, pool_maxsize=32, max_retries=0) http_session.mount('http://', adapter) http_session.mount('https://', adapter) _backend_java_session_local.session = http_session return http_session def _proxy_backend_java(method, path, *, params=None, json_data=None, files=None, data=None, timeout=10): url = f"{backend_java_base_url}{path}" try: requester = requests if files else _get_backend_java_session() resp = requester.request(method=method, url=url, params=params, json=json_data, files=files, data=data, timeout=timeout) 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): created_at = item.get('createdAt') if created_at in (None, ''): created_at = item.get('created_at') if hasattr(created_at, 'strftime'): created_at_text = created_at.strftime('%Y-%m-%d %H:%M') else: created_at_text = (str(created_at).replace('T', ' ')[:16]) if created_at else '' sort_order = item.get('sortOrder') if sort_order is None: sort_order = item.get('sort_order') return { 'id': item.get('id'), 'name': item.get('name') or '', 'column_key': item.get('columnKey') or item.get('column_key') or '', 'menu_type': item.get('menuType') or item.get('menu_type') or 'app', 'route_path': item.get('routePath') or item.get('route_path') or '', 'sort_order': sort_order if sort_order is not None else 0, 'created_at': created_at_text, } 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(): global _column_sort_schema_checked if _column_sort_schema_checked: return with _column_sort_schema_lock: if _column_sort_schema_checked: return 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() _column_sort_schema_checked = True finally: conn.close() return 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() PRODUCT_CATEGORY_DEFAULT_TREE = [ { 'name': '护肤品', 'key': 'skincare', 'children': [ ('面霜', 'skincare_cream'), ('精华', 'skincare_essence'), ('面膜', 'skincare_mask'), ('爽肤水', 'skincare_toner'), ('身体乳', 'skincare_body_lotion'), ], }, { 'name': '彩妆', 'key': 'makeup', 'children': [ ('口红', 'makeup_lipstick'), ('粉底', 'makeup_foundation'), ('眼影', 'makeup_eyeshadow'), ('睫毛', 'makeup_mascara'), ], }, { 'name': '洗护', 'key': 'haircare', 'children': [ ('洗发水', 'haircare_shampoo'), ('护发素', 'haircare_conditioner'), ('染发剂', 'haircare_hair_dye'), ('烫发膏', 'haircare_perm_cream'), ], }, { 'name': '功效类', 'key': 'functional', 'children': [ ('防晒', 'functional_sunscreen'), ('祛斑', 'functional_spot_removal'), ('美白', 'functional_whitening'), ('祛痘产品', 'functional_acne_care'), ], }, { 'name': '香氛', 'key': 'fragrance', 'children': [ ('香水', 'fragrance_perfume'), ('香薰精油(高酒精属于危险品)', 'fragrance_essential_oil_hazardous'), ], }, ] def _ensure_local_admin_menu_item(name, column_key, route_path, sort_order): _ensure_column_sort_schema() conn = get_db() try: with conn.cursor() as cur: cur.execute( """ INSERT INTO columns (name, column_key, menu_type, route_path, sort_order) VALUES (%s, %s, 'admin', %s, %s) ON DUPLICATE KEY UPDATE name = VALUES(name), menu_type = 'admin', route_path = VALUES(route_path), sort_order = VALUES(sort_order) """, (name, column_key, route_path, int(sort_order)), ) conn.commit() finally: conn.close() def _ensure_product_category_schema(): global _product_category_schema_checked if _product_category_schema_checked: return with _product_category_schema_lock: if _product_category_schema_checked: return _ensure_local_admin_menu_item('商品类目', 'admin_product_categories', 'product-categories', 66) _product_category_schema_checked = True 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() cache_key = f'_admin_permission_sets_{user_id}' cached = getattr(g, cache_key, None) if cached is not None: return cached 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()} cached = (key_set, route_set) setattr(g, cache_key, cached) return cached 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 def _load_current_backend_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 def _ensure_backend_menu_access(*menu_names): role, current_row = get_current_admin_role() if role == 'super_admin': return role, current_row, None if not role 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_product_category_access(): role, current_row, items, denied = _load_current_backend_menu_items() if denied: return role, current_row, denied if role == 'super_admin': return role, current_row, None for item in items or []: if (item.get('column_key') or '').strip() == 'admin_product_categories': return role, current_row, None if (item.get('route_path') or '').strip() == 'product-categories': return role, current_row, None return role, current_row, (jsonify({'success': False, 'error': '无权访问商品类目模块'}), 403) def _get_column_ids_by_route_paths(route_paths, menu_type='admin'): normalized_paths = [(path or '').strip() for path in (route_paths or []) if (path or '').strip()] if not normalized_paths: return [] conn = get_db() try: with conn.cursor() as cur: placeholders = ','.join(['%s'] * len(normalized_paths)) cur.execute( f"SELECT id FROM columns WHERE menu_type = %s AND route_path IN ({placeholders}) ORDER BY id ASC", [menu_type] + normalized_paths, ) rows = cur.fetchall() return [int(row['id']) for row in rows if row.get('id') is not None] finally: conn.close() def _grant_backend_menu_permissions(user_ids, route_paths): normalized_user_ids = [int(uid) for uid in (user_ids or []) if str(uid).isdigit() and int(uid) > 0] column_ids = _get_column_ids_by_route_paths(route_paths, 'admin') if not normalized_user_ids or not column_ids: return conn = get_db() try: with conn.cursor() as cur: for user_id in normalized_user_ids: for column_id in column_ids: cur.execute( "INSERT IGNORE INTO user_column_permission (user_id, column_id) VALUES (%s, %s)", (user_id, column_id), ) conn.commit() finally: conn.close() @admin_api.route('/current-user') @login_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') @login_required def get_admin_current_user_menus(): _ensure_product_category_schema() _, _, items, denied = _load_current_backend_menu_items() if denied: return denied return jsonify({'success': True, 'items': items}) @admin_api.route('/logout', methods=['POST']) @login_required def admin_logout(): session.clear() response = jsonify({'success': True, 'msg': '退出成功', 'redirect': '/login?logout=1'}) response.delete_cookie(current_app.config.get('SESSION_COOKIE_NAME', 'session')) response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0' return response # ---------- 用户管理 ---------- @admin_api.route('/users') @login_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_backend_menu_access('users', 'history', 'shop-manage', 'skip-price-asin', 'query-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 can_view_all_users = role == 'super_admin' or (current_row and (current_row.get('username') or '').strip().lower() == 'admin') can_view_child_users = role == 'admin' and not can_view_all_users if not can_view_all_users: created_by_id = None try: conn = get_db() with conn.cursor() as cur: if can_view_all_users: 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()] elif can_view_child_users: 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 = [] else: where_parts = ["u.id = %s"] params = [current_row['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 _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/', 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 and 'column_ids' not in data: 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), ) if 'column_ids' in data: _set_user_column_permissions(cur, uid, data.get('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/', 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 _ensure_product_category_schema() menu_type = (request.args.get('menu_type') or '').strip() 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/', 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/', 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//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() return jsonify({'success': True, 'column_ids': _get_local_user_column_ids(uid, menu_type or None)}) @admin_api.route('/user//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() items = _get_local_user_column_permission_items(uid, menu_type or None) return jsonify({'success': True, 'items': [_format_permission_item(item) for item in items]}) 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) ---------- # ---------- 商品类目 ---------- def _format_product_category_item(item, include_children=True): formatted = { 'id': item.get('id'), 'parent_id': item.get('parentId'), 'name': item.get('name') or '', 'category_key': item.get('categoryKey') or '', 'sort_order': item.get('sortOrder') if item.get('sortOrder') is not None else 0, 'description': item.get('description') or '', 'is_builtin': bool(item.get('isBuiltin')), 'child_count': item.get('childCount') if item.get('childCount') is not None else 0, 'level': item.get('level') if item.get('level') is not None else 0, 'path': item.get('path') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], } if include_children: formatted['children'] = [_format_product_category_item(child) for child in (item.get('children') or [])] return formatted def _build_product_category_payload_for_java(data): sort_order = data.get('sort_order') if 'sort_order' in data else data.get('sortOrder') parent_id = data.get('parent_id') if 'parent_id' in data else data.get('parentId') if parent_id == '': parent_id = None if sort_order == '': sort_order = None return { 'parentId': parent_id, 'name': (data.get('name') or '').strip(), 'sortOrder': sort_order, 'description': (data.get('description') or '').strip(), } @admin_api.route('/product-categories') @login_required def list_product_categories(): _ensure_product_category_schema() _, _, denied = _ensure_product_category_access() if denied: return denied keyword = (request.args.get('keyword') or '').strip() query = f'?keyword={quote(keyword)}' if keyword else '' result, error_response, status = _proxy_backend_java('GET', f'/api/admin/product-categories{query}') if error_response is not None: return error_response, status payload = result.get('data') or {} return jsonify({ 'success': True, 'tree': [_format_product_category_item(item) for item in (payload.get('tree') or [])], 'items': [_format_product_category_item(item, include_children=False) for item in (payload.get('items') or [])], 'total': payload.get('total') or 0, 'page': payload.get('page') or 1, 'page_size': payload.get('pageSize') or 0, 'has_more': bool(payload.get('hasMore')), }) @admin_api.route('/product-categories/children') @login_required def list_product_category_children(): _ensure_product_category_schema() _, _, denied = _ensure_product_category_access() if denied: return denied parent_id_raw = (request.args.get('parent_id') or request.args.get('parentId') or '').strip() page = max(1, int(request.args.get('page', 1))) page_size = min(100, max(1, int(request.args.get('page_size', request.args.get('pageSize', 20))))) params = { 'page': page, 'pageSize': page_size, } if parent_id_raw: params['parentId'] = parent_id_raw result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/product-categories/children', params=params, ) if error_response is not None: return error_response, status payload = result.get('data') or {} return jsonify({ 'success': True, 'items': [_format_product_category_item(item, include_children=False) for item in (payload.get('items') or [])], 'total': payload.get('total') or 0, 'page': payload.get('page') or page, 'page_size': payload.get('pageSize') or page_size, 'has_more': bool(payload.get('hasMore')), }) @admin_api.route('/product-categories/search') @login_required def search_product_categories(): _ensure_product_category_schema() _, _, denied = _ensure_product_category_access() if denied: return denied keyword = (request.args.get('keyword') or '').strip() page = max(1, int(request.args.get('page', 1))) page_size = min(100, max(1, int(request.args.get('page_size', request.args.get('pageSize', 20))))) result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/product-categories/search', params={ 'keyword': keyword, 'page': page, 'pageSize': page_size, }, ) if error_response is not None: return error_response, status payload = result.get('data') or {} return jsonify({ 'success': True, 'items': [_format_product_category_item(item, include_children=False) for item in (payload.get('items') or [])], 'total': payload.get('total') or 0, 'page': payload.get('page') or page, 'page_size': payload.get('pageSize') or page_size, 'has_more': bool(payload.get('hasMore')), }) @admin_api.route('/product-category', methods=['POST']) @login_required def create_product_category(): _ensure_product_category_schema() _, _, denied = _ensure_product_category_access() if denied: return denied result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/product-category', json_data=_build_product_category_payload_for_java(request.get_json() or {}), ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'msg': result.get('message') or '创建成功', 'item': _format_product_category_item(result.get('data') or {}, include_children=False), }) @admin_api.route('/product-category/', methods=['PUT']) @login_required def update_product_category(item_id): _ensure_product_category_schema() _, _, denied = _ensure_product_category_access() if denied: return denied result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/product-category/{item_id}', json_data=_build_product_category_payload_for_java(request.get_json() or {}), ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'msg': result.get('message') or '保存成功', 'item': _format_product_category_item(result.get('data') or {}, include_children=False), }) @admin_api.route('/product-category/', methods=['DELETE']) @login_required def delete_product_category(item_id): _ensure_product_category_schema() _, _, denied = _ensure_product_category_access() if denied: return denied result, error_response, status = _proxy_backend_java('DELETE', f'/api/admin/product-category/{item_id}') if error_response is not None: return error_response, status return jsonify({'success': True, 'msg': result.get('message') or '删除成功'}) @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/', 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/', 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/') @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/') @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/', 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/', 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 '删除成功', }) # ---------- 不符合 ASIN 数据 ---------- def _format_invalid_asin_data_item(item): return { 'id': item.get('id'), 'data_value': item.get('dataValue') or '', 'brand': item.get('brand') or '', 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], } @admin_api.route('/invalid-asin-data') @admin_required def list_invalid_asin_data(): _, _, denied = _ensure_admin_menu_access('invalid-asin-data') 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/invalid-asin-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 = [_format_invalid_asin_data_item(item) 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('/invalid-asin-data', methods=['POST']) @admin_required def create_invalid_asin_data(): _, _, denied = _ensure_admin_menu_access('invalid-asin-data') if denied: return denied data = request.get_json() or {} payload = { 'dataValue': (data.get('data_value') or '').strip(), 'brand': (data.get('brand') or '').strip(), } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/invalid-asin-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': _format_invalid_asin_data_item(item), }) @admin_api.route('/invalid-asin-data/', methods=['PUT']) @admin_required def update_invalid_asin_data(item_id): _, _, denied = _ensure_admin_menu_access('invalid-asin-data') if denied: return denied data = request.get_json() or {} payload = { 'dataValue': (data.get('data_value') or '').strip(), 'brand': (data.get('brand') or '').strip(), } result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/invalid-asin-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': _format_invalid_asin_data_item(item), }) @admin_api.route('/invalid-asin-data/', methods=['DELETE']) @admin_required def delete_invalid_asin_data(item_id): _, _, denied = _ensure_admin_menu_access('invalid-asin-data') if denied: return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/invalid-asin-data/{item_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'msg': result.get('message') or '删除成功', }) # ---------- 店铺管理 ---------- def _format_shop_manage_item(item): return { '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], } def _format_shop_manage_group_item(item): return { 'id': item.get('id'), 'group_name': item.get('groupName') or '', 'leader_user_id': item.get('leaderUserId'), 'leader_username': item.get('leaderUsername') or '', 'member_user_ids': item.get('memberUserIds') or [], 'member_usernames': item.get('memberUsernames') or [], 'member_count': item.get('memberCount') or 0, '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], } def _load_expanded_shop_manage_groups(role, current_row): params = {} if current_row and current_row.get('id'): params['operatorId'] = current_row.get('id') params['superAdmin'] = 'true' if role == 'super_admin' else 'false' result, error_response, status = _proxy_backend_java( 'GET', '/api/admin/shop-manages/groups', params=params, ) if error_response is not None: return None, error_response, status return result.get('data') or [], None, 200 @admin_api.route('/shop-manages') @login_required def list_shop_manages(): role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin', 'query-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() 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 {} return jsonify({ 'success': True, 'items': [_format_shop_manage_item(item) for item in (payload.get('items') or [])], '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']) @login_required def create_shop_manage(): role, current_row, denied = _ensure_backend_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/', methods=['PUT']) @login_required def update_shop_manage(item_id): role, current_row, denied = _ensure_backend_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/', methods=['DELETE']) @login_required def delete_shop_manage(item_id): role, current_row, denied = _ensure_backend_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') @login_required def list_shop_manage_groups(): role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin', 'query-asin') if denied: return denied groups, error_response, status = _load_expanded_shop_manage_groups(role, current_row) if error_response is not None: return error_response, status return jsonify({'success': True, 'items': [_format_shop_manage_group_item(item) for item in (groups or [])]}) @admin_api.route('/shop-manage-group', methods=['POST']) @login_required def create_shop_manage_group(): role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin', 'query-asin') if denied: return denied data = request.get_json() or {} grant_menu_routes = data.get('grant_menu_routes') or [] payload = { 'groupName': (data.get('group_name') or '').strip(), 'memberUserIds': data.get('member_user_ids') or [], '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 _grant_backend_menu_permissions(data.get('member_user_ids') or [], grant_menu_routes) 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 '', 'leader_user_id': item.get('leaderUserId'), 'leader_username': item.get('leaderUsername') or '', 'member_user_ids': item.get('memberUserIds') or [], 'member_usernames': item.get('memberUsernames') or [], 'member_count': item.get('memberCount') or 0, '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/', methods=['PUT']) @login_required def update_shop_manage_group(item_id): role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin', 'query-asin') if denied: return denied data = request.get_json() or {} grant_menu_routes = data.get('grant_menu_routes') or [] payload = { 'groupName': (data.get('group_name') or '').strip(), 'memberUserIds': data.get('member_user_ids') or [], } 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 _grant_backend_menu_permissions(data.get('member_user_ids') or [], grant_menu_routes) 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 '', 'leader_user_id': item.get('leaderUserId'), 'leader_username': item.get('leaderUsername') or '', 'member_user_ids': item.get('memberUserIds') or [], 'member_usernames': item.get('memberUsernames') or [], 'member_count': item.get('memberCount') or 0, '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') @login_required def list_skip_price_asins(): role, current_row, denied = _ensure_backend_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, 'page_size': page_size, 'pageSize': page_size} params.update(_skip_price_asin_auth_params(role, current_row)) if group_id_raw: try: group_id = int(group_id_raw) if group_id > 0: params['group_id'] = group_id params['groupId'] = group_id except (TypeError, ValueError): pass if shop_name: params['shop_name'] = 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 '', 'minimum_price_de': item.get('minimumPriceDe'), 'asin_uk': item.get('asinUk') or '', 'minimum_price_uk': item.get('minimumPriceUk'), 'asin_fr': item.get('asinFr') or '', 'minimum_price_fr': item.get('minimumPriceFr'), 'asin_it': item.get('asinIt') or '', 'minimum_price_it': item.get('minimumPriceIt'), 'asin_es': item.get('asinEs') or '', 'minimum_price_es': item.get('minimumPriceEs'), '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, }) def _skip_price_asin_auth_params(role, current_row): operator_id = current_row.get('id') if current_row else None super_admin = 'true' if role == 'super_admin' else 'false' return { 'operator_id': operator_id, 'operatorId': operator_id, 'super_admin': super_admin, 'superAdmin': super_admin, } @admin_api.route('/skip-price-asins/export') @login_required def export_skip_price_asins(): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied params = _skip_price_asin_auth_params(role, current_row) group_id_raw = (request.args.get('group_id') or request.args.get('groupId') or '').strip() shop_name = (request.args.get('shop_name') or request.args.get('shopName') or '').strip() asin = (request.args.get('asin') or '').strip() if group_id_raw: params['group_id'] = group_id_raw params['groupId'] = group_id_raw if shop_name: params['shop_name'] = shop_name params['shopName'] = shop_name if asin: params['asin'] = asin url = f"{backend_java_base_url}/api/admin/skip-price-asins/export" try: resp = _get_backend_java_session().get(url, params=params, timeout=60) except requests.RequestException: return jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502 if resp.status_code >= 400: try: data = resp.json() error = data.get('message') or data.get('error') or '导出失败' except ValueError: error = '导出失败' return jsonify({'success': False, 'error': error}), resp.status_code headers = {} disposition = resp.headers.get('Content-Disposition') if disposition: headers['Content-Disposition'] = disposition return Response( resp.content, status=resp.status_code, headers=headers, content_type=resp.headers.get( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ), ) @admin_api.route('/skip-price-asin', methods=['POST']) @login_required def create_skip_price_asin(): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied data = request.get_json() or {} asin_mappings = data.get('asin_mappings') or {} minimum_price_mappings = data.get('minimum_price_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 fallback_minimum_price = data.get('minimum_price') if fallback_minimum_price in ('', None) and isinstance(minimum_price_mappings, dict): for value in minimum_price_mappings.values(): if value not in ('', None): fallback_minimum_price = value 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, 'minimumPrice': fallback_minimum_price, 'minimumPriceMappings': minimum_price_mappings, } result, error_response, status = _proxy_backend_java( 'POST', '/api/admin/skip-price-asins', params=_skip_price_asin_auth_params(role, current_row), 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 '', 'minimum_price_de': item.get('minimumPriceDe'), 'asin_uk': item.get('asinUk') or '', 'minimum_price_uk': item.get('minimumPriceUk'), 'asin_fr': item.get('asinFr') or '', 'minimum_price_fr': item.get('minimumPriceFr'), 'asin_it': item.get('asinIt') or '', 'minimum_price_it': item.get('minimumPriceIt'), 'asin_es': item.get('asinEs') or '', 'minimum_price_es': item.get('minimumPriceEs'), 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/skip-price-asin//country/', methods=['DELETE']) @login_required def delete_skip_price_asin_country(item_id, country): role, current_row, denied = _ensure_backend_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=_skip_price_asin_auth_params(role, current_row), ) 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/', methods=['DELETE']) @login_required def delete_shop_manage_group(item_id): role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin', 'query-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//country/', methods=['PUT']) @login_required def update_skip_price_asin_country(item_id, country): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied data = request.get_json() or {} payload = { 'asin': (data.get('asin') or '').strip(), 'minimumPrice': data.get('minimum_price'), } result, error_response, status = _proxy_backend_java( 'PUT', f'/api/admin/skip-price-asins/{item_id}/countries/{country}', params=_skip_price_asin_auth_params(role, current_row), 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 '', 'minimum_price_de': item.get('minimumPriceDe'), 'asin_uk': item.get('asinUk') or '', 'minimum_price_uk': item.get('minimumPriceUk'), 'asin_fr': item.get('asinFr') or '', 'minimum_price_fr': item.get('minimumPriceFr'), 'asin_it': item.get('asinIt') or '', 'minimum_price_it': item.get('minimumPriceIt'), 'asin_es': item.get('asinEs') or '', 'minimum_price_es': item.get('minimumPriceEs'), 'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16], 'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16], }, }) @admin_api.route('/skip-price-asins/import/') @login_required def skip_price_asin_import_progress(import_id): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/skip-price-asins/import/{import_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'progress': _format_query_asin_import_progress(result.get('data') or {}), }) @admin_api.route('/skip-price-asins/import', methods=['POST']) @login_required def import_skip_price_asins(): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择 Excel 文件'}) group_id_raw = (request.form.get('group_id') or '').strip() if not group_id_raw: return jsonify({'success': False, 'error': '请先选择分组'}) params = _skip_price_asin_auth_params(role, current_row) params['group_id'] = group_id_raw params['groupId'] = group_id_raw 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/skip-price-asins/import', params=params, files=files, timeout=60, ) 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('/skip-price-asins/delete-import/') @login_required def skip_price_asin_delete_import_progress(import_id): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/skip-price-asins/delete-import/{import_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'progress': _format_query_asin_import_progress(result.get('data') or {}), }) @admin_api.route('/skip-price-asins/delete-import', methods=['POST']) @login_required def delete_import_skip_price_asins(): role, current_row, denied = _ensure_backend_menu_access('skip-price-asin') if denied: return denied file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择 Excel 文件'}) group_id_raw = (request.form.get('group_id') or '').strip() if not group_id_raw: return jsonify({'success': False, 'error': '请先选择分组'}) params = _skip_price_asin_auth_params(role, current_row) params['group_id'] = group_id_raw params['groupId'] = group_id_raw 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/skip-price-asins/delete-import', params=params, files=files, timeout=60, ) 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 '', }) def _format_query_asin_item(item): return { '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('/query-asins') @login_required def list_query_asins(): role, current_row, denied = _ensure_backend_menu_access('query-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/query-asins', params=params, ) if error_response is not None: return error_response, status payload = result.get('data') or {} return jsonify({ 'success': True, 'items': [_format_query_asin_item(item) for item in (payload.get('items') or [])], 'total': payload.get('total') or 0, 'page': payload.get('page') or page, 'page_size': payload.get('pageSize') or page_size, }) @admin_api.route('/query-asins/export') @login_required def export_query_asins(): role, current_row, denied = _ensure_backend_menu_access('query-asin') if denied: return denied params = { 'operatorId': current_row.get('id') if current_row else None, 'superAdmin': 'true' if role == 'super_admin' else 'false', } group_id_raw = (request.args.get('group_id') or request.args.get('groupId') or '').strip() shop_name = (request.args.get('shop_name') or request.args.get('shopName') or '').strip() asin = (request.args.get('asin') or '').strip() if group_id_raw: params['groupId'] = group_id_raw if shop_name: params['shopName'] = shop_name if asin: params['asin'] = asin url = f"{backend_java_base_url}/api/admin/query-asins/export" try: resp = _get_backend_java_session().get(url, params=params, timeout=60) except requests.RequestException: return jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502 if resp.status_code >= 400: try: data = resp.json() error = data.get('message') or data.get('error') or '导出失败' except ValueError: error = '导出失败' return jsonify({'success': False, 'error': error}), resp.status_code headers = {} disposition = resp.headers.get('Content-Disposition') if disposition: headers['Content-Disposition'] = disposition return Response( resp.content, status=resp.status_code, headers=headers, content_type=resp.headers.get( 'Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ), ) def _format_query_asin_import_progress(progress): return { '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, 'deleted_count': progress.get('deletedCount') or 0, 'skipped_count': progress.get('skippedCount') or 0, 'error_message': progress.get('errorMessage') or '', } @admin_api.route('/query-asins/import/') @login_required def query_asin_import_progress(import_id): role, current_row, denied = _ensure_backend_menu_access('query-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/query-asins/import/{import_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'progress': _format_query_asin_import_progress(result.get('data') or {}), }) @admin_api.route('/query-asins/import', methods=['POST']) @login_required def import_query_asins(): role, current_row, denied = _ensure_backend_menu_access('query-asin') if denied: return denied file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择 Excel 文件'}) group_id_raw = (request.form.get('group_id') or '').strip() params = { 'operatorId': current_row.get('id') if current_row else None, 'superAdmin': 'true' if role == 'super_admin' else 'false', } if group_id_raw: params['groupId'] = group_id_raw 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/query-asins/import', params=params, files=files, timeout=60, ) 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('/query-asins/delete-import/') @login_required def query_asin_delete_import_progress(import_id): role, current_row, denied = _ensure_backend_menu_access('query-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'GET', f'/api/admin/query-asins/delete-import/{import_id}', ) if error_response is not None: return error_response, status return jsonify({ 'success': True, 'progress': _format_query_asin_import_progress(result.get('data') or {}), }) @admin_api.route('/query-asins/delete-import', methods=['POST']) @login_required def delete_import_query_asins(): role, current_row, denied = _ensure_backend_menu_access('query-asin') if denied: return denied file_storage = request.files.get('file') if not file_storage or file_storage.filename == '': return jsonify({'success': False, 'error': '请选择 Excel 文件'}) group_id_raw = (request.form.get('group_id') or '').strip() params = { 'operatorId': current_row.get('id') if current_row else None, 'superAdmin': 'true' if role == 'super_admin' else 'false', } if group_id_raw: params['groupId'] = group_id_raw 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/query-asins/delete-import', params=params, files=files, timeout=60, ) 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('/query-asin', methods=['POST']) @login_required def create_query_asin(): role, current_row, denied = _ensure_backend_menu_access('query-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/query-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 return jsonify({ 'success': True, 'msg': result.get('message') or '保存成功', 'item': _format_query_asin_item(result.get('data') or {}), }) @admin_api.route('/query-asin//country/', methods=['DELETE']) @login_required def delete_query_asin_country(item_id, country): role, current_row, denied = _ensure_backend_menu_access('query-asin') if denied: return denied result, error_response, status = _proxy_backend_java( 'DELETE', f'/api/admin/query-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('/query-asin//country/', methods=['PUT']) @login_required def update_query_asin_country(item_id, country): role, current_row, denied = _ensure_backend_menu_access('query-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/query-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 return jsonify({ 'success': True, 'msg': result.get('message') or '保存成功', 'item': _format_query_asin_item(result.get('data') or {}), }) def _get_local_user_column_ids(user_id, menu_type=None): conn = get_db() try: with conn.cursor() as cur: sql = """ SELECT c.id FROM user_column_permission ucp INNER JOIN columns c ON c.id = ucp.column_id WHERE ucp.user_id = %s """ params = [user_id] if menu_type: sql += " AND c.menu_type = %s" params.append(menu_type) sql += " ORDER BY c.sort_order ASC, c.id ASC" cur.execute(sql, params) rows = cur.fetchall() return [int(row['id']) for row in rows if row.get('id') is not None] finally: conn.close() def _get_local_user_column_permission_items(user_id, menu_type=None): conn = get_db() try: with conn.cursor() as cur: sql = """ SELECT c.id, c.name, c.column_key, c.menu_type, c.route_path, c.sort_order, c.created_at FROM user_column_permission ucp INNER JOIN columns c ON c.id = ucp.column_id WHERE ucp.user_id = %s """ params = [user_id] if menu_type: sql += " AND c.menu_type = %s" params.append(menu_type) sql += " ORDER BY c.sort_order ASC, c.id ASC" cur.execute(sql, params) return cur.fetchall() finally: conn.close()