lrf před 1 měsícem
revize
2ee191c6ed
77 změnil soubory, kde provedl 14310 přidání a 0 odebrání
  1. 11 0
      .editorconfig
  2. 17 0
      .eslintrc.json
  3. 14 0
      .gitignore
  4. 3 0
      .prettierrc.js
  5. 3 0
      README.md
  6. 29 0
      README.zh-CN.md
  7. 2 0
      bootstrap.js
  8. 20 0
      ecosystem.config.js
  9. 6 0
      jest.config.js
  10. 9083 0
      package-lock.json
  11. 54 0
      package.json
  12. 2 0
      service/bootstrap.js
  13. 20 0
      service/ecosystem.config.js
  14. 52 0
      service/package.json
  15. 32 0
      src/config/config.default.ts
  16. 36 0
      src/config/config.local.ts
  17. 40 0
      src/config/config.prod.ts
  18. 7 0
      src/config/config.unittest.ts
  19. 39 0
      src/configuration.ts
  20. 90 0
      src/controller/frame/File.controller.ts
  21. 17 0
      src/controller/frame/Init.controller.ts
  22. 61 0
      src/controller/frame/Login.controller.ts
  23. 31 0
      src/controller/frame/Token.controller.ts
  24. 60 0
      src/controller/system/admin.controller.ts
  25. 46 0
      src/controller/system/config.controller.ts
  26. 48 0
      src/controller/system/dictData.controller.ts
  27. 48 0
      src/controller/system/dictType.controller.ts
  28. 47 0
      src/controller/system/menus.controller.ts
  29. 48 0
      src/controller/system/role.controller.ts
  30. 60 0
      src/controller/system/user.controller.ts
  31. 24 0
      src/decorator/page.decorator.ts
  32. 17 0
      src/entity/frame/loginRecord.entity.ts
  33. 23 0
      src/entity/pageView.entity.ts
  34. 45 0
      src/entity/question.entity.ts
  35. 17 0
      src/entity/system/admin.entity.ts
  36. 9 0
      src/entity/system/config.entity.ts
  37. 15 0
      src/entity/system/dictData.entity.ts
  38. 15 0
      src/entity/system/dictType.entity.ts
  39. 29 0
      src/entity/system/menus.entity.ts
  40. 15 0
      src/entity/system/role.entity.ts
  41. 17 0
      src/entity/system/user.entity.ts
  42. 40 0
      src/error/Codes.ts
  43. 14 0
      src/error/CustomerError.error.ts
  44. 16 0
      src/filter/Default.filter.ts
  45. 26 0
      src/filter/ServiceError.filter.ts
  46. 21 0
      src/frame/BaseModel.ts
  47. 155 0
      src/frame/BaseService.ts
  48. 21 0
      src/frame/Meta.ts
  49. 44 0
      src/frame/Options.ts
  50. 64 0
      src/frame/QueryUtils.ts
  51. 7 0
      src/frame/Utils.ts
  52. 180 0
      src/frame/conditionBuilder.ts
  53. 36 0
      src/frame/dbOpera.ts
  54. 6 0
      src/interface.ts
  55. 87 0
      src/middleware/CheckToken.middleware.ts
  56. 27 0
      src/middleware/report.middleware.ts
  57. 15 0
      src/response/CustomerResponse.ts
  58. 238 0
      src/service/frame/File.service.ts
  59. 241 0
      src/service/frame/Init.service.ts
  60. 90 0
      src/service/frame/Login.service.ts
  61. 96 0
      src/service/frame/LoginRecord.service.ts
  62. 67 0
      src/service/frame/common.service.ts
  63. 28 0
      src/service/pageView.service.ts
  64. 83 0
      src/service/question.service.ts
  65. 31 0
      src/service/system/admin.service.ts
  66. 20 0
      src/service/system/config.service.ts
  67. 11 0
      src/service/system/dictData.service.ts
  68. 11 0
      src/service/system/dictType.service.ts
  69. 36 0
      src/service/system/menus.service.ts
  70. 68 0
      src/service/system/role.service.ts
  71. 21 0
      src/service/system/user.service.ts
  72. 20 0
      test/controller/api.test.ts
  73. 21 0
      test/controller/home.test.ts
  74. 28 0
      tsconfig.json
  75. binární
      upload/logo/20250226165535.png
  76. binární
      upload/logo/20250226165552.png
  77. 2189 0
      water_service.sql

+ 11 - 0
.editorconfig

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

+ 17 - 0
.eslintrc.json

@@ -0,0 +1,17 @@
+{
+  "extends": "./node_modules/mwts/",
+  "ignorePatterns": [
+    "node_modules",
+    "dist",
+    "test",
+    "jest.config.js",
+    "typings"
+  ],
+  "env": {
+    "jest": true
+  },
+  "rules": {
+    "prettier/prettier": 0,
+    "max-len": ["warn", { "code": 250 }]
+  }
+}

+ 14 - 0
.gitignore

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

+ 3 - 0
.prettierrc.js

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

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# 数据库指令
+grant all privileges on water_service_v2.* to 'water_v2'@'%' identified by 'ccshuitou';
+grant SELECT on water_service.* to 'water_v1'@'%' identified by 'ccshuitou'

+ 29 - 0
README.zh-CN.md

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

+ 2 - 0
bootstrap.js

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

+ 20 - 0
ecosystem.config.js

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

+ 6 - 0
jest.config.js

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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 9083 - 0
package-lock.json


+ 54 - 0
package.json

@@ -0,0 +1,54 @@
+{
+  "name": "my-midway-project",
+  "version": "1.0.0",
+  "description": "",
+  "private": true,
+  "dependencies": {
+    "@midwayjs/bootstrap": "^3.19.3",
+    "@midwayjs/busboy": "^3.19.3",
+    "@midwayjs/core": "^3.19.0",
+    "@midwayjs/info": "^3.19.2",
+    "@midwayjs/jwt": "^3.19.3",
+    "@midwayjs/koa": "^3.19.2",
+    "@midwayjs/logger": "^3.1.0",
+    "@midwayjs/typeorm": "^3.19.2",
+    "@midwayjs/validate": "^3.19.2",
+    "bcryptjs": "^3.0.2",
+    "dayjs": "^1.11.13",
+    "fs-extra": "^11.2.0",
+    "lodash": "^4.17.21",
+    "mysql2": "^3.11.5",
+    "typeorm": "^0.3.20"
+  },
+  "devDependencies": {
+    "@midwayjs/mock": "^3.19.2",
+    "@types/jest": "^29.2.0",
+    "@types/lodash": "^4.17.13",
+    "@types/node": "14",
+    "cross-env": "^6.0.0",
+    "jest": "^29.2.2",
+    "mwts": "^1.3.0",
+    "mwtsc": "^1.4.0",
+    "ts-jest": "^29.0.3",
+    "typescript": "~4.8.0"
+  },
+  "engines": {
+    "node": ">=12.0.0"
+  },
+  "scripts": {
+    "start": "NODE_ENV=production node ./bootstrap.js",
+    "dev": "cross-env NODE_ENV=local mwtsc --watch --run @midwayjs/mock/app.js",
+    "test": "cross-env NODE_ENV=unittest jest",
+    "cov": "jest --coverage",
+    "lint": "mwts check",
+    "lint:fix": "mwts fix",
+    "ci": "npm run cov",
+    "build": "mwtsc --cleanOutDir"
+  },
+  "repository": {
+    "type": "git",
+    "url": ""
+  },
+  "author": "anonymous",
+  "license": "MIT"
+}

+ 2 - 0
service/bootstrap.js

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

+ 20 - 0
service/ecosystem.config.js

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

+ 52 - 0
service/package.json

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

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

@@ -0,0 +1,32 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1734594361174_9059',
+  koa: {
+    port: 7001,
+  },
+  /**上传设置, columns:涉及文件的字段:${表名}.${字段名} 表名用model的class名,因为在connect中存的就是这个类 */
+  busboy: {
+    mode: 'file',
+    realdir: 'upload',
+    whitelist: null,
+  },
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: 3600, // 3600
+  },
+  /**启用checkToken中间件 */
+  useCheckTokenMiddleware: true,
+  /**不检查token的路由列表 */
+  passToken: [
+    '/login/:type',
+    '/dictType',
+    '/files/*',
+    '/files/:project/upload',
+    '/files/:project/:catalog/upload',
+    '/files/:project/:catalog/:item/upload',
+  ],
+  /**请求头中token的key名 */
+  tokenKey: 'token',
+} as MidwayConfig;

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

@@ -0,0 +1,36 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+/**数据库ip */
+const ip = '192.168.1.153';
+/**新数据库 */
+const v2DbName = 'water_service_v2'
+/**数据库用户名 */
+const dbUsername = 'root';
+/**数据库密码 */
+const dbPwd = 'root';
+/**旧文件域名 */
+const urlDomain = 'https://36.135.104.33'
+export default {
+  keys: '1697684406848_4978',
+  koa: {
+    port: 9001,
+    globalPrefix: '/warter/admin/api',
+    queryParseMode: 'extended',
+  },
+  urlDomain,
+  typeorm: {
+    dataSource: {
+      default: {
+        type: 'mysql',
+        host: ip,
+        port: 3306,
+        database: v2DbName,
+        username: dbUsername,
+        password: dbPwd,
+        entities: ['./entity'],
+        synchronize: true, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
+        logging: true,
+      }
+    },
+  },
+} as MidwayConfig;

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

@@ -0,0 +1,40 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+/**数据库ip */
+const ip = '10.2.2.17';
+/**新数据库 */
+const v2DbName = 'water_service_v2'
+
+const dbv2 = 'water_v2'
+const dbv2Pwd = 'ccshuitou'
+/**旧文件域名 */
+const urlDomain = 'http://10.2.2.18:9004'
+export default {
+  keys: '1697684406848_4978',
+  koa: {
+    port: 9001,
+    globalPrefix: '/warter/admin/api',
+    queryParseMode: 'extended',
+  },
+  midwayLogger: {
+    default: {
+      level: 'info'
+    }
+  },
+  urlDomain,
+  typeorm: {
+    dataSource: {
+      default: {
+        type: 'mysql',
+        host: ip,
+        port: 3306,
+        database: v2DbName,
+        username: dbv2,
+        password: dbv2Pwd,
+        entities: ['./entity'],
+        synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
+        logging: false,
+      }
+    },
+  },
+} as MidwayConfig;

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

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

+ 39 - 0
src/configuration.ts

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

+ 90 - 0
src/controller/frame/File.controller.ts

@@ -0,0 +1,90 @@
+import { Context } from '@midwayjs/koa';
+import { join, sep } from 'path';
+import { FileService } from '../../service/frame/File.service';
+import { createReadStream } from 'fs';
+import {
+  Controller,
+  Inject,
+  Config,
+  Post,
+  Files,
+  Fields,
+  Get,
+} from '@midwayjs/core';
+import * as mime from 'mime-types';
+import { UploadMiddleware } from '@midwayjs/busboy';
+// import { RF } from '../../response/CustomerResponse';
+/**文件上传默认类 */
+@Controller('/files')
+export class FileController {
+  @Inject()
+  ctx: Context;
+  @Inject()
+  fileService: FileService;
+  @Config('busboy.realdir')
+  uploadDir;
+
+  /**
+   * 文件上传
+   * @param {Array} files 文件数组
+   * @param {object} fields 其他字段
+   * @param {string} project 项目名
+   * @param {string} catalog 文件层级名 用'_'连接上下层级
+   * @param {string} item 文件名,没有默认用时间戳
+   */
+  @Post('/:project/upload', { middleware: [UploadMiddleware] })
+  @Post('/:project/:catalog/upload', { middleware: [UploadMiddleware] })
+  @Post('/:project/:catalog/:item/upload', { middleware: [UploadMiddleware] })
+  async upload(@Files() files, @Fields() fields) {
+    // const hasModel = await this.fileService.hasTmpModel();
+    // if (hasModel) {
+    //   return RF.error('系统正在处理上传文件,暂时无法上传');
+    // }
+    const { project, catalog, item } = this.ctx.params;
+    const dirs = [project];
+    if (catalog && catalog !== '_') {
+      const subs = catalog.split('_');
+      dirs.push(...subs);
+    }
+    let path = this.uploadDir;
+    // 检查分级目录是否存在,不存在则创建
+    for (let i = 0; i < dirs.length; i++) {
+      const p = `${path}${sep}${dirs.slice(0, i + 1).join(sep)}`;
+      this.fileService.mkdir(p);
+    }
+    path = `${join(path, dirs.join(sep))}${sep}`;
+    const file = files[0];
+    const ext = this.fileService.getExt(file.filename);
+    let filename = `${this.fileService.getNowDateTime()}${ext}`;
+    if (item) filename = `${item}${ext}`;
+    const uri = this.fileService.getFileShortPath(dirs, filename);
+    this.fileService.moveFile(file.data, `${path}${filename}`);
+    return { id: filename, name: filename, uri, errcode: 0 };
+  }
+
+  /**
+   * 读取文件
+   */
+  @Get('/*')
+  async readFile() {
+    // const hasModel = await this.fileService.hasTmpModel();
+    // if (hasModel) {
+    //   return RF.error('系统正在处理上传文件,暂时无法上传');
+    // }
+    const shortRealPath = this.fileService.getFileShortRealPath();
+    const realPath = join(this.uploadDir, shortRealPath);
+    this.ctx.body = createReadStream(realPath);
+    const type = mime.lookup(realPath);
+    this.ctx.response.set('content-type', type);
+  }
+
+  @Post('/clear/files')
+  async clearFiles() {
+    try {
+      await this.fileService.deleteNoUseFile();
+    } catch (error) {
+      console.error(error);
+    }
+    return 'ok';
+  }
+}

+ 17 - 0
src/controller/frame/Init.controller.ts

@@ -0,0 +1,17 @@
+import { Context } from '@midwayjs/koa';
+import { Controller, Inject, Post } from '@midwayjs/core';
+import { InitService } from '../../service/frame/Init.service';
+import { RF } from '../../response/CustomerResponse';
+@Controller('/init')
+export class InitController {
+  @Inject()
+  ctx: Context;
+
+  @Inject()
+  service: InitService;
+
+  @Post('/')
+  async index() {
+    return RF.success();
+  }
+}

+ 61 - 0
src/controller/frame/Login.controller.ts

@@ -0,0 +1,61 @@
+import { Controller, Post, Body, Param, Inject, Config } from '@midwayjs/core';
+import { LoginService } from '../../service/frame/Login.service';
+import { LoginType, LoginVO, UPwdDTO } from '../../frame/Options';
+import { JwtService } from '@midwayjs/jwt';
+import { LoginRecordService } from '../../service/frame/LoginRecord.service';
+import { RF } from '../../response/CustomerResponse';
+import { Validate } from '@midwayjs/validate';
+
+@Controller('/login')
+export class LoginController {
+  @Inject()
+  loginService: LoginService;
+  @Inject()
+  loginRecordService: LoginRecordService;
+  @Config('jwt.secret')
+  jwtSecret;
+  @Inject()
+  jwtService: JwtService;
+  /**
+   * 账密登录
+   * @param data 用户名和密码
+   * @param type 用户类型
+   */
+  @Post('/:type')
+  async toLogin(@Body() data, @Param('type') type: string) {
+    const user = await this.loginService.loginByAccount(data, LoginType[type]);
+    if (user) user.role = type;
+    let vo = new LoginVO(user);
+    vo = JSON.parse(JSON.stringify(vo));
+    // user数据写成
+    const token = await this.jwtService.sign(vo, this.jwtSecret);
+    // 创建/更新登录信息
+    await this.loginRecordService.create(token);
+    return RF.success(token);
+  }
+
+  /**
+   * 修改密码
+   * @param data 修改密码所需数据
+   * @param type 账户类型
+   */
+  @Validate()
+  @Post('/updatePwd/:type')
+  async updatePwd(@Body() data: UPwdDTO, @Param('type') type: string) {
+    await this.loginService.updatePwd(data, LoginType[type]);
+    return RF.success()
+  }
+  /**
+   * 重置密码
+   * @param id 用户id
+   * @param type 用户类型
+   */
+  @Post('/resetPwd/:type')
+  async resetPwd(@Body('id') id: string, @Param('type') type: string) {
+    // 随机密码,不需要写密码字段,函数内会给
+    const data = new UPwdDTO();
+    data.id = id;
+    const password = await this.loginService.updatePwd(data, LoginType[type]);
+    return RF.success(password);
+  }
+}

+ 31 - 0
src/controller/frame/Token.controller.ts

@@ -0,0 +1,31 @@
+import { Config, Context, Controller, Get, Inject } from "@midwayjs/core";
+import { JwtService } from "@midwayjs/jwt";
+import assert = require("assert");
+import { get } from "lodash";
+import { RoleService } from "../../service/system/role.service";
+import { RF } from "../../response/CustomerResponse";
+
+
+@Controller('/token')
+export class TokenController {
+  @Inject()
+  ctx: Context;
+  @Inject()
+  jwtService: JwtService;
+  @Inject()
+  roleService: RoleService;
+  @Config('tokenKey')
+  tokenKey: string;
+  @Get('/tokenView')
+  async tokenView() {
+    const token = get(this.ctx, `request.header.${this.tokenKey}`);
+    assert(token, '缺少token信息');
+    const result: any = this.jwtService.decode(token);
+    const userMenusResult = await this.roleService.getUserMenus();
+    const menus = get(userMenusResult, 'menus');
+    const role_code = get(userMenusResult, 'role_code');
+    result.menus = menus;
+    result.role_code = role_code;
+    return RF.success(result);
+  }
+}

+ 60 - 0
src/controller/system/admin.controller.ts

@@ -0,0 +1,60 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { AdminService } from '../../service/system/admin.service';
+import { RF } from '../../response/CustomerResponse';
+import { Page, Query } from '../../decorator/page.decorator';
+import * as bcrypt from 'bcryptjs';
+import { get } from 'lodash';
+
+@Controller('/admin')
+export class AdminController {
+  @Inject()
+  service: AdminService;
+
+  @Post('/')
+  async create(@Body() body) {
+    await this.service.checkInDB(body);
+    // 处理密码
+    const passowrd = get(body, 'password');
+    if (passowrd) {
+      const salt = bcrypt.genSaltSync(10);
+      const hash = bcrypt.hashSync(passowrd, salt);
+      Object.assign(body, { password: hash });
+    }
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.query(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.fetch({ id });
+    return RF.success(result);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') _id: string, @Body() body) {
+    // 删除account字段,不允许更改
+    delete body.account;
+    const result = await this.service.update({ _id }, body);
+    return RF.success(result);
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') _id: string) {
+    await this.service.delete({ _id });
+    return RF.success();
+  }
+}

+ 46 - 0
src/controller/system/config.controller.ts

@@ -0,0 +1,46 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { ConfigService } from '../../service/system/config.service';
+import { RF } from '../../response/CustomerResponse';
+import { ServiceError } from '../../error/CustomerError.error';
+import { ErrorCode } from '../../error/Codes';
+
+@Controller('/config')
+export class ConfigController {
+  @Inject()
+  service: ConfigService;
+
+  @Post('/')
+  async create() {
+    throw new ServiceError(ErrorCode.INTERFACE_NOT_OPEN);
+  }
+
+  @Get('/')
+  async query() {
+    const result = await this.service.getConfig();
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    throw new ServiceError(ErrorCode.INTERFACE_NOT_OPEN);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') id: number, @Body() body) {
+    const result = await this.service.update({ id }, body);
+    return RF.success(result)
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') id: string) {
+    throw new ServiceError(ErrorCode.INTERFACE_NOT_OPEN);
+  }
+}

+ 48 - 0
src/controller/system/dictData.controller.ts

@@ -0,0 +1,48 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { DictDataService } from '../../service/system/dictData.service';
+import { RF } from '../../response/CustomerResponse';
+import { Page, Query } from '../../decorator/page.decorator';
+
+@Controller('/dictData')
+export class DictDataController {
+  @Inject()
+  service: DictDataService;
+
+  @Post('/')
+  async create(@Body() body) {
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.query(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.fetch({ id });
+    return RF.success(result);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') id: string, @Body() body) {
+    const result = await this.service.update({ id }, body);
+    return RF.success(result);
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') id: string) {
+    await this.service.delete({ id });
+    return RF.success();
+  }
+}

+ 48 - 0
src/controller/system/dictType.controller.ts

@@ -0,0 +1,48 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { DictTypeService } from '../../service/system/dictType.service';
+import { RF } from '../../response/CustomerResponse';
+import { Page, Query } from '../../decorator/page.decorator';
+
+@Controller('/dictType')
+export class DictTypeController {
+  @Inject()
+  service: DictTypeService;
+
+  @Post('/')
+  async create(@Body() body) {
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.query(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.fetch({ id });
+    return RF.success(result);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') id: string, @Body() body) {
+    const result = await this.service.update({ id }, body);
+    return RF.success(result);
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') id: string) {
+    await this.service.delete({ id });
+    return RF.success();
+  }
+}

+ 47 - 0
src/controller/system/menus.controller.ts

@@ -0,0 +1,47 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { MenusService } from '../../service/system/menus.service';
+import { RF } from '../../response/CustomerResponse';
+
+@Controller('/menus')
+export class MenusController {
+  @Inject()
+  service: MenusService;
+
+  @Post('/')
+  async create(@Body() body) {
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query() {
+    const result = await this.service.queryMenu();
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.fetch({ id });
+    return RF.success(result);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') id: string, @Body() body) {
+    const result = await this.service.update({ id }, body);
+    return RF.success(result);
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') id: string) {
+    await this.service.delete({ id });
+    return RF.success();
+  }
+}

+ 48 - 0
src/controller/system/role.controller.ts

@@ -0,0 +1,48 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { RoleService } from '../../service/system/role.service';
+import { RF } from '../../response/CustomerResponse';
+import { Page, Query } from '../../decorator/page.decorator';
+
+@Controller('/role')
+export class RoleController {
+  @Inject()
+  service: RoleService;
+
+  @Post('/')
+  async create(@Body() body) {
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.query(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.fetch({ id });
+    return RF.success(result);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') id: string, @Body() body) {
+    const result = await this.service.update({ id }, body);
+    return RF.success(result);
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') id: string) {
+    await this.service.delete({ id });
+    return RF.success();
+  }
+}

+ 60 - 0
src/controller/system/user.controller.ts

@@ -0,0 +1,60 @@
+import {
+  Body,
+  Controller,
+  Del,
+  Get,
+  Inject,
+  Param,
+  Post,
+} from '@midwayjs/core';
+import { UserService } from '../../service/system/user.service';
+import { RF } from '../../response/CustomerResponse';
+import { Page, Query } from '../../decorator/page.decorator';
+import { get } from 'lodash';
+import * as bcrypt from 'bcryptjs';
+
+@Controller('/user')
+export class UserController {
+  @Inject()
+  service: UserService;
+
+  @Post('/')
+  async create(@Body() body) {
+    await this.service.checkInDB(body);
+    // 处理密码
+    const passowrd = get(body, 'password');
+    if (passowrd) {
+      const salt = bcrypt.genSaltSync(10);
+      const hash = bcrypt.hashSync(passowrd, salt);
+      Object.assign(body, { password: hash });
+    }
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.query(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.fetch({ id });
+    return RF.success(result);
+  }
+
+  @Post('/:id')
+  async update(@Param('id') id: string, @Body() body) {
+    // 删除account字段,不允许更改
+    delete body.account;
+    const result = await this.service.update({ id }, body);
+    return RF.success(result);
+  }
+
+  @Del('/:id')
+  async delete(@Param('id') id: string) {
+    await this.service.delete({ id });
+    return RF.success();
+  }
+}

+ 24 - 0
src/decorator/page.decorator.ts

@@ -0,0 +1,24 @@
+import { createRequestParamDecorator } from '@midwayjs/core';
+import { get, pick, omit } from 'lodash';
+import { QueryMapping, QueryReset } from '../frame/QueryUtils';
+const pageKeys = ['skip', 'limit'];
+/**将query的skip和limit提取到object中返回 */
+export const Page = () => {
+  return createRequestParamDecorator(ctx => {
+    const query = get(ctx, 'query', {});
+    const page = pick(query, pageKeys);
+    return page;
+  });
+};
+/**将query中的skip和limit剃除,整理查询参数的值和键的映射 */
+export const Query = (mapping?: object) => {
+  return createRequestParamDecorator(ctx => {
+    const query = get(ctx, 'query', {});
+    let filter: object = omit(query, pageKeys);
+    // 先处理参数的查询方式
+    filter = QueryReset(filter);
+    // 再处理mapping的字段映射
+    filter = QueryMapping(filter, mapping);
+    return filter;
+  });
+};

+ 17 - 0
src/entity/frame/loginRecord.entity.ts

@@ -0,0 +1,17 @@
+import { Entity, Column } from "typeorm";
+import { BaseModel } from "../../frame/BaseModel";
+
+@Entity('loginRecord', { comment: '登陆记录' })
+export class LoginRecord extends BaseModel {
+
+  @Column({ comment: '用户id' })
+  user_id: number;
+  @Column({ comment: 'token', nullable: true })
+  token: string
+  @Column({ comment: '最后使用时间', nullable: true })
+  last_time: string
+  @Column({ comment: '过期时间', nullable: true })
+  expire_time: string
+  @Column({ comment: '最后使用ip', nullable: true })
+  last_ip: string
+}

+ 23 - 0
src/entity/pageView.entity.ts

@@ -0,0 +1,23 @@
+import dayjs = require('dayjs');
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn } from 'typeorm';
+
+@Entity('pageView', { comment: '浏览记录' })
+export class PageView {
+  /**数据id */
+  @PrimaryGeneratedColumn({ type: 'integer' })
+  id: number;
+  /**数据创建时间 */
+  @CreateDateColumn({ type: 'timestamp', transformer: { from: value => (value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : value), to: value => value } })
+  created_time?: Date;
+  /**数据最后更新时间 */
+  @UpdateDateColumn({ type: 'timestamp', transformer: { from: value => (value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : value), to: value => value } })
+  update_time?: Date;
+  /**数据版本 */
+  @VersionColumn({ type: 'integer', default: 1 })
+  __v?: number;
+
+  @Column({ comment: 'content_id', nullable: true })
+  content_id: number;
+  @Column({ comment: '浏览量', default: 1 })
+  num: number
+}

+ 45 - 0
src/entity/question.entity.ts

@@ -0,0 +1,45 @@
+import dayjs = require('dayjs');
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn } from 'typeorm';
+
+@Entity('question', { comment: '投诉与建议' })
+export class Question {
+  /**数据id */
+  @PrimaryGeneratedColumn({ type: 'integer' })
+  question_id: number;
+  /**数据创建时间 */
+  @CreateDateColumn({ type: 'timestamp', transformer: { from: value => (value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : value), to: value => value } })
+  created_time?: Date;
+  /**数据最后更新时间 */
+  @UpdateDateColumn({ type: 'timestamp', transformer: { from: value => (value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : value), to: value => value } })
+  update_time?: Date;
+  /**数据版本 */
+  @VersionColumn({ type: 'integer', default: 1 })
+  __v?: number;
+
+  @Column({ comment: '问题类型: 1-投诉反馈; 2-意见建议; 3-咨询反馈' })
+  type: string;
+  @Column({ comment: '姓名' })
+  name: string;
+  @Column({ comment: '电话' })
+  phone: string;
+  @Column({ comment: '邮箱' })
+  email: string;
+  @Column({ comment: '街/路' })
+  street: string;
+  @Column({ comment: '小区' })
+  community: string;
+  @Column({ comment: '楼栋' })
+  building: string;
+  @Column({ comment: '单元' })
+  unit: string;
+  @Column({ comment: '室' })
+  room: string;
+  @Column({ comment: '地址' })
+  address: string;
+  @Column({ comment: '反馈详情' })
+  description: string;
+  @Column({ comment: '处理状态: 0-未处理;1-已处理', default: '0' })
+  deal_status: string
+  @Column({ type: 'timestamp', nullable: true, transformer: { from: value => (value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : value), to: value => value }, comment: '处理时间' })
+  deal_time: Date;
+}

+ 17 - 0
src/entity/system/admin.entity.ts

@@ -0,0 +1,17 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('admin', { comment: '管理员' })
+export class Admin extends BaseModel {
+  @Column({ type: 'varchar', nullable: true, comment: '管理员名称' })
+  nick_name: string;
+  @Column({ type: 'varchar', comment: '账号', unique: true })
+  account: string;
+  @Column({ type: 'varchar', select: false, comment: '密码' })
+  password: string;
+  @Column({ type: 'json', nullable: true, comment: '角色id数组' })
+  role: any;
+  @Column({ type: 'varchar', default: '0', comment: '是否使用: 0:使用;1:禁用' })
+  is_use: string;
+  @Column({ type: 'varchar', default: '1', comment: '是否是超级管理员: 0:是;1否' })
+  is_super: string;
+}

+ 9 - 0
src/entity/system/config.entity.ts

@@ -0,0 +1,9 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('config', { comment: '系统设置' })
+export class Config extends BaseModel {
+  @Column({ type: 'varchar', nullable: true, comment: '系统名称' })
+  name: string;
+  @Column({ type: 'json', nullable: true, comment: 'logo' })
+  logo: any;
+}

+ 15 - 0
src/entity/system/dictData.entity.ts

@@ -0,0 +1,15 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('dictData', { comment: '字典数据表' })
+export class DictData extends BaseModel {
+  @Column({ type: 'varchar', nullable: true, comment: '字典类型编码' })
+  code: string;
+  @Column({ type: 'varchar', nullable: true, comment: '数据显示值' })
+  label: string;
+  @Column({ type: 'varchar', nullable: true, comment: '数据选择值' })
+  value: string;
+  @Column({ type: 'int', nullable: true, comment: '排序' })
+  sort: number;
+  @Column({ type: 'varchar', default: '0', nullable: true, comment: '是否使用:0:使用;1禁用' })
+  is_use: string;
+}

+ 15 - 0
src/entity/system/dictType.entity.ts

@@ -0,0 +1,15 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('dictType', { comment: '字典目录表' })
+export class DictType extends BaseModel {
+  @Column({ type: 'varchar', comment: '字典类型名称' })
+  title: string;
+  @Column({ type: 'varchar', nullable: true, comment: '字典编码' })
+  code: string;
+  @Column({ type: 'varchar', nullable: true, comment: '备注' })
+  remark: string;
+  @Column({ type: 'int', nullable: true, comment: '排序' })
+  sort: number;
+  @Column({ type: 'varchar', default: '0', nullable: true, comment: '是否使用:0:使用;1禁用' })
+  is_use: string;
+}

+ 29 - 0
src/entity/system/menus.entity.ts

@@ -0,0 +1,29 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('menus', { comment: '菜单表' })
+export class Menus extends BaseModel {
+  @Column({ type: 'varchar', nullable: true, comment: '菜单名称' })
+  name: string;
+  @Column({ type: 'varchar', nullable: true, comment: '路由名称' })
+  route_name: string;
+  @Column({ type: 'int', nullable: true, comment: '父级菜单' })
+  parent_id: number;
+  @Column({ type: 'int', nullable: true, comment: '显示顺序' })
+  order_num: number;
+  @Column({ type: 'varchar', nullable: true, comment: '路由地址' })
+  path: string;
+  @Column({ type: 'varchar', nullable: true, comment: '组件地址' })
+  component: string;
+  @Column({ type: 'varchar', nullable: true, comment: '菜单类型:0:目录;1:菜单;2:子页面' })
+  type: string;
+  @Column({ type: 'varchar', nullable: true, comment: '图标' })
+  icon: string;
+  @Column({ type: 'json', nullable: true, comment: '功能列表' })
+  config: any;
+  @Column({ type: 'varchar', default: '0', comment: '是否默认:0:默认;1:非默认;默认不能删除' })
+  is_default: string;
+  @Column({ type: 'varchar', nullable: true, comment: '备注' })
+  remark: string;
+  @Column({ type: 'varchar', default: '0', comment: '是否使用:0:使用;1禁用' })
+  is_use: string;
+}

+ 15 - 0
src/entity/system/role.entity.ts

@@ -0,0 +1,15 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('role', { comment: '角色表' })
+export class Role extends BaseModel {
+  @Column({ type: 'varchar', nullable: true, comment: '角色名称' })
+  name: string;
+  @Column({ type: 'varchar', comment: '角色编码' })
+  code: string;
+  @Column({ type: 'json', nullable: true, comment: '权限' })
+  menu: any;
+  @Column({ type: 'varchar', nullable: true, comment: '简介' })
+  brief: string;
+  @Column({ type: 'varchar', default: '0', comment: '是否使用: 0:使用;1:禁用' })
+  is_use: string;
+}

+ 17 - 0
src/entity/system/user.entity.ts

@@ -0,0 +1,17 @@
+import { BaseModel } from '../../frame/BaseModel';
+import { Column, Entity } from 'typeorm';
+@Entity('user', { comment: '用户表' })
+export class User extends BaseModel {
+  @Column({ type: 'varchar', nullable: true, comment: '用户昵称' })
+  nick_name: string;
+  @Column({ type: 'varchar', comment: '账号', unique: true })
+  account: string;
+  @Column({ type: 'varchar', select: false, comment: '密码' })
+  password: string;
+  @Column({ type: 'varchar', comment: '电话' })
+  tel: string;
+  @Column({ type: 'json', nullable: true, comment: '角色id数组' })
+  role: any;
+  @Column({ type: 'varchar', default: '0', comment: '是否使用: 0:使用;1:禁用' })
+  is_use: string;
+}

+ 40 - 0
src/error/Codes.ts

@@ -0,0 +1,40 @@
+export const ErrorCode = {
+  /**位置错误 */
+  UNKNOW: { code: '-1', msg: '未知错误' },
+  /**接口错误 */
+  SERVICE_FAULT: msg => ({ code: '400', msg }),
+  /**接口不开放 */
+  INTERFACE_NOT_OPEN: { code: '-2', msg: '此接口暂不开放' },
+  // 参数错误部分
+  /**数据未找到 */
+  DATA_NOT_FOUND: { code: '4041', msg: '未找到数据' },
+  /**数据未找到-自定义错误信息 */
+  DATA_NOT_FOUND_WITH_MSG: (msg: string) => ({ code: '4041', msg }),
+  /**缺少地址参数 */
+  NEED_PARAMS: { code: '4042', msg: '缺少地址参数' },
+  /**缺少地址参数-自定义错误信息 */
+  NEED_PARAMS_WITH_MSG: (msg: string) => ({ code: '4042', msg }),
+  /**缺少查询参数 */
+  NEED_QUERY: { code: '4043', msg: '缺少查询参数' },
+  /**缺少查询参数-自定义错误信息 */
+  NEED_QUERY_WITH_MSG: (msg: string) => ({ code: '4043', msg }),
+  /**缺少参数体参数 */
+  NEED_BODY: { code: '4044', msg: '缺少参数体参数' },
+  /**缺少参数体参数-自定义错误信息 */
+  NEED_BODY_WITH_MSG: (msg: string) => ({ code: '4044', msg }),
+
+  // 登录相关
+  NOT_LOGIN: { code: '401', msg: '您未登录' },
+  IS_LOGOUT: { code: '4011', msg: '该账号已登出' },
+  OTHER_PLACE_LOGIN: { code: '4012', msg: '该账号已在其他地点登录' },
+  IS_EXPIRE: { code: '4014', msg: '登录已超时,请重新登录' },
+  ROLE_IS_DISABLED: { code: '4015', msg: '当前用户角色已被禁用' },
+  USER_NOT_FOUND: { code: '4016', msg: '未找到用户信息' },
+  USER_IS_DISABLED: { code: '4017', msg: '该用户已被禁用' },
+  BAD_PASSWORD: { code: '4018', msg: '密码错误' },
+
+  /**账户已存在 */
+  ACCOUNT_IS_EXISTS: { code: '4001', msg: '账户已存在' },
+  /**业务错误 */
+  ENTITY_NOT_FOUND: { code: '-100', msg: '未找到实体配置' }
+};

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

@@ -0,0 +1,14 @@
+import { MidwayError } from '@midwayjs/core';
+import { get } from 'lodash';
+
+export class ServiceError extends MidwayError {
+  constructor(error: Object) {
+    const errcode = get(error, 'code');
+    const errmsg = get(error, 'msg');
+    super(errcode);
+    this.errcode = errcode;
+    this.errmsg = errmsg;
+  }
+  errcode: string;
+  errmsg: any;
+}

+ 16 - 0
src/filter/Default.filter.ts

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

+ 26 - 0
src/filter/ServiceError.filter.ts

@@ -0,0 +1,26 @@
+import { Catch, Inject, MidwayEnvironmentService } from '@midwayjs/core';
+import { Context } from '@midwayjs/koa';
+import { ServiceError } from '../error/CustomerError.error';
+import { pick } from 'lodash';
+
+const pickList = ['errmsg', 'errcode'];
+const devList = ['name', 'stack'];
+// 指定那些异常来这里处理
+@Catch([ServiceError])
+export class CustomErrorFilter {
+  @Inject()
+  ctx: Context;
+  @Inject()
+  envService: MidwayEnvironmentService;
+  async catch(err: Error, ctx: Context) {
+    const pl = pickList;
+    const is_dev = this.envService.isDevelopmentEnvironment;
+    if (is_dev) {
+      pl.push(...devList);
+    }
+    const obj = pick(err, pl);
+
+    const result: any = obj;
+    return result;
+  }
+}

+ 21 - 0
src/frame/BaseModel.ts

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

+ 155 - 0
src/frame/BaseService.ts

@@ -0,0 +1,155 @@
+import { App, Inject } from '@midwayjs/core';
+import { Application, Context } from '@midwayjs/koa';
+import { get, isNull, isFinite, isString, isUndefined } from 'lodash';
+import { Opera } from './dbOpera';
+import { completeBuilderCondition } from './conditionBuilder';
+/**
+ * query默认的查询方式(哪个字段 是 = 还是 IN 还是 LIKE)的设置函数为getQueryColumnsOpera,如果有需要重写即可,返回object
+ * {
+ *    column: Opera.xxx
+ * }
+ */
+export abstract class BaseService {
+  @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();
+    completeBuilderCondition(builder, query, operas, this.model);
+    // 分页
+    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 };
+  }
+
+  /**
+   * 单查询,不止用id还可以根据别的条件,默认全等,可以在调用时,进行查询方式设置
+   * @param {object} query 查询条件
+   * @param {object} operas 指定查询方式
+   */
+  async fetch(query: object, operas = {}) {
+    const builder = this.model.createQueryBuilder();
+    completeBuilderCondition(builder, query, operas, this.model);
+    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();
+    completeBuilderCondition(originDataBuilder, query, {}, this.model);
+    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();
+    completeBuilderCondition(originDataBuilder, query, {}, this.model);
+    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);
+  }
+}

+ 21 - 0
src/frame/Meta.ts

@@ -0,0 +1,21 @@
+'use strict';
+/**
+ * 表的meta字段
+ * {
+ *  state:{ type: Number, default: 0 },
+ *  createdAt: timestamps,
+ *  updatedAt: timestamps
+ * }
+ */
+export default schema => {
+  schema.add({
+    meta: {
+      state: { type: Number, default: 0 }, // 数据状态: 0-正常;1-标记删除
+      comment: String,
+    }
+  });
+  schema.set('timestamps', {
+    createdAt: 'meta.createdAt',
+    updatedAt: 'meta.updatedAt',
+  });
+};

+ 44 - 0
src/frame/Options.ts

@@ -0,0 +1,44 @@
+import { Rule, RuleType } from "@midwayjs/validate";
+import { get } from "lodash";
+
+/**分页处理参数 */
+export interface PageOptions {
+  skip?: number;
+  limit?: number;
+  sort?: object;
+  [propName: string]: any;
+}
+/**对查询结果处理参数 */
+export interface ResultOptions {
+  lean?: boolean;
+  populate?: boolean;
+  [propName: string]: any;
+}
+
+export enum LoginType {
+  Admin = 'Admin',
+}
+/**登录后token返回参数 */
+export class LoginVO {
+  constructor(data: object) {
+    this.id = get(data, 'id');
+    this.nick_name = get(data, 'nick_name');
+    this.openid = get(data, 'openid');
+    this.role = get(data, 'role');
+    this.is_super = get(data, 'is_super');
+  }
+  id: string;
+  nick_name: string;
+  openid: string;
+  role: string;
+  is_super: string;
+}
+/**修改密码接收对象 */
+export class UPwdDTO {
+  // @ApiProperty({ description: '用户数据id' })
+  @Rule(RuleType['string']().required())
+  id: string = undefined;
+  // @ApiProperty({ description: '密码' })
+  @Rule(RuleType['string']().required())
+  password: string = undefined;
+}

+ 64 - 0
src/frame/QueryUtils.ts

@@ -0,0 +1,64 @@
+import { get } from 'lodash';
+const signMappings = {
+  [`^[%](.*(?=))[%]$`]: { pos: 1, deal: val => new RegExp(val) },
+  [`^[@]([^=].*)`]: { pos: 1, deal: val => ({ $gt: val }) },
+  [`^([@][=])(.*)`]: { pos: 1, deal: val => ({ $gte: val }) },
+  [`(.*[=])[@]$`]: { pos: 1, deal: val => ({ $lt: val }) },
+  [`(.*)([=][@])$`]: { pos: 2, deal: val => ({ $gte: val }) },
+};
+/**
+ * 整理参数为查询可用
+ * 标识符号:
+ *    ${column}: 全等查询 eq =
+ *    %${column}%: 模糊查询 like 只有全模糊,开头/结尾自己写
+ *    @${column}: 大于查询
+ *    @=${column}: 大于等于
+ *    ${column}@: 小于查询
+ *    ${column}=@: 小于等于
+ * 如果涉及字段映射,函数内自己处理
+ * @param filter 前端传来的参数
+ */
+export const QueryReset = (filter: object) => {
+  const regList = Object.keys(signMappings);
+  const nq = {};
+  for (const key in filter) {
+    const val = filter[key];
+    let mappingOver = false;
+    for (const regStr of regList) {
+      const reg = new RegExp(regStr);
+      const r = reg.test(key);
+      if (!r) continue;
+      const config = signMappings[regStr];
+      const er = reg.exec(key);
+      const pos = get(config, 'pos');
+      const deal = get(config, 'deal');
+      const propKey = get(er, pos);
+      const propValue = deal(val);
+      nq[propKey] = propValue;
+      mappingOver = true;
+      break;
+    }
+    if (!mappingOver) {
+      nq[key] = val;
+    }
+  }
+  return nq;
+};
+/**
+ * 将查询的key根据映射换值
+ * @param filter 查询数据对象
+ * @param mapping 映射对象
+ */
+export const QueryMapping = (filter: object, mapping: object = {}) => {
+  const newFilter = {};
+  for (const key in filter) {
+    const mkey = get(mapping, key);
+    const val = get(filter, key);
+    if (!mkey) {
+      newFilter[key] = val;
+      continue;
+    }
+    newFilter[mkey] = val;
+  }
+  return newFilter;
+};

+ 7 - 0
src/frame/Utils.ts

@@ -0,0 +1,7 @@
+/**
+ * 生成随机字符串
+ * @param len 位数,默认6位
+ */
+export const randomStr = (len = 6) => {
+  return Math.random().toString(36).slice(-len);
+};

+ 180 - 0
src/frame/conditionBuilder.ts

@@ -0,0 +1,180 @@
+import { get, head, last, isArray } from 'lodash';
+import { Opera } from './dbOpera';
+/**
+ *
+ * @param builder model的createQueryBuilder,只有到最后要查数据的时候才是异步的
+ * @param {object} query 查询条件
+ * @param {object} operas 指定查询方式
+ * @param {any} model 存在的情况,default会调用columnIsString 判断是否是字符串字段的函数或其他任何形式的参数
+ */
+export const completeBuilderCondition = (builder, query = {}, operas = {}, model?) => {
+  // 组织查询条件
+  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 Opera.Between:
+        str = `"${key}" Between :${valueStr}_1 AND :${valueStr}_2`;
+        params = { [`${valueStr}_1`]: head(value), [`${valueStr}_2`]: last(value) };
+        break;
+      case Opera.Not:
+        str = `"${key}" != :${valueStr}`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case Opera.Like:
+        str = `"${key}" Like :${valueStr}`;
+        params = { [`${valueStr}`]: `%${value}%` };
+        break;
+      case Opera.LikeLeft:
+        str = `"${key}" Like :${valueStr}`;
+        params = { [`${valueStr}`]: `%${value}` };
+        break;
+      case Opera.LikeRight:
+        str = `"${key}" Like :${valueStr}`;
+        params = { [`${valueStr}`]: `${value}%` };
+        break;
+      case Opera.ILike:
+        str = `"${key}" Not Like :${valueStr}`;
+        params = { [`${valueStr}`]: `%${value}%` };
+        break;
+      case Opera.ILikeLeft:
+        str = `"${key}" Not Like :${valueStr}`;
+        params = { [`${valueStr}`]: `%${value}` };
+        break;
+      case Opera.ILikeRight:
+        str = `"${key}" Not Like :${valueStr}`;
+        params = { [`${valueStr}`]: `${value}%` };
+        break;
+      case Opera.LessThan:
+        str = `"${key}" < :${valueStr}`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case Opera.LessThanOrEqual:
+        str = `"${key}" <= :${valueStr}`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case Opera.MoreThan:
+        str = `"${key}" > :${valueStr}`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case Opera.MoreThanOrEqual:
+        str = `"${key}" >= :${valueStr}`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case Opera.In:
+        if (!isArray(value)) str = `"${key}" IN (:${valueStr})`;
+        else str = `"${key}" IN (:...${valueStr})`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case Opera.IsNull:
+        str = `"${key}" IS NULL`;
+        break;
+      case Opera.IsNotNull:
+        str = `"${key}" IS NOT NULL`;
+        params = { [`${valueStr}`]: value };
+        break;
+      case 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 Opera.JsonObject:
+        const jokeys = key.split('.');
+        const jorootCol = head(jokeys);
+        const jolastKey = last(jokeys);
+        const 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 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 Opera.Equal:
+        str = `"${key}" = :${valueStr}`;
+        params = { [`${valueStr}`]: value };
+        break;
+      default:
+        let isString = false;
+        if (model) isString = columnIsString(key, model);
+        if (isString) {
+          // 字符串默认使用模糊查询
+          str = `"${key}" Like :${valueStr}`;
+          params = { [`${valueStr}`]: `%${value}%` };
+        } else {
+          str = `"${key}" = :${valueStr}`;
+          params = { [`${valueStr}`]: value };
+        }
+        break;
+    }
+    if (!str) continue;
+    builder[method](str, params);
+  }
+};
+
+const columnIsString = (columnName: string, model?) => {
+  if (!model) return false;
+  const columns = 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',
+}

+ 6 - 0
src/interface.ts

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

+ 87 - 0
src/middleware/CheckToken.middleware.ts

@@ -0,0 +1,87 @@
+import {
+  IMiddleware,
+  Middleware,
+  NextFunction,
+  MidwayWebRouterService,
+  Inject,
+  Config,
+} from '@midwayjs/core';
+import { JwtService } from '@midwayjs/jwt';
+import { Context } from '@midwayjs/koa';
+import { get } from 'lodash';
+import { LoginRecordService } from '../service/frame/LoginRecord.service';
+import { ErrorCode } from '../error/Codes';
+import { ServiceError } from '../error/CustomerError.error';
+
+@Middleware()
+export class CheckTokenMiddleware
+  implements IMiddleware<Context, NextFunction>
+{
+  @Config('useCheckTokenMiddleware')
+  useCheckTokenMiddleware: boolean;
+  @Config('passToken')
+  passTokenList: Array<string>;
+  @Config('tokenKey')
+  tokenKey: string;
+  @Config('koa.globalPrefix')
+  globalPrefix: string;
+  @Inject()
+  webRouterService: MidwayWebRouterService;
+  @Inject()
+  jwtService: JwtService;
+
+  /** 获取tokenKey,检查header中的token是否存在,并解析出来,放到user中 */
+  async analysisToken(ctx: Context) {
+    const token: any = get(ctx.request, `header.${this.tokenKey}`);
+    if (token) {
+      const data = this.jwtService.decodeSync(token);
+      if (data) {
+        ctx.token = token;
+        ctx.user = data;
+      }
+    }
+    return token;
+  }
+  /** 排除全局前缀的路由路径 */
+  async excludePrefixPath(ctx: Context) {
+    // 获取路由
+    const routeInfo = await this.webRouterService.getMatchedRouterInfo(
+      ctx.path,
+      ctx.method
+    );
+    const fullUrl = routeInfo.fullUrl;
+    return fullUrl.replace(this.globalPrefix, '');
+  }
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      let token = null;
+      if (!this.useCheckTokenMiddleware) {
+        await next();
+      } else {
+        // 解析token
+        token = await this.analysisToken(ctx);
+        const path = await this.excludePrefixPath(ctx);
+        // 检查路由是否在 无需检查token列表中
+        if (this.passTokenList && this.passTokenList.includes(path)) {
+          // 在跳过token检查中,不进行处理
+          await next();
+        } else {
+          // 检查token:是否可用
+          if (!token) {
+            throw new ServiceError(ErrorCode.NOT_LOGIN);
+          }
+          // 需要到LoginRecord表中检查:检查过期时间,token值是否一致
+          const loginRecordService = await ctx.requestContext.getAsync(
+            LoginRecordService
+          );
+          // 检查,不通过会报异常
+          await loginRecordService.check(token);
+          // 续期
+          await loginRecordService.renewal(token);
+          // 进入后续程序
+          await next();
+        }
+      }
+    };
+  }
+}

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

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

+ 15 - 0
src/response/CustomerResponse.ts

@@ -0,0 +1,15 @@
+export class RF {
+  static success(data?: any) {
+    return {
+      errcode: '0',
+      errmsg: 'ok',
+      data,
+    };
+  }
+  static error(e?: string) {
+    return {
+      errcode: '400',
+      errmsg: e || '服务发生错误',
+    };
+  }
+}

+ 238 - 0
src/service/frame/File.service.ts

@@ -0,0 +1,238 @@
+import { existsSync, lstatSync, mkdirSync, readdirSync } from 'fs';
+import { Context } from '@midwayjs/koa';
+import { dirname, extname, join, sep } from 'path';
+import { moveSync, removeSync } from 'fs-extra';
+import { Provide, Inject, Config } from '@midwayjs/core';
+
+/**
+ * 文件上传相关服务
+ */
+@Provide()
+export class FileService {
+  @Inject()
+  ctx: Context;
+  @Config('busboy')
+  uploadConfig;
+  @Config('koa.globalPrefix')
+  routePrefix: string;
+  @Config('dbName')
+  dbName: string;
+
+  /**临时文件表名 */
+  tmpModelName = 'fileTmp';
+  /**临时文件表model */
+  tmpModel: any;
+
+  // #region 递归清理未被使用的上传文件
+  /**
+   * 删除不在文件使用表登记的文件
+   * 1.创建临时表,字段为文件地址;
+   * 2.获取config中配置的${表名}.${字段名}.找到所有文件对象,并把uri拿出来存在临时表中
+   * 3.获取存放根目录地址,递归检索目录下的文件,并查询下文件在临时表中是否存在:
+   *  若存在,则文件保留,跳过处理;
+   *  若不存在,则删除文件.文件可能被替换
+   * 4.并且,在处理文件过程中,禁用上传功能
+   */
+  async deleteNoUseFile() {
+    // const realDir = this.uploadConfig.realdir;
+    // // 制作临时表及当前使用中的文件地址复制
+    // await this.createTmpModel();
+    // // 递归处理所有文件
+    // this.recursionFindFile(realDir);
+    // // 删除临时表
+    // await this.deleteTmpModel();
+  }
+  /**创建临时文件表 */
+  // async createTmpModel() {
+  //   const connect = mongoose.connections.find(
+  //     f => get(f, 'name') === this.dbName
+  //   );
+  //   const schema = new mongoose.Schema({
+  //     uri: String,
+  //   });
+  //   this.tmpModel = connect.model(this.tmpModelName, schema, this.tmpModelName);
+  //   const columns = get(this.uploadConfig, 'columns', []);
+  //   // 处理图片字段,将uri复制到临时表里
+  //   for (const c of columns) {
+  //     const arr = c.split('.');
+  //     const modelName: string = head(arr);
+  //     const columnName: string = last(arr);
+  //     const models = connect.models;
+  //     // 获取model
+  //     const model = get(models, modelName);
+  //     if (!model) continue;
+  //     // 查询数据
+  //     const clist = await model
+  //       .find(
+  //         {
+  //           [columnName]: { $exists: true },
+  //         },
+  //         `${columnName}`
+  //       )
+  //       .lean();
+  //     // 处理数据
+  //     let l = clist.map(i => {
+  //       const icon = get(i, 'icon', []);
+  //       const l = icon.map(ic => get(ic, 'uri'));
+  //       return l;
+  //     });
+  //     l = flattenDeep(l);
+  //     l = l.map(i => ({ uri: i }));
+  //     // 保存到临时表中
+  //     await this.tmpModel.insertMany(l);
+  //   }
+  // }
+
+  // async hasTmpModel() {
+  //   const connect = mongoose.connections.find(
+  //     f => get(f, 'name') === this.dbName
+  //   );
+  //   const models = connect.models;
+  //   const model = get(models, this.tmpModelName);
+  //   return model;
+  // }
+
+  // /**删除临时文件表 */
+  // async deleteTmpModel() {
+  //   const connect = mongoose.connections.find(
+  //     f => get(f, 'name') === this.dbName
+  //   );
+  //   await connect.dropCollection(this.tmpModelName);
+  // }
+  /**
+   * 递归找文件
+   * @param basePath 基础路径
+   * @param list 基础路径下的所有内容
+   */
+  async recursionFindFile(basePath) {
+    const dirExists = existsSync(basePath);
+    if (!dirExists) return;
+    const list = readdirSync(basePath);
+    for (const f of list) {
+      const thisPath = join(basePath, f);
+      // 文件夹就继续递归找
+      if (this.isDir(thisPath)) this.recursionFindFile(thisPath);
+      else if (this.isFile(thisPath)) {
+        // 文件,需要到表里找是否存在,存在保留,不存在就删除
+        const shortPath = this.realPathTurnToShortPath(thisPath);
+        const count = await this.tmpModel.count({ uri: shortPath });
+        if (count <= 0) this.toUnlink(thisPath);
+      }
+    }
+  }
+  /**
+   * 真实路径=>短地址
+   * @param realPath 文件真实路径
+   * @returns string 短地址
+   */
+  realPathTurnToShortPath(realPath: string) {
+    const realDir = this.uploadConfig.realdir;
+    let shortPath = realPath.replace(realDir, this.getFilesPartsPath());
+    while (shortPath.includes('\\')) {
+      shortPath = shortPath.replace('\\', '/');
+    }
+    return shortPath;
+  }
+
+  /**
+   * 删除文件
+   * @param path 文件路径
+   */
+  toUnlink(path) {
+    removeSync(path);
+  }
+
+  /**
+   * 判断路径是否存在
+   * @param path 路径
+   * @returns boolean: true-存在/false-不存在
+   */
+  isExists(path) {
+    return existsSync(path);
+  }
+  /**
+   * 判断是否是文件夹
+   * @param path 路径
+   * @returns boolean: true-是文件夹
+   */
+  isDir(path) {
+    const f = lstatSync(path);
+    return f.isDirectory();
+  }
+  /**
+   * 判断是否是文件
+   * @param path 路径
+   * @returns boolean: true-是文件
+   */
+  isFile(path) {
+    const f = lstatSync(path);
+    return f.isFile();
+  }
+  // #endregion
+
+  // #region 上传部分
+  getFilesPartsPath() {
+    return `${this.routePrefix}/files`;
+  }
+  /**
+   * 获取文件短路径
+   * @param dirs 文件存储路径
+   * @param filename 文件名
+   */
+  getFileShortPath(dirs: Array<string>, filename: string) {
+    return `${this.getFilesPartsPath()}/${dirs.join('/')}/${filename}`;
+  }
+
+  /**文件真实短地址 */
+  getFileShortRealPath() {
+    let originalUrl = this.ctx.request.originalUrl;
+    originalUrl = decodeURI(originalUrl);
+    //先去掉请求前缀
+    originalUrl = originalUrl.replace(this.getFilesPartsPath(), '');
+    const arr = originalUrl.split('/');
+    // 首行为空去掉
+    arr.splice(0, 1);
+    // 最后一个数据为文件,其余的为路径拼成一起就行
+    return arr.join(sep);
+  }
+  /**
+   * 移动文件
+   * @param tempPath 临时上传文件位置
+   * @param path 实际文件应处位置
+   */
+  moveFile(tempPath: string, path: string) {
+    moveSync(tempPath, path);
+  }
+
+  // 创建文件夹
+  mkdir(dn: string) {
+    if (existsSync(dn)) {
+      return true;
+    }
+    if (this.mkdir(dirname(dn))) {
+      mkdirSync(dn);
+      return true;
+    }
+  }
+  /**获取文件名后缀 */
+  getExt(name: string) {
+    return extname(name);
+  }
+  /**获取年月日时分秒, 格式: 年月日时分秒 */
+  getNowDateTime() {
+    const date = new Date();
+    const y = date.getFullYear();
+    const m = date.getMonth() + 1;
+    const mstr = m < 10 ? '0' + m : m;
+    const d = date.getDate();
+    const dstr = d < 10 ? '0' + d : d;
+    const h = date.getHours();
+    const hstr = h < 10 ? '0' + h : h;
+    const minute = date.getMinutes();
+    const minutestr = minute < 10 ? '0' + minute : minute;
+    const second = date.getSeconds();
+    const secondstr = second < 10 ? '0' + second : second;
+    return `${y}${mstr}${dstr}${hstr}${minutestr}${secondstr}`;
+  }
+  // #endregion
+}

+ 241 - 0
src/service/frame/Init.service.ts

@@ -0,0 +1,241 @@
+import { Autoload, Init, Scope, ScopeEnum } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { get } from 'lodash';
+import * as bcrypt from 'bcryptjs';
+import { Config } from '../../entity/system/config.entity';
+import { DictData } from '../../entity/system/dictData.entity';
+import { DictType } from '../../entity/system/dictType.entity';
+import { Menus } from '../../entity/system/menus.entity';
+import { Role } from '../../entity/system/role.entity';
+import { Admin } from '../../entity/system/admin.entity';
+@Autoload()
+@Scope(ScopeEnum.Singleton)
+export class InitService {
+  @InjectEntityModel(Admin)
+  adminModel: Repository<Admin>;
+  @InjectEntityModel(Role)
+  roleModel: Repository<Role>;
+  @InjectEntityModel(Menus)
+  menusModel: Repository<Menus>;
+  @InjectEntityModel(DictType)
+  dictTypeModel: Repository<DictType>;
+  @InjectEntityModel(DictData)
+  dictDataModel: Repository<DictData>;
+  @InjectEntityModel(Config)
+  configModel: Repository<Config>;
+
+
+  @Init()
+  async init() {
+    this.adminUser();
+    this.initRole();
+    this.initMenus();
+    this.initDict();
+    this.initConfig();
+  }
+  async initConfig() {
+    const data: any = { logo: [], name: '平台名' };
+    const num = await this.configModel.count();
+    if (num > 0) return;
+    await this.configModel.insert(data);
+  }
+
+  async adminUser() {
+    const salt = bcrypt.genSaltSync(10);
+    const hash = bcrypt.hashSync('1qaz2wsx', salt);
+    const data: any = {
+      account: 'sadmin',
+      password: hash,
+      nick_name: '系统管理员',
+      is_super: '0',
+    };
+    
+    const query: any = { is_super: '0' }
+    const is_exist = await this.adminModel.count({ where: query });
+    if (!is_exist) return await this.adminModel.insert(data);
+    return;
+  }
+
+  async initRole() {
+    const num = await this.roleModel.count();
+    if (num > 0) return;
+    const datas: any = [{ name: '管理员', code: 'admin' }];
+    await this.roleModel.insert(datas);
+  }
+  async initMenus() {
+    const num = await this.menusModel.count();
+    if (num > 0) return;
+    const datas: any = [
+      {
+        name: '首页',
+        order_num: 1,
+        path: '/',
+        component: '/home/index',
+        type: '1',
+        route_name: 'home',
+      },
+    ];
+    await this.menusModel.insert(datas);
+    // 系统设置
+    const sd = {
+      name: '系统设置',
+      path: '/system',
+      order_num: 2,
+      type: '0',
+      route_name: 'system',
+      is_default: '0',
+    };
+    const rsd = await this.menusModel.insert(sd);
+    const smId = get(rsd, 'identifiers.0.id')
+    const systemMenus = [
+      {
+        name: '平台设置',
+        parent_id: smId,
+        order_num: 1,
+        path: '/system/config',
+        component: '/system/config/index',
+        type: '1',
+        route_name: 'system_config',
+        is_default: '0',
+      },
+      {
+        name: '目录设置',
+        parent_id: smId,
+        order_num: 1,
+        path: '/system/menus',
+        component: '/system/menus/index',
+        type: '1',
+        route_name: 'system_menus',
+        config: [
+          {
+            zh: '添加',
+            code: 'add',
+          },
+          {
+            zh: '修改',
+            code: 'update',
+          },
+          {
+            zh: '添加下一级',
+            code: 'addNext',
+          },
+          {
+            zh: '删除',
+            code: 'delete',
+          },
+        ],
+        is_default: '0',
+      },
+      {
+        name: '角色管理',
+        parent_id: smId,
+        order_num: 2,
+        path: '/system/role',
+        component: '/system/role/index',
+        type: '1',
+        route_name: 'system_role',
+        is_default: '0',
+        config: [
+          {
+            zh: '添加',
+            code: 'add',
+          },
+          {
+            zh: '修改',
+            code: 'update',
+          },
+          {
+            zh: '使用',
+            code: 'toAbled',
+          },
+          {
+            zh: '禁用',
+            code: 'toDisabled',
+          },
+          {
+            zh: '删除',
+            code: 'delete',
+          },
+        ],
+      },
+      {
+        name: '字典管理',
+        parent_id: smId,
+        order_num: 3,
+        path: '/system/dict',
+        component: '/system/dict/index',
+        type: '1',
+        route_name: 'system_dict',
+        is_default: '0',
+      },
+      {
+        name: '字典数据',
+        parent_id: smId,
+        order_num: 4,
+        path: '/system/dictData',
+        component: '/system/dictData/index',
+        type: '2',
+        route_name: 'system_dict_data',
+        is_default: '0',
+      },
+    ];
+    await this.menusModel.insert(systemMenus);
+
+    // 用户管理
+    const ud = {
+      name: '用户管理',
+      order_num: 3,
+      path: '/user',
+      type: '0',
+      route_name: 'user',
+      is_default: '1',
+    };
+    const rud = await this.menusModel.insert(ud);
+    const umId = get(rud, 'identifiers.0.id')
+    const userMenus = [
+      {
+        name: '管理员用户',
+        parent_id: umId.toString(),
+        order_num: 1,
+        path: '/user/admin',
+        component: '/user/admin/index',
+        type: '1',
+        route_name: 'user_admin',
+      },
+      {
+        name: '平台用户',
+        parent_id: umId.toString(),
+        order_num: 2,
+        path: '/user/user',
+        component: '/user/user/index',
+        type: '1',
+        route_name: 'user_user',
+      },
+    ];
+    await this.menusModel.insert(userMenus);
+
+    const password = {
+      name: '账号管理',
+      order_num: 999,
+      path: '/acccount',
+      component: '/acccount/index',
+      type: '1',
+      route_name: 'acccount',
+    };
+    await this.menusModel.insert(password);
+  }
+  async initRoleMenu(admin: Admin) { }
+
+  async initDict() {
+    const num = await this.dictTypeModel.count();
+    if (num > 0) return;
+    const isUseType: any = [{ title: '是否使用', code: 'isUse', is_use: '0' }];
+    const isUseData: any = [
+      { code: 'isUse', label: '使用', value: '0', sort: 1, is_use: '0' },
+      { code: 'isUse', label: '禁用', value: '1', sort: 2, is_use: '0' },
+    ];
+    await this.dictTypeModel.insert(isUseType);
+    await this.dictDataModel.insert(isUseData);
+  }
+}

+ 90 - 0
src/service/frame/Login.service.ts

@@ -0,0 +1,90 @@
+import { Inject, Provide } from '@midwayjs/core';
+import { LoginType, UPwdDTO } from '../../frame/Options';
+import { get } from 'lodash';
+import { ServiceError } from '../../error/CustomerError.error';
+import { ErrorCode } from '../../error/Codes';
+import { RoleService } from '../system/role.service';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Equal, Repository } from 'typeorm';
+import * as bcrypt from 'bcryptjs';
+import { Admin } from '../../entity/system/admin.entity';
+import { User } from '../../entity/system/user.entity';
+
+@Provide()
+export class LoginService {
+  @Inject()
+  roleService: RoleService;
+  @InjectEntityModel(Admin)
+  adminModel: Repository<Admin>;
+  @InjectEntityModel(User)
+  userModel: Repository<User>;
+  /**
+   * 账密登录
+   * @param data 用户名和密码
+   * @param type 用户类型
+   * @returns 用户信息/空值
+   */
+  async loginByAccount(data, type: LoginType) {
+    let model;
+    if (type === LoginType.Admin) model = this.adminModel;
+    else model = this.userModel;
+    const user = await model.createQueryBuilder('t').where('t.account = :account', { account: data.account }).addSelect('t.password').getOne();
+    if (!user) throw new ServiceError(ErrorCode.USER_NOT_FOUND);
+    await this.checkAccountCanLogin(user, type);
+    const result = bcrypt.compareSync(data.password, user.password);
+    if (!result) throw new ServiceError(ErrorCode.BAD_PASSWORD);
+    return user;
+  }
+
+  /**
+   * 检查用户是否可以登录(从用户本身和角色检查)
+   * @param user 用户信息
+   * @param type 用户类型
+   */
+  async checkAccountCanLogin(user, type: LoginType) {
+    /**
+     * 判断是否可以使用:
+     * 管理员:
+     *  超级管理员无视,直接往下;
+     *  普通管理员需要查看
+     *    角色是否能用
+     *    is_use是不是'0'
+     * 其他用户;
+     *  角色是否能用
+     *  is_use是不是'0'
+     * 普通管理员和其他用户是一种判断
+     */
+
+    if (type === 'Admin') {
+      if (get(user, 'is_super') === '1') {
+        if (get(user, 'is_use') === '1') throw new ServiceError(ErrorCode.USER_IS_DISABLED);
+      }
+    } else {
+      if (get(user, 'status') !== '1') throw new ServiceError(ErrorCode.USER_IS_DISABLED);
+      const qb = { code: type, is_use: '0' };
+      const role = await this.roleService.fetch(qb);
+      if (!role) throw new ServiceError(ErrorCode.ROLE_IS_DISABLED);
+    }
+  }
+
+  /**
+   * 修改密码
+   * @param data 修改密码对象
+   * @param type 规定的登录类型
+   * @returns 新密码
+   */
+  async updatePwd(data: UPwdDTO, type: LoginType) {
+    let model;
+    if (type === LoginType.Admin) model = this.adminModel;
+    else model = this.userModel;
+    const user = await model.findOne({ where: { id: Equal(get(data, 'id')) } });
+    if (!user) new ServiceError(ErrorCode.USER_NOT_FOUND);
+    // 处理密码
+    let password = get(data, 'password');
+    if (password) {
+      const salt = bcrypt.genSaltSync(10);
+      password = bcrypt.hashSync(password, salt);
+    }
+    await model.update({ id: get(data, 'id') }, { password });
+  }
+}

+ 96 - 0
src/service/frame/LoginRecord.service.ts

@@ -0,0 +1,96 @@
+import { Config, Inject, Provide } from '@midwayjs/core';
+import { Context } from '@midwayjs/koa';
+import { JwtService } from '@midwayjs/jwt';
+import { get } from 'lodash';
+import * as dayjs from 'dayjs';
+import { ServiceError } from '../../error/CustomerError.error';
+import { ErrorCode } from '../../error/Codes';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { LoginRecord } from '../../entity/frame/loginRecord.entity';
+
+
+@Provide()
+export class LoginRecordService {
+  @InjectEntityModel(LoginRecord)
+  model: Repository<LoginRecord>;
+  @Inject()
+  jwtService: JwtService;
+  @Config('jwt.expiresIn')
+  expire_step: number;
+  @Inject()
+  ctx: Context;
+
+  timeFormat = 'YYYY-MM-DD HH:mm:ss';
+  /**创建登录记录 */
+  async create(token: string) {
+    const userInfo = this.jwtService.decode(token);
+    if (!userInfo) return;
+    const user_id = get(userInfo, 'id', get(userInfo, '_id'));
+    if (!user_id) return;
+    const last_time = dayjs().format(this.timeFormat);
+    const expire_time = dayjs()
+      .add(this.expire_step, 's')
+      .format(this.timeFormat);
+    const req = this.ctx.request;
+    const last_ip = get(req, 'header.x-forwarded-for', req.ip);
+    const data: any = { token, last_time, expire_time, last_ip };
+    const num = await this.model.count({ where: { user_id } });
+    if (num > 0) {
+      // 有过登录记录,那就直接更新token,last_time,expire_time,last_ip就行
+      const q1: any = { user_id }
+      await this.model.update(q1, data);
+    } else {
+      // 没有记录,那就创建
+      data['user_id'] = user_id;
+      await this.model.insert(data);
+    }
+  }
+  /**更新过期时间 */
+  async renewal(token) {
+    const userInfo = this.jwtService.decode(token);
+    if (!userInfo) return;
+    const user_id = get(userInfo, 'id', get(userInfo, '_id'));
+    if (!user_id) return;
+    const data = await this.model.findOne({ where: { user_id } });
+    if (!data) throw new ServiceError(ErrorCode.NOT_LOGIN);
+    const req = this.ctx.request;
+    const last_ip = get(req, 'header.x-forwarded-for', req.ip);
+    const last_time = dayjs().format(this.timeFormat);
+    const expire_time = dayjs()
+      .add(this.expire_step, 's')
+      .format(this.timeFormat);
+    const q1: any = { user_id }
+    const newData: any = { last_ip, last_time, expire_time }
+    await this.model.update(
+      q1,
+      newData
+    );
+  }
+  /**检查是否过期 */
+  async check(token) {
+    const userInfo = this.jwtService.decode(token);
+    if (!userInfo) return;
+    const user_id = get(userInfo, 'id', get(userInfo, '_id'));
+    if (!user_id) return;
+    const q1: any = { user_id }
+    const data = await this.model.findOne({ where: q1 });
+    if (!data) throw new ServiceError(ErrorCode.NOT_LOGIN);
+    const utoken = get(data, 'token');
+    if (utoken === null) {
+      // 抛出异常:用户已注销
+      throw new ServiceError(ErrorCode.IS_LOGOUT);
+    }
+    if (utoken !== token) {
+      // 抛出异常:用户已在其他地点登录
+      throw new ServiceError(ErrorCode.OTHER_PLACE_LOGIN);
+    }
+    const expire_time = get(data, 'expire_time');
+    const isBefore = dayjs().isBefore(expire_time);
+    if (!isBefore) {
+      // 抛出异常:登录已过期
+      throw new ServiceError(ErrorCode.IS_EXPIRE);
+    }
+    return true;
+  }
+}

+ 67 - 0
src/service/frame/common.service.ts

@@ -0,0 +1,67 @@
+import { Provide } from "@midwayjs/core";
+import { InjectDataSource } from "@midwayjs/typeorm";
+import { get, lowerCase, replace } from "lodash";
+import { DataSource, ObjectLiteral, Repository } from "typeorm";
+import { ServiceError } from "../../error/CustomerError.error";
+import { ErrorCode } from "../../error/Codes";
+
+@Provide()
+export class CommonService {
+  @InjectDataSource('default')
+  defaultDataSource: DataSource;
+  /**
+   * 查询列表
+   * @param model 表实体
+   * @param query 查询条件
+   * @param page 分页设置
+   * @param others 其他设置
+   * @returns data 列表数据
+   * @returns total 总数
+   */
+  async search(model: Repository<ObjectLiteral>, query: object = {}, page: object = {}, others: object = {}) {
+    const searchObject: any = {}
+    const countObject: any = {}
+    if (Object.keys(query).length > 0) {
+      searchObject.where = query
+      countObject.where = query
+    }
+    if (Object.keys(page).length > 0) {
+      const skip = get(page, 'skip', 0)
+      if (skip >= 0) searchObject.skip = skip
+      const take = get(page, 'limit', 0)
+      if (take > 0) searchObject.take = take
+    }
+    if (Object.keys(others).length > 0) {
+      for (const key in others) {
+        searchObject[key] = others[key]
+      }
+    }
+    const data = await model.find(searchObject)
+    const total = await model.count(countObject)
+    return { data, total };
+  }
+  /**
+   * 单查询
+   * @param model 表实体
+   * @param query 查询条件
+   * @returns data 数据
+   */
+  async getOne(model: Repository<ObjectLiteral>, query: object) {
+    const data = await model.findOne({ where: query });
+    return data;
+  }
+
+  /**
+   * 根据表名查询表实例
+   * @param tableName 表名
+   * @returns model 表的实例
+   */
+  getModel(tableName: string): Repository<ObjectLiteral> {
+    const entitys = this.defaultDataSource.entityMetadatas;
+    const entity = entitys.find(f => replace(lowerCase(f.name), " ", '') === replace(lowerCase(tableName), " ", ''))
+    if (!entity) throw new ServiceError(ErrorCode.ENTITY_NOT_FOUND)
+    const entityClass = entity.target;
+    const model = this.defaultDataSource.getRepository(entityClass)
+    return model
+  }
+}

+ 28 - 0
src/service/pageView.service.ts

@@ -0,0 +1,28 @@
+import { Provide } from "@midwayjs/core";
+import { InjectEntityModel } from "@midwayjs/typeorm";
+import { Repository } from "typeorm";
+import { PageView } from "../entity/pageView.entity";
+import { get } from "lodash";
+
+@Provide()
+export class PageViewService {
+  @InjectEntityModel(PageView)
+  model: Repository<PageView>;
+  /**查询浏览量,直接把当前的添加上 */
+  async fetch(content_id, getNum = false) {
+    const data = await this.model.createQueryBuilder().where(`content_id=:content_id`, { content_id }).getOne()
+    let num = 1;
+    if (!data) {
+      const body = { content_id }
+      await this.model.save(body);
+    } else {
+      const id = get(data, 'id')
+      num = get(data, 'num', 1);
+      if (!getNum) {
+        num = num + 1
+        await this.model.update({ id }, { num })
+      }
+    }
+    return num
+  }
+}

+ 83 - 0
src/service/question.service.ts

@@ -0,0 +1,83 @@
+import { Provide } from "@midwayjs/core";
+import { InjectEntityModel } from "@midwayjs/typeorm";
+import { Repository } from "typeorm";
+import { Question } from "../entity/question.entity";
+import { get, isNull, isString, isUndefined } from "lodash";
+
+@Provide()
+export class QuestionService {
+  @InjectEntityModel(Question)
+  model: Repository<Question>;
+
+  async query(query: object = {}, meta: any = {}) {
+    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 (query) builder.where(query);
+    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 = { question_id: 'DESC' };
+    // 分页
+    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);
+    const data = await builder.getMany();
+    const total = await builder.getCount();
+    return { data, total };
+  }
+
+  async create(data: object) {
+    const result = await this.model.insert(data);
+    const id = get(result, 'identifiers.0.id');
+    // 没有id估计是出错了
+    if (!id) return;
+    const createData = await this.fetch({ id });
+    return createData;
+  }
+
+  async fetch(query: object) {
+    const builder = this.model.createQueryBuilder();
+    builder.where(query)
+    const result = await builder.getOne();
+    return result;
+  }
+
+  /**修改,单修改/多修改是统一修改为 */
+  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;
+    }
+    await this.model.update(query, updateData);
+  }
+
+}

+ 31 - 0
src/service/system/admin.service.ts

@@ -0,0 +1,31 @@
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { get } from 'lodash';
+import { ServiceError } from '../../error/CustomerError.error';
+import { ErrorCode } from '../../error/Codes';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { Admin } from '../../entity/system/admin.entity';
+@Provide()
+export class AdminService extends BaseService {
+  @InjectEntityModel(Admin)
+  model: Repository<Admin>;
+  /**查重 */
+  async checkInDB(data: object) {
+    // account查重,虽然数据库有unique约束,但是尽量不触发,因为异常捕获不好解析
+    const account = get(data, 'account');
+    const num = await this.model.count({ where: { account } });
+    if (num > 0) throw new ServiceError(ErrorCode.ACCOUNT_IS_EXISTS);
+  }
+  async initSuperAdmin() {
+    const data = {
+      account: 'admin',
+      password: '1qaz2wsx',
+      nick_name: '系统管理员',
+      is_super: '0',
+    };
+    const is_exist = await this.model.count({ where: { is_super: '0' } });
+    if (!is_exist) return await this.model.insert(data);
+    return;
+  }
+}

+ 20 - 0
src/service/system/config.service.ts

@@ -0,0 +1,20 @@
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { Config } from '../../entity/system/config.entity';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+
+@Provide()
+export class ConfigService extends BaseService {
+  @InjectEntityModel(Config)
+  model: Repository<Config>;
+  // 只允许有一个config
+  async getConfig() {
+    let data = await this.fetch({});
+    if (!data) {
+      await this.model.insert({ name: 'midwayjs+vue', logo: [] });
+      data = await this.fetch({});
+    }
+    return data;
+  }
+}

+ 11 - 0
src/service/system/dictData.service.ts

@@ -0,0 +1,11 @@
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { DictData } from '../../entity/system/dictData.entity';
+
+@Provide()
+export class DictDataService extends BaseService {
+  @InjectEntityModel(DictData,'v2')
+  model: Repository<DictData>;
+}

+ 11 - 0
src/service/system/dictType.service.ts

@@ -0,0 +1,11 @@
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { DictType } from '../../entity/system/dictType.entity';
+
+@Provide()
+export class DictTypeService extends BaseService {
+  @InjectEntityModel(DictType,'v2')
+  model: Repository<DictType>;
+}

+ 36 - 0
src/service/system/menus.service.ts

@@ -0,0 +1,36 @@
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Repository } from 'typeorm';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { Menus } from '../../entity/system/menus.entity';
+import { orderBy } from 'lodash';
+
+@Provide()
+export class MenusService extends BaseService {
+  @InjectEntityModel(Menus)
+  model: Repository<Menus>;
+
+  async queryMenu(query = {}): Promise<Array<object>> {
+    const { data } = await this.query(query, { order: { order_num: 'ASC' } });
+    let treeMenu = data.filter(f => !f.parent_id);
+    treeMenu = this.treeMenu(data, treeMenu);
+    return treeMenu;
+  }
+
+  treeMenu(allMenus, nowMenu) {
+    for (const nm of nowMenu) {
+      const { id, parent_id } = nm;
+      // 查下下级其是否有目录
+      let children = allMenus.filter(f => f.parent_id === id);
+      children = this.treeMenu(allMenus, children);
+      if (children.length > 0) nm.children = children;
+      // 换父级组件的名称
+      if (parent_id) {
+        const r = allMenus.find(f => f.id === parent_id);
+        if (r) nm.parent_name = r.name;
+      }
+    }
+    nowMenu = orderBy(nowMenu, ['order_num'], ['asc']);
+    return nowMenu;
+  }
+}

+ 68 - 0
src/service/system/role.service.ts

@@ -0,0 +1,68 @@
+import { BaseService } from '../../frame/BaseService';
+import { Inject, Provide } from '@midwayjs/core';
+import { lowerFirst, upperFirst, last, flattenDeep, uniq, get } from 'lodash';
+import { ServiceError } from '../../error/CustomerError.error';
+import { MenusService } from './menus.service';
+import { ErrorCode } from '../../error/Codes';
+import { Repository, In } from 'typeorm';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { Menus } from '../../entity/system/menus.entity';
+import { Role } from '../../entity/system/role.entity';
+@Provide()
+export class RoleService extends BaseService {
+  @InjectEntityModel(Role)
+  model: Repository<Role>;
+  @InjectEntityModel(Menus)
+  menusModel: Repository<Menus>;
+  @Inject()
+  menusService: MenusService;
+
+  //是否是超级管理员
+  isSuperAdmin() {
+    const user = this.ctx.user;
+    if (user.role === 'Admin' && user.is_super === '0') return true;
+  }
+
+  async getUserMenus(needCode = false) {
+    const user = this.ctx.user;
+    // 这里需要改下. 平台的超级管理员有这些权限
+    if (this.isSuperAdmin()) {
+      const menus = await this.menusService.queryMenu({ is_use: 0 });
+      return { menus };
+    }
+    const roleCode = [lowerFirst(user.role), upperFirst(user.role)];
+    const q1: any = { code: roleCode, is_use: '0' }
+    const role = await this.model.findOne({ where: q1 })
+    if (!role) throw new ServiceError(ErrorCode.ROLE_IS_DISABLED);
+    const roleMenu = get(role, 'menu', []);
+    const menu = roleMenu.map(i => {
+      const arr = i.split('.');
+      return last(arr);
+    });
+    if (needCode) return roleMenu;
+    const q2: any = { route_name: In(menu) }
+    const menuList = await this.menusModel.find({ where: q2 });
+    let treeMenu = menuList.filter(f => !f.parent_id);
+    treeMenu = this.menusService.treeMenu(menuList, treeMenu);
+    return { menus: treeMenu, role_code: roleMenu };
+  }
+
+  /**
+   * 根据角色id数组,整理菜单
+   * @param roleIdList 角色id数组
+   */
+  async getMenuByRoles(roleIdList) {
+    let menus = [];
+    // 第一步,整理所有菜单至一维数组
+    for (const r of roleIdList) {
+      menus.push(...flattenDeep(r));
+    }
+    menus = uniq(menus);
+    const q: any = { id: In(menus), is_use: '0' }
+    const allMenu = await this.menusModel
+      .find({ where: q, order: { order_num: 'ASC' } })
+    let treeMenu = allMenu.filter(f => !f.parent_id);
+    treeMenu = this.menusService.treeMenu(allMenu, treeMenu);
+    return treeMenu;
+  }
+}

+ 21 - 0
src/service/system/user.service.ts

@@ -0,0 +1,21 @@
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { get } from 'lodash';
+import { ErrorCode } from '../../error/Codes';
+import { ServiceError } from '../../error/CustomerError.error';
+import { Repository } from 'typeorm';
+import { InjectEntityModel } from '@midwayjs/typeorm';
+import { User } from '../../entity/system/user.entity';
+
+@Provide()
+export class UserService extends BaseService {
+  @InjectEntityModel(User)
+  model: Repository<User>;
+  /**查重 */
+  async checkInDB(data: object) {
+    // account查重,虽然数据库有unique约束,但是尽量不触发,因为异常捕获不好解析
+    const account = get(data, 'account');
+    const num = await this.model.count({ where: { account } });
+    if (num > 0) throw new ServiceError(ErrorCode.ACCOUNT_IS_EXISTS);
+  }
+}

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

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

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

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

+ 28 - 0
tsconfig.json

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

binární
upload/logo/20250226165535.png


binární
upload/logo/20250226165552.png


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 2189 - 0
water_service.sql