完成后端架构重构等

This commit is contained in:
super
2026-04-23 15:25:41 +08:00
parent 46f46039ea
commit 34bc980eea
76 changed files with 3242 additions and 1111 deletions

View File

@@ -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],
},

View File

@@ -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})

View File

@@ -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')

View File

@@ -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
View 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

View File

@@ -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):

View File

@@ -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, '&quot;') + '">删除</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, '&quot;') + '">删除</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, '&quot;') + '" placeholder="请输入' + label + ' ASIN" style="flex:1;min-width:180px;">' +
'<input type="number" data-skip-price-country-minimum-price-input="' + countryCode + '" value="' + minimumPrice.replace(/"/g, '&quot;') + '" 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, '&quot;') + '">删除</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, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</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, '&quot;') + '">删除</button>' +
infoHtml +
'<button class="btn btn-sm" data-skip-price-asin-edit="' + item.id + '" data-country="' + country.code + '" data-asin="' + asinValue.replace(/"/g, '&quot;') + '" data-minimum-price="' + minimumPriceValue.replace(/"/g, '&quot;') + '" data-shop-name="' + (item.shop_name || '').replace(/"/g, '&quot;') + '">编辑</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(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
var currentAsin = (btn.dataset.asin || '').replace(/&quot;/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(/&quot;/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();
});
});
}