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
|
@@ -1,879 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ShardingCrudPro = void 0;
|
|
4
|
-
const OrderByUtils_1 = require("../../libs/crud-pro/utils/OrderByUtils");
|
|
5
|
-
const keys_1 = require("../../libs/crud-pro/models/keys");
|
|
6
|
-
const ShardingConfig_1 = require("./ShardingConfig");
|
|
7
|
-
const fixSoftDelete_1 = require("../../libs/crud-pro-quick/fixSoftDelete");
|
|
8
|
-
const ShardingRouter_1 = require("./ShardingRouter");
|
|
9
|
-
const ShardingMerger_1 = require("./ShardingMerger");
|
|
10
|
-
const ShardingTableCreator_1 = require("./ShardingTableCreator");
|
|
11
|
-
const ShardingCountCache_1 = require("./ShardingCountCache");
|
|
12
|
-
const CrudResult_1 = require("../../libs/crud-pro/models/CrudResult");
|
|
13
|
-
const ShardingResult_1 = require("./ShardingResult");
|
|
14
|
-
const ShardingUtils_1 = require("./ShardingUtils");
|
|
15
|
-
/**
|
|
16
|
-
* 分表 CRUD 操作封装器
|
|
17
|
-
*
|
|
18
|
-
* 在 CrudPro 之上封装分表功能,不修改 CrudPro 内部代码。
|
|
19
|
-
* 提供透明的分表路由和多表查询结果合并能力。
|
|
20
|
-
*
|
|
21
|
-
* 支持的分表策略:
|
|
22
|
-
* - YEAR: 按年分表,如 order_2024, order_2025
|
|
23
|
-
* - MONTH: 按月分表,如 order_202401, order_202402
|
|
24
|
-
* - DAY: 按日分表,如 order_20240101
|
|
25
|
-
* - RANGE: 按范围分表,如 user_0 ~ user_99
|
|
26
|
-
* - HASH: 按哈希分表,如 order_01 ~ order_16
|
|
27
|
-
* - CUSTOM: 自定义分表规则
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* // 按月分表
|
|
31
|
-
* const sharding = new ShardingCrudPro(crudPro, {
|
|
32
|
-
* type: ShardingType.MONTH,
|
|
33
|
-
* baseTable: 't_order',
|
|
34
|
-
* timeColumn: 'created_at',
|
|
35
|
-
* });
|
|
36
|
-
*
|
|
37
|
-
* // 插入数据(自动路由到正确分表)
|
|
38
|
-
* await sharding.insert({ data: { order_id: '001', amount: 100, created_at: '2024-03-15' } });
|
|
39
|
-
*
|
|
40
|
-
* // 分页查询(自动合并多表结果)
|
|
41
|
-
* const result = await sharding.findPage({
|
|
42
|
-
* condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
|
|
43
|
-
* pageNo: 1,
|
|
44
|
-
* pageSize: 10,
|
|
45
|
-
* });
|
|
46
|
-
*/
|
|
47
|
-
class ShardingCrudPro {
|
|
48
|
-
constructor(crudProFactory, config) {
|
|
49
|
-
this.baseCfg = {};
|
|
50
|
-
this.enableSoftDelete = false;
|
|
51
|
-
this.config = config;
|
|
52
|
-
// 兼容旧的 CrudPro 实例方式和新的工厂函数方式
|
|
53
|
-
if (typeof crudProFactory === 'function') {
|
|
54
|
-
this.crudProFactory = crudProFactory;
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
throw new Error('[ShardingCrudPro] 请使用 CrudProFactory 工厂函数模式');
|
|
58
|
-
}
|
|
59
|
-
// 时间分表必须显式指定 timeColumn
|
|
60
|
-
if ([ShardingConfig_1.ShardingType.YEAR, ShardingConfig_1.ShardingType.MONTH, ShardingConfig_1.ShardingType.DAY].includes(config.type)) {
|
|
61
|
-
if (!config.timeColumn) {
|
|
62
|
-
throw new Error('[ShardingCrudPro] 时间分表(YEAR/MONTH/DAY)必须显式指定 timeColumn 字段');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
this.router = new ShardingRouter_1.ShardingRouter();
|
|
66
|
-
this.merger = new ShardingMerger_1.ShardingMerger();
|
|
67
|
-
this.tableCreator = new ShardingTableCreator_1.ShardingTableCreator(this.crudProFactory, config);
|
|
68
|
-
// 初始化 COUNT 缓存(如果配置了)
|
|
69
|
-
if (config.countCache) {
|
|
70
|
-
const countCache = new ShardingCountCache_1.ShardingCountCache(config.type, config.baseTable, config.countCache.ttlSeconds, config.countCache.maxSize);
|
|
71
|
-
this.merger.setCountCache(countCache);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
// ============ 配置方法 ============
|
|
75
|
-
setBaseCfg(cfg) {
|
|
76
|
-
this.baseCfg = { ...this.baseCfg, ...cfg };
|
|
77
|
-
return this;
|
|
78
|
-
}
|
|
79
|
-
getConfig() {
|
|
80
|
-
return this.config;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* 设置是否启用软删除
|
|
84
|
-
* @param enable 是否启用软删除
|
|
85
|
-
*/
|
|
86
|
-
setEnableSoftDelete(enable) {
|
|
87
|
-
this.enableSoftDelete = enable;
|
|
88
|
-
return this;
|
|
89
|
-
}
|
|
90
|
-
// ============ 写操作(单表路由) ============
|
|
91
|
-
async insert(reqJson) {
|
|
92
|
-
// 时间分表校验:data 必须包含 timeColumn
|
|
93
|
-
this.validateTimeColumnForData(reqJson, 'insert');
|
|
94
|
-
const targetTable = this.resolveSingleTable(reqJson, 'insert');
|
|
95
|
-
// 确保分表存在(根据配置决定是否自动创建)
|
|
96
|
-
await this.createShardingTableIfNeeded(targetTable);
|
|
97
|
-
const ctx = await this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT);
|
|
98
|
-
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
99
|
-
return new CrudResult_1.CrudWriteResult({
|
|
100
|
-
affectedRows: affected.affectedRows,
|
|
101
|
-
insertId: affected.insertId,
|
|
102
|
-
rawContext: ctx,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
async batchInsert(reqJson) {
|
|
106
|
-
var _a, _b;
|
|
107
|
-
const dataArray = reqJson.data;
|
|
108
|
-
if (!Array.isArray(dataArray) || dataArray.length === 0) {
|
|
109
|
-
throw new Error('[ShardingCrudPro] batchInsert requires non-empty data array');
|
|
110
|
-
}
|
|
111
|
-
// 时间分表校验:每条 data 必须包含 timeColumn
|
|
112
|
-
this.validateTimeColumnForBatchData(dataArray, 'batchInsert');
|
|
113
|
-
// 按分表分组
|
|
114
|
-
const groupedData = this.batchInsertGroupDataByTable(dataArray);
|
|
115
|
-
// 确保所有目标分表存在(根据配置决定是否自动创建)
|
|
116
|
-
const tableNames = Array.from(groupedData.keys());
|
|
117
|
-
for (let i = 0; i < tableNames.length; i++) {
|
|
118
|
-
await this.createShardingTableIfNeeded(tableNames[i]);
|
|
119
|
-
}
|
|
120
|
-
// 串行执行各分表的批量插入
|
|
121
|
-
const results = [];
|
|
122
|
-
let lastContext = null;
|
|
123
|
-
for (const [table, items] of groupedData.entries()) {
|
|
124
|
-
try {
|
|
125
|
-
const ctx = await this.executeOnTable(table, { data: items }, keys_1.KeysOfSimpleSQL.SIMPLE_BATCH_INSERT);
|
|
126
|
-
lastContext = ctx;
|
|
127
|
-
results.push({
|
|
128
|
-
table,
|
|
129
|
-
affected: ((_a = ctx.getResModelItem('affected')) === null || _a === void 0 ? void 0 : _a.affectedRows) || items.length,
|
|
130
|
-
rowCount: items.length,
|
|
131
|
-
error: null,
|
|
132
|
-
context: ctx,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
catch (e) {
|
|
136
|
-
results.push({
|
|
137
|
-
table,
|
|
138
|
-
affected: 0,
|
|
139
|
-
rowCount: items.length,
|
|
140
|
-
error: e,
|
|
141
|
-
context: null,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// 汇总结果
|
|
146
|
-
const tableResults = results.map(r => ({
|
|
147
|
-
table: r.table,
|
|
148
|
-
affected: r.affected,
|
|
149
|
-
rowCount: r.rowCount,
|
|
150
|
-
}));
|
|
151
|
-
const errors = results
|
|
152
|
-
.filter(r => r.error !== null)
|
|
153
|
-
.map(r => ({ table: r.table, error: r.error }));
|
|
154
|
-
const totalAffected = results.reduce((sum, r) => sum + r.affected, 0);
|
|
155
|
-
// Use last successful context, or create a minimal one
|
|
156
|
-
const finalContext = lastContext || ((_b = results.find(r => r.context)) === null || _b === void 0 ? void 0 : _b.context);
|
|
157
|
-
if (!finalContext) {
|
|
158
|
-
throw new Error('[ShardingCrudPro] batchInsert failed - no successful inserts');
|
|
159
|
-
}
|
|
160
|
-
return new ShardingResult_1.ShardingBatchInsertResult({
|
|
161
|
-
totalAffected,
|
|
162
|
-
tableResults,
|
|
163
|
-
tableCount: groupedData.size,
|
|
164
|
-
success: errors.length === 0,
|
|
165
|
-
errors,
|
|
166
|
-
lastContext: finalContext,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
async update(reqJson) {
|
|
170
|
-
// 时间分表校验:condition 必须包含路由字段
|
|
171
|
-
this.validateRoutingFieldForCondition(reqJson, 'update');
|
|
172
|
-
const targetTable = this.resolveSingleTable(reqJson, 'update');
|
|
173
|
-
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
174
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
175
|
-
const ctx = await this.executeOnTable(targetTable, cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_UPDATE);
|
|
176
|
-
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
177
|
-
return new CrudResult_1.CrudWriteResult({
|
|
178
|
-
affectedRows: affected.affectedRows,
|
|
179
|
-
insertId: affected.insertId,
|
|
180
|
-
rawContext: ctx,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
async delete(reqJson) {
|
|
184
|
-
// 时间分表校验:condition 必须包含路由字段
|
|
185
|
-
this.validateRoutingFieldForCondition(reqJson, 'delete');
|
|
186
|
-
const targetTable = this.resolveSingleTable(reqJson, 'delete');
|
|
187
|
-
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
188
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
189
|
-
const ctx = await this.executeOnTable(targetTable, cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_DELETE);
|
|
190
|
-
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
191
|
-
return new CrudResult_1.CrudWriteResult({
|
|
192
|
-
affectedRows: affected.affectedRows,
|
|
193
|
-
insertId: affected.insertId,
|
|
194
|
-
rawContext: ctx,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* 恢复软删除的记录
|
|
199
|
-
* 将 deleted_at 字段重置为 0,deleted_by 重置为空字符串
|
|
200
|
-
*
|
|
201
|
-
* @param reqJson 请求参数,通过 condition 指定要恢复的记录
|
|
202
|
-
* @returns CrudWriteResult 包含 affectedRows 和 getRawContext()
|
|
203
|
-
*
|
|
204
|
-
* @example
|
|
205
|
-
* // 恢复指定 ID 的记录
|
|
206
|
-
* await sharding.restore({ condition: { id: 1 } });
|
|
207
|
-
*
|
|
208
|
-
* // 恢复多条记录(使用 $in 操作符)
|
|
209
|
-
* await sharding.restore({ condition: { id: { $in: [1, 2, 3] } } });
|
|
210
|
-
*
|
|
211
|
-
* // 按条件批量恢复记录
|
|
212
|
-
* await sharding.restore({ condition: { status: 'deleted' } });
|
|
213
|
-
*/
|
|
214
|
-
async restore(reqJson) {
|
|
215
|
-
// 时间分表校验:condition 必须包含路由字段
|
|
216
|
-
this.validateRoutingFieldForCondition(reqJson, 'restore');
|
|
217
|
-
const targetTable = this.resolveSingleTable(reqJson, 'restore');
|
|
218
|
-
if (!reqJson.condition || Object.keys(reqJson.condition).length === 0) {
|
|
219
|
-
throw new Error('[ShardingCrudPro] restore 操作必须指定恢复条件');
|
|
220
|
-
}
|
|
221
|
-
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
222
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
223
|
-
// 构建恢复数据:重置软删除字段
|
|
224
|
-
const restoreReqJson = {
|
|
225
|
-
...cleanedReqJson,
|
|
226
|
-
data: {
|
|
227
|
-
...cleanedReqJson.data,
|
|
228
|
-
deleted_at: 0,
|
|
229
|
-
deleted_by: '',
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
const ctx = await this.executeOnTable(targetTable, restoreReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_UPDATE);
|
|
233
|
-
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
234
|
-
return new CrudResult_1.CrudWriteResult({
|
|
235
|
-
affectedRows: affected.affectedRows,
|
|
236
|
-
insertId: affected.insertId,
|
|
237
|
-
rawContext: ctx,
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
async insertOrUpdate(reqJson) {
|
|
241
|
-
var _a;
|
|
242
|
-
// 时间分表校验:data 必须包含 timeColumn,condition 必须包含路由字段
|
|
243
|
-
this.validateTimeColumnForData(reqJson, 'insertOrUpdate');
|
|
244
|
-
this.validateRoutingFieldForCondition(reqJson, 'insertOrUpdate');
|
|
245
|
-
const targetTable = this.resolveSingleTable(reqJson, 'update'); // 使用 condition 路由
|
|
246
|
-
const ctx = await this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE);
|
|
247
|
-
const resModel = ctx.getResModel();
|
|
248
|
-
return new CrudResult_1.CrudUpsertResult({
|
|
249
|
-
affected: resModel.affected,
|
|
250
|
-
insertAffected: resModel.insert_affected,
|
|
251
|
-
updateAffected: resModel.update_affected,
|
|
252
|
-
isExist: (_a = resModel.is_exist) !== null && _a !== void 0 ? _a : false,
|
|
253
|
-
rawContext: ctx,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
async insertOnDuplicateUpdate(reqJson) {
|
|
257
|
-
// 时间分表校验:data 必须包含 timeColumn,condition 必须包含路由字段
|
|
258
|
-
this.validateTimeColumnForData(reqJson, 'insertOnDuplicateUpdate');
|
|
259
|
-
this.validateRoutingFieldForCondition(reqJson, 'insertOnDuplicateUpdate');
|
|
260
|
-
const targetTable = this.resolveSingleTable(reqJson, 'update'); // 使用 condition 路由
|
|
261
|
-
const ctx = await this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE);
|
|
262
|
-
const affected = ctx.getResModel().affected || { affectedRows: 0 };
|
|
263
|
-
return new CrudResult_1.CrudWriteResult({
|
|
264
|
-
affectedRows: affected.affectedRows,
|
|
265
|
-
insertId: affected.insertId,
|
|
266
|
-
rawContext: ctx,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
// ============ 查询操作(可能多表) ============
|
|
270
|
-
async findOne(reqJson) {
|
|
271
|
-
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
272
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
273
|
-
const targetTables = await this.resolveFindTables(reqJson);
|
|
274
|
-
if (targetTables.length === 0) {
|
|
275
|
-
// Return a result with null row - we need a context for this
|
|
276
|
-
// Since there are no tables to query, we throw an error for now
|
|
277
|
-
throw new Error('[ShardingCrudPro] findOne: no matching tables found');
|
|
278
|
-
}
|
|
279
|
-
// 多表:顺序查询,找到即返回
|
|
280
|
-
let lastCtx = null;
|
|
281
|
-
for (const table of targetTables) {
|
|
282
|
-
try {
|
|
283
|
-
const ctx = await this.executeOnTable(table, cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_ONE);
|
|
284
|
-
lastCtx = ctx;
|
|
285
|
-
const row = ctx.getOneObj();
|
|
286
|
-
if (row) {
|
|
287
|
-
return new CrudResult_1.CrudQueryOneResult({ row: row, rawContext: ctx, debugInfo: this.buildDebugInfo(table, reqJson.condition) });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
catch (e) {
|
|
291
|
-
// 表不存在时跳过,继续查下一张表
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// 未找到,使用最后一次查询的 ctx
|
|
295
|
-
return new CrudResult_1.CrudQueryOneResult({ row: null, rawContext: lastCtx, debugInfo: this.buildDebugInfo(targetTables[0], reqJson.condition) });
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* 查询唯一单条记录(期望结果为 0 条或 1 条)
|
|
299
|
-
*
|
|
300
|
-
* 与 findOne 不同,此方法会校验查询结果的唯一性:
|
|
301
|
-
* - 如果查到 0 条:返回 row = null
|
|
302
|
-
* - 如果查到 1 条:正常返回
|
|
303
|
-
* - 如果查到多条:抛出异常,包含详细的定位信息(含分表名列表)
|
|
304
|
-
*
|
|
305
|
-
* 分表场景的特殊处理:
|
|
306
|
-
* - 单分表:直接查询并校验唯一性
|
|
307
|
-
* - 多分表:顺序查询各分表,累计找到的数量,超过 1 条立即抛异常
|
|
308
|
-
*
|
|
309
|
-
* @param reqJson 请求参数
|
|
310
|
-
* @returns CrudQueryOneResult 包含 row、found 和 getRawContext()
|
|
311
|
-
* @throws Error 如果查询到多条记录
|
|
312
|
-
*
|
|
313
|
-
* @example
|
|
314
|
-
* // 根据唯一索引查询单条
|
|
315
|
-
* const result = await sharding.findUniqueOne({ condition: { order_id: 'ORD001' } });
|
|
316
|
-
* if (result.found) {
|
|
317
|
-
* console.log(result.row);
|
|
318
|
-
* }
|
|
319
|
-
*/
|
|
320
|
-
async findUniqueOne(reqJson) {
|
|
321
|
-
// 清理 condition 中的时间字段(如果能确定单一分表)
|
|
322
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
323
|
-
const targetTables = await this.resolveFindTables(reqJson);
|
|
324
|
-
if (targetTables.length === 0) {
|
|
325
|
-
// 无匹配表,返回空结果
|
|
326
|
-
return new CrudResult_1.CrudQueryOneResult({ row: null, rawContext: null, debugInfo: this.buildDebugInfo(undefined, reqJson.condition) });
|
|
327
|
-
}
|
|
328
|
-
// 统一逻辑:每个表查 maxLimit: 2,收集所有结果
|
|
329
|
-
const allRows = [];
|
|
330
|
-
const foundTables = [];
|
|
331
|
-
let firstCtx = null;
|
|
332
|
-
for (const table of targetTables) {
|
|
333
|
-
try {
|
|
334
|
-
const ctx = await this.executeOnTable(table, cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY, { maxLimit: 2 });
|
|
335
|
-
if (!firstCtx) {
|
|
336
|
-
firstCtx = ctx;
|
|
337
|
-
}
|
|
338
|
-
const rows = ctx.getResRows();
|
|
339
|
-
if (rows.length > 0) {
|
|
340
|
-
allRows.push(...rows);
|
|
341
|
-
foundTables.push(table);
|
|
342
|
-
// 超过 1 条,立即抛异常,不再查询后续分表
|
|
343
|
-
if (allRows.length > 1) {
|
|
344
|
-
throw new Error(this.formatUniqueError(allRows.length, foundTables, reqJson.condition, foundTables.length > 1));
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch (e) {
|
|
349
|
-
// 非 formatUniqueError 的异常(如表不存在),继续查询下一张表
|
|
350
|
-
if (e instanceof Error && e.message.startsWith('[ShardingCrudPro] findUniqueOne')) {
|
|
351
|
-
throw e;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
// 返回结果
|
|
356
|
-
return new CrudResult_1.CrudQueryOneResult({
|
|
357
|
-
row: allRows[0] || null,
|
|
358
|
-
rawContext: firstCtx,
|
|
359
|
-
debugInfo: this.buildDebugInfo(foundTables[0] || targetTables[0], reqJson.condition),
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
async findList(reqJson) {
|
|
363
|
-
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
364
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
365
|
-
const targetTables = await this.resolveFindTables(reqJson);
|
|
366
|
-
if (targetTables.length === 0) {
|
|
367
|
-
return new CrudResult_1.CrudQueryListResult({ rows: [], rawContext: null });
|
|
368
|
-
}
|
|
369
|
-
if (targetTables.length === 1) {
|
|
370
|
-
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY);
|
|
371
|
-
const rows = ctx.getResRows();
|
|
372
|
-
return new CrudResult_1.CrudQueryListResult({ rows, rawContext: ctx });
|
|
373
|
-
}
|
|
374
|
-
// 多表查询时,需要参数校验
|
|
375
|
-
this.validateFindOrderBy(reqJson);
|
|
376
|
-
// 根据排序方向确定表顺序:DESC 新→旧,ASC 旧→新
|
|
377
|
-
const tablesForMerge = this.sortTablesForOrderBy(targetTables, reqJson);
|
|
378
|
-
// 多表查询合并
|
|
379
|
-
const mergeResult = await this.merger.mergeQuery(this.crudProFactory(), tablesForMerge, cleanedReqJson, this.buildCfg(keys_1.KeysOfSimpleSQL.SIMPLE_QUERY));
|
|
380
|
-
return new CrudResult_1.CrudQueryListResult({ rows: mergeResult.rows, rawContext: mergeResult.lastCtx });
|
|
381
|
-
}
|
|
382
|
-
async findPage(reqJson) {
|
|
383
|
-
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
384
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
385
|
-
const targetTables = await this.resolveFindTables(reqJson);
|
|
386
|
-
if (targetTables.length === 0) {
|
|
387
|
-
throw new Error('[ShardingCrudPro] findPage: no matching tables found');
|
|
388
|
-
}
|
|
389
|
-
if (targetTables.length === 1) {
|
|
390
|
-
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_PAGE);
|
|
391
|
-
const pageResult = ctx.getResModelForQueryPage();
|
|
392
|
-
return new CrudResult_1.CrudQueryPageResult({
|
|
393
|
-
rows: pageResult.rows,
|
|
394
|
-
totalCount: pageResult.total_count,
|
|
395
|
-
rawContext: ctx,
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
// 多表查询时,需要参数校验
|
|
399
|
-
this.validateFindOrderBy(reqJson);
|
|
400
|
-
// 根据排序方向确定表顺序:DESC 新→旧,ASC 旧→新
|
|
401
|
-
const tablesForMerge = this.sortTablesForOrderBy(targetTables, reqJson);
|
|
402
|
-
// 多表分页查询:顺序累计
|
|
403
|
-
const pageResult = await this.merger.mergePageQuery(this.crudProFactory(), tablesForMerge, cleanedReqJson, this.buildCfg(keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_PAGE));
|
|
404
|
-
return new CrudResult_1.CrudQueryPageResult({
|
|
405
|
-
rows: pageResult.rows,
|
|
406
|
-
totalCount: pageResult.total_count,
|
|
407
|
-
rawContext: pageResult.lastCtx,
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
async findCount(reqJson) {
|
|
411
|
-
var _a;
|
|
412
|
-
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
413
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
414
|
-
const targetTables = await this.resolveFindTables(reqJson);
|
|
415
|
-
if (targetTables.length === 0) {
|
|
416
|
-
throw new Error('[ShardingCrudPro] findCount: no matching tables found');
|
|
417
|
-
}
|
|
418
|
-
if (targetTables.length === 1) {
|
|
419
|
-
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
|
|
420
|
-
const count = ctx.getResModelItem('total_count') || 0;
|
|
421
|
-
return new CrudResult_1.CrudCountResult({ count, rawContext: ctx });
|
|
422
|
-
}
|
|
423
|
-
// 多表统计:串行查询后求和
|
|
424
|
-
const countResults = [];
|
|
425
|
-
for (const table of targetTables) {
|
|
426
|
-
const result = await this.findCountFromTable(table, cleanedReqJson);
|
|
427
|
-
countResults.push(result);
|
|
428
|
-
}
|
|
429
|
-
const totalCount = countResults.reduce((sum, r) => sum + r.count, 0);
|
|
430
|
-
const firstCtx = ((_a = countResults.find(r => r.ctx !== null)) === null || _a === void 0 ? void 0 : _a.ctx) || null;
|
|
431
|
-
return new CrudResult_1.CrudCountResult({ count: totalCount, rawContext: firstCtx });
|
|
432
|
-
}
|
|
433
|
-
async isExist(reqJson) {
|
|
434
|
-
// 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
|
|
435
|
-
const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
|
|
436
|
-
const targetTables = await this.resolveFindTables(reqJson);
|
|
437
|
-
if (targetTables.length === 0) {
|
|
438
|
-
throw new Error('[ShardingCrudPro] isExist: no matching tables found');
|
|
439
|
-
}
|
|
440
|
-
if (targetTables.length === 1) {
|
|
441
|
-
const ctx = await this.executeOnTable(targetTables[0], cleanedReqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
|
|
442
|
-
const exists = ctx.getResModelItem('is_exist') === true;
|
|
443
|
-
return new CrudResult_1.CrudExistResult({ exists, rawContext: ctx });
|
|
444
|
-
}
|
|
445
|
-
// 多表判断:串行查询,任一表存在即返回 true
|
|
446
|
-
let exists = false;
|
|
447
|
-
let firstCtx = null;
|
|
448
|
-
for (const table of targetTables) {
|
|
449
|
-
const result = await this.isExistInTable(table, cleanedReqJson);
|
|
450
|
-
if (!firstCtx && result.ctx) {
|
|
451
|
-
firstCtx = result.ctx;
|
|
452
|
-
}
|
|
453
|
-
if (result.exists) {
|
|
454
|
-
exists = true;
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
return new CrudResult_1.CrudExistResult({ exists, rawContext: firstCtx });
|
|
459
|
-
}
|
|
460
|
-
// ============ 私有方法:分表路由 ============
|
|
461
|
-
batchInsertGroupDataByTable(dataArray) {
|
|
462
|
-
const groupedData = new Map();
|
|
463
|
-
for (const item of dataArray) {
|
|
464
|
-
const targetTable = this.batchInsertResolveTableForData(item);
|
|
465
|
-
if (!groupedData.has(targetTable)) {
|
|
466
|
-
groupedData.set(targetTable, []);
|
|
467
|
-
}
|
|
468
|
-
groupedData.get(targetTable).push(item);
|
|
469
|
-
}
|
|
470
|
-
return groupedData;
|
|
471
|
-
}
|
|
472
|
-
batchInsertResolveTableForData(item) {
|
|
473
|
-
const context = {
|
|
474
|
-
config: this.config,
|
|
475
|
-
data: item,
|
|
476
|
-
};
|
|
477
|
-
// 批量插入使用 resolveForInsert,确保返回单一表名
|
|
478
|
-
return this.router.resolveForInsert(this.config, context);
|
|
479
|
-
}
|
|
480
|
-
resolveSingleTable(reqJson, operation) {
|
|
481
|
-
// insert 操作使用 resolveForInsert(从 data 提取字段)
|
|
482
|
-
if (operation === 'insert') {
|
|
483
|
-
const context = {
|
|
484
|
-
config: this.config,
|
|
485
|
-
data: reqJson.data,
|
|
486
|
-
};
|
|
487
|
-
return this.router.resolveForInsert(this.config, context);
|
|
488
|
-
}
|
|
489
|
-
// 其他操作使用 resolveForCondition(从 condition 提取字段)
|
|
490
|
-
const context = {
|
|
491
|
-
config: this.config,
|
|
492
|
-
condition: reqJson.condition,
|
|
493
|
-
};
|
|
494
|
-
const result = this.router.resolveForCondition(this.config, context);
|
|
495
|
-
if (Array.isArray(result)) {
|
|
496
|
-
throw new Error(`[ShardingCrudPro] ${operation} 操作必须能确定单一目标表,` +
|
|
497
|
-
`但分表规则返回了多个表: ${result.slice(0, 5).join(', ')}${result.length > 5 ? '...' : ''}。` +
|
|
498
|
-
`请检查 condition 中是否包含分表字段(${this.getShardingColumnHint()})。`);
|
|
499
|
-
}
|
|
500
|
-
return result;
|
|
501
|
-
}
|
|
502
|
-
getShardingColumnHint() {
|
|
503
|
-
if (this.config.type === ShardingConfig_1.ShardingType.RANGE || this.config.type === ShardingConfig_1.ShardingType.HASH) {
|
|
504
|
-
return this.config.shardingColumn || '分表字段';
|
|
505
|
-
}
|
|
506
|
-
if ([ShardingConfig_1.ShardingType.YEAR, ShardingConfig_1.ShardingType.MONTH, ShardingConfig_1.ShardingType.DAY].includes(this.config.type)) {
|
|
507
|
-
return this.config.timeColumn || 'created_at';
|
|
508
|
-
}
|
|
509
|
-
return '分表字段';
|
|
510
|
-
}
|
|
511
|
-
// ============ 私有方法:配置构建 ============
|
|
512
|
-
buildCfg(sqlSimpleName) {
|
|
513
|
-
return {
|
|
514
|
-
method: `ShardingCrudProAnonymous_${sqlSimpleName}`,
|
|
515
|
-
...this.baseCfg,
|
|
516
|
-
sqlTable: this.config.baseTable,
|
|
517
|
-
sqlSimpleName,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
// ============ 私有方法:执行操作 ============
|
|
521
|
-
async executeOnTable(table, reqJson, sqlSimpleName, extraCfg) {
|
|
522
|
-
const cfg = this.buildCfg(sqlSimpleName);
|
|
523
|
-
cfg.sqlTable = table; // 替换为实际分表名
|
|
524
|
-
cfg.enableSoftDelete = this.enableSoftDelete;
|
|
525
|
-
if (extraCfg) {
|
|
526
|
-
Object.assign(cfg, extraCfg);
|
|
527
|
-
}
|
|
528
|
-
// 创建 CrudPro 实例并设置 visitor
|
|
529
|
-
const crudPro = this.crudProFactory();
|
|
530
|
-
// 应用软删除处理
|
|
531
|
-
(0, fixSoftDelete_1.fixSoftDelete)(sqlSimpleName, cfg, reqJson, crudPro.getVisitor());
|
|
532
|
-
return crudPro.executeCrudByCfg(reqJson, cfg);
|
|
533
|
-
}
|
|
534
|
-
async findCountFromTable(table, reqJson) {
|
|
535
|
-
try {
|
|
536
|
-
const ctx = await this.executeOnTable(table, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
|
|
537
|
-
return { count: ctx.getResModelItem('total_count') || 0, ctx };
|
|
538
|
-
}
|
|
539
|
-
catch (e) {
|
|
540
|
-
// 表不存在时返回 0
|
|
541
|
-
return { count: 0, ctx: null };
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
async isExistInTable(table, reqJson) {
|
|
545
|
-
try {
|
|
546
|
-
const ctx = await this.executeOnTable(table, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
|
|
547
|
-
return { exists: ctx.getResModelItem('is_exist') === true, ctx };
|
|
548
|
-
}
|
|
549
|
-
catch (e) {
|
|
550
|
-
// 表不存在时返回 false
|
|
551
|
-
return { exists: false, ctx: null };
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
// ============ 表存在性检查和自动创建 ============
|
|
555
|
-
async getExistingTablesSet(skipCache = false) {
|
|
556
|
-
const { sqlDatabase, sqlDbType } = this.baseCfg;
|
|
557
|
-
if (!sqlDatabase || !sqlDbType) {
|
|
558
|
-
throw new Error('[ShardingCrudPro] 未配置 sqlDatabase 或 sqlDbType');
|
|
559
|
-
}
|
|
560
|
-
const { tables } = await this.crudProFactory().getAllTableInfos({
|
|
561
|
-
sqlDatabase,
|
|
562
|
-
sqlDbType: sqlDbType,
|
|
563
|
-
}, { skipCache });
|
|
564
|
-
return new Set(tables.map(t => t.name));
|
|
565
|
-
}
|
|
566
|
-
async isTableExists(tableName) {
|
|
567
|
-
const existingSet = await this.getExistingTablesSet(); // 使用缓存
|
|
568
|
-
return existingSet.has(tableName);
|
|
569
|
-
}
|
|
570
|
-
async createShardingTableIfNeeded(tableName) {
|
|
571
|
-
// 根据配置决定是否自动创建
|
|
572
|
-
if (!this.config.autoCreateTable) {
|
|
573
|
-
// 使用缓存检查表是否存在,不存在则抛异常
|
|
574
|
-
if (!(await this.isTableExists(tableName))) {
|
|
575
|
-
throw new Error(`[ShardingCrudPro] 分表 ${tableName} 不存在。请先创建分表,或设置 autoCreateTable: true 自动创建`);
|
|
576
|
-
}
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
// 检查数据库配置
|
|
580
|
-
if (!this.baseCfg.sqlDatabase || !this.baseCfg.sqlDbType) {
|
|
581
|
-
throw new Error('[ShardingCrudPro] 请先调用 setBaseCfg 设置数据库配置');
|
|
582
|
-
}
|
|
583
|
-
// 执行创建(内部有 checkTableExists + 建表 + 刷新缓存)
|
|
584
|
-
const result = await this.tableCreator.createTableIfNeeded(tableName, {
|
|
585
|
-
sqlDatabase: this.baseCfg.sqlDatabase,
|
|
586
|
-
sqlDbType: this.baseCfg.sqlDbType,
|
|
587
|
-
}, this.config.tableCreateOptions);
|
|
588
|
-
if (!result.success) {
|
|
589
|
-
throw result.error || new Error(`[ShardingCrudPro] 创建分表 ${tableName} 失败`);
|
|
590
|
-
}
|
|
591
|
-
// 建表成功后刷新缓存,确保后续请求命中新表
|
|
592
|
-
if (result.createSql) {
|
|
593
|
-
await this.getExistingTablesSet(true);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// ============ 参数校验 ============
|
|
597
|
-
/**
|
|
598
|
-
* 判断是否为时间分表类型
|
|
599
|
-
*/
|
|
600
|
-
isTimeSharding() {
|
|
601
|
-
return [ShardingConfig_1.ShardingType.YEAR, ShardingConfig_1.ShardingType.MONTH, ShardingConfig_1.ShardingType.DAY].includes(this.config.type);
|
|
602
|
-
}
|
|
603
|
-
/**
|
|
604
|
-
* 校验时间分表插入操作的 data 必须包含 timeColumn
|
|
605
|
-
*
|
|
606
|
-
* 时间分表需要根据时间字段路由到正确的分表,
|
|
607
|
-
* 如果 data 中缺少时间字段,将无法确定数据应插入哪个分表。
|
|
608
|
-
*
|
|
609
|
-
* @param reqJson 请求参数
|
|
610
|
-
* @param operation 操作名称(用于错误提示)
|
|
611
|
-
* @throws Error 如果 data 中缺少时间字段
|
|
612
|
-
*/
|
|
613
|
-
validateTimeColumnForData(reqJson, operation) {
|
|
614
|
-
if (!this.isTimeSharding())
|
|
615
|
-
return;
|
|
616
|
-
const timeColumn = this.config.timeColumn;
|
|
617
|
-
const data = reqJson.data;
|
|
618
|
-
if (!data || data[timeColumn] === undefined) {
|
|
619
|
-
throw new Error(`[ShardingCrudPro] ${operation} 操作的 data 必须包含时间字段 '${timeColumn}',用于路由到正确的分表。`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* 校验时间分表批量插入操作的每条 data 必须包含 timeColumn
|
|
624
|
-
*
|
|
625
|
-
* @param dataArray 数据数组
|
|
626
|
-
* @param operation 操作名称(用于错误提示)
|
|
627
|
-
* @throws Error 如果任一条 data 中缺少时间字段
|
|
628
|
-
*/
|
|
629
|
-
validateTimeColumnForBatchData(dataArray, operation) {
|
|
630
|
-
if (!this.isTimeSharding())
|
|
631
|
-
return;
|
|
632
|
-
const timeColumn = this.config.timeColumn;
|
|
633
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
634
|
-
if (!dataArray[i] || dataArray[i][timeColumn] === undefined) {
|
|
635
|
-
throw new Error(`[ShardingCrudPro] ${operation} 操作的 data[${i}] 必须包含时间字段 '${timeColumn}',用于路由到正确的分表。`);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* 校验时间分表写操作的 condition 必须包含路由字段(timeColumn)
|
|
641
|
-
*
|
|
642
|
-
* 时间分表需要根据时间字段路由到正确的分表,
|
|
643
|
-
* 如果 condition 中缺少时间字段,将无法确定操作哪个分表。
|
|
644
|
-
*
|
|
645
|
-
* @param reqJson 请求参数
|
|
646
|
-
* @param operation 操作名称(用于错误提示)
|
|
647
|
-
* @throws Error 如果 condition 中缺少时间字段
|
|
648
|
-
*/
|
|
649
|
-
validateRoutingFieldForCondition(reqJson, operation) {
|
|
650
|
-
if (!this.isTimeSharding())
|
|
651
|
-
return;
|
|
652
|
-
const timeColumn = this.config.timeColumn;
|
|
653
|
-
const condition = reqJson.condition;
|
|
654
|
-
if (!condition || !condition[timeColumn]) {
|
|
655
|
-
throw new Error(`[ShardingCrudPro] ${operation} 操作的 condition 必须包含时间字段 '${timeColumn}',用于路由到正确的分表。` +
|
|
656
|
-
`请提供 '${timeColumn}' 字段(如 { ${timeColumn}: '2026-03-15' })或时间范围(如 { ${timeColumn}: { $gte: '2024-01-01', $lte: '2024-03-31' } })。`);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
/**
|
|
660
|
-
* 智能处理时间分表场景下的时间字段
|
|
661
|
-
*
|
|
662
|
-
* **问题背景**:
|
|
663
|
-
* 在时间分表(YEAR/MONTH/DAY)场景中,timeColumn 既是路由键也是查询条件。
|
|
664
|
-
* 用户传入的时间值可能精度不一致(如传 '2024-01-15' 但数据库存的是毫秒时间戳),
|
|
665
|
-
* 直接作为 WHERE 条件可能导致匹配失败。
|
|
666
|
-
*
|
|
667
|
-
* **处理规则**(详见 TIME_COLUMN_CLEAN_SPEC.md):
|
|
668
|
-
* 1. 操作符表达式($gte/$lte/$range/$in/$null 等)→ 始终保留,不做处理
|
|
669
|
-
* 2. 精确值 + 有 primaryKey → 清理(从 WHERE 移除,仅用于路由)
|
|
670
|
-
* 3. 精确值 + 无 primaryKey + 日期粒度字符串(年/月/日)→ 转换为 $gte/$lte 范围
|
|
671
|
-
* 4. 精确值 + 无 primaryKey + 其他(时间戳/datetime/null等)→ 保留原值
|
|
672
|
-
*
|
|
673
|
-
* **示例**:
|
|
674
|
-
* ```typescript
|
|
675
|
-
* // 有主键 + 精确值 → 清理
|
|
676
|
-
* { order_id: 'ORD001', created_at: '2024-01-15' }
|
|
677
|
-
* → { order_id: 'ORD001' }
|
|
678
|
-
*
|
|
679
|
-
* // 无主键 + 日期粒度字符串 → 转换为范围
|
|
680
|
-
* { status: 'paid', created_at: '2024-01' }
|
|
681
|
-
* → { status: 'paid', created_at: { $gte: '2024-01-01 00:00:00', $lte: '2024-01-31 23:59:59' } }
|
|
682
|
-
*
|
|
683
|
-
* // 操作符表达式 → 保留
|
|
684
|
-
* { order_id: 'ORD001', created_at: { $gte: '2024-01', $lte: '2024-06' } }
|
|
685
|
-
* → 不做任何处理
|
|
686
|
-
* ```
|
|
687
|
-
*
|
|
688
|
-
* @param reqJson 请求参数
|
|
689
|
-
* @returns 处理后的请求参数
|
|
690
|
-
*/
|
|
691
|
-
cleanTimeColumnForSingleTableQuery(reqJson) {
|
|
692
|
-
const { primaryKey, timeColumn, type } = this.config;
|
|
693
|
-
// 只有时间分表且配置了时间字段才需要处理
|
|
694
|
-
if (!timeColumn || ![ShardingConfig_1.ShardingType.YEAR, ShardingConfig_1.ShardingType.MONTH, ShardingConfig_1.ShardingType.DAY].includes(type)) {
|
|
695
|
-
return reqJson;
|
|
696
|
-
}
|
|
697
|
-
const condition = reqJson.condition;
|
|
698
|
-
if (!condition || typeof condition !== 'object') {
|
|
699
|
-
return reqJson;
|
|
700
|
-
}
|
|
701
|
-
const timeValue = condition[timeColumn];
|
|
702
|
-
// timeColumn 不存在 → 不处理
|
|
703
|
-
if (timeValue === undefined) {
|
|
704
|
-
return reqJson;
|
|
705
|
-
}
|
|
706
|
-
// 操作符表达式($gte/$lte/$range/$in/$null 等)→ 始终保留,但校验时间精度
|
|
707
|
-
if ((0, ShardingUtils_1.isOperatorExpression)(timeValue)) {
|
|
708
|
-
this.validateTimeOperatorPrecision(timeValue, timeColumn);
|
|
709
|
-
return reqJson;
|
|
710
|
-
}
|
|
711
|
-
// 判断是否有主键
|
|
712
|
-
const hasPrimaryKey = primaryKey && condition[primaryKey] !== undefined;
|
|
713
|
-
// ---- 精确值处理 ----
|
|
714
|
-
// Date 对象:精确到毫秒,视为精确值
|
|
715
|
-
// number:时间戳,无法推断粒度,视为精确值
|
|
716
|
-
// boolean:视为精确值
|
|
717
|
-
// string:需要检测日期粒度
|
|
718
|
-
if (typeof timeValue === 'string') {
|
|
719
|
-
// 检测日期字符串粒度
|
|
720
|
-
const range = (0, ShardingUtils_1.expandDateToRange)(timeValue);
|
|
721
|
-
if (hasPrimaryKey) {
|
|
722
|
-
// 有主键 → 无论什么粒度的字符串,都清理
|
|
723
|
-
const { [timeColumn]: _, ...cleanedCondition } = condition;
|
|
724
|
-
return { ...reqJson, condition: cleanedCondition };
|
|
725
|
-
}
|
|
726
|
-
// 无主键 + 年/月/日粒度 → 转换为 $gte/$lte 范围
|
|
727
|
-
if (range) {
|
|
728
|
-
return {
|
|
729
|
-
...reqJson,
|
|
730
|
-
condition: { ...condition, [timeColumn]: range },
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
// 无主键 + datetime 粒度或无法识别的格式 → 保留原值
|
|
734
|
-
return reqJson;
|
|
735
|
-
}
|
|
736
|
-
// Date / number / boolean / null 等其他精确值
|
|
737
|
-
if (hasPrimaryKey) {
|
|
738
|
-
// 有主键 → 清理
|
|
739
|
-
const { [timeColumn]: _, ...cleanedCondition } = condition;
|
|
740
|
-
return { ...reqJson, condition: cleanedCondition };
|
|
741
|
-
}
|
|
742
|
-
// 无主键 → 保留原值
|
|
743
|
-
return reqJson;
|
|
744
|
-
}
|
|
745
|
-
/**
|
|
746
|
-
* 校验时间操作符的值必须精确到秒
|
|
747
|
-
*
|
|
748
|
-
* MySQL 中 '2026-04-30' 等价于 '2026-04-30 00:00:00',
|
|
749
|
-
* 导致 $lte: '2026-04-30' 会丢失当天所有数据。
|
|
750
|
-
* 如果用户需要查整月/整天数据,应传粒度字符串由系统自动转换,而非手动写 $gte/$lte。
|
|
751
|
-
*
|
|
752
|
-
* @param operatorValue 操作符表达式的值
|
|
753
|
-
* @param timeColumn 时间字段名
|
|
754
|
-
* @throws Error 如果操作符值缺少时间部分
|
|
755
|
-
*/
|
|
756
|
-
validateTimeOperatorPrecision(operatorValue, timeColumn) {
|
|
757
|
-
// 需要校验精度的操作符
|
|
758
|
-
const PRECISION_OPERATORS = ['$gte', '$lte', '$gt', '$lt'];
|
|
759
|
-
for (const op of PRECISION_OPERATORS) {
|
|
760
|
-
const value = operatorValue[op];
|
|
761
|
-
if (value !== undefined && typeof value === 'string') {
|
|
762
|
-
// 匹配日期格式但缺少时间部分:'2026-04-30'
|
|
763
|
-
// datetime 格式 '2026-04-30 00:00:00' 不应被拦截
|
|
764
|
-
const dateOnlyPattern = /^\d{4}-\d{1,2}-\d{1,2}$/;
|
|
765
|
-
if (dateOnlyPattern.test(value)) {
|
|
766
|
-
throw new Error(`[ShardingCrudPro] 时间字段 '${timeColumn}' 的 ${op} 值必须精确到秒,` +
|
|
767
|
-
`当前值: '${value}'。` +
|
|
768
|
-
`请使用 '${value} 23:59:59'($lte/$lt)或 '${value} 00:00:00'($gte/$gt),` +
|
|
769
|
-
`或直接传粒度字符串(如 '${value.substring(0, 7)}' 或 '${value}')由系统自动转换范围。`);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
// $range 操作符校验:[start, end] 两个值都必须精确到秒
|
|
774
|
-
const rangeValue = operatorValue.$range;
|
|
775
|
-
if (Array.isArray(rangeValue) && rangeValue.length >= 2) {
|
|
776
|
-
const dateOnlyPattern = /^\d{4}-\d{1,2}-\d{1,2}$/;
|
|
777
|
-
for (let i = 0; i < rangeValue.length; i++) {
|
|
778
|
-
const val = rangeValue[i];
|
|
779
|
-
if (typeof val === 'string' && dateOnlyPattern.test(val)) {
|
|
780
|
-
throw new Error(`[ShardingCrudPro] 时间字段 '${timeColumn}' 的 $range[${i}] 值必须精确到秒,` +
|
|
781
|
-
`当前值: '${val}'。` +
|
|
782
|
-
`请使用 '${val} ${i === 0 ? '00:00:00' : '23:59:59'}',` +
|
|
783
|
-
`或直接传粒度字符串由系统自动转换范围。`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
validateFindOrderBy(reqJson) {
|
|
789
|
-
// 非时间分表不强制 orderBy 约束(多表合并排序由调用方自行保证)
|
|
790
|
-
const timeColumn = this.config.timeColumn;
|
|
791
|
-
if (!timeColumn) {
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
const orderBy = reqJson.orderBy;
|
|
795
|
-
// 1. 必须传 orderBy
|
|
796
|
-
if (!orderBy) {
|
|
797
|
-
throw new Error(`[ShardingCrudPro] 查询操作必须传 orderBy 参数。` +
|
|
798
|
-
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`);
|
|
799
|
-
}
|
|
800
|
-
// 2. 使用工具类解析并校验首个排序字段是否为 timeColumn
|
|
801
|
-
const firstOrderBy = OrderByUtils_1.OrderByUtils.getFirstOrderBy(orderBy);
|
|
802
|
-
if (!firstOrderBy) {
|
|
803
|
-
throw new Error(`[ShardingCrudPro] orderBy 参数格式错误,无法解析。` +
|
|
804
|
-
`期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`);
|
|
805
|
-
}
|
|
806
|
-
if (firstOrderBy.fieldName !== timeColumn) {
|
|
807
|
-
throw new Error(`[ShardingCrudPro] orderBy 首个排序字段必须为 '${timeColumn}',` +
|
|
808
|
-
`当前值: '${firstOrderBy.fieldName}'`);
|
|
809
|
-
}
|
|
810
|
-
// 校验排序方向是否为 ASC 或 DESC
|
|
811
|
-
const orderType = firstOrderBy.orderType.toUpperCase();
|
|
812
|
-
if (orderType !== 'ASC' && orderType !== 'DESC') {
|
|
813
|
-
throw new Error(`[ShardingCrudPro] orderBy 排序方向必须是 'ASC' 或 'DESC',` +
|
|
814
|
-
`当前值: '${firstOrderBy.orderType}'`);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
isAscOrderBy(reqJson) {
|
|
818
|
-
return OrderByUtils_1.OrderByUtils.isFirstOrderByAsc(reqJson.orderBy);
|
|
819
|
-
}
|
|
820
|
-
sortTablesForOrderBy(tables, reqJson) {
|
|
821
|
-
const isAsc = this.isAscOrderBy(reqJson);
|
|
822
|
-
return [...tables].sort((a, b) => isAsc ? a.localeCompare(b) : b.localeCompare(a));
|
|
823
|
-
}
|
|
824
|
-
// ============ 辅助方法 ============
|
|
825
|
-
/**
|
|
826
|
-
* 构建辅助定位信息
|
|
827
|
-
*/
|
|
828
|
-
buildDebugInfo(sqlTable, condition) {
|
|
829
|
-
const info = {
|
|
830
|
-
sqlDatabase: this.baseCfg.sqlDatabase || 'unknown',
|
|
831
|
-
sqlTable: sqlTable || this.config.baseTable,
|
|
832
|
-
};
|
|
833
|
-
if (condition) {
|
|
834
|
-
info.condition = condition;
|
|
835
|
-
}
|
|
836
|
-
return info;
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* 格式化唯一性错误消息
|
|
840
|
-
*/
|
|
841
|
-
formatUniqueError(foundCount, tables, condition, isMultiTable = false) {
|
|
842
|
-
const parts = [
|
|
843
|
-
`[ShardingCrudPro] findUniqueOne 期望唯一一条记录,但查询到 ${foundCount} 条`,
|
|
844
|
-
];
|
|
845
|
-
if (this.baseCfg.sqlDatabase) {
|
|
846
|
-
parts.push(`数据库: ${this.baseCfg.sqlDatabase}`);
|
|
847
|
-
}
|
|
848
|
-
if (isMultiTable && Array.isArray(tables)) {
|
|
849
|
-
parts.push(`基表: ${this.config.baseTable}`);
|
|
850
|
-
parts.push(`分表: [${tables.join(', ')}]`);
|
|
851
|
-
}
|
|
852
|
-
else {
|
|
853
|
-
parts.push(`表: ${typeof tables === 'string' ? tables : tables[0]}`);
|
|
854
|
-
}
|
|
855
|
-
if (condition) {
|
|
856
|
-
try {
|
|
857
|
-
parts.push(`条件: ${JSON.stringify(condition)}`);
|
|
858
|
-
}
|
|
859
|
-
catch (_a) {
|
|
860
|
-
parts.push(`条件: [无法序列化]`);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return parts.join(' | ');
|
|
864
|
-
}
|
|
865
|
-
// ============ 查询表解析(委托给 ShardingRouter) ============
|
|
866
|
-
async resolveFindTables(reqJson) {
|
|
867
|
-
const context = {
|
|
868
|
-
config: this.config,
|
|
869
|
-
condition: reqJson.condition,
|
|
870
|
-
};
|
|
871
|
-
// 创建表信息提供者
|
|
872
|
-
const tableInfoProvider = {
|
|
873
|
-
getExistingTables: () => this.getExistingTablesSet(),
|
|
874
|
-
};
|
|
875
|
-
// 委托给 ShardingRouter 处理查询路由
|
|
876
|
-
return this.router.resolveQuery(this.config, context, tableInfoProvider);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
exports.ShardingCrudPro = ShardingCrudPro;
|