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.
@@ -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 total = getJobsCount();
554
- const statusCounts = {
555
- pending: 0,
556
- processing: 0,
557
- done: 0,
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 status, COUNT(*) as count
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
- .all();
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(...targetLocations);
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: getPendingJobsCount(),
660
- processedUsers: statusCounts.done,
661
- pendingUsers: statusCounts.pending,
662
- processingUsers: statusCounts.processing,
663
- restrictedUsers: statusCounts.restricted,
664
- errorUsers: statusCounts.error,
665
- targetUsers,
666
- userUpdateTasks: getPendingJobsUserUpdateCount(),
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: sourceRow.seed || 0,
671
- video: sourceRow.video || 0,
672
- comment: sourceRow.comment || 0,
673
- guess: sourceRow.guess || 0,
674
- following: sourceRow.following || 0,
675
- follower: sourceRow.follower || 0,
676
- processed: sourceRow.processed || 0,
677
- restricted: sourceRow.restricted || 0,
678
- error: sourceRow.error || 0,
679
- noVideo: sourceRow.noVideo || 0,
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
- SELECT *
1340
- FROM jobs
1341
- ${whereSql}
1342
- ORDER BY
1343
- pinned DESC,
1344
- CASE status
1345
- WHEN 'processing' THEN 0
1346
- WHEN 'pending' THEN 1
1347
- WHEN 'done' THEN 2
1348
- WHEN 'error' THEN 3
1349
- WHEN 'restricted' THEN 4
1350
- ELSE 9
1351
- END ASC,
1352
- CASE
1353
- WHEN status = 'done' THEN -COALESCE(processed_at, 0)
1354
- ELSE 0
1355
- END ASC,
1356
- CASE
1357
- WHEN status = 'pending' AND tt_seller = 1 AND verified = 0 THEN 0
1358
- ELSE 1
1359
- END ASC,
1360
- CASE
1361
- WHEN status = 'pending' AND UPPER(COALESCE(guessed_location, '')) IN ('PL', 'NL', 'BE', 'AT') THEN 0
1362
- WHEN status = 'pending' AND UPPER(COALESCE(guessed_location, '')) IN ('DE', 'FR', 'IT', 'IE', 'ES') THEN 1
1363
- ELSE 2
1364
- END ASC,
1365
- COALESCE(follower_count, 0) DESC,
1366
- COALESCE(processed_at, 0) DESC,
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
- SELECT
1391
- unique_id,
1392
- nickname,
1393
- follower_count,
1394
- tt_seller,
1395
- verified,
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
- const column = camelToSnake(key);
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 = result.userInfo?.signature ?? 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 txn = db.transaction((items) =>
3250
- items.map((item) => {
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
- return updateJobInfo(
3256
- item.uniqueId,
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
- return updateJobInfo(item.uniqueId, info, true);
3262
- }),
3263
- );
3264
- return txn(updates).map((result, index) =>
3265
- result.error
3266
- ? { uniqueId: updates[index].uniqueId, error: result.error }
3267
- : {
3268
- uniqueId: updates[index].uniqueId,
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: result.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 results = [];
3466
+ const memResults = [];
3276
3467
  for (const item of updates) {
3277
3468
  const user = getUser(item.uniqueId);
3278
3469
  if (!user) {
3279
- results.push({ uniqueId: item.uniqueId, error: "user not found" });
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
- results.push({
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 results;
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(200, parseInt(limit) || 50));
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) {