四、从零模拟新浪微博-用户管理
代码仓库:https://github.com/changeclass/koa2-weibo
数据同步
user表数据模型
/**
* @description 用户数据模型
* @author 小康
*/
const seq = require('../seq')
const { STRING, DECIMAL } = require('../type')
const User = seq.define('user', {
userName: {
type: STRING,
allowNull: false,
unique: true,
comment: '唯一'
},
password: {
type: STRING,
allowNull: false,
comment: '密码'
},
nickName: {
type: STRING,
allowNull: false,
comment: '昵称'
},
gender: {
type: DECIMAL,
allowNull: false,
defaultValue: 3,
comment: '性别 (1男性2女性3保密)'
},
picture: {
type: STRING,
comment: '图片地址'
},
city: {
type: STRING,
comment: '城市'
}
})
module.exports = User
分层
业务分层
src
├─ cache
│ └─ _redis.js
├─ config
│ ├─ constant.js 常量
│ └─ db.js 数据库配置
├─ controller 控制器
│ └─ user.js 用户控制器
├─ db 数据库
│ ├─ model 数据库模型
│ │ ├─ index.js
│ │ └─ User.js
│ ├─ seq.js 数据库链接
│ ├─ sync.js 数据库同步
│ └─ type.js 数据类型
├─ model 业务模型
│ ├─ ErrorInfo.js 错误信息
│ └─ ResModel.js 规范返回格式
├─ public
│ ├─ css
│ │ ├─ jquery.atwho.css
│ │ ├─ list.css
│ │ ├─ main.css
│ │ └─ right.css
│ ├─ images
│ ├─ javascripts
│ │ ├─ jquery.atwho.js
│ │ ├─ jquery.caret.js
│ │ ├─ my-ajax.js
│ │ └─ query-object.js
│ └─ stylesheets
│ └─ style.css
├─ routes 路由
│ ├─ api API路由
│ │ └─ user.js
│ ├─ view 视图路由
│ │ ├─ error.js
│ │ └─ users.js
│ └─ index.js
├─ services 数据库服务层
│ ├─ user.js
│ └─ _format.js
├─ utils 工具库
│ └─ env.js
├─ views 视图层
│ ├─ layout
│ │ ├─ footer.ejs
│ │ └─ header.ejs
│ ├─ widgets
│ │ ├─ blog-list.ejs
│ │ ├─ fans.ejs
│ │ ├─ followers.ejs
│ │ ├─ input.ejs
│ │ ├─ load-more.ejs
│ │ └─ user-info.ejs
│ ├─ 404.ejs
│ ├─ atMe.ejs
│ ├─ error.ejs
│ ├─ index.ejs
│ ├─ login.ejs
│ ├─ profile.ejs
│ ├─ register.ejs
│ ├─ setting.ejs
│ └─ square.ejs
└─ app.js 入口文件 将各个功能模块进行分层可以是代码逻辑更加清晰。定义业务模型层,例如错误信息(统一返回格式)。定义server层用于与数据库进行交互。
业务模型层(统一返回格式)
/** * @description: res的数据模型 * @author: 小康 * @url: https://xiaokang.me * @Date: 2020-12-17 14:44:52 * @LastEditTime: 2020-12-17 14:44:52 * @LastEditors: 小康 */ /** * @author: 小康 * @url: https://xiaokang.me * @description: 基础模块 */ class BaseModel { constructor({ errno, data, message }) { this.errno = errno if (data) { this.data = data } if (message) { this.message = message } } } /** * @author: 小康 * @url: https://xiaokang.me * @description: 成功的模型 */ class SuccessModel extends BaseModel { constructor(data = {}) { super({ errno: 0, data }) } } /** * @author: 小康 * @url: https://xiaokang.me * @description: 失败的模型 */ class ErrorModel extends BaseModel { constructor({ errno, message }) { super({ errno, message }) } } module.exports = { SuccessModel, ErrorModel }业务模型层(失败信息集合)
/** * @description: 失败信息集合 * @author: 小康 * @url: https://xiaokang.me * @Date: 2020-12-17 14:54:07 * @LastEditTime: 2020-12-17 14:54:08 * @LastEditors: 小康 */ module.exports = { // 用户名已存在 registerUserNameExistInfo: { errno: 10001, message: '用户名已存在' }, // 注册失败 registerFailInfo: { errno: 10002, message: '注册失败,请重试' }, // 用户名不存在 registerUserNameNotExistInfo: { errno: 10003, message: '用户名未存在' }, // 登录失败 loginFailInfo: { errno: 10004, message: '登录失败,用户名或密码错误' }, // 未登录 loginCheckFailInfo: { errno: 10005, message: '您尚未登录' }, // 修改密码失败 changePasswordFailInfo: { errno: 10006, message: '修改密码失败,请重试' }, // 上传文件过大 uploadFileSizeFailInfo: { errno: 10007, message: '上传文件尺寸过大' }, // 修改基本信息失败 changeInfoFailInfo: { errno: 10008, message: '修改基本信息失败' }, // json schema 校验失败 jsonSchemaFileInfo: { errno: 10009, message: '数据格式校验错误' }, // 删除用户失败 deleteUserFailInfo: { errno: 10010, message: '删除用户失败' }, // 添加关注失败 addFollowerFailInfo: { errno: 10011, message: '添加关注失败' }, // 取消关注失败 deleteFollowerFailInfo: { errno: 10012, message: '取消关注失败' }, // 创建微博失败 createBlogFailInfo: { errno: 11001, message: '创建微博失败,请重试' }, // 删除微博失败 deleteBlogFailInfo: { errno: 11002, message: '删除微博失败,请重试' } }
检查用户是否存在
API路由层
// 用户名是否存在 router.post('/isExist', async (ctx, next) => { const { userName } = ctx.request.body ctx.body = await isExist(userName) })controller层const { getUserInfo, createUser } = require('../services/user') const { SuccessModel, ErrorModel } = require('../model/ResModel') const { registerUserNameNotExistInfo, registerUserNameExistInfo, registerFailInfo } = require('../model/ErrorInfo') /** * @author: 小康 * @url: https://xiaokang.me * @param {String} userName 需要检查的用户名 * @description: 检查用户名是否存在 */ async function isExist(userName) { const userInfo = await getUserInfo(userName) if (userInfo) { // 已经存在 return new SuccessModel(userInfo) } else { // 不存在 return new ErrorModel(registerUserNameNotExistInfo) } }servers层const { User } = require('../db/model/index') const { formatUser } = require('./_format') /** * @author: 小康 * @url: https://xiaokang.me * @param {*} userName 用户名 * @param {*} password 密码 * @description: 获取用户的信息 */ async function getUserInfo(userName, password) { const whereOpt = { userName } if (password) { Object.assign(whereOpt, { password }) } // 查询 const result = await User.findOne({ // 查询的列 attributes: ['id', 'userName', 'nickName', 'picture', 'city'], // 查询条件 where: whereOpt }) if (result == null) { // 未找到 return result } // 格式化 const formatRes = formatUser(result.dataValues) return formatRes }formatUser方法/** * 格式化头像 * @author 小康 * @date 2020-12-17 * @param {Object} obj 用户对象 * @returns {Object} 处理后的结果 */ function _formatUserPicture(obj) { if (obj.picture == null) { obj.picture = DEFAULT_PICTURE } return obj } /** * 格式化用户 * @author 小康 * @date 2020-12-17 * @param {Array|Object} list 用户列表或单个用户对象 * @returns {any} */ function formatUser(list) { if (list == null) { return list } if (list instanceof Array) { // 数组 用户列表 return list.map(_formatUserPicture) } // 单个对象 let result = list result = _formatUserPicture(result) return result }
用户注册
具体逻辑与检查用户是否存在相似。
密码加密
密码加密只需要在存储数据时对数据进行加密处理即可。
user的controller层
// ...
// 注册功能
try {
createUser({
userName,
password: doCrypto(password),
gender
})
return new SuccessModel()
} catch (e) {
console.error(e.message, e.stack)
return new ErrorModel(registerFailInfo)
}
// ...doCrypto函数
/**
* @description: 加密方法
* @author: 小康
* @url: https://xiaokang.me
* @Date: 2020-12-17 15:43:56
* @LastEditTime: 2020-12-17 15:43:56
* @LastEditors: 小康
*/
const crypto = require('crypto')
const { CRYPTO_SECRET_KEY } = require('../config/secretKeys')
/**
* @author: 小康
* @url: https://xiaokang.me
* @param {String} content 要加密的明文
* @description: MD5加密
*/
function _md5(content) {
const md5 = crypto.createHash('md5')
return md5.update(content).digest('hex')
}
/**
* @author: 小康
* @url: https://xiaokang.me
* @param {*} content 明文
* @description: 加密方法
*/
function doCrypto(content) {
const str = `password=${content}&key=${CRYPTO_SECRET_KEY}`
return _md5(str)
}
module.exports = {
doCrypto
}用户信息格式验证
在注册逻辑执行前加入中间件函数。
// 注册路由
router.post('/register', genValidator(userValidate), async (ctx, next) => {
const { userName, password, gender } = ctx.request.body
ctx.body = await register({ userName, password, gender })
})genValidator用于生成中间件函数/** * @description: json schema验证中间件 * @author: 小康 * @url: https://xiaokang.me * @Date: 2020-12-17 16:34:54 * @LastEditTime: 2020-12-17 16:34:54 * @LastEditors: 小康 */ const { jsonSchemaFileInfo } = require('../model/ErrorInfo') const { ErrorModel } = require('../model/ResModel') /** * @author: 小康 * @url: https://xiaokang.me * @param {function} validateFn 验证函数 * @description: 生成json schema 验证中间件 */ function genValidator(validateFn) { async function validator(ctx, next) { const data = ctx.request.body const error = validateFn(data) if (error) { // 验证失败 return (ctx.body = new ErrorModel(jsonSchemaFileInfo)) } // 验证成功 await next() } return validator } module.exports = { genValidator }传入验证函数
userValidate验证函数/** * @description: user 数据格式校验 * @author: 小康 * @url: https://xiaokang.me * @Date: 2020-12-17 16:17:42 * @LastEditTime: 2020-12-17 16:17:43 * @LastEditors: 小康 */ const validate = require('./_validate') // 校验规则 const SCHEMA = { type: 'object', properties: { userName: { type: 'string', pattern: '^[a-zA-Z][a-zA-Z0-9_]+$', // 字母开头,字母数字下划线 maxLength: 255, minLength: 2 }, password: { type: 'string', maxLength: 255, minLength: 3 }, newPassword: { type: 'string', maxLength: 255, minLength: 3 }, nickName: { type: 'string', maxLength: 255 }, picture: { type: 'string', maxLength: 255 }, city: { type: 'string', maxLength: 255, minLength: 2 }, gender: { type: 'number', minimum: 1, maximum: 3 } } } /** * 校验用户数据格式 * @param {Object} data 用户数据 */ function userValidate(data = {}) { return validate(SCHEMA, data) } module.exports = userValidate
登录验证中间件
/**
* @description: user 的控制器
* @author: 小康
* @url: https://xiaokang.me
* @Date: 2020-12-17 14:19:03
* @LastEditTime: 2020-12-17 14:19:05
* @LastEditors: 小康
*/
const { getUserInfo, createUser } = require('../services/user')
const { SuccessModel, ErrorModel } = require('../model/ResModel')
const {
registerUserNameNotExistInfo,
registerUserNameExistInfo,
registerFailInfo,
loginFailInfo
} = require('../model/ErrorInfo')
const { doCrypto } = require('../utils/cryp')
/**
* @author: 小康
* @url: https://xiaokang.me
* @param {String} userName 需要检查的用户名
* @description: 检查用户名是否存在
*/
async function isExist(userName) {
const userInfo = await getUserInfo(userName)
if (userInfo) {
// 已经存在
return new SuccessModel(userInfo)
} else {
// 不存在
return new ErrorModel(registerUserNameNotExistInfo)
}
}
/**
* @author: 小康
* @url: https://xiaokang.me
* @param {String} userName 用户名
* @param {String} password 密码
* @param {Number} gender 性别 1是男 2是女 3是保密
* @description: 注册功能
*/
async function register({ userName, password, gender }) {
const userInfo = await getUserInfo(userName)
if (userInfo) {
// 用户名已存在
return ErrorModel(registerUserNameExistInfo)
}
// 注册功能
try {
createUser({
userName,
password: doCrypto(password),
gender
})
return new SuccessModel()
} catch (e) {
console.error(e.message, e.stack)
return new ErrorModel(registerFailInfo)
}
}
/**
* @author: 小康
* @url: https://xiaokang.me
* @param {*} ctx koa2 ctx
* @param {*} userName 用户名
* @param {*} password 密码
* @description: 登录
*/
async function login(ctx, userName, password) {
// 登录成功之后,将用户信息放到session中
const userInfo = await getUserInfo(userName, doCrypto(password))
if (!userInfo) {
return new ErrorModel(loginFailInfo)
}
// 登录成功
if (ctx.session.userInfo == null) {
ctx.session.userInfo = userInfo
}
return new SuccessModel()
}
module.exports = {
isExist,
register,
login
}单元测试
数据模型
model.test.js
/**
* @description: user model test
* @author: 小康
* @url: https://xiaokang.me
* @Date: 2020-12-17 19:41:28
* @LastEditTime: 2020-12-17 19:41:28
* @LastEditors: 小康
*/
const { User } = require('../../src/db/model/index')
test('User 模型的各个属性,符合预期', () => {
// 构建一个内存的User实例,但不会提交数据库
const user = User.build({
userName: 'zhangsan',
password: 'p1234',
nickName: '张三',
// gender: 1,
picture: '/xxx.png',
city: '北京'
})
// 验证各个属性
expect(user.userName).toBe('zhangsan')
expect(user.password).toBe('p1234')
expect(user.nickName).toBe('张三')
expect(user.gender).toBe(3)
expect(user.picture).toBe('/xxx.png')
expect(user.city).toBe('北京')
})登录相关测试
/**
* @description: user api test
* @author: 小康
* @url: https://xiaokang.me
* @Date: 2020-12-17 19:59:34
* @LastEditTime: 2020-12-17 19:59:34
* @LastEditors: 小康
*/
const server = require('../server')
// 用户信息
const userName = `u_${Date.now()}`
const password = `p_${Date.now()}`
const testUser = {
userName,
password,
nickName: userName,
gender: 1
}
// 存储 cookie
let COOKIE = ''
// 注册
test('注册一个用户,应该成功', async () => {
const res = await server.post('/api/user/register').send(testUser)
expect(res.body.errno).toBe(0)
})
// 重复注册
test('重复注册用户,应该失败', async () => {
const res = await server.post('/api/user/register').send(testUser)
expect(res.body.errno).not.toBe(0)
})
// 查询用户是否存在
test('查询注册的用户名,应该存在', async () => {
const res = await server.post('/api/user/isExist').send({ userName })
expect(res.body.errno).toBe(0)
})
// json schema 检测
test('json schema 检测,非法的格式,注册应该失败', async () => {
const res = await server.post('/api/user/register').send({
userName: '123', // 用户名不是字母(或下划线)开头
password: 'a', // 最小长度不是 3
// nickName: ''
gender: 'mail' // 不是数字
})
expect(res.body.errno).not.toBe(0)
})
// 登录
test('登录,应该成功', async () => {
const res = await server.post('/api/user/login').send({
userName,
password
})
expect(res.body.errno).toBe(0)
// 获取 cookie
COOKIE = res.headers['set-cookie'].join(';')
})
// 删除
test('删除用户,应该成功', async () => {
const res = await server.post('/api/user/delete').set('cookie', COOKIE)
expect(res.body.errno).toBe(0)
})
// 再次查询用户,应该不存在
test('删除之后,再次查询注册的用户名,应该不存在', async () => {
const res = await server.post('/api/user/isExist').send({ userName })
expect(res.body.errno).not.toBe(0)
}) 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 小康博客!
评论
TwikooDisqusjs









