tt-help-cli-ycl 1.3.72 → 1.3.74
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/attach.js +5 -3
- package/src/cli/open.js +51 -3
- package/src/lib/args.js +4 -0
- package/src/lib/parse-ssr.mjs +1 -0
- package/src/watch/data-store.js +346 -155
- package/src/watch/public/app.js +194 -126
- package/src/watch/public/index.html +7 -0
- package/src/watch/server.js +50 -3
package/src/watch/data-store.js
CHANGED
|
@@ -140,6 +140,9 @@ function initUserDb(filePath) {
|
|
|
140
140
|
if (!existingJobColumns.has("modified_at")) {
|
|
141
141
|
db.exec(`ALTER TABLE jobs ADD COLUMN modified_at INTEGER`);
|
|
142
142
|
}
|
|
143
|
+
if (!existingJobColumns.has("bio_link")) {
|
|
144
|
+
db.exec(`ALTER TABLE jobs ADD COLUMN bio_link TEXT`);
|
|
145
|
+
}
|
|
143
146
|
db.exec(`
|
|
144
147
|
CREATE TABLE IF NOT EXISTS raw_jobs (
|
|
145
148
|
unique_id TEXT PRIMARY KEY,
|
|
@@ -196,6 +199,9 @@ function initUserDb(filePath) {
|
|
|
196
199
|
if (!existingRawJobColumns.has("modified_at")) {
|
|
197
200
|
db.exec(`ALTER TABLE raw_jobs ADD COLUMN modified_at INTEGER`);
|
|
198
201
|
}
|
|
202
|
+
if (!existingRawJobColumns.has("bio_link")) {
|
|
203
|
+
db.exec(`ALTER TABLE raw_jobs ADD COLUMN bio_link TEXT`);
|
|
204
|
+
}
|
|
199
205
|
db.exec(`
|
|
200
206
|
CREATE TABLE IF NOT EXISTS videos (
|
|
201
207
|
id TEXT PRIMARY KEY,
|
|
@@ -461,9 +467,10 @@ function addJobToDb(user) {
|
|
|
461
467
|
updated_at,
|
|
462
468
|
region,
|
|
463
469
|
signature,
|
|
470
|
+
bio_link,
|
|
464
471
|
sec_uid
|
|
465
472
|
)
|
|
466
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
473
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
467
474
|
`,
|
|
468
475
|
).run(
|
|
469
476
|
user.uniqueId,
|
|
@@ -507,6 +514,7 @@ function addJobToDb(user) {
|
|
|
507
514
|
user.updatedAt || now,
|
|
508
515
|
user.region || null,
|
|
509
516
|
user.signature || null,
|
|
517
|
+
user.bioLink?.link || user.bioLink?.url || user.bioLink || null,
|
|
510
518
|
user.secUid || null,
|
|
511
519
|
);
|
|
512
520
|
}
|
|
@@ -550,44 +558,46 @@ function getRawJobsCount() {
|
|
|
550
558
|
function getDashboardStatsFromDb(targetLocations = []) {
|
|
551
559
|
if (!db) return null;
|
|
552
560
|
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
error: 0,
|
|
559
|
-
restricted: 0,
|
|
560
|
-
};
|
|
561
|
-
const rows = db
|
|
561
|
+
const targetPlaceholders = targetLocations.map(() => "?").join(", ");
|
|
562
|
+
const targetParams = targetLocations.length ? targetLocations : [];
|
|
563
|
+
|
|
564
|
+
// 合并所有 jobs 表的聚合统计为单次扫描
|
|
565
|
+
const aggregateRow = db
|
|
562
566
|
.prepare(
|
|
563
567
|
`
|
|
564
|
-
SELECT
|
|
568
|
+
SELECT
|
|
569
|
+
COUNT(*) as total,
|
|
570
|
+
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
|
|
571
|
+
SUM(CASE WHEN status = 'processing' THEN 1 ELSE 0 END) as processing,
|
|
572
|
+
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done,
|
|
573
|
+
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
574
|
+
SUM(CASE WHEN status = 'restricted' THEN 1 ELSE 0 END) as restricted,
|
|
575
|
+
SUM(CASE WHEN tt_seller = 1 AND verified = 0 ${
|
|
576
|
+
targetLocations.length
|
|
577
|
+
? `AND location_created IN (${targetPlaceholders})`
|
|
578
|
+
: "AND 1 = 0"
|
|
579
|
+
} THEN 1 ELSE 0 END) as targetUsers,
|
|
580
|
+
SUM(CASE WHEN no_video = 1 THEN 1 ELSE 0 END) as noVideo,
|
|
581
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"video"') > 0 THEN 1 ELSE 0 END) as video,
|
|
582
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"comment"') > 0 THEN 1 ELSE 0 END) as comment,
|
|
583
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"guess"') > 0 THEN 1 ELSE 0 END) as guess,
|
|
584
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"following"') > 0 THEN 1 ELSE 0 END) as following,
|
|
585
|
+
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"follower"') > 0 THEN 1 ELSE 0 END) as follower,
|
|
586
|
+
SUM(CASE
|
|
587
|
+
WHEN status != 'done'
|
|
588
|
+
AND instr(COALESCE(sources, ''), '"video"') = 0
|
|
589
|
+
AND instr(COALESCE(sources, ''), '"comment"') = 0
|
|
590
|
+
AND instr(COALESCE(sources, ''), '"guess"') = 0
|
|
591
|
+
AND instr(COALESCE(sources, ''), '"following"') = 0
|
|
592
|
+
AND instr(COALESCE(sources, ''), '"follower"') = 0
|
|
593
|
+
THEN 1 ELSE 0 END) as seed,
|
|
594
|
+
SUM(CASE WHEN COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) <= 0 THEN 1 ELSE 0 END) as userUpdateTasks
|
|
565
595
|
FROM jobs
|
|
566
|
-
GROUP BY status
|
|
567
596
|
`,
|
|
568
597
|
)
|
|
569
|
-
.
|
|
570
|
-
for (const row of rows) {
|
|
571
|
-
if (row.status && statusCounts[row.status] !== undefined) {
|
|
572
|
-
statusCounts[row.status] = row.count;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const targetPlaceholders = targetLocations.map(() => "?").join(", ");
|
|
577
|
-
const targetUsers = targetLocations.length
|
|
578
|
-
? db
|
|
579
|
-
.prepare(
|
|
580
|
-
`
|
|
581
|
-
SELECT COUNT(*) as c
|
|
582
|
-
FROM jobs
|
|
583
|
-
WHERE tt_seller = 1
|
|
584
|
-
AND verified = 0
|
|
585
|
-
AND location_created IN (${targetPlaceholders})
|
|
586
|
-
`,
|
|
587
|
-
)
|
|
588
|
-
.get(...targetLocations).c
|
|
589
|
-
: 0;
|
|
598
|
+
.get(...targetParams);
|
|
590
599
|
|
|
600
|
+
// countryStats 和 targetCountryStats 需要 GROUP BY,保留为独立查询
|
|
591
601
|
const countryStats = db
|
|
592
602
|
.prepare(
|
|
593
603
|
`
|
|
@@ -607,7 +617,7 @@ function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
607
617
|
ORDER BY count DESC
|
|
608
618
|
`,
|
|
609
619
|
)
|
|
610
|
-
.all(...
|
|
620
|
+
.all(...targetParams);
|
|
611
621
|
|
|
612
622
|
const targetCountryStats = targetLocations.length
|
|
613
623
|
? db
|
|
@@ -625,58 +635,32 @@ function getDashboardStatsFromDb(targetLocations = []) {
|
|
|
625
635
|
.all(...targetLocations)
|
|
626
636
|
: [];
|
|
627
637
|
|
|
628
|
-
const sourceRow = db
|
|
629
|
-
.prepare(
|
|
630
|
-
`
|
|
631
|
-
SELECT
|
|
632
|
-
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as processed,
|
|
633
|
-
SUM(CASE WHEN status = 'restricted' THEN 1 ELSE 0 END) as restricted,
|
|
634
|
-
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error,
|
|
635
|
-
SUM(CASE WHEN no_video = 1 THEN 1 ELSE 0 END) as noVideo,
|
|
636
|
-
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"video"') > 0 THEN 1 ELSE 0 END) as video,
|
|
637
|
-
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"comment"') > 0 THEN 1 ELSE 0 END) as comment,
|
|
638
|
-
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"guess"') > 0 THEN 1 ELSE 0 END) as guess,
|
|
639
|
-
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"following"') > 0 THEN 1 ELSE 0 END) as following,
|
|
640
|
-
SUM(CASE WHEN status != 'done' AND instr(COALESCE(sources, ''), '"follower"') > 0 THEN 1 ELSE 0 END) as follower,
|
|
641
|
-
SUM(CASE
|
|
642
|
-
WHEN status != 'done'
|
|
643
|
-
AND instr(COALESCE(sources, ''), '"video"') = 0
|
|
644
|
-
AND instr(COALESCE(sources, ''), '"comment"') = 0
|
|
645
|
-
AND instr(COALESCE(sources, ''), '"guess"') = 0
|
|
646
|
-
AND instr(COALESCE(sources, ''), '"following"') = 0
|
|
647
|
-
AND instr(COALESCE(sources, ''), '"follower"') = 0
|
|
648
|
-
THEN 1 ELSE 0 END) as seed
|
|
649
|
-
FROM jobs
|
|
650
|
-
`,
|
|
651
|
-
)
|
|
652
|
-
.get();
|
|
653
|
-
|
|
654
638
|
return {
|
|
655
|
-
totalUsers: total,
|
|
639
|
+
totalUsers: aggregateRow.total,
|
|
656
640
|
rawJobs: getRawJobsCount(),
|
|
657
641
|
dbTotalUsers: getUserDbCount(),
|
|
658
|
-
jobsTotal: total,
|
|
659
|
-
jobsPending:
|
|
660
|
-
processedUsers:
|
|
661
|
-
pendingUsers:
|
|
662
|
-
processingUsers:
|
|
663
|
-
restrictedUsers:
|
|
664
|
-
errorUsers:
|
|
665
|
-
targetUsers,
|
|
666
|
-
userUpdateTasks:
|
|
642
|
+
jobsTotal: aggregateRow.total,
|
|
643
|
+
jobsPending: aggregateRow.pending,
|
|
644
|
+
processedUsers: aggregateRow.done,
|
|
645
|
+
pendingUsers: aggregateRow.pending,
|
|
646
|
+
processingUsers: aggregateRow.processing,
|
|
647
|
+
restrictedUsers: aggregateRow.restricted,
|
|
648
|
+
errorUsers: aggregateRow.error,
|
|
649
|
+
targetUsers: aggregateRow.targetUsers,
|
|
650
|
+
userUpdateTasks: aggregateRow.userUpdateTasks,
|
|
667
651
|
targetCountryStats,
|
|
668
652
|
countryStats,
|
|
669
653
|
sourceStats: {
|
|
670
|
-
seed:
|
|
671
|
-
video:
|
|
672
|
-
comment:
|
|
673
|
-
guess:
|
|
674
|
-
following:
|
|
675
|
-
follower:
|
|
676
|
-
processed:
|
|
677
|
-
restricted:
|
|
678
|
-
error:
|
|
679
|
-
noVideo:
|
|
654
|
+
seed: aggregateRow.seed || 0,
|
|
655
|
+
video: aggregateRow.video || 0,
|
|
656
|
+
comment: aggregateRow.comment || 0,
|
|
657
|
+
guess: aggregateRow.guess || 0,
|
|
658
|
+
following: aggregateRow.following || 0,
|
|
659
|
+
follower: aggregateRow.follower || 0,
|
|
660
|
+
processed: aggregateRow.done,
|
|
661
|
+
restricted: aggregateRow.restricted,
|
|
662
|
+
error: aggregateRow.error,
|
|
663
|
+
noVideo: aggregateRow.noVideo || 0,
|
|
680
664
|
},
|
|
681
665
|
};
|
|
682
666
|
}
|
|
@@ -873,7 +857,7 @@ function moveJobsToRawByCountry(scope, country) {
|
|
|
873
857
|
|
|
874
858
|
let scopeWhere = "";
|
|
875
859
|
if (normalizedScope === "pending") {
|
|
876
|
-
scopeWhere = `status = 'pending'`;
|
|
860
|
+
scopeWhere = `status = 'pending' AND COALESCE(user_update_count, 0) >= 2`;
|
|
877
861
|
} else if (normalizedScope === "userUpdate") {
|
|
878
862
|
scopeWhere = `COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) <= 0`;
|
|
879
863
|
} else {
|
|
@@ -1121,14 +1105,14 @@ function restoreRawJobById(uniqueId) {
|
|
|
1121
1105
|
pinned, no_video, restricted, user_update_count, tt_seller, verified,
|
|
1122
1106
|
video_count, comment_count, guessed_location, location_created,
|
|
1123
1107
|
follower_count, following_count, heart_count, refresh_time,
|
|
1124
|
-
processed, processed_at, created_at, updated_at, region, signature, sec_uid
|
|
1108
|
+
processed, processed_at, created_at, updated_at, region, signature, bio_link, sec_uid
|
|
1125
1109
|
)
|
|
1126
1110
|
SELECT
|
|
1127
1111
|
unique_id, nickname, status, sources, claimed_by, claimed_at, error,
|
|
1128
1112
|
pinned, no_video, restricted, user_update_count, tt_seller, verified,
|
|
1129
1113
|
video_count, comment_count, guessed_location, location_created,
|
|
1130
1114
|
follower_count, following_count, heart_count, refresh_time,
|
|
1131
|
-
processed, processed_at, created_at, updated_at, region, signature, sec_uid
|
|
1115
|
+
processed, processed_at, created_at, updated_at, region, signature, bio_link, sec_uid
|
|
1132
1116
|
FROM raw_jobs WHERE unique_id = ?
|
|
1133
1117
|
`,
|
|
1134
1118
|
).run(safeId);
|
|
@@ -1192,14 +1176,14 @@ function restoreRawJobsByFilter({ search, location, hasVideo, hasFollower }) {
|
|
|
1192
1176
|
pinned, no_video, restricted, user_update_count, tt_seller, verified,
|
|
1193
1177
|
video_count, comment_count, guessed_location, location_created,
|
|
1194
1178
|
follower_count, following_count, heart_count, refresh_time,
|
|
1195
|
-
processed, processed_at, created_at, updated_at, region, signature, sec_uid
|
|
1179
|
+
processed, processed_at, created_at, updated_at, region, signature, bio_link, sec_uid
|
|
1196
1180
|
)
|
|
1197
1181
|
SELECT
|
|
1198
1182
|
unique_id, nickname, status, sources, claimed_by, claimed_at, error,
|
|
1199
1183
|
pinned, no_video, restricted, user_update_count, tt_seller, verified,
|
|
1200
1184
|
video_count, comment_count, guessed_location, location_created,
|
|
1201
1185
|
follower_count, following_count, heart_count, refresh_time,
|
|
1202
|
-
processed, processed_at, created_at, updated_at, region, signature, sec_uid
|
|
1186
|
+
processed, processed_at, created_at, updated_at, region, signature, bio_link, sec_uid
|
|
1203
1187
|
FROM raw_jobs WHERE ${whereSql}
|
|
1204
1188
|
`,
|
|
1205
1189
|
).run(...args);
|
|
@@ -1329,46 +1313,59 @@ function getUsersPageFromDb({
|
|
|
1329
1313
|
}
|
|
1330
1314
|
|
|
1331
1315
|
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
1332
|
-
const total = db
|
|
1333
|
-
.prepare(`SELECT COUNT(*) as c FROM jobs ${whereSql}`)
|
|
1334
|
-
.get(...args).c;
|
|
1335
1316
|
|
|
1317
|
+
// COUNT 缓存:134 万条数据全表扫描慢,5 秒内返回缓存值
|
|
1318
|
+
const cacheKey = whereSql + "|" + args.join(",");
|
|
1319
|
+
if (!getUsersPageFromDb._countCache)
|
|
1320
|
+
getUsersPageFromDb._countCache = new Map();
|
|
1321
|
+
const cachedCount = getUsersPageFromDb._countCache.get(cacheKey);
|
|
1322
|
+
let total;
|
|
1323
|
+
if (cachedCount && Date.now() - cachedCount.time < 5000) {
|
|
1324
|
+
total = cachedCount.c;
|
|
1325
|
+
} else {
|
|
1326
|
+
total = db
|
|
1327
|
+
.prepare(`SELECT COUNT(*) as c FROM jobs ${whereSql}`)
|
|
1328
|
+
.get(...args).c;
|
|
1329
|
+
getUsersPageFromDb._countCache.set(cacheKey, {
|
|
1330
|
+
c: total,
|
|
1331
|
+
time: Date.now(),
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// 只查询前端需要的列,避免 SELECT * 带来的大字段传输和 mapJobRow 开销
|
|
1336
1336
|
const rows = db
|
|
1337
1337
|
.prepare(
|
|
1338
1338
|
`
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
unique_id ASC
|
|
1368
|
-
LIMIT ? OFFSET ?
|
|
1369
|
-
`,
|
|
1339
|
+
SELECT
|
|
1340
|
+
unique_id, nickname, sec_uid, status, sources,
|
|
1341
|
+
tt_seller, verified, follower_count, following_count,
|
|
1342
|
+
location_created, latest_video_time, refresh_time,
|
|
1343
|
+
guessed_location, pinned, processed_at, video_count,
|
|
1344
|
+
no_video, claimed_by, claimed_at, created_at, updated_at
|
|
1345
|
+
FROM jobs
|
|
1346
|
+
${whereSql}
|
|
1347
|
+
ORDER BY
|
|
1348
|
+
pinned DESC,
|
|
1349
|
+
CASE
|
|
1350
|
+
WHEN ? = 'done' THEN COALESCE(processed_at, 0) * -1
|
|
1351
|
+
WHEN ? = '1' THEN COALESCE(refresh_time, 0) * -1
|
|
1352
|
+
ELSE 0
|
|
1353
|
+
END ASC,
|
|
1354
|
+
CASE status
|
|
1355
|
+
WHEN 'processing' THEN 0
|
|
1356
|
+
WHEN 'pending' THEN 1
|
|
1357
|
+
WHEN 'done' THEN 2
|
|
1358
|
+
WHEN 'error' THEN 3
|
|
1359
|
+
WHEN 'restricted' THEN 4
|
|
1360
|
+
ELSE 9
|
|
1361
|
+
END ASC,
|
|
1362
|
+
COALESCE(follower_count, 0) DESC,
|
|
1363
|
+
COALESCE(processed_at, 0) DESC,
|
|
1364
|
+
unique_id ASC
|
|
1365
|
+
LIMIT ? OFFSET ?
|
|
1366
|
+
`,
|
|
1370
1367
|
)
|
|
1371
|
-
.all(...args, safeLimit, safeOffset)
|
|
1368
|
+
.all(...args, status || "", target, safeLimit, safeOffset)
|
|
1372
1369
|
.map(mapJobRow);
|
|
1373
1370
|
|
|
1374
1371
|
return {
|
|
@@ -1387,18 +1384,12 @@ function getTargetUsersFromDb(targetLocations = []) {
|
|
|
1387
1384
|
const rows = db
|
|
1388
1385
|
.prepare(
|
|
1389
1386
|
`
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
location_created,
|
|
1397
|
-
latest_video_time,
|
|
1398
|
-
status,
|
|
1399
|
-
sources
|
|
1400
|
-
FROM jobs
|
|
1401
|
-
WHERE tt_seller = 1
|
|
1387
|
+
SELECT
|
|
1388
|
+
unique_id, nickname, sec_uid, status, sources,
|
|
1389
|
+
tt_seller, verified, follower_count, following_count,
|
|
1390
|
+
location_created, latest_video_time, refresh_time,
|
|
1391
|
+
guessed_location, pinned, processed_at, video_count,
|
|
1392
|
+
no_video, claimed_by, claimed_at, created_at, updated_at
|
|
1402
1393
|
AND verified = 0
|
|
1403
1394
|
AND location_created IN (${placeholders})
|
|
1404
1395
|
ORDER BY COALESCE(follower_count, 0) DESC, unique_id ASC
|
|
@@ -1413,13 +1404,109 @@ function getTargetUsersFromDb(targetLocations = []) {
|
|
|
1413
1404
|
};
|
|
1414
1405
|
}
|
|
1415
1406
|
|
|
1416
|
-
function getTargetUsersByCountryFromDb(targetLocations = []) {
|
|
1407
|
+
function getTargetUsersByCountryFromDb(targetLocations = [], options = {}) {
|
|
1417
1408
|
if (!db) return null;
|
|
1418
1409
|
if (!targetLocations.length) {
|
|
1419
1410
|
return { countries: [] };
|
|
1420
1411
|
}
|
|
1421
1412
|
|
|
1413
|
+
const {
|
|
1414
|
+
summaryOnly = false,
|
|
1415
|
+
country: filterCountry,
|
|
1416
|
+
search,
|
|
1417
|
+
limit,
|
|
1418
|
+
offset,
|
|
1419
|
+
} = options;
|
|
1422
1420
|
const placeholders = targetLocations.map(() => "?").join(", ");
|
|
1421
|
+
const baseParams = [...targetLocations];
|
|
1422
|
+
|
|
1423
|
+
// 摘要模式:只返回各国统计数,不返回用户数据
|
|
1424
|
+
if (summaryOnly) {
|
|
1425
|
+
const statsRows = db
|
|
1426
|
+
.prepare(
|
|
1427
|
+
`
|
|
1428
|
+
SELECT location_created as country, COUNT(*) as count
|
|
1429
|
+
FROM jobs
|
|
1430
|
+
WHERE tt_seller = 1
|
|
1431
|
+
AND verified = 0
|
|
1432
|
+
AND location_created IN (${placeholders})
|
|
1433
|
+
GROUP BY location_created
|
|
1434
|
+
ORDER BY count DESC
|
|
1435
|
+
`,
|
|
1436
|
+
)
|
|
1437
|
+
.all(...targetLocations);
|
|
1438
|
+
|
|
1439
|
+
const countries = statsRows.map((r) => ({
|
|
1440
|
+
country: r.country,
|
|
1441
|
+
count: r.count,
|
|
1442
|
+
users: undefined,
|
|
1443
|
+
}));
|
|
1444
|
+
return {
|
|
1445
|
+
total: statsRows.reduce((s, r) => s + r.count, 0),
|
|
1446
|
+
countries,
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// 分页模式:按国家或全局分页查询用户
|
|
1451
|
+
if (limit !== undefined) {
|
|
1452
|
+
let sql = `
|
|
1453
|
+
SELECT
|
|
1454
|
+
unique_id,
|
|
1455
|
+
nickname,
|
|
1456
|
+
follower_count,
|
|
1457
|
+
video_count,
|
|
1458
|
+
tt_seller,
|
|
1459
|
+
verified,
|
|
1460
|
+
location_created,
|
|
1461
|
+
confirmed_location,
|
|
1462
|
+
modified_at,
|
|
1463
|
+
latest_video_time,
|
|
1464
|
+
refresh_time,
|
|
1465
|
+
status,
|
|
1466
|
+
sources
|
|
1467
|
+
FROM jobs
|
|
1468
|
+
WHERE tt_seller = 1
|
|
1469
|
+
AND verified = 0
|
|
1470
|
+
AND location_created IN (${placeholders})
|
|
1471
|
+
`;
|
|
1472
|
+
const params = [...targetLocations];
|
|
1473
|
+
|
|
1474
|
+
if (filterCountry) {
|
|
1475
|
+
sql += ` AND location_created = ?`;
|
|
1476
|
+
params.push(filterCountry);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
if (search) {
|
|
1480
|
+
sql += ` AND (unique_id LIKE ? OR nickname LIKE ?)`;
|
|
1481
|
+
const likeSearch = `%${search}%`;
|
|
1482
|
+
params.push(likeSearch, likeSearch);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
sql += ` ORDER BY location_created ASC, COALESCE(latest_video_time, 0) DESC`;
|
|
1486
|
+
|
|
1487
|
+
const countSql = sql.replace(
|
|
1488
|
+
/SELECT[^FROM]*FROM/,
|
|
1489
|
+
"SELECT COUNT(*) as cnt FROM",
|
|
1490
|
+
);
|
|
1491
|
+
const total = db.prepare(countSql).get(...params)?.cnt || 0;
|
|
1492
|
+
|
|
1493
|
+
sql += ` LIMIT ? OFFSET ?`;
|
|
1494
|
+
const safeLimit = Math.min(Math.floor(limit), 10000);
|
|
1495
|
+
const safeOffset = Math.max(Math.floor(offset), 0);
|
|
1496
|
+
|
|
1497
|
+
const rows = db
|
|
1498
|
+
.prepare(sql)
|
|
1499
|
+
.all(...params, safeLimit, safeOffset)
|
|
1500
|
+
.map(mapJobRow);
|
|
1501
|
+
|
|
1502
|
+
return {
|
|
1503
|
+
total,
|
|
1504
|
+
limit: safeLimit,
|
|
1505
|
+
offset: safeOffset,
|
|
1506
|
+
users: rows,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1423
1510
|
const rows = db
|
|
1424
1511
|
.prepare(
|
|
1425
1512
|
`
|
|
@@ -1519,6 +1606,7 @@ const writableJobColumns = new Set([
|
|
|
1519
1606
|
"updated_at",
|
|
1520
1607
|
"region",
|
|
1521
1608
|
"signature",
|
|
1609
|
+
"bio_link",
|
|
1522
1610
|
"sec_uid",
|
|
1523
1611
|
"status_code",
|
|
1524
1612
|
"latest_video_time",
|
|
@@ -1607,7 +1695,9 @@ function updateJobInfo(uniqueId, info, incrementCount = true) {
|
|
|
1607
1695
|
for (const [key, value] of Object.entries(info || {})) {
|
|
1608
1696
|
if (key === "uniqueId" || key === "unique_id") continue;
|
|
1609
1697
|
if (value === undefined || value === "") continue;
|
|
1610
|
-
|
|
1698
|
+
let column = camelToSnake(key);
|
|
1699
|
+
// 字段别名:bio → signature
|
|
1700
|
+
if (column === "bio") column = "signature";
|
|
1611
1701
|
if (!writableJobColumns.has(column)) continue;
|
|
1612
1702
|
nextValues[column] = normalizeJobValue(column, value);
|
|
1613
1703
|
}
|
|
@@ -2774,7 +2864,9 @@ export function createStore(filePath) {
|
|
|
2774
2864
|
user.ttSeller = result.userInfo?.ttSeller ?? user.ttSeller;
|
|
2775
2865
|
user.verified = result.userInfo?.verified ?? user.verified;
|
|
2776
2866
|
user.region = result.userInfo?.region || user.region;
|
|
2777
|
-
user.signature =
|
|
2867
|
+
user.signature =
|
|
2868
|
+
result.userInfo?.signature ?? result.userInfo?.bio ?? user.signature;
|
|
2869
|
+
user.bioLink = result.userInfo?.bioLink ?? user.bioLink;
|
|
2778
2870
|
user.followingCount =
|
|
2779
2871
|
result.userInfo?.followingCount ?? user.followingCount;
|
|
2780
2872
|
user.heartCount = result.userInfo?.heartCount ?? user.heartCount;
|
|
@@ -3244,39 +3336,138 @@ export function createStore(filePath) {
|
|
|
3244
3336
|
return { ok: true, location, modifiedAt: user.modifiedAt };
|
|
3245
3337
|
}
|
|
3246
3338
|
|
|
3339
|
+
// 将单个 job 移动到 raw_jobs 表(完整字段复制 + 删除原记录)
|
|
3340
|
+
function moveJobToRaw(uniqueId) {
|
|
3341
|
+
if (!db) return false;
|
|
3342
|
+
const safeId = String(uniqueId).trim();
|
|
3343
|
+
if (!safeId) return false;
|
|
3344
|
+
|
|
3345
|
+
const moveSingleTxn = db.transaction(() => {
|
|
3346
|
+
db.prepare(
|
|
3347
|
+
`
|
|
3348
|
+
INSERT OR REPLACE INTO raw_jobs (
|
|
3349
|
+
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3350
|
+
error, pinned, no_video, restricted, user_update_count,
|
|
3351
|
+
tt_seller, verified, video_count, comment_count,
|
|
3352
|
+
guessed_location, location_created, confirmed_location, modified_at,
|
|
3353
|
+
follower_count, following_count, heart_count, refresh_time,
|
|
3354
|
+
processed, processed_at, created_at, updated_at,
|
|
3355
|
+
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3356
|
+
)
|
|
3357
|
+
SELECT
|
|
3358
|
+
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3359
|
+
error, pinned, no_video, restricted, user_update_count,
|
|
3360
|
+
tt_seller, verified, video_count, comment_count,
|
|
3361
|
+
guessed_location, location_created, confirmed_location, modified_at,
|
|
3362
|
+
follower_count, following_count, heart_count, refresh_time,
|
|
3363
|
+
processed, processed_at, created_at, updated_at,
|
|
3364
|
+
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3365
|
+
FROM jobs WHERE unique_id = ?
|
|
3366
|
+
`,
|
|
3367
|
+
).run(safeId);
|
|
3368
|
+
|
|
3369
|
+
db.prepare("DELETE FROM jobs WHERE unique_id = ?").run(safeId);
|
|
3370
|
+
});
|
|
3371
|
+
moveSingleTxn();
|
|
3372
|
+
return true;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3247
3375
|
function batchUpdateUserInfo(updates) {
|
|
3248
3376
|
if (db) {
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3377
|
+
const results = [];
|
|
3378
|
+
const moveList = [];
|
|
3379
|
+
|
|
3380
|
+
const txn = db.transaction((items) => {
|
|
3381
|
+
items.forEach((item) => {
|
|
3382
|
+
const uniqueId = item.uniqueId;
|
|
3251
3383
|
// 处理 { error: true, statusCode: xxx } 的情况
|
|
3252
3384
|
const info = item.info;
|
|
3385
|
+
let updateResult;
|
|
3253
3386
|
if (info && info.error && info.statusCode !== undefined) {
|
|
3254
3387
|
// 只更新 status_code,不更新其他字段
|
|
3255
|
-
|
|
3256
|
-
|
|
3388
|
+
updateResult = updateJobInfo(
|
|
3389
|
+
uniqueId,
|
|
3257
3390
|
{ statusCode: info.statusCode },
|
|
3258
3391
|
true,
|
|
3259
3392
|
);
|
|
3393
|
+
} else {
|
|
3394
|
+
updateResult = updateJobInfo(uniqueId, info, true);
|
|
3260
3395
|
}
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3396
|
+
|
|
3397
|
+
if (updateResult.error) {
|
|
3398
|
+
results.push({ uniqueId, error: updateResult.error });
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
// 检查 tt_seller:非商家则标记为需要移动到毛料表
|
|
3403
|
+
const row = getJobRow(uniqueId);
|
|
3404
|
+
const ttSeller = row ? row.tt_seller : null;
|
|
3405
|
+
if (ttSeller) {
|
|
3406
|
+
// 商家:保持当前逻辑
|
|
3407
|
+
results.push({
|
|
3408
|
+
uniqueId,
|
|
3269
3409
|
ok: true,
|
|
3270
|
-
userUpdateCount:
|
|
3271
|
-
}
|
|
3272
|
-
|
|
3410
|
+
userUpdateCount: updateResult.userUpdateCount,
|
|
3411
|
+
});
|
|
3412
|
+
} else {
|
|
3413
|
+
// 非商家:标记移动
|
|
3414
|
+
results.push({
|
|
3415
|
+
uniqueId,
|
|
3416
|
+
ok: true,
|
|
3417
|
+
userUpdateCount: updateResult.userUpdateCount,
|
|
3418
|
+
_movedToRaw: true,
|
|
3419
|
+
});
|
|
3420
|
+
moveList.push(uniqueId);
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
});
|
|
3424
|
+
txn(updates);
|
|
3425
|
+
|
|
3426
|
+
// 在事务外执行移动操作(避免嵌套 transaction 问题)
|
|
3427
|
+
if (moveList.length > 0) {
|
|
3428
|
+
const moveTxn = db.transaction((ids) => {
|
|
3429
|
+
ids.forEach((uid) => {
|
|
3430
|
+
db.prepare(
|
|
3431
|
+
`
|
|
3432
|
+
INSERT OR REPLACE INTO raw_jobs (
|
|
3433
|
+
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3434
|
+
error, pinned, no_video, restricted, user_update_count,
|
|
3435
|
+
tt_seller, verified, video_count, comment_count,
|
|
3436
|
+
guessed_location, location_created, confirmed_location, modified_at,
|
|
3437
|
+
follower_count, following_count, heart_count, refresh_time,
|
|
3438
|
+
processed, processed_at, created_at, updated_at,
|
|
3439
|
+
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3440
|
+
)
|
|
3441
|
+
SELECT
|
|
3442
|
+
unique_id, nickname, status, sources, claimed_by, claimed_at,
|
|
3443
|
+
error, pinned, no_video, restricted, user_update_count,
|
|
3444
|
+
tt_seller, verified, video_count, comment_count,
|
|
3445
|
+
guessed_location, location_created, confirmed_location, modified_at,
|
|
3446
|
+
follower_count, following_count, heart_count, refresh_time,
|
|
3447
|
+
processed, processed_at, created_at, updated_at,
|
|
3448
|
+
region, signature, bio_link, sec_uid, status_code, latest_video_time
|
|
3449
|
+
FROM jobs WHERE unique_id = ?
|
|
3450
|
+
`,
|
|
3451
|
+
).run(uid);
|
|
3452
|
+
|
|
3453
|
+
db.prepare("DELETE FROM jobs WHERE unique_id = ?").run(uid);
|
|
3454
|
+
});
|
|
3455
|
+
});
|
|
3456
|
+
moveTxn(moveList);
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// 清理内部标记
|
|
3460
|
+
return results.map((r) => {
|
|
3461
|
+
const { _movedToRaw, ...rest } = r;
|
|
3462
|
+
return rest;
|
|
3463
|
+
});
|
|
3273
3464
|
}
|
|
3274
3465
|
|
|
3275
|
-
const
|
|
3466
|
+
const memResults = [];
|
|
3276
3467
|
for (const item of updates) {
|
|
3277
3468
|
const user = getUser(item.uniqueId);
|
|
3278
3469
|
if (!user) {
|
|
3279
|
-
|
|
3470
|
+
memResults.push({ uniqueId: item.uniqueId, error: "user not found" });
|
|
3280
3471
|
continue;
|
|
3281
3472
|
}
|
|
3282
3473
|
const info = item.info;
|
|
@@ -3298,14 +3489,14 @@ export function createStore(filePath) {
|
|
|
3298
3489
|
}
|
|
3299
3490
|
user.userUpdateCount = (user.userUpdateCount || 0) + 1;
|
|
3300
3491
|
user.updatedAt = Date.now();
|
|
3301
|
-
|
|
3492
|
+
memResults.push({
|
|
3302
3493
|
uniqueId: item.uniqueId,
|
|
3303
3494
|
ok: true,
|
|
3304
3495
|
userUpdateCount: user.userUpdateCount,
|
|
3305
3496
|
});
|
|
3306
3497
|
}
|
|
3307
3498
|
save();
|
|
3308
|
-
return
|
|
3499
|
+
return memResults;
|
|
3309
3500
|
}
|
|
3310
3501
|
|
|
3311
3502
|
// 视频登记
|
|
@@ -3393,7 +3584,7 @@ export function createStore(filePath) {
|
|
|
3393
3584
|
}
|
|
3394
3585
|
|
|
3395
3586
|
function getVideosPage(limit, offset) {
|
|
3396
|
-
const safeLimit = Math.max(1, Math.min(
|
|
3587
|
+
const safeLimit = Math.max(1, Math.min(100, parseInt(limit) || 50));
|
|
3397
3588
|
const safeOffset = Math.max(0, parseInt(offset) || 0);
|
|
3398
3589
|
|
|
3399
3590
|
if (db) {
|