befly 2.3.3 → 3.0.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 +329 -0
- package/checks/table.ts +252 -0
- package/config/env.ts +218 -0
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +96 -0
- package/main.ts +47 -0
- package/package.json +26 -11
- package/plugins/db.ts +60 -0
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +47 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +71 -0
- package/scripts/syncDb/ddl.ts +189 -0
- package/scripts/syncDb/helpers.ts +173 -0
- package/scripts/syncDb/index.ts +203 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +106 -0
- package/scripts/syncDb/table.ts +214 -0
- package/scripts/syncDb/tableCreate.ts +148 -0
- package/scripts/syncDb/tests/constants.test.ts +105 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +10 -0
- package/tsconfig.json +58 -0
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +249 -0
- package/types/befly.d.ts +230 -0
- package/types/common.d.ts +215 -0
- package/types/context.d.ts +7 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +273 -0
- package/types/index.d.ts +450 -0
- package/types/index.ts +438 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +46 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +43 -0
- package/types/validator.ts +43 -0
- package/utils/colors.ts +221 -0
- package/utils/crypto.ts +308 -0
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +713 -0
- package/utils/helper.ts +812 -0
- package/utils/index.ts +33 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +191 -0
- package/utils/redisHelper.ts +321 -0
- package/utils/requestContext.ts +167 -0
- package/utils/sqlBuilder.ts +611 -0
- package/utils/validate.ts +493 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/bin/befly.js +0 -109
- package/bunfig.toml +0 -3
- package/checks/table.js +0 -206
- package/config/env.js +0 -64
- package/main.js +0 -579
- package/plugins/db.js +0 -46
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -752
- package/scripts/syncDev.js +0 -96
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/colors.js +0 -83
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -334
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlBuilder.js +0 -498
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -226
package/utils/crypto.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 加密工具类 - TypeScript 版本
|
|
3
|
+
* 提供各种哈希、HMAC、密码加密等功能
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createSign } from 'node:crypto';
|
|
7
|
+
import type { EncodingType, HashAlgorithm, PasswordHashOptions } from '../types/crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 加密工具类
|
|
11
|
+
*/
|
|
12
|
+
export class Crypto2 {
|
|
13
|
+
/**
|
|
14
|
+
* MD5 哈希
|
|
15
|
+
* @param data - 要哈希的数据
|
|
16
|
+
* @param encoding - 输出编码
|
|
17
|
+
* @returns MD5 哈希值
|
|
18
|
+
*/
|
|
19
|
+
static md5(data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
20
|
+
const hasher = new Bun.CryptoHasher('md5');
|
|
21
|
+
hasher.update(data);
|
|
22
|
+
return hasher.digest(encoding);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* HMAC-MD5 签名
|
|
27
|
+
* @param key - 密钥
|
|
28
|
+
* @param data - 要签名的数据
|
|
29
|
+
* @param encoding - 输出编码
|
|
30
|
+
* @returns HMAC-MD5 签名
|
|
31
|
+
*/
|
|
32
|
+
static hmacMd5(key: string | Uint8Array, data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
33
|
+
const hasher = new Bun.CryptoHasher('md5', key);
|
|
34
|
+
hasher.update(data);
|
|
35
|
+
return hasher.digest(encoding);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SHA-1 哈希
|
|
40
|
+
* @param data - 要哈希的数据
|
|
41
|
+
* @param encoding - 输出编码
|
|
42
|
+
* @returns SHA-1 哈希值
|
|
43
|
+
*/
|
|
44
|
+
static sha1(data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
45
|
+
const hasher = new Bun.CryptoHasher('sha1');
|
|
46
|
+
hasher.update(data);
|
|
47
|
+
return hasher.digest(encoding);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* HMAC-SHA1 签名
|
|
52
|
+
* @param key - 密钥
|
|
53
|
+
* @param data - 要签名的数据
|
|
54
|
+
* @param encoding - 输出编码
|
|
55
|
+
* @returns HMAC-SHA1 签名
|
|
56
|
+
*/
|
|
57
|
+
static hmacSha1(key: string | Uint8Array, data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
58
|
+
const hasher = new Bun.CryptoHasher('sha1', key);
|
|
59
|
+
hasher.update(data);
|
|
60
|
+
return hasher.digest(encoding);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* SHA-256 哈希
|
|
65
|
+
* @param data - 要哈希的数据
|
|
66
|
+
* @param encoding - 输出编码
|
|
67
|
+
* @returns SHA-256 哈希值
|
|
68
|
+
*/
|
|
69
|
+
static sha256(data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
70
|
+
const hasher = new Bun.CryptoHasher('sha256');
|
|
71
|
+
hasher.update(data);
|
|
72
|
+
return hasher.digest(encoding);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* RSA-SHA256 签名
|
|
77
|
+
* @param data - 要签名的数据
|
|
78
|
+
* @param privateKey - 私钥
|
|
79
|
+
* @param encoding - 输出编码
|
|
80
|
+
* @returns RSA-SHA256 签名
|
|
81
|
+
*/
|
|
82
|
+
static rsaSha256(data: string, privateKey: string | Buffer, encoding: BufferEncoding = 'hex'): string {
|
|
83
|
+
const sign = createSign('RSA-SHA256');
|
|
84
|
+
sign.update(data);
|
|
85
|
+
const signature = sign.sign(privateKey, encoding);
|
|
86
|
+
return signature;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* HMAC-SHA256 签名
|
|
91
|
+
* @param key - 密钥
|
|
92
|
+
* @param data - 要签名的数据
|
|
93
|
+
* @param encoding - 输出编码
|
|
94
|
+
* @returns HMAC-SHA256 签名
|
|
95
|
+
*/
|
|
96
|
+
static hmacSha256(key: string | Uint8Array, data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
97
|
+
const hasher = new Bun.CryptoHasher('sha256', key);
|
|
98
|
+
hasher.update(data);
|
|
99
|
+
return hasher.digest(encoding);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* SHA-512 哈希
|
|
104
|
+
* @param data - 要哈希的数据
|
|
105
|
+
* @param encoding - 输出编码
|
|
106
|
+
* @returns SHA-512 哈希值
|
|
107
|
+
*/
|
|
108
|
+
static sha512(data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
109
|
+
const hasher = new Bun.CryptoHasher('sha512');
|
|
110
|
+
hasher.update(data);
|
|
111
|
+
return hasher.digest(encoding);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* HMAC-SHA512 签名
|
|
116
|
+
* @param key - 密钥
|
|
117
|
+
* @param data - 要签名的数据
|
|
118
|
+
* @param encoding - 输出编码
|
|
119
|
+
* @returns HMAC-SHA512 签名
|
|
120
|
+
*/
|
|
121
|
+
static hmacSha512(key: string | Uint8Array, data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
122
|
+
const hasher = new Bun.CryptoHasher('sha512', key);
|
|
123
|
+
hasher.update(data);
|
|
124
|
+
return hasher.digest(encoding);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 通用哈希方法
|
|
129
|
+
* @param algorithm - 算法名称
|
|
130
|
+
* @param data - 要哈希的数据
|
|
131
|
+
* @param encoding - 输出编码
|
|
132
|
+
* @returns 哈希值
|
|
133
|
+
*/
|
|
134
|
+
static hash(algorithm: HashAlgorithm, data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
135
|
+
const hasher = new Bun.CryptoHasher(algorithm);
|
|
136
|
+
hasher.update(data);
|
|
137
|
+
return hasher.digest(encoding);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 通用 HMAC 方法
|
|
142
|
+
* @param algorithm - 算法名称
|
|
143
|
+
* @param key - 密钥
|
|
144
|
+
* @param data - 要签名的数据
|
|
145
|
+
* @param encoding - 输出编码
|
|
146
|
+
* @returns HMAC 签名
|
|
147
|
+
*/
|
|
148
|
+
static hmac(algorithm: HashAlgorithm, key: string | Uint8Array, data: string | Uint8Array, encoding: EncodingType = 'hex'): string {
|
|
149
|
+
const hasher = new Bun.CryptoHasher(algorithm, key);
|
|
150
|
+
hasher.update(data);
|
|
151
|
+
return hasher.digest(encoding);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 文件哈希
|
|
156
|
+
* @param filePath - 文件路径
|
|
157
|
+
* @param algorithm - 算法名称
|
|
158
|
+
* @param encoding - 输出编码
|
|
159
|
+
* @returns 文件哈希值
|
|
160
|
+
*/
|
|
161
|
+
static async hashFile(filePath: string, algorithm: HashAlgorithm = 'sha256', encoding: EncodingType = 'hex'): Promise<string> {
|
|
162
|
+
const file = Bun.file(filePath);
|
|
163
|
+
const hasher = new Bun.CryptoHasher(algorithm);
|
|
164
|
+
|
|
165
|
+
const stream = file.stream();
|
|
166
|
+
const reader = stream.getReader();
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
while (true) {
|
|
170
|
+
const { done, value } = await reader.read();
|
|
171
|
+
if (done) break;
|
|
172
|
+
hasher.update(value);
|
|
173
|
+
}
|
|
174
|
+
return hasher.digest(encoding);
|
|
175
|
+
} finally {
|
|
176
|
+
reader.releaseLock();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 创建流式哈希器
|
|
182
|
+
* @param algorithm - 算法名称
|
|
183
|
+
* @param key - 可选的 HMAC 密钥
|
|
184
|
+
* @returns 流式哈希器实例
|
|
185
|
+
*/
|
|
186
|
+
static createHasher(algorithm: HashAlgorithm, key: string | Uint8Array | null = null): StreamHasher {
|
|
187
|
+
return new StreamHasher(algorithm, key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 密码哈希 (使用 Argon2)
|
|
192
|
+
* @param password - 密码
|
|
193
|
+
* @param options - 选项
|
|
194
|
+
* @returns 哈希后的密码
|
|
195
|
+
*/
|
|
196
|
+
static async hashPassword(password: string, options: PasswordHashOptions = {}): Promise<string> {
|
|
197
|
+
// 设置默认算法为 bcrypt
|
|
198
|
+
const finalOptions = {
|
|
199
|
+
algorithm: 'bcrypt',
|
|
200
|
+
...options
|
|
201
|
+
} as any;
|
|
202
|
+
return await Bun.password.hash(password, finalOptions);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 验证密码
|
|
207
|
+
* @param password - 原始密码
|
|
208
|
+
* @param hash - 哈希值
|
|
209
|
+
* @returns 验证结果
|
|
210
|
+
*/
|
|
211
|
+
static async verifyPassword(password: string, hash: string): Promise<boolean> {
|
|
212
|
+
return await Bun.password.verify(password, hash);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Base64 编码
|
|
217
|
+
* @param data - 要编码的数据
|
|
218
|
+
* @returns Base64 编码的字符串
|
|
219
|
+
*/
|
|
220
|
+
static base64Encode(data: string): string {
|
|
221
|
+
return Buffer.from(data, 'utf8').toString('base64');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Base64 解码
|
|
226
|
+
* @param data - Base64 编码的字符串
|
|
227
|
+
* @returns 解码后的字符串
|
|
228
|
+
*/
|
|
229
|
+
static base64Decode(data: string): string {
|
|
230
|
+
return Buffer.from(data, 'base64').toString('utf8');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 生成随机十六进制字符串
|
|
235
|
+
* @param length - 字符串长度
|
|
236
|
+
* @returns 随机十六进制字符串
|
|
237
|
+
*/
|
|
238
|
+
static randomString(length: number): string {
|
|
239
|
+
const bytes = Math.ceil(length / 2);
|
|
240
|
+
const randomBytes = crypto.getRandomValues(new Uint8Array(bytes));
|
|
241
|
+
let result = '';
|
|
242
|
+
for (let i = 0; i < randomBytes.length; i++) {
|
|
243
|
+
result += randomBytes[i].toString(16).padStart(2, '0');
|
|
244
|
+
}
|
|
245
|
+
return result.slice(0, length);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 快速哈希 (非密码学)
|
|
250
|
+
* @param data - 数据
|
|
251
|
+
* @param seed - 种子值
|
|
252
|
+
* @returns 64位哈希值
|
|
253
|
+
*/
|
|
254
|
+
static fastHash(data: string | Uint8Array, seed: number = 0): number {
|
|
255
|
+
return Bun.hash(data, seed);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 流式哈希器类
|
|
261
|
+
*/
|
|
262
|
+
export class StreamHasher {
|
|
263
|
+
private hasher: any;
|
|
264
|
+
private finalized: boolean = false;
|
|
265
|
+
|
|
266
|
+
constructor(algorithm: HashAlgorithm, key: string | Uint8Array | null = null) {
|
|
267
|
+
this.hasher = new Bun.CryptoHasher(algorithm, key);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 更新数据
|
|
272
|
+
* @param data - 数据
|
|
273
|
+
* @returns 支持链式调用
|
|
274
|
+
*/
|
|
275
|
+
update(data: string | Uint8Array): this {
|
|
276
|
+
if (this.finalized) {
|
|
277
|
+
throw new Error('哈希器已经完成,不能再更新数据');
|
|
278
|
+
}
|
|
279
|
+
this.hasher.update(data);
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 生成最终哈希值
|
|
285
|
+
* @param encoding - 输出编码
|
|
286
|
+
* @returns 哈希值
|
|
287
|
+
*/
|
|
288
|
+
digest(encoding: EncodingType = 'hex'): string {
|
|
289
|
+
if (this.finalized) {
|
|
290
|
+
throw new Error('哈希器已经完成');
|
|
291
|
+
}
|
|
292
|
+
this.finalized = true;
|
|
293
|
+
return this.hasher.digest(encoding);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 复制哈希器
|
|
298
|
+
* @returns 新的哈希器实例
|
|
299
|
+
*/
|
|
300
|
+
copy(): StreamHasher {
|
|
301
|
+
if (this.finalized) {
|
|
302
|
+
throw new Error('不能复制已完成的哈希器');
|
|
303
|
+
}
|
|
304
|
+
const newHasher = new StreamHasher('md5'); // 临时算法
|
|
305
|
+
newHasher.hasher = this.hasher.copy();
|
|
306
|
+
return newHasher;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 数据库统一管理工具
|
|
3
|
+
* 提供 Redis 和 SQL 连接的统一初始化和管理
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SQL, RedisClient } from 'bun';
|
|
7
|
+
import { Env } from '../config/env.js';
|
|
8
|
+
import { Logger } from './logger.js';
|
|
9
|
+
import { DbHelper } from './dbHelper.js';
|
|
10
|
+
import { RedisHelper } from './redisHelper.js';
|
|
11
|
+
import type { BeflyContext } from '../types/befly.js';
|
|
12
|
+
import type { SqlClientOptions } from '../types/database.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 数据库连接实例
|
|
16
|
+
*/
|
|
17
|
+
interface DatabaseConnections {
|
|
18
|
+
redis: RedisClient | null;
|
|
19
|
+
sql: any;
|
|
20
|
+
helper: DbHelper | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 全局连接实例
|
|
25
|
+
*/
|
|
26
|
+
const connections: DatabaseConnections = {
|
|
27
|
+
redis: null,
|
|
28
|
+
sql: null,
|
|
29
|
+
helper: null
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 构建 Redis 连接 URL
|
|
34
|
+
* @returns Redis 连接 URL
|
|
35
|
+
*/
|
|
36
|
+
export function buildRedisUrl(): string {
|
|
37
|
+
const { REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB } = Env;
|
|
38
|
+
|
|
39
|
+
// 构建认证部分
|
|
40
|
+
let auth = '';
|
|
41
|
+
if (REDIS_USERNAME && REDIS_PASSWORD) {
|
|
42
|
+
auth = `${REDIS_USERNAME}:${REDIS_PASSWORD}@`;
|
|
43
|
+
} else if (REDIS_PASSWORD) {
|
|
44
|
+
auth = `:${REDIS_PASSWORD}@`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 构建完整 URL
|
|
48
|
+
const url = `redis://${auth}${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}`;
|
|
49
|
+
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 构建数据库连接字符串
|
|
55
|
+
* 根据环境变量自动构建 SQLite、PostgreSQL 或 MySQL 的连接 URL
|
|
56
|
+
* @returns 数据库连接字符串
|
|
57
|
+
* @throws 如果配置不完整或数据库类型不支持
|
|
58
|
+
*/
|
|
59
|
+
export function buildDatabaseUrl(): string {
|
|
60
|
+
const type = Env.DB_TYPE || '';
|
|
61
|
+
const host = Env.DB_HOST || '';
|
|
62
|
+
const port = Env.DB_PORT;
|
|
63
|
+
const user = encodeURIComponent(Env.DB_USER || '');
|
|
64
|
+
const pass = encodeURIComponent(Env.DB_PASS || '');
|
|
65
|
+
const name = Env.DB_NAME || '';
|
|
66
|
+
|
|
67
|
+
if (!type) throw new Error('DB_TYPE 未配置');
|
|
68
|
+
if (!name && type !== 'sqlite') throw new Error('DB_NAME 未配置');
|
|
69
|
+
|
|
70
|
+
if (type === 'sqlite') {
|
|
71
|
+
// 支持内存数据库
|
|
72
|
+
if (!name || name === ':memory:') {
|
|
73
|
+
return 'sqlite://:memory:';
|
|
74
|
+
}
|
|
75
|
+
// 支持绝对路径(以 / 或盘符开头,如 /path 或 C:\path)
|
|
76
|
+
if (name.startsWith('/') || /^[a-zA-Z]:/.test(name)) {
|
|
77
|
+
return `sqlite://${name}`;
|
|
78
|
+
}
|
|
79
|
+
// 相对路径和普通文件名
|
|
80
|
+
return `sqlite://${name}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (type === 'postgresql' || type === 'postgres') {
|
|
84
|
+
if (!host || !port) throw new Error('DB_HOST/DB_PORT 未配置');
|
|
85
|
+
const auth = user || pass ? `${user}:${pass}@` : '';
|
|
86
|
+
return `postgres://${auth}${host}:${port}/${encodeURIComponent(name)}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === 'mysql') {
|
|
90
|
+
if (!host || !port) throw new Error('DB_HOST/DB_PORT 未配置');
|
|
91
|
+
const auth = user || pass ? `${user}:${pass}@` : '';
|
|
92
|
+
return `mysql://${auth}${host}:${port}/${encodeURIComponent(name)}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error(`不支持的 DB_TYPE: ${type}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 创建 Redis 客户端
|
|
100
|
+
* @returns Redis 客户端实例
|
|
101
|
+
* @throws 如果连接失败
|
|
102
|
+
*/
|
|
103
|
+
export async function createRedisClient(): Promise<RedisClient> {
|
|
104
|
+
try {
|
|
105
|
+
const url = buildRedisUrl();
|
|
106
|
+
const redis = new RedisClient(url, {
|
|
107
|
+
// 连接超时(毫秒)
|
|
108
|
+
connectionTimeout: 10000,
|
|
109
|
+
// 空闲超时设为 0,表示永不超时
|
|
110
|
+
idleTimeout: 0,
|
|
111
|
+
// 断开连接时自动重连
|
|
112
|
+
autoReconnect: true,
|
|
113
|
+
// 最大重连次数,0 表示无限重连
|
|
114
|
+
maxRetries: 0,
|
|
115
|
+
// 断开连接时缓存命令
|
|
116
|
+
enableOfflineQueue: true,
|
|
117
|
+
// 自动管道化命令
|
|
118
|
+
enableAutoPipelining: true
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// 测试连接是否成功
|
|
122
|
+
await redis.ping();
|
|
123
|
+
Logger.info('Redis 连接成功');
|
|
124
|
+
|
|
125
|
+
return redis;
|
|
126
|
+
} catch (error: any) {
|
|
127
|
+
Logger.error('Redis 连接失败', error);
|
|
128
|
+
throw new Error(`Redis 连接失败: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 创建 SQL 客户端
|
|
134
|
+
* @param options SQL 客户端选项
|
|
135
|
+
* @returns SQL 客户端实例
|
|
136
|
+
* @throws 如果连接失败
|
|
137
|
+
*/
|
|
138
|
+
export async function createSqlClient(options: SqlClientOptions = {}): Promise<any> {
|
|
139
|
+
const finalUrl = buildDatabaseUrl();
|
|
140
|
+
let sql: any = null;
|
|
141
|
+
|
|
142
|
+
if (Env.DB_TYPE === 'sqlite') {
|
|
143
|
+
sql = new SQL(finalUrl);
|
|
144
|
+
} else {
|
|
145
|
+
sql = new SQL({
|
|
146
|
+
url: finalUrl,
|
|
147
|
+
max: options.max ?? 1,
|
|
148
|
+
bigint: false,
|
|
149
|
+
...options
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
// 连接健康检查 - 添加超时机制
|
|
155
|
+
const timeout = options.connectionTimeout ?? 5000; // 默认5秒超时
|
|
156
|
+
|
|
157
|
+
const healthCheckPromise = (async () => {
|
|
158
|
+
let version = '';
|
|
159
|
+
if (Env.DB_TYPE === 'sqlite') {
|
|
160
|
+
const v = await sql`SELECT sqlite_version() AS version`;
|
|
161
|
+
version = v?.[0]?.version;
|
|
162
|
+
} else if (Env.DB_TYPE === 'postgresql' || Env.DB_TYPE === 'postgres') {
|
|
163
|
+
const v = await sql`SELECT version() AS version`;
|
|
164
|
+
version = v?.[0]?.version;
|
|
165
|
+
} else {
|
|
166
|
+
const v = await sql`SELECT VERSION() AS version`;
|
|
167
|
+
version = v?.[0]?.version;
|
|
168
|
+
}
|
|
169
|
+
return version;
|
|
170
|
+
})();
|
|
171
|
+
|
|
172
|
+
// 创建超时 Promise
|
|
173
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
174
|
+
setTimeout(() => {
|
|
175
|
+
reject(new Error(`数据库连接超时 (${timeout}ms)`));
|
|
176
|
+
}, timeout);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// 使用 Promise.race 实现超时控制
|
|
180
|
+
const version = await Promise.race([healthCheckPromise, timeoutPromise]);
|
|
181
|
+
|
|
182
|
+
Logger.info(`数据库连接成功,version: ${version}`);
|
|
183
|
+
return sql;
|
|
184
|
+
} catch (error: any) {
|
|
185
|
+
Logger.error('数据库连接测试失败', error);
|
|
186
|
+
|
|
187
|
+
// 清理资源
|
|
188
|
+
try {
|
|
189
|
+
await sql.close();
|
|
190
|
+
} catch (cleanupError) {
|
|
191
|
+
// 忽略清理错误
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 初始化数据库连接(Redis + SQL)
|
|
200
|
+
* @param options SQL 客户端选项
|
|
201
|
+
* @returns 数据库连接实例
|
|
202
|
+
*/
|
|
203
|
+
export async function initDatabase(options: SqlClientOptions = {}): Promise<DatabaseConnections> {
|
|
204
|
+
try {
|
|
205
|
+
// 1. 初始化 Redis
|
|
206
|
+
Logger.info('正在初始化 Redis 连接...');
|
|
207
|
+
connections.redis = await createRedisClient();
|
|
208
|
+
|
|
209
|
+
// 2. 初始化 SQL
|
|
210
|
+
Logger.info('正在初始化 SQL 连接...');
|
|
211
|
+
connections.sql = await createSqlClient(options);
|
|
212
|
+
|
|
213
|
+
// 3. 创建 DbHelper 实例
|
|
214
|
+
const befly: BeflyContext = {
|
|
215
|
+
redis: RedisHelper, // 使用 RedisHelper 对象而不是 RedisClient
|
|
216
|
+
db: null as any,
|
|
217
|
+
tool: null as any,
|
|
218
|
+
logger: null as any
|
|
219
|
+
};
|
|
220
|
+
connections.helper = new DbHelper(befly, connections.sql);
|
|
221
|
+
|
|
222
|
+
Logger.info('数据库连接初始化完成(Redis + SQL + DbHelper)');
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
redis: connections.redis,
|
|
226
|
+
sql: connections.sql,
|
|
227
|
+
helper: connections.helper
|
|
228
|
+
};
|
|
229
|
+
} catch (error: any) {
|
|
230
|
+
Logger.error('数据库初始化失败', error);
|
|
231
|
+
// 清理已创建的连接
|
|
232
|
+
await closeDatabase();
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 关闭所有数据库连接
|
|
239
|
+
*/
|
|
240
|
+
export async function closeDatabase(): Promise<void> {
|
|
241
|
+
try {
|
|
242
|
+
// 关闭 SQL 连接
|
|
243
|
+
if (connections.sql) {
|
|
244
|
+
try {
|
|
245
|
+
await connections.sql.close();
|
|
246
|
+
Logger.info('SQL 连接已关闭');
|
|
247
|
+
} catch (error: any) {
|
|
248
|
+
Logger.warn('关闭 SQL 连接时出错:', error.message);
|
|
249
|
+
}
|
|
250
|
+
connections.sql = null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 关闭 Redis 连接
|
|
254
|
+
if (connections.redis) {
|
|
255
|
+
try {
|
|
256
|
+
connections.redis.close();
|
|
257
|
+
Logger.info('Redis 连接已关闭');
|
|
258
|
+
} catch (error: any) {
|
|
259
|
+
Logger.warn('关闭 Redis 连接时出错:', error);
|
|
260
|
+
}
|
|
261
|
+
connections.redis = null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 清理 DbHelper
|
|
265
|
+
connections.helper = null;
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
Logger.error('关闭数据库连接时出错', error);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 获取 Redis 客户端
|
|
273
|
+
* @returns Redis 客户端实例
|
|
274
|
+
*/
|
|
275
|
+
export function getRedis(): RedisClient | null {
|
|
276
|
+
return connections.redis;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 获取 SQL 客户端
|
|
281
|
+
* @returns SQL 客户端实例
|
|
282
|
+
*/
|
|
283
|
+
export function getSql(): any {
|
|
284
|
+
return connections.sql;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 获取 DbHelper 实例
|
|
289
|
+
* @returns DbHelper 实例
|
|
290
|
+
*/
|
|
291
|
+
export function getDbHelper(): DbHelper | null {
|
|
292
|
+
return connections.helper;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 检查数据库连接是否已初始化
|
|
297
|
+
* @returns 是否已初始化
|
|
298
|
+
*/
|
|
299
|
+
export function isDatabaseInitialized(): boolean {
|
|
300
|
+
return connections.redis !== null && connections.sql !== null && connections.helper !== null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 仅初始化 SQL 连接(不需要 Redis 时使用)
|
|
305
|
+
* @param options SQL 客户端选项
|
|
306
|
+
* @returns SQL 客户端和 DbHelper 实例
|
|
307
|
+
*/
|
|
308
|
+
export async function initSqlOnly(options: SqlClientOptions = {}): Promise<{ sql: any; helper: DbHelper }> {
|
|
309
|
+
try {
|
|
310
|
+
Logger.info('正在初始化 SQL 连接(不含 Redis)...');
|
|
311
|
+
connections.sql = await createSqlClient(options);
|
|
312
|
+
|
|
313
|
+
// 创建最小化 befly 上下文(不含 redis)
|
|
314
|
+
const befly: BeflyContext = {
|
|
315
|
+
redis: null as any,
|
|
316
|
+
db: null as any,
|
|
317
|
+
tool: null as any,
|
|
318
|
+
logger: null as any
|
|
319
|
+
};
|
|
320
|
+
connections.helper = new DbHelper(befly, connections.sql);
|
|
321
|
+
|
|
322
|
+
Logger.info('SQL 连接初始化完成');
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
sql: connections.sql,
|
|
326
|
+
helper: connections.helper
|
|
327
|
+
};
|
|
328
|
+
} catch (error: any) {
|
|
329
|
+
Logger.error('SQL 初始化失败', error);
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 仅初始化 Redis 连接(不需要 SQL 时使用)
|
|
336
|
+
* @returns Redis 客户端实例
|
|
337
|
+
*/
|
|
338
|
+
export async function initRedisOnly(): Promise<RedisClient> {
|
|
339
|
+
try {
|
|
340
|
+
Logger.info('正在初始化 Redis 连接(不含 SQL)...');
|
|
341
|
+
connections.redis = await createRedisClient();
|
|
342
|
+
Logger.info('Redis 连接初始化完成');
|
|
343
|
+
return connections.redis;
|
|
344
|
+
} catch (error: any) {
|
|
345
|
+
Logger.error('Redis 初始化失败', error);
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|