befly 3.9.37 → 3.9.39
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/README.md +38 -39
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +225 -235
- package/docs/cipher.md +71 -69
- package/docs/database.md +155 -153
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +7 -7
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +15 -7
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -81
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +211 -109
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +53 -47
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -54
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -66
- package/sync/syncMenu.ts +190 -57
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
package/lib/dbHelper.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 数据库助手 - TypeScript 版本
|
|
3
3
|
* 提供数据库 CRUD 操作的封装
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
6
|
+
import type { BeflyContext } from "../types/befly.js";
|
|
7
|
+
import type { WhereConditions, JoinOption } from "../types/common.js";
|
|
8
|
+
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, AllResult, TransactionCallback } from "../types/database.js";
|
|
9
|
+
|
|
10
|
+
import { snakeCase } from "es-toolkit/string";
|
|
11
|
+
|
|
12
|
+
import { arrayKeysToCamel } from "../utils/arrayKeysToCamel.js";
|
|
13
|
+
import { fieldClear } from "../utils/fieldClear.js";
|
|
14
|
+
import { keysToCamel } from "../utils/keysToCamel.js";
|
|
15
|
+
import { keysToSnake } from "../utils/keysToSnake.js";
|
|
16
|
+
import { CacheKeys } from "./cacheKeys.js";
|
|
17
|
+
import { Logger } from "./logger.js";
|
|
18
|
+
import { SqlBuilder } from "./sqlBuilder.js";
|
|
19
|
+
|
|
20
|
+
const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* 数据库助手类
|
|
@@ -40,38 +44,38 @@ export class DbHelper {
|
|
|
40
44
|
* @throws 如果 fields 格式非法
|
|
41
45
|
*/
|
|
42
46
|
private validateAndClassifyFields(fields?: string[]): {
|
|
43
|
-
type:
|
|
47
|
+
type: "all" | "include" | "exclude";
|
|
44
48
|
fields: string[];
|
|
45
49
|
} {
|
|
46
50
|
// 情况1:空数组或 undefined,表示查询所有
|
|
47
51
|
if (!fields || fields.length === 0) {
|
|
48
|
-
return { type:
|
|
52
|
+
return { type: "all", fields: [] };
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// 检测是否有星号(禁止)
|
|
52
|
-
if (fields.some((f) => f ===
|
|
53
|
-
throw new Error(
|
|
56
|
+
if (fields.some((f) => f === "*")) {
|
|
57
|
+
throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段");
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
// 检测是否有空字符串或无效值
|
|
57
|
-
if (fields.some((f) => !f || typeof f !==
|
|
58
|
-
throw new Error(
|
|
61
|
+
if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
|
|
62
|
+
throw new Error("fields 不能包含空字符串或无效值");
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// 统计包含字段和排除字段
|
|
62
|
-
const includeFields = fields.filter((f) => !f.startsWith(
|
|
63
|
-
const excludeFields = fields.filter((f) => f.startsWith(
|
|
66
|
+
const includeFields = fields.filter((f) => !f.startsWith("!"));
|
|
67
|
+
const excludeFields = fields.filter((f) => f.startsWith("!"));
|
|
64
68
|
|
|
65
69
|
// 情况2:全部是包含字段
|
|
66
70
|
if (includeFields.length > 0 && excludeFields.length === 0) {
|
|
67
|
-
return { type:
|
|
71
|
+
return { type: "include", fields: includeFields };
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// 情况3:全部是排除字段
|
|
71
75
|
if (excludeFields.length > 0 && includeFields.length === 0) {
|
|
72
76
|
// 去掉感叹号前缀
|
|
73
77
|
const cleanExcludeFields = excludeFields.map((f) => f.substring(1));
|
|
74
|
-
return { type:
|
|
78
|
+
return { type: "exclude", fields: cleanExcludeFields };
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
// 混用情况:报错
|
|
@@ -85,7 +89,7 @@ export class DbHelper {
|
|
|
85
89
|
*/
|
|
86
90
|
private async getTableColumns(table: string): Promise<string[]> {
|
|
87
91
|
// 1. 先查 Redis 缓存
|
|
88
|
-
const cacheKey =
|
|
92
|
+
const cacheKey = CacheKeys.tableColumns(table);
|
|
89
93
|
const columns = await this.befly.redis.getObject<string[]>(cacheKey);
|
|
90
94
|
|
|
91
95
|
if (columns && columns.length > 0) {
|
|
@@ -103,7 +107,7 @@ export class DbHelper {
|
|
|
103
107
|
const columnNames = result.map((row: any) => row.Field) as string[];
|
|
104
108
|
|
|
105
109
|
// 3. 写入 Redis 缓存
|
|
106
|
-
await this.befly.redis.setObject(cacheKey, columnNames,
|
|
110
|
+
await this.befly.redis.setObject(cacheKey, columnNames, TABLE_COLUMNS_CACHE_TTL_SECONDS);
|
|
107
111
|
|
|
108
112
|
return columnNames;
|
|
109
113
|
}
|
|
@@ -113,21 +117,21 @@ export class DbHelper {
|
|
|
113
117
|
* 支持排除字段语法
|
|
114
118
|
*/
|
|
115
119
|
private async fieldsToSnake(table: string, fields: string[]): Promise<string[]> {
|
|
116
|
-
if (!fields || !Array.isArray(fields)) return [
|
|
120
|
+
if (!fields || !Array.isArray(fields)) return ["*"];
|
|
117
121
|
|
|
118
122
|
// 验证并分类字段
|
|
119
123
|
const { type, fields: classifiedFields } = this.validateAndClassifyFields(fields);
|
|
120
124
|
|
|
121
125
|
// 情况1:查询所有字段
|
|
122
|
-
if (type ===
|
|
123
|
-
return [
|
|
126
|
+
if (type === "all") {
|
|
127
|
+
return ["*"];
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// 情况2:指定包含字段
|
|
127
|
-
if (type ===
|
|
131
|
+
if (type === "include") {
|
|
128
132
|
return classifiedFields.map((field) => {
|
|
129
133
|
// 保留函数和特殊字段
|
|
130
|
-
if (field.includes(
|
|
134
|
+
if (field.includes("(") || field.includes(" ")) {
|
|
131
135
|
return field;
|
|
132
136
|
}
|
|
133
137
|
return snakeCase(field);
|
|
@@ -135,7 +139,7 @@ export class DbHelper {
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
// 情况3:排除字段
|
|
138
|
-
if (type ===
|
|
142
|
+
if (type === "exclude") {
|
|
139
143
|
// 获取表的所有字段
|
|
140
144
|
const allColumns = await this.getTableColumns(table);
|
|
141
145
|
|
|
@@ -146,13 +150,13 @@ export class DbHelper {
|
|
|
146
150
|
const resultFields = allColumns.filter((col) => !excludeSnakeFields.includes(col));
|
|
147
151
|
|
|
148
152
|
if (resultFields.length === 0) {
|
|
149
|
-
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(
|
|
153
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
return resultFields;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
return [
|
|
159
|
+
return ["*"];
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
/**
|
|
@@ -161,8 +165,8 @@ export class DbHelper {
|
|
|
161
165
|
private orderByToSnake(orderBy: string[]): string[] {
|
|
162
166
|
if (!orderBy || !Array.isArray(orderBy)) return orderBy;
|
|
163
167
|
return orderBy.map((item) => {
|
|
164
|
-
if (typeof item !==
|
|
165
|
-
const [field, direction] = item.split(
|
|
168
|
+
if (typeof item !== "string" || !item.includes("#")) return item;
|
|
169
|
+
const [field, direction] = item.split("#");
|
|
166
170
|
return `${snakeCase(field.trim())}#${direction.trim()}`;
|
|
167
171
|
});
|
|
168
172
|
}
|
|
@@ -182,19 +186,19 @@ export class DbHelper {
|
|
|
182
186
|
*/
|
|
183
187
|
private processJoinField(field: string): string {
|
|
184
188
|
// 跳过函数、星号、已处理的字段
|
|
185
|
-
if (field.includes(
|
|
189
|
+
if (field.includes("(") || field === "*" || field.startsWith("`")) {
|
|
186
190
|
return field;
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
// 处理别名 AS
|
|
190
|
-
if (field.toUpperCase().includes(
|
|
194
|
+
if (field.toUpperCase().includes(" AS ")) {
|
|
191
195
|
const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
|
|
192
196
|
return `${this.processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// 处理表名.字段名
|
|
196
|
-
if (field.includes(
|
|
197
|
-
const [tableName, fieldName] = field.split(
|
|
200
|
+
if (field.includes(".")) {
|
|
201
|
+
const [tableName, fieldName] = field.split(".");
|
|
198
202
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
|
|
199
203
|
}
|
|
200
204
|
|
|
@@ -209,26 +213,26 @@ export class DbHelper {
|
|
|
209
213
|
*/
|
|
210
214
|
private processJoinWhereKey(key: string): string {
|
|
211
215
|
// 保留逻辑操作符
|
|
212
|
-
if (key ===
|
|
216
|
+
if (key === "$or" || key === "$and") {
|
|
213
217
|
return key;
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
// 处理带操作符的字段名(如 user.userId$gt)
|
|
217
|
-
if (key.includes(
|
|
218
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
221
|
+
if (key.includes("$")) {
|
|
222
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
219
223
|
const fieldPart = key.substring(0, lastDollarIndex);
|
|
220
224
|
const operator = key.substring(lastDollarIndex);
|
|
221
225
|
|
|
222
|
-
if (fieldPart.includes(
|
|
223
|
-
const [tableName, fieldName] = fieldPart.split(
|
|
226
|
+
if (fieldPart.includes(".")) {
|
|
227
|
+
const [tableName, fieldName] = fieldPart.split(".");
|
|
224
228
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
|
|
225
229
|
}
|
|
226
230
|
return `${snakeCase(fieldPart)}${operator}`;
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
// 处理表名.字段名
|
|
230
|
-
if (key.includes(
|
|
231
|
-
const [tableName, fieldName] = key.split(
|
|
234
|
+
if (key.includes(".")) {
|
|
235
|
+
const [tableName, fieldName] = key.split(".");
|
|
232
236
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
|
|
233
237
|
}
|
|
234
238
|
|
|
@@ -240,7 +244,7 @@ export class DbHelper {
|
|
|
240
244
|
* 递归处理联查的 where 条件
|
|
241
245
|
*/
|
|
242
246
|
private processJoinWhere(where: any): any {
|
|
243
|
-
if (!where || typeof where !==
|
|
247
|
+
if (!where || typeof where !== "object") return where;
|
|
244
248
|
|
|
245
249
|
if (Array.isArray(where)) {
|
|
246
250
|
return where.map((item) => this.processJoinWhere(item));
|
|
@@ -250,9 +254,9 @@ export class DbHelper {
|
|
|
250
254
|
for (const [key, value] of Object.entries(where)) {
|
|
251
255
|
const newKey = this.processJoinWhereKey(key);
|
|
252
256
|
|
|
253
|
-
if (key ===
|
|
257
|
+
if (key === "$or" || key === "$and") {
|
|
254
258
|
result[newKey] = (value as any[]).map((item) => this.processJoinWhere(item));
|
|
255
|
-
} else if (typeof value ===
|
|
259
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
256
260
|
result[newKey] = this.processJoinWhere(value);
|
|
257
261
|
} else {
|
|
258
262
|
result[newKey] = value;
|
|
@@ -268,8 +272,8 @@ export class DbHelper {
|
|
|
268
272
|
private processJoinOrderBy(orderBy: string[]): string[] {
|
|
269
273
|
if (!orderBy || !Array.isArray(orderBy)) return orderBy;
|
|
270
274
|
return orderBy.map((item) => {
|
|
271
|
-
if (typeof item !==
|
|
272
|
-
const [field, direction] = item.split(
|
|
275
|
+
if (typeof item !== "string" || !item.includes("#")) return item;
|
|
276
|
+
const [field, direction] = item.split("#");
|
|
273
277
|
return `${this.processJoinField(field.trim())}#${direction.trim()}`;
|
|
274
278
|
});
|
|
275
279
|
}
|
|
@@ -283,12 +287,12 @@ export class DbHelper {
|
|
|
283
287
|
|
|
284
288
|
// 联查时使用特殊处理逻辑
|
|
285
289
|
if (hasJoins) {
|
|
286
|
-
//
|
|
290
|
+
// 联查时字段直接处理(支持表名.字段名格式)
|
|
287
291
|
const processedFields = (options.fields || []).map((f) => this.processJoinField(f));
|
|
288
292
|
|
|
289
293
|
return {
|
|
290
294
|
table: this.processTableName(options.table),
|
|
291
|
-
fields: processedFields.length > 0 ? processedFields : [
|
|
295
|
+
fields: processedFields.length > 0 ? processedFields : ["*"],
|
|
292
296
|
where: this.processJoinWhere(cleanWhere),
|
|
293
297
|
joins: options.joins,
|
|
294
298
|
orderBy: this.processJoinOrderBy(options.orderBy || []),
|
|
@@ -319,16 +323,16 @@ export class DbHelper {
|
|
|
319
323
|
|
|
320
324
|
for (const join of joins) {
|
|
321
325
|
const processedTable = this.processTableName(join.table);
|
|
322
|
-
const type = join.type ||
|
|
326
|
+
const type = join.type || "left";
|
|
323
327
|
|
|
324
328
|
switch (type) {
|
|
325
|
-
case
|
|
329
|
+
case "inner":
|
|
326
330
|
builder.innerJoin(processedTable, join.on);
|
|
327
331
|
break;
|
|
328
|
-
case
|
|
332
|
+
case "right":
|
|
329
333
|
builder.rightJoin(processedTable, join.on);
|
|
330
334
|
break;
|
|
331
|
-
case
|
|
335
|
+
case "left":
|
|
332
336
|
default:
|
|
333
337
|
builder.leftJoin(processedTable, join.on);
|
|
334
338
|
break;
|
|
@@ -339,15 +343,26 @@ export class DbHelper {
|
|
|
339
343
|
/**
|
|
340
344
|
* 添加默认的 state 过滤条件
|
|
341
345
|
* 默认查询 state > 0 的数据(排除已删除和特殊状态)
|
|
346
|
+
* @param where - where 条件
|
|
347
|
+
* @param table - 主表名(JOIN 查询时需要,用于添加表名前缀避免歧义)
|
|
348
|
+
* @param hasJoins - 是否有 JOIN 查询
|
|
342
349
|
*/
|
|
343
|
-
private addDefaultStateFilter(where: WhereConditions = {}): WhereConditions {
|
|
350
|
+
private addDefaultStateFilter(where: WhereConditions = {}, table?: string, hasJoins: boolean = false): WhereConditions {
|
|
344
351
|
// 如果用户已经指定了 state 条件,优先使用用户的条件
|
|
345
|
-
const hasStateCondition = Object.keys(where).some((key) => key.startsWith(
|
|
352
|
+
const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
|
|
346
353
|
|
|
347
354
|
if (hasStateCondition) {
|
|
348
355
|
return where;
|
|
349
356
|
}
|
|
350
357
|
|
|
358
|
+
// JOIN 查询时需要指定主表名前缀避免歧义
|
|
359
|
+
if (hasJoins && table) {
|
|
360
|
+
return {
|
|
361
|
+
...where,
|
|
362
|
+
[`${table}.state$gt`]: 0
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
351
366
|
// 默认查询 state > 0 的数据
|
|
352
367
|
return {
|
|
353
368
|
...where,
|
|
@@ -372,7 +387,7 @@ export class DbHelper {
|
|
|
372
387
|
* 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
|
|
373
388
|
* 4. 其他字段保持不变
|
|
374
389
|
*/
|
|
375
|
-
private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = [
|
|
390
|
+
private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ["id", "pid", "sort"]): T[] {
|
|
376
391
|
if (!arr || !Array.isArray(arr)) return arr as T[];
|
|
377
392
|
|
|
378
393
|
return arr.map((item) => {
|
|
@@ -391,9 +406,9 @@ export class DbHelper {
|
|
|
391
406
|
// 3. 以 '_id' 结尾(如 user_id, role_id)
|
|
392
407
|
// 4. 以 'At' 结尾(如 createdAt, updatedAt)
|
|
393
408
|
// 5. 以 '_at' 结尾(如 created_at, updated_at)
|
|
394
|
-
const shouldConvert = fields.includes(key) || key.endsWith(
|
|
409
|
+
const shouldConvert = fields.includes(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
|
|
395
410
|
|
|
396
|
-
if (shouldConvert && typeof value ===
|
|
411
|
+
if (shouldConvert && typeof value === "string") {
|
|
397
412
|
const num = Number(value);
|
|
398
413
|
if (!isNaN(num)) {
|
|
399
414
|
converted[key] = num;
|
|
@@ -406,12 +421,62 @@ export class DbHelper {
|
|
|
406
421
|
}) as T[];
|
|
407
422
|
}
|
|
408
423
|
|
|
424
|
+
/**
|
|
425
|
+
* 序列化数组字段(写入数据库前)
|
|
426
|
+
* 将数组类型的字段转换为 JSON 字符串
|
|
427
|
+
*/
|
|
428
|
+
private serializeArrayFields(data: Record<string, any>): Record<string, any> {
|
|
429
|
+
const serialized = { ...data };
|
|
430
|
+
|
|
431
|
+
for (const [key, value] of Object.entries(serialized)) {
|
|
432
|
+
// 跳过 null 和 undefined
|
|
433
|
+
if (value === null || value === undefined) continue;
|
|
434
|
+
|
|
435
|
+
// 数组类型序列化为 JSON 字符串
|
|
436
|
+
if (Array.isArray(value)) {
|
|
437
|
+
serialized[key] = JSON.stringify(value);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return serialized;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 反序列化数组字段(从数据库读取后)
|
|
446
|
+
* 将 JSON 字符串转换回数组
|
|
447
|
+
*/
|
|
448
|
+
private deserializeArrayFields<T = any>(data: Record<string, any> | null): T | null {
|
|
449
|
+
if (!data) return null;
|
|
450
|
+
|
|
451
|
+
const deserialized = { ...data };
|
|
452
|
+
|
|
453
|
+
for (const [key, value] of Object.entries(deserialized)) {
|
|
454
|
+
// 跳过非字符串值
|
|
455
|
+
if (typeof value !== "string") continue;
|
|
456
|
+
|
|
457
|
+
// 尝试解析 JSON 数组字符串
|
|
458
|
+
// 只解析符合 JSON 数组格式的字符串(以 [ 开头,以 ] 结尾)
|
|
459
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
460
|
+
try {
|
|
461
|
+
const parsed = JSON.parse(value);
|
|
462
|
+
if (Array.isArray(parsed)) {
|
|
463
|
+
deserialized[key] = parsed;
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// 解析失败则保持原值
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return deserialized as T;
|
|
472
|
+
}
|
|
473
|
+
|
|
409
474
|
/**
|
|
410
475
|
* Where 条件键名转下划线格式(递归处理嵌套)(私有方法)
|
|
411
476
|
* 支持操作符字段(如 userId$gt)和逻辑操作符($or, $and)
|
|
412
477
|
*/
|
|
413
478
|
private whereKeysToSnake(where: any): any {
|
|
414
|
-
if (!where || typeof where !==
|
|
479
|
+
if (!where || typeof where !== "object") return where;
|
|
415
480
|
|
|
416
481
|
// 处理数组($or, $and 等)
|
|
417
482
|
if (Array.isArray(where)) {
|
|
@@ -421,14 +486,14 @@ export class DbHelper {
|
|
|
421
486
|
const result: any = {};
|
|
422
487
|
for (const [key, value] of Object.entries(where)) {
|
|
423
488
|
// 保留 $or, $and 等逻辑操作符
|
|
424
|
-
if (key ===
|
|
489
|
+
if (key === "$or" || key === "$and") {
|
|
425
490
|
result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
|
|
426
491
|
continue;
|
|
427
492
|
}
|
|
428
493
|
|
|
429
494
|
// 处理带操作符的字段名(如 userId$gt)
|
|
430
|
-
if (key.includes(
|
|
431
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
495
|
+
if (key.includes("$")) {
|
|
496
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
432
497
|
const fieldName = key.substring(0, lastDollarIndex);
|
|
433
498
|
const operator = key.substring(lastDollarIndex);
|
|
434
499
|
const snakeKey = snakeCase(fieldName) + operator;
|
|
@@ -438,7 +503,7 @@ export class DbHelper {
|
|
|
438
503
|
|
|
439
504
|
// 普通字段:转换键名,递归处理值(支持嵌套对象)
|
|
440
505
|
const snakeKey = snakeCase(key);
|
|
441
|
-
if (typeof value ===
|
|
506
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
442
507
|
result[snakeKey] = this.whereKeysToSnake(value);
|
|
443
508
|
} else {
|
|
444
509
|
result[snakeKey] = value;
|
|
@@ -453,11 +518,11 @@ export class DbHelper {
|
|
|
453
518
|
*/
|
|
454
519
|
private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
|
|
455
520
|
if (!this.sql) {
|
|
456
|
-
throw new Error(
|
|
521
|
+
throw new Error("数据库连接未初始化");
|
|
457
522
|
}
|
|
458
523
|
|
|
459
524
|
// 强制类型检查:只接受字符串类型的 SQL
|
|
460
|
-
if (typeof sqlStr !==
|
|
525
|
+
if (typeof sqlStr !== "string") {
|
|
461
526
|
throw new Error(`executeWithConn 只接受字符串类型的 SQL,收到类型: ${typeof sqlStr},值: ${JSON.stringify(sqlStr)}`);
|
|
462
527
|
}
|
|
463
528
|
|
|
@@ -476,27 +541,35 @@ export class DbHelper {
|
|
|
476
541
|
// 计算执行时间
|
|
477
542
|
const duration = Date.now() - startTime;
|
|
478
543
|
|
|
479
|
-
// 慢查询警告(超过
|
|
480
|
-
if (duration >
|
|
481
|
-
|
|
482
|
-
|
|
544
|
+
// 慢查询警告(超过 5000ms)
|
|
545
|
+
if (duration > 5000) {
|
|
546
|
+
Logger.warn(
|
|
547
|
+
{
|
|
548
|
+
subsystem: "db",
|
|
549
|
+
event: "slow",
|
|
550
|
+
duration: duration,
|
|
551
|
+
sqlPreview: sqlStr,
|
|
552
|
+
params: params || [],
|
|
553
|
+
paramsCount: (params || []).length
|
|
554
|
+
},
|
|
555
|
+
"🐌 检测到慢查询"
|
|
556
|
+
);
|
|
483
557
|
}
|
|
484
558
|
|
|
485
559
|
return result;
|
|
486
560
|
} catch (error: any) {
|
|
487
561
|
const duration = Date.now() - startTime;
|
|
488
562
|
|
|
489
|
-
|
|
490
|
-
Logger.error(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
563
|
+
const sqlPreview = sqlStr.length > 200 ? sqlStr.substring(0, 200) + "..." : sqlStr;
|
|
564
|
+
Logger.error(
|
|
565
|
+
{
|
|
566
|
+
err: error,
|
|
567
|
+
sqlPreview: sqlPreview,
|
|
568
|
+
params: params || [],
|
|
569
|
+
duration: duration
|
|
570
|
+
},
|
|
571
|
+
"SQL 执行错误"
|
|
572
|
+
);
|
|
500
573
|
|
|
501
574
|
const enhancedError: any = new Error(`SQL执行失败: ${error.message}`);
|
|
502
575
|
enhancedError.originalError = error;
|
|
@@ -516,7 +589,7 @@ export class DbHelper {
|
|
|
516
589
|
// 将表名转换为下划线格式
|
|
517
590
|
const snakeTableName = snakeCase(tableName);
|
|
518
591
|
|
|
519
|
-
const result = await this.executeWithConn(
|
|
592
|
+
const result = await this.executeWithConn("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
520
593
|
|
|
521
594
|
return result?.[0]?.count > 0;
|
|
522
595
|
}
|
|
@@ -538,10 +611,13 @@ export class DbHelper {
|
|
|
538
611
|
* where: { 'o.state': 1 }
|
|
539
612
|
* });
|
|
540
613
|
*/
|
|
541
|
-
async getCount(options: Omit<QueryOptions,
|
|
614
|
+
async getCount(options: Omit<QueryOptions, "fields" | "page" | "limit" | "orderBy">): Promise<number> {
|
|
542
615
|
const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
|
|
543
616
|
|
|
544
|
-
const builder = new SqlBuilder()
|
|
617
|
+
const builder = new SqlBuilder()
|
|
618
|
+
.select(["COUNT(*) as count"])
|
|
619
|
+
.from(table)
|
|
620
|
+
.where(this.addDefaultStateFilter(where, table, !!joins));
|
|
545
621
|
|
|
546
622
|
// 添加 JOIN
|
|
547
623
|
this.applyJoins(builder, joins);
|
|
@@ -571,7 +647,10 @@ export class DbHelper {
|
|
|
571
647
|
async getOne<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<T | null> {
|
|
572
648
|
const { table, fields, where, joins } = await this.prepareQueryOptions(options);
|
|
573
649
|
|
|
574
|
-
const builder = new SqlBuilder()
|
|
650
|
+
const builder = new SqlBuilder()
|
|
651
|
+
.select(fields)
|
|
652
|
+
.from(table)
|
|
653
|
+
.where(this.addDefaultStateFilter(where, table, !!joins));
|
|
575
654
|
|
|
576
655
|
// 添加 JOIN
|
|
577
656
|
this.applyJoins(builder, joins);
|
|
@@ -585,8 +664,12 @@ export class DbHelper {
|
|
|
585
664
|
|
|
586
665
|
const camelRow = keysToCamel<T>(row);
|
|
587
666
|
|
|
667
|
+
// 反序列化数组字段(JSON 字符串 → 数组)
|
|
668
|
+
const deserialized = this.deserializeArrayFields<T>(camelRow);
|
|
669
|
+
if (!deserialized) return null;
|
|
670
|
+
|
|
588
671
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
589
|
-
return this.convertBigIntFields<T>([
|
|
672
|
+
return this.convertBigIntFields<T>([deserialized])[0];
|
|
590
673
|
}
|
|
591
674
|
|
|
592
675
|
/**
|
|
@@ -623,10 +706,10 @@ export class DbHelper {
|
|
|
623
706
|
}
|
|
624
707
|
|
|
625
708
|
// 构建查询
|
|
626
|
-
const whereFiltered = this.addDefaultStateFilter(prepared.where);
|
|
709
|
+
const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
|
|
627
710
|
|
|
628
711
|
// 查询总数
|
|
629
|
-
const countBuilder = new SqlBuilder().select([
|
|
712
|
+
const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
|
|
630
713
|
|
|
631
714
|
// 添加 JOIN(计数也需要)
|
|
632
715
|
this.applyJoins(countBuilder, prepared.joins);
|
|
@@ -664,9 +747,12 @@ export class DbHelper {
|
|
|
664
747
|
// 字段名转换:下划线 → 小驼峰
|
|
665
748
|
const camelList = arrayKeysToCamel<T>(list);
|
|
666
749
|
|
|
750
|
+
// 反序列化数组字段
|
|
751
|
+
const deserializedList = camelList.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
|
|
752
|
+
|
|
667
753
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
668
754
|
return {
|
|
669
|
-
lists: this.convertBigIntFields<T>(
|
|
755
|
+
lists: this.convertBigIntFields<T>(deserializedList),
|
|
670
756
|
total: total,
|
|
671
757
|
page: prepared.page,
|
|
672
758
|
limit: prepared.limit,
|
|
@@ -691,17 +777,17 @@ export class DbHelper {
|
|
|
691
777
|
* where: { 'o.state': 1 }
|
|
692
778
|
* })
|
|
693
779
|
*/
|
|
694
|
-
async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions,
|
|
780
|
+
async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, "page" | "limit">): Promise<AllResult<T>> {
|
|
695
781
|
// 添加硬性上限保护,防止内存溢出
|
|
696
782
|
const MAX_LIMIT = 10000;
|
|
697
783
|
const WARNING_LIMIT = 1000;
|
|
698
784
|
|
|
699
785
|
const prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
700
786
|
|
|
701
|
-
const whereFiltered = this.addDefaultStateFilter(prepared.where);
|
|
787
|
+
const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
|
|
702
788
|
|
|
703
789
|
// 查询真实总数
|
|
704
|
-
const countBuilder = new SqlBuilder().select([
|
|
790
|
+
const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
|
|
705
791
|
|
|
706
792
|
// 添加 JOIN(计数也需要)
|
|
707
793
|
this.applyJoins(countBuilder, prepared.joins);
|
|
@@ -733,7 +819,7 @@ export class DbHelper {
|
|
|
733
819
|
|
|
734
820
|
// 警告日志:返回数据超过警告阈值
|
|
735
821
|
if (result.length >= WARNING_LIMIT) {
|
|
736
|
-
Logger.warn({ table: options.table, count: result.length, total: total },
|
|
822
|
+
Logger.warn({ table: options.table, count: result.length, total: total }, "getAll 返回数据过多,建议使用 getList 分页查询");
|
|
737
823
|
}
|
|
738
824
|
|
|
739
825
|
// 如果达到上限,额外警告
|
|
@@ -744,8 +830,11 @@ export class DbHelper {
|
|
|
744
830
|
// 字段名转换:下划线 → 小驼峰
|
|
745
831
|
const camelResult = arrayKeysToCamel<T>(result);
|
|
746
832
|
|
|
833
|
+
// 反序列化数组字段
|
|
834
|
+
const deserializedList = camelResult.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
|
|
835
|
+
|
|
747
836
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
748
|
-
const lists = this.convertBigIntFields<T>(
|
|
837
|
+
const lists = this.convertBigIntFields<T>(deserializedList);
|
|
749
838
|
|
|
750
839
|
return {
|
|
751
840
|
lists: lists,
|
|
@@ -770,8 +859,11 @@ export class DbHelper {
|
|
|
770
859
|
// 字段名转换:小驼峰 → 下划线
|
|
771
860
|
const snakeData = keysToSnake(cleanData);
|
|
772
861
|
|
|
862
|
+
// 序列化数组字段(数组 → JSON 字符串)
|
|
863
|
+
const serializedData = this.serializeArrayFields(snakeData);
|
|
864
|
+
|
|
773
865
|
// 复制用户数据,但移除系统字段(防止用户尝试覆盖)
|
|
774
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } =
|
|
866
|
+
const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
|
|
775
867
|
|
|
776
868
|
const processed: Record<string, any> = { ...userData };
|
|
777
869
|
|
|
@@ -837,8 +929,11 @@ export class DbHelper {
|
|
|
837
929
|
// 字段名转换:小驼峰 → 下划线
|
|
838
930
|
const snakeData = keysToSnake(cleanData);
|
|
839
931
|
|
|
932
|
+
// 序列化数组字段(数组 → JSON 字符串)
|
|
933
|
+
const serializedData = this.serializeArrayFields(snakeData);
|
|
934
|
+
|
|
840
935
|
// 移除系统字段(防止用户尝试覆盖)
|
|
841
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } =
|
|
936
|
+
const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
|
|
842
937
|
|
|
843
938
|
// 强制生成系统字段(不可被用户覆盖)
|
|
844
939
|
return {
|
|
@@ -859,7 +954,7 @@ export class DbHelper {
|
|
|
859
954
|
await this.executeWithConn(sql, params);
|
|
860
955
|
return ids;
|
|
861
956
|
} catch (error: any) {
|
|
862
|
-
Logger.error({ err: error, table: table },
|
|
957
|
+
Logger.error({ err: error, table: table }, "批量插入失败");
|
|
863
958
|
throw error;
|
|
864
959
|
}
|
|
865
960
|
}
|
|
@@ -882,9 +977,12 @@ export class DbHelper {
|
|
|
882
977
|
const snakeData = keysToSnake(cleanData);
|
|
883
978
|
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
884
979
|
|
|
980
|
+
// 序列化数组字段(数组 → JSON 字符串)
|
|
981
|
+
const serializedData = this.serializeArrayFields(snakeData);
|
|
982
|
+
|
|
885
983
|
// 移除系统字段(防止用户尝试修改)
|
|
886
984
|
// 注意:state 允许用户修改(用于设置禁用状态 state=2)
|
|
887
|
-
const { id, created_at, updated_at, deleted_at, ...userData } =
|
|
985
|
+
const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, ...userData } = serializedData;
|
|
888
986
|
|
|
889
987
|
// 强制更新时间戳(不可被用户覆盖)
|
|
890
988
|
const processed: Record<string, any> = {
|
|
@@ -893,7 +991,7 @@ export class DbHelper {
|
|
|
893
991
|
};
|
|
894
992
|
|
|
895
993
|
// 构建 SQL
|
|
896
|
-
const whereFiltered = this.addDefaultStateFilter(snakeWhere);
|
|
994
|
+
const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
897
995
|
const builder = new SqlBuilder().where(whereFiltered);
|
|
898
996
|
const { sql, params } = builder.toUpdateSql(snakeTable, processed);
|
|
899
997
|
|
|
@@ -920,7 +1018,7 @@ export class DbHelper {
|
|
|
920
1018
|
* 硬删除数据(物理删除,不可恢复)
|
|
921
1019
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
922
1020
|
*/
|
|
923
|
-
async delForce(options: Omit<DeleteOptions,
|
|
1021
|
+
async delForce(options: Omit<DeleteOptions, "hard">): Promise<number> {
|
|
924
1022
|
const { table, where } = options;
|
|
925
1023
|
|
|
926
1024
|
// 转换表名:小驼峰 → 下划线
|
|
@@ -942,7 +1040,7 @@ export class DbHelper {
|
|
|
942
1040
|
* 禁用数据(设置 state=2)
|
|
943
1041
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
944
1042
|
*/
|
|
945
|
-
async disableData(options: Omit<DeleteOptions,
|
|
1043
|
+
async disableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
|
|
946
1044
|
const { table, where } = options;
|
|
947
1045
|
|
|
948
1046
|
return await this.updData({
|
|
@@ -958,7 +1056,7 @@ export class DbHelper {
|
|
|
958
1056
|
* 启用数据(设置 state=1)
|
|
959
1057
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
960
1058
|
*/
|
|
961
|
-
async enableData(options: Omit<DeleteOptions,
|
|
1059
|
+
async enableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
|
|
962
1060
|
const { table, where } = options;
|
|
963
1061
|
|
|
964
1062
|
return await this.updData({
|
|
@@ -999,11 +1097,15 @@ export class DbHelper {
|
|
|
999
1097
|
* 检查数据是否存在(优化性能)
|
|
1000
1098
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
1001
1099
|
*/
|
|
1002
|
-
async exists(options: Omit<QueryOptions,
|
|
1100
|
+
async exists(options: Omit<QueryOptions, "fields" | "orderBy" | "page" | "limit">): Promise<boolean> {
|
|
1003
1101
|
const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
1004
1102
|
|
|
1005
1103
|
// 使用 COUNT(1) 性能更好
|
|
1006
|
-
const builder = new SqlBuilder()
|
|
1104
|
+
const builder = new SqlBuilder()
|
|
1105
|
+
.select(["COUNT(1) as cnt"])
|
|
1106
|
+
.from(table)
|
|
1107
|
+
.where(this.addDefaultStateFilter(where, table, false))
|
|
1108
|
+
.limit(1);
|
|
1007
1109
|
|
|
1008
1110
|
const { sql, params } = builder.toSelectSql();
|
|
1009
1111
|
const result = await this.executeWithConn(sql, params);
|
|
@@ -1015,7 +1117,7 @@ export class DbHelper {
|
|
|
1015
1117
|
* 查询单个字段值(带字段名验证)
|
|
1016
1118
|
* @param field - 字段名(支持小驼峰或下划线格式)
|
|
1017
1119
|
*/
|
|
1018
|
-
async getFieldValue<T = any>(options: Omit<QueryOptions,
|
|
1120
|
+
async getFieldValue<T = any>(options: Omit<QueryOptions, "fields"> & { field: string }): Promise<T | null> {
|
|
1019
1121
|
const { field, ...queryOptions } = options;
|
|
1020
1122
|
|
|
1021
1123
|
// 验证字段名格式(只允许字母、数字、下划线)
|
|
@@ -1073,7 +1175,7 @@ export class DbHelper {
|
|
|
1073
1175
|
}
|
|
1074
1176
|
|
|
1075
1177
|
// 验证 value 必须是数字
|
|
1076
|
-
if (typeof value !==
|
|
1178
|
+
if (typeof value !== "number" || isNaN(value)) {
|
|
1077
1179
|
throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
|
|
1078
1180
|
}
|
|
1079
1181
|
|
|
@@ -1084,7 +1186,7 @@ export class DbHelper {
|
|
|
1084
1186
|
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
1085
1187
|
|
|
1086
1188
|
// 使用 SqlBuilder 构建安全的 WHERE 条件
|
|
1087
|
-
const whereFiltered = this.addDefaultStateFilter(snakeWhere);
|
|
1189
|
+
const whereFiltered = this.addDefaultStateFilter(snakeWhere, snakeTable, false);
|
|
1088
1190
|
const builder = new SqlBuilder().where(whereFiltered);
|
|
1089
1191
|
const { sql: whereClause, params: whereParams } = builder.getWhereConditions();
|
|
1090
1192
|
|