├── .babelrc ├── .dockerignore ├── .gitignore ├── .npmrc ├── .yarnrc ├── Dockerfile ├── README.md ├── bin └── index.js ├── favicon.ico ├── package.json ├── process.config.js ├── src ├── config │ ├── default.js │ ├── dev.js │ ├── index.js │ ├── local.js │ └── prod.js ├── decorators │ └── service-decorator.js ├── helpers │ ├── category-helper.js │ └── common │ │ └── base-helper.js ├── hooks │ ├── on-close.js │ ├── on-request.js │ ├── on-response.js │ ├── on-route.js │ ├── on-send.js │ └── pre-handler.js ├── models │ ├── category.js │ ├── file.js │ └── meeting.js ├── plugins │ ├── accepts.js │ ├── compress.js │ ├── favicon.js │ ├── formbody.js │ ├── jwt.js │ ├── mongoose.js │ ├── nodemailer.js │ ├── redis.js │ ├── response-time.js │ ├── sequelize.js │ ├── static.js │ └── swagger.js ├── routers │ └── v1 │ │ └── categories.js ├── server │ ├── cache-static-data.js │ ├── index.js │ ├── load-hooks.js │ ├── load-middlewares.js │ ├── load-plugins.js │ ├── load-routers.js │ └── test-sth.js ├── services │ └── category-service.js ├── static │ ├── a.html │ └── b │ │ └── b.html └── utils │ ├── des-util.js │ ├── joi-util.js │ ├── jwt-util.js │ ├── lcss-sms-util.js │ ├── list-util.js │ ├── mail-util.js │ ├── redis-util.js │ ├── request-util.js │ ├── router-util.js │ ├── store-global-data-util.js │ └── string-util.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0" 5 | ], 6 | "plugins": [ 7 | "transform-runtime", 8 | "transform-decorators-legacy" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | logs 3 | node_modules 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't check auto-generated stuff into git 2 | node_modules 3 | logs 4 | 5 | # Cruft 6 | *. 7 | *.swp 8 | *.bak 9 | *.~ml 10 | *.orig 11 | .DS_Store 12 | npm-debug.* 13 | .idea 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org 2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 3 | phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ 4 | chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver 5 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npm.taobao.org" 2 | sass_binary_site "https://npm.taobao.org/mirrors/node-sass/" 3 | phantomjs_cdnurl "https://npm.taobao.org/mirrors/phantomjs/" 4 | chromedriver_cdnurl "https://npm.taobao.org/mirrors/chromedriver/" 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9.10.0 2 | 3 | MAINTAINER shuperry cn.shperry@gmail.com 4 | 5 | ENV HOME="/root" 6 | 7 | COPY . ${HOME}/apps/fastify-starterkit-api 8 | 9 | WORKDIR ${HOME}/apps/fastify-starterkit-api 10 | 11 | RUN rm -rf node_modules 12 | 13 | RUN npm i yarn pm2 pino pino-pretty -g 14 | 15 | RUN yarn 16 | 17 | EXPOSE 8888 18 | 19 | CMD npm start 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Backend as a service(BaaS) platform. 2 | 3 | ## Features 4 | 5 | * **[fastify](https://www.npmjs.com/package/fastify)** 6 | * **[es6](http://es6.ruanyifeng.com)** 7 | * **[nconf](https://www.npmjs.com/package/nconf)** 8 | * **[pino](https://www.npmjs.com/package/pino)** 9 | * **[sequelize](http://docs.sequelizejs.com)** 10 | * **[sequelize-hierarchy](https://www.npmjs.com/package/sequelize-hierarchy)** 11 | * **[pm2](http://pm2.keymetrics.io/docs/usage/quick-start)** 12 | * **[multer](https://www.npmjs.com/package/multer)** 13 | * **[async / await](http://www.ruanyifeng.com/blog/2015/05/async.html)** 14 | * **[decorator](https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841)** 15 | 16 | ## Quick start 17 | 18 | 1. Clone this repo using `git clone https://github.com/shuperry/fastify-starterkit-api.git`. 19 | 2. Run `npm install` or `yarn` to install dependencies. 20 | 3. Run `npm i pm2 pino pino-pretty -g` to install global dependencies. 21 | 4. Run `npm start` to start service in local development mode. 22 | 5. Run `pm2 logs --raw fastify-starterkit-api-local | pino-pretty -c -f -t SYS:standard` to see logs in local development mode. 23 | 24 | ## Configuration 25 | 26 | We use [nconf](https://www.npmjs.com/package/nconf) to manage configuration between different environment, the configuration of current environment file name is just the same as environment, and it is extend with `src/config/default.js`. 27 | 28 | ## Environments 29 | 30 | ### production mode 31 | 32 | 1. Start core serice script: `npm run prod_cluster`. 33 | 34 | > Execute start service script except local env need install pm2 globally: `npm install pm2 pino -g`. 35 | 36 | ### development mode 37 | 38 | * **startup services:** 39 | 40 | 1. Start core serice script: `npm run dev`. 41 | 42 | * **show log scripts:** 43 | 44 | ```bash 45 | pm2 logs --raw fastify-starterkit-api-dev | pino -L 46 | ``` 47 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register') 2 | require('babel-polyfill') 3 | 4 | require('../src/server') 5 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuperry/fastify-starterkit-api/7e9a767bfb71cadb55a6e189e7e071397db9d0ab/favicon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-starterkit-api", 3 | "version": "1.0.0", 4 | "description": "Backend as a service(BaaS) platform v1.0.", 5 | "author": "Shu Perry ", 6 | "license": "BSD-3-Clause", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/shuperry/fastify-starterkit-api.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/shuperry/fastify-starterkit-api.git/issues" 13 | }, 14 | "homepage": "https://github.com/shuperry/fastify-starterkit-api.git#readme", 15 | "scripts": { 16 | "start": "pm2 start process.config.js --only fastify-starterkit-api-local", 17 | "dev": "pm2 start process.config.js --only fastify-starterkit-api-dev", 18 | "prod": "pm2 start process.config.js --only fastify-starterkit-api", 19 | "prod_cluster": "pm2 start process.config.js --only fastify-starterkit-api-cluster" 20 | }, 21 | "keywords": [ 22 | "fastify", 23 | "sequelize", 24 | "pino", 25 | "redis", 26 | "pm2" 27 | ], 28 | "files": [ 29 | "src" 30 | ], 31 | "engines": { 32 | "node": ">=4.0.0" 33 | }, 34 | "dependencies": { 35 | "async": "^2.6.2", 36 | "base64-img": "^1.0.4", 37 | "bcryptjs": "^2.4.3", 38 | "bluebird": "^3.5.3", 39 | "boom": "^7.3.0", 40 | "change-case": "^3.1.0", 41 | "continuation-local-storage": "^3.2.1", 42 | "core-decorators": "^0.20.0", 43 | "cors": "^2.8.5", 44 | "debug": "^4.1.1", 45 | "deep-diff": "^1.0.2", 46 | "dns-prefetch-control": "^0.1.0", 47 | "download": "^7.1.0", 48 | "email-address": "^1.2.2", 49 | "fastify": "^2.0.0-rc.6", 50 | "fastify-compress": "^0.8.1", 51 | "fastify-formbody": "^3.1.0", 52 | "fastify-mongoose": "^0.2.1", 53 | "fastify-response-time": "^1.1.0", 54 | "fastify-static": "^2.3.3", 55 | "fastify-swagger": "^2.3.0", 56 | "filesize": "^3.5.11", 57 | "filewatcher": "^3.0.1", 58 | "frameguard": "^3.0.0", 59 | "fs-plus": "^3.1.1", 60 | "hide-powered-by": "^1.0.0", 61 | "hsts": "^2.1.0", 62 | "ienoopen": "^1.0.0", 63 | "ioredis": "^4.6.2", 64 | "ip": "^1.1.5", 65 | "joi": "^14.3.1", 66 | "joi-to-json-schema": "^3.5.0", 67 | "js-base64": "^2.5.1", 68 | "jsonwebtoken": "^8.4.0", 69 | "loadbalance": "^0.3.3", 70 | "lodash": "^4.17.11", 71 | "lodash-decorators": "^6.0.1", 72 | "md5": "^2.2.1", 73 | "mime-types": "^2.1.22", 74 | "moment": "^2.24.0", 75 | "multer": "^1.4.1", 76 | "mysql2": "^1.6.5", 77 | "nconf": "^0.10.0", 78 | "node-schedule": "^1.3.2", 79 | "node-xlsx": "^0.14.1", 80 | "nodemailer": "^5.1.1", 81 | "officegen": "^0.4.7", 82 | "pino": "^5.11.1", 83 | "querystring": "^0.2.0", 84 | "request": "^2.88.0", 85 | "sequelize": "^4.42.0", 86 | "sequelize-hierarchy": "^1.3.2", 87 | "string": "^3.3.3", 88 | "urlencode": "^1.1.0", 89 | "uuid": "^3.3.2", 90 | "validate.io": "^2.0.7", 91 | "x-xss-protection": "^1.0.0" 92 | }, 93 | "devDependencies": { 94 | "babel-cli": "^6.26.0", 95 | "babel-core": "^6.26.0", 96 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 97 | "babel-plugin-transform-runtime": "^6.23.0", 98 | "babel-polyfill": "^6.26.0", 99 | "babel-preset-es2015": "^6.24.1", 100 | "babel-preset-stage-0": "^6.24.1", 101 | "babel-runtime": "^6.26.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /process.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'fastify-starterkit-api-local', 5 | env: { 6 | NODE_ENV: 'local', 7 | DEBUG: 'tip' 8 | }, 9 | script: './bin/index.js', 10 | watch: [ 11 | 'src' 12 | ], 13 | ignore_watch: [ 14 | 'node_modules' 15 | ], 16 | log_file: './logs/local/fastify-starterkit-api.log' 17 | }, 18 | { 19 | name: 'fastify-starterkit-api-dev', 20 | env: { 21 | NODE_ENV: 'dev', 22 | DEBUG: 'tip' 23 | }, 24 | script: './bin/index.js', 25 | watch: [ 26 | 'src' 27 | ], 28 | ignore_watch: [ 29 | 'node_modules' 30 | ], 31 | log_file: './logs/dev/fastify-starterkit-api.log' 32 | }, 33 | { 34 | name: 'fastify-starterkit-api', 35 | env: { 36 | NODE_ENV: 'prod', 37 | DEBUG: 'tip' 38 | }, 39 | script: './bin/index.js', 40 | watch: [ 41 | 'src' 42 | ], 43 | ignore_watch: [ 44 | 'node_modules' 45 | ], 46 | log_file: './logs/prod/fastify-starterkit-api.log' 47 | }, 48 | { 49 | name: 'fastify-starterkit-api-cluster', 50 | env: { 51 | NODE_ENV: 'prod', 52 | DEBUG: 'tip' 53 | }, 54 | script: './bin/index.js', 55 | watch: [ 56 | 'src' 57 | ], 58 | ignore_watch: [ 59 | 'node_modules' 60 | ], 61 | log_file: './logs/prod/fastify-starterkit-api-cluster.log', 62 | instances: 4, 63 | exec_mode: 'cluster' 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/config/default.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import convert from 'joi-to-json-schema' 4 | 5 | export default { 6 | name: 'fastify-starterkit-api[DEV]', 7 | log: { 8 | level: 'debug' 9 | }, 10 | 11 | plugins: [ 12 | 'swagger', 13 | 'jwt', 14 | // 'response-time', 15 | 'formbody', 16 | 'redis', 17 | 'sequelize', 18 | 'nodemailer', 19 | 'compress', 20 | 'favicon', 21 | 'mongoose', 22 | 'static', 23 | // 'knexjs' 24 | ], 25 | plugin: { 26 | sequelize: { 27 | database: 'fastify-starterkit-api', 28 | username: 'root', 29 | password: 'mysecretpassword', 30 | options: { 31 | host: '127.0.0.1', 32 | port: 3306, 33 | dialect: 'mysql', 34 | timezone: '+08:00', 35 | logging: console.log, 36 | pool: { 37 | max: 50, 38 | min: 15, 39 | idle: 10000 40 | } 41 | } 42 | }, 43 | nodemailer: { 44 | sender: { 45 | // qq. 46 | // service: 'qq', 47 | // auth: { 48 | // user: '576507045@qq.com', 49 | // pass: 'xxx' 50 | // }, 51 | 52 | // original smtp. 53 | host: 'xxx', 54 | port: 25, 55 | auth: { 56 | user: 'xxx', 57 | pass: 'xxx' 58 | }, 59 | 60 | tls: { 61 | // do not fail on invalid certs. 62 | rejectUnauthorized: false 63 | } 64 | }, 65 | options: { 66 | // qq. 67 | // from: '舒培培 <576507045@qq.com>', 68 | 69 | // original smtp. 70 | from: 'xxx' 71 | }, 72 | retry: { 73 | enable: false, 74 | times: 3, 75 | interval: 50 76 | } 77 | }, 78 | compress: { 79 | global: false 80 | }, 81 | mongoose: { 82 | uri: 'mongodb://localhost:27017/myapp', 83 | autoIndex: false, // Don't build indexes. 84 | reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect. 85 | reconnectInterval: 500, // Reconnect every 500ms. 86 | poolSize: 10, // Maintain up to 10 socket connections. 87 | // If not connected, return errors immediately rather than waiting for reconnect. 88 | bufferMaxEntries: 0, 89 | connectTimeoutMS: 10000, // Give up initial connection after 10 seconds. 90 | socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity. 91 | family: 4 // Use IPv4, skip trying IPv6. 92 | }, 93 | static: { 94 | root: path.join(__dirname, '..', 'static'), 95 | prefix: '/static/' // optional, default: '/'. 96 | }, 97 | swagger: { 98 | routePrefix: '/doc', 99 | exposeRoute: true, 100 | swagger: { 101 | consumes: ['application/json'], 102 | produces: ['application/json'], 103 | info: { 104 | title: 'Swagger API', 105 | version: '1.0.0' 106 | } 107 | }, 108 | transform(schema = {}) { 109 | return Object.keys(schema).reduce((o, key) => { 110 | o[key] = ['params', 'body', 'querystring'].includes(key) && schema[key].isJoi ? convert(schema[key]) : schema[key] 111 | return o 112 | }, {}) 113 | } 114 | } 115 | }, 116 | 117 | routers: { 118 | base_prefix: '/api', 119 | versions: [ 120 | { 121 | enable: true, 122 | root_folder: 'v1', 123 | prefix: '/v1', 124 | logLevel: 'debug' 125 | } 126 | ] 127 | }, 128 | 129 | middlewares: [ 130 | 'cors', 131 | 'dns-prefetch-control', 132 | 'frameguard', 133 | 'hide-powered-by', 134 | 'hsts', 135 | 'ienoopen', 136 | 'x-xss-protection' 137 | ], 138 | 139 | /*** 140 | * 注: 每一个 middleware 的配置项. 141 | e.g: 142 | 143 | middleware: { 144 | cors: { 145 | origin: '*', // also support array, e.g: ["http://example1.com", /\.example2\.com$/]. 146 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 147 | preflightContinue: false, 148 | optionsSuccessStatus: 204 149 | } 150 | } 151 | */ 152 | middleware: {}, 153 | 154 | /** 155 | * 注: 如果 method 配置缺失, 则默认为此地址前缀的所有方法的请求都会被纳入检查范围内. 156 | e.g: 157 | '/api/v1/categories' 158 | 159 | or 160 | 161 | { 162 | path: '/api/v1/categories', 163 | method: 'get' 164 | } 165 | */ 166 | auth: { 167 | /** 168 | * headers 中必须有正确并未失效的 authorization. 169 | */ 170 | urls: [], 171 | pass_urls: [], 172 | 173 | /** 174 | * headers 中有或没有或有错误的 authorization, 请求的权限认证都会被忽略. 175 | */ 176 | ignore_urls: [ 177 | { 178 | path: '/api/v1/categories', 179 | method: 'get' 180 | } 181 | ] 182 | }, 183 | 184 | page: { 185 | limit: 10 186 | }, 187 | files: { 188 | maxUploadCount: 100 189 | }, 190 | 191 | /** 192 | * 公用开关. 193 | * 194 | * @description 如果某项服务开关设置为 false, 在公用工具处此项服务不会生效, 并且会有警告日志. 195 | */ 196 | switches: { 197 | loadPlugins: true, 198 | loadRouters: true, 199 | plugin: { 200 | redis: true, 201 | sequelize: true, 202 | nodemailer: false, 203 | mongoose: false, 204 | static: false, 205 | swagger: true 206 | }, 207 | middleware: {} 208 | }, 209 | sms: { 210 | lcss: { 211 | host: 'api.lingchuangyun.cn', 212 | url: '/send', 213 | params: { 214 | appid: '31262403f0135019', 215 | secret: '24f643a8ecd5db020bbd502d75f17776', 216 | genre: '4' // 发送短信类型id 1-验证码, 2-行业类, 3-营销类, 4-四大类 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/config/dev.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'fastify-starterkit-api[DEV]', 3 | port: 7443, 4 | log: { 5 | level: 'debug' 6 | }, 7 | upload_path: '/apps/crpower/attachments/fastify-starterkit-api-dev', 8 | 9 | middleware: { 10 | cors: { 11 | origin: '*', // also support array, e.g: ["http://example1.com", /\.example2\.com$/]. 12 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 13 | preflightContinue: false, 14 | optionsSuccessStatus: 204 15 | } 16 | }, 17 | 18 | plugin: { 19 | redis: { 20 | key_prefix: 'fastify_starterkit_api_dev_', 21 | name: 'fastify-starterkit-api-redis-dev', 22 | host: 'xxx', 23 | port: 6379, 24 | options: { 25 | connectionName: 'fastify-starterkit-api-redis-dev', 26 | family: '4', 27 | db: 1, 28 | password: 'xxx', 29 | showFriendlyErrorStack: true 30 | } 31 | }, 32 | jwt: { 33 | secretKey: 'xxx', 34 | options: { 35 | // expiresIn: '1d', // e.g: 1d, 1h, 5 (second) 36 | 37 | expiresIn: 30 // second. 38 | } 39 | }, 40 | sequelize: { 41 | database: 'fastify-starterkit-api-dev', 42 | username: 'root', 43 | password: 'xxx', 44 | options: { 45 | host: 'xxx', 46 | port: 3306, 47 | dialect: 'mysql', 48 | timezone: '+08:00', 49 | logging: console.log, 50 | pool: { 51 | max: 50, 52 | min: 15, 53 | idle: 10000 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/config/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import {Provider} from 'nconf/lib/nconf/provider' 4 | 5 | const environment = process.env.NODE_ENV || 'local' 6 | const nconf = new Provider() 7 | 8 | const appPath = process.cwd() 9 | 10 | const globalConf = { 11 | appPath, 12 | environment 13 | } 14 | 15 | nconf 16 | .add('global', { 17 | type: 'literal', 18 | store: globalConf 19 | }) 20 | .add('app_env', { 21 | type: 'literal', 22 | store: require(path.join(__dirname, `${environment}`)).default 23 | }) 24 | .add('app_default', { 25 | type: 'literal', 26 | store: require(path.join(__dirname, 'default')).default 27 | }) 28 | 29 | export default nconf 30 | -------------------------------------------------------------------------------- /src/config/local.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'fastify-starterkit-api[DEV]', 3 | port: 7001, 4 | log: { 5 | level: 'debug' 6 | }, 7 | upload_path: '/Users/shupeipei/crpower-workspace/testUpload/', 8 | 9 | plugin: { 10 | redis: { 11 | key_prefix: 'fastify_starterkit_api_local_', 12 | name: 'fastify-starterkit-api-redis-local', 13 | host: '127.0.0.1', 14 | port: 6379, 15 | options: { 16 | connectionName: 'fastify-starterkit-api-redis-local', 17 | family: '4', 18 | db: 0, 19 | showFriendlyErrorStack: true, 20 | password: 'xxx' 21 | } 22 | }, 23 | jwt: { 24 | secret_key: '2027EkgGJAti9B9iokygFVgsY', 25 | options: { 26 | expiresIn: '1d', // day. 27 | 28 | // expiresIn: 5 // second. 29 | } 30 | }, 31 | sequelize: { 32 | database: 'fastify-starterkit-api', 33 | username: 'root', 34 | password: 'Root123!@', 35 | options: { 36 | host: '127.0.0.1', 37 | port: 3306, 38 | dialect: 'mysql', 39 | timezone: '+08:00', 40 | logging: console.log, 41 | pool: { 42 | max: 50, 43 | min: 15, 44 | idle: 10000 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/config/prod.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'fastify-starterkit-api', 3 | port: 7443, 4 | log: { 5 | level: 'debug' 6 | }, 7 | upload_path: '/apps/crpower/attachments/fastify-starterkit-api', 8 | 9 | middleware: { 10 | }, 11 | plugin: { 12 | redis: { 13 | key_prefix: 'fastify_starterkit_api_prod_', 14 | name: 'fastify-starterkit-api-redis-prod', 15 | host: 'xxx', 16 | port: 6379, 17 | options: { 18 | connectionName: 'fastify-starterkit-api-redis-prod', 19 | family: '4', 20 | db: 1, 21 | password: 'xxx', 22 | showFriendlyErrorStack: true 23 | } 24 | }, 25 | jwt: { 26 | secretKey: 'xxx', 27 | options: { 28 | // expiresIn: '1d', // e.g: 1d, 1h, 5 (second) 29 | 30 | expiresIn: '1h' 31 | } 32 | }, 33 | sequelize: { 34 | database: 'fastify-starterkit-api-prod', 35 | username: 'xxx', 36 | password: 'xxx', 37 | options: { 38 | host: 'xxx', 39 | port: 3306, 40 | dialect: 'mysql', 41 | timezone: '+08:00', 42 | logging: console.log, 43 | pool: { 44 | max: 50, 45 | min: 15, 46 | idle: 10000 47 | } 48 | } 49 | } 50 | }, 51 | 52 | auth: { 53 | urls: [ 54 | ], 55 | pass_urls: [ 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/decorators/service-decorator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by shupeipei on 2017/4/13. 3 | */ 4 | 5 | export const transaction = (target, key, descriptor) => { 6 | const fn = descriptor.value 7 | 8 | if (typeof fn !== 'function') { 9 | throw new SyntaxError(`@transaction can only be used on functions, not: ${fn}`) 10 | } 11 | 12 | descriptor.value = async (...args) => { 13 | return sequelize.transaction(async t1 => { 14 | return await fn.apply(target, args) 15 | }) 16 | } 17 | 18 | return descriptor 19 | } 20 | -------------------------------------------------------------------------------- /src/helpers/category-helper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import BaseHelper from './common/base-helper' 4 | import StringUtil from "../utils/string-util" 5 | 6 | class CategoryHelper extends BaseHelper { 7 | async getMeetings(fastify, { 8 | keyText, enabled, wechat_dept_id, name, place, description, remark, meeting_checkin_unit_id, start_begin_time, end_begin_time, start_finish_time, end_finish_time, created_by, updated_by, start_created_at, end_created_at, start_updated_at, end_updated_at, offset = 0, limit = config.get( 9 | 'page:limit'), needPage = true 10 | }) { 11 | const {models} = sequelize 12 | 13 | const where = {} 14 | 15 | this.genKeyTextFilters({ 16 | where, 17 | attributes: [ 18 | 'name', 'place', 'description', 'remark' 19 | ], 20 | keyText 21 | }) 22 | 23 | this.genEqualFilters({ 24 | where, 25 | params: { 26 | enabled, created_by, updated_by 27 | } 28 | }) 29 | 30 | this.genFuzzyLikeFilers({ 31 | where, 32 | params: { 33 | name, place, description, remark 34 | } 35 | }) 36 | 37 | this.genRangeTimeFilters({ 38 | where, 39 | params: [ 40 | { 41 | start_at: start_begin_time, end_at: end_begin_time, field: 'begin_time' 42 | }, 43 | { 44 | start_at: start_finish_time, end_at: end_finish_time, field: 'finish_time' 45 | }, 46 | { 47 | start_at: start_created_at, end_at: end_created_at, field: 'created_at' 48 | }, 49 | { 50 | start_at: start_updated_at, end_at: end_updated_at, field: 'updated_at' 51 | } 52 | ] 53 | }) 54 | 55 | const wechat_dept_id_or_sql = [] 56 | if (!!wechat_dept_id) { 57 | wechat_dept_id.forEach((dep_id, idx) => { 58 | if (idx === 0) { 59 | wechat_dept_id_or_sql.push(`\`Meeting\`.\`wechat_dept_id\` LIKE '%|${dep_id}|%' `) 60 | } else { 61 | wechat_dept_id_or_sql.push(`OR \`Meeting\`.\`wechat_dept_id\` LIKE '%|${dep_id}|%'`) 62 | } 63 | }) 64 | } 65 | 66 | const pagination = {} 67 | if (StringUtil.isTrue(needPage)) { 68 | pagination.offset = parseInt(offset) 69 | pagination.limit = parseInt(limit) 70 | } 71 | 72 | const select_sql = [ 73 | 'SELECT ', 74 | ' `Meeting`.`meeting_id` ', 75 | ' FROM ', 76 | ' `CRP_MEETING` AS `Meeting` ', 77 | ' WHERE `Meeting`.`is_public` = 1 ' 78 | ] 79 | 80 | let sub_condition_sql_2 = [] 81 | if (wechat_dept_id_or_sql.length > 0) { 82 | sub_condition_sql_2 = [ 83 | ' OR (`Meeting`.`is_public` = 0 ', 84 | ` AND (${wechat_dept_id_or_sql.join('')})`, 85 | ' ) ' 86 | ] 87 | } 88 | 89 | const order_sql = [ 90 | 'ORDER BY `Meeting`.`updated_at` DESC' 91 | ] 92 | 93 | const sql = [ 94 | select_sql.join(''), 95 | sub_condition_sql_2.join(''), 96 | order_sql.join('') 97 | ].join('') 98 | 99 | const include_rows = (await sequelize.query(sql, {type: sequelize.QueryTypes.SELECT})) 100 | 101 | const include_meeting_ids = [] 102 | include_rows.forEach(row => include_meeting_ids.push(row.meeting_id)) 103 | 104 | if (include_meeting_ids.length > 0) { 105 | where['meeting_id'] = { 106 | $in: include_meeting_ids 107 | } 108 | } 109 | 110 | let rows = await models.Meeting.findAll({ 111 | where, 112 | ...pagination, 113 | order: [ 114 | [ 115 | 'updated_at', 116 | 'DESC' 117 | ] 118 | ] 119 | }) 120 | 121 | return { 122 | rows, 123 | count: rows.length 124 | } 125 | } 126 | 127 | async getCategories(fastify, {keyText, name, code, parent_id, category_id}) { 128 | const {models} = sequelize 129 | 130 | let categories 131 | if (!_.isUndefined(parent_id)) { 132 | categories = await models.Category.find({ 133 | where: { 134 | category_id: parent_id 135 | }, 136 | include: [ 137 | { 138 | model: models.Category, 139 | as: 'descendents', 140 | hierarchy: true 141 | } 142 | ], 143 | order: [ 144 | [ 145 | { 146 | model: models.Category, 147 | as: 'descendents' 148 | }, 149 | 'rank', 150 | 'ASC' 151 | ] 152 | ] 153 | }) 154 | } else if (!_.isUndefined(category_id)) { 155 | categories = await models.Category.find({ 156 | where: { 157 | category_id 158 | }, 159 | include: [ 160 | { 161 | model: models.Category, 162 | as: 'descendents', 163 | hierarchy: true 164 | } 165 | ], 166 | order: [ 167 | [ 168 | { 169 | model: models.Category, 170 | as: 'descendents' 171 | }, 172 | 'rank', 173 | 'ASC' 174 | ] 175 | ] 176 | }) 177 | } else if (!_.isUndefined(code)) { 178 | categories = await models.Category.find({ 179 | where: { 180 | code 181 | }, 182 | include: [ 183 | { 184 | model: models.Category, 185 | as: 'descendents', 186 | hierarchy: true 187 | } 188 | ], 189 | order: [ 190 | [ 191 | { 192 | model: models.Category, 193 | as: 'descendents' 194 | }, 195 | 'rank', 196 | 'ASC' 197 | ] 198 | ] 199 | }) 200 | } else if (!_.isUndefined(name) || !_.isUndefined(keyText)) { 201 | const where = {} 202 | 203 | if (!_.isUndefined(name)) where['name'] = {$like: `%${name}%`} 204 | 205 | if (!_.isUndefined(keyText)) { 206 | where['$or'] = [ 207 | { 208 | name: {$like: `%${keyText}%`} 209 | }, 210 | { 211 | code: {$like: `%${keyText}%`} 212 | } 213 | ] 214 | } 215 | 216 | categories = await models.Category.findAll({ 217 | where, 218 | include: [ 219 | { 220 | model: models.Category, 221 | as: 'descendents', 222 | hierarchy: true 223 | } 224 | ], 225 | order: [ 226 | [ 227 | 'rank', 228 | 'ASC' 229 | ], 230 | [ 231 | { 232 | model: models.Category, 233 | as: 'descendents' 234 | }, 235 | 'rank', 236 | 'ASC' 237 | ] 238 | ] 239 | }) 240 | } else { 241 | categories = await models.Category.findAll({ 242 | hierarchy: true, 243 | order: [ 244 | [ 245 | 'rank', 246 | 'ASC' 247 | ] 248 | ] 249 | }) 250 | } 251 | 252 | return categories 253 | } 254 | 255 | async getSimpleCategories(fastify, params) { 256 | const {models} = sequelize 257 | 258 | const {code, parent_id, level, category_id} = params 259 | 260 | const where = {} 261 | 262 | if (!_.isUndefined(code)) where['code'] = code 263 | if (!_.isUndefined(parent_id)) where['parent_id'] = parent_id 264 | if (!_.isUndefined(level)) where['level'] = level 265 | if (!_.isUndefined(category_id)) where['category_id'] = category_id 266 | 267 | return await models.Category.findAll({ 268 | where, 269 | order: [ 270 | [ 271 | 'updated_at', 272 | 'DESC' 273 | ] 274 | ] 275 | }) 276 | } 277 | 278 | async getCategoryById(fastify, {category_id}) { 279 | const {models} = sequelize 280 | 281 | const where = {category_id} 282 | 283 | return await models.Category.find({ 284 | where, 285 | include: [ 286 | { 287 | model: models.Category, 288 | as: 'descendents', 289 | hierarchy: true 290 | }, 291 | { 292 | model: models.Category, 293 | as: 'parent' 294 | } 295 | ], 296 | order: [ 297 | [ 298 | { 299 | model: models.Category, 300 | as: 'descendents' 301 | }, 302 | 'rank', 303 | 'ASC' 304 | ] 305 | ] 306 | }) 307 | } 308 | 309 | async createCategory(fastify, params) { 310 | const {models} = sequelize 311 | 312 | const category = await models.Category.create(params) 313 | 314 | return await this.getCategoryById(fastify, {category_id: category.category_id}) 315 | } 316 | 317 | async updateCategory(fastify, params, existingCategory) { 318 | const category = await existingCategory.update(params) 319 | 320 | return await this.getCategoryById(fastify, {category_id: category.category_id}) 321 | } 322 | } 323 | 324 | export default new CategoryHelper() 325 | -------------------------------------------------------------------------------- /src/helpers/common/base-helper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | class BaseHelper { 4 | constructor() { 5 | 6 | } 7 | 8 | genKeyTextFilters({where = {}, attributes = [], keyText}) { 9 | if (keyText) { 10 | const or = [] 11 | let condition 12 | attributes.forEach(attribute => { 13 | condition = {} 14 | condition[attribute] = {$like: `%${keyText}%`} 15 | or.push(condition) 16 | }) 17 | 18 | where['$or'] = or 19 | } 20 | } 21 | 22 | genFuzzyLikeFilers({where, params = {}}) { 23 | _.keys(params).forEach(key => { 24 | if (params[key]) where[key] = {$like: `%${params[key]}%`} 25 | }) 26 | } 27 | 28 | genEqualFilters({where, params = {}}) { 29 | _.keys(params).forEach(key => { 30 | if (params[key]) where[key] = params[key] 31 | }) 32 | } 33 | 34 | /** 35 | * @description 封装自动挂载查询日期区间条件. 36 | * 37 | * @param where 需要挂载的条件. 38 | * @param params 示例: 39 | { 40 | start_at: '1524844800000', // 需查询的开始时间. 41 | start_at_operate: '$gte', // 查询开始时间时的条件, 支持 $gt ( > )、$gte ( >= )(默认). 42 | end_at: '1524844800001', // 需查询的结束时间. 43 | end_at_operate: '$lte', // 查询结束时间时的条件, 支持 $lt ( < )、$lte ( <= )(默认). 44 | field: 'created_at' // 需查询的字段. 45 | } 46 | * 以上例子生成的 sql 是: (`xx`.`created_at` >= '2018-04-28 00:00:00.000' AND `xx`.`created_at` <= '2018-04-28 00:00:00.001') 47 | */ 48 | genRangeTimeFilters({where, params = []}) { 49 | params.forEach(v => { 50 | logger.info('into generateConditionForTimeFields with v =', v) 51 | 52 | if (!v.field) { 53 | logger.warn('Missing field for generateConditionForFilterRangeTimeFields function.') 54 | } else if (v.start_at || v.end_at) { 55 | where[v.field] = {} 56 | 57 | if (v.start_at) { 58 | where[v.field][v.start_at_operate || '$gte'] = v.start_at 59 | } 60 | 61 | if (v.end_at) { 62 | where[v.field][v.end_at_operate || '$lte'] = v.end_at 63 | } 64 | } 65 | }) 66 | } 67 | } 68 | 69 | export default BaseHelper 70 | -------------------------------------------------------------------------------- /src/hooks/on-close.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | module.exports = (fastify) => { 4 | fastify.addHook('onClose', (instance, next) => { 5 | logger.info('into onClose hook with instance =', instance, 'next =', next) 6 | 7 | if (next && _.isFunction(next)) { 8 | next() 9 | } 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/on-request.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | module.exports = (fastify) => { 4 | fastify.addHook('onRequest', (req, res, next) => { 5 | logger.info('into onRequest hook') 6 | 7 | fastify.server.req = req 8 | fastify.server.res = res 9 | 10 | if (next && _.isFunction(next)) { 11 | next() 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/on-response.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | 4 | module.exports = (fastify) => { 5 | fastify.addHook('onResponse', (res, next) => { 6 | logger.info('into onResponse hook') 7 | 8 | // TODO 此处可对请求响应内容做统一处理. 9 | 10 | const request = fastify.server.request 11 | 12 | if (request && request.raw) { 13 | const finishVisitTime = moment(moment(), 'YYYY-MM-DD HH:mm:ss SSS') 14 | logger.info( 15 | 'finish visiting url at', finishVisitTime.format('YYYY-MM-DD HH:mm:ss SSS'), 16 | 'taking time', (finishVisitTime - fastify.server.beginVisitTime), 'ms =>', 17 | `[${request.raw.method}]`, request.raw.url 18 | ) 19 | } 20 | 21 | if (next && _.isFunction(next)) { 22 | next() 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/on-route.js: -------------------------------------------------------------------------------- 1 | module.exports = (fastify) => { 2 | fastify.addHook('onRoute', (routerOptions) => { 3 | logger.info('loading router:', `[${routerOptions.method}]`, routerOptions.url) 4 | }) 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/on-send.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | module.exports = (fastify) => { 4 | fastify.addHook('onSend', (request, reply, payload, next) => { 5 | logger.info('into onSend hook') 6 | 7 | if (next && _.isFunction(next)) { 8 | next() 9 | } 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/pre-handler.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | 4 | import StringUtil from '../utils/string-util' 5 | 6 | const jwtErrorMessageMap = { 7 | MissJsonWebTokenError: '缺失 token 参数, 请登录', 8 | JsonWebTokenError: '非法 token, 请登录', 9 | TokenExpiredError: 'token 已失效, 请重新登录' 10 | } 11 | 12 | const loggerForBeginVisitUrl = ({fastify, request}) => { 13 | const beginVisitTime = moment(moment(), 'YYYY-MM-DD HH:mm:ss SSS') 14 | fastify.server.beginVisitTime = beginVisitTime 15 | 16 | if (request && request.raw) { 17 | let params 18 | if ('GET' === request.raw.method) { 19 | params = _.extend({}, request.params || {}, request.query || {}) 20 | } else if (_.includes(['POST', 'PUT', 'PATCH', 'DELETE'], request.raw.method)) { 21 | params = request.body 22 | } 23 | 24 | logger.info( 25 | 'begin visiting url at', 26 | beginVisitTime.format('YYYY-MM-DD HH:mm:ss SSS'), '=>', 27 | `[${request.raw.method}]`, request.raw.url, 28 | 'with data =>', params, 29 | '\n header.authorization =>', request.headers && request.headers.authorization ? request.headers.authorization : '') 30 | } 31 | } 32 | 33 | const checkAuthority = ({fastify, request, reply}) => { 34 | if (request && request.raw) { 35 | const authIgnoreUrls = config.get('auth:ignore_urls') || [] 36 | const matchedIgnoreUrls = [] 37 | 38 | authIgnoreUrls 39 | .filter(url => { 40 | if (_.isString(url)) { 41 | return new RegExp(`^${url}.*`).test(request.raw.url) 42 | } else if (_.isPlainObject(url) && StringUtil.isNotBlank(url.path)) { 43 | return new RegExp(`^${url.path}.*`).test(request.raw.url) 44 | } 45 | }) 46 | .forEach(url => { 47 | if (_.isString(url)) { 48 | matchedIgnoreUrls.push(url) 49 | } else if (_.isPlainObject(url) && StringUtil.isNotBlank(url.path)) { 50 | if (url.method) { 51 | if (_.isString(url.method) && _.toLower(url.method) === _.toLower(request.raw.method)) { 52 | matchedIgnoreUrls.push(url) 53 | } else if (_.isArray(url.method) && _.includes(url.method, request.raw.method)) { 54 | matchedIgnoreUrls.push(url) 55 | } 56 | } 57 | } 58 | }) 59 | 60 | // ignore_urls in config ignore check authority. 61 | if (matchedIgnoreUrls.length === 0) { 62 | // pass_urls in config pass check authority. 63 | const authPassUrls = config.get('auth:pass_urls') || [] 64 | const matchedPassUrls = [] 65 | 66 | authPassUrls 67 | .filter(url => { 68 | if (_.isString(url)) { 69 | return new RegExp(`^${url}.*`).test(request.raw.url) 70 | } else if (_.isPlainObject(url) && StringUtil.isNotBlank(url.path)) { 71 | return new RegExp(`^${url.path}.*`).test(request.raw.url) 72 | } 73 | }) 74 | .forEach(url => { 75 | if (_.isString(url)) { 76 | matchedPassUrls.push(url) 77 | } else if (_.isPlainObject(url) && StringUtil.isNotBlank(url.path)) { 78 | if (url.method) { 79 | if (_.isString(url.method) && _.toLower(url.method) === _.toLower(request.raw.method)) { 80 | matchedPassUrls.push(url) 81 | } else if (_.isArray(url.method) && _.includes(url.method, request.raw.method)) { 82 | matchedPassUrls.push(url) 83 | } 84 | } 85 | } 86 | }) 87 | 88 | if (matchedPassUrls.length > 0) { 89 | if (request.headers && StringUtil.isNotBlank(request.headers.authorization)) { 90 | try { 91 | const user = fastify.jwt.verify(request.headers.authorization, { passthrough: true }) 92 | logger.info('current logged in user =', user) 93 | 94 | fastify.server.user = user 95 | } catch (e) { 96 | reply.code(401).send(new Error(jwtErrorMessageMap[e.name])) 97 | 98 | return 99 | } 100 | } else { 101 | reply.code(401).send(new Error(jwtErrorMessageMap['MissJsonWebTokenError'])) 102 | 103 | return 104 | } 105 | } 106 | 107 | // only urls in config file need check authority. 108 | const matchedAuthUrls = [] 109 | const authUrls = config.get('auth:urls') || [] 110 | 111 | authUrls 112 | .filter(url => { 113 | if (_.isString(url)) { 114 | return new RegExp(`^${url}.*`).test(request.raw.url) 115 | } else if (_.isPlainObject(url) && StringUtil.isNotBlank(url.path)) { 116 | return new RegExp(`^${url.path}.*`).test(request.raw.url) 117 | } 118 | }) 119 | .forEach(url => { 120 | if (_.isString(url)) { 121 | matchedAuthUrls.push(url) 122 | } else if (_.isPlainObject(url) && StringUtil.isNotBlank(url.path)) { 123 | if (url.method) { 124 | if (_.isString(url.method) && _.toLower(url.method) === _.toLower(request.raw.method)) { 125 | matchedAuthUrls.push(url) 126 | } else if (_.isArray(url.method) && _.includes(url.method, request.raw.method)) { 127 | matchedAuthUrls.push(url) 128 | } 129 | } 130 | } 131 | }) 132 | 133 | if (matchedAuthUrls.length > 0) { 134 | if (request.headers && StringUtil.isNotBlank(request.headers.authorization)) { 135 | try { 136 | const user = fastify.jwt.verify(request.headers.authorization) 137 | logger.info('current logged in user =', user) 138 | 139 | fastify.server.user = user 140 | } catch (e) { 141 | reply.code(401).send(new Error(jwtErrorMessageMap[e.name])) 142 | 143 | return 144 | } 145 | } else { 146 | reply.code(401).send(new Error(jwtErrorMessageMap['MissJsonWebTokenError'])) 147 | 148 | return 149 | } 150 | } else { // if urls not in urls and passUrls, but has authorization in header, we check authority and passthrough. 151 | if (request.headers && StringUtil.isNotBlank(request.headers.authorization)) { 152 | try { 153 | const user = fastify.jwt.verify(request.headers.authorization, { passthrough: true }) 154 | logger.info('current logged in user =', user) 155 | 156 | fastify.server.user = user 157 | } catch (e) { 158 | logger.error('check authorization with err = ', e) 159 | 160 | reply.code(401).send(new Error(jwtErrorMessageMap[e.name])) 161 | 162 | return 163 | } 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | module.exports = (fastify) => { 171 | fastify.addHook('preHandler', (request, reply, next) => { 172 | /** 173 | * some useful propertities. 174 | logger.info('into preHandler hook with request.raw.method =', request.raw.method) 175 | logger.info('into preHandler hook with request.raw.url =', request.raw.url) 176 | logger.info('into preHandler hook with request.raw.originalUrl =', request.raw.originalUrl) 177 | logger.info('into preHandler hook with request.params =', request.params) 178 | logger.info('into preHandler hook with request.query =', request.query) 179 | logger.info('into preHandler hook with request.body =', request.body) 180 | logger.info('into preHandler hook with request.headers =', request.headers) 181 | */ 182 | 183 | loggerForBeginVisitUrl({fastify, request}) 184 | 185 | checkAuthority({fastify, request, reply}) 186 | 187 | fastify.server.request = request 188 | 189 | if (next && _.isFunction(next)) { 190 | next() 191 | } 192 | }) 193 | } 194 | -------------------------------------------------------------------------------- /src/models/category.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | 4 | export default (sequelize, DataTypes) => { 5 | const model = sequelize.define('Category', { 6 | category_id: { 7 | type: DataTypes.INTEGER, 8 | field: 'category_id', 9 | primaryKey: true, 10 | autoIncrement: true 11 | }, 12 | name: { 13 | type: DataTypes.STRING(100), 14 | defaultValue: '', 15 | field: 'name', 16 | comment: '名称.' 17 | }, 18 | code: { 19 | type: DataTypes.STRING(100), 20 | defaultValue: '', 21 | field: 'code', 22 | comment: '唯一且规范命名的编码, 可在业务中使用.' 23 | }, 24 | rank: { 25 | type: DataTypes.DOUBLE(11, 4), 26 | field: 'rank', 27 | comment: '当前所处层级的排序.' 28 | }, 29 | remark: { 30 | type: DataTypes.TEXT('long'), 31 | field: 'remark', 32 | comment: '备注.' 33 | }, 34 | created_at: { 35 | type: DataTypes.DATE(6), 36 | field: 'created_at', 37 | set: function(val) { 38 | if (_.isNull(val) || _.isUndefined(val) || _.trim(val) === '') { 39 | this.setDataValue('created_at', null) 40 | } else if (_.isNumber(val) || (_.isString(val) && _.isNumber(_.toNumber(val)))) { 41 | this.setDataValue('created_at', new Date(_.toNumber(val))) 42 | } else if (_.isDate(val)) { 43 | this.setDataValue('created_at', val) 44 | } 45 | }, 46 | get: function () { 47 | const dataVal = this.getDataValue('created_at') 48 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 49 | } 50 | }, 51 | updated_at: { 52 | type: DataTypes.DATE(6), 53 | field: 'updated_at', 54 | get: function () { 55 | const dataVal = this.getDataValue('updated_at') 56 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 57 | } 58 | } 59 | }, { 60 | timestamps: false, 61 | tableName: 'WISE_CATEGORY', 62 | comment: '通用类别, 树形结构, parent_id 字段自关联.', 63 | hierarchy: { 64 | levelFieldName: 'level', 65 | foreignKey: 'parent_id', 66 | foreignKeyAttributes: 'parent', 67 | throughTable: 'WISE_CATEGORY_ANCETORS', 68 | throughKey: 'category_id', 69 | throughForeignKey: 'parent_category_id' 70 | }, 71 | hooks: { 72 | beforeCreate(instance) { 73 | instance.created_at = new Date() 74 | instance.updated_at = new Date() 75 | }, 76 | beforeUpdate(instance) { 77 | instance.updated_at = new Date() 78 | }, 79 | beforeBulkCreate(instances) { 80 | instances.forEach(instance => { 81 | instance.created_at = new Date() 82 | instance.updated_at = new Date() 83 | }) 84 | }, 85 | beforeBulkUpdate(instances) { 86 | instances.forEach(instance => { 87 | instance.updated_at = new Date() 88 | }) 89 | } 90 | } 91 | }) 92 | 93 | model.associate = ({Category}) => { 94 | 95 | } 96 | 97 | return model 98 | } 99 | -------------------------------------------------------------------------------- /src/models/file.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | 3 | export default (sequelize, DataTypes) => { 4 | const model = sequelize.define('File', { 5 | file_id: { 6 | type: DataTypes.INTEGER, 7 | field: 'file_id', 8 | primaryKey: true, 9 | autoIncrement: true 10 | }, 11 | original_name: { 12 | type: DataTypes.TEXT, 13 | field: 'original_name', 14 | comment: '原文件名称.' 15 | }, 16 | file_name: { 17 | type: DataTypes.TEXT, 18 | field: 'file_name', 19 | comment: '重命名后的文件名称 (is_folder 值为 1 时此字段也表示文件夹名称).' 20 | }, 21 | fullpath: { 22 | type: DataTypes.TEXT, 23 | field: 'fullpath', 24 | comment: '文件相对路径.' 25 | }, 26 | file_size: { 27 | type: DataTypes.INTEGER, 28 | field: 'file_size', 29 | comment: '文件大小.' 30 | }, 31 | mime_type: { 32 | type: DataTypes.STRING(500), 33 | defaultValue: '', 34 | field: 'mime_type', 35 | comment: '文件类型.' 36 | }, 37 | is_folder: { 38 | type: DataTypes.INTEGER, 39 | defaultValue: 0, 40 | field: 'is_folder', 41 | comment: '是否文件夹, 1: 是, 0: 否.' 42 | }, 43 | is_deleted: { 44 | type: DataTypes.INTEGER, 45 | defaultValue: 0, 46 | field: 'is_deleted', 47 | comment: '文件是否已被删除, 1: 是, 0: 否.' 48 | }, 49 | created_at: { 50 | type: DataTypes.DATE(6), 51 | field: 'created_at', 52 | get: function () { 53 | const dataVal = this.getDataValue('created_at') 54 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 55 | } 56 | }, 57 | updated_at: { 58 | type: DataTypes.DATE(6), 59 | field: 'updated_at', 60 | get: function () { 61 | const dataVal = this.getDataValue('updated_at') 62 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 63 | } 64 | } 65 | }, { 66 | timestamps: false, 67 | tableName: 'CRP_FILE', 68 | comment: '文件.', 69 | hierarchy: { 70 | levelFieldName: 'level', 71 | foreignKey: 'parent_id', 72 | foreignKeyAttributes: 'parent', 73 | throughTable: 'CRP_FILE_ANCETORS', 74 | throughKey: 'file_id', 75 | throughForeignKey: 'parent_file_id' 76 | }, 77 | hooks: { 78 | beforeCreate(instance) { 79 | instance.created_at = new Date() 80 | instance.updated_at = new Date() 81 | }, 82 | beforeUpdate(instance) { 83 | instance.updated_at = new Date() 84 | }, 85 | beforeBulkCreate(instances) { 86 | instances.forEach(instance => { 87 | instance.created_at = new Date() 88 | instance.updated_at = new Date() 89 | }) 90 | }, 91 | beforeBulkUpdate(instances) { 92 | instances.forEach(instance => { 93 | instance.updated_at = new Date() 94 | }) 95 | } 96 | } 97 | }) 98 | 99 | model.associate = ({File}) => { 100 | 101 | } 102 | 103 | return model 104 | } 105 | -------------------------------------------------------------------------------- /src/models/meeting.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import moment from 'moment' 3 | 4 | export default (sequelize, DataTypes) => { 5 | const model = sequelize.define('Meeting', { 6 | meeting_id: { 7 | type: DataTypes.INTEGER, 8 | field: 'meeting_id', 9 | primaryKey: true, 10 | autoIncrement: true 11 | }, 12 | name: { 13 | type: DataTypes.TEXT, 14 | field: 'name', 15 | comment: '会议名称.' 16 | }, 17 | place: { 18 | type: DataTypes.TEXT, 19 | field: 'place', 20 | comment: '会议地点.' 21 | }, 22 | description: { 23 | type: DataTypes.TEXT('long'), 24 | field: 'description', 25 | comment: '会议描述.' 26 | }, 27 | begin_time: { 28 | type: DataTypes.DATE(6), 29 | field: 'begin_time', 30 | comment: '会议开始时间.', 31 | set: function(val) { 32 | if (_.isNull(val) || _.isUndefined(val) || _.trim(val) === '') { 33 | this.setDataValue('begin_time', null) 34 | } else if (_.isNumber(val) || (_.isString(val) && _.isNumber(_.toNumber(val)))) { 35 | this.setDataValue('begin_time', new Date(Number(val))) 36 | } else if (_.isDate(val)) { 37 | this.setDataValue('begin_time', val) 38 | } 39 | }, 40 | get: function () { 41 | const dataVal = this.getDataValue('begin_time') 42 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 43 | } 44 | }, 45 | finish_time: { 46 | type: DataTypes.DATE(6), 47 | field: 'finish_time', 48 | comment: '会议结束时间.', 49 | set: function(val) { 50 | if (_.isNull(val) || _.isUndefined(val) || _.trim(val) === '') { 51 | this.setDataValue('finish_time', null) 52 | } else if (_.isNumber(val) || (_.isString(val) && _.isNumber(_.toNumber(val)))) { 53 | this.setDataValue('finish_time', new Date(Number(val))) 54 | } else if (_.isDate(val)) { 55 | this.setDataValue('finish_time', val) 56 | } 57 | }, 58 | get: function () { 59 | const dataVal = this.getDataValue('finish_time') 60 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 61 | } 62 | }, 63 | avatar: { 64 | type: DataTypes.TEXT, 65 | field: 'avatar', 66 | comment: '会议封面图片, 七牛云文件 key.' 67 | }, 68 | file_pics: { 69 | type: DataTypes.TEXT, 70 | field: 'file_pics', 71 | comment: '图片式会议文件, 七牛云文件 key, 以英文逗号 (,) 分隔.' 72 | }, 73 | enabled: { 74 | type: DataTypes.INTEGER, 75 | defaultValue: 1, 76 | field: 'enabled', 77 | comment: '是否有效, 1: 有效 (默认), 0: 无效.' 78 | }, 79 | is_public: { 80 | type: DataTypes.INTEGER, 81 | defaultValue: 1, 82 | field: 'is_public', 83 | comment: '是否公开会议 (有会议成员时不是公开会议), 1: 是 (默认), 0: 否.' 84 | }, 85 | need_checkin: { 86 | type: DataTypes.INTEGER, 87 | defaultValue: 1, 88 | field: 'need_checkin', 89 | comment: '是否需要报名信息, 1: 是 (默认), 0: 否.' 90 | }, 91 | wechat_dept_id: { 92 | type: DataTypes.TEXT, 93 | field: 'wechat_dept_id', 94 | comment: '有查看会议权限的微信id (用竖线【|】分隔).' 95 | }, 96 | remark: { 97 | type: DataTypes.TEXT('long'), 98 | field: 'remark', 99 | comment: '备注.' 100 | }, 101 | created_by: { 102 | type: DataTypes.STRING(200), 103 | field: 'created_by', 104 | comment: '创建人.' 105 | }, 106 | updated_by: { 107 | type: DataTypes.STRING(200), 108 | field: 'updated_by', 109 | comment: '最后修改人.' 110 | }, 111 | created_at: { 112 | type: DataTypes.DATE(6), 113 | field: 'created_at', 114 | get: function () { 115 | const dataVal = this.getDataValue('created_at') 116 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 117 | } 118 | }, 119 | updated_at: { 120 | type: DataTypes.DATE(6), 121 | field: 'updated_at', 122 | get: function () { 123 | const dataVal = this.getDataValue('updated_at') 124 | return moment.isDate(dataVal) ? moment(dataVal).valueOf() : dataVal 125 | } 126 | } 127 | }, { 128 | timestamps: false, 129 | tableName: 'CRP_MEETING', 130 | comment: '会议.', 131 | hooks: { 132 | beforeCreate(instance) { 133 | instance.created_at = new Date() 134 | instance.updated_at = new Date() 135 | }, 136 | beforeUpdate(instance) { 137 | instance.updated_at = new Date() 138 | }, 139 | beforeBulkCreate(instances) { 140 | instances.forEach(instance => { 141 | instance.created_at = new Date() 142 | instance.updated_at = new Date() 143 | }) 144 | }, 145 | beforeBulkUpdate(instances) { 146 | instances.forEach(instance => { 147 | instance.updated_at = new Date() 148 | }) 149 | } 150 | } 151 | }) 152 | 153 | model.associate = ({Meeting}) => { 154 | } 155 | 156 | return model 157 | } 158 | -------------------------------------------------------------------------------- /src/plugins/accepts.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-accepts') 2 | -------------------------------------------------------------------------------- /src/plugins/compress.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-compress') 2 | -------------------------------------------------------------------------------- /src/plugins/favicon.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | module.exports = (fastify, opts, next) => { 5 | logger.info('loading favicon plugin with opts =', opts) 6 | 7 | fastify.get('/favicon.ico', { 8 | handler(request, reply) { 9 | const stream = fs.createReadStream(opts.path || path.join(config.get('appPath'), 'favicon.ico')) 10 | reply.type('image/x-icon').send(stream) 11 | } 12 | }) 13 | 14 | next() 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/formbody.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-formbody') 2 | -------------------------------------------------------------------------------- /src/plugins/jwt.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | 3 | import JwtUtil from '../utils/jwt-util' 4 | 5 | module.exports = fp((fastify, opts, next) => { 6 | logger.info('loading jwt plugin with opts =', opts) 7 | 8 | fastify.decorate('jwt', new JwtUtil(opts.secret_key, opts.options)) 9 | 10 | next() 11 | }) 12 | -------------------------------------------------------------------------------- /src/plugins/mongoose.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-mongoose') 2 | -------------------------------------------------------------------------------- /src/plugins/nodemailer.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | 3 | import MailUtil from '../utils/mail-util' 4 | 5 | module.exports = fp((fastify, opts, next) => { 6 | logger.info('loading nodemailer plugin with opts =', opts) 7 | 8 | fastify.decorate('nodemailer', new MailUtil(opts)) 9 | 10 | next() 11 | }) 12 | -------------------------------------------------------------------------------- /src/plugins/redis.js: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | 3 | import Redis from 'ioredis' 4 | 5 | import RedisUtil from '../utils/redis-util' 6 | 7 | module.exports = fp((fastify, opts, next) => { 8 | logger.info('loading redis plugin with opts =', opts) 9 | 10 | let client = null, 11 | redisUtil = null 12 | 13 | try { 14 | client = new Redis(opts.port, opts.host, opts.options) 15 | redisUtil = new RedisUtil(client) 16 | } catch (err) { 17 | return next(err) 18 | } 19 | 20 | fastify.decorate('redis', redisUtil) 21 | 22 | next() 23 | }) 24 | 25 | const testRedis = async (fastify) => { 26 | logger.debug(await fastify.redis.store({keyPrefix: 'case_', key: '1', objVal: {a: 'a', case_id: '1'}})) 27 | logger.debug(await fastify.redis.store({keyPrefix: 'case_', key: '2', objVal: {b: 'b', case_id: '2'}})) 28 | logger.debug(await fastify.redis.get({keyPrefix: 'case_', key: '2'})) 29 | logger.debug(await fastify.redis.multiGet({keyPrefix: 'case_', key: ['2', '3']})) 30 | logger.debug(await fastify.redis.del({keyPrefix: 'case_', key: '41415'})) 31 | 32 | // await fastify.redis.flushall() 33 | } 34 | -------------------------------------------------------------------------------- /src/plugins/response-time.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-response-time') 2 | -------------------------------------------------------------------------------- /src/plugins/sequelize.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import _ from 'lodash' 4 | import cls from 'continuation-local-storage' 5 | import fp from 'fastify-plugin' 6 | import fsPlus from 'fs-plus' 7 | import hierachy from 'sequelize-hierarchy' 8 | 9 | const namespace = cls.createNamespace('g_api_cls') 10 | 11 | const Sequelize = hierachy(require('sequelize')) 12 | Sequelize.useCLS(namespace) 13 | 14 | const Op = Sequelize.Op 15 | const operatorsAliases = { 16 | $eq: Op.eq, 17 | $ne: Op.ne, 18 | $gte: Op.gte, 19 | $gt: Op.gt, 20 | $lte: Op.lte, 21 | $lt: Op.lt, 22 | $not: Op.not, 23 | $in: Op.in, 24 | $notIn: Op.notIn, 25 | $is: Op.is, 26 | $like: Op.like, 27 | $notLike: Op.notLike, 28 | $iLike: Op.iLike, 29 | $notILike: Op.notILike, 30 | $regexp: Op.regexp, 31 | $notRegexp: Op.notRegexp, 32 | $iRegexp: Op.iRegexp, 33 | $notIRegexp: Op.notIRegexp, 34 | $between: Op.between, 35 | $notBetween: Op.notBetween, 36 | $overlap: Op.overlap, 37 | $contains: Op.contains, 38 | $contained: Op.contained, 39 | $adjacent: Op.adjacent, 40 | $strictLeft: Op.strictLeft, 41 | $strictRight: Op.strictRight, 42 | $noExtendRight: Op.noExtendRight, 43 | $noExtendLeft: Op.noExtendLeft, 44 | $and: Op.and, 45 | $or: Op.or, 46 | $any: Op.any, 47 | $all: Op.all, 48 | $values: Op.values, 49 | $col: Op.col 50 | } 51 | 52 | module.exports = fp(async (fastify, opts, next) => { 53 | logger.info('loading sequelize plugin with opts =', opts) 54 | 55 | const sequelize = new Sequelize(opts.database, opts.username, opts.password, _.extend(opts.options, {operatorsAliases})) 56 | const modelPath = path.join(config.get('appPath'), 'src', 'models') 57 | 58 | fsPlus.listTreeSync(modelPath) 59 | .reduce((prev, current) => prev.concat(current), []) 60 | .filter(filePath => fsPlus.isFileSync(filePath) && path.extname(filePath) === '.js') 61 | .forEach(filePath => { 62 | logger.info(`importing model: ${filePath.substring(modelPath.length, filePath.length - 3)}`) 63 | sequelize.import(filePath) 64 | }) 65 | 66 | _.values(sequelize.models) 67 | .filter(model => _.isFunction(model.associate)) 68 | .forEach(model => { 69 | logger.info(`executing associate function in model: ${model.name}`) 70 | model.associate(sequelize.models) 71 | }) 72 | 73 | // rebuild hierarchy data for Object and Category model. 74 | // await sequelize.models.Category.rebuildHierarchy() 75 | 76 | // 初始化不存在的数据库表. 77 | await sequelize.sync() 78 | 79 | fastify.decorate('sequelize', sequelize) 80 | 81 | global.sequelize = sequelize 82 | 83 | next() 84 | }) 85 | -------------------------------------------------------------------------------- /src/plugins/static.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-static') 2 | -------------------------------------------------------------------------------- /src/plugins/swagger.js: -------------------------------------------------------------------------------- 1 | module.exports = require('fastify-swagger') 2 | -------------------------------------------------------------------------------- /src/routers/v1/categories.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | 4 | import _ from 'lodash' 5 | import multer from 'multer' 6 | import moment from 'moment' 7 | 8 | import categoryService from '../../services/category-service' 9 | 10 | import Joi from '../../utils/joi-util' 11 | import requestUtil from '../../utils/request-util' 12 | import RouterUtil from '../../utils/router-util' 13 | 14 | const upload = multer({dest: config.get('upload_path')}) 15 | 16 | const moduleCNName = '通用类别' 17 | 18 | const errMessages = { 19 | notExists: `${moduleCNName}不存在`, 20 | parentNotExists: `上级${moduleCNName}不存在` 21 | } 22 | 23 | export default (fastify, opts, next) => { 24 | fastify.post('/test-check-name-request', { 25 | schema: { 26 | description: '测试请求' 27 | }, 28 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: true }), 29 | handler: async (request, reply) => { 30 | const params = _.merge({}, request.query || {}, {timestamp: moment().valueOf()}) 31 | 32 | const res = await requestUtil.sendRequest({ 33 | port: 8001, 34 | path: '/api/v1/check-real/name', 35 | method: 'post', 36 | params: _.merge({}, params, {sign: requestUtil.getSignature(params, 'xtuwmza5jlsb1ky4dg760rhvqp8nce2o')}) 37 | }) 38 | 39 | return reply.send(res.body) 40 | } 41 | }) 42 | 43 | fastify.post('/test-check-person-request', { 44 | schema: { 45 | description: '测试请求' 46 | }, 47 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: true }), 48 | handler: async (request, reply) => { 49 | const params = _.merge({}, request.body || {}, {timestamp: moment().valueOf()}) 50 | 51 | const res = await requestUtil.sendRequest({ 52 | port: 8001, 53 | path: '/api/v1/check-real/person', 54 | method: 'post', 55 | params: _.merge({}, params, {sign: requestUtil.getSignature(params, 'xtuwmza5jlsb1ky4dg760rhvqp8nce2o')}) 56 | }) 57 | 58 | return reply.send(res.body) 59 | } 60 | }) 61 | 62 | fastify.post('/files', { 63 | schema: { 64 | description: '测试上传文件' 65 | }, 66 | preHandler: [ // preHandler 函数只支持同步, 否则会出现提前进入 handler 函数的问题. 67 | (request, reply, next) => { // 上传文件. 68 | upload.fields([ 69 | { name: 'files' }, 70 | { name: 'files2' } 71 | ])(fastify.server.req, fastify.server.res, err => { 72 | if (err) { 73 | reply.code(400).send(err) 74 | } 75 | 76 | next() 77 | }) 78 | }, 79 | (request, reply, next) => { // 处理及验证 multipart/form-data 表单参数. 80 | RouterUtil.dealSpecialMultipartFormdataRouterParam(fastify) 81 | 82 | const schema = Joi.object({ 83 | title: Joi.string().required(), 84 | body: Joi.object({ 85 | item1: Joi.string().required() 86 | }).required(), 87 | files: Joi.object({ 88 | files: Joi.array().min(1).max(config.get('files:maxUploadCount')).optional(), 89 | files2: Joi.array().min(1).max(config.get('files:maxUploadCount')).optional() 90 | }) 91 | }) 92 | 93 | Joi.validate(fastify.server.req.body, schema, { allowUnknown: false }, (err) => { 94 | if (err) { 95 | reply.code(400).send(err) 96 | } 97 | 98 | next() 99 | }) 100 | } 101 | ], 102 | handler: async (request, reply) => { 103 | console.log('into upload file done with fastify.server.req.files =', JSON.stringify(fastify.server.req.files)) 104 | console.log('into upload file done with fastify.server.req.body =', fastify.server.req.body) 105 | 106 | return reply.send({ 107 | flag: 'success' 108 | }) 109 | } 110 | }) 111 | 112 | fastify.get('/download-files', { 113 | schema: { 114 | description: '测试下载文件', 115 | querystring: Joi.object({ 116 | filepath: Joi.string().required() 117 | }) 118 | }, 119 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: false }), 120 | handler: async (request, reply) => { 121 | reply 122 | .type('application/octet-stream') 123 | .headers({ 124 | 'content-disposition': `attachment; filename="${encodeURI('测试中文package.json')}"` 125 | }) 126 | .compress(fs.createReadStream(path.join(config.get('appPath'), 'package.json'))) 127 | } 128 | }) 129 | 130 | fastify.get('/meetings', { 131 | schema: { 132 | description: '查询会议列表', 133 | querystring: Joi.object({ 134 | keyText: Joi.string().optional(), 135 | meeting_member_sid: Joi.string().optional(), 136 | wechat_dept_id: Joi.string().splited_integer('|').optional(), 137 | name: Joi.string().optional(), 138 | place: Joi.string().optional(), 139 | description: Joi.string().optional(), 140 | remark: Joi.string().optional(), 141 | 142 | start_begin_time: Joi.date().timestamp().optional(), 143 | end_begin_time: Joi.date().timestamp().optional(), 144 | 145 | start_finish_time: Joi.date().timestamp().optional(), 146 | end_finish_time: Joi.date().timestamp().optional(), 147 | 148 | created_by: Joi.string().optional(), 149 | updted_by: Joi.string().optional(), 150 | 151 | start_created_at: Joi.date().timestamp().optional(), 152 | end_created_at: Joi.date().timestamp().optional(), 153 | 154 | start_updated_at: Joi.date().timestamp().optional(), 155 | end_updated_at: Joi.date().timestamp().optional(), 156 | 157 | enabled: Joi.number().integer().in([0, 1]).optional(), 158 | 159 | offset: Joi.number().integer().greater(0).optional(), 160 | limit: Joi.number().integer().greater(0).optional(), 161 | 162 | needPage: Joi.string().in(['true', 'false']).optional() 163 | }) 164 | }, 165 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: true }), 166 | handler: async (request, reply) => { 167 | const params = _.extend({}, request.params || {}, request.query || {}) 168 | 169 | const result = await categoryService.getMeetings(fastify, params) 170 | 171 | if (_.isPlainObject(result) && result.flag === false) { 172 | reply.code(400).send(new Error(result.error_msg || errMessages[result.error_code])) 173 | } else { 174 | reply.send({ 175 | result 176 | }) 177 | } 178 | } 179 | }) 180 | 181 | fastify.get('/categories', { 182 | schema: { 183 | description: '查询通用类别列表', 184 | querystring: Joi.object({ 185 | keyText: Joi.string().optional(), 186 | name: Joi.string().optional(), 187 | code: Joi.string().optional(), 188 | parent_id: Joi.number().integer().optional(), 189 | category_id: Joi.number().integer().optional() 190 | }) 191 | }, 192 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: false }), 193 | preHandler(request, reply, next) { 194 | logger.info('into get categories route preHandler hook') 195 | 196 | next() 197 | }, 198 | handler: async (request, reply) => { 199 | const params = _.extend({}, request.params || {}, request.query || {}) 200 | 201 | const result = await categoryService.getCategories(fastify, params) 202 | 203 | if (_.isPlainObject(result) && result.flag === false) { 204 | reply.code(400).send(new Error(result.error_msg || errMessages[result.error_code])) 205 | } else { 206 | reply.send({ 207 | result 208 | }) 209 | } 210 | } 211 | }) 212 | 213 | fastify.post('/categories', { 214 | schema: { 215 | description: '新增通用类别', 216 | body: Joi.object({ 217 | name: Joi.string().required(), 218 | code: Joi.string().allow('').optional(), 219 | rank: Joi.number().optional(), 220 | parent_id: Joi.number().integer().optional() 221 | }) 222 | }, 223 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: false }), 224 | handler: async (request, reply) => { 225 | const params = _.extend({}, request.body || {}) 226 | 227 | const result = await categoryService.createCategory(fastify, params) 228 | 229 | if (_.isPlainObject(result) && result.flag === false) { 230 | reply.code(400).send(new Error(result.error_msg || errMessages[result.error_code])) 231 | } else { 232 | reply.send({ 233 | result 234 | }) 235 | } 236 | } 237 | }) 238 | 239 | fastify.patch('/categories/:category_id', { 240 | schema: { 241 | description: '修改单条通用类别', 242 | params: Joi.object({ 243 | category_id: Joi.number().integer().required() 244 | }), 245 | body: Joi.object({ 246 | name: Joi.string().optional(), 247 | code: Joi.string().allow('').optional(), 248 | rank: Joi.number().optional(), 249 | parent_id: Joi.number().integer().optional() 250 | }) 251 | }, 252 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: false }), 253 | handler: async (request, reply) => { 254 | const params = _.extend({}, request.params || {}, request.body || {}) 255 | 256 | const result = await categoryService.updateCategory(fastify, params) 257 | 258 | if (_.isPlainObject(result) && result.flag === false) { 259 | reply.code(400).send(new Error(result.error_msg || errMessages[result.error_code])) 260 | } else { 261 | reply.send({ 262 | result 263 | }) 264 | } 265 | } 266 | }) 267 | 268 | fastify.get('/categories/:category_id', { 269 | schema: { 270 | description: '查询单条通用类别', 271 | params: Joi.object({ 272 | category_id: Joi.number().integer().required() 273 | }) 274 | }, 275 | schemaCompiler: schema => data => Joi.validate(data, schema, { allowUnknown: false }), 276 | handler: async (request, reply) => { 277 | const params = _.extend({}, request.params || {}) 278 | 279 | const result = await categoryService.getCategoryById(fastify, params) 280 | 281 | if (_.isPlainObject(result) && result.flag === false) { 282 | reply.code(400).send(new Error(result.error_msg || errMessages[result.error_code])) 283 | } else { 284 | reply.send({ 285 | result 286 | }) 287 | } 288 | } 289 | }) 290 | 291 | next() 292 | } 293 | -------------------------------------------------------------------------------- /src/server/cache-static-data.js: -------------------------------------------------------------------------------- 1 | import StoreGlobalDataUtil from '../utils/store-global-data-util' 2 | 3 | export default async (fastify) => { 4 | await StoreGlobalDataUtil.storeGloabalCategories(fastify) 5 | } 6 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | import _ from 'lodash' 4 | import ip from 'ip' 5 | import base64Img from 'base64-img' 6 | import xlsx from 'node-xlsx' 7 | import S from 'string' 8 | 9 | import loadMiddlewares from './load-middlewares' 10 | import loadHooks from './load-hooks' 11 | import loadPlugins from './load-plugins' 12 | import loadRouters from './load-routers' 13 | import testSth from './test-sth' 14 | 15 | import config from '../config' 16 | 17 | global.config = config 18 | 19 | const logger = require('pino')(config.get('log')) 20 | global.logger = logger 21 | 22 | const fastify = require('fastify')({ 23 | logger, 24 | // https: { 25 | // allowHTTP1: true 26 | // key: fs.readFileSync(path.join(__dirname, '../test/https/fastify.key')), 27 | // cert: fs.readFileSync(path.join(__dirname, '../test/https/fastify.cert')) 28 | // } 29 | }) 30 | 31 | fastify.addContentTypeParser('multipart/form-data', async (request, next) => { 32 | // 添加此步骤会导致表单请求进入两次 preHandler hook 的问题. 33 | // next() 34 | }) 35 | 36 | fastify.setErrorHandler((err, request, reply) => { 37 | logger.error(err.stack) 38 | reply.status(500).send({ 39 | error: err.stack 40 | }) 41 | }) 42 | 43 | if (config.get('switches:loadPlugins') !== false) { // 配置了开关为 false 才不加载. 44 | loadPlugins(fastify) 45 | } 46 | 47 | /** 48 | * 配置了开关为 false 才不加载. 49 | * hooks 与 middlewares 本身为 router 服务, 如果不加载 router, 则 hook 没有加载的必要. 50 | */ 51 | if (config.get('switches:loadRouters') !== false) { 52 | loadHooks(fastify) 53 | loadMiddlewares(fastify) 54 | loadRouters(fastify) 55 | } 56 | 57 | global.fastify = fastify 58 | 59 | const port = config.get('port') 60 | 61 | fastify.listen(port, '0.0.0.0', async (err) => { 62 | if (err) { 63 | logger.error(err) 64 | return 65 | } 66 | 67 | const xlsdata = xlsx.parse(`/Users/shupeipei/Downloads/c6c4babfceff5594.xlsx`) 68 | // logger.info('excel data =', xlsdata) 69 | 70 | let buildData = [], buildRowData = [], rowData = [], rowStr = '' 71 | _.each(xlsdata[0]['data'], (v, i) => { 72 | if (i > 0) { 73 | logger.info('v =', _.toString(v).split(',')) 74 | 75 | rowData = _.toString(v).split(',') 76 | 77 | buildRowData = [] 78 | buildRowData.push(rowData[1]) // 姓名 79 | buildRowData.push(rowData[2]) // 电话号码 80 | buildRowData.push(rowData[3]) // 省 81 | buildRowData.push(rowData[4]) // 市 82 | buildRowData.push(rowData[5]) // 区 83 | 84 | // 详细地址 85 | rowStr = S(rowData[6]) 86 | .replaceAll(' ', '') 87 | .replaceAll('深圳市', '') 88 | .replaceAll('深圳市南山区', '') 89 | .replaceAll('深圳南山区', '') 90 | .replaceAll('南山区', '') 91 | .replaceAll('深圳市福田区', '') 92 | .replaceAll('福田区', '') 93 | .s 94 | if (rowStr.indexOf('/') > -1) { 95 | buildRowData.push( 96 | S(rowStr).substr(0, rowStr.indexOf('/')).s 97 | ) 98 | } else { 99 | buildRowData.push(S(rowStr).s) 100 | } 101 | 102 | buildRowData.push(`购买平台:${rowData[9]}, 商品:${rowData[8]}`) 103 | 104 | buildData.push(buildRowData) 105 | } 106 | }) 107 | 108 | fs.writeFileSync('/Users/shupeipei/Downloads/test1.xlsx', xlsx.build([{name: "mySheetName", data: buildData}]), {'flag': 'w'}) 109 | 110 | // const newImgBse64Str = await base64Img.base64Sync('/Users/shupeipei/Desktop/me.jpeg') 111 | // logger.info('imgBse64Str new icon = ', newImgBse64Str) 112 | 113 | fastify.swagger() 114 | 115 | logger.info(`You can also visit server at http://${ip.address()}:${port}`) 116 | 117 | // const query = {timestamp: 1550107261022, access_token: 'ajuhgtgyagtgrtrhytjtuyju', tad: 'aaa', identity: '42032519890304111X'} 118 | // 119 | // logger.info('topairs =', _.toPairs(query)) 120 | // logger.info('sorted topairs =', _.toPairs(query).sort()) 121 | // logger.info('frompairs =', _.fromPairs(_.toPairs(query).sort())) 122 | // logger.info('values of frompairs =', _.values(_.fromPairs(_.toPairs(query).sort()))) 123 | // logger.info('str of frompairs values =', _.values(_.fromPairs(_.toPairs(query).sort())).join(':')) 124 | 125 | // await testSth(fastify) 126 | }) 127 | -------------------------------------------------------------------------------- /src/server/load-hooks.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | // 为免文件过于庞大, 将 hook 拆分单独文件, 并按原顺序执行. 4 | const hooksFileMap = { 5 | 'onRequest': 'on-request', 6 | 'preHandler': 'pre-handler', 7 | 'onSend': 'on-send', 8 | 'onResponse': 'on-response', 9 | 'onRoute': 'on-route', 10 | 'onClose': 'on-close' 11 | } 12 | 13 | export default (fastify) => { 14 | _.keys(hooksFileMap).forEach(hookName => { 15 | require(`../hooks/${hooksFileMap[hookName]}`)(fastify) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/server/load-middlewares.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | const middlewares = config.get('middlewares') 3 | 4 | middlewares.forEach(middleware => { 5 | logger.info('loading middleware:', middleware) 6 | 7 | if (config.get(`switches:middleware:${middleware}`) === false) { // 未配置开关默认开启服务. 8 | logger.warn(middleware, `服务未开启, 如要使用此服务, 请开启 switches:middleware:${middleware} 开关.`) 9 | } else { 10 | fastify.use(require(middleware)(config.get(`middleware:${middleware}`))) 11 | } 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/server/load-plugins.js: -------------------------------------------------------------------------------- 1 | export default (fastify) => { 2 | const plugins = config.get('plugins') 3 | let pluginConf = {} 4 | 5 | plugins.forEach(pluginName => { 6 | pluginConf = config.get(`plugin:${pluginName}`) || {} 7 | 8 | logger.info('loading plugin:', pluginName, 'pluginConf =', pluginConf) 9 | 10 | if (config.get(`switches:plugin:${pluginName}`) === false) { // 未配置开关默认开启服务. 11 | logger.warn(pluginName, `服务未开启, 如要使用此服务, 请开启 switches:plugin:${pluginName} 开关, 否则在调用服务时会报错.`) 12 | } else { 13 | fastify.register(require(`../plugins/${pluginName}`), pluginConf) 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/server/load-routers.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import fsPlus from 'fs-plus' 4 | 5 | export default (fastify) => { 6 | const versionedRouters = config.get('routers:versions') 7 | let router_path 8 | 9 | versionedRouters.forEach(versioned_router => { 10 | router_path = path.join(__dirname, '..', 'routers', versioned_router.root_folder) 11 | 12 | if (versioned_router.enable) { 13 | fsPlus.listTreeSync(router_path) 14 | .filter(filePath => fsPlus.isFileSync(filePath) && path.extname(filePath) === '.js') 15 | .forEach(filePath => { 16 | logger.info(`loading router: ${filePath.substring(router_path.length, filePath.length - 3)}`) 17 | 18 | fastify.register(require(filePath).default, { 19 | prefix: config.get('routers:base_prefix') + versioned_router.prefix, 20 | logLevel: versioned_router.logLevel 21 | }) 22 | }) 23 | } 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/server/test-sth.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import moment from 'moment' 3 | import requestUtil from '../utils/request-util' 4 | import listUtil from '../utils/list-util' 5 | import LCSSSmsUtil from '../utils/lcss-sms-util' 6 | 7 | const testSendMail = async (fastify) => { 8 | const sendMailResult = await fastify.nodemailer.sendMail({ 9 | receiver: ['576507045@qq.com'], 10 | subject: 'Test sending email by nodeJS', 11 | text: `Hello! This is a test email sent by nodeJS.` 12 | }) 13 | logger.debug('sendMailResult =', sendMailResult) 14 | } 15 | 16 | const testRedis = async (fastify) => { 17 | logger.debug(await fastify.redis.store({keyPrefix: 'case_', key: '1', objVal: {a: 'a', case_id: '1'}})) 18 | logger.debug(await fastify.redis.store({keyPrefix: 'case_', key: '2', objVal: {b: 'b', case_id: '2'}})) 19 | logger.debug(await fastify.redis.get({keyPrefix: 'case_', key: '2'})) 20 | logger.debug(await fastify.redis.multiGet({keyPrefix: 'case_', key: ['2', '3']})) 21 | logger.debug(await fastify.redis.del({keyPrefix: 'case_', key: '41415'})) 22 | 23 | // await fastify.redis.flushall() 24 | } 25 | 26 | export default async (fastify) => { 27 | const mobiles = [ 28 | '13600432903', 29 | '13584083823', 30 | '13125132177', 31 | '15602999662', 32 | '15821187263', 33 | '18616270975', 34 | '13951611461', 35 | '18601339235', 36 | '13665095080', 37 | '18601042162', 38 | '13851798323', 39 | '18210318784', 40 | '13636399782', 41 | '18151010777', 42 | '13661555250', 43 | '13910024132', 44 | '13701095418', 45 | '17714050382', 46 | '13661166933', 47 | '15972986360', 48 | '13917117573', 49 | '13761834696', 50 | '18601186298', 51 | '18521500966', 52 | '15288207802', 53 | '18681581592', 54 | '13901208615', 55 | '18602747572', 56 | '13810923988', 57 | '13818398700', 58 | '18610086215', 59 | '13816374343', 60 | '15000423335', 61 | '13552255171', 62 | '15010410639', 63 | '13801888482', 64 | '13510307579', 65 | '13521363055', 66 | '18971361190', 67 | '13641253275', 68 | '13585165010', 69 | '13601724790', 70 | '15650724501', 71 | '13911895325', 72 | '13811693072', 73 | '13918065587', 74 | '13381699069', 75 | '13641988693', 76 | '15651612396', 77 | '13621164381', 78 | '15073121338', 79 | '18610789211', 80 | '13501169893', 81 | '15062251523', 82 | '15921169466', 83 | '13437276867', 84 | '13311001597', 85 | '13538003921', 86 | '15013832071', 87 | '13621018522', 88 | '13817828717', 89 | '13816365017', 90 | '18621962217', 91 | '13917926559', 92 | '18610296349', 93 | '13761903735', 94 | '13533556888', 95 | '13693271827', 96 | '13817975469', 97 | '13691435860', 98 | '18616613820', 99 | '13528863366', 100 | '13924088110', 101 | '18918624686', 102 | '13818199659', 103 | '18301664651', 104 | '18916058285', 105 | '18670375005', 106 | '13636509697', 107 | '13810045763', 108 | '13801012228', 109 | '18871520451', 110 | '15117559822', 111 | '13813977018', 112 | '13815634712', 113 | '13512711016', 114 | '15111230990', 115 | '18511216611', 116 | '18906050156', 117 | '13917103699', 118 | '15878733570', 119 | '13601380822', 120 | '13574168089', 121 | '17315586777', 122 | '18121163885', 123 | '13918713425', 124 | '18616940043', 125 | '13720015618', 126 | '13826590267', 127 | '18918296856', 128 | '18669070513', 129 | '13816541702', 130 | '13869416230', 131 | '13916766782', 132 | '18798042059', 133 | '13851801330', 134 | '17763648718', 135 | '15910578382', 136 | '15802713328', 137 | '13770533999', 138 | '13800000000', 139 | '18916611093', 140 | '13911996808', 141 | '18675558103', 142 | '18610601039', 143 | '18907191517', 144 | '15211005197', 145 | '13584027753', 146 | '15001079254', 147 | '18610016700', 148 | '18373858566', 149 | '17136671466', 150 | '18616569538', 151 | '18911381105', 152 | '15751833353', 153 | '12345678900', 154 | '18611910677', 155 | '18638501725', 156 | '13501756621', 157 | '13810569291', 158 | '15921178003', 159 | '18933934625', 160 | '15001266824', 161 | '13678922288', 162 | '13520821687', 163 | '13554089429', 164 | '18516890640', 165 | '13811224605', 166 | '13427908910', 167 | '15974199411', 168 | '18201300966', 169 | '18901367562', 170 | '15380062888', 171 | '15692116033', 172 | '18721767980', 173 | '13816687578', 174 | '13851599659', 175 | '13774332696', 176 | '15011538240', 177 | '13811861826', 178 | '15715193855', 179 | '13816658600', 180 | '17708479320', 181 | '15925785853', 182 | '18948480888', 183 | '13450715512', 184 | '18601697789', 185 | '13824399864', 186 | '18827081103', 187 | '15226080225', 188 | '13888537117', 189 | '13031182040', 190 | '13574867015', 191 | '13027771290', 192 | '18911832128', 193 | '13683073761', 194 | '13428770005', 195 | '18123650700', 196 | '13585601203', 197 | '15093322321', 198 | '13911198542', 199 | '18625356660', 200 | '15876502263', 201 | '13510453356', 202 | '18473445336', 203 | '13902261014', 204 | '15220051011', 205 | '15288138727', 206 | '13918416691', 207 | '13918866664', 208 | '13761139822', 209 | '13691278421', 210 | '18666210101', 211 | '13636670059', 212 | '13776627791', 213 | '13700578888', 214 | '15800042265', 215 | '18611073083', 216 | '13621380345', 217 | '18672999804', 218 | '15910512262', 219 | '13916115060', 220 | '18752090645', 221 | '13570386224', 222 | '13816786966', 223 | '15989106503', 224 | '18317042352', 225 | '15810614930', 226 | '15202178266', 227 | '18516576268', 228 | '13923443557', 229 | '13603077702', 230 | '18665928834', 231 | '15018504763' 232 | ] 233 | 234 | const content = '【中信银行】特邀您领取全球白金卡,188种任你选,不查征信,最高20万额度,可提现,领取 http://c7.gg/cd8Ph 退订回T' 235 | 236 | logger.info('领创盛世短信发送记录: 号码个数【', mobiles.length, '】, 短信内容【', content, '】') 237 | 238 | await LCSSSmsUtil.sendSMS({ 239 | mobiles, 240 | content 241 | }) 242 | 243 | // await testSendMail(fastify) 244 | 245 | // await testRedis(fastify) 246 | 247 | // console.log('moment start timestamp =', moment(`2018-06-01 00:00:00`, 'YYYY-MM-DD HH:mm:ss').valueOf()) 248 | // console.log('moment end timestamp =', moment(`2018-07-01 00:00:00`, 'YYYY-MM-DD HH:mm:ss').valueOf()) 249 | 250 | // await requestUtil.sendRequest({ 251 | // port: 8911, 252 | // path: '/mail/send', 253 | // method: 'post', 254 | // multipart: true, 255 | // params: { 256 | // files: [ 257 | // // fs.createReadStream('/Users/shupeipei/DeskTop/test1.xlsx'), 258 | // // more file. 259 | // ], 260 | // to: '576507045@qq.com' 261 | // } 262 | // }) 263 | } 264 | -------------------------------------------------------------------------------- /src/services/category-service.js: -------------------------------------------------------------------------------- 1 | import {transaction} from '../decorators/service-decorator' 2 | 3 | import StoreGlobalDataUtil from '../utils/store-global-data-util' 4 | 5 | import categoryHelper from '../helpers/category-helper' 6 | 7 | class CategoryService { 8 | constructor() { 9 | this.redisKeyPrefix = 'category_' 10 | } 11 | 12 | async getMeetings(fastify, params) { 13 | return await categoryHelper.getMeetings(fastify, params) 14 | } 15 | 16 | async getCategories(fastify, params) { 17 | return await categoryHelper.getCategories(fastify, params) 18 | } 19 | 20 | @transaction 21 | async createCategory(fastify, params) { 22 | if (params.parent_id) { 23 | const existingParentCategory = await categoryHelper.getCategoryById(fastify, {category_id: params.parent_id}) 24 | 25 | if (!existingParentCategory) { 26 | return { 27 | flag: false, 28 | error_code: 'parentNotExists' 29 | } 30 | } 31 | } 32 | 33 | const category = await categoryHelper.createCategory(fastify, params) 34 | 35 | StoreGlobalDataUtil.storeGloabalCategories(fastify) 36 | 37 | fastify.redis.store({ 38 | keyPrefix: this.redisKeyPrefix, 39 | key: category.category_id, 40 | objVal: category 41 | }) 42 | 43 | return category 44 | } 45 | 46 | async getCategoryById(fastify, params) { 47 | const {category_id} = params 48 | 49 | const category = await fastify.redis.get({keyPrefix: this.redisKeyPrefix, key: category_id}) || await categoryHelper.getCategoryById(fastify, {category_id}) 50 | 51 | if (!!category) { 52 | fastify.redis.store({ 53 | keyPrefix: this.redisKeyPrefix, 54 | key: category.category_id, 55 | objVal: category 56 | }) 57 | 58 | return category 59 | } 60 | 61 | return { 62 | flag: false, 63 | error_code: 'notExists' 64 | } 65 | } 66 | 67 | @transaction 68 | async updateCategory(fastify, params) { 69 | if (params.parent_id) { 70 | const existingParentCategory = await categoryHelper.getCategoryById(fastify, {category_id: params.parent_id}) 71 | 72 | if (!existingParentCategory) { 73 | return { 74 | flag: false, 75 | error_code: 'parentNotExists' 76 | } 77 | } 78 | } 79 | 80 | const existingCategory = await categoryHelper.getCategoryById(fastify, params) 81 | 82 | if (!!existingCategory) { 83 | const category = await categoryHelper.updateCategory( 84 | fastify, 85 | params, 86 | existingCategory 87 | ) 88 | 89 | StoreGlobalDataUtil.storeGloabalCategories(fastify) 90 | 91 | fastify.redis.store({ 92 | keyPrefix: this.redisKeyPrefix, 93 | key: category.category_id, 94 | objVal: category 95 | }) 96 | 97 | return category 98 | } 99 | 100 | return { 101 | flag: false, 102 | error_code: 'notExists' 103 | } 104 | } 105 | } 106 | 107 | export default new CategoryService() 108 | -------------------------------------------------------------------------------- /src/static/a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 你好,我是静态页面. 9 | 10 | -------------------------------------------------------------------------------- /src/static/b/b.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 你好,我是第二个静态页面. 9 | 10 | -------------------------------------------------------------------------------- /src/utils/des-util.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | 3 | import _ from 'lodash' 4 | 5 | class DesUtil { 6 | constructor(key, iv) { 7 | this.ALGORITHM = 'des-cbc' 8 | this.key = key 9 | this.iv = iv 10 | } 11 | 12 | encrypt(plainText) { 13 | const cipher = crypto.createCipheriv(this.ALGORITHM, this.key, this.iv) 14 | cipher.setAutoPadding(true) 15 | const ciph = cipher.update(_.toString(plainText), 'utf-8', 'hex') 16 | return ciph + cipher.final('hex') 17 | } 18 | 19 | decrypt(encryptedText) { 20 | const cipher = crypto.createDecipheriv(this.ALGORITHM, this.key, this.iv) 21 | cipher.setAutoPadding(true) 22 | const ciph = cipher.update(_.toString(encryptedText), 'hex', 'utf-8') 23 | return ciph + cipher.final('utf-8') 24 | } 25 | } 26 | 27 | export default DesUtil 28 | -------------------------------------------------------------------------------- /src/utils/joi-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import Joi from 'joi' 3 | 4 | export default Joi.extend([ 5 | /** 6 | * @demo: Joi.string().in(['a', '1']).optional() 7 | */ 8 | (Joi) => ({ 9 | base: Joi.string(), 10 | name: 'string', 11 | language: { 12 | in: 'value must be one of {{in}}.' 13 | }, 14 | rules: [ 15 | { 16 | name: 'in', 17 | params: { 18 | in: Joi.array().items(Joi.string()).required() 19 | }, 20 | validate(params, value, state, options) { 21 | if (!_.includes(params.in, value)) { 22 | return this.createError('string.in', {in: params.in}, state, options) 23 | } 24 | 25 | return value 26 | } 27 | } 28 | ] 29 | }), 30 | 31 | /** 32 | * @demo: Joi.string().splited_number(',').optional() 33 | */ 34 | (Joi) => ({ 35 | base: Joi.string(), 36 | name: 'string', 37 | language: { 38 | splited_number: 'value must be a splited with "{{sperator}}" number string.' 39 | }, 40 | rules: [ 41 | { 42 | name: 'splited_number', 43 | params: { 44 | sperator: Joi.string().required() 45 | }, 46 | validate(params, value, state, options) { 47 | const array = value 48 | .split(params.sperator) 49 | .map(sub_val => _.toNumber(sub_val)) 50 | 51 | return Joi.validate(array, Joi.array().items(Joi.number().integer()).required(), (err, value) => { 52 | if (!!err) { 53 | return _this.createError('string.splited_number', {sperator: params.sperator}, state, options) 54 | } else { 55 | return array 56 | } 57 | }) 58 | } 59 | } 60 | ] 61 | }), 62 | 63 | /** 64 | * @demo: Joi.string().splited_integer(',').optional() 65 | */ 66 | (Joi) => ({ 67 | base: Joi.string(), 68 | name: 'string', 69 | language: { 70 | splited_integer: 'value must be a splited with "{{sperator}}" integer string.' 71 | }, 72 | rules: [ 73 | { 74 | name: 'splited_integer', 75 | params: { 76 | sperator: Joi.string().required() 77 | }, 78 | validate(params, value, state, options) { 79 | const array = value 80 | .split(params.sperator) 81 | .map(sub_val => _.toNumber(sub_val)) 82 | 83 | return Joi.validate(array, Joi.array().items(Joi.number()).required(), (err, value) => { 84 | if (!!err) { 85 | return this.createError('string.splited_integer', {sperator: params.sperator}, state, options) 86 | } else { 87 | return array 88 | } 89 | }) 90 | } 91 | } 92 | ] 93 | }), 94 | 95 | /** 96 | * @demo: Joi.number().in([0.1, 1.2]).optional() 97 | */ 98 | (Joi) => ({ 99 | base: Joi.number(), 100 | name: 'number', 101 | language: { 102 | in: 'value must be one of {{in}}.' 103 | }, 104 | rules: [ 105 | { 106 | name: 'in', 107 | params: { 108 | in: Joi.array().items(Joi.number()).required() 109 | }, 110 | validate(params, value, state, options) { 111 | params.in.map(val => _.toNumber(val)) 112 | 113 | if (!_.includes(params.in, _.toNumber(value))) { 114 | return this.createError('number.in', {in: params.in}, state, options) 115 | } 116 | 117 | return value 118 | } 119 | } 120 | ] 121 | }), 122 | 123 | /** 124 | * @demo: Joi.number().integer().in([0, 1]).optional() 125 | */ 126 | (Joi) => ({ 127 | base: Joi.number().integer(), 128 | name: 'integer', 129 | language: { 130 | in: 'value must be one of {{in}}.' 131 | }, 132 | rules: [ 133 | { 134 | name: 'in', 135 | params: { 136 | in: Joi.array().items(Joi.number().integer()).required() 137 | }, 138 | validate(params, value, state, options) { 139 | params.in.map(val => _.toNumber(val)) 140 | 141 | if (!_.includes(params.in, _.toNumber(value))) { 142 | return this.createError('number.integer.in', {in: params.in}, state, options) 143 | } 144 | 145 | return value 146 | } 147 | } 148 | ] 149 | }), 150 | ]) 151 | -------------------------------------------------------------------------------- /src/utils/jwt-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import jwt from 'jsonwebtoken' 3 | 4 | class JwtUtil { 5 | constructor(secret_key, options) { 6 | this.secret_key = secret_key 7 | this.options = options 8 | } 9 | 10 | sign(obj) { 11 | return jwt.sign(obj, this.secret_key, this.options) 12 | } 13 | 14 | verify(obj, opts) { 15 | return jwt.verify(obj, this.secret_key, _.extend({}, this.options, opts)) 16 | } 17 | } 18 | 19 | export default JwtUtil 20 | -------------------------------------------------------------------------------- /src/utils/lcss-sms-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import RequestUtil from './request-util' 3 | 4 | class LCSSSmsUtil { 5 | constructor() { 6 | 7 | } 8 | 9 | static async sendSMS({mobiles = [], content = '谢谢。'}) { 10 | RequestUtil.sendRequest({ 11 | host: config.get('sms:lcss:host'), 12 | path: config.get('sms:lcss:url'), 13 | method: 'post', 14 | params: _.merge(config.get('sms:lcss:params'), {mobile: mobiles.join(','), content}) 15 | }) 16 | } 17 | } 18 | 19 | export default LCSSSmsUtil 20 | -------------------------------------------------------------------------------- /src/utils/list-util.js: -------------------------------------------------------------------------------- 1 | class ListUtil { 2 | constructor() { 3 | 4 | } 5 | 6 | static list2Tree({data = [], rootId, idFieldName = 'id', parentIdFielName = 'parentId'}) { 7 | var r = [], o = {} 8 | data.forEach(function (a) { 9 | if (o[a[idFieldName]] && o[a[idFieldName]].children) { 10 | a.children = o[a[idFieldName]] && o[a[idFieldName]].children 11 | } 12 | o[a[idFieldName]] = a 13 | if (a[parentIdFielName] === rootId) { 14 | r.push(a) 15 | } else { 16 | o[a[parentIdFielName]] = o[a[parentIdFielName]] || {} 17 | o[a[parentIdFielName]].children = o[a[parentIdFielName]].children || [] 18 | o[a[parentIdFielName]].children.push(a) 19 | } 20 | }) 21 | 22 | return r 23 | } 24 | } 25 | 26 | export default ListUtil 27 | -------------------------------------------------------------------------------- /src/utils/mail-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import async from 'async' 3 | import Promise from 'bluebird' 4 | import emailValidator from 'email-address' 5 | import nodemailer from 'nodemailer' 6 | 7 | Promise.promisifyAll(async) 8 | 9 | class MailUtil { 10 | constructor(opts) { 11 | this.opts = opts 12 | this.transporter = nodemailer.createTransport(opts.sender) 13 | } 14 | 15 | /** 16 | * 发送邮件. 17 | * 18 | * @param receiver 收件人 (支持字符串和字符串数组格式). 19 | * @param subject 主题. 20 | * @param text 文本格式邮件内容. 21 | * @param html 网页格式邮件内容, 一般 html 与 text 不同时传, 如果同时存在默认使用 html. 22 | * @param attachments 附件, 参考 api: https://nodemailer.com/message/attachments/. 23 | * 24 | * @returnParam successful 邮件发送成功的邮件地址. 25 | * @returnParam failed 邮件发送失败的邮件地址. 26 | * @returnParam wrong 格式不正确的邮件地址. 27 | */ 28 | async sendMail({receiver, subject, text, html, attachments}) { 29 | const seriesJobs = [] 30 | const successful = [], failed = [], wrong = [] 31 | 32 | const sendMailCoreFunc = async (receiver, retryCallback) => { 33 | try { 34 | await this.transporter.sendMail({ 35 | ...this.opts.options, 36 | to: receiver, 37 | subject, 38 | text: !!html ? '' : text, 39 | html, 40 | attachments 41 | }) 42 | 43 | logger.info('邮件发送成功 receiver =', receiver) 44 | 45 | successful.push(receiver) 46 | 47 | if (_.isFunction(retryCallback)) retryCallback(null) 48 | } catch (err) { 49 | logger.error('邮件发送失败 receiver =', receiver, 'err =', err) 50 | 51 | if (_.isFunction(retryCallback)) retryCallback('failed.') 52 | } 53 | } 54 | 55 | const generageSeriesJobs = (receiver) => { 56 | if (this.opts.retry.enable) { 57 | seriesJobs.push(async (seriesCallback) => { 58 | await async.retryAsync({ 59 | times: this.opts.retry.times, 60 | interval: this.opts.retry.interval, 61 | errorFilter: (err) => { 62 | return !!err 63 | } 64 | }, async (retryCallback) => { 65 | await sendMailCoreFunc(receiver, retryCallback) 66 | }, (err) => { 67 | if (!!err) { // final failed after retry. 68 | failed.push(receiver) 69 | } 70 | 71 | if (_.isFunction(seriesCallback)) seriesCallback(null, receiver) 72 | }) 73 | }) 74 | } else { 75 | seriesJobs.push(async () => { 76 | await sendMailCoreFunc(receiver) 77 | }) 78 | } 79 | } 80 | 81 | if (_.isArray(receiver)) { 82 | let filteredReceivers = [] 83 | receiver.forEach(receiverItem => { 84 | if (!emailValidator.isValid(receiverItem)) { 85 | wrong.push(receiverItem) 86 | } else { 87 | filteredReceivers.push(receiverItem) 88 | } 89 | }) 90 | 91 | filteredReceivers = _.uniq(filteredReceivers) 92 | 93 | if (filteredReceivers.length > 0) { 94 | generageSeriesJobs(filteredReceivers) 95 | } 96 | } else if (_.isString(receiver)) { 97 | if (!emailValidator.isValid(receiver)) { 98 | wrong.push(receiver) 99 | return 100 | } else { 101 | generageSeriesJobs(receiver) 102 | } 103 | } 104 | 105 | if (seriesJobs.length > 0) await async.seriesAsync(seriesJobs) 106 | 107 | return { 108 | successful, 109 | failed, 110 | wrong 111 | } 112 | } 113 | } 114 | 115 | export default MailUtil 116 | -------------------------------------------------------------------------------- /src/utils/redis-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | class RedisUtil { 4 | constructor(redisClient) { 5 | this.redisClient = redisClient 6 | this.app_key_prefix = config.get('plugin:redis:key_prefix') 7 | } 8 | 9 | async set({keyPrefix = '', key, objVal}) { 10 | if (this.redisClient) { 11 | const storedKey = this.app_key_prefix + keyPrefix + _.toString(key) 12 | const res = await this.redisClient.set(storedKey, JSON.stringify(objVal)) 13 | 14 | logger.info('storing to redis with key =', storedKey, 'data =', JSON.stringify(objVal)) 15 | 16 | return res === 'OK' ? 'OK' : 'wrong' 17 | } 18 | } 19 | 20 | async store({keyPrefix = '', key, objVal}) { 21 | if (this.redisClient) { 22 | return await this.set({keyPrefix, key, objVal}) 23 | } 24 | } 25 | 26 | async get({keyPrefix = '', key}) { 27 | if (this.redisClient) { 28 | const storedKey = this.app_key_prefix + keyPrefix + _.toString(key) 29 | let res 30 | try { 31 | res = JSON.parse(await this.redisClient.get(storedKey)) 32 | } catch (e) { 33 | res = await this.redisClient.get(storedKey) 34 | } 35 | 36 | logger.info('getting from redis with key =', storedKey, 'res =', res) 37 | 38 | return res 39 | } 40 | 41 | return null 42 | } 43 | 44 | async multiGet({keyPrefix = '', keys = []}) { 45 | if (this.redisClient) { 46 | const multiGetArr = [], results = [] 47 | let storedKey 48 | 49 | if (_.isArray(keys)) { 50 | keys.forEach(key => { 51 | storedKey = this.app_key_prefix + keyPrefix + _.toString(key) 52 | multiGetArr.push([ 53 | 'get', 54 | this.app_key_prefix + keyPrefix + _.toString(key) 55 | ]) 56 | 57 | logger.info('before deleting from redis with key =', storedKey) 58 | 59 | }) 60 | 61 | const vals = await this.redisClient.pipeline(multiGetArr).exec() 62 | vals.forEach(valArr => { 63 | try { 64 | results.push(JSON.parse(valArr[1])) 65 | } catch (e) { 66 | results.push(valArr[1]) 67 | } 68 | }) 69 | } 70 | 71 | return results 72 | } 73 | 74 | return null 75 | } 76 | 77 | async del({keyPrefix = '', key}) { 78 | if (this.redisClient) { 79 | const storedKey = this.app_key_prefix + keyPrefix + _.toString(key) 80 | const res = await this.redisClient.del(storedKey) 81 | 82 | logger.info('deleting from redis with key =', storedKey) 83 | 84 | return res === 0 ? 'OK' : 'wrong' 85 | } 86 | } 87 | 88 | async batchDel({keyPrefix = '', keys = []}) { 89 | if (this.redisClient) { 90 | const multiDelArr = [] 91 | let storedKey 92 | 93 | if (_.isArray(keys)) { 94 | keys.forEach(key => { 95 | storedKey = this.app_key_prefix + keyPrefix + _.toString(key) 96 | multiDelArr.push([ 97 | 'del', 98 | storedKey 99 | ]) 100 | 101 | logger.info('before deleting from redis with key =', storedKey) 102 | }) 103 | 104 | await this.redisClient.pipeline(multiDelArr).exec() 105 | } 106 | } 107 | } 108 | 109 | getInstance() { 110 | return this.redisClient 111 | } 112 | 113 | async flushdb() { 114 | if (this.redisClient) { 115 | await this.redisClient.flushdb() 116 | } 117 | } 118 | 119 | async flushall() { 120 | if (this.redisClient) { 121 | await this.redisClient.flushall() 122 | } 123 | } 124 | } 125 | 126 | export default RedisUtil 127 | -------------------------------------------------------------------------------- /src/utils/request-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import md5 from 'md5' 3 | import Promise from 'bluebird' 4 | import request from 'request' 5 | import qs from 'querystring' 6 | 7 | Promise.promisifyAll(request) 8 | 9 | class RequestUtil { 10 | constructor() { 11 | 12 | } 13 | 14 | static async sendRequest({protocol = 'http:', host = '127.0.0.1', port, path, method = 'get', params = {}, multipart = false, headers = {}, queryString}) { 15 | let res 16 | 17 | if (protocol === 'http:') { 18 | port = port || 80 19 | } else if (protocol === 'https:') { 20 | port = port || 443 21 | } 22 | 23 | switch (method) { 24 | case 'post': 25 | if (multipart) { 26 | res = await request.postAsync({ 27 | url: !!queryString ? `${protocol}//${host}:${port}${path}?${qs.stringify(queryString)}` : 28 | `${protocol}//${host}:${port}${path}`, 29 | formData: params, 30 | headers 31 | }) 32 | } else { 33 | res = await request.postAsync({ 34 | url: !!queryString ? `${protocol}//${host}:${port}${path}?${qs.stringify(queryString)}` : 35 | `${protocol}//${host}:${port}${path}`, 36 | body: params, 37 | headers, 38 | json: true 39 | }) 40 | } 41 | 42 | break 43 | case 'patch': 44 | if (multipart) { 45 | res = await request.patchAsync({ 46 | url: !!queryString ? `${protocol}//${host}:${port}${path}?${qs.stringify(queryString)}` : 47 | `${protocol}//${host}:${port}${path}`, 48 | formData: params, 49 | headers 50 | }) 51 | } else { 52 | res = await request.patchAsync({ 53 | url: !!queryString ? `${protocol}//${host}:${port}${path}?${qs.stringify(queryString)}` : 54 | `${protocol}//${host}:${port}${path}`, 55 | body: params, 56 | headers, 57 | json: true 58 | }) 59 | } 60 | 61 | break 62 | case 'get': 63 | res = await request.getAsync({ 64 | url: `${protocol}//${host}:${port}${path}`, 65 | qs: queryString, 66 | headers, 67 | json: true 68 | }) 69 | break 70 | } 71 | 72 | logger.debug('visiting url => [', res.request.method, ']', res.request.uri.href, '\n data =>', params, '\n res = ', res) 73 | 74 | return res 75 | } 76 | 77 | static getSignature(params = {}, salt = '') { 78 | return md5(_.values(_.fromPairs(_.toPairs(params).sort())).join(':') + ':' + salt) 79 | } 80 | } 81 | 82 | export default RequestUtil 83 | -------------------------------------------------------------------------------- /src/utils/router-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import validate from 'validate.io' 3 | 4 | class RouterUtil { 5 | constructor() { 6 | 7 | } 8 | 9 | static dealSpecialMultipartFormdataRouterParam(fastify) { 10 | fastify.server.req.body = fastify.server.req.body || {} 11 | 12 | let val 13 | _.keys(fastify.server.req.body).forEach(key => { 14 | val = fastify.server.req.body[key] 15 | 16 | if (_.isString(val) && val !== '') { 17 | if (validate.isJSON(val)) { 18 | fastify.server.req.body[key] = JSON.parse(val) 19 | } else { 20 | if (_.isNumber(_.toNumber(val)) && !_.isNaN(_.toNumber(val))) { 21 | fastify.server.req.body[key] = _.toNumber(val) 22 | } else if (_.toLower(_.trim(val)) === 'undefined' || _.toLower(_.trim(val)) === 'null' || _.trim(val) === 'NaN') { 23 | /** 24 | * set un-normal value to null. 25 | * 26 | * 1. 为支持部分字段从有值改为无值. 27 | */ 28 | fastify.server.req.body[key] = null 29 | } else { 30 | /** 31 | * 1.此方法可以处理类 array 的字符串, e.g: '["ab", 2, "cd"]'. 32 | * 2.其它不能被 json.parse 成功的字符串直接使用原值. 33 | */ 34 | try { 35 | fastify.server.req.body[key] = JSON.parse(val) 36 | } catch (e) { 37 | fastify.server.req.body[key] = val 38 | } 39 | } 40 | } 41 | } 42 | }) 43 | 44 | fastify.server.req.body.files = fastify.server.req.files 45 | } 46 | } 47 | 48 | export default RouterUtil 49 | -------------------------------------------------------------------------------- /src/utils/store-global-data-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | import categoryHelper from '../helpers/category-helper' 4 | 5 | class StoreGlobalDataUtil { 6 | constructor() { 7 | } 8 | 9 | static async storeGloabalCategories(fastify) { 10 | let categoriesCodeMap = {}, 11 | categoriesIdMap = {}, 12 | categoryJson = {} 13 | 14 | const categories = await categoryHelper.getSimpleCategories(fastify, {}) 15 | categories.forEach(category => { 16 | categoryJson = JSON.parse(JSON.stringify(category)) 17 | 18 | if (!_.isEmpty(categoryJson.code)) { 19 | categoriesCodeMap[categoryJson.code] = categoryJson 20 | } 21 | categoriesIdMap[categoryJson.category_id] = categoryJson 22 | }) 23 | 24 | fastify.categoriesCodeMap = categoriesCodeMap 25 | fastify.categoriesIdMap = categoriesIdMap 26 | } 27 | } 28 | 29 | export default StoreGlobalDataUtil 30 | -------------------------------------------------------------------------------- /src/utils/string-util.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | class StringUtil { 4 | constructor() { 5 | } 6 | 7 | static reverse(source) { 8 | return source.split('').reverse().join('') 9 | } 10 | 11 | static replaceLast(source, replaceMent, target) { 12 | return StringUtil.reverse(StringUtil.reverse(source).replace(new RegExp(StringUtil.reverse(replaceMent)), StringUtil.reverse(target))) 13 | } 14 | 15 | static isBlank(sth) { 16 | return _.isEmpty(_.toString(sth)) 17 | } 18 | 19 | static isNotBlank(sth) { 20 | return !_.isEmpty(_.toString(sth)) 21 | } 22 | 23 | static isTrue(sth) { 24 | return _.eq(sth, true) || _.eq(sth, 1) || (_.isString(sth) && _.eq(_.toLower(sth), 'true')) 25 | } 26 | 27 | static isFalse(sth) { 28 | return !StringUtil.isTrue(sth) 29 | } 30 | } 31 | 32 | export default StringUtil 33 | --------------------------------------------------------------------------------