后台管理接口修复 后端BUG修复

This commit is contained in:
super
2026-05-12 21:03:42 +08:00
parent 5c990e651e
commit 0d78d63437
30 changed files with 3444 additions and 394 deletions

View File

@@ -8,7 +8,7 @@ import threading
import requests
from requests.adapters import HTTPAdapter
from flask import Blueprint, request, jsonify, session, current_app, g
from flask import Blueprint, request, jsonify, session, current_app, g, Response
import pymysql
from werkzeug.security import generate_password_hash
@@ -149,14 +149,24 @@ def _proxy_backend_java(method, path, *, params=None, json_data=None, files=None
def _format_permission_item(item):
created_at = item.get('createdAt')
if created_at in (None, ''):
created_at = item.get('created_at')
if hasattr(created_at, 'strftime'):
created_at_text = created_at.strftime('%Y-%m-%d %H:%M')
else:
created_at_text = (str(created_at).replace('T', ' ')[:16]) if created_at else ''
sort_order = item.get('sortOrder')
if sort_order is None:
sort_order = item.get('sort_order')
return {
'id': item.get('id'),
'name': item.get('name') or '',
'column_key': item.get('columnKey') or '',
'menu_type': item.get('menuType') or 'app',
'route_path': item.get('routePath') or '',
'sort_order': item.get('sortOrder') if item.get('sortOrder') is not None else 0,
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
'column_key': item.get('columnKey') or item.get('column_key') or '',
'menu_type': item.get('menuType') or item.get('menu_type') or 'app',
'route_path': item.get('routePath') or item.get('route_path') or '',
'sort_order': sort_order if sort_order is not None else 0,
'created_at': created_at_text,
}
@@ -792,11 +802,9 @@ def create_user():
(username, pwd_hash, is_admin, want_role, want_created_by),
)
new_uid = cur.lastrowid
_set_user_column_permissions(cur, new_uid, column_ids)
conn.commit()
conn.close()
error_response, status = _sync_user_column_permissions(new_uid, column_ids)
if error_response is not None:
return error_response, status
return jsonify({'success': True, 'msg': '用户创建成功'})
except pymysql.IntegrityError:
return jsonify({'success': False, 'error': '用户名已存在'})
@@ -807,7 +815,7 @@ def create_user():
@admin_api.route('/user/<int:uid>', methods=['PUT'])
@admin_required
def update_user(uid):
"""更新用户(密码、角色)"""
"""更新用户(密码、角色、栏目权限"""
data = request.get_json() or {}
_, _, denied = _ensure_admin_menu_access('users')
if denied:
@@ -817,7 +825,7 @@ def update_user(uid):
role, current_row = get_current_admin_role()
if not role:
return jsonify({'success': False, 'error': '需要管理员权限'}), 403
if want_role is None and not password:
if want_role is None and not password and 'column_ids' not in data:
return jsonify({'success': False, 'error': '请提供要修改的内容'})
try:
conn = get_db()
@@ -852,13 +860,10 @@ def update_user(uid):
"UPDATE users SET is_admin = %s, role = %s WHERE id = %s",
(is_admin, want_role, uid),
)
column_ids = data.get('column_ids')
if 'column_ids' in data:
_set_user_column_permissions(cur, uid, data.get('column_ids'))
conn.commit()
conn.close()
if column_ids is not None:
error_response, status = _sync_user_column_permissions(uid, column_ids)
if error_response is not None:
return error_response, status
return jsonify({'success': True, 'msg': '更新成功'})
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
@@ -1139,15 +1144,7 @@ def get_user_columns(uid):
if denied:
return denied
menu_type = (request.args.get('menu_type') or '').strip()
result, error_response, status = _proxy_backend_java(
'GET',
f'/api/admin/permission-users/{uid}/columns',
params={'menuType': menu_type} if menu_type else None,
)
if error_response is not None:
return error_response, status
payload = result.get('data') or {}
return jsonify({'success': True, 'column_ids': payload.get('columnIds') or []})
return jsonify({'success': True, 'column_ids': _get_local_user_column_ids(uid, menu_type or None)})
@admin_api.route('/user/<int:uid>/column-permissions')
@@ -1157,14 +1154,8 @@ def get_user_column_permissions(uid):
if denied:
return denied
menu_type = (request.args.get('menu_type') or '').strip()
result, error_response, status = _proxy_backend_java(
'GET',
f'/api/admin/permission-users/{uid}/column-permissions',
params={'menuType': menu_type} if menu_type else None,
)
if error_response is not None:
return error_response, status
return jsonify({'success': True, 'items': [_format_permission_item(item) for item in (result.get('data') or [])]})
items = _get_local_user_column_permission_items(uid, menu_type or None)
return jsonify({'success': True, 'items': [_format_permission_item(item) for item in items]})
def _set_user_column_permissions(cur, user_id, column_ids):
@@ -2006,18 +1997,18 @@ def list_skip_price_asins():
group_id_raw = (request.args.get('group_id') or '').strip()
shop_name = (request.args.get('shop_name') or '').strip()
asin = (request.args.get('asin') or '').strip()
params = {'page': page, 'pageSize': page_size}
if current_row and current_row.get('id'):
params['operatorId'] = current_row.get('id')
params['superAdmin'] = 'true' if role == 'super_admin' else 'false'
params = {'page': page, 'page_size': page_size, 'pageSize': page_size}
params.update(_skip_price_asin_auth_params(role, current_row))
if group_id_raw:
try:
group_id = int(group_id_raw)
if group_id > 0:
params['group_id'] = group_id
params['groupId'] = group_id
except (TypeError, ValueError):
pass
if shop_name:
params['shop_name'] = shop_name
params['shopName'] = shop_name
if asin:
params['asin'] = asin
@@ -2059,6 +2050,64 @@ def list_skip_price_asins():
})
def _skip_price_asin_auth_params(role, current_row):
operator_id = current_row.get('id') if current_row else None
super_admin = 'true' if role == 'super_admin' else 'false'
return {
'operator_id': operator_id,
'operatorId': operator_id,
'super_admin': super_admin,
'superAdmin': super_admin,
}
@admin_api.route('/skip-price-asins/export')
@login_required
def export_skip_price_asins():
role, current_row, denied = _ensure_backend_menu_access('skip-price-asin')
if denied:
return denied
params = _skip_price_asin_auth_params(role, current_row)
group_id_raw = (request.args.get('group_id') or request.args.get('groupId') or '').strip()
shop_name = (request.args.get('shop_name') or request.args.get('shopName') or '').strip()
asin = (request.args.get('asin') or '').strip()
if group_id_raw:
params['group_id'] = group_id_raw
params['groupId'] = group_id_raw
if shop_name:
params['shop_name'] = shop_name
params['shopName'] = shop_name
if asin:
params['asin'] = asin
url = f"{backend_java_base_url}/api/admin/skip-price-asins/export"
try:
resp = _get_backend_java_session().get(url, params=params, timeout=60)
except requests.RequestException:
return jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
if resp.status_code >= 400:
try:
data = resp.json()
error = data.get('message') or data.get('error') or '导出失败'
except ValueError:
error = '导出失败'
return jsonify({'success': False, 'error': error}), resp.status_code
headers = {}
disposition = resp.headers.get('Content-Disposition')
if disposition:
headers['Content-Disposition'] = disposition
return Response(
resp.content,
status=resp.status_code,
headers=headers,
content_type=resp.headers.get(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
),
)
@admin_api.route('/skip-price-asin', methods=['POST'])
@login_required
def create_skip_price_asin():
@@ -2092,10 +2141,7 @@ def create_skip_price_asin():
result, error_response, status = _proxy_backend_java(
'POST',
'/api/admin/skip-price-asins',
params={
'operatorId': current_row.get('id') if current_row else None,
'superAdmin': 'true' if role == 'super_admin' else 'false',
},
params=_skip_price_asin_auth_params(role, current_row),
json_data=payload,
)
if error_response is not None:
@@ -2134,10 +2180,7 @@ def delete_skip_price_asin_country(item_id, country):
result, error_response, status = _proxy_backend_java(
'DELETE',
f'/api/admin/skip-price-asins/{item_id}/countries/{country}',
params={
'operatorId': current_row.get('id') if current_row else None,
'superAdmin': 'true' if role == 'super_admin' else 'false',
},
params=_skip_price_asin_auth_params(role, current_row),
)
if error_response is not None:
return error_response, status
@@ -2177,10 +2220,7 @@ def update_skip_price_asin_country(item_id, country):
result, error_response, status = _proxy_backend_java(
'PUT',
f'/api/admin/skip-price-asins/{item_id}/countries/{country}',
params={
'operatorId': current_row.get('id') if current_row else None,
'superAdmin': 'true' if role == 'super_admin' else 'false',
},
params=_skip_price_asin_auth_params(role, current_row),
json_data=payload,
)
if error_response is not None:
@@ -2240,11 +2280,9 @@ def import_skip_price_asins():
group_id_raw = (request.form.get('group_id') or '').strip()
if not group_id_raw:
return jsonify({'success': False, 'error': '请先选择分组'})
params = {
'groupId': group_id_raw,
'operatorId': current_row.get('id') if current_row else None,
'superAdmin': 'true' if role == 'super_admin' else 'false',
}
params = _skip_price_asin_auth_params(role, current_row)
params['group_id'] = group_id_raw
params['groupId'] = group_id_raw
files = {
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
}
@@ -2295,11 +2333,9 @@ def delete_import_skip_price_asins():
group_id_raw = (request.form.get('group_id') or '').strip()
if not group_id_raw:
return jsonify({'success': False, 'error': '请先选择分组'})
params = {
'groupId': group_id_raw,
'operatorId': current_row.get('id') if current_row else None,
'superAdmin': 'true' if role == 'super_admin' else 'false',
}
params = _skip_price_asin_auth_params(role, current_row)
params['group_id'] = group_id_raw
params['groupId'] = group_id_raw
files = {
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
}
@@ -2584,3 +2620,46 @@ def update_query_asin_country(item_id, country):
'msg': result.get('message') or '保存成功',
'item': _format_query_asin_item(result.get('data') or {}),
})
def _get_local_user_column_ids(user_id, menu_type=None):
conn = get_db()
try:
with conn.cursor() as cur:
sql = """
SELECT c.id
FROM user_column_permission ucp
INNER JOIN columns c ON c.id = ucp.column_id
WHERE ucp.user_id = %s
"""
params = [user_id]
if menu_type:
sql += " AND c.menu_type = %s"
params.append(menu_type)
sql += " ORDER BY c.sort_order ASC, c.id ASC"
cur.execute(sql, params)
rows = cur.fetchall()
return [int(row['id']) for row in rows if row.get('id') is not None]
finally:
conn.close()
def _get_local_user_column_permission_items(user_id, menu_type=None):
conn = get_db()
try:
with conn.cursor() as cur:
sql = """
SELECT c.id, c.name, c.column_key, c.menu_type, c.route_path, c.sort_order, c.created_at
FROM user_column_permission ucp
INNER JOIN columns c ON c.id = ucp.column_id
WHERE ucp.user_id = %s
"""
params = [user_id]
if menu_type:
sql += " AND c.menu_type = %s"
params.append(menu_type)
sql += " ORDER BY c.sort_order ASC, c.id ASC"
cur.execute(sql, params)
return cur.fetchall()
finally:
conn.close()

View File

@@ -1856,16 +1856,82 @@
minimumPriceMappings: minimumPriceMappings
};
}
function buildSkipPriceAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
function buildSkipPriceAsinFilterQuery() {
var query = '';
var groupId = (document.getElementById('skipPriceAsinFilterGroupId').value || '').trim();
var shopName = (document.getElementById('skipPriceAsinFilterShopName').value || '').trim();
var asin = (document.getElementById('skipPriceAsinFilterAsin').value || '').trim();
if (groupId) query += '&group_id=' + encodeURIComponent(groupId);
if (shopName) query += '&shop_name=' + encodeURIComponent(shopName);
if (asin) query += '&asin=' + encodeURIComponent(asin);
if (groupId) query += (query ? '&' : '') + 'group_id=' + encodeURIComponent(groupId);
if (shopName) query += (query ? '&' : '') + 'shop_name=' + encodeURIComponent(shopName);
if (asin) query += (query ? '&' : '') + 'asin=' + encodeURIComponent(asin);
return query;
}
function buildSkipPriceAsinOperatorQuery() {
var query = '';
if (currentUserId) query += 'operator_id=' + encodeURIComponent(currentUserId);
if (currentUserRole === 'super_admin') query += (query ? '&' : '') + 'super_admin=true';
return query;
}
function buildSkipPriceAsinQuery(page) {
var query = 'page=' + (page || 1) + '&page_size=' + skipPriceAsinPageSize;
var filterQuery = buildSkipPriceAsinFilterQuery();
if (filterQuery) query += '&' + filterQuery;
var operatorQuery = buildSkipPriceAsinOperatorQuery();
if (operatorQuery) query += '&' + operatorQuery;
return query;
}
function extractDownloadFilename(disposition, fallbackName) {
var fallback = fallbackName || 'download.xlsx';
if (!disposition) return fallback;
var utf8Match = disposition.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match && utf8Match[1]) {
try {
return decodeURIComponent(utf8Match[1]);
} catch (e) { }
}
var plainMatch = disposition.match(/filename=\"?([^\";]+)\"?/i);
if (plainMatch && plainMatch[1]) return plainMatch[1];
return fallback;
}
function triggerBrowserDownload(blob, filename) {
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename || 'download.xlsx';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
}
function exportSkipPriceAsin() {
var query = buildSkipPriceAsinFilterQuery();
var operatorQuery = buildSkipPriceAsinOperatorQuery();
if (operatorQuery) query += (query ? '&' : '') + operatorQuery;
var url = '/api/admin/skip-price-asins/export' + (query ? ('?' + query) : '');
fetch(url)
.then(function (response) {
var contentType = response.headers.get('content-type') || '';
if (!response.ok || contentType.indexOf('application/json') >= 0) {
return response.json().then(function (res) {
throw new Error((res && (res.error || res.msg)) || '导出失败');
}).catch(function (err) {
throw err instanceof Error ? err : new Error('导出失败');
});
}
return response.blob().then(function (blob) {
return {
blob: blob,
filename: extractDownloadFilename(response.headers.get('content-disposition'), 'skip-price-asin.xlsx')
};
});
})
.then(function (payload) {
triggerBrowserDownload(payload.blob, payload.filename);
})
.catch(function (err) {
alert((err && err.message) || '导出失败');
});
}
function renderSkipPriceAsinCell(item, country) {
var asinValue = item[country.field] || '';
var minimumPriceValue = formatSkipPriceMinimumPrice(item[country.minimumPriceField]);
@@ -1917,7 +1983,8 @@
var shopName = (btn.dataset.shopName || '').replace(/&quot;/g, '"');
var countryCode = btn.dataset.country || '';
if (!confirm('确定删除店铺“' + shopName + '”在 ' + countryCode + ' 的 ASIN 吗?')) return;
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode, {
var operatorQuery = buildSkipPriceAsinOperatorQuery();
fetch('/api/admin/skip-price-asin/' + btn.dataset.skipPriceAsinDelete + '/country/' + countryCode + (operatorQuery ? ('?' + operatorQuery) : ''), {
method: 'DELETE'
})
.then(function (r) { return r.json(); })
@@ -2184,6 +2251,8 @@
var formData = new FormData();
formData.append('file', file);
formData.append('group_id', groupId);
if (currentUserId) formData.append('operator_id', currentUserId);
if (currentUserRole === 'super_admin') formData.append('super_admin', 'true');
var xhr = new XMLHttpRequest();
xhr.open('POST', deleteMode ? '/api/admin/skip-price-asins/delete-import' : '/api/admin/skip-price-asins/import', true);
xhr.upload.onprogress = function (event) {
@@ -2267,7 +2336,8 @@
return;
}
}
fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode, {
var operatorQuery = buildSkipPriceAsinOperatorQuery();
fetch('/api/admin/skip-price-asin/' + itemId + '/country/' + countryCode + (operatorQuery ? ('?' + operatorQuery) : ''), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -2294,6 +2364,9 @@
document.getElementById('btnSearchSkipPriceAsin').onclick = function () {
loadSkipPriceAsin(1);
};
document.getElementById('btnExportSkipPriceAsin').onclick = function () {
exportSkipPriceAsin();
};
document.getElementById('skipPriceAsinFilterShopName').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
@@ -2334,7 +2407,8 @@
fallbackAsin = asinMappings[countryCode] || '';
return !!fallbackAsin;
});
fetch('/api/admin/skip-price-asin', {
var operatorQuery = buildSkipPriceAsinOperatorQuery();
fetch('/api/admin/skip-price-asin' + (operatorQuery ? ('?' + operatorQuery) : ''), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -3429,8 +3503,10 @@
}
// 初始化
var adminCurrentUserPromise = null;
function loadAdminCurrentUser() {
fetch('/api/admin/current-user')
if (adminCurrentUserPromise) return adminCurrentUserPromise;
adminCurrentUserPromise = fetch('/api/admin/current-user')
.then(function (r) {
if (r.status === 404) {
return fetch('/api/auth/check')
@@ -3474,7 +3550,7 @@
updateShopManageGroupButtonsAccess();
})
.catch(function () {
fetch('/api/auth/check')
return fetch('/api/auth/check')
.then(function (r) { return r.json(); })
.then(function (res) {
document.getElementById('adminCurrentUsername').textContent = res && res.logged_in ? '当前用户' : '未登录';
@@ -3486,7 +3562,13 @@
document.getElementById('adminCurrentUserRole').style.display = 'none';
updateShopManageGroupButtonsAccess();
});
})
.finally(function () {
if (!currentUserId && currentUserRole !== 'super_admin') {
adminCurrentUserPromise = null;
}
});
return adminCurrentUserPromise;
}
document.getElementById('btnAdminLogout').onclick = function () {
@@ -3514,10 +3596,11 @@
});
};
loadAdminCurrentUser();
loadUserOptions();
loadColumnsForPermission();
loadShopManageGroups();
loadAdminMenus();
loadAdminCurrentUser().finally(function () {
loadUserOptions();
loadColumnsForPermission();
loadShopManageGroups();
loadAdminMenus();
});
}) ();

View File

@@ -1202,6 +1202,7 @@
<input type="text" id="skipPriceAsinFilterAsin" placeholder="请输入 ASIN">
</div>
<button class="btn" id="btnSearchSkipPriceAsin">查询</button>
<button class="btn btn-secondary" id="btnExportSkipPriceAsin" type="button">导出 XLSX</button>
</div>
<table>
<thead>