tt-help-cli-ycl 1.4.149 → 1.4.151
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/scripts/add-claim-index.mjs +28 -0
- package/scripts/checkpoint-wal.mjs +20 -0
- package/scripts/test-photo-post.mjs +156 -0
- package/scripts/test-precheck-perf.mjs +270 -0
- package/scripts/test-product-fill-cdp.mjs +44 -0
- package/scripts/test-product-fill-db.mjs +43 -0
- package/scripts/test-seller-any-country.mjs +167 -0
- package/src/cli/product-fill.js +13 -1
- package/src/lib/api-interceptor.js +10 -1
- package/src/lib/app-config.js +15 -1
- package/src/lib/browser/cdp.js +2 -0
- package/src/lib/browser/page.js +14 -0
- package/src/lib/shop-scraper.mjs +79 -0
- package/src/lib/target-locations.js +8 -5
- package/src/scraper/explore-core.js +8 -2
- package/src/watch/data-store.js +136 -130
- package/src/watch/db-columns.js +1 -0
- package/src/watch/db-schema.js +17 -5
- package/src/watch/db-tags.js +15 -5
- package/src/watch/public/app.js +285 -50
- package/src/watch/public/index.html +66 -46
- package/src/watch/public/style.css +267 -0
- package/src/watch/server.js +16 -2
package/src/watch/data-store.js
CHANGED
|
@@ -20,7 +20,23 @@ import fs from "fs";
|
|
|
20
20
|
import path from "path";
|
|
21
21
|
import Database from "better-sqlite3";
|
|
22
22
|
import { isLocationInList } from "../lib/target-locations.js";
|
|
23
|
-
import { getTargetLocations } from "../lib/app-config.js";
|
|
23
|
+
import { getTargetLocations, locationRank } from "../lib/app-config.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 按目标国家配置顺序解析领取顺序(优先级 = 配置顺序,越靠前越先领)。
|
|
27
|
+
* 传入子集时:配置内国家按配置顺序在前,配置外国家按传入顺序附在后面。
|
|
28
|
+
*/
|
|
29
|
+
function resolveOrderedLocations(normalizedLocations) {
|
|
30
|
+
const configured = getTargetLocations();
|
|
31
|
+
if (!normalizedLocations || normalizedLocations.length === 0)
|
|
32
|
+
return [...configured];
|
|
33
|
+
const configSet = new Set(configured);
|
|
34
|
+
const requestedSet = new Set(normalizedLocations);
|
|
35
|
+
return [
|
|
36
|
+
...configured.filter((loc) => requestedSet.has(loc)),
|
|
37
|
+
...normalizedLocations.filter((loc) => !configSet.has(loc)),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
24
40
|
|
|
25
41
|
// Schema 与连接管理
|
|
26
42
|
import {
|
|
@@ -667,7 +683,13 @@ function getPreciseMerchantsFromDb(targetLocations = [], options = {}) {
|
|
|
667
683
|
product_review_count,
|
|
668
684
|
product_sold_count,
|
|
669
685
|
product_name,
|
|
686
|
+
product_skus,
|
|
670
687
|
recent_ec_videos,
|
|
688
|
+
(CASE WHEN json_valid(product_skus) THEN
|
|
689
|
+
(SELECT COUNT(*) FROM json_each(product_skus)) ELSE 0 END) AS skuCount,
|
|
690
|
+
(CASE WHEN json_valid(product_skus) THEN
|
|
691
|
+
(SELECT COALESCE(SUM(CAST(json_extract(value, '$.stock') AS INTEGER)), 0)
|
|
692
|
+
FROM json_each(product_skus)) ELSE 0 END) AS skuTotalStock,
|
|
671
693
|
(SELECT COUNT(*) FROM json_each(recent_ec_videos)
|
|
672
694
|
WHERE CAST(json_extract(value, '$.playCount') AS INTEGER) > 1000) AS ecOver1k,
|
|
673
695
|
(SELECT MAX(CAST(json_extract(value, '$.playCount') AS INTEGER))
|
|
@@ -925,13 +947,9 @@ export function createStore(filePath, options = {}) {
|
|
|
925
947
|
let statusGroups = null;
|
|
926
948
|
let groupsDirty = true;
|
|
927
949
|
|
|
928
|
-
|
|
929
|
-
const tier2LocSet = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
950
|
+
// 优先级 = 目标国家配置顺序(越靠前越优先),空/配置外国家排最后
|
|
930
951
|
function locationTier(u) {
|
|
931
|
-
|
|
932
|
-
if (tier1LocSet.has(loc)) return 0;
|
|
933
|
-
if (tier2LocSet.has(loc)) return 1;
|
|
934
|
-
return 2;
|
|
952
|
+
return locationRank(u.guessedLocation);
|
|
935
953
|
}
|
|
936
954
|
|
|
937
955
|
function sortGroup(key, arr) {
|
|
@@ -1278,13 +1296,13 @@ export function createStore(filePath, options = {}) {
|
|
|
1278
1296
|
};
|
|
1279
1297
|
}
|
|
1280
1298
|
|
|
1281
|
-
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
1282
|
-
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
1283
1299
|
const normalizedLocations = Array.isArray(locations)
|
|
1284
1300
|
? locations
|
|
1285
1301
|
.map((loc) => String(loc).trim().toUpperCase())
|
|
1286
1302
|
.filter(Boolean)
|
|
1287
1303
|
: [];
|
|
1304
|
+
// 优先级 = 目标国家配置顺序;传子集时按配置顺序取交集(配置外国家按传入顺序排后)
|
|
1305
|
+
const orderedCountries = resolveOrderedLocations(normalizedLocations);
|
|
1288
1306
|
|
|
1289
1307
|
// ---- 过期 processing 回收 ----
|
|
1290
1308
|
const expiredRow = getDb()
|
|
@@ -1304,31 +1322,19 @@ export function createStore(filePath, options = {}) {
|
|
|
1304
1322
|
expiredCandidate = expiredRow;
|
|
1305
1323
|
}
|
|
1306
1324
|
|
|
1307
|
-
// ----
|
|
1325
|
+
// ---- 逐级短路查询:按优先级顺序逐个分支执行,命中即领取 ----
|
|
1308
1326
|
const statusCodeWhere = loggedIn
|
|
1309
1327
|
? "(status_code IS NULL OR status_code = 0 OR status_code = 209002)"
|
|
1310
1328
|
: "(status_code IS NULL OR status_code = 0)";
|
|
1311
1329
|
const notSellerWhere = loggedIn ? "" : "AND COALESCE(tt_seller, 0) != 1";
|
|
1312
1330
|
|
|
1313
|
-
const
|
|
1314
|
-
? normalizedLocations.filter((loc) => tier1.has(loc))
|
|
1315
|
-
: [...tier1];
|
|
1316
|
-
const tier2List = normalizedLocations.length
|
|
1317
|
-
? normalizedLocations.filter((loc) => tier2.has(loc))
|
|
1318
|
-
: [...tier2];
|
|
1319
|
-
const otherFiltered = normalizedLocations.length
|
|
1320
|
-
? normalizedLocations.filter(
|
|
1321
|
-
(loc) => !tier1.has(loc) && !tier2.has(loc),
|
|
1322
|
-
)
|
|
1323
|
-
: null;
|
|
1324
|
-
|
|
1325
|
-
const locIn = (list) => {
|
|
1331
|
+
const locIn = (list, { includeEmpty = true } = {}) => {
|
|
1326
1332
|
if (!list || list.length === 0) return null;
|
|
1327
1333
|
const ph = list.map(() => "?").join(", ");
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
};
|
|
1334
|
+
const sql = includeEmpty
|
|
1335
|
+
? `(UPPER(COALESCE(guessed_location, '')) IN (${ph}) OR COALESCE(guessed_location, '') = '')`
|
|
1336
|
+
: `UPPER(COALESCE(guessed_location, '')) IN (${ph})`;
|
|
1337
|
+
return { sql, params: [...list] };
|
|
1332
1338
|
};
|
|
1333
1339
|
const locNotIn = (list) => {
|
|
1334
1340
|
if (!list || list.length === 0) return null;
|
|
@@ -1338,13 +1344,6 @@ export function createStore(filePath, options = {}) {
|
|
|
1338
1344
|
params: [...list],
|
|
1339
1345
|
};
|
|
1340
1346
|
};
|
|
1341
|
-
const locOther = () => {
|
|
1342
|
-
if (otherFiltered && otherFiltered.length > 0)
|
|
1343
|
-
return locIn(otherFiltered);
|
|
1344
|
-
if (normalizedLocations.length === 0)
|
|
1345
|
-
return locNotIn([...tier1, ...tier2]);
|
|
1346
|
-
return null;
|
|
1347
|
-
};
|
|
1348
1347
|
|
|
1349
1348
|
// 全局 location 过滤(pinned 查询用)
|
|
1350
1349
|
let pinnedLocSql = "";
|
|
@@ -1355,45 +1354,38 @@ export function createStore(filePath, options = {}) {
|
|
|
1355
1354
|
pinnedLocParams.push(...normalizedLocations);
|
|
1356
1355
|
}
|
|
1357
1356
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1357
|
+
// 逐级短路:单分支 LIMIT 1 走索引仅几十 ms;原 26 分支 UNION ALL 需 MERGE
|
|
1358
|
+
// 收集全部分支结果再排序,task_queue 增长后成为 CPU/延迟瓶颈。
|
|
1359
|
+
// 语义与原「UNION ALL + ORDER BY _prio,_sub_prio,... LIMIT 1」完全等价:
|
|
1360
|
+
// 分支添加顺序即 (_prio,_sub_prio),首个非空分支的最优行即全局最优行。
|
|
1361
|
+
const branchOrder =
|
|
1362
|
+
" ORDER BY follower_count DESC, created_at ASC, unique_id ASC LIMIT 1";
|
|
1363
|
+
const branches = [];
|
|
1360
1364
|
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
`SELECT *, ${prio} as _prio, ${subPrio} as _sub_prio FROM task_queue WHERE ${conditions}`,
|
|
1364
|
-
);
|
|
1365
|
-
allParams.push(...params);
|
|
1365
|
+
const addBranch = (conditions, params) => {
|
|
1366
|
+
branches.push({ conditions, params });
|
|
1366
1367
|
};
|
|
1367
1368
|
|
|
1368
1369
|
// prio 0: pinned + hasVideo
|
|
1369
|
-
|
|
1370
|
-
0,
|
|
1371
|
-
0,
|
|
1370
|
+
addBranch(
|
|
1372
1371
|
`status = 'pending' AND pinned = 1 AND video_count > 0 ${pinnedLocSql} AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1373
1372
|
pinnedLocParams,
|
|
1374
1373
|
);
|
|
1375
1374
|
// prio 1: pinned (no video req)
|
|
1376
|
-
|
|
1377
|
-
1,
|
|
1378
|
-
0,
|
|
1375
|
+
addBranch(
|
|
1379
1376
|
`status = 'pending' AND pinned = 1 ${pinnedLocSql} AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1380
1377
|
pinnedLocParams,
|
|
1381
1378
|
);
|
|
1382
1379
|
|
|
1383
1380
|
// prio 2-3: expired candidate(回收的超时任务优先)
|
|
1384
1381
|
if (expiredCandidate) {
|
|
1385
|
-
|
|
1386
|
-
2,
|
|
1387
|
-
0,
|
|
1382
|
+
addBranch(
|
|
1388
1383
|
`unique_id = ? AND video_count > 0 AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1389
1384
|
[expiredCandidate.unique_id],
|
|
1390
1385
|
);
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
`unique_id = ? AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1395
|
-
[expiredCandidate.unique_id],
|
|
1396
|
-
);
|
|
1386
|
+
addBranch(`unique_id = ? AND ${statusCodeWhere} ${notSellerWhere}`, [
|
|
1387
|
+
expiredCandidate.unique_id,
|
|
1388
|
+
]);
|
|
1397
1389
|
}
|
|
1398
1390
|
|
|
1399
1391
|
// 非 pinned 优先级:seed → seller → follow → other,每级 × 3 个 location tier × (hasVideo / noVideoReq)
|
|
@@ -1402,7 +1394,11 @@ export function createStore(filePath, options = {}) {
|
|
|
1402
1394
|
filter: `instr(COALESCE(sources, ''), '"seed"') > 0`,
|
|
1403
1395
|
loggedInOnly: false,
|
|
1404
1396
|
},
|
|
1405
|
-
{
|
|
1397
|
+
{
|
|
1398
|
+
filter: `tt_seller = 1 AND verified = 0`,
|
|
1399
|
+
loggedInOnly: true,
|
|
1400
|
+
sellerAnyCountry: true,
|
|
1401
|
+
},
|
|
1406
1402
|
{
|
|
1407
1403
|
filter: `(instr(COALESCE(sources, ''), '"following"') > 0 OR instr(COALESCE(sources, ''), '"follower"') > 0 OR instr(COALESCE(sources, ''), '"comment"') > 0)`,
|
|
1408
1404
|
loggedInOnly: false,
|
|
@@ -1413,27 +1409,45 @@ export function createStore(filePath, options = {}) {
|
|
|
1413
1409
|
let basePrio = 4;
|
|
1414
1410
|
for (const p of priorities) {
|
|
1415
1411
|
if (p.loggedInOnly && !loggedIn) continue;
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1412
|
+
// 逐国顺序分支:配置内国家按优先级顺序;未传子集时末尾补“配置外国家”兑底。
|
|
1413
|
+
// 空位置(loc='')只在首个国家的分支参与:首国的 noVideoReq 分支 miss 即意味着
|
|
1414
|
+
// 空位置池已耗尽,后续分支无需再 OR ''(避免反复扫描空位置大池)
|
|
1415
|
+
const locationBranches = orderedCountries.map((loc, idx) =>
|
|
1416
|
+
locIn([loc], { includeEmpty: idx === 0 }),
|
|
1417
|
+
);
|
|
1418
|
+
if (normalizedLocations.length === 0)
|
|
1419
|
+
locationBranches.push(locNotIn(orderedCountries));
|
|
1420
|
+
// 商家级专用(sellerAnyCountry):国家对商家放开——末尾追加"任意国家"分支。
|
|
1421
|
+
// 传了国家子集时对应国家的商家仍优先,领完再兜任意国家(含空位置商家);
|
|
1422
|
+
// 未传国家时同理(配置内各国 → 配置外 → 任意)。其他来源级分支不受影响。
|
|
1423
|
+
if (p.sellerAnyCountry) locationBranches.push(null); // null → 任意国家分支
|
|
1421
1424
|
let subPrio = 0;
|
|
1422
|
-
for (const
|
|
1423
|
-
|
|
1425
|
+
for (const loc of locationBranches) {
|
|
1426
|
+
// 商家任意国家分支:不限 guessed_location(含空位置的商家)
|
|
1427
|
+
if (loc === null) {
|
|
1428
|
+
// hasVideo
|
|
1429
|
+
addBranch(
|
|
1430
|
+
`status = 'pending' AND pinned = 0 AND ${p.filter} AND video_count > 0 AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1431
|
+
[],
|
|
1432
|
+
);
|
|
1433
|
+
subPrio++;
|
|
1434
|
+
// noVideoReq
|
|
1435
|
+
addBranch(
|
|
1436
|
+
`status = 'pending' AND pinned = 0 AND ${p.filter} AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1437
|
+
[],
|
|
1438
|
+
);
|
|
1439
|
+
subPrio++;
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1424
1442
|
if (!loc) continue;
|
|
1425
1443
|
// hasVideo
|
|
1426
|
-
|
|
1427
|
-
basePrio,
|
|
1428
|
-
subPrio,
|
|
1444
|
+
addBranch(
|
|
1429
1445
|
`status = 'pending' AND pinned = 0 AND ${p.filter} AND video_count > 0 AND ${loc.sql} AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1430
1446
|
loc.params,
|
|
1431
1447
|
);
|
|
1432
1448
|
subPrio++;
|
|
1433
1449
|
// noVideoReq
|
|
1434
|
-
|
|
1435
|
-
basePrio + 1,
|
|
1436
|
-
subPrio,
|
|
1450
|
+
addBranch(
|
|
1437
1451
|
`status = 'pending' AND pinned = 0 AND ${p.filter} AND ${loc.sql} AND ${statusCodeWhere} ${notSellerWhere}`,
|
|
1438
1452
|
loc.params,
|
|
1439
1453
|
);
|
|
@@ -1442,14 +1456,16 @@ export function createStore(filePath, options = {}) {
|
|
|
1442
1456
|
basePrio += 2;
|
|
1443
1457
|
}
|
|
1444
1458
|
|
|
1459
|
+
// 逐级执行:首个命中分支的最优行即领取结果(未命中继续下一优先级分支)
|
|
1445
1460
|
let selectedRow = null;
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1461
|
+
for (const b of branches) {
|
|
1462
|
+
selectedRow =
|
|
1463
|
+
getDb()
|
|
1464
|
+
.prepare(
|
|
1465
|
+
`SELECT * FROM task_queue WHERE ${b.conditions}${branchOrder}`,
|
|
1466
|
+
)
|
|
1467
|
+
.get(...b.params) || null;
|
|
1468
|
+
if (selectedRow) break;
|
|
1453
1469
|
}
|
|
1454
1470
|
|
|
1455
1471
|
if (selectedRow) {
|
|
@@ -1493,15 +1509,8 @@ export function createStore(filePath, options = {}) {
|
|
|
1493
1509
|
};
|
|
1494
1510
|
}
|
|
1495
1511
|
|
|
1496
|
-
//
|
|
1497
|
-
const
|
|
1498
|
-
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
1499
|
-
function locationTier(u) {
|
|
1500
|
-
const loc = (u.guessedLocation || "").toUpperCase();
|
|
1501
|
-
if (tier1.has(loc)) return 0;
|
|
1502
|
-
if (tier2.has(loc)) return 1;
|
|
1503
|
-
return 2;
|
|
1504
|
-
}
|
|
1512
|
+
// 按目标国家配置顺序排序(越靠前越优先)
|
|
1513
|
+
const locationTier = (u) => locationRank(u.guessedLocation);
|
|
1505
1514
|
|
|
1506
1515
|
// 国家过滤:如果指定了 locations,只保留 guessedLocation 在列表中的用户
|
|
1507
1516
|
function locationFilter(u) {
|
|
@@ -1641,8 +1650,6 @@ export function createStore(filePath, options = {}) {
|
|
|
1641
1650
|
}
|
|
1642
1651
|
: null;
|
|
1643
1652
|
|
|
1644
|
-
const tier1 = new Set(["PL", "NL", "BE"]);
|
|
1645
|
-
const tier2 = new Set(["DE", "FR", "IT", "IE", "ES"]);
|
|
1646
1653
|
const normalizedLocations = Array.isArray(locations)
|
|
1647
1654
|
? locations
|
|
1648
1655
|
.map((loc) => String(loc).trim().toUpperCase())
|
|
@@ -1650,29 +1657,11 @@ export function createStore(filePath, options = {}) {
|
|
|
1650
1657
|
: [];
|
|
1651
1658
|
|
|
1652
1659
|
function getLocationGroups() {
|
|
1653
|
-
const
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
: [...tier1];
|
|
1659
|
-
const tier2List = selected
|
|
1660
|
-
? selected.filter((loc) => tier2.has(loc))
|
|
1661
|
-
: [...tier2];
|
|
1662
|
-
const otherList = selected
|
|
1663
|
-
? selected.filter((loc) => !tier1.has(loc) && !tier2.has(loc))
|
|
1664
|
-
: null;
|
|
1665
|
-
const groups = [];
|
|
1666
|
-
if (tier1List.length > 0)
|
|
1667
|
-
groups.push({ type: "include", values: tier1List });
|
|
1668
|
-
if (tier2List.length > 0)
|
|
1669
|
-
groups.push({ type: "include", values: tier2List });
|
|
1670
|
-
if (selected) {
|
|
1671
|
-
if (otherList.length > 0)
|
|
1672
|
-
groups.push({ type: "include", values: otherList });
|
|
1673
|
-
} else {
|
|
1674
|
-
groups.push({ type: "exclude", values: [...tier1, ...tier2] });
|
|
1675
|
-
}
|
|
1660
|
+
const ordered = resolveOrderedLocations(normalizedLocations);
|
|
1661
|
+
if (ordered.length === 0) return [];
|
|
1662
|
+
const groups = [{ type: "include", values: ordered }];
|
|
1663
|
+
if (normalizedLocations.length === 0)
|
|
1664
|
+
groups.push({ type: "exclude", values: [...ordered] });
|
|
1676
1665
|
return groups;
|
|
1677
1666
|
}
|
|
1678
1667
|
|
|
@@ -2493,28 +2482,40 @@ export function createStore(filePath, options = {}) {
|
|
|
2493
2482
|
const placeholders = targetLocations.map(() => "?").join(",");
|
|
2494
2483
|
|
|
2495
2484
|
// SELECT 候选用户
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2485
|
+
// 性能三件套(76 万行实测 1~3s → ~80ms):
|
|
2486
|
+
// 1. WHERE/ORDER BY 不用 COALESCE(refresh_time, ?)(表达式挡索引);
|
|
2487
|
+
// ASC 排序 NULL 天然最前,与 COALESCE 2016 默认值语义等价
|
|
2488
|
+
// 2. INDEXED BY 强制走 idx_jobs_precheck 部分索引(优化器无统计时会
|
|
2489
|
+
// 误选 idx_jobs_seller_verified_loc 走 TEMP B-TREE 全排序)
|
|
2490
|
+
// 3. ORDER BY jobs.refresh_time 限定表名——裸列名会被 SELECT 输出别名
|
|
2491
|
+
// refresh_time(COALESCE 表达式)遮蔽,又退回表达式排序
|
|
2492
|
+
// 索引不存在的环境(老库未重启)抛 "no such index",自动回退普通查询。
|
|
2493
|
+
const baseWhere = `
|
|
2501
2494
|
WHERE tt_seller = 1
|
|
2502
2495
|
AND verified = 0
|
|
2503
2496
|
AND (pending_refresh IS NULL OR pending_refresh = 0)
|
|
2504
2497
|
AND location_created IN (${placeholders})
|
|
2505
|
-
AND
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2498
|
+
AND (refresh_time IS NULL OR refresh_time < ?)`;
|
|
2499
|
+
const orderLimit = `
|
|
2500
|
+
ORDER BY jobs.refresh_time ASC
|
|
2501
|
+
LIMIT ?`;
|
|
2502
|
+
const selectSql = `SELECT unique_id, nickname, video_count, COALESCE(refresh_time, ?) AS refresh_time
|
|
2503
|
+
FROM jobs`;
|
|
2504
|
+
const args = [defaultTime, ...targetLocations, threshold, limit];
|
|
2505
|
+
|
|
2506
|
+
let selected;
|
|
2507
|
+
try {
|
|
2508
|
+
selected = getDb()
|
|
2509
|
+
.prepare(
|
|
2510
|
+
`${selectSql} INDEXED BY idx_jobs_precheck${baseWhere}${orderLimit}`,
|
|
2511
|
+
)
|
|
2512
|
+
.all(...args);
|
|
2513
|
+
} catch (e) {
|
|
2514
|
+
if (!/no such index/i.test(e.message)) throw e;
|
|
2515
|
+
selected = getDb()
|
|
2516
|
+
.prepare(`${selectSql}${baseWhere}${orderLimit}`)
|
|
2517
|
+
.all(...args);
|
|
2518
|
+
}
|
|
2518
2519
|
|
|
2519
2520
|
// 立即更新 refresh_time 防重复领取(非事务,小概率并发重复可接受)
|
|
2520
2521
|
if (selected.length > 0) {
|
|
@@ -2840,8 +2841,9 @@ export function createStore(filePath, options = {}) {
|
|
|
2840
2841
|
/**
|
|
2841
2842
|
* 回写商品详情到 jobs 表(product_* 列)
|
|
2842
2843
|
* @param {string} uniqueId
|
|
2843
|
-
* @param {{rating?:number, reviewCount?:number, soldCount?:number, productName?:string}} productDetail
|
|
2844
|
+
* @param {{rating?:number, reviewCount?:number, soldCount?:number, productName?:string, skus?:Array}} productDetail
|
|
2844
2845
|
* rating = -1 为哨兵值:商品不可售/已下架,写入后任务移出候选(正常评分 0-5)
|
|
2846
|
+
* skus = 多 SKU 概要数组(skuId/specs/stock/stockStatus/warehouseId),存 product_skus JSON
|
|
2845
2847
|
*/
|
|
2846
2848
|
function updateProductInfo(uniqueId, productDetail) {
|
|
2847
2849
|
if (getDb()) {
|
|
@@ -2855,6 +2857,10 @@ export function createStore(filePath, options = {}) {
|
|
|
2855
2857
|
productReviewCount: productDetail.reviewCount ?? null,
|
|
2856
2858
|
productSoldCount: productDetail.soldCount ?? null,
|
|
2857
2859
|
productName: productDetail.productName || null,
|
|
2860
|
+
// 多 SKU 概要:数组经 normalizeJobValue 自动序列化为 JSON(无 skus 存 null)
|
|
2861
|
+
productSkus: Array.isArray(productDetail.skus)
|
|
2862
|
+
? productDetail.skus
|
|
2863
|
+
: null,
|
|
2858
2864
|
},
|
|
2859
2865
|
false, // 商品补全与用户刷新计数无关,不增加 user_update_count
|
|
2860
2866
|
"jobs",
|
package/src/watch/db-columns.js
CHANGED
package/src/watch/db-schema.js
CHANGED
|
@@ -173,6 +173,7 @@ export function initDb(filePath) {
|
|
|
173
173
|
product_review_count: "INTEGER",
|
|
174
174
|
product_sold_count: "INTEGER",
|
|
175
175
|
product_name: "TEXT",
|
|
176
|
+
product_skus: "TEXT",
|
|
176
177
|
user_create_time: "INTEGER",
|
|
177
178
|
recent_ec_videos: "TEXT",
|
|
178
179
|
});
|
|
@@ -209,6 +210,7 @@ export function initDb(filePath) {
|
|
|
209
210
|
product_review_count: "INTEGER",
|
|
210
211
|
product_sold_count: "INTEGER",
|
|
211
212
|
product_name: "TEXT",
|
|
213
|
+
product_skus: "TEXT",
|
|
212
214
|
user_create_time: "INTEGER",
|
|
213
215
|
recent_ec_videos: "TEXT",
|
|
214
216
|
});
|
|
@@ -246,6 +248,7 @@ export function initDb(filePath) {
|
|
|
246
248
|
product_review_count: "INTEGER",
|
|
247
249
|
product_sold_count: "INTEGER",
|
|
248
250
|
product_name: "TEXT",
|
|
251
|
+
product_skus: "TEXT",
|
|
249
252
|
user_create_time: "INTEGER",
|
|
250
253
|
recent_ec_videos: "TEXT",
|
|
251
254
|
});
|
|
@@ -446,19 +449,28 @@ export function initDb(filePath) {
|
|
|
446
449
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_videos_old_id ON videos_old(id)`);
|
|
447
450
|
}
|
|
448
451
|
|
|
449
|
-
// ===== task_queue
|
|
452
|
+
// ===== task_queue 索引(claimNextJob 逐级短路查询 + 监控列表) =====
|
|
453
|
+
// idx_tq_claim_fast:claim 分支查询主索引(2026-09-17 实战验证:分支 7s→1ms)。
|
|
454
|
+
// 普通复合索引,不依赖部分索引的 WHERE 表达式文本匹配(旧 4 个部分索引因
|
|
455
|
+
// pinned=0 与 COALESCE(pinned,0)=0 文本不一致、首国分支 OR 空串,从未被命中,已废弃清理)。
|
|
450
456
|
const tqIndexes = [
|
|
451
457
|
`CREATE INDEX IF NOT EXISTS idx_tq_status ON task_queue(status)`,
|
|
452
458
|
`CREATE INDEX IF NOT EXISTS idx_tq_claimed_by_status ON task_queue(claimed_by, status, claimed_at)`,
|
|
453
459
|
`CREATE INDEX IF NOT EXISTS idx_tq_status_claimed_at ON task_queue(status, claimed_at)`,
|
|
454
|
-
`CREATE INDEX IF NOT EXISTS
|
|
455
|
-
`CREATE INDEX IF NOT EXISTS idx_tq_claim_pending_seller ON task_queue(UPPER(COALESCE(guessed_location, '')), follower_count DESC, created_at ASC, unique_id ASC) WHERE status = 'pending' AND COALESCE(pinned, 0) = 0 AND tt_seller = 1 AND verified = 0`,
|
|
456
|
-
`CREATE INDEX IF NOT EXISTS idx_tq_claim_pending_follow ON task_queue(UPPER(COALESCE(guessed_location, '')), follower_count DESC, created_at ASC, unique_id ASC) WHERE status = 'pending' AND COALESCE(pinned, 0) = 0 AND (instr(COALESCE(sources, ''), '"following"') > 0 OR instr(COALESCE(sources, ''), '"follower"') > 0)`,
|
|
457
|
-
`CREATE INDEX IF NOT EXISTS idx_tq_claim_pending_other ON task_queue(UPPER(COALESCE(guessed_location, '')), follower_count DESC, created_at ASC, unique_id ASC) WHERE status = 'pending' AND COALESCE(pinned, 0) = 0`,
|
|
460
|
+
`CREATE INDEX IF NOT EXISTS idx_tq_claim_fast ON task_queue(status, UPPER(COALESCE(guessed_location,'')), follower_count DESC, created_at ASC, unique_id ASC)`,
|
|
458
461
|
// 监控页面用户列表查询加速
|
|
459
462
|
`CREATE INDEX IF NOT EXISTS idx_tq_list_pending ON task_queue(pinned, processed_at, follower_count, unique_id) WHERE status = 'pending'`,
|
|
460
463
|
`CREATE INDEX IF NOT EXISTS idx_tq_list_processing ON task_queue(pinned, processed_at, follower_count, unique_id) WHERE status = 'processing'`,
|
|
461
464
|
];
|
|
465
|
+
// 清理从未被命中的旧 claim 部分索引(省写入开销与空间,幂等)
|
|
466
|
+
for (const oldIdx of [
|
|
467
|
+
"idx_tq_claim_pending_pinned",
|
|
468
|
+
"idx_tq_claim_pending_seller",
|
|
469
|
+
"idx_tq_claim_pending_follow",
|
|
470
|
+
"idx_tq_claim_pending_other",
|
|
471
|
+
]) {
|
|
472
|
+
db.exec(`DROP INDEX IF EXISTS ${oldIdx}`);
|
|
473
|
+
}
|
|
462
474
|
for (const sql of tqIndexes) {
|
|
463
475
|
db.exec(sql);
|
|
464
476
|
}
|
package/src/watch/db-tags.js
CHANGED
|
@@ -375,15 +375,25 @@ export function getAllTags(
|
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
export function rawQuery(sql, params = []) {
|
|
378
|
-
// 通用查询器:默认查主库(stats/db-query 复用),勿切到 tag
|
|
378
|
+
// 通用查询器:默认查主库(stats/db-query 复用),勿切到 tag 库。
|
|
379
|
+
// 注意:原实现用 prepare().all() 跑一切语句,DDL(CREATE INDEX 等)会抛
|
|
380
|
+
// "This statement does not return data" 且被 catch 吞成 {error} 返回——
|
|
381
|
+
// server.js 启动清单 ensureIdx 曾因此静默失效(生产库从未建过启动索引)。
|
|
382
|
+
// 现按语句类型分流:DDL/写语句走 exec,查询走 all;失败时抛错让调用方感知。
|
|
379
383
|
const db = getMainDb();
|
|
380
|
-
if (!db)
|
|
381
|
-
|
|
384
|
+
if (!db) throw new Error("db not ready");
|
|
385
|
+
const trimmed = sql.trim().toUpperCase();
|
|
386
|
+
const isQuery =
|
|
387
|
+
trimmed.startsWith("SELECT") ||
|
|
388
|
+
trimmed.startsWith("PRAGMA") ||
|
|
389
|
+
trimmed.startsWith("EXPLAIN") ||
|
|
390
|
+
trimmed.startsWith("WITH");
|
|
391
|
+
if (isQuery) {
|
|
382
392
|
const rows = db.prepare(sql).all(...params);
|
|
383
393
|
return { rows };
|
|
384
|
-
} catch (e) {
|
|
385
|
-
return { error: e.message };
|
|
386
394
|
}
|
|
395
|
+
db.exec(sql);
|
|
396
|
+
return { rows: [] };
|
|
387
397
|
}
|
|
388
398
|
|
|
389
399
|
export function normalizeTags() {
|