From a5da06537e68b8aabc1caefa7a1425b3e419442d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=93=AD=E5=9D=A4?= <10372463+ming-kun@user.noreply.gitee.com> Date: Thu, 26 Mar 2026 16:39:39 +0800 Subject: [PATCH] modified: blueprints/__pycache__/__init__.cpython-39.pyc modified: blueprints/__pycache__/admin.cpython-39.pyc modified: blueprints/__pycache__/auth.cpython-39.pyc modified: blueprints/__pycache__/brand.cpython-311.pyc modified: blueprints/__pycache__/brand.cpython-39.pyc modified: blueprints/brand.py new file: brand_spider/__pycache__/main.cpython-311.pyc modified: brand_spider/__pycache__/main.cpython-39.pyc new file: brand_spider/__pycache__/web_dec.cpython-311.pyc modified: brand_spider/main.py modified: config.py modified: web_source/admin.html modified: web_source/brand.html modified: web_source/home.html modified: web_source/index.html modified: web_source/login.html new file: web_source/templates_backup/admin.html new file: web_source/templates_backup/brand.html new file: web_source/templates_backup/home.html new file: web_source/templates_backup/index.html new file: web_source/templates_backup/login.html --- .../__pycache__/__init__.cpython-39.pyc | Bin 144 -> 144 bytes .../__pycache__/admin.cpython-39.pyc | Bin 11271 -> 11271 bytes .../__pycache__/auth.cpython-39.pyc | Bin 3320 -> 3320 bytes .../__pycache__/brand.cpython-311.pyc | Bin 24298 -> 45865 bytes .../__pycache__/brand.cpython-39.pyc | Bin 25387 -> 20764 bytes app/blueprints/brand.py | 820 +-- .../__pycache__/main.cpython-311.pyc | Bin 0 -> 23102 bytes .../__pycache__/main.cpython-39.pyc | Bin 15429 -> 13405 bytes .../__pycache__/web_dec.cpython-311.pyc | Bin 0 -> 6427 bytes app/brand_spider/main.py | 284 +- app/config.py | 11 +- app/web_source/admin.html | 1078 +-- app/web_source/brand.html | 151 +- app/web_source/home.html | 5 +- app/web_source/index.html | 4 +- app/web_source/login.html | 4 +- app/web_source/templates_backup/admin.html | 539 ++ app/web_source/templates_backup/brand.html | 918 +++ app/web_source/templates_backup/home.html | 352 + app/web_source/templates_backup/index.html | 5833 +++++++++++++++++ app/web_source/templates_backup/login.html | 164 + 21 files changed, 8897 insertions(+), 1266 deletions(-) create mode 100644 app/brand_spider/__pycache__/main.cpython-311.pyc create mode 100644 app/brand_spider/__pycache__/web_dec.cpython-311.pyc create mode 100644 app/web_source/templates_backup/admin.html create mode 100644 app/web_source/templates_backup/brand.html create mode 100644 app/web_source/templates_backup/home.html create mode 100644 app/web_source/templates_backup/index.html create mode 100644 app/web_source/templates_backup/login.html diff --git a/app/blueprints/__pycache__/__init__.cpython-39.pyc b/app/blueprints/__pycache__/__init__.cpython-39.pyc index aa87d2c80ebee18baab780866150e2f789e76c25..ac962fa4aa39f51be1b14b47bedf7ea75beeeb22 100644 GIT binary patch delta 19 ZcmbQhIDwHnk(ZZ?0SKI49-PSC3IHdD1i%0Q delta 19 ZcmbQhIDwHnk(ZZ?0SNX6?VZTo3IHc%1fT!_ diff --git a/app/blueprints/__pycache__/admin.cpython-39.pyc b/app/blueprints/__pycache__/admin.cpython-39.pyc index 9f9c3a9207e8ad7e727c40c17d859bb3efa43b70..3e017f55a09402f5a7b2ad3f6a88b04b5aca6f9c 100644 GIT binary patch delta 27 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 W)+W|eyuBCUxw;ysI2M&nX8%844+)9@ literal 24298 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

=GUKf5 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 Bf-nF8 diff --git a/app/blueprints/__pycache__/brand.cpython-39.pyc b/app/blueprints/__pycache__/brand.cpython-39.pyc index 97bcda0c8ca35e81b8fd0bdb900b912789556ebb..d06ce7d60e811c7e3c46488b5fef8c5070524cad 100644 GIT binary patch literal 20764 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 literal 25387 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 diff --git a/app/blueprints/brand.py b/app/blueprints/brand.py index e611f60..3d179e4 100644 --- a/app/blueprints/brand.py +++ b/app/blueprints/brand.py @@ -4,12 +4,10 @@ import os import json import threading -import datetime import traceback import zipfile import io import time -import tempfile import requests from queue import Queue, Empty from concurrent.futures import ThreadPoolExecutor, as_completed @@ -18,7 +16,7 @@ from urllib.parse import urlparse from flask import Blueprint, request, jsonify, session, send_file, redirect, Response from app_common import get_db, login_required, BASE_DIR -from config import bucket_path +from config import bucket_path,JAVA_API_BASE from brand_spider.main import single_file_handle, TaskCancelledError brand_bp = Blueprint('brand', __name__) @@ -39,6 +37,64 @@ _task_event_lock = threading.Lock() # } _task_line_progress = {} _task_line_progress_lock = threading.Lock() +_cancelled_brand_tasks = set() +_cancelled_brand_tasks_lock = threading.Lock() + + +def _mark_task_cancelled(task_id): + with _cancelled_brand_tasks_lock: + _cancelled_brand_tasks.add(task_id) + + +def _is_task_cancelled(task_id): + with _cancelled_brand_tasks_lock: + return task_id in _cancelled_brand_tasks + + +def _get_uid_from_request_headers(): + """ + 从请求头读取 uid。 + 前端会在 headers 里携带 uid,用于确定本次请求的归属用户。 + """ + uid = request.headers.get('uid') + if uid is None: + return None + uid = str(uid).strip() + if not uid: + return None + try: + return int(uid) + except Exception: + return None + + +def _resolve_user_id(): + """ + 优先使用请求头 uid;没有请求头 uid 时回退到 session['user_id']。 + 若二者同时存在但不一致,则视为无效请求并返回 None。 + """ + req_uid = _get_uid_from_request_headers() + if req_uid is not None: + sess_uid = session.get('user_id') + if sess_uid is not None: + try: + if int(sess_uid) != int(req_uid): + return None + except Exception: + if str(sess_uid) != str(req_uid): + return None + return req_uid + return session.get('user_id') + + +def _get_user_id_or_error(): + user_id = _resolve_user_id() + if not user_id: + return None, (jsonify({'success': False, 'error': '未登录或缺少uid'}), 401) + try: + return int(user_id), None + except Exception: + return None, (jsonify({'success': False, 'error': 'uid无效'}), 400) def _push_task_event(task_id, event): @@ -54,10 +110,14 @@ def _push_task_event(task_id, event): # 任务进入终态时,顺便清理行级进度缓存 with _task_line_progress_lock: _task_line_progress.pop(task_id, None) + status = (event or {}).get('status') if isinstance(event, dict) else None + if status in ('success', 'failed', 'cancelled'): + with _cancelled_brand_tasks_lock: + _cancelled_brand_tasks.discard(task_id) def _expand_folder_xlsx(folder_path): - """返回文件夹下第一层 .xlsx 文件的绝对路径列表(保留旧逻辑,不再用于品牌工具文件夹上传)""" + """返回文件夹下所有 .xlsx 文件的绝对路径列表""" if not folder_path or not os.path.isdir(folder_path): return [] paths = [] @@ -67,26 +127,6 @@ def _expand_folder_xlsx(folder_path): return paths -def _expand_folder_xlsx_recursive_items(folder_path): - """返回文件夹下所有 .xlsx 文件的绝对路径和相对路径列表(递归)""" - if not folder_path or not os.path.isdir(folder_path): - return [] - items = [] - root = os.path.normpath(folder_path) - for current_root, _, filenames in os.walk(root): - for name in filenames: - if not (name.endswith('.xlsx') or name.endswith('.XLSX')): - continue - absolute_path = os.path.normpath(os.path.join(current_root, name)) - relative_path = os.path.relpath(absolute_path, root).replace('\\', '/') - items.append({ - 'absolutePath': absolute_path, - 'relativePath': relative_path, - }) - items.sort(key=lambda item: item['relativePath']) - return items - - def _upload_local_xlsx_to_oss(local_path, user_id): """将本地 xlsx 文件上传到 OSS,返回 (url, None) 或 (None, error_message)。""" if not local_path or not os.path.isfile(local_path): @@ -99,7 +139,7 @@ def _upload_local_xlsx_to_oss(local_path, user_id): safe_base = "".join(c if c.isalnum() or c in '-_.' else '_' for c in base) if not safe_base.endswith('.xlsx'): safe_base = safe_base + '.xlsx' - key = f"{bucket_path}brand_input/{int(time.time() * 1000)}/{user_id}_{safe_base}" + key = f"{bucket_path}brand_input/{user_id}/{int(time.time() * 1000)}/{safe_base}" try: with open(local_path, "rb") as f: content = f.read() @@ -109,347 +149,125 @@ def _upload_local_xlsx_to_oss(local_path, user_id): return None, str(e) -def _run_brand_files(file_paths, task_id=None, check_cancelled=None, strategy="Terms"): - """对多个 xlsx 执行 single_file_handle(线程池多线程),返回 (result_paths, error_message)。 - check_cancelled: 可调用对象,返回 True 时中止并返回 (result_paths, 'cancelled')。 - task_id 存在时会在每处理完一个文件后更新 progress_current。 - strategy: 品牌匹配方式,默认 Terms,可选 Simple。""" - os.makedirs(BRAND_OUTPUT_DIR, exist_ok=True) - subdir = str(task_id) if task_id else ('immediate_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S')) - out_dir = os.path.join(BRAND_OUTPUT_DIR, subdir) - os.makedirs(out_dir, exist_ok=True) +def _run_brand_single(taskid,brand_ls, fileUrl,strategy,totalLines,chunkTotal,chunkIndex): + """""" + invalidBrands = [] # 不符合品牌的数据 + queryFailedBrands = [] # 查询失败的数据 + keptRows = [] + for brand in brand_ls: + if _is_task_cancelled(taskid): + raise TaskCancelledError("任务已取消") + faild_data,query_faild_data = single_file_handle(brand,strategy) + invalidBrands.extend(faild_data) + queryFailedBrands.extend(query_faild_data) + if len(faild_data) == 0 and len(query_faild_data) == 0: + keptRows.append(brand) - # 先筛出有效路径并生成 (fp, out_path) 列表 - tasks = [] - for fp in file_paths: - if not fp or not isinstance(fp, str) or not os.path.isfile(fp): - continue - base = os.path.splitext(os.path.basename(fp))[0] - safe_base = "".join(c if c.isalnum() or c in '-_' else '_' for c in base) - out_path = os.path.join(out_dir, safe_base + '_result.xlsx') - tasks.append((fp, out_path)) - - if not tasks: - return [], None - - result_paths = [] - result_lock = threading.Lock() - current = [0] # 用列表以便在闭包中修改 - - def process_one(fp, out_path, file_index, files_total): - # 行级进度回调:由 single_file_handle 在循环中调用,直接写入内存字典,不落库 - def progress_callback(current_line, total_lines, extra=None): - if not task_id: - return - info = { - 'file_index': file_index, - 'file_total': files_total, - 'file_path': fp, - 'current_line': int(current_line or 0), - 'total_lines': int(total_lines or 0), - } - if isinstance(extra, dict): - info.update(extra) - with _task_line_progress_lock: - _task_line_progress[task_id] = info - - single_file_handle(fp, out_path, strategy, check_cancelled=check_cancelled, progress_callback=progress_callback) - return out_path - - max_workers = min(8, len(tasks)) - with ThreadPoolExecutor(max_workers=max_workers) as executor: - future_to_out = { - executor.submit(process_one, fp, out_path, idx + 1, len(tasks)): out_path - for idx, (fp, out_path) in enumerate(tasks) - } - try: - for future in as_completed(future_to_out): - if check_cancelled and check_cancelled(): - for f in future_to_out: - f.cancel() - return result_paths, 'cancelled' - try: - out_path = future.result() - with result_lock: - result_paths.append(out_path) - current[0] += 1 - if task_id: - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET progress_current = %s WHERE id = %s", - (current[0], task_id) - ) - conn.commit() - conn.close() - except Exception: - print(traceback.format_exc()) - except TaskCancelledError: - for f in future_to_out: - f.cancel() - return result_paths, 'cancelled' - except Exception as e: - print(traceback.format_exc()) - print(e) - for f in future_to_out: - f.cancel() - return result_paths, str(e) - except Exception as e: - print(traceback.format_exc()) - return result_paths, str(e) - return result_paths, None + data = { "strategy": strategy , + "files": [ + { + "fileUrl": fileUrl, + "originalFilename": "", + "relativePath": "", + "mainSheetName": "", + "chunkIndex": chunkIndex, + "chunkTotal": chunkTotal, + "totalLines": totalLines, + "keptRows": keptRows, + "invalidBrands": invalidBrands, + "queryFailedBrands": queryFailedBrands + } + ] + } + resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks/{taskid}/result",headers={"accept":"application/json"}, + # json={ "strategy": strategy , + # "files": [ + # { "fileUrl": fileUrl,"originalFilename": "","relativePath": "", + # "mainSheetName": "","columns": [],"keptRows": [], + # "invalidBrands": invalidBrands,"queryFailedBrands": queryFailedBrands + # }]}) + json=data) + print(data) + print(taskid,brand_ls, fileUrl) + print("提交结果",data,"\n-->",resp.text) + return True -def _upload_results_to_oss(source_paths, local_result_paths, task_id): + +def _background_brand_task(task_id,data): """ - 将源文件和结果文件上传到 OSS,并打包成 zip(分两个文件夹:源文件、结果文件)上传。 - 返回 (result_data, error_message),result_data 为 {"urls": [url1, ...], "zip_url": "..."},出错时为 (None, err)。 + 后台执行爬虫任务 + 参数示例: + data : { + "taskId": 348, + "strategy": "Terms", + "files": [ + { + "fileIndex": 1, + "fileUrl": "ab8cce4878754bfd89b4cd3ed20e1395", + "originalFilename": "品牌样例.xlsx", + "relativePath": "店铺A/品牌样例.xlsx", + "sheetName": "Sheet1", + "columns": [ + "品牌", + "ASIH", + "状态", + "时间" + ], + "rows": [ + { + "品牌": "YQAUTEC", + "ASIH": "B0F28NZ752", + "状态": "", + "时间": "", + "__rowIndex": 2 + } + ], + "uniqueBrands": [ + "YQAUTEC" + ] + } + ] + } """ - if not local_result_paths or not task_id: - return None, "无结果文件" try: - from ali_oss import upload_file as oss_upload_file - except ImportError: - return None, "OSS 模块未配置" - urls = [] - subdir = f"{OSS_PREFIX}/{task_id}" - for fp in local_result_paths: - if not fp or not os.path.isfile(fp): - continue - name = os.path.basename(fp) - key = f"{bucket_path}{subdir}/{name}" - try: - with open(fp, "rb") as f: - content = f.read() - url = oss_upload_file(content, key) - urls.append(url) - except Exception as e: - return None, f"上传结果文件失败: {e}" - if not urls: - return None, "没有可上传的结果文件" - # 打包成 zip:源文件 -> 源文件/,结果文件 -> 结果文件/ - buf = io.BytesIO() - with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: - for fp in (source_paths or []): - if fp and os.path.isfile(fp): - zf.write(fp, os.path.join("源文件", os.path.basename(fp))) - for fp in local_result_paths: - if fp and os.path.isfile(fp): - zf.write(fp, os.path.join("结果文件", os.path.basename(fp))) - buf.seek(0) - zip_key = f"{bucket_path}{subdir}/result.zip" - try: - zip_url = oss_upload_file(buf.getvalue(), zip_key) - except Exception as e: - return {"urls": urls, "zip_url": None}, None # 单文件链接已保存,zip 失败不报错 - return {"urls": urls, "zip_url": zip_url}, None - - -def _background_brand_task(task_id): - """后台执行单个品牌爬虫任务并更新数据库""" - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "SELECT id, user_id, file_paths, strategy, `desc` FROM brand_crawl_tasks WHERE id = %s AND status = 'pending'", - (task_id,) - ) - row = cur.fetchone() - conn.close() - if not row: - return - file_paths = row.get('file_paths') - if isinstance(file_paths, str): - file_paths = json.loads(file_paths) if file_paths else [] - # 优先使用表里存的 strategy 字段,兼容旧数据则从 desc 解析 - strategy = (row.get('strategy') or '').strip() or None - if not strategy: - desc_val = (row.get('desc') or '').strip() - strategy = "Simple" if desc_val.startswith('[Simple]') else "Terms" - if strategy not in ('Terms', 'Simple'): - strategy = 'Terms' - if not file_paths: - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", - ('没有有效文件路径', task_id) - ) - conn.commit() - conn.close() - except Exception: - pass - _push_task_event(task_id, {'status': 'failed', 'error_message': '没有有效文件路径'}) - return - # 将 file_paths 解析为本地路径:URL 下载到 BRAND_OUTPUT_DIR 对应任务文件夹下(兼容旧数据中的本地路径) - os.makedirs(BRAND_OUTPUT_DIR, exist_ok=True) - task_subdir = str(task_id) if task_id else ('immediate_' + datetime.datetime.now().strftime('%Y%m%d_%H%M%S')) - task_dir = os.path.join(BRAND_OUTPUT_DIR, task_subdir) - os.makedirs(task_dir, exist_ok=True) - - local_paths = [] - downloaded_files_to_cleanup_on_fail = [] - for idx, p in enumerate(file_paths): - if not p or not isinstance(p, str): + file_ls = data.get("files") + strategy = data.get("strategy") + if not isinstance(file_ls, list): + file_ls = [] + tasks = [] + for file_data in file_ls: + rows = file_data.get("rows") or [] + brand_ls = [i.get("品牌") for i in rows if i.get("品牌")] + fileUrl = file_data.get("fileUrl") + if not brand_ls: continue - p = p.strip() - if _is_url(p): - try: - parsed = urlparse(p) - url_name = os.path.basename(parsed.path or '') - if not url_name: - url_name = f"input_{idx + 1}.xlsx" - safe_name = "".join(c if c.isalnum() or c in '-_.' else '_' for c in url_name) - if not safe_name.lower().endswith('.xlsx'): - safe_name = safe_name + '.xlsx' + for i in range(0, len(brand_ls), 5): + chunk = brand_ls[i:i + 5] + tasks.append((chunk, fileUrl)) - base_no_ext, ext = os.path.splitext(safe_name) - dest = os.path.join(task_dir, safe_name) - n = 1 - while os.path.exists(dest): - dest = os.path.join(task_dir, f"{base_no_ext}_{n}{ext}") - n += 1 - - r = requests.get(p, timeout=120, stream=True) - r.raise_for_status() - with open(dest, "wb") as f: - for chunk in r.iter_content(chunk_size=1024 * 1024): - if chunk: - f.write(chunk) - local_paths.append(dest) - downloaded_files_to_cleanup_on_fail.append(dest) - except Exception as e: - for tf in downloaded_files_to_cleanup_on_fail: - try: - os.unlink(tf) - except Exception: - pass - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", - (f'下载文件失败: {str(e)}', task_id) - ) - conn.commit() - conn.close() - except Exception: - pass - _push_task_event(task_id, {'status': 'failed', 'error_message': f'下载文件失败: {str(e)}'}) + if tasks: + max_workers = min(8, len(tasks)) + with ThreadPoolExecutor(max_workers=max_workers) as executor: + if _is_task_cancelled(task_id): + _push_task_event(task_id, {'status': 'cancelled'}) return - elif os.path.isfile(p): - local_paths.append(p) - if not local_paths: - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", - ('没有有效的本地或可下载文件', task_id) - ) - conn.commit() - conn.close() - except Exception: - pass - _push_task_event(task_id, {'status': 'failed', 'error_message': '没有有效的本地或可下载文件'}) - return - try: - # 原子地将 pending 改为 running,避免用户先点取消时仍被覆盖为 running - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'running', progress_total = %s, progress_current = 0 WHERE id = %s AND status = 'pending'", - (len(local_paths), task_id) - ) - n = cur.rowcount - conn.commit() - conn.close() - if n == 0: - conn = get_db() - with conn.cursor() as cur: - cur.execute("SELECT status FROM brand_crawl_tasks WHERE id = %s", (task_id,)) - r = cur.fetchone() - conn.close() - if r and (r.get('status') or '') == 'cancelled': + futures = [ + executor.submit(_run_brand_single, task_id, chunk, file_url, strategy,len(brand_ls),len(tasks),chunkIndex+1) + for chunkIndex,(chunk, file_url) in enumerate(tasks) + ] + for future in as_completed(futures): + if _is_task_cancelled(task_id): + for f in futures: + f.cancel() _push_task_event(task_id, {'status': 'cancelled'}) - return - except Exception: - pass - - def check_cancelled(): - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute("SELECT status FROM brand_crawl_tasks WHERE id = %s", (task_id,)) - row = cur.fetchone() - conn.close() - return row and (row.get('status') or '') == 'cancelled' - except Exception: - return False - - result_paths, err = _run_brand_files(local_paths, task_id=task_id, check_cancelled=check_cancelled, strategy=strategy) - if err: - try: - conn = get_db() - with conn.cursor() as cur: - st = 'cancelled' if err == 'cancelled' else 'failed' - cur.execute( - "UPDATE brand_crawl_tasks SET status = %s, error_message = %s WHERE id = %s", - (st, err if st == 'failed' else None, task_id) - ) - conn.commit() - conn.close() - except Exception as e: - traceback.print_exc() - print(e) - pass - _push_task_event(task_id, {'status': st, 'error_message': err if st == 'failed' else None}) - return - result_data, upload_err = _upload_results_to_oss(local_paths, result_paths, task_id) - if upload_err: - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", - (upload_err, task_id) - ) - conn.commit() - conn.close() - except Exception: - pass - _push_task_event(task_id, {'status': 'failed', 'error_message': upload_err}) - return - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'success', result_paths = %s WHERE id = %s", - (json.dumps(result_data), task_id) - ) - conn.commit() - conn.close() - except Exception: - pass - _push_task_event(task_id, {'status': 'success'}) - finally: - pass + return + future.result() + _push_task_event(task_id, {'status': 'success'}) except Exception as e: - try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'failed', error_message = %s WHERE id = %s", - (str(e), task_id) - ) - conn.commit() - conn.close() - except Exception: - pass + print("执行出错",traceback.format_exc()) _push_task_event(task_id, {'status': 'failed', 'error_message': str(e)}) + print("执行完成") def _parse_json(val, default=None): @@ -494,6 +312,9 @@ def _normalize_result_paths(raw): def api_brand_expand_folder(): """展开文件夹,返回其下所有 .xlsx 文件路径列表""" try: + _, err = _get_user_id_or_error() + if err: + return err data = request.get_json() or {} folder = (data.get('folder') or '').strip() if not folder: @@ -503,12 +324,33 @@ def api_brand_expand_folder(): except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 +def _expand_folder_xlsx_recursive_items(folder_path): + """返回文件夹下所有 .xlsx 文件的绝对路径和相对路径列表(递归)""" + if not folder_path or not os.path.isdir(folder_path): + return [] + items = [] + root = os.path.normpath(folder_path) + for current_root, _, filenames in os.walk(root): + for name in filenames: + if not (name.endswith('.xlsx') or name.endswith('.XLSX')): + continue + absolute_path = os.path.normpath(os.path.join(current_root, name)) + relative_path = os.path.relpath(absolute_path, root).replace('\\', '/') + items.append({ + 'absolutePath': absolute_path, + 'relativePath': relative_path, + }) + items.sort(key=lambda item: item['relativePath']) + return items @brand_bp.route('/api/brand/expand-folder-recursive', methods=['POST']) @login_required def api_brand_expand_folder_recursive(): """递归展开文件夹,返回 .xlsx 文件绝对路径及相对路径列表""" try: + _, err = _get_user_id_or_error() + if err: + return err data = request.get_json() or {} folder = (data.get('folder') or '').strip() if not folder: @@ -542,65 +384,58 @@ def api_brand_run(): if not paths: return jsonify({'success': False, 'error': '没有有效的 xlsx 文件路径'}), 400 # 先上传到 OSS,获取链接再入库 - user_id = session['user_id'] + user_id, err = _get_user_id_or_error() + if err: + return err urls = [] for p in paths: url, err = _upload_local_xlsx_to_oss(p, user_id) if err: return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500 - urls.append(url) - desc_core = ', '.join(os.path.basename(p) for p in paths)[:500] if paths else '' - desc = f'[{strategy}] ' + (desc_core or '') - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "INSERT INTO brand_crawl_tasks (user_id, file_paths, `desc`, strategy, status, task_type) VALUES (%s, %s, %s, %s, 'pending', %s)", - (user_id, json.dumps(urls), desc or None, strategy, task_type) - ) - task_id = cur.lastrowid - conn.commit() - conn.close() - threading.Thread(target=_background_brand_task, args=(task_id,), daemon=True).start() - return jsonify({'success': True, 'task_id': task_id}) + base_name = os.path.basename(p) + urls.append({"fileUrl": url,"originalFilename": base_name,"relativePath": p }) + + # 请求提交 + resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id}",headers={ + "content-type":"application/json", + },data=json.dumps({ "files": urls, "strategy": strategy,"taskType": 1,"archiveName": ""})) + print({ "files": urls, "strategy": strategy,"taskType": 1,"archiveName": ""}) + resp_data = resp.json() + # print(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id} 返回:",resp_data,"状态:",resp.status_code) + if resp_data.get("success"): + task_id = resp_data.get("data").get("taskId") + data = resp_data.get("data") + threading.Thread(target=_background_brand_task, args=(task_id,data), daemon=True).start() + return jsonify({'success': True, 'task_id': task_id}) + else: + return jsonify({'success': False, 'error': "请求接口失败"}), 500 + except Exception as e: + traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 @brand_bp.route('/api/brand/tasks', methods=['GET', 'POST']) -@login_required +# @login_required def api_brand_tasks(): """GET: 获取当前用户的任务列表;POST: 添加任务(后台执行)""" if request.method == 'GET': try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - """SELECT id, file_paths, `desc`, strategy, status, result_paths, error_message, - COALESCE(progress_current, 0) AS progress_current, - COALESCE(progress_total, 0) AS progress_total, - created_at, updated_at - FROM brand_crawl_tasks WHERE user_id = %s ORDER BY id DESC LIMIT 100""", - (session['user_id'],) - ) - rows = cur.fetchall() - conn.close() - items = [] - for r in rows: - items.append({ - 'id': r['id'], - 'file_paths': _parse_json(r['file_paths'], []), - 'desc': (r.get('desc') or '').strip() or None, - 'strategy': (r.get('strategy') or 'Terms').strip() or 'Terms', - 'status': r.get('status') or 'pending', - 'result_paths': _parse_json(r['result_paths']), - 'error_message': (r.get('error_message') or '').strip() or None, - 'progress_current': int(r.get('progress_current') or 0), - 'progress_total': int(r.get('progress_total') or 0), - 'created_at': r['created_at'].strftime('%Y-%m-%d %H:%M') if r.get('created_at') else '', - 'updated_at': r['updated_at'].strftime('%Y-%m-%d %H:%M') if r.get('updated_at') else '', - }) - return jsonify({'success': True, 'items': items}) + user_id, err = _get_user_id_or_error() + if err: + return err + resp = requests.get(f"{JAVA_API_BASE}/api/brand/tasks", params={ + "userId": user_id + }) + resp_data = resp.json() + print("获取列表",resp.text) + if resp_data.get("success"): + items = resp_data.get("data").get("items") + return jsonify({'success': True, 'items': items}) + return jsonify({'success': True, 'items': []}) except Exception as e: + traceback.print_exc() + print("获取列表失败",e) return jsonify({'success': False, 'error': str(e)}), 500 # POST try: @@ -621,111 +456,95 @@ def api_brand_tasks(): paths = [p.strip() for p in paths if p and isinstance(p, str)] if not paths: return jsonify({'success': False, 'error': '请提供至少一个文件路径或链接'}), 400 - user_id = session['user_id'] + user_id, err = _get_user_id_or_error() + if err: + return err urls = [] for p in paths: - if _is_url(p): - urls.append(p) - elif os.path.isfile(p): - url, err = _upload_local_xlsx_to_oss(p, user_id) - if err: - return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500 - urls.append(url) - else: - return jsonify({'success': False, 'error': f'无效路径或链接: {p[:80]}'}), 400 - desc_core = ', '.join(os.path.basename(urlparse(u).path or u) for u in urls)[:500] if urls else '' - desc = f'[{strategy}] ' + (desc_core or '') - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "INSERT INTO brand_crawl_tasks (user_id, file_paths, `desc`, strategy, status, task_type) VALUES (%s, %s, %s, %s, 'pending', %s)", - (user_id, json.dumps(urls), desc or None, strategy, task_type) - ) - task_id = cur.lastrowid - conn.commit() - conn.close() - # threading.Thread(target=_background_brand_task, args=(task_id,), daemon=True).start() - return jsonify({'success': True, 'task_id': task_id}) + url, err = _upload_local_xlsx_to_oss(p, user_id) + if err: + return jsonify({'success': False, 'error': f'上传文件失败: {os.path.basename(p)} - {err}'}), 500 + base_name = os.path.basename(p) + urls.append({"fileUrl": url, "originalFilename": base_name, "relativePath": p}) + params = { + "userId": user_id + } + # resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id}",headers={ + # "content-type":"application/json", + # },data=json.dumps({ "files": urls, "strategy": strategy,"taskType": 1,"archiveName": ""})) + req_data = {"files": urls, "strategy": strategy, "taskType": 2, "archiveName": ""} + resp = requests.post(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id}", headers={"content-type": "application/json"}, + data=json.dumps(req_data)) + print(req_data) + # print(f"{JAVA_API_BASE}/api/brand/tasks?userId={user_id},返回", resp.text) + resp_data = resp.json() + if resp_data.get("success"): + task_id = resp_data.get("data").get("taskId") + data = resp_data.get("data") + return jsonify({'success': True, 'task_id': task_id}) + return jsonify({'success': False, 'task_id': "请求异常"}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @brand_bp.route('/api/brand/tasks/') -@login_required +# @login_required def api_brand_task_detail(task_id): """获取单个任务详情(用于轮询状态)""" try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - """SELECT id, file_paths, `desc`, strategy, status, result_paths, error_message, - COALESCE(progress_current, 0) AS progress_current, - COALESCE(progress_total, 0) AS progress_total, - created_at, updated_at - FROM brand_crawl_tasks WHERE id = %s AND user_id = %s""", - (task_id, session['user_id']) - ) - row = cur.fetchone() - conn.close() - if not row: - return jsonify({'success': False, 'error': '任务不存在'}), 404 - return jsonify({ - 'success': True, - 'task': { - 'id': row['id'], - 'file_paths': _parse_json(row['file_paths'], []), - 'desc': (row.get('desc') or '').strip() or None, - 'strategy': (row.get('strategy') or 'Terms').strip() or 'Terms', - 'status': row.get('status') or 'pending', - 'result_paths': _parse_json(row['result_paths']), - 'error_message': (row.get('error_message') or '').strip() or None, - 'progress_current': int(row.get('progress_current') or 0), - 'progress_total': int(row.get('progress_total') or 0), - 'created_at': row['created_at'].strftime('%Y-%m-%d %H:%M') if row.get('created_at') else '', - 'updated_at': row['updated_at'].strftime('%Y-%m-%d %H:%M') if row.get('updated_at') else '', - } - }) + resp = requests.get(f"{JAVA_API_BASE}/api/brand/tasks/{task_id}") + resp_data = resp.json() + if resp_data.get("success"): + task = resp_data["data"]["task"] + return jsonify({ + 'success': True, + 'task': task + }) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @brand_bp.route('/api/brand/tasks//line-progress') -@login_required +# @login_required def api_brand_task_line_progress(task_id): """ 获取任务的“当前文件行级进度”(当前行数/总行数)。 该信息仅存放在内存字典 _task_line_progress 中,不写入数据库。 """ try: - with _task_line_progress_lock: - info = _task_line_progress.get(task_id) - if not info: - return jsonify({'success': True, 'has_progress': False}) - # 为了避免暴露完整路径,只返回文件名 - file_name = os.path.basename(info.get('file_path') or '') if info.get('file_path') else '' - resp = { - 'file_index': int(info.get('file_index') or 0), - 'file_total': int(info.get('file_total') or 0), - 'file_name': file_name, - 'current_line': int(info.get('current_line') or 0), - 'total_lines': int(info.get('total_lines') or 0), - } - return jsonify({'success': True, 'has_progress': True, 'info': resp}) + resp = requests.get(f"{JAVA_API_BASE}/api/brand/tasks/{task_id}") + resp_data = resp.json() + if resp_data.get("success"): + if resp_data["data"]["line_progress"]["has_progress"]: + info = resp_data["data"]["line_progress"]["info"] + resp = { + 'file_index': int(info.get('file_index') or 0), + 'file_total': int(info.get('file_total') or 0), + 'file_name': info.get("file_name",""), + 'current_line': int(info.get('current_line') or 0), + 'total_lines': int(info.get('total_lines') or 0), + } + return jsonify({'success': True, 'has_progress': True, 'info': resp}) + raise RuntimeError("获取进度失败") except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @brand_bp.route('/api/brand/tasks//events') -@login_required +# @login_required def api_brand_task_events(task_id): """SSE:任务完成时后端主动推送事件,前端监听后隐藏进度条与取消按钮""" + user_id, err = _get_user_id_or_error() + if err: + return err + def _task_status_event(): conn = get_db() try: with conn.cursor() as cur: cur.execute( "SELECT status, error_message FROM brand_crawl_tasks WHERE id = %s AND user_id = %s", - (task_id, session['user_id']) + (task_id, user_id) ) row = cur.fetchone() finally: @@ -778,21 +597,28 @@ def api_brand_task_events(task_id): @brand_bp.route('/api/brand/tasks//cancel', methods=['POST']) -@login_required +# @login_required def api_brand_task_cancel(task_id): """取消正在执行或等待中的任务(pending/running 均可取消)""" try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "UPDATE brand_crawl_tasks SET status = 'cancelled' WHERE id = %s AND user_id = %s AND status IN ('pending', 'running')", - (task_id, session['user_id']) - ) - n = cur.rowcount - conn.commit() - conn.close() - if n == 0: - return jsonify({'success': False, 'error': '任务不存在或无法取消'}), 400 + user_id, err = _get_user_id_or_error() + if err: + return err + + + resp = requests.post( + f"{JAVA_API_BASE}/api/brand/tasks/{task_id}/cancel", + headers={"accept": "application/json"}, + timeout=20 + ) + try: + resp_data = resp.json() + except Exception: + return jsonify({'success': False, 'error': '取消接口返回非 JSON'}), 500 + if not resp_data.get('success'): + return jsonify({'success': False, 'error': '第三方取消任务失败'}), 400 + + _mark_task_cancelled(task_id) _push_task_event(task_id, {'status': 'cancelled'}) return jsonify({'success': True}) except Exception as e: @@ -804,17 +630,21 @@ def api_brand_task_cancel(task_id): def api_brand_task_delete(task_id): """删除任务(仅限非 running 状态)""" try: - conn = get_db() - with conn.cursor() as cur: - cur.execute( - "DELETE FROM brand_crawl_tasks WHERE id = %s AND user_id = %s AND status != 'running'", - (task_id, session['user_id']) - ) - n = cur.rowcount - conn.commit() - conn.close() - if n == 0: - return jsonify({'success': False, 'error': '任务不存在或正在执行中无法删除'}), 400 + user_id, err = _get_user_id_or_error() + if err: + return err + + resp = requests.delete( + f"{JAVA_API_BASE}/api/brand/tasks/{task_id}", + headers={"accept": "application/json"}, + timeout=20 + ) + try: + resp_data = resp.json() + except Exception: + return jsonify({'success': False, 'error': '取消接口返回非 JSON'}), 500 + if not resp_data.get('success'): + return jsonify({'success': False, 'error': '第三方取消任务失败'}), 400 return jsonify({'success': True}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @@ -825,11 +655,15 @@ def api_brand_task_delete(task_id): def api_brand_download(task_id): """下载任务结果:优先使用 OSS zip_url 重定向;否则本地/单链接/多链接按原逻辑处理""" try: + user_id, err = _get_user_id_or_error() + if err: + return err + conn = get_db() with conn.cursor() as cur: cur.execute( "SELECT result_paths FROM brand_crawl_tasks WHERE id = %s AND user_id = %s", - (task_id, session['user_id']) + (task_id, user_id) ) row = cur.fetchone() conn.close() diff --git a/app/brand_spider/__pycache__/main.cpython-311.pyc b/app/brand_spider/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7305183f0f5b78eda72ad4d07b5fbf98893c7e7c GIT binary patch literal 23102 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 literal 0 HcmV?d00001 diff --git a/app/brand_spider/__pycache__/main.cpython-39.pyc b/app/brand_spider/__pycache__/main.cpython-39.pyc index f8e611a0a90d26f1bdf3839130f42ba21227a85e..14ba46b4254cddeaf2e587933d74fc8c13c5eb99 100644 GIT binary patch delta 2424 zcmZXWO>7&-6@X`EclopY(GN+9lqf0w7ZXcjTv>`_$959At(&GrA~&fwag;S@Mbjde z)a*);g4qxa-Ah}eG!{tEMghe17eRp@bZGNaq!7?!4@FWR2=wQWq;L++xu^8Kl`01* ziTU>Jdo%C7dBd6EpEmw>J!vKqI)Tq-XSY&+;&w96J|eeIo^G;gs1`QDwTKa+fd@;%P7qS;IWD!o)xM zfRVR`tUMoVF=NCUwMJSL!!MG;&|RF!w{{jNrJ*Q@BI-ZR7HHmYvni_ke`JM;JvG{r zfNoM)wfefXWR^;n?K)Y|IVym#I1?#nF@schMnFx|R13U&u05E`d(O^ofNwco2W;{Q4NW197^>Zj=m-`5|=970DY z5#xZavL>qHxc`Cv&E82=P9chj2mPto$dNBiG2~dLC|$>{&UFA$ROTO=P5H0Ho}!oh zzs7dVdm|2}CB6YYH*gGAGeT7wXu8^EOVpea1Lgld_kl1<33&{j&fhfxYsnc`xXLvi z;^D23$9C?o zbQzVA71Gwk9jUi9=gv#JE@`316Z_9`NdiBGEwzmP?%z_pn2dQbm)>A9)E-$?J8SitKGs5a@P9_I9Y3fIZk&i5qRJn=_PneXvj0 z_rtQbD$?NS7$;TT>zDo8u@3A~Np4`m)6h;so4LidF`a4fmI@M>m8l?shh*RDPL!g)Y+S=-!dvCVBr!A5qk%{dda5+~Y9TrIQ@OG@jkWdXx>>gA} z6&r4^gqnD1ncnW}5HDk^5*XBIE{ByklOx`M99SUjCcQf84a&j5=~>7)yDvWsa&BKf z2l9}Fz%nOu9p(-1>&HQ#cl%}DO|`H#pMvZ2+i96c{Xs&SNJB#TFC>(f8JS*DAnkOs zt**BB-CclTBXR@^lkGr>+>C@%Xn|aP*&CG#1Rs^75PVFINp&56a;N05)YfSrWg+w< zfmX=TKt{cQik|vHB0rTOEIW7@hCYvODV--F5QvX?x}E+%Bihh`4s&Y<@@|l>-e%5+ z!EfT|EP8D3_KWPD-gwaooly6NvuG@35#KJh%lK-3WyVhO#X2fWh(*LRh%(|?#0>-V{ z4gdYg%)?hvTtzG+OvD=E8bTnxgK!W}A*zTP;=6v|)RBo7QG61xm)Opmci+7&o`d&p zXt7djRIR%T_6Uw?>c2HLO<(o@G4(>Yt2wSITz{c>m=5~o;v^Fn{r8F&pDQr&Jsfcx zP*A$sHDTJ^2$$+>wktNpFF<{{Fd*n3-^ zqagcp(dK}P=F2j(e#(C4d-c^L<>@8j}|9c3o>B!kyov&4`a}>ro7(4=h1@^!G Nz}e&B%Xks$^uOt-OZ@-< delta 4518 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 diff --git a/app/brand_spider/__pycache__/web_dec.cpython-311.pyc b/app/brand_spider/__pycache__/web_dec.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9ca09435b2aca05db1fc993e79794db2460e0b3 GIT binary patch literal 6427 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:", proxy_resp.text) @@ -125,7 +122,7 @@ class TaskCancelledError(Exception): pass -def single_file_handle(file_path, output_path, strategy="Terms", check_cancelled=None, progress_callback=None): +def single_file_handle(brand, strategy="Terms"): status_info = { "Ended": "已结束", "Expired": "已过期的", @@ -471,208 +468,123 @@ def single_file_handle(file_path, output_path, strategy="Terms", check_cancelled "欧洲联盟" ] check_staus = ["已过期的","已结束"] - # 1. 读取 Excel 活动工作表 - wb = openpyxl.load_workbook(file_path, data_only=True) - active_sheet = wb.active - active_sheet_name = active_sheet.title - print(active_sheet_name) - - # 2. 将数据转换为列表嵌套字典 - rows = list(active_sheet.iter_rows(values_only=True)) - if rows: - columns = list(rows[0]) - data_rows = [] - for row in rows[1:]: - row_dict = {} - for idx, col in enumerate(columns): - row_dict[col] = row[idx] if idx < len(row) else None - data_rows.append(row_dict) - else: - columns = [] - data_rows = [] - - # 3. 确保必要列存在 - # required_cols = ["状态", "国家", "时间"] - # for col in required_cols: - # if col not in columns: - # columns.append(col) - # for row_dict in data_rows: - # row_dict[col] = "" - - # 4. 提取唯一品牌列表 - brand_ls = set() - for row in data_rows: - brand = row.get("品牌") - if brand: - brand_ls.add(brand) # 5. 初始化不符合品牌的数据列表 faild_data = [] # 初始化查询失败的品牌的数据列表 query_faild_data = [] - # 6. 对每个品牌进行处理 - brands = list(brand_ls) - total_brands = len(brands) - for idx, brand in enumerate(brands, start=1): - if check_cancelled and check_cancelled(): - raise TaskCancelledError() - # 行级进度回调:当前第 idx 条 / total_brands - if progress_callback and total_brands > 0: - try: - progress_callback(idx, total_brands, extra={'current_brand': str(brand)}) - except Exception: - # 回调失败不影响主流程 - pass + # Simple 策略下最多翻 3 页(0,30,60),直到命中 554 行判断 + max_pages = 3 if strategy == "Simple" else 1 + page = 0 + matched = False - # Simple 策略下最多翻 3 页(0,30,60),直到命中 554 行判断 - max_pages = 3 if strategy == "Simple" else 1 - page = 0 - matched = False + while page < max_pages and not matched: + try: + as_structure_dict = { + "_id": create_code_generator(), + "boolean": "AND", + "bricks": [ + { + "_id": create_code_generator(), + "key": "brandName", + "value": brand, + "strategy": strategy + } + ] + } + # 构造 asStructure 内部 JSON 对象 + as_structure_dict = as_structure_dict + # print(as_structure_dict) - while page < max_pages and not matched: - try: - as_structure_dict = { - "_id": create_code_generator(), - "boolean": "AND", - "bricks": [ - { - "_id": create_code_generator(), - "key": "brandName", - "value": brand, - "strategy": strategy - } - ] - } - # 构造 asStructure 内部 JSON 对象 - as_structure_dict = as_structure_dict - # print(as_structure_dict) + # 将内部对象转为 JSON 字符串(作为 asStructure 字段的值) + as_structure_str = json.dumps(as_structure_dict, ensure_ascii=False) + data = { + "sort": "score desc", + "rows": "30", + "asStructure": as_structure_str, + "fg": "_void_" + } - # 将内部对象转为 JSON 字符串(作为 asStructure 字段的值) - as_structure_str = json.dumps(as_structure_dict, ensure_ascii=False) - data = { - "sort": "score desc", - "rows": "30", - "asStructure": as_structure_str, - "fg": "_void_" - } - # 翻页:第二页 start=30,第三页 start=60(仅 Simple 策略) - if strategy == "Simple" and page > 0: - data["start"] = str(30 * page) + # 翻页:第二页 start=30,第三页 start=60(仅 Simple 策略) + if strategy == "Simple" and page > 0: + data["start"] = str(30 * page) - # print("请求参数", as_structure_dict, "页码:", page) - hashsearch = get_guid() - enc_text = search(hashsearch, data) - print(f"【{hashsearch}】待解密-->", enc_text) - dec_text = decrypt_via_service(hashsearch, enc_text) - # print(type(dec_text)) - dec_text = str(dec_text) - # print(f"解密之后的结果-->", dec_text) - data = json.loads(dec_text) - except Exception as e: - # 仅第一页请求失败时记录为查询失败品牌 - if page == 0: - import traceback - traceback.print_exc() - safe_brand = brand.encode('gbk', errors='replace').decode('gbk') - print(f"品牌:{safe_brand},处理失败:{e}") - query_faild_data.append({"品牌": brand, "时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) - break + # print("请求参数", as_structure_dict, "页码:", page) + hashsearch = get_guid() + enc_text = search(hashsearch, data) + print(f"【{hashsearch}】待解密-->", enc_text) + dec_text = decrypt_via_service(hashsearch, enc_text) + # print(type(dec_text)) + dec_text = str(dec_text) + # print(f"解密之后的结果-->", dec_text) + data = json.loads(dec_text) + except Exception as e: + # 仅第一页请求失败时记录为查询失败品牌 + if page == 0: + import traceback + traceback.print_exc() + safe_brand = brand.encode('gbk', errors='replace').decode('gbk') + print(f"品牌:{safe_brand},处理失败:{e}") + # query_faild_data.append({"brand": brand, "time": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) + query_faild_data.append(brand) + break - # print(data) + # print(data) + + if data.get("response", {}).get("numFound", 0) > 0: + if strategy == "Simple": + new_brand_name = clean_text(brand) + _brand = new_brand_name.lower() + else: + _brand = brand + + docs = data["response"]["docs"] + for d in docs: + office = d.get("office") + status = d.get("status") + registrationDate = d.get("registrationDate") + status_name = status_info.get(status, status) + brand_name = d.get("brandName") + if isinstance(brand_name, list): + brand_name = "|".join(brand_name) - if data.get("response", {}).get("numFound", 0) > 0: if strategy == "Simple": - new_brand_name = clean_text(brand) - _brand = new_brand_name.lower() - else: - _brand = brand + new_brand_name = clean_text(brand_name) + new_brand_name = new_brand_name.lower() + if _brand != new_brand_name: + continue - docs = data["response"]["docs"] - for d in docs: - office = d.get("office") - status = d.get("status") - registrationDate = d.get("registrationDate") - status_name = status_info.get(status, status) - brand_name = d.get("brandName") - if isinstance(brand_name, list): - brand_name = "|".join(brand_name) + check_office = d.get("designation") - if strategy == "Simple": - new_brand_name = clean_text(brand_name) - new_brand_name = new_brand_name.lower() - if _brand != new_brand_name: + check_office.append(office) + for i in set(check_office): + office_name = country_info.get(i, i) + # print(office_name,office_name in check_value and status_name not in check_staus) + if office_name in check_value and status_name not in check_staus: + # 命中 554 行判断,记录并结束当前品牌后续翻页 + # data_rows = [row for row in data_rows if row.get("品牌") != brand] + # print(office_name, office in ["EM","QZ"] and i not in ["EM","QZ"]) + if office in ["EM","QZ"] and i not in ["EM","QZ"]: continue - check_office = d.get("designation") + if strategy == "Simple": + faild_data.append({"brand": brand, "country": office_name, "status": status_name}) # "reason":"|".join(d.get("brandName","") + matched = True + else: + # print("添加",brand,office_name) + faild_data.append({"brand": brand, "country": office_name, "status": status_name}) + # break + if matched: + break - check_office.append(office) - for i in set(check_office): - office_name = country_info.get(i, i) - # print(office_name,office_name in check_value and status_name not in check_staus) - if office_name in check_value and status_name not in check_staus: - # 命中 554 行判断,记录并结束当前品牌后续翻页 - data_rows = [row for row in data_rows if row.get("品牌") != brand] - # print(office_name, office in ["EM","QZ"] and i not in ["EM","QZ"]) - if office in ["EM","QZ"] and i not in ["EM","QZ"]: - continue + # 如果 Simple 策略下本页未命中,则翻下一页;其他策略不翻页 + if not matched: + page += 1 - if strategy == "Simple": - faild_data.append({"品牌": brand, "国家": office_name, "状态": status_name, "原因":"|".join(d.get("brandName","")),"时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) - matched = True - else: - # print("添加",brand,office_name) - faild_data.append({"品牌": brand, "国家": office_name, "状态": status_name, "时间": datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S")}) - # break - if matched: - break - # 如果 Simple 策略下本页未命中,则翻下一页;其他策略不翻页 - if not matched: - page += 1 - # else: - # # 更新主表数据 - # for row in data_rows: - # if row.get("品牌") == brand: - # row["状态"] = status_name - # row["国家"] = office_name - # row["时间"] = datetime.datetime.now().strftime("%y-%m-%d %H:%M:%S") # 品牌匹配行 - # else: - # # 非匹配行:时间 = 当前国家值(模拟原 mask 逻辑) - # row["时间"] = "" - - # 7. 创建输出工作簿 - out_wb = Workbook() - out_wb.remove(out_wb.active) # 删除默认 sheet - - # 主工作表 - main_ws = out_wb.create_sheet(title=active_sheet_name) - main_ws.append(columns) - for row_dict in data_rows: - main_ws.append([row_dict.get(col, "") for col in columns]) - - # 不符合品牌工作表 - faild_ws = out_wb.create_sheet(title="不符合品牌") - if strategy == "Simple": - faild_columns = ["品牌", "国家", "状态","原因"] - else: - faild_columns = ["品牌", "国家", "状态"] - faild_ws.append(faild_columns) - for row_dict in faild_data: - # print("写入",row_dict) - faild_ws.append([row_dict.get(col, "") for col in faild_columns]) - - # 查询失败品牌工作表 - query_faild_ws = out_wb.create_sheet(title="查询失败品牌") - query_faild_columns = ["品牌", "时间"] - query_faild_ws.append(query_faild_columns) - for row_dict in query_faild_data: - query_faild_ws.append([row_dict.get(col, "") for col in query_faild_columns]) - - out_wb.save(output_path) - print("生成完成--->", output_path) - return output_path + return faild_data,query_faild_data if __name__ == '__main__': diff --git a/app/config.py b/app/config.py index 431860a..64fc653 100644 --- a/app/config.py +++ b/app/config.py @@ -6,9 +6,7 @@ STITCH_WORKFLOW_ID = "7608813873483300907" import os from dotenv import load_dotenv load_dotenv() -_base_url = os.getenv("base_url","http://159.75.121.33:15124") -java_api_base = os.getenv("java_api_base", "http://127.0.0.1:18080") -JAVA_API_BASE = java_api_base +_base_url = os.getenv("base_url","http://159.75.121.33:15124") # MySQL 配置 mysql_host = os.getenv("mysql_host") @@ -18,8 +16,9 @@ mysql_database = os.getenv("mysql_database","aiimage") proxy_url = os.getenv("proxy_url") proxy_mode = int(os.getenv("proxy_mode",1)) - client_name=os.getenv("client_name") + ".exe" +JAVA_API_BASE = os.getenv("java_api_base", "http://127.0.0.1:18080") + cache_path = "./user_data" @@ -39,8 +38,8 @@ os.environ['OSS_ACCESS_KEY_SECRET'] = accessKeySecret os.environ['SECRET_KEY'] = "ddffc7c1d02121d9554d7b080b2511b6" -debug = True -version = "1.0.3" +debug = False +version = "1.0.13" APP_UPDATE_URL = f"{_base_url}/api/version/latest" # 检测更新接口地址,返回 { "file_url": "...", "version": "x.x.x" } os.environ['APP_VERSION'] = version os.environ['APP_UPDATE_URL'] = APP_UPDATE_URL diff --git a/app/web_source/admin.html b/app/web_source/admin.html index 30ddcb0..ac8e238 100644 --- a/app/web_source/admin.html +++ b/app/web_source/admin.html @@ -1,539 +1,539 @@ - - - - - - 管理后台 - 南日AI - - - -

-

管理后台

- -
-
用户管理
-
查看生成记录
-
- - -
-
-

创建用户

-

无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。

-
- - -
-
- - -
-
- - -
- - -

-
- -
-

用户列表

-
-
- - -
- - -
- - - - - - - - - - - - -
ID用户名角色所属管理员创建时间操作
- -
-
- - -
-
-

筛选条件

-
-
- - -
-
- - -
-
- - -
- -
-
-
-

生成记录

- - - - - - - - - - - -
ID用户类型创建时间结果预览
- -
-
- - - - - - - + + + + + + 管理后台 - 数富AI + + + + +

管理后台

+ +
+
用户管理
+
查看生成记录
+
+ + +
+
+

创建用户

+

无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。

+
+ + +
+
+ + +
+
+ + +
+ + +

+
+ +
+

用户列表

+
+
+ + +
+ + +
+ + + + + + + + + + + + +
ID用户名角色所属管理员创建时间操作
+ +
+
+ + +
+
+

筛选条件

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

生成记录

+ + + + + + + + + + + +
ID用户类型创建时间结果预览
+ +
+
+ + + + + + + diff --git a/app/web_source/brand.html b/app/web_source/brand.html index af6d4bd..85fea62 100644 --- a/app/web_source/brand.html +++ b/app/web_source/brand.html @@ -3,7 +3,7 @@ - 亚马逊 - 南日AI + 亚马逊 - 数富AI + + + +

管理后台

+ +
+
用户管理
+
查看生成记录
+
+ + +
+
+

创建用户

+

无注册入口,仅管理员可在此创建用户。层级:超级管理员 → 管理员 → 普通号。

+
+ + +
+
+ + +
+
+ + +
+ + +

+
+ +
+

用户列表

+
+
+ + +
+ + +
+ + + + + + + + + + + + +
ID用户名角色所属管理员创建时间操作
+ +
+
+ + +
+
+

筛选条件

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

生成记录

+ + + + + + + + + + + +
ID用户类型创建时间结果预览
+ +
+
+ + + + + + + diff --git a/app/web_source/templates_backup/brand.html b/app/web_source/templates_backup/brand.html new file mode 100644 index 0000000..f5afd87 --- /dev/null +++ b/app/web_source/templates_backup/brand.html @@ -0,0 +1,918 @@ + + + + + + 亚马逊 - 南日AI + + + +
+
+ 南日AI-亚马逊 + 返回首页 +
+ +
+
+ +
+ +
+ + +
+
+ 任务队列 + +
+
+
    +
    暂无任务
    +
    +
    +
    +
    + +
    + + + + diff --git a/app/web_source/templates_backup/home.html b/app/web_source/templates_backup/home.html new file mode 100644 index 0000000..d1f5541 --- /dev/null +++ b/app/web_source/templates_backup/home.html @@ -0,0 +1,352 @@ + + + + + + 首页 - 南日AI + + + +
    + 南日AI +
    + {% if is_admin %} + + {% endif %} + {{ username }} + + 退出 +
    +
    软件更新
    +
    + 当前版本: {{ version }} + +
    +
    +
    + +
    +
    +
    +
    + + + +
    + + + + diff --git a/app/web_source/templates_backup/index.html b/app/web_source/templates_backup/index.html new file mode 100644 index 0000000..9bfe8f7 --- /dev/null +++ b/app/web_source/templates_backup/index.html @@ -0,0 +1,5833 @@ + + + + + + 南日AI + + + + +
    +
    + + 南日AI + ← 返回首页 + +
    + +
    + +
    +
    + +
    + + + + +
    +
    + + + + + +
    + +
    +
    +
    + +
    +
    生成或恢复到工作台的图片将显示在此处,点击可查看大图
    +
    +
    + +
    右侧显示所有图片竖向拼接后的效果,点击可查看大图并保存
    +
    + +
    + + +
    + + +
    +
    +
    + 设置 + +
    +
    +
    +
    API Key
    +
    用于生成接口鉴权,必须设置后才能进行生成。仅保存在本机,不会上传。
    +
    + +
    +
    +
    +
    自动保存位置
    +
    设置后,生图完成后将自动保存副本到此文件夹。
    +
    + + + +
    +
    +
    +
    软件更新
    +
    + 当前版本: v1.0.0 + +
    +
    +
    + +
    +
    +
    + +
    +
    + + +
    +
    +
    选择参与拼接的图片及顺序(拖拽调整顺序,× 移出拼接)
    +
    +
    点击下方图片可加入拼接顺序:
    +
    +
    + + +
    +
    +
    + + + +
    + + +
    +
    +
    + +
    查看大图
    +
    +
    + + + +
    + +
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    + + + 画笔: 20px + + + +
    +
    + *提示: 红色区域将被重新绘制
    + *提示: 点击图片添加标记,右键删除标记 +
    +
    + + +
    + +
    +
    + + +
    +
    +
    +
    Just the Right Capacity, Easily Holds Daily Essentials.
    +
    Phone, long wallet, keys, lipstick—all fit in without looking bulky.
    +
    + +
    +
    +
    + + +
    + + +
    +
    + + +
    + +
    +
    +
    + + + + diff --git a/app/web_source/templates_backup/login.html b/app/web_source/templates_backup/login.html new file mode 100644 index 0000000..4cb667f --- /dev/null +++ b/app/web_source/templates_backup/login.html @@ -0,0 +1,164 @@ + + + + + + 登录 - 南日AI + + + +
    + 南日AI +
    + + + + +