befly 3.9.12 → 3.9.13

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.
@@ -0,0 +1,799 @@
1
+ # 实战示例
2
+
3
+ > 完整的 CRUD 模块开发示例
4
+
5
+ ## 目录
6
+
7
+ - [用户管理模块](#用户管理模块)
8
+ - [文章管理模块](#文章管理模块)
9
+ - [文件上传](#文件上传)
10
+ - [数据导出](#数据导出)
11
+
12
+ ---
13
+
14
+ ## 用户管理模块
15
+
16
+ 一个完整的用户管理模块,包含注册、登录、信息获取、更新、列表查询。
17
+
18
+ ### 表定义
19
+
20
+ `tables/user.json`:
21
+
22
+ ```json
23
+ {
24
+ "email": "邮箱|string|5|100||true|@email",
25
+ "password": "密码|string|6|100||true",
26
+ "nickname": "昵称|string|2|50|用户",
27
+ "avatar": "头像|string|0|500",
28
+ "phone": "手机号|string|0|20||false|@phone",
29
+ "gender": "性别|number|0|2|0",
30
+ "birthday": "生日|string|0|10",
31
+ "bio": "简介|string|0|500",
32
+ "role": "角色|string|2|20|user",
33
+ "login_count": "登录次数|number|0||0",
34
+ "last_login_at": "最后登录|number|0||"
35
+ }
36
+ ```
37
+
38
+ ### 用户注册
39
+
40
+ `apis/user/register.ts`:
41
+
42
+ ```typescript
43
+ import type { ApiRoute } from 'befly-core/types';
44
+
45
+ export default {
46
+ name: '用户注册',
47
+ method: 'POST',
48
+ auth: false,
49
+ fields: {
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
+ },
54
+ required: ['email', 'password'],
55
+ handler: async (befly, ctx) => {
56
+ // 检查邮箱是否已存在
57
+ const exists = await befly.db.getDetail({
58
+ table: 'user',
59
+ columns: ['id'],
60
+ where: { email: ctx.body.email }
61
+ });
62
+
63
+ if (exists?.id) {
64
+ return No('该邮箱已被注册');
65
+ }
66
+
67
+ // 加密密码
68
+ const hashedPassword = await befly.cipher.hashPassword(ctx.body.password);
69
+
70
+ // 创建用户
71
+ const result = await befly.db.insData({
72
+ table: 'user',
73
+ data: {
74
+ email: ctx.body.email,
75
+ password: hashedPassword,
76
+ nickname: ctx.body.nickname || '用户'
77
+ }
78
+ });
79
+
80
+ return Yes('注册成功', { id: result.insertId });
81
+ }
82
+ } as ApiRoute;
83
+ ```
84
+
85
+ ### 用户登录
86
+
87
+ `apis/user/login.ts`:
88
+
89
+ ```typescript
90
+ import type { ApiRoute } from 'befly-core/types';
91
+
92
+ export default {
93
+ name: '用户登录',
94
+ method: 'POST',
95
+ auth: false,
96
+ fields: {
97
+ email: { name: '邮箱', type: 'string', min: 5, max: 100, regexp: '@email' },
98
+ password: { name: '密码', type: 'string', min: 6, max: 100 }
99
+ },
100
+ required: ['email', 'password'],
101
+ handler: async (befly, ctx) => {
102
+ // 查询用户
103
+ const user = await befly.db.getDetail({
104
+ table: 'user',
105
+ columns: ['id', 'email', 'password', 'nickname', 'avatar', 'role', 'state'],
106
+ where: { email: ctx.body.email }
107
+ });
108
+
109
+ if (!user?.id) {
110
+ return No('用户不存在');
111
+ }
112
+
113
+ if (user.state !== 1) {
114
+ return No('账户已被禁用');
115
+ }
116
+
117
+ // 验证密码
118
+ const isValid = await befly.cipher.verifyPassword(ctx.body.password, user.password);
119
+ if (!isValid) {
120
+ return No('密码错误');
121
+ }
122
+
123
+ // 更新登录信息
124
+ await befly.db.updData({
125
+ table: 'user',
126
+ data: {
127
+ loginCount: user.loginCount + 1,
128
+ lastLoginAt: Date.now()
129
+ },
130
+ where: { id: user.id }
131
+ });
132
+
133
+ // 签发令牌
134
+ const token = befly.jwt.sign({
135
+ userId: user.id,
136
+ role: user.role
137
+ });
138
+
139
+ return Yes('登录成功', {
140
+ token: token,
141
+ user: {
142
+ id: user.id,
143
+ email: user.email,
144
+ nickname: user.nickname,
145
+ avatar: user.avatar,
146
+ role: user.role
147
+ }
148
+ });
149
+ }
150
+ } as ApiRoute;
151
+ ```
152
+
153
+ ### 获取用户信息
154
+
155
+ `apis/user/info.ts`:
156
+
157
+ ```typescript
158
+ import type { ApiRoute } from 'befly-core/types';
159
+
160
+ export default {
161
+ name: '获取用户信息',
162
+ method: 'GET',
163
+ auth: true,
164
+ handler: async (befly, ctx) => {
165
+ const user = await befly.db.getDetail({
166
+ table: 'user',
167
+ columns: ['id', 'email', 'nickname', 'avatar', 'phone', 'gender', 'birthday', 'bio', 'role', 'createdAt'],
168
+ where: { id: ctx.user.userId }
169
+ });
170
+
171
+ if (!user?.id) {
172
+ return No('用户不存在');
173
+ }
174
+
175
+ return Yes('获取成功', user);
176
+ }
177
+ } as ApiRoute;
178
+ ```
179
+
180
+ ### 更新用户信息
181
+
182
+ `apis/user/update.ts`:
183
+
184
+ ```typescript
185
+ import type { ApiRoute } from 'befly-core/types';
186
+
187
+ export default {
188
+ name: '更新用户信息',
189
+ method: 'POST',
190
+ auth: true,
191
+ fields: {
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
+ },
199
+ handler: async (befly, ctx) => {
200
+ const updateData: Record<string, any> = {};
201
+
202
+ // 只更新提交的字段
203
+ if (ctx.body.nickname !== undefined) updateData.nickname = ctx.body.nickname;
204
+ if (ctx.body.avatar !== undefined) updateData.avatar = ctx.body.avatar;
205
+ if (ctx.body.phone !== undefined) updateData.phone = ctx.body.phone;
206
+ if (ctx.body.gender !== undefined) updateData.gender = ctx.body.gender;
207
+ if (ctx.body.birthday !== undefined) updateData.birthday = ctx.body.birthday;
208
+ if (ctx.body.bio !== undefined) updateData.bio = ctx.body.bio;
209
+
210
+ if (Object.keys(updateData).length === 0) {
211
+ return No('没有需要更新的字段');
212
+ }
213
+
214
+ await befly.db.updData({
215
+ table: 'user',
216
+ data: updateData,
217
+ where: { id: ctx.user.userId }
218
+ });
219
+
220
+ return Yes('更新成功');
221
+ }
222
+ } as ApiRoute;
223
+ ```
224
+
225
+ ### 修改密码
226
+
227
+ `apis/user/changePassword.ts`:
228
+
229
+ ```typescript
230
+ import type { ApiRoute } from 'befly-core/types';
231
+
232
+ export default {
233
+ name: '修改密码',
234
+ method: 'POST',
235
+ auth: true,
236
+ fields: {
237
+ oldPassword: { name: '原密码', type: 'string', min: 6, max: 100 },
238
+ newPassword: { name: '新密码', type: 'string', min: 6, max: 100 }
239
+ },
240
+ required: ['oldPassword', 'newPassword'],
241
+ handler: async (befly, ctx) => {
242
+ // 获取用户密码
243
+ const user = await befly.db.getDetail({
244
+ table: 'user',
245
+ columns: ['id', 'password'],
246
+ where: { id: ctx.user.userId }
247
+ });
248
+
249
+ if (!user?.id) {
250
+ return No('用户不存在');
251
+ }
252
+
253
+ // 验证原密码
254
+ const isValid = await befly.cipher.verifyPassword(ctx.body.oldPassword, user.password);
255
+ if (!isValid) {
256
+ return No('原密码错误');
257
+ }
258
+
259
+ // 加密新密码
260
+ const hashedPassword = await befly.cipher.hashPassword(ctx.body.newPassword);
261
+
262
+ // 更新密码
263
+ await befly.db.updData({
264
+ table: 'user',
265
+ data: { password: hashedPassword },
266
+ where: { id: ctx.user.userId }
267
+ });
268
+
269
+ return Yes('密码修改成功');
270
+ }
271
+ } as ApiRoute;
272
+ ```
273
+
274
+ ### 用户列表(管理员)
275
+
276
+ `apis/user/list.ts`:
277
+
278
+ ```typescript
279
+ import type { ApiRoute } from 'befly-core/types';
280
+
281
+ export default {
282
+ name: '用户列表',
283
+ method: 'POST',
284
+ auth: true,
285
+ permission: 'user:list',
286
+ fields: {
287
+ page: { name: '页码', type: 'number', min: 1, default: 1 },
288
+ limit: { name: '每页数量', type: 'number', min: 1, max: 100, default: 20 },
289
+ keyword: { name: '关键词', type: 'string', max: 50 },
290
+ state: { name: '状态', type: 'number', min: -1, max: 1 },
291
+ role: { name: '角色', type: 'string', max: 20 }
292
+ },
293
+ handler: async (befly, ctx) => {
294
+ const { page, limit, keyword, state, role } = ctx.body;
295
+
296
+ // 构建查询条件
297
+ const where: Record<string, any> = {};
298
+
299
+ if (keyword) {
300
+ where.$or = [{ email: { $like: `%${keyword}%` } }, { nickname: { $like: `%${keyword}%` } }, { phone: { $like: `%${keyword}%` } }];
301
+ }
302
+
303
+ if (state !== undefined) {
304
+ where.state = state;
305
+ }
306
+
307
+ if (role) {
308
+ where.role = role;
309
+ }
310
+
311
+ const result = await befly.db.getList({
312
+ table: 'user',
313
+ columns: ['id', 'email', 'nickname', 'avatar', 'phone', 'role', 'state', 'loginCount', 'lastLoginAt', 'createdAt'],
314
+ where: where,
315
+ page: page || 1,
316
+ limit: limit || 20,
317
+ orderBy: { id: 'desc' }
318
+ });
319
+
320
+ return Yes('获取成功', result);
321
+ }
322
+ } as ApiRoute;
323
+ ```
324
+
325
+ ---
326
+
327
+ ## 文章管理模块
328
+
329
+ 一个完整的文章管理模块,包含发布、编辑、删除、列表、详情。
330
+
331
+ ### 表定义
332
+
333
+ `tables/article.json`:
334
+
335
+ ```json
336
+ {
337
+ "title": "标题|string|2|200||true",
338
+ "content": "内容|text|0|100000||true",
339
+ "summary": "摘要|string|0|500",
340
+ "cover": "封面|string|0|500",
341
+ "category_id": "分类ID|number|0||",
342
+ "tags": "标签|array_string|0|10|[]",
343
+ "author_id": "作者ID|number|1||true",
344
+ "view_count": "浏览量|number|0||0",
345
+ "like_count": "点赞量|number|0||0",
346
+ "comment_count": "评论量|number|0||0",
347
+ "is_top": "是否置顶|number|0|1|0",
348
+ "is_recommend": "是否推荐|number|0|1|0",
349
+ "published_at": "发布时间|number|0||"
350
+ }
351
+ ```
352
+
353
+ `tables/category.json`:
354
+
355
+ ```json
356
+ {
357
+ "name": "分类名|string|2|50||true",
358
+ "slug": "别名|string|2|50||true",
359
+ "description": "描述|string|0|200",
360
+ "parent_id": "父分类|number|0||0",
361
+ "sort": "排序|number|0|9999|0",
362
+ "article_count": "文章数|number|0||0"
363
+ }
364
+ ```
365
+
366
+ ### 发布文章
367
+
368
+ `apis/article/create.ts`:
369
+
370
+ ```typescript
371
+ import type { ApiRoute } from 'befly-core/types';
372
+
373
+ export default {
374
+ name: '发布文章',
375
+ method: 'POST',
376
+ auth: true,
377
+ fields: {
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
+ },
385
+ required: ['title', 'content'],
386
+ handler: async (befly, ctx) => {
387
+ const { title, content, summary, cover, categoryId, tags } = ctx.body;
388
+
389
+ // 自动生成摘要
390
+ const autoSummary = summary || content.replace(/<[^>]+>/g, '').slice(0, 200);
391
+
392
+ const result = await befly.db.insData({
393
+ table: 'article',
394
+ data: {
395
+ title: title,
396
+ content: content,
397
+ summary: autoSummary,
398
+ cover: cover || '',
399
+ categoryId: categoryId || 0,
400
+ tags: tags || [],
401
+ authorId: ctx.user.userId,
402
+ publishedAt: Date.now()
403
+ }
404
+ });
405
+
406
+ // 更新分类文章数
407
+ if (categoryId) {
408
+ await befly.db.updData({
409
+ table: 'category',
410
+ data: { articleCount: { $incr: 1 } },
411
+ where: { id: categoryId }
412
+ });
413
+ }
414
+
415
+ return Yes('发布成功', { id: result.insertId });
416
+ }
417
+ } as ApiRoute;
418
+ ```
419
+
420
+ ### 编辑文章
421
+
422
+ `apis/article/update.ts`:
423
+
424
+ ```typescript
425
+ import type { ApiRoute } from 'befly-core/types';
426
+
427
+ export default {
428
+ name: '编辑文章',
429
+ method: 'POST',
430
+ auth: true,
431
+ fields: {
432
+ id: { name: '文章ID', type: 'number', min: 1 },
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
+ },
440
+ required: ['id'],
441
+ handler: async (befly, ctx) => {
442
+ const { id, title, content, summary, cover, categoryId, tags } = ctx.body;
443
+
444
+ // 检查文章是否存在
445
+ const article = await befly.db.getDetail({
446
+ table: 'article',
447
+ columns: ['id', 'authorId', 'categoryId'],
448
+ where: { id: id }
449
+ });
450
+
451
+ if (!article?.id) {
452
+ return No('文章不存在');
453
+ }
454
+
455
+ // 检查权限(只能编辑自己的文章,管理员除外)
456
+ if (article.authorId !== ctx.user.userId && ctx.user.role !== 'admin') {
457
+ return No('没有权限编辑此文章');
458
+ }
459
+
460
+ const updateData: Record<string, any> = {};
461
+ if (title !== undefined) updateData.title = title;
462
+ if (content !== undefined) updateData.content = content;
463
+ if (summary !== undefined) updateData.summary = summary;
464
+ if (cover !== undefined) updateData.cover = cover;
465
+ if (categoryId !== undefined) updateData.categoryId = categoryId;
466
+ if (tags !== undefined) updateData.tags = tags;
467
+
468
+ if (Object.keys(updateData).length === 0) {
469
+ return No('没有需要更新的字段');
470
+ }
471
+
472
+ await befly.db.updData({
473
+ table: 'article',
474
+ data: updateData,
475
+ where: { id: id }
476
+ });
477
+
478
+ // 更新分类文章数(如果分类变更)
479
+ if (categoryId !== undefined && categoryId !== article.categoryId) {
480
+ if (article.categoryId) {
481
+ await befly.db.updData({
482
+ table: 'category',
483
+ data: { articleCount: { $decr: 1 } },
484
+ where: { id: article.categoryId }
485
+ });
486
+ }
487
+ if (categoryId) {
488
+ await befly.db.updData({
489
+ table: 'category',
490
+ data: { articleCount: { $incr: 1 } },
491
+ where: { id: categoryId }
492
+ });
493
+ }
494
+ }
495
+
496
+ return Yes('更新成功');
497
+ }
498
+ } as ApiRoute;
499
+ ```
500
+
501
+ ### 删除文章
502
+
503
+ `apis/article/delete.ts`:
504
+
505
+ ```typescript
506
+ import type { ApiRoute } from 'befly-core/types';
507
+
508
+ export default {
509
+ name: '删除文章',
510
+ method: 'POST',
511
+ auth: true,
512
+ fields: {
513
+ id: { name: '文章ID', type: 'number', min: 1 }
514
+ },
515
+ required: ['id'],
516
+ handler: async (befly, ctx) => {
517
+ const article = await befly.db.getDetail({
518
+ table: 'article',
519
+ columns: ['id', 'authorId', 'categoryId'],
520
+ where: { id: ctx.body.id }
521
+ });
522
+
523
+ if (!article?.id) {
524
+ return No('文章不存在');
525
+ }
526
+
527
+ // 检查权限
528
+ if (article.authorId !== ctx.user.userId && ctx.user.role !== 'admin') {
529
+ return No('没有权限删除此文章');
530
+ }
531
+
532
+ // 软删除
533
+ await befly.db.delData({
534
+ table: 'article',
535
+ where: { id: ctx.body.id }
536
+ });
537
+
538
+ // 更新分类文章数
539
+ if (article.categoryId) {
540
+ await befly.db.updData({
541
+ table: 'category',
542
+ data: { articleCount: { $decr: 1 } },
543
+ where: { id: article.categoryId }
544
+ });
545
+ }
546
+
547
+ return Yes('删除成功');
548
+ }
549
+ } as ApiRoute;
550
+ ```
551
+
552
+ ### 文章列表
553
+
554
+ `apis/article/list.ts`:
555
+
556
+ ```typescript
557
+ import type { ApiRoute } from 'befly-core/types';
558
+
559
+ export default {
560
+ name: '文章列表',
561
+ method: 'POST',
562
+ auth: false,
563
+ fields: {
564
+ page: { name: '页码', type: 'number', min: 1, default: 1 },
565
+ limit: { name: '每页数量', type: 'number', min: 1, max: 50, default: 10 },
566
+ categoryId: { name: '分类', type: 'number', min: 0 },
567
+ authorId: { name: '作者', type: 'number', min: 0 },
568
+ keyword: { name: '关键词', type: 'string', max: 50 },
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
+ },
573
+ handler: async (befly, ctx) => {
574
+ const { page, limit, categoryId, authorId, keyword, isTop, isRecommend, orderBy } = ctx.body;
575
+
576
+ const where: Record<string, any> = { state: 1 };
577
+
578
+ if (categoryId) where.categoryId = categoryId;
579
+ if (authorId) where.authorId = authorId;
580
+ if (isTop !== undefined) where.isTop = isTop;
581
+ if (isRecommend !== undefined) where.isRecommend = isRecommend;
582
+
583
+ if (keyword) {
584
+ where.$or = [{ title: { $like: `%${keyword}%` } }, { summary: { $like: `%${keyword}%` } }];
585
+ }
586
+
587
+ // 排序
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
+
592
+ const result = await befly.db.getList({
593
+ table: 'article',
594
+ columns: ['id', 'title', 'summary', 'cover', 'categoryId', 'tags', 'authorId', 'viewCount', 'likeCount', 'commentCount', 'isTop', 'isRecommend', 'publishedAt'],
595
+ where: where,
596
+ page: page || 1,
597
+ limit: limit || 10,
598
+ orderBy: order
599
+ });
600
+
601
+ return Yes('获取成功', result);
602
+ }
603
+ } as ApiRoute;
604
+ ```
605
+
606
+ ### 文章详情
607
+
608
+ `apis/article/detail.ts`:
609
+
610
+ ```typescript
611
+ import type { ApiRoute } from 'befly-core/types';
612
+
613
+ export default {
614
+ name: '文章详情',
615
+ method: 'GET',
616
+ auth: false,
617
+ fields: {
618
+ id: { name: '文章ID', type: 'number', min: 1 }
619
+ },
620
+ required: ['id'],
621
+ handler: async (befly, ctx) => {
622
+ const article = await befly.db.getDetail({
623
+ table: 'article',
624
+ where: { id: ctx.body.id, state: 1 }
625
+ });
626
+
627
+ if (!article?.id) {
628
+ return No('文章不存在');
629
+ }
630
+
631
+ // 增加浏览量
632
+ await befly.db.updData({
633
+ table: 'article',
634
+ data: { viewCount: { $incr: 1 } },
635
+ where: { id: ctx.body.id }
636
+ });
637
+
638
+ // 获取作者信息
639
+ const author = await befly.db.getDetail({
640
+ table: 'user',
641
+ columns: ['id', 'nickname', 'avatar'],
642
+ where: { id: article.authorId }
643
+ });
644
+
645
+ // 获取分类信息
646
+ let category = null;
647
+ if (article.categoryId) {
648
+ category = await befly.db.getDetail({
649
+ table: 'category',
650
+ columns: ['id', 'name', 'slug'],
651
+ where: { id: article.categoryId }
652
+ });
653
+ }
654
+
655
+ return Yes('获取成功', {
656
+ ...article,
657
+ viewCount: article.viewCount + 1,
658
+ author: author,
659
+ category: category
660
+ });
661
+ }
662
+ } as ApiRoute;
663
+ ```
664
+
665
+ ---
666
+
667
+ ## 文件上传
668
+
669
+ ### 单文件上传
670
+
671
+ `apis/common/upload.ts`:
672
+
673
+ ```typescript
674
+ import { join } from 'pathe';
675
+ import { existsSync, mkdirSync } from 'node:fs';
676
+ import type { ApiRoute } from 'befly-core/types';
677
+
678
+ export default {
679
+ name: '文件上传',
680
+ method: 'POST',
681
+ auth: true,
682
+ handler: async (befly, ctx) => {
683
+ const formData = await ctx.req.formData();
684
+ const file = formData.get('file') as File | null;
685
+
686
+ if (!file) {
687
+ return No('请选择文件');
688
+ }
689
+
690
+ // 检查文件大小(10MB)
691
+ if (file.size > 10 * 1024 * 1024) {
692
+ return No('文件大小不能超过 10MB');
693
+ }
694
+
695
+ // 检查文件类型
696
+ const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
697
+ if (!allowedTypes.includes(file.type)) {
698
+ return No('只支持 jpg/png/gif/webp 格式');
699
+ }
700
+
701
+ // 生成文件名
702
+ const ext = file.name.split('.').pop();
703
+ const fileName = `${Date.now()}_${befly.cipher.randomString(8)}.${ext}`;
704
+
705
+ // 保存目录
706
+ const uploadDir = join(process.cwd(), 'uploads', new Date().toISOString().slice(0, 7));
707
+ if (!existsSync(uploadDir)) {
708
+ mkdirSync(uploadDir, { recursive: true });
709
+ }
710
+
711
+ // 保存文件
712
+ const filePath = join(uploadDir, fileName);
713
+ const buffer = await file.arrayBuffer();
714
+ await Bun.write(filePath, buffer);
715
+
716
+ // 返回 URL
717
+ const url = `/uploads/${new Date().toISOString().slice(0, 7)}/${fileName}`;
718
+
719
+ return Yes('上传成功', {
720
+ url: url,
721
+ name: file.name,
722
+ size: file.size,
723
+ type: file.type
724
+ });
725
+ }
726
+ } as ApiRoute;
727
+ ```
728
+
729
+ ---
730
+
731
+ ## 数据导出
732
+
733
+ ### 导出为 CSV
734
+
735
+ `apis/user/export.ts`:
736
+
737
+ ```typescript
738
+ import type { ApiRoute } from 'befly-core/types';
739
+
740
+ export default {
741
+ name: '导出用户',
742
+ method: 'GET',
743
+ auth: true,
744
+ permission: 'user:export',
745
+ handler: async (befly, ctx) => {
746
+ // 获取所有用户
747
+ const result = await befly.db.getList({
748
+ table: 'user',
749
+ columns: ['id', 'email', 'nickname', 'phone', 'role', 'state', 'createdAt'],
750
+ where: { state: { $gte: 0 } },
751
+ page: 1,
752
+ limit: 10000
753
+ });
754
+
755
+ // 生成 CSV
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
+
759
+ const csv = [headers.join(','), ...rows.map((row: any[]) => row.map((cell) => `"${cell}"`).join(','))].join('\n');
760
+
761
+ // 返回文件
762
+ return new Response(csv, {
763
+ headers: {
764
+ 'Content-Type': 'text/csv; charset=utf-8',
765
+ 'Content-Disposition': `attachment; filename="users_${Date.now()}.csv"`
766
+ }
767
+ });
768
+ }
769
+ } as ApiRoute;
770
+ ```
771
+
772
+ ---
773
+
774
+ ## 完整目录结构
775
+
776
+ ```
777
+ apis/
778
+ ├── user/
779
+ │ ├── register.ts # 注册
780
+ │ ├── login.ts # 登录
781
+ │ ├── info.ts # 获取信息
782
+ │ ├── update.ts # 更新信息
783
+ │ ├── changePassword.ts # 修改密码
784
+ │ ├── list.ts # 用户列表
785
+ │ └── export.ts # 导出用户
786
+ ├── article/
787
+ │ ├── create.ts # 发布文章
788
+ │ ├── update.ts # 编辑文章
789
+ │ ├── delete.ts # 删除文章
790
+ │ ├── list.ts # 文章列表
791
+ │ └── detail.ts # 文章详情
792
+ └── common/
793
+ └── upload.ts # 文件上传
794
+
795
+ tables/
796
+ ├── user.json # 用户表
797
+ ├── article.json # 文章表
798
+ └── category.json # 分类表
799
+ ```