├── .babelrc ├── .gitignore ├── README.md ├── app.js ├── config └── development.js ├── db ├── mongodb.js ├── mysql.js └── redis.js ├── docs ├── SQL │ └── shin_backend.sql └── assets │ ├── 1.png │ ├── architecture.odg │ ├── architecture.png │ ├── shin.odg │ └── shin.png ├── index-worker.js ├── index.js ├── init.js ├── middlewares ├── checkAuth.js ├── checkExport.js ├── errorHandle.js └── index.js ├── models ├── AppGlobalConfig.js ├── BackendUserAccount.js ├── BackendUserRole.js ├── WebMonitor.js ├── WebMonitorRecord.js ├── WebMonitorStatis.js ├── WebPerformance.js ├── WebPerformanceProject.js ├── WebPerformanceStatis.js ├── WebShortChain.js └── index.js ├── package.json ├── public └── blank.gif ├── routers ├── common.js ├── index.js ├── template.js ├── tool.js ├── user.js └── webMonitor.js ├── scripts ├── demo.js └── index.js ├── services ├── backendUserAccount.js ├── backendUserRole.js ├── common.js ├── index.js ├── tool.js └── webMonitor.js ├── static └── img │ ├── avatar.png │ └── cover.jpg ├── test ├── index.js ├── mocha.opts ├── routers │ └── user.js ├── services │ └── user.js └── utils │ └── tool.js ├── utils ├── constant.js ├── index.js ├── murmurhash.js ├── queue.js ├── tools.js └── xfetch.js └── worker ├── agenda.js ├── cronJobs ├── demo.js ├── webMonitorRemove.js ├── webMonitorStatis.js ├── webMonitorWarn.js ├── webPerformanceRemove.js ├── webPerformanceStatis-func.js └── webPerformanceStatis.js └── triggerJobs └── demo.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS 排序文件 2 | .DS_Store 3 | 4 | # vscode 配置目录 5 | .vscode 6 | 7 | # dependencies 8 | /node_modules 9 | /package-lock.json 10 | 11 | 12 | # 本地的配置文件 13 | # config/development.js 14 | 15 | # 生成的目录 16 | dist/ 17 | upload/ 18 | static/upload/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shin-server 2 |   shin 的读音是[ʃɪn],谐音就是行,寓意可行的后端系统服务,它的特点是: 3 | 4 | * 站在巨人的肩膀上,依托[KOA2](https://github.com/demopark/koa-docs-Zh-CN)、[bunyan](https://github.com/trentm/node-bunyan)、[Sequelize](https://www.sequelize.com.cn/)等优秀的框架和库所搭建的定制化后端系统服务。 5 | * 一套完整的 Node.js 后端服务解决方案。 6 | * 调试便捷,实时打印出各类请求、日志和所有的查询语句。 7 | * 配合独立的配置文件可连接 MongoDB、MySQL 以及 Redis。 8 | * 已开辟脚本和定时任务目录,可将相应文件补充进来。 9 | * 容易扩展,可引入第三方库,例如队列、云服务等。 10 | 11 |   与[shin-admin](https://github.com/pwstrick/shin-admin)配合使用的话,大致架构如下图。 12 | 13 | ![架构](https://github.com/pwstrick/shin-server/blob/main/docs/assets/architecture.png) 14 | 15 | # 准备工作 16 | #### 1)安装 17 |   在将项目下载下来后,来到其根目录,运行安装命令,自动将依赖包下载到本地。 18 | ```bash 19 | $ npm install 20 | ``` 21 | 22 | #### 2)启动 23 |   在启动服务器之前,需要确保本地已经安装并已开启 MongoDB、MySQL 以及 Redis。 24 | * mongo 启动命令:mongod 25 | * redis 启动命令:redis-server 26 | 27 |   在 docs/SQL 中有个数据库文件,可初始化所需的表,并且注意将 config/development.js 中数据库的账号和密码修改成本机的。 28 | 29 |   执行项目启动命令,成功后的终端如下图所示,端口号默认是 6060,可在 config/development.js 中自定义端口号。 30 | ```bash 31 | $ npm start 32 | ``` 33 | ![启动](https://github.com/pwstrick/shin-server/blob/main/docs/assets/1.png) 34 | 35 |   运行 http://localhost:6060/user/init 可初始化超级管理员账号,后台账号和权限都保存在 MongoDB 中,其他一些业务保存在 MySQL 中。 36 | * 账号:admin@shin.com 37 | * 密码:admin 38 | 39 | #### 3)运行流程 40 |   当向这套后端系统服务请求一个接口时,其大致流程如下图所示。 41 | 42 |

43 | 44 |

45 | 46 | # 目录结构 47 | ``` 48 | ├── shin-server 49 | │ ├── config --------------------------------- 全局配置文件 50 | │ ├── db ------------------------------------- 数据库连接 51 | │ ├── docs ----------------------------------- 说明文档 52 | │ ├── middlewares ---------------------------- 自定义的中间件 53 | │ ├── models --------------------------------- 数据表映射 54 | │ ├── routers -------------------------------- api 路由层 55 | │ ├── scripts -------------------------------- 脚本文件 56 | │ ├── services ------------------------------- api 服务层 57 | │ ├── static --------------------------------- 静态资源 58 | │ ├── test ----------------------------------- 单元测试 59 | │ ├── utils ---------------------------------- 公用工具 60 | │ ├── worker --------------------------------- 定时任务 61 | │ ├── app.js --------------------------------- 启动文件 62 | │ ├── index.js ------------------------------- 入口文件 63 | └───└── index-worker.js ------------------------ 任务的入口文件 64 | ``` 65 | 66 | #### 1)app.js 67 |   在启动文件中,初始化了 [bunyan](https://github.com/trentm/node-bunyan) 日志框架,并声明了一个全局的 logger 变量(可调用的方法包括 info、error、warn、debug等) ,可随时写日志,所有的请求信息(引入了[koa-bunyan-logger](https://github.com/koajs/bunyan-logger))、数据库查询语句、响应数据等,都会写入到服务器的日志中。 68 | 69 |   [JWT](https://jwt.io/) 认证 HTTP 请求,引入了 koa-jwt 中间件,会在 checkAuth.js 中间件(如下代码所示)中调用 ctx.state.user,以此来判断权限。而判断当前是否是登录会以 GET 方式向 ”api/user“ 接口发送一次请求。 70 | 71 |   还引入了 routers() 函数(位于 routers 目录的 index.js 中),将 services 和 middlewares 两个目录下的文件作为参数传入,这两个目录下都包含 index.js 文件,引用方式为 middlewares.checkAuth()、 services.android 等。 72 | ```javascript 73 | import requireIndex from 'es6-requireindex'; 74 | import services from '../services/'; 75 | import middlewares from '../middlewares'; 76 | 77 | export default (router) => { 78 | const dir = requireIndex(__dirname); 79 | Object.keys(dir).forEach((item) => { 80 | dir[item](router, services, middlewares); 81 | }); 82 | }; 83 | ``` 84 | 85 | #### 2)config 86 |   默认只包含 development.js,即开发环境的配置文件,可包含数据库的地址、各类账号密码等。 87 | 88 |   使用[node-config](https://lorenwest.github.io/node-config/)后,就能根据当前环境(NODE_ENV)调用相应名称的配置文件,例如 production.js、test.js 等。 89 | 90 | #### 3)db 91 |   MySQL 数据库 ORM 系统采用的是 [Sequelize](https://www.sequelize.com.cn/),MongoDB 数据库 ORM系统采用的是 [Mongoose](http://www.mongoosejs.net/docs/guide.html),redis 库采用的是 [ioredis](https://github.com/luin/ioredis/blob/master/API.md)。 92 | 93 | #### 4)models 94 |   声明各张表的结构,可用驼峰,也可用下划线的命名方式,函数的参数为 mysql 或 mongodb,可通过 mysql.backend 来指定要使用的数据库名称。 95 | ```javascript 96 | export default ({ mysql }) => 97 | mysql.backend.define("AppGlobalConfig", 98 | { 99 | id: { 100 | type: Sequelize.INTEGER, 101 | field: "id", 102 | autoIncrement: true, 103 | primaryKey: true 104 | }, 105 | title: { 106 | type: Sequelize.STRING, 107 | field: "title" 108 | }, 109 | }, 110 | { 111 | tableName: "app_global_config", 112 | timestamps: false 113 | } 114 | ); 115 | ``` 116 |   models 目录中的 index.js 文件会将当前所有的 model 文件映射到一个 models 对象中。 117 | 118 | #### 5)routers 119 |   前端访问的接口,在此目录下声明,此处代码相当于 MVC 中的 Control 层。 120 | 121 |   在下面的示例中,完成了一次 GET 请求,middlewares.checkAuth()用于检查权限,其值就是在 [authority.js](https://github.com/pwstrick/shin-admin#9authorityjs) 声明的 id,ctx.body 会返回响应。 122 | ```javascript 123 | router.get( 124 | "/tool/short/query", 125 | middlewares.checkAuth("backend.tool.shortChain"), 126 | async (ctx) => { 127 | const { curPage = 1, short, url } = ctx.query; 128 | const { rows, count } = await services.tool.getShortChainList({ 129 | curPage, 130 | short, 131 | url 132 | }); 133 | ctx.body = { code: 0, data: rows, count }; 134 | } 135 | ); 136 | ``` 137 | 138 | #### 6)services 139 |   处理数据,包括读写数据表、调用后端服务、读写缓存等。 140 | 141 |   services 目录中的 index.js 文件会初始化各个 service 文件,并将之前的 models 对象作为参数传入。 142 | 143 |   注意,MySQL中查询数据返回值中会包含各种信息,如果只要表的数据需要在查询条件中加 ”raw:true“(如下所示)或将返回值调用 toJSON()。 144 | ```javascript 145 | async getConfigContent(where) { 146 | return this.models.AppGlobalConfig.findOne({ 147 | where, 148 | raw: true 149 | }); 150 | } 151 | ``` 152 | 153 | #### 7)scripts 154 |   如果要跑脚本,首先修改 scripts/index.js 文件中的最后一行 require() 的参数,即修改 ”./demo“。 155 | ```javascript 156 | global.env = process.env.NODE_ENV; 157 | global.logger = { 158 | trace: console.log, 159 | info: console.log, 160 | debug: console.log, 161 | error: console.log, 162 | warn: console.log, 163 | }; 164 | require('./demo'); 165 | ``` 166 |   当前位置如果与 scripts 目录平级,则执行命令: 167 | ```bash 168 | $ NODE_ENV=development node scripts/index.js 169 | ``` 170 |   其中 NODE_ENV 为环境常量,test、pre 和 production。 171 | 172 | #### 8)static 173 |   上传的文件默认会保存在 static/upload 目录中,git 会忽略该文件,不会提交到仓库中。 174 | 175 | 176 | # 开发步骤 177 | 1. 首先是在 models 目录中新建对应的表(如果不是新表,该步骤可省略)。 178 | 2. 然后是在 routes 目录中新建或修改某个路由文件。 179 | 3. 最后是在 services 目录中新建或修改某个服务文件。 180 | 181 |   在项目研发的过程中,发现很多操作都是对数据库做简单地增删改查,有时候就需要在上述三个目录中各自新建一个文件。 182 | 183 |   这么操作费时费力,到后期会发现有很多这样的接口,在维护上也会增加挑战,因此抽象了一套通用接口,保存在 routes/common.js 中。 184 | 185 | * get:读取一条数据(单表查询) 186 | * gets:读取多条数据(单表查询) 187 | * head:读取聚合数据,例如count()、sum()、max() 和 min() 188 | * post:提交数据,用于增加记录 189 | * put:更新数据 190 | 191 |   所有的接口采用 post 的请求方式,数据库表都是单表查询,不支持联表,若要联表则单独创建接口。 192 | 193 | # 定时任务 194 |   本地调试全任务可执行: 195 | ```bash 196 | $ npm run worker 197 | ``` 198 |   本地调试单任务可执行下面的命令,其中 ? 代表任务名称,即文件名,不用加后缀。 199 | ```bash 200 | $ JOB_TYPES=? npm run worker 201 | ``` 202 |   在 worker 目录中还包含两个目录:cronJobs 和 triggerJobs。 203 | 204 |   前者是定时类任务 (指定时间点执行),使用了 [node-schedule](https://github.com/node-schedule/node-schedule) 库。 205 | ```javascript 206 | module.exports = async () => { 207 | //每 30 秒执行一次定时任务 208 | schedule.scheduleJob({ rule: "*/30 * * * * *" }, () => { 209 | test(result); 210 | }); 211 | }; 212 | ``` 213 |   后者是触发类任务,在代码中输入指令触发执行,使用 [agenda](https://github.com/agenda/agenda) 库。 214 | ```javascript 215 | module.exports = (agenda) => { 216 | // 例如满足某种条件触发邮件通知 217 | agenda.define('send email report', (job, done) => { 218 | // 传递进来的数据 219 | const data = job.attrs.data; 220 | console.log(data); 221 | // 触发此任务,需要先引入 agenda.js,然后调用 now() 方法 222 | // import agenda from '../worker/agenda'; 223 | // agenda.now('send email report', { 224 | // username: realName, 225 | // }); 226 | }); 227 | }; 228 | ``` 229 |   注意,写好的任务记得添加进入口文件 index-worker.js。 230 | ```javascript 231 | require('./worker/cronJobs/demo')(); 232 | require('./worker/triggerJobs/demo')(agenda); 233 | ``` 234 | 235 | # 单元测试 236 |   运行下面的命令就会执行单元测试。 237 | ```bash 238 | $ npm test 239 | ``` 240 |   单元测试使用的框架是 [mocha 3.4](https://mochajs.cn/),采用的断言是 [chai 4.0](https://www.chaijs.com/api/bdd/),API测试库是 [supertest 3.0](https://github.com/visionmedia/supertest),测试替代库 [sion.js](https://sinonjs.org) 241 | ```javascript 242 | // routers 测试 243 | describe('GET /user/list', () => { 244 | const url = '/user/list'; 245 | it('获取用户列表成功', (done) => { 246 | api 247 | .get(url) 248 | .set('Authorization', authToken) 249 | .expect(200, done); 250 | }); 251 | }); 252 | 253 | // serveices 测试 254 | import backendUserRole from '../../services/backendUserRole'; 255 | describe('用户角色', () => { 256 | it('获取指定id的角色信息', async () => { 257 | const service = new backendUserRole(models); 258 | const res = await service.getInfoById('584a4dc24c886205bd771afe'); 259 | // expect(2).toBe(2); 260 | // expect(res.rolePermisson).to.be.an('array'); 261 | }); 262 | }); 263 | ``` -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:13:00 4 | * @LastEditTime: 2023-04-27 18:05:24 5 | * @LastEditors: strick 6 | * @Description: 启动文件 7 | * @FilePath: /strick/shin-server/app.js 8 | */ 9 | import Koa from 'koa'; 10 | import KoaRouter from 'koa-router'; 11 | import KoaCompress from 'koa-compress'; 12 | import KoaBodyParser from 'koa-bodyparser'; 13 | import KoaValidate from 'koa-validate'; 14 | import KoaStatic from 'koa-static'; 15 | import koaBunyanLogger from 'koa-bunyan-logger'; 16 | import jwt from 'koa-jwt'; 17 | import config from 'config'; 18 | import pino from 'pino'; 19 | import koaPinoLogger from 'koa-pino-logger'; 20 | import routers from './routers/'; 21 | import errorHandle from './middlewares/errorHandle'; 22 | import init from './init'; 23 | 24 | const app = new Koa(); 25 | const router = new KoaRouter(); 26 | 27 | /** 28 | * 基于 OpenTracing 标准的分布式追踪系统,有助于分析服务架构中的计时数据 29 | * https://github.com/openzipkin/zipkin-js 30 | */ 31 | // import { Tracer, ConsoleRecorder, ExplicitContext } from 'zipkin'; 32 | // import { koaMiddleware } from 'zipkin-instrumentation-koa'; 33 | // const ctxImpl = new ExplicitContext(); 34 | // const recorder = new ConsoleRecorder(); 35 | // const tracer = new Tracer({recorder, ctxImpl, localServiceName: 'zipkin-koa-shin'}); 36 | // app.use(koaMiddleware({tracer})); 37 | 38 | 39 | /** 40 | * 全链路日志追踪 41 | * https://github.com/puzpuzpuz/cls-rtracer 42 | */ 43 | const rTracer = require('cls-rtracer'); 44 | 45 | global.logger = pino({ 46 | name: 'shin-server', 47 | level: 'trace', 48 | mixin () { 49 | return { 'req-id': rTracer.id() } 50 | }, 51 | hooks: { 52 | // 格式化日志 53 | logMethod (inputArgs, method) { 54 | const printfs = []; 55 | for(let i=0; i req.id, 85 | res: () => undefined, 86 | }, 87 | customAttributeKeys: { 88 | req: 'req-id', 89 | }, 90 | genReqId: () => rTracer.id(), // 声明 req-id 的值 91 | })); 92 | 93 | app.use(KoaCompress()); 94 | app.use(KoaBodyParser({ jsonLimit: '10mb', textLimit: '10mb', enableTypes: ['json', 'form', 'text'] })); 95 | app.use(KoaStatic('static')); 96 | app.use(KoaStatic('upload')); 97 | app.use(jwt({ 98 | secret: config.get('jwtSecret'), //401 Unauthorized 99 | }).unless({ 100 | path: [/user\/login/, /user\/init/, /\/download/, /common\/upload/, /pe\.gif/, /ma\.gif/, /smap\/del/, /callback/], //跳过登录态的请求 101 | })); 102 | app.use(errorHandle()); 103 | app.use(koaBunyanLogger.requestLogger({ 104 | updateLogFields(fields) { 105 | delete fields.req; 106 | delete fields.res; 107 | fields.operator = this.state.user && this.state.user.realName; 108 | }, 109 | })); 110 | 111 | app.use(router.routes()); 112 | app.use(router.allowedMethods()); 113 | 114 | routers(router); 115 | KoaValidate(app); 116 | 117 | app.listen(config.get('port')); 118 | 119 | logger.info(`Server started on ${config.get('port')}`); 120 | 121 | //开始处理任务 122 | init(); -------------------------------------------------------------------------------- /config/development.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:17:36 4 | * @LastEditTime: 2021-09-06 13:46:25 5 | * @LastEditors: strick 6 | * @Description: 本地配置文件 7 | * @FilePath: /strick/shin-server/config/development.js 8 | */ 9 | module.exports = { 10 | port: 6060, 11 | jwtSecret: 'abcd', 12 | /** 13 | * api服务域名配置 14 | */ 15 | services: { 16 | adminApi: 'http://localhost:8000/api', 17 | }, 18 | /** 19 | * 数据库相关配置 20 | */ 21 | mongodb: { 22 | hosts: [ 23 | '127.0.0.1:27017', 24 | ], 25 | options: { 26 | dbName: 'shin_backend', 27 | }, 28 | }, 29 | redis: { 30 | aws: { 31 | host: '127.0.0.1', 32 | port: 6379, 33 | }, 34 | }, 35 | // JS 映射文件地址 36 | sourceMapPath: '../../smap', 37 | // 任务队列缓存 38 | kueRedis: { 39 | host: '127.0.0.1', 40 | port: 6379, 41 | }, 42 | mysql: { 43 | backend: { 44 | database: 'shin_backend', 45 | username: null, 46 | password: null, 47 | options: { 48 | dialect: 'mysql', 49 | port: 3306, 50 | replication: { 51 | read: [ 52 | { host: '127.0.0.1', username: 'root', password: '123' }, 53 | ], 54 | write: { host: '127.0.0.1', username: 'root', password: '123' }, 55 | }, 56 | benchmark: true, 57 | dialectOptions: { 58 | charset: 'utf8mb4', 59 | collate: 'utf8mb4_unicode_ci', 60 | supportBigNumbers: true, 61 | bigNumberStrings: true, 62 | }, 63 | pool: { 64 | max: 10, 65 | min: 0, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /db/mongodb.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-02-02 16:18:02 5 | * @LastEditTime: 2023-04-24 18:09:48 6 | * @Description: MongoDB配置 7 | * @FilePath: /strick/shin-server/db/mongodb.js 8 | */ 9 | import mongoose from 'mongoose'; 10 | import config from 'config'; 11 | 12 | mongoose.set('debug', (...args) => { 13 | if (args[0] !== 'agendaJobs') { 14 | logger.debug(...args); 15 | } 16 | }); 17 | 18 | const mongooseConfig = config.get('mongodb'); 19 | const url = `mongodb://${mongooseConfig.hosts.join(',')}`; 20 | mongoose.connect(url, { 21 | poolSize: 10, 22 | useNewUrlParser: true, 23 | ...mongooseConfig.options, 24 | promiseLibrary: Promise, 25 | }); 26 | const db = mongoose.connection; 27 | 28 | db.once('open', () => { 29 | logger.info('mongodb connected'); 30 | }); 31 | db.on('error', (err) => { 32 | logger.error(`mongodb err: ${err}`); 33 | }); 34 | 35 | export default mongoose; 36 | -------------------------------------------------------------------------------- /db/mysql.js: -------------------------------------------------------------------------------- 1 | import Sequelize from 'sequelize'; 2 | import config from 'config'; 3 | 4 | const mysql = {}; 5 | const mysqlServers = config.get('mysql'); 6 | const create = (params) => { 7 | const { database, username, password, options } = params; 8 | return new Sequelize(database, username, password, { 9 | ...options, 10 | logging: (msg, benchmark) => logger.debug(msg, `${benchmark}ms`), 11 | }); 12 | }; 13 | Object.keys(mysqlServers).forEach((item) => { 14 | mysql[item] = create(mysqlServers[item]); 15 | mysql[item].authenticate().then(() => { 16 | logger.info(`${item} mysql connected success`); 17 | }).catch((error) => { 18 | logger.error(`${item} mysql 连接错误: ${error.toString()}`); 19 | }); 20 | }); 21 | 22 | global.Sequelize = Sequelize; 23 | export default mysql; 24 | -------------------------------------------------------------------------------- /db/redis.js: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | import config from 'config'; 3 | 4 | const connections = {}; 5 | const redisServers = config.get('redis'); 6 | 7 | Object.keys(redisServers).forEach((item) => { 8 | const redisConfig = redisServers[item]; 9 | let client; 10 | // 如果配置项是数组则使用 cluster 模式连接 11 | if (Array.isArray(redisConfig)) { 12 | client = new Redis.Cluster(redisConfig); 13 | } else { 14 | client = new Redis(redisConfig); 15 | } 16 | client.on('error', (err) => { 17 | logger.error(`${item} redis error: ${err}`); 18 | }); 19 | client.on('ready', () => { 20 | logger.info(`${item} redis connected`); 21 | }); 22 | connections[item] = client; 23 | }); 24 | 25 | export default connections; 26 | 27 | -------------------------------------------------------------------------------- /docs/SQL/shin_backend.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `shin_backend` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */; 2 | USE `shin_backend`; 3 | 4 | DROP TABLE IF EXISTS `app_global_config`; 5 | CREATE TABLE `app_global_config` ( 6 | `id` int(11) NOT NULL AUTO_INCREMENT, 7 | `title` varchar(60) COLLATE utf8mb4_bin NOT NULL COMMENT '标题', 8 | `content` text COLLATE utf8mb4_bin NOT NULL COMMENT 'JSON格式的内容', 9 | `key` varchar(40) COLLATE utf8mb4_bin NOT NULL COMMENT '唯一标识', 10 | `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 11 | `mtime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 12 | `status` tinyint(4) DEFAULT '1' COMMENT '状态', 13 | PRIMARY KEY (`id`), 14 | UNIQUE KEY `key_UNIQUE` (`key`) 15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='全局通用配置'; 16 | 17 | DROP TABLE IF EXISTS `web_short_chain`; 18 | CREATE TABLE `web_short_chain` ( 19 | `id` int(11) NOT NULL AUTO_INCREMENT, 20 | `short` varchar(10) COLLATE utf8mb4_bin NOT NULL COMMENT '短链地址中的key', 21 | `url` varchar(200) COLLATE utf8mb4_bin NOT NULL COMMENT '原始地址', 22 | `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 23 | `mtime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 24 | `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态', 25 | PRIMARY KEY (`id`), 26 | UNIQUE KEY `short_UNIQUE` (`short`) 27 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='短链存储'; 28 | 29 | DROP TABLE IF EXISTS `web_monitor`; 30 | CREATE TABLE `web_monitor` ( 31 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 32 | `project` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '项目名称', 33 | `project_subdir` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '项目的子目录,shin-h5项目下会有很多活动,放置在各个子目录中', 34 | `digit` int(11) NOT NULL DEFAULT '1' COMMENT '出现次数', 35 | `message` text COLLATE utf8mb4_bin NOT NULL COMMENT '聚合信息', 36 | `ua` varchar(600) COLLATE utf8mb4_bin NOT NULL COMMENT '代理信息', 37 | `key` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '去重用的标记', 38 | `category` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '日志类型', 39 | `source` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'SourceMap映射文件的地址', 40 | `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 41 | `identity` varchar(30) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '身份,用于连贯日志上下文', 42 | `day` int(11) DEFAULT NULL COMMENT '格式化的天(冗余字段),用于排序,20210322', 43 | `hour` tinyint(2) DEFAULT NULL COMMENT '格式化的小时(冗余字段),用于分组,11', 44 | `minute` tinyint(2) DEFAULT NULL COMMENT '格式化的分钟(冗余字段),用于分组,20', 45 | `message_status` int(11) DEFAULT NULL COMMENT 'message中的通信状态码', 46 | `message_path` varchar(45) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'message通信中的 path', 47 | `message_type` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 'message中的类别字段', 48 | `referer` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '来源地址', 49 | PRIMARY KEY (`id`), 50 | KEY `idx_key_category_project_identity` (`key`,`category`,`project`,`identity`), 51 | KEY `idx_category_project_identity` (`category`,`project`,`identity`), 52 | KEY `index_ctime` (`ctime`), 53 | KEY `index_ctime_category` (`ctime`,`category`), 54 | KEY `idx_category_project_ctime` (`category`,`project`,`ctime`), 55 | KEY `idx_messagepath` (`message_path`), 56 | KEY `idx_category_messagetype_ctime` (`category`,`message_type`,`ctime`) 57 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='前端监控日志'; 58 | 59 | DROP TABLE IF EXISTS `web_monitor_statis`; 60 | CREATE TABLE `web_monitor_statis` ( 61 | `id` int(11) NOT NULL AUTO_INCREMENT, 62 | `date` int(11) NOT NULL COMMENT '日期,格式为20210318,一天只存一条记录', 63 | `statis` text COLLATE utf8mb4_bin NOT NULL COMMENT '以JSON格式保存的统计信息', 64 | PRIMARY KEY (`id`), 65 | UNIQUE KEY `date_UNIQUE` (`date`) 66 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='前端监控日志统计信息表'; 67 | 68 | DROP TABLE IF EXISTS `web_performance`; 69 | CREATE TABLE `web_performance` ( 70 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 71 | `load` int(11) NOT NULL DEFAULT '0' COMMENT '页面加载总时间', 72 | `ready` int(11) NOT NULL DEFAULT '0' COMMENT '用户可操作时间', 73 | `paint` int(11) NOT NULL DEFAULT '0' COMMENT '白屏时间', 74 | `screen` int(11) NOT NULL DEFAULT '0' COMMENT '首屏时间', 75 | `measure` varchar(1000) COLLATE utf8mb4_bin NOT NULL COMMENT '其它测量参数,用JSON格式保存', 76 | `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 77 | `day` int(11) NOT NULL COMMENT '格式化的天(冗余字段),用于排序,20210322', 78 | `hour` tinyint(2) NOT NULL COMMENT '格式化的小时(冗余字段),用于分组,11', 79 | `minute` tinyint(2) DEFAULT NULL COMMENT '格式化的分钟(冗余字段),用于分组,20', 80 | `identity` varchar(30) COLLATE utf8mb4_bin NOT NULL COMMENT '身份', 81 | `project` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '项目关键字,关联 web_performance_project 表中的key', 82 | `ua` varchar(600) COLLATE utf8mb4_bin NOT NULL COMMENT '代理信息', 83 | `referer` varchar(200) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '来源地址', 84 | `timing` text COLLATE utf8mb4_bin COMMENT '浏览器读取到的性能参数,用于排查', 85 | `resource` text COLLATE utf8mb4_bin COMMENT '静态资源信息', 86 | PRIMARY KEY (`id`), 87 | KEY `idx_project_day` (`project`,`day`), 88 | KEY `idx_project_day_hour` (`project`,`day`,`hour`) 89 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能监控'; 90 | 91 | DROP TABLE IF EXISTS `web_performance_project`; 92 | CREATE TABLE `web_performance_project` ( 93 | `id` int(11) NOT NULL AUTO_INCREMENT, 94 | `key` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '唯一值', 95 | `name` varchar(45) COLLATE utf8mb4_bin NOT NULL COMMENT '项目名称', 96 | `ctime` timestamp NULL DEFAULT CURRENT_TIMESTAMP, 97 | `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1:正常 0:删除', 98 | PRIMARY KEY (`id`), 99 | UNIQUE KEY `name_UNIQUE` (`name`) 100 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能监控项目'; 101 | 102 | DROP TABLE IF EXISTS `web_performance_statis`; 103 | CREATE TABLE `web_performance_statis` ( 104 | `id` int(11) NOT NULL AUTO_INCREMENT, 105 | `date` int(11) NOT NULL COMMENT '日期,格式为20210318', 106 | `statis` mediumtext COLLATE utf8mb4_bin NOT NULL COMMENT '以JSON格式保存的统计信息', 107 | `project` varchar(20) COLLATE utf8mb4_bin NOT NULL COMMENT '项目关键字,关联 web_performance_project 表中的key', 108 | PRIMARY KEY (`id`) 109 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='性能监控统计'; 110 | 111 | DROP TABLE IF EXISTS `web_monitor_record`; 112 | CREATE TABLE `web_monitor_record` ( 113 | `id` INT NOT NULL AUTO_INCREMENT, 114 | `monitor_id` BIGINT NOT NULL COMMENT '监控日志的ID', 115 | `record` MEDIUMTEXT NOT NULL COMMENT '回放信息,包括各类元素和DOM操作记录', 116 | `ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 117 | PRIMARY KEY (`id`), 118 | UNIQUE INDEX `monitor_id_UNIQUE` (`monitor_id` ASC)) 119 | DEFAULT CHARACTER SET = utf8mb4 120 | COLLATE = utf8mb4_bin 121 | COMMENT = '直播监控回放'; -------------------------------------------------------------------------------- /docs/assets/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/docs/assets/1.png -------------------------------------------------------------------------------- /docs/assets/architecture.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/docs/assets/architecture.odg -------------------------------------------------------------------------------- /docs/assets/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/docs/assets/architecture.png -------------------------------------------------------------------------------- /docs/assets/shin.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/docs/assets/shin.odg -------------------------------------------------------------------------------- /docs/assets/shin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/docs/assets/shin.png -------------------------------------------------------------------------------- /index-worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2020-12-16 19:17:57 5 | * @LastEditTime: 2021-02-03 15:01:36 6 | * @Description: 定时任务配置文件 7 | * @FilePath: /strick/shin-server/index-worker.js 8 | */ 9 | require('babel-core/register'); 10 | require('babel-polyfill'); 11 | 12 | const bunyan = require('bunyan'); 13 | 14 | global.logger = bunyan.createLogger({ 15 | name: 'Task', 16 | level: 'trace', 17 | }); 18 | 19 | global.env = process.env.NODE_ENV || 'development'; 20 | 21 | const agenda = require('./worker/agenda.js'); 22 | //读取命令中带的任务名称(用于调试),例如 JOB_TYPES=? npm run worker ( ? 代表任务名称,即文件名) 23 | const jobTypes = process.env.JOB_TYPES ? process.env.JOB_TYPES.split(',') : []; 24 | 25 | /** 26 | * 基于时间的定时任务,采用 node-schedule 库 27 | * https://github.com/node-schedule/node-schedule 28 | */ 29 | if (jobTypes.length) { 30 | jobTypes.forEach((type) => { 31 | try { 32 | require('./worker/cronJobs/' + type)(); 33 | } catch (ex) { 34 | 35 | } 36 | }); 37 | } else { 38 | require('./worker/cronJobs/demo')(); 39 | } 40 | 41 | /** 42 | * 触发型的定时任务,采用 agenda 库 43 | * https://github.com/agenda/agenda 44 | */ 45 | agenda.on('ready', () => { 46 | if (jobTypes.length) { 47 | jobTypes.forEach((type) => { 48 | try { 49 | require('./worker/triggerJobs/' + type)(agenda); 50 | } catch (ex) { 51 | 52 | } 53 | }); 54 | } else { 55 | require('./worker/triggerJobs/demo')(agenda); 56 | } 57 | agenda.start().catch(error => logger.error('job start error', error)); 58 | }); 59 | 60 | async function graceful() { 61 | await agenda.stop(); 62 | process.exit(0); 63 | } 64 | 65 | process.on('SIGTERM', graceful); 66 | process.on('SIGINT', graceful); 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:10:13 4 | * @LastEditTime: 2021-02-02 16:13:08 5 | * @LastEditors: strick 6 | * @Description: 入口文件 7 | * @FilePath: /strick/shin-server/index.js 8 | */ 9 | require('babel-core/register'); 10 | require('babel-polyfill'); 11 | 12 | global.env = process.env.NODE_ENV || 'development'; 13 | 14 | require('./app.js'); -------------------------------------------------------------------------------- /init.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-09-06 12:49:20 5 | * @LastEditTime: 2021-09-06 12:53:00 6 | * @Description: 7 | * @FilePath: /strick/shin-server/init.js 8 | */ 9 | import queue from './utils/queue'; 10 | import services from './services'; 11 | 12 | export default () => { 13 | //处理性能数据 14 | queue.process('handlePerformance', (job, done) => { 15 | services.webMonitor.createPerformance(job.data.performance) 16 | .then(() => done()) 17 | .catch((err) => { 18 | logger.error(err); 19 | done(err); 20 | }); 21 | }); 22 | 23 | //处理监控数据 24 | const taskName = 'handleMonitor'; 25 | queue.process(taskName, (job, done) => { 26 | logger.trace(`${taskName} job begin`, job.id); 27 | services.webMonitor.handleMonitor(job.data.monitor) 28 | .then(() => { 29 | done(); 30 | logger.trace(`${taskName} job completed`, job.id); 31 | job.remove(function(){//手动移除 32 | logger.trace(`${taskName} job removed`, job.id); 33 | }); 34 | }) 35 | .catch((err) => { 36 | logger.error(err); 37 | done(err); 38 | }); 39 | }); 40 | } -------------------------------------------------------------------------------- /middlewares/checkAuth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:24:43 4 | * @LastEditTime: 2021-02-03 18:34:31 5 | * @LastEditors: strick 6 | * @Description: 验证当前用户的操作权限 7 | * @FilePath: /strick/shin-server/middlewares/checkAuth.js 8 | */ 9 | import redis from '../db/redis'; 10 | 11 | export default authority => async (ctx, next) => { 12 | const { id } = ctx.state.user; 13 | const res = await redis.aws.get(`backend:user:account:authorities:${id}`); 14 | if (!res) { 15 | ctx.status = 409; 16 | ctx.body = { error: '服务升级,请重新登录' }; 17 | return; 18 | } 19 | const authorities = res.split(','); 20 | if (authorities.includes(authority) || authorities[0] === '*') { 21 | await next(); 22 | } else { 23 | ctx.status = 403; 24 | ctx.body = { error: '您没有操作权限' }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /middlewares/checkExport.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import jwt from 'jsonwebtoken'; 3 | 4 | export default () => async (ctx, next) => { 5 | const { token } = ctx.query; 6 | try { 7 | jwt.verify(token, config.get('jwtSecret')); 8 | await next(); 9 | } catch (error) { 10 | ctx.status = 400; 11 | ctx.body = { error }; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /middlewares/errorHandle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-02-02 16:24:43 5 | * @LastEditTime: 2023-04-25 17:18:34 6 | * @Description: 所有路由错误处理 7 | * @FilePath: /strick/shin-server/middlewares/errorHandle.js 8 | */ 9 | export default () => async (ctx, next) => { 10 | try { 11 | await next(); 12 | // 在响应头中自定义 req-id,实现全链路监控 13 | ctx.set('Access-Control-Expose-Headers', 'req-id'); 14 | ctx.set('req-id', ctx.req.id); 15 | } catch (error) { 16 | logger.error(ctx.path, error, ctx.path); 17 | ctx.status = 500; 18 | ctx.body = { error: String(error), reqId: ctx.req.id }; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /middlewares/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-02-02 16:22:30 5 | * @LastEditTime: 2023-04-24 18:11:27 6 | * @Description: 中间件配置 7 | * @FilePath: /strick/shin-server/middlewares/index.js 8 | */ 9 | import checkAuth from './checkAuth'; 10 | import checkExport from './checkExport'; 11 | import errorHandle from './errorHandle'; 12 | 13 | const middleswares = { 14 | checkAuth, 15 | checkExport, 16 | errorHandle 17 | }; 18 | 19 | export default middleswares; 20 | -------------------------------------------------------------------------------- /models/AppGlobalConfig.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2020-12-24 11:49:07 5 | * @LastEditTime: 2021-02-03 13:50:48 6 | * @Description: 全局通用配置 7 | * @FilePath: /strick/shin-server/models/AppGlobalConfig.js 8 | */ 9 | export default ({ mysql }) => 10 | mysql.backend.define("AppGlobalConfig", 11 | { 12 | id: { 13 | type: Sequelize.INTEGER, 14 | field: "id", 15 | autoIncrement: true, 16 | primaryKey: true 17 | }, 18 | // 标题 19 | title: { 20 | type: Sequelize.STRING, 21 | field: "title" 22 | }, 23 | // 内容 24 | content: { 25 | type: Sequelize.STRING, 26 | field: "content" 27 | }, 28 | // 唯一标识,用于读取 29 | key: { 30 | type: Sequelize.STRING, 31 | field: "key" 32 | }, 33 | ctime: { 34 | type: Sequelize.DATE, 35 | field: "ctime" 36 | }, 37 | mtime: { 38 | type: Sequelize.DATE, 39 | field: "mtime" 40 | }, 41 | status: { 42 | type: Sequelize.INTEGER, 43 | field: "status" 44 | } 45 | }, 46 | { 47 | tableName: "app_global_config", 48 | timestamps: false 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /models/BackendUserAccount.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:52:54 4 | * @LastEditTime: 2021-02-05 16:16:43 5 | * @LastEditors: strick 6 | * @Description: 管理后台账号 7 | * @FilePath: /strick/shin-server/models/BackendUserAccount.js 8 | */ 9 | export default ({ mongodb }) => 10 | mongodb.model( 11 | "BackendUserAccount", 12 | { 13 | // 用户名 14 | userName: { 15 | type: String, 16 | index: true 17 | }, 18 | // 密码 19 | password: String, 20 | // 真实姓名 21 | realName: String, 22 | // 手机号 23 | cellphone: String, 24 | // 创建时间 25 | createTime: { 26 | type: Date, 27 | default: Date.now 28 | }, 29 | // 更新时间 30 | updateTime: { 31 | type: Date, 32 | default: Date.now 33 | }, 34 | // 密码过期时间 35 | passwordExpireTime: Date, 36 | // 角色 37 | roles: Array, 38 | // 加密秘钥 39 | salt: String, 40 | // 状态 41 | status: { 42 | type: Number, 43 | default: 1 44 | }, 45 | // 在线状态 46 | online: { 47 | type: Number, 48 | default: 0 49 | } 50 | }, 51 | "user_account" 52 | ); 53 | 54 | -------------------------------------------------------------------------------- /models/BackendUserRole.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:52:31 4 | * @LastEditTime: 2021-02-02 16:53:08 5 | * @LastEditors: strick 6 | * @Description: 账号角色 7 | * @FilePath: /strick/shin-server/models/BackendUserRole.js 8 | */ 9 | export default ({ mongodb }) => mongodb.model('BackendUserRole', { 10 | // 角色名称 11 | roleName: String, 12 | // 角色简介 13 | roleDesc: String, 14 | // 角色拥有的权限 15 | rolePermission: Array, 16 | // 创建时间 17 | createTime: { 18 | type: Date, 19 | default: Date.now, 20 | }, 21 | // 更新时间 22 | updateTime: { 23 | type: Date, 24 | default: Date.now, 25 | }, 26 | // 状态 27 | status: { 28 | type: Number, 29 | default: 1, 30 | }, 31 | }, 'user_role'); 32 | -------------------------------------------------------------------------------- /models/WebMonitor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-24 16:17:40 4 | * @LastEditTime: 2023-05-08 17:20:55 5 | * @LastEditors: strick 6 | * @Description: 前端监控日志表 7 | * @FilePath: /strick/shin-server/models/WebMonitor.js 8 | */ 9 | module.exports = ({ mysql }) => 10 | mysql.backend.define( 11 | "WebMonitor", 12 | { 13 | id: { 14 | type: Sequelize.BIGINT, 15 | field: "id", 16 | autoIncrement: true, 17 | primaryKey: true 18 | }, 19 | project: { 20 | type: Sequelize.STRING, 21 | field: "project" 22 | }, 23 | project_subdir: { 24 | type: Sequelize.STRING, 25 | field: "project_subdir" 26 | }, 27 | digit: { 28 | type: Sequelize.INTEGER, 29 | field: "digit" 30 | }, 31 | message: { 32 | type: Sequelize.TEXT, 33 | field: "message" 34 | }, 35 | key: { 36 | type: Sequelize.STRING, 37 | field: "key" 38 | }, 39 | ua: { 40 | type: Sequelize.STRING(600), 41 | field: "ua" 42 | }, 43 | source: { 44 | type: Sequelize.STRING, 45 | field: "source" 46 | }, 47 | category: { 48 | type: Sequelize.STRING, 49 | field: "category" 50 | }, 51 | ctime: { 52 | type: Sequelize.DATE, 53 | field: "ctime" 54 | }, 55 | identity: { 56 | type: Sequelize.STRING, 57 | field: "identity" 58 | }, 59 | day: { 60 | type: Sequelize.INTEGER, 61 | field: "day" 62 | }, 63 | hour: { 64 | type: Sequelize.INTEGER, 65 | field: "hour" 66 | }, 67 | minute: { 68 | type: Sequelize.INTEGER, 69 | field: "minute" 70 | }, 71 | message_status: { 72 | type: Sequelize.INTEGER, 73 | field: "message_status" 74 | }, 75 | message_path: { 76 | type: Sequelize.STRING, 77 | field: "message_path" 78 | }, 79 | message_type: { 80 | type: Sequelize.STRING, 81 | field: "message_type" 82 | }, 83 | referer: { 84 | type: Sequelize.STRING, 85 | field: 'referer', 86 | }, 87 | }, 88 | { 89 | tableName: "web_monitor", 90 | timestamps: false 91 | } 92 | ); 93 | 94 | -------------------------------------------------------------------------------- /models/WebMonitorRecord.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2022-12-21 17:15:12 5 | * @LastEditTime: 2022-12-21 17:15:18 6 | * @Description: 行为记录表 7 | * @FilePath: /strick/shin-server/models/WebMonitorRecord.js 8 | */ 9 | module.exports = ({ mysql }) => mysql.backend.define( 10 | 'WebMonitorRecord', 11 | { 12 | monitor_id: Sequelize.BIGINT, 13 | record: Sequelize.TEXT('medium'), 14 | ctime: { 15 | type: Sequelize.DATE, 16 | field: 'ctime', 17 | }, 18 | }, 19 | { 20 | tableName: 'web_monitor_record', 21 | timestamps: false, 22 | }, 23 | ); -------------------------------------------------------------------------------- /models/WebMonitorStatis.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-17 13:08:09 5 | * @LastEditTime: 2021-09-06 12:18:57 6 | * @Description: 监控统计 7 | * @FilePath: /strick/shin-server/models/WebMonitorStatis.js 8 | */ 9 | module.exports = ({ mysql }) => 10 | mysql.backend.define( 11 | "WebMonitorStatis", 12 | { 13 | id: { 14 | type: Sequelize.INTEGER, 15 | field: "id", 16 | autoIncrement: true, 17 | primaryKey: true 18 | }, 19 | date: { 20 | type: Sequelize.INTEGER, 21 | field: "date" 22 | }, 23 | statis: { 24 | type: Sequelize.TEXT, 25 | field: "statis" 26 | }, 27 | }, 28 | { 29 | tableName: "web_monitor_statis", 30 | timestamps: false 31 | } 32 | ); -------------------------------------------------------------------------------- /models/WebPerformance.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-23 11:48:52 5 | * @LastEditTime: 2022-07-12 16:29:05 6 | * @Description: 性能监控日志 7 | * @FilePath: /strick/shin-server/models/WebPerformance.js 8 | */ 9 | module.exports = ({ mysql }) => 10 | mysql.backend.define( 11 | "WebPerformance", 12 | { 13 | id: { 14 | type: Sequelize.BIGINT, 15 | field: 'id', 16 | autoIncrement: true, 17 | primaryKey: true, 18 | }, 19 | load: { 20 | type: Sequelize.INTEGER, 21 | field: 'load', 22 | }, 23 | ready: { 24 | type: Sequelize.INTEGER, 25 | field: 'ready', 26 | }, 27 | paint: { 28 | type: Sequelize.INTEGER, 29 | field: 'paint', 30 | }, 31 | screen: { 32 | type: Sequelize.INTEGER, 33 | field: 'screen', 34 | }, 35 | ua: { 36 | type: Sequelize.STRING(600), 37 | field: 'ua', 38 | }, 39 | measure: { 40 | type: Sequelize.STRING(1000), 41 | field: 'measure', 42 | }, 43 | day: { 44 | type: Sequelize.INTEGER, 45 | field: 'day', 46 | }, 47 | hour: { 48 | type: Sequelize.INTEGER, 49 | field: 'hour', 50 | }, 51 | minute: { 52 | type: Sequelize.INTEGER, 53 | field: 'minute', 54 | }, 55 | project: { 56 | type: Sequelize.STRING, 57 | field: 'project', 58 | }, 59 | ctime: { 60 | type: Sequelize.DATE, 61 | field: 'ctime', 62 | }, 63 | identity: { 64 | type: Sequelize.STRING, 65 | field: 'identity', 66 | }, 67 | referer: { 68 | type: Sequelize.STRING, 69 | field: 'referer', 70 | }, 71 | referer_path: { 72 | type: Sequelize.STRING, 73 | field: 'referer_path', 74 | }, 75 | timing: { 76 | type: Sequelize.TEXT, 77 | field: 'timing', 78 | }, 79 | resource: { 80 | type: Sequelize.TEXT, 81 | field: 'resource', 82 | }, 83 | }, 84 | { 85 | tableName: "web_performance", 86 | timestamps: false 87 | } 88 | ); 89 | 90 | -------------------------------------------------------------------------------- /models/WebPerformanceProject.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-22 14:23:36 5 | * @LastEditTime: 2021-09-06 12:19:18 6 | * @Description: 性能监控项目 7 | * @FilePath: /strick/shin-server/models/WebPerformanceProject.js 8 | */ 9 | module.exports = ({ mysql }) => 10 | mysql.backend.define( 11 | "WebPerformanceProject", 12 | { 13 | id: { 14 | type: Sequelize.INTEGER, 15 | field: "id", 16 | autoIncrement: true, 17 | primaryKey: true 18 | }, 19 | name: { 20 | type: Sequelize.STRING, 21 | field: "name" 22 | }, 23 | key: { 24 | type: Sequelize.STRING, 25 | field: "key" 26 | }, 27 | status: { 28 | type: Sequelize.INTEGER, 29 | field: "status" 30 | }, 31 | ctime: { 32 | type: Sequelize.DATE, 33 | field: "ctime" 34 | }, 35 | }, 36 | { 37 | tableName: "web_performance_project", 38 | timestamps: false 39 | } 40 | ); 41 | -------------------------------------------------------------------------------- /models/WebPerformanceStatis.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-23 18:06:44 5 | * @LastEditTime: 2021-09-06 12:19:23 6 | * @Description: 性能数据统计 7 | * @FilePath: /strick/shin-server/models/WebPerformanceStatis.js 8 | */ 9 | module.exports = ({ mysql }) => 10 | mysql.backend.define( 11 | "WebPerformanceStatis", 12 | { 13 | id: { 14 | type: Sequelize.INTEGER, 15 | field: "id", 16 | autoIncrement: true, 17 | primaryKey: true 18 | }, 19 | date: { 20 | type: Sequelize.INTEGER, 21 | field: "date" 22 | }, 23 | statis: { 24 | type: Sequelize.TEXT, 25 | field: "statis" 26 | }, 27 | project: { 28 | type: Sequelize.TEXT, 29 | field: "project" 30 | }, 31 | }, 32 | { 33 | tableName: "web_performance_statis", 34 | timestamps: false 35 | } 36 | ); -------------------------------------------------------------------------------- /models/WebShortChain.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-01-19 10:01:09 4 | * @LastEditTime: 2021-02-05 13:42:30 5 | * @LastEditors: strick 6 | * @Description: 短链存储 7 | * @FilePath: /strick/shin-server/models/WebShortChain.js 8 | */ 9 | module.exports = ({ mysql }) => 10 | mysql.backend.define("WebShortChain", 11 | { 12 | id: { 13 | type: Sequelize.INTEGER, 14 | field: "id", 15 | autoIncrement: true, 16 | primaryKey: true 17 | }, 18 | // 短链中的6个字符 19 | short: { 20 | type: Sequelize.STRING, 21 | field: "short" 22 | }, 23 | // 原始地址 24 | url: { 25 | type: Sequelize.STRING, 26 | field: "url" 27 | }, 28 | ctime: { 29 | type: Sequelize.DATE, 30 | field: "ctime" 31 | }, 32 | mtime: { 33 | type: Sequelize.DATE, 34 | field: "mtime" 35 | }, 36 | status: { 37 | type: Sequelize.INTEGER, 38 | field: "status" 39 | } 40 | }, 41 | { 42 | tableName: "web_short_chain", 43 | timestamps: false 44 | } 45 | ); 46 | 47 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-02-02 16:22:56 5 | * @LastEditTime: 2023-04-24 18:11:37 6 | * @Description: model文件配置 7 | * @FilePath: /strick/shin-server/models/index.js 8 | */ 9 | import mysql from '../db/mysql'; 10 | import mongodb from '../db/mongodb'; 11 | import redis from '../db/redis'; 12 | import AppGlobalConfig from './AppGlobalConfig'; 13 | import BackendUserAccount from './BackendUserAccount'; 14 | import BackendUserRole from './BackendUserRole'; 15 | import WebMonitor from './WebMonitor'; 16 | import WebMonitorRecord from './WebMonitorRecord'; 17 | import WebMonitorStatis from './WebMonitorStatis'; 18 | import WebPerformance from './WebPerformance'; 19 | import WebPerformanceProject from './WebPerformanceProject'; 20 | import WebPerformanceStatis from './WebPerformanceStatis'; 21 | import WebShortChain from './WebShortChain'; 22 | 23 | const models = { 24 | AppGlobalConfig, 25 | BackendUserAccount, 26 | BackendUserRole, 27 | WebMonitor, 28 | WebMonitorRecord, 29 | WebMonitorStatis, 30 | WebPerformance, 31 | WebPerformanceProject, 32 | WebPerformanceStatis, 33 | WebShortChain 34 | }; 35 | Object.keys(models).forEach(key => { 36 | models[key] = models[key]({ mysql, mongodb, redis }); 37 | }); 38 | 39 | // Object.keys(models).forEach((modelName) => { 40 | // if (models[modelName].associate) { 41 | // models[modelName].associate(models); 42 | // } 43 | // }); 44 | export default models; 45 | 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shin-server", 3 | "version": "1.0.0", 4 | "description": "后台管理系统", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=development nodemon index.js | pino-pretty", 8 | "worker": "NODE_ENV=development nodemon index-worker.js | bunyan", 9 | "test": "./node_modules/mocha/bin/mocha | bunyan -l warn" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/pwstrick/shin-server.git" 14 | }, 15 | "author": "pwstrick", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/pwstrick/shin-server/issues" 19 | }, 20 | "homepage": "https://github.com/pwstrick/shin-server#readme", 21 | "dependencies": { 22 | "agenda": "^2.0.2", 23 | "axios": "^0.16.2", 24 | "babel-core": "^6.24.1", 25 | "babel-polyfill": "^6.23.0", 26 | "bunyan": "^1.8.12", 27 | "cls-rtracer": "^2.6.2", 28 | "config": "^1.30.0", 29 | "es6-requireindex": "^0.3.10", 30 | "ioredis": "^4.2.0", 31 | "koa": "^2.13.1", 32 | "koa-bodyparser": "^4.2.0", 33 | "koa-bunyan-logger": "^2.1.0", 34 | "koa-compress": "^2.0.0", 35 | "koa-jwt": "^3.2.2", 36 | "koa-logger": "^3.0.0", 37 | "koa-multer": "^1.0.2", 38 | "koa-pino-logger": "^4.0.0", 39 | "koa-router": "7.1.1", 40 | "koa-static": "^4.0.1", 41 | "koa-validate": "^1.0.7", 42 | "koa2-formidable": "^1.0.2", 43 | "kue": "^0.11.6", 44 | "lodash": "^4.17.4", 45 | "moment": "^2.18.1", 46 | "mongodb": "^2.2.27", 47 | "mongoose": "^5.4.17", 48 | "mysql": "^2.13.0", 49 | "node-schedule": "^1.3.2", 50 | "pino": "^8.11.0", 51 | "redis": "^2.7.1", 52 | "redis-clustr": "^1.6.0", 53 | "sequelize": "^3.30.4", 54 | "ua-parser-js": "^0.7.28", 55 | "zipkin": "^0.22.0", 56 | "zipkin-instrumentation-koa": "^0.22.0" 57 | }, 58 | "devDependencies": { 59 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 60 | "babel-preset-env": "^1.7.0", 61 | "babel-preset-es2015": "^6.24.1", 62 | "chai": "^4.0.0", 63 | "mocha": "^3.4.2", 64 | "nodemon": "^1.17.5", 65 | "pino-pretty": "^10.0.0", 66 | "supertest": "^3.0.0" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/blank.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/public/blank.gif -------------------------------------------------------------------------------- /routers/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 15:15:01 4 | * @LastEditTime: 2023-05-08 17:21:20 5 | * @LastEditors: strick 6 | * @Description: 通用路由 7 | * @FilePath: /strick/shin-server/routers/common.js 8 | */ 9 | import _ from "lodash"; 10 | import config from 'config'; 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | import crypto from 'crypto'; 14 | import moment from 'moment'; 15 | import queue from '../utils/queue'; 16 | import { MONITOR_PROJECT } from '../utils/constant'; 17 | import services from '../services/'; 18 | 19 | 20 | export default (router) => { 21 | /** 22 | * 上传逻辑 23 | */ 24 | async function upload(ctx) { 25 | const { files, body } = ctx.request; 26 | const { dir } = body; //上传的目录 27 | const { file } = files; 28 | const stringLib = 'abcdefghijklmnopqrstuvwxyz'; 29 | const randomString = _.sampleSize(stringLib, 6); 30 | const ext = file.name.split('.').pop(); 31 | // 文件的上传目录 32 | let upDir = "upload"; 33 | if(dir) { 34 | upDir = `upload/${dir}`; 35 | } 36 | // 文件的上传路径 37 | let filePath = `${upDir}/${Date.now()}${randomString.join('')}.${ext}`; 38 | // 创建目录 39 | const absdir = path.join(__dirname, `../static/${upDir}`), 40 | absFilePath = path.join(__dirname, `../static/${filePath}`); 41 | return new Promise( resolve => { 42 | // 创建可读流 43 | const reader = fs.createReadStream(file.path); 44 | fs.mkdirSync(absdir, { recursive: true }); 45 | // 创建可写流 46 | const upStream = fs.createWriteStream(absFilePath); 47 | // 可读流通过管道写入可写流 48 | reader.pipe(upStream); 49 | reader.on('end', () => { 50 | resolve(filePath); 51 | }); 52 | }); 53 | } 54 | /** 55 | * 文件上传 56 | */ 57 | router.post( 58 | "/common/upload", 59 | async (ctx) => { 60 | // 内部走的是异步逻辑,用Promise来实现同步 61 | const filePath = await upload(ctx); 62 | ctx.body = { code: 0, key: filePath }; 63 | } 64 | ); 65 | 66 | /** 67 | * 会员详情 68 | */ 69 | router.get( 70 | "/appuser/detail", 71 | async (ctx) => { 72 | const { type, id } = ctx.query; 73 | ctx.body = { code: 0 }; 74 | } 75 | ); 76 | 77 | /** 78 | * 读取表名,查询条件或字段 79 | */ 80 | function getTableAndWhere(body) { 81 | if(Object.keys(body).length == 0) { 82 | return { tableName: null }; 83 | } 84 | const tableName = Object.keys(body)[0]; //表名 85 | const where = body[tableName]; //查询条件 86 | return { tableName, where }; 87 | } 88 | 89 | /** 90 | * 验证表名 91 | */ 92 | function validateTableName(ctx, tableName) { 93 | if(!tableName || tableName == 'undefined') { 94 | ctx.body = { code: 1, msg: '请传输表名' }; 95 | return false; 96 | } 97 | return true; 98 | } 99 | /** 100 | * 读取一条记录 101 | * { 102 | * TableName : { 查询条件 } 103 | * } 104 | * TableName是Model文件的名称,并非数据库表名 105 | */ 106 | router.post('/get', 107 | async (ctx) => { 108 | const { body } = ctx.request; 109 | const { tableName, where } = getTableAndWhere(body); 110 | if(!validateTableName(ctx, tableName)) { 111 | return; 112 | } 113 | // 将表名和查询条件传递给数据库方法 114 | const data = await services.common.getOne(tableName, where); 115 | ctx.body = { code: 0, data }; 116 | }); 117 | 118 | /** 119 | * 读取多条记录 120 | * { 121 | * TableName : { 查询条件 }, 122 | * limit, order, curPage 123 | * } 124 | */ 125 | router.post('/gets', 126 | async (ctx) => { 127 | const { body } = ctx.request; 128 | // 分页,排序,页数 129 | let { limit, order, curPage } = body; 130 | delete body.limit; 131 | delete body.order; 132 | delete body.curPage; 133 | limit = limit && (+limit); 134 | const { tableName, where } = getTableAndWhere(body); 135 | if(!validateTableName(ctx, tableName)) { 136 | return; 137 | } 138 | const { count, rows } = await services.common.getList({ 139 | tableName, 140 | where, 141 | limit, 142 | order, 143 | curPage, 144 | }); 145 | ctx.body = { code: 0, data: rows, count }; 146 | }); 147 | 148 | /** 149 | * 聚合数据 150 | * 例如count,max,min 和 sum 151 | * { 152 | * TableName : { 查询条件 }, 153 | * aggregation 聚合函数 154 | * field 聚合字段(除了count之外,都必传) 155 | * } 156 | */ 157 | router.post('/head', 158 | async (ctx) => { 159 | const { body } = ctx.request; 160 | // 聚合动作 161 | const { aggregation, field } = body; 162 | delete body.aggregation; 163 | delete body.field; 164 | const { tableName, where } = getTableAndWhere(body); 165 | if(!validateTableName(ctx, tableName)) { 166 | return; 167 | } 168 | if(aggregation != "count" && !field) { 169 | ctx.body = { code: 1, msg: '请传输聚合字段' }; 170 | return; 171 | } 172 | const data = await services.common.aggregation({ tableName, where, func: aggregation, field }); 173 | ctx.body = { code: 0, data }; 174 | }); 175 | 176 | /** 177 | * 新增 178 | * { 179 | * TableName : { 新增的字段 } 180 | * } 181 | */ 182 | router.post('/post', 183 | async (ctx) => { 184 | const { body } = ctx.request; 185 | const { tableName, where } = getTableAndWhere(body); 186 | if(!validateTableName(ctx, tableName)) { 187 | return; 188 | } 189 | if(Object.keys(where).length === 0) { 190 | ctx.body = { code: 1, msg: '请传输新增字段' }; 191 | return; 192 | } 193 | const data = await services.common.create(tableName, where); 194 | ctx.body = { code: 0, data }; 195 | }); 196 | 197 | /** 198 | * 修改 199 | * { 200 | * TableName : { 查询条件 } 201 | * set : { 更新的字段 } 202 | * } 203 | */ 204 | router.post('/put', 205 | async (ctx) => { 206 | const { body } = ctx.request; 207 | // 字段更新 208 | const { set } = body; 209 | delete body.set; 210 | if(!set) { 211 | ctx.body = { code: 1, msg: '请传输更新字段' }; 212 | return; 213 | } 214 | if(Object.keys(set).length === 0) { 215 | ctx.body = { code: 1, msg: '请传输更新字段' }; 216 | return; 217 | } 218 | const { tableName, where } = getTableAndWhere(body); 219 | if(!validateTableName(ctx, tableName)) { 220 | return; 221 | } 222 | const affected = await services.common.update(tableName, set, where); 223 | ctx.body = { code: (affected > 0 ? 0 : 1) }; 224 | }); 225 | 226 | /** 227 | * 提取路径中的地址 228 | */ 229 | function extractPath(url) { 230 | /** 231 | * 只提取路径信息,去除协议、域名和端口 232 | * 加 {2,4} 是为了解决 https://// 无法匹配的问题 233 | */ 234 | return url ? url.split('?')[0].replace(/(\w*):?\/{2,4}([^/:]+)(:\d*)?/, '').substring(1).trim() : null; 235 | } 236 | 237 | /** 238 | * 监控信息搜集 239 | */ 240 | router.post('/ma.gif', async (ctx) => { 241 | const { m, r } = JSON.parse(ctx.request.body); 242 | const params = JSON.parse(m); 243 | let { subdir = '', token, category, data, identity, referer } = params; 244 | let { type, status, url } = data; 245 | const projectSubdir = subdir; // 子目录 提前缓存 246 | // 对 Promise 的错误做特殊处理 247 | if (type === 'promise') { 248 | status = data.desc.status; 249 | url = data.desc.url; 250 | } else if (data.desc && data.desc.url) { 251 | // React Vue Runtime Crash Image 等错误也要提取 url 252 | url = data.desc.url; 253 | } 254 | const message = JSON.stringify(data); 255 | // MD5加密 256 | const key = crypto.createHash('md5').update(identity + token + category + message).digest('hex'); 257 | // 读取当前最新的 Source Map 文件 258 | let source = ''; 259 | const dir = `${process.env.NODE_ENV}-${token}`; // 存放 map 文件的目录 260 | const absDir = path.resolve(__dirname, config.get('sourceMapPath'), dir); 261 | logger.trace('sourceMapPath', absDir); 262 | // 目录存在,并且当前是错误类型的日志 263 | if (fs.existsSync(absDir) && category === 'error') { 264 | let readDir = fs.readdirSync(absDir); 265 | // 如果是 chunk-vendors 的错误,需做特殊处理 266 | if (type == 'runtime' && data.desc.prompt.indexOf('chunk-vendors') >= 0) { 267 | subdir = 'chunk-vendors'; 268 | readDir = readDir.filter(name => name.split('.')[0] === subdir); 269 | subdir += '.'; 270 | } else if (subdir) { // 当传递的subdir非空时,需要过滤进行过滤 271 | // map 文件第一个点号之前的前缀必须与 subdir 相同 272 | readDir = readDir.filter(name => name.split('.')[0] === subdir); 273 | subdir += '.'; // 用于后续的去除前缀 274 | } 275 | readDir = readDir.sort((a, b) => b.replace(subdir, '').split('.')[0] - a.replace(subdir, '').split('.')[0]); 276 | source = readDir.length > 0 ? readDir[0] : ''; 277 | } 278 | 279 | // UA信息解析 280 | // const ua = JSON.stringify(uaParser(ctx.headers['user-agent'])); 281 | const ua = ctx.headers['user-agent']; 282 | const monitor = { 283 | project: token, 284 | project_subdir: projectSubdir, 285 | category, 286 | message, 287 | key, 288 | ua, 289 | source, 290 | identity, 291 | referer, 292 | message_type: type && type.toLowerCase(), 293 | message_status: status, 294 | message_path: extractPath(url), // 提取路径 295 | day: moment().format('YYYYMMDD'), 296 | hour: moment().format('HH'), 297 | minute: moment().format('mm'), 298 | ctime: new Date(), // 当前日期 299 | }; 300 | if (r) { 301 | monitor.record = r; 302 | } 303 | const taskName = 'handleMonitor';// + Math.ceil(randomNum(0, 10) / 3); 304 | // 新增队列任务 生存时间60秒 305 | const job = queue.create(taskName, { monitor }).ttl(60000) 306 | .removeOnComplete(true); 307 | // job.on('failed', function(errorMessage){ 308 | // logger.trace(`${taskName} job faild`, errorMessage); 309 | // }); 310 | job.save((err) => { 311 | if (err) { 312 | logger.trace(`${taskName} job failed!`); 313 | } 314 | logger.trace(`${taskName} job saved!`, job.id); 315 | }); 316 | 317 | // queue.on('error', function( err ) { 318 | // logger.trace('handleMonitor queue error', err); 319 | // }); 320 | 321 | const blankUrl = path.resolve(__dirname, '../public/blank.gif'); 322 | ctx.body = fs.readFileSync(blankUrl); // 空白gif图 323 | }); 324 | 325 | /** 326 | * 性能信息搜集 327 | */ 328 | router.post('/pe.gif', async (ctx) => { 329 | let params; 330 | try { 331 | params = JSON.parse(ctx.request.body); 332 | } catch (e) { 333 | params = null; 334 | } 335 | if (!params) { 336 | ctx.body = {}; 337 | return; 338 | } 339 | // UA信息解析 340 | // const ua = JSON.stringify(uaParser(ctx.headers['user-agent'])); 341 | const ua = ctx.headers['user-agent']; 342 | const performance = { 343 | project: params.pkey, 344 | load: params.loadTime, 345 | ready: params.domReadyTime, 346 | paint: params.firstPaint, 347 | screen: params.firstScreen, 348 | identity: params.identity, 349 | ua, 350 | day: moment().format('YYYYMMDD'), 351 | hour: moment().format('HH'), 352 | minute: moment().format('mm'), 353 | referer: params.referer, // 来源地址 354 | referer_path: extractPath(params.referer), // 来源地址路径 355 | timing: params.timing ? JSON.stringify(params.timing) : null, 356 | resource: params.resource ? JSON.stringify(params.resource) : null, 357 | }; 358 | delete params.pkey; 359 | delete params.loadTime; 360 | delete params.domReadyTime; 361 | delete params.firstPaint; 362 | delete params.firstScreen; 363 | delete params.identity; 364 | delete params.referer; 365 | delete params.timing; 366 | delete params.resource; 367 | performance.measure = JSON.stringify(params); 368 | // 新增队列任务 生存时间60秒 369 | const job = queue.create('handlePerformance', { performance }).ttl(60000) 370 | .removeOnComplete(true).save((err) => { 371 | if (err) { 372 | logger.trace('handlePerformance job failed!'); 373 | } 374 | logger.trace('handlePerformance job saved!', job.id); 375 | }); 376 | job.on('failed', (errorMessage) => { 377 | logger.trace('handlePerformance job faild', errorMessage); 378 | }); 379 | // queue.on('error', function( err ) { 380 | // // console.log('handlePerformance queue error', err); 381 | // }); 382 | 383 | // // console.log(performance); 384 | // await services.common.createPerformance(); 385 | ctx.body = {}; 386 | }); 387 | 388 | /** 389 | * 删除过期的 Source Map 日志文件 390 | */ 391 | router.get('/smap/del', async (ctx) => { 392 | const { day = 21 } = ctx.query; 393 | // 删除21天前的文件 394 | const threeWeek = ~~moment().add(-day, 'days').startOf('day').format('YYYYMMDDHHmm'); 395 | // 删除文件 需要调用web-api的接口 396 | const mapPath = path.resolve(__dirname, config.get('sourceMapPath')); 397 | logger.info(`source map目录:${mapPath}`); 398 | if (!fs.existsSync(mapPath)) { 399 | logger.info('source map目录不存在'); 400 | } 401 | // 遍历项目 402 | MONITOR_PROJECT.forEach((dir) => { 403 | // 指定目录 404 | const currentDir = path.resolve(mapPath, `${process.env.NODE_ENV}-${dir}`); 405 | if (!fs.existsSync(currentDir)) { 406 | return; 407 | } 408 | const readDir = fs.readdirSync(currentDir); 409 | readDir.forEach((name) => { 410 | const num = ~~name.replace(/[^0-9]/ig, ''); 411 | // 删除过期文件 412 | if (num <= threeWeek) { 413 | const filePath = path.resolve(currentDir, name); 414 | fs.unlinkSync(filePath); 415 | } 416 | }); 417 | }); 418 | ctx.body = {}; 419 | }); 420 | } -------------------------------------------------------------------------------- /routers/index.js: -------------------------------------------------------------------------------- 1 | import requireIndex from 'es6-requireindex'; 2 | 3 | export default (router) => { 4 | const dir = requireIndex(__dirname); 5 | Object.keys(dir).forEach((item) => { 6 | dir[item](router); 7 | }); 8 | }; -------------------------------------------------------------------------------- /routers/template.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-01-04 15:08:42 4 | * @LastEditTime: 2023-04-24 17:46:53 5 | * @LastEditors: strick 6 | * @Description: 模板组件示例 7 | * @FilePath: /strick/shin-server/routers/template.js 8 | */ 9 | // import services from '../services/'; 10 | // import middlewares from '../middlewares'; 11 | export default (router) => { 12 | /** 13 | * 创建 14 | */ 15 | router.post('/template/create', async (ctx) => { 16 | ctx.body = { code: 0, msg: "错误原因" }; 17 | }); 18 | 19 | /** 20 | * 处理 21 | */ 22 | router.post('/template/handle', async (ctx) => { 23 | ctx.body = { code: 0 }; 24 | }); 25 | 26 | /** 27 | * 查询 28 | */ 29 | router.get('/template/query', async (ctx) => { 30 | ctx.body = { code: 0, data: [ 31 | { 32 | "id": "123456", 33 | "url": "http://localhost:6060/img/avatar.png", 34 | "name": "freedom" + Math.round(Math.random() * 10), 35 | "status": 0, 36 | "price": 9.8, 37 | "date": "2021-01-05T15:19:30.000Z", 38 | "udate": "2021-01-05T15:19:30.000Z", 39 | "urls": ["http://www.pwstrick.com", "https://www.cnblogs.com/strick"], 40 | "icon": [ 41 | '//www.pwstrick.com/upload/avatar.png', 42 | '//www.pwstrick.com/usr/uploads/2020/02/4250591636.jpg', 43 | ], 44 | "csv": [ 45 | {nick: "justify", uid: "1"}, 46 | {nick: "freedom", uid: "2"} 47 | ], 48 | "file": [ 49 | 'http://localhost:6060/img/avatar.png' 50 | ] 51 | }, { 52 | "id": "234567", 53 | "url": "http://localhost:6060/img/cover.jpg", 54 | "name": "justify" + Math.round(Math.random() * 10), 55 | "status": 1, 56 | "price": 18, 57 | "date": "2021-01-05T15:19:29.000Z", 58 | "udate": "2021-01-05T15:19:30.000Z", 59 | "urls": [], 60 | "icon": [], 61 | "csv": [], 62 | }, { 63 | "id": "345678", 64 | "url": "http://localhost:6060/img/avatar.png", 65 | "name": "盐汽水真好喝", 66 | "status": 2, 67 | "price": 12, 68 | "date": "2021-01-05T15:17:52.000Z", 69 | "udate": "2021-01-05T15:19:30.000Z", 70 | "urls": [], 71 | "icon": [], 72 | "csv": [], 73 | }, 74 | { 75 | "id": "4", 76 | "url": "http://localhost:6060/img/cover.jpg", 77 | "name": "jane", 78 | "status": 2, 79 | "price": 12, 80 | "date": "2021-01-05T15:17:52.000Z", 81 | "udate": "2021-01-05T15:19:30.000Z", 82 | "urls": [], 83 | "icon": [], 84 | "csv": [], 85 | }, { 86 | "id": "5", 87 | "url": "http://localhost:6060/img/avatar.png", 88 | "name": "小靖轩", 89 | "status": 2, 90 | "price": 12, 91 | "date": "2021-01-05T15:17:52.000Z", 92 | "udate": "2021-01-05T15:19:30.000Z", 93 | "urls": [], 94 | "icon": [], 95 | "csv": [], 96 | }, { 97 | "id": "6", 98 | "url": "http://localhost:6060/img/cover.jpg", 99 | "name": "凯文", 100 | "status": 2, 101 | "price": 12, 102 | "date": "2021-01-05T15:17:52.000Z", 103 | "udate": "2021-01-05T15:19:30.000Z", 104 | "urls": [], 105 | "icon": [], 106 | "csv": [], 107 | }, { 108 | "id": "7", 109 | "url": "http://localhost:6060/img/avatar.png", 110 | "name": "超级飞侠", 111 | "status": 2, 112 | "price": 12, 113 | "date": "2021-01-05T15:17:52.000Z", 114 | "udate": "2021-01-05T15:19:30.000Z", 115 | "urls": [], 116 | "icon": [], 117 | "csv": [], 118 | }, { 119 | "id": "8", 120 | "url": "http://localhost:6060/img/cover.jpg", 121 | "name": "乐迪", 122 | "status": 2, 123 | "price": 12, 124 | "date": "2021-01-05T15:17:52.000Z", 125 | "udate": "2021-01-05T15:19:30.000Z", 126 | "urls": [], 127 | "icon": [], 128 | "csv": [], 129 | }, { 130 | "id": "9", 131 | "url": "http://localhost:6060/img/avatar.png", 132 | "name": "小爱", 133 | "status": 2, 134 | "price": 12, 135 | "date": "2021-01-05T15:17:52.000Z", 136 | "udate": "2021-01-05T15:19:30.000Z", 137 | "urls": [], 138 | "icon": [], 139 | "csv": [], 140 | } 141 | ], count: Math.round(Math.random() * 100) }; 142 | }); 143 | 144 | } -------------------------------------------------------------------------------- /routers/tool.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2020-12-22 14:54:23 4 | * @LastEditTime: 2023-04-24 17:47:12 5 | * @Description: 业务工具接口 6 | * @FilePath: /strick/shin-server/routers/tool.js 7 | */ 8 | import crypto from "crypto"; 9 | import { string10to62 } from "../utils"; 10 | import services from '../services/'; 11 | import middlewares from '../middlewares'; 12 | 13 | const murmurhash = require('../utils/murmurhash'); 14 | 15 | export default (router) => { 16 | /** 17 | * 创建和编辑通用配置 18 | */ 19 | router.post( 20 | "/tool/config/create", 21 | middlewares.checkAuth("backend.tool.config"), 22 | async (ctx) => { 23 | const { id, title, content } = ctx.request.body; 24 | let result; 25 | //MD5加密 26 | const key = crypto.createHash("md5").update(title).digest("hex"); 27 | const keyData = await services.tool.getOneConfig({ key }); 28 | //编辑 29 | if (id) { 30 | const data = await services.tool.getOneConfig({ id }); 31 | //当根据key可以找到数据,并且本次修改了 title,则报错 32 | if (keyData && data.title != title) { 33 | ctx.body = { code: 1, msg: "标题已存在" }; 34 | return; 35 | } 36 | result = await services.tool.editConfig({ id, title, content }); 37 | ctx.body = { code: 0 }; 38 | return; 39 | } 40 | 41 | //当根据key可以找到数据,则报错 42 | if (keyData) { 43 | ctx.body = { code: 1, msg: "标题已存在" }; 44 | return; 45 | } 46 | //创建 47 | result = await services.tool.createConfig({ title, content, key }); 48 | ctx.body = { code: 0 }; 49 | } 50 | ); 51 | 52 | /** 53 | * 创建和编辑通用配置 54 | */ 55 | router.get( 56 | "/tool/config/query", 57 | middlewares.checkAuth("backend.tool.config"), 58 | async (ctx) => { 59 | const { rows, count } = await services.tool.getConfigList(); 60 | ctx.body = { code: 0, data: rows, count }; 61 | } 62 | ); 63 | 64 | /** 65 | * 删除通用配置 66 | */ 67 | router.post( 68 | "/tool/config/del", 69 | middlewares.checkAuth("backend.tool.config"), 70 | async (ctx) => { 71 | const { id } = ctx.request.body; 72 | await services.tool.delConfig({ id }); 73 | ctx.body = { code: 0 }; 74 | } 75 | ); 76 | 77 | /** 78 | * 创建和修改短链 79 | */ 80 | router.post('/tool/short/create', 81 | middlewares.checkAuth("backend.tool.shortChain"), 82 | async (ctx) => { 83 | const { url, id } = ctx.request.body; 84 | //存在ID 85 | if(id) { //修改 86 | let result = await services.tool.getOneShortChain({ id }); 87 | await services.tool.updateShortChain({ 88 | id, 89 | url, 90 | short: result.short 91 | }); 92 | ctx.body = { code: 0 }; 93 | return; 94 | } 95 | //murmurhash算法 96 | const short = string10to62(murmurhash.v3(url)); 97 | //根据 key 查询短链数据 98 | let result = await services.tool.getOneShortChain({ short }); 99 | if(result) { 100 | ctx.body = { code: 1, msg: "短链已存在" }; 101 | return; 102 | } 103 | //创建 104 | result = await services.tool.createShortChain({ 105 | url, 106 | short, 107 | }); 108 | ctx.body = { code: 0, data: result }; 109 | }); 110 | 111 | /** 112 | * 短链查询 113 | */ 114 | router.get('/tool/short/query', 115 | middlewares.checkAuth("backend.tool.shortChain"), 116 | async (ctx) => { 117 | const { curPage = 1, short, url } = ctx.query; 118 | const { rows, count } = await services.tool.getShortChainList({ curPage, short, url}); 119 | ctx.body = { code:0, data: rows, count }; 120 | }); 121 | 122 | /** 123 | * 短链删除 124 | */ 125 | router.post('/tool/short/del', 126 | middlewares.checkAuth("backend.tool.shortChain"), 127 | async (ctx) => { 128 | const { id } = ctx.request.body; 129 | //读取数据 130 | const data = await services.tool.getOneShortChain({ id }); 131 | const result = await services.tool.delShortChain(data); 132 | ctx.body = result > 0 ? { code: 0 } : { code: 1 }; 133 | }); 134 | }; 135 | -------------------------------------------------------------------------------- /routers/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 管理后台用户 3 | * 用户的账号和角色管理 4 | */ 5 | import crypto from 'crypto'; 6 | import jwt from 'jsonwebtoken'; 7 | import _ from 'lodash'; 8 | import moment from 'moment'; 9 | import config from 'config'; 10 | import { randomString } from '../utils'; 11 | import redis from '../db/redis'; 12 | import services from '../services/'; 13 | import middlewares from '../middlewares'; 14 | 15 | //5分钟内登录次数超过此值账号将被禁用 16 | const loginCountLimit = 3; 17 | 18 | /** 19 | * 加密明文密码 20 | * @param {string} userName 用户名 21 | * @param {string} password 密码 22 | * @param {string} salt 加密秘钥,每个账户都不同 23 | */ 24 | const cryptoPassword = (userName, password, salt) => { 25 | const str = userName + password + salt; 26 | return crypto.createHash('md5').update(str).digest('hex'); 27 | }; 28 | 29 | export default (router) => { 30 | // 查询当前用户信息 31 | router.get( 32 | '/user', 33 | async (ctx) => { 34 | const { realName } = ctx.state.user; 35 | ctx.body = { 36 | username: realName, 37 | }; 38 | }, 39 | ); 40 | 41 | /** 42 | * 获取用户角色列表 43 | */ 44 | router.get( 45 | '/user/role/list', 46 | middlewares.checkAuth('backend.user.role.list'), 47 | async (ctx) => { 48 | const data = ctx.query; 49 | const cursor = parseInt(data.cursor || 1); 50 | const limit = parseInt(data.limit || 10); 51 | const role = data.role; 52 | const res = await services.backendUserRole.getList(role, cursor, limit); 53 | ctx.body = { 54 | list: res.list, 55 | page: { 56 | cursor: Number(cursor), 57 | total: res.count, 58 | }, 59 | }; 60 | }, 61 | ); 62 | 63 | /** 64 | * 添加用户角色 65 | */ 66 | router.post( 67 | '/user/role', 68 | middlewares.checkAuth('backend.user.role.list'), 69 | async (ctx) => { 70 | ctx.checkBody('roleName').notEmpty('角色名称不能为空'); 71 | ctx.checkBody('roleDesc').notEmpty('角色描述不能为空'); 72 | ctx.checkBody('rolePermission').notEmpty('角色权限不能为空'); 73 | if (ctx.errors) { 74 | ctx.status = 400; 75 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 76 | } else { 77 | const { roleName, roleDesc, rolePermission } = ctx.request.body; 78 | const res = await services.backendUserRole.add(roleName, roleDesc, rolePermission); 79 | ctx.body = res; 80 | } 81 | }, 82 | ); 83 | 84 | /** 85 | * 修改用户角色信息 86 | */ 87 | router.put( 88 | '/user/role', 89 | middlewares.checkAuth('backend.user.role.list'), 90 | async (ctx) => { 91 | ctx.checkBody('roleId').notEmpty('角色id不能为空'); 92 | if (ctx.errors) { 93 | ctx.status = 400; 94 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 95 | } else { 96 | const data = ctx.request.body; 97 | const roleId = data.roleId; 98 | delete data.roleId; 99 | await services.backendUserRole.update(roleId, data); 100 | ctx.body = Object.assign({ roleId }, data); 101 | } 102 | }, 103 | ); 104 | 105 | /** 106 | * 禁用角色 107 | */ 108 | router.post( 109 | '/user/role/disable', 110 | middlewares.checkAuth('backend.user.role.list'), 111 | async (ctx) => { 112 | ctx.checkBody('roleId').notEmpty('角色id不能为空'); 113 | if (ctx.errors) { 114 | ctx.status = 400; 115 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 116 | } else { 117 | const { roleId } = ctx.request.body; 118 | await services.backendUserRole.update(roleId, { status: 0 }); 119 | ctx.body = { roleId, status: 0 }; 120 | } 121 | }, 122 | ); 123 | 124 | /** 125 | * 启用角色 126 | */ 127 | router.post( 128 | '/user/role/enable', 129 | middlewares.checkAuth('backend.user.role.list'), 130 | async (ctx) => { 131 | ctx.checkBody('roleId').notEmpty('角色id不能为空'); 132 | if (ctx.errors) { 133 | ctx.status = 400; 134 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 135 | } else { 136 | const { roleId } = ctx.request.body; 137 | await services.backendUserRole.update(roleId, { status: 1 }); 138 | ctx.body = { roleId, status: 1 }; 139 | } 140 | }, 141 | ); 142 | 143 | /** 144 | * 删除用户角色 145 | */ 146 | router.del( 147 | '/user/role', 148 | middlewares.checkAuth('backend.user.role.list'), 149 | async (ctx) => { 150 | ctx.checkBody('roleId').notEmpty('角色id不能为空'); 151 | if (ctx.errors) { 152 | ctx.status = 400; 153 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 154 | return; 155 | } 156 | const { roleId } = ctx.request.body; 157 | await services.backendUserRole.remove(roleId); 158 | ctx.body = { roleId }; 159 | }, 160 | ); 161 | 162 | /** 163 | * 注册 164 | */ 165 | router.post( 166 | '/user', 167 | async (ctx) => { 168 | ctx.checkBody('userName').isEmail('请填写正确的邮箱'); 169 | ctx.checkBody('password').notEmpty('密码不能为空').len(6); 170 | ctx.checkBody('realName').notEmpty('真实姓名不能为空'); 171 | ctx.checkBody('cellphone').isMobilePhone('请填写正确的手机号码', ['zh-CN']); 172 | ctx.checkBody('roles').notEmpty('用户角色不能为空'); 173 | if (ctx.errors) { 174 | ctx.status = 400; 175 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 176 | return; 177 | } 178 | const { userName, password, realName, cellphone, roles } = ctx.request.body; 179 | // 检查邮箱是否已注册 180 | const emailCheck = await services.backendUserAccount.checkEmailExist(userName); 181 | if (emailCheck) { 182 | ctx.status = 400; 183 | ctx.body = { error: '邮箱已注册' }; 184 | return; 185 | } 186 | // 检查手机是否已注册 187 | const cellphoneCheck = await services.backendUserAccount.checkCelphoneExist(cellphone); 188 | if (cellphoneCheck) { 189 | ctx.status = 400; 190 | ctx.body = { error: '手机号已注册' }; 191 | return; 192 | } 193 | const data = ctx.request.body; 194 | const salt = randomString(32); 195 | data.salt = salt; 196 | data.password = cryptoPassword(userName, password, salt); 197 | const res = await services.backendUserAccount.register(data); 198 | ctx.body = { 199 | _id: res._id, 200 | userName, 201 | realName, 202 | status: 1, 203 | roles, 204 | cellphone, 205 | }; 206 | }, 207 | ); 208 | 209 | /** 210 | * 登录 211 | */ 212 | router.post( 213 | '/user/login', 214 | async (ctx) => { 215 | // 验证参数 216 | ctx.checkBody('__userName__').notEmpty('用户名不能为空'); 217 | ctx.checkBody('__password__').notEmpty('密码不能为空'); 218 | if (ctx.errors) { 219 | ctx.body = { code: 400, msg: ctx.errors }; 220 | return; 221 | } 222 | 223 | // 获取用户信息 224 | const { __userName__, __password__ } = ctx.request.body; 225 | const userName = __userName__; 226 | const password = __password__; 227 | const res = await services.backendUserAccount.getInfoByUserName(userName); 228 | 229 | if (!res) { 230 | ctx.body = { code: 400, msg: '用户名不存在' }; 231 | return; 232 | } 233 | 234 | if (res.status === 0) { 235 | ctx.body = { code: 400, msg: '您的账号已被禁用' }; 236 | return; 237 | } 238 | 239 | // 检查登录计数 240 | const loginCount = await services.backendUserAccount.getLoginCount(userName); 241 | if (loginCount > loginCountLimit) { 242 | await services.backendUserAccount.updateStatus(res._id, 0); 243 | await services.backendUserAccount.resetLoginCount(userName); 244 | ctx.body = { code: 400, msg: '登陆错误次数超过限制,请联系管理员解封' }; 245 | return; 246 | } 247 | if (loginCount === null) { 248 | await services.backendUserAccount.setLoginCount(userName); 249 | } 250 | // 登录计数 +1 251 | await services.backendUserAccount.increaseLoginCount(userName); 252 | 253 | // 验证密码 254 | const pwd = cryptoPassword(userName, password, res.salt); 255 | if (pwd === res.password) { 256 | // 获取当前账号的角色权限 257 | const promises = res.roles.map(item => services.backendUserRole.getInfoById(item)); 258 | let roles = await Promise.all(promises); 259 | roles = _.filter(roles, role => role); 260 | // 检查角色状态 261 | let disabledPermissions = []; 262 | roles.forEach((role) => { 263 | if (role.status === 0) { 264 | disabledPermissions = disabledPermissions.concat(role.rolePermission); 265 | } 266 | }); 267 | const authorities = _.chain(roles.map(item => item.rolePermission)) 268 | .flatten(true) 269 | .filter(role => !disabledPermissions.includes(role)) 270 | .uniq() 271 | .value(); 272 | 273 | // 生成token 274 | const token = jwt.sign({ 275 | id: res._id, 276 | // authorities, 277 | userName: res.userName, 278 | realName: res.realName, 279 | }, config.get('jwtSecret'), { 280 | expiresIn: '12h', 281 | }); 282 | 283 | //测试环境 将token放到Redis中 284 | const nodeEnv = process.env.NODE_ENV; 285 | nodeEnv === "development" && (await redis.aws.set(`backend:user:account:token:development`, token)); 286 | 287 | // 权限放入redis存储 288 | await redis.aws.set(`backend:user:account:authorities:${res._id}`, authorities.toString()); 289 | 290 | // 密码过期时间通知 291 | let expireDays = 90; 292 | // const passwordExpireTime = res.passwordExpireTime; 293 | // if (passwordExpireTime) { 294 | // if (moment().isBefore(moment(passwordExpireTime), 'd')) { 295 | // const days = moment(passwordExpireTime).diff(moment(), 'd'); 296 | // expireDays = days > 0 ? days : 0; 297 | // } else { 298 | // ctx.body = { code: 400, msg: '密码过期' }; 299 | // return; 300 | // } 301 | // } else { 302 | // await services.backendUserAccount.updateInfo(res._id, { passwordExpireTime: moment().add(90, 'd').endOf('d') }); 303 | // } 304 | ctx.body = { code:0, token, authorities, nodeEnv, expireDays }; 305 | } else { 306 | ctx.body = { code: 400, msg: '密码错误' }; 307 | } 308 | }, 309 | ); 310 | 311 | /** 312 | * 注销 313 | */ 314 | router.post( 315 | '/user/logout', 316 | async (ctx) => { 317 | const { id, realName } = ctx.state.user; 318 | // 删除权限redis 319 | await redis.aws.del(`backend:user:account:authorities:${id}`); 320 | ctx.body = { 321 | code: 0, 322 | }; 323 | }, 324 | ); 325 | 326 | /** 327 | * 获取用户列表 328 | * 通过角色id获取角色名称 329 | */ 330 | router.get( 331 | '/user/list', 332 | middlewares.checkAuth('backend.user.account.list'), 333 | async (ctx) => { 334 | const data = ctx.query; 335 | const curPage = data.curPage || 1; 336 | const limit = data.limit || 10; 337 | const keywords = data.keywords; 338 | const roleId = data.roleId; 339 | const res = await services.backendUserAccount.getList(curPage, limit, keywords, roleId); 340 | ctx.body = { 341 | data: res.list, 342 | count: res.count, 343 | }; 344 | }, 345 | ); 346 | 347 | /** 348 | * 查询指定用户详情 349 | */ 350 | router.get( 351 | '/user/detail/:id', 352 | middlewares.checkAuth('backend.user.account.list'), 353 | async (ctx) => { 354 | const userId = ctx.params.id; 355 | const userData = await services.backendUserAccount.getDetail(userId); 356 | ctx.body = userData; 357 | }, 358 | ); 359 | 360 | /** 361 | * 更新用户信息 362 | * 不包括密码修改 363 | */ 364 | router.put( 365 | '/user', 366 | middlewares.checkAuth('backend.user.account'), 367 | async (ctx) => { 368 | ctx.checkBody('id').notEmpty('用户id不能为空'); 369 | ctx.checkBody('realName').notEmpty('真实姓名不能为空'); 370 | ctx.checkBody('cellphone').isMobilePhone('请填写正确的手机号码', ['zh-CN']); 371 | ctx.checkBody('roles').notEmpty('用户角色不能为空'); 372 | if (ctx.errors) { 373 | ctx.status = 400; 374 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 375 | return; 376 | } 377 | const data = ctx.request.body; 378 | const { id, userName, cellphone } = data; 379 | // 检查邮箱是否重复 380 | const emailCheck = await services.backendUserAccount.checkEmailExist(userName); 381 | if (emailCheck && (emailCheck._id.toString() !== id)) { 382 | ctx.status = 400; 383 | ctx.body = { error: '邮箱已注册' }; 384 | return; 385 | } 386 | // 检查手机是否已注册 387 | const cellphoneCheck = await services.backendUserAccount.checkCelphoneExist(cellphone); 388 | if (cellphoneCheck && (cellphoneCheck._id.toString() !== id)) { 389 | ctx.status = 400; 390 | ctx.body = { error: '手机号已注册' }; 391 | return; 392 | } 393 | delete data.id; 394 | await services.backendUserAccount.updateInfo(id, data); 395 | ctx.body = Object.assign({ id }, data); 396 | }, 397 | ); 398 | 399 | /** 400 | * 启用用户 401 | */ 402 | router.post( 403 | '/user/enable', 404 | middlewares.checkAuth('backend.user.account'), 405 | async (ctx) => { 406 | ctx.checkBody('id').notEmpty('id不能为空'); 407 | if (ctx.errors) { 408 | ctx.status = 400; 409 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 410 | return; 411 | } 412 | const id = ctx.request.body.id; 413 | await services.backendUserAccount.updateStatus(id, 1); 414 | ctx.body = { 415 | id, 416 | status: 1, 417 | }; 418 | }, 419 | ); 420 | 421 | /** 422 | * 禁用用户 423 | */ 424 | router.post( 425 | '/user/disable', 426 | middlewares.checkAuth('backend.user.account'), 427 | async (ctx) => { 428 | ctx.checkBody('id').notEmpty('id不能为空'); 429 | if (ctx.errors) { 430 | ctx.status = 400; 431 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 432 | return; 433 | } 434 | const id = ctx.request.body.id; 435 | await services.backendUserAccount.updateStatus(id, 0); 436 | ctx.body = { 437 | id, 438 | status: 0, 439 | }; 440 | }, 441 | ); 442 | 443 | /** 444 | * 修改密码 445 | */ 446 | router.put( 447 | '/user/password', 448 | async (ctx) => { 449 | ctx.checkBody('password').notEmpty('密码不能为空'); 450 | if (ctx.errors) { 451 | ctx.body = { code: 400, msg: _.values(ctx.errors[0])[0] }; 452 | return; 453 | } 454 | const { password } = ctx.request.body; 455 | let { id, userName } = ctx.request.body; 456 | if (!id || !userName) { 457 | id = ctx.state.user.id; 458 | userName = ctx.state.user.userName; 459 | } 460 | // 增加密码是否重复判断 461 | const res = await services.backendUserAccount.getInfoByUserName(userName); 462 | if (!res) { 463 | ctx.body = { code: 400, msg: '用户名不存在' }; 464 | return; 465 | } 466 | 467 | if (res.status === 0) { 468 | ctx.body = { code: 400, msg: '您的账号已被禁用' }; 469 | return; 470 | } 471 | // 验证密码 472 | const pwdRes = cryptoPassword(userName, password, res.salt); 473 | if (pwdRes === res.password) { 474 | ctx.body = { code: 400, msg: '新密码与原密码相同' }; 475 | return; 476 | } 477 | const salt = randomString(32); 478 | const pwd = cryptoPassword(userName, password, salt); 479 | // 修改密码后 有效期重置设置90天 480 | await services.backendUserAccount.updateInfo(id, { salt, password: pwd, passwordExpireTime: moment().add(90, 'd').endOf('d') }); 481 | ctx.body = { code: 0 }; 482 | }, 483 | ); 484 | 485 | /** 486 | * 删除用户 487 | */ 488 | router.del( 489 | '/user', 490 | middlewares.checkAuth('backend.user.account'), 491 | async (ctx) => { 492 | ctx.checkBody('userId').notEmpty('id不能为空'); 493 | if (ctx.errors) { 494 | ctx.status = 400; 495 | ctx.body = { error: _.values(ctx.errors[0])[0] }; 496 | return; 497 | } 498 | const id = ctx.request.body.userId; 499 | await services.backendUserAccount.delete(id); 500 | ctx.body = { 501 | id, 502 | }; 503 | }, 504 | ); 505 | 506 | /** 507 | * 初始化管理员用户 508 | */ 509 | router.get( 510 | '/user/init', 511 | async (ctx) => { 512 | // 检查当前数据库状态 513 | const users = await services.backendUserAccount.find(); 514 | if (users.length > 0) { 515 | ctx.status = 400; 516 | ctx.body = { error: '已经初始化过了', users }; 517 | return; 518 | } 519 | // 创建管理员角色 520 | const roleName = '超级管理员'; 521 | const roleDesc = '超级管理员'; 522 | const rolePermission = '*'; 523 | // console.log('创建管理员角色...'); 524 | const res = await services.backendUserRole.add(roleName, roleDesc, rolePermission); 525 | const roleId = res._id.toString(); 526 | // console.log(`创建管理员角色成功, roleId = ${roleId}`); 527 | // 创建管理员账号 528 | // console.log('创建管理员账号...'); 529 | const initPassword = 'admin'; //可自定义密码 530 | const userName = 'admin@shin.com'; //可自定义初始化 531 | const salt = randomString(32); 532 | const createData = { 533 | userName, 534 | realName: '超级管理员', 535 | password: cryptoPassword(userName, initPassword, salt), 536 | salt, 537 | roles: [roleId], 538 | }; 539 | await services.backendUserAccount.register(createData); 540 | // console.log(`创建管理员账号成功, accountId = ${account._id}`); 541 | ctx.body = `创建管理员账号成功, 请用 ${userName} : ${initPassword} 登录`; 542 | }, 543 | ); 544 | }; 545 | -------------------------------------------------------------------------------- /routers/webMonitor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-25 15:40:31 4 | * @LastEditTime: 2023-04-24 17:47:37 5 | * @LastEditors: strick 6 | * @Description: 7 | * @FilePath: /strick/shin-server/routers/webMonitor.js 8 | */ 9 | import config from 'config'; 10 | import moment from 'moment'; 11 | import path from 'path'; 12 | import crypto from "crypto"; 13 | const sourceMap = require("source-map"); 14 | const fs = require("fs"); 15 | import { PERFORMANCT_RATE } from "../utils/constant"; 16 | import { formatDate } from '../utils/tools'; 17 | import uaParser from "ua-parser-js"; 18 | import services from '../services/'; 19 | import middlewares from '../middlewares'; 20 | 21 | export default (router) => { 22 | /** 23 | * 读取指定的Source-Map文件 24 | */ 25 | function readSourceMap(filePath) { 26 | let parsedData = null; 27 | try { 28 | parsedData = fs.readFileSync(filePath, 'utf8'); 29 | parsedData = JSON.parse(parsedData); 30 | }catch(e) { 31 | logger.info(`sourceMap:error`); 32 | } 33 | return parsedData; 34 | } 35 | /** 36 | * 对Vue的错误做特殊处理 37 | */ 38 | function handleVue(message) { 39 | const stacks = message.stack.split("\n"); 40 | if(stacks.length <= 1) 41 | return; 42 | let coordinate 43 | if(stacks[0] === message.desc) { 44 | stacks[1] = stacks[1].replace(/\)/g, ""); 45 | coordinate = stacks[1].split(":"); 46 | }else { 47 | stacks[0] = stacks[0].replace(/\)/g, ""); 48 | coordinate = stacks[0].split(":"); 49 | } 50 | if(coordinate.length <= 2) { 51 | return; 52 | } 53 | message.lineno = ~~coordinate[coordinate.length - 2]; 54 | message.colno = ~~coordinate[coordinate.length - 1]; 55 | } 56 | /** 57 | * 处理映射逻辑 58 | */ 59 | async function getSourceMap(row) { 60 | // 拼接映射文件的地址 61 | // const url = config.get("services").webHttpsApi + '/smap/' + process.env.NODE_ENV + '-' + row.project + '/' + row.source; 62 | const filePath = path.resolve(__dirname, config.get("sourceMapPath"), process.env.NODE_ENV + '-' + row.project + '/' + row.source); 63 | logger.info(`filePath:${filePath}`); 64 | // logger.info(`sourceMap:${url}`); 65 | let { message } = row; 66 | message = JSON.parse(message); 67 | // logger.info(`sourceMap:${message}`); 68 | // VUE 错误需要特殊处理 69 | if(message.type === "vue") { 70 | handleVue(message); 71 | row.message = JSON.stringify(message); 72 | } 73 | // 不存在行号或列号 74 | if(!message.lineno || !message.colno) { 75 | return row; 76 | } 77 | logger.info(`sourceMap`); 78 | // 打包后的sourceMap文件 79 | // const rawSourceMap = await httpSourceMap(url); 80 | const rawSourceMap = readSourceMap(filePath); 81 | if(!rawSourceMap) { 82 | return row; 83 | } 84 | const errorPos = { 85 | line: message.lineno, 86 | column: message.colno, 87 | }; 88 | // 过传入打包后的代码位置来查询源代码的位置 89 | const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap); 90 | // 获取出错代码在哪一个源文件及其对应位置 91 | const originalPosition = consumer.originalPositionFor({ 92 | line: errorPos.line, 93 | column: errorPos.column, 94 | }); 95 | // 根据源文件名寻找对应源文件 96 | const sourceIndex = consumer.sources.findIndex( 97 | (item) => item === originalPosition.source 98 | ); 99 | const sourceCode = consumer.sourcesContent[sourceIndex]; 100 | logger.trace('sourceCode', !!sourceCode); 101 | if(sourceCode) { 102 | row.sourceInfo = { 103 | code: sourceCode, 104 | lineno: originalPosition.line, 105 | path: originalPosition.source 106 | }; 107 | // row.sourceCode = sourceCode; 108 | // row.lineno = originalPosition.line; 109 | } 110 | // 销毁,否则会报内存访问超出范围 111 | consumer.destroy(); 112 | return row; 113 | } 114 | function initMonitorQuery(ctx) { 115 | // 图表默认已小时计算 116 | const { id, category, project, start, end, curPage = 1, 117 | pageSize = 10, identity, match=1, chart=2, status, path } = ctx.query; 118 | const other = {}; 119 | const messages = []; 120 | // if(msg) { 121 | // // 全文检索时还需处理特殊字符的转义 122 | // match == 1 ? 123 | // messages.push(`+*${msg.toLocaleLowerCase().replace(/"/g, "")}*`) : 124 | // messages.push(`%${msg}%`); 125 | // } 126 | let messageType; 127 | let currentCategory = category; 128 | //传递类别以及其子类别 129 | if(Array.isArray(category)) { 130 | currentCategory = category[0]; 131 | messageType = category[1]; 132 | } 133 | return { 134 | id, currentCategory, messages, messageType, messageStatus: status, messagePath: path, 135 | project, start, end, curPage, pageSize, identity, match, chart, other 136 | } 137 | } 138 | /** 139 | * 解析代理信息 140 | */ 141 | function parseAgent(item) { 142 | if(item.ua.indexOf("{") == -1) 143 | item.ua = JSON.stringify(uaParser(item.ua)); 144 | } 145 | /** 146 | * 监控日志明细 147 | */ 148 | router.get( 149 | '/monitor/list', 150 | middlewares.checkAuth('backend.monitor.log'), 151 | async (ctx) => { 152 | const { id, currentCategory, project, start, end, 153 | curPage, pageSize, identity, match, other, 154 | messageType, messageStatus, messagePath } = initMonitorQuery(ctx); 155 | const { rows, count } = await services.webMonitor.getMonitorList({ 156 | id, 157 | category: currentCategory, 158 | msg: ctx.query.msg, 159 | messageType, messageStatus, messagePath, 160 | match, 161 | project, 162 | start, 163 | end, 164 | curPage, 165 | pageSize, 166 | identity, 167 | other 168 | }); 169 | // 查找出错误的原始代码 170 | const sourceRows = await Promise.all(rows.map(item => { 171 | // UA转换 172 | parseAgent(item); 173 | //错误类型,有映射文件 174 | if(item.category === 'error' && item.source) { 175 | return getSourceMap(item); 176 | } 177 | return item; 178 | })); 179 | ctx.body = { code: 0, data: sourceRows, count, query: ctx.query }; 180 | }, 181 | ); 182 | /** 183 | * 监控日志明细图表 184 | */ 185 | router.get( 186 | '/monitor/list/chart', 187 | middlewares.checkAuth('backend.monitor.log'), 188 | async (ctx) => { 189 | const { id, currentCategory, project, start, end, 190 | identity, chart, messageType, messageStatus, messagePath } = initMonitorQuery(ctx); 191 | let attribute, days=[]; 192 | switch(+chart) { 193 | case 1: //按天 194 | attribute = "day"; 195 | break; 196 | case 2: //按小时 197 | //以开始时间作为 day 的值 198 | if(start) { 199 | //先计算出两天的相差的天数,只取年月日 200 | let diff = moment(end.split(" ")[0]).diff(moment(start.split(" ")[0]), 'days') 201 | for(let i=0; i<=diff; i++) { 202 | days.push(moment(start).add(i, "days").format("YYYYMMDD")); 203 | } 204 | }else { 205 | //默认处理 206 | days.push(moment().format("YYYYMMDD")); 207 | } 208 | attribute = "hour"; 209 | break; 210 | } 211 | if(days.length === 0) { 212 | const counts = await services.webMonitor.getMonitorListChart({ 213 | id, 214 | category: currentCategory, 215 | msg: ctx.query.msg, 216 | messageType, messageStatus, messagePath, 217 | project, 218 | start, 219 | end, 220 | identity, 221 | attribute 222 | }); 223 | const x = counts.map(value => value.hour); 224 | const y = counts.map(value => value.count); 225 | ctx.body = { code: 0, data: { 226 | x, y 227 | }}; 228 | return; 229 | } 230 | const coor = { 231 | x: [], 232 | y: [] 233 | }; 234 | //遍历 235 | for(let day of days) { 236 | const counts = await services.webMonitor.getMonitorListChart({ 237 | id, 238 | category: currentCategory, 239 | msg: ctx.query.msg, 240 | messageType, messageStatus, messagePath, 241 | project, 242 | start, 243 | end, 244 | identity, 245 | day, 246 | attribute 247 | }); 248 | // console.log(counts) 249 | const substr = day.substr(4); 250 | const x = counts.map((value, index) => (index === 0 ? (substr + value.hour) : value.hour)); 251 | const y = counts.map(value => value.count); 252 | coor.x = coor.x.concat(x); 253 | coor.y = coor.y.concat(y); 254 | } 255 | ctx.body = { code: 0, data: coor}; 256 | }, 257 | ); 258 | 259 | /** 260 | * 日志上下文查询 261 | */ 262 | router.get( 263 | '/monitor/context', 264 | middlewares.checkAuth('backend.monitor.log'), 265 | async (ctx) => { 266 | let { prevId, nextId } = ctx.query; 267 | prevId = +prevId; //类型转换 268 | nextId = +nextId; //类型转换 269 | let rows = []; 270 | if(prevId) { 271 | // 读取前10条 272 | rows = await services.webMonitor.getMonitorContext({ from: prevId, to: prevId+9 }); 273 | }else if(nextId) { 274 | // 读取后10条 275 | rows = await services.webMonitor.getMonitorContext({ from: nextId-9, to: nextId }); 276 | } 277 | ctx.body = { code: 0, data: rows }; 278 | }, 279 | ); 280 | 281 | /** 282 | * 统计日志趋势图 283 | */ 284 | router.get( 285 | '/monitor/chart', 286 | middlewares.checkAuth('backend.monitor.log'), 287 | async (ctx) => { 288 | let { project, start, end } = ctx.query; 289 | //默认展示前面7天的数据 290 | if(!start || !end) { 291 | start = moment().add(-8, 'days').format("YYYYMMDD"); 292 | end = moment().add(-1, 'days').format("YYYYMMDD"); 293 | } 294 | const list = await services.webMonitor.getStatisList({ start, end }); 295 | if(!list) { 296 | ctx.body = { code: 0, data: {}}; 297 | return; 298 | } 299 | //保存趋势数值 300 | const days = []; 301 | const daysErrorHash = {}, 302 | days500ErrorHash = {}, 303 | days502ErrorHash = {}, 304 | days504ErrorHash = {}, 305 | daysAllCountHash = {}; 306 | list.forEach(current => { 307 | const digits = current.date.toString().split("").splice(-4); 308 | const today = digits.slice(0, 2).join("") + '-' + digits.slice(-2).join(""); 309 | days.push(today); //将数字格式化成日期 例如03-18 310 | let statis = JSON.parse(current.statis); //格式化统计数据 311 | statis = caculateFiled(project, statis); 312 | daysErrorHash[today] = statis.errorCount; 313 | days500ErrorHash[today] = statis.error500Count; 314 | days502ErrorHash[today] = statis.error502Count; 315 | days504ErrorHash[today] = statis.error504Count; 316 | daysAllCountHash[today] = statis.allCount || (statis.errorCount + statis.ajaxCount + statis.consoleCount + statis.eventCount + statis.redirectCount); 317 | }); 318 | ctx.body = { code: 0, data: { 319 | daysErrorHash, 320 | days500ErrorHash, 321 | days502ErrorHash, 322 | days504ErrorHash, 323 | daysAllCountHash 324 | }}; 325 | } 326 | ); 327 | 328 | /** 329 | * 按天读取监控日志 330 | */ 331 | router.get( 332 | '/monitor/date', 333 | middlewares.checkAuth('backend.monitor.log'), 334 | async (ctx) => { 335 | let { project, date } = ctx.query; 336 | const row = await services.webMonitor.getOneStatis({ date }); 337 | if(!row) { 338 | return ctx.body = { code: 0, data: {}}; 339 | } 340 | row.statis = JSON.parse(row.statis); 341 | ctx.body = { code: 0, data: row.statis[project]}; 342 | } 343 | ); 344 | 345 | /** 346 | * 计算字段的累加值 347 | */ 348 | function caculateFiled(project, statis) { 349 | if(project) { 350 | statis = statis[project]; //若选择了项目,则展示项目信息 351 | }else { 352 | //否则将各个项目的数值累加 353 | const calc = {}; 354 | Object.keys(statis).forEach(pro => { 355 | Object.keys(statis[pro]).forEach(field => { 356 | // 若不存在就重新赋值 357 | if(!calc[field]) { 358 | calc[field] = statis[pro][field]; 359 | return; 360 | } 361 | // 否则累加 362 | calc[field] += statis[pro][field]; 363 | }); 364 | }); 365 | statis = calc; 366 | } 367 | return statis; 368 | } 369 | /** 370 | * 统计日志信息 371 | */ 372 | router.get( 373 | '/monitor/statistic', 374 | middlewares.checkAuth('backend.monitor.log'), 375 | async (ctx) => { 376 | const { project } = ctx.query; 377 | //昨日的统计信息 378 | const yesterdayDate = moment().add(-1, 'days').format("YYYYMMDD"); //格式化的昨天日期 379 | let yesterdayStatis = await services.webMonitor.getOneStatis({ date: yesterdayDate }); 380 | if(!yesterdayStatis) { 381 | yesterdayStatis = {}; 382 | }else { 383 | yesterdayStatis = JSON.parse(yesterdayStatis.statis); //格式化存储的数据 384 | yesterdayStatis = caculateFiled(project, yesterdayStatis); 385 | } 386 | const { 387 | allCount=0, 388 | errorCount=0, 389 | errorSum=0, 390 | error500Count=0, 391 | error502Count=0, 392 | error504Count=0, 393 | ajaxCount=0, 394 | consoleCount=0, 395 | eventCount=0, 396 | redirectCount=0 397 | } = yesterdayStatis; 398 | //其他日期 399 | const yesterday = moment().add(-1, 'days').startOf('day').format("YYYY-MM-DD HH:mm"); //昨天凌晨 400 | const yesterdayNow = moment().add(-1, 'days').format("YYYY-MM-DD HH:mm"); //昨天现在的时间 401 | const today = moment().startOf('day').format("YYYY-MM-DD HH:mm"); //今天凌晨 402 | const tomorrow = moment().add(1, 'days').startOf('day').format("YYYY-MM-DD HH:mm"); //明天凌晨 403 | const todayFilter = { project, from: today, to: tomorrow }; 404 | const yesterdayFilter = { project, from: yesterday, to: yesterdayNow }; 405 | // const yesterdayAllDayFilter = { project, from: yesterday, to: today }; 406 | 407 | //日志数 408 | const todayCount = await services.webMonitor.statisticCount(todayFilter); 409 | const yesterdayCount = allCount ? allCount : (errorCount + ajaxCount + consoleCount + eventCount + redirectCount); 410 | 411 | //错误数 412 | const yesterdayErrorCount = await services.webMonitor.statisticCount({ category: "error", ...yesterdayFilter }); 413 | const yesterdayErrorSum = await services.webMonitor.statisticSum({ field: "digit", category: "error", ...yesterdayFilter }); 414 | const todayErrorCount = await services.webMonitor.statisticCount({ category: "error", ...todayFilter }); 415 | const todayErrorSum = await services.webMonitor.statisticSum({ field: "digit", category: "error", ...todayFilter }); 416 | const todayErrorCountRate = yesterdayErrorCount ? (todayErrorCount - yesterdayErrorCount) / yesterdayErrorCount * 100 : 0; 417 | const todayErrorSumRate = yesterdayErrorSum ? (todayErrorSum - yesterdayErrorSum) / yesterdayErrorSum * 100 : 0; 418 | 419 | //通信错误数 420 | // const yesterday50XErrorCountFilter = { category: "error", ...yesterdayAllDayFilter }; 421 | const today50XErrorCountFilter = { category: "error", ...todayFilter }; 422 | const message500 = { message_status: 500 }; 423 | const message502 = { message_status: 502 }; 424 | const message504 = { message_status: 504 }; 425 | const yesterday500ErrorCount = error500Count; 426 | const yesterday502ErrorCount = error502Count; 427 | const yesterday504ErrorCount = error504Count; 428 | const today500ErrorCount = await services.webMonitor.statisticCount({ ...today50XErrorCountFilter, other: message500 }); 429 | const today502ErrorCount = await services.webMonitor.statisticCount({ ...today50XErrorCountFilter, other: message502 }); 430 | const today504ErrorCount = await services.webMonitor.statisticCount({ ...today50XErrorCountFilter, other: message504 }); 431 | 432 | //通信、打印、事件、跳转数 433 | const todayAjaxCount = await services.webMonitor.statisticCount({ category: "ajax", ...todayFilter }); 434 | const todayConsoleCount = await services.webMonitor.statisticCount({ category: "console", ...todayFilter }); 435 | const todayEventCount = await services.webMonitor.statisticCount({ category: "event", ...todayFilter }); 436 | const todayRedirectCount = await services.webMonitor.statisticCount({ category: "redirect", ...todayFilter }); 437 | 438 | ctx.body = { code: 0, data: { 439 | todayCount, 440 | yesterdayCount, 441 | todayErrorCount, 442 | todayErrorSum, 443 | todayAjaxCount, 444 | todayConsoleCount, 445 | todayEventCount, 446 | todayRedirectCount, 447 | todayErrorCountRate: todayErrorCountRate.toFixed(2), 448 | todayErrorSumRate: todayErrorSumRate.toFixed(2), 449 | yesterday500ErrorCount, 450 | yesterday502ErrorCount, 451 | yesterday504ErrorCount, 452 | today500ErrorCount, 453 | today502ErrorCount, 454 | today504ErrorCount, 455 | }}; 456 | }, 457 | ); 458 | 459 | 460 | /** 461 | * 创建性能监控项目 462 | */ 463 | router.post( 464 | "/monitor/performance/project/create", 465 | middlewares.checkAuth("backend.monitor.performance.project"), 466 | async (ctx) => { 467 | const { name, id } = ctx.request.body; 468 | if(id) { 469 | const exist = await services.webMonitor.getOnePerformanceProject({ 470 | name, 471 | id: { 472 | $not: id 473 | } 474 | }); 475 | if(exist) { 476 | ctx.body = { code: 1, msg: '项目已存在' }; 477 | return; 478 | } 479 | // 更新操作 480 | await services.webMonitor.updatePerformanceProject(id, { name }); 481 | ctx.body = { code: 0 }; 482 | return; 483 | } 484 | // MD5加密 取前面的16位 485 | const key = crypto.createHash('md5').update(name).digest('hex').substring(0, 16); 486 | const keyData = await services.webMonitor.getOnePerformanceProject({ key }); 487 | // 当根据key可以找到数据,则报错 488 | if (keyData) { 489 | ctx.body = { code: 1, msg: '项目已存在' }; 490 | return; 491 | } 492 | // 创建 493 | await services.webMonitor.createPerformanceProject({ name, key }); 494 | ctx.body = { code: 0 }; 495 | } 496 | ); 497 | 498 | /** 499 | * 性能项目列表 500 | */ 501 | router.get( 502 | "/monitor/performance/project/list", 503 | middlewares.checkAuth("backend.monitor.performance.project"), 504 | async (ctx) => { 505 | const { curPage = 1, pageSize = 10, name } = ctx.query; 506 | const { count, rows } = await services.webMonitor.getPerformanceProjectList({ name, curPage, pageSize}); 507 | ctx.body = { 508 | code: 0, 509 | data: rows.map((item) => { 510 | item.ctime = formatDate(item.ctime); 511 | return item; 512 | }), 513 | count 514 | }; 515 | } 516 | ); 517 | 518 | /** 519 | * 删除性能项目 520 | */ 521 | router.post( 522 | "/monitor/performance/project/del", 523 | middlewares.checkAuth("backend.monitor.performance.project"), 524 | async (ctx) => { 525 | const { id } = ctx.request.body; 526 | await services.webMonitor.delPerformanceProject({ id }); 527 | ctx.body = { code: 0 }; 528 | } 529 | ); 530 | 531 | function getFloorNumber(count) { 532 | return Math.floor(count * PERFORMANCT_RATE); 533 | } 534 | 535 | /** 536 | * 读取性能的历史记录 537 | */ 538 | async function getHistoryList({ project, start, end, hour }) { 539 | const list = []; 540 | for(let day = start; day <= end; day++) { 541 | const statistic = { 542 | x: [], 543 | load: [], 544 | ready: [], 545 | paint: [], 546 | screen: [], 547 | loadZero: 0, //load时间为0的个数 548 | all: 0, //日志总数 549 | day 550 | }; 551 | const history = await services.webMonitor.getOnePerformanceStatis({ date: day, project }); 552 | //不存在历史记录就返回默认信息 553 | if(!history) { 554 | continue; 555 | } 556 | const historyStatis = JSON.parse(history.statis); 557 | let loadList, readyList, paintList, screenList; 558 | //如果未传递小时 559 | if(!hour) { 560 | const { x, load, ready, paint, screen, loadZero, all=0 } = historyStatis.hour; 561 | //补全24小时 562 | for(let i=0; i<=23; i++) { 563 | statistic.x.push(i); 564 | statistic.load.push(0); 565 | statistic.ready.push(0); 566 | statistic.paint.push(0); 567 | statistic.screen.push(0); 568 | } 569 | loadList = await services.webMonitor.getPerformanceListByIds(load); 570 | readyList = await services.webMonitor.getPerformanceListByIds(ready); 571 | paintList = await services.webMonitor.getPerformanceListByIds(paint); 572 | screenList = await services.webMonitor.getPerformanceListByIds(screen); 573 | x.forEach(value => { 574 | //UA转换 575 | parseAgent(loadList[0]); 576 | parseAgent(readyList[0]); 577 | parseAgent(paintList[0]); 578 | parseAgent(screenList[0]); 579 | statistic.load[value] = loadList[0]; 580 | statistic.ready[value] = readyList[0]; 581 | statistic.paint[value] = paintList[0]; 582 | statistic.screen[value] = screenList[0]; 583 | loadList.shift(); 584 | readyList.shift(); 585 | paintList.shift(); 586 | screenList.shift(); 587 | }); 588 | statistic.loadZero = loadZero; 589 | statistic.all = all; 590 | }else { 591 | //包含小时数据 592 | if(historyStatis.minute[hour]) { 593 | //补全60小时 594 | for(let i=0; i<=59; i++) { 595 | statistic.x.push(i); 596 | statistic.load.push(0); 597 | statistic.ready.push(0); 598 | statistic.paint.push(0); 599 | statistic.screen.push(0); 600 | } 601 | const { x, load, ready, paint, screen, loadZero, all=0 } = historyStatis.minute[hour]; 602 | loadList = await services.webMonitor.getPerformanceListByIds(load); 603 | readyList = await services.webMonitor.getPerformanceListByIds(ready); 604 | paintList= await services.webMonitor.getPerformanceListByIds(paint); 605 | screenList = await services.webMonitor.getPerformanceListByIds(screen); 606 | x.forEach(value => { 607 | //UA转换 608 | parseAgent(loadList[0]); 609 | parseAgent(readyList[0]); 610 | parseAgent(paintList[0]); 611 | parseAgent(screenList[0]); 612 | statistic.load[value] = loadList[0]; 613 | statistic.ready[value] = readyList[0]; 614 | statistic.paint[value] = paintList[0]; 615 | statistic.screen[value] = screenList[0]; 616 | loadList.shift(); 617 | readyList.shift(); 618 | paintList.shift(); 619 | screenList.shift(); 620 | }); 621 | statistic.loadZero = loadZero; 622 | statistic.all = all; 623 | } 624 | } 625 | list.push(statistic); 626 | } 627 | return list; 628 | } 629 | /** 630 | * 读取性能的历史记录 631 | */ 632 | async function getTodayList({ project, day, hour }) { 633 | let counts; 634 | const statistic = { 635 | x: [], 636 | load: [], 637 | ready: [], 638 | paint: [], 639 | screen: [], 640 | loadZero: 0, //load时间为0的个数 641 | all: 0, //日志总数 642 | day 643 | }; 644 | statistic.loadZero = await services.webMonitor.statisticPerformanceCount({ 645 | project, day, hour, 646 | other: { 647 | load: 0 648 | } 649 | }); 650 | statistic.all = await services.webMonitor.statisticPerformanceCount({ 651 | project, day, hour, 652 | }); 653 | if(!hour) { 654 | //补全24小时 655 | for(let i=0; i<=23; i++) { 656 | statistic.x.push(i); 657 | statistic.load.push(0); 658 | statistic.ready.push(0); 659 | statistic.paint.push(0); 660 | statistic.screen.push(0); 661 | } 662 | counts = await services.webMonitor.statisticPerformanceCount({ 663 | project, 664 | day, 665 | group: 'hour', 666 | attributes: ['hour'] 667 | }); 668 | counts = counts.map(value => { 669 | return { 670 | ...value, 671 | offset: getFloorNumber(value.count) 672 | } 673 | }); 674 | // console.log(counts) 675 | //逐条查询排序后排在95%位置的记录 676 | for(let i=0; i { 749 | let { project, start, end, hour } = ctx.query; 750 | //今日的统计信息 751 | const today = moment().format("YYYYMMDD"); //格式化的日期 752 | start = +start; 753 | end = +end; 754 | //结束时间非今天,就查找历史记录 755 | if(today != end) { 756 | const data = await getHistoryList({ project, start, end, hour }); 757 | ctx.body = { code: 0, data: data}; 758 | return; 759 | } 760 | let statistic = []; 761 | //开始时间和结束时间不是同一天 762 | if(start != end) { 763 | statistic = await getHistoryList({ project, start, end: (end-1), hour }); 764 | } 765 | //结束时间是今天,就计算今天的统计信息 766 | const current = await getTodayList({ project, day: end, hour }); 767 | statistic.push(current); 768 | ctx.body = { code: 0, data: statistic}; 769 | } 770 | ); 771 | /** 772 | * 读取一条性能日志 773 | */ 774 | router.get( 775 | '/monitor/performance/get', 776 | middlewares.checkAuth('backend.monitor.performance.dashboard'), 777 | async (ctx) => { 778 | const { 779 | id, 780 | } = ctx.query; 781 | // 结束时间是今天,就计算今天的统计信息 782 | const row = await services.webMonitor.getOnePerformance({ id }); 783 | ctx.body = { code: 0, data: row }; 784 | }, 785 | ); 786 | /** 787 | * 读取性能时序 788 | */ 789 | router.get( 790 | '/monitor/performance/flow', 791 | middlewares.checkAuth('backend.monitor.performance.dashboard'), 792 | async (ctx) => { 793 | const { 794 | id, 795 | type, 796 | range, 797 | identity, 798 | curPage = 1, 799 | pageSize = 10, 800 | start, 801 | end, 802 | path 803 | } = ctx.query; 804 | const { count, rows } = await services.webMonitor.getPerformanceFlow({ 805 | id, type, range, identity, curPage, pageSize, start, end, path 806 | }); 807 | ctx.body = { code: 0, data: rows, count }; 808 | }, 809 | ); 810 | /** 811 | * 飞书告警 demo演示 812 | */ 813 | router.post( 814 | "/monitor/send/warn", 815 | async (ctx) => { 816 | let { email, text } = ctx.request.body; 817 | await services.webMonitor.tenantSend({ email, text }); 818 | ctx.body = { code: 0 }; 819 | } 820 | ); 821 | } 822 | 823 | -------------------------------------------------------------------------------- /scripts/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:06:51 4 | * @LastEditTime: 2021-02-05 16:14:49 5 | * @LastEditors: strick 6 | * @Description: 脚本测试 7 | * @FilePath: /strick/shin-server/scripts/demo.js 8 | */ 9 | import services from '../services'; 10 | 11 | async function test() { 12 | const result = await services.backendUserAccount.find(); 13 | console.log("script test", result); 14 | } 15 | test(); 16 | -------------------------------------------------------------------------------- /scripts/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:06:38 4 | * @LastEditTime: 2021-02-03 15:06:39 5 | * @LastEditors: strick 6 | * @Description: 脚本入口 7 | * @FilePath: /strick/shin-server/scripts/index.js 8 | */ 9 | require('babel-core/register'); 10 | require('babel-polyfill'); 11 | 12 | global.env = process.env.NODE_ENV; 13 | global.logger = { 14 | trace: console.log, 15 | info: console.log, 16 | debug: console.log, 17 | error: console.log, 18 | warn: console.log, 19 | }; 20 | 21 | /** 22 | * 执行命令,当前位置如果与 scripts 目录平级,则 NODE_ENV=development node scripts/index.js 23 | * 其中 NODE_ENV 为环境常量,test、pre 和 production 24 | */ 25 | require('./demo'); 26 | 27 | 28 | -------------------------------------------------------------------------------- /services/backendUserAccount.js: -------------------------------------------------------------------------------- 1 | import redis from '../db/redis'; 2 | const KEY_LOGIN_COUNT = 'backend:login:count'; 3 | import models from '../models'; 4 | 5 | /** 6 | * 管理后台用户账号 7 | */ 8 | class BackendUserAccount { 9 | /** 10 | * 注册 11 | */ 12 | async register(...data) { 13 | const entity = new models.BackendUserAccount(data[0]); 14 | const res = await entity.save(); 15 | return res; 16 | } 17 | 18 | /** 19 | * 通过id查询用户 20 | */ 21 | async getInfoById(id) { 22 | try { 23 | const res = await models.BackendUserAccount.findOne({ _id: id }); 24 | return res; 25 | } catch (err) { 26 | logger.error(err.message); 27 | return ''; 28 | } 29 | } 30 | 31 | /** 32 | * 通过用户名查询账号信息 33 | */ 34 | async getInfoByUserName(userName) { 35 | const res = await models.BackendUserAccount.findOne({ userName }); 36 | return res; 37 | } 38 | 39 | /** 40 | * 更新账号信息 41 | */ 42 | async updateInfo(id, data) { 43 | const res = await models.BackendUserAccount.update({ 44 | _id: id, 45 | }, { 46 | $set: data, 47 | updateTime: new Date(), 48 | }); 49 | return res; 50 | } 51 | 52 | /** 53 | * 获取用户列表 54 | */ 55 | async getList(curPage, limit, keywords, roleId) { 56 | const whereCondition = { 57 | status: { 58 | $lt: 2, 59 | }, 60 | }; 61 | if (keywords) { 62 | whereCondition.$or = [ 63 | { 64 | realName: new RegExp(keywords), 65 | }, 66 | { 67 | userName: new RegExp(keywords), 68 | }, 69 | ]; 70 | } 71 | if (roleId && roleId !== 'all') { 72 | whereCondition.roles = { 73 | $in: [roleId], 74 | }; 75 | } 76 | const list = await models.BackendUserAccount.find( 77 | whereCondition 78 | , { 79 | password: 0, 80 | salt: 0, 81 | }).skip((curPage - 1) * limit).limit(limit).sort({ createTime: 'desc' }); 82 | const count = await models.BackendUserAccount 83 | .find(whereCondition, { 84 | password: 0, 85 | salt: 0, 86 | }) 87 | .count(); 88 | return {list, count}; 89 | } 90 | 91 | /** 92 | * 查询指定用户详情 93 | */ 94 | async getDetail(userId) { 95 | const res = await models.BackendUserAccount.findOne({ 96 | _id: userId, 97 | }) 98 | return res; 99 | } 100 | 101 | /** 102 | * 检查邮箱是否已被注册 103 | */ 104 | async checkEmailExist(email) { 105 | const res = await models.BackendUserAccount.findOne({ 106 | userName: email, 107 | }); 108 | return res; 109 | } 110 | 111 | /** 112 | * 检查手机号是否已注册 113 | */ 114 | async checkCelphoneExist(cellphone) { 115 | const res = await models.BackendUserAccount.findOne({ 116 | cellphone, 117 | }); 118 | return res; 119 | } 120 | 121 | /** 122 | * 更新用户状态 123 | */ 124 | async updateStatus(_id, status) { 125 | const res = await models.BackendUserAccount.update({ 126 | _id, 127 | }, { 128 | $set: { 129 | status, 130 | }, 131 | updateTime: new Date(), 132 | }); 133 | return res; 134 | } 135 | 136 | /** 137 | * 删除用户 138 | */ 139 | async delete(_id) { 140 | const res = await models.BackendUserAccount.remove({ 141 | _id, 142 | }); 143 | return res; 144 | } 145 | 146 | /** 147 | * 查询所有账号 148 | */ 149 | async find() { 150 | const res = await models.BackendUserAccount.find(); 151 | return res; 152 | } 153 | 154 | /** 155 | * 读取缓存中的登录次数 156 | */ 157 | async getLoginCount(userName) { 158 | const res = await redis.aws.get(`${KEY_LOGIN_COUNT}:${userName}`); 159 | return res; 160 | } 161 | 162 | /** 163 | * 增加缓存中的登录次数 164 | */ 165 | async increaseLoginCount(userName) { 166 | const res = await redis.aws.incr(`${KEY_LOGIN_COUNT}:${userName}`); 167 | return res; 168 | } 169 | 170 | /** 171 | * 写入缓存中的登录次数 172 | */ 173 | async setLoginCount(userName) { 174 | const res = await redis.aws.set(`${KEY_LOGIN_COUNT}:${userName}`, 0, 'EX', 300); 175 | return res; 176 | } 177 | 178 | /** 179 | * 移除缓存中的登录次数 180 | */ 181 | async resetLoginCount(userName) { 182 | const res = await redis.aws.del(`${KEY_LOGIN_COUNT}:${userName}`); 183 | return res; 184 | } 185 | 186 | /** 187 | * 根据真实姓名查询账户 188 | */ 189 | async getAccountOfName(realName) { 190 | return models.BackendUserAccount.findOne({ 191 | realName, 192 | }); 193 | } 194 | 195 | /** 196 | * 查询指定角色id的账号 197 | */ 198 | async getListOfRole(id) { 199 | return models.BackendUserAccount.find({ 200 | roles: id.toString(), 201 | }); 202 | } 203 | } 204 | 205 | export default BackendUserAccount; 206 | -------------------------------------------------------------------------------- /services/backendUserRole.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-02 16:51:51 4 | * @LastEditTime: 2023-04-24 18:00:36 5 | * @LastEditors: strick 6 | * @Description: 权限角色 7 | * @FilePath: /strick/shin-server/services/backendUserRole.js 8 | */ 9 | import models from '../models'; 10 | class BackendUserRole { 11 | 12 | /** 13 | * 获取角色列表 14 | */ 15 | async getList(role, cursor, limit) { 16 | const list = await models.BackendUserRole 17 | .find({roleName: {$regex: (role ? role : "")}}) 18 | .skip((cursor - 1) * limit) 19 | .limit(limit) 20 | .sort({ createTime: -1 }); 21 | const count = await models.BackendUserRole 22 | .find({roleName: {$regex: (role ? role : "")}}) 23 | .count(); 24 | return { list, count }; 25 | } 26 | 27 | /** 28 | * 获取角色权限列表 29 | */ 30 | async getInfoById(roleId) { 31 | const res = await models.BackendUserRole.findOne({ _id: roleId }); 32 | return res; 33 | } 34 | 35 | /** 36 | * 添加角色 37 | */ 38 | async add(roleName, roleDesc, rolePermission) { 39 | const entity = new models.BackendUserRole({ 40 | roleName, 41 | roleDesc, 42 | rolePermission, 43 | }); 44 | const res = await entity.save(); 45 | return res; 46 | } 47 | 48 | /** 49 | * 更新角色信息 50 | */ 51 | async update(id, data) { 52 | const res = await models.BackendUserRole.update({ 53 | _id: id, 54 | }, { 55 | $set: data, 56 | updateTime: new Date(), 57 | }); 58 | return res; 59 | } 60 | 61 | /** 62 | * 删除角色 63 | */ 64 | async remove(roleId) { 65 | const res = await models.BackendUserRole.remove({ _id: roleId }); 66 | return res; 67 | } 68 | 69 | /** 70 | * 获取某个角色的所有账号 71 | */ 72 | async getAccountOfRoleName(roleName) { 73 | const role = await models.BackendUserRole.findOne({ 74 | roleName, 75 | }); 76 | if (!role) { 77 | return []; 78 | } 79 | const roleId = role._id; 80 | return models.BackendUserAccount.find({ 81 | roles: roleId.toString(), 82 | }); 83 | } 84 | 85 | /** 86 | * 查询匹配关键字的角色列表 87 | */ 88 | async getRolesOfKeywords(keywords) { 89 | return models.BackendUserRole.find({ 90 | roleName: new RegExp(keywords), 91 | }); 92 | } 93 | } 94 | 95 | export default BackendUserRole; 96 | -------------------------------------------------------------------------------- /services/common.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-07-21 15:47:03 5 | * @LastEditTime: 2023-04-24 18:01:08 6 | * @Description: 通用数据处理 7 | * @FilePath: /strick/shin-server/services/common.js 8 | */ 9 | import models from '../models'; 10 | class Common { 11 | /** 12 | * 数据库查询一条记录 13 | */ 14 | async getOne(tableName, where = {}) { 15 | return models[tableName].findOne({ 16 | where, 17 | raw: true 18 | }); 19 | } 20 | 21 | /** 22 | * 数据库查询多条记录 23 | * 默认提供页码、页数和排序规则 24 | */ 25 | async getList({ 26 | tableName, 27 | where = {}, 28 | curPage = 1, 29 | limit = 20, 30 | order = [["id", "DESC"]] 31 | }) { 32 | return models[tableName].findAndCountAll({ 33 | where, 34 | limit, 35 | offset: (curPage - 1) * limit, 36 | order, 37 | raw: true 38 | }); 39 | } 40 | 41 | /** 42 | * 聚合 43 | */ 44 | async aggregation({ tableName, where = {}, func = "count", field }) { 45 | if (func === "count") 46 | return models[tableName][func]({ 47 | where 48 | }); 49 | return models[tableName][func](field, { 50 | where 51 | }); 52 | } 53 | 54 | /** 55 | * 新增 56 | */ 57 | async create(tableName, data) { 58 | return models[tableName].create(data); 59 | } 60 | 61 | /** 62 | * 修改 63 | */ 64 | async update(tableName, set, where) { 65 | return models[tableName].update(set, { where }); 66 | } 67 | } 68 | export default Common; -------------------------------------------------------------------------------- /services/index.js: -------------------------------------------------------------------------------- 1 | import backendUserAccount from './backendUserAccount'; 2 | import backendUserRole from './backendUserRole'; 3 | import common from './common'; 4 | import tool from './tool'; 5 | import webMonitor from './webMonitor'; 6 | 7 | const services = { 8 | backendUserAccount: new backendUserAccount(), 9 | backendUserRole: new backendUserRole(), 10 | common: new common(), 11 | tool: new tool(), 12 | webMonitor: new webMonitor() 13 | }; 14 | 15 | export default services; 16 | 17 | -------------------------------------------------------------------------------- /services/tool.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2020-12-16 19:17:57 5 | * @LastEditTime: 2023-04-24 18:01:29 6 | * @Description: 后台工具服务 7 | * @FilePath: /strick/shin-server/services/tool.js 8 | */ 9 | import redis from '../db/redis'; 10 | const shortChainKey = 'cache:aws:shortChain'; 11 | import models from '../models'; 12 | 13 | class Tool { 14 | /** 15 | * MongoDB查询 16 | */ 17 | async mongoQuery({ name, options, cursor }) { 18 | let result = models[name].find(options); 19 | for (let key in cursor) { 20 | if (cursor[key] !== undefined) { 21 | result[key](cursor[key]); 22 | continue; 23 | } 24 | result[key](); 25 | } 26 | return result.exec(); 27 | } 28 | 29 | /** 30 | * 创建通用配置 31 | */ 32 | async createConfig({ title, content, key }) { 33 | return models.AppGlobalConfig.create({ title, content, key }); 34 | } 35 | /** 36 | * 编辑通用配置 37 | */ 38 | async editConfig({ title, content, id }) { 39 | return models.AppGlobalConfig.update( 40 | { title, content }, 41 | { 42 | where: { 43 | id 44 | } 45 | } 46 | ); 47 | } 48 | /** 49 | * 删除通用配置 50 | */ 51 | async delConfig({ id }) { 52 | return models.AppGlobalConfig.update( 53 | { status: 0 }, 54 | { 55 | where: { 56 | id 57 | } 58 | } 59 | ); 60 | } 61 | /** 62 | * 通用配置列表 63 | */ 64 | async getConfigList() { 65 | return models.AppGlobalConfig.findAndCountAll({ 66 | where: { 67 | status: 1 68 | } 69 | }); 70 | } 71 | /** 72 | * 读取一条通用配置 73 | */ 74 | async getOneConfig(where) { 75 | return models.AppGlobalConfig.findOne({ 76 | where, 77 | raw: true 78 | }); 79 | } 80 | /** 81 | * 读取通用配置解析后的内容 82 | */ 83 | async getConfigContent(where) { 84 | const result = await models.AppGlobalConfig.findOne({ 85 | where, 86 | raw: true 87 | }); 88 | if (!result) { 89 | return result; 90 | } 91 | const { content } = result; 92 | return JSON.parse(content); 93 | } 94 | /** 95 | * 添加短链 96 | */ 97 | async createShortChain(data) { 98 | const result = models.WebShortChain.create(data); 99 | if(result) { 100 | this.redisShortChainSet(data.short, data.url); 101 | } 102 | return result; 103 | } 104 | /** 105 | * 更新短链 106 | */ 107 | async updateShortChain(data) { 108 | //更新的返回值是一个数组,包含受影响的函数 109 | const [affected] = await models.WebShortChain.update({ 110 | url: data.url 111 | }, { 112 | where: { 113 | id: data.id, 114 | }, 115 | }); 116 | if(affected > 0) { 117 | this.redisShortChainSet(data.short, data.url); 118 | } 119 | return affected; 120 | } 121 | /** 122 | * 查询短链的一条记录 123 | */ 124 | async getOneShortChain(where) { 125 | return models.WebShortChain.findOne({ 126 | where, 127 | raw: true //格式化返回值,只包含表的字段 128 | }); 129 | } 130 | /** 131 | * 查询短链列表 132 | */ 133 | async getShortChainList({ curPage, short, url }) { 134 | const where = { 135 | status: 1 136 | }; 137 | if(short) { 138 | where.short = { 139 | $like: `%${short}%`, 140 | }; 141 | } 142 | if(url) { 143 | where.url = { 144 | $like: `%${url}%`, 145 | }; 146 | } 147 | return models.WebShortChain.findAndCountAll({ 148 | where, 149 | limit: 20, 150 | offset: (curPage - 1) * 20, 151 | order: [['ctime', 'desc']], 152 | }); 153 | } 154 | /** 155 | * 删除短链 156 | * 若有删除,就需要有恢复的操作,否则就得物理删除 157 | * 因为 key 是唯一的,不允许重复 158 | */ 159 | async delShortChain({ id, short }) { 160 | const [affected] = await models.WebShortChain.update({ 161 | status: 0 162 | }, { 163 | where: { 164 | id: id, 165 | }, 166 | }); 167 | if(affected == 0) 168 | return 0; 169 | //删除缓存 170 | this.redisShortChainDel(short); 171 | return 1; 172 | } 173 | /** 174 | * 更新短链缓存 175 | * 默认存7天 176 | */ 177 | async redisShortChainSet(key, url) { 178 | const result = redis.aws.hset(shortChainKey, key, url); 179 | redis.aws.expire(shortChainKey, 604800); //超时时间为7天 180 | return result; 181 | } 182 | //读取redis 183 | async redisShortChainGet(key) { 184 | return redis.aws.hget(shortChainKey, key); 185 | } 186 | //删除redis 187 | async redisShortChainDel(key) { 188 | return redis.aws.hdel(shortChainKey, key); 189 | } 190 | } 191 | export default Tool; 192 | -------------------------------------------------------------------------------- /services/webMonitor.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-02-25 15:32:43 5 | * @LastEditTime: 2023-04-24 18:01:39 6 | * @Description: 前端监控 7 | * @FilePath: /strick/shin-server/services/webMonitor.js 8 | */ 9 | import config from "config"; 10 | import models from '../models'; 11 | import redis from '../db/redis'; 12 | import xfetch from '../utils/xfetch'; 13 | const tokenRedis = 'tenant:access:token'; 14 | class WebMonitor { 15 | /** 16 | * 初始化数据 17 | */ 18 | initMonitorWhere({id, category, msg, project, start, 19 | end, identity, other, messageType, messageStatus, messagePath}) { 20 | let where = { 21 | ...other 22 | }; 23 | if (id) { 24 | where.id = id; 25 | } 26 | if (category) { 27 | where.category = category; 28 | } 29 | if (identity) { 30 | where.identity = identity; 31 | } 32 | if (project) { 33 | where.project = project; 34 | } 35 | if (messageType) { 36 | where.message_type = messageType; 37 | } 38 | if (messageStatus) { 39 | where.message_status = messageStatus; 40 | } 41 | if (messagePath) { 42 | where.message_path = messagePath; 43 | } 44 | if (start && end) { 45 | where.ctime = { 46 | $gte: start, 47 | $lte: end, 48 | }; 49 | } 50 | if (msg && msg.length > 0) { 51 | const messages = msg.map(value => ({ 52 | message: { 53 | $like: `%${value}%`, 54 | } 55 | })); 56 | where = Sequelize.and(where, ...messages); 57 | } 58 | return where; 59 | } 60 | /** 61 | * 查询日志 62 | */ 63 | async getMonitorList({ curPage, pageSize, id, category, msg, 64 | project, start, end, identity, other={}, match, messageType, messageStatus, messagePath }) { 65 | const where = this.initMonitorWhere({ id, category, msg, 66 | project, start, end, identity, other, match, messageType, messageStatus, messagePath }); 67 | return models.WebMonitor.findAndCountAll({ 68 | where, 69 | order: [['id', 'DESC']], 70 | raw: true, 71 | limit: parseInt(pageSize), 72 | offset: (curPage - 1) * pageSize, 73 | }); 74 | } 75 | 76 | 77 | /** 78 | * 统计异常通信 79 | */ 80 | async countErrorAjax({ project, day, messageStatus }) { 81 | const where = { 82 | project, 83 | day, 84 | message_status: messageStatus 85 | } 86 | const field = "message_path"; 87 | return models.WebMonitor.count({ 88 | where, 89 | group: [ field ], 90 | attributes: [ field ] 91 | }); 92 | } 93 | 94 | /** 95 | * 查询日志图表生成 96 | */ 97 | async getMonitorListChart({ id, category, msg, project, start, end, 98 | identity, other={}, match, day, hour, attribute, messageType, messageStatus, messagePath }) { 99 | const where = this.initMonitorWhere({ id, category, msg, project, start, end, 100 | identity, other, match, messageType, messageStatus, messagePath }); 101 | if(day) { 102 | where.day = day; 103 | } 104 | if(hour !== undefined) { 105 | where.hour = hour; 106 | } 107 | const field = attribute; 108 | return models.WebMonitor.count({ 109 | where, 110 | group: [ attribute ], 111 | attributes: [ attribute ], 112 | having: { [field]: { 113 | $ne: null 114 | }} 115 | }); 116 | } 117 | /** 118 | * 查询指定日志的上下文 119 | */ 120 | async getMonitorContext({ from, to }) { 121 | const where = { 122 | id: { 123 | $gte: from, 124 | $lte: to, 125 | } 126 | }; 127 | return models.WebMonitor.findAll({ 128 | where, 129 | raw: true, 130 | }); 131 | } 132 | /** 133 | * 删除过期日志 134 | */ 135 | async delExpiredMonitor({ deadline }) { 136 | const where = { 137 | ctime: { 138 | $lte: deadline , 139 | } 140 | }; 141 | return models.WebMonitor.destroy({ 142 | where, 143 | }); 144 | } 145 | /** 146 | * 删除过期的map文件 147 | */ 148 | async delExpiredMap({ day }) { 149 | return xfetch({ 150 | url: '/smap/del', 151 | method: 'GET', 152 | params: { day }, 153 | baseURL: config.get('services').adminApi, 154 | }); 155 | } 156 | /** 157 | * 统计数量 158 | */ 159 | async statisticCount({ project, category, from, to, other={} }) { 160 | const where = { 161 | ...other 162 | }; 163 | if(project) { 164 | where.project = project; 165 | } 166 | if(category) { 167 | where.category = category; 168 | } 169 | where.ctime = { 170 | $gte: from, 171 | $lt: to, 172 | }; 173 | const count = await models.WebMonitor.count({ 174 | where, 175 | }); 176 | return count ? count : 0; 177 | } 178 | /** 179 | * 统计总量 180 | */ 181 | async statisticSum({ field, project, category, from, to }) { 182 | const where = { 183 | ctime: { 184 | $gte: from, 185 | $lte: to, 186 | } 187 | }; 188 | if(project) { 189 | where.project = project; 190 | } 191 | if(category) { 192 | where.category = category; 193 | } 194 | const sum = await models.WebMonitor.sum(field, { 195 | where 196 | }); 197 | return sum ? sum : 0; 198 | } 199 | /** 200 | * 添加一条统计记录 201 | */ 202 | async createStatis(data) { 203 | return models.WebMonitorStatis.create(data); 204 | } 205 | /** 206 | * 读取一条统计记录 207 | */ 208 | async getOneStatis(where) { 209 | return models.WebMonitorStatis.findOne({ 210 | where, 211 | raw: true 212 | }); 213 | } 214 | /** 215 | * 读取多条统计记录 216 | */ 217 | async getStatisList({ start, end, other={} }) { 218 | const where = { 219 | ...other 220 | }; 221 | if (start && end) { 222 | where.date = { 223 | $gte: start, 224 | $lte: end, 225 | }; 226 | } 227 | return models.WebMonitorStatis.findAll({ 228 | where, 229 | raw: true, 230 | }); 231 | } 232 | /** 233 | * 创建性能监控项目 234 | */ 235 | async createPerformanceProject(data) { 236 | return models.WebPerformanceProject.create(data); 237 | } 238 | /** 239 | * 获取一条性能监控项目 240 | */ 241 | async getOnePerformanceProject(where) { 242 | return models.WebPerformanceProject.findOne({ 243 | where, 244 | raw: true 245 | }); 246 | } 247 | /** 248 | * 获取正常的性能监控项目 249 | */ 250 | async getPerformanceProjectList({ name, curPage, pageSize }) { 251 | const where = { 252 | status: 1, 253 | }; 254 | if(name) { 255 | where.name = { 256 | $like: `%${name}%` 257 | } 258 | } 259 | return models.WebPerformanceProject.findAndCountAll({ 260 | where, 261 | raw: true, 262 | order: 'ctime DESC', 263 | limit: parseInt(pageSize), 264 | offset: (curPage - 1) * pageSize, 265 | }); 266 | } 267 | /** 268 | * 更新性能监控项目 269 | */ 270 | async updatePerformanceProject(id, data) { 271 | return models.WebPerformanceProject.update( 272 | data, 273 | { 274 | where: { id }, 275 | } 276 | ); 277 | } 278 | /** 279 | * 删除性能监控项目 280 | */ 281 | async delPerformanceProject({ id }) { 282 | return models.WebPerformanceProject.update( 283 | { status: 0 }, 284 | { 285 | where: { id } 286 | } 287 | ); 288 | } 289 | /** 290 | * 统计数量 291 | */ 292 | async statisticPerformanceCount({ project, day, hour, other={}, group, attributes }) { 293 | const where = { 294 | ...other 295 | }; 296 | if(project) { 297 | where.project = project; 298 | } 299 | if(day) { 300 | where.day = day; 301 | } 302 | if(hour !== undefined) { 303 | where.hour = hour; 304 | } 305 | const count = await models.WebPerformance.count({ 306 | attributes, 307 | where, 308 | group 309 | }); 310 | return count ? count : 0; 311 | } 312 | /** 313 | * 获取一条性能数据 314 | */ 315 | async getOnePerformance(where) { 316 | return models.WebPerformance.findOne({ 317 | where, 318 | raw: true, 319 | }); 320 | } 321 | /** 322 | * 获取一条性能数据 323 | */ 324 | async getPerformanceFlow({ 325 | id, type, range, identity, curPage, pageSize, start, end, path 326 | }) { 327 | const where = {}; 328 | const types = { 329 | 1: 'paint', 330 | 2: 'screen', 331 | }; 332 | const ranges = { 333 | 1: { 334 | $lte: 1000, 335 | }, 336 | 2: { 337 | $gt: 1000, 338 | $lte: 2000, 339 | }, 340 | 3: { 341 | $gt: 2000, 342 | $lte: 3000, 343 | }, 344 | 4: { 345 | $gt: 3000, 346 | $lte: 4000, 347 | }, 348 | 5: { 349 | $gt: 4000, 350 | }, 351 | }; 352 | if(identity) where.identity = identity; 353 | if (id) where.id = id; 354 | if (type && range) { 355 | where[types[type]] = ranges[range]; 356 | } 357 | if (start && end) { 358 | where.ctime = { 359 | $gte: start, 360 | $lte: end, 361 | }; 362 | } 363 | if(path) where.referer_path = path; 364 | return models.WebPerformance.findAndCountAll({ 365 | where, 366 | raw: true, 367 | order: 'ctime DESC', 368 | limit: parseInt(pageSize), 369 | offset: (curPage - 1) * pageSize, 370 | }); 371 | } 372 | /** 373 | * 获取一条排序处在95%位置的性能数据 374 | */ 375 | async getOneOrderPerformance(where, order, offset) { 376 | return models.WebPerformance.findOne({ 377 | where, 378 | order, 379 | offset, 380 | raw: true 381 | }); 382 | } 383 | /** 384 | * 获取多条性能数据 385 | */ 386 | async getPerformanceListByIds(ids) { 387 | const where = { 388 | id: ids 389 | }; 390 | return models.WebPerformance.findAll({ 391 | where, 392 | raw: true 393 | }); 394 | } 395 | /** 396 | * 创建性能统计项目 397 | */ 398 | async createPerformanceStatis(data) { 399 | return models.WebPerformanceStatis.create(data); 400 | } 401 | /** 402 | * 读取一条性能统计记录 403 | */ 404 | async getOnePerformanceStatis(where) { 405 | return models.WebPerformanceStatis.findOne({ 406 | where, 407 | raw: true 408 | }); 409 | } 410 | /** 411 | * 删除过期性能日志 412 | */ 413 | async delExpiredPerformance({ deadline }) { 414 | const where = { 415 | day: { 416 | $lte: deadline , 417 | } 418 | }; 419 | return models.WebPerformance.destroy({ 420 | where, 421 | }); 422 | } 423 | /** 424 | * 删除过期性能统计日志 425 | */ 426 | async delExpiredPerformanceStatis({ deadline }) { 427 | const where = { 428 | date: { 429 | $lte: deadline , 430 | } 431 | }; 432 | return models.WebPerformanceStatis.destroy({ 433 | where, 434 | }); 435 | } 436 | /** 437 | * 获取飞书的token 438 | * https://open.feishu.cn/document/ukTMukTMukTM/uIjNz4iM2MjLyYzM 439 | */ 440 | async getTenantAccessToken() { 441 | // 读取缓存中的token,缓存有效时间为 2 小时 442 | let token = await redis.single.get(tokenRedis); 443 | if(token) { 444 | return `Bearer ${token}`; 445 | } 446 | // 调用飞书接口 447 | const { data } = await xfetch({ 448 | baseURL: config.get("feishu").url, 449 | url: "open-apis/auth/v3/tenant_access_token/internal", 450 | data: { 451 | app_id: config.get("feishu").app_id, 452 | app_secret: config.get("feishu").app_secret, 453 | }, 454 | method: 'POST', 455 | }); 456 | token = data.tenant_access_token; 457 | // 超时时间为 2 小时 458 | await redis.single.set(tokenRedis, token, 'EX', data.expire); 459 | return `Bearer ${token}`; 460 | } 461 | 462 | /** 463 | * 飞书发送消息 464 | * https://open.feishu.cn/document/ukTMukTMukTM/uUjNz4SN2MjL1YzM?lang=zh-CN 465 | */ 466 | async tenantSend({ email, text }) { 467 | const token = await this.getTenantAccessToken(); 468 | return xfetch({ 469 | headers: { 470 | Authorization: token 471 | }, 472 | baseURL: config.get("feishu").url, 473 | url: "open-apis/message/v4/send/", 474 | data: { 475 | email, 476 | msg_type: "text", 477 | content: { 478 | text 479 | } 480 | }, 481 | method: 'POST', 482 | }); 483 | } 484 | 485 | /** 486 | * 创建性能日志 487 | */ 488 | async createPerformance(data) { 489 | return models.WebPerformance.create(data); 490 | } 491 | 492 | /** 493 | * 处理日志的更新和新增 494 | */ 495 | async handleMonitor(monitor) { 496 | const exist = await this.getMonitorByKey(monitor); //是否存在监控日志 497 | // 存在 498 | if(exist) 499 | return this.updateMonitorDigit(exist.digit+1, exist.id); //更新出现次数 500 | let row = await this.createMonitor(monitor); //创建记录 501 | row = row.toJSON(); 502 | // 添加行为记录 503 | if (monitor.record) { 504 | await models.WebMonitorRecord.create({ monitor_id: row.id, record: monitor.record }); 505 | } 506 | return row; 507 | } 508 | 509 | /** 510 | * 根据 Key 来搜索日志 511 | */ 512 | async getMonitorByKey({ project, category, key, identity }) { 513 | return models.WebMonitor.findOne({ 514 | raw: true, 515 | where: { 516 | project, 517 | category, 518 | key, 519 | identity 520 | } 521 | }); 522 | } 523 | 524 | /** 525 | * 创建监控日志 526 | */ 527 | async createMonitor(data) { 528 | return models.WebMonitor.create(data); 529 | } 530 | 531 | /** 532 | * 更新监控日志的出现次数 533 | */ 534 | async updateMonitorDigit(digit, id) { 535 | return models.WebMonitor.update({ 536 | digit 537 | }, { 538 | where: { id } 539 | }); 540 | } 541 | } 542 | export default WebMonitor; 543 | -------------------------------------------------------------------------------- /static/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/static/img/avatar.png -------------------------------------------------------------------------------- /static/img/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pwstrick/shin-server/3bdb181185a0bf58aa6e18034cb8640a0731a877/static/img/cover.jpg -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:17:34 4 | * @LastEditTime: 2021-02-03 14:28:36 5 | * @LastEditors: strick 6 | * @Description: 单元测试入口 7 | * @FilePath: /strick/shin-server/test/index.js 8 | */ 9 | import redis from '../db/redis'; 10 | import 'babel-polyfill'; 11 | /** 12 | * chai 4.0 13 | * https://www.chaijs.com/api/bdd/ 14 | */ 15 | import { expect } from 'chai'; 16 | /** 17 | * supertest 3.0 18 | * https://github.com/visionmedia/supertest 19 | */ 20 | import supertest from 'supertest'; 21 | import models from '../models'; 22 | 23 | global.models = models; 24 | global.expect = expect; 25 | global.api = supertest('http://localhost:6060'); 26 | global.env = process.env.NODE_ENV || 'development'; 27 | global.logger = { 28 | info: function () {}, 29 | warn: function () {}, 30 | debug: function () {}, 31 | trace: function () {}, 32 | error: function () {}, 33 | fatal: function () {} 34 | }; 35 | 36 | //读取账户的登录态缓存 37 | redis.aws.get("backend:user:account:token:development").then((token) => { 38 | global.authToken = `Bearer ${token}`; 39 | }); 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:babel-core/register 2 | -t 10000 3 | --recursive -------------------------------------------------------------------------------- /test/routers/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:17:34 4 | * @LastEditTime: 2021-02-05 16:10:19 5 | * @LastEditors: strick 6 | * @Description: 账户的路由层测试 7 | * @FilePath: /strick/shin-server/test/routers/user.js 8 | */ 9 | const userName = `test+${Date.now()}@shin.com`; 10 | const password = '123456'; 11 | 12 | describe('GET /user', () => { 13 | const url = '/user'; 14 | it('注册成功:返回用户id和注册时间', (done) => { 15 | api 16 | .get(url) 17 | .set('Authorization', authToken) 18 | .send({ 19 | userName, 20 | password, 21 | realName: '测试账号', 22 | cellphone: '13800138000', 23 | roles: '*', 24 | }) 25 | .expect(200) 26 | .end((err, res) => { 27 | if (err) done(err); 28 | const { username } = res.body; 29 | expect(username).to.be.not.empty; 30 | // expect(createTime).to.be.not.empty; 31 | // console.log(res.body) 32 | done(); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('GET /user/list', () => { 38 | const url = '/user/list'; 39 | it('获取用户列表成功', (done) => { 40 | api 41 | .get(url) 42 | .set('Authorization', authToken) 43 | .expect(200, done); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/services/user.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:17:34 4 | * @LastEditTime: 2021-02-03 14:27:44 5 | * @LastEditors: strick 6 | * @Description: 账户的服务层测试 7 | * @FilePath: /strick/shin-server/test/services/user.js 8 | */ 9 | import backendUserRole from '../../services/backendUserRole'; 10 | 11 | describe('用户角色', () => { 12 | it('获取指定id的角色信息', async () => { 13 | const service = new backendUserRole(models); 14 | const res = await service.getInfoById('584a4dc24c886205bd771afe'); 15 | // expect(2).toBe(2); 16 | // expect(res.rolePermisson).to.be.an('array'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/utils/tool.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:17:34 4 | * @LastEditTime: 2021-02-03 14:28:05 5 | * @LastEditors: strick 6 | * @Description: 工具测试 7 | * @FilePath: /strick/shin-server/test/utils/tool.js 8 | */ 9 | import moment from 'moment'; 10 | 11 | describe("各类工具", () => { 12 | it("moment", () => { 13 | // console.log('测试', moment(Date.now()).add(-1, 'day').format('YYYY-MM-DD')) 14 | // console.log('测试', moment('2020-11-12').add(1, 'd').format('YYYY-MM-DD')); 15 | // const tmp = moment(Date.now()).add(-1, 'day'); 16 | // console.log(tmp.add(8, 'hours').format("YYYY-MM-DD HH:mm")) 17 | // console.log(moment("2020-11-13T07:06:57.000Z").format("YYYY-MM-DD HH:mm")) 18 | // console.log(moment(Date.now()).add(-1, 'day').add(8, 'hours').utcOffset("+00:00").format('YYYY-MM-DD HH:mm')); 19 | // console.log(moment("2020-08-17T08:28:08.000Z")); 20 | // const start = moment().week(moment().week() - 1).startOf('isoWeek').format("YYYY-MM-DD HH:mm:ss"); 21 | // const end = moment().week(moment().week() - 1).endOf('isoWeek').format("YYYY-MM-DD HH:mm:ss"); 22 | // console.log(start, end); 23 | // console.log(moment('2020-11-09').add(1, 'd').format('YYYY-MM-DD')) 24 | }); 25 | it("array.map", () => { 26 | let ids = [1, 2].map((element) => { 27 | if (element == 1) return null; 28 | return element; 29 | }); 30 | expect(ids.length).to.equal(2); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /utils/constant.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-09-06 12:16:33 5 | * @LastEditTime: 2021-09-06 12:17:55 6 | * @Description: 常量 7 | * @FilePath: /strick/shin-server/utils/constant.js 8 | */ 9 | export const MONITOR_PROJECT = [ //监控项目 10 | 'shin-app', 11 | 'shin-h5', 12 | 'shin-mini' 13 | ]; 14 | export const PERFORMANCT_RATE = 0.95; //性能分析的用户比例 15 | -------------------------------------------------------------------------------- /utils/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-02-02 16:35:59 5 | * @LastEditTime: 2021-09-06 13:24:58 6 | * @Description: 7 | * @FilePath: /strick/shin-server/utils/index.js 8 | */ 9 | import moment from 'moment'; 10 | 11 | /** 12 | * 随机字符串 13 | * @export 14 | * @param {number} len 字符串长度 15 | * @returns 16 | */ 17 | export function randomString(len) { 18 | const length = len || 32; 19 | const $chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'; 20 | const maxPos = $chars.length; 21 | let pwd = ''; 22 | for (let i = 0; i < length; i += 1) { 23 | pwd += $chars.charAt(Math.floor(Math.random() * maxPos)); 24 | } 25 | return pwd; 26 | } 27 | 28 | /** 29 | * 延迟执行 30 | * @export 31 | * @param {number} ms 延迟时间 32 | * @returns 33 | */ 34 | export function sleep(ms) { 35 | return new Promise((resolve) => { 36 | setTimeout(resolve, ms); 37 | }); 38 | } 39 | 40 | /** 41 | * 身份证、 邮箱、 银行卡、 手机号加密 42 | * @param str string 43 | */ 44 | export function encrypt(str, num) { 45 | if (str) { 46 | const number = num || 0; 47 | const length = str.length; 48 | const hideLen = Math.floor(length / 2) - number; 49 | const offset = (length - hideLen) / 2; 50 | const head = str.slice(0, offset); 51 | const tail = str.slice(offset + hideLen, length); 52 | let star = ''; 53 | for (let i = 0; i < hideLen; i += 1) { 54 | star += '*'; 55 | } 56 | return head + star + tail; 57 | } 58 | return str; 59 | } 60 | 61 | /** 62 | * 日期格式 63 | */ 64 | export function utcToDate(date, format="YYYY-MM-DD HH:mm:ss") { 65 | return moment(date).utcOffset(480).format(format); 66 | } 67 | 68 | /** 69 | * 10进制转62进制 70 | */ 71 | export function string10to62(n) { 72 | if (n === 0) { 73 | return "0"; 74 | } 75 | var digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 76 | var result = ""; 77 | while (n > 0) { 78 | result = digits[n % digits.length] + result; 79 | n = parseInt(n / digits.length, 10); 80 | } 81 | return result; 82 | } 83 | 84 | /** 85 | * 62进制转10进制 86 | */ 87 | export function string62to10(s) { 88 | var digits = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 89 | var result = 0; 90 | for (var i = 0; i < s.length; i++) { 91 | var p = digits.indexOf(s[i]); 92 | if (p < 0) { 93 | return NaN; 94 | } 95 | result += p * Math.pow(digits.length, s.length - i - 1); 96 | } 97 | return result; 98 | } 99 | 100 | /** 101 | * 定时任务的日志格式 102 | */ 103 | export function setCronFormat({name, result='success', startTimestamp}) { 104 | logger.info(`[${moment().format('YYYY-MM-DD HH:mm:ss')}] 任务[${name}] 执行结果[${result}] 执行时长[${Date.now() - startTimestamp}ms]`); 105 | } -------------------------------------------------------------------------------- /utils/murmurhash.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-01-19 10:50:16 4 | * @LastEditTime: 2021-02-03 14:05:26 5 | * @LastEditors: strick 6 | * @Description: murmurhash算法 7 | * @FilePath: /strick/shin-server/utils/murmurhash.js 8 | */ 9 | const util = require('util'); 10 | (function(){ 11 | var _global = this; 12 | 13 | const createBuffer = (val) => new util.TextEncoder().encode(val) 14 | 15 | /** 16 | * JS Implementation of MurmurHash2 17 | * 18 | * @author Gary Court 19 | * @see http://github.com/garycourt/murmurhash-js 20 | * @author Austin Appleby 21 | * @see http://sites.google.com/site/murmurhash/ 22 | * 23 | * @param {Uint8Array | string} str ASCII only 24 | * @param {number} seed Positive integer only 25 | * @return {number} 32-bit positive integer hash 26 | */ 27 | function MurmurHashV2(str, seed) { 28 | if (typeof str === 'string') str = createBuffer(str); 29 | var 30 | l = str.length, 31 | h = seed ^ l, 32 | i = 0, 33 | k; 34 | 35 | while (l >= 4) { 36 | k = 37 | ((str[i] & 0xff)) | 38 | ((str[++i] & 0xff) << 8) | 39 | ((str[++i] & 0xff) << 16) | 40 | ((str[++i] & 0xff) << 24); 41 | 42 | k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); 43 | k ^= k >>> 24; 44 | k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); 45 | 46 | h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k; 47 | 48 | l -= 4; 49 | ++i; 50 | } 51 | 52 | switch (l) { 53 | case 3: h ^= (str[i + 2] & 0xff) << 16; 54 | case 2: h ^= (str[i + 1] & 0xff) << 8; 55 | case 1: h ^= (str[i] & 0xff); 56 | h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); 57 | } 58 | 59 | h ^= h >>> 13; 60 | h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); 61 | h ^= h >>> 15; 62 | 63 | return h >>> 0; 64 | }; 65 | 66 | /** 67 | * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) 68 | * 69 | * @author Gary Court 70 | * @see http://github.com/garycourt/murmurhash-js 71 | * @author Austin Appleby 72 | * @see http://sites.google.com/site/murmurhash/ 73 | * 74 | * @param {Uint8Array | string} key ASCII only 75 | * @param {number} seed Positive integer only 76 | * @return {number} 32-bit positive integer hash 77 | */ 78 | function MurmurHashV3(key, seed) { 79 | if (typeof key === 'string') key = createBuffer(key); 80 | 81 | var remainder, bytes, h1, h1b, c1, c1b, c2, c2b, k1, i; 82 | 83 | remainder = key.length & 3; // key.length % 4 84 | bytes = key.length - remainder; 85 | h1 = seed; 86 | c1 = 0xcc9e2d51; 87 | c2 = 0x1b873593; 88 | i = 0; 89 | 90 | while (i < bytes) { 91 | k1 = 92 | ((key[i] & 0xff)) | 93 | ((key[++i] & 0xff) << 8) | 94 | ((key[++i] & 0xff) << 16) | 95 | ((key[++i] & 0xff) << 24); 96 | ++i; 97 | 98 | k1 = ((((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16))) & 0xffffffff; 99 | k1 = (k1 << 15) | (k1 >>> 17); 100 | k1 = ((((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16))) & 0xffffffff; 101 | 102 | h1 ^= k1; 103 | h1 = (h1 << 13) | (h1 >>> 19); 104 | h1b = ((((h1 & 0xffff) * 5) + ((((h1 >>> 16) * 5) & 0xffff) << 16))) & 0xffffffff; 105 | h1 = (((h1b & 0xffff) + 0x6b64) + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16)); 106 | } 107 | 108 | k1 = 0; 109 | 110 | switch (remainder) { 111 | case 3: k1 ^= (key[i + 2] & 0xff) << 16; 112 | case 2: k1 ^= (key[i + 1] & 0xff) << 8; 113 | case 1: k1 ^= (key[i] & 0xff); 114 | 115 | k1 = (((k1 & 0xffff) * c1) + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff; 116 | k1 = (k1 << 15) | (k1 >>> 17); 117 | k1 = (((k1 & 0xffff) * c2) + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff; 118 | h1 ^= k1; 119 | } 120 | 121 | h1 ^= key.length; 122 | 123 | h1 ^= h1 >>> 16; 124 | h1 = (((h1 & 0xffff) * 0x85ebca6b) + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff; 125 | h1 ^= h1 >>> 13; 126 | h1 = ((((h1 & 0xffff) * 0xc2b2ae35) + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16))) & 0xffffffff; 127 | h1 ^= h1 >>> 16; 128 | 129 | return h1 >>> 0; 130 | } 131 | 132 | var murmur = MurmurHashV3; 133 | murmur.v2 = MurmurHashV2; 134 | murmur.v3 = MurmurHashV3; 135 | 136 | if (typeof(module) != 'undefined') { 137 | module.exports = murmur; 138 | } else { 139 | var _previousRoot = _global.murmur; 140 | murmur.noConflict = function() { 141 | _global.murmur = _previousRoot; 142 | return murmur; 143 | } 144 | _global.murmur = murmur; 145 | } 146 | }()); -------------------------------------------------------------------------------- /utils/queue.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-04-21 17:23:23 5 | * @LastEditTime: 2023-04-24 18:13:40 6 | * @Description: 任务队列 7 | * @FilePath: /strick/shin-server/utils/queue.js 8 | */ 9 | import kue from 'kue'; 10 | import config from 'config'; 11 | 12 | const redisConfig = config.get('kueRedis'); 13 | const queue = kue.createQueue({ 14 | prefix: 'q', 15 | redis: redisConfig, 16 | }); 17 | // queue.setMaxListeners(1000) 18 | export default queue; -------------------------------------------------------------------------------- /utils/tools.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | /** 4 | * 日期格式 5 | */ 6 | export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') { 7 | return moment(date).format(format); 8 | } -------------------------------------------------------------------------------- /utils/xfetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2020-09-28 11:15:47 5 | * @LastEditTime: 2023-04-24 18:13:21 6 | * @Description: 通信 7 | * @FilePath: /strick/shin-server/utils/xfetch.js 8 | */ 9 | import axios from 'axios'; 10 | import config from 'config'; 11 | 12 | /** 13 | * 请求信息格式 14 | * 15 | * @param {object} req 请求参数 16 | * @param {object} res 返回参数 17 | * @param {number} ms 请求耗时 18 | */ 19 | const createInfo = (req, res, ms) => ` 20 | ******************** request api start ****************** 21 | ${req.method} ${req.baseURL}${req.url} ${ms}ms 22 | request params: ${JSON.stringify(req.params)} 23 | request data: ${JSON.stringify(req.data)} 24 | request body: ${JSON.stringify(res.data)} 25 | ******************** request api end ******************** 26 | `; 27 | 28 | /** 29 | * api请求 30 | * @param {string} url 请求地址 31 | * @param {string} method 请求方法 32 | * @param {string} baseURL 默认域名地址 33 | * @param {object} params 请求参数(GET) 34 | * @param {object} data 请求参数(非GET) 35 | * @param {boolean} disableLog 是否关闭打印日志 36 | */ 37 | export default async ({ url, method, baseURL, params, data, disableLog, headers={} }) => { 38 | const start = Date.now(); 39 | const conf = { 40 | url, 41 | method, 42 | baseURL: baseURL || config.get('services').internalApi, 43 | }; 44 | const _conf = { 45 | headers: { 46 | ...headers 47 | }, 48 | }; 49 | if (method.toUpperCase() === 'GET' || params) { 50 | _conf.params = params; 51 | } else { 52 | !_conf.headers['Content-Type'] && (_conf.headers['Content-Type'] = 'application/json'); 53 | if(typeof data === "string") { 54 | _conf.data = data; 55 | }else { 56 | _conf.data = JSON.stringify(data); 57 | } 58 | } 59 | const req = Object.assign(conf, _conf); 60 | req.validateStatus = status => (status < 500); 61 | const res = await axios(req); 62 | const end = Date.now(); 63 | if (!disableLog) { 64 | logger.info(createInfo(req, res, (end - start))); 65 | global.logMessages && global.logMessages.push(createInfo(req, res, (end - start))); 66 | } 67 | return res; 68 | }; 69 | -------------------------------------------------------------------------------- /worker/agenda.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:34:44 4 | * @LastEditTime: 2021-02-03 14:51:59 5 | * @LastEditors: strick 6 | * @Description: 7 | * @FilePath: /strick/shin-server/worker/agenda.js 8 | */ 9 | import Agenda from 'agenda'; 10 | import mongodb from '../db/mongodb'; 11 | const connectionOpts = { 12 | mongo: mongodb.connection, 13 | }; 14 | 15 | const agenda = new Agenda(connectionOpts); 16 | module.exports = agenda; 17 | -------------------------------------------------------------------------------- /worker/cronJobs/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:35:06 4 | * @LastEditTime: 2021-02-05 16:13:43 5 | * @LastEditors: strick 6 | * @Description: 定时任务的demo 7 | * @FilePath: /strick/shin-server/worker/cronJobs/demo.js 8 | */ 9 | import schedule from 'node-schedule'; 10 | import services from '../../services'; 11 | function test(result) { 12 | console.log(result); 13 | } 14 | module.exports = async () => { 15 | const result = await services.backendUserAccount.find(); 16 | //每 30 秒执行一次定时任务 17 | schedule.scheduleJob({ rule: "*/30 * * * * *" }, () => { 18 | test(result); 19 | }); 20 | }; -------------------------------------------------------------------------------- /worker/cronJobs/webMonitorRemove.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-17 11:34:10 5 | * @LastEditTime: 2021-09-06 13:55:59 6 | * @Description: 删除过期的监控记录和map文件 7 | * @FilePath: /strick/shin-server/worker/cronJobs/webMonitorRemove.js 8 | */ 9 | import schedule from "node-schedule"; 10 | import moment from "moment"; 11 | import { setCronFormat } from "../../utils"; 12 | import services from "../../services"; 13 | 14 | async function monitor() { 15 | logger.info("开始执行监控日志的删除"); 16 | const startTimestamp = Date.now(); 17 | //删除7天前的记录 18 | const twoWeek = moment().add(-7, 'days').startOf('day').format("YYYY-MM-DD HH:mm"); 19 | await services.webMonitor.delExpiredMonitor({ deadline: twoWeek }); 20 | 21 | //删除21天前的文件 需要调用删除接口 22 | // await services.webMonitor.delExpiredMap({ day:21 }); 23 | 24 | setCronFormat({ name: "监控日志的删除", startTimestamp }); 25 | } 26 | module.exports = () => { 27 | // 每日凌晨3点执行 28 | schedule.scheduleJob("0 0 3 * * *", monitor); 29 | // schedule.scheduleJob("*/10 * * * * *", monitor); 30 | }; 31 | -------------------------------------------------------------------------------- /worker/cronJobs/webMonitorStatis.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-17 11:33:41 5 | * @LastEditTime: 2021-09-06 13:26:35 6 | * @Description: 监控统计 7 | * @FilePath: /strick/shin-server/worker/cronJobs/webMonitorStatis.js 8 | */ 9 | import schedule from "node-schedule"; 10 | import moment from "moment"; 11 | import { MONITOR_PROJECT } from "../../utils/constant"; 12 | import { setCronFormat } from "../../utils"; 13 | import services from "../../services"; 14 | 15 | async function monitor() { 16 | logger.info("开始执行监控统计"); 17 | const startTimestamp = Date.now(); 18 | const date = moment().add(-1, 'days').format("YYYYMMDD"); 19 | //判断当前是否已经包含昨日的统计信息 20 | const yesterdayStatis = await services.webMonitor.getOneStatis({ date }); 21 | if(yesterdayStatis) { 22 | logger.info("昨日的监控统计已完成"); 23 | return; 24 | } 25 | const yesterday = moment().add(-1, 'days').startOf('day').format("YYYY-MM-DD HH:mm"); //昨天凌晨 26 | const today = moment().startOf('day').format("YYYY-MM-DD HH:mm"); //今天凌晨 27 | // const message500 = {message: {$like: '%"status":500%'}}; //500的请求 28 | // const message502 = {message: {$like: '%"status":502%'}}; //502的请求 29 | // const message504 = {message: {$like: '%"status":504%'}}; //504的请求 30 | const message500 = { message_status: 500 }; //500的请求 31 | const message502 = { message_status: 502 }; //502的请求 32 | const message504 = { message_status: 504 }; //504的请求 33 | const statis = {}; 34 | for(let i=0; i [item.message_path, item.count]).sort((left, right) => (right[1] - left[1])); //倒序排列 49 | statis[project] = { 50 | allCount: (errorCount + ajaxCount + consoleCount + eventCount + redirectCount), 51 | errorCount, 52 | errorSum, 53 | error500Count, 54 | error502Count, 55 | error504Count, 56 | ajaxCount, 57 | consoleCount, 58 | eventCount, 59 | redirectCount, 60 | error504 61 | }; 62 | } 63 | await services.webMonitor.createStatis({ date, statis: JSON.stringify(statis) }); 64 | setCronFormat({ name: "监控统计", startTimestamp }); 65 | } 66 | module.exports = () => { 67 | // 每日凌晨4点执行 68 | schedule.scheduleJob("0 0 4 * * *", monitor); 69 | // schedule.scheduleJob("*/10 * * * * *", monitor); 70 | }; 71 | -------------------------------------------------------------------------------- /worker/cronJobs/webMonitorWarn.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-07-07 14:28:38 5 | * @LastEditTime: 2021-09-06 13:27:35 6 | * @Description: 监控后台告警 7 | * @FilePath: /strick/shin-server/worker/cronJobs/webMonitorWarn.js 8 | */ 9 | import schedule from "node-schedule"; 10 | import moment from "moment"; 11 | import { setCronFormat } from "../../utils"; 12 | import services from "../../services"; 13 | // 监控对象 14 | const monitor = { 15 | 'test': '测试页面', 16 | }; 17 | // 接收邮箱 18 | const emails = [ 19 | 'test@shin.org', 20 | ]; 21 | async function send(text) { 22 | for(let value of emails) { 23 | await services.webMonitor.tenantSend({ email: value, text }); 24 | } 25 | } 26 | async function warn() { 27 | logger.info("开始执行监控告警"); 28 | const startTimestamp = Date.now(); 29 | const from = moment().add(-5, 'minutes').format("YYYY-MM-DD HH:mm"), //计算前5分钟 30 | to = moment().format("YYYY-MM-DD HH:mm"); 31 | for(let key in monitor) { 32 | const amount = await services.webMonitor.statisticCount({ 33 | from, 34 | to, 35 | other: { 36 | project_subdir: key 37 | } 38 | }); 39 | if(amount < 20) { // 当请求量小于20时,发送白屏警告 40 | const text = `${monitor[key]}白屏异常`; 41 | await send(text); 42 | } 43 | } 44 | 45 | setCronFormat({ name: "监控告警", startTimestamp }); 46 | } 47 | module.exports = () => { 48 | // 每5分钟运行一次 49 | // schedule.scheduleJob("0 */5 * * * *", warn); 50 | // schedule.scheduleJob("*/10 * * * * *", warn); 51 | }; -------------------------------------------------------------------------------- /worker/cronJobs/webPerformanceRemove.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-03-23 18:05:06 5 | * @LastEditTime: 2021-09-06 13:56:31 6 | * @Description: 性能数据清除 7 | * @FilePath: /strick/shin-server/worker/cronJobs/webPerformanceRemove.js 8 | */ 9 | import schedule from "node-schedule"; 10 | import moment from "moment"; 11 | import { setCronFormat } from "../../utils"; 12 | import services from "../../services"; 13 | 14 | async function monitor() { 15 | logger.info("开始执行性能日志的删除"); 16 | const startTimestamp = Date.now(); 17 | //删除28天前的记录 18 | const fourWeek = moment().add(-28, 'days').startOf('day').format("YYYYMMDD"); 19 | await services.webMonitor.delExpiredPerformance({ deadline: fourWeek }); 20 | await services.webMonitor.delExpiredPerformanceStatis({ deadline: fourWeek }); 21 | 22 | setCronFormat({ name: "性能日志的删除", startTimestamp }); 23 | } 24 | module.exports = () => { 25 | // 每日凌晨4点半执行 26 | schedule.scheduleJob("0 30 4 * * *", monitor); 27 | // schedule.scheduleJob("*/10 * * * * *", monitor); 28 | }; 29 | -------------------------------------------------------------------------------- /worker/cronJobs/webPerformanceStatis-func.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @LastEditors: strick 4 | * @Date: 2021-04-01 11:16:38 5 | * @LastEditTime: 2021-09-06 13:25:25 6 | * @Description: 7 | * @FilePath: /strick/shin-server/worker/cronJobs/webPerformanceStatis-func.js 8 | */ 9 | import moment from "moment"; 10 | import { setCronFormat } from "../../utils"; 11 | import { PERFORMANCT_RATE } from "../../utils/constant"; 12 | import services from "../../services"; 13 | 14 | /** 15 | * 计算 95% 的位置 16 | */ 17 | function getFloorNumber(count) { 18 | return Math.floor(count * PERFORMANCT_RATE); 19 | } 20 | /** 21 | * 计算24小时的性能数据 22 | */ 23 | async function calcHour({ project, day }) { 24 | let counts = await services.webMonitor.statisticPerformanceCount({ 25 | project, 26 | day, 27 | group: "hour", 28 | attributes: ["hour"] 29 | }); 30 | if(counts.length == 0) 31 | return null; 32 | const statistic = { 33 | x: [], 34 | load: [], 35 | ready: [], 36 | paint: [], 37 | screen: [], 38 | loadZero: 0, //load时间为0的个数 39 | all: 0, //日志总数 40 | }; 41 | statistic.loadZero = await services.webMonitor.statisticPerformanceCount({ 42 | project, day, 43 | other: { 44 | load: 0 45 | } 46 | }); 47 | statistic.all = await services.webMonitor.statisticPerformanceCount({ 48 | project, day, 49 | }); 50 | counts = counts.map((value) => { 51 | return { 52 | ...value, 53 | offset: getFloorNumber(value.count) 54 | }; 55 | }); 56 | //逐条查询排序后排在95%位置的记录 57 | for(let i=0; i { 108 | return { 109 | ...value, 110 | offset: getFloorNumber(value.count) 111 | } 112 | }); 113 | //逐条查询排序后排在95%位置的记录 114 | for(let i=0; i { 12 | // 每日凌晨3点半执行 13 | schedule.scheduleJob("0 30 3 * * *", monitor); 14 | // schedule.scheduleJob("*/10 * * * * *", monitor); 15 | }; 16 | -------------------------------------------------------------------------------- /worker/triggerJobs/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: strick 3 | * @Date: 2021-02-03 14:35:12 4 | * @LastEditTime: 2021-02-03 15:11:47 5 | * @LastEditors: strick 6 | * @Description: 触发任务的demo 7 | * @FilePath: /strick/shin-server/worker/triggerJobs/demo.js 8 | */ 9 | import services from '../../services'; 10 | 11 | module.exports = (agenda) => { 12 | // 例如满足某种条件触发邮件通知 13 | agenda.define('send email report', (job, done) => { 14 | // 传递进来的数据 15 | const data = job.attrs.data; 16 | console.log(data); 17 | // 触发此任务,需要先引入 agenda.js,然后调用 now() 方法 18 | // import agenda from '../worker/agenda'; 19 | // agenda.now('send email report', { 20 | // username: realName, 21 | // }); 22 | }); 23 | }; 24 | --------------------------------------------------------------------------------