tt-help-cli-ycl 1.3.92 → 1.3.94
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/comments.js +49 -24
- package/src/cli/tag.js +239 -94
- package/src/lib/args.js +23 -0
- package/src/lib/browser/cdp.js +4 -1
- package/src/lib/constants.js +15 -0
- package/src/lib/tag-fetcher.js +69 -63
- package/src/watch/data-store.js +537 -2298
- package/src/watch/data-store.js.bak +5091 -0
- package/src/watch/data-store.js.bak2 +5019 -0
- package/src/watch/db-columns.js +160 -0
- package/src/watch/db-crud.js +458 -0
- package/src/watch/db-mappers.js +128 -0
- package/src/watch/db-raw-jobs.js +235 -0
- package/src/watch/db-schema.js +367 -0
- package/src/watch/db-stats.js +235 -0
- package/src/watch/db-tags.js +348 -0
- package/src/watch/llm-scoring.js +235 -0
- package/src/watch/public/app.js +47 -0
- package/src/watch/public/index.html +6 -0
- package/src/watch/server.js +24 -0
- package/src/watch/tag-service.js +142 -11
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享列名常量 — 消除 SQL 中重复的列名定义
|
|
3
|
+
*
|
|
4
|
+
* jobs / raw_jobs / jobs_base 三张表共享大部分列,
|
|
5
|
+
* 之前每次 INSERT 都手写 30+ 列名,改一个字段要改 7 处。
|
|
6
|
+
* 现在统一在这里定义,所有模块引用即可。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ===== jobs / raw_jobs / jobs_base 共享的核心列 =====
|
|
10
|
+
export const JOB_CORE_COLUMNS = [
|
|
11
|
+
"unique_id",
|
|
12
|
+
"nickname",
|
|
13
|
+
"status",
|
|
14
|
+
"sources",
|
|
15
|
+
"claimed_by",
|
|
16
|
+
"claimed_at",
|
|
17
|
+
"error",
|
|
18
|
+
"pinned",
|
|
19
|
+
"no_video",
|
|
20
|
+
"restricted",
|
|
21
|
+
"user_update_count",
|
|
22
|
+
"tt_seller",
|
|
23
|
+
"verified",
|
|
24
|
+
"video_count",
|
|
25
|
+
"comment_count",
|
|
26
|
+
"guessed_location",
|
|
27
|
+
"location_created",
|
|
28
|
+
"confirmed_location",
|
|
29
|
+
"modified_at",
|
|
30
|
+
"follower_count",
|
|
31
|
+
"following_count",
|
|
32
|
+
"heart_count",
|
|
33
|
+
"refresh_time",
|
|
34
|
+
"processed",
|
|
35
|
+
"processed_at",
|
|
36
|
+
"created_at",
|
|
37
|
+
"updated_at",
|
|
38
|
+
"region",
|
|
39
|
+
"signature",
|
|
40
|
+
"bio_link",
|
|
41
|
+
"sec_uid",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
// jobs 表独有的扩展列(raw_jobs / jobs_base 也逐步迁移过来)
|
|
45
|
+
export const JOB_EXT_COLUMNS = [
|
|
46
|
+
"status_code",
|
|
47
|
+
"latest_video_time",
|
|
48
|
+
"user_create_time",
|
|
49
|
+
"top_video_play_count",
|
|
50
|
+
"top_video_href",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// 完整的 jobs 列 = 核心列 + 扩展列
|
|
54
|
+
export const JOB_FULL_COLUMNS = [...JOB_CORE_COLUMNS, ...JOB_EXT_COLUMNS];
|
|
55
|
+
|
|
56
|
+
// ===== 可写入的列集合(updateJobInfo / updateJobBaseInfo 使用) =====
|
|
57
|
+
export const WRITABLE_JOB_COLUMNS = new Set([
|
|
58
|
+
"nickname",
|
|
59
|
+
"status",
|
|
60
|
+
"sources",
|
|
61
|
+
"claimed_by",
|
|
62
|
+
"claimed_at",
|
|
63
|
+
"error",
|
|
64
|
+
"pinned",
|
|
65
|
+
"no_video",
|
|
66
|
+
"restricted",
|
|
67
|
+
"user_update_count",
|
|
68
|
+
"tt_seller",
|
|
69
|
+
"verified",
|
|
70
|
+
"video_count",
|
|
71
|
+
"comment_count",
|
|
72
|
+
"guessed_location",
|
|
73
|
+
"location_created",
|
|
74
|
+
"confirmed_location",
|
|
75
|
+
"modified_at",
|
|
76
|
+
"follower_count",
|
|
77
|
+
"following_count",
|
|
78
|
+
"heart_count",
|
|
79
|
+
"refresh_time",
|
|
80
|
+
"processed",
|
|
81
|
+
"processed_at",
|
|
82
|
+
"updated_at",
|
|
83
|
+
"region",
|
|
84
|
+
"signature",
|
|
85
|
+
"bio_link",
|
|
86
|
+
"sec_uid",
|
|
87
|
+
"status_code",
|
|
88
|
+
"latest_video_time",
|
|
89
|
+
"top_video_play_count",
|
|
90
|
+
"top_video_href",
|
|
91
|
+
"user_create_time",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
// ===== Boolean 列(存储时 0/1,读取时转为 true/false/null) =====
|
|
95
|
+
export const JOB_BOOLEAN_COLUMNS = new Set([
|
|
96
|
+
"pinned",
|
|
97
|
+
"no_video",
|
|
98
|
+
"restricted",
|
|
99
|
+
"processed",
|
|
100
|
+
"tt_seller",
|
|
101
|
+
"verified",
|
|
102
|
+
"error",
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
export const VIDEO_BOOLEAN_COLUMNS = new Set(["tt_seller"]);
|
|
106
|
+
|
|
107
|
+
// ===== videos 表列 =====
|
|
108
|
+
export const VIDEO_COLUMNS = [
|
|
109
|
+
"id",
|
|
110
|
+
"href",
|
|
111
|
+
"author_unique_id",
|
|
112
|
+
"location_created",
|
|
113
|
+
"tt_seller",
|
|
114
|
+
"registered_at",
|
|
115
|
+
"user_update_count",
|
|
116
|
+
"play_count",
|
|
117
|
+
"digg_count",
|
|
118
|
+
"comment_count",
|
|
119
|
+
"share_count",
|
|
120
|
+
"collect_count",
|
|
121
|
+
"stats_updated_at",
|
|
122
|
+
"create_time",
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
// ===== SQL 生成辅助 =====
|
|
126
|
+
|
|
127
|
+
/** 生成列名列表字符串,如 "unique_id, nickname, status, ..." */
|
|
128
|
+
export function columnsList(columns) {
|
|
129
|
+
return columns.join(", ");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 生成 VALUES 占位符,如 "?, ?, ?, ..." */
|
|
133
|
+
export function valuesPlaceholders(columns) {
|
|
134
|
+
return columns.map(() => "?").join(", ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 生成 INSERT OR IGNORE INTO 语句 */
|
|
138
|
+
export function insertIgnoreSql(table, columns) {
|
|
139
|
+
return `INSERT OR IGNORE INTO ${table} (${columnsList(columns)}) VALUES (${valuesPlaceholders(columns)})`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 生成 INSERT OR REPLACE INTO 语句 */
|
|
143
|
+
export function insertReplaceSql(table, columns) {
|
|
144
|
+
return `INSERT OR REPLACE INTO ${table} (${columnsList(columns)}) VALUES (${valuesPlaceholders(columns)})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 生成 INSERT ... SELECT 语句(从 sourceTable 复制到 targetTable) */
|
|
148
|
+
export function insertSelectSql(
|
|
149
|
+
targetTable,
|
|
150
|
+
sourceTable,
|
|
151
|
+
columns,
|
|
152
|
+
whereSql = "",
|
|
153
|
+
) {
|
|
154
|
+
return `INSERT OR IGNORE INTO ${targetTable} (${columnsList(columns)}) SELECT ${columnsList(columns)} FROM ${sourceTable}${whereSql ? ` WHERE ${whereSql}` : ""}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 生成 UPDATE SET 子句,如 "nickname = ?, status = ?, ..." */
|
|
158
|
+
export function updateSetClause(columns) {
|
|
159
|
+
return columns.map((c) => `${c} = ?`).join(", ");
|
|
160
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库 CRUD 操作
|
|
3
|
+
*
|
|
4
|
+
* 基础的增删改查:用户判重、添加、更新、行映射等。
|
|
5
|
+
* 所有函数依赖 db-schema.js 的 getDb() 获取数据库连接。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
JOB_BOOLEAN_COLUMNS,
|
|
10
|
+
VIDEO_BOOLEAN_COLUMNS,
|
|
11
|
+
WRITABLE_JOB_COLUMNS,
|
|
12
|
+
JOB_CORE_COLUMNS,
|
|
13
|
+
columnsList,
|
|
14
|
+
valuesPlaceholders,
|
|
15
|
+
insertIgnoreSql,
|
|
16
|
+
updateSetClause,
|
|
17
|
+
} from "./db-columns.js";
|
|
18
|
+
import { getDb } from "./db-schema.js";
|
|
19
|
+
|
|
20
|
+
// ===== 行映射工具 =====
|
|
21
|
+
|
|
22
|
+
export function snakeToCamel(key) {
|
|
23
|
+
return key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function camelToSnake(key) {
|
|
27
|
+
return key.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function normalizeJobValue(column, value) {
|
|
31
|
+
if (value === undefined || value === null) return null;
|
|
32
|
+
if (column === "sources") {
|
|
33
|
+
if (!Array.isArray(value)) return JSON.stringify([]);
|
|
34
|
+
return JSON.stringify([...new Set(value)]);
|
|
35
|
+
}
|
|
36
|
+
if (JOB_BOOLEAN_COLUMNS.has(column)) {
|
|
37
|
+
return value ? 1 : 0;
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mapJobRow(row) {
|
|
44
|
+
if (!row) return undefined;
|
|
45
|
+
const mapped = {};
|
|
46
|
+
for (const [key, value] of Object.entries(row)) {
|
|
47
|
+
const camelKey = snakeToCamel(key);
|
|
48
|
+
if (key === "sources") {
|
|
49
|
+
try {
|
|
50
|
+
mapped[camelKey] = value ? JSON.parse(value) : [];
|
|
51
|
+
} catch {
|
|
52
|
+
mapped[camelKey] = [];
|
|
53
|
+
}
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (JOB_BOOLEAN_COLUMNS.has(key)) {
|
|
57
|
+
mapped[camelKey] = value === null || value === undefined ? null : !!value;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
mapped[camelKey] = value;
|
|
61
|
+
}
|
|
62
|
+
return mapped;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function mapVideoRow(row) {
|
|
66
|
+
if (!row) return undefined;
|
|
67
|
+
const mapped = {};
|
|
68
|
+
for (const [key, value] of Object.entries(row)) {
|
|
69
|
+
const camelKey = snakeToCamel(key);
|
|
70
|
+
if (VIDEO_BOOLEAN_COLUMNS.has(key)) {
|
|
71
|
+
mapped[camelKey] = value === null || value === undefined ? null : !!value;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
mapped[camelKey] = value;
|
|
75
|
+
}
|
|
76
|
+
return mapped;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function inferStatus(u) {
|
|
80
|
+
if (u.restricted) return "restricted";
|
|
81
|
+
if (u.error) return "error";
|
|
82
|
+
if (u.processed) return "done";
|
|
83
|
+
return "pending";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ===== 用户判重 =====
|
|
87
|
+
|
|
88
|
+
export function hasUserInDb(uid) {
|
|
89
|
+
const db = getDb();
|
|
90
|
+
if (!db) return false;
|
|
91
|
+
const row = db.prepare("SELECT 1 FROM users WHERE unique_id = ?").get(uid);
|
|
92
|
+
return !!row;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ===== 添加用户 =====
|
|
96
|
+
|
|
97
|
+
export function addUserToDb(user) {
|
|
98
|
+
const db = getDb();
|
|
99
|
+
if (!db) return;
|
|
100
|
+
db.prepare(
|
|
101
|
+
`INSERT OR IGNORE INTO users (unique_id, tt_seller, verified, location_created, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
102
|
+
).run(
|
|
103
|
+
user.uniqueId,
|
|
104
|
+
user.ttSeller === undefined ||
|
|
105
|
+
user.ttSeller === null ||
|
|
106
|
+
user.ttSeller === ""
|
|
107
|
+
? null
|
|
108
|
+
: user.ttSeller
|
|
109
|
+
? 1
|
|
110
|
+
: 0,
|
|
111
|
+
user.verified === undefined ||
|
|
112
|
+
user.verified === null ||
|
|
113
|
+
user.verified === ""
|
|
114
|
+
? null
|
|
115
|
+
: user.verified
|
|
116
|
+
? 1
|
|
117
|
+
: 0,
|
|
118
|
+
user.locationCreated || null,
|
|
119
|
+
new Date().toISOString(),
|
|
120
|
+
new Date().toISOString(),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ===== 添加 Job =====
|
|
125
|
+
|
|
126
|
+
export function addJobToDb(user) {
|
|
127
|
+
const db = getDb();
|
|
128
|
+
if (!db) return;
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const columns = [
|
|
131
|
+
"unique_id",
|
|
132
|
+
"nickname",
|
|
133
|
+
"status",
|
|
134
|
+
"sources",
|
|
135
|
+
"claimed_by",
|
|
136
|
+
"claimed_at",
|
|
137
|
+
"error",
|
|
138
|
+
"pinned",
|
|
139
|
+
"no_video",
|
|
140
|
+
"restricted",
|
|
141
|
+
"user_update_count",
|
|
142
|
+
"tt_seller",
|
|
143
|
+
"verified",
|
|
144
|
+
"video_count",
|
|
145
|
+
"comment_count",
|
|
146
|
+
"guessed_location",
|
|
147
|
+
"location_created",
|
|
148
|
+
"follower_count",
|
|
149
|
+
"following_count",
|
|
150
|
+
"heart_count",
|
|
151
|
+
"refresh_time",
|
|
152
|
+
"processed",
|
|
153
|
+
"processed_at",
|
|
154
|
+
"created_at",
|
|
155
|
+
"updated_at",
|
|
156
|
+
"region",
|
|
157
|
+
"signature",
|
|
158
|
+
"bio_link",
|
|
159
|
+
"sec_uid",
|
|
160
|
+
];
|
|
161
|
+
db.prepare(insertIgnoreSql("jobs", columns)).run(
|
|
162
|
+
user.uniqueId,
|
|
163
|
+
user.nickname || null,
|
|
164
|
+
user.status || inferStatus(user),
|
|
165
|
+
JSON.stringify(
|
|
166
|
+
Array.isArray(user.sources) ? [...new Set(user.sources)] : [],
|
|
167
|
+
),
|
|
168
|
+
user.claimedBy || null,
|
|
169
|
+
user.claimedAt || null,
|
|
170
|
+
user.error || null,
|
|
171
|
+
user.pinned ? 1 : 0,
|
|
172
|
+
user.noVideo ? 1 : 0,
|
|
173
|
+
user.restricted ? 1 : 0,
|
|
174
|
+
user.userUpdateCount || 0,
|
|
175
|
+
user.ttSeller === undefined ||
|
|
176
|
+
user.ttSeller === null ||
|
|
177
|
+
user.ttSeller === ""
|
|
178
|
+
? null
|
|
179
|
+
: user.ttSeller
|
|
180
|
+
? 1
|
|
181
|
+
: 0,
|
|
182
|
+
user.verified === undefined ||
|
|
183
|
+
user.verified === null ||
|
|
184
|
+
user.verified === ""
|
|
185
|
+
? null
|
|
186
|
+
: user.verified
|
|
187
|
+
? 1
|
|
188
|
+
: 0,
|
|
189
|
+
user.videoCount || 0,
|
|
190
|
+
user.commentCount || 0,
|
|
191
|
+
user.guessedLocation || null,
|
|
192
|
+
user.locationCreated || null,
|
|
193
|
+
user.followerCount || 0,
|
|
194
|
+
user.followingCount || 0,
|
|
195
|
+
user.heartCount || 0,
|
|
196
|
+
user.refreshTime || null,
|
|
197
|
+
user.processed ? 1 : 0,
|
|
198
|
+
user.processedAt || null,
|
|
199
|
+
user.createdAt || now,
|
|
200
|
+
user.updatedAt || now,
|
|
201
|
+
user.region || null,
|
|
202
|
+
user.signature || null,
|
|
203
|
+
user.bioLink?.link || user.bioLink?.url || user.bioLink || null,
|
|
204
|
+
user.secUid || null,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ===== 添加 JobBase =====
|
|
209
|
+
|
|
210
|
+
export function addJobBaseToDb(user) {
|
|
211
|
+
const db = getDb();
|
|
212
|
+
if (!db) return;
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const columns = [
|
|
215
|
+
"unique_id",
|
|
216
|
+
"nickname",
|
|
217
|
+
"status",
|
|
218
|
+
"sources",
|
|
219
|
+
"claimed_by",
|
|
220
|
+
"claimed_at",
|
|
221
|
+
"error",
|
|
222
|
+
"pinned",
|
|
223
|
+
"no_video",
|
|
224
|
+
"restricted",
|
|
225
|
+
"user_update_count",
|
|
226
|
+
"tt_seller",
|
|
227
|
+
"verified",
|
|
228
|
+
"video_count",
|
|
229
|
+
"comment_count",
|
|
230
|
+
"guessed_location",
|
|
231
|
+
"location_created",
|
|
232
|
+
"follower_count",
|
|
233
|
+
"following_count",
|
|
234
|
+
"heart_count",
|
|
235
|
+
"refresh_time",
|
|
236
|
+
"processed",
|
|
237
|
+
"processed_at",
|
|
238
|
+
"created_at",
|
|
239
|
+
"updated_at",
|
|
240
|
+
"region",
|
|
241
|
+
"signature",
|
|
242
|
+
"bio_link",
|
|
243
|
+
"sec_uid",
|
|
244
|
+
];
|
|
245
|
+
db.prepare(insertIgnoreSql("jobs_base", columns)).run(
|
|
246
|
+
user.uniqueId,
|
|
247
|
+
user.nickname || null,
|
|
248
|
+
user.status || inferStatus(user),
|
|
249
|
+
JSON.stringify(
|
|
250
|
+
Array.isArray(user.sources) ? [...new Set(user.sources)] : [],
|
|
251
|
+
),
|
|
252
|
+
user.claimedBy || null,
|
|
253
|
+
user.claimedAt || null,
|
|
254
|
+
user.error || null,
|
|
255
|
+
user.pinned ? 1 : 0,
|
|
256
|
+
user.noVideo ? 1 : 0,
|
|
257
|
+
user.restricted ? 1 : 0,
|
|
258
|
+
user.userUpdateCount || 0,
|
|
259
|
+
user.ttSeller === undefined ||
|
|
260
|
+
user.ttSeller === null ||
|
|
261
|
+
user.ttSeller === ""
|
|
262
|
+
? null
|
|
263
|
+
: user.ttSeller
|
|
264
|
+
? 1
|
|
265
|
+
: 0,
|
|
266
|
+
user.verified === undefined ||
|
|
267
|
+
user.verified === null ||
|
|
268
|
+
user.verified === ""
|
|
269
|
+
? null
|
|
270
|
+
: user.verified
|
|
271
|
+
? 1
|
|
272
|
+
: 0,
|
|
273
|
+
user.videoCount || 0,
|
|
274
|
+
user.commentCount || 0,
|
|
275
|
+
user.guessedLocation || null,
|
|
276
|
+
user.locationCreated || null,
|
|
277
|
+
user.followerCount || 0,
|
|
278
|
+
user.followingCount || 0,
|
|
279
|
+
user.heartCount || 0,
|
|
280
|
+
user.refreshTime || null,
|
|
281
|
+
user.processed ? 1 : 0,
|
|
282
|
+
user.processedAt || null,
|
|
283
|
+
user.createdAt || now,
|
|
284
|
+
user.updatedAt || now,
|
|
285
|
+
user.region || null,
|
|
286
|
+
user.signature || null,
|
|
287
|
+
user.bioLink?.link || user.bioLink?.url || user.bioLink || null,
|
|
288
|
+
user.secUid || null,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ===== 添加 Job(事务:users + jobs) =====
|
|
293
|
+
|
|
294
|
+
export function addJob(user) {
|
|
295
|
+
const db = getDb();
|
|
296
|
+
if (!db) {
|
|
297
|
+
addUserToDb(user);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!user.status) user.status = inferStatus(user);
|
|
301
|
+
if (!user.createdAt) user.createdAt = Date.now();
|
|
302
|
+
if (!user.updatedAt) user.updatedAt = user.createdAt;
|
|
303
|
+
const writeTxn = db.transaction((job) => {
|
|
304
|
+
addUserToDb(job);
|
|
305
|
+
addJobToDb(job);
|
|
306
|
+
});
|
|
307
|
+
writeTxn(user);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ===== 查询 Job =====
|
|
311
|
+
|
|
312
|
+
export function getJobRow(uniqueId) {
|
|
313
|
+
const db = getDb();
|
|
314
|
+
if (!db) return null;
|
|
315
|
+
return db.prepare("SELECT * FROM jobs WHERE unique_id = ?").get(uniqueId);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function getJobBaseRow(uniqueId) {
|
|
319
|
+
const db = getDb();
|
|
320
|
+
if (!db) return null;
|
|
321
|
+
return db
|
|
322
|
+
.prepare("SELECT * FROM jobs_base WHERE unique_id = ?")
|
|
323
|
+
.get(uniqueId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function getJob(uniqueId) {
|
|
327
|
+
return mapJobRow(getJobRow(uniqueId));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function getAllJobs() {
|
|
331
|
+
const db = getDb();
|
|
332
|
+
if (!db) return [];
|
|
333
|
+
return db.prepare("SELECT * FROM jobs").all().map(mapJobRow);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ===== 查询 Video =====
|
|
337
|
+
|
|
338
|
+
export function getVideoRow(videoId) {
|
|
339
|
+
const db = getDb();
|
|
340
|
+
if (!db) return null;
|
|
341
|
+
return db.prepare("SELECT * FROM videos WHERE id = ?").get(videoId);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function getAllVideoRows() {
|
|
345
|
+
const db = getDb();
|
|
346
|
+
if (!db) return [];
|
|
347
|
+
return db.prepare("SELECT * FROM videos").all();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ===== 更新 Job 信息 =====
|
|
351
|
+
|
|
352
|
+
export function updateJobInfo(uniqueId, info, incrementCount = true) {
|
|
353
|
+
const db = getDb();
|
|
354
|
+
if (!db) return { error: "db not initialized" };
|
|
355
|
+
const existing = getJobRow(uniqueId);
|
|
356
|
+
if (!existing) return { error: "user not found" };
|
|
357
|
+
|
|
358
|
+
const nextValues = {};
|
|
359
|
+
for (const [key, value] of Object.entries(info || {})) {
|
|
360
|
+
if (key === "uniqueId" || key === "unique_id") continue;
|
|
361
|
+
if (value === undefined || value === "") continue;
|
|
362
|
+
let column = camelToSnake(key);
|
|
363
|
+
if (column === "bio") column = "signature";
|
|
364
|
+
if (column === "create_time") column = "user_create_time";
|
|
365
|
+
if (!WRITABLE_JOB_COLUMNS.has(column)) continue;
|
|
366
|
+
nextValues[column] = normalizeJobValue(column, value);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
nextValues.updated_at = Date.now();
|
|
370
|
+
if (incrementCount) {
|
|
371
|
+
nextValues.user_update_count = (existing.user_update_count || 0) + 1;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const columns = Object.keys(nextValues);
|
|
375
|
+
if (columns.length > 0) {
|
|
376
|
+
const sql = `UPDATE jobs SET ${updateSetClause(columns)} WHERE unique_id = ?`;
|
|
377
|
+
db.prepare(sql).run(...columns.map((c) => nextValues[c]), uniqueId);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
userUpdateCount:
|
|
383
|
+
nextValues.user_update_count ?? existing.user_update_count ?? 0,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function updateJobBaseInfo(uniqueId, info, incrementCount = true) {
|
|
388
|
+
const db = getDb();
|
|
389
|
+
if (!db) return { error: "db not initialized" };
|
|
390
|
+
const existing = getJobBaseRow(uniqueId);
|
|
391
|
+
if (!existing) return { error: "user not found" };
|
|
392
|
+
|
|
393
|
+
const nextValues = {};
|
|
394
|
+
for (const [key, value] of Object.entries(info || {})) {
|
|
395
|
+
if (key === "uniqueId" || key === "unique_id") continue;
|
|
396
|
+
if (value === undefined || value === "") continue;
|
|
397
|
+
let column = camelToSnake(key);
|
|
398
|
+
if (column === "bio") column = "signature";
|
|
399
|
+
if (column === "create_time") column = "user_create_time";
|
|
400
|
+
if (!WRITABLE_JOB_COLUMNS.has(column)) continue;
|
|
401
|
+
nextValues[column] = normalizeJobValue(column, value);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
nextValues.updated_at = Date.now();
|
|
405
|
+
if (incrementCount) {
|
|
406
|
+
nextValues.user_update_count = (existing.user_update_count || 0) + 1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const columns = Object.keys(nextValues);
|
|
410
|
+
if (columns.length > 0) {
|
|
411
|
+
const sql = `UPDATE jobs_base SET ${updateSetClause(columns)} WHERE unique_id = ?`;
|
|
412
|
+
db.prepare(sql).run(...columns.map((c) => nextValues[c]), uniqueId);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
ok: true,
|
|
417
|
+
userUpdateCount:
|
|
418
|
+
nextValues.user_update_count ?? existing.user_update_count ?? 0,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ===== 计数 =====
|
|
423
|
+
|
|
424
|
+
export function getUserDbCount() {
|
|
425
|
+
const db = getDb();
|
|
426
|
+
if (!db) return 0;
|
|
427
|
+
return db.prepare("SELECT COUNT(*) as c FROM users").get().c;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function getJobsCount() {
|
|
431
|
+
const db = getDb();
|
|
432
|
+
if (!db) return 0;
|
|
433
|
+
return db.prepare("SELECT COUNT(*) as c FROM jobs").get().c;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function getPendingJobsCount() {
|
|
437
|
+
const db = getDb();
|
|
438
|
+
if (!db) return 0;
|
|
439
|
+
return db
|
|
440
|
+
.prepare("SELECT COUNT(*) as c FROM jobs WHERE status = 'pending'")
|
|
441
|
+
.get().c;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function getPendingJobsUserUpdateCount() {
|
|
445
|
+
const db = getDb();
|
|
446
|
+
if (!db) return 0;
|
|
447
|
+
return db
|
|
448
|
+
.prepare(
|
|
449
|
+
`SELECT COUNT(*) as c FROM jobs WHERE COALESCE(tt_seller, '') = '' AND COALESCE(user_update_count, 0) <= 0`,
|
|
450
|
+
)
|
|
451
|
+
.get().c;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function getRawJobsCount() {
|
|
455
|
+
const db = getDb();
|
|
456
|
+
if (!db) return 0;
|
|
457
|
+
return db.prepare("SELECT COUNT(*) as c FROM raw_jobs").get().c;
|
|
458
|
+
}
|