Skip to content

12.1 用户管理接口

前面我们开通了函数计算,介绍了 Express 的使用以及数据库操作,下面就正式开始编写第一组接口 —— 用户管理接口。按照增查改删的顺序,我们主要编写用户注册、用户登录、用户信息修改的接口。

在开始编写接口前,首先要创建一个用户集合,并设计好用户数据需要哪些字段。参照掘金用户中心,我们设定用户文档需要的字段如下:

  • _id:用户 ID。
  • phone:手机号,必填。
  • username:用户名,必填。
  • password:密码,必填。
  • avatar:头像。
  • position:职位。
  • company:公司。
  • introduc:个人介绍。
  • jue_power:掘力值。
  • good_num:获赞数量。
  • read_num:阅读数量。

在创建 Model 模型之前,我们先在项目目录下新建一个 model 文件夹,然后创建 model/users.js,在这个文件中创建用户集合的 Schema 和 Model 并导出,代码如下:

js
// model/users.js
const mongoose = require("mongoose");

const usersSchema = new mongoose.Schema({
  phone: {
    type: String,
    required: true,
    unique: true,
  },
  username: {
    type: String,
    required: true,
  },
  password: {
    type: String,
    required: true,
  },
  avatar: {
    type: String,
    default: "http://xxx.png",
  },
  introduc: {
    type: String,
    required: true,
  },
  position: {
    type: String,
    default: "",
  },
  company: {
    type: String,
    default: "",
  },
  jue_power: {
    type: Number,
    default: 0,
  },
  good_num: {
    type: Number,
    default: 0,
  },
  read_num: {
    type: Number,
    default: 0,
  },
});
const Model = mongoose.model("users", usersSchema);

module.exports = Model;

需要注意一点:字段 “_id” 是 MongoDB 自动生成的唯一 ID,因此不需要在 Schema 中定义。接着我们创建路由文件 router/users.js,并导入用户集合的 Model。

js
// router/users.js
var express = require("express");
var router = express.Router();
var UsersModel = require("../model/users");

router.get("/", (req, res) => {
  res.send("用户管理API");
});

module.exports = router;

路由需要注册才能生效。我们新建一个 config/router.js 文件专门用于注册路由。添加用户路由注册的代码如下:

js
const userRouter = require("./router/users.js");
const router = (app) => {
  app.use("/users", userRouter);
};
module.exports = router;

最后在入口文件 index.js 中挂载路由,让所有注册生效:

js
const routerInit = require("./config/router");
routerInit(app);

12.1.1 用户注册接口

用户注册就是在 users 集合中添加数据,我们创建一个路径为 “/create”、方法为 POST 的路由,如下:

js
// router/users.js
var UsersModel = require("../model/users");

router.post("/create", async (req, res) => {
  let body = req.body;
  try {
    let result = await UsersModel.create(body);
    res.send(result);
  } catch (err) {
    res.status(500).send({
      name: err.name,
      message: err.message,
    });
  }
});

上面代码中,使用 req.body 获取客户端传来的请求体参数,然后调用 UsersModel.create() 方法将参数传入,就会执行将数据写入集合的操作。因为数据库操作属于异步操作,因此我们使用 async/await 语法,并使用 try...catch 捕获错误。

现在在 Postman 中请求该接口,不传任何参数,结果如图所示:

可以看到 try...catch 捕获到了异常,原因是必填字段没有传,说明 Schema 中定义的文档字段验证生效了。这样我们就不需要手写文档参数验证逻辑,直接交给 Schema 去做。

不过参数错误的 HTTP 状态码应该是 400,不能是 500。我们可以利用 err.name 判断是否是参数异常,然后返回正确的状态玛,修改如下:

js
try {
  ...
} catch (err) {
  let code = err.name == 'ValidationError' ? 400 : 500
  let { name, message } = err
  res.status(code).send({
    name,
    message,
  })
}

提示:后面所有接口的 catch 部分都是这样处理的,因此下文不再展示该处的代码。

用户注册时,密码不能明文写入数据库,一般要对其进行加密,那么我们这里还要写一个加密的工具函数。加密需要 Node.js 内置包 crypto,无需安装可直接使用。

创建文件 utils/crypto.js,该文件里书写加密函数,代码如下:

js
// utils/crypto.js
const crypto = require("crypto");
// 密匙
const SECRET_KEY = "my_custom_8848"; // 密匙请自定义

// md5 加密
function md5(content) {
  let md5 = crypto.createHash("md5");
  return md5.update(content).digest("hex"); // 把输出编程16进制的格式
}
// 加密函数
function encrypt(password) {
  const str = `password=${password}&key=${SECRET_KEY}`;
  return md5(str);
}

module.exports = encrypt;

接着将其引入用户路由文件中,写一个密码参数的验证,密码无误时将其加密存储。完整的用户注册接口如下:

js
// router/users.js
var UsersModel = require('../model/users')
var encrypt = require('../utils/crypto')

router.post('/create', async (req, res) => {
  let body = req.body
  try {
    if (!body.password || body.password.length < 6) {
      return res.status(400).send({ message: '密码必传且长度不小于6位' })
    }
    body.password = encrypt(body.password)
    let result = await UsersModel.create(body)
    res.send(result)
  } catch (err) {
    ...
  }
})

现在我们传入正确的参数,要注册的用户数据如下:

json
{
  "phone": "12233334444",
  "username": "杨成功",
  "introduc": "激进的前端工程师",
  "password": "123456"
}

在 Postman 中的执行结果如下:

可以看到,接口状态码返回 200,说明执行成功,并返回了我们注册成功的用户数据。数据中自动生成了 “_id” 字段,它是全局唯一的值。“password” 字段也被加密了;没有传入的字段如 “jue_power” 等因为设置了默认值,所以它们也被自动创建了。

此时注册接口已经完成。接下来写登录接口,查询我们刚才注册的用户。

12.1.2 用户登录接口

写完用户注册接口,登录接口就好写了。用户集合中的手机号是唯一的,因此登录时用户传入手机号和密码,我们在集合中查找该用户是否存在即可。

创建一个路径为 “/login”、方法为 POST 的路由,如下:

js
router.post('/login', async (req, res) => {
  let body = req.body
  try {
    if (!body.phone || !body.password) {
      return res.status(400).send({ message: '请输入手机号和密码' })
    }
    let { phone, password } = body
    password = encrypt(password)
    let result = await UsersModel.findOne({ phone, password })
    if (result) {
      res.send({
        code: 200,
        data: result,
      })
    } else {
      res.send({
        code: 20001,
        message: '用户名或密码错误',
      })
    }
  } catch (err) {
    ...
  }
})

上述代码中,首先验证了请求体中 phone、password 两个字段是否存在,这里也要对密码加密才能匹配到数据库中的值。接着使用 model.findOne() 方法查询集合中的单条数据,传入过滤的参数,返回检索到的值。

然后根据返回值判断是否找到用户。如果找到用户,则返回 code=200 并返回用户数据,如果找不到则证明手机号或密码错误,我们返回 code=20001 并提示错误信息即可。

在 Postman 中测试传入正确的用户名和密码,结果如图所示:

换一个错误的密码再请求一次,结果如图所示:

测试结果符合预期。前端调用该接口时,就可以根据返回的 code 值判断是否登录成功,然后进行后面的逻辑。

12.1.3 修改用户信息接口

修改用户接口,允许修改用户名、职位、公司、个人介绍四个字段,绝对不允许修改掘力值、阅读量、点赞量这些字段,因此要做好参数验证。修改接口要用 PUT 方法,创建路由如下:

js
router.put('/update/:id', async (req, res, next) => {
  let body = req.body
  let { id } = req.params
  try {
    let allow_keys = ['username', 'introduc', 'avatar', 'position', 'company']
    Object.keys(body).forEach(key => {
      if (!allow_keys.includes(key)) {
        delete body[key]
      }
    })
    if (Object.keys(body).length == 0) {
      return res.status(400).send({
        message: '请传入要更新的数据',
      })
    }
    await UsersModel.findByIdAndUpdate(id, body)
    res.send({ message: '更新成功' })
  } catch (err) {
    ...
  }
})

上面代码中,路由地址为 “/update/:id”,其中 id 表示用户文档的 ID,请求体参数则是实际要更新的数据。我们在变量 allow_keys 中定义了允许更新的字段,然后将请求体参数中的无效属性删除。如果参数中没有有效字段,则返回 400 错误。

提示:更新头像时请传入一个在线图片地址,后面的所有图片字段都一样,本项目不介绍文件上传功能,感兴趣的读者可以基于阿里云的 OSS 对象存储实现。

验证参数有效之后,我们通过 ID 找到对应文档并执行更新,使用 model.findByIdAndUpdate() 快捷方法来实现。如果方法调用没有报错,表示更新成功,返回 200 即可;如果异常则走 catch 逻辑。

现在我们在 Postman 中更新职位字段,测试结果如下:

然后在调用登录接口查看返回结果,发现 position 字段已经更新了:

12.1.4 更新掘力值、点赞量、阅读量

更新掘力值、点赞量、阅读量的操作不需要单独写一个接口,只需要在文章的操作接口完成后更新一下即可。这三个字段的修改方式都是自增或者自减,我们需要用到 $inc 操作符。

“$inc” 操作符可以对多个数值字段进行增减操作,正数为增,负数为减。假设某个用户的文章被赞了,我们要对该用户的掘力值和点赞量分别自增 1,代码如下:

js
let user_id = "xxx";
await UsersModel.findByIdAndUpdate(user_id, {
  $inc: { jue_power: 1, good_num: 1 },
});

如果用户取消了点赞,我们还要对掘力值和点赞量分别自减 1,代码如下:

js
let user_id = "xxx";
await UsersModel.findByIdAndUpdate(user_id, {
  $inc: { jue_power: -1, good_num: -1 },
});

通过这种方式,我们在文章发布/删除接口、文章点赞/取消点赞接口、文章评论/删除评论接口、文章详情接口等多个位置添加以上代码,实时更新用户的掘力值、点赞量和阅读量。

这部分代码后面不做多余介绍,大家记得在对应的接口中添加即可。