feat(admin): A1 管理后台收敛 Java 单后台完整实现并修复 guard 误拦内部令牌
Build Backend JAR / build (push) Has been cancelled

- Java 补齐后台全部迁移差集:shopduplicatecheck 店铺数据重复检查模块(V108 扫描表+查询/扫描服务)、
  PinyinAbbrUtil 拼音缩写、ImageHistory 接口调整为内部可用、AdminUser 支持内部令牌操作并放宽列表上限
- Flask 后台 admin_api.py 路由收敛转发 Java、admin.html/admin.js 适配新后台形态
- AdminApiGuardFilter 对可信 X-Internal-Token 放行(controller 自校验兜底),修复客户端仅凭
  内部令牌调用 /api/admin/shop-manages/credential 被误拦 401
- 测试:AdminApiGuardFilterTest 补可信/假令牌用例;AdminUserServiceTest 补菜单权限 mock;
  shopduplicatecheck 新增查询/聚合/CSV 单测
This commit is contained in:
2026-09-04 11:30:09 +08:00
parent 3420c72c1d
commit 8241cd704e
43 changed files with 3071 additions and 1318 deletions
+155 -69
View File
@@ -1929,6 +1929,19 @@
var shopDataDuplicateScannedAt = '';
var shopDataDuplicateOverviewPending = false;
var shopDataDuplicateExporting = false;
// 撞款详情卡片区(跨店重复 ASIN)独立分页
var shopDataDuplicateDetailPage = 1;
var shopDataDuplicateDetailPageSize = 6;
var shopDataDuplicateDetailItems = [];
var shopDataDuplicateDetailTotal = 0;
var shopDataDuplicateDetailLoading = false;
// 站点代码 → 中文国家名(撞款详情/抽屉展示用),未识别回退原值
var DUP_SITE_LABELS = { DE: '德国', UK: '英国', FR: '法国', IT: '意大利', ES: '西班牙' };
function dupSiteLabel(site) {
var key = String(site || '').trim().toUpperCase();
return DUP_SITE_LABELS[key] || String(site || '').trim();
}
function buildShopDataDuplicateQuery(page) {
var params = new URLSearchParams();
@@ -2007,14 +2020,21 @@
}
var max = 0;
shops.forEach(function (shop) { if (shop.asin_count > max) max = shop.asin_count; });
// 新样式:双列网格,每格 = 店名 + 记录数 / 进度条 / N 个 ASIN(按 ASIN 数降序)
barsEl.innerHTML = shops.map(function (shop) {
var width = max ? Math.max(3, Math.round(shop.asin_count / max * 100)) : 0;
return '<div class="dup-chart-row">' +
'<span class="dup-chart-name" title="' + escapeHtml(shop.shop_name) + '">' + escapeHtml(shop.shop_name) + '</span>' +
'<span class="dup-chart-track"><span class="dup-chart-fill" style="width:' + width + '%"></span></span>' +
'<span class="dup-chart-num">' + shop.asin_count + '</span></div>';
var width = max ? Math.max(2, Math.round(shop.asin_count / max * 100)) : 0;
var recordCount = Number(shop.record_count || 0);
var asinCount = Number(shop.asin_count || 0);
return '<div class="dup-dist-item">' +
'<div class="dup-dist-item-head">' +
'<span class="dup-dist-name" title="' + escapeHtml(shop.shop_name) + '">' + escapeHtml(shop.shop_name) + '</span>' +
'<span class="dup-dist-record">' + recordCount.toLocaleString() + '</span>' +
'</div>' +
'<div class="dup-dist-track"><span class="dup-dist-fill" style="width:' + width + '%"></span></div>' +
'<div class="dup-dist-asin">' + asinCount.toLocaleString() + ' 个 ASIN</div>' +
'</div>';
}).join('');
noteEl.textContent = '共 ' + shops.length + ' 家店铺 · 按 ASIN 数量排序';
noteEl.textContent = '各店铺在本批站点中的上架记录量 · 共 ' + shops.length + ' 家店铺';
}
function shopDataDuplicateCellCount(item, shopName) {
@@ -2070,24 +2090,25 @@
if (button) openShopDataDuplicateDrawer(button.dataset.openAsinDetail);
}
// 撞款详情卡片区:同一 ASIN 在多条店铺的上架明细(仅跨店铺重复,按店铺数倒序
// 站点徽章:中文国家(未识别回退原值
function dupSiteBadge(site) {
return '<span class="dup-site-badge dup-site-' + escapeHtml(String(site || '-').toUpperCase()) + '">'
+ escapeHtml(dupSiteLabel(site)) + '</span>';
}
// 撞款详情卡片区:同一 ASIN 在多条店铺的上架明细(独立分页,仅跨店铺重复)
function renderShopDataDuplicateDetailCards() {
var block = document.getElementById('dupCheckDetailBlock');
var cardsEl = document.getElementById('dupCheckDetailCards');
if (!block || !cardsEl) return;
var repeated = (shopDataDuplicateItems || []).filter(function (item) {
return Number(item.shop_count) >= 2;
}).slice().sort(function (a, b) {
return (Number(b.shop_count) - Number(a.shop_count)) || dupTimeRank(b.first_date || '') - dupTimeRank(a.first_date || '');
});
if (!repeated.length) {
if (!shopDataDuplicateDetailItems.length) {
block.style.display = 'none';
cardsEl.innerHTML = '';
return;
}
block.style.display = '';
cardsEl.innerHTML = repeated.map(function (item) {
// 按店铺分组:名称 + 次数 + 站点 + 上架时间(倒序)
cardsEl.innerHTML = shopDataDuplicateDetailItems.map(function (item) {
// 按店铺分组:名称 + 次数 + 国家 + 上架时间(倒序)
var shopsMap = {};
(item.occurrences || []).forEach(function (occ) {
var key = occ.shop_name || '-';
@@ -2103,7 +2124,7 @@
info.times.sort(function (a, b) { return (a < b ? 1 : (a > b ? -1 : 0)); });
info.times = Array.from(new Set(info.times));
var rowSites = Object.keys(info.sites).sort().map(function (site) {
return '<span class="dup-check-site">' + escapeHtml(site) + '</span>';
return dupSiteBadge(site);
}).join('');
return '<tr>' +
'<td class="dup-shop">' + escapeHtml(info.shop) + '<span class="dup-detail-count">' + info.count + ' 次</span></td>' +
@@ -2119,70 +2140,130 @@
'<button class="btn btn-sm btn-secondary dup-detail-view-btn" type="button" data-open-asin-detail="' + escapeHtml(item.asin) + '">查看明细</button>' +
'</div>' +
'<table class="duplicate-asin-table dup-card-table"><thead>' +
'<tr><th>店铺</th><th>站点</th><th>上架时间</th></tr></thead>' +
'<tr><th>店铺</th><th>国家</th><th>上架时间</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table>' +
'</div>';
}).join('');
}
// 撞款详情分页加载:筛选/重置时回到第 1 页,矩阵翻页不影响详情页
function loadShopDataDuplicateDetail(page) {
if (shopDataDuplicateDetailLoading) return;
shopDataDuplicateDetailLoading = true;
var block = document.getElementById('dupCheckDetailBlock');
var cardsEl = document.getElementById('dupCheckDetailCards');
var params = new URLSearchParams();
params.set('page', String(page || 1));
params.set('page_size', String(shopDataDuplicateDetailPageSize));
var values = {
asin: document.getElementById('shopDataDupFilterAsin').value.trim(),
shop_name: document.getElementById('shopDataDupFilterShop').value.trim(),
country: document.getElementById('shopDataDupFilterCountry').value.trim(),
site: document.getElementById('shopDataDupFilterSite').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]);
});
cardsEl.innerHTML = '<div class="shop-data-empty-hint">加载中...</div>';
fetch('/api/admin/shop-data-crawl/duplicate-check-detail?' + params.toString())
.then(function (response) { return response.json(); })
.then(function (res) {
if (!res.success) throw new Error(res.error || '加载失败');
shopDataDuplicateDetailItems = res.items || [];
shopDataDuplicateDetailTotal = Number(res.total) || 0;
shopDataDuplicateDetailPage = res.page || 1;
renderShopDataDuplicateDetailCards();
if (block.style.display !== 'none') {
renderPagination('dupCheckDetailPagination', shopDataDuplicateDetailTotal,
shopDataDuplicateDetailPage, shopDataDuplicateDetailPageSize,
loadShopDataDuplicateDetail);
}
})
.catch(function (error) {
shopDataDuplicateDetailItems = [];
shopDataDuplicateDetailTotal = 0;
renderShopDataDuplicateDetailCards();
})
.finally(function () {
shopDataDuplicateDetailLoading = false;
});
}
// 抽屉徽章:基础滴状标签(按上下文附加不同配色类)
function dupDrawerBadge(text, cls) {
return '<span class="dup-drawer-badge' + (cls ? ' ' + cls : '') + '">' + escapeHtml(text) + '</span>';
}
// 图3 样式抽屉:ASIN + 店在售徽章 / 品牌行 / 彩色徽章组 / 按时间升序明细 + 次数列
function openShopDataDuplicateDrawer(asin) {
var item = null;
(shopDataDuplicateItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
(shopDataDuplicateDetailItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
if (!item) {
(shopDataDuplicateItems || []).forEach(function (it) { if (it.asin === asin) item = it; });
}
if (!item) return;
var occurrences = item.occurrences || [];
var brand = '', dateMin = '', dateMax = '', sites = {}, prices = [];
var shopsMap = {};
occurrences.forEach(function (occ) {
if (!brand && occ.brand) brand = occ.brand;
var d = normalizeDuplicateTime(occ.date);
if (d) {
if (!dateMin || d < dateMin) dateMin = d;
if (!dateMax || d > dateMax) dateMax = d;
}
if (occ.country) sites[occ.country] = true;
if (occ.price && prices.indexOf(occ.price) < 0) prices.push(occ.price);
var shopKey = occ.shop_name || '-';
if (!shopsMap[shopKey]) {
shopsMap[shopKey] = { shop: occ.shop_name || '-', group: occ.group_name || '', sites: {}, times: {}, price: '', count: 0 };
}
var info = shopsMap[shopKey];
info.count++;
if (occ.country) info.sites[occ.country] = true;
if (d) info.times[d] = true;
if (occ.price && !info.price) info.price = occ.price;
var occurrences = (item.occurrences || []).slice();
var brand = item.brand || '';
// 上架时间归一化并升序排列(按时间正序展示)
occurrences.forEach(function (occ) { occ._dupTime = normalizeDuplicateTime(occ.date); });
occurrences.sort(function (a, b) {
var ta = a._dupTime || '', tb = b._dupTime || '';
if (ta === tb) return 0;
return ta < tb ? -1 : 1;
});
var dateRange = dateMin && dateMax ? (dateMin === dateMax ? dateMin : dateMin + ' ~ ' + dateMax) : '-';
var priceRange = prices.length ? prices.slice(0, 4).join(' / ') + (prices.length > 4 ? ' 等' : '') : '-';
var siteBadges = Object.keys(sites).sort().map(function (site) {
return '<span class="dup-check-site">' + escapeHtml(site) + '</span>';
}).join('');
var rows = Object.keys(shopsMap).sort().map(function (key) {
var info = shopsMap[key];
var times = Object.keys(info.times).sort().reverse();
var rowSites = Object.keys(info.sites).sort().map(function (site) {
return '<span class="dup-check-site">' + escapeHtml(site) + '</span>';
}).join('');
var sites = {}, firstDate = '';
occurrences.forEach(function (occ) {
if (occ.country) sites[occ.country] = true;
if (!brand && occ.brand) brand = occ.brand;
var t = occ._dupTime;
if (t && (!firstDate || t < firstDate)) firstDate = t;
});
// 店铺 + 国家两级聚合:次数 = 该店铺在该国家的上架记录数
var countsMap = {};
occurrences.forEach(function (occ) {
var shopKey = occ.shop_name || '-';
var siteKey = (occ.country || '').trim().toUpperCase() || '-';
var key = shopKey + '|||' + siteKey;
countsMap[key] = (countsMap[key] || 0) + 1;
});
var rows = occurrences.map(function (occ) {
var shopKey = occ.shop_name || '-';
var siteKey = (occ.country || '').trim().toUpperCase() || '-';
var times = [];
if (occ._dupTime && occ._dupTime.length >= 16) times.push(occ._dupTime.slice(0, 16));
else if (normalizeDuplicateTime(occ.date)) times.push(normalizeDuplicateTime(occ.date));
else occ.date || '';
var count = countsMap[shopKey + '|||' + siteKey] || 0;
var siteBadge = siteKey === '-' ? '<span class="dup-site-badge dup-site-NA">-</span>' : dupSiteBadge(siteKey);
return '<tr>' +
'<td><span class="dup-asin-cell">' + escapeHtml(info.shop) + '</span></td>' +
'<td>' + escapeHtml(info.group || '-') + '</td>' +
'<td>' + (rowSites || '-') + '</td>' +
'<td class="dup-date">' + times.map(escapeHtml).join('、') + '</td>' +
'<td class="dup-date">' + escapeHtml(info.price || '-') + '</td>' +
'<td>' + info.count + '</td></tr>';
'<td><span class="dup-asin-cell">' + escapeHtml(shopKey) + '</span></td>' +
'<td>' + siteBadge + '</td>' +
'<td class="dup-date">' + escapeHtml(times.join('')) + '</td>' +
'<td>' + count + '</td></tr>';
}).join('');
// 顶部「N 店在售」红色徽章
var shopCount = Number(item.shop_count || 0);
var recordCount = Number(item.record_count || 0);
var headerBadges =
dupDrawerBadge(shopCount + ' 店在售', 'dup-drawer-badge-danger') +
dupDrawerBadge(shopCount + ' 家店铺', 'dup-drawer-badge-blue') +
Object.keys(sites).sort().map(function (site) {
return dupDrawerBadge(dupSiteLabel(site), 'dup-site-' + site.toUpperCase());
}).join('') +
dupDrawerBadge(recordCount + ' 次上架', 'dup-drawer-badge-green') +
(firstDate ? dupDrawerBadge('最早上架 ' + firstDate.slice(0, 16), 'dup-drawer-badge-cyan') : '');
document.getElementById('dupCheckDrawerAsin').textContent = item.asin;
document.getElementById('dupCheckDrawerSubtitle').textContent =
item.shop_count + ' 家店铺 · ' + item.record_count + ' 条上架记录';
document.getElementById('dupCheckDrawerAsin').className = 'dup-drawer-asin';
var subtitleEl = document.getElementById('dupCheckDrawerSubtitle');
subtitleEl.className = 'dup-drawer-subtitle dup-drawer-inline-badges';
subtitleEl.innerHTML = headerBadges;
document.getElementById('dupCheckDrawerBody').innerHTML =
'<dl class="dup-check-drawer-meta">' +
'<dt>品牌</dt><dd>' + escapeHtml(brand || '-') + '</dd>' +
'<dt>价格</dt><dd>' + escapeHtml(priceRange) + '</dd>' +
'<dt>日期范围</dt><dd>' + escapeHtml(dateRange) + '</dd>' +
'<dt>站点</dt><dd>' + (siteBadges || '-') + '</dd></dl>' +
'<h4 style="margin:14px 0 6px;font-size:13.5px;">店铺 / 站点 / 上架时间 / 次数</h4>' +
'<div class="dup-drawer-brand-line">品牌:' + escapeHtml(brand || '-') + '</div>' +
'<div class="table-scroll"><table class="dup-check-drawer-table">' +
'<thead><tr><th>店铺</th><th>分组</th><th>站点</th><th>上架时间</th><th>价格</th><th>次数</th></tr></thead>' +
'<tbody>' + rows + '</tbody></table></div>';
'<thead><tr><th>店铺</th><th>国家</th><th>上架时间(按时间升序)</th><th>次数</th></tr></thead>' +
'<tbody>' + (rows || '<tr><td colspan="4" class="shop-data-empty-hint">暂无明细</td></tr>') + '</tbody></table></div>';
document.getElementById('dupCheckDrawerMask').classList.add('show');
}
@@ -2246,7 +2327,7 @@
+ (shopDataDuplicateScannedAt ? ' · 扫描时间 ' + shopDataDuplicateScannedAt
: (shopDataDuplicateOverviewPending ? ' · 尚无扫描结果(点击「重新分析」立即扫描)' : ''));
renderShopDataDuplicateMatrix();
renderShopDataDuplicateDetailCards();
loadShopDataDuplicateDetail(shopDataDuplicateDetailPage || 1);
renderPagination('shopDataDuplicatePagination', shopDataDuplicateTotal,
shopDataDuplicatePage, shopDataDuplicatePageSize, loadShopDataDuplicateCheckItems);
})
@@ -2255,7 +2336,7 @@
shopDataDuplicateTotal = 0;
list.innerHTML = '<div class="shop-data-empty-hint">加载失败:' + escapeHtml(error.message || '') + '</div>';
document.getElementById('shopDataDuplicateTotal').textContent = '';
renderShopDataDuplicateDetailCards();
loadShopDataDuplicateDetail(1);
})
.finally(function () {
shopDataDuplicateLoading = false;
@@ -2274,6 +2355,7 @@
monitorTab.setAttribute('aria-selected', monitorActive ? 'true' : 'false');
allTab.classList.toggle('active', !monitorActive);
allTab.setAttribute('aria-selected', !monitorActive ? 'true' : 'false');
shopDataDuplicateDetailPage = 1;
loadShopDataDuplicateCheckItems(1);
}
@@ -2503,10 +2585,14 @@
loadShopDataCrawlTasks(1);
};
document.getElementById('btnRefreshShopDataDuplicates').onclick = function () { loadShopDataDuplicateCheckOverview(true); };
document.getElementById('btnFilterShopDataDuplicates').onclick = function () { loadShopDataDuplicateCheckItems(1); };
document.getElementById('btnFilterShopDataDuplicates').onclick = function () {
shopDataDuplicateDetailPage = 1;
loadShopDataDuplicateCheckItems(1);
};
document.getElementById('btnResetShopDataDuplicates').onclick = function () {
['shopDataDupFilterAsin', 'shopDataDupFilterShop', 'shopDataDupFilterCountry', 'shopDataDupFilterSite', 'shopDataDupFilterDateFrom', 'shopDataDupFilterDateTo']
.forEach(function (id) { document.getElementById(id).value = ''; });
shopDataDuplicateDetailPage = 1;
loadShopDataDuplicateCheckItems(1);
};
document.getElementById('dupCheckTabMonitor').onclick = function () { switchDupCheckView('monitor'); };