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.
- package/.qoder/skills/midway-fatcms-crud/SKILL.md +375 -0
- package/.qoder/skills/midway-fatcms-crud/examples.md +990 -0
- package/.qoder/skills/midway-fatcms-crud/reference.md +568 -0
- package/dist/libs/crud-pro/models/RequestModel.d.ts +0 -36
- package/dist/libs/crud-pro/models/RequestModel.js +2 -99
- package/dist/libs/crud-pro/utils/OrderByUtils.d.ts +70 -0
- package/dist/libs/crud-pro/utils/OrderByUtils.js +158 -0
- package/dist/libs/crud-sharding/ShardingCrudPro.js +15 -39
- package/dist/libs/crud-sharding/ShardingMerger.d.ts +2 -2
- package/dist/libs/crud-sharding/ShardingMerger.js +11 -9
- package/dist/service/curd/README.md +101 -2
- package/package.json +2 -1
- package/src/libs/crud-pro/models/RequestModel.ts +2 -108
- package/src/libs/crud-pro/utils/OrderByUtils.ts +169 -0
- package/src/libs/crud-sharding/ShardingCrudPro.ts +55 -76
- package/src/libs/crud-sharding/ShardingMerger.ts +14 -12
- package/src/service/curd/README.md +101 -2
|
@@ -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 =
|
|
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.
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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;
|