chanjs 2.7.2 → 2.7.4
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/App.js +232 -16
- package/base/Aop.js +20 -3
- package/base/Container.js +80 -3
- package/base/Controller.js +38 -9
- package/base/Database.js +50 -0
- package/base/Event.js +12 -0
- package/base/{Service.js → Repository.js} +644 -539
- package/common/api.js +18 -8
- package/common/code.js +25 -15
- package/common/email.js +98 -17
- package/common/index.js +1 -1
- package/config/code.js +138 -82
- package/global/index.js +1 -1
- package/helper/index.js +43 -41
- package/index.js +19 -6
- package/loader/index.js +6 -0
- package/{helper → loader}/loader.js +41 -27
- package/middleware/compress.js +185 -0
- package/middleware/cors.js +36 -24
- package/middleware/header.js +5 -10
- package/middleware/index.js +1 -0
- package/middleware/log.js +27 -3
- package/middleware/setBody.js +9 -1
- package/middleware/static.js +2 -1
- package/middleware/template.js +139 -4
- package/middleware/waf.js +136 -76
- package/package.json +4 -4
- package/realtime/index.js +7 -0
- package/realtime/sse.js +424 -0
- package/realtime/websocket.js +540 -0
- package/response/index.js +12 -0
- package/response/response.js +258 -0
- package/schedule/index.js +6 -0
- package/schedule/schedule.js +491 -0
- package/{helper → security}/checker.js +23 -8
- package/security/index.js +14 -0
- package/{helper → security}/jwt.js +175 -107
- package/security/keywords.js +179 -0
- package/security/rate-limit.js +105 -0
- package/security/sign.js +210 -0
- package/security/xss-filter.js +63 -0
- package/storage/cache.js +258 -0
- package/storage/index.js +9 -0
- package/storage/redis.js +258 -0
- package/storage/store.js +266 -0
- package/{helper → utils}/file.js +106 -15
- package/{helper → utils}/filter.js +2 -1
- package/{helper → utils}/html.js +19 -1
- package/utils/index.js +34 -0
- package/{helper → utils}/ip.js +25 -16
- package/utils/request.js +172 -0
- package/{helper → utils}/time.js +1 -1
- package/utils/tree.js +121 -0
- package/common/category.js +0 -22
- package/common/sms.js +0 -104
- package/extend/art-template.js +0 -129
- package/extend/index.js +0 -6
- package/global/global.js +0 -63
- package/helper/cache.js +0 -187
- package/helper/keywords.js +0 -132
- package/helper/rate-limit.js +0 -116
- package/helper/request.js +0 -47
- package/helper/response.js +0 -180
- package/helper/sign.js +0 -96
- package/helper/tree.js +0 -77
- package/helper/xss-filter.js +0 -42
- /package/{helper → utils}/data-parse.js +0 -0
|
@@ -1,539 +1,644 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
*
|
|
113
|
-
* @
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
*
|
|
140
|
-
* @
|
|
141
|
-
* @param {Object}
|
|
142
|
-
* @
|
|
143
|
-
* @
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
*
|
|
222
|
-
* @
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
*
|
|
249
|
-
* @
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
*
|
|
343
|
-
* @param {
|
|
344
|
-
* @param {
|
|
345
|
-
* @param {Object} options.
|
|
346
|
-
* @
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*/
|
|
389
|
-
async
|
|
390
|
-
this._checkDB();
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
.
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 数据访问基类(Repository / DAO 层)
|
|
3
|
+
*
|
|
4
|
+
* ============================================================
|
|
5
|
+
* 设计说明
|
|
6
|
+
* ============================================================
|
|
7
|
+
* 本类是真正的 DAO/Repository 层,负责数据访问(CRUD/分页/事务/软删除)。
|
|
8
|
+
* 业务侧的 Service 层应继承本类,专注于业务编排,数据访问委托给 Repository。
|
|
9
|
+
*
|
|
10
|
+
* 三层架构:
|
|
11
|
+
* Controller(路由)→ Service(业务编排)→ Repository(数据访问)
|
|
12
|
+
*
|
|
13
|
+
* ============================================================
|
|
14
|
+
* 依赖注入(DI)
|
|
15
|
+
* ============================================================
|
|
16
|
+
* 不再依赖 global.Chan,通过 Container 基类的 DI 获取应用资源:
|
|
17
|
+
* - this.db → 数据库连接(动态读取 Chan.db)
|
|
18
|
+
* - this.config → 应用配置(动态读取 Chan.config)
|
|
19
|
+
* - this.app → Chan 类引用(可访问 app.dbManager 等)
|
|
20
|
+
* - this.paths → 路径配置(动态读取 Chan.paths)
|
|
21
|
+
*
|
|
22
|
+
* ============================================================
|
|
23
|
+
* 响应码体系(与 Controller 对齐)
|
|
24
|
+
* ============================================================
|
|
25
|
+
* 所有返回值遵循:{ success, code, msg, data }
|
|
26
|
+
* - code: 0 成功 / 1xxx 业务 / 5xxx 系统 / 6xxx 数据库
|
|
27
|
+
*/
|
|
28
|
+
import { formatDateFields } from "../utils/time.js";
|
|
29
|
+
import Container from "./Container.js";
|
|
30
|
+
import {
|
|
31
|
+
CODE_OK,
|
|
32
|
+
CODE_NOT_FOUND,
|
|
33
|
+
CODE_PARAM_INVALID,
|
|
34
|
+
CODE_PARAM_MISSING,
|
|
35
|
+
} from "../config/code.js";
|
|
36
|
+
|
|
37
|
+
// 业务 code 语义化常量(本文件内部使用,避免硬编码数字)
|
|
38
|
+
const OK = CODE_OK; // 0 成功
|
|
39
|
+
const NOT_FOUND = CODE_NOT_FOUND; // 1004 资源不存在
|
|
40
|
+
const PARAM_INVALID = CODE_PARAM_INVALID; // 1006 参数无效
|
|
41
|
+
const PARAM_MISSING = CODE_PARAM_MISSING; // 1007 参数缺失
|
|
42
|
+
|
|
43
|
+
// 排序字段白名单正则:仅允许字母开头、字母数字下划线组合,拒绝任意 SQL 注入字符
|
|
44
|
+
const SORT_FIELD_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
45
|
+
|
|
46
|
+
class Repository extends Container {
|
|
47
|
+
/**
|
|
48
|
+
* 构造函数
|
|
49
|
+
* @param {string} tableName - 表名
|
|
50
|
+
* @param {string} dbName - 数据库名称,默认使用默认数据库
|
|
51
|
+
*/
|
|
52
|
+
constructor(tableName = null, dbName = null) {
|
|
53
|
+
super('service');
|
|
54
|
+
|
|
55
|
+
// 通过 DI 获取数据库连接(dbName 指定则从 dbManager 取,否则用默认 db)
|
|
56
|
+
// 注意:延迟到首次访问时读取,避免构造时 Chan.db 尚未初始化
|
|
57
|
+
this._dbName = dbName;
|
|
58
|
+
if (dbName) {
|
|
59
|
+
// 指定库名:覆盖父类 getter,从 dbManager 取
|
|
60
|
+
this._customDb = null; // 延迟初始化
|
|
61
|
+
}
|
|
62
|
+
// 默认库名:用父类 getter this.db(动态读取 Chan.db)
|
|
63
|
+
|
|
64
|
+
if (tableName) {
|
|
65
|
+
this.tableName = tableName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._dateFields = [
|
|
69
|
+
'created_at',
|
|
70
|
+
'updated_at',
|
|
71
|
+
'deleted_at',
|
|
72
|
+
'publish_time',
|
|
73
|
+
'start_time',
|
|
74
|
+
'end_time',
|
|
75
|
+
'login_time',
|
|
76
|
+
'created_date',
|
|
77
|
+
'updated_date',
|
|
78
|
+
'createdAt',
|
|
79
|
+
'updatedAt',
|
|
80
|
+
'deletedAt',
|
|
81
|
+
'publishTime',
|
|
82
|
+
'startTime',
|
|
83
|
+
'endTime',
|
|
84
|
+
'loginTime',
|
|
85
|
+
'createdDate',
|
|
86
|
+
'updatedDate'
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
// 自动注册事件监听器(如果子类定义了 on 方法)
|
|
90
|
+
if (typeof this.on === 'function') {
|
|
91
|
+
this.on();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 数据库连接(DI 动态读取)
|
|
97
|
+
* 指定 dbName 时从 dbManager 获取,否则用默认 Chan.db
|
|
98
|
+
*/
|
|
99
|
+
get db() {
|
|
100
|
+
if (this._dbName) {
|
|
101
|
+
// 指定库名:从 dbManager 获取(延迟初始化 + 缓存)
|
|
102
|
+
if (!this._customDb) {
|
|
103
|
+
this._customDb = this.app.dbManager.get(this._dbName);
|
|
104
|
+
}
|
|
105
|
+
return this._customDb;
|
|
106
|
+
}
|
|
107
|
+
// 默认库:动态读取(避免构造时快照)
|
|
108
|
+
return this.app.db;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 获取每页大小配置
|
|
113
|
+
* @returns {number} 每页记录数,默认20
|
|
114
|
+
*/
|
|
115
|
+
get pageSize() {
|
|
116
|
+
return this.config?.PAGE_SIZE || 20;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 获取最大限制配置
|
|
121
|
+
* @returns {number} 最大记录数,默认300
|
|
122
|
+
*/
|
|
123
|
+
get limit() {
|
|
124
|
+
return this.config?.LIMIT_MAX || 300;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 检查数据库连接是否可用
|
|
129
|
+
* @private
|
|
130
|
+
* @throws {Error} 数据库连接不可用时抛出异常
|
|
131
|
+
*/
|
|
132
|
+
_checkDB() {
|
|
133
|
+
if (!this.db) {
|
|
134
|
+
throw new Error("Database connection not available");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 格式化日期字段
|
|
140
|
+
* @private
|
|
141
|
+
* @param {Object} data - 要格式化的数据对象
|
|
142
|
+
* @returns {Object} 格式化后的数据
|
|
143
|
+
* @description 自动将日期字符串转换为数据库可识别的格式
|
|
144
|
+
*/
|
|
145
|
+
_formatDateFields(data) {
|
|
146
|
+
if (!data || typeof data !== 'object') return data;
|
|
147
|
+
|
|
148
|
+
const result = { ...data };
|
|
149
|
+
for (const field of this._dateFields) {
|
|
150
|
+
if (result[field] && typeof result[field] === 'string') {
|
|
151
|
+
try {
|
|
152
|
+
const date = new Date(result[field]);
|
|
153
|
+
if (!isNaN(date.getTime())) {
|
|
154
|
+
result[field] = date;
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(`[Repository._formatDateFields] 日期字段 ${field} 格式化失败:`, {
|
|
158
|
+
value: result[field],
|
|
159
|
+
error: error.message
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 构建基础查询
|
|
169
|
+
* @private
|
|
170
|
+
* @param {Object} options - 查询选项
|
|
171
|
+
* @param {Object} options.query - 查询条件
|
|
172
|
+
* @param {Object} options.sort - 排序条件
|
|
173
|
+
* @param {Array} options.fields - 查询字段
|
|
174
|
+
* @returns {Object} Knex查询构建器
|
|
175
|
+
*/
|
|
176
|
+
_buildBaseQuery({ query = {}, sort = {}, fields = [] } = {}) {
|
|
177
|
+
this._checkDB();
|
|
178
|
+
|
|
179
|
+
let dbQuery = this.db(this.tableName);
|
|
180
|
+
|
|
181
|
+
if (Object.keys(query).length) dbQuery = dbQuery.where(query);
|
|
182
|
+
if (fields.length) dbQuery = dbQuery.select(fields);
|
|
183
|
+
|
|
184
|
+
if (sort && typeof sort === 'object') {
|
|
185
|
+
for (const [field, dir] of Object.entries(sort)) {
|
|
186
|
+
// 排序字段白名单校验,拒绝 SQL 注入字符
|
|
187
|
+
if (!SORT_FIELD_REGEX.test(field)) {
|
|
188
|
+
console.warn(`[Repository] 非法排序字段被拒绝: ${field}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const direction = ['asc', 'desc'].includes(String(dir).toLowerCase()) ? String(dir).toLowerCase() : 'asc';
|
|
192
|
+
dbQuery = dbQuery.orderBy(field, direction);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return dbQuery;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 查询所有记录
|
|
201
|
+
*
|
|
202
|
+
* 安全说明:
|
|
203
|
+
* - 默认 limit 1000,防止全表返回导致 OOM
|
|
204
|
+
* - 业务侧如需更多结果,可显式传 limit 参数覆盖
|
|
205
|
+
* - 如需分页,请使用 find()/query() 而非 all()
|
|
206
|
+
*
|
|
207
|
+
* @param {Object} options - 查询选项
|
|
208
|
+
* @param {Object} options.query - 查询条件
|
|
209
|
+
* @param {Object} options.sort - 排序条件
|
|
210
|
+
* @param {Array} options.fields - 查询字段(推荐显式指定,避免 SELECT * 拉回 longtext content 放大 IO)
|
|
211
|
+
* @param {number} [options.limit=1000] - 返回条数上限,默认 1000
|
|
212
|
+
* @returns {Promise<Object>} 查询结果,包含 data 字段(数组)
|
|
213
|
+
*/
|
|
214
|
+
async all({ query = {}, sort = {}, fields = [], limit = 1000 } = {}) {
|
|
215
|
+
const _query = this._buildBaseQuery({ query, sort, fields });
|
|
216
|
+
const res = await _query.limit(limit).select();
|
|
217
|
+
return { success: true, code: OK, msg: '查询成功', data: formatDateFields(res, this._dateFields) };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 分页查询记录
|
|
222
|
+
* @param {Object} options - 查询选项
|
|
223
|
+
* @param {Object} options.query - 查询条件
|
|
224
|
+
* @param {Object} options.sort - 排序条件
|
|
225
|
+
* @param {Array} options.fields - 查询字段
|
|
226
|
+
* @param {number} options.limit - 限制条数
|
|
227
|
+
* @param {number} options.offset - 偏移量
|
|
228
|
+
* @returns {Promise<Object>} 查询结果,包含data字段
|
|
229
|
+
*/
|
|
230
|
+
async find({ query = {}, sort = {}, fields = [], limit, offset } = {}) {
|
|
231
|
+
let _query = this._buildBaseQuery({ query, sort, fields });
|
|
232
|
+
|
|
233
|
+
if (typeof offset === 'number') {
|
|
234
|
+
_query = _query.offset(offset);
|
|
235
|
+
}
|
|
236
|
+
if (typeof limit === 'number') {
|
|
237
|
+
_query = _query.limit(limit);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const res = await _query.select();
|
|
241
|
+
return { success: true, code: OK, msg: '查询成功', data: formatDateFields(res, this._dateFields) };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 查询单条记录
|
|
246
|
+
* @param {Object} options - 查询选项
|
|
247
|
+
* @param {Object} options.query - 查询条件
|
|
248
|
+
* @param {Array} options.fields - 查询字段
|
|
249
|
+
* @returns {Promise<Object>} 查询结果,包含data字段
|
|
250
|
+
*/
|
|
251
|
+
async findOne({ query = {}, fields = [] } = {}) {
|
|
252
|
+
this._checkDB();
|
|
253
|
+
|
|
254
|
+
let dbQuery = this.db(this.tableName);
|
|
255
|
+
if (Object.keys(query).length) dbQuery = dbQuery.where(query);
|
|
256
|
+
if (fields.length) dbQuery = dbQuery.select(fields);
|
|
257
|
+
const res = await dbQuery.first();
|
|
258
|
+
|
|
259
|
+
if (!res) {
|
|
260
|
+
return { success: false, code: NOT_FOUND, msg: '记录不存在', data: null };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { success: true, code: OK, msg: '查询成功', data: formatDateFields(res, this._dateFields) };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 根据ID查询单条记录
|
|
268
|
+
* @param {number|string} id - 记录ID
|
|
269
|
+
* @param {Object} options - 查询选项
|
|
270
|
+
* @param {Array} options.fields - 查询字段
|
|
271
|
+
* @returns {Promise<Object>} 查询结果,包含data字段
|
|
272
|
+
*/
|
|
273
|
+
async findById(id, { fields = [] } = {}) {
|
|
274
|
+
this._checkDB();
|
|
275
|
+
|
|
276
|
+
let _query = this.db(this.tableName).where({ id });
|
|
277
|
+
if (fields.length) _query = _query.select(fields);
|
|
278
|
+
const res = await _query.first();
|
|
279
|
+
|
|
280
|
+
if (!res) {
|
|
281
|
+
return { success: false, code: NOT_FOUND, msg: '记录不存在', data: null };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { success: true, code: OK, msg: '查询成功', data: formatDateFields(res, this._dateFields) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 插入单条记录
|
|
289
|
+
* @param {Object} data - 要插入的数据
|
|
290
|
+
* @returns {Promise<Object>} 插入结果,包含insertId和affectedRows
|
|
291
|
+
*/
|
|
292
|
+
async insert(data = {}) {
|
|
293
|
+
this._checkDB();
|
|
294
|
+
|
|
295
|
+
if (!Object.keys(data).length) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
296
|
+
const formattedData = this._formatDateFields(data);
|
|
297
|
+
const result = await this.db(this.tableName).insert(formattedData);
|
|
298
|
+
return { success: true, code: OK, msg: '插入成功', data: { insertId: result[0], affectedRows: result.length } };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 批量插入记录
|
|
303
|
+
* @param {Array} records - 要插入的记录数组
|
|
304
|
+
* @returns {Promise<Object>} 插入结果,包含insertIds和affectedRows
|
|
305
|
+
*/
|
|
306
|
+
async insertMany(records = []) {
|
|
307
|
+
this._checkDB();
|
|
308
|
+
|
|
309
|
+
if (!records.length) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
310
|
+
const formattedRecords = records.map(record => this._formatDateFields(record));
|
|
311
|
+
const result = await this.db(this.tableName).insert(formattedRecords);
|
|
312
|
+
return { success: true, code: OK, msg: '批量插入成功', data: { insertIds: result, affectedRows: result.length } };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 根据条件删除记录
|
|
317
|
+
* @param {Object} query - 删除条件
|
|
318
|
+
* @returns {Promise<Object>} 删除结果,包含affectedRows
|
|
319
|
+
*/
|
|
320
|
+
async delete(query = {}) {
|
|
321
|
+
this._checkDB();
|
|
322
|
+
|
|
323
|
+
if (!Object.keys(query).length) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
324
|
+
const affectedRows = await this.db(this.tableName).where(query).del();
|
|
325
|
+
return { success: true, code: OK, msg: '删除成功', data: { affectedRows } };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* 根据ID删除记录
|
|
330
|
+
* @param {number|string} id - 记录ID
|
|
331
|
+
* @returns {Promise<Object>} 删除结果,包含affectedRows
|
|
332
|
+
*/
|
|
333
|
+
async deleteById(id) {
|
|
334
|
+
this._checkDB();
|
|
335
|
+
|
|
336
|
+
if (!id) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
337
|
+
const affectedRows = await this.db(this.tableName).where({ id }).del();
|
|
338
|
+
return { success: true, code: OK, msg: '删除成功', data: { affectedRows } };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 根据条件更新记录
|
|
343
|
+
* @param {Object} options - 更新选项
|
|
344
|
+
* @param {Object} options.query - 更新条件
|
|
345
|
+
* @param {Object} options.data - 更新数据
|
|
346
|
+
* @returns {Promise<Object>} 更新结果,包含affectedRows
|
|
347
|
+
*/
|
|
348
|
+
async updateByQuery({ query, data } = {}) {
|
|
349
|
+
this._checkDB();
|
|
350
|
+
|
|
351
|
+
if (!query || !data || !Object.keys(query).length || !Object.keys(data).length) {
|
|
352
|
+
return { success: false, code: PARAM_INVALID, msg: '参数无效', data: {} };
|
|
353
|
+
}
|
|
354
|
+
const formattedData = this._formatDateFields(data);
|
|
355
|
+
const affectedRows = await this.db(this.tableName).where(query).update(formattedData);
|
|
356
|
+
return { success: true, code: OK, msg: '更新成功', data: { affectedRows } };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 根据ID更新记录
|
|
361
|
+
* @param {number|string} id - 记录ID
|
|
362
|
+
* @param {Object} data - 更新数据
|
|
363
|
+
* @returns {Promise<Object>} 更新结果,包含affectedRows
|
|
364
|
+
*/
|
|
365
|
+
async updateById(id, data = {}) {
|
|
366
|
+
this._checkDB();
|
|
367
|
+
|
|
368
|
+
if (!id || !Object.keys(data).length) {
|
|
369
|
+
return { success: false, code: PARAM_INVALID, msg: '参数无效', data: {} };
|
|
370
|
+
}
|
|
371
|
+
const formattedData = this._formatDateFields(data);
|
|
372
|
+
const affectedRows = await this.db(this.tableName).where({ id }).update(formattedData);
|
|
373
|
+
const updatedRecord = await this.findById(id);
|
|
374
|
+
return { success: true, code: OK, msg: '更新成功', data: updatedRecord.data };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 批量更新记录(事务)
|
|
379
|
+
* @param {Array} updates - 更新数组,每项包含query和data
|
|
380
|
+
* @returns {Promise<Object>} 更新结果,包含affectedRows
|
|
381
|
+
* @description
|
|
382
|
+
* 在事务内执行批量更新,任一更新失败会回滚整个事务
|
|
383
|
+
*
|
|
384
|
+
* 安全改进:
|
|
385
|
+
* - 整体 try/catch 包裹,确保异常时事务一定 rollback
|
|
386
|
+
* - rollback 自身失败也不影响错误抛出(只记日志)
|
|
387
|
+
* - 参数校验提前到事务外,避免无谓事务开销
|
|
388
|
+
*/
|
|
389
|
+
async updateMany(updates = []) {
|
|
390
|
+
this._checkDB();
|
|
391
|
+
|
|
392
|
+
if (!Array.isArray(updates) || !updates.length) {
|
|
393
|
+
return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 参数预校验:在事务外检查,避免开启事务后才发现参数无效
|
|
397
|
+
for (const { query } of updates) {
|
|
398
|
+
if (!query || !Object.keys(query).length) {
|
|
399
|
+
return { success: false, code: PARAM_INVALID, msg: '参数无效:批量更新不允许空条件', data: {} };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const trx = await this.db.transaction();
|
|
404
|
+
try {
|
|
405
|
+
let totalAffected = 0;
|
|
406
|
+
for (const { query, data } of updates) {
|
|
407
|
+
const result = await trx(this.tableName).where(query).update(data);
|
|
408
|
+
if (result === 0) {
|
|
409
|
+
console.log('[updateMany] 未匹配到行, query=%j', query);
|
|
410
|
+
}
|
|
411
|
+
totalAffected += result;
|
|
412
|
+
}
|
|
413
|
+
await trx.commit();
|
|
414
|
+
return { success: true, code: OK, msg: '批量更新成功', data: { affectedRows: totalAffected } };
|
|
415
|
+
} catch (error) {
|
|
416
|
+
// 异常时一定 rollback,且 rollback 自身失败也不影响错误抛出
|
|
417
|
+
await trx.rollback().catch(rollbackErr => {
|
|
418
|
+
console.error('[updateMany] 回滚失败:', rollbackErr.message);
|
|
419
|
+
});
|
|
420
|
+
console.error('[updateMany] 批量更新失败:', error.message);
|
|
421
|
+
throw error; // 重新抛出,由上层处理
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* 分页查询
|
|
427
|
+
* @param {Object} options - 查询选项
|
|
428
|
+
* @param {number} options.current - 当前页码,默认1
|
|
429
|
+
* @param {number} options.pageSize - 每页大小,默认10
|
|
430
|
+
* @param {Object} options.query - 查询条件
|
|
431
|
+
* @param {Object} options.sort - 排序条件
|
|
432
|
+
* @param {Array} options.field - 查询字段
|
|
433
|
+
* @returns {Promise<Object>} 分页结果,包含list、total、current、pageSize、totalPages
|
|
434
|
+
*/
|
|
435
|
+
async query({ current = 1, pageSize = 10, query = {}, sort = {}, field = [] }) {
|
|
436
|
+
this._checkDB();
|
|
437
|
+
|
|
438
|
+
const size = Math.min(Math.max(pageSize, 1), this.limit);
|
|
439
|
+
const offset = (current - 1) * size;
|
|
440
|
+
|
|
441
|
+
let countQuery = this.db(this.tableName).count("* as total");
|
|
442
|
+
let dataQuery = this._buildBaseQuery({ query, sort, fields: field });
|
|
443
|
+
|
|
444
|
+
const [totalResult, list] = await Promise.all([
|
|
445
|
+
countQuery.first(),
|
|
446
|
+
dataQuery.offset(offset).limit(size),
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
const total = Number(totalResult?.total ?? 0);
|
|
450
|
+
const totalPages = Math.ceil(total / size);
|
|
451
|
+
|
|
452
|
+
return { success: true, code: OK, msg: '查询成功', data: { list: formatDateFields(list, this._dateFields), total, current, pageSize: size, totalPages } };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 统计记录数
|
|
457
|
+
* @param {Object} query - 查询条件
|
|
458
|
+
* @returns {Promise<Object>} 统计结果,包含count
|
|
459
|
+
*/
|
|
460
|
+
async count(query = {}) {
|
|
461
|
+
this._checkDB();
|
|
462
|
+
|
|
463
|
+
let dataQuery = this.db(this.tableName);
|
|
464
|
+
if (Object.keys(query).length) dataQuery = dataQuery.where(query);
|
|
465
|
+
const result = await dataQuery.count("* as total").first();
|
|
466
|
+
return { success: true, code: OK, msg: '统计成功', data: { count: Number(result?.total ?? 0) } };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* 检查记录是否存在
|
|
471
|
+
* @param {Object} query - 查询条件
|
|
472
|
+
* @returns {Promise<Object>} 存在性结果,包含exists布尔值
|
|
473
|
+
*/
|
|
474
|
+
async exists(query = {}) {
|
|
475
|
+
this._checkDB();
|
|
476
|
+
|
|
477
|
+
let dataQuery = this.db(this.tableName);
|
|
478
|
+
if (Object.keys(query).length) dataQuery = dataQuery.where(query);
|
|
479
|
+
const result = await dataQuery.first();
|
|
480
|
+
return { success: true, code: OK, msg: '检查成功', data: { exists: !!result } };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 关联查询
|
|
485
|
+
*
|
|
486
|
+
* IO 提醒:
|
|
487
|
+
* 默认 fields=["*"] 会 SELECT 所有列,若主表含 longtext(如 cms_article.content)
|
|
488
|
+
* 或大字段,会显著放大查询 IO。业务侧请显式传入 fields 白名单,仅取必要列。
|
|
489
|
+
*
|
|
490
|
+
* @param {Object} options - 查询选项
|
|
491
|
+
* @param {string} options.joinTable - 关联表名
|
|
492
|
+
* @param {string} options.localField - 本地表字段
|
|
493
|
+
* @param {string} options.foreignField - 关联表字段
|
|
494
|
+
* @param {Array} options.fields - 查询字段,默认["*"](推荐显式指定,避免拉回 longtext content)
|
|
495
|
+
* @param {Object} options.query - 查询条件
|
|
496
|
+
* @param {Object} options.sort - 排序条件
|
|
497
|
+
* @returns {Promise<Array>} 查询结果数组
|
|
498
|
+
*/
|
|
499
|
+
async join({ joinTable, localField, foreignField, fields = ["*"], query = {}, sort = {} }) {
|
|
500
|
+
this._checkDB();
|
|
501
|
+
|
|
502
|
+
let _query = this.db(this.tableName)
|
|
503
|
+
.join(
|
|
504
|
+
joinTable,
|
|
505
|
+
`${this.tableName}.${localField}`,
|
|
506
|
+
"=",
|
|
507
|
+
`${joinTable}.${foreignField}`
|
|
508
|
+
)
|
|
509
|
+
.select(fields);
|
|
510
|
+
|
|
511
|
+
if (Object.keys(query).length) _query = _query.where(query);
|
|
512
|
+
if (sort && typeof sort === 'object') {
|
|
513
|
+
for (const [field, dir] of Object.entries(sort)) {
|
|
514
|
+
// 排序字段白名单校验,拒绝 SQL 注入字符
|
|
515
|
+
if (!SORT_FIELD_REGEX.test(field)) {
|
|
516
|
+
console.warn(`[Repository] 非法排序字段被拒绝: ${field}`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const direction = ['asc', 'desc'].includes(String(dir).toLowerCase()) ? String(dir).toLowerCase() : 'asc';
|
|
520
|
+
_query = _query.orderBy(field, direction);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const res = await _query;
|
|
525
|
+
return { success: true, code: OK, msg: '查询成功', data: formatDateFields(res, this._dateFields) };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* 批量删除记录(根据ID数组)
|
|
530
|
+
* @param {Array} ids - ID数组
|
|
531
|
+
* @returns {Promise<Object>} 删除结果,包含affectedRows
|
|
532
|
+
*/
|
|
533
|
+
async deleteMany(ids = []) {
|
|
534
|
+
this._checkDB();
|
|
535
|
+
|
|
536
|
+
if (!ids.length) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
537
|
+
const affectedRows = await this.db(this.tableName).whereIn('id', ids).del();
|
|
538
|
+
return { success: true, code: OK, msg: '删除成功', data: { affectedRows } };
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 软删除(设置deleted_at字段)
|
|
543
|
+
* @param {number|string} id - 记录ID
|
|
544
|
+
* @returns {Promise<Object>} 删除结果,包含 affectedRows
|
|
545
|
+
*/
|
|
546
|
+
async softDelete(id) {
|
|
547
|
+
this._checkDB();
|
|
548
|
+
|
|
549
|
+
if (!id) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
550
|
+
const affectedRows = await this.db(this.tableName)
|
|
551
|
+
.where({ id })
|
|
552
|
+
.update({ deleted_at: new Date() });
|
|
553
|
+
return { success: true, code: OK, msg: '软删除成功', data: { affectedRows } };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* 恢复软删除的记录
|
|
558
|
+
* @param {number|string} id - 记录ID
|
|
559
|
+
* @returns {Promise<Object>} 恢复结果,包含 affectedRows
|
|
560
|
+
*/
|
|
561
|
+
async restore(id) {
|
|
562
|
+
this._checkDB();
|
|
563
|
+
|
|
564
|
+
if (!id) return { success: false, code: PARAM_MISSING, msg: '参数缺失', data: {} };
|
|
565
|
+
const affectedRows = await this.db(this.tableName)
|
|
566
|
+
.where({ id })
|
|
567
|
+
.update({ deleted_at: null });
|
|
568
|
+
return { success: true, code: OK, msg: '恢复成功', data: { affectedRows } };
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 查询已软删除的记录
|
|
573
|
+
* @param {Object} options - 查询选项
|
|
574
|
+
* @param {Object} options.query - 查询条件
|
|
575
|
+
* @param {Array} options.fields - 查询字段
|
|
576
|
+
* @param {number} options.limit - 限制条数,默认20
|
|
577
|
+
* @param {number} options.offset - 偏移量,默认0
|
|
578
|
+
* @returns {Promise<Object>} 查询结果,包含 data 字段(数组)
|
|
579
|
+
*/
|
|
580
|
+
async findTrashed({ query = {}, fields = [], limit = 20, offset = 0 } = {}) {
|
|
581
|
+
this._checkDB();
|
|
582
|
+
|
|
583
|
+
let _query = this.db(this.tableName).whereNotNull('deleted_at');
|
|
584
|
+
|
|
585
|
+
if (Object.keys(query).length) _query = _query.where(query);
|
|
586
|
+
if (fields.length) _query = _query.select(fields);
|
|
587
|
+
|
|
588
|
+
const res = await _query.limit(limit).offset(offset).orderBy('deleted_at', 'desc');
|
|
589
|
+
return { success: true, code: OK, msg: '查询成功', data: formatDateFields(res, this._dateFields) };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* 强制删除指定天数前的软删除记录
|
|
594
|
+
* @param {number} days - 天数,默认30天
|
|
595
|
+
* @returns {Promise<Object>} 删除结果,包含 affectedRows
|
|
596
|
+
*/
|
|
597
|
+
async forceDelete(days = 30) {
|
|
598
|
+
this._checkDB();
|
|
599
|
+
|
|
600
|
+
const date = new Date();
|
|
601
|
+
date.setDate(date.getDate() - days);
|
|
602
|
+
|
|
603
|
+
const affectedRows = await this.db(this.tableName)
|
|
604
|
+
.whereNotNull('deleted_at')
|
|
605
|
+
.where('deleted_at', '<', date)
|
|
606
|
+
.del();
|
|
607
|
+
|
|
608
|
+
return { success: true, code: OK, msg: '强制删除成功', data: { affectedRows } };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 统计数据(总数和今日新增)
|
|
613
|
+
*
|
|
614
|
+
* 列名说明:
|
|
615
|
+
* 业务表统一使用驼峰 createdAt(与 cms_article / member 等表 DDL 一致)。
|
|
616
|
+
* 若子类对应的表为蛇形 created_at(如 sys_oper_log),请在子类中覆盖 stats()。
|
|
617
|
+
*
|
|
618
|
+
* @returns {Promise<Object>} 统计结果,包含 data: { total, today }
|
|
619
|
+
*/
|
|
620
|
+
async stats() {
|
|
621
|
+
this._checkDB();
|
|
622
|
+
|
|
623
|
+
const total = await this.db(this.tableName).count('* as count').first();
|
|
624
|
+
const today = new Date();
|
|
625
|
+
today.setHours(0, 0, 0, 0);
|
|
626
|
+
|
|
627
|
+
const todayCount = await this.db(this.tableName)
|
|
628
|
+
.where('createdAt', '>=', today)
|
|
629
|
+
.count('* as count')
|
|
630
|
+
.first();
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
success: true,
|
|
634
|
+
code: OK,
|
|
635
|
+
msg: '统计成功',
|
|
636
|
+
data: {
|
|
637
|
+
total: Number(total?.count || 0),
|
|
638
|
+
today: Number(todayCount?.count || 0)
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export default Repository;
|