tt-help-cli-ycl 1.4.18 → 1.4.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/recheck.js +381 -0
- package/src/cli/tag.js +1 -1
- package/src/lib/args.js +64 -7
- package/src/lib/constants.js +11 -0
- package/src/main.js +3 -0
- package/src/watch/data-store.js +270 -9
- package/src/watch/db-tags.js +6 -6
- package/src/watch/public/app.js +247 -1
- package/src/watch/public/index.html +2 -0
- package/src/watch/public/style.css +11 -0
- package/src/watch/server.js +95 -0
package/src/watch/public/app.js
CHANGED
|
@@ -21,6 +21,7 @@ let currentRawLocation = "";
|
|
|
21
21
|
let currentSort = { key: null, asc: true };
|
|
22
22
|
let prevStatValues = {};
|
|
23
23
|
let prevUserMap = {};
|
|
24
|
+
let prevTargetUsers = 0;
|
|
24
25
|
|
|
25
26
|
async function fetchStats() {
|
|
26
27
|
try {
|
|
@@ -164,6 +165,13 @@ function renderStats() {
|
|
|
164
165
|
statJobsBase.textContent = `jobs_base: ${formatStatNum(d.jobsBaseTotal || 0)}`;
|
|
165
166
|
flashEl("statError", d.errorUsers);
|
|
166
167
|
flashEl("statTarget", d.targetUsers, { full: true });
|
|
168
|
+
// 检测目标商家新增,放烟花(仅首页)
|
|
169
|
+
const hash = window.location.hash.replace(/^#/, "");
|
|
170
|
+
if (!hash && prevTargetUsers > 0 && d.targetUsers > prevTargetUsers) {
|
|
171
|
+
const diff = d.targetUsers - prevTargetUsers;
|
|
172
|
+
triggerFirework(diff);
|
|
173
|
+
}
|
|
174
|
+
prevTargetUsers = d.targetUsers || 0;
|
|
167
175
|
flashEl("statUserUpdateTasks", d.userUpdateTasks || 0, { full: true });
|
|
168
176
|
flashEl("statRawJobs", d.rawJobs || 0);
|
|
169
177
|
flashEl("statCommentTask", d.commentTaskCount || 0, { full: true });
|
|
@@ -1464,6 +1472,7 @@ let currentTagsSeq = 0;
|
|
|
1464
1472
|
const TAGS_PAGE_SIZE = 200;
|
|
1465
1473
|
let cachedTagCountries = [];
|
|
1466
1474
|
let currentRefreshHours = 24;
|
|
1475
|
+
let currentTargetRefreshHours = 24;
|
|
1467
1476
|
|
|
1468
1477
|
function showTagsPage() {
|
|
1469
1478
|
document.getElementById("mainPage").classList.add("hidden");
|
|
@@ -1801,6 +1810,111 @@ async function selectRefreshHours(hours) {
|
|
|
1801
1810
|
}
|
|
1802
1811
|
}
|
|
1803
1812
|
|
|
1813
|
+
// ===== 目标商家刷新进度(复用 tags 页面的 UI 模式)=====
|
|
1814
|
+
|
|
1815
|
+
async function loadTargetRefreshStats() {
|
|
1816
|
+
try {
|
|
1817
|
+
const res = await fetch(
|
|
1818
|
+
`/api/targets/refresh-stats?hours=${currentTargetRefreshHours}`,
|
|
1819
|
+
);
|
|
1820
|
+
const data = await res.json();
|
|
1821
|
+
renderTargetRefreshStats(data);
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
console.error("获取目标商家刷新统计失败:", e);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function renderTargetRefreshStats(data) {
|
|
1828
|
+
const el = document.getElementById("targetRefreshStats");
|
|
1829
|
+
if (!el || !data || !data.countries || data.countries.length === 0) {
|
|
1830
|
+
if (el) el.innerHTML = "";
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const pct =
|
|
1834
|
+
data.total > 0 ? Math.round((data.refreshed / data.total) * 100) : 0;
|
|
1835
|
+
const barW = Math.min(pct, 100);
|
|
1836
|
+
|
|
1837
|
+
const HOUR_OPTIONS = [12, 24, 36, 48, 72];
|
|
1838
|
+
let html = `<div style="background:#1a1a2e;border:1px solid #2a2a3a;border-radius:10px;padding:16px">
|
|
1839
|
+
<div id="targetRefreshStatsHeader" style="display:flex;align-items:center;gap:16px;cursor:pointer;user-select:none">
|
|
1840
|
+
<span style="font-size:14px;font-weight:600;color:#e0e0e0;position:relative">
|
|
1841
|
+
📊 最近 <span id="targetRefreshHoursLabel" style="color:#60a5fa;cursor:pointer;text-decoration:underline dotted" onclick="event.stopPropagation();toggleTargetHoursDropdown(event)">${currentTargetRefreshHours}h</span> 刷新进度
|
|
1842
|
+
<div id="targetRefreshHoursDropdown" style="display:none;position:absolute;top:100%;left:0;margin-top:4px;background:#1e1e3a;border:1px solid #3b82f6;border-radius:8px;padding:4px 0;z-index:100;box-shadow:0 4px 16px rgba(0,0,0,0.5);min-width:60px">
|
|
1843
|
+
${HOUR_OPTIONS.map((h) => `<div class="hours-option" data-hours="${h}" style="padding:6px 14px;cursor:pointer;font-size:13px;color:${h === currentTargetRefreshHours ? "#60a5fa" : "#ccc"};font-weight:${h === currentTargetRefreshHours ? "600" : "400"};white-space:nowrap" onclick="event.stopPropagation();selectTargetRefreshHours(${h})">${h}h</div>`).join("")}
|
|
1844
|
+
</div>
|
|
1845
|
+
</span>
|
|
1846
|
+
<span style="font-size:13px;color:#9ca3af">${data.refreshed} / ${data.total}</span>
|
|
1847
|
+
<div style="flex:1;max-width:200px;height:8px;background:#2a2a3a;border-radius:4px;overflow:hidden">
|
|
1848
|
+
<div style="height:100%;width:${barW}%;background:${pct >= 50 ? "#22c55e" : "#facc15"};border-radius:4px;transition:width 0.3s"></div>
|
|
1849
|
+
</div>
|
|
1850
|
+
<span style="font-size:14px;font-weight:700;color:${pct >= 50 ? "#22c55e" : "#facc15"}">${pct}%</span>
|
|
1851
|
+
<span id="targetRefreshStatsArrow" style="color:#888;font-size:12px">▶</span>
|
|
1852
|
+
</div>
|
|
1853
|
+
<div id="targetRefreshStatsBody" style="display:none;margin-top:12px">
|
|
1854
|
+
<table style="width:100%;border-collapse:collapse;font-size:12px">
|
|
1855
|
+
<thead><tr style="color:#888;text-align:left">
|
|
1856
|
+
<th style="padding:4px 8px">国家</th>
|
|
1857
|
+
<th style="padding:4px 8px;text-align:right">商家数</th>
|
|
1858
|
+
<th style="padding:4px 8px;text-align:right">${currentTargetRefreshHours}h已刷</th>
|
|
1859
|
+
<th style="padding:4px 8px;width:120px">进度</th>
|
|
1860
|
+
</tr></thead>
|
|
1861
|
+
<tbody>`;
|
|
1862
|
+
|
|
1863
|
+
for (const c of data.countries) {
|
|
1864
|
+
const cpct = c.total > 0 ? Math.round((c.refreshed / c.total) * 100) : 0;
|
|
1865
|
+
const cBarW = Math.min(cpct, 100);
|
|
1866
|
+
html += `<tr style="color:#ccc;border-top:1px solid #222">
|
|
1867
|
+
<td style="padding:4px 8px">${c.country}</td>
|
|
1868
|
+
<td style="padding:4px 8px;text-align:right">${c.total}</td>
|
|
1869
|
+
<td style="padding:4px 8px;text-align:right;color:${cpct >= 50 ? "#22c55e" : "#facc15"}">${c.refreshed}</td>
|
|
1870
|
+
<td style="padding:4px 8px">
|
|
1871
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
1872
|
+
<div style="flex:1;height:6px;background:#2a2a3a;border-radius:3px;overflow:hidden">
|
|
1873
|
+
<div style="height:100%;width:${cBarW}%;background:${cpct >= 50 ? "#22c55e" : "#facc15"};border-radius:3px"></div>
|
|
1874
|
+
</div>
|
|
1875
|
+
<span style="font-size:11px;color:#9ca3af;min-width:28px;text-align:right">${cpct}%</span>
|
|
1876
|
+
</div>
|
|
1877
|
+
</td>
|
|
1878
|
+
</tr>`;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
html += `</tbody></table></div></div>`;
|
|
1882
|
+
el.innerHTML = html;
|
|
1883
|
+
|
|
1884
|
+
// 绑定展开/折叠
|
|
1885
|
+
const header = document.getElementById("targetRefreshStatsHeader");
|
|
1886
|
+
const body = document.getElementById("targetRefreshStatsBody");
|
|
1887
|
+
const arrow = document.getElementById("targetRefreshStatsArrow");
|
|
1888
|
+
if (header && body && arrow) {
|
|
1889
|
+
header.addEventListener("click", () => {
|
|
1890
|
+
const open = body.style.display !== "none";
|
|
1891
|
+
body.style.display = open ? "none" : "";
|
|
1892
|
+
arrow.textContent = open ? "▶" : "▼";
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function toggleTargetHoursDropdown(event) {
|
|
1898
|
+
const dropdown = document.getElementById("targetRefreshHoursDropdown");
|
|
1899
|
+
if (!dropdown) return;
|
|
1900
|
+
const isOpen = dropdown.style.display !== "none";
|
|
1901
|
+
dropdown.style.display = isOpen ? "none" : "block";
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
async function selectTargetRefreshHours(hours) {
|
|
1905
|
+
const dropdown = document.getElementById("targetRefreshHoursDropdown");
|
|
1906
|
+
if (dropdown) dropdown.style.display = "none";
|
|
1907
|
+
if (hours === currentTargetRefreshHours) return;
|
|
1908
|
+
currentTargetRefreshHours = hours;
|
|
1909
|
+
try {
|
|
1910
|
+
await loadTargetRefreshStats();
|
|
1911
|
+
const label = document.getElementById("targetRefreshHoursLabel");
|
|
1912
|
+
if (label) label.textContent = hours + "h";
|
|
1913
|
+
} catch (e) {
|
|
1914
|
+
console.error("目标商家刷新统计失败:", e);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1804
1918
|
// 点击空白处关闭下拉
|
|
1805
1919
|
document.addEventListener("click", (e) => {
|
|
1806
1920
|
const dropdown = document.getElementById("refreshHoursDropdown");
|
|
@@ -1844,6 +1958,7 @@ function showTargetPage() {
|
|
|
1844
1958
|
showLoading("正在加载目标商家数据...");
|
|
1845
1959
|
fetchTargetByCountry();
|
|
1846
1960
|
fetchTargetOtherCountries();
|
|
1961
|
+
loadTargetRefreshStats();
|
|
1847
1962
|
// 同步统计
|
|
1848
1963
|
if (currentStats) {
|
|
1849
1964
|
document.getElementById("targetPageStatTotal").textContent = formatStatNum(
|
|
@@ -1860,7 +1975,7 @@ let currentTargetLoading = false;
|
|
|
1860
1975
|
let currentTargetAllLoaded = false;
|
|
1861
1976
|
let currentTargetSeq = 0;
|
|
1862
1977
|
let currentTargetSort = { key: null, asc: true };
|
|
1863
|
-
const TARGET_PAGE_SIZE =
|
|
1978
|
+
const TARGET_PAGE_SIZE = 500;
|
|
1864
1979
|
|
|
1865
1980
|
async function fetchTargetOtherCountries() {
|
|
1866
1981
|
try {
|
|
@@ -2256,6 +2371,7 @@ document
|
|
|
2256
2371
|
const btn = document.getElementById("targetReprocessBtn");
|
|
2257
2372
|
if (btn) btn.style.display = "none";
|
|
2258
2373
|
await fetchTargetByCountry();
|
|
2374
|
+
await loadTargetRefreshStats();
|
|
2259
2375
|
showToast("刷新完成");
|
|
2260
2376
|
} catch (e) {
|
|
2261
2377
|
showToast("刷新失败: " + e.message, true);
|
|
@@ -2876,6 +2992,136 @@ function initTargetTableSorting() {
|
|
|
2876
2992
|
});
|
|
2877
2993
|
}
|
|
2878
2994
|
|
|
2995
|
+
// ========== 烟花/鞭炮动画 ==========
|
|
2996
|
+
function triggerFirework(newCount) {
|
|
2997
|
+
const canvas = document.getElementById("fireworkCanvas");
|
|
2998
|
+
if (!canvas) return;
|
|
2999
|
+
const ctx = canvas.getContext("2d");
|
|
3000
|
+
|
|
3001
|
+
canvas.width = window.innerWidth;
|
|
3002
|
+
canvas.height = window.innerHeight;
|
|
3003
|
+
|
|
3004
|
+
const colors = [
|
|
3005
|
+
"#fe2c55",
|
|
3006
|
+
"#ff6b6b",
|
|
3007
|
+
"#ffd93d",
|
|
3008
|
+
"#6bcb77",
|
|
3009
|
+
"#4d96ff",
|
|
3010
|
+
"#a78bfa",
|
|
3011
|
+
"#ff922b",
|
|
3012
|
+
"#20c997",
|
|
3013
|
+
"#f06595",
|
|
3014
|
+
"#ffe066",
|
|
3015
|
+
"#5c7cfa",
|
|
3016
|
+
"#ff8787",
|
|
3017
|
+
];
|
|
3018
|
+
|
|
3019
|
+
const particles = [];
|
|
3020
|
+
const burstCount = Math.min(newCount, 5) + 2; // 最多 7 朵烟花
|
|
3021
|
+
|
|
3022
|
+
// 创建多朵烟花
|
|
3023
|
+
for (let b = 0; b < burstCount; b++) {
|
|
3024
|
+
const cx = Math.random() * canvas.width * 0.8 + canvas.width * 0.1;
|
|
3025
|
+
const cy = Math.random() * canvas.height * 0.5 + canvas.height * 0.1;
|
|
3026
|
+
const hue = Math.random() * 360;
|
|
3027
|
+
const count = 80 + Math.floor(Math.random() * 60);
|
|
3028
|
+
|
|
3029
|
+
for (let i = 0; i < count; i++) {
|
|
3030
|
+
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.3;
|
|
3031
|
+
const speed = 2 + Math.random() * 6;
|
|
3032
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
3033
|
+
particles.push({
|
|
3034
|
+
x: cx,
|
|
3035
|
+
y: cy,
|
|
3036
|
+
vx: Math.cos(angle) * speed,
|
|
3037
|
+
vy: Math.sin(angle) * speed,
|
|
3038
|
+
life: 1,
|
|
3039
|
+
decay: 0.008 + Math.random() * 0.02,
|
|
3040
|
+
size: 1.5 + Math.random() * 2.5,
|
|
3041
|
+
color,
|
|
3042
|
+
gravity: 0.04,
|
|
3043
|
+
});
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
let animFrame;
|
|
3048
|
+
function animate() {
|
|
3049
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3050
|
+
|
|
3051
|
+
let alive = false;
|
|
3052
|
+
for (const p of particles) {
|
|
3053
|
+
p.x += p.vx;
|
|
3054
|
+
p.y += p.vy;
|
|
3055
|
+
p.vy += p.gravity;
|
|
3056
|
+
p.life -= p.decay;
|
|
3057
|
+
|
|
3058
|
+
if (p.life > 0) {
|
|
3059
|
+
alive = true;
|
|
3060
|
+
ctx.save();
|
|
3061
|
+
ctx.globalAlpha = p.life;
|
|
3062
|
+
ctx.fillStyle = p.color;
|
|
3063
|
+
ctx.shadowColor = p.color;
|
|
3064
|
+
ctx.shadowBlur = 6;
|
|
3065
|
+
ctx.beginPath();
|
|
3066
|
+
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
|
|
3067
|
+
ctx.fill();
|
|
3068
|
+
ctx.restore();
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
if (alive) {
|
|
3073
|
+
animFrame = requestAnimationFrame(animate);
|
|
3074
|
+
} else {
|
|
3075
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
if (animFrame) cancelAnimationFrame(animFrame);
|
|
3080
|
+
animate();
|
|
3081
|
+
|
|
3082
|
+
// 显示提示文字
|
|
3083
|
+
showFireworkToast(newCount);
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function showFireworkToast(count) {
|
|
3087
|
+
let toast = document.getElementById("firework-toast");
|
|
3088
|
+
if (!toast) {
|
|
3089
|
+
toast = document.createElement("div");
|
|
3090
|
+
toast.id = "firework-toast";
|
|
3091
|
+
toast.style.cssText = `
|
|
3092
|
+
position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
|
|
3093
|
+
font-size:48px;font-weight:900;z-index:10000;pointer-events:none;
|
|
3094
|
+
color:#ffd93d;text-shadow:0 0 30px #fe2c55,0 0 60px #fe2c55,0 0 90px #ff6b6b;
|
|
3095
|
+
animation:fireworkToast 2.5s ease-out forwards;
|
|
3096
|
+
white-space:nowrap;
|
|
3097
|
+
`;
|
|
3098
|
+
document.body.appendChild(toast);
|
|
3099
|
+
// 注入动画 keyframes
|
|
3100
|
+
if (!document.getElementById("fw-style")) {
|
|
3101
|
+
const style = document.createElement("style");
|
|
3102
|
+
style.id = "fw-style";
|
|
3103
|
+
style.textContent = `
|
|
3104
|
+
@keyframes fireworkToast {
|
|
3105
|
+
0% { opacity:0; transform:translate(-50%,-50%) scale(0.3); }
|
|
3106
|
+
15% { opacity:1; transform:translate(-50%,-50%) scale(1.2); }
|
|
3107
|
+
25% { opacity:1; transform:translate(-50%,-50%) scale(1); }
|
|
3108
|
+
80% { opacity:1; transform:translate(-50%,-50%) scale(1); }
|
|
3109
|
+
100% { opacity:0; transform:translate(-50%,-60%) scale(1.5); }
|
|
3110
|
+
}
|
|
3111
|
+
`;
|
|
3112
|
+
document.head.appendChild(style);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
toast.textContent = `🎆 +${count} 目标商家!`;
|
|
3116
|
+
toast.style.animation = "none";
|
|
3117
|
+
void toast.offsetWidth;
|
|
3118
|
+
toast.style.animation = "fireworkToast 2.5s ease-out forwards";
|
|
3119
|
+
|
|
3120
|
+
setTimeout(() => {
|
|
3121
|
+
if (toast.parentNode) toast.remove();
|
|
3122
|
+
}, 2600);
|
|
3123
|
+
}
|
|
3124
|
+
|
|
2879
3125
|
// 初始化
|
|
2880
3126
|
fetchStats();
|
|
2881
3127
|
fetchUsers();
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
</head>
|
|
10
10
|
|
|
11
11
|
<body>
|
|
12
|
+
<canvas id="fireworkCanvas"></canvas>
|
|
12
13
|
<div class="header">
|
|
13
14
|
<h1>TikTok 采集监控</h1>
|
|
14
15
|
<div class="meta" id="fileMeta">加载中...</div>
|
|
@@ -325,6 +326,7 @@
|
|
|
325
326
|
<div class="label">🔄 刷新</div>
|
|
326
327
|
</div>
|
|
327
328
|
</div>
|
|
329
|
+
<div id="targetRefreshStats" style="margin-bottom:16px"></div>
|
|
328
330
|
<div class="target-page-layout">
|
|
329
331
|
<div class="target-side-card">
|
|
330
332
|
<h3>🎯 目标商家国家分布</h3>
|
|
@@ -1612,3 +1612,14 @@ td.user-id:hover {
|
|
|
1612
1612
|
.checkbox-label input[type="checkbox"] {
|
|
1613
1613
|
accent-color: #a78bfa;
|
|
1614
1614
|
}
|
|
1615
|
+
|
|
1616
|
+
/* ========== 烟花画布 ========== */
|
|
1617
|
+
#fireworkCanvas {
|
|
1618
|
+
position: fixed;
|
|
1619
|
+
top: 0;
|
|
1620
|
+
left: 0;
|
|
1621
|
+
width: 100%;
|
|
1622
|
+
height: 100%;
|
|
1623
|
+
pointer-events: none;
|
|
1624
|
+
z-index: 9999;
|
|
1625
|
+
}
|
package/src/watch/server.js
CHANGED
|
@@ -92,6 +92,7 @@ function sendCSV(res, columns, rows) {
|
|
|
92
92
|
function inferClientType(routePath) {
|
|
93
93
|
if (routePath.startsWith("/api/redo-job")) return "refresh";
|
|
94
94
|
if (routePath.startsWith("/api/user-update-tasks")) return "attach";
|
|
95
|
+
if (routePath.startsWith("/api/recheck-jobs")) return "recheck";
|
|
95
96
|
if (routePath.startsWith("/api/comment-task")) return "comments";
|
|
96
97
|
if (routePath.startsWith("/api/tags/refresh")) return "tag-refresh";
|
|
97
98
|
if (routePath.startsWith("/api/tags")) return "scoring";
|
|
@@ -482,6 +483,30 @@ export function startWatchServer(
|
|
|
482
483
|
return;
|
|
483
484
|
}
|
|
484
485
|
|
|
486
|
+
// 将 jobs 中 status='done' 且无国家数据的记录移动到 jobs_base
|
|
487
|
+
if (
|
|
488
|
+
req.method === "POST" &&
|
|
489
|
+
routePath === "/api/jobs/move-no-country-to-base"
|
|
490
|
+
) {
|
|
491
|
+
try {
|
|
492
|
+
const result = store.moveNoCountryJobsToBase();
|
|
493
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
494
|
+
if (result.ok) {
|
|
495
|
+
console.error(
|
|
496
|
+
`[JOB ${ts}] MOVE-NO-COUNTRY-TO-BASE: moved=${result.moved}, total=${result.total}`,
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
console.error(
|
|
500
|
+
`[JOB ${ts}] MOVE-NO-COUNTRY-TO-BASE ERROR: ${result.error}`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
sendJSON(res, result.ok ? 200 : 400, result);
|
|
504
|
+
} catch (e) {
|
|
505
|
+
sendJSON(res, 500, { error: e.message });
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
485
510
|
// 将 jobs/raw_jobs 中 tt_seller=1 且 video_count=0 的记录移动到 jobs_base
|
|
486
511
|
if (
|
|
487
512
|
req.method === "POST" &&
|
|
@@ -730,6 +755,47 @@ export function startWatchServer(
|
|
|
730
755
|
return;
|
|
731
756
|
}
|
|
732
757
|
|
|
758
|
+
// ── recheck-jobs 端点 ──
|
|
759
|
+
if (req.method === "GET" && routePath === "/api/recheck-jobs") {
|
|
760
|
+
const limit = params.limit;
|
|
761
|
+
const countries = params.countries
|
|
762
|
+
? params.countries
|
|
763
|
+
.split(",")
|
|
764
|
+
.map((c) => c.trim().toUpperCase())
|
|
765
|
+
.filter(Boolean)
|
|
766
|
+
: [];
|
|
767
|
+
const tasks = store.getRecheckJobs(limit, countries);
|
|
768
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
769
|
+
console.error(
|
|
770
|
+
`[JOB ${ts}] RECHECK-JOBS: ${tasks.length} tasks${countries.length ? ` (countries: ${countries.join(",")})` : ""}`,
|
|
771
|
+
);
|
|
772
|
+
sendJSON(res, 200, { total: tasks.length, tasks });
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (req.method === "POST" && routePath === "/api/recheck-jobs-batch") {
|
|
777
|
+
try {
|
|
778
|
+
const body = await readBody(req);
|
|
779
|
+
const updates = body.updates || [];
|
|
780
|
+
const { results } = store.batchUpdateRecheckJobs(updates);
|
|
781
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
782
|
+
const errCount = results.filter((r) => r.error).length;
|
|
783
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
784
|
+
console.error(
|
|
785
|
+
`[JOB ${ts}] RECHECK-JOBS-BATCH: ${okCount} ok, ${errCount} error (total=${updates.length})`,
|
|
786
|
+
);
|
|
787
|
+
sendJSON(res, 200, {
|
|
788
|
+
results,
|
|
789
|
+
total: updates.length,
|
|
790
|
+
ok: okCount,
|
|
791
|
+
error: errCount,
|
|
792
|
+
});
|
|
793
|
+
} catch (e) {
|
|
794
|
+
sendJSON(res, 400, { error: e.message });
|
|
795
|
+
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
733
799
|
const userInfoCommitMatch = routePath.match(
|
|
734
800
|
/^\/api\/user-info\/([^/]+)$/,
|
|
735
801
|
);
|
|
@@ -1047,6 +1113,14 @@ export function startWatchServer(
|
|
|
1047
1113
|
return;
|
|
1048
1114
|
}
|
|
1049
1115
|
|
|
1116
|
+
// GET /api/targets/refresh-stats — 目标商家最近 N 小时刷新进度
|
|
1117
|
+
if (req.method === "GET" && routePath === "/api/targets/refresh-stats") {
|
|
1118
|
+
const hours = Math.max(1, parseInt(params.hours) || 24);
|
|
1119
|
+
const data = store.getTargetRefreshStats(hours);
|
|
1120
|
+
sendJSON(res, 200, data);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1050
1124
|
if (req.method === "GET" && routePath === "/api/client-errors") {
|
|
1051
1125
|
sendJSON(res, 200, { clients: store.getClientErrors() });
|
|
1052
1126
|
return;
|
|
@@ -2219,6 +2293,27 @@ export function startWatchServer(
|
|
|
2219
2293
|
);
|
|
2220
2294
|
}
|
|
2221
2295
|
|
|
2296
|
+
// 每 2 小时自动将无国家数据的 done job 移入 jobs_base 重走流程
|
|
2297
|
+
const NO_COUNTRY_MOVE_INTERVAL_MS = 2 * 60 * 60 * 1000;
|
|
2298
|
+
setInterval(() => {
|
|
2299
|
+
try {
|
|
2300
|
+
const result = store.moveNoCountryJobsToBase();
|
|
2301
|
+
const ts = new Date().toISOString().slice(11, 19);
|
|
2302
|
+
if (result.moved > 0) {
|
|
2303
|
+
console.error(
|
|
2304
|
+
`[JOB ${ts}] AUTO-MOVE-NO-COUNTRY: moved=${result.moved}, total=${result.total}`,
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
} catch (e) {
|
|
2308
|
+
console.error(
|
|
2309
|
+
`[JOB] AUTO-MOVE-NO-COUNTRY 定时任务异常: ${e.message}`,
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
}, NO_COUNTRY_MOVE_INTERVAL_MS);
|
|
2313
|
+
console.error(
|
|
2314
|
+
`[job] 无国家数据自动回炉已启动: 间隔 ${NO_COUNTRY_MOVE_INTERVAL_MS / 3600000} 小时`,
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2222
2317
|
_resolve({ server, port });
|
|
2223
2318
|
});
|
|
2224
2319
|
|