外观
控制器
控制器是用来处理 HTTP 请求和响应的。
命名约定
控制器以 c 开头命名:
typescript
export const cGetUserInfo: ExpressRequestHandler = async (req, res, next) => {
try {
const data = sGetUserInfo.parse(req.query);
const [err, result] = await getUserInfo(data);
if (err) {
res.fail(err.message);
return;
}
res.success<ReturnApiGetUserInfo>(result);
} catch (err) {
next(err);
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
统一响应格式
为方便日常开发,我们使用 ResponseMiddleware 中间件给 Express 的 Response 对象集成了 res.success、res.successPage 和 res.fail 方法用来处理的 JSON 响应,并添加了对应的类型声明。
对 Express 的拓展(src/middlewares/ResponseMiddleware.mts)
typescript
import {
createApiResponse,
pageData,
RESPONSE_CODE_ERROR,
RESPONSE_CODE_SUCCESS,
} from "#utils/ResponseUtils";
import { encrypt } from "#utils/EncryptUtils";
import { SHOULD_ENCRYPT_RESPONSE as shouldEncrypt } from "#utils/ConstantUtils";
import { sendSystemEmailSafe } from "#utils/EmailUtils";
// 添加res.success和res.fail方法,调用这些方法输出的响应格式符合API规范
export function responseMiddleware(): ExpressRequestHandler {
return function (req, res, next) {
// 成功响应的方法
res.success = function <T = unknown>(
data: T,
message = "Operation successful",
statusCode: TResponseCode = RESPONSE_CODE_SUCCESS,
) {
let finalData: T | string = data;
if (shouldEncrypt) {
finalData = encrypt(JSON.stringify(data));
}
const responseBody = createApiResponse<typeof finalData>({
data: finalData,
message,
code: statusCode,
encrypted: shouldEncrypt,
});
res.status(responseBody.code).json(responseBody);
};
// 成功响应分页数据
res.successPage = function <T>(
payload: ParamsPageData<T> & {
message?: string;
statusCode?: TResponseCode;
},
) {
const { list, total, pageNo, pageSize, statusCode, message } = payload;
const data = pageData<T>({
list,
total,
pageNo,
pageSize,
});
res.success(data, message, statusCode);
};
// 错误响应的方法(默认发送邮件告警)
res.fail = function (
message,
statusCode: TResponseCode = RESPONSE_CODE_ERROR,
alert?: ParamsResFailEmailAlert,
) {
// 默认发送邮件告警,除非显式禁用
if (!alert?.disableAlert) {
const alertText = [`[${req.method}] ${req.originalUrl}`, `错误:${message}`, alert?.text]
.filter(Boolean)
.join("\n");
void sendSystemEmailSafe({
subject: alert?.subject,
text: alertText,
ip: req.realIp,
});
}
const data = null;
const responseBody = createApiResponse<typeof data>({
data,
message,
code: statusCode,
});
if (req.accepts("text/html") && !req.accepts("application/json")) {
next(new Error(message));
return;
}
res.status(responseBody.code).json(responseBody);
};
next();
};
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
对 Express 类型的拓展(typings/express.global.d.ts)
typescript
declare global {
namespace Express {
interface Request {
user?: UserInfo;
realIp?: string;
}
interface Response {
/**
* 成功响应方法
* @param data - 响应数据
* @param message - 响应消息,默认为'Operation successful'
* @param statusCode - HTTP状态码,默认为200
*/
success<T = unknown>(data: T, message?: string, statusCode?: number): void;
/**
* 成功响应分页数据
* @param data - 响应数据
* @param message - 响应消息,默认为'Operation successful'
* @param statusCode - HTTP状态码,默认为200
*/
successPage<T = unknown>(
data: ParamsPageData<T>,
message?: string,
statusCode?: number,
): void;
/**
* 错误响应方法(默认发送邮件告警)
* @param message - 错误消息,默认为'An error occurred'
* @param statusCode - HTTP状态码,默认为500
* @param alert - 告警配置,可禁用告警或自定义主题
*/
fail(message: string, statusCode?: number, alert?: ParamsResFailEmailAlert): void;
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
res.sucess
成功响应。
函数签名(typings/express.global.d.ts)
typescript
/**
* 成功响应方法
* @param data - 响应数据
* @param message - 响应消息,默认为'Operation successful'
* @param statusCode - HTTP状态码,默认为200
*/
success<T = unknown>(data: T, message?: string, statusCode?: number): void;1
2
3
4
5
6
7
2
3
4
5
6
7
接口响应数据格式示例:
json
{
"code": 200,
"message": "success",
"encrypted": false,
"timestamp": 1762409800783,
"data": null
}1
2
3
4
5
6
7
2
3
4
5
6
7
res.sucessPage
专为分页场景设计的成功响应。
函数签名(typings/express.global.d.ts)
typescript
/**
* 成功响应分页数据
* @param data - 响应数据
* @param message - 响应消息,默认为'Operation successful'
* @param statusCode - HTTP状态码,默认为200
*/
successPage<T = unknown>(
data: ParamsPageData<T>,
message?: string,
statusCode?: number,
): void;1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
接口响应数据格式示例:
json
{
"code": 200,
"message": "success",
"encrypted": false,
"timestamp": 1762409800783,
"data": {
"list": [1, 4],
"total": 100,
"pageNo": 1,
"pageSize": 20
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
res.fail
失败响应。
函数签名(typings/express.global.d.ts)
typescript
/**
* 错误响应方法(默认发送邮件告警)
* @param message - 错误消息,默认为'An error occurred'
* @param statusCode - HTTP状态码,默认为500
* @param alert - 告警配置,可禁用告警或自定义主题
*/
fail(message: string, statusCode?: number, alert?: ParamsResFailEmailAlert): void;1
2
3
4
5
6
7
2
3
4
5
6
7
接口响应数据格式示例:
json
{
"code": 500,
"message": "错误信息",
"encrypted": false,
"timestamp": 1762409800783,
"data": null
}1
2
3
4
5
6
7
2
3
4
5
6
7
分页处理
分页字段约定
统一使用
在 src/utils/zodUtils.mts 中定义了分页相关的 Zod Schema,可以拿来复用。
typescript
import * as zod from "zod";
/**
* 用于分页的辅助 Zod Schema
*
* 说明:
* 因为分页查询多为 get 请求,实际传递的查询参数是字符串类型的,
* 所以需要使用 `zod.coerce.number()` 处理
*/
export const zodPageQueryCommon = {
pageNo: zod.coerce.number().int().positive("pageNo 必须为正整数"),
pageSize: zod.coerce.number().int().positive("pageSize 必须为正整数"),
};
export const zodSSHConfig = zod
.object({
host: zod.string().describe("服务器地址"),
port: zod.number().describe("服务器端口"),
username: zod.string().describe("用户名"),
password: zod.string().describe("密码"),
})
.describe("SSH配置");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
可以在控制器中这样使用:
typescript
export const sGetAlertRuleList = zod.object({
...zodPageQueryCommon,
projectId: zod.coerce.number().int().positive("项目ID必须为正整数"),
});
export const cGetAlertRuleList: ExpressRequestHandler = async (req, res, next) => {
try {
const query = sGetAlertRuleList.parse(req.query);
const { projectId, pageNo, pageSize } = query;
const knexQuery = knex("b_alert_rule").where({ project_id: projectId });
const offset = (pageNo - 1) * pageSize;
const [rows, countResult] = await Promise.all([
knexQuery.clone().offset(offset).limit(pageSize).orderBy("created_at", "desc"),
knexQuery.clone().count<{ total: number }[]>("* as total").first(),
]);
const total = countResult?.total || 0;
res.successPage({
list: rows,
pageNo,
pageSize,
total,
});
} catch (err) {
logger.error("getAlertRuleList error", err);
next(err);
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32