lrf il y a 1 an
Parent
commit
0e299fa6c6

+ 2 - 1
package.json

@@ -6,6 +6,7 @@
   "dependencies": {
     "@elastic/elasticsearch": "^8.12.2",
     "@midwayjs/bootstrap": "^3.12.0",
+    "@midwayjs/bull": "3",
     "@midwayjs/core": "^3.12.0",
     "@midwayjs/decorator": "^3.12.0",
     "@midwayjs/i18n": "3",
@@ -20,7 +21,7 @@
     "@typegoose/typegoose": "^9.0.0",
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.10",
-    "free-midway-component": "^1.0.53",
+    "free-midway-component": "^1.0.54",
     "lodash": "^4.17.21",
     "mongoose": "^6.0.7"
   },

+ 8 - 4
pnpm-lock.yaml

@@ -11,6 +11,9 @@ dependencies:
   '@midwayjs/bootstrap':
     specifier: ^3.12.0
     version: 3.15.6
+  '@midwayjs/bull':
+    specifier: '3'
+    version: 3.15.6
   '@midwayjs/core':
     specifier: ^3.12.0
     version: 3.15.6
@@ -54,8 +57,8 @@ dependencies:
     specifier: ^1.11.10
     version: 1.11.10
   free-midway-component:
-    specifier: ^1.0.53
-    version: 1.0.53
+    specifier: ^1.0.54
+    version: 1.0.54
   lodash:
     specifier: ^4.17.21
     version: 4.17.21
@@ -4003,8 +4006,8 @@ packages:
       qs: 6.12.0
     dev: true
 
-  /free-midway-component@1.0.53:
-    resolution: {integrity: sha512-iASKBq9P+UmgU9fpFrNer2bhXRdWWeCrh8Pg/ZInqzxs2Hy7kyI0FudEiidgbbWq4zCrRD6NJhORNx3kas7ALQ==}
+  /free-midway-component@1.0.54:
+    resolution: {integrity: sha512-9gp685U78VMuWuaO1IKAmiL3AOscsa2XYqOS/+a7CG2QvIu9XmXx4DUe+iwQU8GETyD/zxG7owet57Y9bqu7bA==}
     dependencies:
       '@midwayjs/axios': 3.15.6
       '@midwayjs/bull': 3.15.6
@@ -6830,6 +6833,7 @@ packages:
   /uuid@8.3.2:
     resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
     hasBin: true
+    requiresBuild: true
     dev: false
 
   /v8-compile-cache-lib@3.0.1:

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

@@ -6,7 +6,14 @@ export default {
   koa: {
     port: 7001,
   },
+  // 请求记录在redis留存时间,超过时间.数据变化将不会记录.以秒为单位--5分钟
+  requestTimeLimit: 300,
   i18n: {
+    defaultLocale: 'zh-cn',
+    fallbacks: {
+      'en*': 'en_us',
+      'zh*': 'zh-cn',
+    },
     localeTable: {
       en_us: {
         default: require('../locales/en_us/defaults'),

+ 23 - 4
src/config/config.local.ts

@@ -1,10 +1,10 @@
 import { MidwayConfig } from '@midwayjs/core';
 const ip = '127.0.0.1'; //120.48.146.1
-const redisHost = '127.0.0.1';
+const redisHost = '120.48.146.1';
 const redisPwd = '123456';
 const redisDB = 6;
 const projectDB = 'vue3js-template-test';
-const recordDB = 'vue3js-template-test-record'
+const recordDB = 'vue3js-template-test-record';
 const loginSign = 'tsFrameDev';
 export default {
   // use for cookie sign key, should change to your own and keep security
@@ -21,6 +21,14 @@ export default {
     secret: 'Ziyouyanfa!@#',
     expiresIn: 3600, // 3600
   },
+  elasticsearch: {
+    node: 'http://localhost:9200',
+    auth: {
+      username: 'elastic',
+      password: 'NAjqFz_7tS2DkdpU7p*x',
+    },
+  },
+  dbName: projectDB,
   mongoose: {
     dataSource: {
       default: {
@@ -33,7 +41,7 @@ export default {
         },
         entities: ['./entity'],
       },
-      record:{
+      record: {
         uri: `mongodb://${ip}:27017/${recordDB}`,
         options: {
           user: 'admin',
@@ -42,7 +50,7 @@ export default {
           useNewUrlParser: true,
         },
         entities: ['./entityRecord'],
-      }
+      },
     },
   },
   redis: {
@@ -53,6 +61,17 @@ export default {
       db: redisDB,
     },
   },
+  bull: {
+    // 默认的队列配置
+    defaultQueueOptions: {
+      redis: {
+        port: 6379,
+        host: redisHost,
+        password: redisPwd,
+        db: redisDB,
+      },
+    },
+  },
   upload: {
     whitelist: null,
   },

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

@@ -20,6 +20,13 @@ export default {
     secret: 'Ziyouyanfa!@#',
     expiresIn: 3600, // 3600
   },
+  elasticsearch: {
+    node: 'http://localhost:9200',
+    auth: {
+      username: 'elastic',
+      password: 'NAjqFz_7tS2DkdpU7p*x',
+    },
+  },
   mongoose: {
     dataSource: {
       default: {
@@ -42,6 +49,17 @@ export default {
       db: redisDB,
     },
   },
+  bull: {
+    // 默认的队列配置
+    defaultQueueOptions: {
+      redis: {
+        port: 6379,
+        host: redisHost,
+        password: redisPwd,
+        db: redisDB,
+      },
+    },
+  },
   upload: {
     whitelist: null,
   },

+ 19 - 4
src/configuration.ts

@@ -1,4 +1,4 @@
-import { Configuration, App, Inject, MidwayDecoratorService } from '@midwayjs/core';
+import { Configuration, App, Inject, MidwayDecoratorService, IMidwayContainer, IMidwayApplication, MidwayApplicationManager } from '@midwayjs/core';
 import * as koa from '@midwayjs/koa';
 import * as validate from '@midwayjs/validate';
 import * as info from '@midwayjs/info';
@@ -13,7 +13,10 @@ import * as redis from '@midwayjs/redis';
 import { CheckOnePointLoginMiddleware } from './middleware/checkOnePointLogin.middleware';
 import * as i18n from '@midwayjs/i18n';
 import { SetLocaleToCtxMiddleware } from './middleware/setLocaleToCtx.middleware';
-import { DataRecordInit } from './decorator/dataRecord';
+import { ElasticsearchService } from './service/elasticsearch';
+import * as bull from '@midwayjs/bull';
+import { DataRecordMiddleware } from './middleware/dataRecord.middleware';
+import { DBService } from './service/db.service';
 @Configuration({
   imports: [
     koa,
@@ -22,6 +25,7 @@ import { DataRecordInit } from './decorator/dataRecord';
     jwt,
     redis,
     i18n,
+    bull,
     {
       component: info,
       enabledEnvironment: ['local'],
@@ -38,13 +42,24 @@ export class MainConfiguration {
   app: koa.Application;
   @Inject()
   decoratorService: MidwayDecoratorService;
+  @Inject()
+  applicationManager: MidwayApplicationManager;
 
+  // 依赖注入容器完成时执行
   async onReady() {
-    this.app.getMiddleware().insertFirst(SetLocaleToCtxMiddleware)
+    this.app.getMiddleware().insertFirst(SetLocaleToCtxMiddleware);
     this.app.getMiddleware().insertAfter(CheckOnePointLoginMiddleware, 'checkToken');
+    this.app.getMiddleware().insertAfter(DataRecordMiddleware, 'checkOnePonitLogin');
     // 注解
     VerifyTokenInit(this.decoratorService);
     CheckPermissionCodeInit(this.decoratorService);
-    DataRecordInit(this.decoratorService)
+  }
+
+  // 应用服务已启动后执行
+  async onServerReady?(container: IMidwayContainer, app: IMidwayApplication) {
+    // 初始化es
+    const esService = await container.getAsync(ElasticsearchService);
+    await esService.preparMapping();
+    await container.getAsync(DBService);
   }
 }

+ 11 - 18
src/controller/es.controller.ts

@@ -1,4 +1,4 @@
-import { Body, Controller, Del, Get, Inject, Param, Post,  Query } from '@midwayjs/core';
+import { Body, Controller, Del, Get, Inject, Param, Post, Query } from '@midwayjs/core';
 import { ElasticsearchService } from '../service/elasticsearch';
 
 @Controller('/es')
@@ -6,28 +6,16 @@ export class ESController {
   @Inject()
   esService: ElasticsearchService;
 
-  @Post('/async')
-  async asyncData() {
-    await this.esService.asyncData();
-    return 'ok';
-  }
-
-  @Get('/')
+  @Get('/', { description: 'ignore' })
   async search(@Query() query: any) {
-    query.build_time = ['2023-01-01', '2024-01-01']
+    // query.build_time = ['2023-01-01', '2024-01-01']
     const result = await this.esService.search(query);
     return result;
   }
 
-  @Post('/')
-  async create(@Body() data: any) {
-    const result = await this.esService.create(data);
-    return 'created';
-  }
-
-  @Post('/:id')
-  async update(@Param('id') id: string, @Body() data: any) {
-    const result = await this.esService.update(id, data);
+  @Post('/:index/:id')
+  async update(@Param('index') index: string, @Param('id') id: string, @Body() data: any) {
+    const result = await this.esService.update(index, id, data);
     console.log(id);
     console.log(data);
     return 'update';
@@ -39,4 +27,9 @@ export class ESController {
     console.log(id);
     return 'delete';
   }
+  @Post('/createIndex', { description: 'ignore' })
+  async createIndex(@Body('index') index: string) {
+    await this.esService.createIndex(index);
+    return 'ok';
+  }
 }

+ 68 - 0
src/controller/record/dataLogs.controller.ts

@@ -0,0 +1,68 @@
+import { Body, Controller, Del, Get, Inject, Param, Post, Query } from '@midwayjs/decorator';
+import { BaseController } from 'free-midway-component';
+import { DataLogsService } from '../../service/record/dataLogs.service';
+import { CDTO_dataLogs, CVO_dataLogs, FVO_dataLogs, QDTO_dataLogs, QVO_dataLogs, UDTO_dataLogs, UVAO_dataLogs } from '../../interface/record/dataLogs.interface';
+import { ApiResponse, ApiTags, ApiQuery } from '@midwayjs/swagger';
+import { Validate } from '@midwayjs/validate';
+@ApiTags(['mongodb oplog'])
+@Controller('/dataLogs')
+export class DataLogsController extends BaseController {
+  @Inject()
+  service: DataLogsService;
+
+  @Post('/')
+  @Validate()
+  @ApiResponse({ type: CVO_dataLogs })
+  async create(@Body() data: CDTO_dataLogs) {
+    const dbData = await this.service.create(data);
+    const result = new CVO_dataLogs(dbData);
+    return result;
+  }
+  @Get('/')
+  @ApiQuery({ name: 'query' })
+  @ApiResponse({ type: QVO_dataLogs })
+  async query(@Query() filter: QDTO_dataLogs, @Query('skip') skip: number, @Query('limit') limit: number) {
+    const list = await this.service.query(filter, { skip, limit });
+    const data = [];
+    for (const i of list) {
+      const newData = new QVO_dataLogs(i);
+      data.push(newData);
+    }
+    const total = await this.service.count(filter);
+    return { data, total };
+  }
+
+  @Get('/:id')
+  @ApiResponse({ type: FVO_dataLogs })
+  async fetch(@Param('id') id: string) {
+    const data = await this.service.fetch(id);
+    const result = new FVO_dataLogs(data);
+    return result;
+  }
+
+  @Post('/:id')
+  @Validate()
+  @ApiResponse({ type: UVAO_dataLogs })
+  async update(@Param('id') id: string, @Body() body: UDTO_dataLogs) {
+    const result = await this.service.updateOne(id, body);
+    return result;
+  }
+
+  @Del('/:id')
+  @Validate()
+  async delete(@Param('id') id: string) {
+    await this.service.delete(id);
+    return 'ok';
+  }
+  async createMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async updateMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+
+  async deleteMany(...args: any[]) {
+    throw new Error('Method not implemented.');
+  }
+}

+ 2 - 5
src/controller/system/role.controller.ts

@@ -1,4 +1,4 @@
-import { Body, Controller, Del, Get, Inject, Param, Post, Query } from '@midwayjs/decorator';
+import { Body, Controller, Del, Get, Inject, Param, Post, Query, sleep } from '@midwayjs/decorator';
 import { BaseController } from 'free-midway-component';
 import { RoleService } from '../../service/system/role.service';
 import { CDTO_role, CVO_role, FVO_role, QDTO_role, QVO_role, UDTO_role, UVAO_role } from '../../interface/system/role.interface';
@@ -7,7 +7,6 @@ import { Validate } from '@midwayjs/validate';
 import { MenusService } from '../../service/system/menus.service';
 import { verifyToken } from '../../decorator/verifyToken.decorator';
 import { checkPermissionCode } from '../../decorator/checkPermissionCode';
-import { dataRecord } from '../../decorator/dataRecord';
 @ApiTags(['角色表'])
 @Controller('/role')
 export class RoleController extends BaseController {
@@ -20,8 +19,8 @@ export class RoleController extends BaseController {
   @verifyToken()
   @Validate()
   @ApiResponse({ type: CVO_role })
-  @dataRecord()
   async create(@Body() data: CDTO_role) {
+    await sleep(5000)
     const dbData = await this.service.create(data);
     const result = new CVO_role(dbData);
     return result;
@@ -56,7 +55,6 @@ export class RoleController extends BaseController {
   @checkPermissionCode({ roleCode: 'system_role.update' })
   @Validate()
   @ApiResponse({ type: UVAO_role })
-  @dataRecord()
   async update(@Param('id') id: string, @Body() body: UDTO_role) {
     const result = await this.service.updateOne(id, body);
     return result;
@@ -65,7 +63,6 @@ export class RoleController extends BaseController {
   @Del('/:id')
   @verifyToken()
   @Validate()
-  @dataRecord()
   async delete(@Param('id') id: string) {
     await this.service.delete(id);
     return 'ok';

+ 10 - 1
src/decorator/dataRecord.ts

@@ -2,6 +2,8 @@ import { JoinPoint, MidwayDecoratorService, REQUEST_OBJ_CTX_KEY, createCustomMet
 import { MidwayI18nService } from '@midwayjs/i18n';
 import { get } from 'lodash';
 import { DataRecordService } from '../service/record/dataRecord.service';
+import { UtilService } from '../service/util.service';
+import { DBService } from '../service/db.service';
 export const DATARECORD_KEY = 'decorator:data_record';
 export const dataRecord = (options?: any) => {
   return createCustomMethodDecorator(DATARECORD_KEY, options);
@@ -17,7 +19,7 @@ const getMethodName = (mapping: Map<string, any>, controller: string, path: stri
 // const i18nName = getMethodName(mapping, controllerName, methodName);
 // console.log(i18nName);
 
-const defaultMethods = ['create', 'update', 'delete'];
+const defaultMethods = ['create', 'update', 'delete', 'status']; // 创建,修改,删除,审核(审核和修改走一个)
 export const DataRecordInit = (decoratorService: MidwayDecoratorService) => {
   decoratorService.registerMethodHandler(DATARECORD_KEY, options => {
     return {
@@ -30,6 +32,13 @@ export const DataRecordInit = (decoratorService: MidwayDecoratorService) => {
           const methodName = options.propertyName;
           const metaData = options.metadata;
           const ctx = instance[REQUEST_OBJ_CTX_KEY];
+          const us = await ctx.requestContext.getAsync(UtilService);
+          const ranstr = us.randomStr();
+          ctx.InsCode = ranstr;
+          console.log(ctx.InsCode);
+          const dbs = await ctx.requestContext.getAsync(DBService);
+          await dbs.init();
+
           recordService = await ctx.requestContext.getAsync(DataRecordService);
           // 组织记录数据
           recordPreData = await recordService.makeLogs(controllerName, methodName);

+ 38 - 0
src/decorator/syncElasticSearch.ts

@@ -0,0 +1,38 @@
+import { JoinPoint, MidwayDecoratorService, REQUEST_OBJ_CTX_KEY, createCustomMethodDecorator } from '@midwayjs/core';
+import { get } from 'lodash';
+import { ElasticsearchService } from '../service/elasticsearch';
+export const SYNC_ELASTICSEARCH_KEY = 'decorator:sync_elasticsearch';
+export const syncElasticSearch = (options?: any) => {
+  return createCustomMethodDecorator(SYNC_ELASTICSEARCH_KEY, options);
+};
+
+export const SyncElasticSearchInit = (decoratorService: MidwayDecoratorService) => {
+  decoratorService.registerMethodHandler(SYNC_ELASTICSEARCH_KEY, options => {
+    return {
+      around: async (joinPoint: JoinPoint) => {
+        const instance = joinPoint.target;
+        const ctx = instance[REQUEST_OBJ_CTX_KEY];
+        const methodName = options.propertyName;
+        const result = await joinPoint.proceed(...joinPoint.args);
+        const controller = await ctx.requestContext.getAsync(options.target);
+        const modelName = get(controller, 'service.model.modelName');
+        try {
+          // 不影响后续进行
+          let res;
+          if (methodName === 'create' || methodName === 'update') {
+            const es = await ctx.requestContext.getAsync(ElasticsearchService);
+            res = await es.asyncData(modelName, result);
+          } else if (methodName === 'delete') {
+            const id = get(ctx.request, 'params.id');
+            const es = await ctx.requestContext.getAsync(ElasticsearchService);
+            res = await es.delete(modelName, id);
+          }
+          console.log(res);
+        } catch (error) {
+          console.log(error);
+        }
+        return result;
+      },
+    };
+  });
+};

+ 0 - 65
src/entity/basic.entity.ts

@@ -1,65 +0,0 @@
-import { modelOptions, prop } from '@typegoose/typegoose';
-import { BaseModel } from 'free-midway-component';
-@modelOptions({
-  schemaOptions: { collection: 'basic' },
-})
-export class Basic extends BaseModel {
-  @prop({ required: false, index: true, zh: '实验室类别', remark: '字典:laboratory_type' })
-  type: string;
-  @prop({ required: false, index: true, zh: '实验室名称' })
-  name: string;
-  @prop({ required: false, index: false, zh: '英文名称' })
-  english_name: string;
-  @prop({ required: false, index: false, zh: '所属学科' })
-  subject: string;
-  @prop({ required: false, index: false, zh: '所属专业' })
-  major: string;
-  @prop({ required: false, index: false, zh: '实验室面积' })
-  lab_acreage: string;
-  @prop({ required: false, index: false, zh: '批建时间' })
-  build_time: string;
-  @prop({ required: false, index: false, zh: '实验室地址' })
-  lab_address: string;
-  @prop({ required: false, index: false, zh: '依托单位id', remark: 'unit_name不需要了,现在所有的实验室都有依托单位' })
-  unit: string;
-  @prop({ required: false, index: false, zh: '实验室定位' })
-  position: string;
-  @prop({ required: false, index: false, zh: '实验室建设方案' })
-  plan: Array<any>;
-  @prop({ required: false, index: false, zh: '实验室主任' })
-  chief_name: string;
-  @prop({ required: false, index: false, zh: '实验室联系人' })
-  lab_person: string;
-  @prop({ required: false, index: false, zh: '实验室联系人电话' })
-  lab_phone: string;
-  @prop({ required: false, index: false, zh: '电子邮箱' })
-  lab_email: string;
-  @prop({ required: false, index: false, zh: '申报单位' })
-  declare_unit: object;
-  @prop({ required: false, index: false, zh: '平台负责人' })
-  leading_cadre: object;
-  @prop({ required: false, index: false, zh: '科研助理' })
-  assistant: object;
-  @prop({ required: false, index: false, zh: '财务助理是否与科研助理同一人' })
-  is_alike: boolean;
-  @prop({ required: false, index: false, zh: '财务助理' })
-  finance: object;
-  @prop({ required: false, index: false, zh: '所属吉林省重点实验室' })
-  ss_laboratory: string;
-  @prop({ required: false, index: false, zh: '整合基础' })
-  zh_basis: string;
-  @prop({ required: false, index: false, zh: '是否公开', remark: '字典:info_show' })
-  is_show: string;
-  @prop({ required: false, index: false, zh: '地区' })
-  region: string;
-  @prop({ required: false, index: true, zh: '状态', default: '2' })
-  status: string;
-  @prop({ required: false, index: false, zh: '主任入驻记录' })
-  chief_logs: Array<any>;
-  @prop({ required: false, index: false, zh: '发展前景' })
-  develop: string;
-  @prop({ required: false, index: false, zh: '运行机制' })
-  mechanism: string;
-  @prop({ required: false, index: false, zh: '运行机制文件' })
-  mechanism_file: Array<any>;
-}

+ 7 - 6
src/entity/system/admin.entity.ts

@@ -5,15 +5,16 @@ import { isString } from 'lodash';
   schemaOptions: { collection: 'admin' },
 })
 export class Admin extends BaseModel {
-  @prop({ required: true, index: true, zh: '账号' })
+  @prop({ required: false, index: false, zh: '账号', esType: 'keyword' })
   account: string;
-  @prop({ required: false, index: false, zh: '名称' })
+  @prop({ required: false, index: false, zh: '昵称', esType: 'text' })
   nick_name: string;
   // 手动删除set前的大括号,处理太麻烦了.就手动删除吧
   @prop({
     required: false,
     index: false,
     zh: '密码',
+    esType: null,
     select: false,
     set: val => {
       if (isString(val)) {
@@ -23,12 +24,12 @@ export class Admin extends BaseModel {
     },
   })
   password: object;
-  @prop({ required: false, index: false, zh: '是否是超级管理员', remark: '0:超级管理员;1普通用户', default: '1' })
+  @prop({ required: false, index: false, zh: '是否是超级管理员', remark: '0:超级管理员;1普通用户', esType: 'keyword', default: '1' })
   is_super: string;
-  @prop({ required: false, index: false, zh: '角色' })
+  @prop({ required: false, index: false, zh: '角色', esType: 'keyword' })
   role: string;
-  @prop({ required: false, index: false, zh: 'openid' })
+  @prop({ required: false, index: false, zh: '微信openid', esType: 'keyword' })
   openid: string;
-  @prop({ required: false, index: false, zh: '是否启用', default: '0' })
+  @prop({ required: false, index: false, zh: '是否启用', remark: '0:启用;1:禁用', esType: 'keyword', default: '0' })
   is_use: string;
 }

+ 5 - 5
src/entity/system/dictData.entity.ts

@@ -4,14 +4,14 @@ import { BaseModel } from 'free-midway-component';
   schemaOptions: { collection: 'dictData' },
 })
 export class DictData extends BaseModel {
-  @prop({ required: false, index: true, zh: '字典类型编码' })
+  @prop({ required: false, index: false, zh: '字典类型编码', esType: 'keyword' })
   code: string;
-  @prop({ required: false, index: false, zh: '数据显示值' })
+  @prop({ required: false, index: false, zh: '数据显示值', esType: 'text' })
   label: string;
-  @prop({ required: false, index: false, zh: '数据选择值' })
+  @prop({ required: false, index: false, zh: '数据选择值', esType: 'keyword' })
   value: string;
-  @prop({ required: false, index: false, zh: '排序' })
+  @prop({ required: false, index: false, zh: '排序', esType: 'number' })
   sort: number;
-  @prop({ required: false, index: true, zh: '是否使用', default: '0' })
+  @prop({ required: false, index: false, zh: '是否使用', remark: '0:使用;1禁用', esType: 'keyword', default: '0' })
   is_use: string;
 }

+ 4 - 4
src/entity/system/dictType.entity.ts

@@ -4,12 +4,12 @@ import { BaseModel } from 'free-midway-component';
   schemaOptions: { collection: 'dictType' },
 })
 export class DictType extends BaseModel {
-  @prop({ required: false, index: false, zh: '字典类型名称' })
+  @prop({ required: false, index: false, zh: '字典类型名称', esType: 'text' })
   title: string;
-  @prop({ required: false, index: true, zh: '编码' })
+  @prop({ required: false, index: false, zh: '编码', esType: 'keyword' })
   code: string;
-  @prop({ required: false, index: false, zh: '备注' })
+  @prop({ required: false, index: false, zh: '备注', esType: null })
   remark: string;
-  @prop({ required: false, index: true, zh: '是否使用' })
+  @prop({ required: false, index: false, zh: '是否使用', remark: '0:使用;1:禁用', esType: 'keyword', default: '0' })
   is_use: string;
 }

+ 13 - 13
src/entity/system/menus.entity.ts

@@ -4,30 +4,30 @@ import { BaseModel } from 'free-midway-component';
   schemaOptions: { collection: 'menus' },
 })
 export class Menus extends BaseModel {
-  @prop({ required: false, index: false, zh: '菜单名称' })
+  @prop({ required: false, index: false, zh: '菜单名称', esType: 'text' })
   name: string;
-  @prop({ required: false, index: false, zh: '路由名称', remark: '英文' })
+  @prop({ required: false, index: false, zh: '路由名称', remark: '英文', esType: 'keyword' })
   route_name: string;
-  @prop({ required: false, index: false, zh: '国际化编码' })
+  @prop({ required: false, index: false, zh: '国际化编码', esType: 'keyword' })
   i18n_code: string;
-  @prop({ required: false, index: true, zh: '父级菜单' })
+  @prop({ required: false, index: true, zh: '父级菜单', esType: 'keyword' })
   parent_id: string;
-  @prop({ required: false, index: false, zh: '显示顺序' })
+  @prop({ required: false, index: false, zh: '显示顺序', esType: 'number' })
   order_num: number;
-  @prop({ required: false, index: false, zh: '路由地址' })
+  @prop({ required: false, index: false, zh: '路由地址', esType: 'text' })
   path: string;
-  @prop({ required: false, index: false, zh: '组件地址' })
+  @prop({ required: false, index: false, zh: '组件地址', esType: 'text' })
   component: string;
-  @prop({ required: false, index: true, zh: '菜单类型', remark: '0:目录;1:菜单;2:子页面' })
+  @prop({ required: false, index: true, zh: '菜单类型', remark: '0:目录;1:菜单;2:子页面', esType: 'keyword' })
   type: string;
-  @prop({ required: false, index: false, zh: '图标' })
+  @prop({ required: false, index: false, zh: '图标', esType: null })
   icon: string;
-  @prop({ required: false, index: false, zh: '功能列表', remark: '不在功能列表中的功能不能使用' })
+  @prop({ required: false, index: false, zh: '功能列表', remark: '不在功能列表中的功能不能使用', esType: 'nested' })
   config: Array<any>;
-  @prop({ required: false, index: true, zh: '是否为默认菜单', default: '1', remark: '默认:0,非默认:1; 默认不能删除' })
+  @prop({ required: false, index: false, zh: '是否默认', remark: '0:默认;1:非默认;默认不能删除', esType: 'keyword', default: '1' })
   is_default: string;
-  @prop({ required: false, index: false, zh: '备注' })
+  @prop({ required: false, index: false, zh: '备注', esType: null })
   remark: string;
-  @prop({ required: false, index: true, zh: '是否启用', default: '0' })
+  @prop({ required: false, index: true, zh: '是否启用', remark: '0:启用;1', esType: 'keyword', default: '0' })
   is_use: string;
 }

+ 5 - 5
src/entity/system/role.entity.ts

@@ -4,14 +4,14 @@ import { BaseModel } from 'free-midway-component';
   schemaOptions: { collection: 'role' },
 })
 export class Role extends BaseModel {
-  @prop({ required: false, index: false, zh: '角色名称' })
+  @prop({ required: false, index: false, zh: '角色名称', esType: 'text' })
   name: string;
-  @prop({ required: false, index: true, zh: '角色编码' })
+  @prop({ required: false, index: false, zh: '角色编码', esType: 'keyword' })
   code: string;
-  @prop({ required: false, index: false, zh: '菜单' })
+  @prop({ required: false, index: false, zh: '权限', remark: '菜单', esType: 'keyword' })
   menu: Array<any>;
-  @prop({ required: false, index: false, zh: '简介' })
+  @prop({ required: false, index: false, zh: '简介', esType: null })
   brief: string;
-  @prop({ required: false, index: true, zh: '是否使用', default: '0' })
+  @prop({ required: false, index: false, zh: '是否使用', remark: '0:使用;1禁用', esType: 'keyword', default: '0' })
   is_use: string;
 }

+ 17 - 0
src/entityRecord/dataLogs.entity.ts

@@ -0,0 +1,17 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from 'free-midway-component';
+@modelOptions({
+  schemaOptions: { collection: 'dataLogs' },
+})
+export class DataLogs extends BaseModel {
+  @prop({ required: false, index: true, zh: '表' }) //, esType: 'keyword'
+  coll: string;
+  @prop({ required: false, index: false, zh: '数据id' })
+  data_id: string;
+  @prop({ required: false, index: false, zh: '时间' })
+  time: string;
+  @prop({ required: false, index: false, zh: '数据库操作方法' })
+  method: string;
+  @prop({ required: false, index: false, zh: '数据' })
+  data: object;
+}

+ 2 - 0
src/entityRecord/dataRecord.entity.ts

@@ -24,4 +24,6 @@ export class DataRecord extends BaseModel {
   device: string;
   @prop({ required: false, index: false, zh: '状态', remark: '0:成功;-1失败' })
   status: string;
+  @prop({ required: false, index: false, zh: 'es是否同步', default: false })
+  es_sync: boolean;
 }

+ 14 - 0
src/error/elasticsearch.error.ts

@@ -0,0 +1,14 @@
+import { MidwayError, registerErrorCode } from '@midwayjs/core';
+
+export enum ErrorCode {
+  ES_ERROR = '-3000',
+  ES_INDEX_NOT_FOUND = '-3001',
+  ES_DATA_NOT_FOUND = '-3404'
+}
+export const EsErrorEnum = registerErrorCode('EsError', ErrorCode);
+
+export class ElasticSearchError extends MidwayError {
+  constructor(str: string, errcode: string = ErrorCode.ES_ERROR) {
+    super(str, errcode);
+  }
+}

+ 83 - 0
src/interface/record/dataLogs.interface.ts

@@ -0,0 +1,83 @@
+import { Rule, RuleType } from '@midwayjs/validate';
+import { ApiProperty } from '@midwayjs/swagger';
+import { SearchBase } from 'free-midway-component';
+import get = require('lodash/get');
+const dealVO = (cla, data) => {
+  for (const key in cla) {
+    const val = get(data, key);
+    if (val || val === 0) cla[key] = val;
+  }
+};
+export class FVO_dataLogs {
+  constructor(data: object) {
+    dealVO(this, data);
+  }
+  @ApiProperty({ description: '数据id' })
+  _id: string = undefined;
+  @ApiProperty({ description: '表' })
+  'collection': string = undefined;
+  @ApiProperty({ description: '数据id' })
+  'id': string = undefined;
+  @ApiProperty({ description: '时间' })
+  'time': string = undefined;
+  @ApiProperty({ description: '数据库操作方法' })
+  'method': string = undefined;
+  @ApiProperty({ description: '数据' })
+  'data': object = undefined;
+}
+
+export class QDTO_dataLogs extends SearchBase {
+  constructor() {
+    const like_prop = [];
+    const props = ['collection'];
+    const mapping = [];
+    super({ like_prop, props, mapping });
+  }
+  @ApiProperty({ description: '表' })
+  'collection': string = undefined;
+}
+
+export class QVO_dataLogs extends FVO_dataLogs {
+  constructor(data: object) {
+    super(data);
+    dealVO(this, data);
+  }
+}
+
+export class CDTO_dataLogs {
+  @ApiProperty({ description: '表' })
+  @Rule(RuleType['string']().empty(''))
+  'collection': string = undefined;
+  @ApiProperty({ description: '数据id' })
+  @Rule(RuleType['string']().empty(''))
+  'id': string = undefined;
+  @ApiProperty({ description: '时间' })
+  @Rule(RuleType['string']().empty(''))
+  'time': string = undefined;
+  @ApiProperty({ description: '数据库操作方法' })
+  @Rule(RuleType['string']().empty(''))
+  'method': string = undefined;
+  @ApiProperty({ description: '数据' })
+  @Rule(RuleType['object']().empty(''))
+  'data': object = undefined;
+}
+
+export class CVO_dataLogs extends FVO_dataLogs {
+  constructor(data: object) {
+    super(data);
+    dealVO(this, data);
+  }
+}
+
+export class UDTO_dataLogs extends CDTO_dataLogs {
+  @ApiProperty({ description: '数据id' })
+  @Rule(RuleType['string']().empty(''))
+  _id: string = undefined;
+}
+
+export class UVAO_dataLogs extends FVO_dataLogs {
+  constructor(data: object) {
+    super(data);
+    dealVO(this, data);
+  }
+}

+ 15 - 3
src/locales/en_us/errors.ts

@@ -1,5 +1,4 @@
-export default {
-  locale: 'error language is: {locale}',
+const codes ={
   '-1': 'unknown error',
   '-2': 'need params',
   '-20': 'params error',
@@ -11,7 +10,20 @@ export default {
   '-50': 'bad args',
   '-10': 'data not found',
   '-100': 'service fault',
-  '-101': 'not login',
+  '-101': 'No login information detected, please log in again',
+  '-101-1': 'Account login has expired, please log in again!',
+  '-101-2': 'This account has been logged in elsewhere, please log in again!',
   '-102': 'bad password',
   '-500': 'request falut',
+  '-3000': 'ElasticSearch error',
+  '-3001': 'ElasticSearch error, index not found',
+}
+const errCodes = {}
+const prefix = 'SERVICEERROR_'
+for (const key in codes) {
+  errCodes[`${prefix}${key}`] = codes[key]
+}
+export default {
+  test: 'error language is: {locale}',
+  ...errCodes,
 };

+ 17 - 3
src/locales/zh_cn/errors.ts

@@ -1,5 +1,4 @@
-export default {
-  locale: '错误测试: {locale}',
+const codes ={
   '-1': '未知错误',
   '-2': '缺少params',
   '-20': 'params错误',
@@ -11,7 +10,22 @@ export default {
   '-50': '参数错误',
   '-10': '未找到数据',
   '-100': '服务发生错误',
-  '-101': '未登录',
+  '-101': '未检测到登录信息,请重新登录!',
+  '-101-1': '账号登录已失效,请重新登录!',
+  '-101-2': '本账号已在其他地方登录,请重新登录!',
   '-102': '密码错误',
   '-500': '请求错误',
+  '-3000': 'ElasticSearch发生错误',
+  '-3001': 'ElasticSearch发生错误,未找到索引',
+}
+const errCodes = {}
+const prefix = 'SERVICEERROR_'
+for (const key in codes) {
+  errCodes[`${prefix}${key}`] = codes[key]
+}
+
+
+export default {
+  locale: '错误测试: {locale}',
+  ...errCodes,
 };

+ 49 - 0
src/middleware/dataRecord.middleware.ts

@@ -0,0 +1,49 @@
+import { App, Config, IMiddleware, Inject, MidwayWebRouterService } from '@midwayjs/core';
+import { Middleware } from '@midwayjs/decorator';
+import { NextFunction, Context, Application } from '@midwayjs/koa';
+import { RedisService } from '@midwayjs/redis';
+import { get } from 'lodash';
+import { DataRecordService } from '../service/record/dataRecord.service';
+import { Types } from 'mongoose';
+const ObjectId = Types.ObjectId;
+@Middleware()
+export class DataRecordMiddleware implements IMiddleware<Context, NextFunction> {
+  @App()
+  app: Application;
+  @Inject()
+  webRouterService: MidwayWebRouterService;
+  @Inject()
+  redisService: RedisService;
+  @Config('requestTimeLimit')
+  requestTimeLimit: number;
+  recordRequestList = ['post', 'delete'];
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      const routeInfo = await this.webRouterService.getMatchedRouterInfo(ctx.path, ctx.method);
+      if (!routeInfo) {
+        await next();
+        return;
+      }
+      if (!this.recordRequestList.includes(routeInfo.requestMethod)) await next();
+      else {
+        // 只记录post和delete请求的数据变化
+        const controller = get(routeInfo, 'controllerClz.name');
+        const method = get(routeInfo, 'method') as string;
+        // 实例操作记录服务
+        const dataRecordService = await ctx.requestContext.getAsync(DataRecordService);
+        // 组织操作记录数据
+        const dataRecord = await dataRecordService.makeLogs(controller, method);
+        // 生成操作记录数据
+        const result = await dataRecordService.create(dataRecord);
+        if (result) {
+          const id = get(result, '_id');
+          if (id) {
+            // 将id放到app中.因为单例模式只能通过app获取;而不用单例模式,会引发多个监听.会重复记录
+            this.app.setAttr('record_id', new ObjectId(id).toString());
+          }
+        }
+        await next();
+      }
+    };
+  }
+}

+ 2 - 2
src/middleware/setLocaleToCtx.middleware.ts

@@ -1,12 +1,12 @@
 import { IMiddleware } from '@midwayjs/core';
 import { Middleware } from '@midwayjs/decorator';
 import { NextFunction, Context } from '@midwayjs/koa';
-import { head, last } from 'lodash';
+import { get, head, last } from 'lodash';
 @Middleware()
 export class SetLocaleToCtxMiddleware implements IMiddleware<Context, NextFunction> {
   resolve() {
     return async (ctx: Context, next: NextFunction) => {
-      const cookies = ctx.request.headers.cookie;
+      const cookies = get(ctx, 'request.headers.cookie', 'zh-cn') as string;
       let arr = cookies.split(';');
       arr = arr.filter(f => f.includes('locale='));
       // 没找到locale就默认使用中文

+ 110 - 0
src/queue/esSync.queue.ts

@@ -0,0 +1,110 @@
+import { Processor, IProcessor } from '@midwayjs/bull';
+import { DataRecordService } from '../service/record/dataRecord.service';
+import { Inject } from '@midwayjs/decorator';
+import { difference, differenceBy, get, intersection, isObject } from 'lodash';
+import { ElasticsearchService } from '../service/elasticsearch';
+import { FORMAT } from '@midwayjs/core';
+@Processor('esSync', {
+  repeat: {
+    cron: FORMAT.CRONTAB.EVERY_MINUTE,
+  },
+})
+export class EsSyncProcessor implements IProcessor {
+  @Inject()
+  dataRecord: DataRecordService;
+  @Inject()
+  es: ElasticsearchService;
+  async execute() {
+    // ...
+    console.log('in es sync processor');
+    const result = await this.dataRecord.noDealQuery({ es_sync: false });
+    // console.log(result.length);
+    for (const i of result) {
+      const origin_data = this.hasContext(get(i, 'origin_data'));
+      const new_data = this.hasContext(get(i, 'new_data'));
+      // 什么也没有直接下一个
+      if (!origin_data && !new_data) continue;
+      if (origin_data && !new_data) {
+        // 有旧数据,但是没有新数据: 所有的旧数据都是要删除的数据
+        const keys = Object.keys(origin_data);
+        await this.allDeleteData(i, keys);
+      } else if (!origin_data && new_data) {
+        // 有新数据但是没有旧数据: 所有的新数据都是添加的
+        const keys = Object.keys(new_data);
+        await this.allCreateData(i, keys);
+      } else {
+        // 新旧数据都存在,那就需要拿出来一个一个对比:
+        // 新旧数据都有的:修改
+        // 有新数据,没有旧数据: 创建
+        // 有旧数据,但是没有新数据: 删除
+        const odkeys = Object.keys(origin_data);
+        const ndkeys = Object.keys(new_data);
+        // 比较下表有没有区别
+        // d1: 以原数据为基础,新数据缺少的表.全都删除
+        const d1 = difference(odkeys, ndkeys);
+        // d2: 以新数据为基础,原数据缺少的表.全都创建
+        const d2 = difference(ndkeys, odkeys);
+        // d3: 两组数据共有的表,都是修改
+        const d3 = intersection(odkeys, ndkeys);
+        // console.log(d1, d2, d3);
+        if (d1.length > 0) {
+          // 全部删除
+          await this.allDeleteData(i, d1);
+        }
+        if (d2.length > 0) {
+          // 全创建
+          await this.allCreateData(i, d2);
+        }
+        if (d3.length > 0) {
+          // 需要两组数据对比着进行数据甄别
+          await this.updateData(i, d3);
+        }
+      }
+      await this.dataRecord.updateOne(i._id, { es_sync: true });
+    }
+  }
+
+  async updateData(searchResult, tables) {
+    const ods = get(searchResult, 'origin_data');
+    const nds = get(searchResult, 'new_data');
+    for (const key of tables) {
+      // 因为走upsert,所以只需要知道那些是删除就行
+      const odList = ods[key];
+      // ndList: upsert的数据
+      const ndList = nds[key];
+      // 以原数据组为基础,不在新数据组中的数据:删除
+      const deleteList = differenceBy(odList, ndList, '_id');
+      // 批量删除
+      if (deleteList.length > 0) await this.es.deleteBat(key, deleteList);
+      // 批量修改
+      if (ndList.length > 0) await this.es.createAndUpdateBat(key, ndList);
+    }
+  }
+
+  async allDeleteData(searchResult, tables) {
+    const ds = get(searchResult, 'origin_data');
+    for (const key of tables) {
+      const datas = ds[key];
+      if (datas.length <= 0) continue;
+      // 批量删除
+      await this.es.deleteBat(key, datas);
+    }
+  }
+
+  async allCreateData(searchResult, tables) {
+    const ds = get(searchResult, 'new_data');
+    for (const key of tables) {
+      const datas = ds[key];
+      if (datas.length <= 0) continue;
+      // 批量创建
+      await this.es.createAndUpdateBat(key, datas);
+    }
+  }
+
+  hasContext(obj) {
+    if (!obj) return false;
+    if (!isObject(obj)) return false;
+    if (Object.keys(obj).length <= 0) return false;
+    return obj;
+  }
+}

+ 106 - 0
src/service/db.service.ts

@@ -0,0 +1,106 @@
+import { App, Config, Init, Inject, Logger, Provide, Singleton } from '@midwayjs/decorator';
+import { Application, Context } from '@midwayjs/koa';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { get, head, last } from 'lodash';
+import { DataLogs } from '../entityRecord/dataLogs.entity';
+import { mongoose } from '@typegoose/typegoose';
+import * as dayjs from 'dayjs';
+import { Types } from 'mongoose';
+import { DataRecordService } from './record/dataRecord.service';
+import { GetModel } from 'free-midway-component';
+import { ILogger } from '@midwayjs/core';
+const ObjectId = Types.ObjectId;
+
+@Provide()
+@Singleton()
+export class DBService {
+  @Config('dbName')
+  dbName: string;
+  @Logger()
+  logger: ILogger;
+  @Config('mongoose.dataSource')
+  mongooseConfig: any;
+  @InjectEntityModel(DataLogs)
+  dataLogsModel: ReturnModelType<typeof DataLogs>;
+  @Inject()
+  dataRecordService: DataRecordService;
+
+  conn: any;
+  @App()
+  app: Application;
+  @Init()
+  async init() {
+    const conns = mongoose.connections;
+    const conn = conns.find(f => f.name === this.dbName);
+    console.log('change stream init');
+    conn.watch([], { fullDocument: 'updateLookup' }).on('change', async data => {
+      const record_id: string = this.app.getAttr('record_id');
+      if (!record_id) return;
+      // 查询日志数据,没有直接返回
+      const record: any = await this.dataRecordService.fetch(record_id);
+      if (!record) return;
+      const { operationType } = data;
+      const modelName: string = get(data, 'ns.coll');
+      // 没有表名不能继续
+      if (!modelName) return;
+      const model = GetModel(modelName);
+      // 未找到model不能继续
+      if (!model) return;
+      const allowOpera = ['insert', 'update', 'delete'];
+      // 只对表数据变化做处理
+      if (!allowOpera.includes(operationType)) return;
+      let data_id;
+      if (get(data, 'documentKey._id') && ObjectId.isValid(get(data, 'documentKey._id'))) data_id = new ObjectId(get(data, 'documentKey._id')).toString();
+      else {
+        console.log('id error');
+        return false;
+      }
+      const logsObj = { coll: modelName, time: dayjs().format('YYYY-MM-DD HH:mm:ss'), data_id, data: get(data, 'fullDocument'), method: operationType };
+      // 多个数据同步,不过根据logsObj去查询.不重复添加记录.一个数据同一个时间只允许记录一个,先来先记
+      const num = await this.dataLogsModel.count(logsObj);
+      if (num > 0) {
+        console.log('多个变更记录');
+        return;
+      }
+      let new_data, origin_data;
+      if (operationType === 'insert') {
+        // 无原数据
+        // 可以直接创建数据
+        await this.dataLogsModel.create(logsObj);
+        new_data = logsObj.data;
+      } else if (operationType === 'update') {
+        // 可以直接创建数据并且取最后两条
+        await this.dataLogsModel.create(logsObj);
+        const logs = await this.dataLogsModel.find({ coll: logsObj.coll, data_id: logsObj.data_id }).sort({ time: -1 }).limit(2).lean();
+        const newLogs = head(logs);
+        const originLogs = last(logs);
+        if (newLogs) new_data = get(newLogs, 'data');
+        if (originLogs) origin_data = get(originLogs, 'data');
+      } else if (operationType === 'delete') {
+        // 无新数据
+        // 先取最后一条,再创建
+        const logs = await this.dataLogsModel.find({ coll: logsObj.coll, data_id: logsObj.data_id }).sort({ time: -1 }).limit(1).lean();
+        const originLogs = head(logs);
+        if (originLogs) origin_data = get(originLogs, 'data');
+        await this.dataLogsModel.create(logsObj);
+      }
+      // 处理数据
+      if (origin_data) {
+        const rod = get(record, 'origin_data', {});
+        if (!rod[modelName]) rod[modelName] = [];
+        rod[modelName].push(origin_data);
+        record.origin_data = rod;
+      }
+      if (new_data) {
+        const nod = get(record, 'new_data', {});
+        if (!nod[modelName]) nod[modelName] = [];
+        nod[modelName].push(new_data);
+        record.new_data = nod;
+      }
+      // 更新记录
+      await this.dataRecordService.updateOne(record_id, record);
+      this.logger.warn('record success')
+    });
+  }
+}

+ 189 - 128
src/service/elasticsearch.ts

@@ -1,176 +1,229 @@
-import { Provide } from '@midwayjs/core';
+import { Config, Init, Inject, Provide } from '@midwayjs/core';
 import { Client, estypes } from '@elastic/elasticsearch';
-import { get, isArray, pickBy } from 'lodash';
-import { ReturnModelType } from '@typegoose/typegoose';
-import { Basic } from '../entity/basic.entity';
-import { InjectEntityModel } from '@midwayjs/typegoose';
-import { FrameworkErrorEnum, ServiceError } from 'free-midway-component';
-import * as dayjs from 'dayjs';
+import { get, isArray, pick, toLower } from 'lodash';
 import { Types } from 'mongoose';
+import { GetModel } from 'free-midway-component';
+import { ElasticSearchError, EsErrorEnum } from '../error/elasticsearch.error';
+import { MidwayI18nService } from '@midwayjs/i18n';
 const ObjectId = Types.ObjectId;
 
 @Provide()
 export class ElasticsearchService {
-  @InjectEntityModel(Basic)
-  basicModel: ReturnModelType<typeof Basic>;
+  @Config('elasticsearch')
+  esConfig: object;
+
+  @Config('mongoose.dataSource')
+  mongooseConfig: any;
+
+  @Inject()
+  i18n: MidwayI18nService;
 
   esClient: Client;
+  @Init()
+  async init() {
+    const esClient = new Client(this.esConfig);
+    this.esClient = esClient;
+  }
+
+  async preparMapping() {
+    console.log('in preparMapping');
+    // 根据mongodb设置,取出各个表中的es类型,形成以表名为索引的mapping
+    for (const db in this.mongooseConfig) {
+      const obj = this.mongooseConfig[db];
+      const entities = obj.entities;
+      for (const e of entities) {
+        this.createIndex(e.name, false);
+      }
+    }
+    console.log('out preparMapping');
+  }
+
+  async createIndex(tableName: string, remake = true) {
+    // 检查是否有该表的索引
+    const index = toLower(tableName);
+    const hasIndex = await this.esClient.indices.exists({ index });
+    if (hasIndex) {
+      if (remake) await this.esClient.indices.delete({ index });
+      else return;
+    }
+    // 没有索引,建立索引,先查表配置,根据配置进行mapping数据类型匹配.生成索引
+    const m = GetModel(tableName);
+    const sch = get(m, 'schema.tree');
+    const esSch = {};
+    for (const key in sch) {
+      const o = sch[key];
+      const type = get(o, 'esType');
+      const esType = this.getType(type);
+      if (esType) esSch[key] = esType;
+    }
+    // 没有一个字段有esType.那就不需要创建索引
+    if (Object.keys(esSch).length <= 0) return;
+    const properties: Record<estypes.PropertyName, estypes.MappingProperty> = esSch;
+    await this.esClient.indices.create({ index, mappings: { properties } });
+    await this.asyncData(tableName);
+  }
+
   /**
-   * 同步数据至es
+   * 初始化同步数据至es; 不负责创建索引
    * 同步数据需要过滤,有些数据类型,如果为不存在或者未 '' e.g.: 字符串日期,被识别成日期.那这个字符串就不能为 '',加入索引会报错
+   * @param {string} index 表名
    */
-  async asyncData() {
+  async asyncData(index) {
     if (!this.esClient) await this.init();
-    const list = await this.basicModel.find({}).lean();
-    let nowData;
-    try {
-      for (const i of list) {
-        const _id = new ObjectId(i._id).toString();
-        const doc = pickBy(i, (val, key) => {
-          if (key === '_id') return false;
-          if (val && val !== '') return true;
-        });
-        const obj = {
-          index: 'basic',
-          id: _id,
-          document: doc,
-        };
-        nowData = obj;
-        await this.esClient.index(obj);
-      }
-    } catch (e) {
-      console.log(e);
+    // es的索引要求都是小写
+    const indexName = toLower(index);
+    const hasIndex = await this.esClient.indices.exists({ index: indexName });
+    if (!hasIndex) return;
+    // 获取model: 因为索引和表名一致,直接用索引获取表
+    const model = GetModel(index);
+    // 获取表设置
+    const sch = get(model, 'schema.tree');
+    // 获取es设置
+    const esSch = {};
+    for (const key in sch) {
+      const o = sch[key];
+      const type = get(o, 'esType', get(o, 'type'));
+      const esType = this.getType(type);
+      if (esType) esSch[key] = esType;
     }
+    // 初始化更新
+    let datas = await model.find({}).lean();
+    datas = datas.map(i => ({ ...pick(i, Object.keys(esSch)), id: new ObjectId(i._id).toString() }));
+    const result = await this.esClient.helpers.bulk({
+      datasource: datas,
+      onDocument(doc) {
+        return {
+          index: { _index: indexName, _id: get(doc, 'id') },
+        };
+      },
+    });
+    console.log(result);
   }
 
   /**
    * 查询es中的数据
+   * 哪里需要,就单独为哪里做查询,这里只是试试好不好使
    * @param query 查询条件
    * @returns
    */
-  async search({ index, skip, limit, ...query }) {
+  async search({ index, skip, limit, ...query }: any) {
     if (!this.esClient) await this.init();
-    const res1 = await this.esClient.indices.exists({ index: 'test1' });
-    const res2 = await this.esClient.indices.exists({ index: 'testtime1' });
-    console.log(res1)
-    console.log(res2)
-    // 需要对查询条件进行处理
-    const matchList = ['name', 'english', 'type', 'subject'];
-    const rangeList = ['lab_acreage'];
-    const timeList = ['build_time'];
     const obj: any = {};
+    if (index) {
+      const hasIndex = await this.esClient.indices.exists({ index });
+      if (!hasIndex) return { data: [], total: 0 };
+      obj.index = index;
+    }
     const oq: any = {};
     for (const key in query) {
-      if (matchList.includes(key)) {
-        if (!oq.match) oq.match = {};
-        oq.match[key] = query[key];
-      } else if (rangeList.includes(key)) {
-        if (!oq.match) oq.range = {};
-        const value = query[key];
-        if (!isArray(value)) {
-          oq.range[key] = { gte: value };
-        } else {
-          oq.range[key] = { gte: value[0], lte: value[1] };
-        }
-      } else if (timeList.includes(key)) {
-        if (!oq.match) oq.range = {};
-        const value = query[key];
-        if (!isArray(value)) {
-          oq.range[`${key}`] = { gte: value };
-        } else {
-          oq.range[`${key}`] = { gte: 'now-1y', lte: 'now' };
-        }
-      }
+      if (!oq.match) oq.match = {};
+      oq.match[key] = query[key];
     }
     if (skip || skip === 0 || skip === '0') obj.from = skip;
     if (limit && limit !== '0') obj.size = limit;
     if (Object.keys(oq).length > 0) obj.query = oq;
     const r = await this.esClient.search(obj);
+    console.log(r);
     const result = this.getSearchResult(r);
     const total = this.getSearchTotal(r);
     return { data: result, total };
   }
+
+  async checkIndexAndGetSchmea(index) {
+    if (!this.esClient) await this.init();
+    // es的索引要求都是小写
+    const indexName = toLower(index);
+    const hasIndex = await this.esClient.indices.exists({ index: indexName });
+    if (!hasIndex) return;
+    // 获取model: 因为索引和表名一致,直接用索引获取表
+    const model = GetModel(index);
+    // 获取表设置
+    const sch = get(model, 'schema.tree');
+    // 获取es设置
+    const esSch = {};
+    for (const key in sch) {
+      const o = sch[key];
+      const type = get(o, 'esType', get(o, 'type'));
+      const esType = this.getType(type);
+      if (esType) esSch[key] = esType;
+    }
+    return esSch;
+  }
   /**
-   * 创建数据
-   * @param index 索引
+   * 批量创建/修改
+   * @param index 表名
    * @param data 数据
    */
-  async create({ index, data }) {
+  async createAndUpdateBat(index, data) {
     if (!this.esClient) await this.init();
-    console.log(data);
-    const mapping: Record<estypes.PropertyName, estypes.MappingProperty> = {
-      key: {
-        type: 'text',
+    const esSch = await this.checkIndexAndGetSchmea(index);
+    // 表没有es设置就不需要继续了
+    if (Object.keys(esSch).length <= 0) return;
+    const indexName = toLower(index);
+    const datas = data.map(i => ({ ...pick(i, Object.keys(esSch)), id: new ObjectId(i._id).toString() }));
+    const result = await this.esClient.helpers.bulk({
+      datasource: datas,
+      onDocument(doc) {
+        return [{ update: { _index: indexName, _id: get(doc, 'id') } }, { doc_as_upsert: true }];
       },
-      keyword: {
-        type: 'keyword',
-      },
-      num: {
-        type: 'float',
-      },
-      time: {
-        type: 'date',
-        format: 'yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis',
-      },
-      boo: {
-        type: 'boolean',
-      },
-      sort: {
-        type: 'integer',
+    });
+    return result;
+  }
+  /**
+   * 批量删除
+   * @param index 表名
+   * @param data 数据
+   */
+  async deleteBat(index, data) {
+    if (!this.esClient) await this.init();
+    const esSch = await this.checkIndexAndGetSchmea(index);
+    // 表没有es设置就不需要继续了
+    if (Object.keys(esSch).length <= 0) return;
+    const indexName = toLower(index);
+    const datas = data.map(i => ({ ...pick(i, Object.keys(esSch)), id: new ObjectId(i._id).toString() }));
+    const result = await this.esClient.helpers.bulk({
+      datasource: datas,
+      onDocument(doc) {
+        return { delete: { _index: indexName, _id: get(doc, 'id') } };
       },
-    };
-    for (let k = 1; k <= 3; k++) {
-      await this.esClient.indices.create({
-        index: `test${k}`,
-        mappings: {
-          properties: mapping,
-        },
-      });
-      for (let i = 1; i <= 3; i++) {
-        await this.esClient.index({
-          index: `test${k}`,
-          document: {
-            key: `${k}-菜单${i}`,
-            keyword: `${k}-菜${i}`,
-            time: `2023-06-0${i}`,
-            num: `${i}`,
-            boo: i % 2 === 0,
-            sort: i,
-            mkey: `${k}-单${i}`,
-          },
-        });
-      }
-    }
-
-    // return r;
+    });
+    return result;
   }
+
   /**
    * 更新数据
+   * @param index 索引
    * @param id 数据在es中的id
    * @param data 新数据
    */
-  async update(id: string, data: any) {
+  async update(index: string, id: string, data: any) {
     if (!this.esClient) await this.init();
+    const indexName = toLower(index);
+    const hasIndex = await this.esClient.indices.exists({ index: indexName });
+    if (!hasIndex) throw new ElasticSearchError(this.i18n.translate(EsErrorEnum.ES_INDEX_NOT_FOUND, { group: 'error' }), EsErrorEnum.ES_INDEX_NOT_FOUND);
     const result = await this.esClient.update({
-      index: 'basic',
+      index,
       id,
-      doc: {
-        doc: data,
-      },
+      doc: data,
     });
+    return result;
   }
   /**
    * 删除数据
    * @param id 数据在es的1id
    */
-  async delete(id: string) {
+  async delete(index: string, id: string) {
     if (!this.esClient) await this.init();
+    const indexName = toLower(index);
+    const hasIndex = await this.esClient.indices.exists({ index: indexName });
+    if (!hasIndex) throw new ElasticSearchError(this.i18n.translate(EsErrorEnum.ES_INDEX_NOT_FOUND, { group: 'error' }), EsErrorEnum.ES_INDEX_NOT_FOUND);
     try {
-      const r = await this.esClient.delete({ index: 'basic', id: null });
+      const result = await this.esClient.delete({ index: indexName, id });
+      return result;
     } catch (e) {
       const code = e.statusCode;
-      if (code === 404) throw new ServiceError('es未找到数据!', FrameworkErrorEnum.DATA_NOT_FOUND);
-      else if (code === 401) throw new ServiceError('es鉴权发生错误!', FrameworkErrorEnum.DATA_NOT_FOUND);
+      if (code === 404) throw new ElasticSearchError(this.i18n.translate(EsErrorEnum.ES_DATA_NOT_FOUND, { group: 'error' }), EsErrorEnum.ES_DATA_NOT_FOUND);
+      else throw new ElasticSearchError(this.i18n.translate(EsErrorEnum.ES_ERROR, { group: 'error' }), EsErrorEnum.ES_ERROR);
     }
   }
   /**
@@ -178,14 +231,33 @@ export class ElasticsearchService {
    * @param index 索引
    */
   async deleteIndex(index: string) {
-    await this.esClient.indices.delete({ index });
+    return await this.esClient.indices.delete({ index });
+  }
+
+  getType(type) {
+    const types = {
+      text: { type: 'text' },
+      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);
   }
+
   getSearchResult(result) {
     const res = get(result, 'hits.hits');
     let rData;
     if (isArray(res)) {
       // 整理结果,将_id放到数据中
-      rData = res.map(i => ({ _id: i._id, ...get(i, '_source.doc') }));
+      rData = res.map(i => ({ ...get(i, '_source'), es_index: i._index }));
     } else {
       rData = get(res, '_source');
     }
@@ -195,15 +267,4 @@ export class ElasticsearchService {
     const res = get(result, 'hits.total.value', 0);
     return res;
   }
-
-  async init() {
-    const esClient = new Client({
-      node: 'http://localhost:9200',
-      auth: {
-        username: 'elastic',
-        password: 'NAjqFz_7tS2DkdpU7p*x',
-      },
-    });
-    this.esClient = esClient;
-  }
 }

+ 23 - 0
src/service/i18n.service.ts

@@ -0,0 +1,23 @@
+import { Config, Inject, Provide } from '@midwayjs/decorator';
+import { MidwayI18nService } from '@midwayjs/i18n';
+import { Context } from '@midwayjs/koa';
+
+@Provide()
+export class I18nService {
+  @Config('i18n.defaultLocale')
+  defaultLocale: string;
+  @Inject()
+  ctx: Context;
+  @Inject()
+  i18n: MidwayI18nService;
+
+  translate(code:any) {
+    return this.i18n.translate(`${code}`, { locale: this.ctx.locale });
+  }
+  translateError(code:any) {
+    return this.i18n.translate(`${code}`, { locale: this.ctx.locale, group: 'error' });
+  }
+  translateMethod(code:any) {
+    return this.i18n.translate(`${code}`, { locale: this.ctx.locale, group: 'error' });
+  }
+}

+ 7 - 4
src/service/login.service.ts

@@ -6,6 +6,7 @@ import { RoleService } from '../service/system/role.service';
 import { RedisService } from '@midwayjs/redis';
 import * as Crypto from 'crypto-js';
 import { Context } from '@midwayjs/koa';
+import { I18nService } from './i18n.service';
 @Provide()
 export class LoginService {
   @Inject()
@@ -21,6 +22,8 @@ export class LoginService {
   jwtExpiresIn;
   @Inject()
   redisService: RedisService;
+  @Inject()
+  i18n: I18nService;
 
   async onePointLogin(loginVo) {
     const { _id, role } = loginVo;
@@ -38,9 +41,9 @@ export class LoginService {
 
   async onePointCheck() {
     const user = this.ctx.user;
-    if (!user) throw new ServiceError('未检测到登录信息,请重新登录!', FrameworkErrorEnum.NOT_LOGIN);
+    if (!user) throw new ServiceError(this.i18n.translateError(FrameworkErrorEnum.NOT_LOGIN), FrameworkErrorEnum.NOT_LOGIN);
     const { _id, role, login_code } = user;
-    if (!login_code) throw new ServiceError('未检测到登录标识,请重新登录!', FrameworkErrorEnum.NOT_LOGIN);
+    if (!login_code) throw new ServiceError(this.i18n.translateError(FrameworkErrorEnum.NOT_LOGIN), FrameworkErrorEnum.NOT_LOGIN);
     // 解密
     const decodeResult = Crypto.AES.decrypt(login_code, this.jwtSecret);
     const decode = Crypto.enc.Utf8.stringify(decodeResult).toString();
@@ -51,13 +54,13 @@ export class LoginService {
     const rediskey = `${this.loginSign}:${role}:${_id}`;
     // 取出当前记录在案的 code
     const redisCode = await this.redisService.get(rediskey);
-    if(!redisCode) throw new ServiceError('账号登录已失效,请重新登录!', FrameworkErrorEnum.NOT_LOGIN);
+    if (!redisCode) throw new ServiceError(this.i18n.translateError(`${FrameworkErrorEnum.NOT_LOGIN}-1`), FrameworkErrorEnum.NOT_LOGIN);
     // 判断是否一致
     if (code === redisCode) {
       // 一致,延时
       await this.redisService.expire(rediskey, this.jwtExpiresIn);
     } else {
-      throw new ServiceError('本账号已在其他地方登录,请重新登录!', FrameworkErrorEnum.NOT_LOGIN);
+      throw new ServiceError(this.i18n.translateError(`${FrameworkErrorEnum.NOT_LOGIN}-2`), FrameworkErrorEnum.NOT_LOGIN);
     }
   }
 

+ 11 - 0
src/service/record/dataLogs.service.ts

@@ -0,0 +1,11 @@
+import { Provide } from '@midwayjs/decorator';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from 'free-midway-component';
+import { DataLogs } from '../../entityRecord/dataLogs.entity';
+type modelType = ReturnModelType<typeof DataLogs>;
+@Provide()
+export class DataLogsService extends BaseService<modelType> {
+  @InjectEntityModel(DataLogs)
+  model: modelType;
+}

+ 3 - 2
src/service/record/dataRecord.service.ts

@@ -1,4 +1,4 @@
-import { Provide } from '@midwayjs/decorator';
+import { Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
 import { InjectEntityModel } from '@midwayjs/typegoose';
 import { ReturnModelType } from '@typegoose/typegoose';
 import { BaseService } from 'free-midway-component';
@@ -8,6 +8,7 @@ import dayjs = require('dayjs');
 
 type modelType = ReturnModelType<typeof DataRecord>;
 @Provide()
+@Scope(ScopeEnum.Request, { allowDowngrade: true })
 export class DataRecordService extends BaseService<modelType> {
   @InjectEntityModel(DataRecord)
   model: modelType;
@@ -54,7 +55,7 @@ export class DataRecordService extends BaseService<modelType> {
     let data = null;
     if (method === 'delete') return data;
     const modelName = get(service, 'model.modelName');
-    if (method === 'update') {
+    if (method === 'update' || method === 'status') {
       if (origin_data) {
         const list = get(origin_data, modelName);
         const arr = [];