befly 3.9.38 → 3.9.40
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/README.md +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# 实战示例
|
|
2
2
|
|
|
3
3
|
> 完整的 CRUD 模块开发示例
|
|
4
4
|
|
|
@@ -40,28 +40,28 @@
|
|
|
40
40
|
`apis/user/register.ts`:
|
|
41
41
|
|
|
42
42
|
```typescript
|
|
43
|
-
import type { ApiRoute } from
|
|
43
|
+
import type { ApiRoute } from "befly/types/api";
|
|
44
44
|
|
|
45
45
|
export default {
|
|
46
|
-
name:
|
|
47
|
-
method:
|
|
46
|
+
name: "用户注册",
|
|
47
|
+
method: "POST",
|
|
48
48
|
auth: false,
|
|
49
49
|
fields: {
|
|
50
|
-
email: { name:
|
|
51
|
-
password: { name:
|
|
52
|
-
nickname: { name:
|
|
50
|
+
email: { name: "邮箱", type: "string", min: 5, max: 100, regexp: "@email" },
|
|
51
|
+
password: { name: "密码", type: "string", min: 6, max: 100 },
|
|
52
|
+
nickname: { name: "昵称", type: "string", min: 2, max: 50 }
|
|
53
53
|
},
|
|
54
|
-
required: [
|
|
54
|
+
required: ["email", "password"],
|
|
55
55
|
handler: async (befly, ctx) => {
|
|
56
56
|
// 检查邮箱是否已存在
|
|
57
57
|
const exists = await befly.db.getDetail({
|
|
58
|
-
table:
|
|
59
|
-
columns: [
|
|
58
|
+
table: "user",
|
|
59
|
+
columns: ["id"],
|
|
60
60
|
where: { email: ctx.body.email }
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
if (exists?.id) {
|
|
64
|
-
return No(
|
|
64
|
+
return No("该邮箱已被注册");
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 加密密码
|
|
@@ -69,15 +69,15 @@ export default {
|
|
|
69
69
|
|
|
70
70
|
// 创建用户
|
|
71
71
|
const result = await befly.db.insData({
|
|
72
|
-
table:
|
|
72
|
+
table: "user",
|
|
73
73
|
data: {
|
|
74
74
|
email: ctx.body.email,
|
|
75
75
|
password: hashedPassword,
|
|
76
|
-
nickname: ctx.body.nickname ||
|
|
76
|
+
nickname: ctx.body.nickname || "用户"
|
|
77
77
|
}
|
|
78
78
|
});
|
|
79
79
|
|
|
80
|
-
return Yes(
|
|
80
|
+
return Yes("注册成功", { id: result.insertId });
|
|
81
81
|
}
|
|
82
82
|
} as ApiRoute;
|
|
83
83
|
```
|
|
@@ -87,42 +87,42 @@ export default {
|
|
|
87
87
|
`apis/user/login.ts`:
|
|
88
88
|
|
|
89
89
|
```typescript
|
|
90
|
-
import type { ApiRoute } from
|
|
90
|
+
import type { ApiRoute } from "befly/types/api";
|
|
91
91
|
|
|
92
92
|
export default {
|
|
93
|
-
name:
|
|
94
|
-
method:
|
|
93
|
+
name: "用户登录",
|
|
94
|
+
method: "POST",
|
|
95
95
|
auth: false,
|
|
96
96
|
fields: {
|
|
97
|
-
email: { name:
|
|
98
|
-
password: { name:
|
|
97
|
+
email: { name: "邮箱", type: "string", min: 5, max: 100, regexp: "@email" },
|
|
98
|
+
password: { name: "密码", type: "string", min: 6, max: 100 }
|
|
99
99
|
},
|
|
100
|
-
required: [
|
|
100
|
+
required: ["email", "password"],
|
|
101
101
|
handler: async (befly, ctx) => {
|
|
102
102
|
// 查询用户
|
|
103
103
|
const user = await befly.db.getDetail({
|
|
104
|
-
table:
|
|
105
|
-
columns: [
|
|
104
|
+
table: "user",
|
|
105
|
+
columns: ["id", "email", "password", "nickname", "avatar", "role", "state"],
|
|
106
106
|
where: { email: ctx.body.email }
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
if (!user?.id) {
|
|
110
|
-
return No(
|
|
110
|
+
return No("用户不存在");
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (user.state !== 1) {
|
|
114
|
-
return No(
|
|
114
|
+
return No("账户已被禁用");
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// 验证密码
|
|
118
118
|
const isValid = await befly.cipher.verifyPassword(ctx.body.password, user.password);
|
|
119
119
|
if (!isValid) {
|
|
120
|
-
return No(
|
|
120
|
+
return No("密码错误");
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// 更新登录信息
|
|
124
124
|
await befly.db.updData({
|
|
125
|
-
table:
|
|
125
|
+
table: "user",
|
|
126
126
|
data: {
|
|
127
127
|
loginCount: user.loginCount + 1,
|
|
128
128
|
lastLoginAt: Date.now()
|
|
@@ -136,7 +136,7 @@ export default {
|
|
|
136
136
|
role: user.role
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
return Yes(
|
|
139
|
+
return Yes("登录成功", {
|
|
140
140
|
token: token,
|
|
141
141
|
user: {
|
|
142
142
|
id: user.id,
|
|
@@ -155,24 +155,24 @@ export default {
|
|
|
155
155
|
`apis/user/info.ts`:
|
|
156
156
|
|
|
157
157
|
```typescript
|
|
158
|
-
import type { ApiRoute } from
|
|
158
|
+
import type { ApiRoute } from "befly/types/api";
|
|
159
159
|
|
|
160
160
|
export default {
|
|
161
|
-
name:
|
|
162
|
-
method:
|
|
161
|
+
name: "获取用户信息",
|
|
162
|
+
method: "GET",
|
|
163
163
|
auth: true,
|
|
164
164
|
handler: async (befly, ctx) => {
|
|
165
165
|
const user = await befly.db.getDetail({
|
|
166
|
-
table:
|
|
167
|
-
columns: [
|
|
166
|
+
table: "user",
|
|
167
|
+
columns: ["id", "email", "nickname", "avatar", "phone", "gender", "birthday", "bio", "role", "createdAt"],
|
|
168
168
|
where: { id: ctx.user.userId }
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
if (!user?.id) {
|
|
172
|
-
return No(
|
|
172
|
+
return No("用户不存在");
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
return Yes(
|
|
175
|
+
return Yes("获取成功", user);
|
|
176
176
|
}
|
|
177
177
|
} as ApiRoute;
|
|
178
178
|
```
|
|
@@ -182,19 +182,19 @@ export default {
|
|
|
182
182
|
`apis/user/update.ts`:
|
|
183
183
|
|
|
184
184
|
```typescript
|
|
185
|
-
import type { ApiRoute } from
|
|
185
|
+
import type { ApiRoute } from "befly/types/api";
|
|
186
186
|
|
|
187
187
|
export default {
|
|
188
|
-
name:
|
|
189
|
-
method:
|
|
188
|
+
name: "更新用户信息",
|
|
189
|
+
method: "POST",
|
|
190
190
|
auth: true,
|
|
191
191
|
fields: {
|
|
192
|
-
nickname: { name:
|
|
193
|
-
avatar: { name:
|
|
194
|
-
phone: { name:
|
|
195
|
-
gender: { name:
|
|
196
|
-
birthday: { name:
|
|
197
|
-
bio: { name:
|
|
192
|
+
nickname: { name: "昵称", type: "string", min: 2, max: 50 },
|
|
193
|
+
avatar: { name: "头像", type: "string", max: 500 },
|
|
194
|
+
phone: { name: "手机号", type: "string", max: 20, regexp: "@phone" },
|
|
195
|
+
gender: { name: "性别", type: "number", min: 0, max: 2 },
|
|
196
|
+
birthday: { name: "生日", type: "string", max: 10 },
|
|
197
|
+
bio: { name: "简介", type: "string", max: 500 }
|
|
198
198
|
},
|
|
199
199
|
handler: async (befly, ctx) => {
|
|
200
200
|
const updateData: Record<string, any> = {};
|
|
@@ -208,16 +208,16 @@ export default {
|
|
|
208
208
|
if (ctx.body.bio !== undefined) updateData.bio = ctx.body.bio;
|
|
209
209
|
|
|
210
210
|
if (Object.keys(updateData).length === 0) {
|
|
211
|
-
return No(
|
|
211
|
+
return No("没有需要更新的字段");
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
await befly.db.updData({
|
|
215
|
-
table:
|
|
215
|
+
table: "user",
|
|
216
216
|
data: updateData,
|
|
217
217
|
where: { id: ctx.user.userId }
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
return Yes(
|
|
220
|
+
return Yes("更新成功");
|
|
221
221
|
}
|
|
222
222
|
} as ApiRoute;
|
|
223
223
|
```
|
|
@@ -227,33 +227,33 @@ export default {
|
|
|
227
227
|
`apis/user/changePassword.ts`:
|
|
228
228
|
|
|
229
229
|
```typescript
|
|
230
|
-
import type { ApiRoute } from
|
|
230
|
+
import type { ApiRoute } from "befly/types/api";
|
|
231
231
|
|
|
232
232
|
export default {
|
|
233
|
-
name:
|
|
234
|
-
method:
|
|
233
|
+
name: "修改密码",
|
|
234
|
+
method: "POST",
|
|
235
235
|
auth: true,
|
|
236
236
|
fields: {
|
|
237
|
-
oldPassword: { name:
|
|
238
|
-
newPassword: { name:
|
|
237
|
+
oldPassword: { name: "原密码", type: "string", min: 6, max: 100 },
|
|
238
|
+
newPassword: { name: "新密码", type: "string", min: 6, max: 100 }
|
|
239
239
|
},
|
|
240
|
-
required: [
|
|
240
|
+
required: ["oldPassword", "newPassword"],
|
|
241
241
|
handler: async (befly, ctx) => {
|
|
242
242
|
// 获取用户密码
|
|
243
243
|
const user = await befly.db.getDetail({
|
|
244
|
-
table:
|
|
245
|
-
columns: [
|
|
244
|
+
table: "user",
|
|
245
|
+
columns: ["id", "password"],
|
|
246
246
|
where: { id: ctx.user.userId }
|
|
247
247
|
});
|
|
248
248
|
|
|
249
249
|
if (!user?.id) {
|
|
250
|
-
return No(
|
|
250
|
+
return No("用户不存在");
|
|
251
251
|
}
|
|
252
252
|
|
|
253
253
|
// 验证原密码
|
|
254
254
|
const isValid = await befly.cipher.verifyPassword(ctx.body.oldPassword, user.password);
|
|
255
255
|
if (!isValid) {
|
|
256
|
-
return No(
|
|
256
|
+
return No("原密码错误");
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
// 加密新密码
|
|
@@ -261,12 +261,12 @@ export default {
|
|
|
261
261
|
|
|
262
262
|
// 更新密码
|
|
263
263
|
await befly.db.updData({
|
|
264
|
-
table:
|
|
264
|
+
table: "user",
|
|
265
265
|
data: { password: hashedPassword },
|
|
266
266
|
where: { id: ctx.user.userId }
|
|
267
267
|
});
|
|
268
268
|
|
|
269
|
-
return Yes(
|
|
269
|
+
return Yes("密码修改成功");
|
|
270
270
|
}
|
|
271
271
|
} as ApiRoute;
|
|
272
272
|
```
|
|
@@ -276,19 +276,19 @@ export default {
|
|
|
276
276
|
`apis/user/list.ts`:
|
|
277
277
|
|
|
278
278
|
```typescript
|
|
279
|
-
import type { ApiRoute } from
|
|
279
|
+
import type { ApiRoute } from "befly/types/api";
|
|
280
280
|
|
|
281
281
|
export default {
|
|
282
|
-
name:
|
|
283
|
-
method:
|
|
282
|
+
name: "用户列表",
|
|
283
|
+
method: "POST",
|
|
284
284
|
auth: true,
|
|
285
|
-
permission:
|
|
285
|
+
permission: "user:list",
|
|
286
286
|
fields: {
|
|
287
|
-
page:
|
|
288
|
-
limit:
|
|
289
|
-
keyword:
|
|
290
|
-
state:
|
|
291
|
-
role: { name:
|
|
287
|
+
page: "@page",
|
|
288
|
+
limit: "@limit",
|
|
289
|
+
keyword: "@keyword",
|
|
290
|
+
state: "@state",
|
|
291
|
+
role: { name: "角色", type: "string", max: 20 }
|
|
292
292
|
},
|
|
293
293
|
handler: async (befly, ctx) => {
|
|
294
294
|
const { page, limit, keyword, state, role } = ctx.body;
|
|
@@ -309,15 +309,15 @@ export default {
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
const result = await befly.db.getList({
|
|
312
|
-
table:
|
|
313
|
-
columns: [
|
|
312
|
+
table: "user",
|
|
313
|
+
columns: ["id", "email", "nickname", "avatar", "phone", "role", "state", "loginCount", "lastLoginAt", "createdAt"],
|
|
314
314
|
where: where,
|
|
315
315
|
page: page || 1,
|
|
316
316
|
limit: limit || 20,
|
|
317
|
-
orderBy: { id:
|
|
317
|
+
orderBy: { id: "desc" }
|
|
318
318
|
});
|
|
319
319
|
|
|
320
|
-
return Yes(
|
|
320
|
+
return Yes("获取成功", result);
|
|
321
321
|
}
|
|
322
322
|
} as ApiRoute;
|
|
323
323
|
```
|
|
@@ -368,34 +368,34 @@ export default {
|
|
|
368
368
|
`apis/article/create.ts`:
|
|
369
369
|
|
|
370
370
|
```typescript
|
|
371
|
-
import type { ApiRoute } from
|
|
371
|
+
import type { ApiRoute } from "befly/types/api";
|
|
372
372
|
|
|
373
373
|
export default {
|
|
374
|
-
name:
|
|
375
|
-
method:
|
|
374
|
+
name: "发布文章",
|
|
375
|
+
method: "POST",
|
|
376
376
|
auth: true,
|
|
377
377
|
fields: {
|
|
378
|
-
title: { name:
|
|
379
|
-
content: { name:
|
|
380
|
-
summary: { name:
|
|
381
|
-
cover: { name:
|
|
382
|
-
categoryId: { name:
|
|
383
|
-
tags: { name:
|
|
378
|
+
title: { name: "标题", type: "string", min: 2, max: 200 },
|
|
379
|
+
content: { name: "内容", type: "text", min: 1, max: 100000 },
|
|
380
|
+
summary: { name: "摘要", type: "string", max: 500 },
|
|
381
|
+
cover: { name: "封面", type: "string", max: 500 },
|
|
382
|
+
categoryId: { name: "分类", type: "number", min: 0 },
|
|
383
|
+
tags: { name: "标签", type: "array_string", max: 10 }
|
|
384
384
|
},
|
|
385
|
-
required: [
|
|
385
|
+
required: ["title", "content"],
|
|
386
386
|
handler: async (befly, ctx) => {
|
|
387
387
|
const { title, content, summary, cover, categoryId, tags } = ctx.body;
|
|
388
388
|
|
|
389
389
|
// 自动生成摘要
|
|
390
|
-
const autoSummary = summary || content.replace(/<[^>]+>/g,
|
|
390
|
+
const autoSummary = summary || content.replace(/<[^>]+>/g, "").slice(0, 200);
|
|
391
391
|
|
|
392
392
|
const result = await befly.db.insData({
|
|
393
|
-
table:
|
|
393
|
+
table: "article",
|
|
394
394
|
data: {
|
|
395
395
|
title: title,
|
|
396
396
|
content: content,
|
|
397
397
|
summary: autoSummary,
|
|
398
|
-
cover: cover ||
|
|
398
|
+
cover: cover || "",
|
|
399
399
|
categoryId: categoryId || 0,
|
|
400
400
|
tags: tags || [],
|
|
401
401
|
authorId: ctx.user.userId,
|
|
@@ -406,13 +406,13 @@ export default {
|
|
|
406
406
|
// 更新分类文章数
|
|
407
407
|
if (categoryId) {
|
|
408
408
|
await befly.db.updData({
|
|
409
|
-
table:
|
|
409
|
+
table: "category",
|
|
410
410
|
data: { articleCount: { $incr: 1 } },
|
|
411
411
|
where: { id: categoryId }
|
|
412
412
|
});
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
return Yes(
|
|
415
|
+
return Yes("发布成功", { id: result.insertId });
|
|
416
416
|
}
|
|
417
417
|
} as ApiRoute;
|
|
418
418
|
```
|
|
@@ -422,39 +422,39 @@ export default {
|
|
|
422
422
|
`apis/article/update.ts`:
|
|
423
423
|
|
|
424
424
|
```typescript
|
|
425
|
-
import type { ApiRoute } from
|
|
425
|
+
import type { ApiRoute } from "befly/types/api";
|
|
426
426
|
|
|
427
427
|
export default {
|
|
428
|
-
name:
|
|
429
|
-
method:
|
|
428
|
+
name: "编辑文章",
|
|
429
|
+
method: "POST",
|
|
430
430
|
auth: true,
|
|
431
431
|
fields: {
|
|
432
|
-
id:
|
|
433
|
-
title: { name:
|
|
434
|
-
content: { name:
|
|
435
|
-
summary: { name:
|
|
436
|
-
cover: { name:
|
|
437
|
-
categoryId: { name:
|
|
438
|
-
tags: { name:
|
|
432
|
+
id: "@id",
|
|
433
|
+
title: { name: "标题", type: "string", min: 2, max: 200 },
|
|
434
|
+
content: { name: "内容", type: "text", min: 1, max: 100000 },
|
|
435
|
+
summary: { name: "摘要", type: "string", max: 500 },
|
|
436
|
+
cover: { name: "封面", type: "string", max: 500 },
|
|
437
|
+
categoryId: { name: "分类", type: "number", min: 0 },
|
|
438
|
+
tags: { name: "标签", type: "array_string", max: 10 }
|
|
439
439
|
},
|
|
440
|
-
required: [
|
|
440
|
+
required: ["id"],
|
|
441
441
|
handler: async (befly, ctx) => {
|
|
442
442
|
const { id, title, content, summary, cover, categoryId, tags } = ctx.body;
|
|
443
443
|
|
|
444
444
|
// 检查文章是否存在
|
|
445
445
|
const article = await befly.db.getDetail({
|
|
446
|
-
table:
|
|
447
|
-
columns: [
|
|
446
|
+
table: "article",
|
|
447
|
+
columns: ["id", "authorId", "categoryId"],
|
|
448
448
|
where: { id: id }
|
|
449
449
|
});
|
|
450
450
|
|
|
451
451
|
if (!article?.id) {
|
|
452
|
-
return No(
|
|
452
|
+
return No("文章不存在");
|
|
453
453
|
}
|
|
454
454
|
|
|
455
455
|
// 检查权限(只能编辑自己的文章,管理员除外)
|
|
456
|
-
if (article.authorId !== ctx.user.userId && ctx.user.role !==
|
|
457
|
-
return No(
|
|
456
|
+
if (article.authorId !== ctx.user.userId && ctx.user.role !== "admin") {
|
|
457
|
+
return No("没有权限编辑此文章");
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
const updateData: Record<string, any> = {};
|
|
@@ -466,11 +466,11 @@ export default {
|
|
|
466
466
|
if (tags !== undefined) updateData.tags = tags;
|
|
467
467
|
|
|
468
468
|
if (Object.keys(updateData).length === 0) {
|
|
469
|
-
return No(
|
|
469
|
+
return No("没有需要更新的字段");
|
|
470
470
|
}
|
|
471
471
|
|
|
472
472
|
await befly.db.updData({
|
|
473
|
-
table:
|
|
473
|
+
table: "article",
|
|
474
474
|
data: updateData,
|
|
475
475
|
where: { id: id }
|
|
476
476
|
});
|
|
@@ -479,21 +479,21 @@ export default {
|
|
|
479
479
|
if (categoryId !== undefined && categoryId !== article.categoryId) {
|
|
480
480
|
if (article.categoryId) {
|
|
481
481
|
await befly.db.updData({
|
|
482
|
-
table:
|
|
482
|
+
table: "category",
|
|
483
483
|
data: { articleCount: { $decr: 1 } },
|
|
484
484
|
where: { id: article.categoryId }
|
|
485
485
|
});
|
|
486
486
|
}
|
|
487
487
|
if (categoryId) {
|
|
488
488
|
await befly.db.updData({
|
|
489
|
-
table:
|
|
489
|
+
table: "category",
|
|
490
490
|
data: { articleCount: { $incr: 1 } },
|
|
491
491
|
where: { id: categoryId }
|
|
492
492
|
});
|
|
493
493
|
}
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
return Yes(
|
|
496
|
+
return Yes("更新成功");
|
|
497
497
|
}
|
|
498
498
|
} as ApiRoute;
|
|
499
499
|
```
|
|
@@ -503,48 +503,48 @@ export default {
|
|
|
503
503
|
`apis/article/delete.ts`:
|
|
504
504
|
|
|
505
505
|
```typescript
|
|
506
|
-
import type { ApiRoute } from
|
|
506
|
+
import type { ApiRoute } from "befly/types/api";
|
|
507
507
|
|
|
508
508
|
export default {
|
|
509
|
-
name:
|
|
510
|
-
method:
|
|
509
|
+
name: "删除文章",
|
|
510
|
+
method: "POST",
|
|
511
511
|
auth: true,
|
|
512
512
|
fields: {
|
|
513
|
-
id:
|
|
513
|
+
id: "@id"
|
|
514
514
|
},
|
|
515
|
-
required: [
|
|
515
|
+
required: ["id"],
|
|
516
516
|
handler: async (befly, ctx) => {
|
|
517
517
|
const article = await befly.db.getDetail({
|
|
518
|
-
table:
|
|
519
|
-
columns: [
|
|
518
|
+
table: "article",
|
|
519
|
+
columns: ["id", "authorId", "categoryId"],
|
|
520
520
|
where: { id: ctx.body.id }
|
|
521
521
|
});
|
|
522
522
|
|
|
523
523
|
if (!article?.id) {
|
|
524
|
-
return No(
|
|
524
|
+
return No("文章不存在");
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
// 检查权限
|
|
528
|
-
if (article.authorId !== ctx.user.userId && ctx.user.role !==
|
|
529
|
-
return No(
|
|
528
|
+
if (article.authorId !== ctx.user.userId && ctx.user.role !== "admin") {
|
|
529
|
+
return No("没有权限删除此文章");
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
// 软删除
|
|
533
533
|
await befly.db.delData({
|
|
534
|
-
table:
|
|
534
|
+
table: "article",
|
|
535
535
|
where: { id: ctx.body.id }
|
|
536
536
|
});
|
|
537
537
|
|
|
538
538
|
// 更新分类文章数
|
|
539
539
|
if (article.categoryId) {
|
|
540
540
|
await befly.db.updData({
|
|
541
|
-
table:
|
|
541
|
+
table: "category",
|
|
542
542
|
data: { articleCount: { $decr: 1 } },
|
|
543
543
|
where: { id: article.categoryId }
|
|
544
544
|
});
|
|
545
545
|
}
|
|
546
546
|
|
|
547
|
-
return Yes(
|
|
547
|
+
return Yes("删除成功");
|
|
548
548
|
}
|
|
549
549
|
} as ApiRoute;
|
|
550
550
|
```
|
|
@@ -554,21 +554,21 @@ export default {
|
|
|
554
554
|
`apis/article/list.ts`:
|
|
555
555
|
|
|
556
556
|
```typescript
|
|
557
|
-
import type { ApiRoute } from
|
|
557
|
+
import type { ApiRoute } from "befly/types/api";
|
|
558
558
|
|
|
559
559
|
export default {
|
|
560
|
-
name:
|
|
561
|
-
method:
|
|
560
|
+
name: "文章列表",
|
|
561
|
+
method: "POST",
|
|
562
562
|
auth: false,
|
|
563
563
|
fields: {
|
|
564
|
-
page:
|
|
565
|
-
limit:
|
|
566
|
-
keyword:
|
|
567
|
-
categoryId: { name:
|
|
568
|
-
authorId: { name:
|
|
569
|
-
isTop: { name:
|
|
570
|
-
isRecommend: { name:
|
|
571
|
-
orderBy: { name:
|
|
564
|
+
page: "@page",
|
|
565
|
+
limit: "@limit",
|
|
566
|
+
keyword: "@keyword",
|
|
567
|
+
categoryId: { name: "分类", type: "number", min: 0 },
|
|
568
|
+
authorId: { name: "作者", type: "number", min: 0 },
|
|
569
|
+
isTop: { name: "置顶", type: "number", min: 0, max: 1 },
|
|
570
|
+
isRecommend: { name: "推荐", type: "number", min: 0, max: 1 },
|
|
571
|
+
orderBy: { name: "排序", type: "string", max: 20 }
|
|
572
572
|
},
|
|
573
573
|
handler: async (befly, ctx) => {
|
|
574
574
|
const { page, limit, categoryId, authorId, keyword, isTop, isRecommend, orderBy } = ctx.body;
|
|
@@ -585,20 +585,20 @@ export default {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
// 排序
|
|
588
|
-
let order: Record<string,
|
|
589
|
-
if (orderBy ===
|
|
590
|
-
if (orderBy ===
|
|
588
|
+
let order: Record<string, "asc" | "desc"> = { isTop: "desc", publishedAt: "desc" };
|
|
589
|
+
if (orderBy === "views") order = { viewCount: "desc" };
|
|
590
|
+
if (orderBy === "likes") order = { likeCount: "desc" };
|
|
591
591
|
|
|
592
592
|
const result = await befly.db.getList({
|
|
593
|
-
table:
|
|
594
|
-
columns: [
|
|
593
|
+
table: "article",
|
|
594
|
+
columns: ["id", "title", "summary", "cover", "categoryId", "tags", "authorId", "viewCount", "likeCount", "commentCount", "isTop", "isRecommend", "publishedAt"],
|
|
595
595
|
where: where,
|
|
596
596
|
page: page || 1,
|
|
597
597
|
limit: limit || 10,
|
|
598
598
|
orderBy: order
|
|
599
599
|
});
|
|
600
600
|
|
|
601
|
-
return Yes(
|
|
601
|
+
return Yes("获取成功", result);
|
|
602
602
|
}
|
|
603
603
|
} as ApiRoute;
|
|
604
604
|
```
|
|
@@ -608,37 +608,37 @@ export default {
|
|
|
608
608
|
`apis/article/detail.ts`:
|
|
609
609
|
|
|
610
610
|
```typescript
|
|
611
|
-
import type { ApiRoute } from
|
|
611
|
+
import type { ApiRoute } from "befly/types/api";
|
|
612
612
|
|
|
613
613
|
export default {
|
|
614
|
-
name:
|
|
615
|
-
method:
|
|
614
|
+
name: "文章详情",
|
|
615
|
+
method: "GET",
|
|
616
616
|
auth: false,
|
|
617
617
|
fields: {
|
|
618
|
-
id:
|
|
618
|
+
id: "@id"
|
|
619
619
|
},
|
|
620
|
-
required: [
|
|
620
|
+
required: ["id"],
|
|
621
621
|
handler: async (befly, ctx) => {
|
|
622
622
|
const article = await befly.db.getDetail({
|
|
623
|
-
table:
|
|
623
|
+
table: "article",
|
|
624
624
|
where: { id: ctx.body.id, state: 1 }
|
|
625
625
|
});
|
|
626
626
|
|
|
627
627
|
if (!article?.id) {
|
|
628
|
-
return No(
|
|
628
|
+
return No("文章不存在");
|
|
629
629
|
}
|
|
630
630
|
|
|
631
631
|
// 增加浏览量
|
|
632
632
|
await befly.db.updData({
|
|
633
|
-
table:
|
|
633
|
+
table: "article",
|
|
634
634
|
data: { viewCount: { $incr: 1 } },
|
|
635
635
|
where: { id: ctx.body.id }
|
|
636
636
|
});
|
|
637
637
|
|
|
638
638
|
// 获取作者信息
|
|
639
639
|
const author = await befly.db.getDetail({
|
|
640
|
-
table:
|
|
641
|
-
columns: [
|
|
640
|
+
table: "user",
|
|
641
|
+
columns: ["id", "nickname", "avatar"],
|
|
642
642
|
where: { id: article.authorId }
|
|
643
643
|
});
|
|
644
644
|
|
|
@@ -646,13 +646,13 @@ export default {
|
|
|
646
646
|
let category = null;
|
|
647
647
|
if (article.categoryId) {
|
|
648
648
|
category = await befly.db.getDetail({
|
|
649
|
-
table:
|
|
650
|
-
columns: [
|
|
649
|
+
table: "category",
|
|
650
|
+
columns: ["id", "name", "slug"],
|
|
651
651
|
where: { id: article.categoryId }
|
|
652
652
|
});
|
|
653
653
|
}
|
|
654
654
|
|
|
655
|
-
return Yes(
|
|
655
|
+
return Yes("获取成功", {
|
|
656
656
|
...article,
|
|
657
657
|
viewCount: article.viewCount + 1,
|
|
658
658
|
author: author,
|
|
@@ -671,39 +671,39 @@ export default {
|
|
|
671
671
|
`apis/common/upload.ts`:
|
|
672
672
|
|
|
673
673
|
```typescript
|
|
674
|
-
import { join } from
|
|
675
|
-
import { existsSync, mkdirSync } from
|
|
676
|
-
import type { ApiRoute } from
|
|
674
|
+
import { join } from "pathe";
|
|
675
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
676
|
+
import type { ApiRoute } from "befly/types/api";
|
|
677
677
|
|
|
678
678
|
export default {
|
|
679
|
-
name:
|
|
680
|
-
method:
|
|
679
|
+
name: "文件上传",
|
|
680
|
+
method: "POST",
|
|
681
681
|
auth: true,
|
|
682
682
|
handler: async (befly, ctx) => {
|
|
683
683
|
const formData = await ctx.req.formData();
|
|
684
|
-
const file = formData.get(
|
|
684
|
+
const file = formData.get("file") as File | null;
|
|
685
685
|
|
|
686
686
|
if (!file) {
|
|
687
|
-
return No(
|
|
687
|
+
return No("请选择文件");
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
// 检查文件大小(10MB)
|
|
691
691
|
if (file.size > 10 * 1024 * 1024) {
|
|
692
|
-
return No(
|
|
692
|
+
return No("文件大小不能超过 10MB");
|
|
693
693
|
}
|
|
694
694
|
|
|
695
695
|
// 检查文件类型
|
|
696
|
-
const allowedTypes = [
|
|
696
|
+
const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
697
697
|
if (!allowedTypes.includes(file.type)) {
|
|
698
|
-
return No(
|
|
698
|
+
return No("只支持 jpg/png/gif/webp 格式");
|
|
699
699
|
}
|
|
700
700
|
|
|
701
701
|
// 生成文件名
|
|
702
|
-
const ext = file.name.split(
|
|
702
|
+
const ext = file.name.split(".").pop();
|
|
703
703
|
const fileName = `${Date.now()}_${befly.cipher.randomString(8)}.${ext}`;
|
|
704
704
|
|
|
705
705
|
// 保存目录
|
|
706
|
-
const uploadDir = join(process.cwd(),
|
|
706
|
+
const uploadDir = join(process.cwd(), "uploads", new Date().toISOString().slice(0, 7));
|
|
707
707
|
if (!existsSync(uploadDir)) {
|
|
708
708
|
mkdirSync(uploadDir, { recursive: true });
|
|
709
709
|
}
|
|
@@ -716,7 +716,7 @@ export default {
|
|
|
716
716
|
// 返回 URL
|
|
717
717
|
const url = `/uploads/${new Date().toISOString().slice(0, 7)}/${fileName}`;
|
|
718
718
|
|
|
719
|
-
return Yes(
|
|
719
|
+
return Yes("上传成功", {
|
|
720
720
|
url: url,
|
|
721
721
|
name: file.name,
|
|
722
722
|
size: file.size,
|
|
@@ -735,34 +735,34 @@ export default {
|
|
|
735
735
|
`apis/user/export.ts`:
|
|
736
736
|
|
|
737
737
|
```typescript
|
|
738
|
-
import type { ApiRoute } from
|
|
738
|
+
import type { ApiRoute } from "befly/types/api";
|
|
739
739
|
|
|
740
740
|
export default {
|
|
741
|
-
name:
|
|
742
|
-
method:
|
|
741
|
+
name: "导出用户",
|
|
742
|
+
method: "GET",
|
|
743
743
|
auth: true,
|
|
744
|
-
permission:
|
|
744
|
+
permission: "user:export",
|
|
745
745
|
handler: async (befly, ctx) => {
|
|
746
746
|
// 获取所有用户
|
|
747
747
|
const result = await befly.db.getList({
|
|
748
|
-
table:
|
|
749
|
-
columns: [
|
|
748
|
+
table: "user",
|
|
749
|
+
columns: ["id", "email", "nickname", "phone", "role", "state", "createdAt"],
|
|
750
750
|
where: { state: { $gte: 0 } },
|
|
751
751
|
page: 1,
|
|
752
752
|
limit: 10000
|
|
753
753
|
});
|
|
754
754
|
|
|
755
755
|
// 生成 CSV
|
|
756
|
-
const headers = [
|
|
757
|
-
const rows = result.list.map((user: any) => [user.id, user.email, user.nickname, user.phone ||
|
|
756
|
+
const headers = ["ID", "邮箱", "昵称", "手机号", "角色", "状态", "注册时间"];
|
|
757
|
+
const rows = result.list.map((user: any) => [user.id, user.email, user.nickname, user.phone || "", user.role, user.state === 1 ? "正常" : "禁用", new Date(user.createdAt).toLocaleString()]);
|
|
758
758
|
|
|
759
|
-
const csv = [headers.join(
|
|
759
|
+
const csv = [headers.join(","), ...rows.map((row: any[]) => row.map((cell) => `"${cell}"`).join(","))].join("\n");
|
|
760
760
|
|
|
761
761
|
// 返回文件
|
|
762
762
|
return new Response(csv, {
|
|
763
763
|
headers: {
|
|
764
|
-
|
|
765
|
-
|
|
764
|
+
"Content-Type": "text/csv; charset=utf-8",
|
|
765
|
+
"Content-Disposition": `attachment; filename="users_${Date.now()}.csv"`
|
|
766
766
|
}
|
|
767
767
|
});
|
|
768
768
|
}
|
|
@@ -788,11 +788,11 @@ if (ctx.body.phone !== undefined) updateData.phone = ctx.body.phone;
|
|
|
788
788
|
if (ctx.body.gender !== undefined) updateData.gender = ctx.body.gender;
|
|
789
789
|
|
|
790
790
|
if (Object.keys(updateData).length === 0) {
|
|
791
|
-
return No(
|
|
791
|
+
return No("没有需要更新的字段");
|
|
792
792
|
}
|
|
793
793
|
|
|
794
794
|
await befly.db.updData({
|
|
795
|
-
table:
|
|
795
|
+
table: "user",
|
|
796
796
|
data: updateData,
|
|
797
797
|
where: { id: ctx.user.userId }
|
|
798
798
|
});
|
|
@@ -809,11 +809,11 @@ const data = { nickname: nickname, avatar: avatar, phone: phone, gender: gender,
|
|
|
809
809
|
// 使用 cleanFields 检查是否有有效数据
|
|
810
810
|
const cleanData = befly.tool.cleanFields(data);
|
|
811
811
|
if (Object.keys(cleanData).length === 0) {
|
|
812
|
-
return No(
|
|
812
|
+
return No("没有需要更新的字段");
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
await befly.db.updData({
|
|
816
|
-
table:
|
|
816
|
+
table: "user",
|
|
817
817
|
data: cleanData,
|
|
818
818
|
where: { id: ctx.user.userId }
|
|
819
819
|
});
|
|
@@ -834,7 +834,7 @@ const data = befly.tool.cleanFields(
|
|
|
834
834
|
);
|
|
835
835
|
|
|
836
836
|
await befly.db.updData({
|
|
837
|
-
table:
|
|
837
|
+
table: "menu",
|
|
838
838
|
data: data,
|
|
839
839
|
where: { id: ctx.body.id }
|
|
840
840
|
});
|
|
@@ -849,8 +849,8 @@ where 条件同样支持自动过滤:
|
|
|
849
849
|
const { keyword, state, categoryId, startDate, endDate } = ctx.body;
|
|
850
850
|
|
|
851
851
|
const result = await befly.db.getList({
|
|
852
|
-
table:
|
|
853
|
-
columns: [
|
|
852
|
+
table: "article",
|
|
853
|
+
columns: ["id", "title", "createdAt"],
|
|
854
854
|
where: {
|
|
855
855
|
state: state, // undefined 时忽略
|
|
856
856
|
categoryId: categoryId, // undefined 时忽略
|