上一篇笔记主要记录了如何快速搭建一个用Node + Express + MySQL
开发的RESULT API小项目,并且编写并运行了一个GET接口。这篇笔记会继续完善这个小项目,将API开发中的CURD完整的写出来。
上一篇笔记的最后我们实现了一个查询接口,用来查询用户的身份(分类),这篇笔记我们来实现通过邮件验证码来注册用户。
创建数据库表 在数据库中创建两个表user,temp_user_code
表,用来存储用户数据和验证码数据,并设计以下结构。
名
类型
长度
小数点
不是null
虚拟
键
注释
id
int
10
0
√
√
用户id(无符号,自动递增)
openid
varchar
255
0
微信openid
email
varchar
15
0
√
邮箱(添加UNIQUE索引)
code
varchar
6
0
邮箱验证码
expires_at
datetime
0
0
验证码过期时间
type_id
tinyint
2
0
用户身份id(添加外键指向user_type表中的id字段)
create_time
datetime
0
0
创建时间(默认值CURRENT_TIMESTAMP)
update_time
datetime
0
0
更新时间(默认值CURRENT_TIMESTAMP,自动更新)
名
类型
长度
小数点
不是null
虚拟
键
注释
id
int
10
0
√
√
验证码id(无符号,自动递增)
email
varchar
15
0
邮箱(添加UNIQUE索引)
code
varchar
6
0
邮箱验证码
expires_at
datetime
0
0
验证码过期时间
实现邮箱+验证码注册 创建好数据库表以后,我们就正式进入接口开发的环节了,这里我们选择使用邮箱+验证码登录的方式来实现用户的创建。
安装依赖 安装nodemailer randomstring
依赖,其中randomstring
库用来生成随机字符串(也就是我们要做的验证码),而nodemailer
用来实现发送邮箱验证码。
1 npm i nodemailer randomstring
编写路由、控制器函数 安装好依赖以后,我们来编写路由函数,打开user.routes.js
,添加以下路由。
1 2 3 4 5 6 ... router.post ('/register' , userController.register ) router.post ('/sendEmaliCode' , userController.sendEmailCode ) ...
接着我们来编写控制函数,打开user.controller.js
,添加以下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 exports .sendEmailCode = async (req, res, next) => { const { email } = req.body if (!email) { res.status (400 ).send ({ code : -1 , message : '请提供邮箱地址' , data : null }) } try { const result = await User .sendEmailCode (email) res.send ({ code : 200 , message : '验证码已发送,请查收邮件' , data : result }) } catch (error) { res.status (error.status || 500 ).send ({ code : error.code || -1 , message : error.message || '服务器内部错误' , data : null , }) } }
接着我们使用接口调试工具(我这里使用的是Apifox ),调用接口,我们发现不管参数email有没有值,接口都会报错,并且在命令也发现打印的email
值也是undefined
。
接口调试
解析req.body入参 其实这是因为express默认没有解析请求体中的数据导致的,现在我们来对它进行设置,打开app.js
,添加以下代码:
1 2 3 4 5 6 7 ...const app = express () app.use (express.json ()) app.use (express.urlencoded ({ extended : false })) ...
再次传入一个空的email
,现在能正常返回错误信息了。
1 2 3 4 5 { "code" : -1 , "message" : "请提供邮箱地址" , "data" : null }
配置邮件config、创建工具文件 打开config.js
文件,写入以下配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module .exports = { dbConfig : { ... }, emailConfig : { host : 'smtp.163.com' , port : 465 , secure : true , auth : { user : 'youremail@163.com' , pass : '这里填你的授权码' , }, }, }
在src
目录下创建一个utils.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 const nodemailer = require ('nodemailer' )const randomstring = require ('randomstring' )const config = require ('./config' )exports .transporter = () => { return nodemailer.createTransport ({ ...config.emailConfig , }) }exports .generateCode = () => { const code = randomstring.generate ({ length : 6 , charset : 'numeric' , }) const expiresAt = new Date (Date .now () + 5 * 60 * 1000 ) return { code, expiresAt } }exports .generateMailOptions = (email, code, expiresAt ) => { return { from : config.emailConfig .auth .user , to : email, subject : `您的验证码 ${code} ` , text : `您好,您的验证码是:${code} 。请不要告诉他人。验证码将在 ${expiresAt.toISOString()} 前有效。` , html : `<b>你的验证码是 ${code} ,请勿告诉他人。</b>` , } }
编写发送验证码函数 打开user.model.js
文件,编写发送验证码函数,这段函数的流程主要如下:
通过utils.generateCode()
方法生成验证码和过期时间
通过辅助函数findUser()
查询当前的邮箱是否已被注册,该函数做了通用化处理,可以根据user
表的任意字段进行查询。
根据查询结果判断当前发送的是登录验证码还是注册验证码,将code,expiresAt
存入数据库中,以供后续验证比对。
通过utils.transporter()
方法发送邮件。
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 const utils = require ('../utils' ) ...exports .sendEmailCode = async email => { try { const { code, expiresAt } = utils.generateCode () const users = await this .findUser ('email' , email) if (users && users.length > 0 ) { let updateSQL = `update user set code=?, expires_at=? where email='${email} '` await pool.query (updateSQL, [code, expiresAt, email]) } else { await this .deleteCode (email) let saveSQL = `insert into temp_user_code(email, code, expires_at) values(?,?,?)` await pool.query (saveSQL, [email, code, expiresAt]) } const mailOptin = utils.generateMailOptions (email, code, expiresAt) const transporter = utils.transporter () await transporter.sendMail (mailOptin) return null } catch (error) { throw '验证码发送失败,请重新尝试' } }exports .findUser = async (findType, data) => { try { let sql = `select * from user where ${findType} = ?` const [rows] = await pool.query (sql, [data]) return rows } catch (error) { throw '服务器内部错误,请稍后重试' } }exports .deleteCode = async email => { try { let deleteSQL = `delete from temp_user_code where email='${email} '` await pool.query (deleteSQL, [email]) } catch (error) { throw '服务器内部错误,请稍后重试' } }
这时我们再次调用发送验证码的接口,可以观察到接口已经调用成功了,并且邮箱也收到了生成的验证码。
1 2 3 4 5 { "code" : 200 , "message" : "验证码已发送,请查收邮件" , "data" : null }
编写校验验证码函数 这个过程比较简单,为了方便使用,该辅助函数也做了复用处理,通过type
入参来决定校验的是注册code还是登录code。并将比对结果进行返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 exports .verifyCode = async (email, code, type = 'register' ) => { try { let sql = `select * from ${ type == 'register' ? 'temp_user_code' : 'user' } where email = '${email} ' and expires_at > NOW()` const [rows] = await pool.query (sql, [email]) if (rows.length === 0 ) { return false } const storeCode = rows[0 ].code if (storeCode === code) { return true } return false } catch (error) { throw '服务器内部错误,请稍后重试' } }
编写注册函数 在user.controller.js
文件内,编写注册控制器函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 exports .register = async (req, res, next) => { const { email, code, type_id } = req.body if (!email || !code) { res.status (400 ).send ({ code : -1 , message : '邮箱或验证码不能为空' , data : null }) } else if (!type_id) { res.status (400 ).send ({ code : -1 , message : '请选择身份' , data : null }) } else if (type_id && (type_id < 1 || type_id > 7 )) { res.status (400 ).send ({ code : -1 , message : '身份值有误' , data : null }) } else { try { const result = await User .register (email, code, type_id) res.send ({ code : 200 , message : '注册成功' , data : result }) } catch (error) { res.status (500 ).send ({ code : 500 , message : error || '服务器内部错误' , data : null , }) } } }
在user.model.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 exports .register = async (email, code, type_id) => { try { const users = await this .findUser ('email' , email) if (users.length > 0 ) { throw '该邮箱已被注册' } else { const isTrue = await this .verifyCode (email, code) if (!isTrue) { throw '验证码错误或已过期' } let createSQL = 'insert into user (email, type_id) value(?,?)' const [results] = await pool.query (createSQL, [email, type_id]) if (results.affectedRows === 1 ) { await this .deleteCode (email) return null } } } catch (error) { throw '注册失败,请稍后重试' } }
小结 通过观察我们可以发现,仅仅实现一个发送验证码的函数,我们就用到了数据的增删改查四种能力。其实这四种sql语句单独使用的话,就对应了前端平时的接口调用GET、POST、PUT、DELETE ,是不是很简单呢?
1 2 3 4 5 6 7 8 9 10 11 12 let saveSQL = `insert into temp_user_code(email, code, expires_at) values(?,?,?)` await pool.query (saveSQL, [email, code, expiresAt])let updateSQL = `update user set code=?, expires_at=? where email='${email} '` await pool.query (updateSQL, [code, expiresAt, email])let deleteSQL = `delete from temp_user_code where email='${email} '` await pool.query (deleteSQL, [email])let sql = `select * from user where ${findType} = ?` const [rows] = await pool.query (sql, [data])
但是我们观察代码也能发现有几个问题:
入参校验需要写一大堆if else 很不美观,而且目前只做到了简单校验,对于入参的数据类型等等都没实现。
错误捕获目前只能靠try catch,而且要重复写很多的res.status().send()
,没有进行统一处理。
config
文件保存很多敏感配置,比如数据库的用户名密码,邮件的授权码等,这种内容一旦传到github等,有暴露的风险。
下一篇笔记会针对这些错误进行优化配置,敬请期待吧!