midway-fatcms 0.0.8 → 0.0.9
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/.qoder/skills/midway-fatcms/02-crud-quick.md +38 -0
- package/.qoder/skills/midway-fatcms/03-crud-sharding.md +37 -36
- package/.qoder/skills/midway-fatcms/07-examples.md +4 -0
- package/dist/configuration.d.ts +10 -0
- package/dist/configuration.js +26 -0
- package/dist/controller/helpers.controller.d.ts +6 -0
- package/dist/controller/helpers.controller.js +19 -0
- package/dist/libs/crud-pro/CrudPro.d.ts +29 -2
- package/dist/libs/crud-pro/CrudPro.js +58 -2
- package/dist/libs/crud-pro/exceptions.d.ts +7 -0
- package/dist/libs/crud-pro/exceptions.js +7 -0
- package/dist/libs/crud-pro/interfaces.d.ts +1 -0
- package/dist/libs/crud-pro/models/CrudResult.d.ts +3 -2
- package/dist/libs/crud-pro/models/CrudResult.js +1 -1
- package/dist/libs/crud-pro/models/ServiceHub.d.ts +2 -0
- package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +70 -2
- package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +205 -13
- package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +36 -0
- package/dist/libs/crud-pro/services/CrudProTableMetaService.js +97 -3
- package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +2 -0
- package/dist/libs/crud-pro/services/CurdProServiceHub.js +6 -0
- package/dist/libs/crud-pro-quick/CrudProQuick.d.ts +93 -6
- package/dist/libs/crud-pro-quick/CrudProQuick.js +192 -32
- package/dist/libs/crud-sharding/ShardingBase.d.ts +78 -0
- package/dist/libs/crud-sharding/ShardingBase.js +179 -0
- package/dist/libs/crud-sharding/ShardingByCustomCrud.d.ts +35 -0
- package/dist/libs/crud-sharding/ShardingByCustomCrud.js +297 -0
- package/dist/libs/crud-sharding/ShardingByHashCrud.d.ts +38 -0
- package/dist/libs/crud-sharding/ShardingByHashCrud.js +86 -0
- package/dist/libs/crud-sharding/ShardingByKeyCrud.d.ts +39 -0
- package/dist/libs/crud-sharding/ShardingByKeyCrud.js +74 -0
- package/dist/libs/crud-sharding/ShardingByTimeCrud.d.ts +66 -0
- package/dist/libs/crud-sharding/ShardingByTimeCrud.js +524 -0
- package/dist/libs/crud-sharding/ShardingConfig.d.ts +10 -8
- package/dist/libs/crud-sharding/ShardingConfig.js +3 -3
- package/dist/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +1 -1
- package/dist/libs/crud-sharding/index.d.ts +10 -13
- package/dist/libs/crud-sharding/index.js +21 -17
- package/dist/models/RedisKeys.d.ts +1 -0
- package/dist/models/RedisKeys.js +1 -0
- package/dist/service/TableMetaCacheRedisSubscriber.d.ts +31 -0
- package/dist/service/TableMetaCacheRedisSubscriber.js +98 -0
- package/dist/service/curd/CurdMixService.d.ts +2 -2
- package/dist/service/curd/CurdProService.d.ts +109 -5
- package/dist/service/curd/CurdProService.js +127 -7
- package/package.json +1 -1
- package/src/configuration.ts +27 -0
- package/src/controller/helpers.controller.ts +15 -0
- package/src/libs/crud-pro/CrudPro.ts +73 -4
- package/src/libs/crud-pro/exceptions.ts +8 -0
- package/src/libs/crud-pro/interfaces.ts +1 -0
- package/src/libs/crud-pro/models/CrudResult.ts +5 -5
- package/src/libs/crud-pro/models/ServiceHub.ts +4 -0
- package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +238 -15
- package/src/libs/crud-pro/services/CrudProTableMetaService.ts +110 -2
- package/src/libs/crud-pro/services/CurdProServiceHub.ts +8 -0
- package/src/libs/crud-pro-quick/CrudProQuick.ts +234 -46
- package/src/libs/crud-sharding/ShardingBase.ts +256 -0
- package/src/libs/crud-sharding/ShardingByCustomCrud.ts +329 -0
- package/src/libs/crud-sharding/ShardingByHashCrud.ts +111 -0
- package/src/libs/crud-sharding/ShardingByKeyCrud.ts +97 -0
- package/src/libs/crud-sharding/ShardingByTimeCrud.ts +628 -0
- package/src/libs/crud-sharding/ShardingConfig.ts +10 -8
- package/src/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +1 -1
- package/src/libs/crud-sharding/index.ts +17 -14
- package/src/models/RedisKeys.ts +1 -0
- package/src/service/TableMetaCacheRedisSubscriber.ts +105 -0
- package/src/service/curd/CurdMixService.ts +2 -2
- package/src/service/curd/CurdProService.ts +131 -9
- package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +0 -208
- package/dist/libs/crud-sharding/ShardingCrudPro.js +0 -879
- package/dist/libs/crud-sharding/ShardingRouter.d.ts +0 -70
- package/dist/libs/crud-sharding/ShardingRouter.js +0 -396
- package/src/libs/crud-sharding/ShardingCrudPro.ts +0 -1105
- package/src/libs/crud-sharding/ShardingRouter.ts +0 -533
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
* - YEAR: 按年分表,如 order_2024, order_2025
|
|
8
8
|
* - MONTH: 按月分表,如 order_202401, order_202402
|
|
9
9
|
* - DAY: 按日分表,如 order_20240101
|
|
10
|
-
* - RANGE: 按范围分表,如 user_0 ~ user_99
|
|
11
10
|
* - HASH: 按哈希分表,如 order_01 ~ order_16
|
|
11
|
+
* - KEY: 按键值分表,字段原值=表后缀,如 order_east, order_west
|
|
12
12
|
* - CUSTOM: 自定义分表规则
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
|
-
* import {
|
|
15
|
+
* import { ShardingByTimeCrud, ShardingType } from '@/libs/crud-sharding';
|
|
16
16
|
*
|
|
17
|
-
* //
|
|
18
|
-
* const sharding = new
|
|
17
|
+
* // 创建时间分表操作器
|
|
18
|
+
* const sharding = new ShardingByTimeCrud(crudProFactory, {
|
|
19
19
|
* type: ShardingType.MONTH,
|
|
20
20
|
* baseTable: 't_order',
|
|
21
21
|
* timeColumn: 'created_at',
|
|
@@ -25,18 +25,21 @@
|
|
|
25
25
|
* await sharding.insert({
|
|
26
26
|
* data: { order_id: '001', amount: 100, created_at: '2024-03-15' },
|
|
27
27
|
* });
|
|
28
|
-
*
|
|
29
|
-
* // 分页查询(自动合并多表结果)
|
|
30
|
-
* const result = await sharding.findPage({
|
|
31
|
-
* condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
|
|
32
|
-
* pageNo: 1,
|
|
33
|
-
* pageSize: 10,
|
|
34
|
-
* });
|
|
35
28
|
*/
|
|
36
29
|
|
|
37
|
-
//
|
|
38
|
-
export {
|
|
39
|
-
|
|
30
|
+
// 基类
|
|
31
|
+
export { ShardingBase } from './ShardingBase';
|
|
32
|
+
|
|
33
|
+
// 四个子类
|
|
34
|
+
export { ShardingByTimeCrud } from './ShardingByTimeCrud';
|
|
35
|
+
export { ShardingByHashCrud } from './ShardingByHashCrud';
|
|
36
|
+
export { ShardingByKeyCrud } from './ShardingByKeyCrud';
|
|
37
|
+
export { ShardingByCustomCrud } from './ShardingByCustomCrud';
|
|
38
|
+
|
|
39
|
+
// 兼容旧代码:ShardingCrudPro → ShardingBase
|
|
40
|
+
export { ShardingBase as ShardingCrudPro } from './ShardingBase';
|
|
41
|
+
|
|
42
|
+
// 合并器(仅 Time 使用)
|
|
40
43
|
export { ShardingMerger } from './ShardingMerger';
|
|
41
44
|
|
|
42
45
|
// 配置和类型
|
package/src/models/RedisKeys.ts
CHANGED
|
@@ -11,6 +11,7 @@ const RedisKeys = {
|
|
|
11
11
|
CURD_MIX_BY_DICT_PREFIX: 'FatCurdMixByDict@@',
|
|
12
12
|
QUERY_ENUM_INFO_PREFIX: 'FatQueryEnum@@',
|
|
13
13
|
REDIS_LOCK_MIDDILEWARE: 'FatRedisLock@@',
|
|
14
|
+
TABLE_META_CACHE_INVALIDATE_CHANNEL: 'FatTableMetaCacheInvalidate',
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export { RedisKeys };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { RedisService } from '@midwayjs/redis';
|
|
2
|
+
import { clearTableMetaCache } from '@/libs/crud-pro/services/CrudProTableMetaService';
|
|
3
|
+
import { RedisKeys } from '@/models/RedisKeys';
|
|
4
|
+
|
|
5
|
+
const CHANNEL = RedisKeys.TABLE_META_CACHE_INVALIDATE_CHANNEL;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 本节点唯一标识,用于区分是自己发出的广播还是其他节点发出的
|
|
9
|
+
* 避免重复清空(本节点已在发布前清空过)
|
|
10
|
+
*/
|
|
11
|
+
const NODE_ID = `node_${process.pid}_${Date.now()}`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 用于发布消息的 Redis 客户端(复用 @midwayjs/redis 注入的实例)
|
|
15
|
+
*/
|
|
16
|
+
let publishClient: RedisService | null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 用于订阅的 Redis 客户端(独立连接,ioredis 规定订阅连接不能执行普通命令)
|
|
20
|
+
*/
|
|
21
|
+
let subscribeClient: RedisService | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 初始化发布客户端
|
|
25
|
+
*
|
|
26
|
+
* 在 Midway 容器初始化完成后调用,注入已有的 RedisService 实例
|
|
27
|
+
* @param client RedisService 实例
|
|
28
|
+
*/
|
|
29
|
+
export function initTableMetaCachePublishClient(client: RedisService): void {
|
|
30
|
+
publishClient = client;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 初始化 Redis 订阅,监听缓存失效广播
|
|
35
|
+
*
|
|
36
|
+
* 使用 RedisService.duplicate() 创建独立的订阅连接。
|
|
37
|
+
* 收到广播后自动清空本节点的本地缓存。
|
|
38
|
+
*
|
|
39
|
+
* @param mainClient 已有的 RedisService 实例,用于 duplicate 出订阅连接
|
|
40
|
+
*/
|
|
41
|
+
export function initTableMetaCacheSubscriber(mainClient: RedisService): void {
|
|
42
|
+
if (subscribeClient) {
|
|
43
|
+
return; // 防止重复初始化
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// duplicate() 创建一个同配置的独立连接
|
|
47
|
+
subscribeClient = mainClient.duplicate() as RedisService;
|
|
48
|
+
|
|
49
|
+
subscribeClient.subscribe(CHANNEL).catch((err: Error) => {
|
|
50
|
+
console.error('[TableMetaCacheRedis] subscribe failed:', err.message);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
subscribeClient.on('message', (channel: string, message: string) => {
|
|
54
|
+
if (channel !== CHANNEL) return;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(message);
|
|
58
|
+
// 跳过自己发出的广播(本节点已在发布前清空过)
|
|
59
|
+
if (data.nodeId === NODE_ID) return;
|
|
60
|
+
|
|
61
|
+
const stats = clearTableMetaCache();
|
|
62
|
+
console.info(`[TableMetaCacheRedis] 收到其他节点(${data.nodeId})的缓存清空广播,已清空本地缓存:`, stats);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error('[TableMetaCacheRedis] 处理缓存失效广播异常:', e);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
subscribeClient.on('error', (err: Error) => {
|
|
69
|
+
console.error('[TableMetaCacheRedis] subscriber connection error:', err.message);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.info('[TableMetaCacheRedis] 订阅已启动,监听频道:', CHANNEL);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 清空本节点缓存并广播通知集群中其他节点
|
|
77
|
+
*
|
|
78
|
+
* 调用流程:
|
|
79
|
+
* 1. 清空本节点本地缓存
|
|
80
|
+
* 2. 通过 Redis PUBLISH 广播消息
|
|
81
|
+
* 3. 其他节点收到消息后清空各自的本地缓存
|
|
82
|
+
*
|
|
83
|
+
* @returns 清空前的缓存统计
|
|
84
|
+
*/
|
|
85
|
+
export function clearTableMetaCacheWithBroadcast(): { metaCacheCount: number; tableInfoCacheCount: number } {
|
|
86
|
+
// 1. 清空本节点缓存
|
|
87
|
+
const stats = clearTableMetaCache();
|
|
88
|
+
|
|
89
|
+
// 2. 广播通知其他节点
|
|
90
|
+
if (publishClient && typeof publishClient.publish === 'function') {
|
|
91
|
+
const message = JSON.stringify({
|
|
92
|
+
nodeId: NODE_ID,
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
beforeClear: stats,
|
|
95
|
+
});
|
|
96
|
+
publishClient.publish(CHANNEL, message).catch((err: Error) => {
|
|
97
|
+
console.error('[TableMetaCacheRedis] publish failed:', err.message);
|
|
98
|
+
});
|
|
99
|
+
} else {
|
|
100
|
+
// 没有 Redis 连接时仅清空本地(单节点部署降级)
|
|
101
|
+
console.info('[TableMetaCacheRedis] 无 Redis 发布客户端,仅清空本节点缓存');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return stats;
|
|
105
|
+
}
|
|
@@ -10,7 +10,7 @@ import { IRequestCfgModel2 } from '@/models/bizmodels';
|
|
|
10
10
|
import { CurdMixByLinkToCustomService } from './CurdMixByLinkToCustomService';
|
|
11
11
|
// import { SqlDbType } from '@/libs/crud-pro/models/keys';
|
|
12
12
|
import { ExecuteContext, IExecuteContextCustom } from '@/libs/crud-pro/models/ExecuteContext';
|
|
13
|
-
import {
|
|
13
|
+
import { ShardingBase } from "@/libs/crud-sharding";
|
|
14
14
|
import { IGetQuickCrudParam, IGetShardingCrudParam } from './CurdProService';
|
|
15
15
|
|
|
16
16
|
export interface ILinkColumnRelationParam {
|
|
@@ -68,7 +68,7 @@ export class CurdMixService {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
public getShardingCrud(param: IGetShardingCrudParam):
|
|
71
|
+
public getShardingCrud(param: IGetShardingCrudParam): ShardingBase {
|
|
72
72
|
this.prepare();
|
|
73
73
|
return this.curdProService.getShardingCrud(param);
|
|
74
74
|
}
|
|
@@ -17,7 +17,7 @@ import { BaseService } from '../base/BaseService';
|
|
|
17
17
|
import { IRequestCfgModel2, IVisitorExt } from '@/models/bizmodels';
|
|
18
18
|
import { IWorkbenchEntity } from '@/models/SystemEntities';
|
|
19
19
|
import { CrudProQuick } from '@/libs/crud-pro-quick';
|
|
20
|
-
import {
|
|
20
|
+
import { ShardingBase, ShardingByTimeCrud, ShardingByHashCrud, ShardingByKeyCrud, ShardingByCustomCrud, ShardingType, IShardingConfig } from '@/libs/crud-sharding';
|
|
21
21
|
import { fixCfgModel } from './fixCfgModel';
|
|
22
22
|
import { GLOBAL_STATIC_CONFIG } from '@/libs/global-config/global-config';
|
|
23
23
|
import { fixSoftDelete } from "@/libs/crud-pro-quick/fixSoftDelete";
|
|
@@ -172,10 +172,40 @@ export class CurdProService extends BaseService {
|
|
|
172
172
|
|
|
173
173
|
private responseCfgHandlers: Record<RelatedType, IExecuteContextHandler> = {} as any;
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* 注册“响应配置处理器”(response cfg handler)。
|
|
177
|
+
*
|
|
178
|
+
* 这些处理器会在一次 CRUD/SQL 执行完成后被调用,用于对 `ExecuteContext` 做统一的二次加工,
|
|
179
|
+
* 例如:追加扩展数据、做字段脱敏、统一包装返回结构等。
|
|
180
|
+
*
|
|
181
|
+
* @param key 处理器类型标识(业务自定义枚举/常量)
|
|
182
|
+
* @param handler 处理器实现
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* curdProService.setResponseCfgHandlers(RelatedType.USER, {
|
|
186
|
+
* async handleExecuteContextPrepare(ctx) {
|
|
187
|
+
* // 可在执行结果处理前做准备
|
|
188
|
+
* },
|
|
189
|
+
* async handleExecuteContext(ctx) {
|
|
190
|
+
* // 对 ctx.result / ctx.data 等进行二次加工
|
|
191
|
+
* },
|
|
192
|
+
* });
|
|
193
|
+
*/
|
|
175
194
|
public setResponseCfgHandlers(key: RelatedType, handler: IExecuteContextHandler) {
|
|
176
195
|
this.responseCfgHandlers[key] = handler;
|
|
177
196
|
}
|
|
178
197
|
|
|
198
|
+
/**
|
|
199
|
+
* 获取当前已注册的“响应配置处理器”集合。
|
|
200
|
+
*
|
|
201
|
+
* 一般用于调试或在外层框架中统一检查/复用已有 handler。
|
|
202
|
+
*
|
|
203
|
+
* @returns 以 `RelatedType` 为 key 的处理器映射表
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* const handlers = curdProService.getResponseCfgHandlers();
|
|
207
|
+
* const userHandler = handlers[RelatedType.USER];
|
|
208
|
+
*/
|
|
179
209
|
public getResponseCfgHandlers(): Record<RelatedType, IExecuteContextHandler> {
|
|
180
210
|
return this.responseCfgHandlers;
|
|
181
211
|
}
|
|
@@ -213,8 +243,21 @@ export class CurdProService extends BaseService {
|
|
|
213
243
|
}
|
|
214
244
|
|
|
215
245
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
246
|
+
* 通过 `method` 走配置化 CRUD 执行(从缓存/加载的配置中解析)。
|
|
247
|
+
*
|
|
248
|
+
* 适用于:前端/调用方只传 `method` + 入参,由服务端根据 `method` 找到对应的 CRUD 配置并执行。
|
|
249
|
+
* 在非调试模式下会启用配置缓存(调试模式关闭缓存,便于开发即时生效)。
|
|
250
|
+
*
|
|
251
|
+
* @param reqJson 请求模型,必须包含 `method`
|
|
252
|
+
* @returns `ExecuteContext`(包含执行过程与结果数据)
|
|
253
|
+
*
|
|
254
|
+
* @example
|
|
255
|
+
* const ctx = await curdProService.executeCrud({
|
|
256
|
+
* method: 'user.list',
|
|
257
|
+
* pageNo: 1,
|
|
258
|
+
* pageSize: 20,
|
|
259
|
+
* });
|
|
260
|
+
* console.log(ctx.result);
|
|
218
261
|
*/
|
|
219
262
|
async executeCrud(reqJson: IRequestModel): Promise<ExecuteContext> {
|
|
220
263
|
if (!reqJson.method) {
|
|
@@ -228,13 +271,46 @@ export class CurdProService extends BaseService {
|
|
|
228
271
|
return curdPro.executeCrud(reqJson, isEnableCache);
|
|
229
272
|
}
|
|
230
273
|
|
|
231
|
-
|
|
274
|
+
/**
|
|
275
|
+
* 直接执行一段 SQL(不依赖 `method` 配置)。
|
|
276
|
+
*
|
|
277
|
+
* 适用于:后台管理/运维脚本/内部工具需要直接跑 SQL 的场景(仍会复用本服务的连接池、日志、visitor、事务等上下文)。
|
|
278
|
+
*
|
|
279
|
+
* @param sqlCfgModel SQL 执行配置(数据库、类型、SQL、参数等)
|
|
280
|
+
* @returns SQL 执行结果
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* const res = await curdProService.executeSQL({
|
|
284
|
+
* sqlDbType: SqlDbType.mysql,
|
|
285
|
+
* sqlDatabase: 'mydb',
|
|
286
|
+
* executeSql: 'select * from t_user where id = ?',
|
|
287
|
+
* executeSqlArgs: [1],
|
|
288
|
+
* });
|
|
289
|
+
*/
|
|
232
290
|
public async executeSQL(sqlCfgModel: ISqlCfgModel): Promise<ExecuteSQLResult> {
|
|
233
291
|
const curdPro = this.getCrudPro();
|
|
234
292
|
return curdPro.executeSQL(sqlCfgModel);
|
|
235
293
|
}
|
|
236
294
|
|
|
237
|
-
|
|
295
|
+
/**
|
|
296
|
+
* 直接按指定配置执行一次 CRUD(不走缓存查找)。
|
|
297
|
+
*
|
|
298
|
+
* 适用于:调用方已经拿到了/动态生成了本次要执行的 CRUD 配置,想直接执行并得到结果。
|
|
299
|
+
* 该方法会对 `cfgModel` 做两类标准化处理:
|
|
300
|
+
* - 统一修正/补全配置(`fixCfgModel`)
|
|
301
|
+
* - 应用软删除规则(`fixSoftDelete`)
|
|
302
|
+
*
|
|
303
|
+
* @param reqJson 本次请求入参
|
|
304
|
+
* @param cfgModel 本次请求对应的 CRUD 配置
|
|
305
|
+
* @returns `ExecuteContext`
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* const cfg = await curdProService.getCachedCfgByMethod('user.list');
|
|
309
|
+
* const ctx = await curdProService.executeCrudByCfg(
|
|
310
|
+
* { method: 'user.list', pageNo: 1, pageSize: 20 },
|
|
311
|
+
* cfg as any
|
|
312
|
+
* );
|
|
313
|
+
*/
|
|
238
314
|
public async executeCrudByCfg(reqJson: IRequestModel, cfgModel: IRequestCfgModel2): Promise<ExecuteContext> {
|
|
239
315
|
this.logInfo('executeCrudByCfg', cfgModel);
|
|
240
316
|
const curdPro = this.getCrudPro();
|
|
@@ -277,7 +353,7 @@ export class CurdProService extends BaseService {
|
|
|
277
353
|
* 获取分表 CRUD 操作器
|
|
278
354
|
*
|
|
279
355
|
* @param param 参数对象
|
|
280
|
-
* @returns
|
|
356
|
+
* @returns ShardingBase 实例(实际类型根据 shardingConfig.type 决定)
|
|
281
357
|
*
|
|
282
358
|
* @example
|
|
283
359
|
* const sharding = curdProService.getShardingCrud({
|
|
@@ -290,20 +366,66 @@ export class CurdProService extends BaseService {
|
|
|
290
366
|
* },
|
|
291
367
|
* });
|
|
292
368
|
*/
|
|
293
|
-
public getShardingCrud(param: IGetShardingCrudParam):
|
|
369
|
+
public getShardingCrud(param: IGetShardingCrudParam): ShardingBase {
|
|
294
370
|
const { sqlDatabase, sqlDbType, shardingConfig } = param;
|
|
295
|
-
|
|
296
|
-
|
|
371
|
+
const factory = () => this.getCrudPro();
|
|
372
|
+
|
|
373
|
+
let sharding: ShardingBase;
|
|
374
|
+
switch (shardingConfig.type) {
|
|
375
|
+
case ShardingType.YEAR:
|
|
376
|
+
case ShardingType.MONTH:
|
|
377
|
+
case ShardingType.DAY:
|
|
378
|
+
sharding = new ShardingByTimeCrud(factory, shardingConfig);
|
|
379
|
+
break;
|
|
380
|
+
case ShardingType.HASH:
|
|
381
|
+
sharding = new ShardingByHashCrud(factory, shardingConfig);
|
|
382
|
+
break;
|
|
383
|
+
case ShardingType.KEY:
|
|
384
|
+
sharding = new ShardingByKeyCrud(factory, shardingConfig);
|
|
385
|
+
break;
|
|
386
|
+
case ShardingType.CUSTOM:
|
|
387
|
+
sharding = new ShardingByCustomCrud(factory, shardingConfig);
|
|
388
|
+
break;
|
|
389
|
+
default:
|
|
390
|
+
throw new Error(`[CurdProService] 不支持的分表类型: ${shardingConfig.type}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
297
393
|
sharding.setBaseCfg({ sqlDatabase, sqlDbType });
|
|
298
394
|
return sharding;
|
|
299
395
|
}
|
|
300
396
|
|
|
397
|
+
/**
|
|
398
|
+
* 根据 `method` 获取(可能来自缓存的)CRUD 配置。
|
|
399
|
+
*
|
|
400
|
+
* 适用于:你只想先拿到配置做检查/二次加工/审计,再决定是否执行。
|
|
401
|
+
* 非调试模式会启用缓存(调试模式关闭缓存)。
|
|
402
|
+
*
|
|
403
|
+
* @param method 配置方法名,例如:`user.list`
|
|
404
|
+
* @returns CRUD 配置模型
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* const cfg = await curdProService.getCachedCfgByMethod('user.list');
|
|
408
|
+
* console.log(cfg.sqlSimpleName);
|
|
409
|
+
*/
|
|
301
410
|
public async getCachedCfgByMethod(method: string): Promise<IRequestCfgModel> {
|
|
302
411
|
const curdPro = this.getCrudPro();
|
|
303
412
|
const isEnableCache = !this.isEnableDebug(); // 开发环境不使用缓存
|
|
304
413
|
return curdPro.getCachedCfgByMethod(method, isEnableCache);
|
|
305
414
|
}
|
|
306
415
|
|
|
416
|
+
/**
|
|
417
|
+
* 获取数据库的表/视图等结构信息列表(用于数据源探测、表选择器、代码生成等场景)。
|
|
418
|
+
*
|
|
419
|
+
* @param query 查询条件(例如:指定数据库、关键字过滤、分页等)
|
|
420
|
+
* @param options 可选项(例如:是否包含视图/系统表、大小写处理等,具体以接口定义为准)
|
|
421
|
+
* @returns 表信息列表结果
|
|
422
|
+
*
|
|
423
|
+
* @example
|
|
424
|
+
* const tables = await curdProService.getAllTableInfos(
|
|
425
|
+
* { sqlDbType: SqlDbType.mysql, sqlDatabase: 'mydb' },
|
|
426
|
+
* { skipCache: true }
|
|
427
|
+
* );
|
|
428
|
+
*/
|
|
307
429
|
public async getAllTableInfos(query: ITableNamesQuery, options?: ITableNamesOptions): Promise<ITableListResult> {
|
|
308
430
|
const curdPro = this.getCrudPro();
|
|
309
431
|
return curdPro.getAllTableInfos(query, options);
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import { IRequestModel, IRequestCfgModel } from '../../libs/crud-pro/interfaces';
|
|
2
|
-
import { IShardingConfig, CrudProFactory } from './ShardingConfig';
|
|
3
|
-
import { CrudWriteResult, CrudQueryOneResult, CrudQueryListResult, CrudQueryPageResult, CrudExistResult, CrudCountResult, CrudUpsertResult } from '../../libs/crud-pro/models/CrudResult';
|
|
4
|
-
import { ShardingBatchInsertResult } from './ShardingResult';
|
|
5
|
-
/**
|
|
6
|
-
* 分表 CRUD 操作封装器
|
|
7
|
-
*
|
|
8
|
-
* 在 CrudPro 之上封装分表功能,不修改 CrudPro 内部代码。
|
|
9
|
-
* 提供透明的分表路由和多表查询结果合并能力。
|
|
10
|
-
*
|
|
11
|
-
* 支持的分表策略:
|
|
12
|
-
* - YEAR: 按年分表,如 order_2024, order_2025
|
|
13
|
-
* - MONTH: 按月分表,如 order_202401, order_202402
|
|
14
|
-
* - DAY: 按日分表,如 order_20240101
|
|
15
|
-
* - RANGE: 按范围分表,如 user_0 ~ user_99
|
|
16
|
-
* - HASH: 按哈希分表,如 order_01 ~ order_16
|
|
17
|
-
* - CUSTOM: 自定义分表规则
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // 按月分表
|
|
21
|
-
* const sharding = new ShardingCrudPro(crudPro, {
|
|
22
|
-
* type: ShardingType.MONTH,
|
|
23
|
-
* baseTable: 't_order',
|
|
24
|
-
* timeColumn: 'created_at',
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* // 插入数据(自动路由到正确分表)
|
|
28
|
-
* await sharding.insert({ data: { order_id: '001', amount: 100, created_at: '2024-03-15' } });
|
|
29
|
-
*
|
|
30
|
-
* // 分页查询(自动合并多表结果)
|
|
31
|
-
* const result = await sharding.findPage({
|
|
32
|
-
* condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
|
|
33
|
-
* pageNo: 1,
|
|
34
|
-
* pageSize: 10,
|
|
35
|
-
* });
|
|
36
|
-
*/
|
|
37
|
-
export declare class ShardingCrudPro {
|
|
38
|
-
private readonly crudProFactory;
|
|
39
|
-
private readonly config;
|
|
40
|
-
private readonly router;
|
|
41
|
-
private readonly merger;
|
|
42
|
-
private readonly tableCreator;
|
|
43
|
-
private baseCfg;
|
|
44
|
-
private enableSoftDelete;
|
|
45
|
-
constructor(crudProFactory: CrudProFactory, config: IShardingConfig);
|
|
46
|
-
setBaseCfg(cfg: Partial<IRequestCfgModel>): this;
|
|
47
|
-
getConfig(): IShardingConfig;
|
|
48
|
-
/**
|
|
49
|
-
* 设置是否启用软删除
|
|
50
|
-
* @param enable 是否启用软删除
|
|
51
|
-
*/
|
|
52
|
-
setEnableSoftDelete(enable: boolean): this;
|
|
53
|
-
insert(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
54
|
-
batchInsert(reqJson: IRequestModel): Promise<ShardingBatchInsertResult>;
|
|
55
|
-
update(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
56
|
-
delete(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
57
|
-
/**
|
|
58
|
-
* 恢复软删除的记录
|
|
59
|
-
* 将 deleted_at 字段重置为 0,deleted_by 重置为空字符串
|
|
60
|
-
*
|
|
61
|
-
* @param reqJson 请求参数,通过 condition 指定要恢复的记录
|
|
62
|
-
* @returns CrudWriteResult 包含 affectedRows 和 getRawContext()
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* // 恢复指定 ID 的记录
|
|
66
|
-
* await sharding.restore({ condition: { id: 1 } });
|
|
67
|
-
*
|
|
68
|
-
* // 恢复多条记录(使用 $in 操作符)
|
|
69
|
-
* await sharding.restore({ condition: { id: { $in: [1, 2, 3] } } });
|
|
70
|
-
*
|
|
71
|
-
* // 按条件批量恢复记录
|
|
72
|
-
* await sharding.restore({ condition: { status: 'deleted' } });
|
|
73
|
-
*/
|
|
74
|
-
restore(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
75
|
-
insertOrUpdate(reqJson: IRequestModel): Promise<CrudUpsertResult>;
|
|
76
|
-
insertOnDuplicateUpdate(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
77
|
-
findOne<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryOneResult<T>>;
|
|
78
|
-
/**
|
|
79
|
-
* 查询唯一单条记录(期望结果为 0 条或 1 条)
|
|
80
|
-
*
|
|
81
|
-
* 与 findOne 不同,此方法会校验查询结果的唯一性:
|
|
82
|
-
* - 如果查到 0 条:返回 row = null
|
|
83
|
-
* - 如果查到 1 条:正常返回
|
|
84
|
-
* - 如果查到多条:抛出异常,包含详细的定位信息(含分表名列表)
|
|
85
|
-
*
|
|
86
|
-
* 分表场景的特殊处理:
|
|
87
|
-
* - 单分表:直接查询并校验唯一性
|
|
88
|
-
* - 多分表:顺序查询各分表,累计找到的数量,超过 1 条立即抛异常
|
|
89
|
-
*
|
|
90
|
-
* @param reqJson 请求参数
|
|
91
|
-
* @returns CrudQueryOneResult 包含 row、found 和 getRawContext()
|
|
92
|
-
* @throws Error 如果查询到多条记录
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* // 根据唯一索引查询单条
|
|
96
|
-
* const result = await sharding.findUniqueOne({ condition: { order_id: 'ORD001' } });
|
|
97
|
-
* if (result.found) {
|
|
98
|
-
* console.log(result.row);
|
|
99
|
-
* }
|
|
100
|
-
*/
|
|
101
|
-
findUniqueOne<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryOneResult<T>>;
|
|
102
|
-
findList<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryListResult<T>>;
|
|
103
|
-
findPage<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryPageResult<T>>;
|
|
104
|
-
findCount(reqJson: IRequestModel): Promise<CrudCountResult>;
|
|
105
|
-
isExist(reqJson: IRequestModel): Promise<CrudExistResult>;
|
|
106
|
-
private batchInsertGroupDataByTable;
|
|
107
|
-
private batchInsertResolveTableForData;
|
|
108
|
-
private resolveSingleTable;
|
|
109
|
-
private getShardingColumnHint;
|
|
110
|
-
private buildCfg;
|
|
111
|
-
private executeOnTable;
|
|
112
|
-
private findCountFromTable;
|
|
113
|
-
private isExistInTable;
|
|
114
|
-
private getExistingTablesSet;
|
|
115
|
-
private isTableExists;
|
|
116
|
-
private createShardingTableIfNeeded;
|
|
117
|
-
/**
|
|
118
|
-
* 判断是否为时间分表类型
|
|
119
|
-
*/
|
|
120
|
-
private isTimeSharding;
|
|
121
|
-
/**
|
|
122
|
-
* 校验时间分表插入操作的 data 必须包含 timeColumn
|
|
123
|
-
*
|
|
124
|
-
* 时间分表需要根据时间字段路由到正确的分表,
|
|
125
|
-
* 如果 data 中缺少时间字段,将无法确定数据应插入哪个分表。
|
|
126
|
-
*
|
|
127
|
-
* @param reqJson 请求参数
|
|
128
|
-
* @param operation 操作名称(用于错误提示)
|
|
129
|
-
* @throws Error 如果 data 中缺少时间字段
|
|
130
|
-
*/
|
|
131
|
-
private validateTimeColumnForData;
|
|
132
|
-
/**
|
|
133
|
-
* 校验时间分表批量插入操作的每条 data 必须包含 timeColumn
|
|
134
|
-
*
|
|
135
|
-
* @param dataArray 数据数组
|
|
136
|
-
* @param operation 操作名称(用于错误提示)
|
|
137
|
-
* @throws Error 如果任一条 data 中缺少时间字段
|
|
138
|
-
*/
|
|
139
|
-
private validateTimeColumnForBatchData;
|
|
140
|
-
/**
|
|
141
|
-
* 校验时间分表写操作的 condition 必须包含路由字段(timeColumn)
|
|
142
|
-
*
|
|
143
|
-
* 时间分表需要根据时间字段路由到正确的分表,
|
|
144
|
-
* 如果 condition 中缺少时间字段,将无法确定操作哪个分表。
|
|
145
|
-
*
|
|
146
|
-
* @param reqJson 请求参数
|
|
147
|
-
* @param operation 操作名称(用于错误提示)
|
|
148
|
-
* @throws Error 如果 condition 中缺少时间字段
|
|
149
|
-
*/
|
|
150
|
-
private validateRoutingFieldForCondition;
|
|
151
|
-
/**
|
|
152
|
-
* 智能处理时间分表场景下的时间字段
|
|
153
|
-
*
|
|
154
|
-
* **问题背景**:
|
|
155
|
-
* 在时间分表(YEAR/MONTH/DAY)场景中,timeColumn 既是路由键也是查询条件。
|
|
156
|
-
* 用户传入的时间值可能精度不一致(如传 '2024-01-15' 但数据库存的是毫秒时间戳),
|
|
157
|
-
* 直接作为 WHERE 条件可能导致匹配失败。
|
|
158
|
-
*
|
|
159
|
-
* **处理规则**(详见 TIME_COLUMN_CLEAN_SPEC.md):
|
|
160
|
-
* 1. 操作符表达式($gte/$lte/$range/$in/$null 等)→ 始终保留,不做处理
|
|
161
|
-
* 2. 精确值 + 有 primaryKey → 清理(从 WHERE 移除,仅用于路由)
|
|
162
|
-
* 3. 精确值 + 无 primaryKey + 日期粒度字符串(年/月/日)→ 转换为 $gte/$lte 范围
|
|
163
|
-
* 4. 精确值 + 无 primaryKey + 其他(时间戳/datetime/null等)→ 保留原值
|
|
164
|
-
*
|
|
165
|
-
* **示例**:
|
|
166
|
-
* ```typescript
|
|
167
|
-
* // 有主键 + 精确值 → 清理
|
|
168
|
-
* { order_id: 'ORD001', created_at: '2024-01-15' }
|
|
169
|
-
* → { order_id: 'ORD001' }
|
|
170
|
-
*
|
|
171
|
-
* // 无主键 + 日期粒度字符串 → 转换为范围
|
|
172
|
-
* { status: 'paid', created_at: '2024-01' }
|
|
173
|
-
* → { status: 'paid', created_at: { $gte: '2024-01-01 00:00:00', $lte: '2024-01-31 23:59:59' } }
|
|
174
|
-
*
|
|
175
|
-
* // 操作符表达式 → 保留
|
|
176
|
-
* { order_id: 'ORD001', created_at: { $gte: '2024-01', $lte: '2024-06' } }
|
|
177
|
-
* → 不做任何处理
|
|
178
|
-
* ```
|
|
179
|
-
*
|
|
180
|
-
* @param reqJson 请求参数
|
|
181
|
-
* @returns 处理后的请求参数
|
|
182
|
-
*/
|
|
183
|
-
private cleanTimeColumnForSingleTableQuery;
|
|
184
|
-
/**
|
|
185
|
-
* 校验时间操作符的值必须精确到秒
|
|
186
|
-
*
|
|
187
|
-
* MySQL 中 '2026-04-30' 等价于 '2026-04-30 00:00:00',
|
|
188
|
-
* 导致 $lte: '2026-04-30' 会丢失当天所有数据。
|
|
189
|
-
* 如果用户需要查整月/整天数据,应传粒度字符串由系统自动转换,而非手动写 $gte/$lte。
|
|
190
|
-
*
|
|
191
|
-
* @param operatorValue 操作符表达式的值
|
|
192
|
-
* @param timeColumn 时间字段名
|
|
193
|
-
* @throws Error 如果操作符值缺少时间部分
|
|
194
|
-
*/
|
|
195
|
-
private validateTimeOperatorPrecision;
|
|
196
|
-
private validateFindOrderBy;
|
|
197
|
-
private isAscOrderBy;
|
|
198
|
-
private sortTablesForOrderBy;
|
|
199
|
-
/**
|
|
200
|
-
* 构建辅助定位信息
|
|
201
|
-
*/
|
|
202
|
-
private buildDebugInfo;
|
|
203
|
-
/**
|
|
204
|
-
* 格式化唯一性错误消息
|
|
205
|
-
*/
|
|
206
|
-
private formatUniqueError;
|
|
207
|
-
private resolveFindTables;
|
|
208
|
-
}
|