befly 2.1.1 → 2.2.0

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/plugins/redis.js CHANGED
@@ -1,78 +1,20 @@
1
1
  import { redis } from 'bun';
2
2
  import { Env } from '../config/env.js';
3
3
  import { Logger } from '../utils/logger.js';
4
+ import { RedisHelper, getRedisClient } from '../utils/redisHelper.js';
4
5
 
5
6
  export default {
6
7
  after: ['_logger'],
7
8
  async onInit(befly) {
8
9
  try {
9
10
  if (Env.REDIS_ENABLE === 1) {
10
- if ((await redis.ping()) !== 'PONG') {
11
+ const client = getRedisClient();
12
+ if ((await client.ping()) !== 'PONG') {
11
13
  throw new Error('Redis 连接失败');
12
14
  }
13
15
 
14
- return {
15
- // 添加对象存储辅助方法
16
- setObject: async (key, obj, ttl = null) => {
17
- try {
18
- const data = JSON.stringify(obj);
19
- if (ttl) {
20
- return await redis.setEx(`${process.env.REDIS_KEY_PREFIX}:${key}`, ttl, data);
21
- }
22
- return await redis.set(`${process.env.REDIS_KEY_PREFIX}:${key}`, data);
23
- } catch (error) {
24
- Logger.error({
25
- msg: 'Redis setObject 错误',
26
- message: error.message,
27
- stack: error.stack
28
- });
29
- }
30
- },
31
-
32
- getObject: async (key) => {
33
- try {
34
- const data = await redis.get(`${process.env.REDIS_KEY_PREFIX}:${key}`);
35
- return data ? JSON.parse(data) : null;
36
- } catch (error) {
37
- Logger.error({
38
- msg: 'Redis getObject 错误',
39
- message: error.message,
40
- stack: error.stack
41
- });
42
- return null;
43
- }
44
- },
45
-
46
- delObject: async (key) => {
47
- try {
48
- await redis.del(`${process.env.REDIS_KEY_PREFIX}:${key}`);
49
- } catch (error) {
50
- Logger.error({
51
- msg: 'Redis delObject 错误',
52
- message: error.message,
53
- stack: error.stack
54
- });
55
- }
56
- },
57
-
58
- // 添加时序ID生成函数
59
- genTimeID: async () => {
60
- const timestamp = Math.floor(Date.now() / 1000);
61
- const key = `time_id_counter:${timestamp}`;
62
-
63
- const counter = await redis.incr(key);
64
- await redis.expire(key, 2);
65
-
66
- // 前3位计数器 + 后3位随机数
67
- const counterPrefix = (counter % 1000).toString().padStart(3, '0'); // 000-999
68
- const randomSuffix = Math.floor(Math.random() * 1000)
69
- .toString()
70
- .padStart(3, '0'); // 000-999
71
- const suffix = `${counterPrefix}${randomSuffix}`;
72
-
73
- return Number(`${timestamp}${suffix}`);
74
- }
75
- };
16
+ // 返回工具对象,向下游以相同 API 暴露
17
+ return RedisHelper;
76
18
  } else {
77
19
  Logger.warn(`Redis 未启用,跳过初始化`);
78
20
  return {};
@@ -83,7 +25,8 @@ export default {
83
25
  message: err.message,
84
26
  stack: err.stack
85
27
  });
86
- process.exit();
28
+ // 插件内禁止直接退出进程,抛出异常交由主流程统一处理
29
+ throw err;
87
30
  }
88
31
  }
89
32
  };
package/plugins/tool.js CHANGED
@@ -1,45 +1,8 @@
1
+ import { Tool } from '../utils/tool.js';
2
+
1
3
  export default {
2
4
  after: ['_redis', '_db'],
3
5
  async onInit(befly) {
4
- // 增强的更新方法 - 自动添加 updated_at
5
-
6
- // 辅助函数:过滤掉 undefined 值和指定字段
7
- const filterData = (obj, excludeFields = []) => {
8
- return Object.fromEntries(Object.entries(obj).filter(([key, value]) => value !== undefined && !excludeFields.includes(key)));
9
- };
10
-
11
- return {
12
- async updData(data) {
13
- const updateData = {
14
- ...filterData(data, ['id', 'created_at', 'deleted_at']),
15
- updated_at: Date.now()
16
- };
17
-
18
- return updateData;
19
- },
20
- async insData(data) {
21
- const now = Date.now();
22
-
23
- if (Array.isArray(data)) {
24
- const data2 = await Promise.all(
25
- data.map(async (item) => ({
26
- ...filterData(item),
27
- id: await befly.redis.genTimeID(),
28
- created_at: now,
29
- updated_at: now
30
- }))
31
- );
32
- return data2;
33
- } else {
34
- const data2 = {
35
- ...filterData(data),
36
- id: await befly.redis.genTimeID(),
37
- created_at: now,
38
- updated_at: now
39
- };
40
- return data2;
41
- }
42
- }
43
- };
6
+ return new Tool(befly);
44
7
  }
45
8
  };
package/scripts/syncDb.js CHANGED
@@ -3,12 +3,11 @@
3
3
  */
4
4
 
5
5
  import path from 'node:path';
6
- import { SQL } from 'bun';
7
6
  import { Env } from '../config/env.js';
8
7
  import { Logger } from '../utils/logger.js';
9
- import { parseFieldRule } from '../utils/util.js';
8
+ import { parseFieldRule, createSqlClient } from '../utils/index.js';
10
9
  import { __dirtables, getProjectDir } from '../system.js';
11
- import tableCheck from '../checks/table.js';
10
+ import { checkTable } from '../checks/table.js';
12
11
 
13
12
  const typeMapping = {
14
13
  number: 'BIGINT',
@@ -54,16 +53,10 @@ const getColumnDefinition = (fieldName, rule) => {
54
53
  return columnDef;
55
54
  };
56
55
 
57
- // 通用执行器:将 '?' 占位符转换为 Bun SQL $1, $2 并执行
58
- const toDollarParams = (query, params) => {
59
- if (!params || params.length === 0) return query;
60
- let i = 0;
61
- return query.replace(/\?/g, () => `$${++i}`);
62
- };
63
-
56
+ // 通用执行器:直接使用 Bun SQL 参数化(MySQL 使用 '?' 占位符)
64
57
  const exec = async (client, query, params = []) => {
65
- if (params.length > 0) {
66
- return await client.unsafe(toDollarParams(query, params), params);
58
+ if (params && params.length > 0) {
59
+ return await client.unsafe(query, params);
67
60
  }
68
61
  return await client.unsafe(query);
69
62
  };
@@ -277,14 +270,13 @@ const SyncDb = async () => {
277
270
  Logger.info('开始数据库表结构同步...');
278
271
 
279
272
  // 验证表定义文件
280
- const tableValidationResult = await tableCheck();
273
+ const tableValidationResult = await checkTable();
281
274
  if (!tableValidationResult) {
282
275
  throw new Error('表定义验证失败');
283
276
  }
284
277
 
285
- // 建立数据库连接并检查版本(Bun SQL)
286
- const url = `mysql://${encodeURIComponent(Env.MYSQL_USER || 'root')}:${encodeURIComponent(Env.MYSQL_PASSWORD || 'root')}@${Env.MYSQL_HOST || '127.0.0.1'}:${Env.MYSQL_PORT || 3306}/${Env.MYSQL_DB || 'test'}`;
287
- client = new SQL({ url, max: 1, bigint: true });
278
+ // 建立数据库连接并检查版本(统一工具函数)
279
+ client = await createSqlClient({ max: 1 });
288
280
  const result = await client`SELECT VERSION() AS version`;
289
281
  const version = result[0].version;
290
282
 
package/utils/api.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Logger } from './logger.js';
2
- import { RYes, RNo } from './util.js';
2
+ import { RYes, RNo } from './index.js';
3
3
  export class Api {
4
4
  // GET 方法
5
5
  static GET(name, auth = false, fields = {}, required = [], handler) {
@@ -1,6 +1,8 @@
1
1
  import { fileURLToPath } from 'node:url';
2
2
  import path from 'node:path';
3
+ import { SQL } from 'bun';
3
4
  import { Env } from '../config/env.js';
5
+ import { Logger } from './logger.js';
4
6
 
5
7
  export const setCorsOptions = (req) => {
6
8
  return {
@@ -73,7 +75,7 @@ export const formatDate = (date = new Date(), format = 'YYYY-MM-DD HH:mm:ss') =>
73
75
  * @param {number} endTime - 结束时间(可选,默认为当前时间)
74
76
  * @returns {string} 时间差(如果小于1秒返回"xx 毫秒",否则返回"xx 秒")
75
77
  */
76
- export const calculateElapsedTime = (startTime, endTime = Bun.nanoseconds()) => {
78
+ export const calcPerfTime = (startTime, endTime = Bun.nanoseconds()) => {
77
79
  const elapsedMs = (endTime - startTime) / 1_000_000;
78
80
 
79
81
  if (elapsedMs < 1000) {
@@ -87,16 +89,12 @@ export const calculateElapsedTime = (startTime, endTime = Bun.nanoseconds()) =>
87
89
  // 类型判断
88
90
  export const isType = (value, type) => {
89
91
  const actualType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
90
- const expectedType = type.toLowerCase();
92
+ const expectedType = String(type).toLowerCase();
91
93
 
92
- // 特殊类型处理
94
+ // 语义类型单独处理,其余走 actualType === expectedType
93
95
  switch (expectedType) {
94
- case 'null':
95
- return value === null;
96
- case 'undefined':
97
- return value === undefined;
98
96
  case 'nan':
99
- return Number.isNaN(value);
97
+ return typeof value === 'number' && Number.isNaN(value);
100
98
  case 'empty':
101
99
  return value === '' || value === null || value === undefined;
102
100
  case 'integer':
@@ -117,15 +115,14 @@ export const isType = (value, type) => {
117
115
  return value !== Object(value);
118
116
  case 'reference':
119
117
  return value === Object(value);
120
- case 'function':
121
- return typeof value === 'function';
122
118
  default:
123
119
  return actualType === expectedType;
124
120
  }
125
121
  };
126
122
 
127
123
  export const pickFields = (obj, keys) => {
128
- if (!obj || typeof obj !== 'object') {
124
+ // 仅对对象或数组进行字段挑选,其他类型返回空对象(保持原有行为)
125
+ if (!obj || (!isType(obj, 'object') && !isType(obj, 'array'))) {
129
126
  return {};
130
127
  }
131
128
 
@@ -140,25 +137,56 @@ export const pickFields = (obj, keys) => {
140
137
  return result;
141
138
  };
142
139
 
143
- export const omitFields = (obj, keys) => {
144
- if (!obj || typeof obj !== 'object' || !Array.isArray(keys)) {
145
- return {};
146
- }
140
+ /**
141
+ * 从对象或数组数据中按“字段名”和“字段值”进行排除过滤。
142
+ * - 支持对象:移除指定字段名,以及值在排除值列表中的字段。
143
+ * - 支持数组:
144
+ * - 如果元素为对象,按同样规则清洗(移除字段名/字段值命中项)。
145
+ * - 如果元素为原始值(数字/字符串等),当元素值命中排除值则从数组中移除该元素。
146
+ *
147
+ * 约定:excludeKeys 与 excludeValues 均为数组类型。
148
+ *
149
+ * 示例:
150
+ * omitFields({ a:1, b:undefined, c:null }, ['a'], [undefined]) -> { c:null }
151
+ * omitFields([{ a:1, b:null }, null, 0], ['a'], [null]) -> [{}, 0]
152
+ *
153
+ * 注意:仅当第一个参数为对象或数组时执行过滤,否则原样返回。
154
+ *
155
+ * @template T
156
+ * @param {Record<string, any> | Array<any>} data - 原始数据(对象或数组)
157
+ * @param {string[]} [excludeKeys=[]] - 要排除的字段名(对象属性名)数组
158
+ * @param {any[]} [excludeValues=[]] - 要排除的字段值数组;当包含 undefined/null 等时,将移除这些值对应的字段或数组元素
159
+ * @returns {T} 过滤后的数据,类型与入参保持一致
160
+ */
161
+ export const omitFields = (data, excludeKeys = [], excludeValues = []) => {
162
+ const shouldDropValue = (v) => excludeValues.some((x) => x === v);
163
+
164
+ const cleanObject = (obj) => {
165
+ if (!isType(obj, 'object')) return obj;
166
+ const result = {};
167
+ for (const [k, v] of Object.entries(obj)) {
168
+ if (excludeKeys.includes(k)) continue;
169
+ if (shouldDropValue(v)) continue;
170
+ result[k] = v;
171
+ }
172
+ return result;
173
+ };
147
174
 
148
- const result = {};
175
+ if (isType(data, 'array')) {
176
+ return /** @type {any} */ (data.filter((item) => !shouldDropValue(item)).map((item) => (isType(item, 'object') ? cleanObject(item) : item)));
177
+ }
149
178
 
150
- for (const key in obj) {
151
- if (obj.hasOwnProperty(key) && !keys.includes(key)) {
152
- result[key] = obj[key];
153
- }
179
+ if (isType(data, 'object')) {
180
+ return /** @type {any} */ (cleanObject(data));
154
181
  }
155
182
 
156
- return result;
183
+ // 非对象/数组则原样返回(不处理)
184
+ return /** @type {any} */ (data);
157
185
  };
158
186
 
159
187
  export const isEmptyObject = (obj) => {
160
188
  // 首先检查是否为对象
161
- if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
189
+ if (!isType(obj, 'object')) {
162
190
  return false;
163
191
  }
164
192
 
@@ -168,7 +196,7 @@ export const isEmptyObject = (obj) => {
168
196
 
169
197
  export const isEmptyArray = (arr) => {
170
198
  // 首先检查是否为数组
171
- if (!Array.isArray(arr)) {
199
+ if (!isType(arr, 'array')) {
172
200
  return false;
173
201
  }
174
202
 
@@ -205,7 +233,8 @@ export const dirname2 = (importMetaUrl) => {
205
233
 
206
234
  // 过滤日志字段的函数
207
235
  export const filterLogFields = (body, excludeFields = '') => {
208
- if (!body || typeof body !== 'object') return body;
236
+ // 仅在对象或数组时进行过滤,保持与原 typeof === 'object' 行为一致(数组也会进入)
237
+ if (!body || (!isType(body, 'object') && !isType(body, 'array'))) return body;
209
238
 
210
239
  // 如果是字符串,按逗号分割并清理空格
211
240
  const fieldsArray = excludeFields
@@ -314,3 +343,32 @@ export const parseFieldRule = (rule) => {
314
343
 
315
344
  return allParts;
316
345
  };
346
+
347
+ /**
348
+ * 创建并校验 Bun SQL 客户端
349
+ * - 否则按 scripts/syncDb.js 的方式拼接 URL
350
+ * - 连接成功后返回 SQL 实例,失败会自动 close 并抛出
351
+ * @param {object} options 传给 new SQL 的参数(如 { max: 1, bigint: true })
352
+ */
353
+ export async function createSqlClient(options = {}) {
354
+ const url = `mysql://${encodeURIComponent(Env.MYSQL_USER)}:${encodeURIComponent(Env.MYSQL_PASSWORD)}@${Env.MYSQL_HOST}:${Env.MYSQL_PORT}/${Env.MYSQL_DB}`;
355
+
356
+ const sql = new SQL({
357
+ url: url,
358
+ max: options.max ?? 1,
359
+ bigint: options.bigint ?? true,
360
+ ...options
361
+ });
362
+ try {
363
+ const ver = await sql`SELECT VERSION() AS version`;
364
+ const version = ver?.[0]?.version;
365
+ Logger.info(`数据库连接成功,MySQL 版本: ${version}`);
366
+ return sql;
367
+ } catch (error) {
368
+ Logger.error('数据库连接测试失败:', error);
369
+ try {
370
+ await sql.close();
371
+ } catch {}
372
+ throw error;
373
+ }
374
+ }
package/utils/jwt.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Jwt as JwtBase } from '../libs/jwt.js';
1
+ import { createHmac } from 'crypto';
2
2
  import { Env } from '../config/env.js';
3
3
 
4
4
  /**
@@ -6,6 +6,54 @@ import { Env } from '../config/env.js';
6
6
  * 提供JWT token的签名、验证和解码功能以及应用层的便捷接口
7
7
  */
8
8
  export class Jwt {
9
+ // 原基础工具:算法映射
10
+ static ALGORITHMS = {
11
+ HS256: 'sha256',
12
+ HS384: 'sha384',
13
+ HS512: 'sha512'
14
+ };
15
+
16
+ // 原基础工具:Base64 URL 编解码
17
+ static base64UrlEncode(input) {
18
+ const base64 = Buffer.isBuffer(input) ? input.toString('base64') : Buffer.from(input, 'utf8').toString('base64');
19
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
20
+ }
21
+ static base64UrlDecode(str) {
22
+ const padding = 4 - (str.length % 4);
23
+ if (padding !== 4) str += '='.repeat(padding);
24
+ str = str.replace(/-/g, '+').replace(/_/g, '/');
25
+ return Buffer.from(str, 'base64').toString('utf8');
26
+ }
27
+
28
+ // 原基础工具:过期时间解析与签名/比较
29
+ static parseExpiration(expiresIn) {
30
+ if (typeof expiresIn === 'number') return expiresIn;
31
+ if (typeof expiresIn !== 'string') throw new Error('过期时间格式无效');
32
+ const numericValue = parseInt(expiresIn);
33
+ if (!isNaN(numericValue) && numericValue.toString() === expiresIn) return numericValue;
34
+ const match = expiresIn.match(/^(\d+)(ms|[smhdwy])$/);
35
+ if (!match) throw new Error('过期时间格式无效');
36
+ const value = parseInt(match[1]);
37
+ const unit = match[2];
38
+ if (unit === 'ms') return Math.floor(value / 1000);
39
+ const multipliers = { s: 1, m: 60, h: 3600, d: 86400, w: 604800, y: 31536000 };
40
+ return value * multipliers[unit];
41
+ }
42
+ static createSignature(algorithm, secret, data) {
43
+ const hashAlgorithm = this.ALGORITHMS[algorithm];
44
+ if (!hashAlgorithm) throw new Error(`不支持的算法: ${algorithm}`);
45
+ const hmac = createHmac(hashAlgorithm, secret);
46
+ hmac.update(data);
47
+ return this.base64UrlEncode(hmac.digest());
48
+ }
49
+ static constantTimeCompare(a, b) {
50
+ if (a.length !== b.length) return false;
51
+ let result = 0;
52
+ for (let i = 0; i < a.length; i++) {
53
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
54
+ }
55
+ return result === 0;
56
+ }
9
57
  /**
10
58
  * 签名JWT token
11
59
  * @param {object} payload - JWT载荷数据
@@ -26,7 +74,7 @@ export class Jwt {
26
74
  const now = Math.floor(Date.now() / 1000);
27
75
 
28
76
  // 创建header
29
- const header = JwtBase.base64UrlEncode(
77
+ const header = Jwt.base64UrlEncode(
30
78
  JSON.stringify({
31
79
  alg: algorithm,
32
80
  typ: 'JWT'
@@ -37,22 +85,22 @@ export class Jwt {
37
85
  const jwtPayload = { ...payload, iat: now };
38
86
 
39
87
  if (options.expiresIn || Env.JWT_EXPIRES_IN) {
40
- const expSeconds = JwtBase.parseExpiration(options.expiresIn || Env.JWT_EXPIRES_IN);
88
+ const expSeconds = Jwt.parseExpiration(options.expiresIn || Env.JWT_EXPIRES_IN);
41
89
  jwtPayload.exp = now + expSeconds;
42
90
  }
43
91
  if (options.issuer) jwtPayload.iss = options.issuer;
44
92
  if (options.audience) jwtPayload.aud = options.audience;
45
93
  if (options.subject) jwtPayload.sub = options.subject;
46
94
  if (options.notBefore) {
47
- jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now + JwtBase.parseExpiration(options.notBefore);
95
+ jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now + Jwt.parseExpiration(options.notBefore);
48
96
  }
49
97
  if (options.jwtId) jwtPayload.jti = options.jwtId;
50
98
 
51
- const encodedPayload = JwtBase.base64UrlEncode(JSON.stringify(jwtPayload));
99
+ const encodedPayload = Jwt.base64UrlEncode(JSON.stringify(jwtPayload));
52
100
 
53
101
  // 创建签名
54
102
  const data = `${header}.${encodedPayload}`;
55
- const signature = JwtBase.createSignature(algorithm, secret, data);
103
+ const signature = Jwt.createSignature(algorithm, secret, data);
56
104
 
57
105
  return `${data}.${signature}`;
58
106
  }
@@ -80,20 +128,20 @@ export class Jwt {
80
128
 
81
129
  try {
82
130
  // 解析header和payload
83
- const header = JSON.parse(JwtBase.base64UrlDecode(parts[0]));
84
- const payload = JSON.parse(JwtBase.base64UrlDecode(parts[1]));
131
+ const header = JSON.parse(Jwt.base64UrlDecode(parts[0]));
132
+ const payload = JSON.parse(Jwt.base64UrlDecode(parts[1]));
85
133
  const signature = parts[2];
86
134
 
87
135
  // 验证算法
88
- if (!JwtBase.ALGORITHMS[header.alg]) {
136
+ if (!Jwt.ALGORITHMS[header.alg]) {
89
137
  throw new Error(`不支持的算法: ${header.alg}`);
90
138
  }
91
139
 
92
140
  // 验证签名
93
141
  const data = `${parts[0]}.${parts[1]}`;
94
- const expectedSignature = JwtBase.createSignature(header.alg, secret, data);
142
+ const expectedSignature = Jwt.createSignature(header.alg, secret, data);
95
143
 
96
- if (!JwtBase.constantTimeCompare(signature, expectedSignature)) {
144
+ if (!Jwt.constantTimeCompare(signature, expectedSignature)) {
97
145
  throw new Error('Token签名无效');
98
146
  }
99
147
 
@@ -147,8 +195,8 @@ export class Jwt {
147
195
  }
148
196
 
149
197
  try {
150
- const header = JSON.parse(JwtBase.base64UrlDecode(parts[0]));
151
- const payload = JSON.parse(JwtBase.base64UrlDecode(parts[1]));
198
+ const header = JSON.parse(Jwt.base64UrlDecode(parts[0]));
199
+ const payload = JSON.parse(Jwt.base64UrlDecode(parts[1]));
152
200
 
153
201
  return complete ? { header, payload, signature: parts[2] } : payload;
154
202
  } catch (error) {
@@ -335,3 +383,5 @@ export class Jwt {
335
383
  return timeToExpiry > 0 && timeToExpiry <= thresholdSeconds;
336
384
  }
337
385
  }
386
+
387
+ // 已使用 `export class Jwt` 具名导出
package/utils/logger.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import path from 'path';
2
2
  import { appendFile, stat } from 'node:fs/promises';
3
- import { formatDate } from './util.js';
3
+ import { formatDate } from './index.js';
4
4
  import { Env } from '../config/env.js';
5
5
 
6
6
  export class Logger {
@@ -0,0 +1,74 @@
1
+ import { redis as bunRedis } from 'bun';
2
+ import { Env } from '../config/env.js';
3
+ import { Logger } from './logger.js';
4
+
5
+ const prefix = Env.REDIS_KEY_PREFIX ? `${Env.REDIS_KEY_PREFIX}:` : '';
6
+
7
+ let redisClient = bunRedis;
8
+ export const setRedisClient = (client) => {
9
+ redisClient = client || bunRedis;
10
+ };
11
+ export const getRedisClient = () => redisClient;
12
+
13
+ export const RedisHelper = {
14
+ async setObject(key, obj, ttl = null) {
15
+ try {
16
+ const data = JSON.stringify(obj);
17
+ const pkey = `${prefix}${key}`;
18
+ if (ttl) {
19
+ return await redisClient.setEx(pkey, ttl, data);
20
+ }
21
+ return await redisClient.set(pkey, data);
22
+ } catch (error) {
23
+ Logger.error({
24
+ msg: 'Redis setObject 错误',
25
+ message: error.message,
26
+ stack: error.stack
27
+ });
28
+ }
29
+ },
30
+
31
+ async getObject(key) {
32
+ try {
33
+ const pkey = `${prefix}${key}`;
34
+ const data = await redisClient.get(pkey);
35
+ return data ? JSON.parse(data) : null;
36
+ } catch (error) {
37
+ Logger.error({
38
+ msg: 'Redis getObject 错误',
39
+ message: error.message,
40
+ stack: error.stack
41
+ });
42
+ return null;
43
+ }
44
+ },
45
+
46
+ async delObject(key) {
47
+ try {
48
+ const pkey = `${prefix}${key}`;
49
+ await redisClient.del(pkey);
50
+ } catch (error) {
51
+ Logger.error({
52
+ msg: 'Redis delObject 错误',
53
+ message: error.message,
54
+ stack: error.stack
55
+ });
56
+ }
57
+ },
58
+
59
+ async genTimeID() {
60
+ const timestamp = Math.floor(Date.now() / 1000);
61
+ const key = `${prefix}time_id_counter:${timestamp}`;
62
+
63
+ const counter = await redisClient.incr(key);
64
+ await redisClient.expire(key, 2);
65
+
66
+ const counterPrefix = (counter % 1000).toString().padStart(3, '0');
67
+ const randomSuffix = Math.floor(Math.random() * 1000)
68
+ .toString()
69
+ .padStart(3, '0');
70
+ const suffix = `${counterPrefix}${randomSuffix}`;
71
+
72
+ return Number(`${timestamp}${suffix}`);
73
+ }
74
+ };