midway-fatcms 0.0.7 → 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.
Files changed (120) hide show
  1. package/.qoder/skills/midway-fatcms/01-quick-start.md +231 -0
  2. package/.qoder/skills/midway-fatcms/02-crud-quick.md +337 -0
  3. package/.qoder/skills/midway-fatcms/03-crud-sharding.md +488 -0
  4. package/.qoder/skills/midway-fatcms/04-condition-operators.md +93 -0
  5. package/.qoder/skills/midway-fatcms/05-configuration.md +290 -0
  6. package/.qoder/skills/midway-fatcms/06-builtin-functions.md +241 -0
  7. package/.qoder/skills/midway-fatcms/07-examples.md +500 -0
  8. package/.qoder/skills/midway-fatcms/SKILL.md +96 -0
  9. package/README.md +9 -9
  10. package/dist/controller/base/BaseApiController.d.ts +1 -2
  11. package/dist/controller/base/BaseApiController.js +0 -4
  12. package/dist/controller/gateway/DocGatewayController.js +1 -1
  13. package/dist/controller/manage/FlowConfigManageApi.js +4 -2
  14. package/dist/controller/manage/SysConfigMangeApi.js +6 -1
  15. package/dist/controller/manage/UserAccountManageApi.js +7 -2
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +2 -2
  18. package/dist/libs/crud-pro/CrudPro.d.ts +23 -2
  19. package/dist/libs/crud-pro/CrudPro.js +53 -2
  20. package/dist/libs/crud-pro/interfaces.d.ts +82 -12
  21. package/dist/libs/crud-pro/models/CrudResult.d.ts +115 -0
  22. package/dist/libs/crud-pro/models/CrudResult.js +126 -0
  23. package/dist/libs/crud-pro/models/RequestModel.d.ts +2 -2
  24. package/dist/libs/crud-pro/services/CrudProExecuteSqlService.js +36 -2
  25. package/dist/libs/crud-pro/services/CrudProGenSqlCondition.js +8 -4
  26. package/dist/libs/crud-pro/services/CrudProTableMetaService.js +1 -2
  27. package/dist/libs/crud-pro-quick/CrudProQuick.d.ts +295 -0
  28. package/dist/libs/crud-pro-quick/CrudProQuick.js +529 -0
  29. package/dist/libs/crud-pro-quick/fixSoftDelete.d.ts +30 -0
  30. package/dist/{service/curd → libs/crud-pro-quick}/fixSoftDelete.js +3 -6
  31. package/dist/libs/crud-pro-quick/index.d.ts +36 -0
  32. package/dist/libs/crud-pro-quick/index.js +49 -0
  33. package/dist/libs/crud-pro-quick/models.d.ts +33 -0
  34. package/dist/libs/crud-pro-quick/models.js +2 -0
  35. package/dist/libs/crud-sharding/ShardingConfig.d.ts +15 -2
  36. package/dist/libs/crud-sharding/ShardingConfig.js +2 -2
  37. package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +119 -274
  38. package/dist/libs/crud-sharding/ShardingCrudPro.js +544 -340
  39. package/dist/libs/crud-sharding/ShardingMerger.d.ts +10 -18
  40. package/dist/libs/crud-sharding/ShardingMerger.js +27 -44
  41. package/dist/libs/crud-sharding/ShardingResult.d.ts +33 -0
  42. package/dist/libs/crud-sharding/ShardingResult.js +16 -0
  43. package/dist/libs/crud-sharding/ShardingRouter.d.ts +1 -0
  44. package/dist/libs/crud-sharding/ShardingRouter.js +25 -6
  45. package/dist/libs/crud-sharding/ShardingTableCreator.d.ts +21 -4
  46. package/dist/libs/crud-sharding/ShardingTableCreator.js +193 -59
  47. package/dist/libs/crud-sharding/ShardingUtils.d.ts +48 -0
  48. package/dist/libs/crud-sharding/ShardingUtils.js +122 -1
  49. package/dist/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +488 -0
  50. package/dist/libs/crud-sharding/index.d.ts +4 -3
  51. package/dist/libs/crud-sharding/index.js +14 -2
  52. package/dist/models/bizmodels.d.ts +2 -6
  53. package/dist/service/SysAppService.d.ts +2 -2
  54. package/dist/service/SysAppService.js +16 -5
  55. package/dist/service/SysConfigService.d.ts +1 -1
  56. package/dist/service/SysConfigService.js +7 -2
  57. package/dist/service/SysDictDataService.js +14 -4
  58. package/dist/service/SysMenuService.js +7 -2
  59. package/dist/service/curd/CurdMixService.d.ts +6 -4
  60. package/dist/service/curd/CurdMixService.js +16 -2
  61. package/dist/service/curd/CurdProService.d.ts +43 -27
  62. package/dist/service/curd/CurdProService.js +32 -33
  63. package/dist/service/flow/FlowConfigService.js +7 -2
  64. package/dist/service/flow/FlowInstanceCrudService.js +22 -19
  65. package/package.json +1 -1
  66. package/src/controller/base/BaseApiController.ts +0 -5
  67. package/src/controller/gateway/DocGatewayController.ts +1 -1
  68. package/src/controller/manage/CrudStandardDesignApi.ts +4 -3
  69. package/src/controller/manage/FlowConfigManageApi.ts +4 -2
  70. package/src/controller/manage/SysConfigMangeApi.ts +6 -1
  71. package/src/controller/manage/UserAccountManageApi.ts +7 -2
  72. package/src/index.ts +2 -2
  73. package/src/libs/crud-pro/CrudPro.ts +62 -4
  74. package/src/libs/crud-pro/interfaces.ts +110 -15
  75. package/src/libs/crud-pro/models/CrudResult.ts +178 -0
  76. package/src/libs/crud-pro/models/RequestModel.ts +2 -2
  77. package/src/libs/crud-pro/services/CrudProExecuteSqlService.ts +41 -2
  78. package/src/libs/crud-pro/services/CrudProGenSqlCondition.ts +11 -7
  79. package/src/libs/crud-pro/services/CrudProTableMetaService.ts +1 -2
  80. package/src/libs/crud-pro-quick/CrudProQuick.ts +594 -0
  81. package/src/{service/curd → libs/crud-pro-quick}/fixSoftDelete.ts +23 -13
  82. package/src/libs/crud-pro-quick/index.ts +52 -0
  83. package/src/libs/crud-pro-quick/models.ts +35 -0
  84. package/src/libs/crud-sharding/ShardingConfig.ts +18 -2
  85. package/src/libs/crud-sharding/ShardingCrudPro.ts +660 -390
  86. package/src/libs/crud-sharding/ShardingMerger.ts +35 -63
  87. package/src/libs/crud-sharding/ShardingResult.ts +29 -0
  88. package/src/libs/crud-sharding/ShardingRouter.ts +27 -6
  89. package/src/libs/crud-sharding/ShardingTableCreator.ts +214 -71
  90. package/src/libs/crud-sharding/ShardingUtils.ts +137 -0
  91. package/src/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +488 -0
  92. package/src/libs/crud-sharding/index.ts +14 -3
  93. package/src/models/bizmodels.ts +4 -7
  94. package/src/service/SysAppService.ts +18 -7
  95. package/src/service/SysConfigService.ts +8 -3
  96. package/src/service/SysDictDataService.ts +14 -4
  97. package/src/service/SysMenuService.ts +7 -2
  98. package/src/service/crudstd/CrudStdService.ts +2 -2
  99. package/src/service/curd/CurdMixService.ts +26 -5
  100. package/src/service/curd/CurdProService.ts +58 -39
  101. package/src/service/flow/FlowConfigService.ts +7 -2
  102. package/src/service/flow/FlowInstanceCrudService.ts +23 -20
  103. package/.qoder/skills/midway-fatcms-crud/SKILL.md +0 -375
  104. package/.qoder/skills/midway-fatcms-crud/examples.md +0 -990
  105. package/.qoder/skills/midway-fatcms-crud/reference.md +0 -568
  106. package/dist/libs/crud-pro/README.md +0 -809
  107. package/dist/libs/crud-pro/README_FUNC.md +0 -193
  108. package/dist/libs/crud-sharding/ROUTING_LOGIC.md +0 -944
  109. package/dist/models/StandardColumns.d.ts +0 -71
  110. package/dist/models/StandardColumns.js +0 -28
  111. package/dist/service/curd/CrudProQuick.d.ts +0 -190
  112. package/dist/service/curd/CrudProQuick.js +0 -319
  113. package/dist/service/curd/README.md +0 -1100
  114. package/dist/service/curd/fixSoftDelete.d.ts +0 -20
  115. package/src/libs/crud-pro/README.md +0 -809
  116. package/src/libs/crud-pro/README_FUNC.md +0 -193
  117. package/src/libs/crud-sharding/ROUTING_LOGIC.md +0 -944
  118. package/src/models/StandardColumns.ts +0 -76
  119. package/src/service/curd/CrudProQuick.ts +0 -360
  120. package/src/service/curd/README.md +0 -1100
@@ -4,10 +4,14 @@ exports.ShardingCrudPro = void 0;
4
4
  const OrderByUtils_1 = require("../../libs/crud-pro/utils/OrderByUtils");
5
5
  const keys_1 = require("../../libs/crud-pro/models/keys");
6
6
  const ShardingConfig_1 = require("./ShardingConfig");
7
+ const fixSoftDelete_1 = require("../../libs/crud-pro-quick/fixSoftDelete");
7
8
  const ShardingRouter_1 = require("./ShardingRouter");
8
9
  const ShardingMerger_1 = require("./ShardingMerger");
9
10
  const ShardingTableCreator_1 = require("./ShardingTableCreator");
10
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");
11
15
  /**
12
16
  * 分表 CRUD 操作封装器
13
17
  *
@@ -34,17 +38,24 @@ const ShardingCountCache_1 = require("./ShardingCountCache");
34
38
  * await sharding.insert({ data: { order_id: '001', amount: 100, created_at: '2024-03-15' } });
35
39
  *
36
40
  * // 分页查询(自动合并多表结果)
37
- * const result = await sharding.queryPage({
41
+ * const result = await sharding.findPage({
38
42
  * condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
39
43
  * pageNo: 1,
40
44
  * pageSize: 10,
41
45
  * });
42
46
  */
43
47
  class ShardingCrudPro {
44
- constructor(crudPro, config) {
48
+ constructor(crudProFactory, config) {
45
49
  this.baseCfg = {};
46
- this.crudPro = crudPro;
50
+ this.enableSoftDelete = false;
47
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
+ }
48
59
  // 时间分表必须显式指定 timeColumn
49
60
  if ([ShardingConfig_1.ShardingType.YEAR, ShardingConfig_1.ShardingType.MONTH, ShardingConfig_1.ShardingType.DAY].includes(config.type)) {
50
61
  if (!config.timeColumn) {
@@ -53,7 +64,7 @@ class ShardingCrudPro {
53
64
  }
54
65
  this.router = new ShardingRouter_1.ShardingRouter();
55
66
  this.merger = new ShardingMerger_1.ShardingMerger();
56
- this.tableCreator = new ShardingTableCreator_1.ShardingTableCreator(crudPro, config);
67
+ this.tableCreator = new ShardingTableCreator_1.ShardingTableCreator(this.crudProFactory, config);
57
68
  // 初始化 COUNT 缓存(如果配置了)
58
69
  if (config.countCache) {
59
70
  const countCache = new ShardingCountCache_1.ShardingCountCache(config.type, config.baseTable, config.countCache.ttlSeconds, config.countCache.maxSize);
@@ -61,101 +72,44 @@ class ShardingCrudPro {
61
72
  }
62
73
  }
63
74
  // ============ 配置方法 ============
64
- /**
65
- * 设置基础配置(会应用到所有操作)
66
- *
67
- * @param cfg 配置项,如 sqlDatabase、sqlDbType 等
68
- * @returns this,支持链式调用
69
- *
70
- * @example
71
- * sharding.setBaseCfg({
72
- * sqlDatabase: 'mydb',
73
- * sqlDbType: SqlDbType.mysql,
74
- * });
75
- */
76
75
  setBaseCfg(cfg) {
77
76
  this.baseCfg = { ...this.baseCfg, ...cfg };
78
77
  return this;
79
78
  }
80
- /**
81
- * 获取分表配置
82
- */
83
79
  getConfig() {
84
80
  return this.config;
85
81
  }
86
- // ============ 写操作(单表路由) ============
87
82
  /**
88
- * 插入数据
89
- *
90
- * 根据分表规则自动路由到目标分表。
91
- * 对于时间分表,需要确保 data 中包含时间字段。
92
- * 对于哈希/范围分表,需要确保 data 中包含分表字段。
93
- *
94
- * 如果目标分表不存在且配置了 autoCreateTable,会自动创建分表。
95
- *
96
- * @param reqJson 请求参数,data 为要插入的数据
97
- * @returns 执行上下文
98
- *
99
- * @example
100
- * await sharding.insert({
101
- * data: {
102
- * order_id: 'ORD001',
103
- * amount: 100,
104
- * created_at: '2024-03-15 10:00:00',
105
- * },
106
- * });
83
+ * 设置是否启用软删除
84
+ * @param enable 是否启用软删除
107
85
  */
86
+ setEnableSoftDelete(enable) {
87
+ this.enableSoftDelete = enable;
88
+ return this;
89
+ }
90
+ // ============ 写操作(单表路由) ============
108
91
  async insert(reqJson) {
92
+ // 时间分表校验:data 必须包含 timeColumn
93
+ this.validateTimeColumnForData(reqJson, 'insert');
109
94
  const targetTable = this.resolveSingleTable(reqJson, 'insert');
110
95
  // 确保分表存在(根据配置决定是否自动创建)
111
96
  await this.createShardingTableIfNeeded(targetTable);
112
- return this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT);
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
+ });
113
104
  }
114
- /**
115
- * 批量插入数据(支持跨分表)
116
- *
117
- * 自动将数据按分表分组,并行插入到对应的分表。
118
- *
119
- * @param reqJson 请求参数,data 为要插入的数据数组
120
- * @returns 批量插入结果
121
- *
122
- * 执行流程:
123
- * 1. 遍历所有数据,根据分表字段计算目标分表
124
- * 2. 将数据按分表分组
125
- * 3. 并行执行各分表的批量插入
126
- * 4. 汇总结果返回
127
- *
128
- * @example
129
- * // 数据分布在不同月份的分表
130
- * const result = await sharding.batchInsert({
131
- * data: [
132
- * { order_id: '001', amount: 100, created_at: '2024-01-15' }, // -> t_order_202401
133
- * { order_id: '002', amount: 200, created_at: '2024-01-20' }, // -> t_order_202401
134
- * { order_id: '003', amount: 150, created_at: '2024-02-10' }, // -> t_order_202402
135
- * { order_id: '004', amount: 300, created_at: '2024-03-05' }, // -> t_order_202403
136
- * ],
137
- * });
138
- *
139
- * console.log(result.totalAffected); // 4
140
- * console.log(result.tableCount); // 3
141
- * console.log(result.tableResults);
142
- * // [
143
- * // { table: 't_order_202401', affected: 2, rowCount: 2 },
144
- * // { table: 't_order_202402', affected: 1, rowCount: 1 },
145
- * // { table: 't_order_202403', affected: 1, rowCount: 1 },
146
- * // ]
147
- */
148
105
  async batchInsert(reqJson) {
106
+ var _a, _b;
149
107
  const dataArray = reqJson.data;
150
108
  if (!Array.isArray(dataArray) || dataArray.length === 0) {
151
- return {
152
- totalAffected: 0,
153
- tableResults: [],
154
- tableCount: 0,
155
- success: true,
156
- errors: [],
157
- };
109
+ throw new Error('[ShardingCrudPro] batchInsert requires non-empty data array');
158
110
  }
111
+ // 时间分表校验:每条 data 必须包含 timeColumn
112
+ this.validateTimeColumnForBatchData(dataArray, 'batchInsert');
159
113
  // 按分表分组
160
114
  const groupedData = this.batchInsertGroupDataByTable(dataArray);
161
115
  // 确保所有目标分表存在(根据配置决定是否自动创建)
@@ -163,27 +117,31 @@ class ShardingCrudPro {
163
117
  for (let i = 0; i < tableNames.length; i++) {
164
118
  await this.createShardingTableIfNeeded(tableNames[i]);
165
119
  }
166
- // 并行执行各分表的批量插入
167
- const insertPromises = Array.from(groupedData.entries()).map(async ([table, items]) => {
120
+ // 串行执行各分表的批量插入
121
+ const results = [];
122
+ let lastContext = null;
123
+ for (const [table, items] of groupedData.entries()) {
168
124
  try {
169
125
  const ctx = await this.executeOnTable(table, { data: items }, keys_1.KeysOfSimpleSQL.SIMPLE_BATCH_INSERT);
170
- return {
126
+ lastContext = ctx;
127
+ results.push({
171
128
  table,
172
- affected: ctx.getResModelItem('affected') || items.length,
129
+ affected: ((_a = ctx.getResModelItem('affected')) === null || _a === void 0 ? void 0 : _a.affectedRows) || items.length,
173
130
  rowCount: items.length,
174
131
  error: null,
175
- };
132
+ context: ctx,
133
+ });
176
134
  }
177
135
  catch (e) {
178
- return {
136
+ results.push({
179
137
  table,
180
138
  affected: 0,
181
139
  rowCount: items.length,
182
140
  error: e,
183
- };
141
+ context: null,
142
+ });
184
143
  }
185
- });
186
- const results = await Promise.all(insertPromises);
144
+ }
187
145
  // 汇总结果
188
146
  const tableResults = results.map(r => ({
189
147
  table: r.table,
@@ -194,223 +152,312 @@ class ShardingCrudPro {
194
152
  .filter(r => r.error !== null)
195
153
  .map(r => ({ table: r.table, error: r.error }));
196
154
  const totalAffected = results.reduce((sum, r) => sum + r.affected, 0);
197
- return {
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({
198
161
  totalAffected,
199
162
  tableResults,
200
163
  tableCount: groupedData.size,
201
164
  success: errors.length === 0,
202
165
  errors,
203
- };
166
+ lastContext: finalContext,
167
+ });
204
168
  }
205
- /**
206
- * 更新数据
207
- *
208
- * 根据条件中的分表字段自动路由到目标分表。
209
- *
210
- * @param reqJson 请求参数,condition 为更新条件,data 为更新数据
211
- * @returns 执行上下文
212
- *
213
- * @example
214
- * await sharding.update({
215
- * condition: { order_id: 'ORD001', created_at: '2024-03-15' },
216
- * data: { amount: 200 },
217
- * });
218
- */
219
169
  async update(reqJson) {
170
+ // 时间分表校验:condition 必须包含路由字段
171
+ this.validateRoutingFieldForCondition(reqJson, 'update');
220
172
  const targetTable = this.resolveSingleTable(reqJson, 'update');
221
- return this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_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
+ });
222
182
  }
223
- /**
224
- * 删除数据
225
- *
226
- * 根据条件中的分表字段自动路由到目标分表。
227
- *
228
- * @param reqJson 请求参数,condition 为删除条件
229
- * @returns 执行上下文
230
- */
231
183
  async delete(reqJson) {
184
+ // 时间分表校验:condition 必须包含路由字段
185
+ this.validateRoutingFieldForCondition(reqJson, 'delete');
232
186
  const targetTable = this.resolveSingleTable(reqJson, 'delete');
233
- return this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_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
+ });
234
196
  }
235
197
  /**
236
- * 插入或更新(存在则更新,不存在则插入)
198
+ * 恢复软删除的记录
199
+ * 将 deleted_at 字段重置为 0,deleted_by 重置为空字符串
237
200
  *
238
- * 路由逻辑:根据 condition 路由(先查询是否存在)
201
+ * @param reqJson 请求参数,通过 condition 指定要恢复的记录
202
+ * @returns CrudWriteResult 包含 affectedRows 和 getRawContext()
239
203
  *
240
- * @param reqJson 请求参数
241
- * @returns 执行上下文
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' } });
242
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
+ }
243
240
  async insertOrUpdate(reqJson) {
241
+ var _a;
242
+ // 时间分表校验:data 必须包含 timeColumn,condition 必须包含路由字段
243
+ this.validateTimeColumnForData(reqJson, 'insertOrUpdate');
244
+ this.validateRoutingFieldForCondition(reqJson, 'insertOrUpdate');
244
245
  const targetTable = this.resolveSingleTable(reqJson, 'update'); // 使用 condition 路由
245
- return this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE);
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
+ });
246
255
  }
247
- /**
248
- * 插入或更新(ON DUPLICATE KEY UPDATE)
249
- *
250
- * 路由逻辑:根据 condition 路由(先查询是否存在)
251
- *
252
- * @param reqJson 请求参数
253
- * @returns 执行上下文
254
- */
255
256
  async insertOnDuplicateUpdate(reqJson) {
257
+ // 时间分表校验:data 必须包含 timeColumn,condition 必须包含路由字段
258
+ this.validateTimeColumnForData(reqJson, 'insertOnDuplicateUpdate');
259
+ this.validateRoutingFieldForCondition(reqJson, 'insertOnDuplicateUpdate');
256
260
  const targetTable = this.resolveSingleTable(reqJson, 'update'); // 使用 condition 路由
257
- return this.executeOnTable(targetTable, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE);
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
+ });
258
268
  }
259
269
  // ============ 查询操作(可能多表) ============
260
- /**
261
- * 查询单条记录
262
- *
263
- * 如果能根据条件确定单一分表,则查询单表;
264
- * 否则按顺序查询各分表,返回第一条匹配记录。
265
- *
266
- * @param reqJson 请求参数,condition 为查询条件
267
- * @returns 单条记录,未找到返回 null
268
- *
269
- * @example
270
- * const order = await sharding.queryOne({
271
- * condition: { order_id: 'ORD001' },
272
- * });
273
- */
274
- async queryOne(reqJson) {
275
- const targetTables = await this.resolveQueryTables(reqJson);
270
+ async findOne(reqJson) {
271
+ // 清理 condition 中的时间字段(如果能确定单一分表)
272
+ const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
273
+ const targetTables = await this.resolveFindTables(reqJson);
276
274
  if (targetTables.length === 0) {
277
- return null; // 无匹配表
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
278
  }
279
279
  // 多表:顺序查询,找到即返回
280
+ let lastCtx = null;
280
281
  for (const table of targetTables) {
281
- const result = await this.queryOneFromTable(table, reqJson);
282
- if (result)
283
- return result;
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
+ }
284
293
  }
285
- return null;
294
+ // 未找到,使用最后一次查询的 ctx
295
+ return new CrudResult_1.CrudQueryOneResult({ row: null, rawContext: lastCtx, debugInfo: this.buildDebugInfo(targetTables[0], reqJson.condition) });
286
296
  }
287
297
  /**
288
- * 查询列表
298
+ * 查询唯一单条记录(期望结果为 0 条或 1 条)
289
299
  *
290
- * 可能涉及多个分表,结果会自动合并。
300
+ * 与 findOne 不同,此方法会校验查询结果的唯一性:
301
+ * - 如果查到 0 条:返回 row = null
302
+ * - 如果查到 1 条:正常返回
303
+ * - 如果查到多条:抛出异常,包含详细的定位信息(含分表名列表)
291
304
  *
292
- * 使用约束:
293
- * - 必须传 orderBy 参数
294
- * - orderBy 必须为 timeColumn DESC 或 timeColumn ASC(如 'created_at DESC' / 'created_at ASC')
305
+ * 分表场景的特殊处理:
306
+ * - 单分表:直接查询并校验唯一性
307
+ * - 多分表:顺序查询各分表,累计找到的数量,超过 1 条立即抛异常
295
308
  *
296
309
  * @param reqJson 请求参数
297
- * @returns 数据列表
310
+ * @returns CrudQueryOneResult 包含 row、found 和 getRawContext()
311
+ * @throws Error 如果查询到多条记录
298
312
  *
299
313
  * @example
300
- * const orders = await sharding.query({
301
- * condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
302
- * orderBy: 'created_at DESC', // 或 'created_at ASC'
303
- * });
314
+ * // 根据唯一索引查询单条
315
+ * const result = await sharding.findUniqueOne({ condition: { order_id: 'ORD001' } });
316
+ * if (result.found) {
317
+ * console.log(result.row);
318
+ * }
304
319
  */
305
- async query(reqJson) {
306
- const targetTables = await this.resolveQueryTables(reqJson);
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);
307
366
  if (targetTables.length === 0) {
308
- return []; // 无匹配表,返回空结果
367
+ return new CrudResult_1.CrudQueryListResult({ rows: [], rawContext: null });
309
368
  }
310
369
  if (targetTables.length === 1) {
311
- const ctx = await this.executeOnTable(targetTables[0], reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY);
312
- return ctx.getResRows();
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 });
313
373
  }
314
374
  // 多表查询时,需要参数校验
315
- this.validateQueryOrderBy(reqJson);
375
+ this.validateFindOrderBy(reqJson);
316
376
  // 根据排序方向确定表顺序:DESC 新→旧,ASC 旧→新
317
377
  const tablesForMerge = this.sortTablesForOrderBy(targetTables, reqJson);
318
378
  // 多表查询合并
319
- return this.merger.mergeQuery(this.crudPro, tablesForMerge, reqJson, this.buildCfg(keys_1.KeysOfSimpleSQL.SIMPLE_QUERY));
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 });
320
381
  }
321
- /**
322
- * 分页查询
323
- *
324
- * 自动处理跨分表的分页:
325
- * 1. 顺序累计查询各分表
326
- * 2. 截取目标数据(无需排序)
327
- *
328
- * 使用约束:
329
- * - 必须传 orderBy 参数
330
- * - orderBy 必须为 timeColumn DESC 或 timeColumn ASC(如 'created_at DESC' / 'created_at ASC')
331
- *
332
- * @param reqJson 请求参数,包含 pageNo、pageSize 和 orderBy
333
- * @returns 分页结果,包含 rows 和 total_count
334
- *
335
- * @example
336
- * const result = await sharding.queryPage({
337
- * condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
338
- * pageNo: 1,
339
- * pageSize: 10,
340
- * orderBy: 'created_at DESC', // 或 'created_at ASC'
341
- * });
342
- * console.log(result.rows, result.total_count);
343
- */
344
- async queryPage(reqJson) {
345
- const targetTables = await this.resolveQueryTables(reqJson);
382
+ async findPage(reqJson) {
383
+ // 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
384
+ const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
385
+ const targetTables = await this.resolveFindTables(reqJson);
346
386
  if (targetTables.length === 0) {
347
- return { rows: [], total_count: 0 }; // 无匹配表
387
+ throw new Error('[ShardingCrudPro] findPage: no matching tables found');
348
388
  }
349
389
  if (targetTables.length === 1) {
350
- const ctx = await this.executeOnTable(targetTables[0], reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_PAGE);
351
- return ctx.getResModelForQueryPage();
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
+ });
352
397
  }
353
398
  // 多表查询时,需要参数校验
354
- this.validateQueryOrderBy(reqJson);
399
+ this.validateFindOrderBy(reqJson);
355
400
  // 根据排序方向确定表顺序:DESC 新→旧,ASC 旧→新
356
401
  const tablesForMerge = this.sortTablesForOrderBy(targetTables, reqJson);
357
402
  // 多表分页查询:顺序累计
358
- return this.merger.mergePageQuery(this.crudPro, tablesForMerge, reqJson, this.buildCfg(keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_PAGE));
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
+ });
359
409
  }
360
- /**
361
- * 查询记录总数
362
- *
363
- * 如果涉及多个分表,会并行查询各分表并汇总。
364
- *
365
- * @param reqJson 请求参数
366
- * @returns 记录总数
367
- */
368
- async queryCount(reqJson) {
369
- const targetTables = await this.resolveQueryTables(reqJson);
410
+ async findCount(reqJson) {
411
+ var _a;
412
+ // 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
413
+ const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
414
+ const targetTables = await this.resolveFindTables(reqJson);
370
415
  if (targetTables.length === 0) {
371
- return 0; // 无匹配表
416
+ throw new Error('[ShardingCrudPro] findCount: no matching tables found');
372
417
  }
373
418
  if (targetTables.length === 1) {
374
- const ctx = await this.executeOnTable(targetTables[0], reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
375
- return ctx.getResModelItem('total_count') || 0;
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);
376
428
  }
377
- // 多表统计:并行查询后求和
378
- const countPromises = targetTables.map(table => this.queryCountFromTable(table, reqJson));
379
- const counts = await Promise.all(countPromises);
380
- return counts.reduce((sum, c) => sum + c, 0);
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 });
381
432
  }
382
- /**
383
- * 判断记录是否存在
384
- *
385
- * 如果涉及多个分表,会并行查询,任一分表存在即返回 true。
386
- *
387
- * @param reqJson 请求参数
388
- * @returns 是否存在
389
- */
390
433
  async isExist(reqJson) {
391
- const targetTables = await this.resolveQueryTables(reqJson);
434
+ // 清理 condition 中的时间字段(粒度字符串自动转范围、有主键时清理)
435
+ const cleanedReqJson = this.cleanTimeColumnForSingleTableQuery(reqJson);
436
+ const targetTables = await this.resolveFindTables(reqJson);
392
437
  if (targetTables.length === 0) {
393
- return false; // 无匹配表
438
+ throw new Error('[ShardingCrudPro] isExist: no matching tables found');
394
439
  }
395
440
  if (targetTables.length === 1) {
396
- const ctx = await this.executeOnTable(targetTables[0], reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
397
- return ctx.getResModelItem('is_exist') === true;
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
+ }
398
457
  }
399
- // 多表判断:任一表存在即返回 true
400
- const promises = targetTables.map(table => this.isExistInTable(table, reqJson));
401
- const results = await Promise.all(promises);
402
- return results.some(exists => exists);
458
+ return new CrudResult_1.CrudExistResult({ exists, rawContext: firstCtx });
403
459
  }
404
460
  // ============ 私有方法:分表路由 ============
405
- /**
406
- * 【batchInsert 专用】按分表对数据进行分组
407
- *
408
- * 仅用于 batchInsert 方法,遍历所有数据项,
409
- * 根据分表规则计算每条数据的目标分表,将相同分表的数据归为一组。
410
- *
411
- * @param dataArray 数据数组
412
- * @returns 分表 -> 数据列表 的映射
413
- */
414
461
  batchInsertGroupDataByTable(dataArray) {
415
462
  const groupedData = new Map();
416
463
  for (const item of dataArray) {
@@ -430,11 +477,6 @@ class ShardingCrudPro {
430
477
  // 批量插入使用 resolveForInsert,确保返回单一表名
431
478
  return this.router.resolveForInsert(this.config, context);
432
479
  }
433
- /**
434
- * 解析单个目标表
435
- *
436
- * 写操作必须确定单一目标表,否则抛出异常。
437
- */
438
480
  resolveSingleTable(reqJson, operation) {
439
481
  // insert 操作使用 resolveForInsert(从 data 提取字段)
440
482
  if (operation === 'insert') {
@@ -457,9 +499,6 @@ class ShardingCrudPro {
457
499
  }
458
500
  return result;
459
501
  }
460
- /**
461
- * 获取分表字段提示
462
- */
463
502
  getShardingColumnHint() {
464
503
  if (this.config.type === ShardingConfig_1.ShardingType.RANGE || this.config.type === ShardingConfig_1.ShardingType.HASH) {
465
504
  return this.config.shardingColumn || '分表字段';
@@ -470,110 +509,78 @@ class ShardingCrudPro {
470
509
  return '分表字段';
471
510
  }
472
511
  // ============ 私有方法:配置构建 ============
473
- /**
474
- * 构建配置
475
- */
476
512
  buildCfg(sqlSimpleName) {
477
513
  return {
514
+ method: `ShardingCrudProAnonymous_${sqlSimpleName}`,
478
515
  ...this.baseCfg,
479
516
  sqlTable: this.config.baseTable,
480
517
  sqlSimpleName,
481
518
  };
482
519
  }
483
520
  // ============ 私有方法:执行操作 ============
484
- /**
485
- * 在指定表上执行操作
486
- */
487
- async executeOnTable(table, reqJson, sqlSimpleName) {
521
+ async executeOnTable(table, reqJson, sqlSimpleName, extraCfg) {
488
522
  const cfg = this.buildCfg(sqlSimpleName);
489
523
  cfg.sqlTable = table; // 替换为实际分表名
490
- return this.crudPro.executeCrudByCfg(reqJson, cfg);
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);
491
533
  }
492
- /**
493
- * 从指定表查询单条
494
- */
495
- async queryOneFromTable(table, reqJson) {
496
- try {
497
- const ctx = await this.executeOnTable(table, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_ONE);
498
- return ctx.getOneObj();
499
- }
500
- catch (e) {
501
- // 表不存在时返回 null
502
- return null;
503
- }
504
- }
505
- /**
506
- * 从指定表查询数量
507
- */
508
- async queryCountFromTable(table, reqJson) {
534
+ async findCountFromTable(table, reqJson) {
509
535
  try {
510
536
  const ctx = await this.executeOnTable(table, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_COUNT);
511
- return ctx.getResModelItem('total_count') || 0;
537
+ return { count: ctx.getResModelItem('total_count') || 0, ctx };
512
538
  }
513
539
  catch (e) {
514
540
  // 表不存在时返回 0
515
- return 0;
541
+ return { count: 0, ctx: null };
516
542
  }
517
543
  }
518
- /**
519
- * 判断指定表中是否存在记录
520
- */
521
544
  async isExistInTable(table, reqJson) {
522
545
  try {
523
546
  const ctx = await this.executeOnTable(table, reqJson, keys_1.KeysOfSimpleSQL.SIMPLE_QUERY_EXIST);
524
- return ctx.getResModelItem('is_exist') === true;
547
+ return { exists: ctx.getResModelItem('is_exist') === true, ctx };
525
548
  }
526
549
  catch (e) {
527
550
  // 表不存在时返回 false
528
- return false;
551
+ return { exists: false, ctx: null };
529
552
  }
530
553
  }
531
554
  // ============ 表存在性检查和自动创建 ============
532
- /**
533
- * 获取数据库中真实存在的表名集合
534
- */
535
- async getExistingTablesSet() {
555
+ async getExistingTablesSet(skipCache = false) {
536
556
  const { sqlDatabase, sqlDbType } = this.baseCfg;
537
557
  if (!sqlDatabase || !sqlDbType) {
538
558
  throw new Error('[ShardingCrudPro] 未配置 sqlDatabase 或 sqlDbType');
539
559
  }
540
- const { tables } = await this.crudPro.getAllTableInfos({
560
+ const { tables } = await this.crudProFactory().getAllTableInfos({
541
561
  sqlDatabase,
542
562
  sqlDbType: sqlDbType,
543
- });
563
+ }, { skipCache });
544
564
  return new Set(tables.map(t => t.name));
545
565
  }
546
- /**
547
- * 检查表是否存在
548
- */
549
566
  async isTableExists(tableName) {
550
- const existingSet = await this.getExistingTablesSet();
567
+ const existingSet = await this.getExistingTablesSet(); // 使用缓存
551
568
  return existingSet.has(tableName);
552
569
  }
553
- /**
554
- * 创建分表(如果需要)
555
- *
556
- * 根据配置检查分表是否存在:
557
- * - 如果分表已存在,直接返回
558
- * - 如果分表不存在且 autoCreateTable=true,自动创建
559
- * - 如果分表不存在且 autoCreateTable=false,抛出异常
560
- *
561
- * @param tableName 目标分表名
562
- */
563
570
  async createShardingTableIfNeeded(tableName) {
564
- // 检查表是否已存在
565
- if (await this.isTableExists(tableName)) {
566
- return;
567
- }
568
571
  // 根据配置决定是否自动创建
569
572
  if (!this.config.autoCreateTable) {
570
- throw new Error(`[ShardingCrudPro] 分表 ${tableName} 不存在。请先创建分表,或设置 autoCreateTable: true 自动创建`);
573
+ // 使用缓存检查表是否存在,不存在则抛异常
574
+ if (!(await this.isTableExists(tableName))) {
575
+ throw new Error(`[ShardingCrudPro] 分表 ${tableName} 不存在。请先创建分表,或设置 autoCreateTable: true 自动创建`);
576
+ }
577
+ return;
571
578
  }
572
579
  // 检查数据库配置
573
580
  if (!this.baseCfg.sqlDatabase || !this.baseCfg.sqlDbType) {
574
581
  throw new Error('[ShardingCrudPro] 请先调用 setBaseCfg 设置数据库配置');
575
582
  }
576
- // 执行创建
583
+ // 执行创建(内部有 checkTableExists + 建表 + 刷新缓存)
577
584
  const result = await this.tableCreator.createTableIfNeeded(tableName, {
578
585
  sqlDatabase: this.baseCfg.sqlDatabase,
579
586
  sqlDbType: this.baseCfg.sqlDbType,
@@ -581,23 +588,204 @@ class ShardingCrudPro {
581
588
  if (!result.success) {
582
589
  throw result.error || new Error(`[ShardingCrudPro] 创建分表 ${tableName} 失败`);
583
590
  }
591
+ // 建表成功后刷新缓存,确保后续请求命中新表
592
+ if (result.createSql) {
593
+ await this.getExistingTablesSet(true);
594
+ }
584
595
  }
585
596
  // ============ 参数校验 ============
586
597
  /**
587
- * 校验查询参数的 orderBy 是否符合约束
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)
588
641
  *
589
- * 约束条件:
590
- * - 必须传 orderBy 参数
591
- * - orderBy 必须为 timeColumn DESC 或 timeColumn ASC(如 'created_at DESC' / 'created_at ASC')
642
+ * 时间分表需要根据时间字段路由到正确的分表,
643
+ * 如果 condition 中缺少时间字段,将无法确定操作哪个分表。
592
644
  *
593
- * 这是时间分表查询的核心约束:
594
- * - 排序字段必须是分表字段(timeColumn),不允许其他字段
595
- * - DESC 时表顺序为 新→旧,ASC 时表顺序为 旧→新(由调用方反转)
596
- * - 这样无需内存排序,直接按表顺序拼接即可
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
+ * ```
597
687
  *
598
688
  * @param reqJson 请求参数
689
+ * @returns 处理后的请求参数
599
690
  */
600
- validateQueryOrderBy(reqJson) {
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) {
601
789
  // 非时间分表不强制 orderBy 约束(多表合并排序由调用方自行保证)
602
790
  const timeColumn = this.config.timeColumn;
603
791
  if (!timeColumn) {
@@ -626,40 +814,56 @@ class ShardingCrudPro {
626
814
  `当前值: '${firstOrderBy.orderType}'`);
627
815
  }
628
816
  }
629
- /**
630
- * 判断 orderBy 是否为 ASC 方向
631
- *
632
- * 前提:validateQueryOrderBy 已通过校验,orderBy 格式合法
633
- *
634
- * @param reqJson 请求参数
635
- * @returns true 表示 ASC,false 表示 DESC
636
- */
637
817
  isAscOrderBy(reqJson) {
638
818
  return OrderByUtils_1.OrderByUtils.isFirstOrderByAsc(reqJson.orderBy);
639
819
  }
640
- /**
641
- * 根据排序方向对分表列表进行排序
642
- *
643
- * 分表名后缀为时间格式(如 202403、20240101),字典序即时间序。
644
- * - DESC:降序排列(新→旧)
645
- * - ASC:升序排列(旧→新)
646
- *
647
- * @param tables 分表列表
648
- * @param reqJson 请求参数
649
- * @returns 排序后的分表列表(新数组,不修改原数组)
650
- */
651
820
  sortTablesForOrderBy(tables, reqJson) {
652
821
  const isAsc = this.isAscOrderBy(reqJson);
653
822
  return [...tables].sort((a, b) => isAsc ? a.localeCompare(b) : b.localeCompare(a));
654
823
  }
655
- // ============ 查询表解析(委托给 ShardingRouter) ============
824
+ // ============ 辅助方法 ============
656
825
  /**
657
- * 解析查询表(带真实表过滤)
658
- *
659
- * 将查询路由委托给 ShardingRouter.resolveQuery,
660
- * 由 ShardingRouter 统一处理所有查询路由逻辑。
826
+ * 构建辅助定位信息
661
827
  */
662
- async resolveQueryTables(reqJson) {
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) {
663
867
  const context = {
664
868
  config: this.config,
665
869
  condition: reqJson.condition,