@kne/fastify-message 1.0.13 → 1.0.14
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/libs/controllers/main.js +31 -12
- package/libs/controllers/statistics.js +11 -2
- package/libs/schema/response.js +33 -0
- package/package.json +1 -1
package/libs/controllers/main.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
2
|
|
|
3
|
+
const RECORD_TAG = '消息-发送记录';
|
|
4
|
+
const TEMPLATE_TAG = '消息-消息模版';
|
|
5
|
+
|
|
3
6
|
module.exports = fp(async (fastify, options) => {
|
|
4
7
|
const { services } = fastify[options.name];
|
|
5
8
|
|
|
@@ -9,16 +12,18 @@ module.exports = fp(async (fastify, options) => {
|
|
|
9
12
|
{
|
|
10
13
|
onRequest: options.getAuthenticate('record'),
|
|
11
14
|
schema: {
|
|
12
|
-
|
|
15
|
+
tags: [RECORD_TAG],
|
|
16
|
+
description: '分页获取消息发送记录列表,按创建时间倒序;perPage 最大 100',
|
|
13
17
|
summary: '查看发送记录',
|
|
14
18
|
querystring: {
|
|
15
19
|
type: 'object',
|
|
16
20
|
properties: {
|
|
17
21
|
currentPage: { type: 'integer', default: 1, description: '页码' },
|
|
18
|
-
perPage: { type: 'integer', default: 20, description: '
|
|
22
|
+
perPage: { type: 'integer', default: 20, description: '每页数量,超过 100 按 100 处理' },
|
|
19
23
|
filter: {
|
|
20
24
|
type: 'object',
|
|
21
25
|
default: {},
|
|
26
|
+
description: '筛选条件,均为精确匹配',
|
|
22
27
|
properties: {
|
|
23
28
|
type: { type: 'integer', description: '发送类型: 0=邮件, 1=短信' },
|
|
24
29
|
code: { type: 'string', description: '模板编码' },
|
|
@@ -29,6 +34,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
29
34
|
},
|
|
30
35
|
response: {
|
|
31
36
|
200: {
|
|
37
|
+
description: '发送记录分页结果',
|
|
32
38
|
type: 'object',
|
|
33
39
|
properties: {
|
|
34
40
|
totalCount: { type: 'integer', description: '总数量' },
|
|
@@ -36,8 +42,10 @@ module.exports = fp(async (fastify, options) => {
|
|
|
36
42
|
perPage: { type: 'integer', description: '每页数量' },
|
|
37
43
|
pageData: {
|
|
38
44
|
type: 'array',
|
|
45
|
+
description: '当前页发送记录列表',
|
|
39
46
|
items: {
|
|
40
47
|
type: 'object',
|
|
48
|
+
description: '发送记录',
|
|
41
49
|
properties: {
|
|
42
50
|
id: { type: 'string', description: '记录ID' },
|
|
43
51
|
name: { type: 'string', description: '发送对象(邮箱/手机号)' },
|
|
@@ -58,12 +66,12 @@ module.exports = fp(async (fastify, options) => {
|
|
|
58
66
|
async request => {
|
|
59
67
|
const { currentPage = 1, perPage = 20, filter = {} } = request.query;
|
|
60
68
|
const { type, code, name } = filter;
|
|
61
|
-
|
|
69
|
+
|
|
62
70
|
const where = {};
|
|
63
71
|
if (type !== undefined) where.type = type;
|
|
64
72
|
if (code) where.code = code;
|
|
65
73
|
if (name) where.name = name;
|
|
66
|
-
|
|
74
|
+
|
|
67
75
|
return await services.record.list({ filter: where, perPage: Math.min(perPage, 100), currentPage });
|
|
68
76
|
}
|
|
69
77
|
);
|
|
@@ -74,7 +82,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
74
82
|
{
|
|
75
83
|
onRequest: options.getAuthenticate('template:send'),
|
|
76
84
|
schema: {
|
|
77
|
-
|
|
85
|
+
tags: [TEMPLATE_TAG],
|
|
86
|
+
description: '使用指定模版渲染内容并通过对应类型的发送器(邮件 / 短信)发送,同时写入发送记录;模版不存在返回 404,模版已禁用返回 400',
|
|
78
87
|
summary: '发送消息',
|
|
79
88
|
body: {
|
|
80
89
|
type: 'object',
|
|
@@ -82,11 +91,12 @@ module.exports = fp(async (fastify, options) => {
|
|
|
82
91
|
properties: {
|
|
83
92
|
templateId: { type: 'string', description: '模版ID' },
|
|
84
93
|
name: { type: 'string', description: '发送对象(邮箱/手机号)' },
|
|
85
|
-
props: { type: 'object', description: '
|
|
94
|
+
props: { type: 'object', description: '模版变量,用于渲染模版内容,不传为空对象', additionalProperties: true }
|
|
86
95
|
}
|
|
87
96
|
},
|
|
88
97
|
response: {
|
|
89
98
|
200: {
|
|
99
|
+
description: '发送成功',
|
|
90
100
|
type: 'object',
|
|
91
101
|
properties: {
|
|
92
102
|
success: { type: 'boolean', description: '是否发送成功' }
|
|
@@ -117,7 +127,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
117
127
|
{
|
|
118
128
|
onRequest: options.getAuthenticate('record'),
|
|
119
129
|
schema: {
|
|
120
|
-
|
|
130
|
+
tags: [RECORD_TAG],
|
|
131
|
+
description: '获取单条消息发送记录详情,记录不存在返回 404',
|
|
121
132
|
summary: '查看发送记录详情',
|
|
122
133
|
params: {
|
|
123
134
|
type: 'object',
|
|
@@ -128,6 +139,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
128
139
|
},
|
|
129
140
|
response: {
|
|
130
141
|
200: {
|
|
142
|
+
description: '发送记录详情',
|
|
131
143
|
type: 'object',
|
|
132
144
|
properties: {
|
|
133
145
|
id: { type: 'string', description: '记录ID' },
|
|
@@ -162,16 +174,18 @@ module.exports = fp(async (fastify, options) => {
|
|
|
162
174
|
{
|
|
163
175
|
onRequest: options.getAuthenticate('template'),
|
|
164
176
|
schema: {
|
|
165
|
-
|
|
177
|
+
tags: [TEMPLATE_TAG],
|
|
178
|
+
description: '分页获取消息模版列表,按创建时间倒序;perPage 最大 100',
|
|
166
179
|
summary: '查看消息模版',
|
|
167
180
|
querystring: {
|
|
168
181
|
type: 'object',
|
|
169
182
|
properties: {
|
|
170
183
|
currentPage: { type: 'integer', default: 1, description: '页码' },
|
|
171
|
-
perPage: { type: 'integer', default: 20, description: '
|
|
184
|
+
perPage: { type: 'integer', default: 20, description: '每页数量,超过 100 按 100 处理' },
|
|
172
185
|
filter: {
|
|
173
186
|
type: 'object',
|
|
174
187
|
default: {},
|
|
188
|
+
description: '筛选条件,均为精确匹配',
|
|
175
189
|
properties: {
|
|
176
190
|
type: { type: 'integer', description: '模版类型: 0=邮件, 1=短信' },
|
|
177
191
|
code: { type: 'string', description: '模版编码' },
|
|
@@ -183,6 +197,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
183
197
|
},
|
|
184
198
|
response: {
|
|
185
199
|
200: {
|
|
200
|
+
description: '消息模版分页结果',
|
|
186
201
|
type: 'object',
|
|
187
202
|
properties: {
|
|
188
203
|
totalCount: { type: 'integer', description: '总数量' },
|
|
@@ -190,8 +205,10 @@ module.exports = fp(async (fastify, options) => {
|
|
|
190
205
|
perPage: { type: 'integer', description: '每页数量' },
|
|
191
206
|
pageData: {
|
|
192
207
|
type: 'array',
|
|
208
|
+
description: '当前页消息模版列表',
|
|
193
209
|
items: {
|
|
194
210
|
type: 'object',
|
|
211
|
+
description: '消息模版',
|
|
195
212
|
properties: {
|
|
196
213
|
id: { type: 'string', description: '模版ID' },
|
|
197
214
|
name: { type: 'string', description: '模版名称' },
|
|
@@ -212,13 +229,13 @@ module.exports = fp(async (fastify, options) => {
|
|
|
212
229
|
async request => {
|
|
213
230
|
const { currentPage = 1, perPage = 20, filter = {} } = request.query;
|
|
214
231
|
const { type, code, level, status } = filter;
|
|
215
|
-
|
|
232
|
+
|
|
216
233
|
const where = {};
|
|
217
234
|
if (type !== undefined) where.type = type;
|
|
218
235
|
if (code) where.code = code;
|
|
219
236
|
if (level !== undefined) where.level = level;
|
|
220
237
|
if (status !== undefined) where.status = status;
|
|
221
|
-
|
|
238
|
+
|
|
222
239
|
return await services.template.list({ filter: where, perPage: Math.min(perPage, 100), currentPage });
|
|
223
240
|
}
|
|
224
241
|
);
|
|
@@ -229,7 +246,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
229
246
|
{
|
|
230
247
|
onRequest: options.getAuthenticate('template'),
|
|
231
248
|
schema: {
|
|
232
|
-
|
|
249
|
+
tags: [TEMPLATE_TAG],
|
|
250
|
+
description: '获取单个消息模版详情,模版不存在返回 404',
|
|
233
251
|
summary: '查看消息模版详情',
|
|
234
252
|
params: {
|
|
235
253
|
type: 'object',
|
|
@@ -240,6 +258,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
240
258
|
},
|
|
241
259
|
response: {
|
|
242
260
|
200: {
|
|
261
|
+
description: '消息模版详情',
|
|
243
262
|
type: 'object',
|
|
244
263
|
properties: {
|
|
245
264
|
id: { type: 'string', description: '模版ID' },
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const { success, statistics } = require('../schema/response');
|
|
3
|
+
|
|
4
|
+
const TAG = '消息-统计';
|
|
2
5
|
|
|
3
6
|
module.exports = fp(async (fastify, options) => {
|
|
4
7
|
const { services } = fastify[options.name];
|
|
@@ -8,7 +11,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
8
11
|
{
|
|
9
12
|
onRequest: options.getAuthenticate('statistics'),
|
|
10
13
|
schema: {
|
|
14
|
+
tags: [TAG],
|
|
11
15
|
summary: '获取消息统计概览',
|
|
16
|
+
description: '按时间范围汇总消息发送数量、类型/模板分布、每日与按小时趋势,以及模板数量统计;不支持的 range 报错,其他查询异常时返回空统计',
|
|
12
17
|
query: {
|
|
13
18
|
type: 'object',
|
|
14
19
|
properties: {
|
|
@@ -17,7 +22,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
17
22
|
type: { type: 'integer', description: '按消息类型筛选: 0=邮件, 1=短信' },
|
|
18
23
|
code: { type: 'string', description: '按模板编码筛选' }
|
|
19
24
|
}
|
|
20
|
-
}
|
|
25
|
+
},
|
|
26
|
+
response: success('消息统计数据', statistics)
|
|
21
27
|
}
|
|
22
28
|
},
|
|
23
29
|
async request => {
|
|
@@ -32,7 +38,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
32
38
|
{
|
|
33
39
|
onRequest: options.getAuthenticate('statistics'),
|
|
34
40
|
schema: {
|
|
41
|
+
tags: [TAG],
|
|
35
42
|
summary: 'SSE实时推送消息统计数据',
|
|
43
|
+
description: '以 text/event-stream 长连接按 interval 推送当天的实时统计(发送总数、类型/模板分布、按小时趋势等),客户端断开后停止;range 参数当前未使用',
|
|
36
44
|
query: {
|
|
37
45
|
type: 'object',
|
|
38
46
|
properties: {
|
|
@@ -42,7 +50,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
42
50
|
code: { type: 'string', description: '按模板编码筛选' },
|
|
43
51
|
interval: { type: 'integer', minimum: 1, default: 5, description: '推送间隔时间(秒),最小1秒,默认5秒' }
|
|
44
52
|
}
|
|
45
|
-
}
|
|
53
|
+
},
|
|
54
|
+
response: { 200: { description: 'text/event-stream 事件流,每条 data 为当天实时统计 JSON' } }
|
|
46
55
|
}
|
|
47
56
|
},
|
|
48
57
|
async (request, reply) => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 新增的响应 schema 只用于 OpenAPI 文档:字段可空、对象允许额外字段,避免序列化改变实际输出
|
|
2
|
+
const str = description => ({ type: 'string', nullable: true, description });
|
|
3
|
+
const num = description => ({ type: 'number', nullable: true, description });
|
|
4
|
+
// 结构不固定的字段不声明类型,原样输出
|
|
5
|
+
const any = description => ({ description });
|
|
6
|
+
const obj = (description, properties = {}) => ({ type: 'object', nullable: true, additionalProperties: true, description, properties });
|
|
7
|
+
|
|
8
|
+
const success = (description, schema) => ({ 200: Object.assign({}, schema || { type: 'object', additionalProperties: true }, { description }) });
|
|
9
|
+
|
|
10
|
+
const MESSAGE_TYPE = '消息类型:0 邮件,1 短信';
|
|
11
|
+
|
|
12
|
+
const statistics = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: true,
|
|
15
|
+
properties: {
|
|
16
|
+
range: str('时间范围:7d / 1m / 3m / 1y'),
|
|
17
|
+
rangeLabel: str('时间范围名称,如「近7天」'),
|
|
18
|
+
totalRecords: num('范围内发送的消息总数'),
|
|
19
|
+
byType: any(`按消息类型统计的发送数,键为类型(${MESSAGE_TYPE})`),
|
|
20
|
+
byCode: any('按模板编码统计的发送数,键为模板编码'),
|
|
21
|
+
recentTrend: any('每日趋势,每项为 { date, count }'),
|
|
22
|
+
recentTrendByType: any('每日按类型趋势,每项为 { date, type, count }'),
|
|
23
|
+
hourlyTrend: any('按小时趋势,每项为 { date, hour, count }'),
|
|
24
|
+
hourlyTrendByType: any('按小时、类型趋势,每项为 { date, hour, type, count }'),
|
|
25
|
+
templateStats: obj('模板统计(不受时间范围筛选)', {
|
|
26
|
+
total: num('模板总数'),
|
|
27
|
+
byStatus: any('按状态统计的模板数,键为状态(0 启用,1 禁用)'),
|
|
28
|
+
byType: any(`按类型统计的模板数,键为类型(${MESSAGE_TYPE})`)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = { str, num, any, obj, success, MESSAGE_TYPE, statistics };
|