├── .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 |
--------------------------------------------------------------------------------