befly 3.0.0 → 3.1.1
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/checks/conflict.ts +35 -114
- package/checks/table.ts +31 -63
- package/config/env.ts +3 -3
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +1 -1
- package/main.ts +17 -71
- package/package.json +7 -28
- package/plugins/db.ts +11 -10
- package/plugins/redis.ts +5 -9
- package/scripts/syncDb/apply.ts +3 -3
- package/scripts/syncDb/constants.ts +2 -1
- package/scripts/syncDb/ddl.ts +15 -8
- package/scripts/syncDb/helpers.ts +3 -2
- package/scripts/syncDb/index.ts +23 -35
- package/scripts/syncDb/state.ts +8 -6
- package/scripts/syncDb/table.ts +32 -22
- package/scripts/syncDb/tableCreate.ts +9 -3
- package/scripts/syncDb/tests/constants.test.ts +2 -1
- package/scripts/syncDb.ts +10 -9
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +17 -14
- package/types/befly.d.ts +2 -6
- package/types/context.d.ts +7 -0
- package/types/database.d.ts +9 -14
- package/types/index.d.ts +442 -8
- package/types/index.ts +35 -56
- package/types/redis.d.ts +2 -0
- package/types/validator.d.ts +0 -2
- package/types/validator.ts +43 -0
- package/utils/colors.ts +117 -37
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +687 -116
- package/utils/helper.ts +812 -0
- package/utils/index.ts +10 -23
- package/utils/logger.ts +78 -171
- package/utils/redisHelper.ts +135 -152
- package/{types/context.ts → utils/requestContext.ts} +3 -3
- package/utils/sqlBuilder.ts +142 -165
- package/utils/validate.ts +51 -9
- package/apis/health/info.ts +0 -64
- package/apis/tool/tokenCheck.ts +0 -51
- package/bin/befly.ts +0 -202
- package/bunfig.toml +0 -3
- package/plugins/tool.ts +0 -34
- package/scripts/syncDev.ts +0 -112
- package/system.ts +0 -149
- package/tables/_common.json +0 -21
- package/tables/admin.json +0 -10
- package/utils/addonHelper.ts +0 -60
- package/utils/api.ts +0 -23
- package/utils/datetime.ts +0 -51
- package/utils/errorHandler.ts +0 -68
- package/utils/objectHelper.ts +0 -68
- package/utils/pluginHelper.ts +0 -62
- package/utils/response.ts +0 -38
- package/utils/sqlHelper.ts +0 -447
- package/utils/tableHelper.ts +0 -167
- package/utils/tool.ts +0 -230
- package/utils/typeHelper.ts +0 -101
package/utils/errorHandler.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 错误处理器
|
|
3
|
-
* 提供统一的错误处理策略
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { Logger } from './logger.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 错误处理器类
|
|
10
|
-
* 提供两种简单的错误处理方式:
|
|
11
|
-
* - critical: 关键错误,记录日志并退出进程
|
|
12
|
-
* - warning: 警告错误,只记录日志继续运行
|
|
13
|
-
*/
|
|
14
|
-
export class ErrorHandler {
|
|
15
|
-
/**
|
|
16
|
-
* 处理关键错误(会退出进程)
|
|
17
|
-
* 用于:系统检查失败、核心插件失败、配置错误等
|
|
18
|
-
*
|
|
19
|
-
* @param message - 错误消息
|
|
20
|
-
* @param error - 错误对象(可选)
|
|
21
|
-
* @param meta - 额外的元数据(可选)
|
|
22
|
-
*/
|
|
23
|
-
static critical(message: string, error?: Error, meta?: Record<string, any>): never {
|
|
24
|
-
Logger.error({
|
|
25
|
-
level: 'CRITICAL',
|
|
26
|
-
msg: message,
|
|
27
|
-
error: error?.message,
|
|
28
|
-
stack: error?.stack,
|
|
29
|
-
...meta
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
Logger.error('系统即将退出...');
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 处理警告错误(记录但继续运行)
|
|
38
|
-
* 用于:用户插件失败、单个API加载失败等非关键错误
|
|
39
|
-
*
|
|
40
|
-
* @param message - 警告消息
|
|
41
|
-
* @param error - 错误对象(可选)
|
|
42
|
-
* @param meta - 额外的元数据(可选)
|
|
43
|
-
*/
|
|
44
|
-
static warning(message: string, error?: Error, meta?: Record<string, any>): void {
|
|
45
|
-
Logger.warn({
|
|
46
|
-
level: 'WARNING',
|
|
47
|
-
msg: message,
|
|
48
|
-
error: error?.message,
|
|
49
|
-
stack: error?.stack,
|
|
50
|
-
...meta
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 处理信息级别的提示
|
|
56
|
-
* 用于:非错误的重要信息提示
|
|
57
|
-
*
|
|
58
|
-
* @param message - 信息消息
|
|
59
|
-
* @param meta - 额外的元数据(可选)
|
|
60
|
-
*/
|
|
61
|
-
static info(message: string, meta?: Record<string, any>): void {
|
|
62
|
-
Logger.info({
|
|
63
|
-
level: 'INFO',
|
|
64
|
-
msg: message,
|
|
65
|
-
...meta
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
}
|
package/utils/objectHelper.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Befly 对象操作工具
|
|
3
|
-
* 提供对象和数组的处理功能
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { isType } from './typeHelper.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 挑选指定字段
|
|
10
|
-
* @param obj - 源对象
|
|
11
|
-
* @param keys - 要挑选的字段名数组
|
|
12
|
-
* @returns 包含指定字段的新对象
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* pickFields({ a: 1, b: 2, c: 3 }, ['a', 'c']) // { a: 1, c: 3 }
|
|
16
|
-
* pickFields({ name: 'John', age: 30 }, ['name']) // { name: 'John' }
|
|
17
|
-
*/
|
|
18
|
-
export const pickFields = <T extends Record<string, any>>(obj: T, keys: string[]): Partial<T> => {
|
|
19
|
-
if (!obj || (!isType(obj, 'object') && !isType(obj, 'array'))) {
|
|
20
|
-
return {};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const result: any = {};
|
|
24
|
-
for (const key of keys) {
|
|
25
|
-
if (key in obj) {
|
|
26
|
-
result[key] = obj[key];
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return result;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 排除指定字段和值
|
|
35
|
-
* @param data - 源数据(对象或数组)
|
|
36
|
-
* @param excludeKeys - 要排除的字段名数组
|
|
37
|
-
* @param excludeValues - 要排除的值数组
|
|
38
|
-
* @returns 排除指定字段和值后的新数据
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* omitFields({ a: 1, b: 2, c: 3 }, ['b']) // { a: 1, c: 3 }
|
|
42
|
-
* omitFields({ a: 1, b: null, c: 3 }, [], [null]) // { a: 1, c: 3 }
|
|
43
|
-
* omitFields([{ a: 1 }, { a: 2 }], ['a']) // [{}, {}]
|
|
44
|
-
*/
|
|
45
|
-
export const omitFields = <T = any>(data: T, excludeKeys: string[] = [], excludeValues: any[] = []): T | Partial<T> => {
|
|
46
|
-
const shouldDropValue = (v: any): boolean => excludeValues.some((x) => x === v);
|
|
47
|
-
|
|
48
|
-
const cleanObject = (obj: any): any => {
|
|
49
|
-
if (!isType(obj, 'object')) return obj;
|
|
50
|
-
const result: any = {};
|
|
51
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
52
|
-
if (excludeKeys.includes(k)) continue;
|
|
53
|
-
if (shouldDropValue(v)) continue;
|
|
54
|
-
result[k] = v;
|
|
55
|
-
}
|
|
56
|
-
return result;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
if (isType(data, 'array')) {
|
|
60
|
-
return (data as any).filter((item: any) => !shouldDropValue(item)).map((item: any) => (isType(item, 'object') ? cleanObject(item) : item));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (isType(data, 'object')) {
|
|
64
|
-
return cleanObject(data);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return data;
|
|
68
|
-
};
|
package/utils/pluginHelper.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Befly 插件系统工具
|
|
3
|
-
* 提供插件依赖排序功能
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Plugin } from '../types/plugin.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 排序插件(根据依赖关系)
|
|
10
|
-
* 使用拓扑排序算法,确保依赖的插件先加载
|
|
11
|
-
*
|
|
12
|
-
* @param plugins - 插件数组
|
|
13
|
-
* @returns 排序后的插件数组,如果存在循环依赖则返回 false
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* const plugins = [
|
|
17
|
-
* { name: 'logger', dependencies: [] },
|
|
18
|
-
* { name: 'db', dependencies: ['logger'] },
|
|
19
|
-
* { name: 'api', dependencies: ['db', 'logger'] }
|
|
20
|
-
* ];
|
|
21
|
-
*
|
|
22
|
-
* const sorted = sortPlugins(plugins);
|
|
23
|
-
* // [
|
|
24
|
-
* // { name: 'logger', dependencies: [] },
|
|
25
|
-
* // { name: 'db', dependencies: ['logger'] },
|
|
26
|
-
* // { name: 'api', dependencies: ['db', 'logger'] }
|
|
27
|
-
* // ]
|
|
28
|
-
*
|
|
29
|
-
* // 循环依赖示例
|
|
30
|
-
* const badPlugins = [
|
|
31
|
-
* { name: 'a', dependencies: ['b'] },
|
|
32
|
-
* { name: 'b', dependencies: ['a'] }
|
|
33
|
-
* ];
|
|
34
|
-
* sortPlugins(badPlugins); // false
|
|
35
|
-
*/
|
|
36
|
-
export const sortPlugins = (plugins: Plugin[]): Plugin[] | false => {
|
|
37
|
-
const result: Plugin[] = [];
|
|
38
|
-
const visited = new Set<string>();
|
|
39
|
-
const visiting = new Set<string>();
|
|
40
|
-
const pluginMap: Record<string, Plugin> = Object.fromEntries(plugins.map((p) => [p.name, p]));
|
|
41
|
-
let isPass = true;
|
|
42
|
-
|
|
43
|
-
const visit = (name: string): void => {
|
|
44
|
-
if (visited.has(name)) return;
|
|
45
|
-
if (visiting.has(name)) {
|
|
46
|
-
isPass = false;
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const plugin = pluginMap[name];
|
|
51
|
-
if (!plugin) return; // 依赖不存在时跳过
|
|
52
|
-
|
|
53
|
-
visiting.add(name);
|
|
54
|
-
(plugin.dependencies || []).forEach(visit);
|
|
55
|
-
visiting.delete(name);
|
|
56
|
-
visited.add(name);
|
|
57
|
-
result.push(plugin);
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
plugins.forEach((p) => visit(p.name));
|
|
61
|
-
return isPass ? result : false;
|
|
62
|
-
};
|
package/utils/response.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Befly API 响应工具
|
|
3
|
-
* 提供统一的成功和失败响应格式
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { KeyValue } from '../types/common.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 成功响应
|
|
10
|
-
* @param msg - 响应消息
|
|
11
|
-
* @param data - 响应数据
|
|
12
|
-
* @param other - 其他字段
|
|
13
|
-
* @returns 成功响应对象 { code: 0, msg, data, ...other }
|
|
14
|
-
*/
|
|
15
|
-
export const Yes = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 0; msg: string; data: T | {} } & KeyValue => {
|
|
16
|
-
return {
|
|
17
|
-
...other,
|
|
18
|
-
code: 0,
|
|
19
|
-
msg: msg,
|
|
20
|
-
data: data
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 失败响应
|
|
26
|
-
* @param msg - 错误消息
|
|
27
|
-
* @param data - 错误数据
|
|
28
|
-
* @param other - 其他字段
|
|
29
|
-
* @returns 失败响应对象 { code: 1, msg, data, ...other }
|
|
30
|
-
*/
|
|
31
|
-
export const No = <T = any>(msg: string = '', data: T | {} = {}, other: KeyValue = {}): { code: 1; msg: string; data: T | {} } & KeyValue => {
|
|
32
|
-
return {
|
|
33
|
-
...other,
|
|
34
|
-
code: 1,
|
|
35
|
-
msg: msg,
|
|
36
|
-
data: data
|
|
37
|
-
};
|
|
38
|
-
};
|
package/utils/sqlHelper.ts
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQL 助手 - TypeScript 版本
|
|
3
|
-
* 提供数据库 CRUD 操作的封装
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { SqlBuilder } from './sqlBuilder.js';
|
|
7
|
-
import type { WhereConditions } from '../types/common.js';
|
|
8
|
-
import type { BeflyContext } from '../types/befly.js';
|
|
9
|
-
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback } from '../types/database.js';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* SQL 助手类
|
|
13
|
-
*/
|
|
14
|
-
export class SqlHelper {
|
|
15
|
-
private befly: BeflyContext;
|
|
16
|
-
private sql: any = null;
|
|
17
|
-
private isTransaction: boolean = false;
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 构造函数
|
|
21
|
-
* @param befly - Befly 上下文
|
|
22
|
-
* @param sql - Bun SQL 客户端(可选,用于事务)
|
|
23
|
-
*/
|
|
24
|
-
constructor(befly: BeflyContext, sql: any = null) {
|
|
25
|
-
this.befly = befly;
|
|
26
|
-
this.sql = sql;
|
|
27
|
-
this.isTransaction = !!sql;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 处理插入数据(强制生成系统字段,用户不可覆盖)
|
|
32
|
-
*/
|
|
33
|
-
private async processDataForInsert(data: Record<string, any>): Promise<Record<string, any>> {
|
|
34
|
-
// 复制用户数据,但移除系统字段(防止用户尝试覆盖)
|
|
35
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } = data;
|
|
36
|
-
|
|
37
|
-
const processed: Record<string, any> = { ...userData };
|
|
38
|
-
|
|
39
|
-
// 强制生成 ID(不可被用户覆盖)
|
|
40
|
-
try {
|
|
41
|
-
processed.id = await this.befly.redis.genTimeID();
|
|
42
|
-
} catch (error: any) {
|
|
43
|
-
throw new Error(`生成 ID 失败,Redis 可能不可用:${error.message}`);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 强制生成时间戳(不可被用户覆盖)
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
processed.created_at = now;
|
|
49
|
-
processed.updated_at = now;
|
|
50
|
-
|
|
51
|
-
// 强制设置 state 为 1(激活状态,不可被用户覆盖)
|
|
52
|
-
processed.state = 1;
|
|
53
|
-
|
|
54
|
-
// 注意:deleted_at 字段不在插入时生成,只在软删除时设置
|
|
55
|
-
|
|
56
|
-
return processed;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 添加默认 state 过滤(排除已删除数据)
|
|
61
|
-
*/
|
|
62
|
-
private addDefaultStateFilter(where: WhereConditions | undefined, includeDeleted: boolean = false, customState?: WhereConditions): WhereConditions {
|
|
63
|
-
if (includeDeleted) {
|
|
64
|
-
return where || {};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 如果有自定义 state 条件,使用自定义条件
|
|
68
|
-
if (customState) {
|
|
69
|
-
return where ? { ...where, ...customState } : customState;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 检查用户是否已经在 where 中指定了 state 条件
|
|
73
|
-
if (where && 'state' in where) {
|
|
74
|
-
// 用户已指定 state 条件,直接返回,不覆盖
|
|
75
|
-
return where;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 默认排除已删除(state = 0)
|
|
79
|
-
const stateFilter: WhereConditions = { state: { $gt: 0 } };
|
|
80
|
-
return where ? { ...where, ...stateFilter } : stateFilter;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 执行 SQL(使用 sql.unsafe,带慢查询日志)
|
|
85
|
-
*/
|
|
86
|
-
private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
|
|
87
|
-
if (!this.sql) {
|
|
88
|
-
throw new Error('数据库连接未初始化');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// 记录开始时间
|
|
92
|
-
const startTime = Date.now();
|
|
93
|
-
|
|
94
|
-
// 使用 sql.unsafe 执行查询
|
|
95
|
-
let result;
|
|
96
|
-
if (params && params.length > 0) {
|
|
97
|
-
result = await this.sql.unsafe(sqlStr, params);
|
|
98
|
-
} else {
|
|
99
|
-
result = await this.sql.unsafe(sqlStr);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 计算执行时间
|
|
103
|
-
const duration = Date.now() - startTime;
|
|
104
|
-
|
|
105
|
-
// 慢查询警告(超过 1000ms)
|
|
106
|
-
if (duration > 1000) {
|
|
107
|
-
const sqlPreview = sqlStr.length > 100 ? sqlStr.substring(0, 100) + '...' : sqlStr;
|
|
108
|
-
console.warn(`🐌 检测到慢查询 (${duration}ms): ${sqlPreview}`);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 查询单条数据
|
|
116
|
-
*/
|
|
117
|
-
async getDetail<T = any>(options: QueryOptions): Promise<T | null> {
|
|
118
|
-
const { table, fields = ['*'], where, includeDeleted = false, customState } = options;
|
|
119
|
-
|
|
120
|
-
const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where, includeDeleted, customState)).limit(1);
|
|
121
|
-
|
|
122
|
-
const { sql, params } = builder.toSelectSql();
|
|
123
|
-
const result = await this.executeWithConn(sql, params);
|
|
124
|
-
|
|
125
|
-
return result?.[0] || null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 查询列表(带分页)
|
|
130
|
-
*/
|
|
131
|
-
async getList<T = any>(options: QueryOptions): Promise<ListResult<T>> {
|
|
132
|
-
const { table, fields = ['*'], where, orderBy = [], page = 1, limit = 10, includeDeleted = false, customState } = options;
|
|
133
|
-
|
|
134
|
-
// P1: 添加参数上限校验
|
|
135
|
-
if (page < 1 || page > 10000) {
|
|
136
|
-
throw new Error('页码必须在 1 到 10000 之间');
|
|
137
|
-
}
|
|
138
|
-
if (limit < 1 || limit > 1000) {
|
|
139
|
-
throw new Error('每页数量必须在 1 到 1000 之间');
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// 构建查询
|
|
143
|
-
const whereFiltered = this.addDefaultStateFilter(where, includeDeleted, customState);
|
|
144
|
-
|
|
145
|
-
// 查询总数
|
|
146
|
-
const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(table).where(whereFiltered);
|
|
147
|
-
|
|
148
|
-
const { sql: countSql, params: countParams } = countBuilder.toSelectSql();
|
|
149
|
-
const countResult = await this.executeWithConn(countSql, countParams);
|
|
150
|
-
const total = countResult?.[0]?.total || 0;
|
|
151
|
-
|
|
152
|
-
// P1: 如果总数为 0,直接返回,不执行第二次查询
|
|
153
|
-
if (total === 0) {
|
|
154
|
-
return {
|
|
155
|
-
list: [],
|
|
156
|
-
total: 0,
|
|
157
|
-
page,
|
|
158
|
-
limit,
|
|
159
|
-
pages: 0
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// 查询数据
|
|
164
|
-
const offset = (page - 1) * limit;
|
|
165
|
-
const dataBuilder = new SqlBuilder().select(fields).from(table).where(whereFiltered).limit(limit).offset(offset);
|
|
166
|
-
|
|
167
|
-
// P1: 只有用户明确指定了 orderBy 才添加排序
|
|
168
|
-
if (orderBy && orderBy.length > 0) {
|
|
169
|
-
dataBuilder.orderBy(orderBy);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const { sql: dataSql, params: dataParams } = dataBuilder.toSelectSql();
|
|
173
|
-
const list = (await this.executeWithConn(dataSql, dataParams)) || [];
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
list,
|
|
177
|
-
total,
|
|
178
|
-
page,
|
|
179
|
-
limit,
|
|
180
|
-
pages: Math.ceil(total / limit)
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* 查询所有数据(不分页,有上限保护)
|
|
186
|
-
* ⚠️ 警告:此方法会查询大量数据,建议使用 getList 分页查询
|
|
187
|
-
*/
|
|
188
|
-
async getAll<T = any>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<T[]> {
|
|
189
|
-
const { table, fields = ['*'], where, orderBy, includeDeleted = false, customState } = options;
|
|
190
|
-
|
|
191
|
-
// 添加硬性上限保护,防止内存溢出
|
|
192
|
-
const MAX_LIMIT = 10000;
|
|
193
|
-
const WARNING_LIMIT = 1000;
|
|
194
|
-
|
|
195
|
-
const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where, includeDeleted, customState)).limit(MAX_LIMIT); // 强制添加上限
|
|
196
|
-
|
|
197
|
-
if (orderBy) {
|
|
198
|
-
builder.orderBy(orderBy);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const { sql, params } = builder.toSelectSql();
|
|
202
|
-
const result = (await this.executeWithConn(sql, params)) || [];
|
|
203
|
-
|
|
204
|
-
// 警告日志:返回数据超过警告阈值
|
|
205
|
-
if (result.length >= WARNING_LIMIT) {
|
|
206
|
-
console.warn(`⚠️ getAll 从表 \`${table}\` 返回了 ${result.length} 行数据,建议使用 getList 分页查询以获得更好的性能。`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// 如果达到上限,额外警告
|
|
210
|
-
if (result.length >= MAX_LIMIT) {
|
|
211
|
-
console.warn(`🚨 getAll 达到了最大限制 (${MAX_LIMIT}),可能还有更多数据。请使用 getList 分页查询。`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return result;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* 插入数据(自动生成 ID、时间戳、state)
|
|
219
|
-
*/
|
|
220
|
-
async insData(options: InsertOptions): Promise<number> {
|
|
221
|
-
const { table, data } = options;
|
|
222
|
-
|
|
223
|
-
// 处理数据(自动添加必要字段)
|
|
224
|
-
const processed = await this.processDataForInsert(data);
|
|
225
|
-
|
|
226
|
-
// 构建 SQL
|
|
227
|
-
const builder = new SqlBuilder();
|
|
228
|
-
const { sql, params } = builder.toInsertSql(table, processed);
|
|
229
|
-
|
|
230
|
-
// 执行
|
|
231
|
-
const result = await this.executeWithConn(sql, params);
|
|
232
|
-
return processed.id || result?.lastInsertRowid || 0;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* 批量插入数据(真正的批量操作)
|
|
237
|
-
* 使用 INSERT INTO ... VALUES (...), (...), (...) 语法
|
|
238
|
-
* 自动生成系统字段并包装在事务中
|
|
239
|
-
*/
|
|
240
|
-
async insDataBatch(table: string, dataList: Record<string, any>[]): Promise<number[]> {
|
|
241
|
-
// 空数组直接返回
|
|
242
|
-
if (dataList.length === 0) {
|
|
243
|
-
return [];
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// 限制批量大小
|
|
247
|
-
const MAX_BATCH_SIZE = 1000;
|
|
248
|
-
if (dataList.length > MAX_BATCH_SIZE) {
|
|
249
|
-
throw new Error(`批量插入数量 ${dataList.length} 超过最大限制 ${MAX_BATCH_SIZE},请分批插入。`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// 批量生成 ID(一次性从 Redis 获取 N 个 ID)
|
|
253
|
-
const ids = await this.befly.redis.genTimeIDBatch(dataList.length);
|
|
254
|
-
const now = Date.now();
|
|
255
|
-
|
|
256
|
-
// 处理所有数据(自动添加系统字段)
|
|
257
|
-
const processedList = dataList.map((data, index) => {
|
|
258
|
-
// 移除系统字段(防止用户尝试覆盖)
|
|
259
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } = data;
|
|
260
|
-
|
|
261
|
-
// 强制生成系统字段(不可被用户覆盖)
|
|
262
|
-
return {
|
|
263
|
-
...userData,
|
|
264
|
-
id: ids[index],
|
|
265
|
-
created_at: now,
|
|
266
|
-
updated_at: now,
|
|
267
|
-
state: 1
|
|
268
|
-
};
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
// 构建批量插入 SQL
|
|
272
|
-
const builder = new SqlBuilder();
|
|
273
|
-
const { sql, params } = builder.toInsertSql(table, processedList);
|
|
274
|
-
|
|
275
|
-
// 在事务中执行批量插入
|
|
276
|
-
try {
|
|
277
|
-
await this.executeWithConn(sql, params);
|
|
278
|
-
return ids;
|
|
279
|
-
} catch (error: any) {
|
|
280
|
-
// 批量插入失败,记录错误
|
|
281
|
-
console.error(`表 \`${table}\` 批量插入失败:`, error.message);
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* 更新数据(强制更新时间戳,系统字段不可修改)
|
|
288
|
-
*/
|
|
289
|
-
async updData(options: UpdateOptions): Promise<number> {
|
|
290
|
-
const { table, data, where, includeDeleted = false } = options;
|
|
291
|
-
|
|
292
|
-
// 移除系统字段(防止用户尝试修改)
|
|
293
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } = data;
|
|
294
|
-
|
|
295
|
-
// 强制更新时间戳(不可被用户覆盖)
|
|
296
|
-
const processed: Record<string, any> = {
|
|
297
|
-
...userData,
|
|
298
|
-
updated_at: Date.now()
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
// 构建 SQL
|
|
302
|
-
const whereFiltered = this.addDefaultStateFilter(where, includeDeleted);
|
|
303
|
-
const builder = new SqlBuilder().where(whereFiltered);
|
|
304
|
-
const { sql, params } = builder.toUpdateSql(table, processed);
|
|
305
|
-
|
|
306
|
-
// 执行
|
|
307
|
-
const result = await this.executeWithConn(sql, params);
|
|
308
|
-
return result?.changes || 0;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* 删除数据(软删除会记录删除时间)
|
|
313
|
-
*/
|
|
314
|
-
async delData(options: DeleteOptions): Promise<number> {
|
|
315
|
-
const { table, where, hard = false } = options;
|
|
316
|
-
|
|
317
|
-
if (hard) {
|
|
318
|
-
// 物理删除
|
|
319
|
-
const builder = new SqlBuilder().where(where);
|
|
320
|
-
const { sql, params } = builder.toDeleteSql(table);
|
|
321
|
-
|
|
322
|
-
const result = await this.executeWithConn(sql, params);
|
|
323
|
-
return result?.changes || 0;
|
|
324
|
-
} else {
|
|
325
|
-
// 软删除(设置 state=0 并记录删除时间)
|
|
326
|
-
const now = Date.now();
|
|
327
|
-
const data: Record<string, any> = {
|
|
328
|
-
state: 0,
|
|
329
|
-
updated_at: now,
|
|
330
|
-
deleted_at: now // 记录删除时间
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
return await this.updData({
|
|
334
|
-
table,
|
|
335
|
-
data,
|
|
336
|
-
where,
|
|
337
|
-
includeDeleted: true // 软删除时允许操作已删除数据
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* 执行事务
|
|
344
|
-
*/
|
|
345
|
-
async trans<T = any>(callback: TransactionCallback<T>): Promise<T> {
|
|
346
|
-
if (this.isTransaction) {
|
|
347
|
-
// 已经在事务中,直接执行回调
|
|
348
|
-
return await callback(this);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// 开启新事务
|
|
352
|
-
const conn = await this.befly.db.transaction();
|
|
353
|
-
|
|
354
|
-
try {
|
|
355
|
-
const trans = new SqlHelper(this.befly, conn);
|
|
356
|
-
const result = await callback(trans);
|
|
357
|
-
await conn.query('COMMIT');
|
|
358
|
-
return result;
|
|
359
|
-
} catch (error) {
|
|
360
|
-
await conn.query('ROLLBACK');
|
|
361
|
-
throw error;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* 执行原始 SQL
|
|
367
|
-
*/
|
|
368
|
-
async query(sql: string, params?: any[]): Promise<any> {
|
|
369
|
-
return await this.executeWithConn(sql, params);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* 检查数据是否存在(优化性能)
|
|
374
|
-
*/
|
|
375
|
-
async exists(options: Omit<QueryOptions, 'fields' | 'orderBy' | 'page' | 'limit'>): Promise<boolean> {
|
|
376
|
-
const { table, where, includeDeleted = false, customState } = options;
|
|
377
|
-
|
|
378
|
-
// 使用 COUNT(1) 性能更好
|
|
379
|
-
const builder = new SqlBuilder().select(['COUNT(1) as cnt']).from(table).where(this.addDefaultStateFilter(where, includeDeleted, customState)).limit(1);
|
|
380
|
-
|
|
381
|
-
const { sql, params } = builder.toSelectSql();
|
|
382
|
-
const result = await this.executeWithConn(sql, params);
|
|
383
|
-
|
|
384
|
-
return (result?.[0]?.cnt || 0) > 0;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* 查询单个字段值(带字段名验证)
|
|
389
|
-
*/
|
|
390
|
-
async getFieldValue<T = any>(options: Omit<QueryOptions, 'fields'> & { field: string }): Promise<T | null> {
|
|
391
|
-
const { field, ...queryOptions } = options;
|
|
392
|
-
|
|
393
|
-
// 验证字段名格式(只允许字母、数字、下划线)
|
|
394
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
395
|
-
throw new Error(`无效的字段名: ${field},只允许字母、数字和下划线。`);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const result = await this.getDetail({
|
|
399
|
-
...queryOptions,
|
|
400
|
-
fields: [field]
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
return result ? result[field] : null;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
/**
|
|
407
|
-
* 自增字段(安全实现,防止 SQL 注入)
|
|
408
|
-
*/
|
|
409
|
-
async increment(table: string, field: string, where: WhereConditions, value: number = 1): Promise<number> {
|
|
410
|
-
// 验证表名格式(只允许字母、数字、下划线)
|
|
411
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) {
|
|
412
|
-
throw new Error(`无效的表名: ${table}`);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// 验证字段名格式(只允许字母、数字、下划线)
|
|
416
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) {
|
|
417
|
-
throw new Error(`无效的字段名: ${field}`);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// 验证 value 必须是数字
|
|
421
|
-
if (typeof value !== 'number' || isNaN(value)) {
|
|
422
|
-
throw new Error(`自增值必须是有效的数字`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// 使用 SqlBuilder 构建安全的 WHERE 条件
|
|
426
|
-
const whereFiltered = this.addDefaultStateFilter(where, false);
|
|
427
|
-
const builder = new SqlBuilder().where(whereFiltered);
|
|
428
|
-
const { sql: selectSql, params: whereParams } = builder.toSelectSql();
|
|
429
|
-
|
|
430
|
-
// 提取 WHERE 子句(找到 WHERE 关键字后的部分)
|
|
431
|
-
const whereIndex = selectSql.indexOf('WHERE');
|
|
432
|
-
const whereClause = whereIndex > -1 ? selectSql.substring(whereIndex + 6).trim() : '1=1';
|
|
433
|
-
|
|
434
|
-
// 构建安全的 UPDATE SQL(表名和字段名使用反引号转义)
|
|
435
|
-
const sql = `UPDATE \`${table}\` SET \`${field}\` = \`${field}\` + ? WHERE ${whereClause}`;
|
|
436
|
-
|
|
437
|
-
const result = await this.executeWithConn(sql, [value, ...whereParams]);
|
|
438
|
-
return result?.changes || 0;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* 自减字段
|
|
443
|
-
*/
|
|
444
|
-
async decrement(table: string, field: string, where: WhereConditions, value: number = 1): Promise<number> {
|
|
445
|
-
return await this.increment(table, field, where, -value);
|
|
446
|
-
}
|
|
447
|
-
}
|