Browse Source

依赖更新,精准匹配,信息检索,检索行为

lrf 7 tháng trước cách đây
mục cha
commit
ca85cb034b

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


+ 13 - 9
package.json

@@ -4,18 +4,22 @@
   "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",
+    "@elastic/elasticsearch": "8.12.2",
+    "@midwayjs/bootstrap": "^3.16.6",
+    "@midwayjs/core": "^3.16.2",
+    "@midwayjs/info": "^3.16.8",
+    "@midwayjs/jwt": "^3.16.8",
+    "@midwayjs/koa": "^3.16.8",
     "@midwayjs/logger": "^3.1.0",
-    "@midwayjs/validate": "^3.12.0",
+    "@midwayjs/typeorm": "^3.16.5",
+    "@midwayjs/validate": "^3.16.8",
     "dayjs": "^1.11.12",
-    "lodash": "^4.17.21"
+    "lodash": "^4.17.21",
+    "pg": "^8.12.0",
+    "typeorm": "^0.3.20"
   },
   "devDependencies": {
-    "@midwayjs/mock": "^3.12.0",
+    "@midwayjs/mock": "^3.16.5",
     "@types/jest": "^29.2.0",
     "@types/lodash": "^4.17.7",
     "@types/node": "14",
@@ -46,4 +50,4 @@
   },
   "author": "anonymous",
   "license": "MIT"
-}
+}

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

@@ -1,4 +1,14 @@
 import { MidwayConfig } from '@midwayjs/core';
+/**数据库ip */
+const ip = '10.120.114.5';
+/**数据库名 */
+// const dbName = 'cxyy';
+/**日志数据库名 */
+const logsDB = 'cxyy_logs';
+/**数据库用户名 */
+const dbUsername = 'system';
+/**数据库密码 */
+const dbPwd = '1234qwer!@#$';
 /**elasticsearch ip */
 const esIp = '10.120.114.6'
 /**elasticsearch 端口 */
@@ -29,4 +39,19 @@ export default {
       password: esPassword,
     },
   },
+  typeorm: {
+    dataSource: {
+      default: {
+        database: logsDB,
+        username: dbUsername,
+        password: dbPwd,
+        host: ip,
+        port: 54321,
+        entities: ['./entityLogs'],
+        type: 'postgres',
+        synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
+        logging: false,
+      },
+    },
+  },
 } as MidwayConfig;

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

@@ -1,4 +1,14 @@
 import { MidwayConfig } from '@midwayjs/core';
+/**数据库ip */
+const ip = '10.120.114.5';
+/**数据库名 */
+// const dbName = 'cxyy';
+/**日志数据库名 */
+const logsDB = 'cxyy_logs';
+/**数据库用户名 */
+const dbUsername = 'system';
+/**数据库密码 */
+const dbPwd = '1234qwer!@#$';
 /**elasticsearch ip */
 const esIp = 'host.docker.internal';
 /**elasticsearch 端口 */
@@ -29,4 +39,19 @@ export default {
       password: esPassword,
     },
   },
+  typeorm: {
+    dataSource: {
+      default: {
+        database: logsDB,
+        username: dbUsername,
+        password: dbPwd,
+        host: ip,
+        port: 54321,
+        entities: ['./entityLogs'],
+        type: 'postgres',
+        synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
+        logging: false,
+      },
+    },
+  },
 } as MidwayConfig;

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

@@ -1,4 +1,14 @@
 import { MidwayConfig } from '@midwayjs/core';
+/**数据库ip */
+const ip = 'localhost'; //localhost
+/**数据库名 */
+// const dbName = 'cxyy';
+/**日志数据库名 */
+const logsDB = 'cxyy_logs';
+/**数据库用户名 */
+const dbUsername = 'system';
+/**数据库密码 */
+const dbPwd = '1234qwer!@#$';
 /**elasticsearch ip */
 const esIp = '127.0.0.1'
 /**elasticsearch 端口 */
@@ -31,4 +41,19 @@ export default {
       password: esPassword,
     },
   },
+  typeorm: {
+    dataSource: {
+      default: {
+        database: logsDB,
+        username: dbUsername,
+        password: dbPwd,
+        host: ip,
+        port: 54321,
+        entities: ['./entityLogs'],
+        type: 'postgres',
+        synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
+        logging: false,
+      },
+    },
+  },
 } as MidwayConfig;

+ 7 - 3
src/configuration.ts

@@ -7,11 +7,15 @@ import { join } from 'path';
 // import { NotFoundFilter } from './filter/notfound.filter';
 import { ReportMiddleware } from './middleware/report.middleware';
 import { ResponseMiddleware } from './middleware/response.middleware';
-
+import { CheckTokenMiddleware } from './middleware/checkToken.middleware';
+import * as orm from '@midwayjs/typeorm';
+import * as jwt from '@midwayjs/jwt';
 @Configuration({
   imports: [
     koa,
     validate,
+    orm,
+    jwt,
     {
       component: info,
       enabledEnvironment: ['local'],
@@ -25,8 +29,8 @@ export class MainConfiguration {
 
   async onReady() {
     // add middleware
-    this.app.useMiddleware([ReportMiddleware, ResponseMiddleware]);
+    this.app.useMiddleware([ReportMiddleware, CheckTokenMiddleware, ResponseMiddleware]);
     // add filter
-    // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
+    // this.app.useFilter([DefaultErrorFilter]);
   }
 }

+ 10 - 1
src/controller/accurateMatching.controller.ts

@@ -18,5 +18,14 @@ export class AccurateMatchingController {
   }
 
   @Get('/supply')
-  async supply(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {}
+  async supply(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const result = await this.amService.supply(keyword, skip, limit);
+    return result;
+  }
+
+  @Get('/achievement')
+  async achievement(@Query('keyword') keyword: string, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const result = await this.amService.achievement(keyword, skip, limit);
+    return result;
+  }
 }

+ 50 - 9
src/controller/search.controller.ts

@@ -2,6 +2,7 @@ import { Controller, Get, Inject, Query } from '@midwayjs/core';
 import { SearchService } from '../service/search.service';
 import { Context } from '@midwayjs/koa';
 import { omit } from 'lodash';
+import { SearchBehaviorService } from '../service/searchBehavior.service';
 
 @Controller('/search')
 export class SearchController {
@@ -9,40 +10,80 @@ export class SearchController {
   searchService: SearchService;
   @Inject()
   ctx: Context;
+  @Inject()
+  sbService: SearchBehaviorService;
 
   @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);
+    // 转至日志库,记录 谁,什么时间.通过什么 内容 进行检索. 形成词云
+    try {
+      if (keyword && keyword !== '') await this.sbService.makeLogs(keyword);
+    } catch (error) {
+      console.error(error);
+    }
     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('/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);
+    const query = this.ctx.query;
+    const others = omit(query, ['keyword', 'skip', 'limit']);
+    const res = await this.searchService.project(keyword, skip, limit, others);
+    // 转至日志库,记录 谁,什么时间.通过什么 内容 进行检索. 形成词云
+    try {
+      if (keyword && keyword !== '') await this.sbService.makeLogs(keyword);
+    } catch (error) {
+      console.error(error);
+    }
     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);
+    const query = this.ctx.query;
+    const others = omit(query, ['keyword', 'skip', 'limit']);
+    const res = await this.searchService.demand(keyword, skip, limit, others);
+    // 转至日志库,记录 谁,什么时间.通过什么 内容 进行检索. 形成词云
+    try {
+      if (keyword && keyword !== '') await this.sbService.makeLogs(keyword);
+    } catch (error) {
+      console.error(error);
+    }
     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);
+    const query = this.ctx.query;
+    const others = omit(query, ['keyword', 'skip', 'limit']);
+    const res = await this.searchService.supply(keyword, skip, limit, others);
+    // 转至日志库,记录 谁,什么时间.通过什么 内容 进行检索. 形成词云
+    try {
+      if (keyword && keyword !== '') await this.sbService.makeLogs(keyword);
+    } catch (error) {
+      console.error(error);
+    }
     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);
+    const query = this.ctx.query;
+    const others = omit(query, ['keyword', 'skip', 'limit']);
+    const res = await this.searchService.achievement(keyword, skip, limit, others);
+    // 转至日志库,记录 谁,什么时间.通过什么 内容 进行检索. 形成词云
+    try {
+      if (keyword && keyword !== '') await this.sbService.makeLogs(keyword);
+    } catch (error) {
+      console.error(error);
+    }
     return res;
   }
 }

+ 18 - 0
src/entityLogs/searchBehavior.entity.ts

@@ -0,0 +1,18 @@
+import { Column, Entity } from 'typeorm';
+import * as dayjs from 'dayjs';
+import { BaseModel } from '../frame/BaseModel';
+@Entity('searchBehavior', { comment: '检索行为日志' })
+export class SearchBehavior extends BaseModel {
+  @Column({ type: 'integer', nullable: true, comment: '操作人id' })
+  operator_id: number;
+  @Column({ type: 'character varying', nullable: true, comment: '操作人名称' })
+  operator_name: string;
+  @Column({ type: 'character varying', nullable: true, comment: 'ip' })
+  ip: string;
+  @Column({ type: 'character varying', nullable: true, comment: '设备' })
+  device: string;
+  @Column({ type: 'timestamp without time zone', nullable: true, comment: '时间', transformer: { from: value => dayjs(value).format('YYYY-MM-DD HH:mm:ss'), to: value => value } })
+  time: Date;
+  @Column({ type: 'character varying', comment: '检索内容' })
+  keyword: string;
+}

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

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

+ 21 - 0
src/frame/BaseModel.ts

@@ -0,0 +1,21 @@
+import { CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn } from 'typeorm';
+import * as dayjs from 'dayjs';
+// transformer.from: 从数据库出来对数据的处理函数
+// transformer.to:进入数据库前对数据的处理
+@Entity()
+export class BaseModel {
+  /**数据id */
+  @PrimaryGeneratedColumn({ type: 'integer' })
+  id?: number;
+
+  /**数据创建时间 */
+  @CreateDateColumn({ type: 'timestamp without time zone', transformer: { from: value => dayjs(value).format('YYYY-MM-DD HH:mm:ss'), to: value => value } })
+  created_time?: Date;
+  /**数据最后更新时间 */
+  @UpdateDateColumn({ type: 'timestamp without time zone', transformer: { from: value => dayjs(value).format('YYYY-MM-DD HH:mm:ss'), to: value => value } })
+  update_time?: Date;
+
+  /**数据版本 */
+  @VersionColumn({ type: 'integer', default: 1 })
+  __v?: number;
+}

+ 330 - 0
src/frame/BaseServiceV2.ts

@@ -0,0 +1,330 @@
+import { App, Inject } from '@midwayjs/core';
+import { Application, Context } from '@midwayjs/koa';
+import { get, head, isArray, isNull, isFinite, isString, isUndefined, last } from 'lodash';
+import { Opera } from './dbOpera';
+/**
+ * query默认的查询方式(哪个字段 是 = 还是 IN 还是 LIKE)的设置函数为getQueryColumnsOpera,如果有需要重写即可,返回object
+ * {
+ *    column: Opera.xxx
+ * }
+ */
+export abstract class BaseServiceV2 {
+  @App()
+  app: Application;
+
+  @Inject()
+  ctx: Context;
+
+  /**service的model,数据库操作 */
+  abstract model: any;
+  /**返回{column:查询方式}.特殊的查询需要写入,不写入默认采用 =  */
+  getQueryColumnsOpera() {
+    return {};
+  }
+  /** */
+  Opera = Opera;
+  /**
+   * 默认查询列表
+   * @param query @Query直接获取的参数
+   * @param param @Query和调用前可组织的参数
+   * @param {object} operas 指定查询方式
+   * @property {number} param.skip 查询数据起始位置
+   * @property {number} param.limit 查询数据的数量
+   * @property {object} param.order 使用的排序 {${column}:'DESC'/'ASC'}
+   * @property {Array<string>} selects 指定显示的字段
+   */
+  async query(query: object = {}, meta: any = {}, operas?) {
+    let skip = get(meta, 'skip', 0);
+    let limit = get(meta, 'limit', 0);
+    const order = get(meta, 'order', {});
+    const selects = get(meta, 'selects', []);
+    const builder = await this.model.createQueryBuilder();
+    if (selects.length > 0) {
+      // 字段是直接传来的,正常限制,需要加上model的name.否则会导致什么字段都没有
+      const modelName = this.model.metadata.name;
+      builder.select(selects.map(i => `${modelName}.${i}`));
+    }
+    // 组织查询顺序
+    let orderObject: any = {};
+    // 如果有自定义顺序,则按照自定义顺序来, 没有自定义顺序,默认按创建时间的desc查询
+    if (Object.keys(order).length > 0) {
+      for (const column in order) orderObject[column] = order[column];
+    } else orderObject = { id: 'DESC' };
+    // 没有传如何查询,就获取query查询设置的默认查询方式
+    if (!operas) operas = this.getQueryColumnsOpera();
+    this.completeBuilderCondition(builder, query, operas);
+    // 分页
+    if (isString(skip)) {
+      skip = parseInt(skip);
+      if (isFinite(skip)) builder.skip(skip);
+    } else if (isFinite(skip)) builder.skip(skip);
+    if (isString(limit)) {
+      limit = parseInt(limit);
+      if (isFinite(limit)) builder.take(limit);
+    } else if (isFinite(limit)) builder.take(limit);
+    // 排序
+    builder.orderBy(orderObject);
+    // 执行
+    if (this.app.getEnv() !== 'prod' && this.app.getEnv() !== 'production') {
+      console.log(builder.getSql());
+    }
+    const data = await builder.getMany();
+    const total = await builder.getCount();
+    return { data, total };
+  }
+
+  /**
+   *
+   * @param builder model的createQueryBuilder,只有到最后要查数据的时候才是异步的
+   * @param {object} query 查询条件
+   * @param {object} operas 指定查询方式
+   */
+  completeBuilderCondition(builder, query = {}, operas = {}) {
+    // 组织查询条件
+    if (!query) return;
+    const searchColumns = Object.keys(query);
+    if (searchColumns.length <= 0) return;
+    for (let i = 0; i < searchColumns.length; i++) {
+      const key = searchColumns[i];
+      const value = query[key];
+      if (!value) continue;
+      /**该字段的查询方式 */
+      const opera = get(operas, key);
+      /**builder的使用函数名 */
+      let method = 'where';
+      if (i === 0) method = 'where';
+      else method = 'andWhere';
+      let str;
+      let params;
+      // 需要给变量位置重命名,否则多个条件叠加后,都会使用第一个参数
+      const valueStr = `value${i}`;
+      let valueArr = [];
+      const strArr = [];
+      switch (opera) {
+        case this.Opera.Between:
+          str = `"${key}" Between :${valueStr}_1 AND :${valueStr}_2`;
+          params = { [`${valueStr}_1`]: head(value), [`${valueStr}_2`]: last(value) };
+          break;
+        case this.Opera.Not:
+          str = `"${key}" != :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.Like:
+          str = `"${key}" Like :${valueStr}`;
+          params = { [`${valueStr}`]: `%${value}%` };
+          break;
+        case this.Opera.LikeLeft:
+          str = `"${key}" Like :${valueStr}`;
+          params = { [`${valueStr}`]: `%${value}` };
+          break;
+        case this.Opera.LikeRight:
+          str = `"${key}" Like :${valueStr}`;
+          params = { [`${valueStr}`]: `${value}%` };
+          break;
+        case this.Opera.ILike:
+          str = `"${key}" Not Like :${valueStr}`;
+          params = { [`${valueStr}`]: `%${value}%` };
+          break;
+        case this.Opera.ILikeLeft:
+          str = `"${key}" Not Like :${valueStr}`;
+          params = { [`${valueStr}`]: `%${value}` };
+          break;
+        case this.Opera.ILikeRight:
+          str = `"${key}" Not Like :${valueStr}`;
+          params = { [`${valueStr}`]: `${value}%` };
+          break;
+        case this.Opera.LessThan:
+          str = `"${key}" < :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.LessThanOrEqual:
+          str = `"${key}" <= :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.MoreThan:
+          str = `"${key}" > :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.MoreThanOrEqual:
+          str = `"${key}" >= :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.In:
+          if (!isArray(value)) str = `"${key}" IN (:${valueStr})`;
+          else str = `"${key}" IN (:...${valueStr})`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.IsNull:
+          str = `"${key}" IS NULL`;
+          break;
+        case this.Opera.IsNotNull:
+          str = `"${key}" IS NOT NULL`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.Json:
+          params = {};
+          if (isArray(value)) valueArr = value;
+          else valueArr = [value];
+          for (let vi = 0; vi < valueArr.length; vi++) {
+            const v = valueArr[vi];
+            const mvalKey = `${valueStr}${vi}`;
+            const mstr = `JSONB_EXISTS("${key}", :${mvalKey})`;
+            strArr.push(mstr);
+            params[mvalKey] = v;
+          }
+          str = `(${strArr.join(' OR ')})`;
+          break;
+        case this.Opera.JsonObject:
+          const jokeys = key.split('.');
+          let jorootCol = head(jokeys);
+          let jolastKey = last(jokeys);
+          let jopath = jokeys.filter(f => f !== jorootCol && f !== jolastKey);
+          str = `"${jorootCol}" `;
+          for (const jok of jopath) {
+            str = `${str} -> ${jok}`;
+          }
+          str = `${str} ->> '${jolastKey}' = :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+          break;
+        case this.Opera.JsonArrayObject:
+          /**
+           * 1.分割key,过来的属性默认以 x.y.z... 形式
+           * x:根子段;后面,数组中依次往下的属性名
+           */
+          const keys = key.split('.');
+          let numberValue;
+          if (isFinite(parseInt(value))) numberValue = parseInt(value);
+          let rootCol = head(keys);
+          let lastKey = last(keys);
+          let path = keys.filter(f => f !== rootCol && f !== lastKey);
+          const getObject = (path, lastKey, value) => {
+            let obj = {};
+            let mid = obj;
+            for (const k of path) {
+              mid[k] = {};
+              mid = mid[k];
+            }
+            mid[lastKey] = value;
+            return obj;
+          };
+          const obj = getObject(path, lastKey, value);
+          let newVal = JSON.stringify([obj]);
+          str = `"${rootCol}" @> :${valueStr}`;
+          params = { [`${valueStr}`]: newVal };
+          if (numberValue) {
+            const numObj = getObject(path, lastKey, numberValue);
+            let numVal = JSON.stringify([numObj]);
+            const valueStrNum = `${valueStr}Num`;
+            str = `${str} OR "${rootCol}" @> :${valueStrNum}`;
+            params[valueStrNum] = numVal;
+          }
+          break;
+        case this.Opera.Equal:
+        default:
+          const isString = this.columnIsString(key);
+          if (isString) {
+            // 字符串默认使用模糊查询
+            str = `"${key}" Like :${valueStr}`;
+            params = { [`${valueStr}`]: `%${value}%` };
+          } else {
+            str = `"${key}" = :${valueStr}`;
+            params = { [`${valueStr}`]: value };
+          }
+          break;
+      }
+      if (!str) continue;
+      builder[method](str, params);
+    }
+  }
+
+  /**
+   * 单查询,不止用id还可以根据别的条件,默认全等,可以在调用时,进行查询方式设置
+   * @param {object} query 查询条件
+   * @param {object} operas 指定查询方式
+   */
+  async fetch(query: object, operas = {}) {
+    const builder = this.model.createQueryBuilder();
+    this.completeBuilderCondition(builder, query, operas);
+    const result = await builder.getOne();
+    return result;
+  }
+
+  /**创建 */
+  async create(data: object) {
+    // 设置 创建数据的人
+    const user = get(this.ctx, 'user');
+    if (user) {
+      // 查询本表是否有data_onwer字段,有再添加.
+      const hasColumn = this.checkModelHaveColumn('data_onwer');
+      if (hasColumn) Object.assign(data, { data_owner: get(user, 'id') });
+    }
+    const result = await this.model.insert(data);
+    const id = get(result, 'identifiers.0.id');
+    // 没有id估计是出错了
+    if (!id) return;
+    const createData = await this.fetch({ id });
+    // 没有查出数据,也是有问题
+    if (!createData) return;
+    return createData;
+  }
+
+  /**修改,单修改/多修改是统一修改为 */
+  async update(query: object = {}, data: object) {
+    // 没有范围的修改不允许执行
+    if (Object.keys(query).length <= 0) return;
+    // 处理数据, 只将是本表的字段拿出来保存
+    const columns = this.model.metadata.columns;
+    /**将array的列设置 转换为object,以便query使用*/
+    const columnsObject = {};
+    // 整理成object
+    for (const c of columns) columnsObject[c.propertyName] = c.type.toString();
+    const updateData = {};
+    const notDealColumn = ['created_time', 'update_time', 'data_owner', '__v'];
+    for (const column in columnsObject) {
+      if (notDealColumn.includes(column)) continue;
+      const val = data[column];
+      if (isNull(val) || isUndefined(val)) continue;
+      updateData[column] = val;
+    }
+    // 找到原数据
+    const originDataBuilder = this.model.createQueryBuilder();
+    this.completeBuilderCondition(originDataBuilder, query);
+    const origin_data = await originDataBuilder.getMany(query);
+    if (origin_data.length <= 0) return;
+    await this.model.update(query, updateData);
+    const new_data = await originDataBuilder.getMany(query);
+    if (new_data.length <= 0) return;
+  }
+
+  /**删除,单删多删都行 */
+  async delete(query: object) {
+    // 没有范围不能删除,清空表需要特殊处理
+    if (query && Object.keys(query).length <= 0) return;
+    // 删除前,先查出来数据, 找到原数据
+    const originDataBuilder = this.model.createQueryBuilder();
+    this.completeBuilderCondition(originDataBuilder, query);
+    const origin_data = await originDataBuilder.getMany(query);
+    if (origin_data.length <= 0) return;
+    const result = await this.model.delete(query);
+    return result;
+  }
+
+  /**检查有没有指定字段,是从model的映射关系中查询下 */
+  private checkModelHaveColumn(columnName: string) {
+    const columns = this.model.metadata.columns;
+    const has = columns.find(f => get(f, 'propertyName') === columnName);
+    return !isUndefined(has);
+  }
+  /**
+   * 检查字段是否是字符串字段
+   * @param {string} columnName 字段名
+   */
+  private columnIsString(columnName: string) {
+    const columns = this.model.metadata.columns;
+    const colSetting = columns.find(f => get(f, 'propertyName') === columnName);
+    if (!colSetting) return false;
+    const type = get(colSetting, 'type');
+    if (!type) return false;
+    return type === 'character varying' || type === 'text';
+  }
+}

+ 36 - 0
src/frame/dbOpera.ts

@@ -0,0 +1,36 @@
+export enum Opera {
+  /**完全等于 Equal('a') */
+  Equal = 'Equal',
+  /**不等于 Not('a')*/
+  Not = 'Not',
+  /**模糊查询,值需要用%包裹起来:Like(%val%)*/
+  Like = 'Like',
+  /**模糊查询,值需要用%包裹起来 Like(%val)*/
+  LikeLeft = 'LikeLeft',
+  /**模糊查询,值需要用%包裹起来 Like(val%)*/
+  LikeRight = 'LikeRight',
+  /**模糊查询取反 */
+  ILike = 'ILike',
+  ILikeLeft = 'ILikeLeft',
+  ILikeRight = 'ILikeRight',
+  /**两者之间(闭区间), Between(1,10) */
+  Between = 'Between',
+  /**小于 LessThan(10)*/
+  LessThan = 'LessThan',
+  /**小于等于 LessThanOrEqual(10)*/
+  LessThanOrEqual = 'LessThanOrEqual',
+  /**大于 MoreThan(10)*/
+  MoreThan = 'MoreThan',
+  /**大于等于 MoreThanOrEqual(10)*/
+  MoreThanOrEqual = 'MoreThanOrEqual',
+  /**值在数组中 In(['a', 'b']) */
+  In = 'In',
+  /**为空 IsNull */
+  IsNull = 'IsNull',
+  IsNotNull = 'IsNotNull',
+  /**JSON */
+  Json = 'Json',
+  /**Json, Object类型需要自定义写法 */
+  JsonArrayObject = 'JsonArrayObject',
+  JsonObject = 'JsonObject',
+}

+ 24 - 0
src/middleware/checkToken.middleware.ts

@@ -0,0 +1,24 @@
+import { IMiddleware, Inject, Middleware } from '@midwayjs/core';
+import { JwtService } from '@midwayjs/jwt';
+import { NextFunction, Context } from '@midwayjs/koa';
+import { get } from 'lodash';
+
+@Middleware()
+export class CheckTokenMiddleware implements IMiddleware<Context, NextFunction> {
+  @Inject()
+  jwtService: JwtService;
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      const token: any = get(ctx.request, 'header.token');
+      if (token) {
+        const data = this.jwtService.decodeSync(token);
+        if (data) ctx.user = data;
+      }
+      await next();
+    };
+  }
+
+  static getName(): string {
+    return 'checkToken';
+  }
+}

+ 1 - 5
src/middleware/report.middleware.ts

@@ -11,11 +11,7 @@ export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
       // 这里可以拿到下一个中间件或者控制器的返回值
       const result = await next();
       // 控制器之后执行的逻辑
-      ctx.logger.info(
-        `Report in "src/middleware/report.middleware.ts", rt = ${
-          Date.now() - startTime
-        }ms`
-      );
+      ctx.logger.info(`Report in "src/middleware/report.middleware.ts", rt = ${Date.now() - startTime}ms`);
       // 返回给上一个中间件的结果
       return result;
     };

+ 42 - 49
src/service/accurateMatching.service.ts

@@ -1,6 +1,6 @@
 import { Client } from '@elastic/elasticsearch';
 import { Config, Init, Provide } from '@midwayjs/core';
-import { floor, get, head } from 'lodash';
+import { floor, get } from 'lodash';
 
 @Provide()
 export class AccurateMatchingService {
@@ -15,65 +15,58 @@ export class AccurateMatchingService {
   }
 
   async demand(keyword: string, skip: number = 0, limit: number = 10) {
-    const configs = [
-      { index: 'supply', fields: ['name', 'tags', 'area', 'brief'] },
-      { index: 'achievement', fields: ['name', 'tags', 'area', 'brief'] },
-    ];
-    return this.search(keyword, configs, skip, limit);
+    const config = { index: 'demand', fields: ['name', 'tags', 'area', 'brief'] };
+    return this.search(keyword, config, skip, limit);
   }
 
   async supply(keyword: string, skip: number = 0, limit: number = 10) {
-    const configs = [
-      { index: 'demand', fields: ['name', 'tags', 'area', 'brief'] },
-      { index: 'achievement', fields: ['name', 'tags', 'area', 'brief'] },
-    ];
-    return this.search(keyword, configs, skip, limit);
+    const config = { index: 'supply', fields: ['name', 'tags', 'area', 'brief'] };
+    return this.search(keyword, config, skip, limit);
+  }
+  async achievement(keyword: string, skip: number = 0, limit: number = 10) {
+    const config = { index: 'achievement', fields: ['name', 'tags', 'area', 'brief'] };
+    return this.search(keyword, config, skip, limit);
   }
 
   /**精准匹配统一查询,使用简单的form+size分页模式.如果数据量大于1w,此处的分页模式应该修改,不过就会影响资源的占用及数据的实时性 */
-  async search(keyword: string, configs: Array<object>, skip: number, limit: number) {
-    const searches = [];
-    for (const c of configs) {
-      searches.push({ index: get(c, 'index') });
-      const obj = {
-        query: { bool: { must: [{ multi_match: { query: keyword, fields: get(c, 'fields', []) } }] } },
-        sort: [{ _score: { order: 'desc' } }],
-        size: limit,
-        from: skip,
-      };
-      searches.push(obj);
-    }
-    const result = await this.esClient.msearch({
-      searches,
+  async search(keyword: string, config: object, skip: number, limit: number) {
+    const index = get(config, 'index');
+    const fields = get(config, 'fields', []);
+    const result = await this.esClient.search({
+      index,
+      query: {
+        bool: {
+          must: [
+            {
+              multi_match: {
+                query: keyword,
+                fields,
+              },
+            },
+          ],
+        },
+      },
+      size: limit,
+      from: skip,
     });
-    const returnData = this.dealResponses(result);
+    const returnData = this.dealResponses(result)
     return returnData;
   }
 
   dealResponses(result) {
-    const returnData = [];
-    const responses = get(result, 'responses');
-    for (const res of responses) {
-      const status = get(res, 'status');
-      if (status !== 200) continue;
-      const hits = get(res, 'hits', []);
-      if (!hits) continue;
-      const total = get(hits, 'total.value', 0);
-      const originList = get(hits, 'hits', []);
-      const list = [];
-      for (const ol of originList) {
-        const _score = get(ol, '_score', 0);
-        const data = get(ol, '_source', {});
-        let recommend = 0;
-        /**推荐星数计算:超过5,就都是5颗星.未超过5的都向下取整 */
-        if (_score >= 5) recommend = 5;
-        else recommend = floor(_score);
-        data._recommend = recommend;
-        list.push(data);
-      }
-      const table = get(head(originList), '_index');
-      returnData.push({ data: list, total, table });
+    const total = get(result, 'hits.total.value', 0);
+    const hits = get(result, 'hits.hits', []);
+    const list = [];
+    for (const ol of hits) {
+      const _score = get(ol, '_score', 0);
+      const data = get(ol, '_source', {});
+      let recommend = 0;
+      /**推荐星数计算:超过5,就都是5颗星.未超过5的都向下取整 */
+      if (_score >= 5) recommend = 5;
+      else recommend = floor(_score);
+      data._recommend = recommend;
+      list.push(data);
     }
-    return returnData;
+    return { total, data: list };
   }
 }

+ 54 - 57
src/service/search.service.ts

@@ -1,6 +1,6 @@
 import { Client } from '@elastic/elasticsearch';
 import { Config, Init, Provide } from '@midwayjs/core';
-import { get, isArray } from 'lodash';
+import { floor, get } from 'lodash';
 
 @Provide()
 export class SearchService {
@@ -20,35 +20,35 @@ export class SearchService {
     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 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) {
+  async project(keyword: string, skip: number = 0, limit: number = 10, others: object) {
     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);
+    return this.oneIndexSearch(keyword, index, fields, skip, limit, others);
   }
   /**信息检索-需求查询 */
-  async demand(keyword: string, skip: number = 0, limit: number = 10) {
+  async demand(keyword: string, skip: number = 0, limit: number = 10, others: object) {
     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);
+    return this.oneIndexSearch(keyword, index, fields, skip, limit, others);
   }
   /**信息检索-供给查询 */
-  async supply(keyword: string, skip: number = 0, limit: number = 10) {
+  async supply(keyword: string, skip: number = 0, limit: number = 10, others: object) {
     const index = 'supply';
     const fields = ['industry', 'tags', 'name', 'field', 'urgent', 'method', 'area', 'brief', 'demand_status'];
-    return this.oneIndexSearch(keyword, index, fields, skip, limit);
+    return this.oneIndexSearch(keyword, index, fields, skip, limit, others);
   }
   /**信息检索-成果查询 */
-  async achievement(keyword: string, skip: number = 0, limit: number = 10) {
+  async achievement(keyword: string, skip: number = 0, limit: number = 10, others: object) {
     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);
+    return this.oneIndexSearch(keyword, index, fields, skip, limit, others);
   }
 
   /**
@@ -62,61 +62,58 @@ export class SearchService {
    * @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 filter = [{ term: { is_use: '0' } }];
+    const must: any = [];
+    if (keyword) {
+      const obj = {
+        multi_match: {
+          query: keyword,
+          fields,
+        },
+      };
+      must.push(obj)
+    }
+
+    if (Object.keys(params).length > 0) {
+      for (const key in params) {
+        const val = params[key];
+        const obj = { match_phrase: { [key]: val } };
+        must.push(obj);
+      }
     }
 
     const result = await this.esClient.search({
       index,
       query: {
         bool: {
-          must: [
-            {
-              multi_match: {
-                query: keyword,
-                fields,
-              },
-            },
-          ],
+          must,
           filter,
         },
       },
       size: limit,
-      sort: [{ id: { order: 'asc' } }],
-      track_total_hits: true,
-      search_after: [skip],
+      from: skip,
+      // sort: [{ _score: { order: 'desc' } }],
+      // track_total_hits: true,
+      // search_after: [skip],
     });
-    const total = this.getSearchTotal(result);
-    const data = this.getSearchResult(result);
-    return { total, data };
+    const retrunData = this.dealResponses(result);
+    return retrunData;
   }
 
-  /**
-   * 获取查询结果
-   * @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');
+  dealResponses(result) {
+    const total = get(result, 'hits.total.value', 0);
+    const hits = get(result, 'hits.hits', []);
+    const list = [];
+    for (const ol of hits) {
+      const _score = get(ol, '_score', 0);
+      const data = get(ol, '_source', {});
+      let recommend = 0;
+      /**推荐星数计算:超过5,就都是5颗星.未超过5的都向下取整 */
+      if (_score >= 5) recommend = 5;
+      else recommend = floor(_score);
+      data._recommend = recommend;
+      list.push(data);
     }
-    return rData;
-  }
-  /**
-   * 获取查询范围的数据总数
-   * @param result es查询直接返回的结果
-   * @returns
-   */
-  getSearchTotal(result) {
-    const res = get(result, 'hits.total.value', 0);
-    return res;
+    return { total, data: list };
   }
 }

+ 35 - 0
src/service/searchBehavior.service.ts

@@ -0,0 +1,35 @@
+import { Provide } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BaseServiceV2 } from '../frame/BaseServiceV2';
+import { SearchBehavior } from '../entityLogs/searchBehavior.entity';
+import { get } from 'lodash';
+import dayjs = require('dayjs');
+@Provide()
+export class SearchBehaviorService extends BaseServiceV2 {
+  @InjectEntityModel(SearchBehavior)
+  model: Repository<SearchBehavior>;
+
+  async makeLogs(keyword: string) {
+    const user = this.ctx.user;
+    if (!user) return;
+    if (!user.id) return;
+    const req = this.ctx.request;
+    // 排除管理员查询
+    const role = get(user, 'role', []);
+    const has_admin = role.find(f => f === 'Admin');
+    if (has_admin) return;
+    const logInfo: any = {
+      operator_id: user.id,
+      operator_name: get(user, 'nick_name', get(user, 'name')),
+      // ip
+      ip: get(req, 'header.x-forwarded-for', req.ip),
+      // 设备
+      device: get(req, 'header.user-agent'),
+      // 操作时间
+      time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      keyword,
+    };
+    await this.model.insert(logInfo);
+  }
+}