@svton/cli 1.2.1 → 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 +13 -6
- package/dist/index.mjs +13 -6
- package/package.json +3 -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,151 @@
|
|
|
1
|
+
# 支付功能示例
|
|
2
|
+
|
|
3
|
+
本示例展示如何使用 `@svton/nestjs-payment` 模块集成微信支付和支付宝。
|
|
4
|
+
|
|
5
|
+
## 文件说明
|
|
6
|
+
|
|
7
|
+
- `order.service.ts` - 订单服务,创建支付订单
|
|
8
|
+
- `order.controller.ts` - 订单控制器,提供支付接口
|
|
9
|
+
- `webhook.controller.ts` - 支付回调处理
|
|
10
|
+
|
|
11
|
+
## 支持的支付方式
|
|
12
|
+
|
|
13
|
+
### 微信支付
|
|
14
|
+
- JSAPI 支付(公众号/小程序)
|
|
15
|
+
- Native 支付(扫码支付)
|
|
16
|
+
- APP 支付
|
|
17
|
+
- H5 支付
|
|
18
|
+
- 小程序支付
|
|
19
|
+
|
|
20
|
+
### 支付宝
|
|
21
|
+
- 电脑网站支付(PC)
|
|
22
|
+
- 手机网站支付(H5)
|
|
23
|
+
- APP 支付
|
|
24
|
+
|
|
25
|
+
## 使用方式
|
|
26
|
+
|
|
27
|
+
### 1. 创建支付订单
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// 微信 JSAPI 支付
|
|
31
|
+
const result = await this.paymentService.wechat.createOrder({
|
|
32
|
+
outTradeNo: orderId,
|
|
33
|
+
totalAmount: amount,
|
|
34
|
+
description: '商品购买',
|
|
35
|
+
userId: openid,
|
|
36
|
+
}, 'jsapi');
|
|
37
|
+
|
|
38
|
+
// 支付宝电脑网站支付
|
|
39
|
+
const result = await this.paymentService.alipay.createOrder({
|
|
40
|
+
outTradeNo: orderId,
|
|
41
|
+
totalAmount: amount,
|
|
42
|
+
description: '商品购买',
|
|
43
|
+
}, 'page');
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 2. 查询订单状态
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const status = await this.paymentService.wechat.queryOrder(outTradeNo);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. 申请退款
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
const refund = await this.paymentService.wechat.refund({
|
|
56
|
+
outTradeNo: orderId,
|
|
57
|
+
outRefundNo: refundId,
|
|
58
|
+
refundAmount: amount,
|
|
59
|
+
totalAmount: totalAmount,
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 4. 处理支付回调
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
@Post('webhook/wechat')
|
|
67
|
+
async wechatWebhook(@Req() req: Request) {
|
|
68
|
+
const result = await this.paymentService.wechat.handleNotify(req);
|
|
69
|
+
// 处理支付成功逻辑
|
|
70
|
+
return { code: 'SUCCESS', message: '成功' };
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 测试接口
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 创建微信 JSAPI 支付订单
|
|
78
|
+
curl -X POST http://localhost:3000/examples/orders/wechat/jsapi \
|
|
79
|
+
-H "Content-Type: application/json" \
|
|
80
|
+
-d '{
|
|
81
|
+
"orderId": "ORDER_001",
|
|
82
|
+
"amount": 100,
|
|
83
|
+
"openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
|
|
84
|
+
}'
|
|
85
|
+
|
|
86
|
+
# 创建微信 Native 支付订单(扫码支付)
|
|
87
|
+
curl -X POST http://localhost:3000/examples/orders/wechat/native \
|
|
88
|
+
-H "Content-Type: application/json" \
|
|
89
|
+
-d '{
|
|
90
|
+
"orderId": "ORDER_002",
|
|
91
|
+
"amount": 100
|
|
92
|
+
}'
|
|
93
|
+
|
|
94
|
+
# 创建支付宝电脑网站支付订单
|
|
95
|
+
curl -X POST http://localhost:3000/examples/orders/alipay/page \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{
|
|
98
|
+
"orderId": "ORDER_003",
|
|
99
|
+
"amount": 100
|
|
100
|
+
}'
|
|
101
|
+
|
|
102
|
+
# 查询订单状态
|
|
103
|
+
curl http://localhost:3000/examples/orders/ORDER_001/status
|
|
104
|
+
|
|
105
|
+
# 申请退款
|
|
106
|
+
curl -X POST http://localhost:3000/examples/orders/ORDER_001/refund \
|
|
107
|
+
-H "Content-Type: application/json" \
|
|
108
|
+
-d '{
|
|
109
|
+
"refundId": "REFUND_001",
|
|
110
|
+
"amount": 100,
|
|
111
|
+
"reason": "用户申请退款"
|
|
112
|
+
}'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 环境变量配置
|
|
116
|
+
|
|
117
|
+
在 `.env` 文件中配置:
|
|
118
|
+
|
|
119
|
+
```env
|
|
120
|
+
# 微信支付
|
|
121
|
+
WECHAT_MCH_ID=商户号
|
|
122
|
+
WECHAT_PRIVATE_KEY=./certs/apiclient_key.pem
|
|
123
|
+
WECHAT_SERIAL_NO=证书序列号
|
|
124
|
+
WECHAT_API_V3_KEY=APIv3密钥
|
|
125
|
+
WECHAT_APP_ID=关联的AppID
|
|
126
|
+
|
|
127
|
+
# 支付宝
|
|
128
|
+
ALIPAY_APP_ID=应用ID
|
|
129
|
+
ALIPAY_PRIVATE_KEY=./certs/alipay_private_key.pem
|
|
130
|
+
ALIPAY_PUBLIC_KEY=./certs/alipay_public_key.pem
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 最佳实践
|
|
134
|
+
|
|
135
|
+
1. **订单号唯一性**:确保 outTradeNo 全局唯一
|
|
136
|
+
2. **金额单位**:统一使用分为单位
|
|
137
|
+
3. **回调幂等性**:支付回调可能重复,需要幂等处理
|
|
138
|
+
4. **异步通知**:优先使用异步通知,不依赖同步返回
|
|
139
|
+
5. **安全验证**:验证回调签名,防止伪造
|
|
140
|
+
6. **错误处理**:妥善处理支付失败、超时等异常情况
|
|
141
|
+
|
|
142
|
+
## 回调地址配置
|
|
143
|
+
|
|
144
|
+
需要在微信支付和支付宝后台配置回调地址:
|
|
145
|
+
|
|
146
|
+
- 微信支付:`https://yourdomain.com/examples/webhooks/wechat`
|
|
147
|
+
- 支付宝:`https://yourdomain.com/examples/webhooks/alipay`
|
|
148
|
+
|
|
149
|
+
## 更多信息
|
|
150
|
+
|
|
151
|
+
查看官方文档:https://751848178.github.io/svton/packages/nestjs-payment
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Controller, Post, Get, Body, Param, Query } from '@nestjs/common';
|
|
2
|
+
import { OrderService } from './order.service';
|
|
3
|
+
|
|
4
|
+
@Controller('examples/orders')
|
|
5
|
+
export class OrderController {
|
|
6
|
+
constructor(private readonly orderService: OrderService) {}
|
|
7
|
+
|
|
8
|
+
@Post('wechat/jsapi')
|
|
9
|
+
async createWechatJsapiOrder(
|
|
10
|
+
@Body() data: { orderId: string; amount: number; openid: string },
|
|
11
|
+
) {
|
|
12
|
+
return this.orderService.createWechatJsapiOrder(
|
|
13
|
+
data.orderId,
|
|
14
|
+
data.amount,
|
|
15
|
+
data.openid,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Post('wechat/native')
|
|
20
|
+
async createWechatNativeOrder(
|
|
21
|
+
@Body() data: { orderId: string; amount: number },
|
|
22
|
+
) {
|
|
23
|
+
return this.orderService.createWechatNativeOrder(
|
|
24
|
+
data.orderId,
|
|
25
|
+
data.amount,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Post('alipay/page')
|
|
30
|
+
async createAlipayPageOrder(
|
|
31
|
+
@Body() data: { orderId: string; amount: number },
|
|
32
|
+
) {
|
|
33
|
+
return this.orderService.createAlipayPageOrder(
|
|
34
|
+
data.orderId,
|
|
35
|
+
data.amount,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Get(':orderId/status')
|
|
40
|
+
async queryOrderStatus(@Param('orderId') orderId: string) {
|
|
41
|
+
return this.orderService.queryOrderStatus(orderId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Post(':orderId/refund')
|
|
45
|
+
async refundOrder(
|
|
46
|
+
@Param('orderId') orderId: string,
|
|
47
|
+
@Body() data: { refundId: string; amount: number; reason?: string },
|
|
48
|
+
) {
|
|
49
|
+
return this.orderService.refundOrder(
|
|
50
|
+
orderId,
|
|
51
|
+
data.refundId,
|
|
52
|
+
data.amount,
|
|
53
|
+
data.reason,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { PaymentService } from '@svton/nestjs-payment';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class OrderService {
|
|
6
|
+
constructor(private readonly paymentService: PaymentService) {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 创建微信 JSAPI 支付订单(公众号/小程序)
|
|
10
|
+
*/
|
|
11
|
+
async createWechatJsapiOrder(
|
|
12
|
+
orderId: string,
|
|
13
|
+
amount: number,
|
|
14
|
+
openid: string,
|
|
15
|
+
) {
|
|
16
|
+
const result = await this.paymentService.wechat.createOrder(
|
|
17
|
+
{
|
|
18
|
+
outTradeNo: orderId,
|
|
19
|
+
totalAmount: amount,
|
|
20
|
+
description: '商品购买',
|
|
21
|
+
userId: openid,
|
|
22
|
+
},
|
|
23
|
+
'jsapi',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
orderId,
|
|
28
|
+
paymentData: result,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 创建微信 Native 支付订单(扫码支付)
|
|
34
|
+
*/
|
|
35
|
+
async createWechatNativeOrder(orderId: string, amount: number) {
|
|
36
|
+
const result = await this.paymentService.wechat.createOrder(
|
|
37
|
+
{
|
|
38
|
+
outTradeNo: orderId,
|
|
39
|
+
totalAmount: amount,
|
|
40
|
+
description: '商品购买',
|
|
41
|
+
},
|
|
42
|
+
'native',
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
orderId,
|
|
47
|
+
qrCode: result.code_url, // 二维码链接
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 创建支付宝电脑网站支付订单
|
|
53
|
+
*/
|
|
54
|
+
async createAlipayPageOrder(orderId: string, amount: number) {
|
|
55
|
+
const result = await this.paymentService.alipay.createOrder(
|
|
56
|
+
{
|
|
57
|
+
outTradeNo: orderId,
|
|
58
|
+
totalAmount: amount,
|
|
59
|
+
description: '商品购买',
|
|
60
|
+
},
|
|
61
|
+
'page',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
orderId,
|
|
66
|
+
paymentUrl: result, // 支付页面 URL
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 查询订单状态
|
|
72
|
+
*/
|
|
73
|
+
async queryOrderStatus(orderId: string) {
|
|
74
|
+
try {
|
|
75
|
+
// 先尝试查询微信支付
|
|
76
|
+
const wechatStatus = await this.paymentService.wechat.queryOrder(orderId);
|
|
77
|
+
return {
|
|
78
|
+
orderId,
|
|
79
|
+
provider: 'wechat',
|
|
80
|
+
status: wechatStatus.trade_state,
|
|
81
|
+
data: wechatStatus,
|
|
82
|
+
};
|
|
83
|
+
} catch (wechatError) {
|
|
84
|
+
// 如果微信查询失败,尝试支付宝
|
|
85
|
+
try {
|
|
86
|
+
const alipayStatus = await this.paymentService.alipay.queryOrder(orderId);
|
|
87
|
+
return {
|
|
88
|
+
orderId,
|
|
89
|
+
provider: 'alipay',
|
|
90
|
+
status: alipayStatus.trade_status,
|
|
91
|
+
data: alipayStatus,
|
|
92
|
+
};
|
|
93
|
+
} catch (alipayError) {
|
|
94
|
+
// 两个都失败,返回详细错误信息
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Failed to query order ${orderId}. ` +
|
|
97
|
+
`Wechat error: ${wechatError instanceof Error ? wechatError.message : 'Unknown'}. ` +
|
|
98
|
+
`Alipay error: ${alipayError instanceof Error ? alipayError.message : 'Unknown'}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 申请退款
|
|
106
|
+
*/
|
|
107
|
+
async refundOrder(
|
|
108
|
+
orderId: string,
|
|
109
|
+
refundId: string,
|
|
110
|
+
amount: number,
|
|
111
|
+
reason?: string,
|
|
112
|
+
) {
|
|
113
|
+
// TODO: 从数据库查询订单信息,确定支付方式和总金额
|
|
114
|
+
const totalAmount = amount; // 实际应从数据库获取
|
|
115
|
+
|
|
116
|
+
// 这里假设是微信支付,实际应根据订单信息判断
|
|
117
|
+
const result = await this.paymentService.wechat.refund({
|
|
118
|
+
outTradeNo: orderId,
|
|
119
|
+
outRefundNo: refundId,
|
|
120
|
+
refundAmount: amount,
|
|
121
|
+
totalAmount: totalAmount,
|
|
122
|
+
reason: reason || '用户申请退款',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
orderId,
|
|
127
|
+
refundId,
|
|
128
|
+
status: result.status,
|
|
129
|
+
data: result,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Controller, Post, Req, Body, HttpCode } from '@nestjs/common';
|
|
2
|
+
import { Request } from 'express';
|
|
3
|
+
import { PaymentService } from '@svton/nestjs-payment';
|
|
4
|
+
|
|
5
|
+
@Controller('examples/webhooks')
|
|
6
|
+
export class WebhookController {
|
|
7
|
+
constructor(private readonly paymentService: PaymentService) {}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 微信支付回调
|
|
11
|
+
*/
|
|
12
|
+
@Post('wechat')
|
|
13
|
+
@HttpCode(200)
|
|
14
|
+
async wechatWebhook(@Req() req: Request) {
|
|
15
|
+
try {
|
|
16
|
+
const result = await this.paymentService.wechat.handleNotify(req);
|
|
17
|
+
|
|
18
|
+
console.log('Wechat payment notification:', result);
|
|
19
|
+
|
|
20
|
+
// TODO: 处理支付成功逻辑
|
|
21
|
+
// 1. 验证订单状态
|
|
22
|
+
// 2. 更新订单状态
|
|
23
|
+
// 3. 发送通知给用户
|
|
24
|
+
// 4. 触发后续业务流程
|
|
25
|
+
|
|
26
|
+
const { out_trade_no, transaction_id, trade_state } = result;
|
|
27
|
+
|
|
28
|
+
if (trade_state === 'SUCCESS') {
|
|
29
|
+
console.log(`Order ${out_trade_no} paid successfully`);
|
|
30
|
+
// await this.orderService.markAsPaid(out_trade_no, transaction_id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 返回成功响应
|
|
34
|
+
return {
|
|
35
|
+
code: 'SUCCESS',
|
|
36
|
+
message: '成功',
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Wechat webhook error:', error);
|
|
40
|
+
return {
|
|
41
|
+
code: 'FAIL',
|
|
42
|
+
message: error.message,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 支付宝支付回调
|
|
49
|
+
*/
|
|
50
|
+
@Post('alipay')
|
|
51
|
+
@HttpCode(200)
|
|
52
|
+
async alipayWebhook(@Body() body: any) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await this.paymentService.alipay.handleNotify(body);
|
|
55
|
+
|
|
56
|
+
console.log('Alipay payment notification:', result);
|
|
57
|
+
|
|
58
|
+
// TODO: 处理支付成功逻辑
|
|
59
|
+
const { out_trade_no, trade_no, trade_status } = result;
|
|
60
|
+
|
|
61
|
+
if (trade_status === 'TRADE_SUCCESS') {
|
|
62
|
+
console.log(`Order ${out_trade_no} paid successfully`);
|
|
63
|
+
// await this.orderService.markAsPaid(out_trade_no, trade_no);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 返回成功响应(支付宝要求返回 success 字符串)
|
|
67
|
+
return 'success';
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Alipay webhook error:', error);
|
|
70
|
+
return 'fail';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# 消息队列示例
|
|
2
|
+
|
|
3
|
+
本示例展示如何使用 `@svton/nestjs-queue` 模块处理异步任务。
|
|
4
|
+
|
|
5
|
+
## 文件说明
|
|
6
|
+
|
|
7
|
+
- `email.processor.ts` - 队列处理器,定义任务执行逻辑
|
|
8
|
+
- `email.service.ts` - 服务层,添加任务到队列
|
|
9
|
+
- `email.controller.ts` - 控制器,提供 API 接口
|
|
10
|
+
|
|
11
|
+
## 核心概念
|
|
12
|
+
|
|
13
|
+
- **Queue(队列)**:任务的容器
|
|
14
|
+
- **Job(任务)**:具体的执行单元
|
|
15
|
+
- **Processor(处理器)**:任务的执行逻辑
|
|
16
|
+
- **Worker(工作进程)**:执行任务的进程
|
|
17
|
+
|
|
18
|
+
## 使用方式
|
|
19
|
+
|
|
20
|
+
### 1. 定义处理器
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
@Processor({ name: 'email' })
|
|
24
|
+
export class EmailProcessor {
|
|
25
|
+
@Process('send')
|
|
26
|
+
async handleSend(job: Job<EmailData>) {
|
|
27
|
+
// 处理任务
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. 添加任务
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
await this.queueService.addJob('email', 'send', {
|
|
36
|
+
to: 'user@example.com',
|
|
37
|
+
subject: 'Welcome',
|
|
38
|
+
body: 'Welcome to our platform!',
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 任务选项
|
|
43
|
+
|
|
44
|
+
### 延迟执行
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
await this.queueService.addJob('email', 'send', data, {
|
|
48
|
+
delay: 60000, // 延迟 60 秒
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 重试策略
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
await this.queueService.addJob('email', 'send', data, {
|
|
56
|
+
attempts: 3,
|
|
57
|
+
backoff: {
|
|
58
|
+
type: 'exponential',
|
|
59
|
+
delay: 1000,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 定时任务
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
await this.queueService.addJob('email', 'send', data, {
|
|
68
|
+
repeat: {
|
|
69
|
+
cron: '0 9 * * *', // 每天 9 点执行
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 优先级
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
await this.queueService.addJob('email', 'send', data, {
|
|
78
|
+
priority: 1, // 数字越小优先级越高
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 测试接口
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# 发送邮件
|
|
86
|
+
curl -X POST http://localhost:3000/examples/emails/send \
|
|
87
|
+
-H "Content-Type: application/json" \
|
|
88
|
+
-d '{
|
|
89
|
+
"to": "user@example.com",
|
|
90
|
+
"subject": "Test Email",
|
|
91
|
+
"body": "This is a test email"
|
|
92
|
+
}'
|
|
93
|
+
|
|
94
|
+
# 延迟发送
|
|
95
|
+
curl -X POST http://localhost:3000/examples/emails/send-delayed \
|
|
96
|
+
-H "Content-Type: application/json" \
|
|
97
|
+
-d '{
|
|
98
|
+
"to": "user@example.com",
|
|
99
|
+
"subject": "Delayed Email",
|
|
100
|
+
"body": "This email will be sent after 10 seconds",
|
|
101
|
+
"delayMs": 10000
|
|
102
|
+
}'
|
|
103
|
+
|
|
104
|
+
# 批量发送
|
|
105
|
+
curl -X POST http://localhost:3000/examples/emails/send-batch \
|
|
106
|
+
-H "Content-Type: application/json" \
|
|
107
|
+
-d '{
|
|
108
|
+
"emails": [
|
|
109
|
+
{"to": "user1@example.com", "subject": "Test 1", "body": "Body 1"},
|
|
110
|
+
{"to": "user2@example.com", "subject": "Test 2", "body": "Body 2"}
|
|
111
|
+
]
|
|
112
|
+
}'
|
|
113
|
+
|
|
114
|
+
# 紧急邮件(高优先级)
|
|
115
|
+
curl -X POST http://localhost:3000/examples/emails/send-urgent \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d '{
|
|
118
|
+
"to": "admin@example.com",
|
|
119
|
+
"subject": "Urgent",
|
|
120
|
+
"body": "This is urgent!"
|
|
121
|
+
}'
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 最佳实践
|
|
125
|
+
|
|
126
|
+
1. **任务幂等性**:确保任务可以安全重试
|
|
127
|
+
2. **合理设置重试**:根据业务场景设置重试次数和策略
|
|
128
|
+
3. **任务拆分**:大任务拆分成小任务,提高并发度
|
|
129
|
+
4. **监控告警**:监听失败事件,及时处理异常
|
|
130
|
+
5. **资源限制**:控制并发数,避免资源耗尽
|
|
131
|
+
|
|
132
|
+
## 更多信息
|
|
133
|
+
|
|
134
|
+
查看官方文档:https://751848178.github.io/svton/packages/nestjs-queue
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Controller, Post, Body } from '@nestjs/common';
|
|
2
|
+
import { EmailService } from './email.service';
|
|
3
|
+
import { EmailData } from './email.processor';
|
|
4
|
+
|
|
5
|
+
@Controller('examples/emails')
|
|
6
|
+
export class EmailController {
|
|
7
|
+
constructor(private readonly emailService: EmailService) {}
|
|
8
|
+
|
|
9
|
+
@Post('send')
|
|
10
|
+
async send(@Body() data: EmailData) {
|
|
11
|
+
await this.emailService.sendEmail(data);
|
|
12
|
+
return { message: 'Email queued successfully' };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Post('send-delayed')
|
|
16
|
+
async sendDelayed(
|
|
17
|
+
@Body() data: EmailData & { delayMs: number },
|
|
18
|
+
) {
|
|
19
|
+
await this.emailService.sendEmailDelayed(data, data.delayMs);
|
|
20
|
+
return { message: `Email will be sent after ${data.delayMs}ms` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Post('send-batch')
|
|
24
|
+
async sendBatch(@Body() data: { emails: EmailData[] }) {
|
|
25
|
+
await this.emailService.sendBatchEmails(data.emails);
|
|
26
|
+
return { message: `${data.emails.length} emails queued successfully` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Post('send-urgent')
|
|
30
|
+
async sendUrgent(@Body() data: EmailData) {
|
|
31
|
+
await this.emailService.sendUrgentEmail(data);
|
|
32
|
+
return { message: 'Urgent email queued successfully' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { Processor, Process, OnQueueCompleted, OnQueueFailed } from '@svton/nestjs-queue';
|
|
3
|
+
import { Job } from 'bullmq';
|
|
4
|
+
|
|
5
|
+
export interface EmailData {
|
|
6
|
+
to: string;
|
|
7
|
+
subject: string;
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@Processor({ name: 'email' })
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class EmailProcessor {
|
|
14
|
+
/**
|
|
15
|
+
* 处理发送邮件任务
|
|
16
|
+
*/
|
|
17
|
+
@Process('send')
|
|
18
|
+
async handleSend(job: Job<EmailData>) {
|
|
19
|
+
const { to, subject, body } = job.data;
|
|
20
|
+
|
|
21
|
+
console.log(`Sending email to ${to}...`);
|
|
22
|
+
console.log(`Subject: ${subject}`);
|
|
23
|
+
console.log(`Body: ${body}`);
|
|
24
|
+
|
|
25
|
+
// TODO: 实际项目中调用邮件服务
|
|
26
|
+
// await this.emailService.send(to, subject, body);
|
|
27
|
+
|
|
28
|
+
// 模拟发送延迟
|
|
29
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
30
|
+
|
|
31
|
+
console.log(`Email sent successfully to ${to}`);
|
|
32
|
+
|
|
33
|
+
return { success: true, to };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 处理批量发送任务
|
|
38
|
+
*/
|
|
39
|
+
@Process('sendBatch')
|
|
40
|
+
async handleSendBatch(job: Job<{ emails: EmailData[] }>) {
|
|
41
|
+
const { emails } = job.data;
|
|
42
|
+
|
|
43
|
+
console.log(`Sending ${emails.length} emails...`);
|
|
44
|
+
|
|
45
|
+
for (const email of emails) {
|
|
46
|
+
await this.handleSend({ data: email } as Job<EmailData>);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { success: true, count: emails.length };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 任务完成回调
|
|
54
|
+
*/
|
|
55
|
+
@OnQueueCompleted({ name: 'email' })
|
|
56
|
+
async onCompleted(job: Job) {
|
|
57
|
+
console.log(`Job ${job.id} completed successfully`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 任务失败回调
|
|
62
|
+
*/
|
|
63
|
+
@OnQueueFailed({ name: 'email' })
|
|
64
|
+
async onFailed(job: Job, error: Error) {
|
|
65
|
+
console.error(`Job ${job.id} failed:`, error.message);
|
|
66
|
+
// TODO: 发送告警通知
|
|
67
|
+
}
|
|
68
|
+
}
|