2941 lines
108 KiB
Python
2941 lines
108 KiB
Python
"""
|
||
管理员 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://api.aishufu.top: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/<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 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/<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
|
||
_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/<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()
|
||
return jsonify({'success': True, 'column_ids': _get_local_user_column_ids(uid, menu_type or None)})
|
||
|
||
|
||
@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()
|
||
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-categories/export')
|
||
@login_required
|
||
def export_product_categories():
|
||
_ensure_product_category_schema()
|
||
_, _, denied = _ensure_product_category_access()
|
||
if denied:
|
||
return denied
|
||
keyword = (request.args.get('keyword') or '').strip()
|
||
params = {}
|
||
if keyword:
|
||
params['keyword'] = keyword
|
||
|
||
url = f"{backend_java_base_url}/api/admin/product-categories/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('/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/<int:item_id>', 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/<int:item_id>', 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/<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 '删除成功',
|
||
})
|
||
|
||
|
||
# ---------- 不符合 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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>', 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/<int:item_id>/country/<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/<int:item_id>', 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/<int:item_id>/country/<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/<import_id>')
|
||
@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/<import_id>')
|
||
@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/<import_id>')
|
||
@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/<import_id>')
|
||
@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/<int:item_id>/country/<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/<int:item_id>/country/<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()
|