+135
-66
@@ -334,7 +334,8 @@ class _PermissionProxyError(Exception):
|
||||
self.status = status
|
||||
|
||||
|
||||
def _proxy_permission_java(method, path, *, params=None, json_data=None, current_row=None):
|
||||
def _proxy_permission_java(
|
||||
method, path, *, params=None, json_data=None, files=None, data=None, current_row=None):
|
||||
"""Call Java permission APIs using either forwarded JWT or trusted Flask identity."""
|
||||
proxy_params = {}
|
||||
request_row = getattr(g, '_current_user_row', None) if has_request_context() else None
|
||||
@@ -353,6 +354,8 @@ def _proxy_permission_java(method, path, *, params=None, json_data=None, current
|
||||
path,
|
||||
params=proxy_params or None,
|
||||
json_data=json_data,
|
||||
files=files,
|
||||
data=data,
|
||||
headers=headers,
|
||||
)
|
||||
return result, error_response, status
|
||||
@@ -905,7 +908,7 @@ def list_users():
|
||||
return jsonify({'success': False, 'error': '需要登录'}), 403
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
_, _, denied = _ensure_backend_menu_access(
|
||||
'users', 'history', 'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data'
|
||||
'users', 'history', 'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data', 'invalid-asin-data'
|
||||
)
|
||||
if denied:
|
||||
return denied
|
||||
@@ -1335,7 +1338,12 @@ def _parse_admin_datetime_arg(name):
|
||||
raise ValueError(f'{name} 时间格式无效') from exc
|
||||
|
||||
|
||||
_SHOP_DATA_CRAWL_ADMIN_COLUMNS = """
|
||||
_SHOP_DATA_CRAWL_LATEST_TIME_SQL = (
|
||||
'COALESCE(df.last_success_at, df.updated_at, t.finished_at, t.updated_at, t.created_at)'
|
||||
)
|
||||
|
||||
|
||||
_SHOP_DATA_CRAWL_ADMIN_COLUMNS = f"""
|
||||
r.id AS result_id, r.task_id, r.user_id, r.source_filename AS shop_name,
|
||||
r.source_file_url AS shop_id, r.result_filename, r.result_file_url,
|
||||
r.result_file_size, r.result_content_type, r.row_count,
|
||||
@@ -1343,6 +1351,7 @@ _SHOP_DATA_CRAWL_ADMIN_COLUMNS = """
|
||||
r.created_at AS result_created_at,
|
||||
t.task_no, t.status AS task_status, t.request_json, t.result_json,
|
||||
t.error_message AS task_error, t.created_at, t.updated_at, t.finished_at,
|
||||
{_SHOP_DATA_CRAWL_LATEST_TIME_SQL} AS latest_file_updated_at,
|
||||
u.username,
|
||||
(SELECT j.id FROM biz_task_file_job j
|
||||
WHERE j.module_type = 'SHOP_DATA_CRAWL' AND j.result_id = r.id
|
||||
@@ -1446,16 +1455,18 @@ def _shop_data_crawl_admin_item(row, group_names=None):
|
||||
|
||||
|
||||
def _shop_data_crawl_group_item(group_row, result_rows, group_names):
|
||||
"""Build one shop group and cap its children to the newest three results."""
|
||||
"""Build one shop group and expose its current daily workbook only."""
|
||||
raw_shop_name = group_row.get('shop_name') or ''
|
||||
display_shop_name = raw_shop_name or '未命名'
|
||||
group_key = _shop_data_crawl_shop_key(raw_shop_name)
|
||||
children = result_rows.get(group_key)
|
||||
if children is None:
|
||||
children = result_rows.get(str(raw_shop_name).strip(), [])
|
||||
children = children[:3]
|
||||
children = children[:1]
|
||||
result_items = [_shop_data_crawl_admin_item(row, group_names) for row in children]
|
||||
latest_created_at = group_row.get('latest_created_at')
|
||||
if latest_created_at is None and children:
|
||||
latest_created_at = children[0].get('latest_file_updated_at')
|
||||
if latest_created_at is None and result_items:
|
||||
latest_created_at = result_items[0].get('created_at')
|
||||
return {
|
||||
@@ -1541,6 +1552,7 @@ def list_shop_data_crawl_tasks():
|
||||
grouped_from_sql = (
|
||||
' FROM biz_file_result r '
|
||||
'JOIN biz_file_task t ON t.id = r.task_id '
|
||||
'LEFT JOIN biz_shop_data_crawl_daily_file df ON df.latest_result_id = r.id '
|
||||
'LEFT JOIN users u ON u.id = r.user_id '
|
||||
'WHERE ' + where_sql
|
||||
)
|
||||
@@ -1554,7 +1566,8 @@ def list_shop_data_crawl_tasks():
|
||||
total = int((cur.fetchone() or {}).get('total') or 0)
|
||||
|
||||
cur.execute(
|
||||
'SELECT ' + shop_key_sql + ' AS shop_name, MAX(t.created_at) AS latest_created_at'
|
||||
'SELECT ' + shop_key_sql + ' AS shop_name, MAX('
|
||||
+ _SHOP_DATA_CRAWL_LATEST_TIME_SQL + ') AS latest_created_at'
|
||||
+ grouped_from_sql +
|
||||
' GROUP BY ' + shop_key_sql +
|
||||
' ORDER BY latest_created_at DESC, shop_name ASC LIMIT %s OFFSET %s',
|
||||
@@ -1570,14 +1583,16 @@ def list_shop_data_crawl_tasks():
|
||||
cur.execute(
|
||||
'SELECT ranked.* FROM (SELECT ' + _SHOP_DATA_CRAWL_ADMIN_COLUMNS +
|
||||
', ROW_NUMBER() OVER (PARTITION BY ' + shop_key_sql +
|
||||
' ORDER BY t.created_at DESC, r.id DESC) AS shop_row_number '
|
||||
' ORDER BY ' + _SHOP_DATA_CRAWL_LATEST_TIME_SQL
|
||||
+ ' DESC, r.id DESC) AS shop_row_number '
|
||||
' FROM biz_file_result r '
|
||||
'JOIN biz_file_task t ON t.id = r.task_id '
|
||||
'LEFT JOIN biz_shop_data_crawl_daily_file df ON df.latest_result_id = r.id '
|
||||
'LEFT JOIN users u ON u.id = r.user_id '
|
||||
'WHERE ' + where_sql +
|
||||
f' AND {shop_key_sql} IN ({placeholders})' +
|
||||
') ranked WHERE ranked.shop_row_number <= 3 '
|
||||
'ORDER BY ranked.created_at DESC, ranked.result_id DESC',
|
||||
') ranked WHERE ranked.shop_row_number <= 1 '
|
||||
'ORDER BY ranked.latest_file_updated_at DESC, ranked.result_id DESC',
|
||||
tuple(params + selected_shop_names),
|
||||
)
|
||||
for row in cur.fetchall():
|
||||
@@ -2863,6 +2878,26 @@ def delete_shop_key(item_id):
|
||||
|
||||
# ---------- 数据去重总数据 ----------
|
||||
|
||||
def _parse_positive_group_id(raw_value):
|
||||
try:
|
||||
group_id = int(str(raw_value or '').strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return group_id if group_id > 0 else None
|
||||
|
||||
|
||||
def _format_dedupe_total_data_item(item):
|
||||
return {
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'group_id': item.get('groupId'),
|
||||
'group_name': item.get('groupName') or '',
|
||||
'uploader_user_id': item.get('uploaderUserId'),
|
||||
'username': item.get('username') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
}
|
||||
|
||||
|
||||
@admin_api.route('/dedupe-total-data')
|
||||
@login_required
|
||||
def list_dedupe_total_data():
|
||||
@@ -2873,6 +2908,7 @@ def list_dedupe_total_data():
|
||||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||||
keyword = (request.args.get('keyword') or '').strip()
|
||||
username = (request.args.get('username') or '').strip()
|
||||
group_id = _parse_positive_group_id(request.args.get('group_id') or request.args.get('groupId'))
|
||||
start_date = (request.args.get('start_date') or request.args.get('startDate') or '').strip()
|
||||
end_date = (request.args.get('end_date') or request.args.get('endDate') or '').strip()
|
||||
params = {
|
||||
@@ -2886,22 +2922,19 @@ def list_dedupe_total_data():
|
||||
params['startDate'] = start_date
|
||||
if end_date:
|
||||
params['endDate'] = end_date
|
||||
data, error_response, status = _proxy_backend_java(
|
||||
if group_id is not None:
|
||||
params['groupId'] = group_id
|
||||
data, error_response, status = _proxy_permission_java(
|
||||
'GET',
|
||||
'/api/admin/dedupe-total-data',
|
||||
params=params,
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
payload = data.get('data') or {}
|
||||
items = [
|
||||
{
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'uploader_user_id': item.get('uploaderUserId'),
|
||||
'username': item.get('username') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
}
|
||||
_format_dedupe_total_data_item(item)
|
||||
for item in (payload.get('items') or [])
|
||||
]
|
||||
return jsonify({
|
||||
@@ -2929,10 +2962,18 @@ def export_dedupe_total_data():
|
||||
params['startDate'] = start_date
|
||||
if end_date:
|
||||
params['endDate'] = end_date
|
||||
group_id = _parse_positive_group_id(request.args.get('group_id') or request.args.get('groupId'))
|
||||
if group_id is not None:
|
||||
params['groupId'] = group_id
|
||||
|
||||
url = f"{backend_java_base_url}/api/admin/dedupe-total-data/export"
|
||||
try:
|
||||
resp = _get_backend_java_session().get(url, params=params, timeout=60)
|
||||
resp = _get_backend_java_session().get(
|
||||
url,
|
||||
params=params,
|
||||
headers={'X-Internal-Token': _resolve_internal_token()},
|
||||
timeout=60,
|
||||
)
|
||||
except requests.RequestException:
|
||||
return jsonify({'success': False, 'error': 'backend-java 服务不可用'}), 502
|
||||
if resp.status_code >= 400:
|
||||
@@ -2964,10 +3005,10 @@ def dedupe_total_data_import_progress(import_id):
|
||||
_, current_row, denied = _ensure_dedupe_total_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'GET',
|
||||
f'/api/admin/dedupe-total-data/import/{import_id}',
|
||||
params={'operatorId': current_row.get('id')},
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -2995,14 +3036,18 @@ def import_dedupe_total_data():
|
||||
file_storage = request.files.get('file')
|
||||
if not file_storage or file_storage.filename == '':
|
||||
return jsonify({'success': False, 'error': '请选择 Excel 文件'})
|
||||
group_id = _parse_positive_group_id(request.form.get('group_id') or request.form.get('groupId'))
|
||||
if group_id is None:
|
||||
return jsonify({'success': False, 'error': '请选择分组'})
|
||||
files = {
|
||||
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
|
||||
}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'POST',
|
||||
'/api/admin/dedupe-total-data/import',
|
||||
files=files,
|
||||
data={'operatorId': current_row.get('id')},
|
||||
data={'groupId': group_id},
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3020,10 +3065,10 @@ def dedupe_total_data_delete_import_progress(import_id):
|
||||
_, current_row, denied = _ensure_dedupe_total_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'GET',
|
||||
f'/api/admin/dedupe-total-data/delete-import/{import_id}',
|
||||
params={'operatorId': current_row.get('id')},
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3051,14 +3096,18 @@ def delete_import_dedupe_total_data():
|
||||
file_storage = request.files.get('file')
|
||||
if not file_storage or file_storage.filename == '':
|
||||
return jsonify({'success': False, 'error': '请选择 Excel 文件'})
|
||||
group_id = _parse_positive_group_id(request.form.get('group_id') or request.form.get('groupId'))
|
||||
if group_id is None:
|
||||
return jsonify({'success': False, 'error': '请选择分组'})
|
||||
files = {
|
||||
'file': (file_storage.filename, file_storage.stream, file_storage.mimetype or 'application/octet-stream')
|
||||
}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'POST',
|
||||
'/api/admin/dedupe-total-data/delete-import',
|
||||
files=files,
|
||||
data={'operatorId': current_row.get('id')},
|
||||
data={'groupId': group_id},
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3077,12 +3126,15 @@ def create_dedupe_total_data():
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json() or {}
|
||||
payload = {'dataValue': (data.get('data_value') or '').strip()}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
payload = {
|
||||
'dataValue': (data.get('data_value') or '').strip(),
|
||||
'groupId': _parse_positive_group_id(data.get('group_id') or data.get('groupId')),
|
||||
}
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'POST',
|
||||
'/api/admin/dedupe-total-data',
|
||||
params={'operatorId': current_row.get('id')},
|
||||
json_data=payload,
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3090,13 +3142,7 @@ def create_dedupe_total_data():
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'msg': result.get('message') or '创建成功',
|
||||
'item': {
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'uploader_user_id': item.get('uploaderUserId'),
|
||||
'username': item.get('username') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
},
|
||||
'item': _format_dedupe_total_data_item(item),
|
||||
})
|
||||
|
||||
|
||||
@@ -3107,12 +3153,15 @@ def update_dedupe_total_data(item_id):
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json() or {}
|
||||
payload = {'dataValue': (data.get('data_value') or '').strip()}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
payload = {
|
||||
'dataValue': (data.get('data_value') or '').strip(),
|
||||
'groupId': _parse_positive_group_id(data.get('group_id') or data.get('groupId')),
|
||||
}
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'PUT',
|
||||
f'/api/admin/dedupe-total-data/{item_id}',
|
||||
params={'operatorId': current_row.get('id')},
|
||||
json_data=payload,
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3120,13 +3169,7 @@ def update_dedupe_total_data(item_id):
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'msg': result.get('message') or '更新成功',
|
||||
'item': {
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'uploader_user_id': item.get('uploaderUserId'),
|
||||
'username': item.get('username') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
},
|
||||
'item': _format_dedupe_total_data_item(item),
|
||||
})
|
||||
|
||||
|
||||
@@ -3136,10 +3179,10 @@ def delete_dedupe_total_data(item_id):
|
||||
_, current_row, denied = _ensure_dedupe_total_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'DELETE',
|
||||
f'/api/admin/dedupe-total-data/{item_id}',
|
||||
params={'operatorId': current_row.get('id')},
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3157,23 +3200,36 @@ def _format_invalid_asin_data_item(item):
|
||||
'id': item.get('id'),
|
||||
'data_value': item.get('dataValue') or '',
|
||||
'brand': item.get('brand') or '',
|
||||
'group_id': item.get('groupId'),
|
||||
'group_name': item.get('groupName') or '',
|
||||
'record_source': item.get('recordSource') or 'AUTO',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
}
|
||||
|
||||
|
||||
@admin_api.route('/invalid-asin-data')
|
||||
@admin_required
|
||||
@login_required
|
||||
def list_invalid_asin_data():
|
||||
_, _, denied = _ensure_admin_menu_access('invalid-asin-data')
|
||||
role, current_row, denied = _ensure_backend_menu_access('invalid-asin-data')
|
||||
if denied:
|
||||
return denied
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
page_size = min(100, max(1, int(request.args.get('page_size', 15))))
|
||||
keyword = (request.args.get('keyword') or '').strip()
|
||||
data, error_response, status = _proxy_backend_java(
|
||||
group_id_raw = (request.args.get('group_id') or '').strip()
|
||||
group_id = int(group_id_raw) if group_id_raw.isdigit() and int(group_id_raw) > 0 else None
|
||||
params = {
|
||||
'page': page,
|
||||
'pageSize': page_size,
|
||||
'keyword': keyword,
|
||||
}
|
||||
if group_id is not None:
|
||||
params['groupId'] = group_id
|
||||
data, error_response, status = _proxy_permission_java(
|
||||
'GET',
|
||||
'/api/admin/invalid-asin-data',
|
||||
params={'page': page, 'pageSize': page_size, 'keyword': keyword},
|
||||
params=params,
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3189,20 +3245,22 @@ def list_invalid_asin_data():
|
||||
|
||||
|
||||
@admin_api.route('/invalid-asin-data', methods=['POST'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def create_invalid_asin_data():
|
||||
_, _, denied = _ensure_admin_menu_access('invalid-asin-data')
|
||||
role, current_row, denied = _ensure_backend_menu_access('invalid-asin-data')
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json() or {}
|
||||
payload = {
|
||||
'dataValue': (data.get('data_value') or '').strip(),
|
||||
'brand': (data.get('brand') or '').strip(),
|
||||
'groupId': data.get('group_id'),
|
||||
}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'POST',
|
||||
'/api/admin/invalid-asin-data',
|
||||
json_data=payload,
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3215,20 +3273,22 @@ def create_invalid_asin_data():
|
||||
|
||||
|
||||
@admin_api.route('/invalid-asin-data/<int:item_id>', methods=['PUT'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def update_invalid_asin_data(item_id):
|
||||
_, _, denied = _ensure_admin_menu_access('invalid-asin-data')
|
||||
role, current_row, denied = _ensure_backend_menu_access('invalid-asin-data')
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json() or {}
|
||||
payload = {
|
||||
'dataValue': (data.get('data_value') or '').strip(),
|
||||
'brand': (data.get('brand') or '').strip(),
|
||||
'groupId': data.get('group_id'),
|
||||
}
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'PUT',
|
||||
f'/api/admin/invalid-asin-data/{item_id}',
|
||||
json_data=payload,
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3241,14 +3301,15 @@ def update_invalid_asin_data(item_id):
|
||||
|
||||
|
||||
@admin_api.route('/invalid-asin-data/<int:item_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
@login_required
|
||||
def delete_invalid_asin_data(item_id):
|
||||
_, _, denied = _ensure_admin_menu_access('invalid-asin-data')
|
||||
role, current_row, denied = _ensure_backend_menu_access('invalid-asin-data')
|
||||
if denied:
|
||||
return denied
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
'DELETE',
|
||||
f'/api/admin/invalid-asin-data/{item_id}',
|
||||
current_row=current_row,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -3515,21 +3576,29 @@ def delete_shop_manage(item_id):
|
||||
@login_required
|
||||
def list_shop_manage_groups():
|
||||
role, current_row, denied = _ensure_backend_menu_access(
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data'
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data', 'invalid-asin-data'
|
||||
)
|
||||
if denied:
|
||||
return denied
|
||||
groups, error_response, status = _load_expanded_shop_manage_groups(role, current_row)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
return jsonify({'success': True, 'items': [_format_shop_manage_group_item(item) for item in (groups or [])]})
|
||||
items = [_format_shop_manage_group_item(item) for item in (groups or [])]
|
||||
locked_group_id = None
|
||||
if role != 'super_admin':
|
||||
locked_group_id = next((item.get('id') for item in items if item.get('id')), None)
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'items': items,
|
||||
'locked_group_id': locked_group_id,
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage-group', methods=['POST'])
|
||||
@login_required
|
||||
def create_shop_manage_group():
|
||||
role, current_row, denied = _ensure_backend_menu_access(
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data'
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data', 'invalid-asin-data'
|
||||
)
|
||||
if denied:
|
||||
return denied
|
||||
@@ -3576,7 +3645,7 @@ def create_shop_manage_group():
|
||||
@login_required
|
||||
def update_shop_manage_group(item_id):
|
||||
role, current_row, denied = _ensure_backend_menu_access(
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data'
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data', 'invalid-asin-data'
|
||||
)
|
||||
if denied:
|
||||
return denied
|
||||
@@ -3823,7 +3892,7 @@ def delete_skip_price_asin_country(item_id, country):
|
||||
@login_required
|
||||
def delete_shop_manage_group(item_id):
|
||||
role, current_row, denied = _ensure_backend_menu_access(
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data'
|
||||
'shop-manage', 'skip-price-asin', 'query-asin', 'dedupe-total-data', 'invalid-asin-data'
|
||||
)
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
@@ -3,6 +3,8 @@ annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
Authlib==1.6.8
|
||||
blinker==1.9.0
|
||||
boto3==1.43.65
|
||||
botocore==1.43.65
|
||||
bottle==0.13.4
|
||||
certifi==2026.1.4
|
||||
cffi==2.0.0
|
||||
|
||||
+174
-19
@@ -120,7 +120,10 @@
|
||||
loadDedupeTotalData(1);
|
||||
loadDedupeGroupSummary();
|
||||
}
|
||||
else if (tabName === 'invalid-asin-data') loadInvalidAsinData(1);
|
||||
else if (tabName === 'invalid-asin-data') {
|
||||
loadShopManageGroups();
|
||||
loadInvalidAsinData(1);
|
||||
}
|
||||
else if (tabName === 'shop-keys') loadShopKeys(1);
|
||||
else if (tabName === 'shop-manage') loadShopManage(1);
|
||||
else if (tabName === 'skip-price-asin') loadSkipPriceAsin(1);
|
||||
@@ -1526,7 +1529,7 @@
|
||||
});
|
||||
groups.forEach(function (group) {
|
||||
group.results.sort(shopDataResultSort);
|
||||
group.results = group.results.slice(0, 3);
|
||||
group.results = group.results.slice(0, 1);
|
||||
if (!group.latest_created_at && group.results.length) {
|
||||
group.latest_created_at = group.results[0].created_at || group.results[0].finished_at || '';
|
||||
}
|
||||
@@ -1579,7 +1582,7 @@
|
||||
'<div class="image-video-card-body">' +
|
||||
'<div class="image-video-card-head shop-data-group-head">' +
|
||||
'<div class="shop-data-task-title"><span title="' + escapeHtml(group.shop_name || '-') + '">' + escapeHtml(group.shop_name || '-') + '</span></div>' +
|
||||
'<span class="shop-data-group-meta">' + results.length + '/3 份结果</span>' +
|
||||
'<span class="shop-data-group-meta">' + results.length + '/1 份当日累计文件</span>' +
|
||||
'</div>' +
|
||||
'<div class="image-video-card-info">' +
|
||||
'<div class="image-video-info-row"><label>分组</label><span>' + escapeHtml(group.group_name || '-') + '</span></div>' +
|
||||
@@ -1641,7 +1644,7 @@
|
||||
var total = payload.total != null ? Number(payload.total) : shopDataTaskGroups.length;
|
||||
var responsePage = payload.page || page;
|
||||
var responsePageSize = payload.page_size || shopDataTaskPageSize;
|
||||
document.getElementById('shopDataTaskTotal').textContent = '共 ' + (total || 0) + ' 家店铺 · 每家店铺保留最新 3 份任务结果';
|
||||
document.getElementById('shopDataTaskTotal').textContent = '共 ' + (total || 0) + ' 家店铺 · 每家店铺保留 1 份当日累计文件';
|
||||
renderShopDataTasks();
|
||||
renderPagination('shopDataTaskPagination', total, responsePage, responsePageSize, loadShopDataCrawlTasks);
|
||||
})
|
||||
@@ -2176,9 +2179,11 @@
|
||||
var q = 'page=' + (page || 1) + '&page_size=' + dedupeTotalDataPageSize;
|
||||
var keyword = (document.getElementById('searchDedupeTotalData').value || '').trim();
|
||||
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
|
||||
var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim();
|
||||
var dateRange = getDedupeTotalDataDateRange();
|
||||
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
|
||||
if (username) q += '&username=' + encodeURIComponent(username);
|
||||
if (groupId) q += '&group_id=' + encodeURIComponent(groupId);
|
||||
if (dateRange.startDate) q += '&start_date=' + encodeURIComponent(dateRange.startDate);
|
||||
if (dateRange.endDate) q += '&end_date=' + encodeURIComponent(dateRange.endDate);
|
||||
return q;
|
||||
@@ -2191,16 +2196,16 @@
|
||||
.then(function (res) {
|
||||
var tbody = document.getElementById('dedupeTotalDataListBody');
|
||||
if (!res.success) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">加载失败: ' + escapeHtml(res.error || '') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
var items = res.items || [];
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无总数据</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="empty-tip">暂无总数据</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(function (item) {
|
||||
return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">编辑</button> ' +
|
||||
return '<tr><td>' + escapeHtml(item.id) + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.username || '') + '</td><td>' + escapeHtml(item.group_name || '未分组') + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-dedupe-total-edit="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '" data-group-id="' + escapeHtml(item.group_id || '') + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-dedupe-total-delete="' + escapeHtml(item.id) + '" data-value="' + escapeHtml(item.data_value || '') + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
}).join('');
|
||||
@@ -2209,7 +2214,7 @@
|
||||
bindDedupeTotalDataActions();
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
|
||||
document.getElementById('dedupeTotalDataListBody').innerHTML = '<tr><td colspan="6" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
function bindDedupeTotalDataActions() {
|
||||
@@ -2219,7 +2224,11 @@
|
||||
document.getElementById('editDedupeTotalDataValue').value = (btn.dataset.value || '').replace(/"/g, '"');
|
||||
document.getElementById('msgEditDedupeTotalData').textContent = '';
|
||||
document.getElementById('msgEditDedupeTotalData').className = 'msg';
|
||||
document.getElementById('editDedupeTotalDataModal').classList.add('show');
|
||||
var groupId = btn.dataset.groupId || '';
|
||||
loadShopManageGroups().then(function () {
|
||||
document.getElementById('editDedupeTotalDataGroupId').value = groupId;
|
||||
document.getElementById('editDedupeTotalDataModal').classList.add('show');
|
||||
});
|
||||
};
|
||||
});
|
||||
document.querySelectorAll('[data-dedupe-total-delete]').forEach(function (btn) {
|
||||
@@ -2238,10 +2247,12 @@
|
||||
document.getElementById('btnSearchDedupeTotalData').onclick = function () { loadDedupeTotalData(1); };
|
||||
document.getElementById('btnExportDedupeTotalData').onclick = function () {
|
||||
var username = (document.getElementById('searchDedupeTotalDataUsername').value || '').trim();
|
||||
var groupId = (document.getElementById('dedupeTotalDataGroupFilterId').value || '').trim();
|
||||
var dateRange = getDedupeTotalDataDateRange();
|
||||
if (!validateDedupeTotalDataDateRange()) return;
|
||||
var params = [];
|
||||
if (username) params.push('username=' + encodeURIComponent(username));
|
||||
if (groupId) params.push('group_id=' + encodeURIComponent(groupId));
|
||||
if (dateRange.startDate) params.push('start_date=' + encodeURIComponent(dateRange.startDate));
|
||||
if (dateRange.endDate) params.push('end_date=' + encodeURIComponent(dateRange.endDate));
|
||||
fetch('/api/admin/dedupe-total-data/export' + (params.length ? ('?' + params.join('&')) : ''))
|
||||
@@ -2386,6 +2397,12 @@
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
var groupId = (document.getElementById('dedupeTotalDataImportGroupId').value || '').trim();
|
||||
if (!groupId) {
|
||||
msgEl.textContent = '请选择分组';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
var file = fileInput.files[0];
|
||||
var lowerName = (file.name || '').toLowerCase();
|
||||
if (!(lowerName.endsWith('.xlsx') || lowerName.endsWith('.xls'))) {
|
||||
@@ -2395,6 +2412,7 @@
|
||||
}
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('group_id', groupId);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/admin/dedupe-total-data/import', true);
|
||||
xhr.upload.onprogress = function (event) {
|
||||
@@ -2449,8 +2467,15 @@
|
||||
if (!confirm('确定按 Excel 中的 ASIN 批量删除匹配的总数据吗?')) {
|
||||
return;
|
||||
}
|
||||
var groupId = (document.getElementById('dedupeTotalDataDeleteGroupId').value || '').trim();
|
||||
if (!groupId) {
|
||||
msgEl.textContent = '请选择分组';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
var formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('group_id', groupId);
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/admin/dedupe-total-data/delete-import', true);
|
||||
xhr.upload.onprogress = function (event) {
|
||||
@@ -2486,6 +2511,7 @@
|
||||
document.getElementById('btnSaveDedupeTotalData').onclick = function () {
|
||||
var itemId = document.getElementById('editDedupeTotalDataId').value;
|
||||
var value = (document.getElementById('editDedupeTotalDataValue').value || '').trim();
|
||||
var groupId = (document.getElementById('editDedupeTotalDataGroupId').value || '').trim();
|
||||
var msgEl = document.getElementById('msgEditDedupeTotalData');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
@@ -2494,10 +2520,15 @@
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
if (!groupId) {
|
||||
msgEl.textContent = '请选择分组';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
fetch('/api/admin/dedupe-total-data/' + itemId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data_value: value })
|
||||
body: JSON.stringify({ data_value: value, group_id: Number(groupId) })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
@@ -2519,7 +2550,9 @@
|
||||
function buildInvalidAsinDataQuery(page) {
|
||||
var q = 'page=' + (page || 1) + '&page_size=' + invalidAsinDataPageSize;
|
||||
var keyword = (document.getElementById('searchInvalidAsinData').value || '').trim();
|
||||
var groupId = (document.getElementById('invalidAsinDataFilterGroupId').value || '').trim();
|
||||
if (keyword) q += '&keyword=' + encodeURIComponent(keyword);
|
||||
if (groupId) q += '&group_id=' + encodeURIComponent(groupId);
|
||||
return q;
|
||||
}
|
||||
function loadInvalidAsinData(page) {
|
||||
@@ -2529,18 +2562,21 @@
|
||||
.then(function (res) {
|
||||
var tbody = document.getElementById('invalidAsinDataListBody');
|
||||
if (!res.success) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">加载失败: ' + (res.error || '') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
var items = res.items || [];
|
||||
if (items.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="empty-tip">暂无数据</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-tip">暂无数据</td></tr>';
|
||||
} else {
|
||||
tbody.innerHTML = items.map(function (item) {
|
||||
var dataValueAttr = (item.data_value || '').replace(/"/g, '"');
|
||||
var brandAttr = (item.brand || '').replace(/"/g, '"');
|
||||
return '<tr><td>' + item.id + '</td><td>' + (item.data_value || '') + '</td><td>' + (item.brand || '') + '</td><td>' + (item.created_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-invalid-asin-edit="' + item.id + '" data-value="' + dataValueAttr + '" data-brand="' + brandAttr + '">编辑</button> ' +
|
||||
var groupId = item.group_id == null ? '' : String(item.group_id);
|
||||
var source = String(item.record_source || 'AUTO').toUpperCase();
|
||||
var sourceLabel = source === 'MANUAL' ? '\u624b\u52a8\u65b0\u589e' : '\u81ea\u52a8\u5bfc\u5165';
|
||||
return '<tr><td>' + escapeHtml(item.id || '') + '</td><td>' + escapeHtml(item.data_value || '') + '</td><td>' + escapeHtml(item.brand || '') + '</td><td>' + escapeHtml(item.group_name || '') + '</td><td>' + sourceLabel + '</td><td>' + escapeHtml(item.created_at || '') + '</td><td>' +
|
||||
'<button class="btn btn-sm" data-invalid-asin-edit="' + item.id + '" data-value="' + dataValueAttr + '" data-brand="' + brandAttr + '" data-group-id="' + groupId + '" data-source="' + source + '">编辑</button> ' +
|
||||
'<button class="btn btn-sm btn-danger" data-invalid-asin-delete="' + item.id + '" data-value="' + dataValueAttr + '">删除</button>' +
|
||||
'</td></tr>';
|
||||
}).join('');
|
||||
@@ -2549,7 +2585,7 @@
|
||||
bindInvalidAsinDataActions();
|
||||
})
|
||||
.catch(function () {
|
||||
document.getElementById('invalidAsinDataListBody').innerHTML = '<tr><td colspan="5" class="empty-tip">请求失败</td></tr>';
|
||||
document.getElementById('invalidAsinDataListBody').innerHTML = '<tr><td colspan="7" class="empty-tip">请求失败</td></tr>';
|
||||
});
|
||||
}
|
||||
function bindInvalidAsinDataActions() {
|
||||
@@ -2558,6 +2594,20 @@
|
||||
document.getElementById('editInvalidAsinDataId').value = btn.dataset.invalidAsinEdit || '';
|
||||
document.getElementById('editInvalidAsinDataValue').value = (btn.dataset.value || '').replace(/"/g, '"');
|
||||
document.getElementById('editInvalidAsinDataBrand').value = (btn.dataset.brand || '').replace(/"/g, '"');
|
||||
var source = String(btn.dataset.source || 'AUTO').toUpperCase();
|
||||
var isManual = source === 'MANUAL';
|
||||
document.getElementById('editInvalidAsinDataRecordSource').value = source;
|
||||
document.getElementById('editInvalidAsinDataSourceLabel').value = isManual
|
||||
? '\u624b\u52a8\u65b0\u589e'
|
||||
: '\u81ea\u52a8\u5bfc\u5165';
|
||||
document.getElementById('editInvalidAsinDataGroupForm').style.display = isManual ? '' : 'none';
|
||||
if (isManual) {
|
||||
loadShopManageGroups().then(function () {
|
||||
document.getElementById('editInvalidAsinDataGroupSelect').value = currentUserRole === 'super_admin'
|
||||
? (btn.dataset.groupId || '')
|
||||
: getInvalidAsinDataLockedGroupId();
|
||||
});
|
||||
}
|
||||
document.getElementById('msgEditInvalidAsinData').textContent = '';
|
||||
document.getElementById('msgEditInvalidAsinData').className = 'msg';
|
||||
document.getElementById('editInvalidAsinDataModal').classList.add('show');
|
||||
@@ -2580,6 +2630,7 @@
|
||||
document.getElementById('btnAddInvalidAsinData').onclick = function () {
|
||||
var dataValue = (document.getElementById('invalidAsinDataValue').value || '').trim();
|
||||
var brand = (document.getElementById('invalidAsinDataBrand').value || '').trim();
|
||||
var groupId = (document.getElementById('invalidAsinDataGroupSelect').value || '').trim();
|
||||
var msgEl = document.getElementById('msgInvalidAsinData');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
@@ -2588,10 +2639,15 @@
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
if (!groupId) {
|
||||
msgEl.textContent = '请选择分组';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
fetch('/api/admin/invalid-asin-data', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data_value: dataValue, brand: brand })
|
||||
body: JSON.stringify({ data_value: dataValue, brand: brand, group_id: Number(groupId) })
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
@@ -2600,6 +2656,9 @@
|
||||
msgEl.classList.add('ok');
|
||||
document.getElementById('invalidAsinDataValue').value = '';
|
||||
document.getElementById('invalidAsinDataBrand').value = '';
|
||||
document.getElementById('invalidAsinDataGroupSelect').value = currentUserRole === 'super_admin'
|
||||
? ''
|
||||
: getInvalidAsinDataLockedGroupId();
|
||||
loadInvalidAsinData(1);
|
||||
} else {
|
||||
msgEl.textContent = res.error || '创建失败';
|
||||
@@ -2615,6 +2674,8 @@
|
||||
var itemId = document.getElementById('editInvalidAsinDataId').value;
|
||||
var dataValue = (document.getElementById('editInvalidAsinDataValue').value || '').trim();
|
||||
var brand = (document.getElementById('editInvalidAsinDataBrand').value || '').trim();
|
||||
var source = String(document.getElementById('editInvalidAsinDataRecordSource').value || 'AUTO').toUpperCase();
|
||||
var groupId = (document.getElementById('editInvalidAsinDataGroupSelect').value || '').trim();
|
||||
var msgEl = document.getElementById('msgEditInvalidAsinData');
|
||||
msgEl.textContent = '';
|
||||
msgEl.className = 'msg';
|
||||
@@ -2623,10 +2684,19 @@
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
if (source === 'MANUAL' && !groupId) {
|
||||
msgEl.textContent = '请选择分组';
|
||||
msgEl.classList.add('err');
|
||||
return;
|
||||
}
|
||||
fetch('/api/admin/invalid-asin-data/' + itemId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ data_value: dataValue, brand: brand })
|
||||
body: JSON.stringify({
|
||||
data_value: dataValue,
|
||||
brand: brand,
|
||||
group_id: source === 'MANUAL' ? Number(groupId) : null
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (res) {
|
||||
@@ -2803,6 +2873,7 @@
|
||||
var shopManagePage = 1, shopManagePageSize = 15;
|
||||
var shopManageGroups = [];
|
||||
var shopManageGroupsLoadedAt = 0;
|
||||
var shopManageLockedGroupId = null;
|
||||
var currentShopManageGroupGrantRoutes = [];
|
||||
|
||||
function buildShopManageQuery(page) {
|
||||
@@ -2926,9 +2997,22 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getInvalidAsinDataLockedGroupId() {
|
||||
if (currentUserRole === 'super_admin') return '';
|
||||
if (shopManageLockedGroupId) return String(shopManageLockedGroupId);
|
||||
return shopManageGroups.length ? String(shopManageGroups[0].id || '') : '';
|
||||
}
|
||||
|
||||
function refreshShopGroupSelects(selectedCreateId, selectedEditId) {
|
||||
var createSel = document.getElementById('shopManageGroupSelect');
|
||||
var editSel = document.getElementById('editShopManageGroupSelect');
|
||||
var invalidCreateSel = document.getElementById('invalidAsinDataGroupSelect');
|
||||
var invalidEditSel = document.getElementById('editInvalidAsinDataGroupSelect');
|
||||
var invalidFilterSel = document.getElementById('invalidAsinDataFilterGroupId');
|
||||
var dedupeImportSel = document.getElementById('dedupeTotalDataImportGroupId');
|
||||
var dedupeDeleteSel = document.getElementById('dedupeTotalDataDeleteGroupId');
|
||||
var dedupeFilterSel = document.getElementById('dedupeTotalDataGroupFilterId');
|
||||
var dedupeEditSel = document.getElementById('editDedupeTotalDataGroupId');
|
||||
var filterSel = document.getElementById('shopManageFilterGroupId');
|
||||
var skipCreateSel = document.getElementById('skipPriceAsinGroupSelect');
|
||||
var skipFilterSel = document.getElementById('skipPriceAsinFilterGroupId');
|
||||
@@ -2936,6 +3020,13 @@
|
||||
var queryCreateSel = document.getElementById('queryAsinGroupSelect');
|
||||
var queryFilterSel = document.getElementById('queryAsinFilterGroupId');
|
||||
var chooseQueryShopSel = document.getElementById('chooseQueryAsinShopGroupId');
|
||||
var selectedInvalidCreateId = invalidCreateSel ? invalidCreateSel.value : '';
|
||||
var selectedInvalidEditId = invalidEditSel ? invalidEditSel.value : '';
|
||||
var selectedInvalidFilterId = invalidFilterSel ? invalidFilterSel.value : '';
|
||||
var selectedDedupeImportId = dedupeImportSel ? dedupeImportSel.value : '';
|
||||
var selectedDedupeDeleteId = dedupeDeleteSel ? dedupeDeleteSel.value : '';
|
||||
var selectedDedupeFilterId = dedupeFilterSel ? dedupeFilterSel.value : '';
|
||||
var selectedDedupeEditId = dedupeEditSel ? dedupeEditSel.value : '';
|
||||
var selectedFilterId = filterSel ? filterSel.value : '';
|
||||
var selectedSkipCreateId = skipCreateSel ? skipCreateSel.value : '';
|
||||
var selectedSkipFilterId = skipFilterSel ? skipFilterSel.value : '';
|
||||
@@ -2952,6 +3043,37 @@
|
||||
});
|
||||
createSel.innerHTML = createOpts.join('');
|
||||
editSel.innerHTML = createOpts.join('');
|
||||
var lockedInvalidGroupId = getInvalidAsinDataLockedGroupId();
|
||||
var lockedGroup = shopManageGroups.find(function (group) {
|
||||
return String(group.id || '') === lockedInvalidGroupId;
|
||||
});
|
||||
var invalidGroupOpts = createOpts;
|
||||
if (currentUserRole !== 'super_admin') {
|
||||
invalidGroupOpts = lockedGroup
|
||||
? ['<option value="' + lockedGroup.id + '">' + (lockedGroup.group_name || '') + '</option>']
|
||||
: [createOpts[0]];
|
||||
}
|
||||
var invalidFilterOpts = currentUserRole === 'super_admin'
|
||||
? filterOpts
|
||||
: (lockedGroup
|
||||
? ['<option value="' + lockedGroup.id + '">' + (lockedGroup.group_name || '') + '</option>']
|
||||
: [createOpts[0]]);
|
||||
if (invalidCreateSel) {
|
||||
invalidCreateSel.innerHTML = invalidGroupOpts.join('');
|
||||
invalidCreateSel.disabled = currentUserRole !== 'super_admin';
|
||||
}
|
||||
if (invalidEditSel) {
|
||||
invalidEditSel.innerHTML = invalidGroupOpts.join('');
|
||||
invalidEditSel.disabled = currentUserRole !== 'super_admin';
|
||||
}
|
||||
if (invalidFilterSel) {
|
||||
invalidFilterSel.innerHTML = invalidFilterOpts.join('');
|
||||
invalidFilterSel.disabled = currentUserRole !== 'super_admin';
|
||||
}
|
||||
if (dedupeImportSel) dedupeImportSel.innerHTML = createOpts.join('');
|
||||
if (dedupeDeleteSel) dedupeDeleteSel.innerHTML = createOpts.join('');
|
||||
if (dedupeFilterSel) dedupeFilterSel.innerHTML = filterOpts.join('');
|
||||
if (dedupeEditSel) dedupeEditSel.innerHTML = createOpts.join('');
|
||||
if (skipCreateSel) skipCreateSel.innerHTML = createOpts.join('');
|
||||
if (queryCreateSel) queryCreateSel.innerHTML = createOpts.join('');
|
||||
if (filterSel) filterSel.innerHTML = filterOpts.join('');
|
||||
@@ -2961,6 +3083,25 @@
|
||||
if (chooseQueryShopSel) chooseQueryShopSel.innerHTML = filterOpts.join('');
|
||||
if (selectedCreateId != null) createSel.value = String(selectedCreateId);
|
||||
if (selectedEditId != null) editSel.value = String(selectedEditId);
|
||||
if (invalidCreateSel) {
|
||||
invalidCreateSel.value = currentUserRole === 'super_admin'
|
||||
? selectedInvalidCreateId
|
||||
: lockedInvalidGroupId;
|
||||
}
|
||||
if (invalidEditSel) {
|
||||
invalidEditSel.value = currentUserRole === 'super_admin'
|
||||
? selectedInvalidEditId
|
||||
: lockedInvalidGroupId;
|
||||
}
|
||||
if (invalidFilterSel) {
|
||||
invalidFilterSel.value = currentUserRole === 'super_admin'
|
||||
? selectedInvalidFilterId
|
||||
: lockedInvalidGroupId;
|
||||
}
|
||||
if (dedupeImportSel && selectedDedupeImportId) dedupeImportSel.value = selectedDedupeImportId;
|
||||
if (dedupeDeleteSel && selectedDedupeDeleteId) dedupeDeleteSel.value = selectedDedupeDeleteId;
|
||||
if (dedupeFilterSel && selectedDedupeFilterId) dedupeFilterSel.value = selectedDedupeFilterId;
|
||||
if (dedupeEditSel && selectedDedupeEditId) dedupeEditSel.value = selectedDedupeEditId;
|
||||
if (filterSel && selectedFilterId) filterSel.value = selectedFilterId;
|
||||
if (skipCreateSel && selectedSkipCreateId) skipCreateSel.value = selectedSkipCreateId;
|
||||
if (skipFilterSel && selectedSkipFilterId) skipFilterSel.value = selectedSkipFilterId;
|
||||
@@ -2980,12 +3121,14 @@
|
||||
.then(function (res) {
|
||||
if (!res.success) throw new Error(res.error || '加载分组失败');
|
||||
shopManageGroups = res.items || [];
|
||||
shopManageLockedGroupId = res.locked_group_id || null;
|
||||
shopManageGroupsLoadedAt = Date.now();
|
||||
refreshShopGroupSelects(selectedCreateId, selectedEditId);
|
||||
return shopManageGroups;
|
||||
})
|
||||
.catch(function () {
|
||||
shopManageGroups = [];
|
||||
shopManageLockedGroupId = null;
|
||||
shopManageGroupsLoadedAt = 0;
|
||||
refreshShopGroupSelects();
|
||||
return [];
|
||||
@@ -3040,7 +3183,7 @@
|
||||
|
||||
function updateShopManageGroupButtonsAccess() {
|
||||
var canManageGroups = !!currentUserId;
|
||||
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups', 'btnManageQueryAsinGroups', 'btnManageDedupeGroups'].forEach(function (id) {
|
||||
['btnManageShopGroups', 'btnManageShopGroupsFromEdit', 'btnManageSkipPriceAsinGroups', 'btnManageQueryAsinGroups', 'btnManageDedupeGroups', 'btnManageInvalidAsinDataGroups', 'btnManageInvalidAsinDataGroupsFromEdit'].forEach(function (id) {
|
||||
var btn = document.getElementById(id);
|
||||
if (btn) btn.style.display = canManageGroups ? '' : 'none';
|
||||
});
|
||||
@@ -3123,6 +3266,8 @@
|
||||
renderDedupeGroupSummary();
|
||||
loadShopManage(shopManagePage);
|
||||
loadSkipPriceAsin(skipPriceAsinPage);
|
||||
if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage);
|
||||
if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -3149,6 +3294,14 @@
|
||||
setShopManageGroupGrantRoutes(['dedupe-total-data']);
|
||||
openShopManageGroupModal();
|
||||
};
|
||||
document.getElementById('btnManageInvalidAsinDataGroups').onclick = function () {
|
||||
setShopManageGroupGrantRoutes(['invalid-asin-data']);
|
||||
openShopManageGroupModal();
|
||||
};
|
||||
document.getElementById('btnManageInvalidAsinDataGroupsFromEdit').onclick = function () {
|
||||
setShopManageGroupGrantRoutes(['invalid-asin-data']);
|
||||
openShopManageGroupModal();
|
||||
};
|
||||
document.getElementById('btnSearchShopManage').onclick = function () {
|
||||
loadShopManage(1);
|
||||
};
|
||||
@@ -3200,6 +3353,8 @@
|
||||
renderDedupeGroupSummary();
|
||||
loadShopManage(shopManagePage);
|
||||
loadSkipPriceAsin(skipPriceAsinPage);
|
||||
if (getActiveAdminTabName() === 'dedupe-total-data') loadDedupeTotalData(dedupeTotalDataPage);
|
||||
if (getActiveAdminTabName() === 'invalid-asin-data') loadInvalidAsinData(invalidAsinDataPage);
|
||||
});
|
||||
})
|
||||
.catch(function () {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import io
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import Flask
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from blueprints import admin_api
|
||||
|
||||
|
||||
class _FakeExportResponse:
|
||||
status_code = 200
|
||||
content = b'xlsx'
|
||||
headers = {
|
||||
'Content-Disposition': 'attachment; filename=dedupe.xlsx',
|
||||
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
}
|
||||
|
||||
|
||||
class AdminDedupeTotalDataTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = Flask(__name__)
|
||||
|
||||
def _menu_access(self):
|
||||
return patch.object(
|
||||
admin_api,
|
||||
'_ensure_backend_menu_access',
|
||||
return_value=('admin', {'id': 7}, None),
|
||||
)
|
||||
|
||||
def test_list_forwards_group_and_formats_item(self):
|
||||
java_response = {
|
||||
'data': {
|
||||
'items': [{
|
||||
'id': 9,
|
||||
'dataValue': 'B012345678',
|
||||
'groupId': 3,
|
||||
'groupName': 'group-a',
|
||||
'uploaderUserId': 7,
|
||||
'username': 'operator',
|
||||
'createdAt': '2026-08-08T12:30:00',
|
||||
}],
|
||||
'total': 1,
|
||||
'page': 1,
|
||||
'pageSize': 15,
|
||||
},
|
||||
}
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/dedupe-total-data?page=1&page_size=15&group_id=3'):
|
||||
with self._menu_access(), patch.object(
|
||||
admin_api,
|
||||
'_proxy_permission_java',
|
||||
return_value=(java_response, None, 200),
|
||||
) as proxy:
|
||||
response = admin_api.list_dedupe_total_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.get_json()['items'][0]['group_name'], 'group-a')
|
||||
self.assertEqual(proxy.call_args.kwargs['params'], {
|
||||
'page': 1,
|
||||
'pageSize': 15,
|
||||
'keyword': '',
|
||||
'username': '',
|
||||
'operatorId': 7,
|
||||
'groupId': 3,
|
||||
})
|
||||
|
||||
def test_create_and_update_forward_group(self):
|
||||
java_response = {
|
||||
'message': 'ok',
|
||||
'data': {'id': 9, 'dataValue': 'B012345678', 'groupId': 3, 'groupName': 'group-a'},
|
||||
}
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/dedupe-total-data', method='POST',
|
||||
json={'data_value': ' B012345678 ', 'group_id': 3}):
|
||||
with self._menu_access(), patch.object(
|
||||
admin_api,
|
||||
'_proxy_permission_java',
|
||||
return_value=(java_response, None, 200),
|
||||
) as proxy:
|
||||
response = admin_api.create_dedupe_total_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(proxy.call_args.kwargs['json_data'], {
|
||||
'dataValue': 'B012345678',
|
||||
'groupId': 3,
|
||||
})
|
||||
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/dedupe-total-data/9', method='PUT',
|
||||
json={'data_value': ' C012345678 ', 'group_id': 8}):
|
||||
with self._menu_access(), patch.object(
|
||||
admin_api,
|
||||
'_proxy_permission_java',
|
||||
return_value=(java_response, None, 200),
|
||||
) as proxy:
|
||||
response = admin_api.update_dedupe_total_data.__wrapped__(9)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(proxy.call_args.kwargs['json_data'], {
|
||||
'dataValue': 'C012345678',
|
||||
'groupId': 8,
|
||||
})
|
||||
|
||||
def test_import_forwards_multipart_group(self):
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/dedupe-total-data/import', method='POST',
|
||||
data={'file': (io.BytesIO(b'xlsx'), 'data.xlsx'), 'group_id': '3'}):
|
||||
with self._menu_access(), patch.object(
|
||||
admin_api,
|
||||
'_proxy_permission_java',
|
||||
return_value=({'message': 'started', 'data': {'importId': 'task-1'}}, None, 200),
|
||||
) as proxy:
|
||||
response = admin_api.import_dedupe_total_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.get_json()['import_id'], 'task-1')
|
||||
self.assertEqual(proxy.call_args.kwargs['data'], {'groupId': 3})
|
||||
self.assertEqual(set(proxy.call_args.kwargs['files']), {'file'})
|
||||
|
||||
def test_export_forwards_group_filter_and_internal_token(self):
|
||||
class _Session:
|
||||
def get(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
return _FakeExportResponse()
|
||||
|
||||
session = _Session()
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/dedupe-total-data/export?group_id=3&username=operator'):
|
||||
with self._menu_access(), patch.object(
|
||||
admin_api,
|
||||
'_get_backend_java_session',
|
||||
return_value=session,
|
||||
), patch.object(admin_api, '_resolve_internal_token', return_value='token'):
|
||||
response = admin_api.export_dedupe_total_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(session.kwargs['params'], {
|
||||
'operatorId': 7,
|
||||
'username': 'operator',
|
||||
'groupId': 3,
|
||||
})
|
||||
self.assertEqual(session.kwargs['headers'], {'X-Internal-Token': 'token'})
|
||||
|
||||
def test_import_requires_group(self):
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/dedupe-total-data/import', method='POST',
|
||||
data={'file': (io.BytesIO(b'xlsx'), 'data.xlsx')}):
|
||||
with self._menu_access(), patch.object(admin_api, '_proxy_permission_java') as proxy:
|
||||
response = admin_api.import_dedupe_total_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(response.get_json()['success'])
|
||||
proxy.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,141 @@
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from flask import Flask
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from blueprints import admin_api
|
||||
|
||||
|
||||
class AdminInvalidAsinDataTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = Flask(__name__)
|
||||
|
||||
def test_list_forwards_operator_and_formats_group_fields(self):
|
||||
java_response = {
|
||||
'data': {
|
||||
'items': [{
|
||||
'id': 9,
|
||||
'dataValue': 'B012345678',
|
||||
'brand': 'acme',
|
||||
'groupId': 3,
|
||||
'groupName': 'group-a',
|
||||
'recordSource': 'MANUAL',
|
||||
'createdAt': '2026-08-08T12:30:00',
|
||||
}],
|
||||
'total': 1,
|
||||
'page': 1,
|
||||
'pageSize': 15,
|
||||
},
|
||||
}
|
||||
with self.app.test_request_context('/api/admin/invalid-asin-data?page=1&page_size=15&group_id=3'):
|
||||
with patch.object(
|
||||
admin_api,
|
||||
'_ensure_backend_menu_access',
|
||||
return_value=('admin', {'id': 7}, None),
|
||||
), patch.object(
|
||||
admin_api,
|
||||
'_proxy_permission_java',
|
||||
return_value=(java_response, None, 200),
|
||||
) as proxy:
|
||||
response = admin_api.list_invalid_asin_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.get_json()['items'], [{
|
||||
'id': 9,
|
||||
'data_value': 'B012345678',
|
||||
'brand': 'acme',
|
||||
'group_id': 3,
|
||||
'group_name': 'group-a',
|
||||
'record_source': 'MANUAL',
|
||||
'created_at': '2026-08-08 12:30',
|
||||
}])
|
||||
self.assertEqual(
|
||||
proxy.call_args.kwargs['params'],
|
||||
{
|
||||
'page': 1,
|
||||
'pageSize': 15,
|
||||
'keyword': '',
|
||||
'groupId': 3,
|
||||
},
|
||||
)
|
||||
self.assertEqual(proxy.call_args.kwargs['current_row'], {'id': 7})
|
||||
|
||||
def test_create_forwards_group_with_trusted_current_user(self):
|
||||
java_response = {
|
||||
'message': 'created',
|
||||
'data': {
|
||||
'id': 9,
|
||||
'dataValue': 'B012345678',
|
||||
'brand': 'acme',
|
||||
'groupId': 3,
|
||||
'groupName': 'group-a',
|
||||
'recordSource': 'MANUAL',
|
||||
},
|
||||
}
|
||||
with self.app.test_request_context(
|
||||
'/api/admin/invalid-asin-data',
|
||||
method='POST',
|
||||
json={'data_value': ' B012345678 ', 'brand': 'Acme', 'group_id': 3},
|
||||
):
|
||||
with patch.object(
|
||||
admin_api,
|
||||
'_ensure_backend_menu_access',
|
||||
return_value=('super_admin', {'id': 1}, None),
|
||||
), patch.object(
|
||||
admin_api,
|
||||
'_proxy_permission_java',
|
||||
return_value=(java_response, None, 200),
|
||||
) as proxy:
|
||||
response = admin_api.create_invalid_asin_data.__wrapped__()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('params', proxy.call_args.kwargs)
|
||||
self.assertEqual(proxy.call_args.kwargs['current_row'], {'id': 1})
|
||||
self.assertEqual(proxy.call_args.kwargs['json_data'], {
|
||||
'dataValue': 'B012345678',
|
||||
'brand': 'Acme',
|
||||
'groupId': 3,
|
||||
})
|
||||
|
||||
def test_group_list_marks_first_group_as_locked_for_normal_account(self):
|
||||
groups = [
|
||||
{'id': 3, 'groupName': 'group-a'},
|
||||
{'id': 8, 'groupName': 'group-b'},
|
||||
]
|
||||
with self.app.test_request_context('/api/admin/shop-manage-groups'):
|
||||
with patch.object(
|
||||
admin_api,
|
||||
'_ensure_backend_menu_access',
|
||||
return_value=('admin', {'id': 7}, None),
|
||||
), patch.object(
|
||||
admin_api,
|
||||
'_load_expanded_shop_manage_groups',
|
||||
return_value=(groups, None, 200),
|
||||
):
|
||||
response = admin_api.list_shop_manage_groups.__wrapped__()
|
||||
|
||||
body = response.get_json()
|
||||
self.assertEqual(body['locked_group_id'], 3)
|
||||
self.assertEqual([item['id'] for item in body['items']], [3, 8])
|
||||
|
||||
def test_group_list_does_not_lock_super_admin(self):
|
||||
with self.app.test_request_context('/api/admin/shop-manage-groups'):
|
||||
with patch.object(
|
||||
admin_api,
|
||||
'_ensure_backend_menu_access',
|
||||
return_value=('super_admin', {'id': 1}, None),
|
||||
), patch.object(
|
||||
admin_api,
|
||||
'_load_expanded_shop_manage_groups',
|
||||
return_value=([{'id': 3, 'groupName': 'group-a'}], None, 200),
|
||||
):
|
||||
response = admin_api.list_shop_manage_groups.__wrapped__()
|
||||
|
||||
self.assertIsNone(response.get_json()['locked_group_id'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -32,7 +32,7 @@ class _FakeCursor:
|
||||
self.calls.append((sql, tuple(params)))
|
||||
if 'COUNT(*) AS total' in sql:
|
||||
self.kind = 'count'
|
||||
elif 'MAX(t.created_at)' in sql:
|
||||
elif 'AS latest_created_at' in sql and 'GROUP BY' in sql:
|
||||
self.kind = 'groups'
|
||||
self.group_limit = int(params[-2])
|
||||
self.group_offset = int(params[-1])
|
||||
@@ -59,14 +59,14 @@ class _FakeCursor:
|
||||
limited = []
|
||||
for row in rows:
|
||||
shop_key = row['shop_name'].strip().casefold()
|
||||
if counts.get(shop_key, 0) >= 3:
|
||||
if counts.get(shop_key, 0) >= 1:
|
||||
continue
|
||||
counts[shop_key] = counts.get(shop_key, 0) + 1
|
||||
limited.append(row)
|
||||
return limited
|
||||
if self.current_shop is not None:
|
||||
rows = [row for row in rows if row['shop_name'].strip() == self.current_shop]
|
||||
return rows[:3]
|
||||
return rows[:1]
|
||||
return []
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ class AdminShopDataGroupTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.app = Flask(__name__)
|
||||
self.group_rows = [
|
||||
{'shop_name': 'Shop A', 'latest_created_at': datetime(2026, 8, 5, 12, 0)},
|
||||
{'shop_name': 'Shop A', 'latest_created_at': datetime(2026, 8, 7, 5, 21, 45)},
|
||||
{'shop_name': '', 'latest_created_at': datetime(2026, 8, 4, 12, 0)},
|
||||
]
|
||||
self.result_rows = [
|
||||
@@ -98,7 +98,8 @@ class AdminShopDataGroupTest(unittest.TestCase):
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _result_row(result_id, shop_name, created_at, result_file_url=None):
|
||||
def _result_row(
|
||||
result_id, shop_name, created_at, result_file_url=None, latest_file_updated_at=None):
|
||||
return {
|
||||
'result_id': result_id,
|
||||
'task_id': result_id + 100,
|
||||
@@ -119,6 +120,7 @@ class AdminShopDataGroupTest(unittest.TestCase):
|
||||
'created_at': created_at,
|
||||
'updated_at': created_at,
|
||||
'finished_at': created_at,
|
||||
'latest_file_updated_at': latest_file_updated_at or created_at,
|
||||
'file_job_id': None,
|
||||
'file_status': 'SUCCESS',
|
||||
'username': 'operator',
|
||||
@@ -133,9 +135,25 @@ class AdminShopDataGroupTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(group['shop_name'], 'Shop A')
|
||||
self.assertEqual(group['group_name'], 'Group 1')
|
||||
self.assertEqual([item['result_id'] for item in group['results']], [5, 4, 3])
|
||||
self.assertEqual([item['result_id'] for item in group['results']], [5])
|
||||
self.assertEqual(group['results'][0]['result_file_url'], 'object-5')
|
||||
|
||||
def test_group_item_falls_back_to_daily_file_update_time(self):
|
||||
row = self._result_row(
|
||||
21427,
|
||||
'Shop A',
|
||||
'2026-08-06T15:20:43',
|
||||
latest_file_updated_at=datetime(2026, 8, 7, 5, 21, 45),
|
||||
)
|
||||
|
||||
group = admin_api._shop_data_crawl_group_item(
|
||||
{'shop_name': 'Shop A', 'latest_created_at': None},
|
||||
{'shop a': [row]},
|
||||
{},
|
||||
)
|
||||
|
||||
self.assertEqual(group['latest_created_at'], '2026-08-07 05:21:45')
|
||||
|
||||
def test_list_paginates_groups_and_ignores_removed_user_status_filters(self):
|
||||
cursor = _FakeCursor(
|
||||
self.group_rows,
|
||||
@@ -160,10 +178,11 @@ class AdminShopDataGroupTest(unittest.TestCase):
|
||||
self.assertEqual(payload['total'], 2)
|
||||
self.assertEqual(payload['page'], 1)
|
||||
self.assertEqual(payload['items'][0]['shop_name'], 'Shop A')
|
||||
self.assertEqual(len(payload['items'][0]['results']), 3)
|
||||
self.assertEqual(payload['items'][0]['latest_created_at'], '2026-08-07 05:21:45')
|
||||
self.assertEqual(len(payload['items'][0]['results']), 1)
|
||||
self.assertEqual(
|
||||
[item['result_id'] for item in payload['items'][0]['results']],
|
||||
[5, 4, 3],
|
||||
[5],
|
||||
)
|
||||
self.assertEqual(payload['items'][1]['shop_name'], '未命名')
|
||||
self.assertEqual(len(payload['items'][1]['results']), 1)
|
||||
@@ -173,7 +192,15 @@ class AdminShopDataGroupTest(unittest.TestCase):
|
||||
self.assertNotIn('FAILED', params)
|
||||
self.assertTrue(any('GROUP BY TRIM(COALESCE(r.source_filename, ' in sql for sql, _ in cursor.calls))
|
||||
self.assertTrue(any('ROW_NUMBER() OVER' in sql for sql, _ in cursor.calls))
|
||||
self.assertTrue(any('shop_row_number <= 3' in sql for sql, _ in cursor.calls))
|
||||
self.assertTrue(any('shop_row_number <= 1' in sql for sql, _ in cursor.calls))
|
||||
self.assertTrue(any(
|
||||
'LEFT JOIN biz_shop_data_crawl_daily_file df ON df.latest_result_id = r.id' in sql
|
||||
for sql, _ in cursor.calls
|
||||
))
|
||||
self.assertTrue(any(
|
||||
'MAX(COALESCE(df.last_success_at, df.updated_at, t.finished_at, t.updated_at, t.created_at))'
|
||||
in sql for sql, _ in cursor.calls
|
||||
))
|
||||
self.assertTrue(any('TRIM(COALESCE(sm.shop_name' in sql for sql, _ in cursor.calls))
|
||||
self.assertTrue(any("TRIM(COALESCE(r.result_file_url, '')) <> ''" in sql for sql, _ in cursor.calls))
|
||||
|
||||
|
||||
@@ -1975,6 +1975,12 @@
|
||||
<label>上传 Excel 文件(读取 ASIN 列)</label>
|
||||
<input type="file" id="dedupeTotalDataFile" accept=".xlsx,.xls">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:220px;">
|
||||
<label>分组</label>
|
||||
<select id="dedupeTotalDataImportGroupId">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" id="btnAddDedupeTotalData">上传并导入</button>
|
||||
</div>
|
||||
<p class="msg" id="msgDedupeTotalData"></p>
|
||||
@@ -1992,6 +1998,12 @@
|
||||
<label>上传 Excel 文件(读取 ASIN 列)</label>
|
||||
<input type="file" id="dedupeTotalDataDeleteFile" accept=".xlsx,.xls">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:220px;">
|
||||
<label>分组</label>
|
||||
<select id="dedupeTotalDataDeleteGroupId">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-danger" id="btnDeleteImportDedupeTotalData">上传并删除</button>
|
||||
</div>
|
||||
<p class="msg" id="msgDeleteDedupeTotalData"></p>
|
||||
@@ -2015,6 +2027,12 @@
|
||||
<label>用户名(模糊搜索)</label>
|
||||
<input type="text" id="searchDedupeTotalDataUsername" placeholder="输入用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>分组</label>
|
||||
<select id="dedupeTotalDataGroupFilterId">
|
||||
<option value="">全部分组</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>开始日期</label>
|
||||
<input type="date" id="exportDedupeTotalDataStartDate">
|
||||
@@ -2034,6 +2052,7 @@
|
||||
<th>ID</th>
|
||||
<th>ASIN值</th>
|
||||
<th>用户名</th>
|
||||
<th>分组</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@@ -2057,6 +2076,15 @@
|
||||
<label>品牌</label>
|
||||
<input type="text" id="invalidAsinDataBrand" placeholder="请输入品牌(可选)">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:220px;">
|
||||
<label>分组</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<select id="invalidAsinDataGroupSelect" style="min-width:150px;">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" id="btnManageInvalidAsinDataGroups" type="button">管理分组</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" id="btnAddInvalidAsinData">新增</button>
|
||||
</div>
|
||||
<p class="msg" id="msgInvalidAsinData"></p>
|
||||
@@ -2068,6 +2096,12 @@
|
||||
<label>ASIN / 品牌(模糊搜索)</label>
|
||||
<input type="text" id="searchInvalidAsinData" placeholder="输入关键字">
|
||||
</div>
|
||||
<div class="form-group" style="min-width:180px;">
|
||||
<label>分组</label>
|
||||
<select id="invalidAsinDataFilterGroupId">
|
||||
<option value="">全部分组</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class=" btn" id="btnSearchInvalidAsinData">查询</button>
|
||||
</div>
|
||||
<table>
|
||||
@@ -2076,6 +2110,8 @@
|
||||
<th>ID</th>
|
||||
<th>ASIN</th>
|
||||
<th>品牌</th>
|
||||
<th>分组</th>
|
||||
<th>来源</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
@@ -2662,6 +2698,12 @@
|
||||
<label>ASIN值</label>
|
||||
<input type="text" id="editDedupeTotalDataValue" placeholder="请输入总数据值">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>分组</label>
|
||||
<select id="editDedupeTotalDataGroupId">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class=" msg" id="msgEditDedupeTotalData"></p>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;">
|
||||
<button class="btn" id="btnSaveDedupeTotalData">保存</button>
|
||||
@@ -2675,6 +2717,7 @@
|
||||
<div class="modal">
|
||||
<h3>编辑不符合ASIN数据</h3>
|
||||
<input type="hidden" id="editInvalidAsinDataId">
|
||||
<input type="hidden" id="editInvalidAsinDataRecordSource">
|
||||
<div class="form-group">
|
||||
<label>ASIN</label>
|
||||
<input type="text" id="editInvalidAsinDataValue" placeholder="请输入 ASIN">
|
||||
@@ -2683,6 +2726,19 @@
|
||||
<label>品牌</label>
|
||||
<input type="text" id="editInvalidAsinDataBrand" placeholder="请输入品牌(可选)">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>来源</label>
|
||||
<input type="text" id="editInvalidAsinDataSourceLabel" readonly>
|
||||
</div>
|
||||
<div class="form-group" id="editInvalidAsinDataGroupForm">
|
||||
<label>分组</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<select id="editInvalidAsinDataGroupSelect" style="min-width:200px;">
|
||||
<option value="">请选择分组</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary" id="btnManageInvalidAsinDataGroupsFromEdit" type="button">管理分组</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class=" msg" id="msgEditInvalidAsinData"></p>
|
||||
<div style="margin-top:16px;display:flex;gap:8px;">
|
||||
<button class="btn" id="btnSaveInvalidAsinData">保存</button>
|
||||
@@ -3052,7 +3108,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/admin.js?v=shop-data-task-admin-1"></script>
|
||||
<script src="/static/admin.js?v=shop-data-task-admin-2"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user