lrf402788946 3 anni fa
commit
0f6b212b24

+ 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 @@
+# customer-service
+
+
+
+## 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

+ 62 - 0
app/controller/.customer_chat.js

@@ -0,0 +1,62 @@
+module.exports = {
+  create: {
+    requestBody: [
+      "_tenant",
+      "client_id",
+      "client_name",
+      "customer_id",
+      "customer_name",
+      "sender_id",
+      "sender_name",
+      "content",
+    ],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: [
+      "_tenant",
+      "client_id",
+      "client_name",
+      "customer_id",
+      "customer_name",
+      "sender_id",
+      "sender_name",
+      "content",
+    ],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        client_id: "client_id",
+        client_name: "client_name",
+        customer_id: "customer_id",
+        customer_name: "customer_name",
+        sender_id: "sender_id",
+        sender_name: "sender_name",
+        _tenant: "_tenant",
+        "create_time@start": "create_time@start",
+        "create_time@end": "create_time@end",
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: "query",
+    options: {
+      query: ["skip", "limit"],
+      sort: ["meta.createdAt"],
+      asc: true,
+      count: true,
+    },
+  },
+};

+ 48 - 0
app/controller/customer.js

@@ -0,0 +1,48 @@
+'use strict';
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 客服调度相关
+class CustomerController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.customer;
+  }
+
+  // 客服登陆/续时
+  async customerLog() {
+    await this.service.customerLog(this.ctx.query);
+    this.ctx.ok();
+  }
+
+  // 客户咨询
+  async clientApply() {
+    const data = await this.service.clientApply(this.ctx.query);
+    this.ctx.ok({ data });
+  }
+
+  // 客户续时
+  async clientContinue() {
+    const data = await this.service.clientContinue(this.ctx.query);
+    this.ctx.ok({ data });
+  }
+
+  // 结束服务
+  async serverEnd() {
+    const data = await this.service.serverEnd(this.ctx.query);
+    this.ctx.ok({ data });
+  }
+
+  // 对话
+  async chat() {
+    const data = await this.service.chat(this.ctx.request.body);
+    this.ctx.ok({ data });
+  }
+
+  // 获取客服正在服务的客户列表
+  async getRecord() {
+    const data = await this.service.getRecord(this.ctx.query);
+    this.ctx.ok({ data });
+  }
+}
+module.exports = CrudController(CustomerController, {});

+ 13 - 0
app/controller/customer_chat.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.customer_chat.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 客服服务对话记录
+class Customer_chatController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.customerChat;
+  }
+}
+module.exports = CrudController(Customer_chatController, meta);

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

+ 32 - 0
app/model/customer_chat.js

@@ -0,0 +1,32 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 客服聊天记录表
+const customer_chat = {
+  _tenant: { type: String }, // 项目
+  client_id: { type: ObjectId }, // 客户id
+  client_name: { type: String }, // 客户名
+  customer_id: { type: ObjectId }, // 客服id
+  customer_name: { type: String }, // 客服名
+  sender_id: { type: ObjectId }, // 发送人id
+  sender_name: { type: String }, // 发送人
+  content: { type: String }, // 内容
+  remark: { type: String },
+};
+const schema = new Schema(customer_chat, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ _tenant: 1 });
+schema.index({ client_id: 1 });
+schema.index({ client_name: 1 });
+schema.index({ customer_id: 1 });
+schema.index({ customer_name: 1 });
+schema.index({ sender_id: 1 });
+schema.index({ sender_name: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Customer_chat', schema, 'customer_chat');
+};

+ 27 - 0
app/model/record.js

@@ -0,0 +1,27 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 工作记录表
+const record = {
+  customer_id: { type: ObjectId }, // 客服id
+  customer_name: { type: ObjectId }, // 客服名
+  client_id: { type: ObjectId }, // 客户id
+  client_name: { type: String }, // 客户名
+  _tenant: { type: String }, // 项目标识
+  out_time: { type: String }, // 失效时间
+  remark: { type: String },
+};
+const schema = new Schema(record, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ customer_id: 1 });
+schema.index({ customer_name: 1 });
+schema.index({ client_id: 1 });
+schema.index({ client_name: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Record', schema, 'record');
+};

+ 27 - 0
app/model/status.js

@@ -0,0 +1,27 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const moment = require('moment');
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+const { ObjectId } = require('mongoose').Types;
+// 客服状态表
+const status = {
+  customer_id: { type: ObjectId }, // 客服id
+  customer_name: { type: String }, // 客服名
+  status: { type: String, default: '0' }, // 状态:0-空闲;1-接待中
+  _tenant: { type: String, default: 'master' }, // 哪个项目
+  logout_time: { type: String }, // 失效时间
+  remark: { type: String },
+};
+const schema = new Schema(status, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ customer_id: 1 });
+schema.index({ customer_name: 1 });
+schema.index({ status: 1 });
+schema.index({ _tenant: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Status', schema, 'status');
+};
+// 定期进行清除

+ 20 - 0
app/router.js

@@ -0,0 +1,20 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+  const prefix = 'capi';
+  router.get('/', controller.home.index);
+  router.get(`/${prefix}/customer`, controller.customer.customerLog);
+  router.get(`/${prefix}/clientApply`, controller.customer.clientApply);
+  router.get(`/${prefix}/continue`, controller.customer.clientContinue);
+  router.get(`/${prefix}/serverEnd`, controller.customer.serverEnd);
+  router.get(`/${prefix}/getRecord`, controller.customer.getRecord);
+  router.post(`/${prefix}/chat`, controller.customer.chat);
+
+  // 对话记录
+  router.resources(prefix, `/${prefix}/chatRecord`, controller.customerChat); // index、create、show、destroy
+  router.post(prefix, `/${prefix}/chatRecord/update/:id`, controller.customerChat.update);
+};

+ 10 - 0
app/schedule/check.js

@@ -0,0 +1,10 @@
+'use strict';
+module.exports = {
+  schedule: {
+    interval: '75s', // 1 分钟间隔
+    type: 'all', // 指定所有的 worker 都需要执行
+  },
+  async task(ctx) {
+    await ctx.service.customer.clear();
+  },
+};

+ 202 - 0
app/service/customer.js

@@ -0,0 +1,202 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+const moment = require('moment');
+
+// 客服相关
+class CustomerService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'customer');
+    this.mq = this.ctx.mq;
+    this.status = this.ctx.model.Status;
+    this.record = this.ctx.model.Record;
+    this.chatRecord = this.ctx.model.CustomerChat;
+
+    // 1分钟检查1次
+    this.time = 1;
+    this.unit = 'm';
+  }
+
+  /**
+   * 发送消息
+   * @param {Object} body 参数
+   * @property _tenant 项目
+   * @property client_id 客户id
+   * @property client_name 客户名
+   * @property customer_id 客服id
+   * @property customer_name 客服名
+   * @property sender_id 发送人id
+   * @property sender_name 发送人
+   * @property content 内容
+   */
+  async chat(body) {
+    const { _tenant, client_id, customer_id } = body;
+    assert(_tenant, '缺少项目标识');
+    assert(client_id, '缺少客户信息');
+    assert(customer_id, '缺少客服信息');
+    if (!this.mq) throw new BusinessError(ErrorCode.SERVICE_FAULT, 'mq配置错误');
+    body.send_time = moment().format('YYYY-MM-DD HH:mm:ss');
+    // 向项目存这条对话数据,再mq执行
+    await this.chatRecord.create(body);
+    // await this.ctx.service.util.httpUtil.$post('/customerChat', 'hnhmain', body);
+    const exchange = _tenant;
+    const routeKey = `customer.${client_id}.${customer_id}`;
+    const mqBody = JSON.stringify(body);
+    const param = { durable: true };
+    await this.mq.topic(exchange, routeKey, mqBody, param);
+  }
+
+  /**
+   * 客服获取正在服务的客户列表
+   * @param {Object} query 参数
+   */
+  async getRecord(query) {
+    const { _tenant, customer_id } = query;
+    assert(_tenant, '缺少项目标识');
+    assert(customer_id, '缺少客服信息');
+    const list = await this.record.find(query);
+    return list;
+  }
+
+
+  /**
+   * 客服登陆/续时,此函数不处理工作状态,只处理时间失效与否
+   * @param {Object} query 参数
+   */
+  async customerLog(query) {
+    const { _tenant, customer_id } = query;
+    assert(_tenant, '缺少项目标识');
+    assert(customer_id, '缺少客服信息');
+    let customer = await this.status.findOne({ _tenant, customer_id });
+    if (customer) {
+      // 该客服已经上线
+      customer.logout_time = this.addTime();
+      await customer.save();
+    } else {
+      // 没上线
+      const obj = { ...query, status: '0', logout_time: this.addTime() };
+      customer = await this.status.create(obj);
+    }
+    return customer;
+  }
+
+  /**
+   * 客户咨询,查找指定项目的空闲客服,如果没有,那就找到所有客服,
+   * 并且找其同时接待最少的客服,让他接待
+   * @param {Object} query 参数
+   */
+  async clientApply(query) {
+    const { _tenant, client_id, client_name } = query;
+    assert(_tenant, '缺少项目标识');
+    assert(client_id, '缺少客户信息');
+    const freeList = await this.status.find({ _tenant, status: '0' });
+    let customer;
+    if (freeList.length > 0) {
+      customer = _.head(freeList);
+      // 修改状态,加次时间
+      customer.status = '1';
+      customer.logout_time = this.addTime();
+      await customer.save();
+    } else {
+      // 没有闲的了,那就找不闲的
+      let list = await this.status.find({ _tenant });
+      if (list.length > 0) {
+        list = JSON.parse(JSON.stringify(list));
+        for (const i of list) {
+          const { customer_id } = i;
+          const num = await this.record.count({ customer_id, _tenant });
+          i.num = num;
+        }
+        list = _.orderBy(list, [ 'num' ], [ 'asc' ]);
+        customer = _.head(list);
+        customer.status = '1';
+        customer.logout_time = this.addTime();
+        await this.customerLog({ _tenant, customer_id: customer.customer_id });
+      } else return '当前没有客服在线';
+    }
+    // 添加记录
+    const obj = { customer_id: customer.customer_id, customer_name: customer.customer_name, _tenant, client_id, client_name };
+    let record = await this.record.findOne(obj);
+    if (record) {
+      record.out_time = this.addTime();
+      await record.save();
+    } else {
+      obj.out_time = this.addTime();
+      record = await this.record.create(obj);
+    }
+
+    if (this.mq) {
+      const exchange = _tenant;
+      const routeKey = `customer.${customer.customer_id}`;
+      const mqBody = 'research';
+      const param = { durable: true };
+      await this.mq.topic(exchange, routeKey, mqBody, param);
+    }
+
+    return record;
+  }
+
+  /**
+   * 客户续时
+   * @param {Object} query 参数
+   */
+  async clientContinue(query) {
+    const { _tenant, client_id, customer_id } = query;
+    assert(_tenant, '缺少项目标识');
+    assert(client_id, '缺少客户信息');
+    assert(customer_id, '缺少客服信息');
+    const record = await this.record.findOne({ _tenant, client_id, customer_id });
+    if (!record) return '服务已经结束';
+    record.out_time = this.addTime();
+    const r = await record.save();
+    if (r) {
+      // 同时给客服续航
+      await this.customerLog({ _tenant, customer_id });
+    }
+    return 'ok';
+  }
+
+  // 结束服务
+  async serverEnd(query) {
+    const { _tenant, client_id, customer_id } = query;
+    assert(_tenant, '缺少项目标识');
+    assert(client_id, '缺少客户信息');
+    assert(customer_id, '缺少客服信息');
+    await this.record.deleteOne({ _tenant, client_id, customer_id });
+    this.checkStatus();
+    return 'ok';
+  }
+
+  // 查询并修改客服状态
+  async checkStatus() {
+    const list = await this.status.find();
+    for (const i of list) {
+      const num = await this.record.count({ customer_id: i._id });
+      if (num <= 0) {
+        i.status = '0';
+        i.save();
+      }
+
+    }
+  }
+
+  // 定时清理
+  async clear() {
+    // 所有状态和工作记录,对比失效时间,过了就删了
+    console.log('in function:clear');
+    await this.status.deleteMany({ logout_time: { $lte: moment().format('YYYY-MM-DD HH:mm:ss') } });
+    // await this.record.deleteMany({ out_time: { $lte: moment().format('YYYY-MM-DD HH:mm:ss') } });
+
+  }
+
+  // 续
+  addTime() {
+    const t = moment().add(this.time, this.unit).format('YYYY-MM-DD HH:mm:ss');
+    return t;
+  }
+
+}
+
+module.exports = CustomerService;

+ 15 - 0
app/service/customer_chat.js

@@ -0,0 +1,15 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 客服服务对话记录
+class Customer_chatService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'customer_chat');
+    this.model = this.ctx.model.CustomerChat;
+  }
+}
+
+module.exports = Customer_chatService;

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

@@ -0,0 +1,96 @@
+'use strict';
+const { AxiosService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { isNullOrUndefined } = require('naf-core').Util;
+const _ = require('lodash');
+
+//
+class HttpUtilService 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 {String} project config中的项目key
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async $get(uri, project, query, options) {
+    return this.toRequest(uri, project, null, query, options);
+  }
+
+  /**
+   * curl-post请求
+   * @param {String} uri 接口地址
+   * @param {String} project config中的项目key
+   * @param {Object} data post的body
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async $post(uri, project, data = {}, query, options) {
+    return this.toRequest(uri, project, data, query, options);
+  }
+
+  async toRequest(uri, project, data, query, options) {
+    const prefix = _.get(this.ctx.app.config.project, project);
+    if (!prefix) {
+      throw new BusinessError(
+        ErrorCode.SERVICE_FAULT,
+        `未设置用户权限项目的关联:config.project.${project} is undefined`
+      );
+    }
+    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' };
+    const url = this.merge(`${prefix}${uri}`, options.params);
+    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.data;
+    }
+    const { status } = res;
+    console.warn(`[${uri}] fail: ${status}-${res.data.message} `);
+  }
+}
+
+module.exports = HttpUtilService;

+ 66 - 0
app/service/util/rabbitMq.js

@@ -0,0 +1,66 @@
+'use strict';
+
+const Service = require('egg').Service;
+
+class RabbitmqService extends Service {
+
+  constructor(ctx) {
+    super(ctx);
+    this.exType = 'topic';
+    this.durable = true;
+  }
+
+  // 接收消息
+  async receiveQueueMsg(ex) {
+    this.ctx.logger.info('调用mq的' + ex);
+    const self = this;
+    const { mq } = self.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      await ch.assertExchange(ex, 'topic', { durable: true });
+      const q = await ch.assertQueue('', { exclusive: true });
+      await ch.bindQueue(q.queue, ex, '*');
+      await ch.consume(q.queue, msg => this.logMessage(msg, this), { noAck: true });
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+
+  async logMessage(msg) {
+    const result = msg.content.toString();
+    const headers = msg.properties.headers;
+  }
+
+  // mission队列处理
+  async mission() {
+    const { mq } = this.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      const queue = 'mission/market';
+      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;

+ 115 - 0
app/service/util/spm.js

@@ -0,0 +1,115 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const _ = require('lodash');
+const assert = require('assert');
+
+// 只属于这个项目的工具service
+class SpmService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'spm');
+    this.org = this.ctx.model.Organization;
+    this.personal = this.ctx.model.Personal;
+  }
+
+  /**
+   * 入口,根据method,分配到函数中进行处理,返回结果
+   * @param {Object} {method} method:在这个service的函数
+   * @param {*} body 条件
+   */
+  async index({ method }, body) {
+    return await this[method](body);
+  }
+
+  /**
+   * 获取用户信息,个人(专家)/企业都有可能
+   * @param {Object} condition 条件
+   */
+  async getAllUser(condition) {
+    const { ids } = condition;
+    const projection = 'name phone';
+    // 直接先用ids在企业表中查询
+    let arr = [];
+    const org = await this.org.find({ _id: ids }, projection);
+    arr = arr.concat(org);
+    if (org.length !== ids.length) {
+      // 还有个人用户,再查
+      const personal = await this.personal.find({ _id: ids }, projection);
+      arr = arr.concat(personal);
+    }
+    return arr;
+  }
+
+  /**
+   * 根据条件查找用户
+   * @param {Object} condition 查询条件
+   */
+  async getUser(condition) {
+    let res;
+    // 先查是不是企业
+    const org = await this.org.findOne(condition);
+    if (org) res = org;
+    else {
+      const personal = await this.personal.findOne(condition);
+      if (personal) res = personal;
+    }
+    return res;
+  }
+
+  /**
+   * 直接操作model做什么
+   * @param {Object} condition 条件
+   * @property {String} model 表
+   * @property {String} method mongoose的函数
+   * @property {Object} query 查询条件
+   * @property {Object} body 添加/修改数据
+   * @property {String/Object} projection 查询使用,选取指定字段
+   */
+  async useModel(condition = {}) {
+    const { method, query = {}, body = {}, projection } = condition;
+    let { model } = condition;
+    model = _.capitalize(model);
+    assert(model, '缺少指定表');
+    assert(method, '缺少使用函数');
+    // 区分method的情况
+    let res;
+    // 添加/批量添加
+    if (method === 'create' || method === 'insertMany') {
+      res = await this.ctx.model[model][method](body);
+    }
+    // 修改(updateOne)/批量修改(updateMany)
+    if (method.includes('update')) {
+      res = await this.ctx.model[model][method](query, body);
+    }
+    // 删除(deleteOne)/批量删除(deleteMany)
+    if (method.includes('delete')) {
+      res = await this.ctx.model[model][method](query);
+    }
+    // 查询(find,findById,findOne),反正都是只要query
+    if (method.includes('find')) {
+      res = await this.ctx.model[model][method](query, projection);
+    }
+    // 再下来,就是聚合了,聚合就算了吧.这个还跨项目写就没啥必要了
+    return res;
+  }
+
+  /**
+   * 调用service
+   * @param {Object} condition 条件
+   * @property {String} model 表
+   * @property {String} method mongoose的函数
+   */
+  async useService(condition) {
+    const { service, method, body } = condition;
+    assert(service, '缺少指定表');
+    assert(method, '缺少使用函数');
+    const arr = service.split('.');
+    let serve = this.ctx.service;
+    for (const i of arr) {
+      serve = serve[i];
+    }
+    return await serve[method](body);
+  }
+}
+
+module.exports = SpmService;

+ 144 - 0
app/service/util/util.js

@@ -0,0 +1,144 @@
+'use strict';
+const _ = require('lodash');
+const moment = require('moment');
+const { CrudService } = require('naf-framework-mongoose/lib/service');
+const { ObjectId } = require('mongoose').Types;
+const fs = require('fs');
+class UtilService extends CrudService {
+  constructor(ctx) {
+    super(ctx);
+    this.mq = this.ctx.mq;
+  }
+  async utilMethod(query, body) {
+    const Path = require('path');
+    const Excel = require('exceljs');
+
+    const { data } = await this.ctx.service.users.expert.query();
+    const root_path = 'E:\\exportFile\\';
+    const file_type = '';
+    if (!fs.existsSync(`${root_path}${file_type}`)) {
+      // 如果不存在文件夹,就创建
+      fs.mkdirSync(`${root_path}${file_type}`);
+    }
+    const workbook = new Excel.Workbook();
+    let sheet;
+    sheet = workbook.addWorksheet('sheet');
+    const meta = this.getHeader();
+    const head = meta.map(i => i.label);
+    // sheet.addRows(head);
+    const rows = [];
+    rows.push(head);
+    for (let i = 0; i < data.length; i++) {
+      const e = data[i];
+      const row = [];
+      let imgid;
+      for (const obj of meta) {
+        const { key } = obj;
+        if (key !== 'img_path') {
+          row.push(e[key] || '');
+        } else if (e.img_path) {
+          try {
+            const suffix = Path.extname(e.img_path).substring(1);
+            // 先请求图片buffer,然后addImage存起来
+            const res = await this.ctx.curl(`http://broadcast.waityou24.cn${e.img_path}`);
+            if (res.status === 200) {
+              const buffer = res.data;
+              imgid = workbook.addImage({
+                buffer,
+                extension: suffix,
+              });
+            }
+          } catch (error) {
+            console.log(`${e.name}图片下载失败`);
+          }
+
+        }
+      }
+      rows.push(row);
+      if (imgid || imgid === 0) {
+        sheet.addImage(imgid, {
+          tl: { col: 15.2, row: i + 1 + 0.2 },
+          br: { col: 16, row: i + 1 + 1 },
+          editAs: 'oneCell',
+        });
+      }
+    }
+    sheet.addRows(rows);
+    const filepath = `${root_path}专家导出.xlsx`;
+    await workbook.xlsx.writeFile(filepath);
+  }
+
+  getHeader() {
+    return [
+      { key: 'name', label: '用户姓名' },
+      { key: 'phone', label: '联系电话' },
+      { key: 'education', label: '最高学历' },
+      { key: 'school', label: '毕业学校' },
+      { key: 'birthDate', label: '出生日期' },
+      { key: 'email', label: '电子邮箱' },
+      { key: 'qqwx', label: 'QQ/微信' },
+      { key: 'company', label: '工作单位' },
+      { key: 'zwzc', label: '职务职称' },
+      { key: 'expertise', label: '擅长领域' },
+      { key: 'workexperience', label: '工作经历' },
+      { key: 'scientific', label: '科研综述' },
+      { key: 'undertakingproject', label: '承担项目' },
+      { key: 'scienceaward', label: '科技奖励' },
+      { key: 'social', label: '社会任职' },
+      { key: 'img_path', label: '用户头像' },
+    ];
+  }
+
+  dealQuery(query) {
+    return this.turnFilter(this.turnDateRangeQuery(query));
+  }
+
+  /**
+   * 将查询条件中模糊查询的标识转换成对应object
+   * @param {Object} filter 查询条件
+   */
+  turnFilter(filter) {
+    const str = /^%\S*%$/;
+    const keys = Object.keys(filter);
+    for (const key of keys) {
+      const res = key.match(str);
+      if (res) {
+        const newKey = key.slice(1, key.length - 1);
+        filter[newKey] = new RegExp(filter[key]);
+        delete filter[key];
+      }
+    }
+    return filter;
+  }
+  /**
+   * 将时间转换成对应查询Object
+   * @param {Object} filter 查询条件
+   */
+  turnDateRangeQuery(filter) {
+    const keys = Object.keys(filter);
+    for (const k of keys) {
+      if (k.includes('@')) {
+        const karr = k.split('@');
+        //  因为是针对处理范围日期,所以必须只有,开始时间和结束时间
+        if (karr.length === 2) {
+          const type = karr[1];
+          if (type === 'start') {
+            filter[karr[0]] = {
+              ..._.get(filter, karr[0], {}),
+              $gte: filter[k],
+            };
+          } else {
+            filter[karr[0]] = {
+              ..._.get(filter, karr[0], {}),
+              $lte: filter[k],
+            };
+          }
+          delete filter[k];
+        }
+      }
+    }
+    return filter;
+  }
+
+}
+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

+ 64 - 0
config/config.default.js

@@ -0,0 +1,64 @@
+/* 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 + '_1625793914189_5278';
+
+  // add your middleware config here
+  config.middleware = [];
+
+  // add your user config here
+  const userConfig = {
+    // myAppName: 'egg',
+  };
+
+  config.cluster = {
+    listen: {
+      port: 9106,
+    },
+  };
+
+  config.amqp = {
+    client: {
+      hostname: '127.0.0.1',
+      username: 'visit',
+      password: 'visit',
+      vhost: 'platform',
+    },
+    app: true,
+    agent: true,
+  };
+
+  config.dbName = 'customer';
+  config.mongoose = {
+    url: `mongodb://localhost:27017/${config.dbName}`,
+    options: {
+      // user: 'admin',
+      // pass: '111111',
+      // authSource: 'admin',
+      // useNewUrlParser: true,
+      // useCreateIndex: true,
+    },
+  };
+
+  config.project = {
+    hnhmain: 'http://127.0.0.1:9201',
+  };
+
+
+  return {
+    ...config,
+    ...userConfig,
+  };
+};

+ 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',
+// };

+ 5 - 0
jsconfig.json

@@ -0,0 +1,5 @@
+{
+  "include": [
+    "**/*"
+  ]
+}

+ 51 - 0
package.json

@@ -0,0 +1,51 @@
+{
+  "name": "customer-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.29.1",
+    "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-customer-service",
+    "stop": "egg-scripts stop --title=egg-server-customer-service",
+    "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": "",
+  "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);
+  });
+});