node后台搭建实战(可能是最详细的了)

3,617 阅读6分钟

coderwwh w:what是什么? w:why为什么? h:how怎么样? (学习王红元老师课程所作笔记

完整的项目接口包括

  • 面向用户的业务接口(管理员,运营,客服...)
  • 面向内部的后台管理接口(管理员,角色权限)

本实战项目要完成:(基于koa)

安装koa npm install koa

  1. 用户管理
  2. 内容管理
  3. 内容评论管理
  4. 内容标签管理
  5. 文件系统管理

项目的搭建

目录结构划分

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

图片.png


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"

图片.png


为了避免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

图片.png

安装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;

图片.png


做一个抽离

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里用一下

接口结构分层

路由 然后中间件 然后服务是去数据库找数据的

图片.png

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();

验证校验

图片.png 写哪里呢?写在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);
  }

密码加密

思考:这个加密密码对应的逻辑放在那里?

现在是这样 图片.png

我想可以在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);
    }
  }

图片.png

查询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耦合度太高


先捋一下思路

  1. 执行auth.middleware中间件判断有没有权限
  2. 权限要去sql查
  3. 有权限了执行update的controller
  4. 去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;
  }

较上面比较,增加一个验证权限,只能修改自己的评论,所以这里要加一个验证,

之前写过一个权限认证,当时那个中间件是认证的是

图片.png

所以现在是对评论的权限进行判断,那之前的不能用了,现在的是在复制一份,是可以的,但是如果之后还有其他的,每个都复制一个,就太冗余了,这时候希望上面那个函数,可以验证很多东西

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没变

删除评论

内容标签管理

在获取动态的同时,获取到该动态有几个评论。

图片.png

  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);
    }
  }

获取评论

图片.png 获取评论不需要权限,就是不登录,也是可以看到别人的评论的

  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
);