后台店铺数据记录页新增 Tab 切换与重复 ASIN 分析;task-169/170 HTTP 客户端配置接入;dedupe/brand 相关改造
Build Backend JAR / build (push) Has been cancelled
Build Backend JAR / build (push) Has been cancelled
- 管理后台:店铺数据记录改为列表分页展示(去任务号/文件列/累计标题,'最新'改'更新时间'),新增'重复 ASIN' Tab 按跨店聚合展示 ASIN/店铺/分组/国家/日期/价格;修复失败/进行中任务被过滤不显示的问题 - task-169: BrandCheckHttpConfigResolver 从 aiimage.http-client.* 读取品牌检查超时/重试(默认同现状、钳制),附 8 条测试 - task-170: surefire 内存调整为 1536m - dedupe: DedupeRunService 重构(事务边界/进度)、新增 DedupeRunProgressVo、Controller 与结果 VO 调整,PermissionMenuService 相应适配 - brand/dedupe 前端:BrandDedupeTab/BrandPatrolDeleteTab 改造、新增 CountrySelector 组件与 country-options、类型与接口端对齐、测试更新 - 移除无引用文件:backend/static/logo.jpg、prompts/
This commit is contained in:
+186
-52
@@ -1722,7 +1722,8 @@
|
||||
if (!rawResult || typeof rawResult !== 'object') return;
|
||||
var result = shopDataNormalizeResult(groupBase, rawResult);
|
||||
var resultId = shopDataResultId(result);
|
||||
if (!resultId || !result.file_ready) return;
|
||||
// 失败/进行中的任务没有结果文件也保留显示(禁用下载、可删除)
|
||||
if (!resultId) return;
|
||||
if (resultId && group.results.some(function (existing) { return shopDataResultId(existing) === resultId; })) return;
|
||||
group.results.push(result);
|
||||
if (!group.shop_name) group.shop_name = result.shop_name || '';
|
||||
@@ -1747,55 +1748,58 @@
|
||||
function renderShopDataStatus(item, status) {
|
||||
var normalized = String(status || '-').toUpperCase();
|
||||
var errorTitle = item && item.error ? ' title="' + escapeHtml(item.error) + '"' : '';
|
||||
return '<span class="image-video-status ' + escapeHtml(normalized) + '"' + errorTitle + '>' + escapeHtml(imageVideoStatusLabel(normalized)) + '</span>';
|
||||
var cls = normalized === 'SUCCESS' || normalized === 'COMPLETED'
|
||||
? 'success'
|
||||
: (normalized === 'FAILED' || normalized === 'CANCELLED' ? 'failed' : 'running');
|
||||
return '<span class="shop-data-status ' + cls + '"' + errorTitle + '>' + escapeHtml(imageVideoStatusLabel(normalized)) + '</span>';
|
||||
}
|
||||
|
||||
function renderShopDataTaskResult(item) {
|
||||
function renderShopDataRecordRow(item) {
|
||||
var resultId = shopDataResultId(item);
|
||||
var selected = resultId > 0 && selectedShopDataResultIds.has(resultId);
|
||||
var status = String(item.status || item.file_status || '').toUpperCase();
|
||||
var terminal = ['SUCCESS', 'FAILED', 'CANCELLED'].indexOf(status) >= 0;
|
||||
var countryCodes = item.country_codes != null ? item.country_codes : item.countryCodes;
|
||||
var countries = countryListLabel(countryCodes);
|
||||
var filename = item.output_filename || '-';
|
||||
var updatedAt = item.updated_at || item.latest_created_at || item.created_at || item.finished_at || '-';
|
||||
var checkbox = '<input type="checkbox" data-shop-data-select="' + (resultId || '') + '"' +
|
||||
(selected ? ' checked' : '') + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>';
|
||||
return '<div class="shop-data-result' + (selected ? ' selected' : '') + '" data-shop-data-card="' + (resultId || '') + '">' +
|
||||
'<div class="shop-data-result-head">' +
|
||||
'<label class="shop-data-task-title">' + checkbox +
|
||||
'<span title="任务 ' + escapeHtml(item.task_id || item.task_no || '-') + '">任务 ' + escapeHtml(item.task_id || item.task_no || '-') + '</span>' +
|
||||
'</label>' +
|
||||
renderShopDataStatus(item, status || item.file_status) +
|
||||
'</div>' +
|
||||
'<div class="image-video-card-info">' +
|
||||
'<div class="image-video-info-row"><label>国家</label><span>' + escapeHtml(countries) + '</span></div>' +
|
||||
'<div class="image-video-info-row"><label>文件</label><span title="' + escapeHtml(filename) + '">' + escapeHtml(filename) + '</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="image-video-card-actions">' +
|
||||
'<button class="image-video-card-action" type="button" data-shop-data-download="' + (resultId || '') + '"' + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载文件</button>' +
|
||||
'<button class="image-video-card-action shop-data-delete-action" type="button" data-shop-data-delete="' + (resultId || '') + '"' + (terminal && resultId > 0 ? '' : ' disabled') + '>' + shopDataDeleteIcon() + '删除</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
return '<tr data-shop-data-card="' + (resultId || '') + '">' +
|
||||
'<td style="width:36px;">' + checkbox + '</td>' +
|
||||
'<td class="dup-shop" title="' + escapeHtml(item.shop_name || '-') + '">' + escapeHtml(item.shop_name || '-') + '</td>' +
|
||||
'<td class="muted">' + escapeHtml(item.group_name || '-') + '</td>' +
|
||||
'<td class="dup-country">' + escapeHtml(countryListLabel(countryCodes)) + '</td>' +
|
||||
'<td>' + renderShopDataStatus(item, status || item.file_status) + '</td>' +
|
||||
'<td class="dup-date">' + escapeHtml(updatedAt) + '</td>' +
|
||||
'<td style="width:170px;">' +
|
||||
'<button class="shop-data-record-action" type="button" data-shop-data-download="' + (resultId || '') + '"' + (item.file_ready && resultId > 0 ? '' : ' disabled') + '>' + imageVideoDownloadIcon() + '下载文件</button>' +
|
||||
'<button class="shop-data-record-action danger" type="button" data-shop-data-delete="' + (resultId || '') + '"' + (terminal && resultId > 0 ? '' : ' disabled') + ' style="margin-left:8px;">' + shopDataDeleteIcon() + '删除</button>' +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
}
|
||||
|
||||
function renderShopDataTaskCard(group) {
|
||||
var results = Array.isArray(group.results) ? group.results : [];
|
||||
var latest = group.latest_created_at || (results[0] && (results[0].created_at || results[0].finished_at)) || '-';
|
||||
return '<article class="image-video-card shop-data-task-card" data-shop-data-group="' + escapeHtml(group.key || '') + '">' +
|
||||
'<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 + '/1 份当日累计文件</span>' +
|
||||
'</div>' +
|
||||
'<div class="image-video-card-info">' +
|
||||
'<div class="image-video-info-row"><label>分组</label><span>' + escapeHtml(group.group_name || '-') + '</span></div>' +
|
||||
'<div class="image-video-info-row"><label>最新</label><span>' + escapeHtml(latest) + '</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="shop-data-result-list">' +
|
||||
(results.length ? results.map(renderShopDataTaskResult).join('') : '<div class="image-video-empty">暂无结果</div>') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</article>';
|
||||
function renderShopDataRecordTable() {
|
||||
var rows = shopDataTasks.map(renderShopDataRecordRow).join('');
|
||||
if (!shopDataTasks.length) {
|
||||
return '<div class="shop-data-empty-hint">暂无符合条件的店铺数据任务</div>';
|
||||
}
|
||||
return '<table>' +
|
||||
'<thead><tr>' +
|
||||
'<th style="width:36px;"></th>' +
|
||||
'<th style="width:18%;">店铺</th>' +
|
||||
'<th style="width:14%;">分组</th>' +
|
||||
'<th style="width:16%;">国家</th>' +
|
||||
'<th style="width:10%;">状态</th>' +
|
||||
'<th style="width:18%;">更新时间</th>' +
|
||||
'<th style="width:170px;">操作</th>' +
|
||||
'</tr></thead>' +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>';
|
||||
}
|
||||
|
||||
function renderShopDataTasks() {
|
||||
var grid = document.getElementById('shopDataTaskGrid');
|
||||
grid.innerHTML = renderShopDataRecordTable();
|
||||
syncShopDataSelectionUi();
|
||||
}
|
||||
|
||||
function syncShopDataSelectionUi() {
|
||||
@@ -1811,9 +1815,11 @@
|
||||
return selectedShopDataResultIds.has(shopDataResultId(item));
|
||||
}).length;
|
||||
var selectAll = document.getElementById('shopDataTaskSelectAll');
|
||||
selectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
|
||||
selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
|
||||
selectAll.disabled = shopDataDownloadInProgress || selectable.length === 0;
|
||||
if (selectAll) {
|
||||
selectAll.checked = selectable.length > 0 && selectedCount === selectable.length;
|
||||
selectAll.indeterminate = selectedCount > 0 && selectedCount < selectable.length;
|
||||
selectAll.disabled = shopDataDownloadInProgress || selectable.length === 0;
|
||||
}
|
||||
var batch = document.getElementById('btnBatchDownloadShopDataTasks');
|
||||
batch.disabled = shopDataDownloadInProgress || selectedCount === 0;
|
||||
batch.innerHTML = imageVideoDownloadIcon() + (shopDataDownloadInProgress
|
||||
@@ -1821,14 +1827,6 @@
|
||||
: '批量下载' + (selectedCount ? ' (' + selectedCount + ')' : ''));
|
||||
}
|
||||
|
||||
function renderShopDataTasks() {
|
||||
var grid = document.getElementById('shopDataTaskGrid');
|
||||
grid.innerHTML = shopDataTaskGroups.length
|
||||
? shopDataTaskGroups.map(renderShopDataTaskCard).join('')
|
||||
: '<div class="image-video-empty">暂无符合条件的店铺数据任务</div>';
|
||||
syncShopDataSelectionUi();
|
||||
}
|
||||
|
||||
function loadShopDataCrawlTasks(page) {
|
||||
shopDataTaskPage = page || 1;
|
||||
selectedShopDataResultIds.clear();
|
||||
@@ -1847,7 +1845,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) + ' 家店铺 · 每家店铺保留 1 份当日累计文件';
|
||||
document.getElementById('shopDataTaskTotal').textContent = '共 ' + (total || 0) + ' 家店铺';
|
||||
renderShopDataTasks();
|
||||
renderPagination('shopDataTaskPagination', total, responsePage, responsePageSize, loadShopDataCrawlTasks);
|
||||
})
|
||||
@@ -1860,6 +1858,131 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 重复 ASIN 分析 ==========
|
||||
var shopDataDuplicatePage = 1, shopDataDuplicatePageSize = 10;
|
||||
var shopDataDuplicateItems = [];
|
||||
var shopDataDuplicateTotal = 0;
|
||||
var shopDataDuplicateAnalyzed = { shopCount: 0, resultCount: 0 };
|
||||
var shopDataDuplicateLoading = false;
|
||||
|
||||
function buildShopDataDuplicateQuery(page) {
|
||||
var params = new URLSearchParams();
|
||||
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
|
||||
};
|
||||
Object.keys(values).forEach(function (key) {
|
||||
if (values[key]) params.set(key, values[key]);
|
||||
});
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
function shopDataDuplicateHeader(header) {
|
||||
return '<thead><tr>' + header.map(function (col) {
|
||||
return '<th style="width:' + (col.width || '') + ';">' + col.label + '</th>';
|
||||
}).join('') + '</tr></thead>';
|
||||
}
|
||||
|
||||
function renderShopDataDuplicateCard(item) {
|
||||
var occurrences = Array.isArray(item.occurrences) ? item.occurrences : [];
|
||||
var brand = '';
|
||||
occurrences.forEach(function (occ) { if (occ.brand && !brand) brand = occ.brand; });
|
||||
var rows = occurrences.map(function (occ) {
|
||||
var countries = countryListLabel(occ.country_codes);
|
||||
return '<tr>' +
|
||||
'<td class="dup-shop">' + escapeHtml(occ.shop_name || '-') + '</td>' +
|
||||
'<td class="dup-country">' + escapeHtml(occ.group_name || '-') + '</td>' +
|
||||
'<td class="dup-country">' + escapeHtml(countries) + '</td>' +
|
||||
'<td class="dup-date">' + escapeHtml(occ.date || '-') + '</td>' +
|
||||
'<td class="dup-date">' + escapeHtml(occ.price || '-') + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
return '<article class="duplicate-asin-card" data-duplicate-asin="' + escapeHtml(item.asin) + '">' +
|
||||
'<div class="duplicate-asin-card-head">' +
|
||||
'<span class="dup-asin">' + escapeHtml(item.asin) + '</span>' +
|
||||
'<span class="dup-count">' + item.shop_count + ' 家店铺</span>' +
|
||||
'<span class="dup-brand" title="' + escapeHtml(brand) + '">' + escapeHtml(brand || '') + '</span>' +
|
||||
'</div>' +
|
||||
'<table class="duplicate-asin-table">' +
|
||||
shopDataDuplicateHeader([
|
||||
{ label: '店铺', width: '18%' },
|
||||
{ label: '分组', width: '14%' },
|
||||
{ label: '国家', width: '16%' },
|
||||
{ label: '日期', width: '14%' },
|
||||
{ label: '价格', width: '12%' }
|
||||
]) +
|
||||
'<tbody>' + rows + '</tbody>' +
|
||||
'</table>' +
|
||||
'</article>';
|
||||
}
|
||||
|
||||
function renderShopDataDuplicateList() {
|
||||
var list = document.getElementById('shopDataDuplicateList');
|
||||
if (!shopDataDuplicateItems.length) {
|
||||
list.innerHTML = '<div class="shop-data-empty-hint">暂无重复 ASIN。请在左侧筛选条件后点击"查询"生效范围,再点击"重新分析"。</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = shopDataDuplicateItems.map(renderShopDataDuplicateCard).join('');
|
||||
}
|
||||
|
||||
function loadShopDataDuplicateAsins(page) {
|
||||
if (shopDataDuplicateLoading) return;
|
||||
shopDataDuplicatePage = page || 1;
|
||||
shopDataDuplicateLoading = true;
|
||||
var list = document.getElementById('shopDataDuplicateList');
|
||||
var progress = document.getElementById('shopDataDuplicateProgress');
|
||||
var button = document.getElementById('btnRefreshShopDataDuplicates');
|
||||
progress.textContent = '正在读取各店铺结果文件并分析,请稍候...';
|
||||
button.disabled = true;
|
||||
list.innerHTML = '<div class="shop-data-empty-hint">分析中...</div>';
|
||||
fetch('/api/admin/shop-data-crawl/duplicate-asins?' + buildShopDataDuplicateQuery(shopDataDuplicatePage))
|
||||
.then(function (response) { return response.json(); })
|
||||
.then(function (res) {
|
||||
if (!res.success) throw new Error(res.error || '分析失败');
|
||||
shopDataDuplicateItems = res.items || [];
|
||||
shopDataDuplicateTotal = Number(res.total) || 0;
|
||||
shopDataDuplicateAnalyzed.shopCount = Number(res.analyzed_shop_count) || 0;
|
||||
shopDataDuplicateAnalyzed.resultCount = Number(res.analyzed_result_count) || 0;
|
||||
var totalEl = document.getElementById('shopDataDuplicateTotal');
|
||||
totalEl.textContent = '共 ' + shopDataDuplicateTotal + ' 个重复 ASIN · 已分析 ' + shopDataDuplicateAnalyzed.shopCount + ' 家店铺';
|
||||
renderShopDataDuplicateList();
|
||||
renderPagination('shopDataDuplicatePagination', shopDataDuplicateTotal, shopDataDuplicatePage, shopDataDuplicatePageSize, loadShopDataDuplicateAsins);
|
||||
})
|
||||
.catch(function (error) {
|
||||
shopDataDuplicateItems = [];
|
||||
shopDataDuplicateTotal = 0;
|
||||
list.innerHTML = '<div class="shop-data-empty-hint">分析失败:' + escapeHtml(error.message || '') + '</div>';
|
||||
document.getElementById('shopDataDuplicateTotal').textContent = '';
|
||||
})
|
||||
.finally(function () {
|
||||
shopDataDuplicateLoading = false;
|
||||
progress.textContent = '';
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function switchShopDataSubTab(view) {
|
||||
var recordsView = document.getElementById('shopDataRecordsView');
|
||||
var duplicatesView = document.getElementById('shopDataDuplicatesView');
|
||||
var recordsTab = document.getElementById('shopDataSubTabRecords');
|
||||
var duplicatesTab = document.getElementById('shopDataSubTabDuplicates');
|
||||
var recordsActive = view === 'records';
|
||||
recordsView.style.display = recordsActive ? '' : 'none';
|
||||
duplicatesView.style.display = recordsActive ? 'none' : '';
|
||||
recordsTab.classList.toggle('active', recordsActive);
|
||||
recordsTab.setAttribute('aria-selected', recordsActive ? 'true' : 'false');
|
||||
duplicatesTab.classList.toggle('active', !recordsActive);
|
||||
duplicatesTab.setAttribute('aria-selected', !recordsActive ? 'true' : 'false');
|
||||
if (!recordsActive) {
|
||||
loadShopDataDuplicateAsins(1);
|
||||
}
|
||||
}
|
||||
|
||||
function downloadShopDataTask(item) {
|
||||
var resultId = shopDataResultId(item);
|
||||
if (!item || !item.file_ready || !resultId) return;
|
||||
@@ -2037,12 +2160,23 @@
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('btnFilterShopDataTasks').onclick = function () { loadShopDataCrawlTasks(1); };
|
||||
document.getElementById('btnFilterShopDataTasks').onclick = function () {
|
||||
loadShopDataCrawlTasks(1);
|
||||
if (document.getElementById('shopDataDuplicatesView').style.display !== 'none') {
|
||||
loadShopDataDuplicateAsins(1);
|
||||
}
|
||||
};
|
||||
document.getElementById('btnResetShopDataTasks').onclick = function () {
|
||||
['shopDataTaskFilterShop', 'shopDataTaskFilterGroup', 'shopDataTaskFilterCountry', 'shopDataTaskFilterFrom', 'shopDataTaskFilterTo']
|
||||
.forEach(function (id) { document.getElementById(id).value = ''; });
|
||||
loadShopDataCrawlTasks(1);
|
||||
if (document.getElementById('shopDataDuplicatesView').style.display !== 'none') {
|
||||
loadShopDataDuplicateAsins(1);
|
||||
}
|
||||
};
|
||||
document.getElementById('shopDataSubTabRecords').onclick = function () { switchShopDataSubTab('records'); };
|
||||
document.getElementById('shopDataSubTabDuplicates').onclick = function () { switchShopDataSubTab('duplicates'); };
|
||||
document.getElementById('btnRefreshShopDataDuplicates').onclick = function () { loadShopDataDuplicateAsins(1); };
|
||||
document.getElementById('shopDataTaskSelectAll').onchange = function (event) {
|
||||
shopDataTasks.forEach(function (item) {
|
||||
var resultId = shopDataResultId(item);
|
||||
|
||||
Reference in New Issue
Block a user