完成后端架构重构等
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,7 @@ import pymysql
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from utils.db import get_db
|
||||
from utils.auth import admin_required, get_current_admin_role
|
||||
from utils.auth import admin_required, login_required, get_current_admin_role
|
||||
|
||||
|
||||
ALLOWED_DEDUPE_TOTAL_DATA_ADMIN_USERNAMES = {'刘丽泓'}
|
||||
@@ -353,8 +353,82 @@ def _load_current_admin_menu_items():
|
||||
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 _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')
|
||||
@admin_required
|
||||
@login_required
|
||||
def get_admin_current_user():
|
||||
role, current_row = get_current_admin_role()
|
||||
if not role or not current_row:
|
||||
@@ -370,16 +444,16 @@ def get_admin_current_user():
|
||||
|
||||
|
||||
@admin_api.route('/current-user/menus')
|
||||
@admin_required
|
||||
@login_required
|
||||
def get_admin_current_user_menus():
|
||||
_, _, items, denied = _load_current_admin_menu_items()
|
||||
_, _, items, denied = _load_current_backend_menu_items()
|
||||
if denied:
|
||||
return denied
|
||||
return jsonify({'success': True, 'items': items})
|
||||
|
||||
|
||||
@admin_api.route('/logout', methods=['POST'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def admin_logout():
|
||||
session.clear()
|
||||
return jsonify({'success': True, 'msg': '退出成功', 'redirect': '/login'})
|
||||
@@ -388,17 +462,14 @@ def admin_logout():
|
||||
# ---------- 用户管理 ----------
|
||||
|
||||
@admin_api.route('/users')
|
||||
@admin_required
|
||||
@login_required
|
||||
def list_users():
|
||||
"""分页获取用户列表,支持用户名模糊搜索和按管理员归属筛选普通用户"""
|
||||
"""分页获取用户列表,支持用户名模糊搜索和按管理员归属筛选普通用户。"""
|
||||
role, current_row = get_current_admin_role()
|
||||
if not role:
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
return jsonify({'success': False, 'error': '需要登录'}), 403
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
_, _, denied = _ensure_admin_menu_access('users', 'history', 'shop-manage', 'skip-price-asin')
|
||||
if denied:
|
||||
return denied
|
||||
_, _, denied = _ensure_admin_menu_access('users', 'history', 'shop-manage', 'skip-price-asin')
|
||||
_, _, denied = _ensure_backend_menu_access('users', 'history', 'shop-manage', 'skip-price-asin')
|
||||
if denied:
|
||||
return denied
|
||||
page_size = min(999, max(5, int(request.args.get('page_size', 15))))
|
||||
@@ -407,6 +478,7 @@ def list_users():
|
||||
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:
|
||||
@@ -435,7 +507,7 @@ def list_users():
|
||||
total = cur.fetchone()['total']
|
||||
cur.execute("SELECT id, username FROM users WHERE role = 'admin' ORDER BY id")
|
||||
admins = [{'id': r['id'], 'username': r['username']} for r in cur.fetchall()]
|
||||
else:
|
||||
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]
|
||||
@@ -458,6 +530,28 @@ def list_users():
|
||||
)
|
||||
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'],
|
||||
@@ -485,7 +579,6 @@ def list_users():
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
@admin_api.route('/user', methods=['POST'])
|
||||
@admin_required
|
||||
def create_user():
|
||||
@@ -1341,9 +1434,9 @@ def delete_dedupe_total_data(item_id):
|
||||
# ---------- 店铺管理 ----------
|
||||
|
||||
@admin_api.route('/shop-manages')
|
||||
@admin_required
|
||||
@login_required
|
||||
def list_shop_manages():
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage')
|
||||
if denied:
|
||||
return denied
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
@@ -1397,9 +1490,9 @@ def list_shop_manages():
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage', methods=['POST'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def create_shop_manage():
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage')
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json() or {}
|
||||
@@ -1441,9 +1534,9 @@ def create_shop_manage():
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage/<int:item_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def update_shop_manage(item_id):
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage')
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json() or {}
|
||||
@@ -1483,9 +1576,9 @@ def update_shop_manage(item_id):
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage/<int:item_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def delete_shop_manage(item_id):
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage')
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
@@ -1505,9 +1598,9 @@ def delete_shop_manage(item_id):
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage-groups')
|
||||
@admin_required
|
||||
@login_required
|
||||
def list_shop_manage_groups():
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin')
|
||||
if denied:
|
||||
return denied
|
||||
params = {}
|
||||
@@ -1543,12 +1636,13 @@ def list_shop_manage_groups():
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage-group', methods=['POST'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def create_shop_manage_group():
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-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 [],
|
||||
@@ -1565,6 +1659,7 @@ def create_shop_manage_group():
|
||||
)
|
||||
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,
|
||||
@@ -1586,12 +1681,13 @@ def create_shop_manage_group():
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def update_shop_manage_group(item_id):
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-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 [],
|
||||
@@ -1607,6 +1703,7 @@ def update_shop_manage_group(item_id):
|
||||
)
|
||||
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,
|
||||
@@ -1628,9 +1725,9 @@ def update_shop_manage_group(item_id):
|
||||
|
||||
|
||||
@admin_api.route('/skip-price-asins')
|
||||
@admin_required
|
||||
@login_required
|
||||
def list_skip_price_asins():
|
||||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||||
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
|
||||
if denied:
|
||||
return denied
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
@@ -1668,10 +1765,15 @@ def list_skip_price_asins():
|
||||
'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],
|
||||
}
|
||||
@@ -1687,25 +1789,34 @@ def list_skip_price_asins():
|
||||
|
||||
|
||||
@admin_api.route('/skip-price-asin', methods=['POST'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def create_skip_price_asin():
|
||||
role, current_row, denied = _ensure_admin_menu_access('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',
|
||||
@@ -1728,10 +1839,15 @@ def create_skip_price_asin():
|
||||
'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],
|
||||
},
|
||||
@@ -1739,9 +1855,9 @@ def create_skip_price_asin():
|
||||
|
||||
|
||||
@admin_api.route('/skip-price-asin/<int:item_id>/country/<country>', methods=['DELETE'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def delete_skip_price_asin_country(item_id, country):
|
||||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||||
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
@@ -1758,9 +1874,9 @@ def delete_skip_price_asin_country(item_id, country):
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage-group/<int:item_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def delete_shop_manage_group(item_id):
|
||||
role, current_row, denied = _ensure_admin_menu_access('shop-manage', 'skip-price-asin')
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage', 'skip-price-asin')
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
@@ -1777,13 +1893,16 @@ def delete_shop_manage_group(item_id):
|
||||
|
||||
|
||||
@admin_api.route('/skip-price-asin/<int:item_id>/country/<country>', methods=['PUT'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def update_skip_price_asin_country(item_id, country):
|
||||
role, current_row, denied = _ensure_admin_menu_access('skip-price-asin')
|
||||
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()}
|
||||
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}',
|
||||
@@ -1805,10 +1924,15 @@ def update_skip_price_asin_country(item_id, country):
|
||||
'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],
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ from flask import Blueprint, request, redirect, url_for, session, jsonify
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from utils.db import get_db
|
||||
from utils.auth import login_required, is_session_user_valid, is_current_user_admin
|
||||
from utils.auth import login_required, is_session_user_valid
|
||||
from utils.render import render_html
|
||||
|
||||
auth = Blueprint('auth', __name__, url_prefix='')
|
||||
@@ -13,7 +13,7 @@ auth = Blueprint('auth', __name__, url_prefix='')
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if session.get('user_id') and is_session_user_valid() and is_current_user_admin():
|
||||
if session.get('user_id') and is_session_user_valid():
|
||||
return redirect(url_for('main.admin_page'))
|
||||
if request.method == 'POST':
|
||||
data = request.get_json() if request.is_json else request.form
|
||||
@@ -31,22 +31,17 @@ def login():
|
||||
(username,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if row and check_password_hash(row['password_hash'], password):
|
||||
session.permanent = True
|
||||
session['user_id'] = row['id']
|
||||
session['username'] = username
|
||||
if not row.get('is_admin'):
|
||||
session.clear()
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
|
||||
return render_html('login.html', error='需要管理员权限')
|
||||
if request.is_json:
|
||||
return jsonify({'success': True, 'redirect': url_for('main.admin_page')})
|
||||
return redirect(url_for('main.admin_page'))
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
except Exception as exc:
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
return jsonify({'success': False, 'error': str(exc)})
|
||||
return render_html('login.html', error='登录失败,请稍后重试')
|
||||
if request.is_json:
|
||||
return jsonify({'success': False, 'error': '用户名或密码错误'})
|
||||
@@ -57,7 +52,7 @@ def login():
|
||||
@auth.route('/api/auth/check')
|
||||
@login_required
|
||||
def api_auth_check():
|
||||
"""校验登录状态,用于页面加载时判断是否已登录"""
|
||||
"""校验登录状态,用于页面加载时判断是否已登录。"""
|
||||
if not session.get('user_id'):
|
||||
return jsonify({'logged_in': False})
|
||||
try:
|
||||
@@ -66,7 +61,7 @@ def api_auth_check():
|
||||
cur.execute("SELECT machine, is_admin FROM users WHERE id = %s", (session['user_id'],))
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row or not row.get('is_admin'):
|
||||
if not row:
|
||||
return jsonify({'logged_in': False})
|
||||
except Exception:
|
||||
return jsonify({'logged_in': False})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import os
|
||||
from flask import Blueprint, redirect, url_for, send_file, session
|
||||
|
||||
from utils.auth import login_required, admin_required, is_session_user_valid, is_current_user_admin
|
||||
from utils.auth import login_required, is_session_user_valid
|
||||
from utils.render import render_html
|
||||
|
||||
main = Blueprint('main', __name__, url_prefix='')
|
||||
@@ -15,14 +15,13 @@ STATIC_DIR = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
@main.route('/')
|
||||
def index():
|
||||
if session.get('user_id') and is_session_user_valid() and is_current_user_admin():
|
||||
if session.get('user_id') and is_session_user_valid():
|
||||
return redirect(url_for('main.admin_page'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@main.route('/admin')
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_page():
|
||||
return render_html('admin.html')
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ bucket_path = "nanri-image/"
|
||||
file_url_pre = f"https://{bucket}.oss-cn-hangzhou.aliyuncs.com/"
|
||||
|
||||
import os
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||
backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://127.0.0.1:18080').rstrip('/')
|
||||
# backend_java_base_url = os.environ.get('BACKEND_JAVA_BASE_URL', 'http://8.136.19.173:18080').rstrip('/')
|
||||
os.environ['OSS_ACCESS_KEY_ID'] = accessKeyId
|
||||
os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret
|
||||
os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6"
|
||||
|
||||
98
backend/run.sh
Normal file
98
backend/run.sh
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${PORT:-15124}"
|
||||
HOST="${HOST:-0.0.0.0}"
|
||||
APP_MODULE="${APP_MODULE:-app.py}"
|
||||
PID_FILE="${PID_FILE:-backend.pid}"
|
||||
LOG_FILE="${LOG_FILE:-backend.log}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "检查端口 ${PORT} ..."
|
||||
if command -v ss >/dev/null 2>&1; then
|
||||
if ss -ltn | awk '{print $4}' | grep -Eq "(^|:)${PORT}$"; then
|
||||
echo "错误: 端口 ${PORT} 已被占用,请先释放后再启动。"
|
||||
exit 1
|
||||
fi
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
if netstat -ltn 2>/dev/null | awk '{print $4}' | grep -Eq "(^|:)${PORT}$"; then
|
||||
echo "错误: 端口 ${PORT} 已被占用,请先释放后再启动。"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "警告: 未找到 ss 或 netstat,跳过端口占用检查。"
|
||||
fi
|
||||
echo "端口 ${PORT} 未被占用。"
|
||||
echo "工作目录已切换至: ${SCRIPT_DIR}"
|
||||
|
||||
PYTHON_BIN=""
|
||||
if [[ -x "${SCRIPT_DIR}/venv/bin/python" ]]; then
|
||||
PYTHON_BIN="${SCRIPT_DIR}/venv/bin/python"
|
||||
elif [[ -x "${SCRIPT_DIR}/.venv/bin/python" ]]; then
|
||||
PYTHON_BIN="${SCRIPT_DIR}/.venv/bin/python"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
PYTHON_BIN="$(command -v python3)"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PYTHON_BIN="$(command -v python)"
|
||||
else
|
||||
echo "错误: 未找到可用的 Python,请先安装 Python 3.9+。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "使用 Python: ${PYTHON_BIN}"
|
||||
"${PYTHON_BIN}" - <<'PY'
|
||||
import sys
|
||||
major, minor = sys.version_info[:2]
|
||||
if (major, minor) < (3, 9):
|
||||
print(f"错误: 当前 Python 版本为 {major}.{minor},项目至少需要 Python 3.9。")
|
||||
print("原因: requirement.txt 中的 Flask 3.x、pandas 2.x 等依赖不支持 Python 3.6。")
|
||||
raise SystemExit(1)
|
||||
print(f"Python 版本检查通过: {major}.{minor}")
|
||||
PY
|
||||
|
||||
if [[ ! -f "requirement.txt" ]]; then
|
||||
echo "错误: 未找到 requirement.txt。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "检查关键依赖 ..."
|
||||
if ! "${PYTHON_BIN}" - <<'PY'
|
||||
import importlib.util
|
||||
modules = ("flask", "flask_cors", "pymysql", "werkzeug")
|
||||
missing = [name for name in modules if importlib.util.find_spec(name) is None]
|
||||
if missing:
|
||||
print("缺少依赖: " + ", ".join(missing))
|
||||
raise SystemExit(1)
|
||||
print("关键依赖已安装。")
|
||||
PY
|
||||
then
|
||||
echo "开始安装 requirement.txt ..."
|
||||
"${PYTHON_BIN}" -m pip install --upgrade pip
|
||||
"${PYTHON_BIN}" -m pip install -r requirement.txt
|
||||
fi
|
||||
|
||||
if [[ -f "${PID_FILE}" ]]; then
|
||||
OLD_PID="$(cat "${PID_FILE}" 2>/dev/null || true)"
|
||||
if [[ -n "${OLD_PID}" ]] && kill -0 "${OLD_PID}" 2>/dev/null; then
|
||||
echo "错误: 检测到服务已在运行,PID=${OLD_PID}"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "${PID_FILE}"
|
||||
fi
|
||||
|
||||
echo "启动 Flask 服务 ${HOST}:${PORT} ..."
|
||||
nohup "${PYTHON_BIN}" "${APP_MODULE}" >> "${LOG_FILE}" 2>&1 &
|
||||
NEW_PID=$!
|
||||
echo "${NEW_PID}" > "${PID_FILE}"
|
||||
|
||||
sleep 2
|
||||
if kill -0 "${NEW_PID}" 2>/dev/null; then
|
||||
echo "启动成功,PID=${NEW_PID}"
|
||||
echo "日志文件: ${SCRIPT_DIR}/${LOG_FILE}"
|
||||
else
|
||||
echo "错误: 服务启动失败,请检查日志 ${SCRIPT_DIR}/${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
Binary file not shown.
@@ -42,9 +42,13 @@ def get_current_admin_role():
|
||||
)
|
||||
row = cur.fetchone()
|
||||
conn.close()
|
||||
if not row or not row.get('is_admin'):
|
||||
return None, row
|
||||
role = row.get('role') or ('super_admin' if row.get('created_by_id') is None else 'admin')
|
||||
if not row:
|
||||
return None, None
|
||||
role = (row.get('role') or '').strip().lower()
|
||||
if not role:
|
||||
role = 'super_admin' if row.get('is_admin') and row.get('created_by_id') is None else (
|
||||
'admin' if row.get('is_admin') else 'normal'
|
||||
)
|
||||
return role, row
|
||||
except Exception:
|
||||
return None, None
|
||||
@@ -56,7 +60,14 @@ def is_current_user_admin():
|
||||
|
||||
|
||||
def _is_ajax_request():
|
||||
return request.headers.get('X-Requested-With') == 'XMLHttpRequest'
|
||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||
return True
|
||||
if request.path.startswith('/api/'):
|
||||
return True
|
||||
accept = (request.headers.get('Accept') or '').lower()
|
||||
if 'application/json' in accept:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def login_required(f):
|
||||
|
||||
@@ -769,10 +769,10 @@
|
||||
<option value="ES">西班牙</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="min-width:260px;flex:1;">
|
||||
<label>ASIN</label>
|
||||
<div class="form-group" style="min-width:320px;flex:1;">
|
||||
<label>ASIN / 最低价</label>
|
||||
<div id="skipPriceAsinInputs" style="display:flex;flex-direction:column;gap:8px;">
|
||||
<div style="color:#999;font-size:13px;">请选择国家后输入对应 ASIN</div>
|
||||
<div style="color:#999;font-size:13px;">请选择国家后输入对应 ASIN 和最低价</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" id="btnCreateSkipPriceAsin">新增 ASIN</button>
|
||||
@@ -966,7 +966,7 @@
|
||||
<label>组员</label>
|
||||
<select id="shopManageGroupMemberSelect" multiple size="6" style="min-height:140px;">
|
||||
</select>
|
||||
<div style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
|
||||
<div id="shopManageGroupMemberHelp" style="color:#888;font-size:12px;margin-top:6px;">可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-bottom:12px;">
|
||||
@@ -1034,6 +1034,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-mask" id="editSkipPriceAsinModal">
|
||||
<div class="modal">
|
||||
<h3>编辑跳过跟价 ASIN</h3>
|
||||
<input type="hidden" id="editSkipPriceAsinId">
|
||||
<input type="hidden" id="editSkipPriceAsinCountry">
|
||||
<div class="form-group">
|
||||
<label>店铺名</label>
|
||||
<input type="text" id="editSkipPriceAsinShopName" readonly style="background:#f5f5f5;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>国家</label>
|
||||
<input type="text" id="editSkipPriceAsinCountryLabel" readonly style="background:#f5f5f5;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>ASIN</label>
|
||||
<input type="text" id="editSkipPriceAsinValue" placeholder="请输入 ASIN">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>最低价</label>
|
||||
<input type="number" id="editSkipPriceMinimumPrice" min="0" step="0.01" placeholder="请输入最低价,可留空">
|
||||
</div>
|
||||
<p class="msg" id="msgEditSkipPriceAsin"></p>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;">
|
||||
<button class="btn" id="btnSaveSkipPriceAsin">保存</button>
|
||||
<button class="btn btn-secondary" id="btnCloseEditSkipPriceAsin">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-mask" id="editShopManageModal">
|
||||
<div class="modal">
|
||||
<h3>编辑店铺</h3>
|
||||
@@ -1655,22 +1684,6 @@
|
||||
document.getElementById('historyListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
function loadUserOptions() {
|
||||
fetch('/api/admin/users?page=1&page_size=999')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
var sel = document.getElementById('filterUser');
|
||||
var cur = sel.value;
|
||||
sel.innerHTML = '<option value="">全部用户</option>';
|
||||
(res.items || []).forEach(function (u) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = u.id;
|
||||
opt.textContent = u.username + ' (' + roleLabel(u.role || 'normal') + ')';
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur || '';
|
||||
});
|
||||
}
|
||||
var shopManageAllUsers = [];
|
||||
function getEligibleShopManageGroupUsers(leaderUserId) {
|
||||
var canViewAllMembers = currentUserRole === 'super_admin';
|
||||
@@ -1683,15 +1696,29 @@
|
||||
}
|
||||
function refreshShopManageGroupMemberSelect(leaderUserId, selectedUserIds) {
|
||||
var sel = document.getElementById('shopManageGroupMemberSelect');
|
||||
var helpEl = document.getElementById('shopManageGroupMemberHelp');
|
||||
if (!sel) return;
|
||||
var selectedMap = {};
|
||||
(selectedUserIds || []).forEach(function (id) {
|
||||
selectedMap[String(id)] = true;
|
||||
});
|
||||
var options = getEligibleShopManageGroupUsers(leaderUserId).map(function (u) {
|
||||
var eligibleUsers = getEligibleShopManageGroupUsers(leaderUserId);
|
||||
var options = eligibleUsers.map(function (u) {
|
||||
return '<option value="' + u.id + '"' + (selectedMap[String(u.id)] ? ' selected' : '') + '>' + (u.username || '') + '</option>';
|
||||
});
|
||||
sel.innerHTML = options.join('');
|
||||
if (options.length) {
|
||||
sel.innerHTML = options.join('');
|
||||
if (helpEl) {
|
||||
helpEl.textContent = '可添加当前组长创建的普通员工账号,按住 Ctrl 或 Command 可多选。';
|
||||
}
|
||||
return;
|
||||
}
|
||||
sel.innerHTML = '<option value="" disabled>当前组长暂无可添加组员</option>';
|
||||
if (helpEl) {
|
||||
helpEl.textContent = currentUserRole === 'normal'
|
||||
? '普通账号没有下属普通员工时,这里会为空;当前账号只能作为组长使用。'
|
||||
: '当前组长名下暂无可添加的普通员工账号。';
|
||||
}
|
||||
}
|
||||
function setShopManageGroupLeader(leaderUserId, leaderUsername, selectedUserIds) {
|
||||
var leaderIdEl = document.getElementById('shopManageGroupLeaderUserId');
|
||||
@@ -1712,6 +1739,9 @@
|
||||
return fetch('/api/admin/users?page=1&page_size=999')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) {
|
||||
throw new Error(res.error || '加载用户失败');
|
||||
}
|
||||
var items = res.items || [];
|
||||
shopManageAllUsers = items.map(function (u) {
|
||||
return {
|
||||
@@ -1736,6 +1766,17 @@
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
sel.value = cur || '';
|
||||
})
|
||||
.catch(function () {
|
||||
shopManageAllUsers = [];
|
||||
var sel = document.getElementById('filterUser');
|
||||
if (sel) {
|
||||
sel.innerHTML = '<option value="">全部用户</option>';
|
||||
}
|
||||
refreshShopManageGroupMemberSelect(
|
||||
document.getElementById('shopManageGroupLeaderUserId') ? document.getElementById('shopManageGroupLeaderUserId').value : '',
|
||||
[]
|
||||
);
|
||||
});
|
||||
}
|
||||
document.getElementById('btnFilterHistory').onclick = function () { loadHistory(1); };
|
||||
@@ -2184,6 +2225,7 @@
|
||||
// ========== 店铺管理 ==========
|
||||
var shopManagePage = 1, shopManagePageSize = 15;
|
||||
var shopManageGroups = [];
|
||||
var currentShopManageGroupGrantRoutes = [];
|
||||
|
||||
function buildShopManageQuery(page) {
|
||||
var query = 'page=' + (page || 1) + '&page_size=' + shopManagePageSize;
|
||||
@@ -2313,6 +2355,18 @@
|
||||
refreshShopManageGroupMemberSelect(currentUserId, []);
|
||||
}
|
||||
|
||||
function setShopManageGroupGrantRoutes(routes) {
|
||||
currentShopManageGroupGrantRoutes = Array.isArray(routes) ? routes.slice() : [];
|
||||
}
|
||||
|
||||
function updateShopManageGroupButtonsAccess() {
|
||||
var canManageGroups = !!currentUserId;
|
||||
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups'].forEach(function (id) {
|
||||
var btn = document.getElementById(id);
|
||||
if (btn) btn.style.display = canManageGroups ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function openShopManageGroupModal() {
|
||||
resetShopManageGroupForm();
|
||||
loadUserOptions()
|
||||
@@ -2331,10 +2385,13 @@
|
||||
}
|
||||
tbody.innerHTML = shopManageGroups.map(function (item, index) {
|
||||
var memberNames = Array.isArray(item.member_usernames) ? item.member_usernames.join('、') : '';
|
||||
var canEdit = currentUserRole === 'super_admin' || String(item.leader_user_id || '') === String(currentUserId || '');
|
||||
var actionHtml = canEdit
|
||||
? ('<button class="btn btn-sm" data-shop-group-edit="' + item.id + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '"') + '">删除</button>')
|
||||
: '<span style="color:#999;">-</span>';
|
||||
return '<tr><td>' + (index + 1) + '</td><td>' + (item.group_name || '') + '</td><td>' + (item.leader_username || '') + '</td><td>' + (item.member_count || 0) + '</td><td>' + (memberNames || '-') + '</td><td>' + (item.created_at || '') + '</td><td>' + (item.updated_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-shop-group-edit="' + item.id + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-shop-group-delete="' + item.id + '" data-shop-group-name="' + (item.group_name || '').replace(/"/g, '"') + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
actionHtml + '</td></tr>';
|
||||
}).join('');
|
||||
|
||||
document.querySelectorAll('[data-shop-group-edit]').forEach(function (btn) {
|
||||
@@ -2378,9 +2435,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btnManageShopGroups').onclick = openShopManageGroupModal;
|
||||
document.getElementById('btnManageShopGroupsFromEdit').onclick = openShopManageGroupModal;
|
||||
document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal;
|
||||
document.getElementById('btnManageShopGroups').onclick = function () {
|
||||
setShopManageGroupGrantRoutes(['shop-manage']);
|
||||
openShopManageGroupModal();
|
||||
};
|
||||
document.getElementById('btnManageShopGroupsFromEdit').onclick = function () {
|
||||
setShopManageGroupGrantRoutes(['shop-manage']);
|
||||
openShopManageGroupModal();
|
||||
};
|
||||
document.getElementById('btnManageSkipPriceAsinGroups').onclick = function () {
|
||||
setShopManageGroupGrantRoutes(['skip-price-asin']);
|
||||
openShopManageGroupModal();
|
||||
};
|
||||
document.getElementById('btnSearchShopManage').onclick = function () {
|
||||
loadShopManage(1);
|
||||
};
|
||||
@@ -2411,7 +2477,11 @@
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group_name: groupName, member_user_ids: memberUserIds })
|
||||
body: JSON.stringify({
|
||||
group_name: groupName,
|
||||
member_user_ids: memberUserIds,
|
||||
grant_menu_routes: currentShopManageGroupGrantRoutes
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
@@ -2520,11 +2590,11 @@
|
||||
var skipPriceAsinPage = 1, skipPriceAsinPageSize = 15;
|
||||
var chooseSkipPriceAsinShopPage = 1, chooseSkipPriceAsinShopPageSize = 10;
|
||||
var skipPriceCountryColumns = [
|
||||
{ code: 'DE', field: 'asin_de', label: '德国' },
|
||||
{ code: 'UK', field: 'asin_uk', label: '英国' },
|
||||
{ code: 'FR', field: 'asin_fr', label: '法国' },
|
||||
{ code: 'IT', field: 'asin_it', label: '意大利' },
|
||||
{ code: 'ES', field: 'asin_es', label: '西班牙' }
|
||||
{ code: 'DE', field: 'asin_de', minimumPriceField: 'minimum_price_de', label: '德国' },
|
||||
{ code: 'UK', field: 'asin_uk', minimumPriceField: 'minimum_price_uk', label: '英国' },
|
||||
{ code: 'FR', field: 'asin_fr', minimumPriceField: 'minimum_price_fr', label: '法国' },
|
||||
{ code: 'IT', field: 'asin_it', minimumPriceField: 'minimum_price_it', label: '意大利' },
|
||||
{ code: 'ES', field: 'asin_es', minimumPriceField: 'minimum_price_es', label: '西班牙' }
|
||||
];
|
||||
function buildChooseSkipPriceAsinShopQuery(page) {
|
||||
var query = 'page=' + (page || 1) + '&page_size=' + chooseSkipPriceAsinShopPageSize;
|
||||
@@ -2607,41 +2677,68 @@
|
||||
return option.value;
|
||||
});
|
||||
}
|
||||
function formatSkipPriceMinimumPrice(value) {
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
var num = Number(value);
|
||||
if (!isFinite(num)) return String(value);
|
||||
return num.toFixed(2);
|
||||
}
|
||||
function getSkipPriceCountryLabel(countryCode) {
|
||||
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
|
||||
return country ? country.label : countryCode;
|
||||
}
|
||||
function renderSkipPriceAsinInputs() {
|
||||
var container = document.getElementById('skipPriceAsinInputs');
|
||||
var selectedCountries = getSelectedSkipPriceCountries();
|
||||
var existingValues = {};
|
||||
var existingMinimumPrices = {};
|
||||
container.querySelectorAll('[data-skip-price-country-input]').forEach(function (input) {
|
||||
existingValues[input.getAttribute('data-skip-price-country-input')] = input.value;
|
||||
});
|
||||
container.querySelectorAll('[data-skip-price-country-minimum-price-input]').forEach(function (input) {
|
||||
existingMinimumPrices[input.getAttribute('data-skip-price-country-minimum-price-input')] = input.value;
|
||||
});
|
||||
if (!selectedCountries.length) {
|
||||
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN</div>';
|
||||
container.innerHTML = '<div style="color:#999;font-size:13px;">请选择国家后输入 ASIN 和最低价</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = selectedCountries.map(function (countryCode) {
|
||||
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
|
||||
var label = country ? country.label : countryCode;
|
||||
var label = getSkipPriceCountryLabel(countryCode);
|
||||
var value = existingValues[countryCode] || '';
|
||||
var minimumPrice = existingMinimumPrices[countryCode] || '';
|
||||
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
|
||||
'<span style="min-width:56px;color:#555;">' + label + '</span>' +
|
||||
'<input type="text" data-skip-price-country-input="' + countryCode + '" value="' + value.replace(/"/g, '"') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
|
||||
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '"') + '" min="0" step="0.01" placeholder="最低价" style="width:140px;">' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
function collectSkipPriceAsinMappings(countries) {
|
||||
var mappings = {};
|
||||
var asinMappings = {};
|
||||
var minimumPriceMappings = {};
|
||||
for (var i = 0; i < countries.length; i++) {
|
||||
var countryCode = countries[i];
|
||||
var input = document.querySelector('[data-skip-price-country-input="' + countryCode + '"]');
|
||||
var minimumPriceInput = document.querySelector('[data-skip-price-country-minimum-price-input="' + countryCode + '"]');
|
||||
var asin = input ? (input.value || '').trim().toUpperCase() : '';
|
||||
if (!asin) {
|
||||
var country = skipPriceCountryColumns.find(function (item) { return item.code === countryCode; });
|
||||
var label = country ? country.label : countryCode;
|
||||
var label = getSkipPriceCountryLabel(countryCode);
|
||||
throw new Error(label + ' ASIN 不能为空');
|
||||
}
|
||||
mappings[countryCode] = asin;
|
||||
var minimumPrice = minimumPriceInput ? (minimumPriceInput.value || '').trim() : '';
|
||||
if (minimumPrice) {
|
||||
var minimumPriceNumber = Number(minimumPrice);
|
||||
if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) {
|
||||
throw new Error(getSkipPriceCountryLabel(countryCode) + ' 最低价格式不正确');
|
||||
}
|
||||
minimumPriceMappings[countryCode] = minimumPrice;
|
||||
}
|
||||
asinMappings[countryCode] = asin;
|
||||
}
|
||||
return mappings;
|
||||
return {
|
||||
asinMappings: asinMappings,
|
||||
minimumPriceMappings: minimumPriceMappings
|
||||
};
|
||||
}
|
||||
function buildSkipPriceAsinQuery(page) {
|
||||
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
|
||||
@@ -2654,37 +2751,49 @@
|
||||
return query;
|
||||
}
|
||||
function renderSkipPriceAsinCell(item, country) {
|
||||
var value = item[country.field] || '';
|
||||
if (!value) return '<span style="color:#999;">-</span>';
|
||||
var asinValue = item[country.field] || '';
|
||||
var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]);
|
||||
var hasValue = !!asinValue || !!minimumPriceValue;
|
||||
var infoHtml = hasValue
|
||||
? ('<div style="display:flex;flex-direction:column;gap:4px;min-width:0;">' +
|
||||
'<span>' + (asinValue || '<span style="color:#999;">-</span>') + '</span>' +
|
||||
'<span style="color:#666;font-size:12px;">最低价:' + (minimumPriceValue || '-') + '</span>' +
|
||||
'</div>')
|
||||
: '<span style="color:#999;">-</span>';
|
||||
var deleteHtml = hasValue
|
||||
? ('<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">删除</button>')
|
||||
: '';
|
||||
return '<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">' +
|
||||
'<span>' + value + '</span>' +
|
||||
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + value.replace(/"/g, '"') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">编辑</button>' +
|
||||
'<button class="btn btn-sm btn-danger" data-skip-price-asin-delete="' + item.id + '" data-country="' + country.code + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">删除</button>' +
|
||||
infoHtml +
|
||||
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '"') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '"') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '"') + '">编辑</button>' +
|
||||
deleteHtml +
|
||||
'</div>';
|
||||
}
|
||||
function openEditSkipPriceAsinModal(itemId, countryCode, shopName, asinValue, minimumPriceValue) {
|
||||
document.getElementById('editSkipPriceAsinId').value = itemId || '';
|
||||
document.getElementById('editSkipPriceAsinCountry').value = countryCode || '';
|
||||
document.getElementById('editSkipPriceAsinShopName').value = shopName || '';
|
||||
document.getElementById('editSkipPriceAsinCountryLabel').value = getSkipPriceCountryLabel(countryCode || '');
|
||||
document.getElementById('editSkipPriceAsinValue').value = asinValue || '';
|
||||
document.getElementById('editSkipPriceMinimumPrice').value = minimumPriceValue || '';
|
||||
document.getElementById('msgEditSkipPriceAsin').textContent = '';
|
||||
document.getElementById('msgEditSkipPriceAsin').className = 'msg';
|
||||
document.getElementById('editSkipPriceAsinModal').classList.add('show');
|
||||
}
|
||||
function bindSkipPriceAsinActions() {
|
||||
document.querySelectorAll('[data-skip-price-asin-edit]').forEach(function (btn) {
|
||||
btn.onclick = function () {
|
||||
var shopName = (btn.dataset.shopName || '').replace(/"/g, '"');
|
||||
var countryCode = btn.dataset.country || '';
|
||||
var currentAsin = (btn.dataset.asin || '').replace(/"/g, '"');
|
||||
var nextAsin = prompt('请输入店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN', currentAsin);
|
||||
if (nextAsin === null) return;
|
||||
nextAsin = (nextAsin || '').trim();
|
||||
if (!nextAsin) {
|
||||
alert('ASIN 不能为空');
|
||||
return;
|
||||
}
|
||||
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinEdit + '/country/' + countryCode, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ asin: nextAsin })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (res.success) loadSkipPriceAsin(skipPriceAsinPage);
|
||||
else alert(res.error || '保存失败');
|
||||
});
|
||||
var currentMinimumPrice = (btn.dataset.minimumPrice || '').replace(/"/g, '"');
|
||||
openEditSkipPriceAsinModal(
|
||||
btn.dataset.skipPriceAsinEdit,
|
||||
countryCode,
|
||||
shopName,
|
||||
currentAsin,
|
||||
currentMinimumPrice
|
||||
);
|
||||
};
|
||||
});
|
||||
document.querySelectorAll('[data-skip-price-asin-delete]').forEach(function (btn) {
|
||||
@@ -2733,7 +2842,6 @@
|
||||
document.getElementById('skipPriceAsinListBody').innerHTML = '<tr><td colspan="8" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
document.getElementById('btnManageSkipPriceAsinGroups').onclick = openShopManageGroupModal;
|
||||
document.getElementById('btnChooseSkipPriceAsinShop').onclick = openChooseSkipPriceAsinShopModal;
|
||||
document.getElementById('btnSearchChooseSkipPriceAsinShop').onclick = function () {
|
||||
loadChooseSkipPriceAsinShops(1);
|
||||
@@ -2750,6 +2858,58 @@
|
||||
document.getElementById('btnCloseChooseSkipPriceAsinShopModal').onclick = function () {
|
||||
document.getElementById('chooseSkipPriceAsinShopModal').classList.remove('show');
|
||||
};
|
||||
document.getElementById('btnCloseEditSkipPriceAsin').onclick = function () {
|
||||
document.getElementById('editSkipPriceAsinModal').classList.remove('show');
|
||||
};
|
||||
document.getElementById('btnSaveSkipPriceAsin').onclick = function () {
|
||||
var itemId = (document.getElementById('editSkipPriceAsinId').value || '').trim();
|
||||
var countryCode = (document.getElementById('editSkipPriceAsinCountry').value || '').trim();
|
||||
var asin = (document.getElementById('editSkipPriceAsinValue').value || '').trim().toUpperCase();
|
||||
var minimumPrice = (document.getElementById('editSkipPriceMinimumPrice').value || '').trim();
|
||||
var msgEl = document.getElementById('msgEditSkipPriceAsin');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
if (!itemId || !countryCode) {
|
||||
msgEl.textContent = '缺少编辑记录';
|
||||
msgEl.className = 'msg err';
|
||||
return;
|
||||
}
|
||||
if (!asin) {
|
||||
msgEl.textContent = 'ASIN 不能为空';
|
||||
msgEl.className = 'msg err';
|
||||
return;
|
||||
}
|
||||
if (minimumPrice) {
|
||||
var minimumPriceNumber = Number(minimumPrice);
|
||||
if (!isFinite(minimumPriceNumber) || minimumPriceNumber < 0) {
|
||||
msgEl.textContent = '最低价格式不正确';
|
||||
msgEl.className = 'msg err';
|
||||
return;
|
||||
}
|
||||
}
|
||||
fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
asin: asin,
|
||||
minimum_price: minimumPrice || null
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) {
|
||||
msgEl.textContent = res.error || '保存失败';
|
||||
msgEl.className = 'msg err';
|
||||
return;
|
||||
}
|
||||
document.getElementById('editSkipPriceAsinModal').classList.remove('show');
|
||||
loadSkipPriceAsin(skipPriceAsinPage);
|
||||
})
|
||||
.catch(function () {
|
||||
msgEl.textContent = '请求失败';
|
||||
msgEl.className = 'msg err';
|
||||
});
|
||||
};
|
||||
document.getElementById('skipPriceAsinCountries').addEventListener('change', renderSkipPriceAsinInputs);
|
||||
document.getElementById('btnSearchSkipPriceAsin').onclick = function () {
|
||||
loadSkipPriceAsin(1);
|
||||
@@ -2779,8 +2939,11 @@
|
||||
return;
|
||||
}
|
||||
var asinMappings = {};
|
||||
var minimumPriceMappings = {};
|
||||
try {
|
||||
asinMappings = collectSkipPriceAsinMappings(countries);
|
||||
var mappings = collectSkipPriceAsinMappings(countries);
|
||||
asinMappings = mappings.asinMappings || {};
|
||||
minimumPriceMappings = mappings.minimumPriceMappings || {};
|
||||
} catch (err) {
|
||||
msgEl.textContent = err.message || '请输入 ASIN';
|
||||
msgEl.className = 'msg err';
|
||||
@@ -2799,7 +2962,8 @@
|
||||
shop_name: shopName,
|
||||
countries: countries,
|
||||
asin: fallbackAsin,
|
||||
asin_mappings: asinMappings
|
||||
asin_mappings: asinMappings,
|
||||
minimum_price_mappings: minimumPriceMappings
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
@@ -3245,13 +3409,19 @@
|
||||
return;
|
||||
}
|
||||
var item = res.item || {};
|
||||
currentUserId = item.id || currentUserId;
|
||||
currentUserRole = item.role || currentUserRole;
|
||||
currentUserUsername = item.username || currentUserUsername;
|
||||
nameEl.textContent = item.username || '管理员';
|
||||
if (item.role) {
|
||||
roleEl.textContent = item.role === 'super_admin' ? '超级管理员' : '管理员';
|
||||
roleEl.textContent = item.role === 'super_admin'
|
||||
? '超级管理员'
|
||||
: (item.role === 'admin' ? '管理员' : '普通账号');
|
||||
roleEl.style.display = 'inline-block';
|
||||
} else {
|
||||
roleEl.style.display = 'none';
|
||||
}
|
||||
updateShopManageGroupButtonsAccess();
|
||||
})
|
||||
.catch(function () {
|
||||
fetch('/api/auth/check')
|
||||
@@ -3259,10 +3429,12 @@
|
||||
.then(function (res) {
|
||||
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
|
||||
document.getElementById('adminCurrentUserRole').style.display = 'none';
|
||||
updateShopManageGroupButtonsAccess();
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('adminCurrentUsername').textContent = '当前用户';
|
||||
document.getElementById('adminCurrentUserRole').style.display = 'none';
|
||||
updateShopManageGroupButtonsAccess();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user