guhongwei 3 yıl önce
işleme
26c89fb1b7

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

+ 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-question
+
+问卷调查
+
+## 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

+ 73 - 0
app/controller/.answer.js

@@ -0,0 +1,73 @@
+module.exports = {
+  create: {
+    requestBody: [
+      "questionnaire_id",
+      "answer",
+      "user_id",
+      "phone",
+      "contacts",
+      "company",
+      "create_user",
+      "address",
+      "attribute",
+      "category",
+    ],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: [
+      "questionnaire_id",
+      "answer",
+      "user_id",
+      "phone",
+      "contacts",
+      "company",
+      "address",
+      "attribute",
+      "category",
+    ],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        questionnaire_id: "questionnaire_id",
+        answer: "answer",
+        user_id: "user_id",
+        phone: "phone",
+        contacts: "contacts",
+        company: "company",
+        "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"],
+      desc: true,
+      count: true,
+    },
+  },
+  getAnswer: {
+    parameters: {
+      query: {
+        questionnaire_id: "questionnaire_id",
+        user_id: "user_id",
+      },
+    },
+    service: "findOne",
+  },
+};

+ 97 - 0
app/controller/.investigation.js

@@ -0,0 +1,97 @@
+module.exports = {
+  create: {
+    requestBody: [
+      'name',
+      'address',
+      'postal',
+      'web_site',
+      'register_type',
+      'field',
+      'register_time',
+      'funds',
+      'register_address',
+      'brief',
+      'legal_person',
+      'person_number',
+      'bk_number',
+      'research_number',
+      'advanced_number',
+      'contact',
+      'contact_tel',
+      'email',
+      'qq',
+      'products',
+      'requirement',
+      'techol_name',
+      'urgent',
+      'cooperation',
+      'budget',
+    ],
+  },
+  destroy: {
+    params: ['!id'],
+    service: 'delete',
+  },
+  update: {
+    params: ['!id'],
+    requestBody: [
+      'name',
+      'address',
+      'postal',
+      'web_site',
+      'register_type',
+      'field',
+      'register_time',
+      'funds',
+      'register_address',
+      'brief',
+      'legal_person',
+      'person_number',
+      'bk_number',
+      'research_number',
+      'advanced_number',
+      'contact',
+      'contact_tel',
+      'email',
+      'qq',
+      'products',
+      'requirement',
+      'techol_name',
+      'urgent',
+      'cooperation',
+      'budget',
+    ],
+  },
+  show: {
+    parameters: {
+      params: ['!id'],
+    },
+    service: 'fetch',
+  },
+  index: {
+    parameters: {
+      query: {
+        'meta.createdAt@start': 'meta.createdAt@start',
+        'meta.createdAt@end': 'meta.createdAt@end',
+        name: 'name',
+        field: 'field',
+        register_time: 'register_time',
+        register_type: 'register_type',
+        contact_tel: 'contact_tel',
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: 'query',
+    options: {
+      query: ['skip', 'limit'],
+      sort: ['meta.createdAt'],
+      desc: true,
+      count: true,
+    },
+  },
+  export: {
+    requestBody:[]
+  },
+};

+ 40 - 0
app/controller/.question.js

@@ -0,0 +1,40 @@
+module.exports = {
+  create: {
+    requestBody: ["title", "type", "selects", "user_id"],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: ["title", "type", "selects", "user_id"],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        title: "title",
+        user_id: "user_id",
+        type: "type",
+        "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"],
+      desc: true,
+      count: true,
+    },
+  },
+};

+ 40 - 0
app/controller/.questionnaire.js

@@ -0,0 +1,40 @@
+module.exports = {
+  create: {
+    requestBody: ["title", "user_id", "questions", "brief", "column"],
+  },
+  destroy: {
+    params: ["!id"],
+    service: "delete",
+  },
+  update: {
+    params: ["!id"],
+    requestBody: ["title", "user_id", "questions", "brief", "column"],
+  },
+  show: {
+    parameters: {
+      params: ["!id"],
+    },
+    service: "fetch",
+  },
+  index: {
+    parameters: {
+      query: {
+        title: "title",
+        user_id: "user_id",
+        column: "column",
+        "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"],
+      desc: true,
+      count: true,
+    },
+  },
+};

+ 13 - 0
app/controller/answer.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.answer.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 问卷回答
+class AnswerController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.answer;
+  }
+}
+module.exports = CrudController(AnswerController, meta);

+ 17 - 0
app/controller/home.js

@@ -0,0 +1,17 @@
+'use strict';
+
+const Controller = require('egg').Controller;
+
+class HomeController extends Controller {
+  async index() {
+    const { ctx } = this;
+    ctx.body = 'hi, egg';
+  }
+
+  async qrcode() {
+    const data = await this.ctx.service.qrcode.create(this.ctx.request.body);
+    this.ctx.ok({ data });
+  }
+}
+
+module.exports = HomeController;

+ 13 - 0
app/controller/investigation.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.investigation.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 企业调查
+class InvestigationController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.investigation;
+  }
+}
+module.exports = CrudController(InvestigationController, meta);

+ 13 - 0
app/controller/question.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.question.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 问题
+class QuestionController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.question;
+  }
+}
+module.exports = CrudController(QuestionController, meta);

+ 13 - 0
app/controller/questionnaire.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./.questionnaire.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose/lib/controller');
+
+// 问卷
+class QuestionnaireController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.questionnaire;
+  }
+}
+module.exports = CrudController(QuestionnaireController, meta);

+ 48 - 0
app/middleware/user.js

@@ -0,0 +1,48 @@
+'use strict';
+const _ = require('lodash');
+const { ObjectId } = require('mongoose').Types;
+const querystring = require('querystring');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+module.exports = options => {
+  return async function user(ctx, next) {
+    // 查user_name去换user_id回来,加到查询条件里
+    const url = ctx.request.url.split('?')[1];
+    const query = JSON.parse(JSON.stringify(querystring.parse(url)));
+    console.log(query);
+    if (query['user.name']) {
+      // 查用户
+      const res = await ctx.service.util.httpUtil.cpost('/spm', 'live', { name: query['user.name'] }, { method: 'getUser' });
+      if (res) {
+        // 有这个用户,则在url追加user_id的条件,让框架的controller去过滤条件,查询
+        const { _id: user_id } = res;
+        ctx.request.url = `${ctx.request.url}&user_id=${user_id}`;
+      } else {
+        throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '该用户不存在');
+      }
+    }
+    await next();
+    // 获取query参数
+    if (query.user && query.user === 'true') {
+      // 将列表的用户id换成用户信息
+      let data = _.get(ctx.response, 'body.data');
+      if (data) {
+        data = JSON.parse(JSON.stringify(data));
+        const query = { method: 'getAllUser' };
+        const body = { ids: data.map(i => i.user_id) };
+        try {
+          const res = await ctx.service.util.httpUtil.cpost('/spm', 'live', body, query);
+          for (const i of data) {
+            const r = res.find(f => ObjectId(f._id).equals(i.user_id));
+            if (r) {
+              i.user = r;
+            } else i.user = {};
+          }
+        } catch (error) {
+          ctx.logger.error('user中间件:获取用户信息失败');
+        }
+        ctx.response.body.data = data;
+      }
+    }
+  };
+
+};

+ 32 - 0
app/model/answer.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 answer = {
+  questionnaire_id: { type: ObjectId }, // 问卷id
+  answer: { type: Array }, // 答案
+  user_id: { type: ObjectId },//用户id
+  phone: { type: String },//电话
+  contacts: { type: String },//联系人
+  company: { type: String },//企业名称
+  address: { type: String },//联系地址
+  attribute: { type: String },//企业属性
+  category: { type: String },//产品类别
+  remark: { type: String, maxLength: 200 },
+  create_time: {
+    type: String,
+    default: moment(new Date()).format("YYYY-MM-DD HH:mm:ss"),
+  },
+};
+const schema = new Schema(answer, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ questionnaire_id: 1 });
+schema.index({ user_id: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Answer', schema, 'answer');
+};

+ 48 - 0
app/model/investigation.js

@@ -0,0 +1,48 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose/lib/model/meta-plugin');
+
+// 企业调查
+const investigation = {
+  name: { type: String, required: true }, // 企业名称
+  address: { type: String, required: false }, // 通讯地址
+  postal: { type: String, required: false }, // 邮编
+  web_site: { type: String, required: false }, // 网址
+  register_type: { type: Array, required: false }, // 注册登记类型
+  field: { type: Array, required: false }, // 所属领域
+  register_time: { type: String, required: false }, // 注册时间
+  funds: { type: String, required: false }, // 注册资金
+  register_address: { type: String, required: false }, // 注册地
+  brief: { type: String, required: false }, // 企业概况
+  legal_person: { type: Object, required: false }, // 法人信息
+  person_number: { type: String, required: false }, // 职工人数
+  bk_number: { type: String, required: false }, // 本科以上人数
+  research_number: { type: String, required: false }, // 研究开发人数
+  advanced_number: { type: String, required: false }, // 高级职称人数
+  contact: { type: String, required: false }, // 联系人
+  contact_tel: { type: String, required: false }, // 联系电话
+  email: { type: String, required: false }, // 电子邮箱
+  qq: { type: String, required: false }, // qq
+  products: { type: Array, required: false }, // 产品列表
+  requirement: { type: Object, required: false }, // 企业需求情况
+  // 2021-11-14
+  techol_name: { type: String, required: false }, // 需求名称
+  urgent: { type: String, required: false }, // 紧急程度
+  cooperation: { type: Array, required: false }, // 合作方式
+  budget: { type: String, required: false }, // 投资预算
+};
+
+const schema = new Schema(investigation, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.index({ name: 1 });
+schema.index({ field: 1 });
+schema.index({ register_time: 1 });
+schema.index({ register_type: 1 });
+schema.index({ contact_tel: 1 });
+
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Investigation', schema, 'investigation');
+};

+ 25 - 0
app/model/question.js

@@ -0,0 +1,25 @@
+'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 question = {
+  title: { type: String },
+  type: { type: String }, // 类型:0=>单选;1=>多选;2=>简答
+  selects: { type: Array }, // 选项
+  user_id: { type: ObjectId }, // 所属用户
+  remark: { type: String, maxLength: 200 },
+  create_time: { type: String, default: moment(new Date()).format('YYYY-MM-DD HH:mm:ss') },
+};
+const schema = new Schema(question, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ title: 1 });
+schema.index({ user_id: 1 });
+schema.index({ type: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Question', schema, 'question');
+};

+ 26 - 0
app/model/questionnaire.js

@@ -0,0 +1,26 @@
+'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 questionnaire = {
+  title: { type: String },
+  user_id: { type: ObjectId },
+  questions: { type: Array, select: false },
+  brief: { type: String }, // 简介
+  column: { type: String }, // 简介
+  remark: { type: String, maxLength: 200 },
+  create_time: { type: String, default: moment(new Date()).format('YYYY-MM-DD HH:mm:ss') },
+};
+const schema = new Schema(questionnaire, { toJSON: { virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ title: 1 });
+schema.index({ column: 1 });
+schema.index({ questions: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.plugin(metaPlugin);
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('Questionnaire', schema, 'questionnaire');
+};

+ 14 - 0
app/router.js

@@ -0,0 +1,14 @@
+'use strict';
+
+/**
+ * @param {Egg.Application} app - egg application
+ */
+module.exports = app => {
+  const { router, controller } = app;
+  const profix = '/api/question/';
+  router.get('/', controller.home.index);
+  require('./router/question')(app); // 问题
+  require('./router/questionnaire')(app); // 问卷
+  require('./router/answer')(app); // 回答
+  require('./router/investigation')(app); // 企业调查
+};

+ 12 - 0
app/router/answer.js

@@ -0,0 +1,12 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller } = app;
+  const profix = '/api/question/';
+  const target = 'answer';
+  const user = app.middleware.user();
+  router.get(target, `${profix}${target}/getAnswer`, controller[target].getAnswer);
+  router.resources(target, `${profix}${target}`, user, controller[target]); // index、create、show、destroy
+  router.post(target, `${profix}${target}/update/:id`, controller[target].update);
+};

+ 11 - 0
app/router/investigation.js

@@ -0,0 +1,11 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller } = app;
+  const profix = '/api/question/';
+  const target = 'investigation';
+  router.post(target, `${profix}${target}/export`, controller[target].export);
+  router.resources(target, `${profix}${target}`, controller[target]); // index、create、show、destroy
+  router.post(target, `${profix}${target}/update/:id`, controller[target].update);
+};

+ 10 - 0
app/router/question.js

@@ -0,0 +1,10 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller } = app;
+  const profix = '/api/question/';
+  const target = 'question';
+  router.resources(target, `${profix}${target}`, controller[target]); // index、create、show、destroy
+  router.post(target, `${profix}${target}/update/:id`, controller[target].update);
+};

+ 10 - 0
app/router/questionnaire.js

@@ -0,0 +1,10 @@
+'use strict';
+
+
+module.exports = app => {
+  const { router, controller } = app;
+  const profix = '/api/question/';
+  const target = 'questionnaire';
+  router.resources(target, `${profix}${target}`, controller[target]); // index、create、show、destroy
+  router.post(target, `${profix}${target}/update/:id`, controller[target].update);
+};

+ 43 - 0
app/service/answer.js

@@ -0,0 +1,43 @@
+'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 AnswerService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'answer');
+    this.model = this.ctx.model.Answer;
+  }
+
+  async create(body) {
+    const { phone, user_id, create_user = true } = body;
+    if (!user_id && phone && create_user) {
+      // 没有用户id,但是有电话,且需要注册
+      // 查询用户
+      let user = await this.ctx.service.util.httpUtil.cpost('/spm', 'live', { model: 'personal', method: 'findOne', query: { phone } }, { method: 'useModel' });
+      if (user) {
+        body.user_id = user._id;
+        delete body.phone;
+      } else {
+        user = await this.ctx.service.util.httpUtil.cpost('/spm', 'live', { service: 'users.personal', method: 'create', body: { phone, name: '个人', code: 'WJDCXT', password: '123456', status: '1' } }, { method: 'useService' });
+        if (user) {
+          body.user_id = user._id;
+          delete body.phone;
+        } else throw new BusinessError(ErrorCode.SERVICE_FAULT, '创建用户失败');
+      }
+    }
+    const res = await this.model.create(body);
+    return res;
+  }
+
+  async findOne({ questionnaire_id, user_id }) {
+    assert(questionnaire_id, '缺少问卷信息');
+    assert(user_id, '缺少填写人信息');
+    const res = await this.model.findOne({ questionnaire_id, user_id });
+    return res;
+  }
+}
+
+module.exports = AnswerService;

+ 103 - 0
app/service/investigation.js

@@ -0,0 +1,103 @@
+'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 fs = require('fs');
+const path = require('path');
+const PizZip = require('pizzip');
+const Docxtemplater = require('docxtemplater');
+const archiver = require('archiver');
+const moment = require('moment');
+// 企业需求
+class InvestigationService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'investigation');
+    this.model = this.ctx.model.Investigation;
+    this.root_path = _.get(this.ctx.app.config.export, 'root_path');
+    this.dir = [ 'question', 'investigation' ];
+    this.exportPrefix = '企业需求调查表';
+  }
+
+  async query(filter, options = {}) {
+    const { filter: nf, options: no } = this.ctx.service.util.util.dealQuery(_.cloneDeep(filter), _.cloneDeep(options));
+    const { skip, limit, sort, projection } = no;
+    const rs = await this.model.find(nf, projection, { skip, limit, sort }).exec();
+    return rs;
+  }
+
+  async count(filter) {
+    const nf = this.ctx.service.util.util.dealFilter(_.cloneDeep(filter));
+    const res = await this.model.countDocuments(nf).exec();
+    return res;
+  }
+
+  async export() {
+    // 检验数据是否存在
+    const data = await this.model.find();
+    if (data.length <= 0) return '没有企业需求调查的数据';
+    // 检测,创建目录
+    let newPath = this.root_path;
+    for (const p of this.dir) {
+      newPath = path.join(newPath, p);
+      if (!fs.existsSync(newPath)) {
+        fs.mkdirSync(newPath);
+      }
+    }
+    // 循环处理
+    // 存储文件名, 为了之后打包文件,然后把压缩包内对应的文件删除掉
+    const fileArray = [];
+    for (const dup of data) {
+      const d = _.omit(JSON.parse(JSON.stringify(dup)), [ 'meta' ]);
+      d['meta.createdAt'] = moment(_.get(dup, 'meta.createdAt')).format('YYYY-MM-DD');
+      for (const key in d) {
+        const e = d[key];
+        if (!_.isObject(e)) continue;
+        for (const okey of Object.keys(e)) {
+          d[`${key}.${okey}`] = e[okey];
+        }
+      }
+      // 获取模板
+      const content = fs.readFileSync(path.resolve('./', 'template', 'template.docx'));
+      const zip = new PizZip(content);
+      const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true });
+      doc.render(d);
+      const buf = doc.getZip().generate({ type: 'nodebuffer' });
+      const fileName = `${_.get(d, 'name', _.get(d, '_id'))}.docx`;
+      fileArray.push(fileName);
+      fs.writeFileSync(path.join(newPath, fileName), buf);
+    }
+    const zipPath = await this.buildZip(newPath, fileArray);
+    return zipPath;
+  }
+
+  /**
+   * 打zip包
+   * @param {String} dirPath 路径
+   * @param {Array} files 文件数组
+   */
+  async buildZip(dirPath, files) {
+    const zipName = `${this.exportPrefix}-${new Date().getTime()}.zip`;
+    const output = fs.createWriteStream(path.join(`${dirPath}`, zipName));
+    const archive = archiver('zip', {
+      zlib: { level: 9 },
+    });
+    archive.pipe(output);
+    for (const f of files) {
+      archive.file(path.join(dirPath, f), {
+        name: path.basename(f),
+      });
+    }
+    await archive.finalize();
+    // 删除文件
+    for (const f of files) {
+      const filePath = path.join(dirPath, f);
+      fs.unlinkSync(filePath);
+    }
+    // 拼接口返回的路径 /files/...
+    const returnPath = `/files/${this.dir.join('/')}/${zipName}`;
+    return returnPath;
+  }
+}
+
+module.exports = InvestigationService;

+ 43 - 0
app/service/question.js

@@ -0,0 +1,43 @@
+'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 { ObjectId } = require('mongoose').Types;
+
+// 问题
+class QuestionService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'question');
+    this.model = this.ctx.model.Question;
+    this.questionnaire = this.ctx.model.Questionnaire;
+  }
+  /**
+   * 重写删除函数:因为删除题目同时也需要删除问卷保存该题目的id
+   * @param {Object} { id } 数据id
+   */
+  async delete({ id }) {
+    const res = await this.questionnaire.find({ questions: id }, '+questions');
+    if (res.length > 0) {
+      for (const i of res) {
+        i.questions = i.questions.filter(f => f !== id);
+        await i.save();
+      }
+    }
+    await this.model.deleteOne({ _id: id });
+  }
+
+  async getQuestions(ids) {
+    const res = await this.model.find({ _id: ids });
+    // 按选择顺序排序
+    const arr = [];
+    for (let i = 0; i < ids.length; i++) {
+      const id = ids[i];
+      const r = res.find(f => ObjectId(id).equals(f._id));
+      if (r) arr.push(r);
+    }
+    return arr;
+  }
+}
+
+module.exports = QuestionService;

+ 35 - 0
app/service/questionnaire.js

@@ -0,0 +1,35 @@
+'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 { ObjectId } = require('mongoose').Types;
+
+// 问卷
+class QuestionnaireService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'questionnaire');
+    this.model = this.ctx.model.Questionnaire;
+    this.question = this.ctx.service.question;
+  }
+  async fetch({ id }) {
+    const data = await this.model.findById(id, '+questions');
+    if (!data) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到指定问卷!');
+    const obj = data.questions;
+    if (obj.every(e => _.isObject(e))) return data;
+    // 如果到这里了,说明不全是现编的题,有从库里选的,是string类型的id,过滤出这部分
+    const ids = obj.filter(f => _.isString(f));
+    if (ids.length > 0) {
+      const partQuests = await this.question.getQuestions(ids);
+      // 查出了后将对应位置的id换成题目
+      for (const i of partQuests) {
+        const index = obj.findIndex(f => _.isString(f) && ObjectId(i._id).equals(f));
+        obj[index] = i;
+      }
+      data.questions = obj;
+    }
+    return data;
+  }
+}
+
+module.exports = QuestionnaireService;

+ 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 cget(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 cpost(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;

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

@@ -0,0 +1,82 @@
+'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) {
+    this.ctx.service.patent.patentearly.needWarning();
+  }
+
+  dealQuery(filter, { skip, limit, sort, desc, projection } = {}) {
+    // 处理排序
+    if (sort && _.isString(sort)) {
+      sort = { [sort]: desc ? -1 : 1 };
+    } else if (sort && _.isArray(sort)) {
+      sort = sort.map((f) => ({ [f]: desc ? -1 : 1 })).reduce((p, c) => ({ ...p, ...c }), {});
+    }
+    const options = { skip, limit, sort, desc, projection };
+    filter = this.turnFilter(this.turnDateRangeQuery(filter));
+    return { filter, options };
+  }
+
+  dealFilter(filter) {
+    return this.turnFilter(this.turnDateRangeQuery(filter));
+  }
+
+  /**
+   * 将查询条件中模糊查询的标识转换成对应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') {
+            if (filter[k] && filter[k] !== '') {
+              filter[karr[0]] = {
+                ..._.get(filter, karr[0], {}),
+                $gte: filter[k],
+              };
+            }
+          } else {
+            if (filter[k] && filter[k] !== '') {
+              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

+ 59 - 0
config/config.default.js

@@ -0,0 +1,59 @@
+/* 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 + '_1617689037805_3801';
+
+  // add your middleware config here
+  config.middleware = [];
+
+  // add your user config here
+  const userConfig = {
+    // myAppName: 'egg',
+  };
+  config.cluster = {
+    listen: {
+      port: 9104,
+    },
+  };
+
+  config.dbName = 'question';
+  config.mongoose = {
+    url: `mongodb://localhost:27017/${config.dbName}`,
+    options: {
+      user: 'admin',
+      pass: 'admin',
+      authSource: 'admin',
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+  config.project = {
+    live: 'http://127.0.0.1:9101/api/live/v0',
+  };
+  config.baseUrl = 'http://broadcast.waityou24.cn';
+  config.wxapi = {
+    appid: 'wx6db5d25b3e7cfc14', // 微信公众号APPID
+    baseUrl: 'http://wx.cc-lotus.info', // 微信网关地址
+  };
+  config.export = {
+    root_path: 'S:\\workspace\\exportFile',
+    domain: 'http://broadcast.waityou24.cn',
+  };
+
+  return {
+    ...config,
+    ...userConfig,
+  };
+};

+ 21 - 0
config/config.prod.js

@@ -0,0 +1,21 @@
+'use strict';
+
+module.exports = () => {
+  const config = exports = {};
+  config.dbName = 'question';
+  config.mongoose = {
+    url: `mongodb://localhost:27017/${config.dbName}`,
+    options: {
+      user: 'admin',
+      pass: 'admin',
+      authSource: 'admin',
+      useNewUrlParser: true,
+      useCreateIndex: true,
+    },
+  };
+  config.export = {
+    root_path: '/usr/local/workspace/server/service-file/upload',
+    domain: 'http://127.0.0.1',
+  };
+  return config;
+};

+ 9 - 0
config/plugin.js

@@ -0,0 +1,9 @@
+'use strict';
+
+/** @type Egg.EggPlugin */
+module.exports = {
+  // had enabled by egg
+  // static: {
+  //   enable: true,
+  // }
+};

+ 17 - 0
ecosystem.config.js

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

+ 54 - 0
package.json

@@ -0,0 +1,54 @@
+{
+  "name": "service-question",
+  "version": "1.0.0",
+  "description": "问卷调查",
+  "private": true,
+  "egg": {
+    "framework": "naf-framework-mongoose"
+  },
+  "dependencies": {
+    "archiver": "^5.3.0",
+    "docxtemplater": "^3.26.4",
+    "egg": "^2.15.1",
+    "egg-naf-amqp": "0.0.13",
+    "egg-redis": "^2.4.0",
+    "egg-scripts": "^2.11.0",
+    "lodash": "^4.17.15",
+    "moment": "^2.24.0",
+    "naf-framework-mongoose": "^0.6.11",
+    "pizzip": "^3.1.1"
+  },
+  "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-service-question",
+    "stop": "egg-scripts stop --title=egg-server-service-question",
+    "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": "lrf",
+  "license": "MIT"
+}

+ 9 - 0
server.js

@@ -0,0 +1,9 @@
+
+// eslint-disable-next-line strict
+const egg = require('egg');
+
+const workers = Number(1);
+egg.startCluster({
+  workers,
+  baseDir: __dirname,
+});

BIN
template/template.docx


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