lrf402788946 4 years ago
commit
a5a8f6809e

+ 29 - 0
.autod.conf.js

@@ -0,0 +1,29 @@
+'use strict';
+
+module.exports = {
+  write: true,
+  prefix: '^',
+  plugin: 'autod-egg',
+  test: [
+    'test',
+    'benchmark',
+  ],
+  dep: [
+    'egg',
+    'egg-scripts',
+  ],
+  devdep: [
+    'egg-ci',
+    'egg-bin',
+    'egg-mock',
+    'autod',
+    'autod-egg',
+    'eslint',
+    'eslint-config-egg',
+  ],
+  exclude: [
+    './test/fixtures',
+    './dist',
+  ],
+};
+

+ 1 - 0
.eslintignore

@@ -0,0 +1 @@
+coverage

+ 3 - 0
.eslintrc

@@ -0,0 +1,3 @@
+{
+  "extends": "eslint-config-egg"
+}

+ 42 - 0
.github/workflows/nodejs.yml

@@ -0,0 +1,42 @@
+# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
+
+name: Node.js CI
+
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+  schedule:
+    - cron: '0 2 * * *'
+
+jobs:
+  build:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      fail-fast: false
+      matrix:
+        node-version: [10]
+        os: [ubuntu-latest, windows-latest, macos-latest]
+
+    steps:
+    - name: Checkout Git Source
+      uses: actions/checkout@v2
+
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+
+    - name: Install Dependencies
+      run: npm i -g npminstall && npminstall
+
+    - name: Continuous Integration
+      run: npm run ci
+
+    - name: Code Coverage
+      uses: codecov/codecov-action@v1
+      with:
+        token: ${{ secrets.CODECOV_TOKEN }}

+ 15 - 0
.gitignore

@@ -0,0 +1,15 @@
+logs/
+npm-debug.log
+yarn-error.log
+node_modules/
+package-lock.json
+yarn.lock
+coverage/
+.idea/
+run/
+.DS_Store
+*.sw*
+*.un~
+typings/
+.nyc_output/
+package-lock.json

+ 12 - 0
.travis.yml

@@ -0,0 +1,12 @@
+
+language: node_js
+node_js:
+  - '10'
+before_install:
+  - npm i npminstall -g
+install:
+  - npminstall
+script:
+  - npm run ci
+after_script:
+  - npminstall codecov && codecov

+ 18 - 0
README.md

@@ -0,0 +1,18 @@
+# mission
+
+任务系统
+
+### TODO util,rabbitmq的queue队列未完成
+> 将util中的rabbitmq.js放到service中,将app.js放到根目录中
+
+## 参数
+
+### create:最主要的是params和tenant(项目) params:
+  |参数名|必要|说明|
+  |-----|----|----|
+  |project|否|项目名|
+  |router|否|路由|
+  |service|是|服务|
+  |method|是|函数|
+  |body|否|post方法的参数|
+  |query|否|地址栏参数|

+ 42 - 0
app/controller/.mission.js

@@ -0,0 +1,42 @@
+module.exports = {
+  create: {
+    requestBody: ["title", "params", "user", "!tenant"],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: ["title", "params", "status", "dot", "progress", "uri"],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        title: "title",
+        create_time: "create_time",
+        status: "status",
+        dot: "dot",
+        user: "user",
+        project: "params.project",
+      },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      desc: true,
+      count: true,
+    },
+  },
+  start: {
+    params: ["!id"],
+    service: "start",
+  },
+};

+ 12 - 0
app/controller/home.js

@@ -0,0 +1,12 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class HomeController extends Controller {
+  async index() {
+    const { ctx } = this;
+    ctx.body = 'hi, egg';
+  }
+}
+
+module.exports = HomeController;

+ 24 - 0
app/controller/mission.js

@@ -0,0 +1,24 @@
+'use strict';
+
+const _ = require('lodash');
+const meta = require('./.mission.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 教师申请讲课表管理
+class MissionController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.mission;
+  }
+  async count() {
+    const res = await this.service.count(this.ctx.query);
+    this.ctx.ok({ total: res });
+  }
+  async progress() {
+    await this.service.updateProgress(this.ctx.request.body);
+    this.ctx.ok();
+  }
+}
+
+module.exports = CrudController(MissionController, meta);

+ 39 - 0
app/model/mission.js

@@ -0,0 +1,39 @@
+"use strict";
+const moment = require("moment");
+const Schema = require("mongoose").Schema;
+const metaPlugin = require("naf-framework-mongoose/lib/model/meta-plugin");
+// params不做限制,方便扩展,Object就行
+const params = {
+  project: { type: String, required: true }, // 项目名称(需要设置好,然后找对应路由前置)
+  router: { type: String, required: true }, // 要执行的路由
+  service: { type: String, required: true }, //服务
+  method: { type: String, required: true }, // 方法, get,post,delete...
+  type: { type: String, required: false }, // 类型,需要怎么处理;例如,文件,需要下载
+  body: { type: [Object, Array] },
+  query: { type: Object },
+};
+
+const Mission = {
+  title: { type: String, required: true, maxLength: 200 }, // 教师
+  create_time: {
+    type: String,
+    required: false,
+    maxLength: 200,
+    default: moment().format("YYYY-MM-DD HH:SS:mm"),
+  }, // 创建时间
+  params: { type: Object, required: false },
+  status: { type: String, maxLength: 200, default: "0" }, // 状态:0=>未开始;1=>正在进行;2=>已完成;3=>失败
+  dot: { type: Boolean, default: true }, // 需要提醒
+  progress: { type: String, required: false, maxLength: 200 },
+  user: { type: String, required: false, maxLength: 200 }, // 用户,监听指定用户用,没有的话,mq就监听项目
+  uri: { type: String, required: false }, // 文件/图片等需要地址的地方就放这
+  tenant: { type: String, required: true }, //所属项目
+};
+const schema = new Schema(Mission, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.plugin(metaPlugin);
+
+module.exports = (app) => {
+  const { mongoose } = app;
+  return mongoose.model("Mission", schema, "mission");
+};

+ 14 - 0
app/router.js

@@ -0,0 +1,14 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+  router.get('/', controller.home.index);
+  router.post('mission', '/api/mission/progress', controller.mission.progress);
+  router.get('mission', '/api/mission/count', controller.mission.count);
+  router.post('mission', '/api/mission/start/:id', controller.mission.start);
+  router.resources('mission', '/api/mission', controller.mission); // index、create、show、destroy
+  router.post('mission', '/api/mission/update/:id', controller.mission.update);
+};

+ 114 - 0
app/service/mission.js

@@ -0,0 +1,114 @@
+'use strict';
+
+const assert = require('assert');
+const _ = require('lodash');
+const moment = require('moment');
+const { ObjectId } = require('mongoose').Types;
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+
+class MissionService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'mission');
+    this.model = this.ctx.model.Mission;
+    this.mq = this.ctx.mq;
+  }
+
+  async create(data) {
+    const res = await this.model.create(data);
+    await this.toMq({ id: res.id });
+    await this.start({ id: res.id });
+    return res;
+  }
+
+  async update({ id }, { params, ...data }) {
+    // 需要精确修改,所以分成2部分去更改,一部分是非params部分,一部分是params更新
+    const obj = {};
+    if (params) {
+      const keys = Object.keys(params);
+      for (const key of keys) {
+        obj[`params.${key}`] = params[key];
+      }
+    }
+    if (data) {
+      const dks = Object.keys(data);
+      for (const dk of dks) {
+        obj[dk] = data[dk];
+      }
+    }
+    if (obj.status === '2') obj.progress = '100';
+    const mission = await this.model.update({ _id: ObjectId(id) }, obj);
+    await this.toMq({ id });
+    return mission;
+  }
+
+  async delete({ id }) {
+    const res = await this.model.findByIdAndDelete(id);
+    await this.toMq({ id });
+    return res;
+  }
+
+  async start({ id }) {
+    const mission = await this.model.findById(id);
+    if (!mission) {
+      throw new BusinessError(
+        ErrorCode.DATA_NOT_EXIST,
+        '未找到指定任务,无法开始任务!'
+      );
+    }
+    if (!mission.params) { throw new BusinessError(ErrorCode.DATA_INVALID, '任务信息缺少参数设置'); }
+    const { project, service, method, body } = mission.params;
+    assert(project, '任务信息中未找到需要执行的项目名称');
+    assert(service, '任务信息中未找到需要访问的服务');
+    assert(method, '任务信息中未找到要执行的方法');
+    try {
+      this.toQueue(
+        project,
+        JSON.stringify({ ...body, missionid: id, project, service, method })
+      );
+    } catch (error) {
+      console.error(error.toString());
+      await this.update({ id }, { status: '3' });
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '执行任务失败');
+    }
+    await this.update({ id }, { status: '1', progress: undefined });
+  }
+
+  async updateProgress({ id, progress, status = '1' }) {
+    if (id && progress) {
+      await this.model.update({ _id: ObjectId(id) }, { progress, status });
+      this.toMq({ id });
+    }
+  }
+
+  async toMq({ id }) {
+    if (this.mq) {
+      const exchange = 'mission';
+      const prefix = [];
+      if (id) {
+        const mission = await this.model.findById(id);
+        if (mission) {
+          const { user, params } = mission;
+          const { project } = params;
+          if (project) prefix.push(project);
+          if (user)prefix.push(user);
+        }
+      }
+      const routerKey = `${prefix.join('/')}/remind`;
+      console.log(routerKey);
+      const parm = { durable: true };
+      await this.mq.fanout(exchange, routerKey, 'to refresh', parm);
+    }
+  }
+
+  async toQueue(project, data) {
+    const ch = await this.mq.conn.createChannel();
+    const queue = `mission/${project}`;
+    await ch.assertQueue(queue, { durable: false });
+    await ch.sendToQueue(queue, Buffer.from(data));
+    await ch.close();
+  }
+
+}
+
+module.exports = MissionService;

+ 14 - 0
appveyor.yml

@@ -0,0 +1,14 @@
+environment:
+  matrix:
+    - nodejs_version: '10'
+
+install:
+  - ps: Install-Product node $env:nodejs_version
+  - npm i npminstall && node_modules\.bin\npminstall
+
+test_script:
+  - node --version
+  - npm --version
+  - npm run test
+
+build: off

+ 80 - 0
config/config.default.js

@@ -0,0 +1,80 @@
+/* eslint valid-jsdoc: "off" */
+
+'use strict';
+
+/**
+ * @param {Egg.EggAppInfo} appInfo app info
+ */
+module.exports = appInfo => {
+  /**
+   * built-in config
+   * @type {Egg.EggAppConfig}
+   **/
+  const config = (exports = {});
+
+  // use for cookie sign key, should change to your own and keep security
+  config.keys = appInfo.name + '_1603878155652_3375';
+
+  // add your middleware config here
+  config.middleware = [];
+
+  // add your user config here
+  const userConfig = {
+    // myAppName: 'egg',
+  };
+  config.cluster = {
+    listen: {
+      port: 4001,
+    },
+  };
+  // mq配置
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'visit',
+      password: 'visit',
+      vhost: 'platform',
+    },
+    app: true,
+    agent: true,
+  };
+
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: '',
+      db: 0,
+    },
+  };
+
+  // mongoose config
+  config.mongoose = {
+    url: 'mongodb://127.0.0.1:27017/mission',
+    options: {
+      // user: 'admin',
+      // pass: 'admin',
+      // authSource: 'admin',
+      // useNewUrlParser: true,
+      // useCreateIndex: true,
+    },
+  };
+
+  config.project = {
+    zhwl: 'http://127.0.0.1:7002/api/servicezhwl',
+    userAuth: 'http://127.0.0.1:7003/api/role/auth',
+  };
+
+  // 安全配置
+  config.security = {
+    csrf: {
+      // ignoreJSON: true, // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
+      enable: false,
+    },
+  };
+  return {
+    ...config,
+    ...userConfig,
+  };
+};

+ 29 - 0
config/config.local.js

@@ -0,0 +1,29 @@
+'use strict';
+
+module.exports = () => {
+  const config = (exports = {});
+  config.cluster = {
+    listen: {
+      port: 4001,
+    },
+  };
+
+  config.logger = {
+    level: 'DEBUG',
+    consoleLevel: 'DEBUG',
+  };
+
+  config.mongoose = {
+    url: 'mongodb://localhost:27017/mission',
+    options: {
+      // user: 'demo',
+      // pass: 'demo',
+      // authSource: 'admin',
+      // useNewUrlParser: true,
+      // useCreateIndex: true,
+      // useUnifiedTopology: true,
+    },
+  };
+
+  return config;
+};

+ 55 - 0
config/config.prod.js

@@ -0,0 +1,55 @@
+'use strict';
+
+module.exports = () => {
+  const config = (exports = {});
+
+  config.logger = {
+    level: 'DEBUG',
+    consoleLevel: 'DEBUG',
+  };
+  // 服务器发布路径
+  config.baseUrl = 'http://jytz.jilinjobs.cn';
+
+
+  // mongoose config
+  config.mongoose = {
+    url: 'mongodb://127.0.0.1:27017/mission',
+    options: {
+      user: 'admin',
+      pass: 'admin',
+      authSource: 'admin',
+      useUnifiedTopology: true,
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+
+  // mq配置
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'visit',
+      password: 'visit',
+      vhost: 'platform',
+    },
+    app: true,
+    agent: true,
+  };
+
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: 123456,
+      db: 0,
+    },
+  };
+
+  config.project = {
+    zhwl: 'http://127.0.0.1:4001',
+    userAuth: 'http://127.0.0.1:4000',
+  };
+
+  return config;
+};

+ 12 - 0
config/plugin.js

@@ -0,0 +1,12 @@
+'use strict';
+
+/** @type Egg.EggPlugin */
+exports.amqp = {
+  enable: true,
+  package: 'egg-naf-amqp',
+};
+
+exports.redis = {
+  enable: true,
+  package: 'egg-redis',
+};

+ 18 - 0
ecosystem.config.js

@@ -0,0 +1,18 @@
+'use strict';
+
+const app = 'mission';
+const moment = require('moment');
+module.exports = {
+  apps: [{
+    name: app, // 应用名称
+    script: './server.js', // 实际启动脚本
+    out: `./logs/${app}-${moment().format('YYYY-MM-DD')}.log`,
+    error: `./logs/${app}-${moment().format('YYYY-MM-DD')}.err`,
+    watch: [ // 监控变化的目录,一旦变化,自动重启
+      'app', 'config',
+    ],
+    env: {
+      NODE_ENV: 'production', // 环境参数,当前指定为生产环境
+    },
+  }],
+};

+ 51 - 0
package.json

@@ -0,0 +1,51 @@
+{
+  "name": "mission-service",
+  "version": "1.0.0",
+  "description": "任务系统",
+  "private": true,
+  "egg": {
+    "framework": "naf-framework-mongoose"
+  },
+  "dependencies": {
+    "egg": "^2.15.1",
+    "egg-scripts": "^2.11.0",
+    "egg-naf-amqp": "0.0.13",
+    "egg-redis": "^2.4.0",
+    "lodash": "^4.17.15",
+    "moment": "^2.27.0",
+    "naf-framework-mongoose": "^0.6.11"
+  },
+  "devDependencies": {
+    "autod": "^3.0.1",
+    "autod-egg": "^1.1.0",
+    "egg-bin": "^4.11.0",
+    "egg-ci": "^1.11.0",
+    "egg-mock": "^3.21.0",
+    "eslint": "^5.13.0",
+    "eslint-config-egg": "^7.1.0"
+  },
+  "engines": {
+    "node": ">=10.0.0"
+  },
+  "scripts": {
+    "start": "egg-scripts start --daemon --title=egg-server-mission",
+    "stop": "egg-scripts stop --title=egg-server-mission",
+    "dev": "egg-bin dev",
+    "debug": "egg-bin debug",
+    "test": "npm run lint -- --fix && npm run test-local",
+    "test-local": "egg-bin test",
+    "cov": "egg-bin cov",
+    "lint": "eslint .",
+    "ci": "npm run lint && npm run cov",
+    "autod": "autod"
+  },
+  "ci": {
+    "version": "10"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "lrf402788946",
+  "license": "MIT"
+}

+ 9 - 0
server.js

@@ -0,0 +1,9 @@
+
+// eslint-disable-next-line strict
+const egg = require('egg');
+
+const workers = Number(process.argv[2] || require('os').cpus().length);
+egg.startCluster({
+  workers,
+  baseDir: __dirname,
+});

+ 20 - 0
test/app/controller/home.test.js

@@ -0,0 +1,20 @@
+'use strict';
+
+const { app, assert } = require('egg-mock/bootstrap');
+
+describe('test/app/controller/home.test.js', () => {
+  it('should assert', () => {
+    const pkg = require('../../../package.json');
+    assert(app.config.keys.startsWith(pkg.name));
+
+    // const ctx = app.mockContext({});
+    // yield ctx.service.xx();
+  });
+
+  it('should GET /', () => {
+    return app.httpRequest()
+      .get('/')
+      .expect('hi, egg')
+      .expect(200);
+  });
+});

+ 18 - 0
util/app.js

@@ -0,0 +1,18 @@
+'use strict';
+class AppBootHook {
+  constructor(app) {
+    this.app = app;
+  }
+
+  async didReady() {
+    // 应用已经启动完毕
+    const ctx = await this.app.createAnonymousContext();
+    await ctx.service.rabbitmq.mission();
+  }
+
+  async serverDidReady() {
+    // 应用已经启动完毕
+    const ctx = await this.app.createAnonymousContext();
+  }
+}
+module.exports = AppBootHook;

+ 44 - 0
util/rabbitmq.js

@@ -0,0 +1,44 @@
+'use strict';
+
+const Service = require('egg').Service;
+
+class RabbitmqService extends Service {
+
+  constructor(ctx) {
+    super(ctx);
+  }
+
+  // mission队列处理
+  async mission() {
+    const { mq } = this.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      // TODO 需要写成匹配的形式
+      const queue = 'mission/center';
+      try {
+        // 创建队列:在没有队列的情况,直接获取会导致程序无法启动
+        await ch.assertQueue(queue, { durable: false });
+        await ch.consume(queue, msg => this.dealMission(msg), { noAck: true });
+      } catch (error) {
+        this.ctx.logger.error('未找到订阅的队列');
+      }
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+  // 执行任务
+  async dealMission(bdata) {
+    if (!bdata) this.ctx.logger.error('mission队列中信息不存在');
+    let data = bdata.content.toString();
+    try {
+      data = JSON.parse(data);
+    } catch (error) {
+      this.ctx.logger.error('数据不是object');
+    }
+    const { service, method, ...others } = data;
+    if (service && method) this.ctx.service[service][method](others);
+
+  }
+}
+
+module.exports = RabbitmqService;