midway-fatcms 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.qoder/skills/midway-fatcms/02-crud-quick.md +38 -0
  2. package/.qoder/skills/midway-fatcms/03-crud-sharding.md +37 -36
  3. package/.qoder/skills/midway-fatcms/07-examples.md +4 -0
  4. package/dist/configuration.d.ts +10 -0
  5. package/dist/configuration.js +26 -0
  6. package/dist/controller/helpers.controller.d.ts +6 -0
  7. package/dist/controller/helpers.controller.js +19 -0
  8. package/dist/libs/crud-pro/CrudPro.d.ts +29 -2
  9. package/dist/libs/crud-pro/CrudPro.js +58 -2
  10. package/dist/libs/crud-pro/exceptions.d.ts +7 -0
  11. package/dist/libs/crud-pro/exceptions.js +7 -0
  12. package/dist/libs/crud-pro/interfaces.d.ts +1 -0
  13. package/dist/libs/crud-pro/models/CrudResult.d.ts +3 -2
  14. package/dist/libs/crud-pro/models/CrudResult.js +1 -1
  15. package/dist/libs/crud-pro/models/ServiceHub.d.ts +2 -0
  16. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +70 -2
  17. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +205 -13
  18. package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +36 -0
  19. package/dist/libs/crud-pro/services/CrudProTableMetaService.js +97 -3
  20. package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +2 -0
  21. package/dist/libs/crud-pro/services/CurdProServiceHub.js +6 -0
  22. package/dist/libs/crud-pro-quick/CrudProQuick.d.ts +93 -6
  23. package/dist/libs/crud-pro-quick/CrudProQuick.js +192 -32
  24. package/dist/libs/crud-sharding/ShardingBase.d.ts +78 -0
  25. package/dist/libs/crud-sharding/ShardingBase.js +179 -0
  26. package/dist/libs/crud-sharding/ShardingByCustomCrud.d.ts +35 -0
  27. package/dist/libs/crud-sharding/ShardingByCustomCrud.js +297 -0
  28. package/dist/libs/crud-sharding/ShardingByHashCrud.d.ts +38 -0
  29. package/dist/libs/crud-sharding/ShardingByHashCrud.js +86 -0
  30. package/dist/libs/crud-sharding/ShardingByKeyCrud.d.ts +39 -0
  31. package/dist/libs/crud-sharding/ShardingByKeyCrud.js +74 -0
  32. package/dist/libs/crud-sharding/ShardingByTimeCrud.d.ts +66 -0
  33. package/dist/libs/crud-sharding/ShardingByTimeCrud.js +524 -0
  34. package/dist/libs/crud-sharding/ShardingConfig.d.ts +10 -8
  35. package/dist/libs/crud-sharding/ShardingConfig.js +3 -3
  36. package/dist/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +1 -1
  37. package/dist/libs/crud-sharding/index.d.ts +10 -13
  38. package/dist/libs/crud-sharding/index.js +21 -17
  39. package/dist/models/RedisKeys.d.ts +1 -0
  40. package/dist/models/RedisKeys.js +1 -0
  41. package/dist/service/TableMetaCacheRedisSubscriber.d.ts +31 -0
  42. package/dist/service/TableMetaCacheRedisSubscriber.js +98 -0
  43. package/dist/service/curd/CurdMixService.d.ts +2 -2
  44. package/dist/service/curd/CurdProService.d.ts +109 -5
  45. package/dist/service/curd/CurdProService.js +127 -7
  46. package/package.json +1 -1
  47. package/src/configuration.ts +27 -0
  48. package/src/controller/helpers.controller.ts +15 -0
  49. package/src/libs/crud-pro/CrudPro.ts +73 -4
  50. package/src/libs/crud-pro/exceptions.ts +8 -0
  51. package/src/libs/crud-pro/interfaces.ts +1 -0
  52. package/src/libs/crud-pro/models/CrudResult.ts +5 -5
  53. package/src/libs/crud-pro/models/ServiceHub.ts +4 -0
  54. package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +238 -15
  55. package/src/libs/crud-pro/services/CrudProTableMetaService.ts +110 -2
  56. package/src/libs/crud-pro/services/CurdProServiceHub.ts +8 -0
  57. package/src/libs/crud-pro-quick/CrudProQuick.ts +234 -46
  58. package/src/libs/crud-sharding/ShardingBase.ts +256 -0
  59. package/src/libs/crud-sharding/ShardingByCustomCrud.ts +329 -0
  60. package/src/libs/crud-sharding/ShardingByHashCrud.ts +111 -0
  61. package/src/libs/crud-sharding/ShardingByKeyCrud.ts +97 -0
  62. package/src/libs/crud-sharding/ShardingByTimeCrud.ts +628 -0
  63. package/src/libs/crud-sharding/ShardingConfig.ts +10 -8
  64. package/src/libs/crud-sharding/TIME_COLUMN_CLEAN_SPEC.md +1 -1
  65. package/src/libs/crud-sharding/index.ts +17 -14
  66. package/src/models/RedisKeys.ts +1 -0
  67. package/src/service/TableMetaCacheRedisSubscriber.ts +105 -0
  68. package/src/service/curd/CurdMixService.ts +2 -2
  69. package/src/service/curd/CurdProService.ts +131 -9
  70. package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +0 -208
  71. package/dist/libs/crud-sharding/ShardingCrudPro.js +0 -879
  72. package/dist/libs/crud-sharding/ShardingRouter.d.ts +0 -70
  73. package/dist/libs/crud-sharding/ShardingRouter.js +0 -396
  74. package/src/libs/crud-sharding/ShardingCrudPro.ts +0 -1105
  75. package/src/libs/crud-sharding/ShardingRouter.ts +0 -533
@@ -38,6 +38,7 @@ const quick = this.curdProService.getQuickCrud({
38
38
  | `restore(req, table?)` | `CrudWriteResult` | 恢复软删除记录(重置 deleted_at/deleted_by) |
39
39
  | `insertOrUpdate(req, table?)` | `CrudUpsertResult` | 先查再插/更 |
40
40
  | `insertOnDuplicateUpdate(req, uniqueCols?, table?)` | `CrudWriteResult` | 原生 upsert |
41
+ | `save(req, table?)` | `CrudUpsertResult` | 自动判断 INSERT/UPDATE |
41
42
 
42
43
  ### SQL 方法
43
44
 
@@ -220,6 +221,21 @@ console.log(result.isExist ? '更新' : '插入');
220
221
 
221
222
  ### insertOnDuplicateUpdate - 原生 Upsert
222
223
 
224
+ > **前置条件**:目标表必须拥有 UNIQUE 索引(除自增主键外)。
225
+ >
226
+ > `INSERT ON DUPLICATE KEY UPDATE` 只在 UNIQUE 索引或 PRIMARY KEY 冲突时触发 UPDATE。
227
+ > 如果表只有普通索引(KEY/INDEX),MySQL 不会报错,但**永远走 INSERT 路径,不会触发 UPDATE**,且每次调用都会插入新行。
228
+ >
229
+ > ```sql
230
+ > -- 检查表是否有 UNIQUE 索引
231
+ > SELECT DISTINCT INDEX_NAME FROM information_schema.STATISTICS
232
+ > WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'your_table'
233
+ > AND NON_UNIQUE = 0 AND INDEX_NAME != 'PRIMARY';
234
+ >
235
+ > -- 添加 UNIQUE 索引
236
+ > ALTER TABLE your_table ADD UNIQUE KEY uk_xxx (column_name);
237
+ > ```
238
+
223
239
  ```typescript
224
240
  const result = await quick.insertOnDuplicateUpdate(
225
241
  { data: { id: 1, name: '用户' } },
@@ -227,6 +243,28 @@ const result = await quick.insertOnDuplicateUpdate(
227
243
  );
228
244
  ```
229
245
 
246
+ ### save - 自动判断 INSERT/UPDATE
247
+
248
+ 只需传入 `data`,无需手动指定 `condition`。方法会自动查询表的主键列,根据 `data` 中是否包含主键值来决定操作:
249
+
250
+ - **data 包含主键且值不为 null/undefined** → 调用 `insertOrUpdate`(先查后写,存在则更新,不存在则插入)
251
+ - **data 不包含主键或主键值为 null/undefined** → 调用 `insert`(直接插入)
252
+
253
+ ```typescript
254
+ // 有主键 → 先查后写
255
+ await quick.save({ data: { id: 1, name: '张三' } });
256
+
257
+ // 无主键 → 直接 INSERT
258
+ await quick.save({ data: { name: '李四' } });
259
+
260
+ // 主键值为 null → 视为无主键,直接 INSERT
261
+ await quick.save({ data: { id: null, name: '王五' } });
262
+ ```
263
+
264
+ > **与 insertOrUpdate 的区别**:`save` 不需要传 `condition`,自动从 `data` 中提取主键组成 `condition`。本质是 `insertOrUpdate` 的便捷版。
265
+ >
266
+ > **性能说明**:首次调用 `save` 时会查询表的主键列信息,结果缓存 24 小时,后续调用无额外开销。
267
+
230
268
  ---
231
269
 
232
270
  ## SQL 方法详解
@@ -1,6 +1,8 @@
1
- # ShardingCrudPro 分表操作
1
+ # 分表 CRUD 操作
2
2
 
3
- ShardingCrudPro 提供自动路由分表的能力,支持时间分表、哈希分表、范围分表、自定义分表四种策略。
3
+ 分表模块提供自动路由分表的能力,支持四种策略:时间分表(ShardingByTimeCrud)、哈希分表(ShardingByHashCrud)、键值分表(ShardingByKeyCrud)、自定义分表(ShardingByCustomCrud)。
4
+
5
+ 基类 `ShardingBase` 提供通用基础设施。`ShardingByTimeCrud` 和 `ShardingByCustomCrud` 各自包含完整的路由和 CRUD 逻辑;`ShardingByHashCrud` 和 `ShardingByKeyCrud` 内部委托 `ShardingByCustomCrud` 实现,仅在构造函数中定义路由逻辑。
4
6
 
5
7
  ## 获取实例
6
8
 
@@ -26,20 +28,20 @@ const sharding = this.curdProService.getShardingCrud({
26
28
 
27
29
  ## 分表类型
28
30
 
29
- | 类型 | 枚举值 | 后缀示例 | 适用场景 |
30
- |------|--------|----------|----------|
31
- | 按年 | `ShardingType.YEAR` | `_2024` | 数据量适中,按年归档 |
32
- | 按月 | `ShardingType.MONTH` | `_202401` | 大数据量,按月分表 |
33
- | 按日 | `ShardingType.DAY` | `_20240115` | 超大数据量,按天分表 |
34
- | 范围 | `ShardingType.RANGE` | `_0`, `_1`... | 按数值范围分布 |
35
- | 哈希 | `ShardingType.HASH` | `_01`, `_02`... | 均匀分布数据 |
36
- | 自定义 | `ShardingType.CUSTOM` | 自定义 | 特殊业务规则 |
31
+ | 类型 | 枚举值 | 实现类 | 后缀示例 | 适用场景 |
32
+ |------|--------|--------|----------|----------|
33
+ | 按年 | `ShardingType.YEAR` | `ShardingByTimeCrud` | `_2024` | 数据量适中,按年归档 |
34
+ | 按月 | `ShardingType.MONTH` | `ShardingByTimeCrud` | `_202401` | 大数据量,按月分表 |
35
+ | 按日 | `ShardingType.DAY` | `ShardingByTimeCrud` | `_20240115` | 超大数据量,按天分表 |
36
+ | 哈希 | `ShardingType.HASH` | `ShardingByHashCrud` | `_00`, `_01`... | 均匀分布数据 |
37
+ | 键值 | `ShardingType.KEY` | `ShardingByKeyCrud` | `_east`, `_west`... | 字段原值=表后缀,一值一表 |
38
+ | 自定义 | `ShardingType.CUSTOM` | `ShardingByCustomCrud` | 自定义 | 特殊业务规则(含原 RANGE) |
37
39
 
38
40
  ## 配置选项
39
41
 
40
42
  ```typescript
41
43
  interface IShardingConfig {
42
- type: ShardingType; // 分表类型(必填)
44
+ type: ShardingType; // 分表类型(YEAR/MONTH/DAY/HASH/KEY/CUSTOM)
43
45
  baseTable: string; // 基础表名(必填)
44
46
 
45
47
  // 时间分表(YEAR/MONTH/DAY)
@@ -52,9 +54,9 @@ interface IShardingConfig {
52
54
  tableOptions?: string; // 表选项(如MySQL引擎配置)
53
55
  };
54
56
 
55
- // 范围/哈希分表(RANGE/HASH
56
- shardingColumn?: string; // 分表字段(RANGE/HASH必填)
57
- tableCount?: number; // 分表数量(RANGE默认10,HASH默认16)
57
+ // 哈希/键值分表(HASH/KEY
58
+ shardingColumn?: string; // 分表字段(HASH/KEY必填)
59
+ tableCount?: number; // 分表数量(仅 HASH,必填)
58
60
 
59
61
  // COUNT 缓存(仅时间分表有效)
60
62
  countCache?: {
@@ -74,8 +76,8 @@ interface IShardingConfig {
74
76
 
75
77
  | 方法 | 返回类型 | 说明 |
76
78
  |------|----------|------|
77
- | `findOne(req)` | `CrudQueryOneResult<T>` | 查询单条(顺序查多表,找到即返回) |
78
- | `findUniqueOne(req)` | `CrudQueryOneResult<T>` | 唯一查询(0条返回null,多条报错) |
79
+ | `findOne(req)` | `CrudQueryOneResult<T>` | 查询单条(顺序查多表,找到即返回,含 fromTable 物理表名) |
80
+ | `findUniqueOne(req)` | `CrudQueryOneResult<T>` | 唯一查询(0条返回null,多条报错,含 fromTable 物理表名) |
79
81
  | `find(req)` | `T[]` | 列表查询(多表自动合并) |
80
82
  | `findList(req)` | `CrudQueryListResult<T>` | 列表查询(带结果包装) |
81
83
  | `findPage(req)` | `CrudQueryPageResult<T>` | 分页查询(跨分表自动处理) |
@@ -91,8 +93,7 @@ interface IShardingConfig {
91
93
  | `update(req)` | `CrudWriteResult` | 更新(必须能确定单一目标表) |
92
94
  | `delete(req)` | `CrudWriteResult` | 删除(必须能确定单一目标表) |
93
95
  | `restore(req)` | `CrudWriteResult` | 恢复软删除记录(重置 deleted_at/deleted_by) |
94
- | `insertOrUpdate(req)` | `CrudUpsertResult` | 插入或更新(condition决定分表) |
95
- | `insertOnDuplicateUpdate(req)` | `CrudWriteResult` | 原生upsert(condition决定分表) |
96
+ | `insertOrUpdate(req)` | `CrudUpsertResult` | 插入或更新(统一由 condition 路由定位分表) |
96
97
 
97
98
  ### 配置方法
98
99
 
@@ -113,8 +114,7 @@ interface IShardingConfig {
113
114
  | `insert` | `data[分表字段]` | 提取值 → 计算后缀 → 单表 | 插入单条数据 |
114
115
  | `batchInsert` | `data[i][分表字段]` | 每条数据单独计算分表 | 支持跨分表并行写入 |
115
116
  | `update/delete` | `condition[分表字段]` | 提取值 → 计算后缀 → 单表 | condition决定分表,data是更新内容 |
116
- | `insertOrUpdate` | `condition[分表字段]` | 提取值 计算后缀 单表 | condition决定分表 |
117
- | `insertOnDuplicateUpdate` | `condition[分表字段]` | 提取值 → 计算后缀 → 单表 | condition决定分表 |
117
+ | `insertOrUpdate` | `condition[分表字段]` | condition 路由定位分表,查找与插入在同一分表 | 统一由 condition 决定目标分表 |
118
118
  | `find/findPage` | `condition[分表字段]` | 时间范围 → 候选表 → 与真实表取交集 → 多表 | 自动合并多表结果 |
119
119
  | `findOne/findUniqueOne` | `condition[分表字段]` | 同 find | 顺序查询各分表 |
120
120
  | `findCount/isExist` | `condition[分表字段]` | 同 find | 多表并行查询后汇总 |
@@ -123,7 +123,8 @@ interface IShardingConfig {
123
123
 
124
124
  - **插入类操作**(insert/batchInsert):只使用 `data`,不需要传 `condition`
125
125
  - **查询类操作**(find/delete/isExist等):只使用 `condition`,不使用 `data`
126
- - **更新类操作**(update/insertOrUpdate/insertOnDuplicateUpdate):`condition` 用于路由,`data` 是更新内容
126
+ - **更新类操作**(update):`condition` 用于路由,`data` 是更新内容
127
+ - **插入或更新**(insertOrUpdate):统一由 `condition` 路由定位分表,查找与插入在同一分表
127
128
 
128
129
  ## 时间分表约束
129
130
 
@@ -141,7 +142,6 @@ interface IShardingConfig {
141
142
  | `delete` | `condition` 必须包含 `timeColumn` | `delete 操作的 condition 必须包含时间字段 'xxx'` |
142
143
  | `restore` | `condition` 必须包含 `timeColumn` | `restore 操作的 condition 必须包含时间字段 'xxx'` |
143
144
  | `insertOrUpdate` | `data` + `condition` 均须包含 `timeColumn` | 同上 |
144
- | `insertOnDuplicateUpdate` | `data` + `condition` 均须包含 `timeColumn` | 同上 |
145
145
 
146
146
  ```typescript
147
147
  // ✅ 正确 - insert 包含时间字段
@@ -277,7 +277,7 @@ condition: { created_at: '2026' } // → { $gte: '2026-01-01 00:00:00',
277
277
 
278
278
  ### 软删除支持
279
279
 
280
- ShardingCrudPro 支持软删除模式,启用后 `delete` 操作会设置 `deleted_at` 和 `deleted_by` 字段,而非物理删除:
280
+ 分表 CRUD 支持软删除模式,启用后 `delete` 操作会设置 `deleted_at` 和 `deleted_by` 字段,而非物理删除:
281
281
 
282
282
  ```typescript
283
283
  const sharding = this.curdProService
@@ -410,7 +410,7 @@ const sharding = this.curdProService.getShardingCrud({
410
410
  type: ShardingType.HASH,
411
411
  baseTable: 't_user',
412
412
  shardingColumn: 'user_id',
413
- tableCount: 16, // t_user_01 ~ t_user_16
413
+ tableCount: 16, // t_user_00 ~ t_user_15
414
414
  },
415
415
  });
416
416
 
@@ -419,15 +419,14 @@ await sharding.insert({
419
419
  data: { user_id: 10001, name: '张三' } // → 路由到 t_user_05
420
420
  });
421
421
 
422
- // 查询 - 需要提供 user_id 用于路由
422
+ // 查询 - 必须提供 user_id 用于路由
423
423
  const user = await sharding.findOne({
424
424
  condition: { user_id: 10001 } // 根据 user_id 确定分表
425
425
  });
426
426
 
427
- // 查询多用户 - user_id 时扫描多个分表
428
- const users = await sharding.find({
429
- condition: { status: 'active' },
430
- orderBy: 'created_at DESC',
427
+ // 错误 - 缺少 shardingColumn 会抛出异常
428
+ await sharding.findOne({
429
+ condition: { status: 'active' } // 报错:condition 必须包含分表字段 'user_id'
431
430
  });
432
431
  ```
433
432
 
@@ -479,10 +478,12 @@ const sharding = this.curdProService.getShardingCrud({
479
478
  ## 注意事项
480
479
 
481
480
  1. **时间分表写操作必须包含 timeColumn**:insert 的 `data` 或 update/delete 的 `condition` 中必须提供时间字段值
482
- 2. **插入操作必须包含分表字段**:`data` 中必须有分表字段值
483
- 3. **批量插入支持跨分表**:自动按分表分组并行写入
484
- 4. **查询时无时间范围限制**:默认查询最近 N 张分表(YEAR=2, MONTH=3, DAY=7,可配置 `recentTableCount`)
485
- 5. **COUNT 缓存仅对时间分表有效**:哈希/范围分表不启用缓存
486
- 6. **时间字段智能处理**:有主键时精确值自动清理;无主键时日期粒度字符串自动转换为 $gte/$lte 范围;操作符表达式始终保留
487
- 7. **软删除恢复**:启用 setEnableSoftDelete 后可使用 restore 方法恢复已删除记录
488
- 8. **$gte/$lte 必须精确到秒**:使用操作符查询时间字段时值必须包含时分秒(如 `'2026-04-30 23:59:59'`),不允许只传日期部分(`'2026-04-30'`);如需查整月/整天数据,推荐直接传粒度字符串(`'2026-04'`/`'2026-04-15'`)由系统自动转换
481
+ 2. **哈希/键值分表所有操作必须包含 shardingColumn**:insert 的 `data`、query/update/delete 的 `condition` 中必须提供分表字段值,否则抛出异常
482
+ 3. **insertOrUpdate 统一路由**:由 `condition` 路由定位分表,查找与插入在同一分表
483
+ 4. **批量插入支持跨分表**:自动按分表分组并行写入
484
+ 5. **查询时无时间范围限制**:默认查询最近 N 张分表(YEAR=2, MONTH=3, DAY=7,可配置 `recentTableCount`)
485
+ 6. **COUNT 缓存仅对时间分表有效**:哈希/键值/自定义分表不启用缓存
486
+ 7. **时间字段智能处理**:有主键时精确值自动清理;无主键时日期粒度字符串自动转换为 $gte/$lte 范围;操作符表达式始终保留
487
+ 8. **软删除恢复**:启用 setEnableSoftDelete 后可使用 restore 方法恢复已删除记录
488
+ 9. **$gte/$lte 必须精确到秒**:使用操作符查询时间字段时值必须包含时分秒(如 `'2026-04-30 23:59:59'`),不允许只传日期部分(`'2026-04-30'`);如需查整月/整天数据,推荐直接传粒度字符串(`'2026-04'`/`'2026-04-15'`)由系统自动转换
489
+ 10. **findOne/findUniqueOne 返回 fromTable**:结果中的 `fromTable` 字段标识数据来源的物理分表名(如 `t_order_202403`),未找到数据时为 undefined
@@ -196,6 +196,10 @@ await quick.insertOnDuplicateUpdate(
196
196
  { data: { id: 1, name: '用户', age: 25 } },
197
197
  ['id'] // 唯一列,PostgreSQL/SQL Server 必填
198
198
  );
199
+
200
+ // save - 自动判断 INSERT/UPDATE(只需 data)
201
+ await quick.save({ data: { id: 1, name: '用户', age: 25 } }); // 有主键 → 先查后写
202
+ await quick.save({ data: { name: '新用户', age: 20 } }); // 无主键 → 直接插入
199
203
  ```
200
204
 
201
205
  ## 软删除与恢复
@@ -3,5 +3,15 @@ export declare class ContainerLifeCycle {
3
3
  app: koa.Application;
4
4
  onConfigLoad(): Promise<any>;
5
5
  onReady(): Promise<void>;
6
+ /**
7
+ * 初始化表元数据缓存的 Redis Pub/Sub 广播
8
+ *
9
+ * - 发布客户端:复用已有的 RedisService 实例
10
+ * - 订阅客户端:通过 duplicate() 创建独立连接
11
+ *
12
+ * 集群部署时,任意节点调用 clearTableMetaCacheWithBroadcast()
13
+ * 会通过 Redis 广播通知所有节点清空本地缓存。
14
+ */
15
+ private initTableMetaCacheRedis;
6
16
  private startScheduleOnReady;
7
17
  }
@@ -27,6 +27,8 @@ const global_middleware_1 = require("./middleware/global.middleware");
27
27
  const forbidden_middleware_1 = require("./middleware/forbidden.middleware");
28
28
  const schedule_1 = require("./schedule");
29
29
  const crypto_utils_1 = require("./libs/utils/crypto-utils");
30
+ const TableMetaCacheRedisSubscriber_1 = require("./service/TableMetaCacheRedisSubscriber");
31
+ const redis_1 = require("@midwayjs/redis");
30
32
  let ContainerLifeCycle = class ContainerLifeCycle {
31
33
  async onConfigLoad() {
32
34
  const config = this.app.getConfig();
@@ -53,6 +55,10 @@ let ContainerLifeCycle = class ContainerLifeCycle {
53
55
  * 让ANONYMOUS_CONTEXT获取app对象
54
56
  */
55
57
  schedule_1.ANONYMOUS_CONTEXT.setApp(this.app);
58
+ /**
59
+ * 初始化表元数据缓存 Redis 广播
60
+ */
61
+ await this.initTableMetaCacheRedis();
56
62
  /**
57
63
  * 启动定时任务
58
64
  */
@@ -63,6 +69,26 @@ let ContainerLifeCycle = class ContainerLifeCycle {
63
69
  // add filter
64
70
  // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
65
71
  }
72
+ /**
73
+ * 初始化表元数据缓存的 Redis Pub/Sub 广播
74
+ *
75
+ * - 发布客户端:复用已有的 RedisService 实例
76
+ * - 订阅客户端:通过 duplicate() 创建独立连接
77
+ *
78
+ * 集群部署时,任意节点调用 clearTableMetaCacheWithBroadcast()
79
+ * 会通过 Redis 广播通知所有节点清空本地缓存。
80
+ */
81
+ async initTableMetaCacheRedis() {
82
+ try {
83
+ const redisService = await this.app.getApplicationContext().getAsync(redis_1.RedisService);
84
+ (0, TableMetaCacheRedisSubscriber_1.initTableMetaCachePublishClient)(redisService);
85
+ (0, TableMetaCacheRedisSubscriber_1.initTableMetaCacheSubscriber)(redisService);
86
+ }
87
+ catch (e) {
88
+ // Redis 未配置或不可用时,降级为单节点模式(仅清空本地缓存)
89
+ this.app.getLogger().warn('ContainerLifeCycle ==> initTableMetaCacheRedis Redis不可用,降级为单节点模式:', e.message);
90
+ }
91
+ }
66
92
  async startScheduleOnReady() {
67
93
  const logger = this.app.getLogger();
68
94
  const config = this.app.getConfig();
@@ -32,5 +32,11 @@ export declare class HelpersApi {
32
32
  * @param queryData
33
33
  */
34
34
  cryptoAes128CBC(queryData: any): Promise<CommonResult>;
35
+ /**
36
+ * 工具函数: 清空表元数据缓存(集群广播)
37
+ * 清空本节点缓存,并通过 Redis Pub/Sub 广播通知集群中其他节点。
38
+ * 清空后,下次请求会自动从数据库重新加载(懒加载刷新)。
39
+ */
40
+ clearTableMetaCacheApi(): Promise<CommonResult>;
35
41
  private checkLocalPermissionEnv;
36
42
  }
@@ -21,6 +21,7 @@ const functions_1 = require("../libs/utils/functions");
21
21
  const crypto_utils_1 = require("../libs/utils/crypto-utils");
22
22
  const common_dto_1 = require("../libs/utils/common-dto");
23
23
  const SystemPerm_1 = require("../models/SystemPerm");
24
+ const TableMetaCacheRedisSubscriber_1 = require("../service/TableMetaCacheRedisSubscriber");
24
25
  // http://127.0.0.1:7002/ns/api/helpers/getApiScript?prefix=/ns/api/manage
25
26
  // http://127.0.0.1:7002/ns/api/helpers/getApiScript
26
27
  // http://127.0.0.1:7002/ns/api/helpers/getApiEnv
@@ -124,6 +125,18 @@ let HelpersApi = class HelpersApi {
124
125
  isEqual: input === decrypted,
125
126
  });
126
127
  }
128
+ /**
129
+ * 工具函数: 清空表元数据缓存(集群广播)
130
+ * 清空本节点缓存,并通过 Redis Pub/Sub 广播通知集群中其他节点。
131
+ * 清空后,下次请求会自动从数据库重新加载(懒加载刷新)。
132
+ */
133
+ async clearTableMetaCacheApi() {
134
+ const stats = (0, TableMetaCacheRedisSubscriber_1.clearTableMetaCacheWithBroadcast)();
135
+ return common_dto_1.CommonResult.successRes({
136
+ message: '表元数据缓存已清空(已广播集群)',
137
+ beforeClear: stats,
138
+ });
139
+ }
127
140
  checkLocalPermissionEnv() {
128
141
  //是否是开发者
129
142
  const isDevelopUser = () => {
@@ -181,6 +194,12 @@ __decorate([
181
194
  __metadata("design:paramtypes", [Object]),
182
195
  __metadata("design:returntype", Promise)
183
196
  ], HelpersApi.prototype, "cryptoAes128CBC", null);
197
+ __decorate([
198
+ (0, core_1.Get)('/clearTableMetaCache'),
199
+ __metadata("design:type", Function),
200
+ __metadata("design:paramtypes", []),
201
+ __metadata("design:returntype", Promise)
202
+ ], HelpersApi.prototype, "clearTableMetaCacheApi", null);
184
203
  HelpersApi = __decorate([
185
204
  (0, core_1.Controller)('/ns/api/helpers')
186
205
  ], HelpersApi);
@@ -1,4 +1,4 @@
1
- import { ICrudProCfg, ILogger, IRequestCfgModel, IRequestModel, ISqlCfgModel, ITableListResult, ITableNamesOptions, ITableNamesQuery, IVisitor, ExecuteSQLResult } from './interfaces';
1
+ import { ICrudProCfg, ILogger, IRequestCfgModel, IRequestModel, ISqlCfgModel, ITableListResult, ITableMetaQuery, ITableNamesOptions, ITableNamesQuery, IVisitor, ExecuteSQLResult } from './interfaces';
2
2
  import { ExecuteContext } from './models/ExecuteContext';
3
3
  import { Transaction } from './models/Transaction';
4
4
  import { IExecuteContextFunc } from './models/ExecuteContextFunc';
@@ -51,6 +51,16 @@ declare class CrudPro {
51
51
  private executeCrudByCfgInternal;
52
52
  getCachedCfgByMethod(method: string, isEnableCache: boolean): Promise<IRequestCfgModel>;
53
53
  getAllTableInfos(query: ITableNamesQuery, options?: ITableNamesOptions): Promise<ITableListResult>;
54
+ /**
55
+ * 获取表的主键列名列表
56
+ *
57
+ * 复用 CrudProTableMetaService 的 getTableMeta 缓存,
58
+ * 从 columnDetails 中筛选 isPrimaryKey 的列。
59
+ *
60
+ * @param query 表元数据查询参数
61
+ * @returns 主键列名数组
62
+ */
63
+ getPrimaryKeyColumns(query: ITableMetaQuery): Promise<string[]>;
54
64
  /**
55
65
  * 如果是 INSERT/UPDATE 操作(sqlSimpleName 模式),根据真实表结构过滤 reqModel.data 中的字段
56
66
  * 避免传入不存在的字段导致 SQL 执行报错
@@ -59,6 +69,16 @@ declare class CrudPro {
59
69
  * @param cfgModel 配置模型
60
70
  */
61
71
  private filterDataByTableMetaIfNeeded;
72
+ /**
73
+ * 校验特定操作的必填参数
74
+ *
75
+ * - insertOrUpdate 和 update:condition 和 data 均为必填
76
+ * - insertOnDuplicateUpdate 和 insert:只有data 为必填
77
+ * @param reqModel 请求模型
78
+ * @param cfgModel 配置模型
79
+ * @throws CommonException 如果缺少必填参数
80
+ */
81
+ private validateParamsIfNeeded;
62
82
  /**
63
83
  * 判断 sqlSimpleName 是否为 INSERT/UPDATE 类型
64
84
  * @param sqlSimpleName 简单 SQL 名称
@@ -66,12 +86,19 @@ declare class CrudPro {
66
86
  */
67
87
  private isSimpleInsertOrUpdateType;
68
88
  /**
69
- * 根据表结构字段类型,自动转换数据格式
89
+ * 根据表结构字段类型,自动转换 data 数据格式
70
90
  * 例如:PostgreSQL 的 ARRAY 类型字段,需要将 JSON 数组转为 PG 数组字面量格式
71
91
  * @param reqModel 请求模型
72
92
  * @param cfgModel 配置模型
73
93
  */
74
94
  private convertDataFieldTypeIfNeeded;
95
+ /**
96
+ * 查询时:根据表结构字段类型,自动转换 condition 数据格式
97
+ * 防止数据库隐式类型转换导致索引失效(如 WHERE int_col = '123' 导致索引无法命中)
98
+ * @param reqModel 请求模型
99
+ * @param cfgModel 配置模型
100
+ */
101
+ private convertConditionTypeIfNeeded;
75
102
  private executeSQLList;
76
103
  private parseRunSqlException;
77
104
  private afterExecuteSQLList;
@@ -138,10 +138,14 @@ class CrudPro {
138
138
  const cfgModel = new RequestCfgModel_1.RequestCfgModel(cfgJson);
139
139
  exeCtx.setReqModel(reqModel);
140
140
  exeCtx.setCfgModel(cfgModel);
141
+ // insertOrUpdate 参数校验
142
+ this.validateParamsIfNeeded(reqModel, cfgModel);
141
143
  // 如果是 sqlSimpleName模式的 update/insert,则需要将 reqJson.data 部分根据真实的表结构进行过滤
142
144
  await this.filterDataByTableMetaIfNeeded(reqModel, cfgModel);
143
- // 根据表结构字段类型,自动转换数据格式(如 PostgreSQL ARRAY 字段)
145
+ //插入和更新时: 根据表结构字段类型,自动转换 data 数据格式(如 PostgreSQL ARRAY 字段)
144
146
  await this.convertDataFieldTypeIfNeeded(reqModel, cfgModel);
147
+ //查询时:根据表结构字段类型,自动转换 condition 数据格式(防止数据库隐式类型转换导致索引失效)
148
+ await this.convertConditionTypeIfNeeded(reqModel, cfgModel);
145
149
  // 参数校验
146
150
  this.serviceHub.validateDataType(cfgModel, reqModel);
147
151
  this.serviceHub.validateByAllow(cfgModel, reqModel);
@@ -175,6 +179,18 @@ class CrudPro {
175
179
  async getAllTableInfos(query, options) {
176
180
  return this.serviceHub.getAllTableInfos(query, options);
177
181
  }
182
+ /**
183
+ * 获取表的主键列名列表
184
+ *
185
+ * 复用 CrudProTableMetaService 的 getTableMeta 缓存,
186
+ * 从 columnDetails 中筛选 isPrimaryKey 的列。
187
+ *
188
+ * @param query 表元数据查询参数
189
+ * @returns 主键列名数组
190
+ */
191
+ async getPrimaryKeyColumns(query) {
192
+ return this.serviceHub.getPrimaryKeyColumns(query);
193
+ }
178
194
  /**
179
195
  * 如果是 INSERT/UPDATE 操作(sqlSimpleName 模式),根据真实表结构过滤 reqModel.data 中的字段
180
196
  * 避免传入不存在的字段导致 SQL 执行报错
@@ -219,6 +235,37 @@ class CrudPro {
219
235
  // 直接修改 reqModel.data,只保留表中存在的字段
220
236
  reqModel.data = filteredData;
221
237
  }
238
+ /**
239
+ * 校验特定操作的必填参数
240
+ *
241
+ * - insertOrUpdate 和 update:condition 和 data 均为必填
242
+ * - insertOnDuplicateUpdate 和 insert:只有data 为必填
243
+ * @param reqModel 请求模型
244
+ * @param cfgModel 配置模型
245
+ * @throws CommonException 如果缺少必填参数
246
+ */
247
+ validateParamsIfNeeded(reqModel, cfgModel) {
248
+ // insertOrUpdate 和 update:condition 和 data 均为必填
249
+ if (cfgModel.sqlSimpleName === keys_1.KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE ||
250
+ cfgModel.sqlSimpleName === keys_1.KeysOfSimpleSQL.SIMPLE_UPDATE) {
251
+ if (!reqModel.condition || Object.keys(reqModel.condition).length === 0) {
252
+ throw new exceptions_1.CommonException(exceptions_1.Exceptions.INSERT_OR_UPDATE_CONDITION_EMPTY, '[CrudPro] insertOrUpdate/update 操作的 condition 不能为空。' +
253
+ 'condition 用于判断记录是否存在或定位更新目标。');
254
+ }
255
+ if (!reqModel.data || Object.keys(reqModel.data).length === 0) {
256
+ throw new exceptions_1.CommonException(exceptions_1.Exceptions.INSERT_OR_UPDATE_DATA_EMPTY, '[CrudPro] insertOrUpdate/update 操作的 data 不能为空。' +
257
+ 'data 用于指定插入或更新的内容。');
258
+ }
259
+ }
260
+ // insertOnDuplicateUpdate 和 insert:只有 data 为必填
261
+ if (cfgModel.sqlSimpleName === keys_1.KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE ||
262
+ cfgModel.sqlSimpleName === keys_1.KeysOfSimpleSQL.SIMPLE_INSERT) {
263
+ if (!reqModel.data || Object.keys(reqModel.data).length === 0) {
264
+ throw new exceptions_1.CommonException(exceptions_1.Exceptions.INSERT_OR_DUPLICATE_UPDATE_DATA_EMPTY, '[CrudPro] insert/insertOnDuplicateUpdate 操作的 data 不能为空。' +
265
+ 'data 用于指定插入或更新的内容。');
266
+ }
267
+ }
268
+ }
222
269
  /**
223
270
  * 判断 sqlSimpleName 是否为 INSERT/UPDATE 类型
224
271
  * @param sqlSimpleName 简单 SQL 名称
@@ -234,7 +281,7 @@ class CrudPro {
234
281
  return insertOrUpdateTypes.includes(sqlSimpleName);
235
282
  }
236
283
  /**
237
- * 根据表结构字段类型,自动转换数据格式
284
+ * 根据表结构字段类型,自动转换 data 数据格式
238
285
  * 例如:PostgreSQL 的 ARRAY 类型字段,需要将 JSON 数组转为 PG 数组字面量格式
239
286
  * @param reqModel 请求模型
240
287
  * @param cfgModel 配置模型
@@ -242,6 +289,15 @@ class CrudPro {
242
289
  async convertDataFieldTypeIfNeeded(reqModel, cfgModel) {
243
290
  await this.serviceHub.convertDataTypeByTableMeta(reqModel, cfgModel);
244
291
  }
292
+ /**
293
+ * 查询时:根据表结构字段类型,自动转换 condition 数据格式
294
+ * 防止数据库隐式类型转换导致索引失效(如 WHERE int_col = '123' 导致索引无法命中)
295
+ * @param reqModel 请求模型
296
+ * @param cfgModel 配置模型
297
+ */
298
+ async convertConditionTypeIfNeeded(reqModel, cfgModel) {
299
+ await this.serviceHub.convertConditionTypeByTableMeta(reqModel, cfgModel);
300
+ }
245
301
  async executeSQLList() {
246
302
  try {
247
303
  await this.serviceHub.executeSqlCfgModels();
@@ -56,6 +56,12 @@ export declare enum Exceptions {
56
56
  DATA_GET_DATA_EMPTY_ON_INSERT_KEYS = "DATA_GET_DATA_EMPTY_ON_INSERT_KEYS",
57
57
  DATA_GET_DATA_EMPTY_ON_INSERT_VALUES = "DATA_GET_DATA_EMPTY_ON_INSERT_VALUES",
58
58
  BATCH_INSERT_KEYS_MISMATCH = "BATCH_INSERT_KEYS_MISMATCH",
59
+ /**
60
+ * insertOrUpdate 参数缺失
61
+ */
62
+ INSERT_OR_UPDATE_CONDITION_EMPTY = "INSERT_OR_UPDATE_CONDITION_EMPTY",
63
+ INSERT_OR_UPDATE_DATA_EMPTY = "INSERT_OR_UPDATE_DATA_EMPTY",
64
+ INSERT_OR_DUPLICATE_UPDATE_DATA_EMPTY = "INSERT_ON_DUPLICATE_UPDATE_DATA_EMPTY",
59
65
  /**
60
66
  * 请求的Method字段为空
61
67
  */
@@ -79,6 +85,7 @@ export declare enum Exceptions {
79
85
  RUN_PICK_ERR_VISITOR_FIELD_NULL = "RUN_PICK_ERR_VISITOR_FIELD_NULL",
80
86
  RUN_EXECUTE_VALIDATE = "RUN_EXECUTE_VALIDATE",
81
87
  RUN_FUNCTION_NOT_FOUND = "RUN_FUNCTION_NOT_FOUND",
88
+ MORE_THAN_ONE_RECORDS_FOUND = "MORE_THAN_ONE_RECORDS_FOUND",
82
89
  /**
83
90
  * 参数校验错误
84
91
  */
@@ -60,6 +60,12 @@ var Exceptions;
60
60
  Exceptions["DATA_GET_DATA_EMPTY_ON_INSERT_KEYS"] = "DATA_GET_DATA_EMPTY_ON_INSERT_KEYS";
61
61
  Exceptions["DATA_GET_DATA_EMPTY_ON_INSERT_VALUES"] = "DATA_GET_DATA_EMPTY_ON_INSERT_VALUES";
62
62
  Exceptions["BATCH_INSERT_KEYS_MISMATCH"] = "BATCH_INSERT_KEYS_MISMATCH";
63
+ /**
64
+ * insertOrUpdate 参数缺失
65
+ */
66
+ Exceptions["INSERT_OR_UPDATE_CONDITION_EMPTY"] = "INSERT_OR_UPDATE_CONDITION_EMPTY";
67
+ Exceptions["INSERT_OR_UPDATE_DATA_EMPTY"] = "INSERT_OR_UPDATE_DATA_EMPTY";
68
+ Exceptions["INSERT_OR_DUPLICATE_UPDATE_DATA_EMPTY"] = "INSERT_ON_DUPLICATE_UPDATE_DATA_EMPTY";
63
69
  /**
64
70
  * 请求的Method字段为空
65
71
  */
@@ -83,6 +89,7 @@ var Exceptions;
83
89
  Exceptions["RUN_PICK_ERR_VISITOR_FIELD_NULL"] = "RUN_PICK_ERR_VISITOR_FIELD_NULL";
84
90
  Exceptions["RUN_EXECUTE_VALIDATE"] = "RUN_EXECUTE_VALIDATE";
85
91
  Exceptions["RUN_FUNCTION_NOT_FOUND"] = "RUN_FUNCTION_NOT_FOUND";
92
+ Exceptions["MORE_THAN_ONE_RECORDS_FOUND"] = "MORE_THAN_ONE_RECORDS_FOUND";
86
93
  /**
87
94
  * 参数校验错误
88
95
  */
@@ -13,6 +13,7 @@ export interface ITableColumn {
13
13
  type: string;
14
14
  isNullable: boolean;
15
15
  isPrimaryKey?: boolean;
16
+ isIdentity?: boolean;
16
17
  defaultValue?: any;
17
18
  maxLength?: number;
18
19
  comment?: string;
@@ -51,10 +51,13 @@ declare class CrudWriteResult extends CrudResultBase {
51
51
  declare class CrudQueryOneResult<T = Record<string, any>> extends CrudResultBase {
52
52
  readonly row: T | null;
53
53
  readonly found: boolean;
54
+ /** 数据来源的物理表名(分表场景下为实际分表名,如 t_order_202403) */
55
+ readonly fromTable: string;
54
56
  constructor(data: {
55
57
  row: T | null;
56
58
  rawContext: ExecuteContext;
57
59
  debugInfo?: CrudResultDebugInfo;
60
+ fromTable: string;
58
61
  });
59
62
  }
60
63
  /** 列表查询结果 */
@@ -99,12 +102,10 @@ declare class CrudCountResult extends CrudResultBase {
99
102
  }
100
103
  /** InsertOrUpdate 结果 */
101
104
  declare class CrudUpsertResult extends CrudResultBase {
102
- readonly affected?: ResModelAffected;
103
105
  readonly insertAffected?: ResModelAffected;
104
106
  readonly updateAffected?: ResModelAffected;
105
107
  readonly isExist: boolean;
106
108
  constructor(data: {
107
- affected?: ResModelAffected;
108
109
  insertAffected?: ResModelAffected;
109
110
  updateAffected?: ResModelAffected;
110
111
  isExist: boolean;
@@ -75,6 +75,7 @@ class CrudQueryOneResult extends CrudResultBase {
75
75
  super(data.rawContext, data.debugInfo);
76
76
  this.row = data.row;
77
77
  this.found = (this.row !== null && this.row !== undefined);
78
+ this.fromTable = data.fromTable;
78
79
  }
79
80
  }
80
81
  exports.CrudQueryOneResult = CrudQueryOneResult;
@@ -117,7 +118,6 @@ exports.CrudCountResult = CrudCountResult;
117
118
  class CrudUpsertResult extends CrudResultBase {
118
119
  constructor(data) {
119
120
  super(data.rawContext, data.debugInfo);
120
- this.affected = data.affected;
121
121
  this.insertAffected = data.insertAffected;
122
122
  this.updateAffected = data.updateAffected;
123
123
  this.isExist = data.isExist;
@@ -17,5 +17,7 @@ export interface ICurdProServiceHub {
17
17
  convertOriginToExecuteSql(sqlCfgModel: SqlCfgModel): Promise<void>;
18
18
  executeFuncCfg(tmpFunCfg: IFuncCfgModel, exeFunCtx: FuncContext): string;
19
19
  getTableMeta(query: ITableMetaQuery): Promise<ITableMeta>;
20
+ getPrimaryKeyColumns(query: ITableMetaQuery): Promise<string[]>;
20
21
  convertDataTypeByTableMeta(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void>;
22
+ convertConditionTypeByTableMeta(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void>;
21
23
  }