midway-fatcms 0.0.5 → 0.0.7
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-crud/SKILL.md +375 -0
- package/.qoder/skills/midway-fatcms-crud/examples.md +990 -0
- package/.qoder/skills/midway-fatcms-crud/reference.md +568 -0
- package/README.md +377 -134
- package/dist/controller/manage/CrudStandardDesignApi.d.ts +0 -2
- package/dist/controller/manage/CrudStandardDesignApi.js +11 -85
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/libs/crud-pro/CrudPro.d.ts +9 -1
- package/dist/libs/crud-pro/CrudPro.js +15 -0
- package/dist/libs/crud-pro/README.md +809 -0
- package/dist/libs/crud-pro/README_FUNC.md +193 -0
- package/dist/libs/crud-pro/exceptions.d.ts +2 -0
- package/dist/libs/crud-pro/exceptions.js +2 -0
- package/dist/libs/crud-pro/interfaces.d.ts +34 -1
- package/dist/libs/crud-pro/models/ExecuteContext.d.ts +3 -3
- package/dist/libs/crud-pro/models/ExecuteContext.js +2 -0
- package/dist/libs/crud-pro/models/RequestModel.d.ts +6 -2
- package/dist/libs/crud-pro/models/RequestModel.js +20 -53
- package/dist/libs/crud-pro/models/ResModel.d.ts +6 -4
- package/dist/libs/crud-pro/models/ServiceHub.d.ts +1 -0
- package/dist/libs/crud-pro/models/keys.d.ts +6 -1
- package/dist/libs/crud-pro/models/keys.js +5 -0
- package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +52 -0
- package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +158 -0
- package/dist/libs/crud-pro/services/CrudProExecuteSqlService.js +20 -1
- package/dist/libs/crud-pro/services/CrudProFieldValidateService.d.ts +7 -0
- package/dist/libs/crud-pro/services/CrudProFieldValidateService.js +32 -0
- package/dist/libs/crud-pro/services/CrudProGenSqlService.d.ts +13 -0
- package/dist/libs/crud-pro/services/CrudProGenSqlService.js +44 -7
- package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.d.ts +43 -0
- package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.js +132 -1
- package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +15 -1
- package/dist/libs/crud-pro/services/CrudProTableMetaService.js +107 -0
- package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +5 -1
- package/dist/libs/crud-pro/services/CurdProServiceHub.js +11 -0
- package/dist/libs/crud-pro/utils/DateTimeUtils.d.ts +1 -0
- package/dist/libs/crud-pro/utils/DateTimeUtils.js +3 -0
- package/dist/libs/crud-pro/utils/MixinUtils.d.ts +32 -0
- package/dist/libs/crud-pro/utils/MixinUtils.js +85 -1
- 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/utils/ValidateUtils.js +1 -1
- package/dist/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
- package/dist/libs/crud-sharding/ShardingConfig.d.ts +218 -0
- package/dist/libs/crud-sharding/ShardingConfig.js +32 -0
- package/dist/libs/crud-sharding/ShardingCountCache.d.ts +69 -0
- package/dist/libs/crud-sharding/ShardingCountCache.js +160 -0
- package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +363 -0
- package/dist/libs/crud-sharding/ShardingCrudPro.js +675 -0
- package/dist/libs/crud-sharding/ShardingMerger.d.ts +130 -0
- package/dist/libs/crud-sharding/ShardingMerger.js +282 -0
- package/dist/libs/crud-sharding/ShardingRouter.d.ts +69 -0
- package/dist/libs/crud-sharding/ShardingRouter.js +377 -0
- package/dist/libs/crud-sharding/ShardingTableCreator.d.ts +146 -0
- package/dist/libs/crud-sharding/ShardingTableCreator.js +805 -0
- package/dist/libs/crud-sharding/ShardingUtils.d.ts +38 -0
- package/dist/libs/crud-sharding/ShardingUtils.js +77 -0
- package/dist/libs/crud-sharding/index.d.ts +45 -0
- package/dist/libs/crud-sharding/index.js +55 -0
- package/dist/models/StandardColumns.d.ts +71 -0
- package/dist/models/StandardColumns.js +28 -0
- package/dist/service/SysAppService.js +2 -2
- package/dist/service/SysConfigService.js +1 -1
- package/dist/service/SysDictDataService.js +2 -2
- package/dist/service/SysMenuService.js +1 -1
- package/dist/service/UserAccountService.d.ts +1 -1
- package/dist/service/crudstd/CrudStdService.d.ts +0 -1
- package/dist/service/crudstd/CrudStdService.js +0 -27
- package/dist/service/curd/CrudProQuick.d.ts +134 -4
- package/dist/service/curd/CrudProQuick.js +155 -3
- package/dist/service/curd/CurdMixService.d.ts +2 -1
- package/dist/service/curd/CurdMixService.js +5 -1
- package/dist/service/curd/CurdProService.d.ts +44 -2
- package/dist/service/curd/CurdProService.js +53 -1
- package/dist/service/curd/README.md +1100 -0
- package/dist/service/curd/fixSoftDelete.d.ts +14 -0
- package/dist/service/curd/fixSoftDelete.js +29 -11
- package/dist/service/flow/FlowConfigService.js +1 -1
- package/dist/service/flow/FlowInstanceCrudService.js +1 -1
- package/package.json +4 -1
- package/src/controller/gateway/AsyncTaskController.ts +1 -1
- package/src/controller/manage/CrudStandardDesignApi.ts +16 -100
- package/src/index.ts +3 -0
- package/src/libs/crud-pro/CrudPro.ts +19 -1
- package/src/libs/crud-pro/README.md +809 -0
- package/src/libs/crud-pro/README_FUNC.md +193 -0
- package/src/libs/crud-pro/exceptions.ts +2 -0
- package/src/libs/crud-pro/interfaces.ts +38 -1
- package/src/libs/crud-pro/models/ExecuteContext.ts +6 -3
- package/src/libs/crud-pro/models/RequestModel.ts +23 -65
- package/src/libs/crud-pro/models/ResModel.ts +10 -4
- package/src/libs/crud-pro/models/ServiceHub.ts +2 -0
- package/src/libs/crud-pro/models/keys.ts +5 -0
- package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +171 -0
- package/src/libs/crud-pro/services/CrudProExecuteSqlService.ts +24 -1
- package/src/libs/crud-pro/services/CrudProFieldValidateService.ts +53 -1
- package/src/libs/crud-pro/services/CrudProGenSqlService.ts +51 -7
- package/src/libs/crud-pro/services/CrudProOriginToExecuteSql.ts +159 -2
- package/src/libs/crud-pro/services/CrudProTableMetaService.ts +139 -1
- package/src/libs/crud-pro/services/CurdProServiceHub.ts +16 -1
- package/src/libs/crud-pro/utils/DateTimeUtils.ts +3 -0
- package/src/libs/crud-pro/utils/MixinUtils.ts +97 -1
- package/src/libs/crud-pro/utils/OrderByUtils.ts +169 -0
- package/src/libs/crud-pro/utils/ValidateUtils.ts +1 -1
- package/src/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
- package/src/libs/crud-sharding/ShardingConfig.ts +240 -0
- package/src/libs/crud-sharding/ShardingCountCache.ts +200 -0
- package/src/libs/crud-sharding/ShardingCrudPro.ts +835 -0
- package/src/libs/crud-sharding/ShardingMerger.ts +384 -0
- package/src/libs/crud-sharding/ShardingRouter.ts +512 -0
- package/src/libs/crud-sharding/ShardingTableCreator.ts +1007 -0
- package/src/libs/crud-sharding/ShardingUtils.ts +84 -0
- package/src/libs/crud-sharding/index.ts +64 -0
- package/src/models/StandardColumns.ts +76 -0
- package/src/service/FileCenterService.ts +1 -1
- package/src/service/SysAppService.ts +2 -2
- package/src/service/SysConfigService.ts +1 -1
- package/src/service/SysDictDataService.ts +2 -2
- package/src/service/SysMenuService.ts +2 -2
- package/src/service/WorkbenchService.ts +1 -1
- package/src/service/anyapi/AnyApiService.ts +1 -1
- package/src/service/asyncTask/AsyncTaskRunnerService.ts +1 -1
- package/src/service/crudstd/CrudStdService.ts +0 -32
- package/src/service/curd/CrudProQuick.ts +164 -5
- package/src/service/curd/CurdMixService.ts +7 -2
- package/src/service/curd/CurdProService.ts +62 -3
- package/src/service/curd/README.md +1100 -0
- package/src/service/curd/fixCfgModel.ts +1 -2
- package/src/service/curd/fixSoftDelete.ts +38 -16
- package/src/service/flow/FlowConfigService.ts +1 -1
- package/src/service/flow/FlowInstanceCrudService.ts +1 -1
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import * as moment from 'moment';
|
|
2
|
+
import { ShardingType, IShardingConfig, IShardingRouterContext, IShardingTimeRange } from './ShardingConfig';
|
|
3
|
+
import { ShardingTimeGranularity, getTimeSuffix } from './ShardingUtils';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 空值合并(兼容低版本 Node.js)
|
|
7
|
+
* 返回第一个非 null/undefined 的值
|
|
8
|
+
*/
|
|
9
|
+
function coalesce(a: any, b: any): any {
|
|
10
|
+
return a !== null && a !== undefined ? a : b;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 表信息提供者接口
|
|
15
|
+
* 用于获取数据库中真实存在的表
|
|
16
|
+
*/
|
|
17
|
+
export interface ITableInfoProvider {
|
|
18
|
+
/**
|
|
19
|
+
* 获取所有真实存在的表名
|
|
20
|
+
* @returns 表名集合
|
|
21
|
+
*/
|
|
22
|
+
getExistingTables(): Promise<Set<string>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 分表路由器
|
|
27
|
+
*/
|
|
28
|
+
export class ShardingRouter {
|
|
29
|
+
// ============ Public 方法 ============
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 插入类操作专用路由(从 data 提取字段)
|
|
33
|
+
*/
|
|
34
|
+
public resolveForInsert(
|
|
35
|
+
config: IShardingConfig,
|
|
36
|
+
context: IShardingRouterContext
|
|
37
|
+
): string {
|
|
38
|
+
this.validateConfig(config);
|
|
39
|
+
|
|
40
|
+
switch (config.type) {
|
|
41
|
+
case ShardingType.YEAR:
|
|
42
|
+
return this.resolveTimeForInsert(config, context, 'year');
|
|
43
|
+
case ShardingType.MONTH:
|
|
44
|
+
return this.resolveTimeForInsert(config, context, 'month');
|
|
45
|
+
case ShardingType.DAY:
|
|
46
|
+
return this.resolveTimeForInsert(config, context, 'day');
|
|
47
|
+
case ShardingType.RANGE:
|
|
48
|
+
return this.resolveRangeForInsert(config, context);
|
|
49
|
+
case ShardingType.HASH:
|
|
50
|
+
return this.resolveHashForInsert(config, context);
|
|
51
|
+
case ShardingType.CUSTOM:
|
|
52
|
+
return this.resolveCustomForInsert(config, context);
|
|
53
|
+
default:
|
|
54
|
+
return config.baseTable;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 条件类操作专用路由(从 condition 提取字段)
|
|
60
|
+
*/
|
|
61
|
+
public resolveForCondition(
|
|
62
|
+
config: IShardingConfig,
|
|
63
|
+
context: IShardingRouterContext
|
|
64
|
+
): string | string[] {
|
|
65
|
+
this.validateConfig(config);
|
|
66
|
+
|
|
67
|
+
switch (config.type) {
|
|
68
|
+
case ShardingType.YEAR:
|
|
69
|
+
return this.resolveTimeForCondition(config, context, 'year');
|
|
70
|
+
case ShardingType.MONTH:
|
|
71
|
+
return this.resolveTimeForCondition(config, context, 'month');
|
|
72
|
+
case ShardingType.DAY:
|
|
73
|
+
return this.resolveTimeForCondition(config, context, 'day');
|
|
74
|
+
case ShardingType.RANGE:
|
|
75
|
+
return this.resolveRangeForCondition(config, context);
|
|
76
|
+
case ShardingType.HASH:
|
|
77
|
+
return this.resolveHashForCondition(config, context);
|
|
78
|
+
case ShardingType.CUSTOM:
|
|
79
|
+
return this.resolveCustomForCondition(config, context);
|
|
80
|
+
default:
|
|
81
|
+
return config.baseTable;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 查询专用路由(带表存在性过滤)
|
|
87
|
+
*/
|
|
88
|
+
public async resolveQuery(
|
|
89
|
+
config: IShardingConfig,
|
|
90
|
+
context: IShardingRouterContext,
|
|
91
|
+
tableInfoProvider: ITableInfoProvider
|
|
92
|
+
): Promise<string[]> {
|
|
93
|
+
const existingTables = await tableInfoProvider.getExistingTables();
|
|
94
|
+
|
|
95
|
+
// 时间分表:从真实存在的表中取最近N张,再与条件取交集
|
|
96
|
+
if (this.isTimeSharding(config)) {
|
|
97
|
+
return this.getRecentExistingTablesFromAll(existingTables, config, context.condition);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 非时间分表:先计算候选表,再过滤存在的表
|
|
101
|
+
const candidateResult = this.resolveForCondition(config, context);
|
|
102
|
+
const candidateTables = Array.isArray(candidateResult)
|
|
103
|
+
? candidateResult
|
|
104
|
+
: [candidateResult];
|
|
105
|
+
|
|
106
|
+
return this.filterExistingTables(candidateTables, existingTables);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 过滤出真实存在的分表
|
|
111
|
+
*/
|
|
112
|
+
public filterExistingTables(
|
|
113
|
+
candidateTables: string | string[],
|
|
114
|
+
existingTables: Set<string>
|
|
115
|
+
): string[] {
|
|
116
|
+
const candidates = Array.isArray(candidateTables) ? candidateTables : [candidateTables];
|
|
117
|
+
return candidates.filter(table => existingTables.has(table));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============ 配置校验 ============
|
|
121
|
+
|
|
122
|
+
private validateConfig(config: IShardingConfig): void {
|
|
123
|
+
if (!config.baseTable) {
|
|
124
|
+
throw new Error('[ShardingRouter] baseTable 不能为空');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if ((config.type === ShardingType.RANGE || config.type === ShardingType.HASH) && !config.shardingColumn) {
|
|
128
|
+
throw new Error(`[ShardingRouter] ${config.type} 分表必须指定 shardingColumn`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (config.type === ShardingType.CUSTOM && !config.customRouter) {
|
|
132
|
+
throw new Error('[ShardingRouter] CUSTOM 分表必须提供 customRouter 函数');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============ 时间分表路由 ============
|
|
137
|
+
|
|
138
|
+
private resolveTimeForInsert(
|
|
139
|
+
config: IShardingConfig,
|
|
140
|
+
context: IShardingRouterContext,
|
|
141
|
+
granularity: ShardingTimeGranularity
|
|
142
|
+
): string {
|
|
143
|
+
const { baseTable, timeColumn } = config;
|
|
144
|
+
const timeValue = this.extractTimeFromData(context.data, timeColumn!);
|
|
145
|
+
const suffix = getTimeSuffix(timeValue, granularity);
|
|
146
|
+
return this.formatTableName(baseTable, suffix, config);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private resolveTimeForCondition(
|
|
150
|
+
config: IShardingConfig,
|
|
151
|
+
context: IShardingRouterContext,
|
|
152
|
+
granularity: ShardingTimeGranularity
|
|
153
|
+
): string | string[] {
|
|
154
|
+
const { baseTable, timeColumn } = config;
|
|
155
|
+
const condition = context.condition || {};
|
|
156
|
+
|
|
157
|
+
const timeRange = this.extractTimeRange(condition, timeColumn!);
|
|
158
|
+
const singleTimeValue = this.tryExtractTimeFromCondition(condition, timeColumn!);
|
|
159
|
+
|
|
160
|
+
if (timeRange) {
|
|
161
|
+
return this.generateTablesInRange(baseTable, timeRange.start, timeRange.end, granularity, config);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (singleTimeValue) {
|
|
165
|
+
const suffix = getTimeSuffix(singleTimeValue, granularity);
|
|
166
|
+
return this.formatTableName(baseTable, suffix, config);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const recentCount = this.getRecentCountByGranularity(granularity, config);
|
|
170
|
+
return this.getRecentTables(baseTable, granularity, recentCount, config);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============ 范围分表路由 ============
|
|
174
|
+
|
|
175
|
+
private resolveRangeForInsert(config: IShardingConfig, context: IShardingRouterContext): string {
|
|
176
|
+
const { baseTable, tableCount = 10, shardingColumn } = config;
|
|
177
|
+
const value = this.extractColumnFromData(context.data, shardingColumn);
|
|
178
|
+
const index = this.calculateRangeIndex(value, tableCount);
|
|
179
|
+
return this.formatTableName(baseTable, String(index), config);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private resolveRangeForCondition(config: IShardingConfig, context: IShardingRouterContext): string {
|
|
183
|
+
const { baseTable, tableCount = 10, shardingColumn } = config;
|
|
184
|
+
const value = this.extractColumnFromCondition(context.condition, shardingColumn);
|
|
185
|
+
const index = this.calculateRangeIndex(value, tableCount);
|
|
186
|
+
return this.formatTableName(baseTable, String(index), config);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ============ 哈希分表路由 ============
|
|
190
|
+
|
|
191
|
+
private resolveHashForInsert(config: IShardingConfig, context: IShardingRouterContext): string {
|
|
192
|
+
const { baseTable, tableCount = 16, shardingColumn } = config;
|
|
193
|
+
const value = this.extractColumnFromData(context.data, shardingColumn);
|
|
194
|
+
const index = this.calculateHash(value, tableCount);
|
|
195
|
+
return this.formatTableName(baseTable, String(index).padStart(2, '0'), config);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private resolveHashForCondition(config: IShardingConfig, context: IShardingRouterContext): string {
|
|
199
|
+
const { baseTable, tableCount = 16, shardingColumn } = config;
|
|
200
|
+
const value = this.extractColumnFromCondition(context.condition, shardingColumn);
|
|
201
|
+
const index = this.calculateHash(value, tableCount);
|
|
202
|
+
return this.formatTableName(baseTable, String(index).padStart(2, '0'), config);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ============ 自定义分表路由 ============
|
|
206
|
+
|
|
207
|
+
private resolveCustomForInsert(config: IShardingConfig, context: IShardingRouterContext): string {
|
|
208
|
+
const result = config.customRouter!(context);
|
|
209
|
+
return Array.isArray(result) ? result[0] : result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private resolveCustomForCondition(config: IShardingConfig, context: IShardingRouterContext): string | string[] {
|
|
213
|
+
return config.customRouter!(context);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============ 查询相关私有方法 ============
|
|
217
|
+
|
|
218
|
+
private isTimeSharding(config: IShardingConfig): boolean {
|
|
219
|
+
return [ShardingType.YEAR, ShardingType.MONTH, ShardingType.DAY].includes(config.type);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private getRecentExistingTablesFromAll(
|
|
223
|
+
existingTables: Set<string>,
|
|
224
|
+
config: IShardingConfig,
|
|
225
|
+
condition?: Record<string, any>
|
|
226
|
+
): string[] {
|
|
227
|
+
const baseTablePrefix = `${config.baseTable}_`;
|
|
228
|
+
const shardingTables = Array.from(existingTables).filter(t =>
|
|
229
|
+
t.startsWith(baseTablePrefix)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
if (shardingTables.length === 0) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
shardingTables.sort();
|
|
237
|
+
|
|
238
|
+
// 配置了 recentTableCount:取最近N张,再与条件取交集
|
|
239
|
+
if (config.recentTableCount !== undefined) {
|
|
240
|
+
const recentTables = shardingTables.slice(-config.recentTableCount);
|
|
241
|
+
return this.intersectWithCondition(recentTables, config, condition);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 未配置 recentTableCount:所有表与条件取交集,最多取3张
|
|
245
|
+
const intersected = this.intersectWithCondition(shardingTables, config, condition);
|
|
246
|
+
return intersected.slice(0, 3);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private intersectWithCondition(
|
|
250
|
+
tables: string[],
|
|
251
|
+
config: IShardingConfig,
|
|
252
|
+
condition?: Record<string, any>
|
|
253
|
+
): string[] {
|
|
254
|
+
if (!condition) {
|
|
255
|
+
return this.sortTablesByTimeDesc(tables);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const timeColumn = config.timeColumn!;
|
|
259
|
+
const timeRange = this.extractTimeRange(condition, timeColumn);
|
|
260
|
+
if (timeRange) {
|
|
261
|
+
return this.intersectTablesByTimeRange(tables, timeRange, config);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const singleTimeValue = this.tryExtractTimeFromCondition(condition, timeColumn);
|
|
265
|
+
if (singleTimeValue) {
|
|
266
|
+
const suffix = getTimeSuffix(singleTimeValue, config.type as any);
|
|
267
|
+
const targetTable = `${config.baseTable}_${suffix}`;
|
|
268
|
+
return tables.includes(targetTable) ? [targetTable] : [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return this.sortTablesByTimeDesc(tables);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private intersectTablesByTimeRange(
|
|
275
|
+
tables: string[],
|
|
276
|
+
timeRange: IShardingTimeRange,
|
|
277
|
+
config: IShardingConfig
|
|
278
|
+
): string[] {
|
|
279
|
+
const startSuffix = getTimeSuffix(timeRange.start, config.type as any);
|
|
280
|
+
const endSuffix = getTimeSuffix(timeRange.end, config.type as any);
|
|
281
|
+
const baseTablePrefix = `${config.baseTable}_`;
|
|
282
|
+
|
|
283
|
+
const prefixLen = baseTablePrefix.length;
|
|
284
|
+
return this.sortTablesByTimeDesc(tables.filter(table => {
|
|
285
|
+
const suffix = table.substring(prefixLen);
|
|
286
|
+
return suffix >= startSuffix && suffix <= endSuffix;
|
|
287
|
+
}));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============ 排序 ============
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 将分表按时间后缀降序排列(新→旧)
|
|
294
|
+
*
|
|
295
|
+
* 分表名格式为 baseTable_suffix,后缀为时间格式(如 202403、20240101),
|
|
296
|
+
* 字典序即时间序,降序即新→旧。
|
|
297
|
+
*/
|
|
298
|
+
private sortTablesByTimeDesc(tables: string[]): string[] {
|
|
299
|
+
return [...tables].sort((a, b) => b.localeCompare(a));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============ 值提取:基础方法 ============
|
|
303
|
+
|
|
304
|
+
private extractFromData(data: any, column: string): any {
|
|
305
|
+
return data?.[column];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private extractFromCondition(condition: any, column: string): any {
|
|
309
|
+
return condition?.[column];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private parseTimeValue(value: any, source: string, operation: string): Date {
|
|
313
|
+
if (value === undefined || value === null) {
|
|
314
|
+
throw new Error(`[ShardingRouter] ${operation} 操作无法从 ${source} 中获取时间分表字段的值`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const m = moment(value);
|
|
318
|
+
if (!m.isValid()) {
|
|
319
|
+
throw new Error(`[ShardingRouter] 无法解析时间分表字段的值:${value}`);
|
|
320
|
+
}
|
|
321
|
+
return m.toDate();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private parseColumnValue(value: any, source: string, operation: string): any {
|
|
325
|
+
if (value === undefined || value === null) {
|
|
326
|
+
throw new Error(`[ShardingRouter] ${operation} 操作无法从 ${source} 中获取分表字段的值`);
|
|
327
|
+
}
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ============ 值提取:时间字段 ============
|
|
332
|
+
|
|
333
|
+
private extractTimeFromData(data: any, column: string): Date {
|
|
334
|
+
const value = this.extractFromData(data, column);
|
|
335
|
+
return this.parseTimeValue(value, 'data', 'insert');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private tryExtractTimeFromCondition(condition: any, column: string): Date | null {
|
|
339
|
+
const value = this.extractFromCondition(condition, column);
|
|
340
|
+
if (value === undefined || value === null) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const m = moment(value);
|
|
345
|
+
if (!m.isValid()) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
return m.toDate();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private extractTimeRange(condition: any, column: string): IShardingTimeRange | null {
|
|
352
|
+
const colCond = condition[column];
|
|
353
|
+
if (!colCond || typeof colCond !== 'object') return null;
|
|
354
|
+
|
|
355
|
+
const start = coalesce(colCond.$gte, colCond.$gt);
|
|
356
|
+
const end = coalesce(colCond.$lte, colCond.$lt);
|
|
357
|
+
|
|
358
|
+
if (start && end) {
|
|
359
|
+
const startMoment = moment(start);
|
|
360
|
+
const endMoment = moment(end);
|
|
361
|
+
if (!startMoment.isValid() || !endMoment.isValid()) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
start: startMoment.toDate(),
|
|
366
|
+
end: endMoment.toDate(),
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (start) {
|
|
371
|
+
const startMoment = moment(start);
|
|
372
|
+
if (!startMoment.isValid()) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
start: startMoment.toDate(),
|
|
377
|
+
end: new Date(),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (end) {
|
|
382
|
+
const endMoment = moment(end);
|
|
383
|
+
if (!endMoment.isValid()) {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
start: new Date(0),
|
|
388
|
+
end: endMoment.toDate(),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ============ 值提取:分表字段 ============
|
|
396
|
+
|
|
397
|
+
private extractColumnFromData(data: any, column: string): any {
|
|
398
|
+
const value = this.extractFromData(data, column);
|
|
399
|
+
return this.parseColumnValue(value, 'data', 'insert');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private extractColumnFromCondition(condition: any, column: string): any {
|
|
403
|
+
const value = this.extractFromCondition(condition, column);
|
|
404
|
+
return this.parseColumnValue(value, 'condition', '查询/更新/删除');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ============ 时间处理 ============
|
|
408
|
+
|
|
409
|
+
private generateTablesInRange(
|
|
410
|
+
baseTable: string,
|
|
411
|
+
start: Date,
|
|
412
|
+
end: Date,
|
|
413
|
+
granularity: ShardingTimeGranularity,
|
|
414
|
+
config: IShardingConfig
|
|
415
|
+
): string[] {
|
|
416
|
+
const tables: string[] = [];
|
|
417
|
+
const current = moment(start);
|
|
418
|
+
const maxTables = 100;
|
|
419
|
+
|
|
420
|
+
while (current.toDate() <= end && tables.length < maxTables) {
|
|
421
|
+
const suffix = getTimeSuffix(current.toDate(), granularity);
|
|
422
|
+
tables.push(this.formatTableName(baseTable, suffix, config));
|
|
423
|
+
|
|
424
|
+
switch (granularity) {
|
|
425
|
+
case 'year':
|
|
426
|
+
current.add(1, 'year');
|
|
427
|
+
break;
|
|
428
|
+
case 'month':
|
|
429
|
+
current.add(1, 'month');
|
|
430
|
+
break;
|
|
431
|
+
case 'day':
|
|
432
|
+
current.add(1, 'day');
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return tables;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private getRecentTables(
|
|
441
|
+
baseTable: string,
|
|
442
|
+
granularity: ShardingTimeGranularity,
|
|
443
|
+
count: number,
|
|
444
|
+
config: IShardingConfig
|
|
445
|
+
): string[] {
|
|
446
|
+
const tables: string[] = [];
|
|
447
|
+
const current = moment();
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < count; i++) {
|
|
450
|
+
const suffix = getTimeSuffix(current.toDate(), granularity);
|
|
451
|
+
tables.unshift(this.formatTableName(baseTable, suffix, config));
|
|
452
|
+
|
|
453
|
+
switch (granularity) {
|
|
454
|
+
case 'year':
|
|
455
|
+
current.subtract(1, 'year');
|
|
456
|
+
break;
|
|
457
|
+
case 'month':
|
|
458
|
+
current.subtract(1, 'month');
|
|
459
|
+
break;
|
|
460
|
+
case 'day':
|
|
461
|
+
current.subtract(1, 'day');
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return tables;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private getRecentCountByGranularity(granularity: ShardingTimeGranularity, config: IShardingConfig): number {
|
|
470
|
+
if (config.recentTableCount !== undefined && config.recentTableCount > 0) {
|
|
471
|
+
return config.recentTableCount;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
switch (granularity) {
|
|
475
|
+
case 'year':
|
|
476
|
+
return 3;
|
|
477
|
+
case 'month':
|
|
478
|
+
return 12;
|
|
479
|
+
case 'day':
|
|
480
|
+
return 7;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ============ 计算相关 ============
|
|
485
|
+
|
|
486
|
+
private calculateRangeIndex(value: any, count: number): number {
|
|
487
|
+
if (typeof value === 'number') {
|
|
488
|
+
return Math.abs(value) % count;
|
|
489
|
+
}
|
|
490
|
+
return Math.abs(this.simpleHash(String(value))) % count;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private calculateHash(value: any, count: number): number {
|
|
494
|
+
return Math.abs(this.simpleHash(String(value))) % count;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private simpleHash(str: string): number {
|
|
498
|
+
let hash = 5381;
|
|
499
|
+
for (let i = 0; i < str.length; i++) {
|
|
500
|
+
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
|
501
|
+
hash = hash & hash;
|
|
502
|
+
}
|
|
503
|
+
return hash;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ============ 格式化 ============
|
|
507
|
+
|
|
508
|
+
private formatTableName(baseTable: string, suffix: string, config: IShardingConfig): string {
|
|
509
|
+
const formattedSuffix = config.suffixFormatter ? config.suffixFormatter(suffix) : suffix;
|
|
510
|
+
return `${baseTable}_${formattedSuffix}`;
|
|
511
|
+
}
|
|
512
|
+
}
|