├── .autod.conf.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── Dockerfile ├── README.md ├── app.js ├── app ├── constants │ ├── index.js │ ├── redis.js │ └── result.js ├── controller │ ├── home.js │ ├── login.js │ ├── system.js │ ├── task.js │ └── ws.js ├── extend │ ├── application.js │ └── helper.js ├── middleware │ ├── checkTokenHandler.js │ ├── errorHandler.js │ └── logHandler.js ├── router.js ├── routers │ ├── login.js │ ├── system.js │ └── task.js ├── service │ ├── loginService.js │ ├── scheduleService.js │ ├── systemService.js │ └── taskService.js └── utils │ ├── GlobalError.js │ ├── JobHandlerLog.js │ ├── index.js │ └── os.js ├── appveyor.yml ├── config ├── config.default.js └── plugin.js ├── docs └── admin_demo.sql ├── jsconfig.json ├── package.json └── test └── app └── controller └── home.test.js /.autod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | write: true, 5 | prefix: '^', 6 | plugin: 'autod-egg', 7 | test: [ 8 | 'test', 9 | 'benchmark', 10 | ], 11 | dep: [ 12 | 'egg', 13 | 'egg-scripts', 14 | ], 15 | devdep: [ 16 | 'egg-ci', 17 | 'egg-bin', 18 | 'egg-mock', 19 | 'autod', 20 | 'autod-egg', 21 | 'eslint', 22 | 'eslint-config-egg', 23 | ], 24 | exclude: [ 25 | './test/fixtures', 26 | './dist', 27 | ], 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-egg" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | npm-debug.log 3 | yarn-error.log 4 | node_modules/ 5 | package-lock.json 6 | yarn.lock 7 | coverage/ 8 | .idea/ 9 | run/ 10 | .DS_Store 11 | *.sw* 12 | *.un~ 13 | typings/ 14 | .nyc_output/ 15 | .github 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - '10' 5 | before_install: 6 | - npm i npminstall@5 -g 7 | install: 8 | - npminstall 9 | script: 10 | - npm run ci 11 | after_script: 12 | - npminstall codecov && codecov 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 设置基础镜像,基于node:16.0.0版本 2 | FROM node:16.0.0 3 | 4 | # 配置环境变量 5 | ENV NODE_ENV production 6 | 7 | # 配置阿里性能平台参数 8 | # Node.js 性能平台给您的项目生成的 appid 9 | ENV ADMIN_DEMO_ALINODE_APPID appid 10 | # Node.js 性能平台给您的项目生成的 secret 11 | ENV ADMIN_DEMO_ALINODE_APPSECRET secret 12 | 13 | # 创建工作目录 14 | RUN mkdir -p /usr/src/app 15 | 16 | # 指定工作目录 17 | WORKDIR /usr/src/app 18 | 19 | # 拷贝package.json文件到工作目录 20 | # !!重要:package.json需要单独添加。 21 | # Docker在构建镜像的时候,是一层一层构建的,仅当这一层有变化时,重新构建对应的层。 22 | # 如果package.json和源代码一起添加到镜像,则每次修改源码都需要重新安装npm模块,这样木有必要。 23 | # 所以,正确的顺序是: 添加package.json;安装npm模块;添加源代码。 24 | COPY package.json /usr/src/app/package.json 25 | 26 | # 安装runtime 27 | RUN npm i nodeinstall -g 28 | RUN nodeinstall --install-alinode ^3 29 | 30 | # 安装项目依赖 31 | RUN npm install 32 | 33 | # 拷贝所有文件到工作目录 34 | COPY . /usr/src/app 35 | 36 | # 暴露端口(可忽略,在启动时指定) 37 | EXPOSE 7002 38 | 39 | # 启动项目 40 | CMD npm start 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # admin-server 2 | 3 | 4 | 5 | ## QuickStart 6 | 7 | 8 | 9 | see [egg docs][egg] for more detail. 10 | 11 | ### Development 12 | 13 | ```bash 14 | $ npm i 15 | $ npm run dev 16 | $ open http://localhost:7002/ 17 | ``` 18 | 19 | ### Deploy 20 | 21 | ```bash 22 | $ npm start 23 | $ npm stop 24 | ``` 25 | 26 | ### Docker Deploy 27 | 28 | ```bash 29 | # 构建镜像 30 | docker build -t `镜像名`:tag . 31 | # 由于在项目中使用了hosts,所以需要需要在启动的时候追加数据库指向,也可在项目配置中将其修改成IP 32 | docker run -itd --net=host --name admin-server --add-host=adminDemodb:<数据库地址> admin-server 33 | ``` 34 | 35 | ### npm scripts 36 | 37 | - Use `npm run lint` to check code style. 38 | - Use `npm test` to run unit test. 39 | - Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail. 40 | 41 | [egg]: https://eggjs.org 42 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SCHEDULE_STATUS, SCHEDULE_DELETE, ACTIVE_KYES } = require('./app/constants'); 4 | 5 | class AppBootHook { 6 | constructor(app) { 7 | this.app = app; 8 | this.ctx = app.createAnonymousContext(); 9 | } 10 | 11 | async willReady() { 12 | await this.app.logger.info('【初始化定时任务】开始...'); 13 | // 查询启动状态的定时任务 14 | const schedules = await this.app.mysql.select('schedule_job', { where: { status: SCHEDULE_STATUS.RUN, is_delete: SCHEDULE_DELETE.MANUAL } }); 15 | // 循环注册定时任务 16 | schedules.forEach(async schedule => { 17 | await this.app.logger.info('【注册job】name:%s, handler: %s', schedule.jobName, schedule.jobHandler); 18 | await this.ctx.helper.generateSchedule(schedule.job_id, schedule.cron, schedule.jobName, schedule.jobHandler); 19 | }); 20 | await this.app.logger.info('【初始化定时任务】初始化定时任务: %d,结束...', schedules.length); 21 | 22 | /** 23 | * 监听取消/停止任务 24 | * data: 传入任务信息 25 | */ 26 | this.app.messenger.on(ACTIVE_KYES.STOP_SCHEDULS, async data => { 27 | await this.app.logger.info('【任务通知】收到取消任务通知,任务名:', data.jobName); 28 | // 取消任务 29 | await this.ctx.helper.cancelSchedule(data.jobName); 30 | }); 31 | 32 | /** 33 | * 监听取消/停止任务 34 | * data: 传入任务信息 35 | */ 36 | this.app.messenger.on(ACTIVE_KYES.RUN_SCHEDULS, async data => { 37 | await this.app.logger.info('【任务通知】收到启动任务通知,任务名:', data.jobName); 38 | // 取消任务 39 | await this.ctx.helper.generateSchedule(data.job_id, data.cron, data.jobName, data.jobHandler); 40 | }); 41 | } 42 | 43 | async beforeClose() { 44 | await this.app.logger.info('【销毁定时任务】开始...'); 45 | const scheduleStacks = await this.app.scheduleStacks; 46 | Reflect.ownKeys(scheduleStacks).forEach(async key => { 47 | await this.ctx.helper.cancelSchedule(key); 48 | }); 49 | await this.app.logger.info('【销毁定时任务】销毁定时任务数: %d,结束...', Reflect.ownKeys(scheduleStacks).length); 50 | } 51 | } 52 | 53 | module.exports = AppBootHook; 54 | -------------------------------------------------------------------------------- /app/constants/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONSTANTS = { 4 | // 定时任务状态 5 | SCHEDULE_STATUS: { 6 | // 启用 7 | RUN: 0, 8 | // 停止 9 | STOP: -1, 10 | }, 11 | // 定时任务触发类型 12 | SCHEDULE_TRIGGER_TYPE: { 13 | // 任务触发 14 | TASK: 0, 15 | // 手动出发 16 | MANUAL: 1, 17 | }, 18 | // 定时任务执行状态 19 | SCHEDULE_EXECUTION_STATUS: { 20 | // 执行中 21 | RUN: 0, 22 | // 执行完成 23 | END: 1, 24 | }, 25 | // 运行模式 26 | SCHEDULE_RUN_MODE: { 27 | BEAN: 0, 28 | SHELL: 1, 29 | }, 30 | // 任务删除状态 31 | SCHEDULE_DELETE: { 32 | // 删除 33 | DELETE: -1, 34 | // 正常 35 | MANUAL: 0, 36 | }, 37 | ACTIVE_KYES: { 38 | STOP_SCHEDULS: 'stop_schedule', 39 | RUN_SCHEDULS: 'run_schedule', 40 | }, 41 | }; 42 | 43 | module.exports = CONSTANTS; 44 | -------------------------------------------------------------------------------- /app/constants/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const REDIS = { 4 | // token存储前缀 5 | ADMIN_PREFIX: 'admin:', 6 | // token有效期 7 | ADMIN_EXPIRE_TIME: 3600, 8 | // 任务调度记录前缀 scheduleStacks 9 | SCHEDULE_STACKS: 'schedule_stacks:' 10 | }; 11 | 12 | module.exports = REDIS; 13 | -------------------------------------------------------------------------------- /app/constants/result.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 接口返回码: 0 成功 1 失败 */ 4 | const RESULT = { 5 | // 成功 6 | RESULT_SUCC: 0, 7 | // 通用失败 8 | RESULT_FAIL: 1, 9 | // 登录超时 10 | RESULT_LOGIN_FAIL: -1, 11 | // 账号封禁 12 | RESULT_USER_STATUS_BAN: -2, 13 | }; 14 | 15 | module.exports = RESULT; 16 | -------------------------------------------------------------------------------- /app/controller/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | class HomeController extends Controller { 6 | async index() { 7 | const { ctx } = this; 8 | ctx.body = 'hi, egg'; 9 | } 10 | } 11 | 12 | module.exports = HomeController; 13 | -------------------------------------------------------------------------------- /app/controller/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Controller } = require('egg'); 4 | const { ADMIN_PREFIX } = require('../constants/redis'); 5 | const { setResult } = require('../utils'); 6 | 7 | /** 8 | * 登陆相关 9 | */ 10 | class LoginController extends Controller { 11 | /** 12 | * 登陆 13 | * username:String 用户名 14 | * password:String 密码 15 | */ 16 | async login() { 17 | const { ctx } = this; 18 | const token = await ctx.service.loginService.login(ctx.request.body); 19 | ctx.body = setResult({ data: { token } }); 20 | } 21 | /** 22 | * 获取登陆详细信息 23 | */ 24 | async loginInfo() { 25 | const { ctx } = this; 26 | const adminInfo = JSON.parse(await ctx.app.redis.get(ADMIN_PREFIX + ctx.query.token)); 27 | ctx.body = setResult({ data: adminInfo }); 28 | } 29 | /** 30 | * 登出 31 | */ 32 | async logout() { 33 | const { ctx } = this; 34 | await ctx.app.redis.del(ADMIN_PREFIX + ctx.request.header.token); 35 | ctx.body = setResult(); 36 | } 37 | } 38 | 39 | module.exports = LoginController; 40 | -------------------------------------------------------------------------------- /app/controller/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Controller } = require('egg'); 4 | const { setResult } = require('../utils'); 5 | 6 | class SystemController extends Controller { 7 | /** 8 | * 获取管理员账号 9 | */ 10 | async adminList() { 11 | const { ctx } = this; 12 | const list = await ctx.service.systemService.adminList(); 13 | ctx.body = setResult({ data: { list } }); 14 | } 15 | /** 16 | * 编辑/新增管理员账号 17 | */ 18 | async editAdmin() { 19 | const { ctx } = this; 20 | const { username } = ctx.request.headers; 21 | const pwd = await ctx.service.systemService.editAdmin(username, ctx.request.body); 22 | ctx.body = setResult({ data: { pwd } }); 23 | } 24 | /** 25 | * 删除管理员 26 | */ 27 | async deleteAdmin() { 28 | const { ctx } = this; 29 | await ctx.service.systemService.deleteAdmin(ctx.request.body); 30 | ctx.body = setResult(); 31 | } 32 | /** 33 | * 重置管理员密码 34 | */ 35 | async resetAdminPwd() { 36 | const { ctx } = this; 37 | const { username } = ctx.request.headers; 38 | const pwd = await ctx.service.systemService.resetAdminPwd(username, ctx.request.body); 39 | ctx.body = setResult({ data: { pwd } }); 40 | } 41 | /** 42 | * 修改管理员密码 43 | */ 44 | async editAdminPwd() { 45 | const { ctx } = this; 46 | await ctx.service.systemService.editAdminPwd(); 47 | ctx.body = setResult(); 48 | } 49 | /** 50 | * 获取菜单列表 51 | */ 52 | async menuList() { 53 | const { ctx } = this; 54 | const list = await ctx.service.systemService.menuList(); 55 | ctx.body = setResult({ data: { list } }); 56 | } 57 | /** 58 | * 编辑菜单 59 | */ 60 | async editMenu() { 61 | const { ctx } = this; 62 | const { username } = ctx.request.headers; 63 | await ctx.service.systemService.editMenu(username, ctx.request.body); 64 | ctx.body = setResult(); 65 | } 66 | /** 67 | * 删除菜单 68 | */ 69 | async deleteMenu() { 70 | const { ctx } = this; 71 | const { username } = ctx.request.headers; 72 | await ctx.service.systemService.deleteMenu(username, ctx.request.body); 73 | ctx.body = setResult(); 74 | } 75 | /** 76 | * 获取角色列表 77 | */ 78 | async roleList() { 79 | const { ctx } = this; 80 | const list = await ctx.service.systemService.roleList(); 81 | ctx.body = setResult({ data: { list } }); 82 | } 83 | /** 84 | * 编辑角色 85 | */ 86 | async editRole() { 87 | const { ctx } = this; 88 | const { username } = ctx.request.headers; 89 | await ctx.service.systemService.editRole(username, ctx.request.body); 90 | ctx.body = setResult(); 91 | } 92 | /** 93 | * 编辑角色菜单 94 | */ 95 | async editRoleMenu() { 96 | const { ctx } = this; 97 | await ctx.service.systemService.editRoleMenu(ctx.request.body); 98 | ctx.body = setResult(); 99 | } 100 | /** 101 | * 获取谷歌验证码绑定信息 102 | */ 103 | async openGoogleAuth() { 104 | const { ctx } = this; 105 | const result = await ctx.service.systemService.openGoogleAuth(); 106 | ctx.body = setResult({ data: result }); 107 | } 108 | /** 109 | * 谷歌身份验证绑定 110 | */ 111 | async googleVerify() { 112 | const { ctx } = this; 113 | await ctx.service.systemService.googleVerify(ctx.request.body); 114 | ctx.body = setResult(); 115 | } 116 | } 117 | 118 | module.exports = SystemController; 119 | -------------------------------------------------------------------------------- /app/controller/task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Controller = require('egg').Controller; 4 | 5 | const { setResult } = require('../utils'); 6 | 7 | /** 8 | * task Controller 9 | */ 10 | class TaskController extends Controller { 11 | /** 12 | * 定时任务管理 13 | */ 14 | async scheduleList() { 15 | const { ctx } = this; 16 | const result = await ctx.service.taskService.scheduleList(ctx.request.query); 17 | ctx.body = setResult({ data: result }); 18 | } 19 | /** 20 | * 修改/新增定时任务 21 | */ 22 | async editSchedule() { 23 | const { ctx } = this; 24 | const { username } = ctx.request.headers; 25 | await ctx.service.taskService.editSchedule(username, ctx.request.body); 26 | ctx.body = setResult(); 27 | } 28 | /** 29 | * 删除定时任务 30 | */ 31 | async deleteSchedule() { 32 | const { ctx } = this; 33 | await ctx.service.taskService.deleteSchedule(ctx.request.body); 34 | ctx.body = setResult(); 35 | } 36 | /** 37 | * 更新定时任务状态 38 | */ 39 | async updateStatusSchedule() { 40 | const { ctx } = this; 41 | await ctx.service.taskService.updateStatusSchedule(ctx.request.body); 42 | ctx.body = setResult(); 43 | } 44 | /** 45 | * 执行任务 46 | */ 47 | async runSchedule() { 48 | const { ctx } = this; 49 | await ctx.service.taskService.runSchedule(ctx.request.body); 50 | ctx.body = setResult(); 51 | } 52 | /** 53 | * 获取任务执行日志 54 | */ 55 | async scheduleLogList() { 56 | const { ctx } = this; 57 | const result = await ctx.service.taskService.scheduleLogList(ctx.request.query); 58 | ctx.body = setResult({ data: result }); 59 | } 60 | /** 61 | * 获取任务执行日志详细信息 62 | */ 63 | async scheduleLogDateil() { 64 | const { ctx } = this; 65 | const result = await ctx.service.taskService.scheduleLogDateil(ctx.request.query); 66 | ctx.body = setResult({ data: result }); 67 | } 68 | } 69 | 70 | module.exports = TaskController; 71 | -------------------------------------------------------------------------------- /app/controller/ws.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Controller } = require('egg'); 4 | 5 | const { cpu, mem, sys, disk } = require('../utils/os'); 6 | 7 | class WsController extends Controller { 8 | async hello() { 9 | const { ctx } = this; 10 | if (!ctx.websocket) { 11 | throw new Error('this function can only be use in websocket router'); 12 | } 13 | 14 | let useCpu = await cpu(); 15 | let useMem = await mem(); 16 | let _sys = await sys(); 17 | let _disk = await disk(); 18 | ctx.websocket.send(JSON.stringify({ cpu: useCpu, mem: useMem, time: new Date().toISOString(), sys: _sys, disk: _disk })); 19 | 20 | const time = setInterval(async () => { 21 | useCpu = await cpu(); 22 | useMem = await mem(); 23 | _sys = await sys(); 24 | _disk = await disk(); 25 | ctx.websocket.send(JSON.stringify({ cpu: useCpu, mem: useMem, time: new Date().toISOString(), sys: _sys, disk: _disk })); 26 | }, 1000); 27 | 28 | 29 | // 监听断开清除定时器 30 | ctx.websocket.on('close', () => { 31 | clearInterval(time); 32 | }); 33 | } 34 | } 35 | 36 | module.exports = WsController; 37 | -------------------------------------------------------------------------------- /app/extend/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const REDLOCK = Symbol('Application#redlock'); 4 | const SCHEDULESTACKS = Symbol('Application#scheduleStacks'); 5 | 6 | /** 7 | * Application扩展 8 | */ 9 | module.exports = { 10 | /** 11 | * redis锁 12 | */ 13 | get redlock() { 14 | if (!this[REDLOCK]) { 15 | this[REDLOCK] = {}; 16 | this[REDLOCK].lock = async (key, value, ttl) => { 17 | return await this.redis.set(key, value, 'EX', ttl, 'NX'); 18 | }; 19 | this[REDLOCK].unlock = async key => { 20 | return await this.redis.del(key); 21 | }; 22 | } 23 | return this[REDLOCK]; 24 | }, 25 | /** 26 | * 用于存放定时任务的堆栈 27 | */ 28 | get scheduleStacks() { 29 | if (!this[SCHEDULESTACKS]) { 30 | this[SCHEDULESTACKS] = {}; 31 | } 32 | return this[SCHEDULESTACKS]; 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /app/extend/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const { spawn } = require('child_process'); 5 | const schedule = require('node-schedule'); 6 | const NodeUUID = require('node-uuid'); 7 | const JobHandlerLog = require('../utils/JobHandlerLog'); 8 | const { SCHEDULE_STACKS } = require('../constants/redis'); 9 | const { SCHEDULE_STATUS, SCHEDULE_RUN_MODE, SCHEDULE_TRIGGER_TYPE } = require('../constants'); 10 | 11 | module.exports = { 12 | /** 13 | * 创建定时任务 14 | * @param {*} id 任务ID 15 | * @param {*} cron Cron 16 | * @param {*} jobName 任务名 17 | * @param {*} jobHandler 任务方法 18 | * 在日常使用中,可能会存在同一处理程序有不同的处理逻辑,所以需要传入任务的ID 19 | * 如:在消息推送中,会存在不同时间对相同用户推送不同内容,而内容存放在任务信息中,业务代码需要查询到对应的任务信息读取推送信息,处理下一步逻辑 20 | */ 21 | async generateSchedule(id, cron, jobName, jobHandler) { 22 | this.ctx.logger.info('[创建定时任务],任务ID: %s,cron: %s,任务名: %s,任务方法: %s', id, cron, jobName, jobHandler); 23 | // 生成任务唯一值 24 | const uuid = NodeUUID.v4(); 25 | this.app.scheduleStacks[jobName] = schedule.scheduleJob(uuid, cron, async () => { 26 | await this.executeSchedule(id); 27 | }); 28 | await this.app.redis.set(`${SCHEDULE_STACKS}${uuid}`, `${jobName}-${Date.now()}`); 29 | }, 30 | /** 31 | * 取消/停止定时任务 32 | * @param {*} jobName 任务名 33 | */ 34 | async cancelSchedule(jobName) { 35 | this.ctx.logger.info('[取消定时任务],任务名:%s', jobName); 36 | await this.app.redis.del(`${SCHEDULE_STACKS}${this.app.scheduleStacks[jobName].name}`); 37 | this.app.scheduleStacks[jobName] && this.app.scheduleStacks[jobName].cancel(); 38 | }, 39 | /** 40 | * 执行任务 41 | * @param {*} id 任务ID 42 | * @param {*} checkLocked 是否单一执行 43 | * @param {*} checkStatus 是否判断当前状态 44 | * @param {*} triggerType 触发类型 45 | */ 46 | async executeSchedule(id, checkLocked = true, checkStatus = true, triggerType = SCHEDULE_TRIGGER_TYPE.TASK) { 47 | // 读取锁,保证一个任务同时只能有一个进程执行 48 | if (checkLocked) { 49 | const locked = await this.app.redlock.lock('sendAllUserBroadcast:' + id, 'sendAllUserBroadcast', 180); 50 | if (!locked) return false; 51 | } 52 | 53 | const jobHandlerLog = new JobHandlerLog(this.app); 54 | 55 | // 获取任务信息 56 | const schedule = await this.app.mysql.get('schedule_job', { job_id: id }); 57 | 58 | try { 59 | // 判断任务状态 60 | if (schedule.status === SCHEDULE_STATUS.STOP && checkStatus) { 61 | // 当任务处于停止状态时,取消当前执行 62 | // 任务容错,防止用户在调用停止接口时不是当前worker 63 | await this.cancelSchedule(schedule.jobName); 64 | await this.logger.info('执行任务`%s`时,任务状态为停止状态'); 65 | } else { 66 | // 执行日志初始化 67 | await jobHandlerLog.init(schedule, triggerType); 68 | 69 | if (schedule.runMode === SCHEDULE_RUN_MODE.BEAN) { 70 | // 调用任务方法 71 | await this.service.scheduleService[schedule.jobHandler](schedule.params, jobHandlerLog); 72 | } else if (schedule.runMode === SCHEDULE_RUN_MODE.SHELL) { 73 | // 执行脚本文件 74 | await this.execScript(schedule, jobHandlerLog); 75 | } 76 | } 77 | } catch (error) { 78 | await this.logger.info('执行任务`%s`失败,时间:%s, 错误信息:%j', schedule.jobName, new Date().toLocaleString(), error); 79 | // 记录失败日志 80 | await jobHandlerLog.error('执行任务`{0}`失败,时间:{1}, 错误信息:{2}', schedule.jobName, new Date().toLocaleString(), error); 81 | } finally { 82 | // 释放锁 83 | checkLocked && await this.app.redlock.unlock('sendAllUserBroadcast:' + id); 84 | // 更新日志记录状态 85 | await jobHandlerLog.end(); 86 | } 87 | }, 88 | /** 89 | * 执行脚本文件 90 | * @param {*} schedule 任务信息 91 | * @param {*} jobHandlerLog 日志 92 | */ 93 | async execScript(schedule, jobHandlerLog) { 94 | return new Promise((resolve, reject) => { 95 | // 创建脚本临时文件 96 | // /tmp 97 | const filePath = './task' + schedule.job_id + Date.now() + '.sh'; 98 | try { 99 | // 写入文件 100 | fs.writeFileSync(filePath, schedule.runSource); 101 | // 处理用户参数 102 | const params = schedule.params ? schedule.params.split(',') : []; 103 | // 执行脚本 104 | const ls = spawn('/bin/bash', [ filePath, ...params ]); 105 | // 监听输出 106 | ls.stdout.on('data', data => { 107 | jobHandlerLog.log(data); 108 | }); 109 | 110 | ls.on('error', data => { 111 | jobHandlerLog.error(JSON.stringify(data)); 112 | resolve(new Error(data)); 113 | }); 114 | 115 | ls.on('close', () => { 116 | resolve(); 117 | }); 118 | // 监听异常 119 | ls.on('exit', code => { 120 | if (code !== 0) { 121 | jobHandlerLog.error(code); 122 | reject(new Error(code)); 123 | } 124 | resolve(); 125 | }); 126 | } catch (err) { 127 | throw new Error(err); 128 | } finally { 129 | fs.unlinkSync(filePath); 130 | } 131 | }); 132 | }, 133 | }; 134 | -------------------------------------------------------------------------------- /app/middleware/checkTokenHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ADMIN_PREFIX, ADMIN_EXPIRE_TIME } = require('../constants/redis'); 4 | const { RESULT_LOGIN_FAIL } = require('../constants/result'); 5 | const GlobalError = require('../utils/GlobalError'); 6 | 7 | module.exports = () => { 8 | return async function(ctx, next) { 9 | const { headers } = ctx.request; 10 | const { token } = headers; 11 | 12 | let adminInfo = await ctx.app.redis.get(ADMIN_PREFIX + token); 13 | if (!adminInfo) throw new GlobalError(RESULT_LOGIN_FAIL, '登陆超时,请重新登陆'); 14 | 15 | // 延长token的失效时间 16 | await ctx.app.redis.expire(ADMIN_PREFIX + token, ADMIN_EXPIRE_TIME); 17 | 18 | adminInfo = JSON.parse(adminInfo); 19 | headers.adminId = adminInfo.adminId; 20 | headers.username = adminInfo.username; 21 | headers.avatarUrl = adminInfo.avatarUrl; 22 | 23 | await next(); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /app/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const GlobalError = require('../utils/GlobalError'); 4 | const { setResult } = require('../utils'); 5 | const { RESULT_FAIL } = require('../constants/result'); 6 | 7 | /** 8 | * 全局异常处理 9 | */ 10 | module.exports = () => { 11 | return async function errorHandler(ctx, next) { 12 | try { 13 | await next(); 14 | } catch (err) { 15 | /** 自定义异常 */ 16 | if (err instanceof GlobalError) { 17 | ctx.body = setResult(err); 18 | return false; 19 | } 20 | /** 运行异常 */ 21 | // 记录一条错误日志 22 | ctx.app.emit('error', err, ctx); 23 | ctx.body = setResult(RESULT_FAIL, '服务器繁忙'); 24 | } 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /app/middleware/logHandler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 接口日志打印 5 | */ 6 | module.exports = () => { 7 | return async function logHandler(ctx, next) { 8 | const time = Date.now(); 9 | const { body, url, method, ip } = ctx.request; 10 | await next(); 11 | let infoStr = `ip: ${ip}, url: ${url}, method: ${method}, `; 12 | if (url.startsWith('/page') || ctx.response.header['content-type'] === 'application/octet-stream') { 13 | infoStr += `params: ${JSON.stringify(body)}, time: ${Date.now() - time}ms`; 14 | } else { 15 | infoStr += `params: ${JSON.stringify(body)}, resp: ${JSON.stringify(ctx.body)}, time: ${Date.now() - time}ms`; 16 | } 17 | ctx.logger.info(infoStr); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /app/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | /** 7 | * @param {Egg.Application} app - egg application 8 | */ 9 | module.exports = app => { 10 | const { router, controller, config } = app; 11 | router.get(`${config.contextPath}/`, controller.home.index); 12 | 13 | /** 14 | * 动态注册路由文件 15 | */ 16 | const routerPath = path.join(__dirname, 'routers'); 17 | (function initRouter(pathUrl) { 18 | fs.readdirSync(pathUrl).forEach(dir => { 19 | if (fs.statSync(path.join(pathUrl, dir)).isFile()) { 20 | require(path.join(pathUrl, dir))(app); 21 | } else { 22 | // 文件夹 23 | initRouter(path.join(pathUrl, dir)); 24 | } 25 | }); 26 | })(routerPath); 27 | 28 | app.ws.route('/ws', app.controller.ws.hello); 29 | }; 30 | -------------------------------------------------------------------------------- /app/routers/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | const { router, controller, config } = app; 8 | const checkTokenHandler = app.middleware.checkTokenHandler(); 9 | 10 | // 登陆 11 | router.post(`${config.contextPath}/login`, controller.login.login); 12 | // 获取登陆详细信息 13 | router.get(`${config.contextPath}/login/info`, checkTokenHandler, controller.login.loginInfo); 14 | // 登出 15 | router.post(`${config.contextPath}/logout`, checkTokenHandler, controller.login.logout); 16 | }; 17 | -------------------------------------------------------------------------------- /app/routers/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = app => { 4 | const { router, controller, config } = app; 5 | const checkTokenHandler = app.middleware.checkTokenHandler(); 6 | 7 | // 获取管理员账号 8 | router.get(`${config.contextPath}/system/admin/list`, checkTokenHandler, controller.system.adminList); 9 | // 编辑/新增管理员账号 10 | router.post(`${config.contextPath}/system/admin/edit`, checkTokenHandler, controller.system.editAdmin); 11 | // 删除管理员 12 | router.post(`${config.contextPath}/system/admin/delete`, checkTokenHandler, controller.system.deleteAdmin); 13 | // 重置管理员密码 14 | router.post(`${config.contextPath}/system/admin/pwd/reset`, checkTokenHandler, controller.system.resetAdminPwd); 15 | // 修改管理员密码 16 | router.post(`${config.contextPath}/system/admin/pwd/edit`, checkTokenHandler, controller.system.editAdminPwd); 17 | /** ** 菜单管理 ****/ 18 | // 获取菜单列表 19 | router.get(`${config.contextPath}/system/menu/list`, checkTokenHandler, controller.system.menuList); 20 | // 编辑菜单 21 | router.post(`${config.contextPath}/system/menu/edit`, checkTokenHandler, controller.system.editMenu); 22 | // 删除菜单 23 | router.post(`${config.contextPath}/system/menu/delete`, checkTokenHandler, controller.system.deleteMenu); 24 | /** ** 角色管理 ****/ 25 | // 获取角色列表 26 | router.get(`${config.contextPath}/system/role/list`, checkTokenHandler, controller.system.roleList); 27 | // 编辑角色 28 | router.post(`${config.contextPath}/system/role/edit`, checkTokenHandler, controller.system.editRole); 29 | // 编辑角色菜单 30 | router.post(`${config.contextPath}/system/role/menu/edit`, checkTokenHandler, controller.system.editRoleMenu); 31 | // 获取谷歌验证码绑定信息 32 | router.get(`${config.contextPath}/system/admin/open/google/auth`, checkTokenHandler, controller.system.openGoogleAuth); 33 | // 谷歌身份验证绑定 34 | router.get(`${config.contextPath}/system/admin/google/verify`, checkTokenHandler, controller.system.googleVerify); 35 | }; 36 | -------------------------------------------------------------------------------- /app/routers/task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @param {Egg.Application} app - egg application 5 | */ 6 | module.exports = app => { 7 | const { router, controller, config, middleware } = app; 8 | const checkTokenHandler = middleware.checkTokenHandler(); 9 | // 定时任务列表 10 | router.get(`${config.contextPath}/task/schedule/list`, checkTokenHandler, controller.task.scheduleList); 11 | // 修改/新增定时任务 12 | router.post(`${config.contextPath}/task/schedule/edit`, checkTokenHandler, controller.task.editSchedule); 13 | // 删除定时任务 14 | router.post(`${config.contextPath}/task/schedule/delete`, checkTokenHandler, controller.task.deleteSchedule); 15 | // 更新定时任务状态 16 | router.post(`${config.contextPath}/task/schedule/status/update`, checkTokenHandler, controller.task.updateStatusSchedule); 17 | // 执行任务 18 | router.post(`${config.contextPath}/task/schedule/run`, checkTokenHandler, controller.task.runSchedule); 19 | // 定时任务日志列表 20 | router.get(`${config.contextPath}/task/schedule/log/list`, checkTokenHandler, controller.task.scheduleLogList); 21 | // 获取任务执行日志详细信息 22 | router.get(`${config.contextPath}/task/schedule/log/detail`, checkTokenHandler, controller.task.scheduleLogDateil); 23 | }; 24 | -------------------------------------------------------------------------------- /app/service/loginService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Service } = require('egg'); 4 | const { ADMIN_EXPIRE_TIME, ADMIN_PREFIX } = require('../constants/redis'); 5 | const { RESULT_FAIL } = require('../constants/result'); 6 | const { getMd5 } = require('../utils'); 7 | const GlobalError = require('../utils/GlobalError'); 8 | 9 | /** 10 | * 登陆相关 11 | */ 12 | class LoginService extends Service { 13 | // 登陆 14 | async login({ username, password }) { 15 | const adminInfo = await this.app.mysql.get('sys_admin', { username, password: getMd5(password) }); 16 | if (adminInfo === null) throw new GlobalError(RESULT_FAIL, '用户名或密码错误'); 17 | 18 | // 构建token生成数据 19 | const result = { 20 | adminId: adminInfo.admin_id, 21 | username: adminInfo.username, 22 | avatarUrl: adminInfo.avatar_url, 23 | }; 24 | // 生成token 25 | const token = Buffer.from(JSON.stringify(result)).toString('base64'); 26 | 27 | // 获取菜单 28 | result.asyncRoutes = await this.generateMenu(adminInfo.admin_id); 29 | 30 | // 保存缓存 31 | await this.ctx.app.redis.set(ADMIN_PREFIX + token, JSON.stringify(result), 'Ex', ADMIN_EXPIRE_TIME); 32 | 33 | // 更新登录时间 34 | await this.app.mysql.update('sys_admin', { update_time: new Date() }, { where: { admin_id: adminInfo.admin_id } }); 35 | 36 | return token; 37 | } 38 | // 生成菜单 39 | async generateMenu(adminId) { 40 | let routers = await this.app.mysql.query('SELECT * FROM sys_menu WHERE menu_id IN (SELECT sys_roles_menus.menu_id FROM sys_admin, sys_roles_menus WHERE sys_admin.role_id = sys_roles_menus.role_id AND sys_admin.admin_id = ?) OR pid = 0', [ adminId ]); 41 | if (routers.length === 0) return []; 42 | 43 | routers = routers.map(router => { 44 | return { 45 | menu_id: router.menu_id, 46 | pid: router.pid, 47 | path: router.path, 48 | component: router.component, 49 | name: router.name, 50 | menu_sort: router.menu_sort, 51 | meta: { 52 | title: router.title, 53 | icon: router.icon, 54 | }, 55 | hidden: !!router.hidden, 56 | }; 57 | }); 58 | let routersByOne = routers.filter(router => router.pid === 0).sort((a, b) => a.menu_sort - b.menu_sort); 59 | const routersByTwo = routers.filter(router => router.pid !== 0); 60 | const tempObj = {}; 61 | routersByTwo.forEach(router => { 62 | if (!tempObj[router.pid] || tempObj[router.pid].length <= 0) { 63 | tempObj[router.pid] = [ router ]; 64 | } else { 65 | tempObj[router.pid].push(router); 66 | } 67 | }); 68 | routersByOne = routersByOne.map(router => { 69 | router.children = tempObj[router.menu_id] ? tempObj[router.menu_id].sort((a, b) => a.menu_sort - b.menu_sort) : []; 70 | return router; 71 | }); 72 | return routersByOne.filter(item => item.children.length > 0); 73 | } 74 | } 75 | 76 | module.exports = LoginService; 77 | -------------------------------------------------------------------------------- /app/service/scheduleService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Service } = require('egg'); 4 | 5 | class ScheduleService extends Service { 6 | /** 7 | * 测试处理程序 8 | * @param {*} params 任务参数 9 | * @param {*} jobHandlerLog 日志 10 | */ 11 | async testHandler(params, jobHandlerLog) { 12 | // 此处替换成具体业务代码 13 | await this.logger.info('我是测试任务,任务参数: %s', params); 14 | await jobHandlerLog.log('我是测试任务,任务参数: {0}', params); 15 | } 16 | /** 17 | * 测试调用接口任务 18 | * @param {*} params 任务参数 19 | * @param {*} jobHandlerLog 日志 20 | */ 21 | async testCurlHandler(params, jobHandlerLog) { 22 | // 获取参数 23 | const paramsObj = JSON.parse(params); 24 | const result = await this.ctx.curl(paramsObj.url, { 25 | method: paramsObj.method, 26 | data: paramsObj.data, 27 | dataType: 'json', 28 | }); 29 | await jobHandlerLog.log('测试调用接口任务,状态码:{0}', result.status); 30 | await jobHandlerLog.log('测试调用接口任务,响应数据:{0}', JSON.stringify(result.data)); 31 | } 32 | } 33 | 34 | module.exports = ScheduleService; 35 | -------------------------------------------------------------------------------- /app/service/systemService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Service } = require('egg'); 4 | const { RESULT_FAIL } = require('../constants/result'); 5 | const { generateAdminPwd, getMd5 } = require('../utils'); 6 | const GlobalError = require('../utils/GlobalError'); 7 | 8 | const QRCode = require('qrcode'); 9 | 10 | class SystemService extends Service { 11 | // 获取管理员账号 12 | async adminList() { 13 | return await this.app.mysql.query(`SELECT admin.admin_id adminId, admin.username username, admin.avatar_url avatarUrl, admin.status status, admin.role_id roleId, 14 | IFNULL(role.name, '') roleName, admin.create_by createBy, admin.create_time createTime, admin.update_by updateBy, admin.update_time updateTime FROM sys_admin admin 15 | LEFT JOIN sys_role role ON admin.role_id = role.role_id ORDER BY admin.create_time ASC;`); 16 | } 17 | // 编辑/新增管理员账号 18 | async editAdmin(userName, { adminId, username, avatarUrl = '', roleId }) { 19 | if (!adminId) { 20 | // 新增 21 | const pwd = generateAdminPwd(8); 22 | await this.app.mysql.insert('sys_admin', { 23 | username, 24 | avatar_url: avatarUrl, 25 | role_id: roleId, 26 | password: getMd5(pwd), 27 | create_time: new Date(), 28 | create_by: userName, 29 | update_time: new Date(), 30 | update_by: userName, 31 | }); 32 | return pwd; 33 | } 34 | // 修改 35 | await this.app.mysql.update('sys_admin', { 36 | update_time: new Date(), 37 | update_by: userName, 38 | username, 39 | avatar_url: avatarUrl, 40 | role_id: roleId, 41 | }, { where: { admin_id: adminId } }); 42 | } 43 | // 删除管理员 44 | async deleteAdmin({ adminId }) { 45 | if (adminId === 1) throw new GlobalError(RESULT_FAIL, '超级管理员禁止删除!!!'); 46 | await this.app.mysql.delete('sys_admin', { admin_id: adminId }); 47 | } 48 | // 重置管理员密码 49 | async resetAdminPwd(username, { adminId }) { 50 | const pwd = generateAdminPwd(8); 51 | await this.app.mysql.update('sys_admin', { password: getMd5(pwd), update_by: username, update_time: new Date() }, { where: { admin_id: adminId } }); 52 | return pwd; 53 | } 54 | // 修改管理员密码 55 | async editAdminPwd({ adminId, oldPwd, newPwd }) { 56 | const result = await this.app.mysql.get('sys_admin', { admin_id: adminId, password: getMd5(oldPwd) }); 57 | if (result === null) throw new GlobalError(RESULT_FAIL, '旧密码错误'); 58 | await this.app.mysql.update('sys_admin', { password: getMd5(newPwd) }, { where: { admin_id: adminId } }); 59 | } 60 | // 获取菜单列表 61 | async menuList() { 62 | const routers = await this.app.mysql.select('sys_menu', { where: { status: 0 } }); 63 | let routersByOne = routers.filter(router => router.pid === 0).sort((a, b) => a.menu_sort - b.menu_sort); 64 | const routersByTwo = routers.filter(router => router.pid !== 0); 65 | const tempObj = {}; 66 | routersByTwo.forEach(router => { 67 | if (!tempObj[router.pid] || tempObj[router.pid].length <= 0) { 68 | tempObj[router.pid] = [ router ]; 69 | } else { 70 | tempObj[router.pid].push(router); 71 | } 72 | }); 73 | routersByOne = routersByOne.map(router => { 74 | router.children = tempObj[router.menu_id] ? tempObj[router.menu_id].sort((a, b) => a.menu_sort - b.menu_sort) : []; 75 | return router; 76 | }); 77 | return routersByOne; 78 | } 79 | // 编辑菜单 80 | async editMenu(username, { menu_id, title, name, component, icon, path, redirect, pid, menu_sort, hidden }) { 81 | if (menu_id) { 82 | // 修改 83 | await this.app.mysql.update('sys_menu', { title, name, component, icon, path, redirect, pid, menu_sort, hidden, update_by: username, update_time: new Date() }, 84 | { where: { menu_id } }); 85 | } else { 86 | // 创建 87 | await this.app.mysql.insert('sys_menu', { title, name, component, icon, path, redirect: redirect || '', pid, menu_sort, hidden, update_by: username, 88 | update_time: new Date(), create_by: username, create_time: new Date() }); 89 | } 90 | } 91 | // 删除菜单 92 | async deleteMenu(username, { menu_id }) { 93 | this.app.mysql.update('sys_menu', { status: -1, update_by: username, update_time: new Date() }, { where: { menu_id } }); 94 | } 95 | // 获取角色列表 96 | async roleList() { 97 | const list = await this.app.mysql.query('SELECT sys_role.*, IFNULL(GROUP_CONCAT(sys_roles_menus.menu_id), \'\') menus FROM sys_role LEFT JOIN sys_roles_menus ON (sys_roles_menus.role_id = sys_role.role_id) GROUP BY sys_role.role_id'); 98 | return list.map(item => { 99 | item.menus = item.menus.split(',').map(Number); 100 | return item; 101 | }); 102 | } 103 | // 编辑角色 104 | async editRole(username, { role_id, name, description }) { 105 | if (role_id) { 106 | // 修改 107 | await this.app.mysql.update('sys_role', { name, description, update_by: username, update_time: new Date() }, { where: { role_id } }); 108 | } else { 109 | await this.app.mysql.insert('sys_role', { name, description, update_by: username, update_time: new Date(), create_by: username, create_time: new Date() }); 110 | } 111 | } 112 | // 编辑角色菜单 113 | async editRoleMenu({ role_id, menuIds }) { 114 | if (role_id === 1) throw new GlobalError(RESULT_FAIL, '超级管理员权限不允许更改'); 115 | // 删除当前所有绑定关系 116 | await this.app.mysql.delete('sys_roles_menus', { role_id }); 117 | // 保存更新后的绑定关系 118 | const insertArr = menuIds.map(id => { 119 | return { menu_id: id, role_id }; 120 | }); 121 | await this.app.mysql.insert('sys_roles_menus', insertArr); 122 | } 123 | // 获取谷歌验证码绑定信息 124 | async openGoogleAuth() { 125 | const { adminId } = ctx.request.headers; 126 | // 获取用户信息 127 | const adminInfo = await this.app.mysql.get('sys_admin', { admin_id: adminId }); 128 | if (adminInfo && adminInfo.google_secret_key !== '') throw new GlobalError(RESULT_FAIL, '您已开启谷歌身份验证!'); 129 | // 生成私钥 130 | const secretKey = await this.ctx.helper.generateGoogleSecretKey(); 131 | // 生成二维码信息 132 | const imgStr = await this.ctx.helper.generateGoogleQrCodeData(secretKey, adminInfo.username); 133 | return { 134 | googleImg: await QRCode.toDataURL(imgStr), 135 | googleKey: secretKey, 136 | googleUser: adminInfo.username, 137 | }; 138 | } 139 | // 谷歌身份验证绑定 140 | async googleVerify({ googleKey, googleCode, loginPwd }) { 141 | const { adminId } = ctx.request.headers; 142 | // 验证谷歌验证码 143 | const verify = await this.ctx.helper.googleAuthVerify(googleKey, googleCode); 144 | if (!verify) throw new GlobalError(RESULT_FAIL, '谷歌验证码不正确'); 145 | // 更新用户信息 146 | const result = await this.app.mysql.update('sys_admin', { google_secret_key: googleKey }, { where: { admin_id: adminId, password: getMd5(loginPwd) } }); 147 | if (result.affectedRows === 1) throw new GlobalError(RESULT_FAIL, '密码错误,请重新输入'); 148 | } 149 | } 150 | 151 | module.exports = SystemService; 152 | -------------------------------------------------------------------------------- /app/service/taskService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Service } = require('egg'); 4 | const { SCHEDULE_STATUS, SCHEDULE_TRIGGER_TYPE, SCHEDULE_RUN_MODE, SCHEDULE_DELETE, ACTIVE_KYES } = require('../constants'); 5 | const { RESULT_FAIL } = require('../constants/result'); 6 | const GlobalError = require('../utils/GlobalError'); 7 | 8 | /** 9 | * task Service 10 | */ 11 | class TaskService extends Service { 12 | // 定时任务管理 13 | async scheduleList({ page = 1, size = 20 }) { 14 | const limit = parseInt(size), 15 | offset = parseInt(page - 1) * parseInt(size); 16 | 17 | const [ list, total ] = await Promise.all([ 18 | this.app.mysql.select('schedule_job', { 19 | where: { is_delete: SCHEDULE_DELETE.MANUAL }, 20 | orders: [[ 'create_time', 'desc' ]], 21 | limit, 22 | offset, 23 | }), 24 | this.app.mysql.count('schedule_job'), 25 | ]); 26 | return { list, total }; 27 | } 28 | // 修改/新增定时任务 29 | async editSchedule(userName, { job_id, cron, jobName, runMode, jobHandler = '', runSource = '', params = '', description = '' }) { 30 | // 判断 31 | // const jobInfo = await this.app.mysql.get('schedule_job', { jobHandler }); 32 | const jobInfo = this.service.scheduleService[jobHandler]; 33 | if (!jobInfo && runMode === SCHEDULE_RUN_MODE.BEAN) throw new GlobalError(RESULT_FAIL, '任务处理程序不存在,请重新输入'); 34 | if (!job_id) { 35 | // 新增 36 | await this.app.mysql.insert('schedule_job', { 37 | cron, 38 | jobName, 39 | runMode, 40 | jobHandler, 41 | runSource, 42 | description, 43 | params, 44 | create_by: userName, 45 | update_by: userName, 46 | create_time: new Date(), 47 | update_time: new Date(), 48 | }); 49 | return; 50 | } 51 | // 修改时先判断任务状态 52 | const schedule = await this.app.mysql.get('schedule_job', { job_id }); 53 | if (schedule && schedule.status === SCHEDULE_STATUS.RUN) throw new GlobalError(RESULT_FAIL, '任务正在运行中,请停止后修改'); 54 | // 修改 55 | await this.app.mysql.update('schedule_job', { 56 | cron, 57 | jobName, 58 | runMode, 59 | jobHandler, 60 | runSource, 61 | description, 62 | params, 63 | update_by: userName, 64 | update_time: new Date(), 65 | }, { where: { job_id } }); 66 | 67 | // if (result.affectedRows === 1) { 68 | // const schedule = await this.app.mysql.get('schedule_job', { job_id }); 69 | // this.app.messenger.sendToApp(ACTIVE_KYES.UPDATE_SCHEDULS, schedule); 70 | // // 此处在版本允许的情况下可使用可选链操作符`?` 71 | // if (schedule && schedule.status === SCHEDULE_STATUS.RUN) { 72 | // // 启动状态下重置任务 73 | // await this.ctx.helper.cancelSchedule(jobName); 74 | // await this.ctx.helper.generateSchedule(job_id, cron, jobName, jobHandler); 75 | // } 76 | // } 77 | } 78 | // 删除定时任务 79 | async deleteSchedule({ job_id }) { 80 | const schedule = await this.app.mysql.get('schedule_job', { job_id }); 81 | if (schedule.status === SCHEDULE_STATUS.RUN) throw new GlobalError(RESULT_FAIL, '任务再运行中,如需删除请先停止任务'); 82 | await this.app.mysql.update('schedule_job', { is_delete: SCHEDULE_DELETE.DELETE }, { where: { job_id } }); 83 | } 84 | // 更新定时任务状态 85 | async updateStatusSchedule({ job_id, status }) { 86 | const result = await this.app.mysql.update('schedule_job', { status }, { where: { job_id } }); 87 | // 判断是否更新成功 88 | if (result.affectedRows === 1) { 89 | const schedule = await this.app.mysql.get('schedule_job', { job_id }); 90 | if (status === SCHEDULE_STATUS.RUN) { 91 | // 启动任务 92 | // await this.ctx.helper.generateSchedule(job_id, schedule.cron, schedule.jobName, schedule.jobHandler); 93 | this.app.messenger.sendToApp(ACTIVE_KYES.RUN_SCHEDULS, schedule); 94 | } else { 95 | // 停止任务 96 | // await this.ctx.helper.cancelSchedule(schedule.jobName); 97 | this.app.messenger.sendToApp(ACTIVE_KYES.STOP_SCHEDULS, schedule); 98 | } 99 | } 100 | } 101 | // 执行任务 102 | async runSchedule({ job_id }) { 103 | const schedule = await this.app.mysql.get('schedule_job', { job_id }); 104 | if (schedule === null) throw new GlobalError(RESULT_FAIL, '任务不存在'); 105 | 106 | await this.ctx.helper.executeSchedule(job_id, false, false, SCHEDULE_TRIGGER_TYPE.MANUAL); 107 | } 108 | // 获取任务执行日志 109 | async scheduleLogList({ job_id, page = 1, size = 20 }) { 110 | const limit = parseInt(size), 111 | offset = parseInt(page - 1) * parseInt(size); 112 | 113 | const [ list, total ] = await Promise.all([ 114 | this.app.mysql.query(`SELECT job.jobName jobName, log.id id, log.job_handler jobHandler, log.job_param jobParam, log.handle_time handleTime, 115 | log.job_status jobStatus, log.trigger_type triggerType, log.execution_status executionStatus, log.error_log errorLog FROM schedule_job job, 116 | schedule_job_log log WHERE job.job_id = log.job_id AND log.job_id = ? ORDER BY log.create_time DESC LIMIT ?,?`, [ job_id, offset, limit]), 117 | this.app.mysql.count('schedule_job_log', { job_id }) 118 | ]); 119 | 120 | return { list, total }; 121 | } 122 | // 获取任务执行日志详细信息 123 | async scheduleLogDateil({ id, error }) { 124 | const result = await this.app.mysql.get('schedule_job_log', { id }); 125 | 126 | return { detail: !error ? result.job_log : result.error_log, executionStatus: result.execution_status }; 127 | } 128 | } 129 | 130 | module.exports = TaskService; 131 | -------------------------------------------------------------------------------- /app/utils/GlobalError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 自定义异常 5 | * @param {*} code 异常状态吗 6 | * @param {*} msg 异常信息 7 | */ 8 | function GlobalError(code, msg) { 9 | this.code = code; 10 | this.messages = msg; 11 | } 12 | GlobalError.prototype = Object.create(Error.prototype); 13 | GlobalError.prototype.constructor = GlobalError; 14 | 15 | module.exports = GlobalError; 16 | -------------------------------------------------------------------------------- /app/utils/JobHandlerLog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { SCHEDULE_EXECUTION_STATUS, SCHEDULE_TRIGGER_TYPE } = require('../constants'); 4 | const { formatStr } = require('./index') 5 | 6 | class JobHandlerLog { 7 | constructor(app) { 8 | this.app = app; 9 | this.ctx = app.ctx; 10 | } 11 | 12 | // 初始化日志 13 | async init(schedule, triggerType = SCHEDULE_TRIGGER_TYPE.TASK) { 14 | const result = await this.app.mysql.insert('schedule_job_log', { 15 | job_id: schedule.job_id, 16 | job_handler: schedule.jobHandler, 17 | job_param: schedule.params, 18 | trigger_type: triggerType, 19 | error_log: '', 20 | job_log: `任务触发类型:${triggerType === SCHEDULE_TRIGGER_TYPE.TASK ? 'Cron触发' : '手动触发'}
`, 21 | }); 22 | this.id = result.insertId; 23 | } 24 | 25 | // 追加日志 26 | async log(logStr, ...args) { 27 | const content = formatStr(logStr, ...args); 28 | await this.app.mysql.query('UPDATE schedule_job_log SET job_log = CONCAT(job_log, ?) WHERE id = ?', [ `${content}
`, this.id ]); 29 | } 30 | 31 | // 记录执行异常日志 32 | async error(logStr, ...args) { 33 | const errorMsg = formatStr(logStr, ...args); 34 | await this.app.mysql.query('UPDATE schedule_job_log SET job_status = -1, error_log = ? WHERE id = ?', [ errorMsg, this.id ]); 35 | } 36 | 37 | // 定时任务执行结束 38 | async end() { 39 | await this.app.mysql.update('schedule_job_log', { execution_status: SCHEDULE_EXECUTION_STATUS.END }, { where: { id: this.id } }); 40 | } 41 | } 42 | 43 | module.exports = JobHandlerLog; 44 | -------------------------------------------------------------------------------- /app/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | const { RESULT_SUCC } = require('./../constants/result'); 6 | 7 | /** 8 | * 接口统一返回格式 9 | * @param {*} data 10 | * code:Number 状态码 11 | * message:String 提示,code非RESULT_SUCC时必传 12 | * data:Object 返回数据 13 | */ 14 | exports.setResult = data => { 15 | return { 16 | code: (data && data.code) || RESULT_SUCC, 17 | message: (data && data.messages) || 'success', 18 | data: data && data.data, 19 | }; 20 | }; 21 | 22 | /** 23 | * 生成管理员密码 24 | * @param {*} length 密码长度 25 | */ 26 | exports.generateAdminPwd = length => { 27 | const pasArr = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 28 | 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 29 | 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', '-', '$', '%', '&', '@', '+', '!' ]; 30 | let pwd = ''; 31 | for (let i = 0; i < length; i++) { 32 | pwd += pasArr[Math.floor(Math.random() * pasArr.length)]; 33 | } 34 | return pwd; 35 | }; 36 | 37 | /** 38 | * 获取md5 39 | * @param {*} str 字符串 40 | */ 41 | exports.getMd5 = str => { 42 | return crypto.createHash('md5').update(str).digest('hex'); 43 | }; 44 | 45 | /** 46 | * 生成管理员密码 47 | * @param {*} length 密码长度 48 | */ 49 | exports.generateAdminPwd = length => { 50 | const pasArr = [ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 51 | 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 52 | 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', '-', '$', '%', '&', '@', '+', '!' ]; 53 | let pwd = ''; 54 | for (let i = 0; i < length; i++) { 55 | pwd += pasArr[Math.floor(Math.random() * pasArr.length)]; 56 | } 57 | return pwd; 58 | }; 59 | 60 | /** 61 | * 模版字符串替换 62 | * @param {*} str 模版字符串 63 | * @param {*} args 替换值 64 | */ 65 | exports.formatStr = (str, ...args) => { 66 | if (str === '') return ''; 67 | for (const i in args) { 68 | str = str.replace(new RegExp('\\{' + i + '\\}', 'g'), args[i] || ''); 69 | } 70 | return str; 71 | }; 72 | -------------------------------------------------------------------------------- /app/utils/os.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | const process = require('child_process'); 5 | const { promisify } = require('util'); 6 | 7 | const { formatStr } = require('./'); 8 | 9 | const exec = promisify(process.exec); 10 | 11 | /** 12 | * 获取CPU使用情况 13 | */ 14 | exports.cpu = async () => { 15 | function cpuAverage() { 16 | // Initialise sum of idle and time of cores and fetch CPU info 17 | let totalIdle = 0, 18 | totalTick = 0; 19 | const cpus = os.cpus(); 20 | // Loop through CPU cores 21 | for (let i = 0, len = cpus.length; i < len; i++) { 22 | // Select CPU core 23 | const cpu = cpus[i]; 24 | // Total up the time in the cores tick 25 | for (const type in cpu.times) { 26 | totalTick += cpu.times[type]; 27 | } 28 | // Total up the idle time of the core 29 | totalIdle += cpu.times.idle; 30 | } 31 | // Return the average Idle and Tick times 32 | return { idle: totalIdle / cpus.length, total: totalTick / cpus.length }; 33 | } 34 | 35 | const startMeasure = cpuAverage(); 36 | return new Promise(resolve => { 37 | setTimeout(function() { 38 | // Grab second Measure 39 | const endMeasure = cpuAverage(); 40 | // Calculate the difference in idle and total time between the measures 41 | const idleDifference = endMeasure.idle - startMeasure.idle; 42 | const totalDifference = endMeasure.total - startMeasure.total; 43 | // Calculate the average percentage CPU usage 44 | const percentageCPU = 100 - ~~(100 * idleDifference / totalDifference); 45 | // Output result to console 46 | resolve({ used: percentageCPU, name: os.cpus()[0].model, threadNumber: os.cpus().length }); 47 | }, 100); 48 | }); 49 | }; 50 | 51 | /** 52 | * 获取内存使用情况 53 | * *mac系统获取存在误差* 54 | */ 55 | exports.mem = async () => { 56 | return new Promise(async resolve => { 57 | let totalmem = 0, 58 | freemem = 0, 59 | usedmem = 0, 60 | usageRate = 0; 61 | 62 | if (os.type() === 'Linux') { 63 | const { stdout } = await exec('free -m'); 64 | const str = stdout.split('\n')[1].split(' ').filter(item => item != ''); 65 | 66 | totalmem = str[1]; 67 | freemem = str[1] - str[2]; 68 | usedmem = str[2]; 69 | usageRate = (usedmem / totalmem * 100).toFixed(2); 70 | } else { 71 | totalmem = (os.totalmem() / 1024 / 1024 / 1024).toFixed(2); 72 | freemem = (os.freemem() / 1024 / 1024 / 1024).toFixed(2); 73 | usedmem = ((os.totalmem() - os.freemem()) / 1024 / 1024 / 1024).toFixed(2); 74 | usageRate = parseInt(usedmem / totalmem * 100); 75 | } 76 | 77 | resolve({ totalmem, freemem, usedmem, usageRate }); 78 | }); 79 | }; 80 | 81 | /** 82 | * 获取系统相关信息 83 | */ 84 | exports.sys = async () => { 85 | // 获取系统运行时间 86 | let date = '', 87 | sys = '', 88 | ip = ''; 89 | 90 | const time = os.uptime(); 91 | const day = Math.floor(time / 86400); 92 | const hour = Math.floor((time - day * 86400) / 3600); 93 | const minute = Math.floor((time - day * 86400 - hour * 3600) / 60); 94 | const second = Math.floor(time - day * 86400 - hour * 3600 - minute * 60); 95 | 96 | date = formatStr('{0}天{1}时{2}分{3}秒', day, hour, minute, second); 97 | 98 | // 获取系统信息 99 | if (os.type() === 'Linux') { 100 | const { stdout } = await exec('cat /etc/redhat-release'); 101 | sys = stdout.trim(); 102 | } else if (os.type() === 'Darwin') { 103 | const { stdout } = await exec('sw_vers'); 104 | stdout.split('\n').forEach(item => { 105 | sys += item.split(':')[1] ? item.split(':')[1] : ''; 106 | }); 107 | sys = sys.trim(); 108 | } else if (os.type() === 'Windows_NT') { 109 | const { stdout } = await exec('ver'); 110 | sys = stdout.trim(); 111 | } 112 | 113 | ip = '39.99.238.155'; 114 | 115 | // 获取系统负载 116 | const loadavg = os.loadavg(); 117 | const loadavg1m = loadavg[0].toFixed(2); 118 | const loadavg5m = loadavg[1].toFixed(2); 119 | const loadavg12m = loadavg[2].toFixed(2); 120 | 121 | return Promise.resolve({ date, sys, ip, loadavg1m, loadavg5m, loadavg12m }); 122 | }; 123 | 124 | /** 125 | * 获取磁盘使用情况 126 | * *目前只能获取linux、windows系统使用情况* 127 | */ 128 | exports.disk = async () => { 129 | let total = 0, 130 | available = 0, 131 | used = 0, 132 | usageRate = 0; 133 | if (os.type() === 'Windows_NT') { 134 | let { stdout } = await exec('df -lh'); 135 | stdout = stdout.split('\n').filter(item => item.length > 0); 136 | stdout.shift(); 137 | stdout.forEach(line => { 138 | line = line.split(' ').filter(item => item != ''); 139 | total += parseFloat(line[1]); 140 | available += parseFloat(line[3]); 141 | used += parseFloat(line[1]) * (parseFloat(line[4]) / 100); 142 | }); 143 | usageRate = (used / total * 100).toFixed(2); 144 | } else { 145 | let { stdout } = await exec('df -hl /'); 146 | stdout = stdout.split('\n')[1].split(' ').filter(item => item != ''); 147 | 148 | total = stdout[1]; 149 | available = stdout[3]; 150 | used = parseFloat(stdout[1]) * (parseFloat(stdout[4]) / 100); 151 | usageRate = parseFloat(stdout[4]); 152 | } 153 | 154 | return Promise.resolve({ total, available, used, usageRate }); 155 | }; 156 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '10' 4 | 5 | install: 6 | - ps: Install-Product node $env:nodejs_version 7 | - npm i npminstall@5 && node_modules\.bin\npminstall 8 | 9 | test_script: 10 | - node --version 11 | - npm --version 12 | - npm run test 13 | 14 | build: off 15 | -------------------------------------------------------------------------------- /config/config.default.js: -------------------------------------------------------------------------------- 1 | /* eslint valid-jsdoc: "off" */ 2 | 3 | 'use strict'; 4 | 5 | const { setResult } = require('../app/utils'); 6 | const { RESULT_FAIL } = require('../app/constants/result'); 7 | 8 | /** 9 | * @param {Egg.EggAppInfo} appInfo app info 10 | */ 11 | module.exports = appInfo => { 12 | /** 13 | * built-in config 14 | * @type {Egg.EggAppConfig} 15 | **/ 16 | const config = exports = {}; 17 | 18 | // use for cookie sign key, should change to your own and keep security 19 | config.keys = appInfo.name + '_1607609908869_9400'; 20 | 21 | // add your middleware config here 22 | config.middleware = [ 'logHandler', 'errorHandler' ]; 23 | 24 | // add your user config here 25 | const userConfig = { 26 | // myAppName: 'egg', 27 | }; 28 | 29 | config.contextPath = '/api'; 30 | 31 | config.proxy = true; 32 | 33 | /** 启动端口配置 */ 34 | config.cluster = { 35 | listen: { 36 | port: 7002, 37 | }, 38 | }; 39 | 40 | /** 跨域,仅用于本地环境 */ 41 | config.security = { 42 | csrf: { 43 | enable: false, 44 | }, 45 | domainWhiteList: [ '*' ], 46 | }; 47 | 48 | /** mysql配置 */ 49 | config.mysql = { 50 | client: { 51 | // host 52 | host: 'rm-wz96yk7n9ka06t3eh.mysql.rds.aliyuncs.com', 53 | // 端口号 54 | port: '3306', 55 | // 用户名 56 | user: 'admin_demo_user', 57 | // 密码 58 | password: 'admin_pwd@2020', 59 | // 数据库名 60 | database: 'admin_demo', 61 | }, 62 | }; 63 | 64 | /** redis配置 */ 65 | config.redis = { 66 | client: { 67 | port: 6379, 68 | host: 'adminDemodb', 69 | password: '2020redis!', 70 | db: 0, 71 | }, 72 | }; 73 | 74 | config.googleAuth = { 75 | appName: 'AdminDemo', 76 | }; 77 | 78 | // 性能监控 79 | config.alinode = { 80 | server: 'wss://agentserver.node.aliyun.com:8080', 81 | appid: process.env.ADMIN_DEMO_ALINODE_APPID, 82 | secret: process.env.ADMIN_DEMO_ALINODE_APPSECRET, 83 | }; 84 | 85 | /** 运行异常 */ 86 | config.onerror = { 87 | all(err, ctx) { 88 | // 记录一条错误日志 89 | ctx.app.emit('error', err, ctx); 90 | ctx.body = setResult(RESULT_FAIL, '服务器繁忙'); 91 | }, 92 | }; 93 | 94 | return { 95 | ...config, 96 | ...userConfig, 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /config/plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.mysql = { 4 | enable: true, 5 | package: 'egg-mysql', 6 | }; 7 | 8 | exports.redis = { 9 | enable: true, 10 | package: 'egg-redis', 11 | }; 12 | 13 | exports.websocket = { 14 | enable: true, 15 | package: 'egg-websocket-plugin', 16 | }; 17 | 18 | exports.googleAuth = { 19 | enable: true, 20 | package: 'egg-google-auth', 21 | }; 22 | 23 | exports.alinode = { 24 | enable: true, 25 | package: 'egg-alinode', 26 | }; 27 | -------------------------------------------------------------------------------- /docs/admin_demo.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat Premium Data Transfer 3 | 4 | Source Server : uat-mysql 5 | Source Server Type : MySQL 6 | Source Server Version : 50736 7 | Source Host : 39.99.238.155:3306 8 | Source Schema : admin_demo 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50736 12 | File Encoding : 65001 13 | 14 | Date: 26/04/2022 11:38:08 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for schedule_job 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `schedule_job`; 24 | CREATE TABLE `schedule_job` ( 25 | `job_id` int(11) NOT NULL AUTO_INCREMENT, 26 | `cron` varchar(50) NOT NULL DEFAULT '' COMMENT 'cron表达式', 27 | `jobName` varchar(100) NOT NULL DEFAULT '' COMMENT '任务名', 28 | `jobHandler` varchar(100) NOT NULL DEFAULT '' COMMENT '任务处理方法', 29 | `params` varchar(255) NOT NULL COMMENT '参数', 30 | `description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述', 31 | `runMode` tinyint(1) NOT NULL DEFAULT '0' COMMENT '运行模式\n\n0: BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务\n1: Shell模式:任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本\n', 32 | `runSource` mediumtext COMMENT '源代码', 33 | `status` int(1) NOT NULL DEFAULT '-1' COMMENT '状态 0启用 -1停止', 34 | `create_by` varchar(100) NOT NULL COMMENT '创建人', 35 | `update_by` varchar(100) NOT NULL COMMENT '更新人', 36 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 37 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 38 | `is_delete` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除:0:未删除 -1:删除', 39 | PRIMARY KEY (`job_id`) USING BTREE, 40 | UNIQUE KEY `ind_id` (`job_id`) USING BTREE, 41 | KEY `ind_handler` (`jobHandler`) USING BTREE 42 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='定时任务表'; 43 | 44 | -- ---------------------------- 45 | -- Records of schedule_job 46 | -- ---------------------------- 47 | BEGIN; 48 | INSERT INTO `schedule_job` VALUES (1, '*/5 * * * * *', 'testHandler', 'testHandler', '', '', 0, '', -1, 'admin', 'admin', '2022-04-26 11:19:52', '2022-04-26 11:35:18', 0); 49 | INSERT INTO `schedule_job` VALUES (2, '0 0 0 * * *', 'testCurlHandler', 'testCurlHandler', '{\n \"url\": \"http://daodi-herbs.com/more_system/demo.php\",\n \"method\": \"POST\",\n \"data\": {\n \"name\": \"XXX\",\n \"address\": \"123\"\n }\n}', '', 0, '', -1, 'admin', 'admin', '2022-01-21 19:10:08', '2022-04-26 11:23:14', 0); 50 | INSERT INTO `schedule_job` VALUES (3, '0 0 0 * * *', 'testShell', '', 'wq21', '123213', 1, '#!/bin/sh\n\n# 由于当前实现原因,接受参数应从第二位开始\n\nnode -v\n\nnpm -v\n\necho \"hello shell\"\n\nexit 0', -1, 'admin', 'admin', '2022-03-26 22:57:45', '2022-04-26 11:27:06', 0); 51 | COMMIT; 52 | 53 | -- ---------------------------- 54 | -- Table structure for schedule_job_log 55 | -- ---------------------------- 56 | DROP TABLE IF EXISTS `schedule_job_log`; 57 | CREATE TABLE `schedule_job_log` ( 58 | `id` int(11) NOT NULL AUTO_INCREMENT, 59 | `job_id` int(11) NOT NULL COMMENT '任务ID', 60 | `job_handler` varchar(100) NOT NULL DEFAULT '' COMMENT '任务处理方法', 61 | `job_param` varchar(255) NOT NULL DEFAULT '' COMMENT '任务参数', 62 | `handle_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '任务执行时间', 63 | `job_log` text NOT NULL COMMENT '任务日志', 64 | `job_status` int(1) NOT NULL DEFAULT '0' COMMENT '任务执行状态:0-成功 -1-失败', 65 | `error_log` text NOT NULL COMMENT '任务异常日志', 66 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 67 | `trigger_type` int(1) NOT NULL DEFAULT '0' COMMENT '触发类型:0-任务触发 1-手动触发', 68 | `execution_status` int(1) NOT NULL DEFAULT '0' COMMENT '任务状态:0-执行中 1-执行完成', 69 | PRIMARY KEY (`id`) USING BTREE, 70 | KEY `ind_job_id` (`job_id`), 71 | KEY `ind_job_handler` (`job_handler`), 72 | KEY `ind_job_status` (`job_status`), 73 | KEY `ind_execution_status` (`execution_status`), 74 | KEY `ind_trigger_type` (`trigger_type`) 75 | ) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='定时任务执行日志'; 76 | 77 | -- ---------------------------- 78 | -- Records of schedule_job_log 79 | -- ---------------------------- 80 | BEGIN; 81 | COMMIT; 82 | 83 | -- ---------------------------- 84 | -- Table structure for sys_admin 85 | -- ---------------------------- 86 | DROP TABLE IF EXISTS `sys_admin`; 87 | CREATE TABLE `sys_admin` ( 88 | `admin_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '管理员ID', 89 | `username` varchar(255) NOT NULL DEFAULT '' COMMENT '用户名', 90 | `avatar_url` varchar(255) NOT NULL DEFAULT '' COMMENT '头像', 91 | `password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码', 92 | `role_id` int(2) NOT NULL DEFAULT '0' COMMENT '角色', 93 | `google_secret_key` varchar(100) NOT NULL DEFAULT '' COMMENT '谷歌私钥', 94 | `status` int(1) NOT NULL DEFAULT '0' COMMENT '状态', 95 | `create_by` varchar(255) NOT NULL DEFAULT '' COMMENT '创建人', 96 | `update_by` varchar(255) NOT NULL DEFAULT '' COMMENT '更新人', 97 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 98 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 99 | PRIMARY KEY (`admin_id`) USING BTREE 100 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系统管理员'; 101 | 102 | -- ---------------------------- 103 | -- Records of sys_admin 104 | -- ---------------------------- 105 | BEGIN; 106 | INSERT INTO `sys_admin` VALUES (1, 'admin', 'https://oss-blog.myjerry.cn/files/avatar/blog-avatar.jpg', 'e10adc3949ba59abbe56e057f20f883e', 1, '', 0, 'system', 'system', '2020-12-11 06:28:54', '2022-04-26 11:15:22'); 107 | COMMIT; 108 | 109 | -- ---------------------------- 110 | -- Table structure for sys_log 111 | -- ---------------------------- 112 | DROP TABLE IF EXISTS `sys_log`; 113 | CREATE TABLE `sys_log` ( 114 | `id` int(11) NOT NULL AUTO_INCREMENT, 115 | `admin_id` int(11) NOT NULL COMMENT '操作人ID', 116 | `role_id` int(11) NOT NULL COMMENT '操作人角色', 117 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 118 | `ip` varchar(50) NOT NULL DEFAULT '' COMMENT '操作人IP', 119 | `client` varchar(100) NOT NULL DEFAULT '' COMMENT '操作人客户端信息', 120 | `content` varchar(255) NOT NULL DEFAULT '' COMMENT '操作内容', 121 | `level` tinyint(1) NOT NULL DEFAULT '0' COMMENT '操作等级', 122 | PRIMARY KEY (`id`), 123 | KEY `ind_admin_id` (`admin_id`) USING BTREE, 124 | KEY `ind_ip` (`ip`) USING BTREE, 125 | KEY `ind_level` (`level`) USING BTREE 126 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统日志'; 127 | 128 | -- ---------------------------- 129 | -- Records of sys_log 130 | -- ---------------------------- 131 | BEGIN; 132 | COMMIT; 133 | 134 | -- ---------------------------- 135 | -- Table structure for sys_menu 136 | -- ---------------------------- 137 | DROP TABLE IF EXISTS `sys_menu`; 138 | CREATE TABLE `sys_menu` ( 139 | `menu_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', 140 | `pid` int(11) NOT NULL DEFAULT '0' COMMENT '上一级菜单ID', 141 | `title` varchar(255) NOT NULL COMMENT '菜单标题', 142 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '组件名称', 143 | `component` varchar(255) NOT NULL DEFAULT '' COMMENT '组件', 144 | `menu_sort` int(2) NOT NULL DEFAULT '0' COMMENT '排序', 145 | `icon` varchar(255) NOT NULL DEFAULT '' COMMENT '图标', 146 | `path` varchar(255) NOT NULL DEFAULT '' COMMENT '路径', 147 | `redirect` varchar(255) NOT NULL DEFAULT '' COMMENT '重定向', 148 | `status` int(1) NOT NULL DEFAULT '0' COMMENT '状态', 149 | `create_by` varchar(255) NOT NULL DEFAULT '' COMMENT '创建人', 150 | `update_by` varchar(255) NOT NULL COMMENT '更新人', 151 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 152 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', 153 | `hidden` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否显示', 154 | PRIMARY KEY (`menu_id`) USING BTREE 155 | ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系统菜单'; 156 | 157 | -- ---------------------------- 158 | -- Records of sys_menu 159 | -- ---------------------------- 160 | BEGIN; 161 | INSERT INTO `sys_menu` VALUES (1, 0, '系统管理', 'System', 'layout', 99, 'setting', 'system', '/system/admin', 0, 'admin', 'admin', '2020-12-13 04:42:46', '2020-12-30 23:00:57', 0); 162 | INSERT INTO `sys_menu` VALUES (3, 1, '账号管理', 'SystemAdmin', 'system/admin', 1, 'eye-open', 'admin', '', 0, 'admin', 'admin', '2020-12-13 04:57:48', '2022-03-29 12:03:49', 0); 163 | INSERT INTO `sys_menu` VALUES (4, 1, '菜单管理', 'SystemMenu', 'system/menu', 2, 'menu', 'menu', '', -1, 'admin', 'admin', '2020-12-13 04:58:25', '2022-04-09 03:35:16', 0); 164 | INSERT INTO `sys_menu` VALUES (5, 1, '角色管理', 'SystemRole', 'system/role', 3, 'user', 'role', '', 0, 'admin', 'admin', '2020-12-13 04:59:11', '2021-07-29 04:55:26', 0); 165 | INSERT INTO `sys_menu` VALUES (6, 0, '任务管理', 'Task', 'layout', 1, 'task', 'task', '/task/schedule', 0, 'admin', 'admin', '2020-12-15 23:13:09', '2022-01-20 12:41:13', 0); 166 | INSERT INTO `sys_menu` VALUES (7, 6, '定时任务管理', 'TaskSchedule', 'task/schedule', 2, 'schedule', 'schedule', '', 0, 'admin', 'admin', '2020-12-15 23:15:54', '2022-04-02 17:49:12', 0); 167 | COMMIT; 168 | 169 | -- ---------------------------- 170 | -- Table structure for sys_role 171 | -- ---------------------------- 172 | DROP TABLE IF EXISTS `sys_role`; 173 | CREATE TABLE `sys_role` ( 174 | `role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID', 175 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '角色名', 176 | `description` varchar(255) NOT NULL DEFAULT '' COMMENT '描述', 177 | `create_by` varchar(255) NOT NULL DEFAULT '' COMMENT '创建人', 178 | `update_by` varchar(255) NOT NULL DEFAULT '' COMMENT '更新人', 179 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 180 | `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间', 181 | PRIMARY KEY (`role_id`) USING BTREE 182 | ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色表'; 183 | 184 | -- ---------------------------- 185 | -- Records of sys_role 186 | -- ---------------------------- 187 | BEGIN; 188 | INSERT INTO `sys_role` VALUES (1, '超级管理员', '拥有系统所有权限', 'admin', 'admin', '2020-12-13 04:55:17', '2021-09-04 00:56:58'); 189 | INSERT INTO `sys_role` VALUES (2, '运营', '运营角色', 'admin', 'admin', '2020-12-16 01:08:16', '2021-11-30 17:00:39'); 190 | INSERT INTO `sys_role` VALUES (3, '业务员', '业务员', 'admin', 'admin', '2021-09-17 06:06:59', '2021-09-18 17:06:37'); 191 | INSERT INTO `sys_role` VALUES (4, '1', '1', 'admin', 'admin', '2021-12-22 10:04:33', '2021-12-22 10:04:33'); 192 | INSERT INTO `sys_role` VALUES (5, 'v', '方法', 'admin', 'admin', '2022-04-25 12:00:02', '2022-04-25 12:00:02'); 193 | COMMIT; 194 | 195 | -- ---------------------------- 196 | -- Table structure for sys_roles_menus 197 | -- ---------------------------- 198 | DROP TABLE IF EXISTS `sys_roles_menus`; 199 | CREATE TABLE `sys_roles_menus` ( 200 | `menu_id` int(11) NOT NULL COMMENT '菜单ID', 201 | `role_id` int(11) NOT NULL COMMENT '角色ID' 202 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色菜单关联'; 203 | 204 | -- ---------------------------- 205 | -- Records of sys_roles_menus 206 | -- ---------------------------- 207 | BEGIN; 208 | INSERT INTO `sys_roles_menus` VALUES (7, 1); 209 | INSERT INTO `sys_roles_menus` VALUES (3, 1); 210 | INSERT INTO `sys_roles_menus` VALUES (4, 1); 211 | INSERT INTO `sys_roles_menus` VALUES (5, 1); 212 | INSERT INTO `sys_roles_menus` VALUES (7, 2); 213 | INSERT INTO `sys_roles_menus` VALUES (7, 3); 214 | INSERT INTO `sys_roles_menus` VALUES (4, 3); 215 | INSERT INTO `sys_roles_menus` VALUES (5, 3); 216 | INSERT INTO `sys_roles_menus` VALUES (4, 4); 217 | INSERT INTO `sys_roles_menus` VALUES (5, 4); 218 | COMMIT; 219 | 220 | SET FOREIGN_KEY_CHECKS = 1; 221 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "**/*" 4 | ] 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "egg": { 7 | "declarations": true 8 | }, 9 | "dependencies": { 10 | "egg": "^2.15.1", 11 | "egg-alinode": "^2.0.1", 12 | "egg-google-auth": "^0.0.3", 13 | "egg-mysql": "^3.0.0", 14 | "egg-redis": "^2.4.0", 15 | "egg-scripts": "^2.11.0", 16 | "egg-websocket-plugin": "^1.0.1", 17 | "jsonwebtoken": "^8.5.1", 18 | "node-schedule": "^1.3.2", 19 | "node-uuid": "^1.4.8", 20 | "qrcode": "^1.4.4" 21 | }, 22 | "devDependencies": { 23 | "autod": "^3.0.1", 24 | "autod-egg": "^1.1.0", 25 | "egg-bin": "^4.11.0", 26 | "egg-ci": "^1.11.0", 27 | "egg-mock": "^3.21.0", 28 | "eslint": "^5.13.0", 29 | "eslint-config-egg": "^7.1.0" 30 | }, 31 | "engines": { 32 | "node": ">=10.0.0" 33 | }, 34 | "scripts": { 35 | "start": "egg-scripts start --daemon --title=egg-server-admin-server", 36 | "stop": "egg-scripts stop --title=egg-server-admin-server", 37 | "dev": "egg-bin dev", 38 | "debug": "egg-bin debug", 39 | "test": "npm run lint -- --fix && npm run test-local", 40 | "test-local": "egg-bin test", 41 | "cov": "egg-bin cov", 42 | "lint": "eslint .", 43 | "ci": "npm run lint && npm run cov", 44 | "autod": "autod" 45 | }, 46 | "ci": { 47 | "version": "10" 48 | }, 49 | "repository": { 50 | "type": "git", 51 | "url": "" 52 | }, 53 | "author": "Jerry", 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /test/app/controller/home.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, assert } = require('egg-mock/bootstrap'); 4 | // const { getMd5 } = require('../../../app/utils'); 5 | 6 | describe('test/app/controller/home.test.js', () => { 7 | it('should assert', () => { 8 | const pkg = require('../../../package.json'); 9 | assert(app.config.keys.startsWith(pkg.name)); 10 | 11 | // const ctx = app.mockContext({}); 12 | // yield ctx.service.xx(); 13 | }); 14 | 15 | it('should GET /', async () => { 16 | // console.log(getMd5('123456')); 17 | const ctx = app.mockContext(); 18 | // const result = await ctx.app.mysql.select('sys_admin'); 19 | // const result = await ctx.app.mysql.update('sys_menu', { title: '角色管理1' }, { where: { menu_id: 5 } }); 20 | // const result = await ctx.app.mysql.get('sys_menu', { menu_id: 5 }); 21 | const result = await ctx.app.mysql.insert('schedule_job_log', { job_id: 6 }); 22 | console.log(result); 23 | // return app.httpRequest() 24 | // .get('/') 25 | // .expect('hi, egg') 26 | // .expect(200); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------