coderwwh w:
what是什么? w:why为什么? h:how怎么样? (学习王红元老师课程所作笔记
完整的项目接口包括
- 面向用户的业务接口(管理员,运营,客服...)
- 面向内部的后台管理接口(管理员,角色权限)
本实战项目要完成:(基于koa)
安装koa npm install koa
- 用户管理
- 内容管理
- 内容评论管理
- 内容标签管理
- 文件系统管理
项目的搭建
目录结构划分
- 按照功能模块划分
- 按照业务模块划分
const koa = require("koa");
const app = new koa();
app.listen(8001, () => {
console.log("8001 OK");
});
把项目跑起来,不直接执行文件了,先安装一个nodemon npm install nodemon -D
加package.json里"start": "nodemon ./src/main.js"
为了避免main.js中和app相关的业务越来越多,可以把app抽离出去
app-index.js:
const koa = require("koa");
const app = new koa();
module.exports = app
main.js:
const app = require('./app')
app.listen(8001, () => {
console.log("8001 OK");
});
配置信息写入环境变量
端口不要硬编码,也抽出来
.env
APP_PORT = 8001
安装dotenv加载根目录下env文件,加载到环境变量里面process.envprocess.env是什么
APP_PORT = 8001
const dotenv = require('dotenv')
dotenv.config()
module.exports = {
APP_PORT
} = process.env
const app = require('./app')
const config = require('./app/config')
app.listen(config.APP_PORT, () => {
console.log("8001 OK");
});
开始写接口
用户注册接口
路径和中间件处理的映射
因为koa里面没有app.post,所以需要借助另一个库
npm install koa-router
const koa = require("koa");
const Router = require("koa-router");
const app = new koa();
const userRouter = new Router({ prefix: "/users" });
userRouter.post("/", (ctx, next) => {
ctx.body = "user OK"
});
app.use(userRouter.routes());
app.use(userRouter.allowedMethods());
module.exports = app;
做一个抽离
router ——> user.router.js
const Router = require("koa-router");
const userRouter = new Router({ prefix: "/users" });
userRouter.post("/", (ctx, next) => {
ctx.body = "user OK"
});
module.exports = userRouter
为了解析用户请求参数
npm install koa-bodyparser在app里用一下
接口结构分层
路由 然后中间件 然后服务是去数据库找数据的
index.js
// 这个主要是创建app,直接导入到main里
const koa = require("koa");
//解析请求参数的
const bodyParser = require('koa-bodyparser')
const userRouter = require('../router/user.router')
const app = new koa();
app.use(bodyParser())//解析请求参数的
// 调用router.routes()来组装匹配好的路由,返回一个合并好的中间件
app.use(userRouter.routes());
// 调用router.allowedMethods()获得一个中间件
// 当发送了不符合的请求时,会返回 `405 Method Not Allowed` 或 `501 Not Implemented`
app.use(userRouter.allowedMethods({
// throw: true, // 抛出错误,代替设置响应头状态
// notImplemented: () => '不支持当前请求所需要的功能',
// methodNotAllowed: () => '不支持的请求方式'
}));
module.exports = app;
user.router
// Koa-router 是 koa 的一个路由中间件,
// 它可以将请求的URL和方法(如:GET 、 POST 、 PUT 、 DELETE 等)
// 匹配到对应的响应程序或页面
const Router = require("koa-router");
const {
create
} = require('../controller/user.controller')
// 通过调用 router.prefix(prefix) 来设置路由的前缀
const userRouter = new Router({ prefix: "/users" });
userRouter.post("/", create);
module.exports = userRouter
user.controller
const service = require('../service/user.service')
class UserController{
async create(ctx,next){
// 获取用户请求参数
const user = ctx.request.body
// 查数据库
const result = await service.create(user)
// 返回数据
ctx.body = result
}
}
module.exports = new UserController
user.service
class UserService {
async create(user) {
console.log("user存数据库",user);
// 将user 存储到数据库
return "user OKK!"
}
}
module.exports = new UserService
写入数据库
先创建个表
CREATE TABLE IF NOT EXISTS `users`(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20) NOT NULL UNIQUE,
password VARCHAR(50) NOT NULL,
createAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
npm install mysql2
database.js
const mysql = require('mysql2');
const config = require("./config");
const connections = mysql.createPool({
host: config.MYSQL_HOST,
port: config.MYSQL_PORT,
database: config.MYSQL_DATABASE,
user: config.MYSQL_USER,
password: config.MYSQL_PASSWORD,
});
connections.getConnection((err, conn) => {
conn.connect((err) => {
if (err) {
console.log("sql error");
} else {
console.log("sql OK");
}
});
});
module.exports = connections.promise()
user.service
const connection = require("../app/database");
class UserService {
async create(user) {
console.log("user存数据库",user);
const { name, password } = user;
const statement = `INSERT INTO users (name, password) VALUES (?, ?);`;
const result = await connection.execute(statement, [name, password]);
// 将user 存储到数据库
return result;
}
}
module.exports = new UserService();
验证校验
写哪里呢?写在user.router不好,根目录下创建middleware
user.router
const Router = require("koa-router");
const {
create
} = require('../controller/user.controller')
// 数据处理中间件
const {
verifyUser
} = require('../middleware/user.middleware')
// 通过调用 router.prefix(prefix) 来设置路由的前缀
const userRouter = new Router({ prefix: "/users" });
userRouter.post("/", verifyUser, create);
module.exports = userRouter
user.middleware.js
const errorType = require('../constants/error-types')// 获取错误常量
const verifyUser = async (ctx, next) => {
// 获取
const { name, password } = ctx.request.body;
// 判空
if (!name || !password || name === "" || password === "") {
const error = new Error(errorType.NAME_OR_PWD_IS_REQUIRED);
return ctx.app.emit("error", error, ctx);
}
// 判断唯一
await next(); //只有执行了next,后面的create才可以继续执行
};
module.exports = {
verifyUser,
};
出现错误可以传下去,index.js里做处理
const errorHandler = require('./error-handle')//处理错误函数
app.on('error', errorHandler)
一般把错误处理也单独拎出来error-handle.js(创建在app下)
const errorType = require('../constants/error-types')// 获取错误常量
const errorHandler = (error, ctx) => {
let status, message;
switch (error.message) {
case errorType.NAME_OR_PWD_IS_REQUIRED:
status = 400;
message = "用户名或密码不能为空";
break;
default:
status = 404;
message = "NOT FOUND";
break;
}
ctx.status = status;
ctx.body = message;
};
module.exports = errorHandler;
创建错误常量,在根目录下创建constants下error-types.js
const NAME_OR_PWD_IS_REQUIRED = "name_or_pwd_is_required";
module.exports = {
NAME_OR_PWD_IS_REQUIRED,
};
已存在校验
user.service
// 根据name查询
async getUserByName(name){
const statement = `SLECT * FROM users WHERE name = ?;`
const result = await connection.execute(statement, [name])
return result
}
user.middleware.js
const service = require("../service/user.service"); //判断是否存在
// 判断唯一
const result = await service.getUserByName(name);
//如果length不是0(判断为真)说明已经存在,抛出错误
if (result.length) {
const error = new Error(errorType.USER_ALREADY_EXISTS);
return ctx.app.emit("error", error, ctx);
}
密码加密
思考:这个加密密码对应的逻辑放在那里?
现在是这样
我想可以在controller或者service里,对passord做处理之后在存入数据库,但是 老师说这样不好,我也不知道为什么
老师说,可以再多加一个中间件来对密码进行加密
user.router
userRouter.post("/", verifyUser, handlePassword, create);
middleware
// 对密码加密
const handlePassword = async (ctx, next) => {
const { password } = ctx.request.body;
ctx.request.body.password = md5password(password);
};
utils里password-handle
const crypto = require('crypto');
const md5password = (password) => {
const md5 = crypto.createHash('md5');
const result = md5.update(password).digest('hex');
return result;
}
module.exports = md5password;
用户登录-逻辑框架
先搭好框架:不管输入什么东西,都登录成功
这个写好后,再往里面添加各种验证
怎么写路由呢,老师的思路是,先把需要的文件想好,框架搭好,再去实现细节
router -- auth.router.js
index.js
const authRouter = require("../router/auth.router");
app.use(authRouter.routes());
app.use(authRouter.allowedMethods({}));
auth.router
const Router = require("koa-router");
const authRouter = new Router();
const { login } = require('../controller/auth.controller');
authRouter.post("/login", login);
module.exports = authRouter;
auth.controller
class AuthController {
async login(ctx, next) {
const { name } = ctx.request.body;
ctx.body = `login OK,welcome ${name}`;
}
}
module.exports = new AuthController();
至此,大的逻辑已经成功,接下来就往这个逻辑里面添加细节
怎么校验呢?插入中间件
auth.router
const { verifyLogin } = require("../middleware/auth.middleware");
authRouter.post("/login", verifyLogin, login);
auth.middleware
const errorType = require("../constants/error-types"); // 获取错误常量
const service = require("../service/user.service"); //判断是否存在
const md5password = require("../utils/password-handle");
const verifyLogin = async (ctx, next) => {
// 1, get name & password
const { name, password } = ctx.request.body;
// 2, check empty
if (!name || !password) {
const error = new Error(errorType.NAME_OR_PWD_IS_REQUIRED);
return ctx.app.emit("error", error, ctx);
}
// 3, check user is exists
const result = await service.getUserByName(name);
const user = result[0];
if (!user) {
const error = new Error(errorType.USER_DOES_NOT_EXISTS);
return ctx.app.emit("error", error, ctx);
}
// 4, check password
if (md5password(password) !== user.password) {
const error = new Error(errorType.PASSWORD_IS_INCORRECT);
return ctx.app.emit("error", error, ctx);
}
await next();
};
module.exports = { verifyLogin };
动态加载路由
app-index.js
// 这个主要是创建app,直接导入到main里
const koa = require("koa");
const bodyParser = require("koa-bodyparser"); //解析请求参数的
const errorHandler = require("./error-handle"); //处理错误函数
const useRouter = require('../router');//引入路由
const app = new koa();
app.use(bodyParser()); //解析请求参数的
useRouter(app)
app.on("error", errorHandler);
module.exports = app;
router-index.js
const fs = require("fs");
const { userInfo } = require("os");
const useRouter = (app) => {
fs.readdirSync(__dirname).forEach((fire) => {
if (fire === "index.js") return;
const router = require(`./${fire}`);
app.use(router.routes()); Implemented`
app.use(router.allowedMethods());
});
};
module.exports = useRouter;
添加token
在middleware中,当所有验证通过后,给ctx添加user
ctx.user = user;
安装jsonwebtoken
npm install jsonwebtoken
用openssl生成公钥和私钥
$ openssl
OpenSSL> genrsa -out private.key 1024
Generating RSA private key, 1024 bit long modulus (2 primes)
................+++++
.......+++++
e is 65537 (0x010001)
OpenSSL> rsa -in private.key -pubout -out public.key
writing RSA key
OpenSSL>
auth.controller
const jwt = require("jsonwebtoken");
const { PRIVATE_KEY } = require("../app/config");
class AuthController {
async login(ctx, next) {
const { id, name } = ctx.user;
const token = jwt.sign({ id, name }, PRIVATE_KEY, {
expiresIn: 60 * 60 * 24,
algorithm: "RS256",
});
ctx.body = { id, name, token };
}
}
module.exports = new AuthController();
验证授权
authRouter.get("/test", verifyAuth, success);
verifyAuth
const verifyAuth = async (ctx, next) => {
// get token
const token = ctx.headers["token"];
// verify token
try {
const result = jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"],
});
ctx.user = result;
await next();
} catch (err) {
const error = new Error(errorType.UNAUTHORUZATUON);
ctx.app.emit("error", error, ctx);
}
};
内容管理接口
moment.router
const Router = require("koa-router");
const momentRouter = new Router({ prefix: "./moment" });
const { create } = require("../controller/moment.controller.js");
const { verifyAuth } = require("../middleware/auth.middleware");
momentRouter.post("/", verifyAuth, create);
module.exports = momentRouter;
const verifyAuth = async (ctx, next) => {
// get token
let token = "";
if (!ctx.headers["token"]) {
const error = new Error(errorType.UNAUTHORUZATUON);
ctx.app.emit("error", error, ctx);
} else {
token = ctx.headers["token"];
}
// verify token
try {
const result = jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"],
});
ctx.user = result;
await next();
} catch (err) {
console.log(err);
const error = new Error(errorType.UNAUTHORUZATUON);
ctx.app.emit("error", error, ctx);
}
};
创建表
CREATE TABLE IF NOT EXISTS `moment`(
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)
)
create
moment.controller
const momentService = require("../service/moment.service");
class MomentController {
async create(ctx, next) {
// 1. get data
const userId = ctx.user.id;
const content = ctx.request.body.content;
// 2. insert data to database
const result = await momentService.create(userId, content);
ctx.body = result
}
}
module.exports = new MomentController();
moment.service
const connection = require("../app/database");
class momentService {
async create(userId, content) {
const statement = `INSERT INTO moment (content,user_id) VALUES (?,?)`;
const [result] = await connection.execute(statement, [content, userId]);
return result;
}
}
module.exports = new momentService();
查询moment:获取某一moment
momentRouter.get("/:momentId", detail);
moment.controller
async detail(ctx, next) {
// 1. get data
const momentId = ctx.params.momentId;
// 2. search moment data by id
const result = await momentService.getMomentById(momentId);
ctx.body = result;
}
moment.service
async getMomentById(id) {
try {
const statement = `
SELECT
m.id id, m.content content, m.createAt createTime, m.updateAt updateTime,
JSON_OBJECT('id', u.id, 'name', u.name) users
FROM moment m
LEFT JOIN users u ON m.user_id = u.id
WHERE m.id = ?;
`;
const [result] = await connection.execute(statement, [id]);
return result[0];
} catch (error) {
console.log(error);
}
}
查询moment:获取多个moment
momentRouter.get("/", list);
moment.controller
async list(ctx, next) {
// 1. get data
const { offset, size } = ctx.query;
// 2. search moment data by
const result = await momentService.getMomentList(offset, size);
ctx.body = result;
}
moment.service
async getMomentList(offset, size) {
try {
const statement = `
SELECT
m.id id, m.content content, m.createAt createTime, m.updateAt updateTime,
JSON_OBJECT('id', u.id, 'name', u.name) users
FROM moment m
LEFT JOIN users u ON m.user_id = u.id
LIMIT ?, ?;
`;
const [result] = await connection.execute(statement, [offset, size]);
return result;
} catch (error) {
console.log(error);
}
}
修改moment
修改前的验证:
- 必须登录(中间件)
- 修改自己的moment(中间件)
momentRouter.patch("/:momentId", verifyAuth, verifyPermission, update);
在写verifyPermission的时候,考虑到
- 修改动态权限
- 评论动态权限
- 修改头像权限
- 标签权限
- 等等
考虑到这些,在写这个的时候,就需要考虑到通用性,不能把这个权限写的和moment耦合度太高
先捋一下思路
- 执行auth.middleware中间件判断有没有权限
- 权限要去sql查
- 有权限了执行update的controller
- 去sql里update
auth.middleware
const verifyPermission = async (ctx, next) => {
// 1. get params
const { momentId } = ctx.params;
const { id } = ctx.user;
// 2. query whether have permission
const isPermission = await authService.checkMoment(momentId, id);
if (!isPermission) {
const error = new Error(errorType.UNPERMISSION);
return ctx.app.emit("error", error, ctx);
}
await next();
};
auth.service
const connection = require("../app/database");
class authService {
async checkMoment(momentId, userId) {
const statement = `SELECT * FROM moment WHERE id = ? and user_id = ?;`;
const [result] = await connection.execute(statement, [momentId, userId]);
return result.length === 0 ? false : true;
}
}
module.exports = new authService();
moment.controller
async update(ctx, next) {
//1. get params
const { momentId } = ctx.params;
const { content } = ctx.request.body;
//2. update content
const result = await momentService.update(content, momentId);
ctx.body = result;
}
moment.serve.js
async update(content, momentId) {
const statement = `UPDATE moment set content = ? where id = ?;`;
const [result] = await connection.execute(statement, [content, momentId]);
return result;
}
删除moment
momentRouter.delete("/:momentId", verifyAuth, verifyPermission, remove);
//moment.controller.js
async remove(ctx, next) {
const { momentId } = ctx.params;
const result = await momentService.remove(momentId);
ctx.body = result;
}
//moment.service.js
async remove(momentId) {
const statement = `delete from moment where id = ?;`;
const [result] = await connection.execute(statement, [momentId]);
return result;
}
内容评论管理
创建表
CREATE TABLE IF NOT EXISTS `comment`(
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 moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(comment_id) REFERENCES comment(id) ON DELETE CASCADE ON UPDATE CASCADE
);
思考参数有哪些?
- id(评论的是哪个moment)
- 内容
- userid,不需要传,
添加评论
const Router = require("koa-router");
const { verifyAuth } = require("../middleware/auth.middleware");
const { create } = require("../controller/common.controller");
const commentsRouter = new Router({ prefix: "/comment" });
commentsRouter.post("/", verifyAuth, create);
module.exports = commentsRouter;
//========================================
const service = require("../service/comment.service");
class CommentController {
async create(ctx, next) {
const { momentId, content } = ctx.request.body;
const { id } = ctx.user;
const result = await service.create(momentId, content, id);
ctx.body = result;
}
}
module.exports = new CommentController();
//========================================
const connection = require("../app/database");
class CommentService {
async create(momentId, content, userId) {
const statement = `insert into comment (content,moment_id,user_id) values (?,?,?);`;
const [result] = await connection.execute(statement, [
content,
momentId,
userId,
]);
return result;
}
}
module.exports = new CommentService();
评论别人的评论
commentRouter.post('/reply', verifyAuth, reply)
就是多传一个commentId
修改评论
commentsRouter.patch("/:commentId", verifyAuth, update);
async update(ctx, next) {
const { commentId } = ctx.params;
const { content } = ctx.request.body;
const result = await service.update(commentId, content);
ctx.body = result;
}
//=====================================
async update(commentId, content) {
const statement = `update comment set content = ? where id = ?;`;
const [result] = await connection.execute(statement, [content, commentId]);
return result;
}
较上面比较,增加一个验证权限,只能修改自己的评论,所以这里要加一个验证,
之前写过一个权限认证,当时那个中间件是认证的是
所以现在是对评论的权限进行判断,那之前的不能用了,现在的是在复制一份,是可以的,但是如果之后还有其他的,每个都复制一个,就太冗余了,这时候希望上面那个函数,可以验证很多东西
commentsRouter.patch("/:commentId", verifyAuth, verifyPermission, update);
中间件
const verifyPermission = async (ctx, next) => {
// 1. get params
console.log("---->1");
const [resourceKey] = Object.keys(ctx.params);
const tableName = resourceKey.replace("Id", "");
const resourceId = ctx.params[resourceKey];
console.log("---->1.5",tableName,resourceId);
const { id } = ctx.user;
// 2. query whether have permission
try {
const isPermission = await authService.checkResource(
tableName,
resourceId,
id
);
if (!isPermission) throw new Error();
await next();
} catch (err) {
console.log(err);
const error = new Error(errorType.UNPERMISSION);
return ctx.app.emit("error", error, ctx);
}
};
auth.service
const connection = require("../app/database");
class authService {
async checkResource(tableName, id, userId) {
try {
const statement = `SELECT * FROM ${tableName} WHERE id = ? and user_id = ?;`;
const [result] = await connection.execute(statement, [id, userId]);
return result.length === 0 ? false : true;
} catch (error) {
console.log(error);
}
}
}
module.exports = new authService();
common.controller
async update(ctx, next) {
const { commentId } = ctx.params;
const { content } = ctx.request.body;
console.log(ctx.user);
const result = await service.update(commentId, content);
ctx.body = result;
}
comment.service没变
删除评论
内容标签管理
在获取动态的同时,获取到该动态有几个评论。
async getMomentById(id) {
try {
const statement = `
SELECT
m.id id, m.content content, m.createAt createTime, m.updateAt updateTime,
JSON_OBJECT('id', u.id, 'name', u.name) users,
(select count(*) from comment c where c.moment_id = m.id) commentCount
FROM moment m
LEFT JOIN users u ON m.user_id = u.id
WHERE m.id = ?;
`;
const [result] = await connection.execute(statement, [id]);
return result[0];
} catch (error) {
console.log(error);
}
}
获取评论
获取评论不需要权限,就是不登录,也是可以看到别人的评论的
async list(ctx, next) {
const { momentId } = ctx.query;
const result = await service.getCommentsByMomentId(momentId);
ctx.body = result;
}
async getCommentsByMomentId(momentId) {
const statement = `select * from comment where moment_id = ?;`;
const [result] = await connection.execute(statement, [momentId]);
return result;
}
标签接口开发(多对多)
多对多
一个moment可以有多个标签 , 一个标签也可以打给多个moment
创建标签表
CREATE TABLE IF NOT EXISTS `label`(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(10) NOT NULL UNIQUE,
createAt TIMESTAMP DEFAULT CURRENT_TIMEsTAMP,
updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
创建标签
其实写多了就发现,都差不多
const Router = require("koa-router");
const { verifyAuth } = require("../middleware/auth.middleware");
const { create } = require("../controller/lable.controller");
const labelRouter = new Router({ prefix: "/label" });
// labelRouter.post("/", verifyAuth, create);
labelRouter.post("/", create);
module.exports = labelRouter;
=======================================
const service = require("../service/label.service");
class labelController {
async create(ctx, next) {
const { name } = ctx.request.body;
const result = await service.create(name);
ctx.body = result;
}
}
module.exports = new labelController();
=======================================
const connection = require("../app/database");
class labelService {
async create(name) {
const statement = `insert into label (name) values (?);`;
const result = connection.execute(statement, [name]);
return result;
}
}
module.exports = new labelService();
给动态添加标签(多对多
创建表(多对多的表
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 moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (label_id) REFERENCES label(id) ON DELETE CASCADE ON UPDATE CASCADE
);