REPAI 商品工作台

采集商品、编辑 Ozon 分类属性、人工审核后上传店铺

下载插件
请选择左侧商品

店铺管理 (所有已配置的 Ozon 店铺)

加载中...

通知中心 (上架成功/失败、店铺事件)

暂无通知

Ozon 店铺管理

每个 Ozon 店铺可单独设置:价格倍数、固定库存(所有商品上传时使用此库存)、默认仓库。所有设置在点击"保存店铺设置"后立即生效,后续所有商品一键上架都会按这里设置的上传。

当前选中的店铺

下面这个下拉框决定 Ozon 店铺中心的所有操作目标。切换店铺会自动加载该店铺的仓库、在售产品和最近订单。
正在加载店铺...

编辑店铺 / 添加新店铺

等待操作...

仓库 / 发运方式

未加载

订单数据待加载

未加载

在售产品

未加载

Ozon 选品搜索 公开 API 未配置

全站共 个商品,第 1
输入关键词或选择类目,点击「搜索全站商品」开始选品
就绪 — 输入关键词或类目 ID 即可搜索 Ozon 全平台真实商品

多平台店铺授权

越南语内容上传到 TikTok Shop 越南;繁体中文内容上传到 Shopee 虾皮台湾。先填写开放平台应用参数,再点击官方授权。

TikTok Shop 越南

Shopee 虾皮台湾

等待操作...

图片翻译 API 配置

这里保存后会立即用于“翻译选中图片”和右下角余额查询;输入框留空时保留服务器已保存的密钥。

象寄图片翻译(阿里云版本)

AI 主图 / 详情图 prompt 模板 (支持自定义编辑,留空恢复默认)

这里编辑的 prompt 会用于"AI 生成主图"和"一键上架"中的图片生成。可用占位符: {i} 当前变体序号 · {total} 总数 · {langHint} 目标语言描述。所有 prompt 已内置"严禁虚假宣传"约束,改写时请保留该原则。
详情图 prompt 列表 系统会按顺序循环使用这些 prompt 生成详情图;空字符串会被跳过

Ozon 类目模板(自动学习)

系统会在自动识别属性时把该类目下出现的高频属性值保存为模板;再次识别同类目时优先使用模板里的值,减少乱猜。这里可以查看所有模板,以及根据类目关键字搜索模板。

搜索类目模板

正在加载...
// ============================================================ // \u524d\u7aef patch 2026-08-01: \u591a\u5e97\u94fa\u4e0a\u67b6 + \u901a\u77e5\u4e2d\u5fc3 + \u5e97\u94fa\u7ba1\u7406 // 1) \u4fee\u6539 autoPublishBtn handler -> \u652f\u6301 batchMode // 2) \u65b0\u589e\u5168\u5c40\u901a\u77e5 polling (\u6bcf 5s \u68c0\u67e5 /api/notifications) // 3) \u65b0\u589e 2 \u4e2a Tab: notificationsView + shopsView // 4) \u5728\u4ea7\u54c1\u8be6\u60c5\u589e\u52a0\u591a\u5e97\u94fa batch \u6309\u94ae // ============================================================ // === 1. \u901a\u77e5 polling \u5168\u5c40\u673a\u5236 === let __lastNotificationId = null; let __notificationPollTimer = null; function getCurrentUserId() { try { const t = (localStorage.getItem('repai_token') || localStorage.getItem('repai.jwt') || ''); if (!t) return null; const parts = t.split('.'); if (parts.length < 2) return null; const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload.id || payload.sub || payload.userId || null; } catch (e) { return null; } } async function pollNotifications(silent = false) { try { const url = `/api/notifications?limit=20`; const r = await request(url); if (r.code !== 200) return; const items = r.data || []; // \u53ea\u663e\u793a\u672a\u8bfb\u7684 (readAt is null) const unread = items.filter(x => !x.readAt); if (unread.length === 0) return; const lastId = __lastNotificationId || unread[0]?.id; const newest = unread[0]; if (__lastNotificationId && newest.id !== __lastNotificationId) { // \u6709\u65b0\u901a\u77e5\u5230\u6765 const ok = newest.level === 'success' || newest.level === 'info'; notify(`🔔 ${newest.title}\n${newest.message}`, ok); } __lastNotificationId = newest.id; if (!silent) renderNotifications(items); } catch (e) { // \u9759\u9ed8\u5931\u8d25 } } function startNotificationPolling() { if (__notificationPollTimer) return; pollNotifications(); __notificationPollTimer = setInterval(() => pollNotifications(), 5000); } function renderNotifications(items) { const el = $('#notificationsList'); if (!el) return; if (!items.length) { el.innerHTML = '
暂无通知
'; return; } el.innerHTML = items.map(n => { const level = n.level || 'info'; const cls = level === 'success' ? 'ok' : level === 'error' ? 'err' : level === 'warn' ? 'warn' : ''; const readCls = n.readAt ? 'is-read' : 'is-unread'; const time = new Date(n.createdAt + 'Z').toLocaleString('zh-CN', { hour12: false }); return `
${level === 'success' ? '✓' : level === 'error' ? '✗' : level === 'warn' ? '!' : 'ⓘ'}
${esc(n.title)}
${esc(n.message)}
${time} · ${esc(n.type)}
`; }).join(''); } async function loadNotifications() { const r = await request('/api/notifications?limit=50'); if (r.code === 200) renderNotifications(r.data || []); } // === 2. \u5e97\u94fa\u7ba1\u7406 UI === async function loadShopsList() { const r = await request('/api/admin/ozon-shops/list-full'); const el = $('#shopsList'); if (!el) return; if (r.code !== 200 || !r.data) { el.innerHTML = `
\u52a0\u8f7d\u5931\u8d25: ${esc(r.msg || '\u672a\u77e5\u9519\u8bef')}
`; return; } const shops = r.data; if (!shops.length) { el.innerHTML = '
还没有店铺
'; return; } el.innerHTML = shops.map(s => { const status = s.status || 'active'; const statusBadge = status === 'active' ? '正常' : status === 'error' ? '异常' : `${esc(status)}`; const st = s.stats || { success: 0, failed: 0, pending: 0 }; const lastCheck = s.lastCheckedAt ? new Date(s.lastCheckedAt).toLocaleString('zh-CN', { hour12: false }) : '从未'; return `
${esc(s.name)} ${statusBadge}
clientId: ${esc(s.clientId)} · 仓库: ${esc(s.warehouseId || '未设置')} · 倍率: ${s.priceMultiplier}x
API Key: ${esc(s.apiKey.substring(0, 8))}...${esc(s.apiKey.substring(s.apiKey.length - 4))}
最后检查: ${lastCheck}
✓ 成功 ${st.success || 0} ✗ 失败 ${st.failed || 0} ⏳ 进行 ${st.pending || 0}
`; }).join(''); } async function testShop(id, btn) { notify('\u6b63\u5728\u6d4b\u8bd5\u5e97\u94fa...', true); if (btn) { btn.disabled = true; btn.textContent = '⏳ 测试中'; } try { const r = await request(`/api/admin/ozon-shops/test/${id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }); if (r.code === 200 && r.data?.ok) { notify(`\u2713 \u8fde\u901a\u6210\u529f: ${r.data.duration}ms`, true); } else { notify(`\u2717 \u8fde\u901a\u5931\u8d25: ${r.data?.error || r.msg || '\u672a\u77e5'}`, false); } await loadShopsList(); } catch (e) { notify('\u6d4b\u8bd5\u51fa\u9519: ' + e.message, false); } finally { if (btn) { btn.disabled = false; btn.textContent = '测试'; } } } function editShop(id) { notify('编辑功能开发中... 如需修改请直接修改数据库', false); } async function openBulkPublishDialog() { const currentProduct = window.__currentProduct; if (!currentProduct) { notify('请先选中商品', false); return; } const r = await request('/api/admin/ozon-shops/list-full'); if (r.code !== 200 || !r.data) { notify('加载店铺列表失败', false); return; } const activeShops = r.data.filter(s => s.status === 'active'); if (!activeShops.length) { notify('没有 active 状态的店铺', false); return; } const modal = $('#bulkPublishModal'); if (!modal) { notify('弹窗未初始化', false); return; } modal.querySelector('.bp-product-name').textContent = currentProduct.title; const list = modal.querySelector('.bp-shops'); list.innerHTML = activeShops.map(s => ` `).join(''); modal.querySelector('.bp-count').textContent = activeShops.length; modal.style.display = 'flex'; } async function confirmBulkPublish() { const modal = $('#bulkPublishModal'); const cbs = modal.querySelectorAll('.bp-shop-cb:checked'); const shopIds = Array.from(cbs).map(cb => cb.value); if (!shopIds.length) { notify('请至少选择 1 个店铺', false); return; } const productId = window.__currentProduct?.id; if (!productId) { notify('商品未选中', false); return; } notify(`\u6b63\u5728\u63a8\u9001\u5230 ${shopIds.length} \u4e2a\u5e97\u94fa...`, true); modal.style.display = 'none'; try { const r = await request('/api/admin/auto-publish-batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ productId, shopIds, options: { skipCategoryMatch: true } }) }); if (r.code === 200) { const ok = r.data.tasks.filter(t => t.status === 'queued').length; const fail = r.data.tasks.filter(t => t.status === 'failed').length; notify(`\u5df2\u63a8\u9001 ${ok} \u4e2a\u5e97\u94fa (${fail} \u5931\u8d25)\u3002\u8fdb\u5ea6\u53ef\u5728\u901a\u77e5\u4e2d\u5fc3\u67e5\u770b\u3002`, fail === 0); } else { notify('\u63a8\u9001\u5931\u8d25: ' + r.msg, false); } } catch (e) { notify('\u63a8\u9001\u51fa\u9519: ' + e.message, false); } } // === 3. \u9875\u9762\u521d\u59cb\u5316 === document.addEventListener('DOMContentLoaded', () => { startNotificationPolling(); // \u76d1\u542c tab \u5207\u6362 document.addEventListener('click', e => { const tab = e.target.closest('.tab'); if (tab?.dataset?.view === 'shopsView') { setTimeout(() => loadShopsList(), 50); } else if (tab?.dataset?.view === 'notificationsView') { setTimeout(() => loadNotifications(), 50); } }); // \u521d\u59cb\u52a0\u8f7d\u5e97\u94fa\u5217\u8868 (\u4ee5\u4f9b\u4e0a\u67b6\u5904\u9009\u62e9\u4f7f\u7528) loadShopsList().catch(() => {}); });