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
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
CrudCountResult,
|
|
12
12
|
CrudUpsertResult,
|
|
13
13
|
} from '@/libs/crud-pro/models/CrudResult';
|
|
14
|
+
import { CommonException, Exceptions } from "@/libs/crud-pro/exceptions";
|
|
14
15
|
|
|
15
16
|
const DEFAULT_MAX_LIMIT = 10 * 10000;
|
|
17
|
+
const DEFAULT_BATCH_SIZE_FOR_IDS_QUERY = 200;
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* CrudProQuick 构造参数(工厂函数模式)
|
|
@@ -55,15 +57,18 @@ export class CrudProQuick {
|
|
|
55
57
|
private readonly sqlDbType: SqlDbType;
|
|
56
58
|
private readonly sqlDatabase: string;
|
|
57
59
|
private readonly sqlTable?: string;
|
|
60
|
+
|
|
58
61
|
private baseCfgModel: {
|
|
59
62
|
maxLimit?: number;
|
|
63
|
+
batchSizeForIdsQuery?: number;
|
|
60
64
|
enableSoftDelete?: boolean;
|
|
61
65
|
enableStandardUpdateCfg?: boolean;
|
|
62
66
|
enableStandardUpdateCfgCondition?: string[];
|
|
63
67
|
[key: string]: any;
|
|
64
68
|
} = {
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
maxLimit: DEFAULT_MAX_LIMIT,
|
|
70
|
+
batchSizeForIdsQuery: DEFAULT_BATCH_SIZE_FOR_IDS_QUERY,
|
|
71
|
+
};
|
|
67
72
|
|
|
68
73
|
/**
|
|
69
74
|
* 构造函数(工厂函数模式)
|
|
@@ -102,7 +107,7 @@ export class CrudProQuick {
|
|
|
102
107
|
throw new Error('[CrudProQuick] sqlTable not found');
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
|
|
110
|
+
// 创建 CrudPro 实例并设置 visitor
|
|
106
111
|
const crudPro = this.crudProFactory();
|
|
107
112
|
|
|
108
113
|
// 应用软删除动作。
|
|
@@ -118,28 +123,36 @@ export class CrudProQuick {
|
|
|
118
123
|
* @returns CrudQueryOneResult 包含 row、found 和 getRawContext()
|
|
119
124
|
*/
|
|
120
125
|
public async findOne<T = Record<string, any>>(reqJson: IRequestModel, sqlTable?: string): Promise<CrudQueryOneResult<T>> {
|
|
126
|
+
const effectiveTable = sqlTable || this.sqlTable;
|
|
127
|
+
|
|
121
128
|
const cfgModel: IRequestCfgModel = {
|
|
122
|
-
sqlTable,
|
|
129
|
+
sqlTable: effectiveTable,
|
|
123
130
|
sqlSimpleName: KeysOfSimpleSQL.SIMPLE_QUERY_ONE,
|
|
124
131
|
};
|
|
132
|
+
|
|
125
133
|
const ctx = await this.executeCrudByCfg(reqJson, cfgModel);
|
|
126
134
|
const row = ctx.getOneObj() as T | null;
|
|
127
|
-
return new CrudQueryOneResult<T>({
|
|
135
|
+
return new CrudQueryOneResult<T>({
|
|
136
|
+
row,
|
|
137
|
+
rawContext: ctx,
|
|
138
|
+
debugInfo: this.buildDebugInfo(effectiveTable, reqJson.condition),
|
|
139
|
+
fromTable: effectiveTable
|
|
140
|
+
});
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
/**
|
|
131
144
|
* 查询唯一单条记录(期望结果为 0 条或 1 条)
|
|
132
|
-
*
|
|
145
|
+
*
|
|
133
146
|
* 与 findOne 不同,此方法会校验查询结果的唯一性:
|
|
134
147
|
* - 如果查到 0 条:返回 row = null
|
|
135
148
|
* - 如果查到 1 条:正常返回
|
|
136
149
|
* - 如果查到多条:抛出异常,包含详细的定位信息
|
|
137
|
-
*
|
|
150
|
+
*
|
|
138
151
|
* @param reqJson 请求参数
|
|
139
152
|
* @param sqlTable 数据库表名称
|
|
140
153
|
* @returns CrudQueryOneResult 包含 row、found 和 getRawContext()
|
|
141
154
|
* @throws Error 如果查询到多条记录
|
|
142
|
-
*
|
|
155
|
+
*
|
|
143
156
|
* @example
|
|
144
157
|
* // 根据唯一索引查询单条
|
|
145
158
|
* const result = await quick.findUniqueOne({ condition: { email: 'user@example.com' } });
|
|
@@ -149,7 +162,7 @@ export class CrudProQuick {
|
|
|
149
162
|
*/
|
|
150
163
|
public async findUniqueOne<T = Record<string, any>>(reqJson: IRequestModel, sqlTable?: string): Promise<CrudQueryOneResult<T>> {
|
|
151
164
|
const effectiveTable = sqlTable || this.sqlTable;
|
|
152
|
-
|
|
165
|
+
|
|
153
166
|
// 使用 maxLimit: 2 查询,判断是否有多条
|
|
154
167
|
const cfgModel: IRequestCfgModel = {
|
|
155
168
|
sqlTable: effectiveTable,
|
|
@@ -158,17 +171,16 @@ export class CrudProQuick {
|
|
|
158
171
|
};
|
|
159
172
|
const ctx = await this.executeCrudByCfg(reqJson, cfgModel);
|
|
160
173
|
const rows = ctx.getResRows() as T[];
|
|
161
|
-
|
|
174
|
+
|
|
162
175
|
if (rows.length > 1) {
|
|
163
|
-
|
|
164
|
-
const errorMsg = this.formatUniqueError(rows.length, debugInfo);
|
|
165
|
-
throw new Error(errorMsg);
|
|
176
|
+
throw new CommonException(Exceptions.MORE_THAN_ONE_RECORDS_FOUND, '发现了不止一条数据')
|
|
166
177
|
}
|
|
167
|
-
|
|
168
|
-
return new CrudQueryOneResult<T>({
|
|
169
|
-
row: rows[0] || null,
|
|
178
|
+
|
|
179
|
+
return new CrudQueryOneResult<T>({
|
|
180
|
+
row: rows[0] || null,
|
|
170
181
|
rawContext: ctx,
|
|
171
182
|
debugInfo: this.buildDebugInfo(effectiveTable, reqJson.condition),
|
|
183
|
+
fromTable: effectiveTable,
|
|
172
184
|
});
|
|
173
185
|
}
|
|
174
186
|
|
|
@@ -185,32 +197,6 @@ export class CrudProQuick {
|
|
|
185
197
|
}
|
|
186
198
|
return info;
|
|
187
199
|
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* 格式化唯一性错误消息
|
|
191
|
-
*/
|
|
192
|
-
private formatUniqueError(foundCount: number, debugInfo: { sqlDatabase: string; sqlTable: string; condition?: Record<string, any> }): string {
|
|
193
|
-
const parts = [
|
|
194
|
-
`[CrudProQuick] findUniqueOne 期望唯一一条记录,但查询到 ${foundCount} 条`,
|
|
195
|
-
];
|
|
196
|
-
|
|
197
|
-
if (debugInfo.sqlDatabase) {
|
|
198
|
-
parts.push(`数据库: ${debugInfo.sqlDatabase}`);
|
|
199
|
-
}
|
|
200
|
-
if (debugInfo.sqlTable) {
|
|
201
|
-
parts.push(`表: ${debugInfo.sqlTable}`);
|
|
202
|
-
}
|
|
203
|
-
if (debugInfo.condition) {
|
|
204
|
-
try {
|
|
205
|
-
parts.push(`条件: ${JSON.stringify(debugInfo.condition)}`);
|
|
206
|
-
} catch {
|
|
207
|
-
parts.push(`条件: [无法序列化]`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return parts.join(' | ');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
200
|
/**
|
|
215
201
|
* 查询数据列表
|
|
216
202
|
* @param reqJson 请求参数
|
|
@@ -355,6 +341,9 @@ export class CrudProQuick {
|
|
|
355
341
|
|
|
356
342
|
/**
|
|
357
343
|
* 插入或更新数据记录(upsert)
|
|
344
|
+
* 限制:
|
|
345
|
+
* - 必须有condition字段,用于判断记录是否存在
|
|
346
|
+
* - 必须有data字段,用于指定插入/更新的内容
|
|
358
347
|
* 内部使用 SIMPLE_INSERT_OR_UPDATE 模式,若记录已存在则更新,不存在则插入
|
|
359
348
|
* @param reqJson 请求参数,data 为要插入/更新的内容,condition 为判断条件
|
|
360
349
|
* @param sqlTable 数据库表名,若构造实例时已设置全局 sqlTable 则可省略
|
|
@@ -368,7 +357,6 @@ export class CrudProQuick {
|
|
|
368
357
|
const ctx = await this.executeCrudByCfg(reqJson, cfgModel);
|
|
369
358
|
const resModel = ctx.getResModel();
|
|
370
359
|
return new CrudUpsertResult({
|
|
371
|
-
affected: resModel.affected,
|
|
372
360
|
insertAffected: resModel.insert_affected,
|
|
373
361
|
updateAffected: resModel.update_affected,
|
|
374
362
|
isExist: resModel.is_exist ?? false,
|
|
@@ -476,6 +464,98 @@ export class CrudProQuick {
|
|
|
476
464
|
});
|
|
477
465
|
}
|
|
478
466
|
|
|
467
|
+
/**
|
|
468
|
+
* 保存数据记录(自动判断 INSERT 或 UPDATE)
|
|
469
|
+
*
|
|
470
|
+
* 只需传入 data,无需手动指定 condition。方法会自动从表结构中获取主键列,
|
|
471
|
+
* 根据 data 中是否包含主键值来决定操作类型:
|
|
472
|
+
* - data 包含主键且主键值不为 undefined/null → 调用 insertOrUpdate(先查后写,存在则更新,不存在则插入)
|
|
473
|
+
* - data 不包含主键或主键值为 undefined/null → 调用 insert(直接插入)
|
|
474
|
+
*
|
|
475
|
+
* **前置条件**:data 不能为空
|
|
476
|
+
*
|
|
477
|
+
* @param reqJson 请求参数,只需 data 字段
|
|
478
|
+
* @param sqlTable 数据库表名,若构造实例时已设置全局 sqlTable 则可省略
|
|
479
|
+
* @returns CrudUpsertResult 包含 affected、insertAffected、updateAffected、isExist 和 getRawContext()
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* // 有主键 → 先查后写(存在则 UPDATE,不存在则 INSERT)
|
|
483
|
+
* await quick.save({ data: { id: 1, name: '张三' } });
|
|
484
|
+
*
|
|
485
|
+
* // 无主键 → 直接 INSERT
|
|
486
|
+
* await quick.save({ data: { name: '李四' } });
|
|
487
|
+
*
|
|
488
|
+
* // 主键值为 null → 视为无主键,直接 INSERT
|
|
489
|
+
* await quick.save({ data: { id: null, name: '王五' } });
|
|
490
|
+
*/
|
|
491
|
+
public async save(reqJson: IRequestModel, sqlTable?: string): Promise<CrudUpsertResult> {
|
|
492
|
+
if (!reqJson.data || Object.keys(reqJson.data as Record<string, any>).length === 0) {
|
|
493
|
+
throw new Error('[CrudProQuick] save 操作的 data 不能为空。data 用于指定保存的内容。');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const effectiveTable = sqlTable || this.sqlTable;
|
|
497
|
+
if (!effectiveTable) {
|
|
498
|
+
throw new Error('[CrudProQuick] sqlTable not found');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 获取表的主键列
|
|
502
|
+
const primaryKeys = await this.getPrimaryKeyColumns(effectiveTable);
|
|
503
|
+
const data = reqJson.data as Record<string, any>;
|
|
504
|
+
|
|
505
|
+
// 检查 data 中是否包含所有主键且值有效
|
|
506
|
+
const hasPrimaryKeyValue = primaryKeys.length > 0 &&
|
|
507
|
+
primaryKeys.every(pk => pk in data && data[pk] !== undefined && data[pk] !== null);
|
|
508
|
+
|
|
509
|
+
if (hasPrimaryKeyValue) {
|
|
510
|
+
// 从 data 中提取主键作为 condition
|
|
511
|
+
const condition: Record<string, any> = {};
|
|
512
|
+
for (const pk of primaryKeys) {
|
|
513
|
+
condition[pk] = data[pk];
|
|
514
|
+
}
|
|
515
|
+
// 调用 insertOrUpdate(先查后写)
|
|
516
|
+
return this.insertOrUpdate({ ...reqJson, condition }, sqlTable);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const condition = reqJson.condition;
|
|
520
|
+
if (condition && Object.keys(condition).length > 0) {
|
|
521
|
+
return this.insertOrUpdate(reqJson, sqlTable);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// 无主键值 → 直接 INSERT
|
|
525
|
+
const insertResult = await this.insert(reqJson, sqlTable);
|
|
526
|
+
// 将 CrudWriteResult 转换为 CrudUpsertResult
|
|
527
|
+
return new CrudUpsertResult({
|
|
528
|
+
insertAffected: { affectedRows: insertResult.affectedRows, insertId: insertResult.insertId },
|
|
529
|
+
updateAffected: { affectedRows: 0, insertId: null },
|
|
530
|
+
isExist: false,
|
|
531
|
+
rawContext: insertResult.getRawContext(),
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* 获取表的主键列名列表
|
|
537
|
+
*
|
|
538
|
+
* 委托给 CrudPro.getPrimaryKeyColumns,复用 CrudProTableMetaService 的 getTableMeta 缓存,
|
|
539
|
+
* 避免重复查询数据库。
|
|
540
|
+
*
|
|
541
|
+
* @param sqlTable 表名
|
|
542
|
+
* @returns 主键列名数组,如果查询失败返回 ['id'] 作为保守默认值
|
|
543
|
+
*/
|
|
544
|
+
private async getPrimaryKeyColumns(sqlTable: string): Promise<string[]> {
|
|
545
|
+
try {
|
|
546
|
+
const crudPro = this.crudProFactory();
|
|
547
|
+
return await crudPro.getPrimaryKeyColumns({
|
|
548
|
+
sqlTable,
|
|
549
|
+
sqlDatabase: this.sqlDatabase,
|
|
550
|
+
sqlDbType: this.sqlDbType,
|
|
551
|
+
});
|
|
552
|
+
} catch (error) {
|
|
553
|
+
// 查询失败时保守返回 ['id'],避免阻断正常流程
|
|
554
|
+
console.warn('[CrudProQuick] 获取主键列失败,使用默认值 ["id"]:', error);
|
|
555
|
+
return ['id'];
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
479
559
|
/**
|
|
480
560
|
* 根据主键 ID 查询单条记录
|
|
481
561
|
*
|
|
@@ -503,9 +583,14 @@ export class CrudProQuick {
|
|
|
503
583
|
/**
|
|
504
584
|
* 根据主键 ID 列表查询多条记录
|
|
505
585
|
*
|
|
506
|
-
*
|
|
586
|
+
* 实现逻辑:
|
|
587
|
+
* 1. 先对 ids 去重,避免重复 ID 导致无效查询和重复结果
|
|
588
|
+
* 2. 若去重后为空,直接返回空结果
|
|
589
|
+
* 3. 若 ID 数量不超过批次大小,单次 findList 查询完成
|
|
590
|
+
* 4. 若 ID 数量超过批次大小,自动分批查询(每批默认 200 个 ID),
|
|
591
|
+
* 避免单次 IN 列表过大导致执行计划退化和内存尖峰,最后合并结果返回
|
|
507
592
|
*
|
|
508
|
-
* @param ids
|
|
593
|
+
* @param ids 主键值数组(内部会自动去重)
|
|
509
594
|
* @param sqlTable 数据库表名,若构造实例时已设置全局 sqlTable 则可省略
|
|
510
595
|
* @returns CrudQueryListResult 包含 rows、count 和 getRawContext()
|
|
511
596
|
*
|
|
@@ -518,7 +603,32 @@ export class CrudProQuick {
|
|
|
518
603
|
* const result = await quick.findListByIds(['ORD001', 'ORD002'], 't_order');
|
|
519
604
|
*/
|
|
520
605
|
public async findListByIds<T = Record<string, any>>(ids: (number | string)[], sqlTable?: string): Promise<CrudQueryListResult<T>> {
|
|
521
|
-
|
|
606
|
+
// 去重:避免重复 ID 导致无效查询和重复结果
|
|
607
|
+
const uniqueIds = [...new Set(ids)];
|
|
608
|
+
|
|
609
|
+
const batchSize = this.baseCfgModel.batchSizeForIdsQuery || DEFAULT_BATCH_SIZE_FOR_IDS_QUERY;
|
|
610
|
+
if (uniqueIds.length === 0) {
|
|
611
|
+
return new CrudQueryListResult<T>({ rows: [], rawContext: null });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (uniqueIds.length <= batchSize) {
|
|
615
|
+
return this.findList<T>({ condition: { id: { $in: uniqueIds } } }, sqlTable);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const allRows: T[] = [];
|
|
619
|
+
let lastRawContext: any = null;
|
|
620
|
+
|
|
621
|
+
for (let i = 0; i < uniqueIds.length; i += batchSize) {
|
|
622
|
+
const batchIds = uniqueIds.slice(i, i + batchSize);
|
|
623
|
+
const batchResult = await this.findList<T>(
|
|
624
|
+
{ condition: { id: { $in: batchIds } } },
|
|
625
|
+
sqlTable
|
|
626
|
+
);
|
|
627
|
+
allRows.push(...batchResult.rows);
|
|
628
|
+
lastRawContext = batchResult.getRawContext();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return new CrudQueryListResult<T>({ rows: allRows, rawContext: lastRawContext });
|
|
522
632
|
}
|
|
523
633
|
|
|
524
634
|
/**
|
|
@@ -591,4 +701,82 @@ export class CrudProQuick {
|
|
|
591
701
|
return await this.crudProFactory().executeSQL(sqlCfgModel) as ExecuteSQLResult<T>;
|
|
592
702
|
}
|
|
593
703
|
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* findOne 的快捷方式,直接返回 row
|
|
710
|
+
*
|
|
711
|
+
* @example
|
|
712
|
+
* const row = await quick.getOne({ condition: { id: 1 } });
|
|
713
|
+
* // 等价于 (await quick.findOne({ condition: { id: 1 } })).row
|
|
714
|
+
*/
|
|
715
|
+
public async getOne(reqJson: IRequestModel, sqlTable?: string): Promise<Record<string, any>> {
|
|
716
|
+
const res = await this.findOne(reqJson, sqlTable);
|
|
717
|
+
return res.row;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* findUniqueOne 的快捷方式,直接返回 row
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* const row = await quick.getUniqueOne({ condition: { order_id: 'ORD001' } });
|
|
725
|
+
* // 等价于 (await quick.findUniqueOne({ condition: { order_id: 'ORD001' } })).row
|
|
726
|
+
*/
|
|
727
|
+
public async getUniqueOne(reqJson: IRequestModel, sqlTable?: string): Promise<Record<string, any>> {
|
|
728
|
+
const res = await this.findUniqueOne(reqJson, sqlTable);
|
|
729
|
+
return res.row;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* findList 的快捷方式,直接返回 rows
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* const rows = await quick.getList({ condition: { status: 'active' } });
|
|
737
|
+
* // 等价于 (await quick.findList({ condition: { status: 'active' } })).rows
|
|
738
|
+
*/
|
|
739
|
+
public async getList(reqJson: IRequestModel, sqlTable?: string): Promise<Record<string, any>[]> {
|
|
740
|
+
const res = await this.findList(reqJson, sqlTable);
|
|
741
|
+
return res.rows;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* findCount 的快捷方式,直接返回 count
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* const count = await quick.getCount({ condition: { status: 'active' } });
|
|
749
|
+
* // 等价于 (await quick.findCount({ condition: { status: 'active' } })).count
|
|
750
|
+
*/
|
|
751
|
+
public async getCount(reqJson: IRequestModel, sqlTable?: string): Promise<number> {
|
|
752
|
+
const res = await this.findCount(reqJson, sqlTable);
|
|
753
|
+
return res.count;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* isExist 的快捷方式,直接返回 exists
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* const exists = await quick.getIsExist({ condition: { id: 1 } });
|
|
761
|
+
* // 等价于 (await quick.isExist({ condition: { id: 1 } })).exists
|
|
762
|
+
*/
|
|
763
|
+
public async getIsExist(reqJson: IRequestModel, sqlTable?: string): Promise<boolean> {
|
|
764
|
+
const res = await this.isExist(reqJson, sqlTable);
|
|
765
|
+
return res.exists;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* findOneById 的快捷方式,直接返回 row
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* const row = await quick.getOneById(1);
|
|
773
|
+
* // 等价于 (await quick.findOneById(1)).row
|
|
774
|
+
*/
|
|
775
|
+
public async getOneById(id: number | string, sqlTable?: string): Promise<Record<string, any>> {
|
|
776
|
+
const res = await this.findOneById(id, sqlTable);
|
|
777
|
+
return res.row;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
|
|
594
782
|
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { IRequestModel, IRequestCfgModel } from '@/libs/crud-pro/interfaces';
|
|
2
|
+
import { ExecuteContext } from '@/libs/crud-pro/models/ExecuteContext';
|
|
3
|
+
import { KeysOfSimpleSQL } from '@/libs/crud-pro/models/keys';
|
|
4
|
+
import { IShardingConfig, IShardingRouterContext, CrudProFactory } from './ShardingConfig';
|
|
5
|
+
import { fixSoftDelete } from '@/libs/crud-pro-quick/fixSoftDelete';
|
|
6
|
+
import { ShardingTableCreator } from './ShardingTableCreator';
|
|
7
|
+
import {
|
|
8
|
+
CrudWriteResult,
|
|
9
|
+
CrudQueryOneResult,
|
|
10
|
+
CrudQueryListResult,
|
|
11
|
+
CrudQueryPageResult,
|
|
12
|
+
CrudExistResult,
|
|
13
|
+
CrudCountResult,
|
|
14
|
+
CrudUpsertResult,
|
|
15
|
+
} from '@/libs/crud-pro/models/CrudResult';
|
|
16
|
+
import { ShardingBatchInsertResult } from './ShardingResult';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 分表 CRUD 基类
|
|
20
|
+
*
|
|
21
|
+
* 提供所有分表子类共用的基础设施方法,不包含任何路由逻辑。
|
|
22
|
+
* 路由逻辑由各子类自行实现。
|
|
23
|
+
*
|
|
24
|
+
* 子类:
|
|
25
|
+
* - ShardingByTimeCrud: 时间分表(YEAR/MONTH/DAY)
|
|
26
|
+
* - ShardingByHashCrud: 哈希分表
|
|
27
|
+
* - ShardingByKeyCrud: 键值分表
|
|
28
|
+
* - ShardingByCustomCrud: 自定义分表
|
|
29
|
+
*/
|
|
30
|
+
export abstract class ShardingBase {
|
|
31
|
+
protected readonly crudProFactory: CrudProFactory;
|
|
32
|
+
protected readonly config: IShardingConfig;
|
|
33
|
+
protected readonly tableCreator: ShardingTableCreator;
|
|
34
|
+
|
|
35
|
+
protected baseCfg: Partial<IRequestCfgModel> = {};
|
|
36
|
+
protected enableSoftDelete: boolean = false;
|
|
37
|
+
|
|
38
|
+
constructor(crudProFactory: CrudProFactory, config: IShardingConfig) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
|
|
41
|
+
if (typeof crudProFactory === 'function') {
|
|
42
|
+
this.crudProFactory = crudProFactory;
|
|
43
|
+
} else {
|
|
44
|
+
throw new Error('[ShardingBase] 请使用 CrudProFactory 工厂函数模式');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.tableCreator = new ShardingTableCreator(this.crudProFactory, config);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============ 配置方法 ============
|
|
51
|
+
|
|
52
|
+
public setBaseCfg(cfg: Partial<IRequestCfgModel>): this {
|
|
53
|
+
this.baseCfg = { ...this.baseCfg, ...cfg };
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public getConfig(): IShardingConfig {
|
|
58
|
+
return this.config;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public setEnableSoftDelete(enable: boolean): this {
|
|
62
|
+
this.enableSoftDelete = enable;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============ 抽象 CRUD 方法(子类必须实现) ============
|
|
67
|
+
|
|
68
|
+
public abstract insert(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
69
|
+
public abstract batchInsert(reqJson: IRequestModel): Promise<ShardingBatchInsertResult>;
|
|
70
|
+
public abstract update(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
71
|
+
public abstract delete(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
72
|
+
public abstract restore(reqJson: IRequestModel): Promise<CrudWriteResult>;
|
|
73
|
+
public abstract insertOrUpdate(reqJson: IRequestModel): Promise<CrudUpsertResult>;
|
|
74
|
+
public abstract findOne<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryOneResult<T>>;
|
|
75
|
+
public abstract findUniqueOne<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryOneResult<T>>;
|
|
76
|
+
public abstract findList<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryListResult<T>>;
|
|
77
|
+
public abstract findPage<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryPageResult<T>>;
|
|
78
|
+
public abstract findCount(reqJson: IRequestModel): Promise<CrudCountResult>;
|
|
79
|
+
public abstract isExist(reqJson: IRequestModel): Promise<CrudExistResult>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 判断当前路由上下文是否处于「插入阶段」(应从 data 提取分表字段)
|
|
83
|
+
*
|
|
84
|
+
* - INSERT / BATCH_INSERT → 始终从 data
|
|
85
|
+
* - 其余操作(QUERY / UPDATE / DELETE / INSERT_OR_UPDATE)→ 从 condition
|
|
86
|
+
*/
|
|
87
|
+
protected static isInsertPhase(ctx: IShardingRouterContext): boolean {
|
|
88
|
+
return [KeysOfSimpleSQL.SIMPLE_INSERT, KeysOfSimpleSQL.SIMPLE_BATCH_INSERT].includes(ctx.sqlSimpleName);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============ 执行基础设施 ============
|
|
92
|
+
|
|
93
|
+
protected buildCfg(sqlSimpleName: KeysOfSimpleSQL): IRequestCfgModel & { enableSoftDelete?: boolean } {
|
|
94
|
+
return {
|
|
95
|
+
method: `ShardingCrudProAnonymous_${sqlSimpleName}`,
|
|
96
|
+
...this.baseCfg,
|
|
97
|
+
sqlTable: this.config.baseTable,
|
|
98
|
+
sqlSimpleName,
|
|
99
|
+
} as IRequestCfgModel & { enableSoftDelete?: boolean };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected async executeOnTable(
|
|
103
|
+
table: string,
|
|
104
|
+
reqJson: IRequestModel,
|
|
105
|
+
sqlSimpleName: KeysOfSimpleSQL,
|
|
106
|
+
extraCfg?: Partial<IRequestCfgModel>
|
|
107
|
+
): Promise<ExecuteContext> {
|
|
108
|
+
const cfg = this.buildCfg(sqlSimpleName);
|
|
109
|
+
cfg.sqlTable = table;
|
|
110
|
+
cfg.enableSoftDelete = this.enableSoftDelete;
|
|
111
|
+
|
|
112
|
+
if (extraCfg) {
|
|
113
|
+
Object.assign(cfg, extraCfg);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const crudPro = this.crudProFactory();
|
|
117
|
+
fixSoftDelete(sqlSimpleName, cfg as any, reqJson, crudPro.getVisitor());
|
|
118
|
+
return crudPro.executeCrudByCfg(reqJson, cfg);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============ 单表查询辅助 ============
|
|
122
|
+
|
|
123
|
+
protected async findCountFromTable(table: string, reqJson: IRequestModel): Promise<{ count: number; ctx: ExecuteContext | null }> {
|
|
124
|
+
try {
|
|
125
|
+
const ctx = await this.executeOnTable(table, reqJson, KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
|
|
126
|
+
return { count: ctx.getResModelItem('total_count') || 0, ctx };
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return { count: 0, ctx: null };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected async isExistInTable(table: string, reqJson: IRequestModel): Promise<{ exists: boolean; ctx: ExecuteContext | null }> {
|
|
133
|
+
try {
|
|
134
|
+
const ctx = await this.executeOnTable(table, reqJson, KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
|
|
135
|
+
return { exists: ctx.getResModelItem('is_exist') === true, ctx };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
return { exists: false, ctx: null };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ============ 表管理 ============
|
|
142
|
+
|
|
143
|
+
protected async getExistingTablesSet(skipCache = false): Promise<Set<string>> {
|
|
144
|
+
const { sqlDatabase, sqlDbType } = this.baseCfg;
|
|
145
|
+
|
|
146
|
+
if (!sqlDatabase || !sqlDbType) {
|
|
147
|
+
throw new Error('[ShardingBase] 未配置 sqlDatabase 或 sqlDbType');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { tables } = await this.crudProFactory().getAllTableInfos(
|
|
151
|
+
{ sqlDatabase, sqlDbType: sqlDbType as any },
|
|
152
|
+
{ skipCache }
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return new Set(tables.map(t => t.name));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
protected async isTableExists(tableName: string): Promise<boolean> {
|
|
159
|
+
const existingSet = await this.getExistingTablesSet();
|
|
160
|
+
return existingSet.has(tableName);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
protected async createShardingTableIfNeeded(tableName: string): Promise<void> {
|
|
164
|
+
if (!this.config.autoCreateTable) {
|
|
165
|
+
if (!(await this.isTableExists(tableName))) {
|
|
166
|
+
throw new Error(`[ShardingBase] 分表 ${tableName} 不存在。请先创建分表,或设置 autoCreateTable: true 自动创建`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!this.baseCfg.sqlDatabase || !this.baseCfg.sqlDbType) {
|
|
172
|
+
throw new Error('[ShardingBase] 请先调用 setBaseCfg 设置数据库配置');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = await this.tableCreator.createTableIfNeeded(
|
|
176
|
+
tableName,
|
|
177
|
+
{
|
|
178
|
+
sqlDatabase: this.baseCfg.sqlDatabase,
|
|
179
|
+
sqlDbType: this.baseCfg.sqlDbType,
|
|
180
|
+
},
|
|
181
|
+
this.config.tableCreateOptions
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
if (!result.success) {
|
|
185
|
+
throw result.error || new Error(`[ShardingBase] 创建分表 ${tableName} 失败`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (result.createSql) {
|
|
189
|
+
await this.getExistingTablesSet(true);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============ 辅助方法 ============
|
|
194
|
+
|
|
195
|
+
protected buildDebugInfo(sqlTable: string | undefined, condition: Record<string, any> | undefined): { sqlDatabase: string; sqlTable: string; condition?: Record<string, any> } {
|
|
196
|
+
const info: { sqlDatabase: string; sqlTable: string; condition?: Record<string, any> } = {
|
|
197
|
+
sqlDatabase: this.baseCfg.sqlDatabase || 'unknown',
|
|
198
|
+
sqlTable: sqlTable || this.config.baseTable,
|
|
199
|
+
};
|
|
200
|
+
if (condition) {
|
|
201
|
+
info.condition = condition;
|
|
202
|
+
}
|
|
203
|
+
return info;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected formatUniqueError(
|
|
207
|
+
foundCount: number,
|
|
208
|
+
tables: string | string[],
|
|
209
|
+
condition: Record<string, any> | undefined,
|
|
210
|
+
isMultiTable: boolean = false
|
|
211
|
+
): string {
|
|
212
|
+
const parts = [
|
|
213
|
+
`[ShardingBase] findUniqueOne 期望唯一一条记录,但查询到 ${foundCount} 条`,
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
if (this.baseCfg.sqlDatabase) {
|
|
217
|
+
parts.push(`数据库: ${this.baseCfg.sqlDatabase}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isMultiTable && Array.isArray(tables)) {
|
|
221
|
+
parts.push(`基表: ${this.config.baseTable}`);
|
|
222
|
+
parts.push(`分表: [${tables.join(', ')}]`);
|
|
223
|
+
} else {
|
|
224
|
+
parts.push(`表: ${typeof tables === 'string' ? tables : tables[0]}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (condition) {
|
|
228
|
+
try {
|
|
229
|
+
parts.push(`条件: ${JSON.stringify(condition)}`);
|
|
230
|
+
} catch {
|
|
231
|
+
parts.push(`条件: [无法序列化]`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return parts.join(' | ');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 获取所有以 baseTable_ 开头的已存在分表(按字典序排序)
|
|
240
|
+
*/
|
|
241
|
+
protected async getAllExistingShardingTables(): Promise<string[]> {
|
|
242
|
+
const existingTables = await this.getExistingTablesSet();
|
|
243
|
+
const prefix = `${this.config.baseTable}_`;
|
|
244
|
+
const tables = Array.from(existingTables).filter(t => t.startsWith(prefix));
|
|
245
|
+
tables.sort();
|
|
246
|
+
return tables;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 过滤出真实存在的分表
|
|
251
|
+
*/
|
|
252
|
+
protected async filterExistingTables(candidateTables: string[]): Promise<string[]> {
|
|
253
|
+
const existingTables = await this.getExistingTablesSet();
|
|
254
|
+
return candidateTables.filter(table => existingTables.has(table));
|
|
255
|
+
}
|
|
256
|
+
}
|