代码仓库:https://github.com/changeclass/koa2-weibo

数据同步

user表数据模型

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
/**
* @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

业务分层

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
54
55
56
57
58
59
60
61
62
63
64
65
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层用于与数据库进行交互。

  • 业务模型层(统一返回格式)

    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
    54
    55
    56
    57
    58
    /**
    * @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
    }
  • 业务模型层(失败信息集合)

    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
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    /**
    * @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路由层

    1
    2
    3
    4
    5
    // 用户名是否存在
    router.post('/isExist', async (ctx, next) => {
    const { userName } = ctx.request.body
    ctx.body = await isExist(userName)
    })
  2. controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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

    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
    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方法

    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
    /**
    * 格式化头像
    * @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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ...
// 注册功能
try {
createUser({
userName,
password: doCrypto(password),
gender
})
return new SuccessModel()
} catch (e) {
console.error(e.message, e.stack)
return new ErrorModel(registerFailInfo)
}
// ...

doCrypto函数

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
/**
* @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
}

用户信息格式验证

在注册逻辑执行前加入中间件函数。

1
2
3
4
5
// 注册路由
router.post('/register', genValidator(userValidate), async (ctx, next) => {
const { userName, password, gender } = ctx.request.body
ctx.body = await register({ userName, password, gender })
})
  • genValidator用于生成中间件函数

    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
    /**
    * @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验证函数

    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
    54
    55
    56
    57
    58
    59
    60
    /**
    * @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

登录验证中间件

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* @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

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
/**
* @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('北京')
})

登录相关测试

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* @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)
})