From 672b9f9b46f4533ae8deb177e738ddcd44c48cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Thu, 3 Sep 2026 09:32:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E5=BA=97=E9=93=BA=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=87=8D=E5=A4=8DASIN=EF=BC=9A=E5=85=A8=E9=80=89?= =?UTF-8?q?=E7=A7=BB=E8=87=B3=E8=A1=A8=E5=A4=B4=E3=80=81Tab=E6=80=BB?= =?UTF-8?q?=E6=95=B0=E3=80=81=E7=8B=AC=E7=AB=8B=E7=AD=9B=E9=80=89=E3=80=81?= =?UTF-8?q?=E6=8C=89ASIN=E5=88=86=E7=BB=84=E8=A1=A8=E6=A0=BC=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 列表全选复选框从工具栏移到表格表头,事件委托处理动态元素 - 重复ASIN Tab 标题加总数(XX 为重复 ASIN 数量) - 重复ASIN 筛选独立:ASIN/店铺/国家/上架时间(开始+结束),与原店铺数据筛选互不影响 - 后端支持 asin/shop_name/country/date_from/date_to 参数: 行级筛选命中 ASIN 后展示完整记录(含其他店铺,跨店语义更直观) - 日期文本归一化(支持 2026年8月19日 上午4:34 中文格式) - 按 ASIN 分组表格展示:ASIN/店铺数/记录条数/品牌/对应店铺/国家/上架时间/操作, 详情展开显示每条记录的店铺/分组/国家/日期/价格/品牌 - 修复 _shop_data_crawl_fetch_result_bytes 兼容 result_id 列别名(生产首个真实调用报 KeyError) - 新增单测:筛选语义、日期归一化、record_count --- backend/blueprints/admin_api.py | 88 ++++++----- backend/static/admin.js | 140 ++++++++++++++---- backend/static/logo.jpg | Bin 0 -> 10772 bytes .../test_admin_shop_data_duplicate_asins.py | 56 ++++++- backend/web_source/admin.html | 94 +++++------- 5 files changed, 263 insertions(+), 115 deletions(-) create mode 100644 backend/static/logo.jpg diff --git a/backend/blueprints/admin_api.py b/backend/blueprints/admin_api.py index b4cacef5..cdff9425 100644 --- a/backend/blueprints/admin_api.py +++ b/backend/blueprints/admin_api.py @@ -1699,7 +1699,9 @@ _SHOP_DATA_CRAWL_ASIN_ANALYSIS_TIMEOUT = (10, 60) def _shop_data_crawl_fetch_result_bytes(row, timeout=None): """从 Java 下载接口拉取结果文件字节流(仅内存,不落盘)。""" - url = f"{backend_java_base_url}/api/admin/shop-data-crawl/results/{int(row['id'])}/download" + # row 支持 id(下载行)或 result_id(管理列集别名) + result_id = row.get('id') or row.get('result_id') or 0 + url = f"{backend_java_base_url}/api/admin/shop-data-crawl/results/{int(result_id)}/download" headers, params = _backend_java_internal_request() response = _get_backend_java_session().get( url, @@ -1738,6 +1740,29 @@ def _shop_data_crawl_cell_text(cell): return str(value).strip() +def _shop_data_date_key(date_text): + """把各种日期文本归一化为 YYYY-MM-DD,供范围比较。 + + 支持:ISO(2026-08-19)、中文(2026年8月19日 上午4:34)、 + yyyy.m.d / yyyy/m/d 等常见格式;无法识别时返回原字符串。 + """ + text = (date_text or '').strip() + if not text: + return '' + if len(text) >= 10 and text[4] == '-' and text[7] == '-': + return text[:10] + year = month = day = None + m = re.search(r'(\d{4})\s*[年./-]\s*(\d{1,2})\s*[月./-]\s*(\d{1,2})', text) + if m: + year, month, day = int(m.group(1)), int(m.group(2)), int(m.group(3)) + if year and month and day: + try: + return f'{year:04d}-{month:02d}-{day:02d}' + except ValueError: + return text + return text + + def _shop_data_crawl_parse_workbook(workbook): """从结果 Workbook 提取 ASIN 行:字典列表,表头按 日期/ASIN/…/价格/…/品牌 识别,跳过无 ASIN 行。""" rows = [] @@ -1781,11 +1806,11 @@ def shop_data_crawl_duplicate_asins(): 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() - country = (request.args.get('country') or '').strip().upper() - created_from = _parse_admin_datetime_arg('created_from') - created_to = _parse_admin_datetime_arg('created_to') + shop_name_filter = (request.args.get('shop_name') or request.args.get('shop') or '').strip() + country_filter = (request.args.get('country') or '').strip().upper() + asin_filter = (request.args.get('asin') or '').strip().upper() + date_from = (request.args.get('date_from') or '').strip()[:10] + date_to = (request.args.get('date_to') or '').strip()[:10] conditions = [ "r.module_type = 'SHOP_DATA_CRAWL'", @@ -1793,34 +1818,6 @@ def shop_data_crawl_duplicate_asins(): "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 country: - if country not in ('DE', 'UK', 'FR', 'IT', 'ES'): - raise ValueError('不支持的国家代码: ' + country) - conditions.append( - 'JSON_CONTAINS(' - 'COALESCE(df.country_codes_json, ' - 'JSON_EXTRACT(t.request_json, \'$.countryCodes\'), ' - 'JSON_EXTRACT(t.request_json, \'$.country_codes\'), \'[]\'), %s)' - ) - params.append('"' + country + '"') - 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 @@ -1907,10 +1904,13 @@ def shop_data_crawl_duplicate_asins(): current_app.logger.warning( '[shop-data-crawl] 拉取结果文件失败 result_id=%s: %s', result_id, exc) - # 按 ASIN 聚合其出现的店铺/分组/国家与行细节 + # 先全量聚合所有行,再用筛选条件圈定「命中 ASIN」; + # 命中且跨店重复的 ASIN 展示完整 occurrences(方便看该 ASIN 与哪些店重复)。 asin_occurrences = {} + matched_asins = set() for shop_item in shop_items: shop_name = shop_item['shop_name'] or '未命名' + country_codes = shop_item['country_codes'] or [] for row in shop_item['rows']: asin = row['asin'].strip().upper() if not asin: @@ -1924,15 +1924,31 @@ def shop_data_crawl_duplicate_asins(): 'group_name': shop_item['group_name'], 'country_codes': shop_item['country_codes'], }) + # 行级筛选:命中则将此 ASIN 加入待展示集合 + if asin_filter and asin_filter not in asin: + continue + if shop_name_filter and shop_name_filter.lower() not in (shop_name or '').lower() \ + and shop_name_filter.lower() not in (shop_item['group_name'] or '').lower(): + continue + if country_filter and country_filter not in [c.upper() for c in country_codes]: + continue + row_date_key = _shop_data_date_key((row['date'] or '').strip()) + if date_from and row_date_key and row_date_key < date_from: + continue + if date_to and row_date_key and row_date_key > date_to: + continue + matched_asins.add(asin) occurrences_list = [] - for asin, occurrences in asin_occurrences.items(): + for asin in matched_asins: + occurrences = asin_occurrences.get(asin, []) shop_count = len({item['shop_name'] for item in occurrences}) if shop_count < 2: continue occurrences_list.append({ 'asin': asin, 'shop_count': shop_count, + 'record_count': len(occurrences), 'occurrences': occurrences, }) occurrences_list.sort(key=lambda item: (-item['shop_count'], item['asin'])) diff --git a/backend/static/admin.js b/backend/static/admin.js index b01e10e8..db47d605 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -1784,7 +1784,7 @@ } return '
| ' + + ' | ' + ' | 店铺 | ' + '分组 | ' + '国家 | ' + @@ -1870,11 +1870,11 @@ params.set('page', String(page || 1)); params.set('page_size', String(shopDataDuplicatePageSize)); var values = { - shop_name: document.getElementById('shopDataTaskFilterShop').value.trim(), - group_name: document.getElementById('shopDataTaskFilterGroup').value.trim(), - country: document.getElementById('shopDataTaskFilterCountry').value.trim(), - created_from: document.getElementById('shopDataTaskFilterFrom').value, - created_to: document.getElementById('shopDataTaskFilterTo').value + asin: document.getElementById('shopDataDupFilterAsin').value.trim(), + shop_name: document.getElementById('shopDataDupFilterShop').value.trim(), + country: document.getElementById('shopDataDupFilterCountry').value.trim(), + date_from: document.getElementById('shopDataDupFilterDateFrom').value, + date_to: document.getElementById('shopDataDupFilterDateTo').value }; Object.keys(values).forEach(function (key) { if (values[key]) params.set(key, values[key]); @@ -1924,10 +1924,69 @@ function renderShopDataDuplicateList() { var list = document.getElementById('shopDataDuplicateList'); if (!shopDataDuplicateItems.length) { - list.innerHTML = '
|---|
dhN)gnO&vp_+z6W+Rfbh|
zB_YoFR(l&4B-ecrI+i%TF8o_2Kxx ?vYf}6Z<
zsdoZvPmtfMosaAqGFd`~R3bTh>Ro&<6lEBj*j1s?ZiuZY=DG3dnDaL*Z1#DJGL-L?
zh!1O~s~IsqWH%ql&fi;VX0194YEk_l#a5!kH0<*pJyl6i>ND5WkJNTrkfFFbFg>X2
zc&i+1SQ}I)JC6eQobR4BEj{`AZ}7cr>Mh1(p0VDg#db4^3Xure3sosDX8M-uK_;@-
zy6)O&M>E^Msg2TQld*G`)~gF4!`|UM34UFLjBh7D=!YRGdwMsB)@bfC9ql&^H3qvQ
zAwV3JTOr7;kUuxhtH?VCSI >;NdOPTFJq&KAn694Bs~n4h}j)UGVT}M`|~anl>manf)yq
zpZgiZFzPYl-GO`he)hR4&Da?Y`)et&bD0NVdw>14G0d<+5neSz7ePu{FM*uMS}r!K
zm^_y2us?u{N>Z)fLOq-d`-G8B!nyAU*m+`tGveb%13N2xB+PG3!b>$Uyl6`H3ct?Q
zee_~C66A=h#C3_4Cc2u7QRu8qaMkmf$tf+99>doloYHPr^(?Drd&l}`CCqIMc4>Mp
zGvlrC)ZUJRQ;>g;!U~aUc(QaOT18syT3eT6i_(OH&mO(IBPrp;rDq6z(pNHjLxN=R
z57U9q&b?~(GFc1+TY~&n^}2`ZUEer~TvBwPm0jrTbrFG
z?gS8B?
l40
z05Ut1BmLfmPol~&>V2YsQQ*x6q`eI?4a3|jpnbhkpN
zyl!b6Sr%EGsAVZV#+#e@VyOShssGaC?N5RpRhEuG?I!M`MQLQwI;yH_KNbFaKE+01
zdsF>x`M&ddx6h+yQDTqy2#0#$tm0S2HI1Pef0DzrS5m
筛选条件