@svton/cli 1.2.0 → 1.2.2
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/dist/index.js +20 -7
- package/dist/index.mjs +20 -7
- package/features.json +339 -0
- package/package.json +4 -1
- package/templates/apps/admin/next-env.d.ts +2 -0
- package/templates/apps/admin/next.config.js +15 -0
- package/templates/apps/admin/package.json.tpl +54 -0
- package/templates/apps/admin/postcss.config.js +6 -0
- package/templates/apps/admin/src/app/globals.css +37 -0
- package/templates/apps/admin/src/app/layout.tsx +19 -0
- package/templates/apps/admin/src/app/login/page.tsx +96 -0
- package/templates/apps/admin/src/app/page.tsx +8 -0
- package/templates/apps/admin/src/app/users/page.tsx +165 -0
- package/templates/apps/admin/src/components/ui/switch.tsx +29 -0
- package/templates/apps/admin/src/hooks/useAPI.ts +130 -0
- package/templates/apps/admin/src/lib/api-client.ts +112 -0
- package/templates/apps/admin/src/lib/api-server.ts +95 -0
- package/templates/apps/admin/tailwind.config.js +54 -0
- package/templates/apps/admin/tsconfig.json +22 -0
- package/templates/apps/backend/.env.example +29 -0
- package/templates/apps/backend/nest-cli.json +8 -0
- package/templates/apps/backend/package.json.tpl +57 -0
- package/templates/apps/backend/prisma/schema.prisma +72 -0
- package/templates/apps/backend/prisma/seed.ts +32 -0
- package/templates/apps/backend/src/app.controller.ts +15 -0
- package/templates/apps/backend/src/app.module.ts +85 -0
- package/templates/apps/backend/src/app.service.ts +12 -0
- package/templates/apps/backend/src/auth/auth.controller.ts +31 -0
- package/templates/apps/backend/src/auth/auth.module.ts +27 -0
- package/templates/apps/backend/src/auth/auth.service.ts +89 -0
- package/templates/apps/backend/src/auth/jwt-auth.guard.ts +5 -0
- package/templates/apps/backend/src/auth/jwt.strategy.ts +27 -0
- package/templates/apps/backend/src/config/env.schema.ts +35 -0
- package/templates/apps/backend/src/main.ts +51 -0
- package/templates/apps/backend/src/object-storage/object-storage.controller.ts +114 -0
- package/templates/apps/backend/src/object-storage/object-storage.module.ts +7 -0
- package/templates/apps/backend/src/prisma/prisma.module.ts +9 -0
- package/templates/apps/backend/src/prisma/prisma.service.ts +13 -0
- package/templates/apps/backend/src/user/user.controller.ts +50 -0
- package/templates/apps/backend/src/user/user.module.ts +12 -0
- package/templates/apps/backend/src/user/user.service.ts +117 -0
- package/templates/apps/backend/tsconfig.json +23 -0
- package/templates/apps/mobile/babel.config.js +8 -0
- package/templates/apps/mobile/config/index.ts +65 -0
- package/templates/apps/mobile/package.json.tpl +48 -0
- package/templates/apps/mobile/project.config.json.tpl +17 -0
- package/templates/apps/mobile/src/app.config.ts +9 -0
- package/templates/apps/mobile/src/app.scss +4 -0
- package/templates/apps/mobile/src/app.ts +8 -0
- package/templates/apps/mobile/src/hooks/useAPI.ts +285 -0
- package/templates/apps/mobile/src/pages/index/index.scss +7 -0
- package/templates/apps/mobile/src/pages/index/index.tsx +49 -0
- package/templates/apps/mobile/src/services/api.ts +155 -0
- package/templates/apps/mobile/src/services/upload.service.ts +41 -0
- package/templates/apps/mobile/tsconfig.json +21 -0
- package/templates/configs/authz.config.ts +10 -0
- package/templates/configs/cache.config.ts +14 -0
- package/templates/configs/oauth.config.ts +20 -0
- package/templates/configs/payment.config.ts +44 -0
- package/templates/configs/queue.config.ts +21 -0
- package/templates/configs/rate-limit.config.ts +16 -0
- package/templates/configs/sms.config.ts +11 -0
- package/templates/configs/storage.config.ts +14 -0
- package/templates/examples/README.md +258 -0
- package/templates/examples/authz/README.md +273 -0
- package/templates/examples/authz/roles.guard.ts +37 -0
- package/templates/examples/authz/user.controller.ts +116 -0
- package/templates/examples/cache/README.md +82 -0
- package/templates/examples/cache/user.controller.ts +42 -0
- package/templates/examples/cache/user.service.ts +78 -0
- package/templates/examples/oauth/README.md +192 -0
- package/templates/examples/oauth/auth.controller.ts +99 -0
- package/templates/examples/oauth/auth.service.ts +97 -0
- package/templates/examples/payment/README.md +151 -0
- package/templates/examples/payment/order.controller.ts +56 -0
- package/templates/examples/payment/order.service.ts +132 -0
- package/templates/examples/payment/webhook.controller.ts +73 -0
- package/templates/examples/queue/README.md +134 -0
- package/templates/examples/queue/email.controller.ts +34 -0
- package/templates/examples/queue/email.processor.ts +68 -0
- package/templates/examples/queue/email.service.ts +64 -0
- package/templates/examples/rate-limit/README.md +249 -0
- package/templates/examples/rate-limit/api.controller.ts +113 -0
- package/templates/examples/sms/README.md +121 -0
- package/templates/examples/sms/sms.service.ts +69 -0
- package/templates/examples/sms/verification.controller.ts +100 -0
- package/templates/examples/storage/README.md +224 -0
- package/templates/examples/storage/upload.controller.ts +117 -0
- package/templates/examples/storage/upload.service.ts +123 -0
- package/templates/packages/types/package.json.tpl +16 -0
- package/templates/packages/types/src/api.ts +88 -0
- package/templates/packages/types/src/common.ts +89 -0
- package/templates/packages/types/src/index.ts +3 -0
- package/templates/packages/types/tsconfig.json +16 -0
- package/templates/skills/authz.skill.md +42 -0
- package/templates/skills/base.skill.md +57 -0
- package/templates/skills/cache.skill.md +88 -0
- package/templates/skills/oauth.skill.md +41 -0
- package/templates/skills/payment.skill.md +129 -0
- package/templates/skills/queue.skill.md +140 -0
- package/templates/skills/rate-limit.skill.md +38 -0
- package/templates/skills/sms.skill.md +39 -0
- package/templates/skills/storage.skill.md +42 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 缓存功能示例
|
|
2
|
+
|
|
3
|
+
本示例展示如何使用 `@svton/nestjs-cache` 模块进行声明式缓存。
|
|
4
|
+
|
|
5
|
+
## 文件说明
|
|
6
|
+
|
|
7
|
+
- `user.service.ts` - 使用缓存装饰器的 Service 层
|
|
8
|
+
- `user.controller.ts` - 对应的 Controller 层
|
|
9
|
+
|
|
10
|
+
## 核心装饰器
|
|
11
|
+
|
|
12
|
+
### @Cacheable - 缓存查询结果
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
@Cacheable({ key: 'user:#id', ttl: 3600 })
|
|
16
|
+
async findOne(id: number) {
|
|
17
|
+
// 首次调用会执行方法并缓存结果
|
|
18
|
+
// 后续调用直接返回缓存数据
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### @CacheEvict - 清除缓存
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
@CacheEvict({ key: 'user:#id' })
|
|
26
|
+
async update(id: number, data: any) {
|
|
27
|
+
// 方法执行后会清除对应的缓存
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### @CachePut - 更新缓存
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
@CachePut({ key: 'user:#id' })
|
|
35
|
+
async updateAndRefresh(id: number, data: any) {
|
|
36
|
+
// 方法执行后会用返回值更新缓存
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Key 表达式
|
|
41
|
+
|
|
42
|
+
- `#id` - 从 request.params 获取
|
|
43
|
+
- `#0`, `#1` - 位置参数
|
|
44
|
+
- `#paramName` - 参数名
|
|
45
|
+
- `#body.field` - 从 request.body 获取
|
|
46
|
+
- `user:*` - 通配符模式(需要 pattern: true)
|
|
47
|
+
|
|
48
|
+
## 测试接口
|
|
49
|
+
|
|
50
|
+
启动项目后,可以通过以下接口测试:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 查询用户(首次会查询数据库,后续返回缓存)
|
|
54
|
+
curl http://localhost:3000/examples/users/1
|
|
55
|
+
|
|
56
|
+
# 查询用户列表
|
|
57
|
+
curl http://localhost:3000/examples/users?page=1&pageSize=10
|
|
58
|
+
|
|
59
|
+
# 更新用户(会清除缓存)
|
|
60
|
+
curl -X PUT http://localhost:3000/examples/users/1 \
|
|
61
|
+
-H "Content-Type: application/json" \
|
|
62
|
+
-d '{"name":"New Name"}'
|
|
63
|
+
|
|
64
|
+
# 更新用户并刷新缓存
|
|
65
|
+
curl -X PUT http://localhost:3000/examples/users/1/refresh \
|
|
66
|
+
-H "Content-Type: application/json" \
|
|
67
|
+
-d '{"name":"New Name"}'
|
|
68
|
+
|
|
69
|
+
# 清除所有用户缓存
|
|
70
|
+
curl -X DELETE http://localhost:3000/examples/users/cache
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 最佳实践
|
|
74
|
+
|
|
75
|
+
1. **合理设置 TTL**:根据数据更新频率设置过期时间
|
|
76
|
+
2. **及时清除缓存**:更新/删除操作使用 @CacheEvict
|
|
77
|
+
3. **避免缓存穿透**:对空结果也进行缓存
|
|
78
|
+
4. **使用命名空间**:key 前缀区分不同业务
|
|
79
|
+
|
|
80
|
+
## 更多信息
|
|
81
|
+
|
|
82
|
+
查看官方文档:https://751848178.github.io/svton/packages/nestjs-cache
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Controller, Get, Put, Delete, Param, Body, Query } from '@nestjs/common';
|
|
2
|
+
import { UserService, User } from './user.service';
|
|
3
|
+
|
|
4
|
+
@Controller('examples/users')
|
|
5
|
+
export class UserController {
|
|
6
|
+
constructor(private readonly userService: UserService) {}
|
|
7
|
+
|
|
8
|
+
@Get(':id')
|
|
9
|
+
async findOne(@Param('id') id: string): Promise<User> {
|
|
10
|
+
return this.userService.findOne(Number(id));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@Get()
|
|
14
|
+
async findAll(
|
|
15
|
+
@Query('page') page: string = '1',
|
|
16
|
+
@Query('pageSize') pageSize: string = '10',
|
|
17
|
+
): Promise<User[]> {
|
|
18
|
+
return this.userService.findAll(Number(page), Number(pageSize));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Put(':id')
|
|
22
|
+
async update(
|
|
23
|
+
@Param('id') id: string,
|
|
24
|
+
@Body() data: Partial<User>,
|
|
25
|
+
): Promise<User> {
|
|
26
|
+
return this.userService.update(Number(id), data);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Put(':id/refresh')
|
|
30
|
+
async updateAndRefresh(
|
|
31
|
+
@Param('id') id: string,
|
|
32
|
+
@Body() data: Partial<User>,
|
|
33
|
+
): Promise<User> {
|
|
34
|
+
return this.userService.updateAndRefresh(Number(id), data);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Delete('cache')
|
|
38
|
+
async clearCache(): Promise<{ message: string }> {
|
|
39
|
+
await this.userService.clearAllCache();
|
|
40
|
+
return { message: 'Cache cleared successfully' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Cacheable, CacheEvict, CachePut } from '@svton/nestjs-cache';
|
|
3
|
+
|
|
4
|
+
export interface User {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
email: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class UserService {
|
|
12
|
+
/**
|
|
13
|
+
* 缓存查询结果
|
|
14
|
+
* key: user:#id 会自动从参数中获取 id 值
|
|
15
|
+
* ttl: 缓存过期时间(秒)
|
|
16
|
+
*/
|
|
17
|
+
@Cacheable({ key: 'user:#id', ttl: 3600 })
|
|
18
|
+
async findOne(id: number): Promise<User> {
|
|
19
|
+
console.log(`Fetching user ${id} from database...`);
|
|
20
|
+
// TODO: 实际项目中从数据库查询
|
|
21
|
+
return {
|
|
22
|
+
id,
|
|
23
|
+
name: `User ${id}`,
|
|
24
|
+
email: `user${id}@example.com`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 更新数据时清除缓存
|
|
30
|
+
*/
|
|
31
|
+
@CacheEvict({ key: 'user:#id' })
|
|
32
|
+
async update(id: number, data: Partial<User>): Promise<User> {
|
|
33
|
+
console.log(`Updating user ${id}...`);
|
|
34
|
+
// TODO: 实际项目中更新数据库
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
name: data.name || `User ${id}`,
|
|
38
|
+
email: data.email || `user${id}@example.com`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 更新数据并刷新缓存
|
|
44
|
+
*/
|
|
45
|
+
@CachePut({ key: 'user:#id' })
|
|
46
|
+
async updateAndRefresh(id: number, data: Partial<User>): Promise<User> {
|
|
47
|
+
console.log(`Updating and refreshing cache for user ${id}...`);
|
|
48
|
+
// TODO: 实际项目中更新数据库
|
|
49
|
+
return {
|
|
50
|
+
id,
|
|
51
|
+
name: data.name || `User ${id}`,
|
|
52
|
+
email: data.email || `user${id}@example.com`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 批量清除缓存(使用通配符)
|
|
58
|
+
*/
|
|
59
|
+
@CacheEvict({ key: 'user:*', pattern: true })
|
|
60
|
+
async clearAllCache(): Promise<void> {
|
|
61
|
+
console.log('Clearing all user cache...');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 列表查询缓存
|
|
66
|
+
* 支持多个参数
|
|
67
|
+
*/
|
|
68
|
+
@Cacheable({ key: 'users:list:#page:#pageSize', ttl: 300 })
|
|
69
|
+
async findAll(page: number, pageSize: number): Promise<User[]> {
|
|
70
|
+
console.log(`Fetching users page ${page}...`);
|
|
71
|
+
// TODO: 实际项目中从数据库查询
|
|
72
|
+
return Array.from({ length: pageSize }, (_, i) => ({
|
|
73
|
+
id: (page - 1) * pageSize + i + 1,
|
|
74
|
+
name: `User ${(page - 1) * pageSize + i + 1}`,
|
|
75
|
+
email: `user${(page - 1) * pageSize + i + 1}@example.com`,
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# OAuth 登录示例
|
|
2
|
+
|
|
3
|
+
本示例展示如何使用 `@svton/nestjs-oauth` 模块实现微信登录。
|
|
4
|
+
|
|
5
|
+
## 文件说明
|
|
6
|
+
|
|
7
|
+
- `auth.service.ts` - 认证服务,处理 OAuth 流程
|
|
8
|
+
- `auth.controller.ts` - 认证控制器,提供登录接口
|
|
9
|
+
|
|
10
|
+
## 支持的登录方式
|
|
11
|
+
|
|
12
|
+
### 1. 微信开放平台(PC 扫码登录)
|
|
13
|
+
|
|
14
|
+
适用于网站应用,用户扫码登录。
|
|
15
|
+
|
|
16
|
+
### 2. 微信公众号(网页授权)
|
|
17
|
+
|
|
18
|
+
适用于公众号内网页,静默授权或用户授权。
|
|
19
|
+
|
|
20
|
+
### 3. 微信小程序
|
|
21
|
+
|
|
22
|
+
适用于小程序,使用 wx.login() 获取 code。
|
|
23
|
+
|
|
24
|
+
## 使用方式
|
|
25
|
+
|
|
26
|
+
### PC 扫码登录
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// 1. 获取授权 URL
|
|
30
|
+
const url = this.authService.getWechatOpenAuthUrl('/dashboard');
|
|
31
|
+
|
|
32
|
+
// 2. 重定向到微信授权页面
|
|
33
|
+
// 用户扫码授权后,微信会回调到 callback 接口
|
|
34
|
+
|
|
35
|
+
// 3. 处理回调
|
|
36
|
+
const userInfo = await this.authService.handleWechatOpenCallback(code);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 公众号网页授权
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
// 1. 获取授权 URL
|
|
43
|
+
const url = this.authService.getWechatMpAuthUrl('/profile');
|
|
44
|
+
|
|
45
|
+
// 2. 重定向到微信授权页面
|
|
46
|
+
// 用户授权后,微信会回调到 callback 接口
|
|
47
|
+
|
|
48
|
+
// 3. 处理回调
|
|
49
|
+
const userInfo = await this.authService.handleWechatMpCallback(code);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 小程序登录
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// 小程序端
|
|
56
|
+
wx.login({
|
|
57
|
+
success: (res) => {
|
|
58
|
+
// 将 code 发送到后端
|
|
59
|
+
wx.request({
|
|
60
|
+
url: 'https://api.example.com/examples/auth/wechat/miniprogram/login',
|
|
61
|
+
method: 'POST',
|
|
62
|
+
data: { code: res.code },
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 后端处理
|
|
68
|
+
const result = await this.authService.miniprogramLogin(code);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 小程序获取手机号
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// 小程序端
|
|
75
|
+
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
|
|
76
|
+
获取手机号
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
// 获取到 code 后发送到后端
|
|
80
|
+
const result = await this.authService.getMiniprogramPhoneNumber(code);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 测试接口
|
|
84
|
+
|
|
85
|
+
### PC 扫码登录
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
# 1. 访问登录页面(会重定向到微信)
|
|
89
|
+
curl http://localhost:3000/examples/auth/wechat/open/login
|
|
90
|
+
|
|
91
|
+
# 2. 扫码授权后,微信会回调到:
|
|
92
|
+
# http://localhost:3000/examples/auth/wechat/open/callback?code=xxx&state=/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 小程序登录
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl -X POST http://localhost:3000/examples/auth/wechat/miniprogram/login \
|
|
99
|
+
-H "Content-Type: application/json" \
|
|
100
|
+
-d '{"code":"081234567890abcdef"}'
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 小程序获取手机号
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
curl -X POST http://localhost:3000/examples/auth/wechat/miniprogram/phone \
|
|
107
|
+
-H "Content-Type: application/json" \
|
|
108
|
+
-d '{"code":"081234567890abcdef"}'
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 环境变量配置
|
|
112
|
+
|
|
113
|
+
在 `.env` 文件中配置:
|
|
114
|
+
|
|
115
|
+
```env
|
|
116
|
+
# 微信开放平台
|
|
117
|
+
WECHAT_OPEN_APP_ID=wx1234567890abcdef
|
|
118
|
+
WECHAT_OPEN_APP_SECRET=1234567890abcdef1234567890abcdef
|
|
119
|
+
WECHAT_OPEN_CALLBACK_URL=https://yourdomain.com/examples/auth/wechat/open/callback
|
|
120
|
+
|
|
121
|
+
# 微信公众号
|
|
122
|
+
WECHAT_MP_APP_ID=wx1234567890abcdef
|
|
123
|
+
WECHAT_MP_APP_SECRET=1234567890abcdef1234567890abcdef
|
|
124
|
+
|
|
125
|
+
# 微信小程序
|
|
126
|
+
WECHAT_MINI_APP_ID=wx1234567890abcdef
|
|
127
|
+
WECHAT_MINI_APP_SECRET=1234567890abcdef1234567890abcdef
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## 配置回调地址
|
|
131
|
+
|
|
132
|
+
需要在微信后台配置回调地址:
|
|
133
|
+
|
|
134
|
+
### 开放平台
|
|
135
|
+
1. 登录微信开放平台
|
|
136
|
+
2. 进入网站应用详情
|
|
137
|
+
3. 配置授权回调域:`yourdomain.com`
|
|
138
|
+
|
|
139
|
+
### 公众号
|
|
140
|
+
1. 登录微信公众平台
|
|
141
|
+
2. 设置与开发 -> 接口权限 -> 网页授权
|
|
142
|
+
3. 配置授权回调域:`yourdomain.com`
|
|
143
|
+
|
|
144
|
+
### 小程序
|
|
145
|
+
1. 登录微信小程序后台
|
|
146
|
+
2. 开发 -> 开发管理 -> 开发设置
|
|
147
|
+
3. 配置服务器域名:`https://yourdomain.com`
|
|
148
|
+
|
|
149
|
+
## 最佳实践
|
|
150
|
+
|
|
151
|
+
1. **UnionID 机制**:使用 unionid 关联同一用户在不同应用的身份
|
|
152
|
+
2. **Token 管理**:使用 JWT 生成 token,设置合理的过期时间
|
|
153
|
+
3. **刷新机制**:实现 refresh_token 机制,避免频繁授权
|
|
154
|
+
4. **错误处理**:妥善处理授权失败、token 过期等异常情况
|
|
155
|
+
5. **安全性**:验证 state 参数,防止 CSRF 攻击
|
|
156
|
+
|
|
157
|
+
## 常见场景
|
|
158
|
+
|
|
159
|
+
### 网站登录
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// 1. 用户点击"微信登录"
|
|
163
|
+
// 2. 重定向到微信授权页面
|
|
164
|
+
// 3. 用户扫码授权
|
|
165
|
+
// 4. 微信回调到后端
|
|
166
|
+
// 5. 后端获取用户信息,生成 token
|
|
167
|
+
// 6. 重定向到前端,携带 token
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 小程序登录
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
// 1. 小程序调用 wx.login() 获取 code
|
|
174
|
+
// 2. 将 code 发送到后端
|
|
175
|
+
// 3. 后端调用 code2Session 获取 openid
|
|
176
|
+
// 4. 查询或创建用户
|
|
177
|
+
// 5. 返回 token 给小程序
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### 绑定手机号
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// 1. 用户点击"获取手机号"按钮
|
|
184
|
+
// 2. 小程序获取到 code
|
|
185
|
+
// 3. 将 code 发送到后端
|
|
186
|
+
// 4. 后端调用接口获取手机号
|
|
187
|
+
// 5. 绑定手机号到用户账号
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## 更多信息
|
|
191
|
+
|
|
192
|
+
查看官方文档:https://751848178.github.io/svton/packages/nestjs-oauth
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Controller, Get, Query, Redirect, Post, Body } from '@nestjs/common';
|
|
2
|
+
import { AuthService } from './auth.service';
|
|
3
|
+
|
|
4
|
+
@Controller('examples/auth')
|
|
5
|
+
export class AuthController {
|
|
6
|
+
constructor(private readonly authService: AuthService) {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 微信开放平台登录 - 获取授权 URL
|
|
10
|
+
*/
|
|
11
|
+
@Get('wechat/open/login')
|
|
12
|
+
@Redirect()
|
|
13
|
+
wechatOpenLogin(@Query('redirect') redirect?: string) {
|
|
14
|
+
const state = redirect || '/';
|
|
15
|
+
const url = this.authService.getWechatOpenAuthUrl(state);
|
|
16
|
+
|
|
17
|
+
return { url };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 微信开放平台登录 - 回调处理
|
|
22
|
+
*/
|
|
23
|
+
@Get('wechat/open/callback')
|
|
24
|
+
async wechatOpenCallback(
|
|
25
|
+
@Query('code') code: string,
|
|
26
|
+
@Query('state') state: string,
|
|
27
|
+
) {
|
|
28
|
+
const userInfo = await this.authService.handleWechatOpenCallback(code);
|
|
29
|
+
|
|
30
|
+
// TODO: 生成 JWT token
|
|
31
|
+
// const token = await this.jwtService.sign({ userId: user.id });
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
message: 'Login successful',
|
|
35
|
+
userInfo,
|
|
36
|
+
redirectUrl: state,
|
|
37
|
+
// token,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 微信公众号登录 - 获取授权 URL
|
|
43
|
+
*/
|
|
44
|
+
@Get('wechat/mp/login')
|
|
45
|
+
@Redirect()
|
|
46
|
+
wechatMpLogin(@Query('redirect') redirect?: string) {
|
|
47
|
+
const state = redirect || '/';
|
|
48
|
+
const url = this.authService.getWechatMpAuthUrl(state);
|
|
49
|
+
|
|
50
|
+
return { url };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 微信公众号登录 - 回调处理
|
|
55
|
+
*/
|
|
56
|
+
@Get('wechat/mp/callback')
|
|
57
|
+
async wechatMpCallback(
|
|
58
|
+
@Query('code') code: string,
|
|
59
|
+
@Query('state') state: string,
|
|
60
|
+
) {
|
|
61
|
+
const userInfo = await this.authService.handleWechatMpCallback(code);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
message: 'Login successful',
|
|
65
|
+
userInfo,
|
|
66
|
+
redirectUrl: state,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 小程序登录
|
|
72
|
+
*/
|
|
73
|
+
@Post('wechat/miniprogram/login')
|
|
74
|
+
async miniprogramLogin(@Body() body: { code: string }) {
|
|
75
|
+
const result = await this.authService.miniprogramLogin(body.code);
|
|
76
|
+
|
|
77
|
+
// TODO: 生成 JWT token
|
|
78
|
+
// const token = await this.jwtService.sign({ userId: user.id });
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
message: 'Login successful',
|
|
82
|
+
...result,
|
|
83
|
+
// token,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 小程序获取手机号
|
|
89
|
+
*/
|
|
90
|
+
@Post('wechat/miniprogram/phone')
|
|
91
|
+
async getMiniprogramPhone(@Body() body: { code: string }) {
|
|
92
|
+
const result = await this.authService.getMiniprogramPhoneNumber(body.code);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
message: 'Phone number retrieved successfully',
|
|
96
|
+
...result,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { OAuthService } from '@svton/nestjs-oauth';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class AuthService {
|
|
6
|
+
constructor(private readonly oauthService: OAuthService) {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 获取微信开放平台授权 URL(PC 扫码登录)
|
|
10
|
+
*/
|
|
11
|
+
getWechatOpenAuthUrl(state: string): string {
|
|
12
|
+
return this.oauthService.wechat.getAuthorizationUrl('open', state);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 获取微信公众号授权 URL(网页授权)
|
|
17
|
+
*/
|
|
18
|
+
getWechatMpAuthUrl(state: string): string {
|
|
19
|
+
return this.oauthService.wechat.getAuthorizationUrl('mp', state);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 处理微信开放平台回调
|
|
24
|
+
*/
|
|
25
|
+
async handleWechatOpenCallback(code: string) {
|
|
26
|
+
// 获取 access_token
|
|
27
|
+
const tokenResult = await this.oauthService.wechat.getAccessToken('open', code);
|
|
28
|
+
|
|
29
|
+
// 获取用户信息
|
|
30
|
+
const userInfo = await this.oauthService.wechat.getUserInfo(
|
|
31
|
+
'open',
|
|
32
|
+
tokenResult.access_token,
|
|
33
|
+
tokenResult.openid,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// TODO: 根据 openid 查询或创建用户
|
|
37
|
+
// const user = await this.userService.findOrCreateByWechatOpenId(userInfo.unionid);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
openid: userInfo.openid,
|
|
41
|
+
unionid: userInfo.unionid,
|
|
42
|
+
nickname: userInfo.nickname,
|
|
43
|
+
avatar: userInfo.headimgurl,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 处理微信公众号回调
|
|
49
|
+
*/
|
|
50
|
+
async handleWechatMpCallback(code: string) {
|
|
51
|
+
const tokenResult = await this.oauthService.wechat.getAccessToken('mp', code);
|
|
52
|
+
|
|
53
|
+
const userInfo = await this.oauthService.wechat.getUserInfo(
|
|
54
|
+
'mp',
|
|
55
|
+
tokenResult.access_token,
|
|
56
|
+
tokenResult.openid,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// TODO: 根据 openid 查询或创建用户
|
|
60
|
+
// const user = await this.userService.findOrCreateByWechatMpOpenId(userInfo.openid);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
openid: userInfo.openid,
|
|
64
|
+
nickname: userInfo.nickname,
|
|
65
|
+
avatar: userInfo.headimgurl,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 小程序登录
|
|
71
|
+
*/
|
|
72
|
+
async miniprogramLogin(code: string) {
|
|
73
|
+
const result = await this.oauthService.wechat.code2Session(code);
|
|
74
|
+
|
|
75
|
+
// TODO: 根据 openid 查询或创建用户
|
|
76
|
+
// const user = await this.userService.findOrCreateByWechatMiniOpenId(result.openid);
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
openid: result.openid,
|
|
80
|
+
sessionKey: result.session_key,
|
|
81
|
+
unionid: result.unionid,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 小程序获取手机号
|
|
87
|
+
*/
|
|
88
|
+
async getMiniprogramPhoneNumber(code: string) {
|
|
89
|
+
const result = await this.oauthService.wechat.getPhoneNumber(code);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
phoneNumber: result.phone_info.phoneNumber,
|
|
93
|
+
purePhoneNumber: result.phone_info.purePhoneNumber,
|
|
94
|
+
countryCode: result.phone_info.countryCode,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|