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