midway-fatcms 0.0.5 → 0.0.6

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 (126) hide show
  1. package/README.md +377 -134
  2. package/dist/controller/manage/CrudStandardDesignApi.d.ts +0 -2
  3. package/dist/controller/manage/CrudStandardDesignApi.js +11 -85
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +2 -0
  6. package/dist/libs/crud-pro/CrudPro.d.ts +9 -1
  7. package/dist/libs/crud-pro/CrudPro.js +15 -0
  8. package/dist/libs/crud-pro/README.md +809 -0
  9. package/dist/libs/crud-pro/README_FUNC.md +193 -0
  10. package/dist/libs/crud-pro/exceptions.d.ts +2 -0
  11. package/dist/libs/crud-pro/exceptions.js +2 -0
  12. package/dist/libs/crud-pro/interfaces.d.ts +34 -1
  13. package/dist/libs/crud-pro/models/ExecuteContext.d.ts +3 -3
  14. package/dist/libs/crud-pro/models/ExecuteContext.js +2 -0
  15. package/dist/libs/crud-pro/models/RequestModel.d.ts +41 -1
  16. package/dist/libs/crud-pro/models/RequestModel.js +103 -39
  17. package/dist/libs/crud-pro/models/ResModel.d.ts +6 -4
  18. package/dist/libs/crud-pro/models/ServiceHub.d.ts +1 -0
  19. package/dist/libs/crud-pro/models/keys.d.ts +6 -1
  20. package/dist/libs/crud-pro/models/keys.js +5 -0
  21. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.d.ts +52 -0
  22. package/dist/libs/crud-pro/services/CrudProDataTypeConvertService.js +158 -0
  23. package/dist/libs/crud-pro/services/CrudProExecuteSqlService.js +20 -1
  24. package/dist/libs/crud-pro/services/CrudProFieldValidateService.d.ts +7 -0
  25. package/dist/libs/crud-pro/services/CrudProFieldValidateService.js +32 -0
  26. package/dist/libs/crud-pro/services/CrudProGenSqlService.d.ts +13 -0
  27. package/dist/libs/crud-pro/services/CrudProGenSqlService.js +44 -7
  28. package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.d.ts +43 -0
  29. package/dist/libs/crud-pro/services/CrudProOriginToExecuteSql.js +132 -1
  30. package/dist/libs/crud-pro/services/CrudProTableMetaService.d.ts +15 -1
  31. package/dist/libs/crud-pro/services/CrudProTableMetaService.js +107 -0
  32. package/dist/libs/crud-pro/services/CurdProServiceHub.d.ts +5 -1
  33. package/dist/libs/crud-pro/services/CurdProServiceHub.js +11 -0
  34. package/dist/libs/crud-pro/utils/DateTimeUtils.d.ts +1 -0
  35. package/dist/libs/crud-pro/utils/DateTimeUtils.js +3 -0
  36. package/dist/libs/crud-pro/utils/MixinUtils.d.ts +32 -0
  37. package/dist/libs/crud-pro/utils/MixinUtils.js +85 -1
  38. package/dist/libs/crud-pro/utils/ValidateUtils.js +1 -1
  39. package/dist/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
  40. package/dist/libs/crud-sharding/ShardingConfig.d.ts +218 -0
  41. package/dist/libs/crud-sharding/ShardingConfig.js +32 -0
  42. package/dist/libs/crud-sharding/ShardingCountCache.d.ts +69 -0
  43. package/dist/libs/crud-sharding/ShardingCountCache.js +160 -0
  44. package/dist/libs/crud-sharding/ShardingCrudPro.d.ts +363 -0
  45. package/dist/libs/crud-sharding/ShardingCrudPro.js +699 -0
  46. package/dist/libs/crud-sharding/ShardingMerger.d.ts +130 -0
  47. package/dist/libs/crud-sharding/ShardingMerger.js +280 -0
  48. package/dist/libs/crud-sharding/ShardingRouter.d.ts +69 -0
  49. package/dist/libs/crud-sharding/ShardingRouter.js +377 -0
  50. package/dist/libs/crud-sharding/ShardingTableCreator.d.ts +146 -0
  51. package/dist/libs/crud-sharding/ShardingTableCreator.js +805 -0
  52. package/dist/libs/crud-sharding/ShardingUtils.d.ts +38 -0
  53. package/dist/libs/crud-sharding/ShardingUtils.js +77 -0
  54. package/dist/libs/crud-sharding/index.d.ts +45 -0
  55. package/dist/libs/crud-sharding/index.js +55 -0
  56. package/dist/models/StandardColumns.d.ts +71 -0
  57. package/dist/models/StandardColumns.js +28 -0
  58. package/dist/service/SysAppService.js +2 -2
  59. package/dist/service/SysConfigService.js +1 -1
  60. package/dist/service/SysDictDataService.js +2 -2
  61. package/dist/service/SysMenuService.js +1 -1
  62. package/dist/service/UserAccountService.d.ts +1 -1
  63. package/dist/service/crudstd/CrudStdService.d.ts +0 -1
  64. package/dist/service/crudstd/CrudStdService.js +0 -27
  65. package/dist/service/curd/CrudProQuick.d.ts +134 -4
  66. package/dist/service/curd/CrudProQuick.js +155 -3
  67. package/dist/service/curd/CurdMixService.d.ts +2 -1
  68. package/dist/service/curd/CurdMixService.js +5 -1
  69. package/dist/service/curd/CurdProService.d.ts +44 -2
  70. package/dist/service/curd/CurdProService.js +53 -1
  71. package/dist/service/curd/README.md +1001 -0
  72. package/dist/service/curd/fixSoftDelete.d.ts +14 -0
  73. package/dist/service/curd/fixSoftDelete.js +29 -11
  74. package/dist/service/flow/FlowConfigService.js +1 -1
  75. package/dist/service/flow/FlowInstanceCrudService.js +1 -1
  76. package/package.json +3 -1
  77. package/src/controller/gateway/AsyncTaskController.ts +1 -1
  78. package/src/controller/manage/CrudStandardDesignApi.ts +16 -100
  79. package/src/index.ts +3 -0
  80. package/src/libs/crud-pro/CrudPro.ts +19 -1
  81. package/src/libs/crud-pro/README.md +809 -0
  82. package/src/libs/crud-pro/README_FUNC.md +193 -0
  83. package/src/libs/crud-pro/exceptions.ts +2 -0
  84. package/src/libs/crud-pro/interfaces.ts +38 -1
  85. package/src/libs/crud-pro/models/ExecuteContext.ts +6 -3
  86. package/src/libs/crud-pro/models/RequestModel.ts +108 -44
  87. package/src/libs/crud-pro/models/ResModel.ts +10 -4
  88. package/src/libs/crud-pro/models/ServiceHub.ts +2 -0
  89. package/src/libs/crud-pro/models/keys.ts +5 -0
  90. package/src/libs/crud-pro/services/CrudProDataTypeConvertService.ts +171 -0
  91. package/src/libs/crud-pro/services/CrudProExecuteSqlService.ts +24 -1
  92. package/src/libs/crud-pro/services/CrudProFieldValidateService.ts +53 -1
  93. package/src/libs/crud-pro/services/CrudProGenSqlService.ts +51 -7
  94. package/src/libs/crud-pro/services/CrudProOriginToExecuteSql.ts +159 -2
  95. package/src/libs/crud-pro/services/CrudProTableMetaService.ts +139 -1
  96. package/src/libs/crud-pro/services/CurdProServiceHub.ts +16 -1
  97. package/src/libs/crud-pro/utils/DateTimeUtils.ts +3 -0
  98. package/src/libs/crud-pro/utils/MixinUtils.ts +97 -1
  99. package/src/libs/crud-pro/utils/ValidateUtils.ts +1 -1
  100. package/src/libs/crud-sharding/ROUTING_LOGIC.md +944 -0
  101. package/src/libs/crud-sharding/ShardingConfig.ts +240 -0
  102. package/src/libs/crud-sharding/ShardingCountCache.ts +200 -0
  103. package/src/libs/crud-sharding/ShardingCrudPro.ts +856 -0
  104. package/src/libs/crud-sharding/ShardingMerger.ts +382 -0
  105. package/src/libs/crud-sharding/ShardingRouter.ts +512 -0
  106. package/src/libs/crud-sharding/ShardingTableCreator.ts +1007 -0
  107. package/src/libs/crud-sharding/ShardingUtils.ts +84 -0
  108. package/src/libs/crud-sharding/index.ts +64 -0
  109. package/src/models/StandardColumns.ts +76 -0
  110. package/src/service/FileCenterService.ts +1 -1
  111. package/src/service/SysAppService.ts +2 -2
  112. package/src/service/SysConfigService.ts +1 -1
  113. package/src/service/SysDictDataService.ts +2 -2
  114. package/src/service/SysMenuService.ts +2 -2
  115. package/src/service/WorkbenchService.ts +1 -1
  116. package/src/service/anyapi/AnyApiService.ts +1 -1
  117. package/src/service/asyncTask/AsyncTaskRunnerService.ts +1 -1
  118. package/src/service/crudstd/CrudStdService.ts +0 -32
  119. package/src/service/curd/CrudProQuick.ts +164 -5
  120. package/src/service/curd/CurdMixService.ts +7 -2
  121. package/src/service/curd/CurdProService.ts +62 -3
  122. package/src/service/curd/README.md +1001 -0
  123. package/src/service/curd/fixCfgModel.ts +1 -2
  124. package/src/service/curd/fixSoftDelete.ts +38 -16
  125. package/src/service/flow/FlowConfigService.ts +1 -1
  126. package/src/service/flow/FlowInstanceCrudService.ts +1 -1
@@ -0,0 +1,171 @@
1
+ import { CrudProServiceBase } from './CrudProServiceBase';
2
+ import { RequestModel } from '../models/RequestModel';
3
+ import { RequestCfgModel } from '../models/RequestCfgModel';
4
+ import { KeysOfSimpleSQL, SqlDbType } from '../models/keys';
5
+ import { ITableMetaQuery } from '../interfaces';
6
+
7
+ /**
8
+ * 数据类型转换服务
9
+ * 根据表结构字段类型,在 INSERT/UPDATE 操作前自动转换数据格式,确保数据与数据库方言兼容
10
+ *
11
+ * 当前支持的转换:
12
+ * - PostgreSQL ARRAY 类型:JSON 数组 → PG 数组字面量 {"a","b","c"}
13
+ */
14
+ class CrudProDataTypeConvertService extends CrudProServiceBase {
15
+
16
+ /**
17
+ * 根据表结构字段类型自动转换 reqModel.data 中的数据格式
18
+ * 仅对 PostgreSQL 的 INSERT/UPDATE 类型操作生效
19
+ *
20
+ * @param reqModel 请求模型
21
+ * @param cfgModel 配置模型
22
+ */
23
+ async convertDataTypeByTableMeta(reqModel: RequestModel, cfgModel: RequestCfgModel): Promise<void> {
24
+ if (cfgModel.sqlDbType !== SqlDbType.postgres) {
25
+ return;
26
+ }
27
+
28
+ const sqlSimpleName = cfgModel.sqlSimpleName;
29
+ if (!this.isInsertOrUpdateType(sqlSimpleName)) {
30
+ return;
31
+ }
32
+
33
+ if (!reqModel.data) {
34
+ return;
35
+ }
36
+
37
+ const query: ITableMetaQuery = {
38
+ sqlTable: cfgModel.sqlTable,
39
+ sqlDatabase: cfgModel.sqlDatabase,
40
+ sqlDbType: cfgModel.sqlDbType,
41
+ sqlSchema: cfgModel.sqlSchema,
42
+ };
43
+
44
+ const tableMeta = await this.serviceHub.getTableMeta(query);
45
+ if (!tableMeta || !tableMeta.columnDetails || tableMeta.columnDetails.length === 0) {
46
+ this.logger.warn('CrudProDataTypeConvertService: 无法获取表结构信息,跳过数据类型转换', cfgModel.sqlTable);
47
+ return;
48
+ }
49
+
50
+ const arrayFieldSet = new Set<string>();
51
+ for (const col of tableMeta.columnDetails) {
52
+ if (('' + col.type).toUpperCase() === 'ARRAY') {
53
+ arrayFieldSet.add(col.name);
54
+ }
55
+ }
56
+
57
+ if (arrayFieldSet.size === 0) {
58
+ return;
59
+ }
60
+
61
+ const data = reqModel.data;
62
+ if (Array.isArray(data)) {
63
+ for (const row of data) {
64
+ this.convertArrayFields(row, arrayFieldSet);
65
+ }
66
+ } else {
67
+ this.convertArrayFields(data, arrayFieldSet);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 遍历数据对象中的 ARRAY 类型字段,将 JSON 数组转为 PG 数组字面量格式
73
+ */
74
+ private convertArrayFields(dataObj: Record<string, any>, arrayFieldSet: Set<string>): void {
75
+ const keys = Object.keys(dataObj);
76
+ for (const key of keys) {
77
+ if (!arrayFieldSet.has(key)) {
78
+ continue;
79
+ }
80
+ const value = dataObj[key];
81
+ const parsed = this.tryParseArray(value);
82
+ if (Array.isArray(parsed)) {
83
+ dataObj[key] = this.toPgArrayLiteral(parsed);
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 尝试将值解析为数组
90
+ * 支持:数组对象、JSON 数组字符串
91
+ */
92
+ private tryParseArray(value: any): any {
93
+ if (value === null || value === undefined) {
94
+ return null;
95
+ }
96
+ if (Array.isArray(value)) {
97
+ return value;
98
+ }
99
+ if (typeof value === 'string') {
100
+ const trimmed = value.trim();
101
+ if (trimmed.startsWith('[')) {
102
+ try {
103
+ return JSON.parse(trimmed);
104
+ } catch (e) {
105
+ return null;
106
+ }
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * 将 JS 数组转为 PostgreSQL 数组字面量格式
114
+ *
115
+ * PG 数组字面量规则:
116
+ * - 字符串:双引号包裹,内部 " 转义为 \",\ 转义为 \\ 例:{"hello","world"}
117
+ * - 数字:不加引号 例:{1,2,3}
118
+ * - 布尔:转为 t/f 不加引号 例:{t,f}
119
+ * - null/undefined:输出 NULL 不加引号 例:{1,NULL,3}
120
+ * - 空数组:输出 {}
121
+ *
122
+ * @param arr JS 数组
123
+ * @returns PG 数组字面量字符串
124
+ */
125
+ private toPgArrayLiteral(arr: any[]): string {
126
+ if (arr.length === 0) {
127
+ return '{}';
128
+ }
129
+ const items = arr.map(v => this.toPgArrayItem(v));
130
+ return '{' + items.join(',') + '}';
131
+ }
132
+
133
+ /**
134
+ * 将单个 JS 值转为 PG 数组元素的字面量表示
135
+ */
136
+ private toPgArrayItem(value: any): string {
137
+ if (value === null || value === undefined) {
138
+ return 'NULL';
139
+ }
140
+ if (typeof value === 'number') {
141
+ return '' + value;
142
+ }
143
+ if (typeof value === 'boolean') {
144
+ return value ? 't' : 'f';
145
+ }
146
+ // 字符串及其它类型:转义后用双引号包裹
147
+ const escaped = ('' + value)
148
+ .replace(/\\/g, '\\\\')
149
+ .replace(/"/g, '\\"');
150
+ return '"' + escaped + '"';
151
+ }
152
+
153
+ /**
154
+ * 判断 sqlSimpleName 是否为 INSERT/UPDATE 类型
155
+ */
156
+ private isInsertOrUpdateType(sqlSimpleName: string): boolean {
157
+ if (!sqlSimpleName) {
158
+ return false;
159
+ }
160
+ const types = [
161
+ KeysOfSimpleSQL.SIMPLE_INSERT,
162
+ KeysOfSimpleSQL.SIMPLE_UPDATE,
163
+ KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE,
164
+ KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE,
165
+ KeysOfSimpleSQL.SIMPLE_BATCH_INSERT,
166
+ ];
167
+ return types.includes(sqlSimpleName as KeysOfSimpleSQL);
168
+ }
169
+ }
170
+
171
+ export { CrudProDataTypeConvertService };
@@ -201,7 +201,15 @@ class CrudProExecuteSqlService extends CrudProServiceBase {
201
201
  }
202
202
 
203
203
  private toQueryResByResPicker(rows: any[], originRes: any, sqlCfgModel: SqlCfgModel) {
204
- const resPicker = sqlCfgModel.resPicker;
204
+ let resPicker = sqlCfgModel.resPicker;
205
+
206
+ // resPicker 未设置时,根据 SQL 类型自动推断
207
+ if (isEmpty(resPicker)) {
208
+ const crudType = sqlCfgModel.getCrudType();
209
+ if (crudType === KeyOfCrudTypes.INSERT || crudType === KeyOfCrudTypes.UPDATE || crudType === KeyOfCrudTypes.DELETE) {
210
+ resPicker = KeysOfSqlResPicker.UPDATE_RESULT;
211
+ }
212
+ }
205
213
 
206
214
  // 返回第一行
207
215
  if (KeysOfSqlResPicker.RESULT_FIRST_ROW === resPicker) {
@@ -220,6 +228,19 @@ class CrudProExecuteSqlService extends CrudProServiceBase {
220
228
  return Number(map0['total_count'] || 0);
221
229
  }
222
230
 
231
+ // 判断元素是否存在,返回 boolean
232
+ if (KeysOfSqlResPicker.RESULT_IS_EXIST === resPicker) {
233
+ if (isEmpty(rows) || rows.length === 0) {
234
+ return false;
235
+ }
236
+ const val = rows[0]['is_exist'];
237
+ // MySQL/SQLServer 返回 0/1,PostgreSQL 返回 false/true
238
+ if (typeof val === 'boolean') {
239
+ return val;
240
+ }
241
+ return Number(val) > 0;
242
+ }
243
+
223
244
  // 增删改res的内容是修改结果:包括: insert\delete\update
224
245
  if (KeysOfSqlResPicker.UPDATE_RESULT === resPicker) {
225
246
  if (sqlCfgModel.sqlDbType === SqlDbType.postgres) {
@@ -248,6 +269,8 @@ class CrudProExecuteSqlService extends CrudProServiceBase {
248
269
  return rows;
249
270
  }
250
271
 
272
+
273
+
251
274
  //其他配置:形如: sqlRes[0].total_count
252
275
  if (typeof resPicker === 'string') {
253
276
  if (resPicker.startsWith('sqlRes')) {
@@ -2,7 +2,7 @@ import { RequestModel } from '../models/RequestModel';
2
2
  import { CrudProServiceBase } from './CrudProServiceBase';
3
3
  import { RequestCfgModel } from '../models/RequestCfgModel';
4
4
  import { MixinUtils } from '../utils/MixinUtils';
5
- import { KeysOfConditions, KeysOfValidators } from '../models/keys';
5
+ import { KeysOfConditions, KeysOfSimpleSQL, KeysOfValidators } from '../models/keys';
6
6
  import { CommonException, Exceptions } from '../exceptions';
7
7
  import { ModelUtils } from '../utils/ModelUtils';
8
8
  import { ICustomValidateFunc, IFuncCfgModel, IValidatorCfgItem } from '../interfaces';
@@ -175,6 +175,58 @@ class CrudProFieldValidateService extends CrudProServiceBase {
175
175
  }
176
176
  }
177
177
  }
178
+
179
+ /**
180
+ * 校验 data 字段类型是否符合 sqlSimpleName 的要求
181
+ *
182
+ * - SIMPLE_BATCH_INSERT: data 必须是数组
183
+ * - 其他 INSERT/UPDATE 类型: data 必须是普通对象
184
+ */
185
+ validateDataType(cfgModel: RequestCfgModel, reqModel: RequestModel): void {
186
+ const sqlSimpleName = cfgModel.sqlSimpleName;
187
+ if (!sqlSimpleName) {
188
+ return;
189
+ }
190
+
191
+ const data = reqModel.data;
192
+
193
+ const isArrayData = Array.isArray(data);
194
+
195
+ if (sqlSimpleName === KeysOfSimpleSQL.SIMPLE_BATCH_INSERT) {
196
+ if (!isArrayData || data.length === 0) {
197
+ throw new CommonException(
198
+ Exceptions.CFG_INVALID_DATA_TYPE,
199
+ `${sqlSimpleName} 要求 data 必须是数组。并且不能为空数组。`
200
+ );
201
+ }
202
+ return;
203
+ }
204
+
205
+ const needsObjectData = [
206
+ KeysOfSimpleSQL.SIMPLE_INSERT,
207
+ KeysOfSimpleSQL.SIMPLE_UPDATE,
208
+ KeysOfSimpleSQL.SIMPLE_INSERT_ON_DUPLICATE_UPDATE,
209
+ KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE,
210
+ ].includes(sqlSimpleName as KeysOfSimpleSQL);
211
+
212
+
213
+ if(needsObjectData && (!data || Object.keys(data).length === 0)) {
214
+ throw new CommonException(
215
+ Exceptions.CFG_INVALID_DATA_TYPE,
216
+ `${sqlSimpleName} 要求 data 必须是对象,不能为空`
217
+ );
218
+ }
219
+
220
+ if (needsObjectData && isArrayData) {
221
+ throw new CommonException(
222
+ Exceptions.CFG_INVALID_DATA_TYPE,
223
+ `${sqlSimpleName} 要求 data 必须是普通对象,不能是数组`
224
+ );
225
+ }
226
+
227
+
228
+
229
+ }
178
230
  }
179
231
 
180
232
  export { CrudProFieldValidateService };
@@ -39,7 +39,11 @@ class CrudProGenSqlService extends CrudProServiceBase {
39
39
  let sql1 = null;
40
40
  let sql2 = null;
41
41
  let sql3 = null;
42
- if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY, simpleSqlName)) {
42
+
43
+ if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY_EXIST, simpleSqlName)) {
44
+ sql1 = this.generateOriginSqlForIsExist(cfgModel);
45
+ cfgModel.addSqlCfgModel('is_exist', sql1, KeysOfSqlResPicker.RESULT_IS_EXIST);
46
+ } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY, simpleSqlName)) {
43
47
  sql1 = 'select @@columns from @@table where @@asWhere:condition @@orderBys @@offsetLimit';
44
48
  cfgModel.addSqlCfgModel('rows', sql1);
45
49
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_QUERY_ONE, simpleSqlName)) {
@@ -61,6 +65,9 @@ class CrudProGenSqlService extends CrudProServiceBase {
61
65
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_INSERT, simpleSqlName)) {
62
66
  sql1 = this.generateOriginSqlForInsert(cfgModel);
63
67
  cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
68
+ } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_BATCH_INSERT, simpleSqlName)) {
69
+ sql1 = this.generateOriginSqlForBatchInsert(cfgModel);
70
+ cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
64
71
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_UPDATE, simpleSqlName)) {
65
72
  sql1 = 'update @@table set @@asUpdate:data where @@asWhere:condition ';
66
73
  cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
@@ -68,20 +75,21 @@ class CrudProGenSqlService extends CrudProServiceBase {
68
75
  sql1 = this.generateOriginSqlForDuplicateInsert(cfgModel);
69
76
  cfgModel.addSqlCfgModel('affected', sql1, KeysOfSqlResPicker.UPDATE_RESULT);
70
77
  } else if (equalsIgnoreCase(KeysOfSimpleSQL.SIMPLE_INSERT_OR_UPDATE, simpleSqlName)) {
71
- sql1 = 'select count(0) as total_count from @@table where @@asWhere:condition ';
72
- sql2 = 'insert into @@table ( @@asInsertKeys:data ) values( @@asInsertValues:data )';
78
+
79
+ sql1 = this.generateOriginSqlForIsExist(cfgModel); // 判断元素是否存在
80
+ sql2 = this.generateOriginSqlForInsert(cfgModel); // 插入语句
73
81
  sql3 = 'update @@table set @@asUpdate:data where @@asWhere:condition ';
74
82
 
75
83
  const insertWhen2 = {
76
84
  functionName: 'eq',
77
- functionParams: [{ contextAsNumber: 'res.total_count' }, { constNumber: 0 }],
85
+ functionParams: [{ contextAsBool: 'res.is_exist' }, { constBool: false }],
78
86
  };
79
87
  const updateWhen3 = {
80
- functionName: 'gt',
81
- functionParams: [{ contextAsNumber: 'res.total_count' }, { constNumber: 0 }],
88
+ functionName: 'eq',
89
+ functionParams: [{ contextAsBool: 'res.is_exist' }, { constBool: true }],
82
90
  };
83
91
 
84
- cfgModel.addSqlCfgModel('total_count', sql1, KeysOfSqlResPicker.RESULT_TOTAL_COUNT);
92
+ cfgModel.addSqlCfgModel('is_exist', sql1, KeysOfSqlResPicker.RESULT_IS_EXIST);
85
93
  cfgModel.addSqlCfgModel('insert_affected', sql2, KeysOfSqlResPicker.UPDATE_RESULT, insertWhen2);
86
94
  cfgModel.addSqlCfgModel('update_affected', sql3, KeysOfSqlResPicker.UPDATE_RESULT, updateWhen3);
87
95
  }
@@ -91,6 +99,27 @@ class CrudProGenSqlService extends CrudProServiceBase {
91
99
  }
92
100
  }
93
101
 
102
+
103
+
104
+
105
+
106
+ /**
107
+ * 判断元素是否存在
108
+ * 使用 SELECT EXISTS 子查询,数据库引擎找到第一条匹配记录即停止扫描,比 COUNT(*) 更高效
109
+ * @param cfgModel
110
+ * @private
111
+ */
112
+ private generateOriginSqlForIsExist(cfgModel: RequestCfgModel): string {
113
+ if (cfgModel.sqlDbType === SqlDbType.postgres) {
114
+ return 'SELECT EXISTS(SELECT 1 FROM @@table WHERE @@asWhere:condition) AS is_exist';
115
+ }
116
+ if (cfgModel.sqlDbType === SqlDbType.sqlserver) {
117
+ return 'SELECT CASE WHEN EXISTS(SELECT 1 FROM @@table WHERE @@asWhere:condition) THEN 1 ELSE 0 END AS is_exist';
118
+ }
119
+ return 'SELECT EXISTS(SELECT 1 FROM @@table WHERE @@asWhere:condition) AS is_exist';
120
+ }
121
+
122
+
94
123
  /**
95
124
  * 删除语句
96
125
  * @param cfgModel
@@ -157,6 +186,21 @@ class CrudProGenSqlService extends CrudProServiceBase {
157
186
  return 'insert into @@table ( @@asInsertKeys:data ) values( @@asInsertValues:data )'; // 关键字的前后,必须有空格
158
187
  }
159
188
 
189
+ /**
190
+ * 创建批量插入语句
191
+ * @param cfgModel
192
+ * @private
193
+ */
194
+ private generateOriginSqlForBatchInsert(cfgModel: RequestCfgModel): string {
195
+ if (cfgModel.sqlDbType === SqlDbType.postgres) {
196
+ return 'insert into @@table ( @@asBatchInsertKeys:data ) values @@asBatchInsertValues:data RETURNING * ';
197
+ }
198
+ if (cfgModel.sqlDbType === SqlDbType.sqlserver) {
199
+ return 'insert into @@table ( @@asBatchInsertKeys:data ) OUTPUT INSERTED.* values @@asBatchInsertValues:data ';
200
+ }
201
+ return 'insert into @@table ( @@asBatchInsertKeys:data ) values @@asBatchInsertValues:data '; // MySQL 默认语法
202
+ }
203
+
160
204
  private async generateExecuteSql() {
161
205
  const exeCtx = this.getExecuteContext();
162
206
 
@@ -3,7 +3,7 @@ import { SqlCfgModel } from '../models/SqlCfgModel';
3
3
  import { ITableMetaQuery } from '../interfaces';
4
4
  import { MixinUtils } from '../utils/MixinUtils';
5
5
  import { SqlSegArg } from '../models/SqlSegArg';
6
- import { KeysOfCustomSQL, SqlDbType } from '../models/keys';
6
+ import { KeysOfCustomSQL, KeyOfCrudTypes, SqlDbType } from '../models/keys';
7
7
  import { RequestModel } from '../models/RequestModel';
8
8
  import { CommonException, Exceptions } from '../exceptions';
9
9
  import { RequestCfgModel } from '../models/RequestCfgModel';
@@ -73,7 +73,43 @@ class CrudProOriginToExecuteSql extends CrudProServiceBase {
73
73
  }
74
74
 
75
75
  sqlCfgModel.executeSqlArgs = argList;
76
- sqlCfgModel.executeSql = executeSql;
76
+ sqlCfgModel.executeSql = this.appendDialectClause(executeSql, sqlCfgModel);
77
+ }
78
+
79
+ /**
80
+ * 对于自定义 INSERT SQL,自动追加数据库方言子句:
81
+ * - PostgreSQL: RETURNING *
82
+ * - SQL Server: OUTPUT INSERTED.*
83
+ * - MySQL: 无需追加,驱动自动返回 insertId
84
+ *
85
+ * 仅当 originSql 中用户未手动添加这些子句时才追加
86
+ */
87
+ private appendDialectClause(executeSql: string, sqlCfgModel: SqlCfgModel): string {
88
+ const sqlDbType = sqlCfgModel.sqlDbType;
89
+ const sqlUpper = executeSql.trim().toUpperCase();
90
+
91
+ if (!sqlUpper.startsWith(KeyOfCrudTypes.INSERT)) {
92
+ return executeSql;
93
+ }
94
+
95
+ if (sqlDbType === SqlDbType.postgres) {
96
+ if (sqlUpper.indexOf('RETURNING') < 0) {
97
+ return executeSql + ' RETURNING *';
98
+ }
99
+ } else if (sqlDbType === SqlDbType.sqlserver) {
100
+ if (sqlUpper.indexOf('OUTPUT INSERTED') < 0) {
101
+ // SQL Server: OUTPUT INSERTED.* 需放在 VALUES 之前
102
+ // INSERT INTO table (...) OUTPUT INSERTED.* VALUES (...)
103
+ const valuesIndex = sqlUpper.lastIndexOf('VALUES');
104
+ if (valuesIndex > 0) {
105
+ const beforeValues = executeSql.substring(0, valuesIndex);
106
+ const fromValues = executeSql.substring(valuesIndex);
107
+ return beforeValues + ' OUTPUT INSERTED.* ' + fromValues;
108
+ }
109
+ }
110
+ }
111
+
112
+ return executeSql;
77
113
  }
78
114
 
79
115
  private async generateWordMap(originSql: string, sqlCfgModel: SqlCfgModel): Promise<Record<string, SqlSegArg>> {
@@ -123,6 +159,10 @@ class CrudProOriginToExecuteSql extends CrudProServiceBase {
123
159
  return this.generateDataAsInsertKeys(reqModel, sqlCfg, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_INSERT_KEYS));
124
160
  } else if (word.startsWith(KeysOfCustomSQL.SQL_AS_INSERT_VALUES)) {
125
161
  return this.generateDataAsInsertValues(reqModel, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_INSERT_VALUES));
162
+ } else if (word.startsWith(KeysOfCustomSQL.SQL_AS_BATCH_INSERT_KEYS)) {
163
+ return this.generateDataAsBatchInsertKeys(reqModel, sqlCfg, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_BATCH_INSERT_KEYS));
164
+ } else if (word.startsWith(KeysOfCustomSQL.SQL_AS_BATCH_INSERT_VALUES)) {
165
+ return this.generateDataAsBatchInsertValues(reqModel, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_AS_BATCH_INSERT_VALUES));
126
166
  } else if (word.startsWith(KeysOfCustomSQL.SQL_FUNCTION)) {
127
167
  return this.generateSqlJavaFunction(reqCfg, MixinUtils.removeStringPrefix(word, KeysOfCustomSQL.SQL_FUNCTION));
128
168
  } else {
@@ -334,6 +374,123 @@ class CrudProOriginToExecuteSql extends CrudProServiceBase {
334
374
  return new SqlSegArg(sql, argList);
335
375
  }
336
376
 
377
+ /**
378
+ * 获取批量插入的字段名列表
379
+ *
380
+ * 策略:
381
+ * - 如果指定了 columns,直接使用 columns(允许数据行包含额外字段,仅插入指定列)
382
+ * - 否则取第一条数据的 keys,并校验后续行是否一致(严格模式)
383
+ *
384
+ * 严格模式限制:
385
+ * - 所有数据行必须包含完全相同的字段
386
+ * - 字段数量必须一致
387
+ * - 字段名必须一致(顺序可不同)
388
+ * - 若不一致,抛出 BATCH_INSERT_KEYS_MISMATCH 异常
389
+ *
390
+ * @param req RequestModel 请求模型
391
+ * @param dataArray 数据数组
392
+ * @returns 排序后的字段名数组
393
+ */
394
+ private getBatchInsertKeys(req: RequestModel, dataArray: Record<string, any>[]): string[] {
395
+ // 如果指定了 columns,直接使用
396
+ if (MixinUtils.isNotEmpty(req.columns) && req.columns.length > 0) {
397
+ return [...req.columns].sort();
398
+ }
399
+
400
+ // 严格模式:取第一条数据的 keys,并校验后续行
401
+ const firstRow = dataArray[0];
402
+ const keys = Object.keys(firstRow).sort();
403
+
404
+ for (let i = 1; i < dataArray.length; i++) {
405
+ const row = dataArray[i];
406
+ const rowKeys = Object.keys(row).sort();
407
+
408
+ if (rowKeys.length !== keys.length) {
409
+ throw new CommonException(
410
+ Exceptions.BATCH_INSERT_KEYS_MISMATCH,
411
+ `批量插入第 ${i + 1} 行数据字段数量不一致,期望 ${keys.length} 个,实际 ${rowKeys.length} 个`
412
+ );
413
+ }
414
+
415
+ for (let j = 0; j < keys.length; j++) {
416
+ if (rowKeys[j] !== keys[j]) {
417
+ throw new CommonException(
418
+ Exceptions.BATCH_INSERT_KEYS_MISMATCH,
419
+ `批量插入第 ${i + 1} 行数据字段不一致,缺少字段 "${keys[j]}" 或包含额外字段`
420
+ );
421
+ }
422
+ }
423
+ }
424
+
425
+ return keys;
426
+ }
427
+
428
+ /**
429
+ * 批量插入:生成列名部分
430
+ *
431
+ * 列名获取策略:
432
+ * - 优先使用 req.columns(若指定)
433
+ * - 否则使用严格模式:取第一条数据的 keys,校验后续行字段一致性
434
+ */
435
+ private generateDataAsBatchInsertKeys(req: RequestModel, sqlCfgModel: SqlCfgModel, dataName: string): SqlSegArg {
436
+ const dataArray = req.getCondOrDataAsArray(dataName);
437
+
438
+ if (MixinUtils.isEmpty(dataArray) || dataArray.length === 0) {
439
+ throw new CommonException(Exceptions.DATA_GET_DATA_EMPTY_ON_INSERT_KEYS, dataName);
440
+ }
441
+
442
+ const keys = this.getBatchInsertKeys(req, dataArray);
443
+
444
+ const columnList = keys.map(columnName => {
445
+ return toSqlColumnName(columnName, sqlCfgModel);
446
+ });
447
+
448
+ const sql = columnList.join(','); // `column1`,`column2`,`column3`
449
+ return new SqlSegArg(sql);
450
+ }
451
+
452
+ /**
453
+ * 批量插入:生成值部分,格式为 (?,?,?),(?,?,?),...
454
+ *
455
+ * 列名获取策略:
456
+ * - 优先使用 req.columns(若指定)
457
+ * - 否则使用严格模式:取第一条数据的 keys,校验后续行字段一致性
458
+ */
459
+ private generateDataAsBatchInsertValues(req: RequestModel, dataName: string): SqlSegArg {
460
+ const dataArray = req.getCondOrDataAsArray(dataName);
461
+
462
+ if (MixinUtils.isEmpty(dataArray) || dataArray.length === 0) {
463
+ throw new CommonException(Exceptions.DATA_GET_DATA_EMPTY_ON_INSERT_VALUES, dataName);
464
+ }
465
+
466
+ const keys = this.getBatchInsertKeys(req, dataArray);
467
+
468
+ const valueGroups: string[] = [];
469
+ const argList: any[] = [];
470
+
471
+ for (let i = 0; i < dataArray.length; i++) {
472
+ const row = dataArray[i];
473
+ const rowValues: string[] = [];
474
+
475
+ for (let j = 0; j < keys.length; j++) {
476
+ const key = keys[j];
477
+ const arg = row[key];
478
+
479
+ if (sqlFuncUtils.isSqlFuncArg(arg)) {
480
+ rowValues.push(sqlFuncUtils.toFuncSQL(arg));
481
+ } else {
482
+ rowValues.push('?');
483
+ argList.push(arg);
484
+ }
485
+ }
486
+
487
+ valueGroups.push('(' + rowValues.join(',') + ')');
488
+ }
489
+
490
+ const sql = valueGroups.join(','); // "(?,?,?),(?,?,?),..."
491
+ return new SqlSegArg(sql, argList);
492
+ }
493
+
337
494
  private generateSqlJavaFunction(requestCfg: RequestCfgModel, tmpFuncName: string): SqlSegArg {
338
495
  const exeCtx = this.getExecuteContext();
339
496