代码仓库: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

分层

image-20201217142759378

业务分层

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: '删除微博失败,请重试'
      }
    }

检查用户是否存在

  1. API路由层

    // 用户名是否存在
    router.post('/isExist', async (ctx, next) => {
      const { userName } = ctx.request.body
      ctx.body = await isExist(userName)
    })
  2. 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)
        }
    }
  3. 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
    }
    

用户注册

具体逻辑与检查用户是否存在相似。

密码加密

密码加密只需要在存储数据时对数据进行加密处理即可。

usercontroller

// ...
// 注册功能
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)
})