nodejs学习笔记10-项目实战

mysql2

1. 项目功能接口说明

  • 完整的项目接口包括:
    • 面向用户的业务接口;
    • 面向企业或者内部的后台管理接口;
  • 课堂上完成的功能如下:
    1. 用户管理系统
    2. 内容管理系统
    3. 内容评论管理
    4. 内容标签管理
    5. 文件管理系统
    6. 等等

2. 项目的搭建

2.1. 初始化项目

项目基于koa框架开发

终端命令

1
2
3
4
5
6
7
npm init -y
npm i nodemon -D
npm i koa
npm i koa-router
npm i mysql2
npm i dotenv
npm i koa-bodyparser

2.2. 目录结构的划分

  • 按照功能模块划分;
  • 按照业务模块划分;

该项目按照功能模块划分:

1
2
3
4
5
6
7
8
9
10
|- node_modules
|- src
|- app
|- controller
|- router 路由设计
|- service 与数据库相关处理
|- utils
|- index.js
|- package-lock.js
|- package.js

2.3. 创建和启动服务器

2.3.1. 应用配置信息写到环境变量

  • 编写.env文件,放在根目录下

    1
    APP_PORT = 8000
  • 通过dotenv加载配置的变量

    /src/app/ 中添加 config.js

    1
    2
    3
    4
    5
    6
    const env = require("dotenv");
    env.config();

    module.exports = {
    APP_PORT
    } = process.env;

2.3.2. 创建服务器

/src/app 中添加 index.js

1
2
3
4
const koa = require("koa");
const app = new koa();

module.exports = app;

src/index.js 中添加代码

1
2
3
4
5
6
7
8
9
// 环境变量
const {APP_PORT} = require("./app/config");

// app处理模块
const app = require("./app");

app.listen(APP_PORT,()=>{
console.log(`开启 ${APP_PORT} 端口`);
});

2.3.3. 添加命令

package.json 中添加命令

1
2
3
4
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon ./src/index.js"
}

2.3.4. 启动服务器

终端输入

1
npm start

在终端输出

1
2
3
4
5
6
7
8
9
> coderhub@1.0.0 start
> nodemon ./src/index.js

[nodemon] 2.0.19
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./src/index.js`
开启 8000 端口

表示服务正常启动

3. 用户管理系统

3.1. 用户注册接口编写流程

3.1.1. 注册用户路由router编写

src/router下新建 user.router.js

1
2
3
4
5
6
7
8
9
10
const Router = require("koa-router");
const useRouter = new Router({prefix: "/user"});

const { create } = require("../controller/user.controller");
// 判断 空值,已存在 中间件
const { verifyUser } = require("../middleware/user.middleware");

useRouter.post("/",verifyUser,create);

module.exports = useRouter;

src/app/index.js中引入 user.router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const koa = require("koa");
const app = new koa();

// 解析 json
const bodyparser = require("koa-bodyparser");

// 错误处理
const errorHandler = require("./errorHandler");

// 用户路由
const useRouter = require("../router/user.router");


app.use(bodyparser());
app.use(useRouter.routes());
app.use(useRouter.allowedMethods());

// 处理错误信息
app.on("error",errorHandler);

module.exports = app;

3.1.2. 处理函数的控制器controller编写

src/controller 下新建 user.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const {create} = require("../service/user.service");

class UserController{
async create(ctx,next){
// 获取用户请求传递的参数
const user = ctx.request.body;

// 查询数据
const result = await create(user);

// 返回数据
ctx.body = result;
};
}

module.exports = new UserController();

3.1.3. 创建数据库连接

.env中添加数据库配置

1
2
3
4
5
DATABASE_HOST = localhost
DATABASE_PORT = 3306
DATABASE_USER = root
DATABASE_PASSWORD = 密码
DATABASE = coderhub

src/app/config.js中添加数据

1
2
3
4
5
6
7
8
9
10
11
const env = require("dotenv");
env.config();

module.exports = {
APP_PORT,
DATABASE_HOST,
DATABASE_PORT,
DATABASE_USER,
DATABASE_PASSWORD,
DATABASE
} = process.env;

src/app下新建database.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const mysql = require("mysql2");
const config = require("./config");

const connection = mysql.createPool({
host: config.DATABASE_HOST,
port: config.DATABASE_PORT,
user: config.DATABASE_USER,
password: config.DATABASE_PASSWORD,
database: config.DATABASE,
connectionLimit: 10
});

connection.getConnection((err,con)=>{
if(err){
console.log("数据库连接失败: "+err);
}else{
console.log("数据库连接成功");
}

})

module.exports = connection.promise();

3.1.4. 测试数据库连接是否成功

/index.js中导入数据库连接

1
require("./app/database");

运行 npm start

终端输出 数据库连接成功 表示连接成功

3.1.5. 操作数据库的service编写

src/service下新建 user.service.js

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
const mysql = require("../app/database");

class UserService{
// 注册用户
async create(user){
// 解构
const {name,password} = user;
// 将user存储到数据库中
const statement = 'insert into users (name,password) values (?,?)';
const result = await mysql.execute(statement,[name,password]);
// console.log(result);
// 返回结果
return result;
}
// 查找用户名是否存在
async getUserByName(name){
// 将user存储到数据库中
const statement = 'select * from users where name = ?';
const result = await mysql.execute(statement,[name]);
// console.log(result);
// 返回结果
return result[0];
}
}

module.exports = new UserService();

3.1.6. 错误统一处理

HTTP响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status

  • 封装错误处理函数

    src下新建 constants文件夹,新建 errorType.js

    1
    2
    3
    4
    5
    6
    7
    const REQUIRED = 'field is required';
    const NAME_EXIST = 'username is exist';

    module.exports = {
    REQUIRED,
    NAME_EXIST
    }

    src/app下新建 errorHandler.js

    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
    const { REQUIRED, NAME_EXIST } = require("../constants/errorType");

    const errorHandler = (error,ctx)=>{
    // console.log(error.message);
    let status,message;
    switch (error.message){
    case REQUIRED:
    // Bad Request
    status = 400;
    message = '值不能为空';
    break;
    case NAME_EXIST:
    // conflict
    status = 409;
    message = '该用户名已存在';
    break;
    default:
    status = 404;
    message = 'NOT FOUND';

    }

    ctx.status = status;
    ctx.body = message;
    }

    module.exports = errorHandler;
  • app监听错误事件

    src/app/index.js 中对应错误监听

    1
    2
    3
    const errorHandler = require("./app/errorHandler");
    // 处理错误信息
    app.on("error",errorHandler);

3.1.7. 注册用户校验

src下新建 middleware 文件夹,新建 user.middleware.js

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
const { REQUIRED, NAME_EXIST } = require("../constants/errorType");
const { getUserByName } = require("../service/user.service");

const verifyUser = async (ctx,next)=>{
// 得到输入的用户名和密码
const {name,password} = ctx.request.body;
// 判断是否为 undefined or ''
if(!name || !password){
// 返回错误信息
const error = new Error(REQUIRED);
return ctx.app.emit("error",error,ctx);
}
// 判断用户名是否已经存在于数据库
const nameExist = await getUserByName(name);
// console.log(nameExist.length>0);
if(nameExist.length>0){
// 返回错误信息
const error = new Error(NAME_EXIST);
return ctx.app.emit("error",error,ctx);
}
await next();
}

module.exports = {
verifyUser
}

3.1.8. 密码加密存储

src/utils 下新建 passwordHandler.js

1
2
3
4
5
6
7
8
9
const crypto = require("crypto");

const md5Password = (password)=>{
const md5 = crypto.createHash("md5");
const result = md5.update(password).digest('hex');
return result;
}

module.exports = md5Password;

src/middleware/user.middleware.js中添加方法

1
2
3
4
5
6
7
8
9
10
const md5Password = require("../utils/passwordHandler");
const handlePassword = async (ctx,next)=>{
let { password } = ctx.request.body;
ctx.request.body.password = md5Password(password);
await next();
}
module.exports = {
verifyUser,
handlePassword
}

src/router/user.router.js中添加 中间件

1
2
3
const { verifyUser, handlePassword } = require("../middleware/user.middleware");

useRouter.post("/",verifyUser,handlePassword,create);

3.2. 用户登录接口编写流程

3.2.1. 授权router的编写

src/router中新建auth.router.js

1
2
3
4
5
6
7
8
9
const Router = require("koa-router");
const { login } = require("../controller/auth.controller");
const { verifyLogin } = require("../middleware/auth.middleware");

const authRouter = new Router({prefix: "/login"});

authRouter.post("/",verifyLogin,login);

module.exports = authRouter;

3.2.2. 处理函数的controller编写

src/controller 中新建 auth.controller.js

1
2
3
4
5
6
7
class AuthController {
async login(ctx,next){
const {name,password} = ctx.request.body;
ctx.body = `登录成功,欢迎${name}`;
}
}
module.exports = new AuthController();

3.2.3. 验证的中间件

  • 账号和密码是否为空;
  • 用户名是否存在;
  • 校验密码是否一致;

编写错误代码 src/constans/errorType.js

1
2
3
4
5
6
7
8
9
10
11
const REQUIRED = 'field is required';
const NAME_EXIST = 'username is exist';
const NAME_NOT_EXISTS = 'username is not exist';
const PASSWORD_NOT_SAME = 'password is not same';

module.exports = {
REQUIRED,
NAME_EXIST,
NAME_NOT_EXISTS,
PASSWORD_NOT_SAME
}

src/middleware中新建 auth.middleware.js

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
const { REQUIRED, NAME_NOT_EXISTS, PASSWORD_NOT_SAME } = require("../constants/errorType");
const { getUserByName } = require("../service/user.service");
const md5Password = require("../utils/passwordHandler");

const verifyLogin = async (ctx,next)=>{
// 1.获取用户名和密码
const {name,password} = ctx.request.body;

// 2.判断用户名和密码是否为空
if(!name||!password){
const error = new Error(REQUIRED);
return ctx.app.emit("error",err,ctx);
}

// 3.判断用户是否存在
const result = await getUserByName(name);
// console.log(result[0]);
if(result.length == 0){
const error = new Error(NAME_NOT_EXISTS);
return ctx.app.emit("error",error,ctx);
}

// 4.判断密码是否和数据库中的密码一致
if(md5Password(password) != result[0].password){
const error = new Error(PASSWORD_NOT_SAME);
return ctx.app.emit("error",error,ctx);
}
await next();
}

module.exports = {
verifyLogin
}

处理错误信息 src/app/errorHandle.js

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
const { REQUIRED, NAME_EXIST, NAME_NOT_EXISTS, PASSWORD_NOT_SAME } = require("../constants/errorType");

const errorHandler = (error,ctx)=>{
// console.log(error.message);
let status,message;
switch (error.message){
case REQUIRED:
// Bad Request
status = 400;
message = '值不能为空';
break;
case NAME_EXIST:
// conflict
status = 409;
message = '该用户名已存在';
break;
case NAME_NOT_EXISTS:
// Bad Request
status = 400;
message = '该用户名不存在';
break;
case PASSWORD_NOT_SAME:
// Bad Request
status = 400;
message = '密码不正确';
break;
default:
status = 404;
message = 'NOT FOUND';

}

ctx.status = status;
ctx.body = message;
}

module.exports = errorHandler;

3.2.4. 整合路由

src/router中新建 index.js ,这样就可以自动扫描所有路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require("fs");

const useRoutes = function(){
fs.readdirSync(__dirname).forEach(file=>{
// console.log(file);
if(file == 'index.js'){
return;
}
const router = require(`./${file}`);
this.use(router.routes());
this.use(router.allowedMethods());
})
}

module.exports = useRoutes;

注册路由 src/app/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const koa = require("koa");
const app = new koa();

// 解析 json
const bodyparser = require("koa-bodyparser");
const useRoutes = require("../router");

// 错误处理
const errorHandler = require("./errorHandler");

app.use(bodyparser());

// 注册路由
app.useRoutes = useRoutes;
app.useRoutes();

// 处理错误信息
app.on("error",errorHandler);

module.exports = app;

3.3. 登录成功返回凭证

3.3.1. 为什么需要登录凭证呢?

  • web开发中,使用最多的协议是http,但是http是一个无状态的协议

    • 无状态的协议?什么叫做无状态协议呢?
  • 举个例子:

    • 我们登录了一个网站 www.coderhub.com;
    • 登录的时候需要输入用户名和密码:比如用户名coderwhy,密码:Coderwhy666.;
    • 登录成功之后,以coderwhy的身份去访问其他的数据和资源,还是通过http请求去访问。
      • coderhub的服务器会问:你谁呀?
      • coderwhy说:我是coderwhy呀,刚刚登录过呀;
      • coderhub:怎么证明你刚刚登录过呀?
      • coderwhy说:这。。。,http没有告诉你吗?
      • coderhub:http的每次请求对我来说都是一个单独的请求,和之前请求过什么没有关系。
    • 这就是http的无状态,也就是服务器不知道上一步做了什么,必须得有一个办法证明登录过

3.3.2. 认识cookie

  • Cookie(复数形态Cookies),又称为“小甜饼”。类型为“小型文本文件,某些网站为了辨别用户身份而存储在用户本地终端(Client Side)上的数据
    • 浏览器会在特定的情况下携带上cookie来发送请求,通过cookie来获取一些信息;
  • Cookie总是保存在客户端中,按在客户端中的存储位置,Cookie可以分为内存Cookie和硬盘Cookie
    • 内存Cookie由浏览器维护,保存在内存中,浏览器关闭时Cookie就会消失,其存在时间是短暂的;
    • 硬盘Cookie保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理;
  • 如果判断一个cookie是内存cookie还是硬盘cookie呢?
    • 没有设置过期时间,默认情况下cookie是内存cookie,在关闭浏览器时会自动删除;
    • 有设置过期时间,并且过期时间不为0或者负数的cookie,是硬盘cookie,需要手动或者到期时,才会删除;

3.3.3. cookie常见的属性

  • cookie的生命周期
    • 默认情况下的cookie是内存cookie,也称之为会话cookie,也就是在浏览器关闭时会自动被删除;
  • 通过设置expires或者max-age来设置过期的时间
    • expires:设置的是Date.toUTCString(),设置格式是;expires=date-in-GMTString-format;
    • max-age:设置过期的秒钟; max-age=max-age-in-seconds (例如一年为606024*365);
  • cookie的作用域:(允许cookie发送给哪些URL)
  • Domain:指定哪些主机可以接受cookie
    • 如果不指定,那么默认是 origin,不包括子域名。
    • 如果指定Domain,则包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。
  • Path:指定主机下哪些路径可以接受cookie
    • 例如,设置 Path=/docs,则以下地址都会匹配:
      • /docs
      • /docs/Web/
      • /docs/Web/HTTP

3.3.4. 客户端设置cookie

  • js直接设置和获取cookie:

  • 这个cookie会在会话关闭时被删除掉;

    1
    document.cookie = "name = li";
  • 设置cookie,同时设置过期时间(默认单位是秒钟)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>
    <body>
    <button>按钮</button>
    <script>
    const btn = document.getElementsByTagName("button")[0];
    btn.onclick = function(){
    document.cookie = "name = li; max-age = 50"
    }
    </script>
    </body>
    </html>

    浏览器界面

3.3.5. 服务器设置cookie

  • Koa中默认支持直接操作cookie
    • /login 请求中设置cookie
    • /use 请求中获取cookie
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
const koa = require("koa");
const app = new koa();

const Router = require("koa-router");
const loginRouter = new Router();

loginRouter.get("/login",(ctx,next)=>{
ctx.cookies.set("name","lilei",{
/**
* a number representing the milliseconds from Date.now() for expiry
*/
maxAge: 5 * 1000
});
ctx.body = "设置cookie";
});

const useRouter = new Router();

useRouter.get("/use",(ctx,next)=>{
const name = ctx.cookies.get("name");
ctx.body = name;
});

app.use(loginRouter.routes());
app.use(loginRouter.allowedMethods());

app.use(useRouter.routes());
app.use(useRouter.allowedMethods());

app.listen(8989,()=>{
console.log("开启 8989");
});

3.3.6. Session是基于cookie实现机制

在koa中,我们可以借助于 koa-session 来实现session认证

1
npm i koa-session

实现

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
const koa = require("koa");
const app = new koa();

const Router = require("koa-router");
const loginRouter = new Router();

const Session = require("koa-session");
const session = Session({
key: "sessionid",
/**
* maxAge in ms
*/
maxAge: 10 * 1000,
// 签名-防止数据被篡改
signed: true
},app);
// 秘钥
app.keys = ["test"];
app.use(session);

loginRouter.get("/login",(ctx,next)=>{
const user = {
id: 1,
name: 'root'
}
ctx.session.user = user;
ctx.body = "设置session";
});

const useRouter = new Router();

useRouter.get("/use",(ctx,next)=>{
const user = ctx.session.user;
ctx.body = user;
});

app.use(loginRouter.routes());
app.use(loginRouter.allowedMethods());

app.use(useRouter.routes());
app.use(useRouter.allowedMethods());

app.listen(8990,()=>{
console.log("开启 8990");
});

session是存在cookie中的

3.3.7. 认识token

  • cookie和session的方式有很多的缺点
    • Cookie会被附加在每个HTTP请求中,所以无形中增加了流量(事实上某些请求是不需要的);
    • Cookie是明文传递的,所以存在安全性的问题;
    • Cookie的大小限制是4KB,对于复杂的需求来说是不够的;
    • 对于浏览器外的其他客户端(比如iOS、Android),必须手动的设置cookie和session;
    • 对于分布式系统和服务器集群中如何可以保证其他系统也可以正确的解析session?
      • 不同系统分属于不同服务器
      • session属于一次会话,request.getSession(),在不同服务器中,request请求对象不同,session不同
  • 所以,在目前的前后端分离的开发过程中,使用token来进行身份验证的是最多的情况:
    • token可以翻译为令牌;
    • 也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;
    • 这个令牌作为后续用户访问一些接口或者资源的凭证;
    • 根据这个凭证来判断用户是否有权限来访问;
  • 所以token的使用应该分成两个重要的步骤:
    • 生成token:登录的时候,颁发token;
    • 验证token:访问某些资源或者接口时,验证token;

3.3.8. JWT实现Token机制

  • JWT生成的Token由三部分组成

  • header

    • alg:采用的加密算法,默认是 HMAC SHA256(HS256),采用同一个密钥进行 加密和解密;
    • typ:JWT,固定值,通常都写成JWT即可;
    • 会通过base64Url算法进行编码;
  • payload

  • 携带的数据,比如我们可以将用户的id和name放到payload中;

  • 默认也会携带iat(issued at),令牌的签发时间;

  • 也可以设置过期时间:exp(expiration time);

  • 会通过base64Url算法进行编码

  • signature

  • 设置一个secretKey,通过将前两个的结果合并后进行HMACSHA256的算法;

  • HMACSHA256(base64Url(header)+.+base64Url(payload), secretKey);

  • 但是如果secretKey暴露是一件非常危险的事情,因为之后就可以模拟颁发token, 也可以解密token;

3.3.9. Token的使用

当然,在真实开发中,我们可以直接使用一个库来完成: npm i jsonwebtoken;

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
const koa = require("koa");
const app = new koa();

const Router = require("koa-router");
const loginRouter = new Router();

const jwt = require("jsonwebtoken");

const privateKey = "coderHub";

loginRouter.get("/login",(ctx,next)=>{
const user = {
id: 1,
name: 'root'
}
const token = jwt.sign(user,privateKey,{
// expressed in seconds
expiresIn: 60
})
ctx.body = "设置token "+token;
});

const useRouter = new Router();

useRouter.get("/use",(ctx,next)=>{
try{
const authorization = ctx.headers.authorization;
const token = authorization.replace("Bearer ","");
const user = jwt.verify(token,privateKey);
ctx.body = user;
}
catch(err){
return ctx.app.emit("error",err,ctx);
}
});

app.use(loginRouter.routes());
app.use(loginRouter.allowedMethods());

app.use(useRouter.routes());
app.use(useRouter.allowedMethods());

app.on("error",(error,ctx)=>{
ctx.body = error.message;
})

app.listen(8990,()=>{
console.log("开启 8990");
});

3.3.10. 非对称加密

  • HS256加密算法一单密钥暴露就是非常危险的事情

    • 比如在分布式系统中,每一个子系统都需要获取到密钥;
    • 那么拿到这个密钥后这个子系统既可以发布另外,也可以验证令牌;
    • 但是对于一些资源服务器来说,它们只需要有验证令牌的能力就可以了;
  • 可以使用非对称加密,RS256:

    • 私钥(private key):用于发布令牌;
    • 公钥(public key):用于验证令牌;
  • 我们可以使用openssl来生成一对私钥和公钥:

    • Mac直接使用terminal终端即可;
    • Windows默认的cmd终端是不能直接使用的,建议直接使用git bash终端;
1
2
3
 openssl
> genrsa -out private.key 1024
> rsa -in private.key -pubout -out public.key

genrsa -out private.key 1024 使用RS256算法生成大小为1024字节的私钥,输出到private.key文件

rsa -in private.key -pubout -out public.key 以私钥为参考生成公钥

3.3.11. 使用公钥和私钥签发和验证签名

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
const koa = require("koa");
const app = new koa();

const Router = require("koa-router");
const loginRouter = new Router();

const jwt = require("jsonwebtoken");

const fs = require("fs");
const path = require("path");
const privateKey = fs.readFileSync(path.resolve(__dirname,"private.key"));
const publicKey = fs.readFileSync(path.resolve(__dirname,"public.key"));

loginRouter.get("/login",(ctx,next)=>{
const user = {
id: 1,
name: 'root'
}
// 派发令牌
const token = jwt.sign(user,privateKey,{
// expressed in seconds
expiresIn: 60,
algorithm: 'RS256'
})
ctx.body = "设置token "+token;
});

const useRouter = new Router();

useRouter.get("/use",(ctx,next)=>{
try{
// 验证令牌
const authorization = ctx.headers.authorization;
const token = authorization.replace("Bearer ","");
const user = jwt.verify(token,publicKey,{
algorithms: ['RS256']
});
ctx.body = user;
}
catch(err){
return ctx.app.emit("error",err,ctx);
}
});

app.use(loginRouter.routes());
app.use(loginRouter.allowedMethods());

app.use(useRouter.routes());
app.use(useRouter.allowedMethods());

app.on("error",(error,ctx)=>{
ctx.body = error.message;
})

app.listen(8990,()=>{
console.log("开启 8990");
});

3.3.12. 项目中加入token

src/app中新建文件夹 keys ,导入 private.keypublic.key

src/app/config.js中引入变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const env = require("dotenv");
env.config();

const fs = require("fs");
const path = require("path");

// 放入token key
const PRIVATE_KEY = fs.readFileSync(path.resolve(__dirname,"keys/private.key"));
const PUBLIC_KEY = fs.readFileSync(path.resolve(__dirname,"keys/public.key"));

module.exports = {
APP_PORT,
DATABASE_HOST,
DATABASE_PORT,
DATABASE_USER,
DATABASE_PASSWORD,
DATABASE
} = process.env;

module.exports.PRIVATE_KEY = PRIVATE_KEY;
module.exports.PUBLIC_KEY = PUBLIC_KEY;

src/constants/errorType.js中添加错误信息

1
2
3
4
5
6
7
8
9
10
11
12
13
const REQUIRED = 'field is required';
const NAME_EXIST = 'username is exist';
const NAME_NOT_EXISTS = 'username is not exist';
const PASSWORD_NOT_SAME = 'password is not same';
const UNAUTHORIZED = "Unauthorized";

module.exports = {
REQUIRED,
NAME_EXIST,
NAME_NOT_EXISTS,
PASSWORD_NOT_SAME,
UNAUTHORIZED
}

src/app/errorHandler.js中处理错误信息

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
const { REQUIRED, NAME_EXIST, NAME_NOT_EXISTS, PASSWORD_NOT_SAME, UNAUTHORIZED } = require("../constants/errorType");

const errorHandler = (error,ctx)=>{
// console.log(error.message);
let status,message;
switch (error.message){
case REQUIRED:
// Bad Request
status = 400;
message = '值不能为空';
break;
case NAME_EXIST:
// conflict
status = 409;
message = '该用户名已存在';
break;
case NAME_NOT_EXISTS:
// Bad Request
status = 400;
message = '该用户名不存在';
break;
case PASSWORD_NOT_SAME:
// Bad Request
status = 400;
message = '密码不正确';
break;
case UNAUTHORIZED:
// unauthorized
status = 401;
message = '用户没有访问权限';
break;
default:
status = 404;
message = 'NOT FOUND';

}

ctx.status = status;
ctx.body = message;
}

module.exports = errorHandler;

src/middleware/auth.middleware.js中添加验证令牌代码

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
const { REQUIRED, NAME_NOT_EXISTS, PASSWORD_NOT_SAME, UNAUTHORIZED } = require("../constants/errorType");
const { getUserByName } = require("../service/user.service");
const md5Password = require("../utils/passwordHandler");

const jwt = require("jsonwebtoken");
const { PUBLIC_KEY } = require("../app/config");

const verifyLogin = async (ctx,next)=>{
// 1.获取用户名和密码
const {name,password} = ctx.request.body;

// 2.判断用户名和密码是否为空
if(!name||!password){
const error = new Error(REQUIRED);
return ctx.app.emit("error",err,ctx);
}

// 3.判断用户是否存在
const result = await getUserByName(name);
// console.log(result[0]);
if(result.length == 0){
const error = new Error(NAME_NOT_EXISTS);
return ctx.app.emit("error",error,ctx);
}

const user = result[0];
// 4.判断密码是否和数据库中的密码一致
if(md5Password(password) != user.password){
const error = new Error(PASSWORD_NOT_SAME);
return ctx.app.emit("error",error,ctx);
}

ctx.user = user;
await next();
}

const verifyAuth = async (ctx,next)=>{
try {
const authorization = ctx.headers.authorization;
const token = authorization.replace("Bearer ","");
const user = jwt.verify(token,PUBLIC_KEY,{
algorithms: ['RS256']
});
ctx.user = user;
await next();
} catch (error) {
const err = new Error(UNAUTHORIZED);
return ctx.app.emit("error",err,ctx);
}
}

module.exports = {
verifyLogin,
verifyAuth
}

src/controller/auth.controller.js中颁发令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const jwt = require("jsonwebtoken");
const { PRIVATE_KEY } = require("../app/config");

class AuthController {
async login(ctx,next){
// console.log(ctx.user);
const {id,name} = ctx.user;

const token = jwt.sign({id,name},PRIVATE_KEY,{
expiresIn: 60 * 60 * 24,
algorithm: "RS256"
});
ctx.body = {
id,
name,
token
};
}

async test(ctx,next){
ctx.body = ctx.user;
}
}
module.exports = new AuthController();

src/router/auth.router.js中添加测试 /test

1
2
3
4
5
6
7
8
9
10
11
const Router = require("koa-router");
const { login, test } = require("../controller/auth.controller");
const { verifyLogin, verifyAuth } = require("../middleware/auth.middleware");

const authRouter = new Router({prefix: "/login"});

authRouter.post("/",verifyLogin,login);

authRouter.get("/test",verifyAuth,test);

module.exports = authRouter;

3.3.13. Postman自动添加token

  1. Postman中添加全局变量

    1
    2
    const res = pm.response.json();
    pm.globals.set("token",res.token)
  2. 添加token

  3. 测试 /test

4. 内容管理系统

4.1. 创建新的表 moment

1
2
3
4
5
6
7
8
create table if not exists comments(
id int primary key auto_increment,
content varchar(1000) not null,
user_id int not null,
createAt timestamp default current_timestamp,
updateAt timestamp default current_timestamp on update current_timestamp,
foreign key(user_id) references users(id)
);

4.2. 实现功能

  • 定义发布动态内容的接口

    • 定义路由接口

    • 验证用户登录

    • Controller和Service中处理内容

  • 定义修改动态内容的接口

    • 定义路由接口
    • 验证用户登录
    • 验证用户的权限
    • Controller和Service中的处理
  • 定义删除内容的接口

    • 定义路由接口
    • 验证用户登录
    • 验证用户权限
    • Controller和Service的处理
  • 定义查询单个内容的接口

  • 根据momentId查询接口内容;

  • 定义查询多条内容的接口

  • 查询所有moment接口内容(根据offset和limit决定查询数量)

4.3. moment.router.js

src/router 中新建 moment.router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const Router = require("koa-router");

const momentRouter = new Router({prefix:"/moment"});

const { verifyAuth, verifyPermission } = require("../middleware/auth.middleware");
const { create, getById, list, update, deleteById } = require("../controller/moment.controller");

momentRouter.post("/upload",verifyAuth,create);

momentRouter.get("/get/:momentId",getById);

momentRouter.get("/list",list);

// 更新
momentRouter.patch("/update/:momentId",verifyAuth,verifyPermission,update);

// 删除
momentRouter.delete("/delete/:momentId",verifyAuth,verifyPermission,deleteById);

// 内容添加标签

module.exports = momentRouter;

4.4. moment.controller.js

src/controller 中新建 moment.controller.js

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
const { create, getMomentById, getMomentList, update, deleteById } = require("../service/moment.service");

class MomentController{
async create(ctx,next){
const {id} = ctx.user;
const content = ctx.request.body.content;
const result = await create({id,content});

ctx.body = result;
}

async getById(ctx,next){
const id = ctx.params.id;

const result = await getMomentById(id);

ctx.body = result;
}

async list(ctx,next){
const {offset,size} = ctx.query;
// console.log(ctx.query);
const result = await getMomentList(offset,size);
ctx.body = result;
}

async update(ctx,next){
const id = ctx.params.id;
const content = ctx.request.body.content;
const userId = ctx.user.id;
const result = await update(id,content,userId);
ctx.body = result;
}

async deleteById(ctx,next){
const id = ctx.params.id;
const userId = ctx.user.id;
const result = await deleteById(id,userId);
ctx.body = result;
}

}

module.exports = new MomentController();

4.5. moment.service.js

src/service 中新建 moment.service.js

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
const mysql = require("../app/database");

const mysqlFragment = `
select
m.id as id, m.content as content,
json_object("id",u.id,"name",u.name) as user
from moments m
left join users u on
m.user_id = u.id
`;

class MomentService{
async create(user){
// console.log(user);
const statement = "insert into moments (content,user_id) values (?,?)";
const [result] = await mysql.execute(statement,[user.content,user.id]);
// console.log(result);
return result;
}

async getMomentById(id){
// console.log(id);
const statement = `${mysqlFragment} where m.id = ? `;
const [result] = await mysql.execute(statement,[id]);
// console.log(result);
return result[0];
}

async getMomentList(offest,size){
const statement = `${mysqlFragment} limit ?,?`;
const [result] = await mysql.execute(statement,[offest,size]);
// console.log(result);
return result;
}

async update(id,content,userId){
// console.log(id,content,userId);
const statement = `update moments set content = ? where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[content,id,userId]);
return result;
}

async deleteById(id,userId){
const statement = `delete from moments where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[id,userId]);
return result;
}
}

module.exports = new MomentService();

4.6. 重点 - 用户权限功能

面对不同的登录用户,应该因为user_id不同,限制登录用户进行其他的user_id的更新,删除内容

src/middleware/auth.middleware.js中添加代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const { checkMoment } = require("../service/auth.service");

const verifyPermission = async (ctx,next)=>{
const result = await checkMoment(ctx.params.id,ctx.user.id);
// console.log(result);
if(result){
await next();
}else{
const error = new Error(UNPERMISSION);
return ctx.app.emit("error",error,ctx);
}

}

module.exports = {
verifyLogin,
verifyAuth,
verifyPermission
}

src/service/auth.service.js中添加验证代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const mysql = require("../app/database");

class AuthService{
async checkMoment(momentId,userId){
const statement = `select * from moments where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[momentId,userId]);
// console.log(result);
if(result.length == 0){
return false;
}
return true;
}
}

module.exports = new AuthService();

src/constans/errorType.js中添加错误代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const REQUIRED = 'field is required';
const NAME_EXIST = 'username is exist';
const NAME_NOT_EXISTS = 'username is not exist';
const PASSWORD_NOT_SAME = 'password is not same';
const UNAUTHORIZED = "Unauthorized";
const UNPERMISSION = "Unpermission";

module.exports = {
REQUIRED,
NAME_EXIST,
NAME_NOT_EXISTS,
PASSWORD_NOT_SAME,
UNAUTHORIZED,
UNPERMISSION
}

src/app/errorHandler.js中添加处理错误代码

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
const { REQUIRED, NAME_EXIST, NAME_NOT_EXISTS, PASSWORD_NOT_SAME, UNAUTHORIZED, UNPERMISSION } = require("../constants/errorType");

const errorHandler = (error,ctx)=>{
// console.log(error.message);
let status,message;
switch (error.message){
case REQUIRED:
// Bad Request
status = 400;
message = '值不能为空';
break;
case NAME_EXIST:
// conflict
status = 409;
message = '该用户名已存在';
break;
case NAME_NOT_EXISTS:
// Bad Request
status = 400;
message = '该用户名不存在';
break;
case PASSWORD_NOT_SAME:
// Bad Request
status = 400;
message = '密码不正确';
break;
case UNAUTHORIZED:
// unauthorized
status = 401;
message = 'token无效';
break;
case UNPERMISSION:
// Forbidden
status = 403;
message = '用户没有操作权限';
break;
default:
status = 404;
message = 'NOT FOUND';

}

ctx.status = status;
ctx.body = message;
}

module.exports = errorHandler;

5. 评论管理系统

5.1. 创建新的表 comment

1
2
3
4
5
6
7
8
9
10
11
12
13
create table if not exists comments(
id int primary key auto_increment,
content varchar(1000) not null,
moment_id int not null,
user_id int not null,
comment_id int default null,
createAt timestamp default current_timestamp,
updateAt timestamp default current_timestamp on update current_timestamp,

foreign key(moment_id) references moments(id) on update cascade on delete cascade,
foreign key(user_id) references users(id) on update cascade on delete cascade,
foreign key(comment_id) references comments(id) on update cascade on delete cascade
);

5.2. 实现功能

  • 定义发布评论内容的接口
    • 定义路由接口
    • 验证用户登录
    • Controller和Service中处理内容
  • 定义修改评论内容的接口
    • 定义路由接口
    • 验证用户登录
    • 验证用户的权限
    • Controller和Service中的处理
  • 定义删除评论内容的接口
    • 定义路由接口
    • 验证用户登录
    • 验证用户权限
    • Controller和Service的处理
  • 查询动态的时候,同时显示评论信息
  • 查询多个动态时,显示评论的个数
  • 查询单个动态时,显示评论的列表

5.3. comment.router.js

src/router中新建comment.router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Router = require("koa-router");
const { create, reply, update, deleteById, getByMomentId } = require("../controller/comment.controller");
const commentRouter = new Router({prefix: "/comment"});
const { verifyAuth, verifyPermission } = require("../middleware/auth.middleware");

commentRouter.post("/",verifyAuth,create);

// 回复
commentRouter.post("/:commentId/reply",verifyAuth,reply);

// 更新
commentRouter.patch("/:commentId",verifyAuth,verifyPermission,update);

// 删除
commentRouter.delete("/:commentId",verifyAuth,verifyPermission,deleteById);

// 获取评论
commentRouter.get("/",getByMomentId);

module.exports = commentRouter;

5.4. comment.controller.js

src/router 中新建 comment.controller.js

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
const { create, reply, update, deleteById, getByMomentId } = require("../service/comment.service");

class CommentController{
async create(ctx,next){
const {momentId,content} = ctx.request.body;
const userId = ctx.user.id;

const result = await create(userId,momentId,content);
ctx.body = result;
}

async reply(ctx,next){
const userId = ctx.user.id;
const commentId = ctx.params.commentId;
const {momentId,content} = ctx.request.body;

const result = await reply(userId,momentId,commentId,content);
ctx.body = result;
}

async update(ctx,next){
const commentId = ctx.params.commentId;
const content = ctx.request.body.content;
const userId = ctx.user.id;
const result = await update(commentId,userId,content);
ctx.body = result;
}

async deleteById(ctx,next){
const commentId = ctx.params.commentId;
const userId = ctx.user.id;
// console.log(commentId,userId);
const result = await deleteById(commentId,userId);
ctx.body = result;
}

async getByMomentId(ctx,next){
const momentId = ctx.query.momentId;

const result = await getByMomentId(momentId);

ctx.body = result;
}
}

module.exports = new CommentController();

5.5. comment.service.js

src/service中新建 comment.service.js

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
const mysql = require("../app/database");

class CommentService{
async create(userId,momentId,content){
// console.log(userId,momentId,content);

const statement = `insert into comments (moment_id, user_id,content) values (?,?,?)`;
const [result] = await mysql.execute(statement,[momentId,userId,content]);
// console.log(result);
return result;
}

async reply(userId,momentId,commentId,content){
const statement = `insert into comments (comment_id,moment_id, user_id,content) values (?,?,?,?)`;
const [result] = await mysql.execute(statement,[commentId,momentId,userId,content]);
// console.log(result);
return result;
}

async update(commentId,userId,content){
// console.log(content,commentId,userId);
const statement = `update comments set content = ? where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[content,commentId,userId]);
// console.log(result);
return result;
}

async deleteById(commentId,userId){
// console.log(commentId,userId);
const statement = `delete from comments where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[commentId,userId]);
// console.log(result);
return result;
}

async getByMomentId(momentId){
const statement = `
select
c.id id,c.content content,c.comment_id commentId,c.createAt createAt,c.updateAt updateAt,
json_object("id",u.id,"name",u.name) user
from comments c
left join users u on
c.user_id = u.id
where c.moment_id = ?
`;
const [result] = await mysql.execute(statement,[momentId]);
return result;
}
}

module.exports = new CommentService();

5.6. 修改auth验证权限

src/middleware/auth.middleware.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const verifyPermission = async (ctx,next)=>{
try{
const [key] = Object.keys(ctx.params);
const tableName = key.replace("Id","s");
const id = ctx.params[key];
const userId = ctx.user.id;
// console.log(tableName,id,userId);
const result = await checkResource(tableName,id,userId);
// console.log(result);
if(result){
await next();
}else{
throw new Error();
}
}catch(err){
const error = new Error(UNPERMISSION);
return ctx.app.emit("error",error,ctx);
}

}

src/service/auth.service.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const mysql = require("../app/database");

class AuthService{
async checkResource(tableName,id,userId){
// console.log(tableName,id,userId);
const statement = `select * from ${tableName} where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[id,userId]);
// console.log(result);
if(result.length == 0){
return false;
}
return true;
}
}

module.exports = new AuthService();

5.7. 修改内容获取列表

src/service/moment.service.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const mysqlFragment = `
select
m.id id, m.content content,m.createAt createAt,m.updateAt updateAt,
json_object("id",u.id,"name",u.name) author,
json_arrayagg(
json_object("id",c.id,"content",c.content,"commentId",c.comment_id,
"createAt",m.createAt,"updateAt",m.updateAt,
"user",json_object("id",us.id,"name",us.name)
)
) comments
from moments m
left join users u on
m.user_id = u.id
left join comments c on
c.moment_id = m.id
left join users us on
us.id = c.user_id
group by m.id
`;

6. 内容标签管理系统

6.1. 创建标签的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
create table if not exists labels(
id int primary key auto_increment,
name varchar(255) not null unique,
user_id int not null,
createAt timestamp default current_timestamp,
updateAt timestamp default current_timestamp on update current_timestamp,
foreign key(user_id) references users(id) on delete cascade on update cascade
);

create table if not exists moment_label(
moment_id int not null,
label_id int not null,
createAt timestamp default current_timestamp,
updateAt timestamp default current_timestamp on update current_timestamp,
primary key(moment_id,label_id),
foreign key(moment_id) references moments(id) on delete cascade on update cascade,
foreign key(label_id) references labels(id) on delete cascade on update cascade
);

6.2. 实现功能

  • 定义创建标签接口
  • 路由配置Router
  • 验证用户登录
  • 创建标签
  • 创建标签和动态关系表
  • 定义给动态添加标签的接口
  • 给动态添加新的接口
  • 查询标签接口
  • 查询动态列表,展示标签数量
  • 查询动态详情,展示标签列表

6.3. label.router.js

src/router 中新建 label.router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Router = require("koa-router");

const labelRouter = new Router({prefix:"/label"});

const { create, list, update, deleteById } = require("../controller/label.controller");
const {verifyAuth, verifyPermission} = require("../middleware/auth.middleware");

labelRouter.post("/",verifyAuth,create);

labelRouter.get("/",list);

labelRouter.patch("/:labelId",verifyAuth, verifyPermission ,update);

labelRouter.delete("/:labelId",verifyAuth, verifyPermission ,deleteById);

module.exports = labelRouter;

6.4. label.controller.js

src/controller 中新建 label.controller.js

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
const { create, list, update, deleteById } = require("../service/label.service");

class LabelController{
async create(ctx,next){
const {name} = ctx.request.body;
const userId = ctx.user.id;
const result = await create(name,userId);
ctx.body = result;
}

async list(ctx,next){
const {offset,size} = ctx.query;
// console.log(ctx.query);
const result = await list(offset,size);
ctx.body = result;
}

async update(ctx,next){
const labelId = ctx.params.labelId;
const name = ctx.request.body.name;
const userId = ctx.user.id;
const result = await update(name,labelId,userId);
ctx.body = result;
}

async deleteById(ctx,next){
const labelId = ctx.params.labelId;
const userId = ctx.user.id;
const result = await deleteById(labelId,userId);
ctx.body = result;
}
}

module.exports = new LabelController();

6.5. label.middleware.js

src/middleware中新建 label.middleware.js

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
const { create, getLabelByName } = require("../service/label.service");

const verifyLabelExists = async (ctx,next)=>{
const userId = ctx.user.id;
// 取出所有标签
const{labels} = ctx.request.body;
// 判断标签是否存在
const newLabels = [];
for(let name of labels){
const labelResult = await getLabelByName(name);
// console.log(!labelResult);
const label = {name};
if(!labelResult){
const result = await create(name,userId);
// console.log(result);
label.id = result.insertId;
}else{
label.id = labelResult.id;
}
newLabels.push(label);
}
ctx.labels = newLabels;
// console.log(newLabels);
await next();
}

module.exports = {
verifyLabelExists
}

6.6. label.service.js

src/service 中新建 label.service.js

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
const mysql = require("../app/database");

class LabelService{
async create(name,userId){
const statement = `insert into labels (name,user_id) values (?,?)`;
const [result] = await mysql.execute(statement,[name,userId]);
return result;
}

async getLabelByName(name){
const statement = `select * from labels where name = ?`;
const [result] = await mysql.execute(statement,[name]);
return result[0];
}

async hasLabel(momentId,labelId){
const statement = `select * from moment_label where moment_id = ? and label_id = ?`;
const [result] = await mysql.execute(statement,[momentId,labelId]);
return result[0] ? true: false;
}

async createMomentLabel(momentId,labelId){
const statement = `insert into moment_label (moment_id,label_id) values (?,?)`;
const [result] = await mysql.execute(statement,[momentId,labelId]);
return result;
}

async list(offset,size){
const statement = `select * from labels limit ?,?`;
const [result] = await mysql.execute(statement,[offset,size]);
return result;
}

async update(name,labelId,userId){
// console.log(name,labelId);
const statement = `update labels set name = ? where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[name,labelId,userId]);
return result;
}

async deleteById(labelId,userId){
// console.log(labelId,userId);
const statement = `delete from labels where id = ? and user_id = ?`;
const [result] = await mysql.execute(statement,[labelId,userId]);
return result;
}

}

module.exports=new LabelService();

6.7. moment添加标签

src/router/moment.router.js

1
2
// 内容添加标签
momentRouter.post("/:momentId/labels",verifyAuth,verifyPermission,verifyLabelExists,addLabels);

src/controller/moment.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async addLabels(ctx,next){
// 获取标签和动态id
const labels = ctx.labels;
const momentId = ctx.params.momentId;

// 添加所有标签
for(let label of labels){
// 判断标签是否已经关联内容
const exist = await hasLabel(momentId,label.id);
if(!exist){
const result = await createMomentLabel(momentId,label.id);
}
}
ctx.body = "内容添加标签";
}

src/service/moment.service.js

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
const mysqlFragment = `
select
m.id id, m.content content,m.createAt createAt,m.updateAt updateAt,
json_object("id",u.id,"name",u.name) author,
(select count(*) from comments c where c.moment_id = m.id) commentCount,
(select count(*) from moment_label ml where ml.moment_id = m.id) labelCount,
(select
if(count(c.id),
json_arrayagg(
json_object("id",c.id,"content",c.content,"commentId",c.comment_id,"createAt",m.createAt,"updateAt",m.updateAt,
"user",json_object("id",us.id,"name",us.name)
)
),null)
from comments c
left join users us on
us.id = c.user_id
where c.moment_id = m.id
)comments,
if(count(l.id),json_arrayagg(
json_object("id",l.id,"name",l.name)
) ,null) labels
from moments m
left join users u on
u.id = m.user_id
left join moment_label ml on
ml.moment_id = m.id
left join labels l on
l.id = ml.label_id
group by m.id
`;

7. 文件管理系统

7.1. 创建头像表

1
2
3
4
5
6
7
8
9
10
create table if not exists avatars(
id int primary key auto_increment,
filename varchar(1000) not null,
mimetype varchar(255) not null,
size int,
user_id int,
createAt timestamp default current_timestamp,
updateAt timestamp default current_timestamp on update current_timestamp,
foreign key(user_id) references users(id) on delete cascade on update cascade
);

7.2. 创建文件表

1
2
3
4
5
6
7
8
9
10
11
12
create table if not exists files(
id int primary key auto_increment,
filename varchar(1000) not null,
mimetype varchar(255) not null,
size int,
moment_id int,
user_id int,
createAt timestamp default current_timestamp,
updateAt timestamp default current_timestamp on update current_timestamp,
foreign key(user_id) references users(id) on delete cascade on update cascade,
foreign key(moment_id) references moments(id) on delete cascade on update cascade
);

7.3. 更新用户头像列

1
alter table users add avatar_url varchar(255);

7.4. 实现功能

  • 上传头像逻辑

    • 定义上传图像的接口
    • 定义获取图像的接口
    • 请求用户信息时,获取头像
  • 实现思路

    1. 图片(文件)上传 /upload/avatar
      • 目的:服务器端可以保存一张图片
    2. 提供一个接口,可以让用户获取图片
      • /1/avatar -> 找到图片 -> 读取图片content-type: image/jpeg -> 返回图像的信息
    3. 将URL存储到用户信息中
      • avatarURL:头像的地址
    4. 获取信息时,获取用户的头像
  • 上传动态的配图

  • 定义上传动态配图的接口

  • 定义获取动态配图的接口

  • 获取动态时,获取配图信息

7.5. 下载插件

1
2
npm i @koa/multer
npm i jimp

7.6. file.router.js

src/router 中新建file.router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Router = require("koa-router");
const { saveAvatarInfo, savePictureInfo } = require("../controller/file.controller");

const { verifyAuth } = require("../middleware/auth.middleware");
const { avatarHandler, pictureHandler, pictureResize } = require("../middleware/file.middleware");

const fileRouter = new Router({prefix:"/upload"});


fileRouter.post("/avatar",verifyAuth,avatarHandler,saveAvatarInfo);

fileRouter.post("/picture",verifyAuth,pictureHandler,pictureResize,savePictureInfo);

module.exports = fileRouter;

7.7. file.controller.js

src/controller 中新建 file.controller.js

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
const { createAvatar, createFiles } = require("../service/file.service");
const { updateAvatarUrl } = require("../service/user.service");
const { APP_HOST, APP_PORT } = require("../app/config");

class FileController{
async saveAvatarInfo(ctx,next){
/**
* {
fieldname: 'avatar',
originalname: 'LanternsLarge.png',
encoding: '7bit',
mimetype: 'image/png',
destination: './upload',
filename: '1659167959021.png',
path: 'upload/1659167959021.png',
size: 13625
}
*/
const {mimetype,filename,size} = ctx.request.file;
// console.log(ctx.request.file);
const userId = ctx.user.id;
const result = await createAvatar(mimetype,filename,size,userId);

// 图片路由放入users表
const avatarUrl = `${APP_HOST}:${APP_PORT}/user/${userId}/avatar`;
// console.log(avatarUrl);
await updateAvatarUrl(avatarUrl,userId);

ctx.body = result;
}

async savePictureInfo(ctx,next){
const files = ctx.request.files;
const {momentId} = ctx.query;
const userId = ctx.user.id;
// console.log(files);
for(let file of files){
const {mimetype,filename,size} = file;
// console.log(mimetype,filename,size,userId,momentId);
await createFiles(mimetype,filename,size,userId,momentId);
}
ctx.body = "内容配图上传成功";

}


}

module.exports = new FileController();

7.8. 更新用户头像链接

src/service/user.service.js

1
2
3
4
5
6
7
async updateAvatarUrl(avatarUrl,userId){
// console.log(avatarUrl,userId);
const statement = `update users set avatar_url = ? where id = ?`;
const [result] = await mysql.execute(statement,[avatarUrl,userId]);
// console.log(result);
return result;
}

7.9. 文件存储路径

src/constants/filePath.js

1
2
3
4
5
6
7
const AVATAR_PATH = "./uploads/avatar/";
const PICTURE_PATH = "./uploads/picture/";

module.exports = {
AVATAR_PATH,
PICTURE_PATH
};

7.10. file.middle.js

src/middleware 中新建file.middleware.js

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
const path = require("path");
const Multer = require("@koa/multer");
const Jimp = require("jimp");

const {AVATAR_PATH,PICTURE_PATH} = require("../constants/filePath");

const avatar = Multer({
dest: AVATAR_PATH
});
const avatarHandler = avatar.single("avatar");


const picture = Multer({
dest: PICTURE_PATH
});
const pictureHandler = picture.array("picture",9);

const pictureResize = async (ctx,next)=>{
const files = ctx.request.files;

for(let file of files){
const filePath = path.join(file.destination,file.filename);
Jimp.read(file.path).then(image=>{
image.resize(1280,Jimp.AUTO).write(`${filePath}-large`);
image.resize(640,Jimp.AUTO).write(`${filePath}-middle`);
image.resize(320,Jimp.AUTO).write(`${filePath}-small`);
})
}
await next();
}


module.exports = {
avatarHandler,
pictureHandler,
pictureResize
};

7.11. file.service.js

src/service中新建 file.service.js

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
const mysql = require("../app/database");
class FileService{

async createAvatar(mimetype,filename,size,userId){
const statement = `insert into avatars (mimetype,filename,size,user_id) values (?,?,?,?)`;
const [result] = await mysql.execute(statement,[mimetype,filename,size,userId]);
return result;
}

async getAvatarById(userId){
const statement = `select * from avatars where user_id = ?`;
const [result] = await mysql.execute(statement,[userId]);
return result[0];
}

async createFiles(mimetype,filename,size,userId,momentId){
const statement = `insert into files (mimetype,filename,size,user_id,moment_id) values (?,?,?,?,?)`;
const [result] = await mysql.execute(statement,[mimetype,filename,size,userId,momentId]);
return result;
}

async getFileByName(name){
const statement = `select * from files where filename = ?`;
const [result] = await mysql.execute(statement,[name]);
return result[0];
}
}

module.exports = new FileService();

7.12. 添加头像访问链接

src/router/user.router.js

1
2
// 获取头像
useRouter.get("/:userId/avatar",getAvatarInfo);

src/controller/user.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
const { getAvatarById } = require("../service/file.service");

async getAvatarInfo(ctx,next){
const {userId} = ctx.params;
// console.log(userId);
const result = await getAvatarById(userId);
// console.log(result);
ctx.response.set("content-type",result.mimetype);
// console.log(AVATAR_PATH+result.filename);
ctx.body = fs.createReadStream(AVATAR_PATH+result.filename);

}

7.13. 添加图片访问链接

src/router/moment.router.js

1
2
// 内容配置
momentRouter.get("/images/:filename",fileInfo);

src/controller/moment.controller.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { getFileByName } = require("../service/file.service");

async fileInfo(ctx,next){
const {filename} = ctx.params;
const {type} = ctx.query;
const result = await getFileByName(filename);
ctx.response.set("content-type",result.mimetype);
let path = PICTURE_PATH;

if(!type){
path += result.filename;
}else{
path += result.filename+ "-"+type;
}
// console.log(path);
ctx.body = fs.createReadStream(path);
}

7.14. moment添加内容配图

src/service/moment.service.js

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
const sql = `${APP_HOST}:${APP_PORT}/moment/images/`;

const mysqlFragment = `
select
m.id id, m.content content,m.createAt createAt,m.updateAt updateAt,
json_object("id",u.id,"name",u.name,"avatarUrl",u.avatar_url) author,
(select count(*) from comments c where c.moment_id = m.id) commentCount,
(select count(*) from moment_label ml where ml.moment_id = m.id) labelCount,
(select
if(count(c.id),
json_arrayagg(
json_object("id",c.id,"content",c.content,"commentId",c.comment_id,"createAt",m.createAt,"updateAt",m.updateAt,
"user",json_object("id",us.id,"name",us.name,"avatarUrl",us.avatar_url)
)
),null)
from comments c
left join users us on
us.id = c.user_id
where c.moment_id = m.id
)comments,
if(count(l.id),json_arrayagg(
json_object("id",l.id,"name",l.name)
) ,null) labels,
(select
json_arrayagg(concat("${sql}",f.filename))
from files f where f.moment_id = m.id
) images
from moments m
left join users u on
u.id = m.user_id
left join moment_label ml on
ml.moment_id = m.id
left join labels l on
l.id = ml.label_id
group by m.id
`;

8. 部署云服务器

8.1. 购买云服务器

8.1.1. 注册阿里云的账号

  • 云服务器有很多的选择:阿里云、腾讯云、华为云

  • 目前在公司使用比较多的是阿里云;

  • 需要注册阿里云账号

  • https://aliyun.com/

8.1.2. 购买云服务器

购买云服务器其实是购买一个实例

  1. 来到控制台

  2. 创建实例,选择类型和配置

  3. 配置网络安全组

  4. 创建实例

8.2. 配置云服务器

8.2.1. 连接云服务器

  • 通常情况下,通过ssh连接云服务器:

    • Windows电脑上我推荐直接使用git bash ssh工具;

    • Mac OS电脑上我们可以直接通过终端来使用ssh工具;

      1
      ssh root@<<公网ip>>
  • 知识点补充:如果在计算机中想要更改主机名

    • 修改之后需要重启服务器

      1
      hostnamectl --static set-hostname coderhub

8.2.2. vscode连接云服务器

安装插件 【remote-ssh】

连接云服务器,输入 ssh root@<<公网ip>>

输入密码

输入文件地址

8.2.3. 安装Node.js

  • 安装软件使用工具:dnf

    • DNF,全称 Dandified (时髦的、华丽的) Yum;

    • 是Yum的下一个版本,也被称之为Yum的替代品;

    • 如果是centos7的版本,需要通过yum进行安装(这个自行安装一下)

      1
      2
      # yum install epel-release
      # yum install dnf

      出现 ImportError: No module named _conf

      1
      2
      3
      4
      5
      # yum update python*
      # yum install dnf-data dnf-plugins-core libdnf-devel libdnf python2-dnf-plugin-migrate dnf-automatic
      #
      #
      # dnf --version

      出现 UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 4: ordinal not in range(128)

      使用vscode连接云服务器,进入 /usr/lib/python2.7/site-packages/

      该文件目录下新建 sitecustomize.py

      输入

      1
      2
      3
      4
      # encoding=utf8
      import sys
      reload(sys)
      sys.setdefaultencoding('utf8')

      重启服务器

  • 检查dnf是否可用

    1
    dnf --help
  • 安装一个软件包,可以进行如下的操作:

    1
    2
    3
    4
    5
    6
    # 搜索软件包
    dnf search nodejs
    # 查看软件包信息
    dnf info nodejs
    # 安装nodejs
    dnf install nodejs
  • 版本切换工具 n:

    • 希望使用更高的版本,比如最新的LTS或者Current版本;

      1
      2
      3
      4
      5
      6
      7
      # 安装n
      npm install n -g
      # 通过n安装最新的lts和current
      n install lts
      n install latest
      # 通过n切换版本
      n
    • 如果发现切换之后终端没有反应,可以进行重启:

      1. 方式一:重新通过ssh建立连接;
      2. 方式二:重启ssh service sshd restart

8.2.4. 安装MySQL

8.2.4.1. 配置MySQL

官网文件下载安装

或者使用dnf安装MySQL

1
2
3
4
5
6
7
8
9
10
11
12
# 查找MySQL
dnf search mysql-server
# 查看MySQL
dnf info mysql-server
# 安装MySQL,这里加-y的意思是依赖的内容也安装
dnf install mysql-server -y
# 开启MySQL后台服务
systemctl start mysqld
# 查看MySQL服务:active (running)表示启动成功
systemctl status mysql
# 随着系统一起启动
systemctl enable mysqld

配置MySQL账号和密码

1
2
3
4
mysql_secure_installation
# 接下来有一些选项,比如密码强度等等一些
# MySQL8开始通常设置密码强度较强,选择2
# 其他的选项可以自行选择

8.2.4.2. 操作mysql

1
2
3
# mysql -uroot -p<密码>
# 或者是
# mysql -uroot -p 再输入密码

8.2.4.3. 建立远程连接

1
2
3
4
5
6
# 使用mysql数据库
use mysql;
# 查看user表中,连接权限,默认看到root是localhost
select host, user from user;
# 修改权限
update user set host = '%' where user = 'root';

配置阿里云服务器 3306的安全组

Navicat工具中连接MySQL

8.2.4.4. 数据迁移

转储sql文件

创建数据库

1
create database if not exists coderhub default character set utf8mb4;

执行sql文件

8.3. 部署Node项目

8.3.1. 手动部署

  1. 代码托管到Git仓库

    • 在GitHub中创建仓库,并且将当前代码放到GitHub中

      1
      2
      3
      4
      5
      cd coderHub
      git init
      git remote add origin https://<<token>>@github.com/<<username>>/coderHub.git
      git fetch origin
      git merge origin/master
    • 添加.gitignore文件时,忽略一些文件:

      1
      2
      3
      4
      .DS_Store
      node_modules/
      .env
      uploads/
    • 上传

      1
      2
      3
      git add .
      git commit -m "上传"
      git push -u origin master
  2. 代码clone到服务器

    • 使用Git来clone代码:服务器中默认是没有安装Git的;

    • 通过dnf来安装;

      1
      2
      3
      dnf search git;
      dnf info git;
      dnf install git;
  3. VSCode打开代码,使用remote-ssh

    1
    2
    # cd /app/node/
    # git clone https://github.com/WTlumos/coderHub.git
  4. openssl

    1
    2
    3
     openssl
    > genrsa -out private.key 1024
    > rsa -in private.key -pubout -out public.key
  5. .env

    1
    2
    3
    4
    5
    6
    7
    8
    APP_HOST = 
    APP_PORT =

    DATABASE_HOST =
    DATABASE_PORT =
    DATABASE_USER =
    DATABASE_PASSWORD =
    DATABASE =
  6. 安装插件

    1
    npm install
  7. 开放端口

    1
    2
    3
    # systemctl start firewalld
    # firewall-cmd --zone=public --add-port=8000/tcp --permanent
    # firewall-cmd --reload
  8. 运行node

    1
    node ./src/index.js

8.3.2. pm2启动node程序

  • 通过终端启动的node程序,那么如果终端被关闭掉了呢?

    • 那么这个时候相当于启动的Node进程会被关闭掉;
    • 无法继续访问服务器;
  • 在真实的部署过程中,会使用一个工具pm2来管理Node的进程:

    • PM2是一个Node的进程管理器;
    • 可以使用它来管理Node的后台进程;
    • 这样在关闭终端时,Node进程会继续执行,那么服务器就可以继续为前端提供服务了;

安装pm2:

1
npm install pm2 -g

pm2常用的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 命名进程
pm2 start app.js --name my-api
# 显示所有进程状态
pm2 list
# 停止指定的进程
pm2 stop 0
# 停止所有进程
pm2 stop all
# 重启所有进程
pm2 restart all
# 重启指定的进程
pm2 restart 0
# 杀死指定的进程
pm2 delete 0
# 杀死全部进程
pm2 delete all
#后台运行pm2,启动4个app.js,实现负载均衡
pm2 start app.js -i 4
本文结束  感谢您的阅读