lrf402788946 4 years ago
commit
fc40fa3aee

+ 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 }}

+ 14 - 0
.gitignore

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

+ 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

+ 33 - 0
README.md

@@ -0,0 +1,33 @@
+# service-front
+
+前端bff
+
+## QuickStart
+
+<!-- add docs here for user -->
+
+see [egg docs][egg] for more detail.
+
+### Development
+
+```bash
+$ npm i
+$ npm run dev
+$ open http://localhost:7001/
+```
+
+### Deploy
+
+```bash
+$ npm start
+$ npm stop
+```
+
+### npm scripts
+
+- Use `npm run lint` to check code style.
+- Use `npm test` to run unit test.
+- Use `npm run autod` to auto detect dependencies upgrade, see [autod](https://www.npmjs.com/package/autod) for more detail.
+
+
+[egg]: https://eggjs.org

+ 16 - 0
app.js

@@ -0,0 +1,16 @@
+'use strict';
+class AppBootHook {
+  constructor(app) {
+    this.app = app;
+  }
+
+  async didReady() {
+    // 应用已经启动完毕
+    const ctx = await this.app.createAnonymousContext();
+    // 检查种子
+    await ctx.service.install.index();
+    // await ctx.service.rabbitmq.receiveQueueMsg('user_bind');
+  }
+
+}
+module.exports = AppBootHook;

+ 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;

+ 25 - 0
app/controller/intercept.js

@@ -0,0 +1,25 @@
+'use strict';
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 拦截
+class InterceptController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.intercept;
+  }
+
+  async get() {
+    const res = await this.service.getDeal();
+    this.ctx.body = res;
+  }
+  async post() {
+    const res = await this.service.postDeal();
+    this.ctx.body = res;
+  }
+  async delete() {
+    const res = await this.service.deleteDeal();
+    this.ctx.body = res;
+  }
+}
+module.exports = CrudController(InterceptController, {});

+ 22 - 0
app/model/client.js

@@ -0,0 +1,22 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { Secret } = require('naf-framework-mongoose/lib/model/schema');
+const record = new Schema({
+  ip: { type: String, required: true }, // 真实ip
+  create_time: { type: String, default: moment().format('YYYY-MM-DD HH:mm:ss') },
+});
+// ip访问量表
+const client = {
+  date: { type: String, default: moment().format('YYYY-MM-DD') }, // 日期
+  _tenant: { type: String },
+  record: { type: [ record ] }, // 记录
+};
+const schema = new Schema(client, { 'multi-tenancy': true, toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Client', schema, 'client');
+};

+ 24 - 0
app/model/hits.js

@@ -0,0 +1,24 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { Secret } = require('naf-framework-mongoose/lib/model/schema');
+const record = new Schema({
+  ip: { type: String, required: true }, // 真实ip
+  route: { type: String, required: true }, // 路由
+  create_time: { type: String, default: moment().format('YYYY-MM-DD HH:mm:ss') },
+});
+
+// 网页访问量表
+const hits = {
+  date: { type: String, default: moment().format('YYYY-MM-DD') }, // 日期
+  _tenant: { type: String },
+  record: { type: [ record ] }, // 记录
+};
+const schema = new Schema(hits, { 'multi-tenancy': true, toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Hits', schema, 'hits');
+};

+ 12 - 0
app/router.js

@@ -0,0 +1,12 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+  router.get('/', controller.home.index);
+  router.get(/^\/site*/, controller.intercept.get);
+  router.post(/^\/site*/, controller.intercept.post);
+  router.delete(/^\/site*/, controller.intercept.delete);
+};

+ 21 - 0
app/schedule/check.js

@@ -0,0 +1,21 @@
+'use strict';
+
+const Subscription = require('egg').Subscription;
+
+class CheckCheck extends Subscription {
+  // 通过 schedule 属性来设置定时任务的执行间隔等配置
+  // 更改执行时间
+  static get schedule() {
+    return {
+      // cron: '0 0 0 * * *',
+      interval: '30s',
+      type: 'worker', // 指定所有的 worker 都需要执行
+    };
+  }
+
+  // subscribe 是真正定时任务执行时被运行的函数
+  async subscribe() {
+    await this.ctx.service.intercept.check();
+  }
+}
+module.exports = CheckCheck;

+ 24 - 0
app/service/install.js

@@ -0,0 +1,24 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+
+// 初始化
+class InstallService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'install');
+    this.redis = this.app.redis;
+  }
+
+  async index() {
+    await this.initProject();
+  }
+
+  /**
+   * 初始化redis中,项目名的缓存
+   */
+  async initProject() {
+    const project = [ 'platlive', 'zhwl' ];
+    await this.redis.set('project', JSON.stringify(project));
+  }
+}
+
+module.exports = InstallService;

+ 132 - 0
app/service/intercept.js

@@ -0,0 +1,132 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { ObjectId } = require('mongoose').Types;
+const _ = require('lodash');
+const moment = require('moment');
+const assert = require('assert');
+
+// 拦截
+class InterceptService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'intercept');
+    this.redis = this.app.redis;
+    this.hits = this.ctx.model.Hits;
+    this.client = this.ctx.model.Client;
+    this.httpUtil = this.ctx.service.util.httpUtil;
+  }
+
+  async getDeal() {
+    await this.count();
+    const url = this.ctx.request.url.replace('/site', '');
+    const res = await this.httpUtil.cget(url);
+    return res;
+
+  }
+
+  async postDeal() {
+    const url = this.ctx.request.url.replace('/site', '');
+    const body = this.ctx.request.body;
+    const res = await this.httpUtil.cpost(url, body);
+    return res;
+  }
+  async deleteDeal() {
+    const url = this.ctx.request.url.replace('/site', '');
+    const body = this.ctx.request.body;
+    const res = await this.httpUtil.cdelete(url, body);
+    return res;
+  }
+
+  /**
+   * 添加页面访问量和客户访问量
+   * TODO 定时任务,将redis的数据放进数据库
+   */
+  async count() {
+    try {
+      const referer = _.get(this.ctx.request, 'header.referer');
+      const clientIp = _.get(this.ctx.request, 'header.x-real-ip');
+      // if (clientIp.includes('192.168')) {
+      //   // 局域网随意
+      //   console.info('local area network');
+      //   return;
+      // }
+      const url = new URL(referer);
+      const arr = url.pathname.split('/');
+      if (arr.length <= 2) throw new BusinessError();
+      const _tenant = arr[1];
+      let project = await this.redis.get('project');
+      if (!project) throw new BusinessError(ErrorCode.SERVICE_FAULT, '为设置中间层项目参数');
+      project = JSON.parse(project);
+      if (!project.includes(_tenant)) throw new BusinessError();
+      const obj = { ip: clientIp, create_time: moment().format('YYYY-MM-DD HH:mm:ss') };
+      // 访问量的添加
+      let hitsList = await this.redis.get(`${_tenant}/hitsList`);
+      if (!hitsList) {
+        hitsList = [{ ...obj, route: url.pathname }];
+        await this.redis.set(`${_tenant}/hitsList`, JSON.stringify(hitsList));
+      } else {
+        hitsList = JSON.parse(hitsList);
+        const is_in = hitsList.find(f => f.ip === obj.ip && f.create_time === obj.create_time);
+        if (!is_in) {
+          hitsList.push({ ...obj, route: url.pathname });
+          await this.redis.set(`${_tenant}/hitsList`, JSON.stringify(hitsList));
+        }
+      }
+      // 用户(ip)访问添加
+      let clientList = await this.redis.get(`${_tenant}/clientList`);
+      if (!clientList) {
+        clientList = [ obj ];
+        await this.redis.set(`${_tenant}/clientList`, JSON.stringify(clientList));
+      } else {
+        clientList = JSON.parse(clientList);
+        const t_s = moment().format('YYYY-MM-DD');
+        const t_e = moment().add(1, 'd').format('YYYY-MM-DD');
+        const is_in = clientList.find(f => f.ip === obj.ip && moment(f.create_time).isBetween(t_s, t_e, null, '[]'));
+        if (!is_in) {
+          clientList.push(obj);
+          await this.redis.set(`${_tenant}/clientList`, JSON.stringify(clientList));
+        }
+      }
+    } catch (error) {
+      console.log(error);
+      throw new BusinessError(ErrorCode.ACCESS_DENIED, '非法连接!');
+    }
+
+  }
+
+  async check() {
+    let project = await this.redis.get('project');
+    if (!project) throw new BusinessError(ErrorCode.SERVICE_FAULT, '为设置中间层项目参数');
+    project = JSON.parse(project);
+    for (const _tenant of project) {
+      let hitsList = await this.redis.get(`${_tenant}/hitsList`);
+      if (hitsList) {
+        hitsList = JSON.parse(hitsList);
+        const today = await this.hits.findOne({ date: moment().format('YYYY-MM-DD'), _tenant });
+        if (today) {
+          today.record.push(...hitsList);
+          await today.save();
+        } else {
+          await this.hits.create({ date: moment().format('YYYY-MM-DD'), record: hitsList || [], _tenant });
+        }
+        await this.redis.del(`${_tenant}/hitsList`);
+      }
+      let clientList = await this.redis.get(`${_tenant}/clientList`);
+      if (clientList) {
+        clientList = JSON.parse(clientList);
+        const today = await this.client.findOne({ date: moment().format('YYYY-MM-DD') });
+        if (today) {
+          today.record.push(...clientList);
+          today.record = _.uniqBy(today.record, 'ip');
+          await today.save();
+        } else {
+          await this.client.create({ date: moment().format('YYYY-MM-DD'), record: clientList || [], _tenant });
+        }
+        await this.redis.del(`${_tenant}/clientList`);
+      }
+    }
+
+  }
+}
+
+module.exports = InterceptService;

+ 97 - 0
app/service/util/http-util.js

@@ -0,0 +1,97 @@
+'use strict';
+const _ = require('lodash');
+const { AxiosService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { isNullOrUndefined } = require('naf-core').Util;
+
+class UtilService extends AxiosService {
+  constructor(ctx) {
+    super(ctx, {}, {});
+  }
+
+  // 替换uri中的参数变量
+  merge(uri, query = {}) {
+    const keys = Object.keys(query);
+    const arr = [];
+    for (const k of keys) {
+      arr.push(`${k}=${query[k]}`);
+    }
+    if (arr.length > 0) {
+      uri = `${uri}?${arr.join('&')}`;
+    }
+    return uri;
+  }
+
+  /**
+   * curl-get请求
+   * @param {String} uri 接口地址
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async cget(uri, query, options) {
+    return this.toRequest(uri, null, query, options);
+  }
+
+  /**
+   * curl-post请求
+   * @param {String} uri 接口地址
+   * @param {Object} data post的body
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async cpost(uri, data = {}, query, options) {
+    return this.toRequest(uri, data, query, options);
+  }
+
+  /**
+   * curl-delete请求
+   * @param {String} uri 接口地址
+   * @param {Object} data post的body
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数默认 method:delete
+   */
+  async cdelete(uri, data = {}, query, options) {
+    options = { ...options, method: 'delete' };
+    return this.$request(uri, data, query, options);
+  }
+
+  async toRequest(uri, data, query, options) {
+    query = _.pickBy(
+      query,
+      val => val !== '' && val !== 'undefined' && val !== 'null'
+    );
+    if (!uri) console.error('uri不能为空');
+    if (_.isObject(query) && _.isObject(options)) {
+      const params = query.params ? query.params : query;
+      options = { ...options, params };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    // 是否多租户模式,需要改变headers
+    const headers = { 'content-type': 'application/json' };
+    let url = this.merge(`${uri}`, options.params);
+    if (!url.includes('http')) url = `http://localhost${url}`;
+    let res = await this.ctx.curl(url, {
+      method: isNullOrUndefined(data) ? 'get' : 'post',
+      url,
+      data,
+      dataType: 'json',
+      headers,
+      ...options,
+    });
+    if (res.status === 200) {
+      res = res.data || {};
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return { errcode, errmsg };
+      }
+      return res;
+    }
+    const { status } = res;
+    console.warn(`[${uri}] fail: ${status}-${res.data.message} `);
+  }
+}
+module.exports = UtilService;

+ 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

+ 56 - 0
config/config.default.js

@@ -0,0 +1,56 @@
+/* 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 + '_1611907459516_421';
+
+  // add your middleware config here
+  config.middleware = [];
+
+  // add your user config here
+  const userConfig = {
+    // myAppName: 'egg',
+  };
+  config.cluster = {
+    listen: {
+      port: 9102,
+    },
+  };
+  config.dbName = 'statistics';
+  config.mongoose = {
+    url: `mongodb://localhost:27017/${config.dbName}`,
+    options: {
+      user: 'admin',
+      pass: '111111',
+      authSource: 'admin',
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+
+
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: 123456,
+      db: 0,
+    },
+  };
+  config.proxy = true;
+  return {
+    ...config,
+    ...userConfig,
+  };
+};

+ 32 - 0
config/config.prod.js

@@ -0,0 +1,32 @@
+'use strict';
+
+module.exports = () => {
+  const config = exports = {};
+
+  config.logger = {
+    level: 'INFO',
+    consoleLevel: 'INFO',
+  };
+  config.dbName = 'new-platform';
+  config.mongoose = {
+    url: `mongodb://localhost:27017/${config.dbName}`,
+    options: {
+      user: 'admin',
+      pass: 'admin',
+      authSource: 'admin',
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+  // redis config
+  config.redis = {
+    client: {
+      port: 6379, // Redis port
+      host: '127.0.0.1', // Redis host
+      password: 123456,
+      db: 0,
+    },
+  };
+
+  return config;
+};

+ 11 - 0
config/plugin.js

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

+ 52 - 0
package.json

@@ -0,0 +1,52 @@
+{
+  "name": "service-front",
+  "version": "1.0.0",
+  "description": "前端bff",
+  "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.24.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",
+    "jsonwebtoken": "^8.5.1"
+  },
+  "engines": {
+    "node": ">=10.0.0"
+  },
+  "scripts": {
+    "start": "egg-scripts start --daemon --title=egg-server-service-front",
+    "stop": "egg-scripts stop --title=egg-server-service-front",
+    "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": "free",
+  "license": "MIT"
+}

+ 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);
+  });
+});