lrf 8 tháng trước cách đây
commit
4e5d99b45e

+ 11 - 0
.editorconfig

@@ -0,0 +1,11 @@
+# 🎨 editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+insert_final_newline = true

+ 7 - 0
.eslintrc.json

@@ -0,0 +1,7 @@
+{
+  "extends": "./node_modules/mwts/",
+  "ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
+  "env": {
+    "jest": true
+  }
+}

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+logs/
+npm-debug.log
+yarn-error.log
+node_modules/
+coverage/
+dist/
+.idea/
+run/
+.DS_Store
+*.sw*
+*.un~
+.tsbuildinfo
+.tsbuildinfo.*

+ 4 - 0
.prettierrc.js

@@ -0,0 +1,4 @@
+module.exports = {
+  ...require('mwts/.prettierrc.json'),
+  printWidth: 250,
+}

+ 29 - 0
README.md

@@ -0,0 +1,29 @@
+# my_midway_project
+
+## QuickStart
+
+<!-- add docs here for user -->
+
+see [midway docs][midway] for more detail.
+
+### Development
+
+```bash
+$ npm i
+$ npm run dev
+$ open http://localhost:7001/
+```
+
+### Deploy
+
+```bash
+$ npm start
+```
+
+### npm scripts
+
+- Use `npm run lint` to check code style.
+- Use `npm test` to run unit test.
+
+
+[midway]: https://midwayjs.org

+ 29 - 0
README.zh-CN.md

@@ -0,0 +1,29 @@
+# my_midway_project
+
+## 快速入门
+
+<!-- 在此次添加使用文档 -->
+
+如需进一步了解,参见 [midway 文档][midway]。
+
+### 本地开发
+
+```bash
+$ npm i
+$ npm run dev
+$ open http://localhost:7001/
+```
+
+### 部署
+
+```bash
+$ npm start
+```
+
+### 内置指令
+
+- 使用 `npm run lint` 来做代码风格检查。
+- 使用 `npm test` 来执行单元测试。
+
+
+[midway]: https://midwayjs.org

+ 2 - 0
bootstrap.js

@@ -0,0 +1,2 @@
+const { Bootstrap } = require('@midwayjs/bootstrap');
+Bootstrap.run();

+ 20 - 0
ecosystem.config.js

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

+ 6 - 0
jest.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
+  coveragePathIgnorePatterns: ['<rootDir>/test/'],
+};

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 8334 - 0
package-lock.json


+ 49 - 0
package.json

@@ -0,0 +1,49 @@
+{
+  "name": "my-midway-project",
+  "version": "1.0.0",
+  "description": "",
+  "private": true,
+  "dependencies": {
+    "@elastic/elasticsearch": "^8.12.2",
+    "@midwayjs/bootstrap": "^3.12.0",
+    "@midwayjs/core": "^3.12.0",
+    "@midwayjs/info": "^3.12.0",
+    "@midwayjs/koa": "^3.12.0",
+    "@midwayjs/logger": "^3.1.0",
+    "@midwayjs/validate": "^3.12.0",
+    "dayjs": "^1.11.12",
+    "lodash": "^4.17.21"
+  },
+  "devDependencies": {
+    "@midwayjs/mock": "^3.12.0",
+    "@types/jest": "^29.2.0",
+    "@types/lodash": "^4.17.7",
+    "@types/node": "14",
+    "cross-env": "^6.0.0",
+    "jest": "^29.2.2",
+    "mwts": "^1.3.0",
+    "mwtsc": "^1.4.0",
+    "ts-jest": "^29.0.3",
+    "typescript": "~4.8.0"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  },
+  "scripts": {
+    "start": "NODE_ENV=production node ./bootstrap.js",
+    "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
+    "self": "cross-env NODE_ENV=self mwtsc --watch --run @midwayjs/mock/app.js",
+    "test": "cross-env NODE_ENV=unittest jest",
+    "cov": "jest --coverage",
+    "lint": "mwts check",
+    "lint:fix": "mwts fix",
+    "ci": "npm run cov",
+    "build": "mwtsc --cleanOutDir"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "anonymous",
+  "license": "MIT"
+}

+ 9 - 0
src/config/config.default.ts

@@ -0,0 +1,9 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1723890705491_4391',
+  koa: {
+    port: 7001,
+  },
+} as MidwayConfig;

+ 32 - 0
src/config/config.local.ts

@@ -0,0 +1,32 @@
+import { MidwayConfig } from '@midwayjs/core';
+/**elasticsearch ip */
+const esIp = '10.120.114.6'
+/**elasticsearch 端口 */
+const esPort= '9200'
+/**elasticsearch 用户名 */
+const esUserName = 'elastic'
+/**elasticsearch 密码 */
+const esPassword = 'NAjqFz_7tS2DkdpU7p*x'
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1697684406848_4978',
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: 3600, // 3600
+  },
+  koa: {
+    port: 9701,
+    globalPrefix: '/cxyy/es',
+    queryParseMode: 'extended',
+  },
+  swagger: {
+    swaggerPath: '/doc/api',
+  },
+  elasticsearch: {
+    node: `http://${esIp}:${esPort}`,
+    auth: {
+      username: esUserName,
+      password: esPassword,
+    },
+  },
+} as MidwayConfig;

+ 32 - 0
src/config/config.prod.ts

@@ -0,0 +1,32 @@
+import { MidwayConfig } from '@midwayjs/core';
+/**elasticsearch ip */
+const esIp = '10.120.114.6'
+/**elasticsearch 端口 */
+const esPort= '9200'
+/**elasticsearch 用户名 */
+const esUserName = 'elastic'
+/**elasticsearch 密码 */
+const esPassword = 'NAjqFz_7tS2DkdpU7p*x'
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1697684406848_4978',
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: 3600, // 3600
+  },
+  koa: {
+    port: 9701,
+    globalPrefix: '/cxyy/es',
+    queryParseMode: 'extended',
+  },
+  swagger: {
+    swaggerPath: '/doc/api',
+  },
+  elasticsearch: {
+    node: `http://${esIp}:${esPort}`,
+    auth: {
+      username: esUserName,
+      password: esPassword,
+    },
+  },
+} as MidwayConfig;

+ 34 - 0
src/config/config.self.ts

@@ -0,0 +1,34 @@
+import { MidwayConfig } from '@midwayjs/core';
+/**elasticsearch ip */
+const esIp = '127.0.0.1'
+/**elasticsearch 端口 */
+const esPort= '9200'
+/**elasticsearch 用户名 */
+const esUserName = 'elastic'
+/**elasticsearch 密码 */
+const esPassword = 'NAjqFz_7tS2DkdpU7p*x'
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1697684406848_4978',
+  // 请求记录在redis留存时间,超过时间.数据变化将不会记录.以秒为单位--5分钟
+  requestTimeLimit: 300,
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: 3600, // 3600
+  },
+  koa: {
+    port: 9701,
+    globalPrefix: '/cxyy/es',
+    queryParseMode: 'extended',
+  },
+  swagger: {
+    swaggerPath: '/doc/api',
+  },
+  elasticsearch: {
+    node: `http://${esIp}:${esPort}`,
+    auth: {
+      username: esUserName,
+      password: esPassword,
+    },
+  },
+} as MidwayConfig;

+ 7 - 0
src/config/config.unittest.ts

@@ -0,0 +1,7 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+export default {
+  koa: {
+    port: null,
+  },
+} as MidwayConfig;

+ 31 - 0
src/configuration.ts

@@ -0,0 +1,31 @@
+import { Configuration, App } from '@midwayjs/core';
+import * as koa from '@midwayjs/koa';
+import * as validate from '@midwayjs/validate';
+import * as info from '@midwayjs/info';
+import { join } from 'path';
+// import { DefaultErrorFilter } from './filter/default.filter';
+// import { NotFoundFilter } from './filter/notfound.filter';
+import { ReportMiddleware } from './middleware/report.middleware';
+
+@Configuration({
+  imports: [
+    koa,
+    validate,
+    {
+      component: info,
+      enabledEnvironment: ['local'],
+    },
+  ],
+  importConfigs: [join(__dirname, './config')],
+})
+export class MainConfiguration {
+  @App('koa')
+  app: koa.Application;
+
+  async onReady() {
+    // add middleware
+    this.app.useMiddleware([ReportMiddleware]);
+    // add filter
+    // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
+  }
+}

+ 9 - 0
src/controller/home.controller.ts

@@ -0,0 +1,9 @@
+import { Controller, Get } from '@midwayjs/core';
+
+@Controller('/')
+export class HomeController {
+  @Get('/')
+  async home(): Promise<string> {
+    return 'es service starting...';
+  }
+}

+ 26 - 0
src/controller/init.controller.ts

@@ -0,0 +1,26 @@
+import { Body, Controller, Inject, Post } from '@midwayjs/core';
+import { ESService } from '../service/elasticsearch/es.service';
+
+@Controller('/init')
+export class InitController {
+  @Inject()
+  esService: ESService;
+
+  /**
+   * 初始化es索引
+   * @param index es中的索引名
+   */
+  @Post('/indices')
+  async indices() {
+    console.log('in init indices')
+    const data = await this.esService.initIndex();
+    return data;
+  }
+
+  @Post('/data')
+  async data(@Body('index') index: string, @Body('data') data: Array<any>) {
+    console.log('in init data')
+    await this.esService.initData(index, data);
+    return 'ok'
+  }
+}

+ 48 - 0
src/controller/search.controller.ts

@@ -0,0 +1,48 @@
+import { Controller, Get, Inject, Query } from '@midwayjs/core';
+import { SearchService } from '../service/search.service';
+import { Context } from '@midwayjs/koa';
+import { omit } from 'lodash';
+
+@Controller('/search')
+export class SearchController {
+  @Inject()
+  searchService: SearchService;
+  @Inject()
+  ctx: Context;
+
+  @Get('/company')
+  async company(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const query = this.ctx.query;
+    const others = omit(query, ['keyword', 'skip', 'limit']);
+    const res = await this.searchService.company(keyword, skip, limit, others);
+    return res;
+  }
+  @Get('/expert')
+  async expert(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const res = await this.searchService.expert(keyword, skip, limit);
+    return res;
+  }
+  @Get('/project')
+  async project(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const res = await this.searchService.project(keyword, skip, limit);
+    return res;
+  }
+
+  /**智能匹配也可以使用 */
+  @Get('/demand')
+  async demand(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const res = await this.searchService.demand(keyword, skip, limit);
+    return res;
+  }
+  /**智能匹配也可以使用 */
+  @Get('/supply')
+  async supply(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const res = await this.searchService.supply(keyword, skip, limit);
+    return res;
+  }
+  @Get('/achievement')
+  async achievement(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const res = await this.searchService.achievement(keyword, skip, limit);
+    return res;
+  }
+}

+ 34 - 0
src/controller/sync.controller.ts

@@ -0,0 +1,34 @@
+import { Body, Controller, Inject, Post } from '@midwayjs/core';
+import { SyncService } from '../service/sync.service';
+import { toLower } from 'lodash';
+import { Methods } from '../service/elasticsearch/indices'
+
+@Controller('/sync')
+export class SyncController {
+  @Inject()
+  syncService: SyncService;
+
+  /**
+   * es同步数据
+   * @param index es中的索引名
+   * @param data 数据
+   * @param method 是由什么数据库操作后来的(create,update,delete)
+   */
+  @Post('/')
+  async syncData(@Body('index') index: string, @Body('data') data: object, @Body('method') method: Methods) {
+    index = toLower(index);
+    let res;
+    switch (method) {
+      case Methods.CREATE:
+      case Methods.UPDATE:
+        res = await this.syncService.upsert(index, data);
+        break;
+      case Methods.DELETE:
+        res = await this.syncService.delete(index, data);
+        break;
+      default:
+        break;
+    }
+    return res;
+  }
+}

+ 12 - 0
src/error/service.error.ts

@@ -0,0 +1,12 @@
+export enum ErrorCode {
+  ES_ERROR = 'ES_ERROR',
+  ES_INDEX_NOT_FOUND = 'ES_INDEX_NOT_FOUND',
+  ES_DATA_NOT_FOUND = 'ES_DATA_NOT_FOUND',
+  
+}
+export class ServiceError extends Error {
+  constructor(errcode: string) {
+    super(errcode);
+    this.name = 'ServiceError';
+  }
+}

+ 13 - 0
src/filter/default.filter.ts

@@ -0,0 +1,13 @@
+import { Catch } from '@midwayjs/core';
+import { Context } from '@midwayjs/koa';
+
+@Catch()
+export class DefaultErrorFilter {
+  async catch(err: Error, ctx: Context) {
+    // 所有的未分类错误会到这里
+    return {
+      success: false,
+      message: err.message,
+    };
+  }
+}

+ 10 - 0
src/filter/notfound.filter.ts

@@ -0,0 +1,10 @@
+import { Catch, httpError, MidwayHttpError } from '@midwayjs/core';
+import { Context } from '@midwayjs/koa';
+
+@Catch(httpError.NotFoundError)
+export class NotFoundFilter {
+  async catch(err: MidwayHttpError, ctx: Context) {
+    // 404 错误会到这里
+    ctx.redirect('/404.html');
+  }
+}

+ 9 - 0
src/frame/ErrorVO.ts

@@ -0,0 +1,9 @@
+import { get } from 'lodash';
+export class ErrorVO {
+  constructor(response: object) {
+    this.errcode = get(response, 'code');
+    this.errmsg = get(response, 'message');
+  }
+  errcode: string;
+  errmsg: string;
+}

+ 30 - 0
src/frame/VOBase.ts

@@ -0,0 +1,30 @@
+import { get } from 'lodash';
+/**
+ * 输出至视图的格式化基类
+ */
+export class VOBase {
+  constructor(response: object) {
+    this.errcode = get(response, 'errcode', 0);
+    this.errmsg = get(response, 'errmsg', 'ok');
+    this.details = get(response, 'details');
+    this.total = get(response, 'total');
+    // 查询列表和只返回结果的区分,根据total决定
+    if (this.total || this.total === 0) {
+      this.data = get(response, 'data');
+    } else {
+      this.data = response;
+    }
+  }
+  errcode: number;
+  errmsg: string;
+  details?: string;
+  data?: any;
+  total?: number;
+}
+/**处理VO */
+export const dealVO = (cla, data) => {
+  for (const key in cla) {
+    const val = get(data, key);
+    if (val || val === 0) cla[key] = val;
+  }
+};

+ 6 - 0
src/interface.ts

@@ -0,0 +1,6 @@
+/**
+ * @description User-Service parameters
+ */
+export interface IUserOptions {
+  uid: number;
+}

+ 27 - 0
src/middleware/report.middleware.ts

@@ -0,0 +1,27 @@
+import { Middleware, IMiddleware } from '@midwayjs/core';
+import { NextFunction, Context } from '@midwayjs/koa';
+
+@Middleware()
+export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      // 控制器前执行的逻辑
+      const startTime = Date.now();
+      // 执行下一个 Web 中间件,最后执行到控制器
+      // 这里可以拿到下一个中间件或者控制器的返回值
+      const result = await next();
+      // 控制器之后执行的逻辑
+      ctx.logger.info(
+        `Report in "src/middleware/report.middleware.ts", rt = ${
+          Date.now() - startTime
+        }ms`
+      );
+      // 返回给上一个中间件的结果
+      return result;
+    };
+  }
+
+  static getName(): string {
+    return 'report';
+  }
+}

+ 45 - 0
src/middleware/response.middleware.ts

@@ -0,0 +1,45 @@
+import { App, IMiddleware, Middleware } from '@midwayjs/core';
+import { NextFunction, Context, Application } from '@midwayjs/koa';
+import { VOBase } from '../frame/VOBase';
+import { get } from 'lodash';
+import { ErrorVO } from '../frame/ErrorVO';
+import { ServiceError } from '../error/service.error';
+/**
+ * 返回结果处理拦截器
+ * 响应头的content-type含有'/files'的情况为请求文件,重写响应头即可
+ * 正常请求都要通过视图处理基类(VOBase)格式化后返回
+ */
+@Middleware()
+export class ResponseMiddleware implements IMiddleware<Context, NextFunction> {
+  @App()
+  app: Application;
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      await next();
+      // 过滤掉 api文档的请求
+      const swaggerPath = this.app.getConfig()?.swagger?.swaggerPath;
+      const url = get(ctx, 'request.originalUrl');
+      if (!url.includes(swaggerPath)) {
+        //检查返回头,如果返回头中有/files,那就说明是读取文件.不能更改body
+        const response = ctx.response;
+        const resHeaderContentType = response.header['content-type'] as string;
+        // 没有数据的情况,保证也可以返回
+        if (!resHeaderContentType || !resHeaderContentType.includes('/files')) {
+          const body = response.body;
+          if (body instanceof ServiceError) {
+            const r = new ErrorVO(body);
+            return r;
+          }
+          const nb = new VOBase(body as object);
+          return nb;
+        } else {
+          response.set('content-type', resHeaderContentType.replace('/files', ''));
+        }
+      }
+    };
+  }
+
+  static getName(): string {
+    return 'response';
+  }
+}

+ 102 - 0
src/service/elasticsearch/es.service.ts

@@ -0,0 +1,102 @@
+import { Client } from '@elastic/elasticsearch';
+import { Config, Init, Provide } from '@midwayjs/core';
+import { indices, getType } from './indices';
+import { get, toLower } from 'lodash';
+import * as dayjs from 'dayjs';
+
+@Provide()
+export class ESService {
+  @Config('elasticsearch')
+  esConfig: object;
+  /**es连接实例 */
+  esClient: Client;
+
+  @Init()
+  async initClient() {
+    const esClient = new Client(this.esConfig);
+    this.esClient = esClient;
+  }
+  /**
+   * 1.索引(es中的表)建立:根据编写的文件生成
+   * 2.初始化数据,通过接口触发
+   * 3.对指定表进行的数据修改做同步
+   */
+
+  async initIndex() {
+    const tables = [];
+    for (const tableName in indices) {
+      const index = toLower(tableName);
+      const hasIndex = await this.esClient.indices.exists({ index });
+      const mappings = indices[tableName];
+      if (hasIndex) {
+        await this.esClient.indices.delete({ index });
+      }
+      const properties = {};
+      for (const key in mappings) {
+        let pk = key;
+        const val = mappings[key];
+        properties[pk] = getType(val);
+      }
+      await this.esClient.indices.create({ index, mappings: { properties } });
+      tables.push(tableName);
+    }
+    return tables;
+  }
+  /**
+   * 按表,初始化es数据
+   * 1.获取数据总量
+   * 2.${this.limit}步进循环处理
+   * 3.处理特殊字段的数据,然后添加进es中
+   * @param tableName 表名,全小写
+   * @param data 数据
+   * @param mappings 设置的映射
+   */
+  async initData(tableName, data) {
+    const mapping = indices[tableName];
+    const datas = await this.dealData(data, mapping);
+    if (datas.length <= 0) return;
+    await this.addDatas(tableName, datas);
+  }
+
+  /**
+   * 处理数据
+   * @param list model查出来的数据
+   * @param mappings indices设置的映射
+   */
+  async dealData(list, mappings) {
+    const datas = [];
+    for (const i of list) {
+      const obj = {};
+      for (const key in mappings) {
+        const val = get(i, key);
+        if (val) {
+          // 检查是否是date类型,如果是date类型需要检查数据是否正确
+          const dataType = mappings[key];
+          if (dataType === 'date') {
+            const isDate = dayjs(val).isValid();
+            if (!isDate) continue;
+          }
+          let pk = key;
+          obj[pk] = val;
+        }
+      }
+      const keys = Object.keys(obj);
+      if (keys.length > 0) datas.push(obj);
+    }
+    return datas;
+  }
+
+  /**
+   * upsert至es
+   * @param index 全小写表名
+   * @param datas 数据
+   */
+  async addDatas(index, datas) {
+    await this.esClient.helpers.bulk({
+      datasource: datas,
+      onDocument(doc) {
+        return [{ update: { _index: index, _id: get(doc, 'id') } }, { doc_as_upsert: true }];
+      },
+    });
+  }
+}

+ 265 - 0
src/service/elasticsearch/indices.ts

@@ -0,0 +1,265 @@
+import { get } from 'lodash';
+const indices = {
+  achievement: {
+    id: 'number',
+    user: 'number',
+    industry: 'text',
+    tags: 'text',
+    patent: 'text',
+    name: 'text',
+    attribute: 'text',
+    sell: 'text',
+    num: 'number',
+    mature: 'text',
+    field: 'text',
+    technology: 'text',
+    area: 'text',
+    time: 'date',
+    money: 'text',
+    brief: 'text',
+    file: 'nested',
+    source: 'text',
+    person: 'text',
+    tel: 'text',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  demand: {
+    id: 'number',
+    user: 'number',
+    tags: 'text',
+    name: 'text',
+    type: 'text',
+    field: 'text',
+    urgent: 'text',
+    method: 'text',
+    start_time: 'date',
+    end_time: 'date',
+    money: 'text',
+    area: 'text',
+    brief: 'text',
+    demand_status: 'text',
+    industry: 'text',
+    company: 'text',
+    company_brief: 'text',
+    contacts: 'text',
+    tel: 'text',
+    year: 'text',
+    month: 'text',
+    tec_name: 'text',
+    question: 'text',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  footplate: {
+    id: 'number',
+    user: 'number',
+    file: 'nested',
+    industry: 'text',
+    tags: 'text',
+    name: 'text',
+    build: 'text',
+    operate: 'text',
+    field: 'text',
+    area: 'text',
+    address: 'text',
+    contacts: 'text',
+    phone: 'text',
+    brief: 'text',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  match: {
+    id: 'number',
+    user: 'number',
+    tags: 'text',
+    name: 'text',
+    type: 'text',
+    match_type: 'text',
+    scale: 'text',
+    href: 'text',
+    work: 'text',
+    address: 'text',
+    industry: 'text',
+    form: 'text',
+    start_time: 'date',
+    end_time: 'date',
+    money: 'text',
+    rules: 'nested',
+    brief: 'text',
+    file: 'nested',
+    video: 'nested',
+    match_status: 'keyword',
+    order_num: 'number',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  project: {
+    id: 'number',
+    user: 'number',
+    tags: 'text',
+    name: 'text',
+    time: 'date',
+    type: 'text',
+    maturity: 'text',
+    skill: 'text',
+    field: 'text',
+    cooperate: 'text',
+    area: 'text',
+    brief: 'text',
+    main: 'text',
+    progress: 'text',
+    track_unit: 'text',
+    source: 'text',
+    industry: 'text',
+    file: 'nested',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  supply: {
+    id: 'number',
+    user: 'number',
+    name: 'text',
+    tags: 'text',
+    type: 'text',
+    field: 'text',
+    urgent: 'text',
+    method: 'text',
+    start_time: 'date',
+    end_time: 'date',
+    money: 'text',
+    area: 'text',
+    brief: 'text',
+    demand_status: 'text',
+    industry: 'text',
+    source: 'text',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  support: {
+    id: 'number',
+    user: 'number',
+    file: 'nested',
+    industry: 'text',
+    tags: 'text',
+    name: 'text',
+    field: 'text',
+    time: 'date',
+    area: 'text',
+    address: 'text',
+    contacts: 'text',
+    phone: 'text',
+    brief: 'text',
+    is_use: 'keyword',
+    status: 'keyword',
+  },
+  company: {
+    id: 'number',
+    user: 'number',
+    industry: 'text',
+    tags: 'text',
+    name: 'text',
+    logo: 'nested',
+    code: 'text',
+    pattern: 'keyword',
+    scale: 'keyword',
+    phone: 'text',
+    type: 'keyword',
+    area: 'text',
+    representative: 'text',
+    email: 'text',
+    person: 'number',
+    register: 'text',
+    create_time: 'date',
+    address: 'text',
+    brief: 'text',
+    companyStatus: 'text',
+    products: 'text',
+    is_show: 'keyword',
+    status: 'keyword',
+  },
+  expert: {
+    id: 'number',
+    user: 'number',
+    tags: 'text',
+    name: 'text',
+    icon: 'nested',
+    gender: 'keyword',
+    birth: 'date',
+    cardType: 'keyword',
+    card: 'keyword',
+    phone: 'text',
+    field: 'text',
+    direction: 'text',
+    work: 'text',
+    education: 'keyword',
+    title: 'text',
+    brief: 'text',
+    area: 'text',
+    industry_type: 'text',
+    work_type: 'text',
+    is_show: 'keyword',
+    status: 'keyword',
+  },
+  incubator: {
+    id: 'number',
+    user: 'number',
+    industry: 'text',
+    tags: 'text',
+    name: 'text',
+    person: 'keyword',
+    person_phone: 'text',
+    area: 'text',
+    address: 'text',
+    brief: 'text',
+    is_show: 'keyword',
+    cooperate: 'keyword',
+    status: 'keyword',
+  },
+  school: {
+    id: 'number',
+    user: 'number',
+    name: 'text',
+    person: 'keyword',
+    person_phone: 'text',
+    brief: 'text',
+    address: 'text',
+    is_show: 'keyword',
+    status: 'keyword',
+  },
+  unit: {
+    id: 'number',
+    user: 'number',
+    name: 'text',
+    person: 'keyword',
+    person_phone: 'text',
+    brief: 'text',
+    address: 'text',
+    is_show: 'keyword',
+    status: 'keyword',
+  },
+};
+
+const getType = type => {
+  const types = {
+    text: { type: 'text', analyzer: 'ik_max_word', search_analyzer: 'ik_smart' },
+    keyword: { type: 'keyword' },
+    number: { type: 'integer' },
+    long: { type: 'long' },
+    float: { type: 'float' },
+    double: { type: 'double' },
+    date: {
+      type: 'date',
+      format: 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis',
+    },
+    boolean: { type: 'boolean' },
+    nested: { type: 'nested' },
+  };
+  return get(types, type);
+};
+enum Methods {
+  CREATE = 'CREATE',
+  UPDATE = 'UPDATE',
+  DELETE = 'DELETE',
+}
+export { indices, getType, Methods };

+ 122 - 0
src/service/search.service.ts

@@ -0,0 +1,122 @@
+import { Client } from '@elastic/elasticsearch';
+import { Config, Init, Provide } from '@midwayjs/core';
+import { get, isArray } from 'lodash';
+
+@Provide()
+export class SearchService {
+  @Config('elasticsearch')
+  esConfig: object;
+  /**es连接实例 */
+  esClient: Client;
+  @Init()
+  async initClient() {
+    const esClient = new Client(this.esConfig);
+    this.esClient = esClient;
+  }
+  /**信息检索-企业查询 */
+  async company(keyword: string, skip: number = 0, limit: number = 10, others: object) {
+    const index = 'company';
+    const fields = ['industry', 'tags', 'name', 'pattern', 'scale', 'type', 'area', 'email', 'address', 'brief', 'products'];
+    return this.oneIndexSearch(keyword, index, fields, skip, limit, others);
+  }
+
+  /**信息检索-专家查询 */
+  async expert(keyword: string, skip: number = 0, limit: number = 10) {
+    const index = 'expert';
+    const fields = ['tags', 'name', 'area', 'industry', 'field', 'direction', 'work', 'education', 'title', 'brief', 'industry_type', 'work_type'];
+    return this.oneIndexSearch(keyword, index, fields, skip, limit);
+  }
+  /**信息检索-项目查询 */
+  async project(keyword: string, skip: number = 0, limit: number = 10) {
+    const index = 'project';
+    const fields = ['tags', 'name', 'maturity', 'skill', 'type', 'area', 'field', 'cooperate', 'brief', 'main', 'progress', 'track_unit', 'source', 'industry'];
+    return this.oneIndexSearch(keyword, index, fields, skip, limit);
+  }
+  /**信息检索-需求查询 */
+  async demand(keyword: string, skip: number = 0, limit: number = 10) {
+    const index = 'demand';
+    const fields = ['industry', 'tags', 'name', 'field', 'urgent', 'method', 'area', 'brief', 'demand_status', 'company', 'company_brief', 'contacts', 'tec_name', 'question'];
+    return this.oneIndexSearch(keyword, index, fields, skip, limit);
+  }
+  /**信息检索-供给查询 */
+  async supply(keyword: string, skip: number = 0, limit: number = 10) {
+    const index = 'supply';
+    const fields = ['industry', 'tags', 'name', 'field', 'urgent', 'method', 'area', 'brief', 'demand_status'];
+    return this.oneIndexSearch(keyword, index, fields, skip, limit);
+  }
+  /**信息检索-成果查询 */
+  async achievement(keyword: string, skip: number = 0, limit: number = 10) {
+    const index = 'achievement';
+    const fields = ['industry', 'tags', 'name', 'patent', 'attribute', 'sell', 'mature', 'field', 'technology', 'area', 'brief', 'achievement_status', 'source'];
+    return this.oneIndexSearch(keyword, index, fields, skip, limit);
+  }
+
+  /**
+   * 信息检索查询
+   * @param keyword 前端输入的关键词
+   * @param index 针对哪个表查询
+   * @param fields 哪些字段在查询范围内
+   * @param skip 数据起始位置
+   * @param limit 查询数量
+   * @param params 模糊查询外的其他条件约束,都是用精确查询term
+   * @returns
+   */
+  async oneIndexSearch(keyword: string, index: string, fields: Array<string>, skip: number, limit: number, params: object = {}) {
+    const filter = [];
+    for (const key in params) {
+      const val = params[key];
+      const obj = { term: { [key]: val } };
+      filter.push(obj);
+    }
+
+    const result = await this.esClient.search({
+      index,
+      query: {
+        bool: {
+          must: [
+            {
+              multi_match: {
+                query: keyword,
+                fields,
+              },
+            },
+          ],
+          filter,
+        },
+      },
+      size: limit,
+      sort: [{ id: { order: 'asc' } }],
+      track_total_hits: true,
+      search_after: [skip],
+    });
+    const total = this.getSearchTotal(result);
+    const data = this.getSearchResult(result);
+    return { total, data };
+  }
+
+  /**
+   * 获取查询结果
+   * @param result es查询直接返回的结果
+   * @returns
+   */
+  getSearchResult(result) {
+    const res = get(result, 'hits.hits');
+    let rData;
+    if (isArray(res)) {
+      // 整理结果,将_id放到数据中
+      rData = res.map(i => ({ ...get(i, '_source'), es_index: i._index }));
+    } else {
+      rData = get(res, '_source');
+    }
+    return rData;
+  }
+  /**
+   * 获取查询范围的数据总数
+   * @param result es查询直接返回的结果
+   * @returns
+   */
+  getSearchTotal(result) {
+    const res = get(result, 'hits.total.value', 0);
+    return res;
+  }
+}

+ 92 - 0
src/service/sync.service.ts

@@ -0,0 +1,92 @@
+import { Client } from '@elastic/elasticsearch';
+import { Provide, Config, Init } from '@midwayjs/core';
+import * as dayjs from 'dayjs';
+import { get } from 'lodash';
+import { indices } from './elasticsearch/indices';
+@Provide()
+export class SyncService {
+  @Config('elasticsearch')
+  esConfig: object;
+  /**es连接实例 */
+  esClient: Client;
+  @Init()
+  async initClient() {
+    const esClient = new Client(this.esConfig);
+    this.esClient = esClient;
+  }
+  /**
+   * 数据库创建&修改操作的同步
+   * 1.根据mapping映射,处理下数据(e.g.:时间需要查看下是不是正常的时间格式)
+   * @param index es索引(表名全小写)
+   * @param data 数据
+   * @param mapping 数据映射到es的设置
+   */
+  async upsert(index: string, data: object) {
+    const hasIndex = await this.esClient.indices.exists({ index });
+    if (!hasIndex) {
+      // TODO: es没有找到索引,同步失败,需要写到日志库中
+      console.error('upser no index');
+      return;
+    }
+    const mapping = indices[index];
+    if (!mapping) {
+      // TODO: 没有找到mapping,同步失败,需要写到日志库中
+      console.error('upser no mapping');
+      return;
+    }
+    const afterDealData = this.dealData(data, mapping);
+    const datasource = [afterDealData];
+    const result = await this.esClient.helpers.bulk({
+      datasource: datasource,
+      onDocument(doc) {
+        return [{ update: { _index: index, _id: get(doc, 'id') } }, { doc_as_upsert: true }];
+      },
+    });
+    console.log('upsert finished');
+    console.log(result);
+    // TODO: es同步结果,记录到日志库中
+
+    return 'ok';
+  }
+
+  /**
+   * 数据库删除操作同步
+   * @param index es索引(表名全小写)
+   * @param data 数据
+   */
+  async delete(index: string, data: object) {
+    const hasIndex = await this.esClient.indices.exists({ index });
+    if (hasIndex) {
+      // TODO: es没有找到索引,同步失败,需要写到日志库中
+      console.error('delete no index');
+      return;
+    }
+    const result = await this.esClient.helpers.bulk({
+      datasource: [data],
+      onDocument(doc) {
+        return { delete: { _index: index, _id: get(doc, 'id') } };
+      },
+    });
+    console.log('delete finished');
+    console.log(result);
+    // TODO: es同步结果,记录到日志库中
+  }
+
+  dealData(data: object, mapping: object) {
+    const obj = {};
+    for (const key in mapping) {
+      const val = get(data, key);
+      if (val) {
+        // 检查是否是date类型,如果是date类型需要检查数据是否正确
+        const dataType = mapping[key];
+        if (dataType === 'date') {
+          const isDate = dayjs(val).isValid();
+          if (!isDate) continue;
+        }
+        let pk = key;
+        obj[pk] = val;
+      }
+    }
+    return obj;
+  }
+}

+ 14 - 0
src/service/user.service.ts

@@ -0,0 +1,14 @@
+import { Provide } from '@midwayjs/core';
+import { IUserOptions } from '../interface';
+
+@Provide()
+export class UserService {
+  async getUser(options: IUserOptions) {
+    return {
+      uid: options.uid,
+      username: 'mockedName',
+      phone: '12345678901',
+      email: 'xxx.xxx@xxx.com',
+    };
+  }
+}

+ 20 - 0
test/controller/api.test.ts

@@ -0,0 +1,20 @@
+import { createApp, close, createHttpRequest } from '@midwayjs/mock';
+import { Framework } from '@midwayjs/koa';
+
+describe('test/controller/home.test.ts', () => {
+
+  it('should POST /api/get_user', async () => {
+    // create app
+    const app = await createApp<Framework>();
+
+    // make request
+    const result = await createHttpRequest(app).get('/api/get_user').query({ uid: 123 });
+
+    // use expect by jest
+    expect(result.status).toBe(200);
+    expect(result.body.message).toBe('OK');
+
+    // close app
+    await close(app);
+  });
+});

+ 21 - 0
test/controller/home.test.ts

@@ -0,0 +1,21 @@
+import { createApp, close, createHttpRequest } from '@midwayjs/mock';
+import { Framework } from '@midwayjs/koa';
+
+describe('test/controller/home.test.ts', () => {
+
+  it('should GET /', async () => {
+    // create app
+    const app = await createApp<Framework>();
+
+    // make request
+    const result = await createHttpRequest(app).get('/');
+
+    // use expect by jest
+    expect(result.status).toBe(200);
+    expect(result.text).toBe('Hello Midwayjs!');
+
+    // close app
+    await close(app);
+  });
+
+});

+ 28 - 0
tsconfig.json

@@ -0,0 +1,28 @@
+{
+  "compileOnSave": true,
+  "compilerOptions": {
+    "target": "es2018",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "inlineSourceMap":true,
+    "noImplicitThis": true,
+    "noUnusedLocals": true,
+    "stripInternal": true,
+    "skipLibCheck": true,
+    "pretty": true,
+    "declaration": true,
+    "forceConsistentCasingInFileNames": true,
+    "typeRoots": [ "./typings", "./node_modules/@types"],
+    "outDir": "dist",
+    "rootDir": "src"
+  },
+  "exclude": [
+    "*.js",
+    "*.ts",
+    "dist",
+    "node_modules",
+    "test"
+  ]
+}