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 并导出,代码如下:
// 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。
// 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 文件专门用于注册路由。添加用户路由注册的代码如下:
const userRouter = require("./router/users.js");
const router = (app) => {
app.use("/users", userRouter);
};
module.exports = router;
最后在入口文件 index.js 中挂载路由,让所有注册生效:
const routerInit = require("./config/router");
routerInit(app);
12.1.1 用户注册接口
用户注册就是在 users 集合中添加数据,我们创建一个路径为 “/create”、方法为 POST 的路由,如下:
// 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 判断是否是参数异常,然后返回正确的状态玛,修改如下:
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,该文件里书写加密函数,代码如下:
// 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;
接着将其引入用户路由文件中,写一个密码参数的验证,密码无误时将其加密存储。完整的用户注册接口如下:
// 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) {
...
}
})
现在我们传入正确的参数,要注册的用户数据如下:
{
"phone": "12233334444",
"username": "杨成功",
"introduc": "激进的前端工程师",
"password": "123456"
}
在 Postman 中的执行结果如下:
可以看到,接口状态码返回 200,说明执行成功,并返回了我们注册成功的用户数据。数据中自动生成了 “_id” 字段,它是全局唯一的值。“password” 字段也被加密了;没有传入的字段如 “jue_power” 等因为设置了默认值,所以它们也被自动创建了。
此时注册接口已经完成。接下来写登录接口,查询我们刚才注册的用户。
12.1.2 用户登录接口
写完用户注册接口,登录接口就好写了。用户集合中的手机号是唯一的,因此登录时用户传入手机号和密码,我们在集合中查找该用户是否存在即可。
创建一个路径为 “/login”、方法为 POST 的路由,如下:
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 方法,创建路由如下:
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,代码如下:
let user_id = "xxx";
await UsersModel.findByIdAndUpdate(user_id, {
$inc: { jue_power: 1, good_num: 1 },
});
如果用户取消了点赞,我们还要对掘力值和点赞量分别自减 1,代码如下:
let user_id = "xxx";
await UsersModel.findByIdAndUpdate(user_id, {
$inc: { jue_power: -1, good_num: -1 },
});
通过这种方式,我们在文章发布/删除接口、文章点赞/取消点赞接口、文章评论/删除评论接口、文章详情接口等多个位置添加以上代码,实时更新用户的掘力值、点赞量和阅读量。
这部分代码后面不做多余介绍,大家记得在对应的接口中添加即可。