feat: 外观专利改直连 LLM(DeepSeek/Gemini)、无效ASIN菜单更名 BRAND_DB、紫鸟店铺索引场景化
Build Backend JAR / build (push) Has been cancelled

- AppearancePatent:Coze 工作流切换为 llm-host 直连模型(title/appearance 双模型、行并发、重试),配置键迁移并保留旧环境变量兜底
- InvalidAsinDataMapper 补充按值+品牌唯一键查询
- ZiniaoShopIndexService 新增店铺分类/索引场景
- V98:无效ASIN菜单 rename 补充 BRAND_DB;docs 架构优化规划
- 配套单测更新
This commit is contained in:
2026-08-31 17:02:59 +08:00
parent 14a41e64f1
commit c8cd851b2f
20 changed files with 1291 additions and 2338 deletions
+45 -1
View File
@@ -1359,7 +1359,7 @@ _SHOP_DATA_CRAWL_ADMIN_COLUMNS = f"""
r.result_file_size, r.result_content_type, r.row_count,
r.success AS result_success, r.error_message AS result_error,
r.created_at AS result_created_at,
t.task_no, t.status AS task_status, t.request_json, t.result_json,
t.task_no, t.status AS task_status, t.request_json,
t.error_message AS task_error, t.created_at, t.updated_at, t.finished_at,
{_SHOP_DATA_CRAWL_LATEST_TIME_SQL} AS latest_file_updated_at,
df.country_codes_json, COALESCE(df.row_count, r.row_count) AS row_count_display,
@@ -3395,6 +3395,19 @@ def delete_invalid_asin_data(item_id):
# ---------- 店铺管理 ----------
def _format_shop_manage_item(item):
latest_check = item.get('latestCheck')
if isinstance(latest_check, dict):
latest_check = {
'id': latest_check.get('id'),
'status': latest_check.get('status') or '',
'detail': latest_check.get('detail') or '',
'client_host': latest_check.get('clientHost') or '',
'try_requested_at': (latest_check.get('tryRequestedAt') or '').replace('T', ' ')[:19],
'check_started_at': (latest_check.get('checkStartedAt') or '').replace('T', ' ')[:19],
'check_finished_at': (latest_check.get('checkFinishedAt') or '').replace('T', ' ')[:19],
}
else:
latest_check = None
return {
'id': item.get('id'),
'group_id': item.get('groupId'),
@@ -3404,6 +3417,7 @@ def _format_shop_manage_item(item):
'zn_username': item.get('znUsername') or '',
'account': item.get('account') or '',
'password': item.get('passwordMasked') or '',
'latest_check': latest_check,
'created_at': (item.get('createdAt') or '').replace('T', ' ')[:16],
'updated_at': (item.get('updatedAt') or '').replace('T', ' ')[:16],
}
@@ -3483,6 +3497,36 @@ def list_shop_manages():
})
@admin_api.route('/shop-manage/<int:item_id>/credential-check', methods=['POST'])
@login_required
def create_shop_credential_check(item_id):
role, current_row, denied = _ensure_backend_menu_access('shop-manage')
if denied:
return denied
shop_name = (request.args.get('shop_name') or '').strip()
if not shop_name:
return jsonify({'success': False, 'error': '店铺名不能为空'}), 400
internal_token = _resolve_internal_token()
if not internal_token:
return jsonify({'success': False, 'error': '内部凭据服务未配置'}), 503
result, error_response, status = _proxy_backend_java(
'POST',
'/api/admin/shop-credential-checks',
json_data={'shopName': shop_name},
headers={'X-Internal-Token': internal_token},
)
if error_response is not None:
return error_response, status
check = result.get('data') or {}
return jsonify({
'success': True,
'msg': '检测任务已创建,客户端将在 1 分钟内执行',
'check': {'id': check.get('id'), 'status': check.get('status') or 'PENDING'},
})
@admin_api.route('/shop-manage/<int:item_id>/credential')
@login_required
def get_shop_manage_credential(item_id):
+58 -3
View File
@@ -2777,7 +2777,7 @@
document.getElementById('editDedupeTotalDataModal').classList.remove('show');
};
// ========== 不符合ASIN数据 ==========
// ========== 品牌数据 ==========
var invalidAsinDataPage = 1, invalidAsinDataPageSize = 15;
function buildInvalidAsinDataQuery(page) {
var q = 'page=' + (page || 1) + '&page_size=' + invalidAsinDataPageSize;
@@ -3128,11 +3128,40 @@
function renderShopPasswordCell(item) {
var maskedPassword = item.password || '******';
return '<span class="shop-password-cell">' +
'<span class="shop-password-value" data-shop-password-value>' + escapeHtml(maskedPassword) + '</span>' +
'<span class="shop-password-value" data-shop-password-value title="' + escapeHtml(maskedPassword) + '">' + escapeHtml(maskedPassword) + '</span>' +
'<button type="button" class="shop-password-toggle" data-shop-password-toggle="' + escapeHtml(item.id) + '" data-shop-name="' + escapeHtml(item.shop_name || '') + '" data-masked-password="' + escapeHtml(maskedPassword) + '" aria-label="显示密码" aria-pressed="false" title="显示密码">' +
shopPasswordIcon(false) + '</button></span>';
}
function renderShopCheckBadge(check) {
if (!check) return '';
var map = {
'SUCCESS': ['ok', '密码正确'],
'FAILED': ['bad', '密码错误'],
'RUNNING': ['run', '检测中'],
'PENDING': ['wait', '等待客户端'],
'NO_NEED_LOGIN': ['warn', '已登录态'],
'ERROR': ['bad', '检测异常']
};
var entry = map[check.status] || ['wait', check.status || '未知'];
var tipText = [check.status, check.detail, check.check_finished_at].filter(Boolean).join(' · ');
return '<div class="shop-check-badge ' + entry[0] + '" title="' + escapeHtml(tipText) + '">' + escapeHtml(entry[1]) + '</div>';
}
var shopCheckPollTimer = null;
function startShopCheckPolling() {
if (shopCheckPollTimer) return;
var ticks = 0;
shopCheckPollTimer = setInterval(function () {
ticks += 1;
loadShopManage(shopManagePage);
if (ticks >= 9) {
clearInterval(shopCheckPollTimer);
shopCheckPollTimer = null;
}
}, 20000);
}
function renderShopTableText(value, fallback) {
var text = String(value == null ? '' : value).trim();
var shown = text || fallback || '-';
@@ -3162,11 +3191,12 @@
'<td class="shop-col-mall">' + renderShopTableText(item.mall_name) + '</td>' +
'<td class="shop-col-automation">' + renderShopTableText(item.zn_username) + '</td>' +
'<td class="shop-col-account">' + renderShopTableText(item.account) + '</td>' +
'<td class="shop-col-password">' + renderShopPasswordCell(item) + '</td>' +
'<td class="shop-col-password">' + renderShopPasswordCell(item) + renderShopCheckBadge(item.latest_check) + '</td>' +
'<td class="shop-col-created">' + renderShopTableText(item.created_at) + '</td>' +
'<td class="shop-col-updated">' + renderShopTableText(item.updated_at) + '</td>' +
'<td class="shop-col-actions">' +
'<button type="button" class="btn btn-sm" data-shop-manage-edit="' + escapeHtml(item.id) + '" data-shop-manage="' + (JSON.stringify(item).replace(/"/g, '&quot;')) + '">编辑</button> ' +
'<button type="button" class="btn btn-sm btn-check" data-shop-credential-check="' + escapeHtml(item.id) + '" data-shop-check-name="' + escapeHtml(item.shop_name || '') + '">检测密码</button> ' +
'<button type="button" class="btn btn-sm btn-danger" data-shop-manage-delete="' + escapeHtml(item.id) + '" data-shop-manage-name="' + escapeHtml(item.shop_name || '') + '">删除</button>' +
'</td></tr>';
}).join('');
@@ -3186,6 +3216,7 @@
var revealed = btn.dataset.revealed === 'true';
if (revealed) {
valueEl.textContent = btn.dataset.maskedPassword || '******';
valueEl.title = btn.dataset.maskedPassword || '******';
btn.dataset.revealed = 'false';
btn.setAttribute('aria-label', '显示密码');
btn.setAttribute('aria-pressed', 'false');
@@ -3201,6 +3232,7 @@
.then(function (res) {
if (!res.success) throw new Error(res.error || '读取密码失败');
valueEl.textContent = res.password || '';
valueEl.title = res.password || '';
btn.dataset.revealed = 'true';
btn.setAttribute('aria-label', '隐藏密码');
btn.setAttribute('aria-pressed', 'true');
@@ -3245,6 +3277,29 @@
});
};
});
document.querySelectorAll('[data-shop-credential-check]').forEach(function (btn) {
btn.onclick = function () {
var name = (btn.dataset.shopCheckName || '').replace(/&quot;/g, '"');
btn.disabled = true;
btn.textContent = '已提交...';
fetch('/api/admin/shop-manage/' + encodeURIComponent(btn.dataset.shopCredentialCheck) + '/credential-check?shop_name=' + encodeURIComponent(name), { method: 'POST' })
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.success) {
alert(res.msg || '检测任务已创建,客户端将在 1 分钟内执行');
startShopCheckPolling();
loadShopManage(shopManagePage);
} else {
alert(res.error || '发起检测失败');
}
})
.catch(function () { alert('发起检测失败'); })
.finally(function () {
btn.disabled = false;
btn.textContent = '检测密码';
});
};
});
}
function getInvalidAsinDataLockedGroupId() {
+41 -11
View File
@@ -1048,6 +1048,34 @@
pointer-events: none;
}
.shop-check-badge {
display: inline-block;
margin-left: 8px;
padding: 1px 7px;
border-radius: 9px;
font-size: 11px;
line-height: 17px;
white-space: nowrap;
cursor: default;
}
.shop-check-badge.ok { color: #067647; background: #e6f4ea; border: 1px solid #b7e0c3; }
.shop-check-badge.bad { color: #b42318; background: #fee4e2; border: 1px solid #fecdca; }
.shop-check-badge.run { color: #175cd3; background: #eaf2ff; border: 1px solid #b8d2ff; }
.shop-check-badge.wait { color: #667085; background: #f2f4f7; border: 1px solid #d0d5dd; }
.shop-check-badge.warn { color: #b54708; background: #fef0c7; border: 1px solid #fedf89; }
.btn-check {
color: #5158d9;
border-color: #c7cbfa;
}
.btn-check:hover:not(:disabled) {
color: #fff;
background: #5158d9;
border-color: #5158d9;
}
.dedupe-group-access {
display: flex;
align-items: center;
@@ -3478,20 +3506,22 @@
#panel-dedupe-total-data .dedupe-table-scroll th:nth-child(6) { width: 16%; }
#panel-dedupe-total-data .dedupe-table-scroll th:nth-child(7) { width: 9%; }
.shop-manage-table-scroll > table { min-width: 1220px; table-layout: fixed; }
.shop-manage-table-scroll > table { min-width: 1360px; table-layout: fixed; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(1) { width: 58px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(2) { width: 116px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(3) { width: 126px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(4) { width: 230px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(5) { width: 150px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(6) { width: 190px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(7) { width: 108px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(4) { width: 210px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(5) { width: 140px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(6) { width: 180px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(7) { width: 152px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(8), #panel-shop-manage .shop-manage-table-scroll th:nth-child(9) { width: 132px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(10) { width: 150px; }
#panel-shop-manage .shop-manage-table-scroll th:nth-child(10) { width: 200px; }
#panel-shop-manage .shop-manage-table-scroll td { overflow: hidden; }
.table-ellipsis { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shop-password-cell { max-width: 100%; }
.shop-password-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shop-col-password { min-width: 0; }
.shop-col-actions { white-space: nowrap; }
.category-table-scroll > table { min-width: 860px; }
.category-table-scroll td { vertical-align: middle; }
@@ -3887,7 +3917,7 @@
</button>
<div class="menu-group-body">
<button class="tab" type="button" data-tab="dedupe-total-data" aria-controls="panel-dedupe-total-data"><span class="adm-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"></path><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"></path><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"></path></svg></span><span class="adm-label">去重数据汇总</span></button>
<button class="tab" type="button" data-tab="invalid-asin-data" aria-controls="panel-invalid-asin-data"><span class="adm-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span><span class="adm-label">无效ASIN数据</span></button>
<button class="tab" type="button" data-tab="invalid-asin-data" aria-controls="panel-invalid-asin-data"><span class="adm-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg></span><span class="adm-label">品牌数据</span></button>
<button class="tab" type="button" data-tab="query-asin" aria-controls="panel-query-asin"><span class="adm-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg></span><span class="adm-label">查询 ASIN</span></button>
<button class="tab" type="button" data-tab="product-categories" aria-controls="panel-product-categories"><span class="adm-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"></path></svg></span><span class="adm-label">商品类目</span></button>
</div>
@@ -4302,7 +4332,7 @@
<!-- 不符合ASIN数据 -->
<div id="panel-invalid-asin-data" class="tab-panel">
<div class="form-box">
<h3 style="margin-bottom:16px;">新增无效ASIN数据</h3>
<h3 style="margin-bottom:16px;">新增品牌数据</h3>
<div class="form-row">
<div class="form-group" style="min-width:220px;">
<label>ASIN</label>
@@ -4326,7 +4356,7 @@
<p class="msg" id="msgInvalidAsinData"></p>
</div>
<div class="panel-box">
<h3 style="margin-bottom:16px;">无效ASIN数据列表</h3>
<h3 style="margin-bottom:16px;">品牌数据列表</h3>
<div class="form-row" style="margin-bottom:16px;">
<div class="form-group" style="min-width:220px;">
<label>ASIN / 品牌(模糊搜索)</label>
@@ -4970,7 +5000,7 @@
<!-- 编辑不符合ASIN数据弹窗 -->
<div class="modal-mask" id="editInvalidAsinDataModal">
<div class="modal">
<h3>编辑无效ASIN数据</h3>
<h3>编辑品牌数据</h3>
<input type="hidden" id="editInvalidAsinDataId">
<input type="hidden" id="editInvalidAsinDataRecordSource">
<div class="form-group">
@@ -5415,7 +5445,7 @@
window.__initAdminMenuCollapse();
})();
</script>
<script src="/static/admin.js?v=admin-menu-v2"></script>
<script src="/static/admin.js?v=admin-menu-v3"></script>
<div class="admin-toast-region" id="adminToastRegion" role="status" aria-live="polite" aria-atomic="true"></div>
<div class="admin-confirm-mask" id="adminConfirmModal" aria-hidden="true">
<section class="admin-confirm-dialog" role="alertdialog" aria-modal="true" aria-labelledby="adminConfirmTitle" aria-describedby="adminConfirmMessage">