midway-fatcms 0.0.6 → 0.0.7

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.
@@ -1,6 +1,7 @@
1
1
  import * as _ from 'lodash';
2
2
  import { ILimitOffset, IOrderByItem, IRequestModel, IVisitor } from '../interfaces';
3
3
  import { MixinUtils } from '../utils/MixinUtils';
4
+ import { OrderByUtils } from '../utils/OrderByUtils';
4
5
  import { CommonException, Exceptions } from '../exceptions';
5
6
  import { DEFAULT_LIMIT } from '../defaultConfigs';
6
7
 
@@ -19,119 +20,12 @@ class RequestModel {
19
20
  Object.assign(this, req);
20
21
  this.visitor = visitor;
21
22
  this.columns = MixinUtils.parseColumns(req.columns);
22
- this.orderBys = this.parseOrderBys(req.orderBy);
23
+ this.orderBys = OrderByUtils.parseOrderBys(req.orderBy);
23
24
  const limitOffset = this.parseOffsetList(req);
24
25
  this.limit = limitOffset.limit;
25
26
  this.offset = limitOffset.offset;
26
27
  }
27
28
 
28
- /**
29
- * 解析 orderBy 参数为 IOrderByItem 数组
30
- *
31
- * 支持的格式:
32
- * 1. 字符串格式(逗号分隔多个字段):
33
- * - 标准 SQL 格式:'created_at DESC, amount ASC'
34
- * - 简写 +/- 格式:'created_at-, amount+'('-' 表示 DESC,'+' 或省略表示 ASC)
35
- * - 默认升序:'order_id'(无后缀时默认为 ASC)
36
- *
37
- * 2. 数组格式:
38
- * - 纯对象数组:[{ fieldName: 'created_at', orderType: 'desc' }]
39
- * - 混合数组1(字符串+对象):['order_id', { fieldName: 'amount', orderType: 'asc' }]
40
- * - 混合数组2(字符串+对象):['order_id+', { fieldName: 'amount', orderType: 'asc' }]
41
- * - 混合数组3(字符串+对象):['order_id DESC', { fieldName: 'amount', orderType: 'asc' }]
42
- * 注:数组中的字符串元素支持标准 SQL 格式和简写格式,与字符串参数格式一致。
43
- *
44
- * SQL 注入防护:
45
- * - 所有字段名必须通过 MixinUtils.isValidFieldName() 校验
46
- * - 只允许 ASC/DESC 作为排序方向
47
- *
48
- * @param orderByStr 排序参数,可以是字符串或数组
49
- * @returns IOrderByItem[] 解析后的排序项数组
50
- */
51
- private parseOrderBys(orderByStr: any): IOrderByItem[] {
52
- if (MixinUtils.isEmpty(orderByStr)) {
53
- return [];
54
- }
55
-
56
- // 数组格式:支持字符串和对象混合
57
- if (Array.isArray(orderByStr)) {
58
- return orderByStr
59
- .map(item => this.parseOrderByItem(item))
60
- .filter((o): o is IOrderByItem => !!o);
61
- }
62
-
63
- // 字符串格式:逗号分隔多个字段
64
- return orderByStr
65
- .split(',')
66
- .map(s => s.trim())
67
- .filter(s => !!s)
68
- .map(item => this.parseOrderByString(item))
69
- .filter((o): o is IOrderByItem => !!o);
70
- }
71
-
72
- /**
73
- * 解析单个排序项(数组元素)
74
- */
75
- private parseOrderByItem(item: any): IOrderByItem | null {
76
- // 字符串格式:解析标准 SQL 格式或简写格式
77
- if (typeof item === 'string') {
78
- return this.parseOrderByString(item);
79
- }
80
-
81
- // 对象格式:提取 fieldName 和 orderType
82
- const { fieldName, orderType = 'asc' } = item || {};
83
- if (!fieldName) {
84
- return null;
85
- }
86
-
87
- this.validateFieldName(fieldName, fieldName);
88
- return { fieldName, orderType };
89
- }
90
-
91
- /**
92
- * 解析字符串格式的排序项(支持标准 SQL 和简写格式)
93
- */
94
- private parseOrderByString(orderByStr: string): IOrderByItem | null {
95
- let orderType = 'asc';
96
- let fieldName = orderByStr;
97
-
98
- // 检查是否为空格分隔的标准 SQL 格式(如 'created_at DESC')
99
- const spaceIndex = orderByStr.lastIndexOf(' ');
100
- if (spaceIndex > 0) {
101
- const beforeSpace = orderByStr.substring(0, spaceIndex).trim();
102
- const afterSpace = orderByStr.substring(spaceIndex + 1).trim().toUpperCase();
103
-
104
- if (afterSpace === 'ASC' || afterSpace === 'DESC') {
105
- fieldName = beforeSpace;
106
- orderType = afterSpace.toLowerCase() as 'asc' | 'desc';
107
- } else {
108
- throw new CommonException(Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, orderByStr);
109
- }
110
- } else if (orderByStr.endsWith('+')) {
111
- // 简写格式:+ 表示升序
112
- fieldName = orderByStr.slice(0, -1);
113
- orderType = 'asc';
114
- } else if (orderByStr.endsWith('-')) {
115
- // 简写格式:- 表示降序
116
- fieldName = orderByStr.slice(0, -1);
117
- orderType = 'desc';
118
- }
119
-
120
- fieldName = fieldName.trim();
121
- this.validateFieldName(fieldName, orderByStr);
122
-
123
- return { fieldName, orderType };
124
- }
125
-
126
- /**
127
- * SQL 注入防护:校验字段名格式
128
- */
129
- private validateFieldName(fieldName: string, originalValue: string): void {
130
- if (MixinUtils.isEmpty(fieldName) || !MixinUtils.isValidFieldName(fieldName)) {
131
- throw new CommonException(Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, originalValue);
132
- }
133
- }
134
-
135
29
  private parseOffsetList(req: IRequestModel): ILimitOffset {
136
30
  const { limit, offset, pageSize, pageNo } = req;
137
31
 
@@ -0,0 +1,169 @@
1
+ import { IOrderByItem } from '../interfaces';
2
+ import { MixinUtils } from './MixinUtils';
3
+ import { CommonException, Exceptions } from '../exceptions';
4
+
5
+ /**
6
+ * OrderBy 解析工具类
7
+ *
8
+ * 提供统一的 orderBy 参数解析功能,支持多种格式:
9
+ * 1. 字符串格式(逗号分隔多个字段):
10
+ * - 标准 SQL 格式:'created_at DESC, amount ASC'
11
+ * - 简写 +/- 格式:'created_at-, amount+'('-' 表示 DESC,'+' 或省略表示 ASC)
12
+ * - 默认升序:'order_id'(无后缀时默认为 ASC)
13
+ *
14
+ * 2. 数组格式:
15
+ * - 纯对象数组:[{ fieldName: 'created_at', orderType: 'desc' }]
16
+ * - 混合数组(字符串+对象):['order_id+', { fieldName: 'amount', orderType: 'asc' }]
17
+ *
18
+ * SQL 注入防护:
19
+ * - 所有字段名必须通过 MixinUtils.isValidFieldName() 校验
20
+ * - 只允许 ASC/DESC 作为排序方向
21
+ */
22
+ export class OrderByUtils {
23
+
24
+ /**
25
+ * 解析 orderBy 参数为 IOrderByItem 数组
26
+ *
27
+ * @param orderByStr 排序参数,可以是字符串或数组
28
+ * @returns IOrderByItem[] 解析后的排序项数组
29
+ */
30
+ public static parseOrderBys(orderByStr: any): IOrderByItem[] {
31
+ if (MixinUtils.isEmpty(orderByStr)) {
32
+ return [];
33
+ }
34
+
35
+ // 数组格式:支持字符串和对象混合
36
+ if (Array.isArray(orderByStr)) {
37
+ return orderByStr
38
+ .map(item => this.parseOrderByItem(item))
39
+ .filter((o): o is IOrderByItem => !!o);
40
+ }
41
+
42
+ // 字符串格式:逗号分隔多个字段
43
+ return orderByStr
44
+ .split(',')
45
+ .map(s => s.trim())
46
+ .filter(s => !!s)
47
+ .map(item => this.parseOrderByString(item))
48
+ .filter((o): o is IOrderByItem => !!o);
49
+ }
50
+
51
+ /**
52
+ * 获取第一个排序项
53
+ *
54
+ * @param orderByStr 排序参数
55
+ * @returns IOrderByItem | null 第一个排序项,无则返回 null
56
+ */
57
+ public static getFirstOrderBy(orderByStr: any): IOrderByItem | null {
58
+ const orderBys = this.parseOrderBys(orderByStr);
59
+ return orderBys.length > 0 ? orderBys[0] : null;
60
+ }
61
+
62
+ /**
63
+ * 判断是否为 ASC 升序排序
64
+ *
65
+ * @param orderByStr 排序参数
66
+ * @returns true 表示 ASC,false 表示 DESC 或无排序
67
+ */
68
+ public static isFirstOrderByAsc(orderByStr: any): boolean {
69
+ const first = this.getFirstOrderBy(orderByStr);
70
+ if (!first) {
71
+ return false;
72
+ }
73
+ return first.orderType.toUpperCase() === 'ASC';
74
+ }
75
+
76
+ /**
77
+ * 判断是否为 DESC 降序排序
78
+ *
79
+ * @param orderByStr 排序参数
80
+ * @returns true 表示 DESC,false 表示 ASC 或无排序
81
+ */
82
+ public static isFirstOrderByDesc(orderByStr: any): boolean {
83
+ const first = this.getFirstOrderBy(orderByStr);
84
+ if (!first) {
85
+ return false;
86
+ }
87
+ return first.orderType.toUpperCase() === 'DESC';
88
+ }
89
+
90
+ /**
91
+ * 判断排序字段是否匹配指定的时间字段
92
+ *
93
+ * 用于分表查询时校验排序字段是否为分表字段
94
+ *
95
+ * @param orderByStr 排序参数
96
+ * @param timeColumn 时间字段名
97
+ * @returns true 表示匹配,false 表示不匹配或无排序
98
+ */
99
+ public static isOrderByTimeColumn(orderByStr: any, timeColumn: string): boolean {
100
+ const first = this.getFirstOrderBy(orderByStr);
101
+ if (!first) {
102
+ return false;
103
+ }
104
+ return first.fieldName === timeColumn;
105
+ }
106
+
107
+ /**
108
+ * 解析单个排序项(数组元素)
109
+ */
110
+ private static parseOrderByItem(item: any): IOrderByItem | null {
111
+ // 字符串格式:解析标准 SQL 格式或简写格式
112
+ if (typeof item === 'string') {
113
+ return this.parseOrderByString(item);
114
+ }
115
+
116
+ // 对象格式:提取 fieldName 和 orderType
117
+ const { fieldName, orderType = 'asc' } = item || {};
118
+ if (!fieldName) {
119
+ return null;
120
+ }
121
+
122
+ this.validateFieldName(fieldName, fieldName);
123
+ return { fieldName, orderType };
124
+ }
125
+
126
+ /**
127
+ * 解析字符串格式的排序项(支持标准 SQL 和简写格式)
128
+ */
129
+ private static parseOrderByString(orderByStr: string): IOrderByItem | null {
130
+ let orderType = 'asc';
131
+ let fieldName = orderByStr;
132
+
133
+ // 检查是否为空格分隔的标准 SQL 格式(如 'created_at DESC')
134
+ const spaceIndex = orderByStr.lastIndexOf(' ');
135
+ if (spaceIndex > 0) {
136
+ const beforeSpace = orderByStr.substring(0, spaceIndex).trim();
137
+ const afterSpace = orderByStr.substring(spaceIndex + 1).trim().toUpperCase();
138
+
139
+ if (afterSpace === 'ASC' || afterSpace === 'DESC') {
140
+ fieldName = beforeSpace;
141
+ orderType = afterSpace.toLowerCase() as 'asc' | 'desc';
142
+ } else {
143
+ throw new CommonException(Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, orderByStr);
144
+ }
145
+ } else if (orderByStr.endsWith('+')) {
146
+ // 简写格式:+ 表示升序
147
+ fieldName = orderByStr.slice(0, -1);
148
+ orderType = 'asc';
149
+ } else if (orderByStr.endsWith('-')) {
150
+ // 简写格式:- 表示降序
151
+ fieldName = orderByStr.slice(0, -1);
152
+ orderType = 'desc';
153
+ }
154
+
155
+ fieldName = fieldName.trim();
156
+ this.validateFieldName(fieldName, orderByStr);
157
+
158
+ return { fieldName, orderType };
159
+ }
160
+
161
+ /**
162
+ * SQL 注入防护:校验字段名格式
163
+ */
164
+ private static validateFieldName(fieldName: string, originalValue: string): void {
165
+ if (MixinUtils.isEmpty(fieldName) || !MixinUtils.isValidFieldName(fieldName)) {
166
+ throw new CommonException(Exceptions.REQUEST_MODEL_PARSE_ORDER_BY_FAILED, originalValue);
167
+ }
168
+ }
169
+ }
@@ -1,5 +1,6 @@
1
1
  import { CrudPro } from '@/libs/crud-pro/CrudPro';
2
2
  import { IRequestModel, IRequestCfgModel } from '@/libs/crud-pro/interfaces';
3
+ import { OrderByUtils } from '@/libs/crud-pro/utils/OrderByUtils';
3
4
  import { ExecuteContext } from '@/libs/crud-pro/models/ExecuteContext';
4
5
  import { KeysOfSimpleSQL } from '@/libs/crud-pro/models/keys';
5
6
  import { ShardingType, IShardingConfig, IShardingRouterContext } from './ShardingConfig';
@@ -30,10 +31,10 @@ export interface IShardingSmartBatchInsertResult {
30
31
 
31
32
  /**
32
33
  * 分表 CRUD 操作封装器
33
- *
34
+ *
34
35
  * 在 CrudPro 之上封装分表功能,不修改 CrudPro 内部代码。
35
36
  * 提供透明的分表路由和多表查询结果合并能力。
36
- *
37
+ *
37
38
  * 支持的分表策略:
38
39
  * - YEAR: 按年分表,如 order_2024, order_2025
39
40
  * - MONTH: 按月分表,如 order_202401, order_202402
@@ -41,7 +42,7 @@ export interface IShardingSmartBatchInsertResult {
41
42
  * - RANGE: 按范围分表,如 user_0 ~ user_99
42
43
  * - HASH: 按哈希分表,如 order_01 ~ order_16
43
44
  * - CUSTOM: 自定义分表规则
44
- *
45
+ *
45
46
  * @example
46
47
  * // 按月分表
47
48
  * const sharding = new ShardingCrudPro(crudPro, {
@@ -49,10 +50,10 @@ export interface IShardingSmartBatchInsertResult {
49
50
  * baseTable: 't_order',
50
51
  * timeColumn: 'created_at',
51
52
  * });
52
- *
53
+ *
53
54
  * // 插入数据(自动路由到正确分表)
54
55
  * await sharding.insert({ data: { order_id: '001', amount: 100, created_at: '2024-03-15' } });
55
- *
56
+ *
56
57
  * // 分页查询(自动合并多表结果)
57
58
  * const result = await sharding.queryPage({
58
59
  * condition: { created_at: { $gte: '2024-01-01', $lte: '2024-03-31' } },
@@ -100,10 +101,10 @@ export class ShardingCrudPro {
100
101
 
101
102
  /**
102
103
  * 设置基础配置(会应用到所有操作)
103
- *
104
+ *
104
105
  * @param cfg 配置项,如 sqlDatabase、sqlDbType 等
105
106
  * @returns this,支持链式调用
106
- *
107
+ *
107
108
  * @example
108
109
  * sharding.setBaseCfg({
109
110
  * sqlDatabase: 'mydb',
@@ -156,18 +157,18 @@ export class ShardingCrudPro {
156
157
 
157
158
  /**
158
159
  * 批量插入数据(支持跨分表)
159
- *
160
+ *
160
161
  * 自动将数据按分表分组,并行插入到对应的分表。
161
- *
162
+ *
162
163
  * @param reqJson 请求参数,data 为要插入的数据数组
163
164
  * @returns 批量插入结果
164
- *
165
+ *
165
166
  * 执行流程:
166
167
  * 1. 遍历所有数据,根据分表字段计算目标分表
167
168
  * 2. 将数据按分表分组
168
169
  * 3. 并行执行各分表的批量插入
169
170
  * 4. 汇总结果返回
170
- *
171
+ *
171
172
  * @example
172
173
  * // 数据分布在不同月份的分表
173
174
  * const result = await sharding.batchInsert({
@@ -178,7 +179,7 @@ export class ShardingCrudPro {
178
179
  * { order_id: '004', amount: 300, created_at: '2024-03-05' }, // -> t_order_202403
179
180
  * ],
180
181
  * });
181
- *
182
+ *
182
183
  * console.log(result.totalAffected); // 4
183
184
  * console.log(result.tableCount); // 3
184
185
  * console.log(result.tableResults);
@@ -256,12 +257,12 @@ export class ShardingCrudPro {
256
257
 
257
258
  /**
258
259
  * 更新数据
259
- *
260
+ *
260
261
  * 根据条件中的分表字段自动路由到目标分表。
261
- *
262
+ *
262
263
  * @param reqJson 请求参数,condition 为更新条件,data 为更新数据
263
264
  * @returns 执行上下文
264
- *
265
+ *
265
266
  * @example
266
267
  * await sharding.update({
267
268
  * condition: { order_id: 'ORD001', created_at: '2024-03-15' },
@@ -275,9 +276,9 @@ export class ShardingCrudPro {
275
276
 
276
277
  /**
277
278
  * 删除数据
278
- *
279
+ *
279
280
  * 根据条件中的分表字段自动路由到目标分表。
280
- *
281
+ *
281
282
  * @param reqJson 请求参数,condition 为删除条件
282
283
  * @returns 执行上下文
283
284
  */
@@ -288,9 +289,9 @@ export class ShardingCrudPro {
288
289
 
289
290
  /**
290
291
  * 插入或更新(存在则更新,不存在则插入)
291
- *
292
+ *
292
293
  * 路由逻辑:根据 condition 路由(先查询是否存在)
293
- *
294
+ *
294
295
  * @param reqJson 请求参数
295
296
  * @returns 执行上下文
296
297
  */
@@ -301,9 +302,9 @@ export class ShardingCrudPro {
301
302
 
302
303
  /**
303
304
  * 插入或更新(ON DUPLICATE KEY UPDATE)
304
- *
305
+ *
305
306
  * 路由逻辑:根据 condition 路由(先查询是否存在)
306
- *
307
+ *
307
308
  * @param reqJson 请求参数
308
309
  * @returns 执行上下文
309
310
  */
@@ -316,13 +317,13 @@ export class ShardingCrudPro {
316
317
 
317
318
  /**
318
319
  * 查询单条记录
319
- *
320
+ *
320
321
  * 如果能根据条件确定单一分表,则查询单表;
321
322
  * 否则按顺序查询各分表,返回第一条匹配记录。
322
- *
323
+ *
323
324
  * @param reqJson 请求参数,condition 为查询条件
324
325
  * @returns 单条记录,未找到返回 null
325
- *
326
+ *
326
327
  * @example
327
328
  * const order = await sharding.queryOne({
328
329
  * condition: { order_id: 'ORD001' },
@@ -443,9 +444,9 @@ export class ShardingCrudPro {
443
444
 
444
445
  /**
445
446
  * 查询记录总数
446
- *
447
+ *
447
448
  * 如果涉及多个分表,会并行查询各分表并汇总。
448
- *
449
+ *
449
450
  * @param reqJson 请求参数
450
451
  * @returns 记录总数
451
452
  */
@@ -471,9 +472,9 @@ export class ShardingCrudPro {
471
472
 
472
473
  /**
473
474
  * 判断记录是否存在
474
- *
475
+ *
475
476
  * 如果涉及多个分表,会并行查询,任一分表存在即返回 true。
476
- *
477
+ *
477
478
  * @param reqJson 请求参数
478
479
  * @returns 是否存在
479
480
  */
@@ -680,12 +681,12 @@ export class ShardingCrudPro {
680
681
 
681
682
  /**
682
683
  * 创建分表(如果需要)
683
- *
684
+ *
684
685
  * 根据配置检查分表是否存在:
685
686
  * - 如果分表已存在,直接返回
686
687
  * - 如果分表不存在且 autoCreateTable=true,自动创建
687
688
  * - 如果分表不存在且 autoCreateTable=false,抛出异常
688
- *
689
+ *
689
690
  * @param tableName 目标分表名
690
691
  */
691
692
  private async createShardingTableIfNeeded(tableName: string): Promise<void> {
@@ -743,8 +744,6 @@ export class ShardingCrudPro {
743
744
  }
744
745
 
745
746
  const orderBy = reqJson.orderBy;
746
- const expectedDesc = `${timeColumn} DESC`;
747
- const expectedAsc = `${timeColumn} ASC`;
748
747
 
749
748
  // 1. 必须传 orderBy
750
749
  if (!orderBy) {
@@ -754,36 +753,29 @@ export class ShardingCrudPro {
754
753
  );
755
754
  }
756
755
 
757
- // 2. 校验 orderBy 格式(直接字符串比较,避免正则转义问题)
758
- if (typeof orderBy === 'string') {
759
- const upper = orderBy.trim().toUpperCase();
760
- if (upper !== expectedDesc.toUpperCase() && upper !== expectedAsc.toUpperCase()) {
761
- throw new Error(
762
- `[ShardingCrudPro] orderBy 参数必须是 '${timeColumn} DESC' 或 '${timeColumn} ASC',` +
763
- `当前值: '${orderBy}'`
764
- );
765
- }
766
- } else if (Array.isArray(orderBy)) {
767
- // 数组格式:首个排序字段必须是 timeColumn DESC/ASC,允许附加次级排序
768
- if (orderBy.length === 0) {
769
- throw new Error(
770
- `[ShardingCrudPro] orderBy 数组不能为空,首个排序字段必须为 '${timeColumn} DESC' 或 '${timeColumn} ASC'`
771
- );
772
- }
773
- const firstItem = orderBy[0];
774
- const firstItemStr = typeof firstItem === 'string'
775
- ? firstItem
776
- : `${firstItem.fieldName} ${firstItem.orderType || 'ASC'}`;
777
- const upper = firstItemStr.trim().toUpperCase();
778
- if (upper !== expectedDesc.toUpperCase() && upper !== expectedAsc.toUpperCase()) {
779
- throw new Error(
780
- `[ShardingCrudPro] orderBy 数组首个排序字段必须为 '${timeColumn} DESC' 或 '${timeColumn} ASC',` +
781
- `当前值: '${firstItemStr}'`
782
- );
783
- }
784
- } else {
756
+ // 2. 使用工具类解析并校验首个排序字段是否为 timeColumn
757
+ const firstOrderBy = OrderByUtils.getFirstOrderBy(orderBy);
758
+
759
+ if (!firstOrderBy) {
785
760
  throw new Error(
786
- `[ShardingCrudPro] orderBy 参数格式错误,期望字符串或数组。`
761
+ `[ShardingCrudPro] orderBy 参数格式错误,无法解析。` +
762
+ `期望值: '${timeColumn} DESC' 或 '${timeColumn} ASC'`
763
+ );
764
+ }
765
+
766
+ if (firstOrderBy.fieldName !== timeColumn) {
767
+ throw new Error(
768
+ `[ShardingCrudPro] orderBy 首个排序字段必须为 '${timeColumn}',` +
769
+ `当前值: '${firstOrderBy.fieldName}'`
770
+ );
771
+ }
772
+
773
+ // 校验排序方向是否为 ASC 或 DESC
774
+ const orderType = firstOrderBy.orderType.toUpperCase();
775
+ if (orderType !== 'ASC' && orderType !== 'DESC') {
776
+ throw new Error(
777
+ `[ShardingCrudPro] orderBy 排序方向必须是 'ASC' 或 'DESC',` +
778
+ `当前值: '${firstOrderBy.orderType}'`
787
779
  );
788
780
  }
789
781
  }
@@ -797,20 +789,7 @@ export class ShardingCrudPro {
797
789
  * @returns true 表示 ASC,false 表示 DESC
798
790
  */
799
791
  private isAscOrderBy(reqJson: IRequestModel): boolean {
800
- const orderBy = reqJson.orderBy;
801
- if (!orderBy) return false;
802
-
803
- if (typeof orderBy === 'string') {
804
- return orderBy.trim().toUpperCase().endsWith('ASC');
805
- }
806
- if (Array.isArray(orderBy) && orderBy.length > 0) {
807
- const firstItem = orderBy[0];
808
- const firstItemStr = typeof firstItem === 'string'
809
- ? firstItem
810
- : `${firstItem.fieldName} ${firstItem.orderType || 'ASC'}`;
811
- return firstItemStr.trim().toUpperCase().endsWith('ASC');
812
- }
813
- return false;
792
+ return OrderByUtils.isFirstOrderByAsc(reqJson.orderBy);
814
793
  }
815
794
 
816
795
  /**
@@ -853,4 +832,4 @@ export class ShardingCrudPro {
853
832
  // 委托给 ShardingRouter 处理查询路由
854
833
  return this.router.resolveQuery(this.config, context, tableInfoProvider);
855
834
  }
856
- }
835
+ }
@@ -102,8 +102,8 @@ export class ShardingMerger {
102
102
  * - orderBy 为 timeColumn DESC 或 timeColumn ASC
103
103
  *
104
104
  * 执行流程:
105
- * 1. 并行查询所有分表
106
- * 2. 按表顺序拼接结果(无需排序)
105
+ * 1. 串行查询分表,按表顺序拼接
106
+ * 2. 达到 maxRows 上限后立即停止,避免查询无关表
107
107
  *
108
108
  * @param crudPro CrudPro 实例
109
109
  * @param tables 分表列表(DESC: 新→旧,ASC: 旧→新)
@@ -119,17 +119,19 @@ export class ShardingMerger {
119
119
  cfgJson: IRequestCfgModel,
120
120
  maxRows: number = 10000
121
121
  ): Promise<any[]> {
122
- // 并行查询所有分表
123
- const dataPromises = tables.map(table =>
124
- this.queryRowsSafe(crudPro, table, reqJson, cfgJson, maxRows)
125
- );
126
- const dataResults = await Promise.all(dataPromises);
127
-
128
- // 按表顺序拼接(表顺序即数据顺序,无需排序)
122
+ // 串行查询分表,按表顺序拼接,达到上限即停
129
123
  let allRows: any[] = [];
130
- for (let i = 0; i < dataResults.length; i++) {
131
- allRows = allRows.concat(dataResults[i]);
132
- // 达到上限后停止
124
+
125
+ for (const table of tables) {
126
+ const needMore = maxRows - allRows.length;
127
+ if (needMore <= 0) {
128
+ break;
129
+ }
130
+
131
+ const rows = await this.queryRowsSafe(crudPro, table, reqJson, cfgJson, needMore);
132
+ allRows = allRows.concat(rows);
133
+
134
+ // 达到上限后停止,不再查询后续表
133
135
  if (allRows.length >= maxRows) {
134
136
  allRows = allRows.slice(0, maxRows);
135
137
  break;