├── .gitignore ├── chapter10 └── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ ├── config.js │ └── index.js │ ├── migrations │ ├── 20180818141548-create-shops-table.js │ └── 20180818150507-create-goods-table.js │ ├── models │ ├── goods.js │ ├── index.js │ └── shops.js │ ├── package.json │ ├── plugins │ ├── hapi-auth-jwt2.js │ ├── hapi-pagination.js │ └── hapi-swagger.js │ ├── routes │ ├── hello-hapi.js │ ├── orders.js │ ├── shops.js │ └── users.js │ ├── seeders │ ├── 20180819061006-init-shops.js │ └── 20180819102052-init-goods.js │ └── utils │ └── router-helper.js ├── chapter11 └── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ ├── config.js │ └── index.js │ ├── migrations │ ├── 20180818141548-create-shops-table.js │ ├── 20180818150507-create-goods-table.js │ └── 20180826142118-create-users-table.js │ ├── models │ ├── goods.js │ ├── index.js │ ├── shops.js │ └── users.js │ ├── package.json │ ├── plugins │ ├── hapi-auth-jwt2.js │ ├── hapi-pagination.js │ └── hapi-swagger.js │ ├── routes │ ├── hello-hapi.js │ ├── orders.js │ ├── shops.js │ └── users.js │ ├── seeders │ ├── 20180819061006-init-shops.js │ └── 20180819102052-init-goods.js │ └── utils │ ├── decryped-data.js │ └── router-helper.js ├── chapter12 └── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ ├── config.js │ └── index.js │ ├── migrations │ ├── 20180818141548-create-shops-table.js │ ├── 20180818150507-create-goods-table.js │ ├── 20180826142118-create-users-table.js │ ├── 20180826181335-create-orders-table.js │ └── 20180826181505-create-order-goods-table.js │ ├── models │ ├── goods.js │ ├── index.js │ ├── order-goods.js │ ├── orders.js │ ├── shops.js │ └── users.js │ ├── package.json │ ├── plugins │ ├── hapi-auth-jwt2.js │ ├── hapi-pagination.js │ └── hapi-swagger.js │ ├── routes │ ├── hello-hapi.js │ ├── orders.js │ ├── shops.js │ └── users.js │ ├── seeders │ ├── 20180819061006-init-shops.js │ └── 20180819102052-init-goods.js │ └── utils │ ├── decryped-data.js │ └── router-helper.js ├── chapter13 └── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ ├── config.js │ └── index.js │ ├── migrations │ ├── 20180818141548-create-shops-table.js │ ├── 20180818150507-create-goods-table.js │ ├── 20180826142118-create-users-table.js │ ├── 20180826181335-create-orders-table.js │ └── 20180826181505-create-order-goods-table.js │ ├── models │ ├── goods.js │ ├── index.js │ ├── order-goods.js │ ├── orders.js │ ├── shops.js │ └── users.js │ ├── package.json │ ├── plugins │ ├── hapi-auth-jwt2.js │ ├── hapi-pagination.js │ └── hapi-swagger.js │ ├── routes │ ├── hello-hapi.js │ ├── orders.js │ ├── shops.js │ └── users.js │ ├── seeders │ ├── 20180819061006-init-shops.js │ └── 20180819102052-init-goods.js │ └── utils │ ├── decryped-data.js │ └── router-helper.js ├── chapter5 ├── hapi-tutorial-1 │ ├── .eslintrc.json │ ├── app.js │ └── package.json ├── hapi-tutorial-2 │ ├── .eslintrc.json │ ├── app.js │ ├── config │ │ └── index.js │ ├── package.json │ └── routes │ │ └── hello-hapi.js └── hapi-tutorial-3 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ └── index.js │ ├── package.json │ └── routes │ └── hello-hapi.js ├── chapter6 ├── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ │ └── index.js │ ├── package.json │ ├── plugins │ │ └── hapi-swagger.js │ └── routes │ │ └── hello-hapi.js ├── hapi-tutorial-2 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ │ └── index.js │ ├── package.json │ ├── plugins │ │ └── hapi-swagger.js │ └── routes │ │ ├── hello-hapi.js │ │ ├── orders.js │ │ └── shops.js └── hapi-tutorial-3 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ └── index.js │ ├── package.json │ ├── plugins │ └── hapi-swagger.js │ └── routes │ ├── hello-hapi.js │ ├── orders.js │ └── shops.js ├── chapter7 ├── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ │ ├── config.js │ │ └── index.js │ ├── migrations │ │ ├── 20180818141548-create-shops-table.js │ │ └── 20180818150507-create-goods-table.js │ ├── models │ │ └── index.js │ ├── package.json │ ├── plugins │ │ └── hapi-swagger.js │ └── routes │ │ ├── hello-hapi.js │ │ ├── orders.js │ │ └── shops.js └── hapi-tutorial-2 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ ├── config.js │ └── index.js │ ├── migrations │ ├── 20180818141548-create-shops-table.js │ └── 20180818150507-create-goods-table.js │ ├── models │ └── index.js │ ├── package.json │ ├── plugins │ └── hapi-swagger.js │ ├── routes │ ├── hello-hapi.js │ ├── orders.js │ └── shops.js │ └── seeders │ ├── 20180819061006-init-shops.js │ └── 20180819102052-init-goods.js ├── chapter8 └── hapi-tutorial-1 │ ├── .env.example │ ├── .eslintrc.json │ ├── app.js │ ├── config │ ├── config.js │ └── index.js │ ├── migrations │ ├── 20180818141548-create-shops-table.js │ └── 20180818150507-create-goods-table.js │ ├── models │ ├── goods.js │ ├── index.js │ └── shops.js │ ├── package.json │ ├── plugins │ ├── hapi-pagination.js │ └── hapi-swagger.js │ ├── routes │ ├── hello-hapi.js │ ├── orders.js │ └── shops.js │ ├── seeders │ ├── 20180819061006-init-shops.js │ └── 20180819102052-init-goods.js │ └── utils │ └── router-helper.js └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .DS_Store 4 | .env -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password 11 | 12 | # JWT 的签发秘钥 13 | JWT_SECRET = your-secret -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const hapiAuthJWT2 = require('hapi-auth-jwt2'); 3 | require('env2')('./.env'); 4 | const config = require('./config'); 5 | const routesHelloHapi = require('./routes/hello-hapi'); 6 | const routesShops = require('./routes/shops'); 7 | const routesOrders = require('./routes/orders'); 8 | const routesUsers = require('./routes/users'); 9 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 10 | const pluginHapiPagination = require('./plugins/hapi-pagination'); 11 | const pluginHapiAuthJWT2 = require('./plugins/hapi-auth-jwt2'); 12 | 13 | const server = new Hapi.Server(); 14 | // 配置服务器启动host与端口 15 | server.connection({ 16 | port: config.port, 17 | host: config.host, 18 | }); 19 | 20 | const init = async () => { 21 | // 注册插件 22 | await server.register([ 23 | ...pluginHapiSwagger, 24 | pluginHapiPagination, 25 | hapiAuthJWT2, 26 | ]); 27 | pluginHapiAuthJWT2(server); 28 | // 注册路由 29 | server.route([ 30 | // 创建一个简单的hello hapi接口 31 | ...routesHelloHapi, 32 | ...routesShops, 33 | ...routesOrders, 34 | ...routesUsers, 35 | ]); 36 | // 启动服务 37 | await server.start(); 38 | 39 | console.log(`Server running at: ${server.info.uri}`); 40 | }; 41 | 42 | init(); 43 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | jwtSecret: env.JWT_SECRET, 7 | }; 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/models/goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | shop_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | thumb_url: DataTypes.STRING, 18 | }, 19 | { 20 | tableName: 'goods', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = { 9 | ...configs[env], 10 | define: { 11 | underscored: true, 12 | }, 13 | }; 14 | const db = {}; 15 | let sequelize = null; 16 | 17 | if (config.use_env_variable) { 18 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 19 | } else { 20 | sequelize = new Sequelize(config.database, config.username, config.password, config); 21 | } 22 | 23 | fs 24 | .readdirSync(__dirname) 25 | .filter((file) => { 26 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 27 | return result; 28 | }) 29 | .forEach((file) => { 30 | const model = sequelize.import(path.join(__dirname, file)); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach((modelName) => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/models/shops.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'shops', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | thumb_url: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'shops', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations seeders --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-auth-jwt2": "^7.4.1", 18 | "hapi-pagination": "^1.22.0", 19 | "hapi-swagger": "^7.10.0", 20 | "inert": "^4.2.1", 21 | "joi": "^13.6.0", 22 | "jsonwebtoken": "^8.3.0", 23 | "mysql2": "^1.6.1", 24 | "package": "^1.0.1", 25 | "sequelize": "^4.38.0", 26 | "vision": "^4.1.1" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^5.3.0", 30 | "eslint-config-airbnb-base": "^13.1.0", 31 | "eslint-plugin-import": "^2.14.0", 32 | "sequelize-cli": "^4.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/plugins/hapi-auth-jwt2.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | const validate = (decoded, request, callback) => { 4 | let error; 5 | /* 6 | 接口 POST /users/createJWT 中的 jwt 签发规则 7 | 8 | const payload = { 9 | userId: jwtInfo.userId, 10 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 11 | }; 12 | return JWT.sign(payload, config.jwtSecret); 13 | */ 14 | 15 | // decoded 为 JWT payload 被解码后的数据 16 | const { userId } = decoded; 17 | 18 | if (!userId) { 19 | return callback(error, false, userId); 20 | } 21 | const credentials = { 22 | userId, 23 | }; 24 | // 在路由接口的 handler 通过 request.auth.credentials 获取 jwt decoded 的值 25 | return callback(error, true, credentials); 26 | }; 27 | 28 | module.exports = (server) => { 29 | server.auth.strategy('jwt', 'jwt', { 30 | key: config.jwtSecret, 31 | validateFunc: validate, 32 | }); 33 | server.auth.default('jwt'); 34 | }; 35 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/plugins/hapi-pagination.js: -------------------------------------------------------------------------------- 1 | const hapiPagination = require('hapi-pagination'); 2 | 3 | const options = { 4 | query: { 5 | page: { 6 | name: 'page', 7 | default: 1, 8 | }, 9 | limit: { 10 | name: 'limit', 11 | default: 25, 12 | }, 13 | pagination: { 14 | name: 'pagination', 15 | default: true, 16 | }, 17 | invalid: 'defaults', 18 | }, 19 | meta: { 20 | name: 'meta', 21 | count: { 22 | active: true, 23 | name: 'count', 24 | }, 25 | totalCount: { 26 | active: true, 27 | name: 'totalCount', 28 | }, 29 | pageCount: { 30 | active: true, 31 | name: 'pageCount', 32 | }, 33 | self: { 34 | active: true, 35 | name: 'self', 36 | }, 37 | previous: { 38 | active: true, 39 | name: 'previous', 40 | }, 41 | next: { 42 | active: true, 43 | name: 'next', 44 | }, 45 | first: { 46 | active: true, 47 | name: 'first', 48 | }, 49 | last: { 50 | active: true, 51 | name: 'last', 52 | }, 53 | page: { 54 | active: false, 55 | // name == default.query.page.name 56 | }, 57 | limit: { 58 | active: false, 59 | // name == default.query.limit.name 60 | }, 61 | }, 62 | results: { 63 | name: 'results', 64 | }, 65 | reply: { 66 | paginate: 'paginate', 67 | }, 68 | routes: { 69 | include: [ 70 | '/shops', 71 | '/shops/{shopId}/goods', 72 | ], 73 | exclude: [], 74 | }, 75 | }; 76 | 77 | module.exports = { 78 | register: hapiPagination, 79 | options, 80 | }; 81 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | { name: 'users', description: '用户相关' }, 23 | ], 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | const { jwtHeaderDefine } = require('../utils/router-helper'); 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: '/', 7 | handler: (request, reply) => { 8 | /* 9 | plugins/hapi-auth-jwt2.js 中的 credentials 定义 10 | 11 | const credentials = { 12 | userId, 13 | }; 14 | */ 15 | console.log(request.auth.credentials); // 控制台输出 { userId: 1} 16 | reply('hello hapi'); 17 | }, 18 | config: { 19 | tags: ['api', 'tests'], 20 | description: '测试hello-hapi', 21 | validate: { 22 | ...jwtHeaderDefine, // 增加需要 jwt auth 认证的接口 header 校验 23 | }, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { jwtHeaderDefine } = require('../utils/router-helper'); 3 | 4 | const GROUP_NAME = 'orders'; 5 | module.exports = [ 6 | { 7 | method: 'POST', 8 | path: `/${GROUP_NAME}`, 9 | handler: async (request, reply) => { 10 | reply(); 11 | }, 12 | config: { 13 | tags: ['api', GROUP_NAME], 14 | description: '创建订单', 15 | validate: { 16 | payload: { 17 | goodsList: Joi.array().items( 18 | Joi.object().keys({ 19 | goods_id: Joi.number().integer(), 20 | count: Joi.number().integer(), 21 | }), 22 | ), 23 | }, 24 | ...jwtHeaderDefine, 25 | }, 26 | }, 27 | }, 28 | { 29 | method: 'POST', 30 | path: `/${GROUP_NAME}/{orderId}/pay`, 31 | handler: async (request, reply) => { 32 | reply(); 33 | }, 34 | config: { 35 | tags: ['api', GROUP_NAME], 36 | description: '支付某条订单', 37 | validate: { 38 | params: { 39 | orderId: Joi.string().required(), 40 | }, 41 | }, 42 | }, 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { paginationDefine } = require('../utils/router-helper'); 3 | const models = require('../models'); 4 | 5 | const GROUP_NAME = 'shops'; 6 | 7 | module.exports = [ 8 | { 9 | method: 'GET', 10 | path: `/${GROUP_NAME}`, 11 | handler: async (request, reply) => { 12 | const { rows: results, count: totalCount } = await models.shops.findAndCountAll({ 13 | attributes: [ 14 | 'id', 15 | 'name', 16 | ], 17 | limit: request.query.limit, 18 | offset: (request.query.page - 1) * request.query.limit, 19 | }); 20 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 21 | reply({ results, totalCount }); 22 | }, 23 | config: { 24 | tags: ['api', GROUP_NAME], 25 | auth: false, 26 | description: '获取店铺列表', 27 | validate: { 28 | query: { 29 | ...paginationDefine, 30 | }, 31 | }, 32 | }, 33 | }, 34 | { 35 | method: 'GET', 36 | path: `/${GROUP_NAME}/{shopId}/goods`, 37 | handler: async (request, reply) => { 38 | // 增加带有where的条件查询 39 | const { rows: results, count: totalCount } = await models.goods.findAndCountAll({ 40 | // 基于 shop_id 的条件查询 41 | where: { 42 | shop_id: request.params.shopId, 43 | }, 44 | attributes: [ 45 | 'id', 46 | 'name', 47 | ], 48 | limit: request.query.limit, 49 | offset: (request.query.page - 1) * request.query.limit, 50 | }); 51 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 52 | reply({ results, totalCount }); 53 | }, 54 | config: { 55 | tags: ['api', GROUP_NAME], 56 | auth: false, 57 | description: '获取店铺的商品列表', 58 | validate: { 59 | params: { 60 | shopId: Joi.string().required().description('店铺的id'), 61 | }, 62 | query: { 63 | ...paginationDefine, 64 | }, 65 | }, 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/routes/users.js: -------------------------------------------------------------------------------- 1 | const JWT = require('jsonwebtoken'); 2 | const config = require('../config'); 3 | 4 | const GROUP_NAME = 'users'; 5 | 6 | module.exports = [{ 7 | method: 'POST', 8 | path: `/${GROUP_NAME}/createJWT`, 9 | handler: async (request, reply) => { 10 | const generateJWT = (jwtInfo) => { 11 | const payload = { 12 | userId: jwtInfo.userId, 13 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 14 | }; 15 | return JWT.sign(payload, config.jwtSecret); 16 | }; 17 | reply(generateJWT({ 18 | userId: 1, 19 | })); 20 | }, 21 | config: { 22 | tags: ['api', GROUP_NAME], 23 | description: '用于测试的用户 JWT 签发', 24 | auth: false, // 约定此接口不参与 JWT 的用户验证,会结合下面的 hapi-auth-jwt 来使用 25 | }, 26 | }]; 27 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/seeders/20180819061006-init-shops.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'shops', 9 | [ 10 | { 11 | id: 1, name: '店铺1', thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '店铺2', thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '店铺3', thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '店铺4', thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('shops', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/seeders/20180819102052-init-goods.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'goods', 9 | [ 10 | { 11 | id: 1, name: '商品1-1', shop_id: 1, thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '商品1-2', shop_id: 1, thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '商品1-3', shop_id: 1, thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '商品2-1', shop_id: 2, thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('goods', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter10/hapi-tutorial-1/utils/router-helper.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const paginationDefine = { 4 | limit: Joi.number().integer().min(1).default(10) 5 | .description('每页的条目数'), 6 | page: Joi.number().integer().min(1).default(1) 7 | .description('页码数'), 8 | pagination: Joi.boolean().default(true).description('是否开启分页,默认为true'), 9 | }; 10 | 11 | const jwtHeaderDefine = { 12 | headers: Joi.object({ 13 | authorization: Joi.string().required(), 14 | }).unknown(), 15 | }; 16 | 17 | module.exports = { paginationDefine, jwtHeaderDefine }; 18 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password 11 | 12 | # JWT 的签发秘钥 13 | JWT_SECRET = your-secret 14 | 15 | # 微信小程序配置 16 | WX_APPID = your-app-id 17 | WX_SECRET = your-secret -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const hapiAuthJWT2 = require('hapi-auth-jwt2'); 3 | require('env2')('./.env'); 4 | const config = require('./config'); 5 | const routesHelloHapi = require('./routes/hello-hapi'); 6 | const routesShops = require('./routes/shops'); 7 | const routesOrders = require('./routes/orders'); 8 | const routesUsers = require('./routes/users'); 9 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 10 | const pluginHapiPagination = require('./plugins/hapi-pagination'); 11 | const pluginHapiAuthJWT2 = require('./plugins/hapi-auth-jwt2'); 12 | 13 | const server = new Hapi.Server(); 14 | // 配置服务器启动host与端口 15 | server.connection({ 16 | port: config.port, 17 | host: config.host, 18 | }); 19 | 20 | const init = async () => { 21 | // 注册插件 22 | await server.register([ 23 | ...pluginHapiSwagger, 24 | pluginHapiPagination, 25 | hapiAuthJWT2, 26 | ]); 27 | pluginHapiAuthJWT2(server); 28 | // 注册路由 29 | server.route([ 30 | // 创建一个简单的hello hapi接口 31 | ...routesHelloHapi, 32 | ...routesShops, 33 | ...routesOrders, 34 | ...routesUsers, 35 | ]); 36 | // 启动服务 37 | await server.start(); 38 | 39 | console.log(`Server running at: ${server.info.uri}`); 40 | }; 41 | 42 | init(); 43 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | jwtSecret: env.JWT_SECRET, 7 | wxSecret: env.WX_SECRET, 8 | wxAppid: env.WX_APPID, 9 | }; 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/migrations/20180826142118-create-users-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'users', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | nick_name: Sequelize.STRING, 11 | avatar_url: Sequelize.STRING, 12 | gender: Sequelize.INTEGER, 13 | open_id: Sequelize.STRING, 14 | session_key: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('users'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/models/goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | shop_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | thumb_url: DataTypes.STRING, 18 | }, 19 | { 20 | tableName: 'goods', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = { 9 | ...configs[env], 10 | define: { 11 | underscored: true, 12 | }, 13 | }; 14 | const db = {}; 15 | let sequelize = null; 16 | 17 | if (config.use_env_variable) { 18 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 19 | } else { 20 | sequelize = new Sequelize(config.database, config.username, config.password, config); 21 | } 22 | 23 | fs 24 | .readdirSync(__dirname) 25 | .filter((file) => { 26 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 27 | return result; 28 | }) 29 | .forEach((file) => { 30 | const model = sequelize.import(path.join(__dirname, file)); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach((modelName) => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/models/shops.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'shops', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | thumb_url: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'shops', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/models/users.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'users', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | nick_name: DataTypes.STRING, 10 | avatar_url: DataTypes.STRING, 11 | gender: DataTypes.INTEGER, 12 | open_id: DataTypes.STRING, 13 | session_key: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'users', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations seeders --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.18.0", 16 | "env2": "^2.2.2", 17 | "hapi": "^16.6.3", 18 | "hapi-auth-jwt2": "^7.4.1", 19 | "hapi-pagination": "^1.22.0", 20 | "hapi-swagger": "^7.10.0", 21 | "inert": "^4.2.1", 22 | "joi": "^13.6.0", 23 | "jsonwebtoken": "^8.3.0", 24 | "mysql2": "^1.6.1", 25 | "package": "^1.0.1", 26 | "sequelize": "^4.38.0", 27 | "vision": "^4.1.1" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^5.3.0", 31 | "eslint-config-airbnb-base": "^13.1.0", 32 | "eslint-plugin-import": "^2.14.0", 33 | "sequelize-cli": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/plugins/hapi-auth-jwt2.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | const validate = (decoded, request, callback) => { 4 | let error; 5 | /* 6 | 接口 POST /users/createJWT 中的 jwt 签发规则 7 | 8 | const payload = { 9 | userId: jwtInfo.userId, 10 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 11 | }; 12 | return JWT.sign(payload, config.jwtSecret); 13 | */ 14 | 15 | // decoded 为 JWT payload 被解码后的数据 16 | const { userId } = decoded; 17 | 18 | if (!userId) { 19 | return callback(error, false, userId); 20 | } 21 | const credentials = { 22 | userId, 23 | }; 24 | // 在路由接口的 handler 通过 request.auth.credentials 获取 jwt decoded 的值 25 | return callback(error, true, credentials); 26 | }; 27 | 28 | module.exports = (server) => { 29 | server.auth.strategy('jwt', 'jwt', { 30 | key: config.jwtSecret, 31 | validateFunc: validate, 32 | }); 33 | server.auth.default('jwt'); 34 | }; 35 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/plugins/hapi-pagination.js: -------------------------------------------------------------------------------- 1 | const hapiPagination = require('hapi-pagination'); 2 | 3 | const options = { 4 | query: { 5 | page: { 6 | name: 'page', 7 | default: 1, 8 | }, 9 | limit: { 10 | name: 'limit', 11 | default: 25, 12 | }, 13 | pagination: { 14 | name: 'pagination', 15 | default: true, 16 | }, 17 | invalid: 'defaults', 18 | }, 19 | meta: { 20 | name: 'meta', 21 | count: { 22 | active: true, 23 | name: 'count', 24 | }, 25 | totalCount: { 26 | active: true, 27 | name: 'totalCount', 28 | }, 29 | pageCount: { 30 | active: true, 31 | name: 'pageCount', 32 | }, 33 | self: { 34 | active: true, 35 | name: 'self', 36 | }, 37 | previous: { 38 | active: true, 39 | name: 'previous', 40 | }, 41 | next: { 42 | active: true, 43 | name: 'next', 44 | }, 45 | first: { 46 | active: true, 47 | name: 'first', 48 | }, 49 | last: { 50 | active: true, 51 | name: 'last', 52 | }, 53 | page: { 54 | active: false, 55 | // name == default.query.page.name 56 | }, 57 | limit: { 58 | active: false, 59 | // name == default.query.limit.name 60 | }, 61 | }, 62 | results: { 63 | name: 'results', 64 | }, 65 | reply: { 66 | paginate: 'paginate', 67 | }, 68 | routes: { 69 | include: [ 70 | '/shops', 71 | '/shops/{shopId}/goods', 72 | ], 73 | exclude: [], 74 | }, 75 | }; 76 | 77 | module.exports = { 78 | register: hapiPagination, 79 | options, 80 | }; 81 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | { name: 'users', description: '用户相关' }, 23 | ], 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | const { jwtHeaderDefine } = require('../utils/router-helper'); 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: '/', 7 | handler: (request, reply) => { 8 | /* 9 | plugins/hapi-auth-jwt2.js 中的 credentials 定义 10 | 11 | const credentials = { 12 | userId, 13 | }; 14 | */ 15 | console.log(request.auth.credentials); // 控制台输出 { userId: 1} 16 | reply('hello hapi'); 17 | }, 18 | config: { 19 | tags: ['api', 'tests'], 20 | description: '测试hello-hapi', 21 | validate: { 22 | ...jwtHeaderDefine, // 增加需要 jwt auth 认证的接口 header 校验 23 | }, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { jwtHeaderDefine } = require('../utils/router-helper'); 3 | 4 | const GROUP_NAME = 'orders'; 5 | module.exports = [ 6 | { 7 | method: 'POST', 8 | path: `/${GROUP_NAME}`, 9 | handler: async (request, reply) => { 10 | reply(); 11 | }, 12 | config: { 13 | tags: ['api', GROUP_NAME], 14 | description: '创建订单', 15 | validate: { 16 | payload: { 17 | goodsList: Joi.array().items( 18 | Joi.object().keys({ 19 | goods_id: Joi.number().integer(), 20 | count: Joi.number().integer(), 21 | }), 22 | ), 23 | }, 24 | ...jwtHeaderDefine, 25 | }, 26 | }, 27 | }, 28 | { 29 | method: 'POST', 30 | path: `/${GROUP_NAME}/{orderId}/pay`, 31 | handler: async (request, reply) => { 32 | reply(); 33 | }, 34 | config: { 35 | tags: ['api', GROUP_NAME], 36 | description: '支付某条订单', 37 | validate: { 38 | params: { 39 | orderId: Joi.string().required(), 40 | }, 41 | }, 42 | }, 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { paginationDefine } = require('../utils/router-helper'); 3 | const models = require('../models'); 4 | 5 | const GROUP_NAME = 'shops'; 6 | 7 | module.exports = [ 8 | { 9 | method: 'GET', 10 | path: `/${GROUP_NAME}`, 11 | handler: async (request, reply) => { 12 | const { rows: results, count: totalCount } = await models.shops.findAndCountAll({ 13 | attributes: [ 14 | 'id', 15 | 'name', 16 | ], 17 | limit: request.query.limit, 18 | offset: (request.query.page - 1) * request.query.limit, 19 | }); 20 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 21 | reply({ results, totalCount }); 22 | }, 23 | config: { 24 | tags: ['api', GROUP_NAME], 25 | auth: false, 26 | description: '获取店铺列表', 27 | validate: { 28 | query: { 29 | ...paginationDefine, 30 | }, 31 | }, 32 | }, 33 | }, 34 | { 35 | method: 'GET', 36 | path: `/${GROUP_NAME}/{shopId}/goods`, 37 | handler: async (request, reply) => { 38 | // 增加带有where的条件查询 39 | const { rows: results, count: totalCount } = await models.goods.findAndCountAll({ 40 | // 基于 shop_id 的条件查询 41 | where: { 42 | shop_id: request.params.shopId, 43 | }, 44 | attributes: [ 45 | 'id', 46 | 'name', 47 | ], 48 | limit: request.query.limit, 49 | offset: (request.query.page - 1) * request.query.limit, 50 | }); 51 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 52 | reply({ results, totalCount }); 53 | }, 54 | config: { 55 | tags: ['api', GROUP_NAME], 56 | auth: false, 57 | description: '获取店铺的商品列表', 58 | validate: { 59 | params: { 60 | shopId: Joi.string().required().description('店铺的id'), 61 | }, 62 | query: { 63 | ...paginationDefine, 64 | }, 65 | }, 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/routes/users.js: -------------------------------------------------------------------------------- 1 | const JWT = require('jsonwebtoken'); 2 | const Joi = require('joi'); 3 | const axios = require('axios'); 4 | const config = require('../config'); 5 | const models = require('../models'); 6 | const decryptData = require('../utils/decryped-data'); 7 | 8 | const GROUP_NAME = 'users'; 9 | 10 | module.exports = [ 11 | { 12 | method: 'POST', 13 | path: `/${GROUP_NAME}/createJWT`, 14 | handler: async (request, reply) => { 15 | const generateJWT = (jwtInfo) => { 16 | const payload = { 17 | userId: jwtInfo.userId, 18 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 19 | }; 20 | return JWT.sign(payload, config.jwtSecret); 21 | }; 22 | reply(generateJWT({ 23 | userId: 1, 24 | })); 25 | }, 26 | config: { 27 | tags: ['api', GROUP_NAME], 28 | description: '用于测试的用户 JWT 签发', 29 | auth: false, // 约定此接口不参与 JWT 的用户验证,会结合下面的 hapi-auth-jwt 来使用 30 | }, 31 | }, 32 | { 33 | method: 'POST', 34 | path: `/${GROUP_NAME}/wxLogin`, 35 | handler: async (req, reply) => { 36 | const appid = config.wxAppid; // 你的小程序 appId 37 | const secret = config.wxSecret; // 你的小程序 appSecret 38 | const { code, encryptedData, iv } = req.payload; 39 | // 向微信小程序开放平台 换取 openid 与 session_key 40 | const response = await axios({ 41 | url: 'https://api.weixin.qq.com/sns/jscode2session', 42 | method: 'GET', 43 | params: { 44 | appid, 45 | secret, 46 | js_code: code, 47 | grant_type: 'authorization_code', 48 | }, 49 | }); 50 | // response 中返回 openid 与 session_key 51 | const { openid, session_key: sessionKey } = response.data; 52 | // 基于 openid 查找或创建一个用户 53 | const user = await models.users.findOrCreate({ 54 | where: { open_id: openid }, 55 | }); 56 | // decrypt 解码用户信息 57 | const userInfo = decryptData(encryptedData, iv, sessionKey, appid); 58 | // 更新user表中的用户的资料信息 59 | await models.users.update({ 60 | nick_name: userInfo.nickName, 61 | gender: userInfo.gender, 62 | avatar_url: userInfo.avatarUrl, 63 | open_id: openid, 64 | session_key: sessionKey, 65 | }, { 66 | where: { open_id: openid }, 67 | }); 68 | // 签发 jwt 69 | const generateJWT = (jwtInfo) => { 70 | const payload = { 71 | userId: jwtInfo.userId, 72 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 73 | }; 74 | return JWT.sign(payload, config.jwtSecret); 75 | }; 76 | reply(generateJWT({ 77 | userId: user[0].id, 78 | })); 79 | }, 80 | config: { 81 | auth: false, // 不需要用户验证 82 | tags: ['api', GROUP_NAME], 83 | validate: { 84 | payload: { 85 | code: Joi.string().required().description('微信用户登录的临时code'), 86 | encryptedData: Joi.string().required().description('微信用户信息encryptedData'), 87 | iv: Joi.string().required().description('微信用户信息iv'), 88 | }, 89 | }, 90 | }, 91 | }, 92 | ]; 93 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/seeders/20180819061006-init-shops.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'shops', 9 | [ 10 | { 11 | id: 1, name: '店铺1', thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '店铺2', thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '店铺3', thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '店铺4', thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('shops', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/seeders/20180819102052-init-goods.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'goods', 9 | [ 10 | { 11 | id: 1, name: '商品1-1', shop_id: 1, thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '商品1-2', shop_id: 1, thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '商品1-3', shop_id: 1, thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '商品2-1', shop_id: 2, thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('goods', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/utils/decryped-data.js: -------------------------------------------------------------------------------- 1 | // 封装的 decryptData,用于解码小程序的 encryptData 2 | 3 | const crypto = require('crypto'); 4 | 5 | const decryptData = (encryptedData, iv, sessionKey, appid) => { 6 | // base64 decode 7 | const encryptedDataNew = Buffer.from(encryptedData, 'base64'); 8 | const sessionKeyNew = Buffer.from(sessionKey, 'base64'); 9 | const ivNew = Buffer.from(iv, 'base64'); 10 | 11 | let decoded = ''; 12 | try { 13 | // 解密,使用的算法是aes-128-cbc 14 | const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyNew, ivNew); 15 | // 设置自动 padding 为 true,删除填充补位 16 | decipher.setAutoPadding(true); 17 | decoded = decipher.update(encryptedDataNew, 'binary', 'utf8'); 18 | decoded += decipher.final('utf8'); 19 | decoded = JSON.parse(decoded); 20 | // decoded是解密后的用户信息 21 | } catch (err) { 22 | throw new Error('Illegal Buffer'); 23 | } 24 | 25 | // 解密后的用户数据中会有一个watermark属性,这个属性中包含这个小程序的appid和时间戳,下面是校验appid 26 | if (decoded.watermark.appid !== appid) { 27 | throw new Error('Illegal Buffer'); 28 | } 29 | 30 | // 返回解密后的用户数据 31 | return decoded; 32 | }; 33 | 34 | module.exports = decryptData; 35 | -------------------------------------------------------------------------------- /chapter11/hapi-tutorial-1/utils/router-helper.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const paginationDefine = { 4 | limit: Joi.number().integer().min(1).default(10) 5 | .description('每页的条目数'), 6 | page: Joi.number().integer().min(1).default(1) 7 | .description('页码数'), 8 | pagination: Joi.boolean().default(true).description('是否开启分页,默认为true'), 9 | }; 10 | 11 | const jwtHeaderDefine = { 12 | headers: Joi.object({ 13 | authorization: Joi.string().required(), 14 | }).unknown(), 15 | }; 16 | 17 | module.exports = { paginationDefine, jwtHeaderDefine }; 18 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password 11 | 12 | # JWT 的签发秘钥 13 | JWT_SECRET = your-secret 14 | 15 | # 微信小程序配置 16 | WX_APPID = your-app-id 17 | WX_SECRET = your-secret -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const hapiAuthJWT2 = require('hapi-auth-jwt2'); 3 | require('env2')('./.env'); 4 | const config = require('./config'); 5 | const routesHelloHapi = require('./routes/hello-hapi'); 6 | const routesShops = require('./routes/shops'); 7 | const routesOrders = require('./routes/orders'); 8 | const routesUsers = require('./routes/users'); 9 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 10 | const pluginHapiPagination = require('./plugins/hapi-pagination'); 11 | const pluginHapiAuthJWT2 = require('./plugins/hapi-auth-jwt2'); 12 | 13 | const server = new Hapi.Server(); 14 | // 配置服务器启动host与端口 15 | server.connection({ 16 | port: config.port, 17 | host: config.host, 18 | }); 19 | 20 | const init = async () => { 21 | // 注册插件 22 | await server.register([ 23 | ...pluginHapiSwagger, 24 | pluginHapiPagination, 25 | hapiAuthJWT2, 26 | ]); 27 | pluginHapiAuthJWT2(server); 28 | // 注册路由 29 | server.route([ 30 | // 创建一个简单的hello hapi接口 31 | ...routesHelloHapi, 32 | ...routesShops, 33 | ...routesOrders, 34 | ...routesUsers, 35 | ]); 36 | // 启动服务 37 | await server.start(); 38 | 39 | console.log(`Server running at: ${server.info.uri}`); 40 | }; 41 | 42 | init(); 43 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | jwtSecret: env.JWT_SECRET, 7 | wxSecret: env.WX_SECRET, 8 | wxAppid: env.WX_APPID, 9 | }; 10 | module.exports = config; 11 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/migrations/20180826142118-create-users-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'users', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | nick_name: Sequelize.STRING, 11 | avatar_url: Sequelize.STRING, 12 | gender: Sequelize.INTEGER, 13 | open_id: Sequelize.STRING, 14 | session_key: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('users'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/migrations/20180826181335-create-orders-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'orders', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | user_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | payment_status: { 15 | type: Sequelize.ENUM('0', '1'), // 0 未支付, 1 已支付 16 | defaultValue: '0', 17 | }, 18 | created_at: Sequelize.DATE, 19 | updated_at: Sequelize.DATE, 20 | }, 21 | ), 22 | 23 | down: queryInterface => queryInterface.dropTable('orders'), 24 | }; 25 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/migrations/20180826181505-create-order-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'order_goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | order_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | goods_id: { 15 | type: Sequelize.INTEGER, 16 | allowNull: false, 17 | }, 18 | single_price: { 19 | type: Sequelize.FLOAT, 20 | allowNull: false, 21 | }, 22 | count: { 23 | type: Sequelize.INTEGER, 24 | allowNull: false, 25 | }, 26 | created_at: Sequelize.DATE, 27 | updated_at: Sequelize.DATE, 28 | }, 29 | ), 30 | 31 | down: queryInterface => queryInterface.dropTable('order_goods'), 32 | }; 33 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/models/goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | shop_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | thumb_url: DataTypes.STRING, 18 | }, 19 | { 20 | tableName: 'goods', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = { 9 | ...configs[env], 10 | define: { 11 | underscored: true, 12 | }, 13 | }; 14 | const db = {}; 15 | let sequelize = null; 16 | 17 | if (config.use_env_variable) { 18 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 19 | } else { 20 | sequelize = new Sequelize(config.database, config.username, config.password, config); 21 | } 22 | 23 | fs 24 | .readdirSync(__dirname) 25 | .filter((file) => { 26 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 27 | return result; 28 | }) 29 | .forEach((file) => { 30 | const model = sequelize.import(path.join(__dirname, file)); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach((modelName) => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/models/order-goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'order_goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | order_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | goods_id: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | }, 17 | single_price: { 18 | type: DataTypes.FLOAT, 19 | allowNull: false, 20 | }, 21 | count: { 22 | type: DataTypes.INTEGER, 23 | allowNull: false, 24 | }, 25 | }, 26 | { 27 | tableName: 'order_goods', 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/models/orders.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'orders', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | user_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | payment_status: { 14 | type: DataTypes.ENUM('0', '1'), // 0 未支付, 1 已支付 15 | defaultValue: '0', 16 | }, 17 | }, 18 | { 19 | tableName: 'orders', 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/models/shops.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'shops', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | thumb_url: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'shops', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/models/users.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'users', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | nick_name: DataTypes.STRING, 10 | avatar_url: DataTypes.STRING, 11 | gender: DataTypes.INTEGER, 12 | open_id: DataTypes.STRING, 13 | session_key: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'users', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations seeders --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.18.0", 16 | "env2": "^2.2.2", 17 | "hapi": "^16.6.3", 18 | "hapi-auth-jwt2": "^7.4.1", 19 | "hapi-pagination": "^1.22.0", 20 | "hapi-swagger": "^7.10.0", 21 | "inert": "^4.2.1", 22 | "joi": "^13.6.0", 23 | "jsonwebtoken": "^8.3.0", 24 | "mysql2": "^1.6.1", 25 | "package": "^1.0.1", 26 | "sequelize": "^4.38.0", 27 | "vision": "^4.1.1" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^5.3.0", 31 | "eslint-config-airbnb-base": "^13.1.0", 32 | "eslint-plugin-import": "^2.14.0", 33 | "sequelize-cli": "^4.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/plugins/hapi-auth-jwt2.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | const validate = (decoded, request, callback) => { 4 | let error; 5 | /* 6 | 接口 POST /users/createJWT 中的 jwt 签发规则 7 | 8 | const payload = { 9 | userId: jwtInfo.userId, 10 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 11 | }; 12 | return JWT.sign(payload, config.jwtSecret); 13 | */ 14 | 15 | // decoded 为 JWT payload 被解码后的数据 16 | const { userId } = decoded; 17 | 18 | if (!userId) { 19 | return callback(error, false, userId); 20 | } 21 | const credentials = { 22 | userId, 23 | }; 24 | // 在路由接口的 handler 通过 request.auth.credentials 获取 jwt decoded 的值 25 | return callback(error, true, credentials); 26 | }; 27 | 28 | module.exports = (server) => { 29 | server.auth.strategy('jwt', 'jwt', { 30 | key: config.jwtSecret, 31 | validateFunc: validate, 32 | }); 33 | server.auth.default('jwt'); 34 | }; 35 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/plugins/hapi-pagination.js: -------------------------------------------------------------------------------- 1 | const hapiPagination = require('hapi-pagination'); 2 | 3 | const options = { 4 | query: { 5 | page: { 6 | name: 'page', 7 | default: 1, 8 | }, 9 | limit: { 10 | name: 'limit', 11 | default: 25, 12 | }, 13 | pagination: { 14 | name: 'pagination', 15 | default: true, 16 | }, 17 | invalid: 'defaults', 18 | }, 19 | meta: { 20 | name: 'meta', 21 | count: { 22 | active: true, 23 | name: 'count', 24 | }, 25 | totalCount: { 26 | active: true, 27 | name: 'totalCount', 28 | }, 29 | pageCount: { 30 | active: true, 31 | name: 'pageCount', 32 | }, 33 | self: { 34 | active: true, 35 | name: 'self', 36 | }, 37 | previous: { 38 | active: true, 39 | name: 'previous', 40 | }, 41 | next: { 42 | active: true, 43 | name: 'next', 44 | }, 45 | first: { 46 | active: true, 47 | name: 'first', 48 | }, 49 | last: { 50 | active: true, 51 | name: 'last', 52 | }, 53 | page: { 54 | active: false, 55 | // name == default.query.page.name 56 | }, 57 | limit: { 58 | active: false, 59 | // name == default.query.limit.name 60 | }, 61 | }, 62 | results: { 63 | name: 'results', 64 | }, 65 | reply: { 66 | paginate: 'paginate', 67 | }, 68 | routes: { 69 | include: [ 70 | '/shops', 71 | '/shops/{shopId}/goods', 72 | ], 73 | exclude: [], 74 | }, 75 | }; 76 | 77 | module.exports = { 78 | register: hapiPagination, 79 | options, 80 | }; 81 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | { name: 'users', description: '用户相关' }, 23 | ], 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | const { jwtHeaderDefine } = require('../utils/router-helper'); 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: '/', 7 | handler: (request, reply) => { 8 | /* 9 | plugins/hapi-auth-jwt2.js 中的 credentials 定义 10 | 11 | const credentials = { 12 | userId, 13 | }; 14 | */ 15 | console.log(request.auth.credentials); // 控制台输出 { userId: 1} 16 | reply('hello hapi'); 17 | }, 18 | config: { 19 | tags: ['api', 'tests'], 20 | description: '测试hello-hapi', 21 | validate: { 22 | ...jwtHeaderDefine, // 增加需要 jwt auth 认证的接口 header 校验 23 | }, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const models = require('../models'); 3 | const { jwtHeaderDefine } = require('../utils/router-helper'); 4 | 5 | const GROUP_NAME = 'orders'; 6 | module.exports = [ 7 | { 8 | method: 'POST', 9 | path: `/${GROUP_NAME}`, 10 | handler: async (request, reply) => { 11 | await models.sequelize.transaction((t) => { 12 | const result = models.orders.create( 13 | { user_id: request.auth.credentials.userId }, 14 | { transaction: t }, 15 | ).then((order) => { 16 | const goodsList = []; 17 | request.payload.goodsList.forEach((item) => { 18 | goodsList.push(models.order_goods.create({ 19 | order_id: order.dataValues.id, 20 | goods_id: item.goods_id, 21 | // 此处单价的数值应该从商品表中反查出写入,出于教程的精简性而省略该步骤 22 | single_price: 4.9, 23 | count: item.count, 24 | })); 25 | }); 26 | return Promise.all(goodsList); 27 | }); 28 | return result; 29 | }).then(() => { 30 | // 事务已被提交 31 | reply('success'); 32 | }).catch(() => { 33 | // 事务已被回滚 34 | reply('error'); 35 | }); 36 | }, 37 | config: { 38 | tags: ['api', GROUP_NAME], 39 | description: '创建订单', 40 | validate: { 41 | payload: { 42 | goodsList: Joi.array().items( 43 | Joi.object().keys({ 44 | goods_id: Joi.number().integer(), 45 | count: Joi.number().integer(), 46 | }), 47 | ), 48 | }, 49 | ...jwtHeaderDefine, 50 | }, 51 | }, 52 | }, 53 | { 54 | method: 'POST', 55 | path: `/${GROUP_NAME}/{orderId}/pay`, 56 | handler: async (request, reply) => { 57 | reply(); 58 | }, 59 | config: { 60 | tags: ['api', GROUP_NAME], 61 | description: '支付某条订单', 62 | validate: { 63 | params: { 64 | orderId: Joi.string().required(), 65 | }, 66 | }, 67 | }, 68 | }, 69 | ]; 70 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { paginationDefine } = require('../utils/router-helper'); 3 | const models = require('../models'); 4 | 5 | const GROUP_NAME = 'shops'; 6 | 7 | module.exports = [ 8 | { 9 | method: 'GET', 10 | path: `/${GROUP_NAME}`, 11 | handler: async (request, reply) => { 12 | const { rows: results, count: totalCount } = await models.shops.findAndCountAll({ 13 | attributes: [ 14 | 'id', 15 | 'name', 16 | ], 17 | limit: request.query.limit, 18 | offset: (request.query.page - 1) * request.query.limit, 19 | }); 20 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 21 | reply({ results, totalCount }); 22 | }, 23 | config: { 24 | tags: ['api', GROUP_NAME], 25 | auth: false, 26 | description: '获取店铺列表', 27 | validate: { 28 | query: { 29 | ...paginationDefine, 30 | }, 31 | }, 32 | }, 33 | }, 34 | { 35 | method: 'GET', 36 | path: `/${GROUP_NAME}/{shopId}/goods`, 37 | handler: async (request, reply) => { 38 | // 增加带有where的条件查询 39 | const { rows: results, count: totalCount } = await models.goods.findAndCountAll({ 40 | // 基于 shop_id 的条件查询 41 | where: { 42 | shop_id: request.params.shopId, 43 | }, 44 | attributes: [ 45 | 'id', 46 | 'name', 47 | ], 48 | limit: request.query.limit, 49 | offset: (request.query.page - 1) * request.query.limit, 50 | }); 51 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 52 | reply({ results, totalCount }); 53 | }, 54 | config: { 55 | tags: ['api', GROUP_NAME], 56 | auth: false, 57 | description: '获取店铺的商品列表', 58 | validate: { 59 | params: { 60 | shopId: Joi.string().required().description('店铺的id'), 61 | }, 62 | query: { 63 | ...paginationDefine, 64 | }, 65 | }, 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/routes/users.js: -------------------------------------------------------------------------------- 1 | const JWT = require('jsonwebtoken'); 2 | const Joi = require('joi'); 3 | const axios = require('axios'); 4 | const config = require('../config'); 5 | const models = require('../models'); 6 | const decryptData = require('../utils/decryped-data'); 7 | 8 | const GROUP_NAME = 'users'; 9 | 10 | module.exports = [ 11 | { 12 | method: 'POST', 13 | path: `/${GROUP_NAME}/createJWT`, 14 | handler: async (request, reply) => { 15 | const generateJWT = (jwtInfo) => { 16 | const payload = { 17 | userId: jwtInfo.userId, 18 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 19 | }; 20 | return JWT.sign(payload, config.jwtSecret); 21 | }; 22 | reply(generateJWT({ 23 | userId: 1, 24 | })); 25 | }, 26 | config: { 27 | tags: ['api', GROUP_NAME], 28 | description: '用于测试的用户 JWT 签发', 29 | auth: false, // 约定此接口不参与 JWT 的用户验证,会结合下面的 hapi-auth-jwt 来使用 30 | }, 31 | }, 32 | { 33 | method: 'POST', 34 | path: `/${GROUP_NAME}/wxLogin`, 35 | handler: async (req, reply) => { 36 | const appid = config.wxAppid; // 你的小程序 appId 37 | const secret = config.wxSecret; // 你的小程序 appSecret 38 | const { code, encryptedData, iv } = req.payload; 39 | // 向微信小程序开放平台 换取 openid 与 session_key 40 | const response = await axios({ 41 | url: 'https://api.weixin.qq.com/sns/jscode2session', 42 | method: 'GET', 43 | params: { 44 | appid, 45 | secret, 46 | js_code: code, 47 | grant_type: 'authorization_code', 48 | }, 49 | }); 50 | // response 中返回 openid 与 session_key 51 | const { openid, session_key: sessionKey } = response.data; 52 | // 基于 openid 查找或创建一个用户 53 | const user = await models.users.findOrCreate({ 54 | where: { open_id: openid }, 55 | }); 56 | // decrypt 解码用户信息 57 | const userInfo = decryptData(encryptedData, iv, sessionKey, appid); 58 | // 更新user表中的用户的资料信息 59 | await models.users.update({ 60 | nick_name: userInfo.nickName, 61 | gender: userInfo.gender, 62 | avatar_url: userInfo.avatarUrl, 63 | open_id: openid, 64 | session_key: sessionKey, 65 | }, { 66 | where: { open_id: openid }, 67 | }); 68 | // 签发 jwt 69 | const generateJWT = (jwtInfo) => { 70 | const payload = { 71 | userId: jwtInfo.userId, 72 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 73 | }; 74 | return JWT.sign(payload, config.jwtSecret); 75 | }; 76 | reply(generateJWT({ 77 | userId: user[0].id, 78 | })); 79 | }, 80 | config: { 81 | auth: false, // 不需要用户验证 82 | tags: ['api', GROUP_NAME], 83 | validate: { 84 | payload: { 85 | code: Joi.string().required().description('微信用户登录的临时code'), 86 | encryptedData: Joi.string().required().description('微信用户信息encryptedData'), 87 | iv: Joi.string().required().description('微信用户信息iv'), 88 | }, 89 | }, 90 | }, 91 | }, 92 | ]; 93 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/seeders/20180819061006-init-shops.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'shops', 9 | [ 10 | { 11 | id: 1, name: '店铺1', thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '店铺2', thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '店铺3', thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '店铺4', thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('shops', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/seeders/20180819102052-init-goods.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'goods', 9 | [ 10 | { 11 | id: 1, name: '商品1-1', shop_id: 1, thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '商品1-2', shop_id: 1, thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '商品1-3', shop_id: 1, thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '商品2-1', shop_id: 2, thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('goods', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/utils/decryped-data.js: -------------------------------------------------------------------------------- 1 | // 封装的 decryptData,用于解码小程序的 encryptData 2 | 3 | const crypto = require('crypto'); 4 | 5 | const decryptData = (encryptedData, iv, sessionKey, appid) => { 6 | // base64 decode 7 | const encryptedDataNew = Buffer.from(encryptedData, 'base64'); 8 | const sessionKeyNew = Buffer.from(sessionKey, 'base64'); 9 | const ivNew = Buffer.from(iv, 'base64'); 10 | 11 | let decoded = ''; 12 | try { 13 | // 解密,使用的算法是aes-128-cbc 14 | const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyNew, ivNew); 15 | // 设置自动 padding 为 true,删除填充补位 16 | decipher.setAutoPadding(true); 17 | decoded = decipher.update(encryptedDataNew, 'binary', 'utf8'); 18 | decoded += decipher.final('utf8'); 19 | decoded = JSON.parse(decoded); 20 | // decoded是解密后的用户信息 21 | } catch (err) { 22 | throw new Error('Illegal Buffer'); 23 | } 24 | 25 | // 解密后的用户数据中会有一个watermark属性,这个属性中包含这个小程序的appid和时间戳,下面是校验appid 26 | if (decoded.watermark.appid !== appid) { 27 | throw new Error('Illegal Buffer'); 28 | } 29 | 30 | // 返回解密后的用户数据 31 | return decoded; 32 | }; 33 | 34 | module.exports = decryptData; 35 | -------------------------------------------------------------------------------- /chapter12/hapi-tutorial-1/utils/router-helper.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const paginationDefine = { 4 | limit: Joi.number().integer().min(1).default(10) 5 | .description('每页的条目数'), 6 | page: Joi.number().integer().min(1).default(1) 7 | .description('页码数'), 8 | pagination: Joi.boolean().default(true).description('是否开启分页,默认为true'), 9 | }; 10 | 11 | const jwtHeaderDefine = { 12 | headers: Joi.object({ 13 | authorization: Joi.string().required(), 14 | }).unknown(), 15 | }; 16 | 17 | module.exports = { paginationDefine, jwtHeaderDefine }; 18 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password 11 | 12 | # JWT 的签发秘钥 13 | JWT_SECRET = your-secret 14 | 15 | # 微信小程序配置 16 | WX_APPID = your-app-id 17 | WX_SECRET = your-secret 18 | WX_MCHID = your-mchid # 支付商户号 19 | WX_PAY_API_KEY = your-pay-api-key # 微信支付的 api key -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const hapiAuthJWT2 = require('hapi-auth-jwt2'); 3 | require('env2')('./.env'); 4 | const config = require('./config'); 5 | const routesHelloHapi = require('./routes/hello-hapi'); 6 | const routesShops = require('./routes/shops'); 7 | const routesOrders = require('./routes/orders'); 8 | const routesUsers = require('./routes/users'); 9 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 10 | const pluginHapiPagination = require('./plugins/hapi-pagination'); 11 | const pluginHapiAuthJWT2 = require('./plugins/hapi-auth-jwt2'); 12 | 13 | const server = new Hapi.Server(); 14 | // 配置服务器启动host与端口 15 | server.connection({ 16 | port: config.port, 17 | host: config.host, 18 | }); 19 | 20 | const init = async () => { 21 | // 注册插件 22 | await server.register([ 23 | ...pluginHapiSwagger, 24 | pluginHapiPagination, 25 | hapiAuthJWT2, 26 | ]); 27 | pluginHapiAuthJWT2(server); 28 | // 注册路由 29 | server.route([ 30 | // 创建一个简单的hello hapi接口 31 | ...routesHelloHapi, 32 | ...routesShops, 33 | ...routesOrders, 34 | ...routesUsers, 35 | ]); 36 | // 启动服务 37 | await server.start(); 38 | 39 | console.log(`Server running at: ${server.info.uri}`); 40 | }; 41 | 42 | init(); 43 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | jwtSecret: env.JWT_SECRET, 7 | wxSecret: env.WX_SECRET, 8 | wxAppid: env.WX_APPID, 9 | wxMchid: env.WX_MCHID, 10 | wxPayApiKey: env.WX_PAY_API_KEY, 11 | }; 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/migrations/20180826142118-create-users-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'users', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | nick_name: Sequelize.STRING, 11 | avatar_url: Sequelize.STRING, 12 | gender: Sequelize.INTEGER, 13 | open_id: Sequelize.STRING, 14 | session_key: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('users'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/migrations/20180826181335-create-orders-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'orders', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | user_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | payment_status: { 15 | type: Sequelize.ENUM('0', '1'), // 0 未支付, 1 已支付 16 | defaultValue: '0', 17 | }, 18 | created_at: Sequelize.DATE, 19 | updated_at: Sequelize.DATE, 20 | }, 21 | ), 22 | 23 | down: queryInterface => queryInterface.dropTable('orders'), 24 | }; 25 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/migrations/20180826181505-create-order-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'order_goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | order_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | goods_id: { 15 | type: Sequelize.INTEGER, 16 | allowNull: false, 17 | }, 18 | single_price: { 19 | type: Sequelize.FLOAT, 20 | allowNull: false, 21 | }, 22 | count: { 23 | type: Sequelize.INTEGER, 24 | allowNull: false, 25 | }, 26 | created_at: Sequelize.DATE, 27 | updated_at: Sequelize.DATE, 28 | }, 29 | ), 30 | 31 | down: queryInterface => queryInterface.dropTable('order_goods'), 32 | }; 33 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/models/goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | shop_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | thumb_url: DataTypes.STRING, 18 | }, 19 | { 20 | tableName: 'goods', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = { 9 | ...configs[env], 10 | define: { 11 | underscored: true, 12 | }, 13 | }; 14 | const db = {}; 15 | let sequelize = null; 16 | 17 | if (config.use_env_variable) { 18 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 19 | } else { 20 | sequelize = new Sequelize(config.database, config.username, config.password, config); 21 | } 22 | 23 | fs 24 | .readdirSync(__dirname) 25 | .filter((file) => { 26 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 27 | return result; 28 | }) 29 | .forEach((file) => { 30 | const model = sequelize.import(path.join(__dirname, file)); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach((modelName) => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/models/order-goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'order_goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | order_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | goods_id: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | }, 17 | single_price: { 18 | type: DataTypes.FLOAT, 19 | allowNull: false, 20 | }, 21 | count: { 22 | type: DataTypes.INTEGER, 23 | allowNull: false, 24 | }, 25 | }, 26 | { 27 | tableName: 'order_goods', 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/models/orders.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'orders', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | user_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | payment_status: { 14 | type: DataTypes.ENUM('0', '1'), // 0 未支付, 1 已支付 15 | defaultValue: '0', 16 | }, 17 | }, 18 | { 19 | tableName: 'orders', 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/models/shops.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'shops', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | thumb_url: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'shops', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/models/users.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'users', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | nick_name: DataTypes.STRING, 10 | avatar_url: DataTypes.STRING, 11 | gender: DataTypes.INTEGER, 12 | open_id: DataTypes.STRING, 13 | session_key: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'users', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations seeders --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "axios": "^0.18.0", 16 | "env2": "^2.2.2", 17 | "hapi": "^16.6.3", 18 | "hapi-auth-jwt2": "^7.4.1", 19 | "hapi-pagination": "^1.22.0", 20 | "hapi-swagger": "^7.10.0", 21 | "inert": "^4.2.1", 22 | "joi": "^13.6.0", 23 | "jsonwebtoken": "^8.3.0", 24 | "mysql2": "^1.6.1", 25 | "package": "^1.0.1", 26 | "sequelize": "^4.38.0", 27 | "vision": "^4.1.1", 28 | "xml2js": "^0.4.19" 29 | }, 30 | "devDependencies": { 31 | "eslint": "^5.3.0", 32 | "eslint-config-airbnb-base": "^13.1.0", 33 | "eslint-plugin-import": "^2.14.0", 34 | "sequelize-cli": "^4.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/plugins/hapi-auth-jwt2.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | const validate = (decoded, request, callback) => { 4 | let error; 5 | /* 6 | 接口 POST /users/createJWT 中的 jwt 签发规则 7 | 8 | const payload = { 9 | userId: jwtInfo.userId, 10 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 11 | }; 12 | return JWT.sign(payload, config.jwtSecret); 13 | */ 14 | 15 | // decoded 为 JWT payload 被解码后的数据 16 | const { userId } = decoded; 17 | 18 | if (!userId) { 19 | return callback(error, false, userId); 20 | } 21 | const credentials = { 22 | userId, 23 | }; 24 | // 在路由接口的 handler 通过 request.auth.credentials 获取 jwt decoded 的值 25 | return callback(error, true, credentials); 26 | }; 27 | 28 | module.exports = (server) => { 29 | server.auth.strategy('jwt', 'jwt', { 30 | key: config.jwtSecret, 31 | validateFunc: validate, 32 | }); 33 | server.auth.default('jwt'); 34 | }; 35 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/plugins/hapi-pagination.js: -------------------------------------------------------------------------------- 1 | const hapiPagination = require('hapi-pagination'); 2 | 3 | const options = { 4 | query: { 5 | page: { 6 | name: 'page', 7 | default: 1, 8 | }, 9 | limit: { 10 | name: 'limit', 11 | default: 25, 12 | }, 13 | pagination: { 14 | name: 'pagination', 15 | default: true, 16 | }, 17 | invalid: 'defaults', 18 | }, 19 | meta: { 20 | name: 'meta', 21 | count: { 22 | active: true, 23 | name: 'count', 24 | }, 25 | totalCount: { 26 | active: true, 27 | name: 'totalCount', 28 | }, 29 | pageCount: { 30 | active: true, 31 | name: 'pageCount', 32 | }, 33 | self: { 34 | active: true, 35 | name: 'self', 36 | }, 37 | previous: { 38 | active: true, 39 | name: 'previous', 40 | }, 41 | next: { 42 | active: true, 43 | name: 'next', 44 | }, 45 | first: { 46 | active: true, 47 | name: 'first', 48 | }, 49 | last: { 50 | active: true, 51 | name: 'last', 52 | }, 53 | page: { 54 | active: false, 55 | // name == default.query.page.name 56 | }, 57 | limit: { 58 | active: false, 59 | // name == default.query.limit.name 60 | }, 61 | }, 62 | results: { 63 | name: 'results', 64 | }, 65 | reply: { 66 | paginate: 'paginate', 67 | }, 68 | routes: { 69 | include: [ 70 | '/shops', 71 | '/shops/{shopId}/goods', 72 | ], 73 | exclude: [], 74 | }, 75 | }; 76 | 77 | module.exports = { 78 | register: hapiPagination, 79 | options, 80 | }; 81 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | { name: 'users', description: '用户相关' }, 23 | ], 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | const { jwtHeaderDefine } = require('../utils/router-helper'); 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: '/', 7 | handler: (request, reply) => { 8 | /* 9 | plugins/hapi-auth-jwt2.js 中的 credentials 定义 10 | 11 | const credentials = { 12 | userId, 13 | }; 14 | */ 15 | console.log(request.auth.credentials); // 控制台输出 { userId: 1} 16 | reply('hello hapi'); 17 | }, 18 | config: { 19 | tags: ['api', 'tests'], 20 | description: '测试hello-hapi', 21 | validate: { 22 | ...jwtHeaderDefine, // 增加需要 jwt auth 认证的接口 header 校验 23 | }, 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const xml2js = require('xml2js'); 3 | const axios = require('axios'); 4 | const crypto = require('crypto'); 5 | const models = require('../models'); 6 | const config = require('../config'); 7 | const { jwtHeaderDefine } = require('../utils/router-helper'); 8 | 9 | const GROUP_NAME = 'orders'; 10 | 11 | module.exports = [ 12 | { 13 | method: 'POST', 14 | path: `/${GROUP_NAME}`, 15 | handler: async (request, reply) => { 16 | await models.sequelize.transaction((t) => { 17 | const result = models.orders.create( 18 | { user_id: request.auth.credentials.userId }, 19 | { transaction: t }, 20 | ).then((order) => { 21 | const goodsList = []; 22 | request.payload.goodsList.forEach((item) => { 23 | goodsList.push(models.order_goods.create({ 24 | order_id: order.dataValues.id, 25 | goods_id: item.goods_id, 26 | // 此处单价的数值应该从商品表中反查出写入,出于教程的精简性而省略该步骤 27 | single_price: 4.9, 28 | count: item.count, 29 | })); 30 | }); 31 | return Promise.all(goodsList); 32 | }); 33 | return result; 34 | }).then(() => { 35 | // 事务已被提交 36 | reply('success'); 37 | }).catch(() => { 38 | // 事务已被回滚 39 | reply('error'); 40 | }); 41 | }, 42 | config: { 43 | tags: ['api', GROUP_NAME], 44 | description: '创建订单', 45 | validate: { 46 | payload: { 47 | goodsList: Joi.array().items( 48 | Joi.object().keys({ 49 | goods_id: Joi.number().integer(), 50 | count: Joi.number().integer(), 51 | }), 52 | ), 53 | }, 54 | ...jwtHeaderDefine, 55 | }, 56 | }, 57 | }, 58 | { 59 | method: 'POST', 60 | path: `/${GROUP_NAME}/{orderId}/pay`, 61 | handler: async (request, reply) => { 62 | // 从用户表中获取 openid 63 | const user = await models.users.findOne({ where: { id: request.auth.credentials.userId } }); 64 | const { openid } = user; 65 | // 构造 unifiedorder 所需入参 66 | const unifiedorderObj = { 67 | appid: config.wxAppid, // 小程序id 68 | body: '小程序支付', // 商品简单描述 69 | mch_id: config.wxMchid, // 商户号 70 | nonce_str: Math.random().toString(36).substr(2, 15), // 随机字符串 71 | notify_url: 'https://yourhost.com/orders/pay/notify', // 支付成功的回调地址 72 | openid, // 用户 openid 73 | out_trade_no: request.params.orderId, // 商户订单号 74 | spbill_create_ip: request.info.remoteAddress, // 调用支付接口的用户 ip 75 | total_fee: 1, // 总金额,单位为分 76 | trade_type: 'JSAPI', // 交易类型,默认 77 | }; 78 | // 签名的数据 79 | const getSignData = (rawData, apiKey) => { 80 | let keys = Object.keys(rawData); 81 | keys = keys.sort(); 82 | let string = ''; 83 | keys.forEach((key) => { 84 | string += `&${key}=${rawData[key]}`; 85 | }); 86 | string = string.substr(1); 87 | return crypto.createHash('md5').update(`${string}&key=${apiKey}`).digest('hex').toUpperCase(); 88 | }; 89 | // 将基础数据信息 sign 签名 90 | const sign = getSignData(unifiedorderObj, config.wxPayApiKey); 91 | // 需要被 post 的数据源 92 | const unifiedorderWithSign = { 93 | ...unifiedorderObj, 94 | sign, 95 | }; 96 | // 将需要 post 出去的订单参数,转换位 xml 格式 97 | const builder = new xml2js.Builder({ rootName: 'xml', headless: true }); 98 | const unifiedorderXML = builder.buildObject(unifiedorderWithSign); 99 | const result = await axios({ 100 | url: 'https://api.mch.weixin.qq.com/pay/unifiedorder', 101 | method: 'POST', 102 | data: unifiedorderXML, 103 | headers: { 'content-type': 'text/xml' }, 104 | }); 105 | // result 是一个 xml 结构的 response,转换为 jsonObject,并返回前端 106 | xml2js.parseString(result.data, (err, parsedResult) => { 107 | if (parsedResult.xml) { 108 | if (parsedResult.xml.return_code[0] === 'SUCCESS' 109 | && parsedResult.xml.result_code[0] === 'SUCCESS') { 110 | // 待签名的原始支付数据 111 | const replyData = { 112 | appId: parsedResult.xml.appid[0], 113 | timeStamp: (Date.now() / 1000).toString(), 114 | nonceStr: parsedResult.xml.nonce_str[0], 115 | package: `prepay_id=${parsedResult.xml.prepay_id[0]}`, 116 | signType: 'MD5', 117 | }; 118 | replyData.paySign = getSignData(replyData, config.wxPayApiKey); 119 | reply(replyData); 120 | } 121 | } 122 | }); 123 | }, 124 | config: { 125 | tags: ['api', GROUP_NAME], 126 | description: '支付某条订单', 127 | validate: { 128 | params: { 129 | orderId: Joi.string().required(), 130 | }, 131 | ...jwtHeaderDefine, 132 | }, 133 | }, 134 | }, 135 | { 136 | method: 'POST', 137 | path: `/${GROUP_NAME}/pay/notify`, 138 | handler: async (request, reply) => { 139 | xml2js.parseString(request.payload, async (err, parsedResult) => { 140 | if (parsedResult.xml.return_code[0] === 'SUCCESS') { 141 | // 微信统一支付状态成功,需要检验本地数据的逻辑一致性 142 | // 省略...细节逻辑校验 143 | // 更新该订单编号下的支付状态未已支付 144 | const orderId = parsedResult.xml.out_trade_no[0]; 145 | const orderResult = await models.orders.findOne({ where: { id: orderId } }); 146 | orderResult.payment_status = '1'; 147 | await orderResult.save(); 148 | // 返回微信,校验成功 149 | const retVal = { 150 | return_code: 'SUCCESS', 151 | return_msg: 'OK', 152 | }; 153 | const builder = new xml2js.Builder({ 154 | rootName: 'xml', 155 | headless: true, 156 | }); 157 | reply(builder.buildObject(retVal)); 158 | } 159 | }); 160 | }, 161 | config: { 162 | tags: ['api', GROUP_NAME], 163 | description: '微信支付成功的消息推送', 164 | auth: false, 165 | }, 166 | }, 167 | ]; 168 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { paginationDefine } = require('../utils/router-helper'); 3 | const models = require('../models'); 4 | 5 | const GROUP_NAME = 'shops'; 6 | 7 | module.exports = [ 8 | { 9 | method: 'GET', 10 | path: `/${GROUP_NAME}`, 11 | handler: async (request, reply) => { 12 | const { rows: results, count: totalCount } = await models.shops.findAndCountAll({ 13 | attributes: [ 14 | 'id', 15 | 'name', 16 | ], 17 | limit: request.query.limit, 18 | offset: (request.query.page - 1) * request.query.limit, 19 | }); 20 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 21 | reply({ results, totalCount }); 22 | }, 23 | config: { 24 | tags: ['api', GROUP_NAME], 25 | auth: false, 26 | description: '获取店铺列表', 27 | validate: { 28 | query: { 29 | ...paginationDefine, 30 | }, 31 | }, 32 | }, 33 | }, 34 | { 35 | method: 'GET', 36 | path: `/${GROUP_NAME}/{shopId}/goods`, 37 | handler: async (request, reply) => { 38 | // 增加带有where的条件查询 39 | const { rows: results, count: totalCount } = await models.goods.findAndCountAll({ 40 | // 基于 shop_id 的条件查询 41 | where: { 42 | shop_id: request.params.shopId, 43 | }, 44 | attributes: [ 45 | 'id', 46 | 'name', 47 | ], 48 | limit: request.query.limit, 49 | offset: (request.query.page - 1) * request.query.limit, 50 | }); 51 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 52 | reply({ results, totalCount }); 53 | }, 54 | config: { 55 | tags: ['api', GROUP_NAME], 56 | auth: false, 57 | description: '获取店铺的商品列表', 58 | validate: { 59 | params: { 60 | shopId: Joi.string().required().description('店铺的id'), 61 | }, 62 | query: { 63 | ...paginationDefine, 64 | }, 65 | }, 66 | }, 67 | }, 68 | ]; 69 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/routes/users.js: -------------------------------------------------------------------------------- 1 | const JWT = require('jsonwebtoken'); 2 | const Joi = require('joi'); 3 | const axios = require('axios'); 4 | const config = require('../config'); 5 | const models = require('../models'); 6 | const decryptData = require('../utils/decryped-data'); 7 | 8 | const GROUP_NAME = 'users'; 9 | 10 | module.exports = [ 11 | { 12 | method: 'POST', 13 | path: `/${GROUP_NAME}/createJWT`, 14 | handler: async (request, reply) => { 15 | const generateJWT = (jwtInfo) => { 16 | const payload = { 17 | userId: jwtInfo.userId, 18 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 19 | }; 20 | return JWT.sign(payload, config.jwtSecret); 21 | }; 22 | reply(generateJWT({ 23 | userId: 1, 24 | })); 25 | }, 26 | config: { 27 | tags: ['api', GROUP_NAME], 28 | description: '用于测试的用户 JWT 签发', 29 | auth: false, // 约定此接口不参与 JWT 的用户验证,会结合下面的 hapi-auth-jwt 来使用 30 | }, 31 | }, 32 | { 33 | method: 'POST', 34 | path: `/${GROUP_NAME}/wxLogin`, 35 | handler: async (req, reply) => { 36 | const appid = config.wxAppid; // 你的小程序 appId 37 | const secret = config.wxSecret; // 你的小程序 appSecret 38 | const { code, encryptedData, iv } = req.payload; 39 | // 向微信小程序开放平台 换取 openid 与 session_key 40 | const response = await axios({ 41 | url: 'https://api.weixin.qq.com/sns/jscode2session', 42 | method: 'GET', 43 | params: { 44 | appid, 45 | secret, 46 | js_code: code, 47 | grant_type: 'authorization_code', 48 | }, 49 | }); 50 | // response 中返回 openid 与 session_key 51 | const { openid, session_key: sessionKey } = response.data; 52 | // 基于 openid 查找或创建一个用户 53 | const user = await models.users.findOrCreate({ 54 | where: { open_id: openid }, 55 | }); 56 | // decrypt 解码用户信息 57 | const userInfo = decryptData(encryptedData, iv, sessionKey, appid); 58 | // 更新user表中的用户的资料信息 59 | await models.users.update({ 60 | nick_name: userInfo.nickName, 61 | gender: userInfo.gender, 62 | avatar_url: userInfo.avatarUrl, 63 | open_id: openid, 64 | session_key: sessionKey, 65 | }, { 66 | where: { open_id: openid }, 67 | }); 68 | // 签发 jwt 69 | const generateJWT = (jwtInfo) => { 70 | const payload = { 71 | userId: jwtInfo.userId, 72 | exp: Math.floor(new Date().getTime() / 1000) + 7 * 24 * 60 * 60, 73 | }; 74 | return JWT.sign(payload, config.jwtSecret); 75 | }; 76 | reply(generateJWT({ 77 | userId: user[0].id, 78 | })); 79 | }, 80 | config: { 81 | auth: false, // 不需要用户验证 82 | tags: ['api', GROUP_NAME], 83 | validate: { 84 | payload: { 85 | code: Joi.string().required().description('微信用户登录的临时code'), 86 | encryptedData: Joi.string().required().description('微信用户信息encryptedData'), 87 | iv: Joi.string().required().description('微信用户信息iv'), 88 | }, 89 | }, 90 | }, 91 | }, 92 | ]; 93 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/seeders/20180819061006-init-shops.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'shops', 9 | [ 10 | { 11 | id: 1, name: '店铺1', thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '店铺2', thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '店铺3', thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '店铺4', thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('shops', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/seeders/20180819102052-init-goods.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'goods', 9 | [ 10 | { 11 | id: 1, name: '商品1-1', shop_id: 1, thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '商品1-2', shop_id: 1, thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '商品1-3', shop_id: 1, thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '商品2-1', shop_id: 2, thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('goods', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/utils/decryped-data.js: -------------------------------------------------------------------------------- 1 | // 封装的 decryptData,用于解码小程序的 encryptData 2 | 3 | const crypto = require('crypto'); 4 | 5 | const decryptData = (encryptedData, iv, sessionKey, appid) => { 6 | // base64 decode 7 | const encryptedDataNew = Buffer.from(encryptedData, 'base64'); 8 | const sessionKeyNew = Buffer.from(sessionKey, 'base64'); 9 | const ivNew = Buffer.from(iv, 'base64'); 10 | 11 | let decoded = ''; 12 | try { 13 | // 解密,使用的算法是aes-128-cbc 14 | const decipher = crypto.createDecipheriv('aes-128-cbc', sessionKeyNew, ivNew); 15 | // 设置自动 padding 为 true,删除填充补位 16 | decipher.setAutoPadding(true); 17 | decoded = decipher.update(encryptedDataNew, 'binary', 'utf8'); 18 | decoded += decipher.final('utf8'); 19 | decoded = JSON.parse(decoded); 20 | // decoded是解密后的用户信息 21 | } catch (err) { 22 | throw new Error('Illegal Buffer'); 23 | } 24 | 25 | // 解密后的用户数据中会有一个watermark属性,这个属性中包含这个小程序的appid和时间戳,下面是校验appid 26 | if (decoded.watermark.appid !== appid) { 27 | throw new Error('Illegal Buffer'); 28 | } 29 | 30 | // 返回解密后的用户数据 31 | return decoded; 32 | }; 33 | 34 | module.exports = decryptData; 35 | -------------------------------------------------------------------------------- /chapter13/hapi-tutorial-1/utils/router-helper.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const paginationDefine = { 4 | limit: Joi.number().integer().min(1).default(10) 5 | .description('每页的条目数'), 6 | page: Joi.number().integer().min(1).default(1) 7 | .description('页码数'), 8 | pagination: Joi.boolean().default(true).description('是否开启分页,默认为true'), 9 | }; 10 | 11 | const jwtHeaderDefine = { 12 | headers: Joi.object({ 13 | authorization: Joi.string().required(), 14 | }).unknown(), 15 | }; 16 | 17 | module.exports = { paginationDefine, jwtHeaderDefine }; 18 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | 3 | const server = new Hapi.Server(); 4 | // 配置服务器启动host与端口 5 | server.connection({ 6 | port: 3000, 7 | host: '127.0.0.1', 8 | }); 9 | 10 | const init = async () => { 11 | server.route([ 12 | // 创建一个简单的hello hapi接口 13 | { 14 | method: 'GET', 15 | path: '/', 16 | handler: (request, reply) => { 17 | reply('hello hapi'); 18 | }, 19 | }, 20 | ]); 21 | // 启动服务 22 | await server.start(); 23 | console.log(`Server running at: ${server.info.uri}`); 24 | }; 25 | 26 | init(); 27 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "hapi": "^16.6.3" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^5.3.0", 18 | "eslint-config-airbnb-base": "^13.1.0", 19 | "eslint-plugin-import": "^2.14.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-2/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-2/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | const config = require('./config'); 3 | const routesHelloHapi = require('./routes/hello-hapi'); 4 | 5 | const server = new Hapi.Server(); 6 | // 配置服务器启动host与端口 7 | server.connection({ 8 | port: config.port, 9 | host: config.host, 10 | }); 11 | 12 | const init = async () => { 13 | server.route([ 14 | // 创建一个简单的hello hapi接口 15 | ...routesHelloHapi, 16 | ]); 17 | // 启动服务 18 | await server.start(); 19 | console.log(`Server running at: ${server.info.uri}`); 20 | }; 21 | 22 | init(); 23 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-2/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: '127.0.0.1', 3 | port: 3000, 4 | } -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "hapi": "^16.6.3" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^5.3.0", 18 | "eslint-config-airbnb-base": "^13.1.0", 19 | "eslint-plugin-import": "^2.14.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-2/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | } 8 | }, 9 | ] -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-3/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-3/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-3/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | 6 | const server = new Hapi.Server(); 7 | // 配置服务器启动host与端口 8 | server.connection({ 9 | port: config.port, 10 | host: config.host, 11 | }); 12 | 13 | const init = async () => { 14 | server.route([ 15 | // 创建一个简单的hello hapi接口 16 | ...routesHelloHapi, 17 | ]); 18 | // 启动服务 19 | await server.start(); 20 | console.log(`Server running at: ${server.info.uri}`); 21 | }; 22 | 23 | init(); 24 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-3/config/index.js: -------------------------------------------------------------------------------- 1 | const env = process.env; 2 | 3 | module.exports = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | } -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "env2": "^2.2.2", 15 | "hapi": "^16.6.3" 16 | }, 17 | "devDependencies": { 18 | "eslint": "^5.3.0", 19 | "eslint-config-airbnb-base": "^13.1.0", 20 | "eslint-plugin-import": "^2.14.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /chapter5/hapi-tutorial-3/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | } 8 | }, 9 | ] -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 6 | 7 | const server = new Hapi.Server(); 8 | // 配置服务器启动host与端口 9 | server.connection({ 10 | port: config.port, 11 | host: config.host, 12 | }); 13 | 14 | const init = async () => { 15 | await server.register([ 16 | ...pluginHapiSwagger, 17 | ]); 18 | server.route([ 19 | // 创建一个简单的hello hapi接口 20 | ...routesHelloHapi, 21 | ]); 22 | // 启动服务 23 | await server.start(); 24 | console.log(`Server running at: ${server.info.uri}`); 25 | }; 26 | 27 | init(); 28 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | module.exports = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | }; 7 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-swagger": "^7.10.0", 18 | "inert": "^4.2.1", 19 | "package": "^1.0.1", 20 | "vision": "^4.1.1" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^5.3.0", 24 | "eslint-config-airbnb-base": "^13.1.0", 25 | "eslint-plugin-import": "^2.14.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | ], 21 | }, 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | }, 8 | config: { 9 | tags: ['api', 'tests'], 10 | description: '测试hello-hapi', 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | const routesShops = require('./routes/shops'); 6 | const routesOrders = require('./routes/orders'); 7 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 8 | 9 | const server = new Hapi.Server(); 10 | // 配置服务器启动host与端口 11 | server.connection({ 12 | port: config.port, 13 | host: config.host, 14 | }); 15 | 16 | const init = async () => { 17 | await server.register([ 18 | ...pluginHapiSwagger, 19 | ]); 20 | server.route([ 21 | // 创建一个简单的hello hapi接口 22 | ...routesHelloHapi, 23 | ...routesShops, 24 | ...routesOrders, 25 | ]); 26 | // 启动服务 27 | await server.start(); 28 | console.log(`Server running at: ${server.info.uri}`); 29 | }; 30 | 31 | init(); 32 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | module.exports = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | }; 7 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-swagger": "^7.10.0", 18 | "inert": "^4.2.1", 19 | "package": "^1.0.1", 20 | "vision": "^4.1.1" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^5.3.0", 24 | "eslint-config-airbnb-base": "^13.1.0", 25 | "eslint-plugin-import": "^2.14.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | ], 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | }, 8 | config: { 9 | tags: ['api', 'tests'], 10 | description: '测试hello-hapi', 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/routes/orders.js: -------------------------------------------------------------------------------- 1 | const GROUP_NAME = 'orders'; 2 | 3 | module.exports = [ 4 | { 5 | method: 'POST', 6 | path: `/${GROUP_NAME}`, 7 | handler: async (request, reply) => { 8 | reply(); 9 | }, 10 | config: { 11 | tags: ['api', GROUP_NAME], 12 | description: '创建订单', 13 | }, 14 | }, 15 | { 16 | method: 'POST', 17 | path: `/${GROUP_NAME}/{orderId}/pay`, 18 | handler: async (request, reply) => { 19 | reply(); 20 | }, 21 | config: { 22 | tags: ['api', GROUP_NAME], 23 | description: '支付某条订单', 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-2/routes/shops.js: -------------------------------------------------------------------------------- 1 | const GROUP_NAME = 'shops'; 2 | 3 | module.exports = [ 4 | { 5 | method: 'GET', 6 | path: `/${GROUP_NAME}`, 7 | handler: async (request, reply) => { 8 | reply(); 9 | }, 10 | config: { 11 | tags: ['api', GROUP_NAME], 12 | description: '获取店铺列表', 13 | }, 14 | }, 15 | { 16 | method: 'GET', 17 | path: `/${GROUP_NAME}/{shopId}/goods`, 18 | handler: async (request, reply) => { 19 | reply(); 20 | }, 21 | config: { 22 | tags: ['api', GROUP_NAME], 23 | description: '获取店铺的商品列表', 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | const routesShops = require('./routes/shops'); 6 | const routesOrders = require('./routes/orders'); 7 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 8 | 9 | const server = new Hapi.Server(); 10 | // 配置服务器启动host与端口 11 | server.connection({ 12 | port: config.port, 13 | host: config.host, 14 | }); 15 | 16 | const init = async () => { 17 | await server.register([ 18 | ...pluginHapiSwagger, 19 | ]); 20 | server.route([ 21 | // 创建一个简单的hello hapi接口 22 | ...routesHelloHapi, 23 | ...routesShops, 24 | ...routesOrders, 25 | ]); 26 | // 启动服务 27 | await server.start(); 28 | console.log(`Server running at: ${server.info.uri}`); 29 | }; 30 | 31 | init(); 32 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | }; 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-swagger": "^7.10.0", 18 | "inert": "^4.2.1", 19 | "joi": "^13.6.0", 20 | "package": "^1.0.1", 21 | "vision": "^4.1.1" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^5.3.0", 25 | "eslint-config-airbnb-base": "^13.1.0", 26 | "eslint-plugin-import": "^2.14.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | ], 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | }, 8 | config: { 9 | tags: ['api', 'tests'], 10 | description: '测试hello-hapi', 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'orders'; 4 | module.exports = [ 5 | { 6 | method: 'POST', 7 | path: `/${GROUP_NAME}`, 8 | handler: async (request, reply) => { 9 | reply(); 10 | }, 11 | config: { 12 | tags: ['api', GROUP_NAME], 13 | description: '创建订单', 14 | validate: { 15 | payload: { 16 | goodsList: Joi.array().items( 17 | Joi.object().keys({ 18 | goods_id: Joi.number().integer(), 19 | count: Joi.number().integer(), 20 | }), 21 | ), 22 | }, 23 | headers: Joi.object({ 24 | authorization: Joi.string().required(), 25 | }).unknown(), 26 | }, 27 | }, 28 | }, 29 | { 30 | method: 'POST', 31 | path: `/${GROUP_NAME}/{orderId}/pay`, 32 | handler: async (request, reply) => { 33 | reply(); 34 | }, 35 | config: { 36 | tags: ['api', GROUP_NAME], 37 | description: '支付某条订单', 38 | validate: { 39 | params: { 40 | orderId: Joi.string().required(), 41 | }, 42 | }, 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /chapter6/hapi-tutorial-3/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'shops'; 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: `/${GROUP_NAME}`, 9 | handler: async (request, reply) => { 10 | reply(); 11 | }, 12 | config: { 13 | tags: ['api', GROUP_NAME], 14 | description: '获取店铺列表', 15 | validate: { 16 | query: { 17 | limit: Joi.number().integer().min(1).default(10) 18 | .description('每页的条目数'), 19 | page: Joi.number().integer().min(1).default(1) 20 | .description('页码数'), 21 | }, 22 | }, 23 | }, 24 | }, 25 | { 26 | method: 'GET', 27 | path: `/${GROUP_NAME}/{shopId}/goods`, 28 | handler: async (request, reply) => { 29 | reply(); 30 | }, 31 | config: { 32 | tags: ['api', GROUP_NAME], 33 | description: '获取店铺的商品列表', 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | const routesShops = require('./routes/shops'); 6 | const routesOrders = require('./routes/orders'); 7 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 8 | 9 | const server = new Hapi.Server(); 10 | // 配置服务器启动host与端口 11 | server.connection({ 12 | port: config.port, 13 | host: config.host, 14 | }); 15 | 16 | const init = async () => { 17 | await server.register([ 18 | ...pluginHapiSwagger, 19 | ]); 20 | server.route([ 21 | // 创建一个简单的hello hapi接口 22 | ...routesHelloHapi, 23 | ...routesShops, 24 | ...routesOrders, 25 | ]); 26 | // 启动服务 27 | await server.start(); 28 | 29 | console.log(`Server running at: ${server.info.uri}`); 30 | }; 31 | 32 | init(); 33 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | }; 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = configs[env]; 9 | const db = {}; 10 | let sequelize = null; 11 | 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } 17 | 18 | fs 19 | .readdirSync(__dirname) 20 | .filter((file) => { 21 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 22 | return result; 23 | }) 24 | .forEach((file) => { 25 | const model = sequelize.import(path.join(__dirname, file)); 26 | db[model.name] = model; 27 | }); 28 | 29 | Object.keys(db).forEach((modelName) => { 30 | if (db[modelName].associate) { 31 | db[modelName].associate(db); 32 | } 33 | }); 34 | 35 | db.sequelize = sequelize; 36 | db.Sequelize = Sequelize; 37 | 38 | module.exports = db; 39 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-swagger": "^7.10.0", 18 | "inert": "^4.2.1", 19 | "joi": "^13.6.0", 20 | "mysql2": "^1.6.1", 21 | "package": "^1.0.1", 22 | "sequelize": "^4.38.0", 23 | "vision": "^4.1.1" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^5.3.0", 27 | "eslint-config-airbnb-base": "^13.1.0", 28 | "eslint-plugin-import": "^2.14.0", 29 | "sequelize-cli": "^4.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | ], 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | }, 8 | config: { 9 | tags: ['api', 'tests'], 10 | description: '测试hello-hapi', 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'orders'; 4 | module.exports = [ 5 | { 6 | method: 'POST', 7 | path: `/${GROUP_NAME}`, 8 | handler: async (request, reply) => { 9 | reply(); 10 | }, 11 | config: { 12 | tags: ['api', GROUP_NAME], 13 | description: '创建订单', 14 | validate: { 15 | payload: { 16 | goodsList: Joi.array().items( 17 | Joi.object().keys({ 18 | goods_id: Joi.number().integer(), 19 | count: Joi.number().integer(), 20 | }), 21 | ), 22 | }, 23 | headers: Joi.object({ 24 | authorization: Joi.string().required(), 25 | }).unknown(), 26 | }, 27 | }, 28 | }, 29 | { 30 | method: 'POST', 31 | path: `/${GROUP_NAME}/{orderId}/pay`, 32 | handler: async (request, reply) => { 33 | reply(); 34 | }, 35 | config: { 36 | tags: ['api', GROUP_NAME], 37 | description: '支付某条订单', 38 | validate: { 39 | params: { 40 | orderId: Joi.string().required(), 41 | }, 42 | }, 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-1/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'shops'; 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: `/${GROUP_NAME}`, 9 | handler: async (request, reply) => { 10 | reply(); 11 | }, 12 | config: { 13 | tags: ['api', GROUP_NAME], 14 | description: '获取店铺列表', 15 | validate: { 16 | query: { 17 | limit: Joi.number().integer().min(1).default(10) 18 | .description('每页的条目数'), 19 | page: Joi.number().integer().min(1).default(1) 20 | .description('页码数'), 21 | }, 22 | }, 23 | }, 24 | }, 25 | { 26 | method: 'GET', 27 | path: `/${GROUP_NAME}/{shopId}/goods`, 28 | handler: async (request, reply) => { 29 | reply(); 30 | }, 31 | config: { 32 | tags: ['api', GROUP_NAME], 33 | description: '获取店铺的商品列表', 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | const routesShops = require('./routes/shops'); 6 | const routesOrders = require('./routes/orders'); 7 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 8 | 9 | const server = new Hapi.Server(); 10 | // 配置服务器启动host与端口 11 | server.connection({ 12 | port: config.port, 13 | host: config.host, 14 | }); 15 | 16 | const init = async () => { 17 | await server.register([ 18 | ...pluginHapiSwagger, 19 | ]); 20 | server.route([ 21 | // 创建一个简单的hello hapi接口 22 | ...routesHelloHapi, 23 | ...routesShops, 24 | ...routesOrders, 25 | ]); 26 | // 启动服务 27 | await server.start(); 28 | 29 | console.log(`Server running at: ${server.info.uri}`); 30 | }; 31 | 32 | init(); 33 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | }; 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = configs[env]; 9 | const db = {}; 10 | let sequelize = null; 11 | 12 | if (config.use_env_variable) { 13 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 14 | } else { 15 | sequelize = new Sequelize(config.database, config.username, config.password, config); 16 | } 17 | 18 | fs 19 | .readdirSync(__dirname) 20 | .filter((file) => { 21 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 22 | return result; 23 | }) 24 | .forEach((file) => { 25 | const model = sequelize.import(path.join(__dirname, file)); 26 | db[model.name] = model; 27 | }); 28 | 29 | Object.keys(db).forEach((modelName) => { 30 | if (db[modelName].associate) { 31 | db[modelName].associate(db); 32 | } 33 | }); 34 | 35 | db.sequelize = sequelize; 36 | db.Sequelize = Sequelize; 37 | 38 | module.exports = db; 39 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations seeders --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-swagger": "^7.10.0", 18 | "inert": "^4.2.1", 19 | "joi": "^13.6.0", 20 | "mysql2": "^1.6.1", 21 | "package": "^1.0.1", 22 | "sequelize": "^4.38.0", 23 | "vision": "^4.1.1" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^5.3.0", 27 | "eslint-config-airbnb-base": "^13.1.0", 28 | "eslint-plugin-import": "^2.14.0", 29 | "sequelize-cli": "^4.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | ], 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | }, 8 | config: { 9 | tags: ['api', 'tests'], 10 | description: '测试hello-hapi', 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'orders'; 4 | module.exports = [ 5 | { 6 | method: 'POST', 7 | path: `/${GROUP_NAME}`, 8 | handler: async (request, reply) => { 9 | reply(); 10 | }, 11 | config: { 12 | tags: ['api', GROUP_NAME], 13 | description: '创建订单', 14 | validate: { 15 | payload: { 16 | goodsList: Joi.array().items( 17 | Joi.object().keys({ 18 | goods_id: Joi.number().integer(), 19 | count: Joi.number().integer(), 20 | }), 21 | ), 22 | }, 23 | headers: Joi.object({ 24 | authorization: Joi.string().required(), 25 | }).unknown(), 26 | }, 27 | }, 28 | }, 29 | { 30 | method: 'POST', 31 | path: `/${GROUP_NAME}/{orderId}/pay`, 32 | handler: async (request, reply) => { 33 | reply(); 34 | }, 35 | config: { 36 | tags: ['api', GROUP_NAME], 37 | description: '支付某条订单', 38 | validate: { 39 | params: { 40 | orderId: Joi.string().required(), 41 | }, 42 | }, 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'shops'; 4 | 5 | module.exports = [ 6 | { 7 | method: 'GET', 8 | path: `/${GROUP_NAME}`, 9 | handler: async (request, reply) => { 10 | reply(); 11 | }, 12 | config: { 13 | tags: ['api', GROUP_NAME], 14 | description: '获取店铺列表', 15 | validate: { 16 | query: { 17 | limit: Joi.number().integer().min(1).default(10) 18 | .description('每页的条目数'), 19 | page: Joi.number().integer().min(1).default(1) 20 | .description('页码数'), 21 | }, 22 | }, 23 | }, 24 | }, 25 | { 26 | method: 'GET', 27 | path: `/${GROUP_NAME}/{shopId}/goods`, 28 | handler: async (request, reply) => { 29 | reply(); 30 | }, 31 | config: { 32 | tags: ['api', GROUP_NAME], 33 | description: '获取店铺的商品列表', 34 | }, 35 | }, 36 | ]; 37 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/seeders/20180819061006-init-shops.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'shops', 9 | [ 10 | { 11 | id: 1, name: '店铺1', thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '店铺2', thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '店铺3', thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '店铺4', thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('shops', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter7/hapi-tutorial-2/seeders/20180819102052-init-goods.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'goods', 9 | [ 10 | { 11 | id: 1, name: '商品1-1', shop_id: 1, thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '商品1-2', shop_id: 1, thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '商品1-3', shop_id: 1, thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '商品2-1', shop_id: 2, thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('goods', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/.env.example: -------------------------------------------------------------------------------- 1 | # 服务的启动名字和端口,但也可以缺省不填值,默认值的填写只是一定程度减少起始数据配置工作 2 | HOST = 127.0.0.1 3 | PORT = 3000 4 | 5 | # MySQL 数据库链接配置 6 | MYSQL_HOST = your-host 7 | MYSQL_PORT = your-port 8 | MYSQL_DB_NAME = your-db-name 9 | MYSQL_USERNAME = your-username 10 | MYSQL_PASSWORD = your-password -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base" 3 | } -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/app.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | require('env2')('./.env'); 3 | const config = require('./config'); 4 | const routesHelloHapi = require('./routes/hello-hapi'); 5 | const routesShops = require('./routes/shops'); 6 | const routesOrders = require('./routes/orders'); 7 | const pluginHapiSwagger = require('./plugins/hapi-swagger'); 8 | const pluginHapiPagination = require('./plugins/hapi-pagination'); 9 | 10 | const server = new Hapi.Server(); 11 | // 配置服务器启动host与端口 12 | server.connection({ 13 | port: config.port, 14 | host: config.host, 15 | }); 16 | 17 | const init = async () => { 18 | // 注册插件 19 | await server.register([ 20 | ...pluginHapiSwagger, 21 | pluginHapiPagination, 22 | ]); 23 | // 注册路由 24 | server.route([ 25 | // 创建一个简单的hello hapi接口 26 | ...routesHelloHapi, 27 | ...routesShops, 28 | ...routesOrders, 29 | ]); 30 | // 启动服务 31 | await server.start(); 32 | 33 | console.log(`Server running at: ${server.info.uri}`); 34 | }; 35 | 36 | init(); 37 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/config/config.js: -------------------------------------------------------------------------------- 1 | const env2 = require('env2'); 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | env2('./.env.prod'); 5 | } else { 6 | env2('./.env'); 7 | } 8 | 9 | 10 | const { env } = process; 11 | 12 | module.exports = { 13 | development: { 14 | username: env.MYSQL_USERNAME, 15 | password: env.MYSQL_PASSWORD, 16 | database: env.MYSQL_DB_NAME, 17 | host: env.MYSQL_HOST, 18 | port: env.MYSQL_PORT, 19 | dialect: 'mysql', 20 | operatorsAliases: false, 21 | }, 22 | production: { 23 | username: env.MYSQL_USERNAME, 24 | password: env.MYSQL_PASSWORD, 25 | database: env.MYSQL_DB_NAME, 26 | host: env.MYSQL_HOST, 27 | port: env.MYSQL_PORT, 28 | dialect: 'mysql', 29 | operatorsAliases: false, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/config/index.js: -------------------------------------------------------------------------------- 1 | const { env } = process; 2 | 3 | const config = { 4 | host: env.HOST, 5 | port: env.PORT, 6 | }; 7 | module.exports = config; 8 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/migrations/20180818141548-create-shops-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'shops', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false, 13 | }, 14 | thumb_url: Sequelize.STRING, 15 | created_at: Sequelize.DATE, 16 | updated_at: Sequelize.DATE, 17 | }, 18 | ), 19 | 20 | down: queryInterface => queryInterface.dropTable('shops'), 21 | }; 22 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/migrations/20180818150507-create-goods-table.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => queryInterface.createTable( 3 | 'goods', 4 | { 5 | id: { 6 | type: Sequelize.INTEGER, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | }, 10 | shop_id: { 11 | type: Sequelize.INTEGER, 12 | allowNull: false, 13 | }, 14 | name: { 15 | type: Sequelize.STRING, 16 | allowNull: false, 17 | }, 18 | thumb_url: Sequelize.STRING, 19 | created_at: Sequelize.DATE, 20 | updated_at: Sequelize.DATE, 21 | }, 22 | ), 23 | 24 | down: queryInterface => queryInterface.dropTable('goods'), 25 | }; 26 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/models/goods.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'goods', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | shop_id: { 10 | type: DataTypes.INTEGER, 11 | allowNull: false, 12 | }, 13 | name: { 14 | type: DataTypes.STRING, 15 | allowNull: false, 16 | }, 17 | thumb_url: DataTypes.STRING, 18 | }, 19 | { 20 | tableName: 'goods', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/models/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Sequelize = require('sequelize'); 4 | const configs = require('../config/config.js'); 5 | 6 | const basename = path.basename(__filename); 7 | const env = process.env.NODE_ENV || 'development'; 8 | const config = { 9 | ...configs[env], 10 | define: { 11 | underscored: true, 12 | }, 13 | }; 14 | const db = {}; 15 | let sequelize = null; 16 | 17 | if (config.use_env_variable) { 18 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 19 | } else { 20 | sequelize = new Sequelize(config.database, config.username, config.password, config); 21 | } 22 | 23 | fs 24 | .readdirSync(__dirname) 25 | .filter((file) => { 26 | const result = file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js'; 27 | return result; 28 | }) 29 | .forEach((file) => { 30 | const model = sequelize.import(path.join(__dirname, file)); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach((modelName) => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/models/shops.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => sequelize.define( 2 | 'shops', 3 | { 4 | id: { 5 | type: DataTypes.INTEGER, 6 | primaryKey: true, 7 | autoIncrement: true, 8 | }, 9 | name: { 10 | type: DataTypes.STRING, 11 | allowNull: false, 12 | }, 13 | thumb_url: DataTypes.STRING, 14 | }, 15 | { 16 | tableName: 'shops', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-tutorial", 3 | "version": "1.0.0", 4 | "description": "", 5 | "github": "https://github.com/yeshengfei/hapi-tutorial.git", 6 | "email": "ye.shengfei@qq.com", 7 | "main": "index.js", 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "lint": "node_modules/.bin/eslint app.js routes config plugins migrations seeders --fix" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "env2": "^2.2.2", 16 | "hapi": "^16.6.3", 17 | "hapi-pagination": "^1.22.0", 18 | "hapi-swagger": "^7.10.0", 19 | "inert": "^4.2.1", 20 | "joi": "^13.6.0", 21 | "mysql2": "^1.6.1", 22 | "package": "^1.0.1", 23 | "sequelize": "^4.38.0", 24 | "vision": "^4.1.1" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.3.0", 28 | "eslint-config-airbnb-base": "^13.1.0", 29 | "eslint-plugin-import": "^2.14.0", 30 | "sequelize-cli": "^4.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/plugins/hapi-pagination.js: -------------------------------------------------------------------------------- 1 | const hapiPagination = require('hapi-pagination'); 2 | 3 | const options = { 4 | query: { 5 | page: { 6 | name: 'page', 7 | default: 1, 8 | }, 9 | limit: { 10 | name: 'limit', 11 | default: 25, 12 | }, 13 | pagination: { 14 | name: 'pagination', 15 | default: true, 16 | }, 17 | invalid: 'defaults', 18 | }, 19 | meta: { 20 | name: 'meta', 21 | count: { 22 | active: true, 23 | name: 'count', 24 | }, 25 | totalCount: { 26 | active: true, 27 | name: 'totalCount', 28 | }, 29 | pageCount: { 30 | active: true, 31 | name: 'pageCount', 32 | }, 33 | self: { 34 | active: true, 35 | name: 'self', 36 | }, 37 | previous: { 38 | active: true, 39 | name: 'previous', 40 | }, 41 | next: { 42 | active: true, 43 | name: 'next', 44 | }, 45 | first: { 46 | active: true, 47 | name: 'first', 48 | }, 49 | last: { 50 | active: true, 51 | name: 'last', 52 | }, 53 | page: { 54 | active: false, 55 | // name == default.query.page.name 56 | }, 57 | limit: { 58 | active: false, 59 | // name == default.query.limit.name 60 | }, 61 | }, 62 | results: { 63 | name: 'results', 64 | }, 65 | reply: { 66 | paginate: 'paginate', 67 | }, 68 | routes: { 69 | include: [ 70 | '/shops', 71 | '/shops/{shopId}/goods', 72 | ], 73 | exclude: [], 74 | }, 75 | }; 76 | 77 | module.exports = { 78 | register: hapiPagination, 79 | options, 80 | }; 81 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/plugins/hapi-swagger.js: -------------------------------------------------------------------------------- 1 | const inert = require('inert'); 2 | const vision = require('vision'); 3 | const packageModule = require('package'); 4 | const hapiSwagger = require('hapi-swagger'); 5 | 6 | module.exports = [ 7 | inert, 8 | vision, 9 | { 10 | register: hapiSwagger, 11 | options: { 12 | info: { 13 | title: '接口文档', 14 | version: packageModule.version, 15 | }, 16 | // 定义接口以tags属性定义为分组 17 | grouping: 'tags', 18 | tags: [ 19 | { name: 'tests', description: '测试相关' }, 20 | { name: 'shops', description: '店铺、商品相关' }, 21 | { name: 'orders', description: '订单相关' }, 22 | ], 23 | }, 24 | }, 25 | ]; 26 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/routes/hello-hapi.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | method: 'GET', 4 | path: '/', 5 | handler: (request, reply) => { 6 | reply('hello hapi'); 7 | }, 8 | config: { 9 | tags: ['api', 'tests'], 10 | description: '测试hello-hapi', 11 | }, 12 | }, 13 | ]; 14 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/routes/orders.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const GROUP_NAME = 'orders'; 4 | module.exports = [ 5 | { 6 | method: 'POST', 7 | path: `/${GROUP_NAME}`, 8 | handler: async (request, reply) => { 9 | reply(); 10 | }, 11 | config: { 12 | tags: ['api', GROUP_NAME], 13 | description: '创建订单', 14 | validate: { 15 | payload: { 16 | goodsList: Joi.array().items( 17 | Joi.object().keys({ 18 | goods_id: Joi.number().integer(), 19 | count: Joi.number().integer(), 20 | }), 21 | ), 22 | }, 23 | headers: Joi.object({ 24 | authorization: Joi.string().required(), 25 | }).unknown(), 26 | }, 27 | }, 28 | }, 29 | { 30 | method: 'POST', 31 | path: `/${GROUP_NAME}/{orderId}/pay`, 32 | handler: async (request, reply) => { 33 | reply(); 34 | }, 35 | config: { 36 | tags: ['api', GROUP_NAME], 37 | description: '支付某条订单', 38 | validate: { 39 | params: { 40 | orderId: Joi.string().required(), 41 | }, 42 | }, 43 | }, 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/routes/shops.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | const { paginationDefine } = require('../utils/router-helper'); 3 | const models = require('../models'); 4 | 5 | const GROUP_NAME = 'shops'; 6 | 7 | module.exports = [ 8 | { 9 | method: 'GET', 10 | path: `/${GROUP_NAME}`, 11 | handler: async (request, reply) => { 12 | const { rows: results, count: totalCount } = await models.shops.findAndCountAll({ 13 | attributes: [ 14 | 'id', 15 | 'name', 16 | ], 17 | limit: request.query.limit, 18 | offset: (request.query.page - 1) * request.query.limit, 19 | }); 20 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 21 | reply({ results, totalCount }); 22 | }, 23 | config: { 24 | tags: ['api', GROUP_NAME], 25 | description: '获取店铺列表', 26 | validate: { 27 | query: { 28 | ...paginationDefine, 29 | }, 30 | }, 31 | }, 32 | }, 33 | { 34 | method: 'GET', 35 | path: `/${GROUP_NAME}/{shopId}/goods`, 36 | handler: async (request, reply) => { 37 | // 增加带有where的条件查询 38 | const { rows: results, count: totalCount } = await models.goods.findAndCountAll({ 39 | // 基于 shop_id 的条件查询 40 | where: { 41 | shop_id: request.params.shopId, 42 | }, 43 | attributes: [ 44 | 'id', 45 | 'name', 46 | ], 47 | limit: request.query.limit, 48 | offset: (request.query.page - 1) * request.query.limit, 49 | }); 50 | // 开启分页的插件,返回的数据结构里,需要带上result与totalCount两个字段 51 | reply({ results, totalCount }); 52 | }, 53 | config: { 54 | tags: ['api', GROUP_NAME], 55 | description: '获取店铺的商品列表', 56 | validate: { 57 | params: { 58 | shopId: Joi.string().required().description('店铺的id'), 59 | }, 60 | query: { 61 | ...paginationDefine, 62 | }, 63 | }, 64 | }, 65 | }, 66 | ]; 67 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/seeders/20180819061006-init-shops.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'shops', 9 | [ 10 | { 11 | id: 1, name: '店铺1', thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '店铺2', thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '店铺3', thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '店铺4', thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('shops', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/seeders/20180819102052-init-goods.js: -------------------------------------------------------------------------------- 1 | const timestamps = { 2 | created_at: new Date(), 3 | updated_at: new Date(), 4 | }; 5 | 6 | module.exports = { 7 | up: queryInterface => queryInterface.bulkInsert( 8 | 'goods', 9 | [ 10 | { 11 | id: 1, name: '商品1-1', shop_id: 1, thumb_url: '1.png', ...timestamps, 12 | }, 13 | { 14 | id: 2, name: '商品1-2', shop_id: 1, thumb_url: '2.png', ...timestamps, 15 | }, 16 | { 17 | id: 3, name: '商品1-3', shop_id: 1, thumb_url: '3.png', ...timestamps, 18 | }, 19 | { 20 | id: 4, name: '商品2-1', shop_id: 2, thumb_url: '4.png', ...timestamps, 21 | }, 22 | ], 23 | {}, 24 | ), 25 | 26 | down: (queryInterface, Sequelize) => { 27 | const { Op } = Sequelize; 28 | return queryInterface.bulkDelete('goods', { id: { [Op.in]: [1, 2, 3, 4] } }, {}); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /chapter8/hapi-tutorial-1/utils/router-helper.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | const paginationDefine = { 4 | limit: Joi.number().integer().min(1).default(10) 5 | .description('每页的条目数'), 6 | page: Joi.number().integer().min(1).default(1) 7 | .description('页码数'), 8 | pagination: Joi.boolean().default(true).description('是否开启分页,默认为true'), 9 | }; 10 | 11 | module.exports = { paginationDefine }; 12 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 内容介绍 2 | 3 | 掘金小册的配套章节案例教程 4 | 5 | 掘金小册资料地址 [https://juejin.im/book/5b63fdba6fb9a04fde5ae6d0](https://juejin.im/book/5b63fdba6fb9a04fde5ae6d0) 6 | 7 | 进入相关的工程,请自行执行 `npm i` 安装项目库的基础依赖,并在有 .env.example 的项目中,复制一份 .env 配置项 8 | 9 | ## FAQ 10 | 11 | ### 教程章节里的代码案例启无法启动怎么办? 12 | 13 | 1. node_modules有了,但还是服务启动失败? 14 | 15 | 检查项目工程更目录下是否有 .env 文件,没有则从 .env.example 复制一枚,改名 .env,填入适当的配置值 16 | 17 | ```bash 18 | cp .env.example .env 19 | ``` 20 | --------------------------------------------------------------------------------