@kmlckj/licos-ai-cli 1.3.6 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,761 +1,763 @@
1
- # LICOS Mini Program
2
-
3
- 这是一个基于 [Taro 4](https://docs.taro.zone/docs/) + [Nest.js](https://nestjs.com/) 的前后端分离项目,由LICOS CLI 创建。
4
-
5
- ## 技术栈
6
-
7
- - **整体框架**: Taro 4.1.9
8
- - **语言**: TypeScript 5.4.5
9
- - **渲染**: React 18.0.0
10
- - **样式**: TailwindCSS 4.1.18
11
- - **Tailwind 适配层**: weapp-tailwindcss 4.9.2
12
- - **状态管理**: Zustand 5.0.9
13
- - **图标库**: lucide-react-taro latest
14
- - **工程化**: Vite 4.2.0
15
- - **包管理**: pnpm
16
- - **运行时**: Node.js >= 18
17
- - **服务端**: NestJS 10.4.15
18
- - **数据库 ORM**: Drizzle ORM 0.45.1
19
- - **类型校验**: Zod 4.3.5
20
-
21
- ## 项目结构
22
-
23
- ```
24
- ├── .licosproj/ # LICOS 平台配置
25
- │ └── scripts/ # 构建和运行脚本
26
- ├── config/ # Taro 构建配置
27
- │ ├── index.ts # 主配置文件
28
- │ ├── dev.ts # 开发环境配置
29
- │ └── prod.ts # 生产环境配置
30
- ├── server/ # NestJS 后端服务
31
- │ └── src/
32
- │ ├── main.ts # 服务入口
33
- │ ├── app.module.ts # 根模块
34
- │ ├── app.controller.ts # 应用控制器
35
- │ └── app.service.ts # 应用服务
36
- ├── src/ # 前端源码
37
- │ ├── pages/ # 页面组件
38
- │ ├── presets/ # 框架预置逻辑(无需读取,如无必要不改动)
39
- │ ├── utils/ # 工具函数
40
- │ ├── network.ts # 封装好的网络请求工具
41
- │ ├── app.ts # 应用入口
42
- │ ├── app.config.ts # 应用配置
43
- │ └── app.css # 全局样式
44
- ├── types/ # TypeScript 类型定义
45
- ├── key/ # 小程序密钥(CI 上传用)
46
- ├── .env.local # 环境变量
47
- └── project.config.json # 微信小程序项目配置
48
- ```
49
-
50
- ## 快速开始
51
-
52
- ### 安装依赖
53
-
54
- ```bash
55
- pnpm install --registry=https://registry.npmmirror.com
56
- ```
57
-
58
- ### 本地开发
59
-
60
- 同时启动 H5 前端和 NestJS 后端:
61
-
62
- ```bash
63
- pnpm dev
64
- ```
65
-
66
- - 前端地址:http://localhost:5000
67
- - 后端地址:http://localhost:3000
68
-
69
- 单独启动:
70
-
71
- ```bash
72
- pnpm dev:web # 仅 H5 前端
73
- pnpm dev:weapp # 仅微信小程序
74
- pnpm dev:server # 仅后端服务
75
- ```
76
-
77
- ### 构建
78
-
79
- ```bash
80
- pnpm build # 构建部署产物(H5 + 后端)
81
- pnpm build:all # 构建所有(H5 + 小程序 + 后端)
82
- pnpm build:web # 仅构建 H5,输出到 dist-web
83
- pnpm build:weapp # 仅构建微信小程序,输出到 dist
84
- pnpm build:server # 仅构建后端
85
- ```
86
-
87
- ### 预览小程序
88
-
89
- ```bash
90
- pnpm preview:weapp # 构建并生成预览小程序二维码
91
- ```
92
-
93
- ## 前端核心开发规范
94
-
95
- ### 新建页面流程
96
-
97
- 1. 在 \`src/pages/\` 下创建页面目录
98
- 2. 创建 \`index.tsx\`(页面组件)
99
- 3. 创建 \`index.config.ts\`(页面配置)
100
- 4. 创建 \`index.css\`(页面样式,可选)
101
- 5. 在 \`src/app.config.ts\` 的 \`pages\` 数组中注册页面路径
102
-
103
- 或使用 Taro 脚手架命令:
104
-
105
- ```bash
106
- pnpm new # 交互式创建页面/组件
107
- ```
108
-
109
- ### 组件库
110
-
111
- #### UI 组件
112
-
113
- UI 组件位于 `@/components/ui`,推荐按需引入:
114
-
115
- ```typescript
116
- import { Button } from '@/components/ui/button'
117
- import { Card, CardContent } from '@/components/ui/card'
118
- ```
119
-
120
- UI 组件列表:
121
-
122
- Accordion,Alert,AlertDialog,AspectRatio,Avatar,Badge,Breadcrumb,Button,ButtonGroup,Calendar,Card,Carousel,Checkbox,CodeBlock,Collapsible,Command,ContextMenu,Dialog,Drawer,DropdownMenu,Field,HoverCard,Input,InputGroup,InputOTP,Label,Menubar,NavigationMenu,Pagination,Popover,Portal,Progress,RadioGroup,Resizable,ScrollArea,Select,Separator,Sheet,Skeleton,Slider,Sonner,Switch,Table,Tabs,Textarea,Toast,Toggle,ToggleGroup,Tooltip
123
-
124
- #### Taro 原生组件
125
-
126
- 可以使用的 Taro 组件(UI 未覆盖)
127
-
128
- ```typescript
129
- import { View, Text, Icon, Image } from '@tarojs/components'
130
- ```
131
-
132
- Taro 原生组件列表:
133
-
134
- Text,Icon,RichText,CheckboxGroup,Editor,Form,Picker,PickerView,PickerViewColumn,Radio,FunctionalPageNavigator,NavigationBar,Navigator,TabItem,Camera,Image,Video,ScrollView,Swiper,SwiperItem,View
135
-
136
- ### 路径别名
137
-
138
- 项目配置了 `@/*` 路径别名指向 `src/*`:
139
-
140
- ```typescript
141
- import { SomeComponent } from '@/components/some-component'
142
- import { useUserStore } from '@/stores/user'
143
- ```
144
-
145
- ### 代码模板
146
-
147
- #### 页面组件 (TypeScript + React)
148
-
149
- ```tsx
150
- // src/pages/example/index.tsx
151
- import { View } from '@tarojs/components'
152
- import { useLoad, useDidShow } from '@tarojs/taro'
153
- import type { FC } from 'react'
154
- import { Button } from '@/components/ui/button'
155
- import {
156
- Card,
157
- CardContent,
158
- CardDescription,
159
- CardFooter,
160
- CardHeader,
161
- CardTitle,
162
- } from '@/components/ui/card'
163
- import './index.css'
164
-
165
- const ExamplePage: FC = () => {
166
- useLoad(() => {
167
- console.log('Page loaded.')
168
- })
169
-
170
- useDidShow(() => {
171
- console.log('Page showed.')
172
- })
173
-
174
- return (
175
- <View className="p-4">
176
- <Card>
177
- <CardHeader>
178
- <CardTitle>Hello Taro!</CardTitle>
179
- <CardDescription>
180
- 页面布局用 Taro 基础组件,交互与视觉优先用项目内置 UI 组件。
181
- </CardDescription>
182
- </CardHeader>
183
- <CardContent>
184
- <View className="text-sm text-muted-foreground">
185
- 组件位于 src/components/ui,推荐按需从 @/components/ui/* 引入。
186
- </View>
187
- </CardContent>
188
- <CardFooter className="justify-end">
189
- <Button size="sm" onClick={() => console.log('clicked')}>
190
- 点击
191
- </Button>
192
- </CardFooter>
193
- </Card>
194
- </View>
195
- )
196
- }
197
-
198
- export default ExamplePage
199
- ```
200
-
201
- #### 页面配置
202
-
203
- ```typescript
204
- // src/pages/example/index.config.ts
205
- import { definePageConfig } from '@tarojs/taro'
206
-
207
- export default definePageConfig({
208
- navigationBarTitleText: '示例页面',
209
- enablePullDownRefresh: true,
210
- backgroundTextStyle: 'dark',
211
- })
212
- ```
213
-
214
- #### 应用配置
215
-
216
- ```typescript
217
- // src/app.config.ts
218
- import { defineAppConfig } from '@tarojs/taro'
219
-
220
- export default defineAppConfig({
221
- pages: [
222
- 'pages/index/index',
223
- 'pages/example/index',
224
- ],
225
- window: {
226
- backgroundTextStyle: 'light',
227
- navigationBarBackgroundColor: '#fff',
228
- navigationBarTitleText: 'App',
229
- navigationBarTextStyle: 'black',
230
- },
231
- // TabBar 配置 (可选)
232
- // tabBar: {
233
- // list: [
234
- // { pagePath: 'pages/index/index', text: '首页' },
235
- // ],
236
- // },
237
- })
238
- ```
239
-
240
- ### 发送请求
241
-
242
- **IMPORTANT: 禁止直接使用 Taro.request、Taro.uploadFile、Taro.downloadFile,使用 Network.request、Network.uploadFile、Network.downloadFile 替代。**
243
-
244
- Network 是对 Taro.request、Taro.uploadFile、Taro.downloadFile 的封装,自动添加项目域名前缀,参数与 Taro 一致。
245
-
246
- ✅ 正确使用方式
247
-
248
- ```typescript
249
- import { Network } from '@/network'
250
-
251
- // GET 请求
252
- const data = await Network.request({
253
- url: '/api/hello'
254
- })
255
-
256
- // POST 请求
257
- const result = await Network.request({
258
- url: '/api/user/login',
259
- method: 'POST',
260
- data: { username, password }
261
- })
262
-
263
- // 文件上传
264
- await Network.uploadFile({
265
- url: '/api/upload',
266
- filePath: tempFilePath,
267
- name: 'file'
268
- })
269
-
270
- // 文件下载
271
- await Network.downloadFile({
272
- url: '/api/download/file.pdf'
273
- })
274
- ```
275
-
276
- ❌ 错误用法
277
-
278
- ```typescript
279
- import Taro from '@tarojs/taro'
280
-
281
- // ❌ 会导致自动域名拼接无法生效,除非是特殊指定域名
282
- const data = await Network.request({
283
- url: 'http://localhost/api/hello'
284
- })
285
-
286
- // ❌ 不要直接使用 Taro.request
287
- await Taro.request({ url: '/api/hello' })
288
-
289
- // ❌ 不要直接使用 Taro.uploadFile
290
- await Taro.uploadFile({ url: '/api/upload', filePath, name: 'file' })
291
- ```
292
-
293
- ### Zustand 状态管理
294
-
295
- ```typescript
296
- // src/stores/user.ts
297
- import { create } from 'zustand'
298
-
299
- interface UserState {
300
- userInfo: UserInfo | null
301
- token: string
302
- setUserInfo: (info: UserInfo) => void
303
- setToken: (token: string) => void
304
- logout: () => void
305
- }
306
-
307
- interface UserInfo {
308
- id: string
309
- name: string
310
- avatar: string
311
- }
312
-
313
- export const useUserStore = create<UserState>((set) => ({
314
- userInfo: null,
315
- token: '',
316
- setUserInfo: (info) => set({ userInfo: info }),
317
- setToken: (token) => set({ token }),
318
- logout: () => set({ userInfo: null, token: '' }),
319
- }))
320
- ```
321
-
322
- ### Taro 生命周期 Hooks
323
-
324
- ```typescript
325
- import {
326
- useLoad, // 页面加载 (onLoad)
327
- useReady, // 页面初次渲染完成 (onReady)
328
- useDidShow, // 页面显示 (onShow)
329
- useDidHide, // 页面隐藏 (onHide)
330
- usePullDownRefresh, // 下拉刷新 (onPullDownRefresh)
331
- useReachBottom, // 触底加载 (onReachBottom)
332
- useShareAppMessage, // 分享 (onShareAppMessage)
333
- useRouter, // 获取路由参数
334
- } from '@tarojs/taro'
335
- ```
336
-
337
- ### 路由导航
338
-
339
- ```typescript
340
- import Taro from '@tarojs/taro'
341
-
342
- // 保留当前页面,跳转到新页面
343
- Taro.navigateTo({ url: '/pages/detail/index?id=1' })
344
-
345
- // 关闭当前页面,跳转到新页面
346
- Taro.redirectTo({ url: '/pages/detail/index' })
347
-
348
- // 跳转到 tabBar 页面
349
- Taro.switchTab({ url: '/pages/index/index' })
350
-
351
- // 返回上一页
352
- Taro.navigateBack({ delta: 1 })
353
-
354
- // 获取路由参数
355
- const router = useRouter()
356
- const { id } = router.params
357
- ```
358
-
359
- ### 图标使用 (lucide-react-taro)
360
-
361
- **IMPORTANT: 禁止使用 lucide-react,必须使用 lucide-react-taro 替代。**
362
-
363
- lucide-react-taro 是 Lucide 图标库的 Taro 适配版本,专为小程序环境优化,API 与 lucide-react 一致:
364
-
365
- ```tsx
366
- import { View } from '@tarojs/components'
367
- import { House, Settings, User, Search, Camera, Zap } from 'lucide-react-taro'
368
-
369
- const IconDemo = () => {
370
- return (
371
- <View className="flex gap-4">
372
- {/* 基本用法 */}
373
- <House />
374
- {/* 自定义尺寸和颜色 */}
375
- <Settings size={32} color="#1890ff" />
376
- {/* 自定义描边宽度 */}
377
- <User size={24} strokeWidth={1.5} />
378
- {/* 绝对描边宽度(描边不随 size 缩放) */}
379
- <Camera size={48} strokeWidth={2} absoluteStrokeWidth />
380
- {/* 组合使用 */}
381
- <Zap size={32} color="#ff6b00" strokeWidth={1.5} className="my-icon" />
382
- </View>
383
- )
384
- }
385
- ```
386
-
387
- 常用属性:
388
- - `size` - 图标大小(默认 24)
389
- - `color` - 图标颜色(默认 currentColor,小程序中建议显式设置)
390
- - `strokeWidth` - 线条粗细(默认 2)
391
- - `absoluteStrokeWidth` - 绝对描边宽度,启用后描边不随 size 缩放
392
- - `className` / `style` - 自定义样式
393
-
394
- 更多图标请访问:https://lucide.dev/icons
395
-
396
- ### TabBar 图标生成 (CLI 工具)
397
-
398
- **IMPORTANT: 微信小程序的 TabBar 不支持 base64 或 SVG 图片,必须使用本地 PNG 文件。**
399
-
400
- lucide-react-taro 提供了 CLI 工具来生成 TabBar 所需的 PNG 图标:
401
-
402
- ```bash
403
- # 生成带选中状态的图标
404
- pnpm exec taro-lucide-tabbar House Settings User -c "#999999" -a "#1890ff"
405
-
406
- # 指定输出目录和尺寸
407
- pnpm exec taro-lucide-tabbar House Settings User -c "#999999" -a "#1890ff" -o ./src/assets/tabbar -s 81
408
- ```
409
-
410
- CLI 参数:
411
- - `--color, -c` (默认 #000000): 图标颜色
412
- - `--active-color, -a`: 选中状态颜色
413
- - `--size, -s` (默认 81): 图标尺寸
414
- - `--output, -o` (默认 ./tabbar-icons): 输出目录
415
- - `--stroke-width` (默认 2): 描边宽度
416
-
417
- 在 `app.config.ts` 中使用生成的图标:
418
-
419
- > IMPORTANT:iconPath 和 selectedIconPath 必须以 `./` 开头,否则图标无法渲染
420
-
421
- ```typescript
422
- export default defineAppConfig({
423
- tabBar: {
424
- color: '#999999',
425
- selectedColor: '#1890ff',
426
- backgroundColor: '#ffffff',
427
- borderStyle: 'black',
428
- list: [
429
- {
430
- pagePath: 'pages/index/index',
431
- text: '首页',
432
- iconPath: './assets/tabbar/house.png',
433
- selectedIconPath: './assets/tabbar/house-active.png',
434
- },
435
- {
436
- pagePath: 'pages/settings/index',
437
- text: '设置',
438
- iconPath: './assets/tabbar/settings.png',
439
- selectedIconPath: './assets/tabbar/settings-active.png',
440
- },
441
- {
442
- pagePath: 'pages/user/index',
443
- text: '用户',
444
- iconPath: './assets/tabbar/user.png',
445
- selectedIconPath: './assets/tabbar/user-active.png',
446
- },
447
- ],
448
- },
449
- })
450
-
451
- ### Tailwind CSS 样式开发
452
-
453
- IMPORTANT:必须使用 tailwindcss 实现样式,只有在必要情况下才能 fallback 到 css / less
454
-
455
- > 项目已集成 Tailwind CSS 4.x + weapp-tailwindcss,支持跨端原子化样式:
456
-
457
- ```tsx
458
- import { View, Text } from '@tarojs/components'
459
- import { Button } from '@/components/ui/button'
460
-
461
- <View className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
462
- <Text className="text-2xl font-bold text-blue-600 mb-4">标题</Text>
463
- <View className="w-full px-4">
464
- <Button className="w-full" size="lg">
465
- 按钮
466
- </Button>
467
- </View>
468
- </View>
469
- ```
470
-
471
- ### 性能优化
472
-
473
- #### 图片懒加载
474
-
475
- ```tsx
476
- import { Image } from '@tarojs/components'
477
-
478
- <Image src={imageUrl} lazyLoad mode="aspectFill" />
479
- ```
480
-
481
- #### 虚拟列表
482
-
483
- ```tsx
484
- import { VirtualList } from '@tarojs/components'
485
-
486
- <VirtualList
487
- height={500}
488
- itemData={list}
489
- itemCount={list.length}
490
- itemSize={100}
491
- renderItem={({ index, style, data }) => (
492
- <View style={style}>{data[index].name}</View>
493
- )}
494
- />
495
- ```
496
-
497
- #### 分包加载
498
-
499
- ```typescript
500
- // src/app.config.ts
501
- export default defineAppConfig({
502
- pages: ['pages/index/index'],
503
- subPackages: [
504
- {
505
- root: 'packageA',
506
- pages: ['pages/detail/index'],
507
- },
508
- ],
509
- })
510
- ```
511
-
512
- ### 小程序限制
513
-
514
- | 限制项 | 说明 |
515
- | -------- | ---------------------------------------- |
516
- | 主包体积 | ≤ 2MB |
517
- | 总包体积 | ≤ 20MB |
518
- | 域名配置 | 生产环境需在小程序后台配置合法域名 |
519
- | 本地开发 | 需在微信开发者工具开启「不校验合法域名」 |
520
-
521
- ### 权限配置
522
-
523
- ```typescript
524
- // src/app.config.ts
525
- export default defineAppConfig({
526
- // ...其他配置
527
- permission: {
528
- 'scope.userLocation': {
529
- desc: '你的位置信息将用于小程序位置接口的效果展示'
530
- }
531
- },
532
- requiredPrivateInfos: ['getLocation', 'chooseAddress']
533
- })
534
- ```
535
-
536
- ### 位置服务
537
-
538
- ```typescript
539
- // 需先在 app.config.ts 中配置 permission
540
- async function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
541
- return await Taro.getLocation({ type: 'gcj02' })
542
- }
543
- ```
544
-
545
- ## 后端核心开发规范
546
-
547
- 本项目后端基于 NestJS + TypeScript 构建,提供高效、可扩展的服务端能力。
548
-
549
- ### 项目结构
550
-
551
- ```sh
552
- .
553
- ├── server/ # NestJS 后端服务
554
- │ └── src/
555
- │ ├── main.ts # 服务入口
556
- │ ├── app.module.ts # 根模块
557
- │ ├── app.controller.ts # 根控制器
558
- │ └── app.service.ts # 根服务
559
- ```
560
-
561
- ### 开发命令
562
-
563
- ```sh
564
- pnpm dev:server // 启动开发服务 (热重载, 默认端口 3000)
565
- pnpm build:server // 构建生产版本
566
- ```
567
-
568
- ### 新建模块流程 (CLI)
569
-
570
- 快速生成样板代码:
571
-
572
- ```bash
573
- cd server
574
-
575
- # 生成完整的 CRUD 资源 (包含 Module, Controller, Service, DTO, Entity)
576
- pnpm exec nest g resource modules/product
577
-
578
- # 仅生成特定部分
579
- pnpm exec nest g module modules/order
580
- pnpm exec nest g controller modules/order
581
- pnpm exec nest g service modules/order
582
- ```
583
-
584
- ### 环境变量配置
585
-
586
- 在 server/ 根目录创建 .env 文件:
587
-
588
- ```sh
589
- ## 服务端口
590
- PORT=3000
591
-
592
- ## 微信小程序配置
593
- WX_APP_ID=你的AppID
594
- WX_APP_SECRET=你的AppSecret
595
- ```
596
-
597
- 在代码中使用 @nestjs/config 读取环境变量:
598
-
599
- ```typescript
600
- import { ConfigService } from '@nestjs/config';
601
-
602
- // 在 Service 中注入
603
- constructor(private configService: ConfigService) {}
604
-
605
- getWxConfig() {
606
- return {
607
- appId: this.configService.get<string>('WX_APP_ID'),
608
- secret: this.configService.get<string>('WX_APP_SECRET'),
609
- };
610
- }
611
- ```
612
-
613
- ### 标准响应封装
614
-
615
- 建议使用拦截器 (Interceptor) 统一 API 响应格式:
616
-
617
- ```typeScript
618
- // src/common/interceptors/transform.interceptor.ts
619
- import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
620
- import { Observable } from 'rxjs';
621
- import { map } from 'rxjs/operators';
622
-
623
- export interface Response<T> {
624
- code: number;
625
- data: T;
626
- message: string;
627
- }
628
-
629
- @Injectable()
630
- export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
631
- intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
632
- return next.handle().pipe(
633
- map((data) => ({
634
- code: 200,
635
- data,
636
- message: 'success',
637
- })),
638
- );
639
- }
640
- }
641
- ```
642
-
643
- 在 main.ts 中全局注册:
644
-
645
- ```typescript
646
- app.useGlobalInterceptors(new TransformInterceptor());
647
- ```
648
-
649
- ### 微信登录后端实现
650
-
651
- ```typescript
652
- // src/modules/auth/auth.service.ts
653
- import { Injectable, UnauthorizedException } from '@nestjs/common';
654
- import { HttpService } from '@nestjs/axios';
655
- import { ConfigService } from '@nestjs/config';
656
- import { lastValueFrom } from 'rxjs';
657
-
658
- @Injectable()
659
- export class AuthService {
660
- constructor(
661
- private httpService: HttpService,
662
- private configService: ConfigService,
663
- ) {}
664
-
665
- async code2Session(code: string) {
666
- const appId = this.configService.get('WX_APP_ID');
667
- const secret = this.configService.get('WX_APP_SECRET');
668
- const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${secret}&js_code=${code}&grant_type=authorization_code`;
669
-
670
- const { data } = await lastValueFrom(this.httpService.get(url));
671
-
672
- if (data.errcode) {
673
- throw new UnauthorizedException(`微信登录失败: ${data.errmsg}`);
674
- }
675
-
676
- return data; // 包含 openid, session_key
677
- }
678
- }
679
- ```
680
-
681
- ### 异常处理
682
-
683
- 使用全局异常过滤器 (Filter) 统一错误响应:
684
-
685
- ```typescript
686
- // src/common/filters/http-exception.filter.ts
687
- import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
688
- import { Response } from 'express';
689
-
690
- @Catch(HttpException)
691
- export class HttpExceptionFilter implements ExceptionFilter {
692
- catch(exception: HttpException, host: ArgumentsHost) {
693
- const ctx = host.switchToHttp();
694
- const response = ctx.getResponse<Response>();
695
- const status = exception.getStatus();
696
- const exceptionResponse = exception.getResponse();
697
-
698
- response.status(status).json({
699
- code: status,
700
- message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message,
701
- data: null,
702
- });
703
- }
704
- }
705
- ```
706
-
707
- 在 main.ts 中注册:
708
-
709
- ```
710
- app.useGlobalFilters(new HttpExceptionFilter());
711
- ```
712
-
713
- ### 数据库
714
-
715
- 数据库能力统一通过平台 API / SDK 使用,模板不预置本地数据库或数据库直连依赖。
716
-
717
- ### 类型校验 (Zod)
718
-
719
- 项目集成了 [Zod](https://zod.dev/) 用于运行时类型校验。
720
-
721
- #### 定义 Schema
722
-
723
- ```typescript
724
- import { z } from 'zod';
725
-
726
- // 基础类型
727
- const userSchema = z.object({
728
- id: z.number(),
729
- name: z.string().min(1).max(50),
730
- email: z.string().email(),
731
- age: z.number().int().positive().optional(),
732
- });
733
-
734
- // 从 schema 推导 TypeScript 类型
735
- type User = z.infer<typeof userSchema>;
736
- ```
737
-
738
- #### 请求校验
739
-
740
- ```typescript
741
- // src/modules/user/dto/create-user.dto.ts
742
- import { z } from 'zod';
743
-
744
- export const createUserSchema = z.object({
745
- nickname: z.string().min(1, '昵称不能为空').max(20, '昵称最多20个字符'),
746
- avatar: z.string().url('头像必须是有效的URL').optional(),
747
- phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确').optional(),
748
- });
749
-
750
- export type CreateUserDto = z.infer<typeof createUserSchema>;
751
-
752
- // 在 Controller 中使用
753
- @Post()
754
- create(@Body() body: unknown) {
755
- const result = createUserSchema.safeParse(body);
756
- if (!result.success) {
757
- throw new BadRequestException(result.error.errors);
758
- }
759
- return this.userService.create(result.data);
760
- }
761
- ```
1
+ # LICOS Mini Program
2
+
3
+ 这是一个基于 [Taro 4](https://docs.taro.zone/docs/) + [Nest.js](https://nestjs.com/) 的前后端分离项目,由LICOS CLI 创建。
4
+
5
+ ## 技术栈
6
+
7
+ - **整体框架**: Taro 4.2.1
8
+ - **语言**: TypeScript 5.4.5
9
+ - **渲染**: React 18.0.0
10
+ - **样式**: TailwindCSS 4.1.18
11
+ - **Tailwind 适配层**: weapp-tailwindcss 4.9.2
12
+ - **状态管理**: Zustand 5.0.9
13
+ - **图标库**: lucide-react-taro latest
14
+ - **工程化**: Vite 4.2.0
15
+ - **包管理**: pnpm
16
+ - **运行时**: Node.js >= 18
17
+ - **服务端**: NestJS 10.4.15
18
+ - **数据库 ORM**: Drizzle ORM 0.45.1
19
+ - **类型校验**: Zod 4.3.5
20
+
21
+ Linux 构建环境使用 glibc,已验证 Node.js 24 + Debian Bookworm 的 amd64 和 arm64。Taro 原生绑定由通用 `@tarojs/binding` 按平台选择;安装时保留 pnpm 锁文件与可选依赖,不要强制安装某个 x86 平台包。
22
+
23
+ ## 项目结构
24
+
25
+ ```
26
+ ├── .licosproj/ # LICOS 平台配置
27
+ │ └── scripts/ # 构建和运行脚本
28
+ ├── config/ # Taro 构建配置
29
+ │ ├── index.ts # 主配置文件
30
+ │ ├── dev.ts # 开发环境配置
31
+ │ └── prod.ts # 生产环境配置
32
+ ├── server/ # NestJS 后端服务
33
+ │ └── src/
34
+ │ ├── main.ts # 服务入口
35
+ │ ├── app.module.ts # 根模块
36
+ │ ├── app.controller.ts # 应用控制器
37
+ │ └── app.service.ts # 应用服务
38
+ ├── src/ # 前端源码
39
+ │ ├── pages/ # 页面组件
40
+ │ ├── presets/ # 框架预置逻辑(无需读取,如无必要不改动)
41
+ │ ├── utils/ # 工具函数
42
+ │ ├── network.ts # 封装好的网络请求工具
43
+ │ ├── app.ts # 应用入口
44
+ │ ├── app.config.ts # 应用配置
45
+ │ └── app.css # 全局样式
46
+ ├── types/ # TypeScript 类型定义
47
+ ├── key/ # 小程序密钥(CI 上传用)
48
+ ├── .env.local # 环境变量
49
+ └── project.config.json # 微信小程序项目配置
50
+ ```
51
+
52
+ ## 快速开始
53
+
54
+ ### 安装依赖
55
+
56
+ ```bash
57
+ pnpm install --registry=https://registry.npmmirror.com
58
+ ```
59
+
60
+ ### 本地开发
61
+
62
+ 同时启动 H5 前端和 NestJS 后端:
63
+
64
+ ```bash
65
+ pnpm dev
66
+ ```
67
+
68
+ - 前端地址:http://localhost:5000
69
+ - 后端地址:http://localhost:3000
70
+
71
+ 单独启动:
72
+
73
+ ```bash
74
+ pnpm dev:web # 仅 H5 前端
75
+ pnpm dev:weapp # 仅微信小程序
76
+ pnpm dev:server # 仅后端服务
77
+ ```
78
+
79
+ ### 构建
80
+
81
+ ```bash
82
+ pnpm build # 构建部署产物(H5 + 后端)
83
+ pnpm build:all # 构建所有(H5 + 小程序 + 后端)
84
+ pnpm build:web # 仅构建 H5,输出到 dist-web
85
+ pnpm build:weapp # 仅构建微信小程序,输出到 dist
86
+ pnpm build:server # 仅构建后端
87
+ ```
88
+
89
+ ### 预览小程序
90
+
91
+ ```bash
92
+ pnpm preview:weapp # 构建并生成预览小程序二维码
93
+ ```
94
+
95
+ ## 前端核心开发规范
96
+
97
+ ### 新建页面流程
98
+
99
+ 1. 在 \`src/pages/\` 下创建页面目录
100
+ 2. 创建 \`index.tsx\`(页面组件)
101
+ 3. 创建 \`index.config.ts\`(页面配置)
102
+ 4. 创建 \`index.css\`(页面样式,可选)
103
+ 5. 在 \`src/app.config.ts\` 的 \`pages\` 数组中注册页面路径
104
+
105
+ 或使用 Taro 脚手架命令:
106
+
107
+ ```bash
108
+ pnpm new # 交互式创建页面/组件
109
+ ```
110
+
111
+ ### 组件库
112
+
113
+ #### UI 组件
114
+
115
+ UI 组件位于 `@/components/ui`,推荐按需引入:
116
+
117
+ ```typescript
118
+ import { Button } from '@/components/ui/button'
119
+ import { Card, CardContent } from '@/components/ui/card'
120
+ ```
121
+
122
+ UI 组件列表:
123
+
124
+ Accordion,Alert,AlertDialog,AspectRatio,Avatar,Badge,Breadcrumb,Button,ButtonGroup,Calendar,Card,Carousel,Checkbox,CodeBlock,Collapsible,Command,ContextMenu,Dialog,Drawer,DropdownMenu,Field,HoverCard,Input,InputGroup,InputOTP,Label,Menubar,NavigationMenu,Pagination,Popover,Portal,Progress,RadioGroup,Resizable,ScrollArea,Select,Separator,Sheet,Skeleton,Slider,Sonner,Switch,Table,Tabs,Textarea,Toast,Toggle,ToggleGroup,Tooltip
125
+
126
+ #### Taro 原生组件
127
+
128
+ 可以使用的 Taro 组件(UI 未覆盖)
129
+
130
+ ```typescript
131
+ import { View, Text, Icon, Image } from '@tarojs/components'
132
+ ```
133
+
134
+ Taro 原生组件列表:
135
+
136
+ Text,Icon,RichText,CheckboxGroup,Editor,Form,Picker,PickerView,PickerViewColumn,Radio,FunctionalPageNavigator,NavigationBar,Navigator,TabItem,Camera,Image,Video,ScrollView,Swiper,SwiperItem,View
137
+
138
+ ### 路径别名
139
+
140
+ 项目配置了 `@/*` 路径别名指向 `src/*`:
141
+
142
+ ```typescript
143
+ import { SomeComponent } from '@/components/some-component'
144
+ import { useUserStore } from '@/stores/user'
145
+ ```
146
+
147
+ ### 代码模板
148
+
149
+ #### 页面组件 (TypeScript + React)
150
+
151
+ ```tsx
152
+ // src/pages/example/index.tsx
153
+ import { View } from '@tarojs/components'
154
+ import { useLoad, useDidShow } from '@tarojs/taro'
155
+ import type { FC } from 'react'
156
+ import { Button } from '@/components/ui/button'
157
+ import {
158
+ Card,
159
+ CardContent,
160
+ CardDescription,
161
+ CardFooter,
162
+ CardHeader,
163
+ CardTitle,
164
+ } from '@/components/ui/card'
165
+ import './index.css'
166
+
167
+ const ExamplePage: FC = () => {
168
+ useLoad(() => {
169
+ console.log('Page loaded.')
170
+ })
171
+
172
+ useDidShow(() => {
173
+ console.log('Page showed.')
174
+ })
175
+
176
+ return (
177
+ <View className="p-4">
178
+ <Card>
179
+ <CardHeader>
180
+ <CardTitle>Hello Taro!</CardTitle>
181
+ <CardDescription>
182
+ 页面布局用 Taro 基础组件,交互与视觉优先用项目内置 UI 组件。
183
+ </CardDescription>
184
+ </CardHeader>
185
+ <CardContent>
186
+ <View className="text-sm text-muted-foreground">
187
+ 组件位于 src/components/ui,推荐按需从 @/components/ui/* 引入。
188
+ </View>
189
+ </CardContent>
190
+ <CardFooter className="justify-end">
191
+ <Button size="sm" onClick={() => console.log('clicked')}>
192
+ 点击
193
+ </Button>
194
+ </CardFooter>
195
+ </Card>
196
+ </View>
197
+ )
198
+ }
199
+
200
+ export default ExamplePage
201
+ ```
202
+
203
+ #### 页面配置
204
+
205
+ ```typescript
206
+ // src/pages/example/index.config.ts
207
+ import { definePageConfig } from '@tarojs/taro'
208
+
209
+ export default definePageConfig({
210
+ navigationBarTitleText: '示例页面',
211
+ enablePullDownRefresh: true,
212
+ backgroundTextStyle: 'dark',
213
+ })
214
+ ```
215
+
216
+ #### 应用配置
217
+
218
+ ```typescript
219
+ // src/app.config.ts
220
+ import { defineAppConfig } from '@tarojs/taro'
221
+
222
+ export default defineAppConfig({
223
+ pages: [
224
+ 'pages/index/index',
225
+ 'pages/example/index',
226
+ ],
227
+ window: {
228
+ backgroundTextStyle: 'light',
229
+ navigationBarBackgroundColor: '#fff',
230
+ navigationBarTitleText: 'App',
231
+ navigationBarTextStyle: 'black',
232
+ },
233
+ // TabBar 配置 (可选)
234
+ // tabBar: {
235
+ // list: [
236
+ // { pagePath: 'pages/index/index', text: '首页' },
237
+ // ],
238
+ // },
239
+ })
240
+ ```
241
+
242
+ ### 发送请求
243
+
244
+ **IMPORTANT: 禁止直接使用 Taro.request、Taro.uploadFile、Taro.downloadFile,使用 Network.request、Network.uploadFile、Network.downloadFile 替代。**
245
+
246
+ Network 是对 Taro.request、Taro.uploadFile、Taro.downloadFile 的封装,自动添加项目域名前缀,参数与 Taro 一致。
247
+
248
+ ✅ 正确使用方式
249
+
250
+ ```typescript
251
+ import { Network } from '@/network'
252
+
253
+ // GET 请求
254
+ const data = await Network.request({
255
+ url: '/api/hello'
256
+ })
257
+
258
+ // POST 请求
259
+ const result = await Network.request({
260
+ url: '/api/user/login',
261
+ method: 'POST',
262
+ data: { username, password }
263
+ })
264
+
265
+ // 文件上传
266
+ await Network.uploadFile({
267
+ url: '/api/upload',
268
+ filePath: tempFilePath,
269
+ name: 'file'
270
+ })
271
+
272
+ // 文件下载
273
+ await Network.downloadFile({
274
+ url: '/api/download/file.pdf'
275
+ })
276
+ ```
277
+
278
+ ❌ 错误用法
279
+
280
+ ```typescript
281
+ import Taro from '@tarojs/taro'
282
+
283
+ // ❌ 会导致自动域名拼接无法生效,除非是特殊指定域名
284
+ const data = await Network.request({
285
+ url: 'http://localhost/api/hello'
286
+ })
287
+
288
+ // ❌ 不要直接使用 Taro.request
289
+ await Taro.request({ url: '/api/hello' })
290
+
291
+ // ❌ 不要直接使用 Taro.uploadFile
292
+ await Taro.uploadFile({ url: '/api/upload', filePath, name: 'file' })
293
+ ```
294
+
295
+ ### Zustand 状态管理
296
+
297
+ ```typescript
298
+ // src/stores/user.ts
299
+ import { create } from 'zustand'
300
+
301
+ interface UserState {
302
+ userInfo: UserInfo | null
303
+ token: string
304
+ setUserInfo: (info: UserInfo) => void
305
+ setToken: (token: string) => void
306
+ logout: () => void
307
+ }
308
+
309
+ interface UserInfo {
310
+ id: string
311
+ name: string
312
+ avatar: string
313
+ }
314
+
315
+ export const useUserStore = create<UserState>((set) => ({
316
+ userInfo: null,
317
+ token: '',
318
+ setUserInfo: (info) => set({ userInfo: info }),
319
+ setToken: (token) => set({ token }),
320
+ logout: () => set({ userInfo: null, token: '' }),
321
+ }))
322
+ ```
323
+
324
+ ### Taro 生命周期 Hooks
325
+
326
+ ```typescript
327
+ import {
328
+ useLoad, // 页面加载 (onLoad)
329
+ useReady, // 页面初次渲染完成 (onReady)
330
+ useDidShow, // 页面显示 (onShow)
331
+ useDidHide, // 页面隐藏 (onHide)
332
+ usePullDownRefresh, // 下拉刷新 (onPullDownRefresh)
333
+ useReachBottom, // 触底加载 (onReachBottom)
334
+ useShareAppMessage, // 分享 (onShareAppMessage)
335
+ useRouter, // 获取路由参数
336
+ } from '@tarojs/taro'
337
+ ```
338
+
339
+ ### 路由导航
340
+
341
+ ```typescript
342
+ import Taro from '@tarojs/taro'
343
+
344
+ // 保留当前页面,跳转到新页面
345
+ Taro.navigateTo({ url: '/pages/detail/index?id=1' })
346
+
347
+ // 关闭当前页面,跳转到新页面
348
+ Taro.redirectTo({ url: '/pages/detail/index' })
349
+
350
+ // 跳转到 tabBar 页面
351
+ Taro.switchTab({ url: '/pages/index/index' })
352
+
353
+ // 返回上一页
354
+ Taro.navigateBack({ delta: 1 })
355
+
356
+ // 获取路由参数
357
+ const router = useRouter()
358
+ const { id } = router.params
359
+ ```
360
+
361
+ ### 图标使用 (lucide-react-taro)
362
+
363
+ **IMPORTANT: 禁止使用 lucide-react,必须使用 lucide-react-taro 替代。**
364
+
365
+ lucide-react-taro 是 Lucide 图标库的 Taro 适配版本,专为小程序环境优化,API 与 lucide-react 一致:
366
+
367
+ ```tsx
368
+ import { View } from '@tarojs/components'
369
+ import { House, Settings, User, Search, Camera, Zap } from 'lucide-react-taro'
370
+
371
+ const IconDemo = () => {
372
+ return (
373
+ <View className="flex gap-4">
374
+ {/* 基本用法 */}
375
+ <House />
376
+ {/* 自定义尺寸和颜色 */}
377
+ <Settings size={32} color="#1890ff" />
378
+ {/* 自定义描边宽度 */}
379
+ <User size={24} strokeWidth={1.5} />
380
+ {/* 绝对描边宽度(描边不随 size 缩放) */}
381
+ <Camera size={48} strokeWidth={2} absoluteStrokeWidth />
382
+ {/* 组合使用 */}
383
+ <Zap size={32} color="#ff6b00" strokeWidth={1.5} className="my-icon" />
384
+ </View>
385
+ )
386
+ }
387
+ ```
388
+
389
+ 常用属性:
390
+ - `size` - 图标大小(默认 24)
391
+ - `color` - 图标颜色(默认 currentColor,小程序中建议显式设置)
392
+ - `strokeWidth` - 线条粗细(默认 2)
393
+ - `absoluteStrokeWidth` - 绝对描边宽度,启用后描边不随 size 缩放
394
+ - `className` / `style` - 自定义样式
395
+
396
+ 更多图标请访问:https://lucide.dev/icons
397
+
398
+ ### TabBar 图标生成 (CLI 工具)
399
+
400
+ **IMPORTANT: 微信小程序的 TabBar 不支持 base64 或 SVG 图片,必须使用本地 PNG 文件。**
401
+
402
+ lucide-react-taro 提供了 CLI 工具来生成 TabBar 所需的 PNG 图标:
403
+
404
+ ```bash
405
+ # 生成带选中状态的图标
406
+ pnpm exec taro-lucide-tabbar House Settings User -c "#999999" -a "#1890ff"
407
+
408
+ # 指定输出目录和尺寸
409
+ pnpm exec taro-lucide-tabbar House Settings User -c "#999999" -a "#1890ff" -o ./src/assets/tabbar -s 81
410
+ ```
411
+
412
+ CLI 参数:
413
+ - `--color, -c` (默认 #000000): 图标颜色
414
+ - `--active-color, -a`: 选中状态颜色
415
+ - `--size, -s` (默认 81): 图标尺寸
416
+ - `--output, -o` (默认 ./tabbar-icons): 输出目录
417
+ - `--stroke-width` (默认 2): 描边宽度
418
+
419
+ 在 `app.config.ts` 中使用生成的图标:
420
+
421
+ > IMPORTANT:iconPath 和 selectedIconPath 必须以 `./` 开头,否则图标无法渲染
422
+
423
+ ```typescript
424
+ export default defineAppConfig({
425
+ tabBar: {
426
+ color: '#999999',
427
+ selectedColor: '#1890ff',
428
+ backgroundColor: '#ffffff',
429
+ borderStyle: 'black',
430
+ list: [
431
+ {
432
+ pagePath: 'pages/index/index',
433
+ text: '首页',
434
+ iconPath: './assets/tabbar/house.png',
435
+ selectedIconPath: './assets/tabbar/house-active.png',
436
+ },
437
+ {
438
+ pagePath: 'pages/settings/index',
439
+ text: '设置',
440
+ iconPath: './assets/tabbar/settings.png',
441
+ selectedIconPath: './assets/tabbar/settings-active.png',
442
+ },
443
+ {
444
+ pagePath: 'pages/user/index',
445
+ text: '用户',
446
+ iconPath: './assets/tabbar/user.png',
447
+ selectedIconPath: './assets/tabbar/user-active.png',
448
+ },
449
+ ],
450
+ },
451
+ })
452
+
453
+ ### Tailwind CSS 样式开发
454
+
455
+ IMPORTANT:必须使用 tailwindcss 实现样式,只有在必要情况下才能 fallback 到 css / less
456
+
457
+ > 项目已集成 Tailwind CSS 4.x + weapp-tailwindcss,支持跨端原子化样式:
458
+
459
+ ```tsx
460
+ import { View, Text } from '@tarojs/components'
461
+ import { Button } from '@/components/ui/button'
462
+
463
+ <View className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
464
+ <Text className="text-2xl font-bold text-blue-600 mb-4">标题</Text>
465
+ <View className="w-full px-4">
466
+ <Button className="w-full" size="lg">
467
+ 按钮
468
+ </Button>
469
+ </View>
470
+ </View>
471
+ ```
472
+
473
+ ### 性能优化
474
+
475
+ #### 图片懒加载
476
+
477
+ ```tsx
478
+ import { Image } from '@tarojs/components'
479
+
480
+ <Image src={imageUrl} lazyLoad mode="aspectFill" />
481
+ ```
482
+
483
+ #### 虚拟列表
484
+
485
+ ```tsx
486
+ import { VirtualList } from '@tarojs/components'
487
+
488
+ <VirtualList
489
+ height={500}
490
+ itemData={list}
491
+ itemCount={list.length}
492
+ itemSize={100}
493
+ renderItem={({ index, style, data }) => (
494
+ <View style={style}>{data[index].name}</View>
495
+ )}
496
+ />
497
+ ```
498
+
499
+ #### 分包加载
500
+
501
+ ```typescript
502
+ // src/app.config.ts
503
+ export default defineAppConfig({
504
+ pages: ['pages/index/index'],
505
+ subPackages: [
506
+ {
507
+ root: 'packageA',
508
+ pages: ['pages/detail/index'],
509
+ },
510
+ ],
511
+ })
512
+ ```
513
+
514
+ ### 小程序限制
515
+
516
+ | 限制项 | 说明 |
517
+ | -------- | ---------------------------------------- |
518
+ | 主包体积 | ≤ 2MB |
519
+ | 总包体积 | ≤ 20MB |
520
+ | 域名配置 | 生产环境需在小程序后台配置合法域名 |
521
+ | 本地开发 | 需在微信开发者工具开启「不校验合法域名」 |
522
+
523
+ ### 权限配置
524
+
525
+ ```typescript
526
+ // src/app.config.ts
527
+ export default defineAppConfig({
528
+ // ...其他配置
529
+ permission: {
530
+ 'scope.userLocation': {
531
+ desc: '你的位置信息将用于小程序位置接口的效果展示'
532
+ }
533
+ },
534
+ requiredPrivateInfos: ['getLocation', 'chooseAddress']
535
+ })
536
+ ```
537
+
538
+ ### 位置服务
539
+
540
+ ```typescript
541
+ // 需先在 app.config.ts 中配置 permission
542
+ async function getLocation(): Promise<Taro.getLocation.SuccessCallbackResult> {
543
+ return await Taro.getLocation({ type: 'gcj02' })
544
+ }
545
+ ```
546
+
547
+ ## 后端核心开发规范
548
+
549
+ 本项目后端基于 NestJS + TypeScript 构建,提供高效、可扩展的服务端能力。
550
+
551
+ ### 项目结构
552
+
553
+ ```sh
554
+ .
555
+ ├── server/ # NestJS 后端服务
556
+ │ └── src/
557
+ │ ├── main.ts # 服务入口
558
+ │ ├── app.module.ts # 根模块
559
+ │ ├── app.controller.ts # 根控制器
560
+ │ └── app.service.ts # 根服务
561
+ ```
562
+
563
+ ### 开发命令
564
+
565
+ ```sh
566
+ pnpm dev:server // 启动开发服务 (热重载, 默认端口 3000)
567
+ pnpm build:server // 构建生产版本
568
+ ```
569
+
570
+ ### 新建模块流程 (CLI)
571
+
572
+ 快速生成样板代码:
573
+
574
+ ```bash
575
+ cd server
576
+
577
+ # 生成完整的 CRUD 资源 (包含 Module, Controller, Service, DTO, Entity)
578
+ pnpm exec nest g resource modules/product
579
+
580
+ # 仅生成特定部分
581
+ pnpm exec nest g module modules/order
582
+ pnpm exec nest g controller modules/order
583
+ pnpm exec nest g service modules/order
584
+ ```
585
+
586
+ ### 环境变量配置
587
+
588
+ 在 server/ 根目录创建 .env 文件:
589
+
590
+ ```sh
591
+ ## 服务端口
592
+ PORT=3000
593
+
594
+ ## 微信小程序配置
595
+ WX_APP_ID=你的AppID
596
+ WX_APP_SECRET=你的AppSecret
597
+ ```
598
+
599
+ 在代码中使用 @nestjs/config 读取环境变量:
600
+
601
+ ```typescript
602
+ import { ConfigService } from '@nestjs/config';
603
+
604
+ // 在 Service 中注入
605
+ constructor(private configService: ConfigService) {}
606
+
607
+ getWxConfig() {
608
+ return {
609
+ appId: this.configService.get<string>('WX_APP_ID'),
610
+ secret: this.configService.get<string>('WX_APP_SECRET'),
611
+ };
612
+ }
613
+ ```
614
+
615
+ ### 标准响应封装
616
+
617
+ 建议使用拦截器 (Interceptor) 统一 API 响应格式:
618
+
619
+ ```typeScript
620
+ // src/common/interceptors/transform.interceptor.ts
621
+ import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
622
+ import { Observable } from 'rxjs';
623
+ import { map } from 'rxjs/operators';
624
+
625
+ export interface Response<T> {
626
+ code: number;
627
+ data: T;
628
+ message: string;
629
+ }
630
+
631
+ @Injectable()
632
+ export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
633
+ intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
634
+ return next.handle().pipe(
635
+ map((data) => ({
636
+ code: 200,
637
+ data,
638
+ message: 'success',
639
+ })),
640
+ );
641
+ }
642
+ }
643
+ ```
644
+
645
+ 在 main.ts 中全局注册:
646
+
647
+ ```typescript
648
+ app.useGlobalInterceptors(new TransformInterceptor());
649
+ ```
650
+
651
+ ### 微信登录后端实现
652
+
653
+ ```typescript
654
+ // src/modules/auth/auth.service.ts
655
+ import { Injectable, UnauthorizedException } from '@nestjs/common';
656
+ import { HttpService } from '@nestjs/axios';
657
+ import { ConfigService } from '@nestjs/config';
658
+ import { lastValueFrom } from 'rxjs';
659
+
660
+ @Injectable()
661
+ export class AuthService {
662
+ constructor(
663
+ private httpService: HttpService,
664
+ private configService: ConfigService,
665
+ ) {}
666
+
667
+ async code2Session(code: string) {
668
+ const appId = this.configService.get('WX_APP_ID');
669
+ const secret = this.configService.get('WX_APP_SECRET');
670
+ const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${secret}&js_code=${code}&grant_type=authorization_code`;
671
+
672
+ const { data } = await lastValueFrom(this.httpService.get(url));
673
+
674
+ if (data.errcode) {
675
+ throw new UnauthorizedException(`微信登录失败: ${data.errmsg}`);
676
+ }
677
+
678
+ return data; // 包含 openid, session_key
679
+ }
680
+ }
681
+ ```
682
+
683
+ ### 异常处理
684
+
685
+ 使用全局异常过滤器 (Filter) 统一错误响应:
686
+
687
+ ```typescript
688
+ // src/common/filters/http-exception.filter.ts
689
+ import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
690
+ import { Response } from 'express';
691
+
692
+ @Catch(HttpException)
693
+ export class HttpExceptionFilter implements ExceptionFilter {
694
+ catch(exception: HttpException, host: ArgumentsHost) {
695
+ const ctx = host.switchToHttp();
696
+ const response = ctx.getResponse<Response>();
697
+ const status = exception.getStatus();
698
+ const exceptionResponse = exception.getResponse();
699
+
700
+ response.status(status).json({
701
+ code: status,
702
+ message: typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message,
703
+ data: null,
704
+ });
705
+ }
706
+ }
707
+ ```
708
+
709
+ 在 main.ts 中注册:
710
+
711
+ ```
712
+ app.useGlobalFilters(new HttpExceptionFilter());
713
+ ```
714
+
715
+ ### 数据库
716
+
717
+ 数据库能力统一通过平台 API / SDK 使用,模板不预置本地数据库或数据库直连依赖。
718
+
719
+ ### 类型校验 (Zod)
720
+
721
+ 项目集成了 [Zod](https://zod.dev/) 用于运行时类型校验。
722
+
723
+ #### 定义 Schema
724
+
725
+ ```typescript
726
+ import { z } from 'zod';
727
+
728
+ // 基础类型
729
+ const userSchema = z.object({
730
+ id: z.number(),
731
+ name: z.string().min(1).max(50),
732
+ email: z.string().email(),
733
+ age: z.number().int().positive().optional(),
734
+ });
735
+
736
+ // 从 schema 推导 TypeScript 类型
737
+ type User = z.infer<typeof userSchema>;
738
+ ```
739
+
740
+ #### 请求校验
741
+
742
+ ```typescript
743
+ // src/modules/user/dto/create-user.dto.ts
744
+ import { z } from 'zod';
745
+
746
+ export const createUserSchema = z.object({
747
+ nickname: z.string().min(1, '昵称不能为空').max(20, '昵称最多20个字符'),
748
+ avatar: z.string().url('头像必须是有效的URL').optional(),
749
+ phone: z.string().regex(/^1[3-9]\d{9}$/, '手机号格式不正确').optional(),
750
+ });
751
+
752
+ export type CreateUserDto = z.infer<typeof createUserSchema>;
753
+
754
+ // 在 Controller 中使用
755
+ @Post()
756
+ create(@Body() body: unknown) {
757
+ const result = createUserSchema.safeParse(body);
758
+ if (!result.success) {
759
+ throw new BadRequestException(result.error.errors);
760
+ }
761
+ return this.userService.create(result.data);
762
+ }
763
+ ```