@fastcar/cli 0.1.3 → 0.1.4
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/bin/cli.js +239 -235
- package/package.json +1 -1
- package/skills/AGENTS.md +251 -0
- package/skills/fastcar-database/SKILL.md +225 -49
- package/skills/fastcar-framework/SKILL.md +577 -576
- package/src/init.js +708 -700
- package/src/skill.js +493 -364
package/skills/AGENTS.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# FastCar 项目 AI 开发规范
|
|
2
|
+
|
|
3
|
+
本文件用于指导 AI agent 在 FastCar 项目中进行开发。当你读取或修改本项目中的文件时,请遵循以下规范,避免常见错误。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. @fastcar/koa Controller 传参规范
|
|
8
|
+
|
|
9
|
+
**核心原则:FastCar 没有 `@Body`、`@Param`、`@Query` 装饰器。**
|
|
10
|
+
|
|
11
|
+
- **第一个参数**:请求数据对象(POST 的 `body`、GET 的 `query` / `params`)。
|
|
12
|
+
- **第二个参数**:Koa 上下文 `ctx: Context`,**可选,可省略**。
|
|
13
|
+
- `Context` 必须从 `koa` 导入:`import { Context } from "koa";`。
|
|
14
|
+
- 表单验证时,`@ValidForm` 放在**方法**上,`@Rule()` 放在**第一个** DTO 参数前。
|
|
15
|
+
|
|
16
|
+
### ✅ 正确示例
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { GET, POST, REQUEST } from "@fastcar/koa/annotation";
|
|
20
|
+
import { Context } from "koa";
|
|
21
|
+
|
|
22
|
+
@Controller
|
|
23
|
+
@REQUEST("/api/items")
|
|
24
|
+
class ItemController {
|
|
25
|
+
@GET("/:id")
|
|
26
|
+
async getById(id: string, ctx: Context) {
|
|
27
|
+
return { id };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@POST()
|
|
31
|
+
async create(body: ItemDTO, ctx: Context) {
|
|
32
|
+
return { created: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@ValidForm
|
|
36
|
+
@POST("/login")
|
|
37
|
+
async login(@Rule() body: LoginDTO, ctx: Context) {
|
|
38
|
+
// body: 校验后的请求体
|
|
39
|
+
// ctx: 可选的 Koa 上下文
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### ❌ 常见错误
|
|
45
|
+
|
|
46
|
+
- 从 `@fastcar/koa` 导入 `Context`。
|
|
47
|
+
- 将 `ctx` 放在第一个参数位置。
|
|
48
|
+
- 使用 `@Body`、`@Param`、`@Query` 等不存在的装饰器。
|
|
49
|
+
- 忘记 `ctx` 是可选的。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. 禁止使用 `@Body`、`@Param`、`@Query` 装饰器
|
|
54
|
+
|
|
55
|
+
FastCar **没有** `@Body`、`@Param`、`@Query` 这些装饰器。请求参数直接通过方法参数传入,不要套用 NestJS / Express 的习惯。
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// ❌ 错误
|
|
59
|
+
import { Body, Param, Query } from "@fastcar/koa/annotation";
|
|
60
|
+
|
|
61
|
+
@GET("/:id")
|
|
62
|
+
async getById(@Param("id") id: string) { }
|
|
63
|
+
|
|
64
|
+
@POST()
|
|
65
|
+
async create(@Body body: ItemDTO) { }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 3. 路由装饰器必须带括号
|
|
71
|
+
|
|
72
|
+
路由装饰器**必须**以函数调用的形式使用,不能省略括号。
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// ❌ 错误
|
|
76
|
+
@GET
|
|
77
|
+
async list() { }
|
|
78
|
+
|
|
79
|
+
// ✅ 正确
|
|
80
|
+
@GET()
|
|
81
|
+
async list() { }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 4. 数据库查询必须在数据库层完成
|
|
87
|
+
|
|
88
|
+
### 分页查询
|
|
89
|
+
|
|
90
|
+
**严禁**先全表查询再在 JS 内存中用 `.slice()` 分页。
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// ✅ 正确:使用 SQL limit/offset
|
|
94
|
+
const list = await this.mapper.select({
|
|
95
|
+
where: { status: 1 },
|
|
96
|
+
orders: { id: OrderEnum.desc },
|
|
97
|
+
offset: (page - 1) * pageSize,
|
|
98
|
+
limit: pageSize,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ❌ 错误:全表查询后内存切片
|
|
102
|
+
const all = await this.mapper.select({ where: { status: 1 } });
|
|
103
|
+
const pageData = all.slice((page - 1) * pageSize, page * pageSize);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 分组聚合
|
|
107
|
+
|
|
108
|
+
**严禁**先全表查询再在 JS 中用 `.reduce()` 分组统计。
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// ✅ 正确:使用 SQL GROUP BY
|
|
112
|
+
const stats = await this.mapper.selectByCustom({
|
|
113
|
+
fields: ["status", "COUNT(*) as count", "SUM(amount) as totalAmount"],
|
|
114
|
+
groups: ["status"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ❌ 错误:全表查询后 JS 分组
|
|
118
|
+
const all = await this.mapper.select({});
|
|
119
|
+
const grouped = all.reduce((acc, item) => { /* ... */ }, {});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 复杂关联查询
|
|
123
|
+
|
|
124
|
+
**严禁**用 N+1 循环查询再在内存中组装数据。
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// ✅ 正确:使用 selectByCustom + JOIN 一条 SQL 完成
|
|
128
|
+
const results = await this.mapper.selectByCustom<QueryResult>({
|
|
129
|
+
tableAlias: "t",
|
|
130
|
+
fields: ["t.id", "t.name", "r.name as relatedName"],
|
|
131
|
+
join: [{
|
|
132
|
+
type: "INNER",
|
|
133
|
+
table: "related_table r",
|
|
134
|
+
on: "r.entity_id = t.id",
|
|
135
|
+
}],
|
|
136
|
+
where: { "t.status": 1 },
|
|
137
|
+
camelcaseStyle: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ❌ 错误:N+1 循环查询
|
|
141
|
+
const list = await this.mapper.select({});
|
|
142
|
+
for (const item of list) {
|
|
143
|
+
const related = await this.relatedMapper.selectOne({ ... });
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 5. 实体 / 模型规范
|
|
150
|
+
|
|
151
|
+
### 创建实体
|
|
152
|
+
|
|
153
|
+
**必须**通过构造函数的对象形式创建实体,禁止逐行赋值。
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// ✅ 正确
|
|
157
|
+
const entity = new Entity({
|
|
158
|
+
name: "示例",
|
|
159
|
+
status: 1,
|
|
160
|
+
createdAt: new Date(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ❌ 错误
|
|
164
|
+
const entity = new Entity();
|
|
165
|
+
entity.name = "示例";
|
|
166
|
+
entity.status = 1;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### 排序
|
|
170
|
+
|
|
171
|
+
排序**必须**使用 `OrderEnum`,禁止使用字符串。
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
import { OrderEnum } from "@fastcar/core/db";
|
|
175
|
+
|
|
176
|
+
// ✅ 正确
|
|
177
|
+
await this.mapper.select({
|
|
178
|
+
orders: { createdAt: OrderEnum.desc },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ❌ 错误
|
|
182
|
+
await this.mapper.select({
|
|
183
|
+
orders: { createdAt: "DESC" },
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 状态字段必须使用枚举
|
|
188
|
+
|
|
189
|
+
状态、类型等离散取值字段**必须**使用 `enum`,禁止使用裸数字或魔法字符串。
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// ✅ 正确
|
|
193
|
+
export enum JobStatus {
|
|
194
|
+
pending = "pending",
|
|
195
|
+
running = "running",
|
|
196
|
+
completed = "completed",
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await this.mapper.updateOne({
|
|
200
|
+
where: { id },
|
|
201
|
+
row: { status: JobStatus.running },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ❌ 错误
|
|
205
|
+
await this.mapper.updateOne({
|
|
206
|
+
where: { id },
|
|
207
|
+
row: { status: 1 },
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 更新少量字段
|
|
212
|
+
|
|
213
|
+
当更新字段少于 3 个时,**直接**使用 `updateOne` / `update`,禁止先查出整行再修改。
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// ✅ 正确
|
|
217
|
+
await this.mapper.updateOne({
|
|
218
|
+
where: { id },
|
|
219
|
+
row: { lastLoginTime: new Date() },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ❌ 错误
|
|
223
|
+
const entity = await this.mapper.selectByPrimaryKey({ id });
|
|
224
|
+
entity.lastLoginTime = new Date();
|
|
225
|
+
await this.mapper.updateByPrimaryKey(entity);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 6. 接口返回规范
|
|
231
|
+
|
|
232
|
+
**必须**如实返回空数据,禁止为了"美观"而注入模拟数据。
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// ✅ 正确
|
|
236
|
+
if (records.length === 0) {
|
|
237
|
+
return Result.ok({ list: [], total: 0 });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ❌ 错误
|
|
241
|
+
if (records.length === 0) {
|
|
242
|
+
return Result.ok({ list: [{ name: "模拟数据1" }] });
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 7. 主键操作规范
|
|
249
|
+
|
|
250
|
+
- `selectByPrimaryKey` 和 `updateByPrimaryKey` 需要传入**包含主键字段的对象**。
|
|
251
|
+
- 批量插入 `saveList` 会自动分批处理(每批 1000 条)。
|
|
@@ -26,7 +26,14 @@ export default new APP();
|
|
|
26
26
|
#### 定义实体模型
|
|
27
27
|
|
|
28
28
|
```typescript
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
Table,
|
|
31
|
+
Field,
|
|
32
|
+
DBType,
|
|
33
|
+
PrimaryKey,
|
|
34
|
+
NotNull,
|
|
35
|
+
Size,
|
|
36
|
+
} from "@fastcar/core/annotation";
|
|
30
37
|
|
|
31
38
|
@Table("entities")
|
|
32
39
|
class Entity {
|
|
@@ -82,7 +89,7 @@ class EntityService {
|
|
|
82
89
|
private mapper!: EntityMapper;
|
|
83
90
|
|
|
84
91
|
// ===== 查询方法 =====
|
|
85
|
-
|
|
92
|
+
|
|
86
93
|
// 查询列表
|
|
87
94
|
async list() {
|
|
88
95
|
return this.mapper.select({});
|
|
@@ -118,28 +125,92 @@ class EntityService {
|
|
|
118
125
|
// 多条件 AND
|
|
119
126
|
async queryByConditions(name: string, status: number) {
|
|
120
127
|
return this.mapper.select({
|
|
121
|
-
where: { name, status, delStatus: false }
|
|
128
|
+
where: { name, status, delStatus: false },
|
|
122
129
|
});
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
// 比较运算符
|
|
126
133
|
async queryByRange(min: number, max: number) {
|
|
127
134
|
return this.mapper.select({
|
|
128
|
-
where: { value: { [OperatorEnum.gte]: min, [OperatorEnum.lte]: max } }
|
|
135
|
+
where: { value: { [OperatorEnum.gte]: min, [OperatorEnum.lte]: max } },
|
|
129
136
|
});
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
// IN 查询
|
|
133
140
|
async queryByIds(ids: number[]) {
|
|
134
141
|
return this.mapper.select({
|
|
135
|
-
where: { id: { [OperatorEnum.in]: ids } }
|
|
142
|
+
where: { id: { [OperatorEnum.in]: ids } },
|
|
136
143
|
});
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// IS NULL 查询
|
|
140
147
|
async queryDeleted() {
|
|
141
148
|
return this.mapper.select({
|
|
142
|
-
where: { deletedAt: { [OperatorEnum.isNUll]: true } }
|
|
149
|
+
where: { deletedAt: { [OperatorEnum.isNUll]: true } },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ===== AND 条件查询 =====
|
|
154
|
+
|
|
155
|
+
// 方式1:默认多字段自动 AND
|
|
156
|
+
async queryByMultipleConditions(name: string, status: number) {
|
|
157
|
+
return this.mapper.select({
|
|
158
|
+
where: { name, status, delStatus: false },
|
|
159
|
+
// 生成 SQL: WHERE name = ? AND status = ? AND del_status = ?
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 方式2:同一字段多条件 AND
|
|
164
|
+
async queryByAgeRange(min: number, max: number) {
|
|
165
|
+
return this.mapper.select({
|
|
166
|
+
where: {
|
|
167
|
+
age: { [OperatorEnum.gte]: min, [OperatorEnum.lte]: max },
|
|
168
|
+
// 生成 SQL: WHERE age >= ? AND age <= ?
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ===== OR 条件查询 =====
|
|
174
|
+
|
|
175
|
+
// 方式1:对象形式(推荐)- 不同字段 OR
|
|
176
|
+
async queryByNameOrEmail(keyword: string) {
|
|
177
|
+
return this.mapper.select({
|
|
178
|
+
where: {
|
|
179
|
+
[JoinEnum.or]: {
|
|
180
|
+
name: keyword,
|
|
181
|
+
email: keyword,
|
|
182
|
+
},
|
|
183
|
+
// 生成 SQL: WHERE name = ? OR email = ?
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 方式2:数组形式 - 复杂条件 OR
|
|
189
|
+
async queryComplexOr() {
|
|
190
|
+
return this.mapper.select({
|
|
191
|
+
where: {
|
|
192
|
+
[JoinEnum.or]: [
|
|
193
|
+
{ status: 1, type: "A" },
|
|
194
|
+
{ status: 2, type: "B" },
|
|
195
|
+
],
|
|
196
|
+
// 生成 SQL: WHERE (status = 1 AND type = 'A') OR (status = 2 AND type = 'B')
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ===== AND 与 OR 组合 =====
|
|
202
|
+
|
|
203
|
+
async queryComplex(department: string, status: number, keyword: string) {
|
|
204
|
+
return this.mapper.select({
|
|
205
|
+
where: {
|
|
206
|
+
department,
|
|
207
|
+
[JoinEnum.or]: {
|
|
208
|
+
name: { [OperatorEnum.like]: `%${keyword}%` },
|
|
209
|
+
email: { [OperatorEnum.like]: `%${keyword}%` },
|
|
210
|
+
},
|
|
211
|
+
status,
|
|
212
|
+
// 生成 SQL: WHERE department = ? AND (name LIKE ? OR email LIKE ?) AND status = ?
|
|
213
|
+
},
|
|
143
214
|
});
|
|
144
215
|
}
|
|
145
216
|
|
|
@@ -148,7 +219,7 @@ class EntityService {
|
|
|
148
219
|
async getOrdered() {
|
|
149
220
|
return this.mapper.select({
|
|
150
221
|
where: { status: 1 },
|
|
151
|
-
orders: { createdAt: OrderEnum.desc }
|
|
222
|
+
orders: { createdAt: OrderEnum.desc },
|
|
152
223
|
});
|
|
153
224
|
}
|
|
154
225
|
|
|
@@ -156,7 +227,7 @@ class EntityService {
|
|
|
156
227
|
return this.mapper.select({
|
|
157
228
|
orders: { id: OrderEnum.desc },
|
|
158
229
|
offset: (page - 1) * pageSize,
|
|
159
|
-
limit: pageSize
|
|
230
|
+
limit: pageSize,
|
|
160
231
|
});
|
|
161
232
|
}
|
|
162
233
|
|
|
@@ -184,7 +255,7 @@ class EntityService {
|
|
|
184
255
|
async updateName(id: number, name: string) {
|
|
185
256
|
return this.mapper.update({
|
|
186
257
|
where: { id },
|
|
187
|
-
row: { name, updatedAt: new Date() }
|
|
258
|
+
row: { name, updatedAt: new Date() },
|
|
188
259
|
});
|
|
189
260
|
}
|
|
190
261
|
|
|
@@ -202,7 +273,7 @@ class EntityService {
|
|
|
202
273
|
async softDelete(id: number) {
|
|
203
274
|
return this.mapper.update({
|
|
204
275
|
where: { id },
|
|
205
|
-
row: { delStatus: true, deletedAt: new Date() }
|
|
276
|
+
row: { delStatus: true, deletedAt: new Date() },
|
|
206
277
|
});
|
|
207
278
|
}
|
|
208
279
|
|
|
@@ -221,18 +292,20 @@ class EntityService {
|
|
|
221
292
|
// selectByCustom 支持 JOIN、分组、聚合
|
|
222
293
|
async advancedQuery() {
|
|
223
294
|
// 指定字段 + 泛型类型
|
|
224
|
-
const results = await this.mapper.selectByCustom<{
|
|
225
|
-
id: number;
|
|
226
|
-
name: string;
|
|
295
|
+
const results = await this.mapper.selectByCustom<{
|
|
296
|
+
id: number;
|
|
297
|
+
name: string;
|
|
227
298
|
relatedName: string;
|
|
228
299
|
}>({
|
|
229
300
|
tableAlias: "t",
|
|
230
301
|
fields: ["t.id", "t.name", "r.name as relatedName"],
|
|
231
|
-
join: [
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
302
|
+
join: [
|
|
303
|
+
{
|
|
304
|
+
type: "LEFT",
|
|
305
|
+
table: "related_table r",
|
|
306
|
+
on: "r.entity_id = t.id",
|
|
307
|
+
},
|
|
308
|
+
],
|
|
236
309
|
where: { "t.status": 1 },
|
|
237
310
|
camelcaseStyle: true,
|
|
238
311
|
});
|
|
@@ -242,10 +315,10 @@ class EntityService {
|
|
|
242
315
|
fields: [
|
|
243
316
|
"status",
|
|
244
317
|
"COUNT(*) as totalCount",
|
|
245
|
-
"MAX(created_at) as lastCreated"
|
|
318
|
+
"MAX(created_at) as lastCreated",
|
|
246
319
|
],
|
|
247
320
|
groups: ["status"],
|
|
248
|
-
orders: { totalCount: OrderEnum.desc }
|
|
321
|
+
orders: { totalCount: OrderEnum.desc },
|
|
249
322
|
});
|
|
250
323
|
|
|
251
324
|
return { results, stats };
|
|
@@ -255,7 +328,7 @@ class EntityService {
|
|
|
255
328
|
async customQuery() {
|
|
256
329
|
return this.mapper.query(
|
|
257
330
|
"SELECT * FROM entities WHERE status = ? AND created_at > ?",
|
|
258
|
-
[1, "2024-01-01"]
|
|
331
|
+
[1, "2024-01-01"],
|
|
259
332
|
);
|
|
260
333
|
}
|
|
261
334
|
}
|
|
@@ -284,6 +357,18 @@ import { OperatorEnum, OrderEnum } from "@fastcar/core/db";
|
|
|
284
357
|
{ where: { deletedAt: { [OperatorEnum.isNUll]: true } } }
|
|
285
358
|
{ where: { deletedAt: { [OperatorEnum.isNotNull]: true } } }
|
|
286
359
|
|
|
360
|
+
// AND(默认,多字段自动 AND)
|
|
361
|
+
{ where: { name: "A", status: 1 } }
|
|
362
|
+
|
|
363
|
+
// OR(对象形式 - 推荐,不同字段)
|
|
364
|
+
{ where: { [JoinEnum.or]: { name: "A", email: "A" } } }
|
|
365
|
+
|
|
366
|
+
// OR(数组形式,复杂条件)
|
|
367
|
+
{ where: { [JoinEnum.or]: [{ status: 1 }, { status: 2 }] } }
|
|
368
|
+
|
|
369
|
+
// AND + OR 组合
|
|
370
|
+
{ where: { status: 1, [JoinEnum.or]: { type: 1, category: 2 } } }
|
|
371
|
+
|
|
287
372
|
// 排序
|
|
288
373
|
{ orders: { createdAt: OrderEnum.desc } }
|
|
289
374
|
|
|
@@ -310,14 +395,18 @@ class BizService {
|
|
|
310
395
|
|
|
311
396
|
async transactionExample(dataA: any, dataB: any) {
|
|
312
397
|
const sessionId = await this.dsm.beginTransaction();
|
|
313
|
-
|
|
398
|
+
|
|
314
399
|
try {
|
|
315
400
|
await this.mapperA.saveOne(dataA, undefined, sessionId);
|
|
316
|
-
await this.mapperB.update(
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
401
|
+
await this.mapperB.update(
|
|
402
|
+
{
|
|
403
|
+
where: { id: dataB.id },
|
|
404
|
+
row: { status: dataB.status },
|
|
405
|
+
},
|
|
406
|
+
undefined,
|
|
407
|
+
sessionId,
|
|
408
|
+
);
|
|
409
|
+
|
|
321
410
|
await this.dsm.commit(sessionId);
|
|
322
411
|
return true;
|
|
323
412
|
} catch (error) {
|
|
@@ -469,13 +558,13 @@ redis:
|
|
|
469
558
|
|
|
470
559
|
## 完整模块列表
|
|
471
560
|
|
|
472
|
-
| 模块
|
|
473
|
-
|
|
474
|
-
| @fastcar/mysql
|
|
475
|
-
| @fastcar/pgsql
|
|
476
|
-
| @fastcar/mongo
|
|
477
|
-
| @fastcar/redis
|
|
478
|
-
| @fastcar/mysql-tool | `npm i @fastcar/mysql-tool` | 逆向生成工具
|
|
561
|
+
| 模块 | 安装命令 | 用途 |
|
|
562
|
+
| ------------------- | --------------------------- | -------------- |
|
|
563
|
+
| @fastcar/mysql | `npm i @fastcar/mysql` | MySQL ORM |
|
|
564
|
+
| @fastcar/pgsql | `npm i @fastcar/pgsql` | PostgreSQL ORM |
|
|
565
|
+
| @fastcar/mongo | `npm i @fastcar/mongo` | MongoDB |
|
|
566
|
+
| @fastcar/redis | `npm i @fastcar/redis` | Redis 缓存 |
|
|
567
|
+
| @fastcar/mysql-tool | `npm i @fastcar/mysql-tool` | 逆向生成工具 |
|
|
479
568
|
|
|
480
569
|
## 快速开始
|
|
481
570
|
|
|
@@ -498,6 +587,93 @@ npm run debug
|
|
|
498
587
|
3. **批量插入**:`saveList` 会自动分批处理(每批1000条)
|
|
499
588
|
4. **软删除**:建议使用 `update` 方法更新 `delStatus` 字段,而不是物理删除
|
|
500
589
|
|
|
590
|
+
## 数据库查询最佳实践
|
|
591
|
+
|
|
592
|
+
### ⚠️ 分页查询 - 必须使用数据库层分页
|
|
593
|
+
|
|
594
|
+
**核心原则**:分页必须在数据库层完成,严禁全表查询后在内存中切片。
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
// ✅ 正确:使用 SQL LIMIT/OFFSET 分页(数据库层完成分页)
|
|
598
|
+
const list = await this.mapper.select({
|
|
599
|
+
where: { status: 1 },
|
|
600
|
+
orders: { id: OrderEnum.desc },
|
|
601
|
+
offset: (page - 1) * pageSize,
|
|
602
|
+
limit: pageSize, // 只取需要的记录
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// ✅ 正确:使用游标/滚动分页(适合大数据量)
|
|
606
|
+
const list = await this.mapper.select({
|
|
607
|
+
where: {
|
|
608
|
+
id: { [OperatorEnum.lt]: lastId } // 基于上一页最后ID
|
|
609
|
+
},
|
|
610
|
+
orders: { id: OrderEnum.desc },
|
|
611
|
+
limit: pageSize,
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// ❌ 错误:全表查询后在内存中切片(数据量大时会导致 OOM)
|
|
615
|
+
const allData = await this.mapper.select({ where: { status: 1 } }); // 可能百万级数据
|
|
616
|
+
const pageData = allData.slice((page - 1) * pageSize, page * pageSize); // 内存中切片
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**为什么重要**:
|
|
620
|
+
- 全表查询会将所有数据加载到 Node.js 内存,数据量大时会导致内存溢出(OOM)
|
|
621
|
+
- 网络传输大量无用数据,严重影响性能
|
|
622
|
+
- 数据库层分页只返回需要的记录,内存占用固定
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
### ⚠️ 分组聚合 - 必须使用数据库层 GROUP BY
|
|
627
|
+
|
|
628
|
+
**核心原则**:分组统计必须在数据库层完成,严禁全表查询后在 JS 中分组。
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// ✅ 正确:使用 SQL GROUP BY 分组(数据库层聚合)
|
|
632
|
+
const stats = await this.mapper.selectByCustom({
|
|
633
|
+
fields: [
|
|
634
|
+
"status",
|
|
635
|
+
"COUNT(*) as count",
|
|
636
|
+
"SUM(amount) as totalAmount",
|
|
637
|
+
"MAX(created_at) as latestTime",
|
|
638
|
+
],
|
|
639
|
+
groups: ["status"], // 数据库层分组
|
|
640
|
+
orders: { count: OrderEnum.desc },
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// ✅ 正确:使用 JOIN + GROUP BY 关联聚合
|
|
644
|
+
const userOrderStats = await this.mapper.selectByCustom({
|
|
645
|
+
fields: [
|
|
646
|
+
"u.id",
|
|
647
|
+
"u.name",
|
|
648
|
+
"COUNT(o.id) as orderCount",
|
|
649
|
+
"SUM(o.amount) as totalAmount",
|
|
650
|
+
],
|
|
651
|
+
join: [{
|
|
652
|
+
type: "LEFT",
|
|
653
|
+
table: "orders o",
|
|
654
|
+
on: "o.user_id = u.id",
|
|
655
|
+
}],
|
|
656
|
+
groups: ["u.id", "u.name"], // 数据库层分组聚合
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// ❌ 错误:全表查询后在 JS 中分组(数据量大时会导致 OOM)
|
|
660
|
+
const allRecords = await this.mapper.select({}); // 加载所有数据
|
|
661
|
+
const grouped = allRecords.reduce((acc, item) => {
|
|
662
|
+
if (!acc[item.status]) acc[item.status] = { count: 0, total: 0 };
|
|
663
|
+
acc[item.status].count++;
|
|
664
|
+
acc[item.status].total += item.amount;
|
|
665
|
+
return acc;
|
|
666
|
+
}, {}); // 内存中分组,大数据量时性能极差
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**为什么重要**:
|
|
670
|
+
- 数据库专为聚合计算优化,性能远高于 JS 遍历
|
|
671
|
+
- 减少网络传输(只返回聚合结果,而非原始数据)
|
|
672
|
+
- 避免 JS 单线程处理大量数据的性能瓶颈
|
|
673
|
+
- 防止内存溢出风险
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
501
677
|
## 编码规范
|
|
502
678
|
|
|
503
679
|
### 1. 实体对象创建规范
|
|
@@ -505,9 +681,9 @@ npm run debug
|
|
|
505
681
|
```typescript
|
|
506
682
|
// ✅ 正确:使用 key-value 形式创建对象
|
|
507
683
|
const entity = new Entity({
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
684
|
+
name: "示例",
|
|
685
|
+
status: 1,
|
|
686
|
+
createTime: new Date(),
|
|
511
687
|
});
|
|
512
688
|
|
|
513
689
|
// ❌ 错误:逐行赋值创建对象
|
|
@@ -522,10 +698,10 @@ entity.status = 1;
|
|
|
522
698
|
// ✅ 正确:使用 SQL limit/offset 分页
|
|
523
699
|
const total = await this.mapper.count(where);
|
|
524
700
|
const list = await this.mapper.select({
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
701
|
+
where: where,
|
|
702
|
+
orders: { createTime: OrderEnum.desc },
|
|
703
|
+
offset: (page - 1) * pageSize,
|
|
704
|
+
limit: pageSize,
|
|
529
705
|
});
|
|
530
706
|
|
|
531
707
|
// ❌ 错误:先全表查询再用 JS slice 分页
|
|
@@ -540,10 +716,10 @@ import { OperatorEnum } from "@fastcar/core/db";
|
|
|
540
716
|
|
|
541
717
|
// ✅ 正确:使用 OperatorEnum
|
|
542
718
|
const list = await this.mapper.select({
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
719
|
+
where: {
|
|
720
|
+
age: { [OperatorEnum.gte]: 18, [OperatorEnum.lte]: 60 },
|
|
721
|
+
status: { [OperatorEnum.in]: [1, 2, 3] },
|
|
722
|
+
},
|
|
547
723
|
});
|
|
548
724
|
```
|
|
549
725
|
|
|
@@ -552,12 +728,12 @@ const list = await this.mapper.select({
|
|
|
552
728
|
```typescript
|
|
553
729
|
// ✅ 正确:返回空数据
|
|
554
730
|
if (records.length === 0) {
|
|
555
|
-
|
|
731
|
+
return Result.ok({ list: [], total: 0 });
|
|
556
732
|
}
|
|
557
733
|
|
|
558
734
|
// ❌ 错误:返回模拟数据
|
|
559
735
|
if (records.length === 0) {
|
|
560
|
-
|
|
736
|
+
return Result.ok({ list: [{ name: "模拟数据1" }] });
|
|
561
737
|
}
|
|
562
738
|
```
|
|
563
739
|
|
|
@@ -566,8 +742,8 @@ if (records.length === 0) {
|
|
|
566
742
|
```typescript
|
|
567
743
|
// ✅ 正确:更新少于3个字段时使用 update/updateOne
|
|
568
744
|
await this.mapper.updateOne({
|
|
569
|
-
|
|
570
|
-
|
|
745
|
+
where: { id },
|
|
746
|
+
row: { lastLoginTime: new Date() },
|
|
571
747
|
});
|
|
572
748
|
|
|
573
749
|
// ❌ 错误:为了更新1-2个字段而查询整个实体对象
|