新需求更新 同步更新
This commit is contained in:
@@ -23,6 +23,7 @@ from flask import (
|
||||
g,
|
||||
Response,
|
||||
send_file,
|
||||
stream_with_context,
|
||||
has_request_context,
|
||||
)
|
||||
|
||||
@@ -44,6 +45,7 @@ admin_api = Blueprint('admin_api', __name__, url_prefix='/api/admin')
|
||||
_backend_java_session_local = threading.local()
|
||||
_internal_token_lock = threading.Lock()
|
||||
IMAGE_VIDEO_DATA_PERMISSION_KEY = 'admin_image_video_task_data'
|
||||
SHOP_DATA_CRAWL_DATA_PERMISSION_KEY = 'admin_shop_data_crawl_task_data'
|
||||
|
||||
ADMIN_MENU_ACCESS_CONFIG = {
|
||||
'dedupe-total-data': {
|
||||
@@ -81,6 +83,11 @@ ADMIN_MENU_ACCESS_CONFIG = {
|
||||
'route_path': 'image-video-tasks',
|
||||
'error': '无权访问视频任务管理模块',
|
||||
},
|
||||
'shop-data-crawl-tasks': {
|
||||
'column_key': 'admin_shop_data_crawl_tasks',
|
||||
'route_path': 'shop-data-crawl-tasks',
|
||||
'error': '无权访问店铺数据任务管理模块',
|
||||
},
|
||||
}
|
||||
|
||||
ADMIN_MENU_ACCESS_CONFIG.update({
|
||||
@@ -147,6 +154,29 @@ def _backend_java_forward_headers():
|
||||
return headers
|
||||
|
||||
|
||||
def _backend_java_internal_headers():
|
||||
"""Headers for Java routes that are callable only from the Flask admin service."""
|
||||
internal_token = _resolve_internal_token()
|
||||
if not internal_token:
|
||||
raise ValueError('内部凭据服务未配置')
|
||||
headers = _backend_java_forward_headers()
|
||||
headers['X-Internal-Token'] = internal_token
|
||||
return headers
|
||||
|
||||
|
||||
def _backend_java_internal_request():
|
||||
headers = _backend_java_internal_headers()
|
||||
_, current_row = get_current_admin_role()
|
||||
operator_id = _get_current_admin_id(current_row)
|
||||
try:
|
||||
operator_id = int(operator_id)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValueError('当前管理员身份无效') from exc
|
||||
if operator_id <= 0:
|
||||
raise ValueError('当前管理员身份无效')
|
||||
return headers, {'operatorId': operator_id}
|
||||
|
||||
|
||||
def _proxy_backend_java(
|
||||
method,
|
||||
path,
|
||||
@@ -751,6 +781,27 @@ def _ensure_image_video_data_access():
|
||||
return role, current_row, (jsonify({'success': False, 'error': '无权查看视频任务数据'}), 403)
|
||||
|
||||
|
||||
def _ensure_shop_data_crawl_data_access():
|
||||
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)
|
||||
try:
|
||||
_, key_set, route_set = _effective_permission_sets(
|
||||
_get_current_admin_id(current_row),
|
||||
menu_type=None,
|
||||
current_row=current_row,
|
||||
role=role,
|
||||
)
|
||||
except _PermissionProxyError as exc:
|
||||
return role, current_row, (exc.response, exc.status)
|
||||
if (SHOP_DATA_CRAWL_DATA_PERMISSION_KEY in key_set
|
||||
or 'shop-data-crawl-task-data' in route_set):
|
||||
return role, current_row, None
|
||||
return role, current_row, (jsonify({'success': False, 'error': '无权查看店铺数据任务'}), 403)
|
||||
|
||||
|
||||
def _ensure_product_category_access():
|
||||
role, current_row, items, denied = _load_current_backend_menu_items()
|
||||
if denied:
|
||||
@@ -1283,6 +1334,458 @@ def _parse_admin_datetime_arg(name):
|
||||
raise ValueError(f'{name} 时间格式无效') from exc
|
||||
|
||||
|
||||
_SHOP_DATA_CRAWL_ADMIN_COLUMNS = """
|
||||
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,
|
||||
r.success AS result_success, r.error_message AS result_error,
|
||||
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,
|
||||
u.username,
|
||||
(SELECT j.id FROM biz_task_file_job j
|
||||
WHERE j.module_type = 'SHOP_DATA_CRAWL' AND j.result_id = r.id
|
||||
AND j.job_type = 'ASSEMBLE_RESULT'
|
||||
ORDER BY j.id DESC LIMIT 1) AS file_job_id,
|
||||
(SELECT j.status FROM biz_task_file_job j
|
||||
WHERE j.module_type = 'SHOP_DATA_CRAWL' AND j.result_id = r.id
|
||||
AND j.job_type = 'ASSEMBLE_RESULT'
|
||||
ORDER BY j.id DESC LIMIT 1) AS file_status,
|
||||
(SELECT j.error_message FROM biz_task_file_job j
|
||||
WHERE j.module_type = 'SHOP_DATA_CRAWL' AND j.result_id = r.id
|
||||
AND j.job_type = 'ASSEMBLE_RESULT'
|
||||
ORDER BY j.id DESC LIMIT 1) AS file_error
|
||||
"""
|
||||
|
||||
|
||||
def _shop_data_crawl_country_codes(request_json):
|
||||
payload = _parse_json_value(request_json, {})
|
||||
if not isinstance(payload, dict):
|
||||
return []
|
||||
raw = payload.get('countryCodes')
|
||||
if raw is None:
|
||||
raw = payload.get('country_codes')
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
return [str(value).strip().upper() for value in raw if str(value or '').strip()]
|
||||
|
||||
|
||||
def _shop_data_crawl_group_names(cursor, rows):
|
||||
shop_names = sorted({
|
||||
_shop_data_crawl_shop_key(row.get('shop_name'))
|
||||
for row in rows
|
||||
if _shop_data_crawl_shop_key(row.get('shop_name'))
|
||||
})
|
||||
if not shop_names:
|
||||
return {}
|
||||
placeholders = ','.join(['%s'] * len(shop_names))
|
||||
cursor.execute(
|
||||
"SELECT TRIM(sm.shop_name) AS shop_name, "
|
||||
"GROUP_CONCAT(DISTINCT COALESCE(NULLIF(g.group_name, ''), NULLIF(sm.group_name, '')) "
|
||||
"ORDER BY sm.id SEPARATOR '、') AS group_name "
|
||||
"FROM biz_shop_manage sm "
|
||||
"LEFT JOIN biz_shop_manage_group g ON g.id = sm.group_id "
|
||||
f"WHERE TRIM(sm.shop_name) IN ({placeholders}) GROUP BY TRIM(sm.shop_name)",
|
||||
tuple(shop_names),
|
||||
)
|
||||
return {
|
||||
_shop_data_crawl_shop_key(row.get('shop_name')): row.get('group_name') or ''
|
||||
for row in cursor.fetchall()
|
||||
}
|
||||
|
||||
|
||||
def _shop_data_crawl_shop_key(value):
|
||||
"""Normalize a shop name for grouping while preserving the display value."""
|
||||
return str(value or '').strip().casefold()
|
||||
|
||||
|
||||
def _shop_data_crawl_group_name(group_names, shop_name):
|
||||
"""Resolve a group label from either normalized or legacy exact-key maps."""
|
||||
if not group_names:
|
||||
return ''
|
||||
normalized = _shop_data_crawl_shop_key(shop_name)
|
||||
return group_names.get(normalized, group_names.get(str(shop_name or '').strip(), '')) or ''
|
||||
|
||||
|
||||
def _shop_data_crawl_admin_item(row, group_names=None):
|
||||
result_success = row.get('result_success')
|
||||
file_ready = bool((row.get('result_file_url') or '').strip())
|
||||
if result_success is None or int(result_success) < 0:
|
||||
success = None
|
||||
else:
|
||||
success = bool(int(result_success))
|
||||
group_names = group_names or {}
|
||||
shop_name = row.get('shop_name') or ''
|
||||
file_status = row.get('file_status') or ('SUCCESS' if file_ready else '')
|
||||
return {
|
||||
'task_id': row.get('task_id'),
|
||||
'task_no': row.get('task_no') or '',
|
||||
'result_id': row.get('result_id'),
|
||||
'user_id': row.get('user_id'),
|
||||
'username': row.get('username') or '',
|
||||
'shop_name': shop_name,
|
||||
'shop_id': row.get('shop_id') or '',
|
||||
'group_name': _shop_data_crawl_group_name(group_names, shop_name),
|
||||
'status': row.get('task_status') or '',
|
||||
'success': success,
|
||||
'error': row.get('result_error') or row.get('task_error') or row.get('file_error') or '',
|
||||
'country_codes': _shop_data_crawl_country_codes(row.get('request_json')),
|
||||
'output_filename': row.get('result_filename') or '',
|
||||
'result_file_url': row.get('result_file_url') or '',
|
||||
'file_ready': file_ready,
|
||||
'file_job_id': row.get('file_job_id'),
|
||||
'file_status': file_status,
|
||||
'file_error': row.get('file_error') or '',
|
||||
'file_size': int(row.get('result_file_size') or 0),
|
||||
'row_count': int(row.get('row_count') or 0),
|
||||
'created_at': _format_admin_datetime(row.get('created_at') or row.get('result_created_at')),
|
||||
'updated_at': _format_admin_datetime(row.get('updated_at')),
|
||||
'finished_at': _format_admin_datetime(row.get('finished_at')),
|
||||
}
|
||||
|
||||
|
||||
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."""
|
||||
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]
|
||||
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 result_items:
|
||||
latest_created_at = result_items[0].get('created_at')
|
||||
return {
|
||||
'shop_name': display_shop_name,
|
||||
'shop_id': result_items[0].get('shop_id', '') if result_items else '',
|
||||
'group_name': _shop_data_crawl_group_name(group_names, raw_shop_name),
|
||||
'latest_created_at': _format_admin_datetime(latest_created_at),
|
||||
'results': result_items,
|
||||
}
|
||||
|
||||
|
||||
@admin_api.route('/shop-data-crawl-task-permissions', methods=['GET', 'PUT'])
|
||||
@login_required
|
||||
def manage_shop_data_crawl_task_permissions():
|
||||
json_data = None
|
||||
if request.method == 'PUT':
|
||||
data = request.get_json(silent=True) or {}
|
||||
raw_user_ids = data.get('user_ids') if 'user_ids' in data else data.get('userIds')
|
||||
json_data = {'userIds': raw_user_ids}
|
||||
result, error_response, status = _proxy_permission_java(
|
||||
request.method,
|
||||
'/api/admin/shop-data-crawl-task-permissions',
|
||||
json_data=json_data,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
if request.method == 'GET':
|
||||
return jsonify({'success': True, 'items': _permission_response_items(result)})
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'granted_count': result.get('data'),
|
||||
'msg': result.get('message') or '店铺数据任务权限已更新',
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/shop-data-crawl-tasks')
|
||||
@login_required
|
||||
def list_shop_data_crawl_tasks():
|
||||
_, _, denied = _ensure_backend_menu_access('shop-data-crawl-tasks')
|
||||
if not denied:
|
||||
_, _, denied = _ensure_shop_data_crawl_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
try:
|
||||
page = max(1, int(request.args.get('page', 1)))
|
||||
page_size = min(100, max(10, int(request.args.get('page_size', 20))))
|
||||
shop_name = (request.args.get('shop_name') or request.args.get('shop') or '').strip()
|
||||
group_name = (request.args.get('group_name') or request.args.get('group') or '').strip()
|
||||
created_from = _parse_admin_datetime_arg('created_from')
|
||||
created_to = _parse_admin_datetime_arg('created_to')
|
||||
|
||||
conditions = [
|
||||
"r.module_type = 'SHOP_DATA_CRAWL'",
|
||||
"t.module_type = 'SHOP_DATA_CRAWL'",
|
||||
"TRIM(COALESCE(r.result_file_url, '')) <> ''",
|
||||
]
|
||||
params = []
|
||||
if shop_name:
|
||||
conditions.append('r.source_filename LIKE %s')
|
||||
params.append('%' + shop_name + '%')
|
||||
if group_name:
|
||||
conditions.append(
|
||||
'EXISTS (SELECT 1 FROM biz_shop_manage sm '
|
||||
'LEFT JOIN biz_shop_manage_group g ON g.id = sm.group_id '
|
||||
'WHERE TRIM(COALESCE(sm.shop_name, \'\')) = '
|
||||
'TRIM(COALESCE(r.source_filename, \'\')) '
|
||||
"AND COALESCE(NULLIF(g.group_name, ''), NULLIF(sm.group_name, '')) LIKE %s)"
|
||||
)
|
||||
params.append('%' + group_name + '%')
|
||||
if created_from:
|
||||
conditions.append('t.created_at >= %s')
|
||||
params.append(created_from)
|
||||
if created_to:
|
||||
conditions.append('t.created_at <= %s')
|
||||
params.append(created_to)
|
||||
where_sql = ' AND '.join(conditions)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
conn = get_db()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
shop_key_sql = "TRIM(COALESCE(r.source_filename, ''))"
|
||||
grouped_from_sql = (
|
||||
' FROM biz_file_result r '
|
||||
'JOIN biz_file_task t ON t.id = r.task_id '
|
||||
'LEFT JOIN users u ON u.id = r.user_id '
|
||||
'WHERE ' + where_sql
|
||||
)
|
||||
cur.execute(
|
||||
'SELECT COUNT(*) AS total FROM ('
|
||||
'SELECT ' + shop_key_sql + ' AS shop_key' + grouped_from_sql +
|
||||
' GROUP BY ' + shop_key_sql +
|
||||
') shop_groups',
|
||||
tuple(params),
|
||||
)
|
||||
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'
|
||||
+ grouped_from_sql +
|
||||
' GROUP BY ' + shop_key_sql +
|
||||
' ORDER BY latest_created_at DESC, shop_name ASC LIMIT %s OFFSET %s',
|
||||
tuple(params + [page_size, offset]),
|
||||
)
|
||||
group_rows = cur.fetchall()
|
||||
group_names = _shop_data_crawl_group_names(cur, group_rows)
|
||||
|
||||
result_rows_by_shop = {}
|
||||
selected_shop_names = [row.get('shop_name') for row in group_rows]
|
||||
if selected_shop_names:
|
||||
placeholders = ','.join(['%s'] * len(selected_shop_names))
|
||||
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 '
|
||||
' FROM biz_file_result r '
|
||||
'JOIN biz_file_task t ON t.id = r.task_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',
|
||||
tuple(params + selected_shop_names),
|
||||
)
|
||||
for row in cur.fetchall():
|
||||
shop_key = _shop_data_crawl_shop_key(row.get('shop_name'))
|
||||
result_rows_by_shop.setdefault(shop_key, []).append(row)
|
||||
finally:
|
||||
conn.close()
|
||||
payload = {
|
||||
'items': [
|
||||
_shop_data_crawl_group_item(group, result_rows_by_shop, group_names)
|
||||
for group in group_rows
|
||||
],
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
}
|
||||
# Keep the existing admin response shape while exposing the grouped
|
||||
# payload for clients that use the newer data envelope.
|
||||
return jsonify({'success': True, **payload, 'data': payload})
|
||||
except ValueError as exc:
|
||||
return jsonify({'success': False, 'error': str(exc)}), 400
|
||||
except Exception as exc:
|
||||
return jsonify({'success': False, 'error': str(exc)}), 500
|
||||
|
||||
|
||||
def _load_shop_data_crawl_download_rows(result_ids):
|
||||
normalized = sorted({int(result_id) for result_id in result_ids if int(result_id) > 0})
|
||||
if not normalized:
|
||||
return {}
|
||||
placeholders = ','.join(['%s'] * len(normalized))
|
||||
conn = get_db()
|
||||
try:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
'SELECT r.id, r.task_id, r.user_id, r.result_filename, r.source_filename, '
|
||||
'r.result_file_url, t.status AS task_status '
|
||||
'FROM biz_file_result r JOIN biz_file_task t ON t.id = r.task_id '
|
||||
'WHERE r.module_type = %s AND t.module_type = %s '
|
||||
f'AND r.id IN ({placeholders})',
|
||||
tuple(['SHOP_DATA_CRAWL', 'SHOP_DATA_CRAWL'] + normalized),
|
||||
)
|
||||
return {int(row['id']): row for row in cur.fetchall()}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _open_shop_data_crawl_download(row):
|
||||
url = f"{backend_java_base_url}/api/admin/shop-data-crawl/results/{int(row['id'])}/download"
|
||||
headers, params = _backend_java_internal_request()
|
||||
return _get_backend_java_session().get(
|
||||
url,
|
||||
params=params,
|
||||
headers=headers,
|
||||
stream=True,
|
||||
timeout=(10, 180),
|
||||
)
|
||||
|
||||
|
||||
@admin_api.route('/shop-data-crawl-tasks/<int:result_id>/download')
|
||||
@login_required
|
||||
def download_shop_data_crawl_task(result_id):
|
||||
_, _, denied = _ensure_backend_menu_access('shop-data-crawl-tasks')
|
||||
if not denied:
|
||||
_, _, denied = _ensure_shop_data_crawl_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
rows = _load_shop_data_crawl_download_rows([result_id])
|
||||
row = rows.get(result_id)
|
||||
if not row or not (row.get('result_file_url') or '').strip():
|
||||
return jsonify({'success': False, 'error': '结果文件不存在或尚未生成'}), 404
|
||||
try:
|
||||
remote = _open_shop_data_crawl_download(row)
|
||||
except ValueError as exc:
|
||||
return jsonify({'success': False, 'error': str(exc)}), 503
|
||||
except requests.RequestException as exc:
|
||||
return jsonify({'success': False, 'error': f'结果文件下载失败: {exc}'}), 502
|
||||
if remote.status_code != 200:
|
||||
message = remote.text[:500] if remote.content else ''
|
||||
remote.close()
|
||||
return jsonify({'success': False, 'error': message or '结果文件下载失败'}), remote.status_code
|
||||
|
||||
def generate():
|
||||
try:
|
||||
for chunk in remote.iter_content(chunk_size=1024 * 1024):
|
||||
if chunk:
|
||||
yield chunk
|
||||
finally:
|
||||
remote.close()
|
||||
|
||||
response = Response(
|
||||
stream_with_context(generate()),
|
||||
content_type=remote.headers.get('Content-Type') or
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
)
|
||||
disposition = remote.headers.get('Content-Disposition')
|
||||
if disposition:
|
||||
response.headers['Content-Disposition'] = disposition
|
||||
else:
|
||||
filename = row.get('result_filename') or f"{row.get('source_filename') or result_id}.xlsx"
|
||||
response.headers['Content-Disposition'] = "attachment; filename*=UTF-8''" + quote(filename)
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
return response
|
||||
|
||||
|
||||
@admin_api.route('/shop-data-crawl-tasks/<int:result_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_shop_data_crawl_task(result_id):
|
||||
_, _, denied = _ensure_backend_menu_access('shop-data-crawl-tasks')
|
||||
if not denied:
|
||||
_, _, denied = _ensure_shop_data_crawl_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
rows = _load_shop_data_crawl_download_rows([result_id])
|
||||
row = rows.get(result_id)
|
||||
if not row:
|
||||
return jsonify({'success': False, 'error': '店铺数据任务不存在'}), 404
|
||||
if (row.get('task_status') or '').upper() not in {'SUCCESS', 'FAILED', 'CANCELLED'}:
|
||||
return jsonify({'success': False, 'error': '任务仍在处理中,不能删除'}), 409
|
||||
try:
|
||||
headers, params = _backend_java_internal_request()
|
||||
except ValueError as exc:
|
||||
return jsonify({'success': False, 'error': str(exc)}), 503
|
||||
result, error_response, status = _proxy_backend_java(
|
||||
'DELETE',
|
||||
f'/api/admin/shop-data-crawl/history/{result_id}',
|
||||
params=params,
|
||||
headers=headers,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
return jsonify({'success': True, 'msg': result.get('message') or '删除成功'})
|
||||
|
||||
|
||||
@admin_api.route('/shop-data-crawl-tasks/download-zip', methods=['POST'])
|
||||
@login_required
|
||||
def download_shop_data_crawl_tasks_zip():
|
||||
_, _, denied = _ensure_backend_menu_access('shop-data-crawl-tasks')
|
||||
if not denied:
|
||||
_, _, denied = _ensure_shop_data_crawl_data_access()
|
||||
if denied:
|
||||
return denied
|
||||
data = request.get_json(silent=True) or {}
|
||||
raw_ids = data.get('result_ids') if 'result_ids' in data else data.get('resultIds')
|
||||
if not isinstance(raw_ids, list) or not raw_ids:
|
||||
return jsonify({'success': False, 'error': '请至少选择一个结果文件'}), 400
|
||||
if len(raw_ids) > 100:
|
||||
return jsonify({'success': False, 'error': '单次最多打包 100 个结果文件'}), 400
|
||||
try:
|
||||
result_ids = []
|
||||
for raw_id in raw_ids:
|
||||
result_id = int(raw_id)
|
||||
if result_id <= 0:
|
||||
raise ValueError
|
||||
if result_id not in result_ids:
|
||||
result_ids.append(result_id)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'success': False, 'error': '结果文件参数无效'}), 400
|
||||
|
||||
rows = _load_shop_data_crawl_download_rows(result_ids)
|
||||
archive = tempfile.SpooledTemporaryFile(max_size=64 * 1024 * 1024, mode='w+b')
|
||||
errors = []
|
||||
file_count = 0
|
||||
used_names = set()
|
||||
try:
|
||||
with zipfile.ZipFile(archive, mode='w', compression=zipfile.ZIP_STORED, allowZip64=True) as output_zip:
|
||||
for result_id in result_ids:
|
||||
row = rows.get(result_id)
|
||||
if not row or not (row.get('result_file_url') or '').strip():
|
||||
errors.append(f'result-{result_id}: 结果文件不存在或尚未生成')
|
||||
continue
|
||||
filename = row.get('result_filename') or f"{row.get('source_filename') or result_id}.xlsx"
|
||||
filename = re.sub(r'[\\/:*?"<>|]+', '_', filename).strip() or f'result-{result_id}.xlsx'
|
||||
if filename in used_names:
|
||||
stem, extension = os.path.splitext(filename)
|
||||
filename = f'{stem}-{result_id}{extension or ".xlsx"}'
|
||||
used_names.add(filename)
|
||||
remote = None
|
||||
try:
|
||||
remote = _open_shop_data_crawl_download(row)
|
||||
remote.raise_for_status()
|
||||
with output_zip.open(filename, mode='w', force_zip64=True) as target:
|
||||
for chunk in remote.iter_content(chunk_size=1024 * 1024):
|
||||
if chunk:
|
||||
target.write(chunk)
|
||||
file_count += 1
|
||||
except (requests.RequestException, ValueError) as exc:
|
||||
errors.append(f'{filename}: 下载失败 ({exc})')
|
||||
finally:
|
||||
if remote is not None:
|
||||
remote.close()
|
||||
if errors:
|
||||
output_zip.writestr('download-errors.txt', '\n'.join(errors).encode('utf-8'))
|
||||
archive.seek(0)
|
||||
response = send_file(
|
||||
archive,
|
||||
mimetype='application/zip',
|
||||
as_attachment=True,
|
||||
download_name=f"shop-data-tasks-{datetime.now().strftime('%Y%m%d-%H%M%S')}.zip",
|
||||
max_age=0,
|
||||
)
|
||||
response.headers['X-Archive-File-Count'] = str(file_count)
|
||||
response.headers['X-Archive-Error-Count'] = str(len(errors))
|
||||
response.call_on_close(archive.close)
|
||||
return response
|
||||
except Exception:
|
||||
archive.close()
|
||||
raise
|
||||
|
||||
|
||||
@admin_api.route('/image-video-task-permissions', methods=['GET', 'PUT'])
|
||||
@login_required
|
||||
def manage_image_video_task_permissions():
|
||||
@@ -1525,7 +2028,10 @@ def list_columns():
|
||||
return error_response, status
|
||||
items = [
|
||||
item for item in items
|
||||
if (item.get('column_key') or '').strip() != IMAGE_VIDEO_DATA_PERMISSION_KEY
|
||||
if (item.get('column_key') or '').strip() not in {
|
||||
IMAGE_VIDEO_DATA_PERMISSION_KEY,
|
||||
SHOP_DATA_CRAWL_DATA_PERMISSION_KEY,
|
||||
}
|
||||
]
|
||||
# Keep the legacy `items` field; some Java-aware callers use `data`/`columns`.
|
||||
return jsonify({'success': True, 'items': items, 'columns': items, 'data': items})
|
||||
@@ -2250,6 +2756,9 @@ def list_shop_keys():
|
||||
'remark_name': item.get('remarkName') or '',
|
||||
'ziniao_account_name': item.get('ziniaoAccountName') or '',
|
||||
'ziniao_token': item.get('ziniaoToken') or '',
|
||||
'ip_whitelist_status': item.get('ipWhitelistStatus') or 'UNKNOWN',
|
||||
'ip_whitelist_checked_at': (item.get('ipWhitelistCheckedAt') or '').replace('T', ' ')[:19],
|
||||
'ip_whitelist_message': item.get('ipWhitelistMessage') or '',
|
||||
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
|
||||
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
|
||||
}
|
||||
@@ -2362,16 +2871,23 @@ 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()
|
||||
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 = {
|
||||
'page': page,
|
||||
'pageSize': page_size,
|
||||
'keyword': keyword,
|
||||
'username': username,
|
||||
'operatorId': current_row.get('id'),
|
||||
}
|
||||
if start_date:
|
||||
params['startDate'] = start_date
|
||||
if end_date:
|
||||
params['endDate'] = end_date
|
||||
data, error_response, status = _proxy_backend_java(
|
||||
'GET',
|
||||
'/api/admin/dedupe-total-data',
|
||||
params={
|
||||
'page': page,
|
||||
'pageSize': page_size,
|
||||
'keyword': keyword,
|
||||
'username': username,
|
||||
'operatorId': current_row.get('id'),
|
||||
},
|
||||
params=params,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
@@ -2830,6 +3346,61 @@ def list_shop_manages():
|
||||
})
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage/<int:item_id>/credential')
|
||||
@login_required
|
||||
def get_shop_manage_credential(item_id):
|
||||
role, current_row, denied = _ensure_backend_menu_access('shop-manage')
|
||||
if denied:
|
||||
return denied
|
||||
|
||||
shop_name = (request.args.get('shop_name') or '').strip()
|
||||
if not shop_name:
|
||||
return jsonify({'success': False, 'error': '店铺名不能为空'}), 400
|
||||
|
||||
access_params = {
|
||||
'page': 1,
|
||||
'pageSize': 100,
|
||||
'shopName': shop_name,
|
||||
'superAdmin': 'true' if role == 'super_admin' else 'false',
|
||||
}
|
||||
if current_row and current_row.get('id'):
|
||||
access_params['operatorId'] = current_row.get('id')
|
||||
access_result, error_response, status = _proxy_backend_java(
|
||||
'GET',
|
||||
'/api/admin/shop-manages',
|
||||
params=access_params,
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
|
||||
accessible_items = ((access_result.get('data') or {}).get('items') or [])
|
||||
accessible_item = next((
|
||||
item for item in accessible_items
|
||||
if str(item.get('id')) == str(item_id) and (item.get('shopName') or '') == shop_name
|
||||
), None)
|
||||
if accessible_item is None:
|
||||
return jsonify({'success': False, 'error': '店铺不存在或无权访问'}), 404
|
||||
|
||||
internal_token = _resolve_internal_token()
|
||||
if not internal_token:
|
||||
return jsonify({'success': False, 'error': '内部凭据服务未配置'}), 503
|
||||
credential_result, error_response, status = _proxy_backend_java(
|
||||
'GET',
|
||||
'/api/admin/shop-manages/credential',
|
||||
params={'shopName': shop_name},
|
||||
headers={'X-Internal-Token': internal_token},
|
||||
)
|
||||
if error_response is not None:
|
||||
return error_response, status
|
||||
|
||||
credential = credential_result.get('data') or {}
|
||||
if str(credential.get('id')) != str(item_id):
|
||||
return jsonify({'success': False, 'error': '店铺凭据不匹配'}), 409
|
||||
response = jsonify({'success': True, 'password': credential.get('password') or ''})
|
||||
response.headers['Cache-Control'] = 'no-store'
|
||||
return response
|
||||
|
||||
|
||||
@admin_api.route('/shop-manage', methods=['POST'])
|
||||
@login_required
|
||||
def create_shop_manage():
|
||||
|
||||
Reference in New Issue
Block a user