midway-fatcms 0.0.6 → 0.0.8
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/01-quick-start.md +231 -0
- package/.qoder/skills/midway-fatcms/02-crud-quick.md +337 -0
- package/.qoder/skills/midway-fatcms/03-crud-sharding.md +488 -0
- package/.qoder/skills/midway-fatcms/04-condition-operators.md +93 -0
- package/.qoder/skills/midway-fatcms/05-configuration.md +290 -0
- package/.qoder/skills/midway-fatcms/06-builtin-functions.md +241 -0
- package/.qoder/skills/midway-fatcms/07-examples.md +500 -0
- package/.qoder/skills/midway-fatcms/SKILL.md +96 -0
- package/README.md +9 -9
- package/dist/controller/base/BaseApiController.d.ts +1 -2
- package/dist/controller/base/BaseApiController.js +0 -4
- package/dist/controller/gateway/DocGatewayController.js +1 -1
- package/dist/controller/manage/FlowConfigManageApi.js +4 -2
- package/dist/controller/manage/SysConfigMangeApi.js +6 -1
- package/dist/controller/manage/UserAccountManageApi.js +7 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/libs/crud-pro/CrudPro.d.ts +23 -2
- package/dist/libs/crud-pro/CrudPro.js +53 -2
- package/dist/libs/crud-pro/interfaces.d.ts +82 -12
- package/dist/libs/crud-pro/models/CrudResult.d.ts +115 -0
- package/dist/libs/crud-pro/models/CrudResult.js +126 -0
- package/dist/libs/crud-pro/models/RequestModel.d.ts +2 -38
- package/dist/libs/crud-pro/models/RequestModel.js +2 -99
- package/dist/libs/crud-pro/services/CrudProExecuteSqlService.js +36 -2
- package/dist/libs/crud-pro/services/CrudProGenSqlCondition.js +8 -4
- package/dist/libs/crud-pro/services/CrudProTableMetaService.js +1 -2
- package/dist/libs/crud-pro/utils/OrderByUtils.d.ts +70 -0
- package/dist/libs/crud-pro/utils/OrderByUtils.js +158 -0
- package/dist/libs/crud-pro-quick/CrudProQuick.d.ts +295 -0
- package/dist/libs/crud-pro-quick/CrudProQuick.js +529 -0
- package/dist/libs/crud-pro-quick/fixSoftDelete.d.ts +30 -0
- package/dist/{service/curd → libs/crud-pro-quick}/fixSoftDelete.js +3 -6
- package/dist/libs/crud-pro-quick/index.d.ts +36 -0
- package/dist/libs/crud-pro-quick/index.js +49 -0
- package/dist/libs/crud-pro-quick/models.d.ts +33 -0
- package/dist/libs/crud-pro-quick/models.js +2 -0
- package/dist/libs/crud-sharding/ShardingConfig.d.ts +15 -2
- package/dist/libs/crud-sharding/ShardingConfig.js +2 -2
- package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +119 -274
- package/dist/libs/crud-sharding/ShardingCrudPro.js +559 -379
- package/dist/libs/crud-sharding/ShardingMerger.d.ts +12 -20
- package/dist/libs/crud-sharding/ShardingMerger.js +36 -51
- package/dist/libs/crud-sharding/ShardingResult.d.ts +33 -0
- package/dist/libs/crud-sharding/ShardingResult.js +16 -0
- package/dist/libs/crud-sharding/ShardingRouter.d.ts +1 -0
- package/dist/libs/crud-sharding/ShardingRouter.js +25 -6
- package/dist/libs/crud-sharding/ShardingTableCreator.d.ts +21 -4
- package/dist/libs/crud-sharding/ShardingTableCreator.js +193 -59
- package/dist/libs/crud-sharding/ShardingUtils.d.ts +48 -0
- package/dist/libs/crud-sharding/ShardingUtils.js +122 -1
- package/dist/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +488 -0
- package/dist/libs/crud-sharding/index.d.ts +4 -3
- package/dist/libs/crud-sharding/index.js +14 -2
- package/dist/models/bizmodels.d.ts +2 -6
- package/dist/service/SysAppService.d.ts +2 -2
- package/dist/service/SysAppService.js +16 -5
- package/dist/service/SysConfigService.d.ts +1 -1
- package/dist/service/SysConfigService.js +7 -2
- package/dist/service/SysDictDataService.js +14 -4
- package/dist/service/SysMenuService.js +7 -2
- package/dist/service/curd/CurdMixService.d.ts +6 -4
- package/dist/service/curd/CurdMixService.js +16 -2
- package/dist/service/curd/CurdProService.d.ts +43 -27
- package/dist/service/curd/CurdProService.js +32 -33
- package/dist/service/flow/FlowConfigService.js +7 -2
- package/dist/service/flow/FlowInstanceCrudService.js +22 -19
- package/package.json +2 -1
- package/src/controller/base/BaseApiController.ts +0 -5
- package/src/controller/gateway/DocGatewayController.ts +1 -1
- package/src/controller/manage/CrudStandardDesignApi.ts +4 -3
- package/src/controller/manage/FlowConfigManageApi.ts +4 -2
- package/src/controller/manage/SysConfigMangeApi.ts +6 -1
- package/src/controller/manage/UserAccountManageApi.ts +7 -2
- package/src/index.ts +2 -2
- package/src/libs/crud-pro/CrudPro.ts +62 -4
- package/src/libs/crud-pro/interfaces.ts +110 -15
- package/src/libs/crud-pro/models/CrudResult.ts +178 -0
- package/src/libs/crud-pro/models/RequestModel.ts +4 -110
- package/src/libs/crud-pro/services/CrudProExecuteSqlService.ts +41 -2
- package/src/libs/crud-pro/services/CrudProGenSqlCondition.ts +11 -7
- package/src/libs/crud-pro/services/CrudProTableMetaService.ts +1 -2
- package/src/libs/crud-pro/utils/OrderByUtils.ts +169 -0
- package/src/libs/crud-pro-quick/CrudProQuick.ts +594 -0
- package/src/{service/curd → libs/crud-pro-quick}/fixSoftDelete.ts +23 -13
- package/src/libs/crud-pro-quick/index.ts +52 -0
- package/src/libs/crud-pro-quick/models.ts +35 -0
- package/src/libs/crud-sharding/ShardingConfig.ts +18 -2
- package/src/libs/crud-sharding/ShardingCrudPro.ts +689 -440
- package/src/libs/crud-sharding/ShardingMerger.ts +47 -73
- package/src/libs/crud-sharding/ShardingResult.ts +29 -0
- package/src/libs/crud-sharding/ShardingRouter.ts +27 -6
- package/src/libs/crud-sharding/ShardingTableCreator.ts +214 -71
- package/src/libs/crud-sharding/ShardingUtils.ts +137 -0
- package/src/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +488 -0
- package/src/libs/crud-sharding/index.ts +14 -3
- package/src/models/bizmodels.ts +4 -7
- package/src/service/SysAppService.ts +18 -7
- package/src/service/SysConfigService.ts +8 -3
- package/src/service/SysDictDataService.ts +14 -4
- package/src/service/SysMenuService.ts +7 -2
- package/src/service/crudstd/CrudStdService.ts +2 -2
- package/src/service/curd/CurdMixService.ts +26 -5
- package/src/service/curd/CurdProService.ts +58 -39
- package/src/service/flow/FlowConfigService.ts +7 -2
- package/src/service/flow/FlowInstanceCrudService.ts +23 -20
- package/dist/libs/crud-pro/README.md +0 -809
- package/dist/libs/crud-pro/README_FUNC.md +0 -193
- package/dist/libs/crud-sharding/ROUTING_LOGIC.md +0 -944
- package/dist/models/StandardColumns.d.ts +0 -71
- package/dist/models/StandardColumns.js +0 -28
- package/dist/service/curd/CrudProQuick.d.ts +0 -190
- package/dist/service/curd/CrudProQuick.js +0 -319
- package/dist/service/curd/README.md +0 -1001
- package/dist/service/curd/fixSoftDelete.d.ts +0 -20
- package/src/libs/crud-pro/README.md +0 -809
- package/src/libs/crud-pro/README_FUNC.md +0 -193
- package/src/libs/crud-sharding/ROUTING_LOGIC.md +0 -944
- package/src/models/StandardColumns.ts +0 -76
- package/src/service/curd/CrudProQuick.ts +0 -360
- package/src/service/curd/README.md +0 -1001
|
@@ -1,39 +1,32 @@
|
|
|
1
|
-
import { CrudPro } from '@/libs/crud-pro/CrudPro';
|
|
2
1
|
import { IRequestModel, IRequestCfgModel } from '@/libs/crud-pro/interfaces';
|
|
2
|
+
import { OrderByUtils } from '@/libs/crud-pro/utils/OrderByUtils';
|
|
3
3
|
import { ExecuteContext } from '@/libs/crud-pro/models/ExecuteContext';
|
|
4
4
|
import { KeysOfSimpleSQL } from '@/libs/crud-pro/models/keys';
|
|
5
|
-
import { ShardingType, IShardingConfig, IShardingRouterContext } from './ShardingConfig';
|
|
5
|
+
import { ShardingType, IShardingConfig, IShardingRouterContext, CrudProFactory } from './ShardingConfig';
|
|
6
|
+
import { fixSoftDelete } from '@/libs/crud-pro-quick/fixSoftDelete';
|
|
6
7
|
import { ShardingRouter, ITableInfoProvider } from './ShardingRouter';
|
|
7
|
-
import { ShardingMerger
|
|
8
|
+
import { ShardingMerger } from './ShardingMerger';
|
|
8
9
|
import { ShardingTableCreator } from './ShardingTableCreator';
|
|
9
10
|
import { ShardingCountCache } from './ShardingCountCache';
|
|
11
|
+
import {
|
|
12
|
+
CrudWriteResult,
|
|
13
|
+
CrudQueryOneResult,
|
|
14
|
+
CrudQueryListResult,
|
|
15
|
+
CrudQueryPageResult,
|
|
16
|
+
CrudExistResult,
|
|
17
|
+
CrudCountResult,
|
|
18
|
+
CrudUpsertResult,
|
|
19
|
+
} from '@/libs/crud-pro/models/CrudResult';
|
|
20
|
+
import { ShardingBatchInsertResult } from './ShardingResult';
|
|
21
|
+
import { isOperatorExpression, expandDateToRange } from './ShardingUtils';
|
|
10
22
|
|
|
11
|
-
/**
|
|
12
|
-
* 智能批量插入结果
|
|
13
|
-
*/
|
|
14
|
-
export interface IShardingSmartBatchInsertResult {
|
|
15
|
-
/** 总插入条数 */
|
|
16
|
-
totalAffected: number;
|
|
17
|
-
/** 各分表插入结果 */
|
|
18
|
-
tableResults: Array<{
|
|
19
|
-
table: string;
|
|
20
|
-
affected: number;
|
|
21
|
-
rowCount: number;
|
|
22
|
-
}>;
|
|
23
|
-
/** 涉及的分表数量 */
|
|
24
|
-
tableCount: number;
|
|
25
|
-
/** 是否全部成功 */
|
|
26
|
-
success: boolean;
|
|
27
|
-
/** 错误信息(如果有) */
|
|
28
|
-
errors: Array<{ table: string; error: Error }>;
|
|
29
|
-
}
|
|
30
23
|
|
|
31
24
|
/**
|
|
32
25
|
* 分表 CRUD 操作封装器
|
|
33
|
-
*
|
|
26
|
+
*
|
|
34
27
|
* 在 CrudPro 之上封装分表功能,不修改 CrudPro 内部代码。
|
|
35
28
|
* 提供透明的分表路由和多表查询结果合并能力。
|
|
36
|
-
*
|
|
29
|
+
*
|
|
37
30
|
* 支持的分表策略:
|
|
38
31
|
* - YEAR: 按年分表,如 order_2024, order_2025
|
|
39
32
|
* - MONTH: 按月分表,如 order_202401, order_202402
|
|
@@ -41,7 +34,7 @@ export interface IShardingSmartBatchInsertResult {
|
|
|
41
34
|
* - RANGE: 按范围分表,如 user_0 ~ user_99
|
|
42
35
|
* - HASH: 按哈希分表,如 order_01 ~ order_16
|
|
43
36
|
* - CUSTOM: 自定义分表规则
|
|
44
|
-
*
|
|
37
|
+
*
|
|
45
38
|
* @example
|
|
46
39
|
* // 按月分表
|
|
47
40
|
* const sharding = new ShardingCrudPro(crudPro, {
|
|
@@ -49,30 +42,37 @@ export interface IShardingSmartBatchInsertResult {
|
|
|
49
42
|
* baseTable: 't_order',
|
|
50
43
|
* timeColumn: 'created_at',
|
|
51
44
|
* });
|
|
52
|
-
*
|
|
45
|
+
*
|
|
53
46
|
* // 插入数据(自动路由到正确分表)
|
|
54
47
|
* await sharding.insert({ data: { order_id: '001', amount: 100, created_at: '2024-03-15' } });
|
|
55
|
-
*
|
|
48
|
+
*
|
|
56
49
|
* // 分页查询(自动合并多表结果)
|
|
57
|
-
* const result = await sharding.
|
|
50
|
+
* const result = await sharding.findPage({
|
|
58
51
|
* condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
|
|
59
52
|
* pageNo: 1,
|
|
60
53
|
* pageSize: 10,
|
|
61
54
|
* });
|
|
62
55
|
*/
|
|
63
56
|
export class ShardingCrudPro {
|
|
64
|
-
private readonly
|
|
57
|
+
private readonly crudProFactory: CrudProFactory;
|
|
65
58
|
private readonly config: IShardingConfig;
|
|
66
59
|
private readonly router: ShardingRouter;
|
|
67
60
|
private readonly merger: ShardingMerger;
|
|
68
61
|
private readonly tableCreator: ShardingTableCreator;
|
|
69
62
|
|
|
70
63
|
private baseCfg: Partial<IRequestCfgModel> = {};
|
|
64
|
+
private enableSoftDelete: boolean = false;
|
|
71
65
|
|
|
72
|
-
constructor(
|
|
73
|
-
this.crudPro = crudPro;
|
|
66
|
+
constructor(crudProFactory: CrudProFactory, config: IShardingConfig) {
|
|
74
67
|
this.config = config;
|
|
75
68
|
|
|
69
|
+
// 兼容旧的 CrudPro 实例方式和新的工厂函数方式
|
|
70
|
+
if (typeof crudProFactory === 'function') {
|
|
71
|
+
this.crudProFactory = crudProFactory;
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error('[ShardingCrudPro] 请使用 CrudProFactory 工厂函数模式');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
76
|
// 时间分表必须显式指定 timeColumn
|
|
77
77
|
if ([ShardingType.YEAR, ShardingType.MONTH, ShardingType.DAY].includes(config.type)) {
|
|
78
78
|
if (!config.timeColumn) {
|
|
@@ -82,7 +82,7 @@ export class ShardingCrudPro {
|
|
|
82
82
|
|
|
83
83
|
this.router = new ShardingRouter();
|
|
84
84
|
this.merger = new ShardingMerger();
|
|
85
|
-
this.tableCreator = new ShardingTableCreator(
|
|
85
|
+
this.tableCreator = new ShardingTableCreator(this.crudProFactory, config);
|
|
86
86
|
|
|
87
87
|
// 初始化 COUNT 缓存(如果配置了)
|
|
88
88
|
if (config.countCache) {
|
|
@@ -98,109 +98,54 @@ export class ShardingCrudPro {
|
|
|
98
98
|
|
|
99
99
|
// ============ 配置方法 ============
|
|
100
100
|
|
|
101
|
-
/**
|
|
102
|
-
* 设置基础配置(会应用到所有操作)
|
|
103
|
-
*
|
|
104
|
-
* @param cfg 配置项,如 sqlDatabase、sqlDbType 等
|
|
105
|
-
* @returns this,支持链式调用
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* sharding.setBaseCfg({
|
|
109
|
-
* sqlDatabase: 'mydb',
|
|
110
|
-
* sqlDbType: SqlDbType.mysql,
|
|
111
|
-
* });
|
|
112
|
-
*/
|
|
113
101
|
public setBaseCfg(cfg: Partial<IRequestCfgModel>): this {
|
|
114
102
|
this.baseCfg = { ...this.baseCfg, ...cfg };
|
|
115
103
|
return this;
|
|
116
104
|
}
|
|
117
105
|
|
|
118
|
-
/**
|
|
119
|
-
* 获取分表配置
|
|
120
|
-
*/
|
|
121
106
|
public getConfig(): IShardingConfig {
|
|
122
107
|
return this.config;
|
|
123
108
|
}
|
|
124
109
|
|
|
125
|
-
// ============ 写操作(单表路由) ============
|
|
126
|
-
|
|
127
110
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* 根据分表规则自动路由到目标分表。
|
|
131
|
-
* 对于时间分表,需要确保 data 中包含时间字段。
|
|
132
|
-
* 对于哈希/范围分表,需要确保 data 中包含分表字段。
|
|
133
|
-
*
|
|
134
|
-
* 如果目标分表不存在且配置了 autoCreateTable,会自动创建分表。
|
|
135
|
-
*
|
|
136
|
-
* @param reqJson 请求参数,data 为要插入的数据
|
|
137
|
-
* @returns 执行上下文
|
|
138
|
-
*
|
|
139
|
-
* @example
|
|
140
|
-
* await sharding.insert({
|
|
141
|
-
* data: {
|
|
142
|
-
* order_id: 'ORD001',
|
|
143
|
-
* amount: 100,
|
|
144
|
-
* created_at: '2024-03-15 10:00:00',
|
|
145
|
-
* },
|
|
146
|
-
* });
|
|
111
|
+
* 设置是否启用软删除
|
|
112
|
+
* @param enable 是否启用软删除
|
|
147
113
|
*/
|
|
148
|
-
public
|
|
114
|
+
public setEnableSoftDelete(enable: boolean): this {
|
|
115
|
+
this.enableSoftDelete = enable;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============ 写操作(单表路由) ============
|
|
120
|
+
|
|
121
|
+
public async insert(reqJson: IRequestModel): Promise<CrudWriteResult> {
|
|
122
|
+
// 时间分表校验:data 必须包含 timeColumn
|
|
123
|
+
this.validateTimeColumnForData(reqJson, 'insert');
|
|
124
|
+
|
|
149
125
|
const targetTable = this.resolveSingleTable(reqJson, 'insert');
|
|
150
126
|
|
|
151
127
|
// 确保分表存在(根据配置决定是否自动创建)
|
|
152
128
|
await this.createShardingTableIfNeeded(targetTable);
|
|
153
129
|
|
|
154
|
-
|
|
130
|
+
const ctx = await this.executeOnTable(targetTable, reqJson, KeysOfSimpleSQL.SIMPLE_INSERT);
|
|
131
|
+
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
132
|
+
return new CrudWriteResult({
|
|
133
|
+
affectedRows: affected.affectedRows,
|
|
134
|
+
insertId: affected.insertId,
|
|
135
|
+
rawContext: ctx,
|
|
136
|
+
});
|
|
155
137
|
}
|
|
156
138
|
|
|
157
|
-
|
|
158
|
-
* 批量插入数据(支持跨分表)
|
|
159
|
-
*
|
|
160
|
-
* 自动将数据按分表分组,并行插入到对应的分表。
|
|
161
|
-
*
|
|
162
|
-
* @param reqJson 请求参数,data 为要插入的数据数组
|
|
163
|
-
* @returns 批量插入结果
|
|
164
|
-
*
|
|
165
|
-
* 执行流程:
|
|
166
|
-
* 1. 遍历所有数据,根据分表字段计算目标分表
|
|
167
|
-
* 2. 将数据按分表分组
|
|
168
|
-
* 3. 并行执行各分表的批量插入
|
|
169
|
-
* 4. 汇总结果返回
|
|
170
|
-
*
|
|
171
|
-
* @example
|
|
172
|
-
* // 数据分布在不同月份的分表
|
|
173
|
-
* const result = await sharding.batchInsert({
|
|
174
|
-
* data: [
|
|
175
|
-
* { order_id: '001', amount: 100, created_at: '2024-01-15' }, // -> t_order_202401
|
|
176
|
-
* { order_id: '002', amount: 200, created_at: '2024-01-20' }, // -> t_order_202401
|
|
177
|
-
* { order_id: '003', amount: 150, created_at: '2024-02-10' }, // -> t_order_202402
|
|
178
|
-
* { order_id: '004', amount: 300, created_at: '2024-03-05' }, // -> t_order_202403
|
|
179
|
-
* ],
|
|
180
|
-
* });
|
|
181
|
-
*
|
|
182
|
-
* console.log(result.totalAffected); // 4
|
|
183
|
-
* console.log(result.tableCount); // 3
|
|
184
|
-
* console.log(result.tableResults);
|
|
185
|
-
* // [
|
|
186
|
-
* // { table: 't_order_202401', affected: 2, rowCount: 2 },
|
|
187
|
-
* // { table: 't_order_202402', affected: 1, rowCount: 1 },
|
|
188
|
-
* // { table: 't_order_202403', affected: 1, rowCount: 1 },
|
|
189
|
-
* // ]
|
|
190
|
-
*/
|
|
191
|
-
public async batchInsert(reqJson: IRequestModel): Promise<IShardingSmartBatchInsertResult> {
|
|
139
|
+
public async batchInsert(reqJson: IRequestModel): Promise<ShardingBatchInsertResult> {
|
|
192
140
|
const dataArray = reqJson.data as Record<string, any>[];
|
|
193
141
|
|
|
194
142
|
if (!Array.isArray(dataArray) || dataArray.length === 0) {
|
|
195
|
-
|
|
196
|
-
totalAffected: 0,
|
|
197
|
-
tableResults: [],
|
|
198
|
-
tableCount: 0,
|
|
199
|
-
success: true,
|
|
200
|
-
errors: [],
|
|
201
|
-
};
|
|
143
|
+
throw new Error('[ShardingCrudPro] batchInsert requires non-empty data array');
|
|
202
144
|
}
|
|
203
145
|
|
|
146
|
+
// 时间分表校验:每条 data 必须包含 timeColumn
|
|
147
|
+
this.validateTimeColumnForBatchData(dataArray, 'batchInsert');
|
|
148
|
+
|
|
204
149
|
// 按分表分组
|
|
205
150
|
const groupedData = this.batchInsertGroupDataByTable(dataArray);
|
|
206
151
|
|
|
@@ -210,27 +155,31 @@ export class ShardingCrudPro {
|
|
|
210
155
|
await this.createShardingTableIfNeeded(tableNames[i]);
|
|
211
156
|
}
|
|
212
157
|
|
|
213
|
-
//
|
|
214
|
-
const
|
|
158
|
+
// 串行执行各分表的批量插入
|
|
159
|
+
const results: Array<{ table: string; affected: number; rowCount: number; error: Error | null; context: ExecuteContext | null }> = [];
|
|
160
|
+
let lastContext: ExecuteContext | null = null;
|
|
161
|
+
|
|
162
|
+
for (const [table, items] of groupedData.entries()) {
|
|
215
163
|
try {
|
|
216
164
|
const ctx = await this.executeOnTable(table, { data: items }, KeysOfSimpleSQL.SIMPLE_BATCH_INSERT);
|
|
217
|
-
|
|
165
|
+
lastContext = ctx;
|
|
166
|
+
results.push({
|
|
218
167
|
table,
|
|
219
|
-
affected: ctx.getResModelItem('affected') || items.length,
|
|
168
|
+
affected: ctx.getResModelItem('affected')?.affectedRows || items.length,
|
|
220
169
|
rowCount: items.length,
|
|
221
|
-
error: null
|
|
222
|
-
|
|
170
|
+
error: null,
|
|
171
|
+
context: ctx,
|
|
172
|
+
});
|
|
223
173
|
} catch (e) {
|
|
224
|
-
|
|
174
|
+
results.push({
|
|
225
175
|
table,
|
|
226
176
|
affected: 0,
|
|
227
177
|
rowCount: items.length,
|
|
228
178
|
error: e as Error,
|
|
229
|
-
|
|
179
|
+
context: null,
|
|
180
|
+
});
|
|
230
181
|
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const results = await Promise.all(insertPromises);
|
|
182
|
+
}
|
|
234
183
|
|
|
235
184
|
// 汇总结果
|
|
236
185
|
const tableResults = results.map(r => ({
|
|
@@ -245,269 +194,381 @@ export class ShardingCrudPro {
|
|
|
245
194
|
|
|
246
195
|
const totalAffected = results.reduce((sum, r) => sum + r.affected, 0);
|
|
247
196
|
|
|
248
|
-
|
|
197
|
+
// Use last successful context, or create a minimal one
|
|
198
|
+
const finalContext = lastContext || results.find(r => r.context)?.context;
|
|
199
|
+
if (!finalContext) {
|
|
200
|
+
throw new Error('[ShardingCrudPro] batchInsert failed - no successful inserts');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new ShardingBatchInsertResult({
|
|
249
204
|
totalAffected,
|
|
250
205
|
tableResults,
|
|
251
206
|
tableCount: groupedData.size,
|
|
252
207
|
success: errors.length === 0,
|
|
253
208
|
errors,
|
|
254
|
-
|
|
209
|
+
lastContext: finalContext,
|
|
210
|
+
});
|
|
255
211
|
}
|
|
256
212
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
*
|
|
262
|
-
* @param reqJson 请求参数,condition 为更新条件,data 为更新数据
|
|
263
|
-
* @returns 执行上下文
|
|
264
|
-
*
|
|
265
|
-
* @example
|
|
266
|
-
* await sharding.update({
|
|
267
|
-
* condition: { order_id: 'ORD001', created_at: '2024-03-15' },
|
|
268
|
-
* data: { amount: 200 },
|
|
269
|
-
* });
|
|
270
|
-
*/
|
|
271
|
-
public async update(reqJson: IRequestModel): Promise<ExecuteContext> {
|
|
213
|
+
public async update(reqJson: IRequestModel): Promise<CrudWriteResult> {
|
|
214
|
+
// 时间分表校验:condition 必须包含路由字段
|
|
215
|
+
this.validateRoutingFieldForCondition(reqJson, 'update');
|
|
216
|
+
|
|
272
217
|
const targetTable = this.resolveSingleTable(reqJson, 'update');
|
|
273
|
-
|
|
218
|
+
|
|
219
|
+
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
220
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
221
|
+
|
|
222
|
+
const ctx = await this.executeOnTable(targetTable, cleanedReqJson, KeysOfSimpleSQL.SIMPLE_UPDATE);
|
|
223
|
+
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
224
|
+
return new CrudWriteResult({
|
|
225
|
+
affectedRows: affected.affectedRows,
|
|
226
|
+
insertId: affected.insertId,
|
|
227
|
+
rawContext: ctx,
|
|
228
|
+
});
|
|
274
229
|
}
|
|
275
230
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
*
|
|
281
|
-
* @param reqJson 请求参数,condition 为删除条件
|
|
282
|
-
* @returns 执行上下文
|
|
283
|
-
*/
|
|
284
|
-
public async delete(reqJson: IRequestModel): Promise<ExecuteContext> {
|
|
231
|
+
public async delete(reqJson: IRequestModel): Promise<CrudWriteResult> {
|
|
232
|
+
// 时间分表校验:condition 必须包含路由字段
|
|
233
|
+
this.validateRoutingFieldForCondition(reqJson, 'delete');
|
|
234
|
+
|
|
285
235
|
const targetTable = this.resolveSingleTable(reqJson, 'delete');
|
|
286
|
-
|
|
236
|
+
|
|
237
|
+
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
238
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
239
|
+
|
|
240
|
+
const ctx = await this.executeOnTable(targetTable, cleanedReqJson, KeysOfSimpleSQL.SIMPLE_DELETE);
|
|
241
|
+
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
242
|
+
return new CrudWriteResult({
|
|
243
|
+
affectedRows: affected.affectedRows,
|
|
244
|
+
insertId: affected.insertId,
|
|
245
|
+
rawContext: ctx,
|
|
246
|
+
});
|
|
287
247
|
}
|
|
288
248
|
|
|
289
249
|
/**
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* @
|
|
295
|
-
*
|
|
250
|
+
* 恢复软删除的记录
|
|
251
|
+
* 将 deleted_at 字段重置为 0,deleted_by 重置为空字符串
|
|
252
|
+
*
|
|
253
|
+
* @param reqJson 请求参数,通过 condition 指定要恢复的记录
|
|
254
|
+
* @returns CrudWriteResult 包含 affectedRows 和 getRawContext()
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* // 恢复指定 ID 的记录
|
|
258
|
+
* await sharding.restore({ condition: { id: 1 } });
|
|
259
|
+
*
|
|
260
|
+
* // 恢复多条记录(使用 $in 操作符)
|
|
261
|
+
* await sharding.restore({ condition: { id: { $in: [1, 2, 3] } } });
|
|
262
|
+
*
|
|
263
|
+
* // 按条件批量恢复记录
|
|
264
|
+
* await sharding.restore({ condition: { status: 'deleted' } });
|
|
296
265
|
*/
|
|
297
|
-
public async
|
|
266
|
+
public async restore(reqJson: IRequestModel): Promise<CrudWriteResult> {
|
|
267
|
+
// 时间分表校验:condition 必须包含路由字段
|
|
268
|
+
this.validateRoutingFieldForCondition(reqJson, 'restore');
|
|
269
|
+
|
|
270
|
+
const targetTable = this.resolveSingleTable(reqJson, 'restore');
|
|
271
|
+
|
|
272
|
+
if (!reqJson.condition || Object.keys(reqJson.condition).length === 0) {
|
|
273
|
+
throw new Error('[ShardingCrudPro] restore 操作必须指定恢复条件');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
277
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
278
|
+
|
|
279
|
+
// 构建恢复数据:重置软删除字段
|
|
280
|
+
const restoreReqJson: IRequestModel = {
|
|
281
|
+
...cleanedReqJson,
|
|
282
|
+
data: {
|
|
283
|
+
...cleanedReqJson.data,
|
|
284
|
+
deleted_at: 0,
|
|
285
|
+
deleted_by: '',
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const ctx = await this.executeOnTable(targetTable, restoreReqJson, KeysOfSimpleSQL.SIMPLE_UPDATE);
|
|
290
|
+
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
291
|
+
return new CrudWriteResult({
|
|
292
|
+
affectedRows: affected.affectedRows,
|
|
293
|
+
insertId: affected.insertId,
|
|
294
|
+
rawContext: ctx,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public async insertOrUpdate(reqJson: IRequestModel): Promise<CrudUpsertResult> {
|
|
299
|
+
// 时间分表校验:data 必须包含 timeColumn,condition 必须包含路由字段
|
|
300
|
+
this.validateTimeColumnForData(reqJson, 'insertOrUpdate');
|
|
301
|
+
this.validateRoutingFieldForCondition(reqJson, 'insertOrUpdate');
|
|
302
|
+
|
|
298
303
|
const targetTable = this.resolveSingleTable(reqJson, 'update'); // 使用 condition 路由
|
|
299
|
-
|
|
304
|
+
const ctx = await this.executeOnTable(targetTable, reqJson, KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE);
|
|
305
|
+
const resModel = ctx.getResModel();
|
|
306
|
+
return new CrudUpsertResult({
|
|
307
|
+
affected: resModel.affected,
|
|
308
|
+
insertAffected: resModel.insert_affected,
|
|
309
|
+
updateAffected: resModel.update_affected,
|
|
310
|
+
isExist: resModel.is_exist ?? false,
|
|
311
|
+
rawContext: ctx,
|
|
312
|
+
});
|
|
300
313
|
}
|
|
301
314
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
* @param reqJson 请求参数
|
|
308
|
-
* @returns 执行上下文
|
|
309
|
-
*/
|
|
310
|
-
public async insertOnDuplicateUpdate(reqJson: IRequestModel): Promise<ExecuteContext> {
|
|
315
|
+
public async insertOnDuplicateUpdate(reqJson: IRequestModel): Promise<CrudWriteResult> {
|
|
316
|
+
// 时间分表校验:data 必须包含 timeColumn,condition 必须包含路由字段
|
|
317
|
+
this.validateTimeColumnForData(reqJson, 'insertOnDuplicateUpdate');
|
|
318
|
+
this.validateRoutingFieldForCondition(reqJson, 'insertOnDuplicateUpdate');
|
|
319
|
+
|
|
311
320
|
const targetTable = this.resolveSingleTable(reqJson, 'update'); // 使用 condition 路由
|
|
312
|
-
|
|
321
|
+
const ctx = await this.executeOnTable(targetTable, reqJson, KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE);
|
|
322
|
+
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
323
|
+
return new CrudWriteResult({
|
|
324
|
+
affectedRows: affected.affectedRows,
|
|
325
|
+
insertId: affected.insertId,
|
|
326
|
+
rawContext: ctx,
|
|
327
|
+
});
|
|
313
328
|
}
|
|
314
329
|
|
|
330
|
+
|
|
315
331
|
// ============ 查询操作(可能多表) ============
|
|
316
332
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
*
|
|
323
|
-
* @param reqJson 请求参数,condition 为查询条件
|
|
324
|
-
* @returns 单条记录,未找到返回 null
|
|
325
|
-
*
|
|
326
|
-
* @example
|
|
327
|
-
* const order = await sharding.queryOne({
|
|
328
|
-
* condition: { order_id: 'ORD001' },
|
|
329
|
-
* });
|
|
330
|
-
*/
|
|
331
|
-
public async queryOne(reqJson: IRequestModel): Promise<any> {
|
|
332
|
-
const targetTables = await this.resolveQueryTables(reqJson);
|
|
333
|
+
public async findOne<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryOneResult<T>> {
|
|
334
|
+
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
335
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
336
|
+
|
|
337
|
+
const targetTables = await this.resolveFindTables(reqJson);
|
|
333
338
|
|
|
334
339
|
if (targetTables.length === 0) {
|
|
335
|
-
|
|
340
|
+
// Return a result with null row - we need a context for this
|
|
341
|
+
// Since there are no tables to query, we throw an error for now
|
|
342
|
+
throw new Error('[ShardingCrudPro] findOne: no matching tables found');
|
|
336
343
|
}
|
|
337
344
|
|
|
338
345
|
// 多表:顺序查询,找到即返回
|
|
346
|
+
let lastCtx: ExecuteContext | null = null;
|
|
339
347
|
for (const table of targetTables) {
|
|
340
|
-
|
|
341
|
-
|
|
348
|
+
try {
|
|
349
|
+
const ctx = await this.executeOnTable(table, cleanedReqJson, KeysOfSimpleSQL.SIMPLE_QUERY_ONE);
|
|
350
|
+
lastCtx = ctx;
|
|
351
|
+
const row = ctx.getOneObj();
|
|
352
|
+
if (row) {
|
|
353
|
+
return new CrudQueryOneResult<T>({ row: row as T, rawContext: ctx, debugInfo: this.buildDebugInfo(table, reqJson.condition) });
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {
|
|
356
|
+
// 表不存在时跳过,继续查下一张表
|
|
357
|
+
}
|
|
342
358
|
}
|
|
343
|
-
|
|
359
|
+
// 未找到,使用最后一次查询的 ctx
|
|
360
|
+
return new CrudQueryOneResult<T>({ row: null, rawContext: lastCtx, debugInfo: this.buildDebugInfo(targetTables[0], reqJson.condition) });
|
|
344
361
|
}
|
|
345
362
|
|
|
346
363
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
* -
|
|
353
|
-
*
|
|
354
|
-
*
|
|
364
|
+
* 查询唯一单条记录(期望结果为 0 条或 1 条)
|
|
365
|
+
*
|
|
366
|
+
* 与 findOne 不同,此方法会校验查询结果的唯一性:
|
|
367
|
+
* - 如果查到 0 条:返回 row = null
|
|
368
|
+
* - 如果查到 1 条:正常返回
|
|
369
|
+
* - 如果查到多条:抛出异常,包含详细的定位信息(含分表名列表)
|
|
370
|
+
*
|
|
371
|
+
* 分表场景的特殊处理:
|
|
372
|
+
* - 单分表:直接查询并校验唯一性
|
|
373
|
+
* - 多分表:顺序查询各分表,累计找到的数量,超过 1 条立即抛异常
|
|
374
|
+
*
|
|
355
375
|
* @param reqJson 请求参数
|
|
356
|
-
* @returns
|
|
357
|
-
*
|
|
376
|
+
* @returns CrudQueryOneResult 包含 row、found 和 getRawContext()
|
|
377
|
+
* @throws Error 如果查询到多条记录
|
|
378
|
+
*
|
|
358
379
|
* @example
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
380
|
+
* // 根据唯一索引查询单条
|
|
381
|
+
* const result = await sharding.findUniqueOne({ condition: { order_id: 'ORD001' } });
|
|
382
|
+
* if (result.found) {
|
|
383
|
+
* console.log(result.row);
|
|
384
|
+
* }
|
|
363
385
|
*/
|
|
364
|
-
public async
|
|
386
|
+
public async findUniqueOne<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryOneResult<T>> {
|
|
387
|
+
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
388
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
389
|
+
|
|
390
|
+
const targetTables = await this.resolveFindTables(reqJson);
|
|
391
|
+
|
|
392
|
+
if (targetTables.length === 0) {
|
|
393
|
+
// 无匹配表,返回空结果
|
|
394
|
+
return new CrudQueryOneResult<T>({ row: null, rawContext: null as any, debugInfo: this.buildDebugInfo(undefined, reqJson.condition) });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 统一逻辑:每个表查 maxLimit: 2,收集所有结果
|
|
398
|
+
const allRows: T[] = [];
|
|
399
|
+
const foundTables: string[] = [];
|
|
400
|
+
let firstCtx: ExecuteContext | null = null;
|
|
401
|
+
|
|
402
|
+
for (const table of targetTables) {
|
|
403
|
+
try {
|
|
404
|
+
const ctx = await this.executeOnTable(table, cleanedReqJson, KeysOfSimpleSQL.SIMPLE_QUERY, { maxLimit: 2 });
|
|
405
|
+
if (!firstCtx) {
|
|
406
|
+
firstCtx = ctx;
|
|
407
|
+
}
|
|
408
|
+
const rows = ctx.getResRows() as T[];
|
|
409
|
+
if (rows.length > 0) {
|
|
410
|
+
allRows.push(...rows);
|
|
411
|
+
foundTables.push(table);
|
|
412
|
+
// 超过 1 条,立即抛异常,不再查询后续分表
|
|
413
|
+
if (allRows.length > 1) {
|
|
414
|
+
throw new Error(this.formatUniqueError(allRows.length, foundTables, reqJson.condition, foundTables.length > 1));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} catch (e) {
|
|
418
|
+
// 非 formatUniqueError 的异常(如表不存在),继续查询下一张表
|
|
419
|
+
if (e instanceof Error && e.message.startsWith('[ShardingCrudPro] findUniqueOne')) {
|
|
420
|
+
throw e;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 返回结果
|
|
426
|
+
return new CrudQueryOneResult<T>({
|
|
427
|
+
row: allRows[0] || null,
|
|
428
|
+
rawContext: firstCtx,
|
|
429
|
+
debugInfo: this.buildDebugInfo(foundTables[0] || targetTables[0], reqJson.condition),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
365
432
|
|
|
433
|
+
public async findList<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryListResult<T>> {
|
|
434
|
+
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
435
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
366
436
|
|
|
367
|
-
const targetTables = await this.
|
|
437
|
+
const targetTables = await this.resolveFindTables(reqJson);
|
|
368
438
|
|
|
369
439
|
if (targetTables.length === 0) {
|
|
370
|
-
return []
|
|
440
|
+
return new CrudQueryListResult<T>({ rows: [], rawContext: null as any });
|
|
371
441
|
}
|
|
372
442
|
|
|
373
443
|
if (targetTables.length === 1) {
|
|
374
|
-
const ctx = await this.executeOnTable(targetTables[0],
|
|
375
|
-
|
|
444
|
+
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, KeysOfSimpleSQL.SIMPLE_QUERY);
|
|
445
|
+
const rows = ctx.getResRows() as T[];
|
|
446
|
+
return new CrudQueryListResult<T>({ rows, rawContext: ctx });
|
|
376
447
|
}
|
|
377
448
|
|
|
378
449
|
// 多表查询时,需要参数校验
|
|
379
|
-
this.
|
|
450
|
+
this.validateFindOrderBy(reqJson);
|
|
380
451
|
|
|
381
452
|
// 根据排序方向确定表顺序:DESC 新→旧,ASC 旧→新
|
|
382
453
|
const tablesForMerge = this.sortTablesForOrderBy(targetTables, reqJson);
|
|
383
454
|
|
|
384
455
|
// 多表查询合并
|
|
385
|
-
|
|
386
|
-
this.
|
|
456
|
+
const mergeResult = await this.merger.mergeQuery(
|
|
457
|
+
this.crudProFactory(),
|
|
387
458
|
tablesForMerge,
|
|
388
|
-
|
|
459
|
+
cleanedReqJson,
|
|
389
460
|
this.buildCfg(KeysOfSimpleSQL.SIMPLE_QUERY)
|
|
390
461
|
);
|
|
462
|
+
|
|
463
|
+
return new CrudQueryListResult<T>({ rows: mergeResult.rows as T[], rawContext: mergeResult.lastCtx });
|
|
391
464
|
}
|
|
392
465
|
|
|
393
|
-
/**
|
|
394
|
-
* 分页查询
|
|
395
|
-
*
|
|
396
|
-
* 自动处理跨分表的分页:
|
|
397
|
-
* 1. 顺序累计查询各分表
|
|
398
|
-
* 2. 截取目标数据(无需排序)
|
|
399
|
-
*
|
|
400
|
-
* 使用约束:
|
|
401
|
-
* - 必须传 orderBy 参数
|
|
402
|
-
* - orderBy 必须为 timeColumn DESC 或 timeColumn ASC(如 'created_at DESC' / 'created_at ASC')
|
|
403
|
-
*
|
|
404
|
-
* @param reqJson 请求参数,包含 pageNo、pageSize 和 orderBy
|
|
405
|
-
* @returns 分页结果,包含 rows 和 total_count
|
|
406
|
-
*
|
|
407
|
-
* @example
|
|
408
|
-
* const result = await sharding.queryPage({
|
|
409
|
-
* condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
|
|
410
|
-
* pageNo: 1,
|
|
411
|
-
* pageSize: 10,
|
|
412
|
-
* orderBy: 'created_at DESC', // 或 'created_at ASC'
|
|
413
|
-
* });
|
|
414
|
-
* console.log(result.rows, result.total_count);
|
|
415
|
-
*/
|
|
416
|
-
public async queryPage(reqJson: IRequestModel): Promise<IShardingPageQueryResult> {
|
|
417
466
|
|
|
418
|
-
|
|
467
|
+
|
|
468
|
+
public async findPage<T = Record<string, any>>(reqJson: IRequestModel): Promise<CrudQueryPageResult<T>> {
|
|
469
|
+
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
470
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
471
|
+
|
|
472
|
+
const targetTables = await this.resolveFindTables(reqJson);
|
|
419
473
|
|
|
420
474
|
if (targetTables.length === 0) {
|
|
421
|
-
|
|
475
|
+
throw new Error('[ShardingCrudPro] findPage: no matching tables found');
|
|
422
476
|
}
|
|
423
477
|
|
|
424
478
|
if (targetTables.length === 1) {
|
|
425
|
-
const ctx = await this.executeOnTable(targetTables[0],
|
|
426
|
-
|
|
479
|
+
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, KeysOfSimpleSQL.SIMPLE_QUERY_PAGE);
|
|
480
|
+
const pageResult = ctx.getResModelForQueryPage();
|
|
481
|
+
return new CrudQueryPageResult<T>({
|
|
482
|
+
rows: pageResult.rows as T[],
|
|
483
|
+
totalCount: pageResult.total_count,
|
|
484
|
+
rawContext: ctx,
|
|
485
|
+
});
|
|
427
486
|
}
|
|
428
487
|
|
|
429
488
|
// 多表查询时,需要参数校验
|
|
430
|
-
this.
|
|
489
|
+
this.validateFindOrderBy(reqJson);
|
|
431
490
|
|
|
432
491
|
// 根据排序方向确定表顺序:DESC 新→旧,ASC 旧→新
|
|
433
492
|
const tablesForMerge = this.sortTablesForOrderBy(targetTables, reqJson);
|
|
434
493
|
|
|
435
494
|
// 多表分页查询:顺序累计
|
|
436
|
-
|
|
437
|
-
this.
|
|
495
|
+
const pageResult = await this.merger.mergePageQuery(
|
|
496
|
+
this.crudProFactory(),
|
|
438
497
|
tablesForMerge,
|
|
439
|
-
|
|
498
|
+
cleanedReqJson,
|
|
440
499
|
this.buildCfg(KeysOfSimpleSQL.SIMPLE_QUERY_PAGE)
|
|
441
500
|
);
|
|
501
|
+
|
|
502
|
+
return new CrudQueryPageResult<T>({
|
|
503
|
+
rows: pageResult.rows as T[],
|
|
504
|
+
totalCount: pageResult.total_count,
|
|
505
|
+
rawContext: pageResult.lastCtx,
|
|
506
|
+
});
|
|
442
507
|
}
|
|
443
508
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
* @param reqJson 请求参数
|
|
450
|
-
* @returns 记录总数
|
|
451
|
-
*/
|
|
452
|
-
public async queryCount(reqJson: IRequestModel): Promise<number> {
|
|
453
|
-
const targetTables = await this.resolveQueryTables(reqJson);
|
|
509
|
+
public async findCount(reqJson: IRequestModel): Promise<CrudCountResult> {
|
|
510
|
+
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
511
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
512
|
+
|
|
513
|
+
const targetTables = await this.resolveFindTables(reqJson);
|
|
454
514
|
|
|
455
515
|
if (targetTables.length === 0) {
|
|
456
|
-
|
|
516
|
+
throw new Error('[ShardingCrudPro] findCount: no matching tables found');
|
|
457
517
|
}
|
|
458
518
|
|
|
459
519
|
if (targetTables.length === 1) {
|
|
460
|
-
const ctx = await this.executeOnTable(targetTables[0],
|
|
461
|
-
|
|
520
|
+
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
|
|
521
|
+
const count = ctx.getResModelItem('total_count') || 0;
|
|
522
|
+
return new CrudCountResult({ count, rawContext: ctx });
|
|
462
523
|
}
|
|
463
524
|
|
|
464
|
-
//
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
525
|
+
// 多表统计:串行查询后求和
|
|
526
|
+
const countResults: Array<{ count: number; ctx: ExecuteContext | null }> = [];
|
|
527
|
+
for (const table of targetTables) {
|
|
528
|
+
const result = await this.findCountFromTable(table, cleanedReqJson);
|
|
529
|
+
countResults.push(result);
|
|
530
|
+
}
|
|
531
|
+
const totalCount = countResults.reduce((sum, r) => sum + r.count, 0);
|
|
532
|
+
const firstCtx = countResults.find(r => r.ctx !== null)?.ctx || null;
|
|
533
|
+
|
|
534
|
+
return new CrudCountResult({ count: totalCount, rawContext: firstCtx as any });
|
|
470
535
|
}
|
|
471
536
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
* @param reqJson 请求参数
|
|
478
|
-
* @returns 是否存在
|
|
479
|
-
*/
|
|
480
|
-
public async isExist(reqJson: IRequestModel): Promise<boolean> {
|
|
481
|
-
const targetTables = await this.resolveQueryTables(reqJson);
|
|
537
|
+
public async isExist(reqJson: IRequestModel): Promise<CrudExistResult> {
|
|
538
|
+
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
539
|
+
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
540
|
+
|
|
541
|
+
const targetTables = await this.resolveFindTables(reqJson);
|
|
482
542
|
|
|
483
543
|
if (targetTables.length === 0) {
|
|
484
|
-
|
|
544
|
+
throw new Error('[ShardingCrudPro] isExist: no matching tables found');
|
|
485
545
|
}
|
|
486
546
|
|
|
487
547
|
if (targetTables.length === 1) {
|
|
488
|
-
const ctx = await this.executeOnTable(targetTables[0],
|
|
489
|
-
|
|
548
|
+
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
|
|
549
|
+
const exists = ctx.getResModelItem('is_exist') === true;
|
|
550
|
+
return new CrudExistResult({ exists, rawContext: ctx });
|
|
490
551
|
}
|
|
491
552
|
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
)
|
|
496
|
-
|
|
497
|
-
|
|
553
|
+
// 多表判断:串行查询,任一表存在即返回 true
|
|
554
|
+
let exists = false;
|
|
555
|
+
let firstCtx: ExecuteContext | null = null;
|
|
556
|
+
for (const table of targetTables) {
|
|
557
|
+
const result = await this.isExistInTable(table, cleanedReqJson);
|
|
558
|
+
if (!firstCtx && result.ctx) {
|
|
559
|
+
firstCtx = result.ctx;
|
|
560
|
+
}
|
|
561
|
+
if (result.exists) {
|
|
562
|
+
exists = true;
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return new CrudExistResult({ exists, rawContext: firstCtx as any });
|
|
498
568
|
}
|
|
499
569
|
|
|
500
570
|
// ============ 私有方法:分表路由 ============
|
|
501
571
|
|
|
502
|
-
/**
|
|
503
|
-
* 【batchInsert 专用】按分表对数据进行分组
|
|
504
|
-
*
|
|
505
|
-
* 仅用于 batchInsert 方法,遍历所有数据项,
|
|
506
|
-
* 根据分表规则计算每条数据的目标分表,将相同分表的数据归为一组。
|
|
507
|
-
*
|
|
508
|
-
* @param dataArray 数据数组
|
|
509
|
-
* @returns 分表 -> 数据列表 的映射
|
|
510
|
-
*/
|
|
511
572
|
private batchInsertGroupDataByTable(dataArray: Record<string, any>[]): Map<string, Record<string, any>[]> {
|
|
512
573
|
const groupedData = new Map<string, Record<string, any>[]>();
|
|
513
574
|
|
|
@@ -533,14 +594,9 @@ export class ShardingCrudPro {
|
|
|
533
594
|
return this.router.resolveForInsert(this.config, context);
|
|
534
595
|
}
|
|
535
596
|
|
|
536
|
-
/**
|
|
537
|
-
* 解析单个目标表
|
|
538
|
-
*
|
|
539
|
-
* 写操作必须确定单一目标表,否则抛出异常。
|
|
540
|
-
*/
|
|
541
597
|
private resolveSingleTable(
|
|
542
598
|
reqJson: IRequestModel,
|
|
543
|
-
operation: 'insert' | 'update' | 'delete'
|
|
599
|
+
operation: 'insert' | 'update' | 'delete' | 'restore'
|
|
544
600
|
): string {
|
|
545
601
|
// insert 操作使用 resolveForInsert(从 data 提取字段)
|
|
546
602
|
if (operation === 'insert') {
|
|
@@ -569,9 +625,6 @@ export class ShardingCrudPro {
|
|
|
569
625
|
return result;
|
|
570
626
|
}
|
|
571
627
|
|
|
572
|
-
/**
|
|
573
|
-
* 获取分表字段提示
|
|
574
|
-
*/
|
|
575
628
|
private getShardingColumnHint(): string {
|
|
576
629
|
if (this.config.type === ShardingType.RANGE || this.config.type === ShardingType.HASH) {
|
|
577
630
|
return this.config.shardingColumn || '分表字段';
|
|
@@ -584,119 +637,95 @@ export class ShardingCrudPro {
|
|
|
584
637
|
|
|
585
638
|
// ============ 私有方法:配置构建 ============
|
|
586
639
|
|
|
587
|
-
|
|
588
|
-
* 构建配置
|
|
589
|
-
*/
|
|
590
|
-
private buildCfg(sqlSimpleName: KeysOfSimpleSQL): IRequestCfgModel {
|
|
640
|
+
private buildCfg(sqlSimpleName: KeysOfSimpleSQL): IRequestCfgModel & { enableSoftDelete?: boolean } {
|
|
591
641
|
return {
|
|
642
|
+
method: `ShardingCrudProAnonymous_${sqlSimpleName}`,
|
|
592
643
|
...this.baseCfg,
|
|
593
644
|
sqlTable: this.config.baseTable,
|
|
594
645
|
sqlSimpleName,
|
|
595
|
-
};
|
|
646
|
+
} as IRequestCfgModel & { enableSoftDelete?: boolean };
|
|
596
647
|
}
|
|
597
648
|
|
|
598
649
|
// ============ 私有方法:执行操作 ============
|
|
599
650
|
|
|
600
|
-
/**
|
|
601
|
-
* 在指定表上执行操作
|
|
602
|
-
*/
|
|
603
651
|
private async executeOnTable(
|
|
604
652
|
table: string,
|
|
605
653
|
reqJson: IRequestModel,
|
|
606
|
-
sqlSimpleName: KeysOfSimpleSQL
|
|
654
|
+
sqlSimpleName: KeysOfSimpleSQL,
|
|
655
|
+
extraCfg?: Partial<IRequestCfgModel>
|
|
607
656
|
): Promise<ExecuteContext> {
|
|
608
657
|
const cfg = this.buildCfg(sqlSimpleName);
|
|
609
658
|
cfg.sqlTable = table; // 替换为实际分表名
|
|
659
|
+
cfg.enableSoftDelete = this.enableSoftDelete;
|
|
660
|
+
|
|
661
|
+
if (extraCfg) {
|
|
662
|
+
Object.assign(cfg, extraCfg);
|
|
663
|
+
}
|
|
610
664
|
|
|
611
|
-
|
|
612
|
-
|
|
665
|
+
// 创建 CrudPro 实例并设置 visitor
|
|
666
|
+
const crudPro = this.crudProFactory();
|
|
613
667
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
const ctx = await this.executeOnTable(table, reqJson, KeysOfSimpleSQL.SIMPLE_QUERY_ONE);
|
|
620
|
-
return ctx.getOneObj();
|
|
621
|
-
} catch (e) {
|
|
622
|
-
// 表不存在时返回 null
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
668
|
+
// 应用软删除处理
|
|
669
|
+
fixSoftDelete(sqlSimpleName, cfg as any, reqJson, crudPro.getVisitor());
|
|
670
|
+
|
|
671
|
+
return crudPro.executeCrudByCfg(reqJson, cfg);
|
|
625
672
|
}
|
|
626
673
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
private async queryCountFromTable(table: string, reqJson: IRequestModel): Promise<number> {
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
private async findCountFromTable(table: string, reqJson: IRequestModel): Promise<{ count: number; ctx: ExecuteContext | null }> {
|
|
631
677
|
try {
|
|
632
678
|
const ctx = await this.executeOnTable(table, reqJson, KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
|
|
633
|
-
return ctx.getResModelItem('total_count') || 0;
|
|
679
|
+
return { count: ctx.getResModelItem('total_count') || 0, ctx };
|
|
634
680
|
} catch (e) {
|
|
635
681
|
// 表不存在时返回 0
|
|
636
|
-
return 0;
|
|
682
|
+
return { count: 0, ctx: null };
|
|
637
683
|
}
|
|
638
684
|
}
|
|
639
685
|
|
|
640
|
-
|
|
641
|
-
* 判断指定表中是否存在记录
|
|
642
|
-
*/
|
|
643
|
-
private async isExistInTable(table: string, reqJson: IRequestModel): Promise<boolean> {
|
|
686
|
+
private async isExistInTable(table: string, reqJson: IRequestModel): Promise<{ exists: boolean; ctx: ExecuteContext | null }> {
|
|
644
687
|
try {
|
|
645
688
|
const ctx = await this.executeOnTable(table, reqJson, KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
|
|
646
|
-
return ctx.getResModelItem('is_exist') === true;
|
|
689
|
+
return { exists: ctx.getResModelItem('is_exist') === true, ctx };
|
|
647
690
|
} catch (e) {
|
|
648
691
|
// 表不存在时返回 false
|
|
649
|
-
return false;
|
|
692
|
+
return { exists: false, ctx: null };
|
|
650
693
|
}
|
|
651
694
|
}
|
|
652
695
|
|
|
653
696
|
// ============ 表存在性检查和自动创建 ============
|
|
654
697
|
|
|
655
|
-
|
|
656
|
-
* 获取数据库中真实存在的表名集合
|
|
657
|
-
*/
|
|
658
|
-
private async getExistingTablesSet(): Promise<Set<string>> {
|
|
698
|
+
private async getExistingTablesSet(skipCache = false): Promise<Set<string>> {
|
|
659
699
|
const { sqlDatabase, sqlDbType } = this.baseCfg;
|
|
660
700
|
|
|
661
701
|
if (!sqlDatabase || !sqlDbType) {
|
|
662
702
|
throw new Error('[ShardingCrudPro] 未配置 sqlDatabase 或 sqlDbType');
|
|
663
703
|
}
|
|
664
704
|
|
|
665
|
-
const { tables } = await this.
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
705
|
+
const { tables } = await this.crudProFactory().getAllTableInfos(
|
|
706
|
+
{
|
|
707
|
+
sqlDatabase,
|
|
708
|
+
sqlDbType: sqlDbType as any,
|
|
709
|
+
},
|
|
710
|
+
{ skipCache }
|
|
711
|
+
);
|
|
669
712
|
|
|
670
713
|
return new Set(tables.map(t => t.name));
|
|
671
714
|
}
|
|
672
715
|
|
|
673
|
-
/**
|
|
674
|
-
* 检查表是否存在
|
|
675
|
-
*/
|
|
676
716
|
private async isTableExists(tableName: string): Promise<boolean> {
|
|
677
|
-
const existingSet = await this.getExistingTablesSet();
|
|
717
|
+
const existingSet = await this.getExistingTablesSet(); // 使用缓存
|
|
678
718
|
return existingSet.has(tableName);
|
|
679
719
|
}
|
|
680
720
|
|
|
681
|
-
/**
|
|
682
|
-
* 创建分表(如果需要)
|
|
683
|
-
*
|
|
684
|
-
* 根据配置检查分表是否存在:
|
|
685
|
-
* - 如果分表已存在,直接返回
|
|
686
|
-
* - 如果分表不存在且 autoCreateTable=true,自动创建
|
|
687
|
-
* - 如果分表不存在且 autoCreateTable=false,抛出异常
|
|
688
|
-
*
|
|
689
|
-
* @param tableName 目标分表名
|
|
690
|
-
*/
|
|
691
721
|
private async createShardingTableIfNeeded(tableName: string): Promise<void> {
|
|
692
|
-
// 检查表是否已存在
|
|
693
|
-
if (await this.isTableExists(tableName)) {
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
722
|
// 根据配置决定是否自动创建
|
|
698
723
|
if (!this.config.autoCreateTable) {
|
|
699
|
-
|
|
724
|
+
// 使用缓存检查表是否存在,不存在则抛异常
|
|
725
|
+
if (!(await this.isTableExists(tableName))) {
|
|
726
|
+
throw new Error(`[ShardingCrudPro] 分表 ${tableName} 不存在。请先创建分表,或设置 autoCreateTable: true 自动创建`);
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
700
729
|
}
|
|
701
730
|
|
|
702
731
|
// 检查数据库配置
|
|
@@ -704,7 +733,7 @@ export class ShardingCrudPro {
|
|
|
704
733
|
throw new Error('[ShardingCrudPro] 请先调用 setBaseCfg 设置数据库配置');
|
|
705
734
|
}
|
|
706
735
|
|
|
707
|
-
//
|
|
736
|
+
// 执行创建(内部有 checkTableExists + 建表 + 刷新缓存)
|
|
708
737
|
const result = await this.tableCreator.createTableIfNeeded(
|
|
709
738
|
tableName,
|
|
710
739
|
{
|
|
@@ -717,113 +746,288 @@ export class ShardingCrudPro {
|
|
|
717
746
|
if (!result.success) {
|
|
718
747
|
throw result.error || new Error(`[ShardingCrudPro] 创建分表 ${tableName} 失败`);
|
|
719
748
|
}
|
|
749
|
+
|
|
750
|
+
// 建表成功后刷新缓存,确保后续请求命中新表
|
|
751
|
+
if (result.createSql) {
|
|
752
|
+
await this.getExistingTablesSet(true);
|
|
753
|
+
}
|
|
720
754
|
}
|
|
721
755
|
|
|
722
756
|
// ============ 参数校验 ============
|
|
723
757
|
|
|
724
758
|
/**
|
|
725
|
-
*
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
759
|
+
* 判断是否为时间分表类型
|
|
760
|
+
*/
|
|
761
|
+
private isTimeSharding(): boolean {
|
|
762
|
+
return [ShardingType.YEAR, ShardingType.MONTH, ShardingType.DAY].includes(this.config.type);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* 校验时间分表插入操作的 data 必须包含 timeColumn
|
|
730
767
|
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
* - DESC 时表顺序为 新→旧,ASC 时表顺序为 旧→新(由调用方反转)
|
|
734
|
-
* - 这样无需内存排序,直接按表顺序拼接即可
|
|
768
|
+
* 时间分表需要根据时间字段路由到正确的分表,
|
|
769
|
+
* 如果 data 中缺少时间字段,将无法确定数据应插入哪个分表。
|
|
735
770
|
*
|
|
736
771
|
* @param reqJson 请求参数
|
|
772
|
+
* @param operation 操作名称(用于错误提示)
|
|
773
|
+
* @throws Error 如果 data 中缺少时间字段
|
|
737
774
|
*/
|
|
738
|
-
private
|
|
739
|
-
|
|
740
|
-
const timeColumn = this.config.timeColumn;
|
|
741
|
-
if (!timeColumn) {
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
775
|
+
private validateTimeColumnForData(reqJson: IRequestModel, operation: string): void {
|
|
776
|
+
if (!this.isTimeSharding()) return;
|
|
744
777
|
|
|
745
|
-
const
|
|
746
|
-
const
|
|
747
|
-
const expectedAsc = `${timeColumn} ASC`;
|
|
778
|
+
const timeColumn = this.config.timeColumn!;
|
|
779
|
+
const data = reqJson.data as Record<string, any>;
|
|
748
780
|
|
|
749
|
-
|
|
750
|
-
if (!orderBy) {
|
|
781
|
+
if (!data || data[timeColumn] === undefined) {
|
|
751
782
|
throw new Error(
|
|
752
|
-
`[ShardingCrudPro]
|
|
753
|
-
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`
|
|
783
|
+
`[ShardingCrudPro] ${operation} 操作的 data 必须包含时间字段 '${timeColumn}',用于路由到正确的分表。`
|
|
754
784
|
);
|
|
755
785
|
}
|
|
786
|
+
}
|
|
756
787
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
`[ShardingCrudPro] orderBy 数组不能为空,首个排序字段必须为 '${timeColumn} DESC' 或 '${timeColumn} ASC'`
|
|
771
|
-
);
|
|
772
|
-
}
|
|
773
|
-
const firstItem = orderBy[0];
|
|
774
|
-
const firstItemStr = typeof firstItem === 'string'
|
|
775
|
-
? firstItem
|
|
776
|
-
: `${firstItem.fieldName} ${firstItem.orderType || 'ASC'}`;
|
|
777
|
-
const upper = firstItemStr.trim().toUpperCase();
|
|
778
|
-
if (upper !== expectedDesc.toUpperCase() && upper !== expectedAsc.toUpperCase()) {
|
|
788
|
+
/**
|
|
789
|
+
* 校验时间分表批量插入操作的每条 data 必须包含 timeColumn
|
|
790
|
+
*
|
|
791
|
+
* @param dataArray 数据数组
|
|
792
|
+
* @param operation 操作名称(用于错误提示)
|
|
793
|
+
* @throws Error 如果任一条 data 中缺少时间字段
|
|
794
|
+
*/
|
|
795
|
+
private validateTimeColumnForBatchData(dataArray: Record<string, any>[], operation: string): void {
|
|
796
|
+
if (!this.isTimeSharding()) return;
|
|
797
|
+
|
|
798
|
+
const timeColumn = this.config.timeColumn!;
|
|
799
|
+
for (let i = 0; i < dataArray.length; i++) {
|
|
800
|
+
if (!dataArray[i] || dataArray[i][timeColumn] === undefined) {
|
|
779
801
|
throw new Error(
|
|
780
|
-
`[ShardingCrudPro]
|
|
781
|
-
`当前值: '${firstItemStr}'`
|
|
802
|
+
`[ShardingCrudPro] ${operation} 操作的 data[${i}] 必须包含时间字段 '${timeColumn}',用于路由到正确的分表。`
|
|
782
803
|
);
|
|
783
804
|
}
|
|
784
|
-
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* 校验时间分表写操作的 condition 必须包含路由字段(timeColumn)
|
|
810
|
+
*
|
|
811
|
+
* 时间分表需要根据时间字段路由到正确的分表,
|
|
812
|
+
* 如果 condition 中缺少时间字段,将无法确定操作哪个分表。
|
|
813
|
+
*
|
|
814
|
+
* @param reqJson 请求参数
|
|
815
|
+
* @param operation 操作名称(用于错误提示)
|
|
816
|
+
* @throws Error 如果 condition 中缺少时间字段
|
|
817
|
+
*/
|
|
818
|
+
private validateRoutingFieldForCondition(reqJson: IRequestModel, operation: string): void {
|
|
819
|
+
if (!this.isTimeSharding()) return;
|
|
820
|
+
|
|
821
|
+
const timeColumn = this.config.timeColumn!;
|
|
822
|
+
const condition = reqJson.condition;
|
|
823
|
+
|
|
824
|
+
if (!condition || !condition[timeColumn]) {
|
|
785
825
|
throw new Error(
|
|
786
|
-
`[ShardingCrudPro]
|
|
826
|
+
`[ShardingCrudPro] ${operation} 操作的 condition 必须包含时间字段 '${timeColumn}',用于路由到正确的分表。` +
|
|
827
|
+
`请提供 '${timeColumn}' 字段(如 { ${timeColumn}: '2026-03-15' })或时间范围(如 { ${timeColumn}: { $gte: '2024-01-01', $lte: '2024-03-31' } })。`
|
|
787
828
|
);
|
|
788
829
|
}
|
|
789
830
|
}
|
|
790
831
|
|
|
791
832
|
/**
|
|
792
|
-
*
|
|
833
|
+
* 智能处理时间分表场景下的时间字段
|
|
834
|
+
*
|
|
835
|
+
* **问题背景**:
|
|
836
|
+
* 在时间分表(YEAR/MONTH/DAY)场景中,timeColumn 既是路由键也是查询条件。
|
|
837
|
+
* 用户传入的时间值可能精度不一致(如传 '2024-01-15' 但数据库存的是毫秒时间戳),
|
|
838
|
+
* 直接作为 WHERE 条件可能导致匹配失败。
|
|
839
|
+
*
|
|
840
|
+
* **处理规则**(详见 TIME_COLUMN_CLEAN_SPEC.md):
|
|
841
|
+
* 1. 操作符表达式($gte/$lte/$range/$in/$null 等)→ 始终保留,不做处理
|
|
842
|
+
* 2. 精确值 + 有 primaryKey → 清理(从 WHERE 移除,仅用于路由)
|
|
843
|
+
* 3. 精确值 + 无 primaryKey + 日期粒度字符串(年/月/日)→ 转换为 $gte/$lte 范围
|
|
844
|
+
* 4. 精确值 + 无 primaryKey + 其他(时间戳/datetime/null等)→ 保留原值
|
|
845
|
+
*
|
|
846
|
+
* **示例**:
|
|
847
|
+
* ```typescript
|
|
848
|
+
* // 有主键 + 精确值 → 清理
|
|
849
|
+
* { order_id: 'ORD001', created_at: '2024-01-15' }
|
|
850
|
+
* → { order_id: 'ORD001' }
|
|
793
851
|
*
|
|
794
|
-
*
|
|
852
|
+
* // 无主键 + 日期粒度字符串 → 转换为范围
|
|
853
|
+
* { status: 'paid', created_at: '2024-01' }
|
|
854
|
+
* → { status: 'paid', created_at: { $gte: '2024-01-01 00:00:00', $lte: '2024-01-31 23:59:59' } }
|
|
855
|
+
*
|
|
856
|
+
* // 操作符表达式 → 保留
|
|
857
|
+
* { order_id: 'ORD001', created_at: { $gte: '2024-01', $lte: '2024-06' } }
|
|
858
|
+
* → 不做任何处理
|
|
859
|
+
* ```
|
|
795
860
|
*
|
|
796
861
|
* @param reqJson 请求参数
|
|
797
|
-
* @returns
|
|
862
|
+
* @returns 处理后的请求参数
|
|
798
863
|
*/
|
|
799
|
-
private
|
|
800
|
-
const
|
|
801
|
-
if (!orderBy) return false;
|
|
864
|
+
private cleanTimeColumnForSingleTableQuery(reqJson: IRequestModel): IRequestModel {
|
|
865
|
+
const { primaryKey, timeColumn, type } = this.config;
|
|
802
866
|
|
|
803
|
-
|
|
804
|
-
|
|
867
|
+
// 只有时间分表且配置了时间字段才需要处理
|
|
868
|
+
if (!timeColumn || ![ShardingType.YEAR, ShardingType.MONTH, ShardingType.DAY].includes(type)) {
|
|
869
|
+
return reqJson;
|
|
805
870
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
: `${firstItem.fieldName} ${firstItem.orderType || 'ASC'}`;
|
|
811
|
-
return firstItemStr.trim().toUpperCase().endsWith('ASC');
|
|
871
|
+
|
|
872
|
+
const condition = reqJson.condition;
|
|
873
|
+
if (!condition || typeof condition !== 'object') {
|
|
874
|
+
return reqJson;
|
|
812
875
|
}
|
|
813
|
-
|
|
876
|
+
|
|
877
|
+
const timeValue = condition[timeColumn];
|
|
878
|
+
|
|
879
|
+
// timeColumn 不存在 → 不处理
|
|
880
|
+
if (timeValue === undefined) {
|
|
881
|
+
return reqJson;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// 操作符表达式($gte/$lte/$range/$in/$null 等)→ 始终保留,但校验时间精度
|
|
885
|
+
if (isOperatorExpression(timeValue)) {
|
|
886
|
+
this.validateTimeOperatorPrecision(timeValue as any, timeColumn);
|
|
887
|
+
return reqJson;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// 判断是否有主键
|
|
891
|
+
const hasPrimaryKey = primaryKey && condition[primaryKey] !== undefined;
|
|
892
|
+
|
|
893
|
+
// ---- 精确值处理 ----
|
|
894
|
+
|
|
895
|
+
// Date 对象:精确到毫秒,视为精确值
|
|
896
|
+
// number:时间戳,无法推断粒度,视为精确值
|
|
897
|
+
// boolean:视为精确值
|
|
898
|
+
// string:需要检测日期粒度
|
|
899
|
+
|
|
900
|
+
if (typeof timeValue === 'string') {
|
|
901
|
+
// 检测日期字符串粒度
|
|
902
|
+
const range = expandDateToRange(timeValue);
|
|
903
|
+
|
|
904
|
+
if (hasPrimaryKey) {
|
|
905
|
+
// 有主键 → 无论什么粒度的字符串,都清理
|
|
906
|
+
const { [timeColumn]: _, ...cleanedCondition } = condition;
|
|
907
|
+
return { ...reqJson, condition: cleanedCondition };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// 无主键 + 年/月/日粒度 → 转换为 $gte/$lte 范围
|
|
911
|
+
if (range) {
|
|
912
|
+
return {
|
|
913
|
+
...reqJson,
|
|
914
|
+
condition: { ...condition, [timeColumn]: range },
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// 无主键 + datetime 粒度或无法识别的格式 → 保留原值
|
|
919
|
+
return reqJson;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// Date / number / boolean / null 等其他精确值
|
|
923
|
+
if (hasPrimaryKey) {
|
|
924
|
+
// 有主键 → 清理
|
|
925
|
+
const { [timeColumn]: _, ...cleanedCondition } = condition;
|
|
926
|
+
return { ...reqJson, condition: cleanedCondition };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 无主键 → 保留原值
|
|
930
|
+
return reqJson;
|
|
814
931
|
}
|
|
815
932
|
|
|
816
933
|
/**
|
|
817
|
-
*
|
|
934
|
+
* 校验时间操作符的值必须精确到秒
|
|
818
935
|
*
|
|
819
|
-
*
|
|
820
|
-
* -
|
|
821
|
-
*
|
|
936
|
+
* MySQL 中 '2026-04-30' 等价于 '2026-04-30 00:00:00',
|
|
937
|
+
* 导致 $lte: '2026-04-30' 会丢失当天所有数据。
|
|
938
|
+
* 如果用户需要查整月/整天数据,应传粒度字符串由系统自动转换,而非手动写 $gte/$lte。
|
|
822
939
|
*
|
|
823
|
-
* @param
|
|
824
|
-
* @param
|
|
825
|
-
* @
|
|
940
|
+
* @param operatorValue 操作符表达式的值
|
|
941
|
+
* @param timeColumn 时间字段名
|
|
942
|
+
* @throws Error 如果操作符值缺少时间部分
|
|
826
943
|
*/
|
|
944
|
+
private validateTimeOperatorPrecision(operatorValue: Record<string, any>, timeColumn: string): void {
|
|
945
|
+
// 需要校验精度的操作符
|
|
946
|
+
const PRECISION_OPERATORS = ['$gte', '$lte', '$gt', '$lt'];
|
|
947
|
+
|
|
948
|
+
for (const op of PRECISION_OPERATORS) {
|
|
949
|
+
const value = operatorValue[op];
|
|
950
|
+
if (value !== undefined && typeof value === 'string') {
|
|
951
|
+
// 匹配日期格式但缺少时间部分:'2026-04-30'
|
|
952
|
+
// datetime 格式 '2026-04-30 00:00:00' 不应被拦截
|
|
953
|
+
const dateOnlyPattern = /^\d{4}-\d{1,2}-\d{1,2}$/;
|
|
954
|
+
if (dateOnlyPattern.test(value)) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`[ShardingCrudPro] 时间字段 '${timeColumn}' 的 ${op} 值必须精确到秒,` +
|
|
957
|
+
`当前值: '${value}'。` +
|
|
958
|
+
`请使用 '${value} 23:59:59'($lte/$lt)或 '${value} 00:00:00'($gte/$gt),` +
|
|
959
|
+
`或直接传粒度字符串(如 '${value.substring(0, 7)}' 或 '${value}')由系统自动转换范围。`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// $range 操作符校验:[start, end] 两个值都必须精确到秒
|
|
966
|
+
const rangeValue = operatorValue.$range;
|
|
967
|
+
if (Array.isArray(rangeValue) && rangeValue.length >= 2) {
|
|
968
|
+
const dateOnlyPattern = /^\d{4}-\d{1,2}-\d{1,2}$/;
|
|
969
|
+
for (let i = 0; i < rangeValue.length; i++) {
|
|
970
|
+
const val = rangeValue[i];
|
|
971
|
+
if (typeof val === 'string' && dateOnlyPattern.test(val)) {
|
|
972
|
+
throw new Error(
|
|
973
|
+
`[ShardingCrudPro] 时间字段 '${timeColumn}' 的 $range[${i}] 值必须精确到秒,` +
|
|
974
|
+
`当前值: '${val}'。` +
|
|
975
|
+
`请使用 '${val} ${i === 0 ? '00:00:00' : '23:59:59'}',` +
|
|
976
|
+
`或直接传粒度字符串由系统自动转换范围。`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private validateFindOrderBy(reqJson: IRequestModel): void {
|
|
984
|
+
// 非时间分表不强制 orderBy 约束(多表合并排序由调用方自行保证)
|
|
985
|
+
const timeColumn = this.config.timeColumn;
|
|
986
|
+
if (!timeColumn) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const orderBy = reqJson.orderBy;
|
|
991
|
+
|
|
992
|
+
// 1. 必须传 orderBy
|
|
993
|
+
if (!orderBy) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`[ShardingCrudPro] 查询操作必须传 orderBy 参数。` +
|
|
996
|
+
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// 2. 使用工具类解析并校验首个排序字段是否为 timeColumn
|
|
1001
|
+
const firstOrderBy = OrderByUtils.getFirstOrderBy(orderBy);
|
|
1002
|
+
|
|
1003
|
+
if (!firstOrderBy) {
|
|
1004
|
+
throw new Error(
|
|
1005
|
+
`[ShardingCrudPro] orderBy 参数格式错误,无法解析。` +
|
|
1006
|
+
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (firstOrderBy.fieldName !== timeColumn) {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
`[ShardingCrudPro] orderBy 首个排序字段必须为 '${timeColumn}',` +
|
|
1013
|
+
`当前值: '${firstOrderBy.fieldName}'`
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// 校验排序方向是否为 ASC 或 DESC
|
|
1018
|
+
const orderType = firstOrderBy.orderType.toUpperCase();
|
|
1019
|
+
if (orderType !== 'ASC' && orderType !== 'DESC') {
|
|
1020
|
+
throw new Error(
|
|
1021
|
+
`[ShardingCrudPro] orderBy 排序方向必须是 'ASC' 或 'DESC',` +
|
|
1022
|
+
`当前值: '${firstOrderBy.orderType}'`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
private isAscOrderBy(reqJson: IRequestModel): boolean {
|
|
1028
|
+
return OrderByUtils.isFirstOrderByAsc(reqJson.orderBy);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
827
1031
|
private sortTablesForOrderBy(tables: string[], reqJson: IRequestModel): string[] {
|
|
828
1032
|
const isAsc = this.isAscOrderBy(reqJson);
|
|
829
1033
|
return [...tables].sort((a, b) =>
|
|
@@ -831,15 +1035,60 @@ export class ShardingCrudPro {
|
|
|
831
1035
|
);
|
|
832
1036
|
}
|
|
833
1037
|
|
|
834
|
-
// ============
|
|
1038
|
+
// ============ 辅助方法 ============
|
|
835
1039
|
|
|
836
1040
|
/**
|
|
837
|
-
*
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1041
|
+
* 构建辅助定位信息
|
|
1042
|
+
*/
|
|
1043
|
+
private buildDebugInfo(sqlTable: string | undefined, condition: Record<string, any> | undefined): { sqlDatabase: string; sqlTable: string; condition?: Record<string, any> } {
|
|
1044
|
+
const info: { sqlDatabase: string; sqlTable: string; condition?: Record<string, any> } = {
|
|
1045
|
+
sqlDatabase: this.baseCfg.sqlDatabase || 'unknown',
|
|
1046
|
+
sqlTable: sqlTable || this.config.baseTable,
|
|
1047
|
+
};
|
|
1048
|
+
if (condition) {
|
|
1049
|
+
info.condition = condition;
|
|
1050
|
+
}
|
|
1051
|
+
return info;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* 格式化唯一性错误消息
|
|
841
1056
|
*/
|
|
842
|
-
private
|
|
1057
|
+
private formatUniqueError(
|
|
1058
|
+
foundCount: number,
|
|
1059
|
+
tables: string | string[],
|
|
1060
|
+
condition: Record<string, any> | undefined,
|
|
1061
|
+
isMultiTable: boolean = false
|
|
1062
|
+
): string {
|
|
1063
|
+
const parts = [
|
|
1064
|
+
`[ShardingCrudPro] findUniqueOne 期望唯一一条记录,但查询到 ${foundCount} 条`,
|
|
1065
|
+
];
|
|
1066
|
+
|
|
1067
|
+
if (this.baseCfg.sqlDatabase) {
|
|
1068
|
+
parts.push(`数据库: ${this.baseCfg.sqlDatabase}`);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (isMultiTable && Array.isArray(tables)) {
|
|
1072
|
+
parts.push(`基表: ${this.config.baseTable}`);
|
|
1073
|
+
parts.push(`分表: [${tables.join(', ')}]`);
|
|
1074
|
+
} else {
|
|
1075
|
+
parts.push(`表: ${typeof tables === 'string' ? tables : tables[0]}`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (condition) {
|
|
1079
|
+
try {
|
|
1080
|
+
parts.push(`条件: ${JSON.stringify(condition)}`);
|
|
1081
|
+
} catch {
|
|
1082
|
+
parts.push(`条件: [无法序列化]`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return parts.join(' | ');
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ============ 查询表解析(委托给 ShardingRouter) ============
|
|
1090
|
+
|
|
1091
|
+
private async resolveFindTables(reqJson: IRequestModel): Promise<string[]> {
|
|
843
1092
|
const context: IShardingRouterContext = {
|
|
844
1093
|
config: this.config,
|
|
845
1094
|
condition: reqJson.condition,
|
|
@@ -853,4 +1102,4 @@ export class ShardingCrudPro {
|
|
|
853
1102
|
// 委托给 ShardingRouter 处理查询路由
|
|
854
1103
|
return this.router.resolveQuery(this.config, context, tableInfoProvider);
|
|
855
1104
|
}
|
|
856
|
-
}
|
|
1105
|
+
}
|