创建头像模块

avatar 目录下创建 avatar.controller.ts、avater.middleware.ts、avatar.router.ts、avatar.service.ts

avatar.controller.ts:

1
import { Request, Response, NextFunction } from "express";

avater.middleware.ts:

1
import { Request, Response, NextFunction } from "express";

avatar.router.ts:

1
2
3
4
5
6
7
8
9
import express from "express";
import * as avatarController from "./avatar.controller";

const router = express.Router();

/**
* 导出路由
*/
export default router;

avatar.service.ts:

1
import { connection } from "../app/database/mysql";

在 app.index.ts 中导入 router 并使用


定义文件过滤器

file.middleware.ts:

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
import multer, { FileFilterCallback } from "multer";
/**
* 文件过滤器
*/
export const fileFilter = (fileTypes: Array<string>) => {
return (
request: Request,
file: Express.Multer.File,
callback: FileFilterCallback
) => {
// 测试文件类型
const allowed = fileTypes.some((type) => type === file.mimetype);

if (allowed) {
// 允许上传
callback(null, true);
} else {
// 拒绝上传
callback(new Error("FILE_TYPE_NOT_ACCEPT")); //app.middleware.ts中统一处理错误
}
};
};

const fileUploadFilter = fileFilter(["image/png", "image/jpg", "image/jpeg"]);

/**
* 创建一个multer
*/
const fileUpload = multer({
dest: "uploads/",
fileFilter: fileUploadFilter,
});

创建上传头像中间件

avatar.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Request, Response, NextFunction } from "express";
import multer from "multer";
import { fileFilter } from "../file/file.middleware";

/**
* 文件过滤器
*/
const avatarUploadFilter = fileFilter(["image/png", "image/jpg", "image/jpeg"]);

/**
* 创建一个multer
*/
const avatarUpload = multer({
dest: "uploads/avatar",
fileFilter: avatarUploadFilter,
});

/**
* 文件拦截器
*/
export const avatarInterceptor = avatarUpload.single("avatar");

定义头像上传接口

avatar.controller.ts:

1
2
3
4
5
6
7
8
9
10
11
12
import { Request, Response, NextFunction } from "express";

/**
* 上传头像
*/
export const store = async (
request: Request,
response: Response,
next: NextFunction
) => {
response.sendStatus(200);
};

avatar.router.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import express from "express";
import * as avatarController from "./avatar.controller";
import { authGuard } from "../auth/auth.middleware";
import { avatarInterceptor } from "./avatar.middleware";

const router = express.Router();

/**
* 上传头像
*/
router.post("/avatar", authGuard, avatarInterceptor, avatarController.store);

/**
* 导出路由
*/
export default router;

调整头像的尺寸

avatar.middleware.ts:

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
import path from "path";
import Jimp from "jimp";
import { Request, Response, NextFunction } from "express";
import multer from "multer";
import { fileFilter } from "../file/file.middleware";

/**
* 文件过滤器
*/
const avatarUploadFilter = fileFilter(["image/png", "image/jpg", "image/jpeg"]);

/**
* 创建一个multer
*/
const avatarUpload = multer({
dest: "uploads/avatar",
fileFilter: avatarUploadFilter,
});

/**
* 文件拦截器
*/
export const avatarInterceptor = avatarUpload.single("avatar");

/**
* 头像处理器
*/
export const avatarProcessor = async (
request: Request,
response: Response,
next: NextFunction
) => {
// 准备文件信息
const { file } = request;

// 准备文件路径
const filePath = path.join(file.destination, "resized", "file.filename");

// 处理头像文件

try {
// 读取文件
const image = await Jimp.read(file.path);

// 调整尺寸
image
.cover(256, 256)
.quality(85)
.write(`${filePath}-large`);

image
.cover(128, 128)
.quality(85)
.write(`${filePath}-medium`);

image
.cover(64, 64)
.quality(85)
.write(`${filePath}-small`);
} catch (error) {
next(error);
}

// 下一步
next();
};

avatar.router.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import express from "express";
import * as avatarController from "./avatar.controller";
import { authGuard } from "../auth/auth.middleware";
import { avatarInterceptor, avatarProcessor } from "./avatar.middleware";

const router = express.Router();

/**
* 上传头像
*/
router.post(
"/avatar",
authGuard,
avatarInterceptor,
avatarProcessor,
avatarController.store
);

/**
* 导出路由
*/
export default router;

创建头像数据表

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `avatar`(
`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`mimetype` VARCHAR(255) NOT NULL,
`filename` VARCHAR(255) NOT NULL,
`size` INT(11) NOT NULL,
`userId` INT(11) NOT NULL,

FOREIGN KEY (`userId`) REFERENCES `user`(`id`)
ON DELETE NO ACTION ON UPDATE NO ACTION
) DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;

定义保存头像数据的功能

新建 avatar.model.ts:

1
2
3
4
5
6
7
export class AvatarModel {
id?: number;
mimetype?: string;
filename?: string;
size?: number;
userId?: number;
}

avatar.service.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { connection } from "../app/database/mysql";
import { AvatarModel } from "./avatar.model";

/**
* 保存头像文件信息
*/
export const createAvatar = async (avatar: AvatarModel) => {
// 准备查询
const statement = `
INSERT INTO avatar
SET ?
`;

// 执行查询
const [data] = await connection.promise().query(statement, avatar);

// 提供数据
return data;
};

保存头像数据

avatar.controller.ts:

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
import { Request, Response, NextFunction } from "express";
import _ from "lodash";
import { createAvatar } from "./avatar.service";

/**
* 上传头像
*/
export const store = async (
request: Request,
response: Response,
next: NextFunction
) => {
// 当前用户id
const { id: userId } = request.user;

// 头像文件信息
const fileInfo = _.pick(request.file, ["mimetype", "filename", "size"]);

// 准备头像数据
const avatar = {
...fileInfo,
userId,
};

try {
// 保存头像数据
const data = await createAvatar(avatar);

// 做出响应
response.sendStatus(201).send(data);
} catch (error) {
next(error);
}
};

定义按用户 ID 查找头像的功能

avatar.service.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 按用户ID查找头像
*/
export const findAvatarByUserId = async (userId: number) => {
// 准备查询
const statement = `
SELECT * FROM avatar
WHERE userId = ?
ORDER BY avatar.id DESC
LIMIT 1
`;

// 执行查询
const [data] = await connection.promise().query(statement, userId);

// 提供数据
return data[0];
};

定义头像服务接口

avatar.controller.ts:

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
import path from "path";
import fs from "fs";
import { Request, Response, NextFunction } from "express";
import _ from "lodash";
import { createAvatar, findAvatarByUserId } from "./avatar.service";
/**
* 头像服务
*/
export const serve = async (
request: Request,
response: Response,
next: NextFunction
) => {
// 用户id
const { userId } = request.params;

try {
// 查找头像信息
const avatar = await findAvatarByUserId(parseInt(userId, 10));
if (!avatar) {
throw new Error("FILE_NOT_FIND");
}

// 要提供的头像尺寸
const { size } = request.query;
// 文件名与目录
let filename = avatar.filename;
let root = path.join("uploads", "avatar");
let resized = "resized";

if (size) {
// 可用的头像尺寸
const imageSizes = ["large", "medium", "small"];
// 测试可用的头像尺寸
if (!imageSizes.some((item) => item == size)) {
throw new Error("FILE_NOT_FIND");
}

// 检查文件是否存在
const fileExist = fs.existsSync(
path.join(root, resized, `${filename}-${size}`)
);

if (!fileExist) {
throw new Error("FILE_NOT_FIND");
}
if (fileExist) {
filename = `${filename}-${size}`; // 文件名
root = path.join(root, resized); //目录路径
}
}
//做出响应
response.sendFile(filename, {
root,
headers: {
"Content-Type": avatar.mimetype,
},
});
} catch (error) {
next(error);
}
};

avatar.router.ts:

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
import express from "express";
import * as avatarController from "./avatar.controller";
import { authGuard } from "../auth/auth.middleware";
import { avatarInterceptor, avatarProcessor } from "./avatar.middleware";

const router = express.Router();

/**
* 上传头像
*/
router.post(
"/avatar",
authGuard,
avatarInterceptor,
avatarProcessor,
avatarController.store
);

/**
* 头像服务
*/
router.get("/users/:userId/avatar", avatarController.serve);

/**
* 导出路由
*/
export default router;

在内容中标记用户是否上传过头像

post.provider.ts:

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
/**
* 查询片段
*/
export const sqlFragment = {
user: `
JSON_OBJECT(
'id', user.id,
'name', user.name,
'avatar',IF(COUNT(avatar.id),1,null)
) as user
`,
leftJoinUser: `
LEFT JOIN user
ON user.id = post.userId
LEFT JOIN avatar
ON user.id = avatar.userId
`,
totalComments: `
(
SELECT
COUNT(comment.id)
FROM
comment
WHERE
comment.postId = post.id
) as totalComents
`,
leftJoinOneFile: `
LEFT JOIN LATERAL (
SELECT *
FROM file
WHERE file.postId = post.id
ORDER BY file.id DESC
LIMIT 1
) AS file ON post.id = file.postId
`,
file: `
CAST(
IF(
COUNT(file.id),
GROUP_CONCAT(
DISTINCT JSON_OBJECT(
'id',file.id,
'width',file.width,
'height',file.height
)
),
null
) AS JSON
) AS file
`,
leftJoinTag: `
LEFT JOIN
post_tag ON post_tag.postId = post.id
LEFT JOIN
tag ON post_tag.tagId = tag.id
`,
tags: `
CAST(
IF(
COUNT(tag.id),
CONCAT(
'[',
GROUP_CONCAT(
DISTINCT JSON_OBJECT(
'id',tag.id,
'name',tag.name
)
),
']'
),
NULL
) AS JSON
) AS tags
`,
};