lrf 5 kuukautta sitten
commit
8fbc8ba92b
64 muutettua tiedostoa jossa 11376 lisäystä ja 0 poistoa
  1. 11 0
      .editorconfig
  2. 17 0
      .eslintrc.json
  3. 14 0
      .gitignore
  4. 3 0
      .prettierrc.js
  5. 65 0
      README-Frame.md
  6. 29 0
      README.md
  7. 2 0
      bootstrap.js
  8. 19 0
      ecosystem.config.js
  9. 6 0
      jest.config.js
  10. 8870 0
      package-lock.json
  11. 54 0
      package.json
  12. 32 0
      src/config/config.default.ts
  13. 57 0
      src/config/config.local.ts
  14. 7 0
      src/config/config.unittest.ts
  15. 44 0
      src/configuration.ts
  16. 90 0
      src/controller/frame/File.controller.ts
  17. 22 0
      src/controller/frame/Init.controller.ts
  18. 59 0
      src/controller/frame/Login.controller.ts
  19. 29 0
      src/controller/frame/Token.controller.ts
  20. 9 0
      src/controller/home.controller.ts
  21. 51 0
      src/controller/system/admin.controller.ts
  22. 46 0
      src/controller/system/config.controller.ts
  23. 48 0
      src/controller/system/dictData.controller.ts
  24. 48 0
      src/controller/system/dictType.controller.ts
  25. 47 0
      src/controller/system/menus.controller.ts
  26. 48 0
      src/controller/system/role.controller.ts
  27. 51 0
      src/controller/system/user.controller.ts
  28. 24 0
      src/decorator/page.decorator.ts
  29. 18 0
      src/entity/frame/loginRecord.entity.ts
  30. 57 0
      src/entity/system/admin.entity.ts
  31. 11 0
      src/entity/system/config.entity.ts
  32. 29 0
      src/entity/system/dictData.entity.ts
  33. 22 0
      src/entity/system/dictType.entity.ts
  34. 31 0
      src/entity/system/menus.entity.ts
  35. 17 0
      src/entity/system/role.entity.ts
  36. 32 0
      src/entity/system/user.entity.ts
  37. 38 0
      src/error/Codes.ts
  38. 14 0
      src/error/CustomerError.error.ts
  39. 13 0
      src/filter/Default.filter.ts
  40. 26 0
      src/filter/ServiceError.filter.ts
  41. 8 0
      src/frame/BaseModel.ts
  42. 150 0
      src/frame/BaseService.ts
  43. 21 0
      src/frame/Meta.ts
  44. 43 0
      src/frame/Options.ts
  45. 64 0
      src/frame/QueryUtils.ts
  46. 35 0
      src/frame/Utils.ts
  47. 0 0
      src/interface.ts
  48. 82 0
      src/middleware/CheckToken.middleware.ts
  49. 27 0
      src/middleware/report.middleware.ts
  50. 15 0
      src/response/CustomerResponse.ts
  51. 241 0
      src/service/frame/File.service.ts
  52. 139 0
      src/service/frame/Init.service.ts
  53. 78 0
      src/service/frame/Login.service.ts
  54. 93 0
      src/service/frame/LoginRecord.service.ts
  55. 22 0
      src/service/system/admin.service.ts
  56. 18 0
      src/service/system/config.service.ts
  57. 12 0
      src/service/system/dictData.service.ts
  58. 12 0
      src/service/system/dictType.service.ts
  59. 44 0
      src/service/system/menus.service.ts
  60. 71 0
      src/service/system/role.service.ts
  61. 22 0
      src/service/system/user.service.ts
  62. 20 0
      test/controller/api.test.ts
  63. 21 0
      test/controller/home.test.ts
  64. 28 0
      tsconfig.json

+ 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.*
+/upload

+ 3 - 0
.prettierrc.js

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

+ 65 - 0
README-Frame.md

@@ -0,0 +1,65 @@
+# 新框架
+## 不在通过npm发布,直接拉取项目直接用.这样方便各个项目修改对框架的需求
+
+## 1.不再使用中间件做返回处理,使用静态类处理: 
+```
+    RF => ResponseFormat
+```
+```
+    RF.success(data?:any):
+    return {
+      errcode: '0',
+      errmsg: 'ok',
+      data,
+    }
+```
+```
+  RF.error(e?:string):
+  return {
+    errcode: '400',
+    errmsg: e || '服务发生错误',
+  }
+```
+## 2.controller的interface,负责验证参数部分,有需要自己写
+```
+  export class XXXDTO {
+    @ApiProperty({ description: "xxx" }) // (swagger参数说明)
+    @Rule(RuleType['${dataType}']().${methodName}()) // dataType:数据类型; methodName:判断方法
+    [${columnName}]: ${dataType} = undefined; // columnName:接收的字段名; dataType:数据类型; = undefined 可写可不写;
+  }
+```
+
+## 3.异常类ServiceError和异常过滤器ServiceError 重写
+现在声明异常为以下方式即可使用
+```
+  export const ErrorCode = {
+    UNKNOW: { code: '-1', msg: '未知错误' },
+    NOT_LOGIN: { code: '401', msg: '未登录' },
+  };
+```
+使用方式:
+```
+  throw new ServiceError(ErrorCode.UNKNOW)
+```
+
+## 4.重写查询条件处理,单独生成函数.默认在自定义Query中使用
+### 新增两个参数注解:
+#### 1.Query: @Query(mapping?:object): object
+重做了下Query注解: 新Query注解中, 自动过滤掉skip和limit; 剩下的查询条件根据key的符号.由QueryUtils.QueryReset函数处理. mapping为字段映射,{ ${接收字段}: ${映射为的字段} }
+#### 2.Page: @Page():object
+提取分页对象的注解,里面有skip和limit两个参数
+## TODO: 5.checkToken中间件重写,根据config.passToken:Array 中的路由设置判断
+```
+passToken: [], //写Get/Post/Delete注解的路径,遇到这些路径时,checkToken处理token,但是跳过检查
+tokenKey: 'token' // 请求头中的token的key名,默认为token
+```
+## 6.框架自带上传,下载;可以设置文件位置,默认在项目根目录下:
+原upload插件改为busboy,所以设置也得改成busboy
+```
+  busboy:{
+    mode:'file' // 默认,基本不用改
+    realdir: 'upload' // 真实路径,默认是项目根目录的upload文件夹下
+    whitelist: null,//默认不限制,如果要限制,就到midwayjs官网看下咋写
+    columns: [] // 涉及文件的字段:${model的class名}.${字段名}.清除不用的文件时就根据这里设置的去清除,没写进去的都会被删掉
+  }
+```

+ 29 - 0
README.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();

+ 19 - 0
ecosystem.config.js

@@ -0,0 +1,19 @@
+'use strict';
+const app = 'service';
+module.exports = {
+  apps: [
+    {
+      name: app, // app name
+      script: './bootstrap.js', // startup script
+      out: `./logs/${app}.log`,
+      error: `./logs/${app}.err`,
+      // if change ,pm2 will restart it
+      watch: [
+        'dist',
+      ],
+      env: {
+        NODE_ENV: 'production', // env
+      },
+    },
+  ],
+};

+ 6 - 0
jest.config.js

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

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 8870 - 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.12.0",
+    "@midwayjs/busboy": "^3.18.2",
+    "@midwayjs/core": "^3.12.0",
+    "@midwayjs/info": "^3.12.0",
+    "@midwayjs/jwt": "^3.18.2",
+    "@midwayjs/koa": "^3.12.0",
+    "@midwayjs/logger": "^3.1.0",
+    "@midwayjs/typegoose": "^3.0.0",
+    "@midwayjs/validate": "^3.12.0",
+    "@typegoose/typegoose": "^11.0.0",
+    "dayjs": "^1.11.13",
+    "fs-extra": "^11.2.0",
+    "lodash-es": "^4.17.21",
+    "mime-types": "^2.1.35",
+    "mongoose": "^7.0.0"
+  },
+  "devDependencies": {
+    "@midwayjs/mock": "^3.12.0",
+    "@types/jest": "^29.2.0",
+    "@types/lodash": "^4.17.12",
+    "@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: '1729663431890_1006',
+  koa: {
+    port: 7001,
+  },
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: 3600, // 3600
+  },
+  /**启用checkToken中间件 */
+  useCheckTokenMiddleware: true,
+  /**不检查token的路由列表 */
+  passToken: [
+    '/login',
+    '/dictType',
+    '/:project/upload',
+    '/:project/:catalog/upload',
+    '/:project/:catalog/:item/upload',
+  ],
+  /**请求头中token的key名 */
+  tokenKey: 'token',
+  /**上传设置, columns:涉及文件的字段:${表名}.${字段名} 表名用model的class名,因为在connect中存的就是这个类 */
+  busboy: {
+    mode: 'file',
+    realdir: 'upload',
+    whitelist: null,
+    columns: ['Admin.icon'],
+  },
+} as MidwayConfig;

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

@@ -0,0 +1,57 @@
+import { MidwayConfig } from '@midwayjs/core';
+const ip = '192.168.1.153'; //120.48.146.1
+const redisHost = '192.168.1.153';
+const redisPwd = '123456';
+const redisDB = 6;
+const projectDB = 'vue3js-template-test';
+const recordDB = 'vue3js-template-test-record';
+const loginSign = 'tsFrameDev';
+export default {
+  // use for cookie sign key, should change to your own and keep security
+  keys: '1697684406848_4978',
+  loginSign,
+  koa: {
+    port: 9700,
+    globalPrefix: '/ts/frame/api',
+  },
+  swagger: {
+    swaggerPath: '/doc/api',
+  },
+  jwt: {
+    secret: 'Ziyouyanfa!@#',
+    expiresIn: 3600, // 3600
+  },
+  dbName: projectDB,
+  mongoose: {
+    dataSource: {
+      default: {
+        uri: `mongodb://${ip}:27017/${projectDB}`,
+        options: {
+          user: 'admin',
+          pass: 'admin',
+          authSource: 'admin',
+          useNewUrlParser: true,
+        },
+        entities: ['./entity'],
+      },
+      record: {
+        uri: `mongodb://${ip}:27017/${recordDB}`,
+        options: {
+          user: 'admin',
+          pass: 'admin',
+          authSource: 'admin',
+          useNewUrlParser: true,
+        },
+        entities: ['./entityRecord'],
+      },
+    },
+  },
+  redis: {
+    client: {
+      port: 6379, // Redis port
+      host: redisHost, // Redis host
+      password: redisPwd,
+      db: redisDB,
+    },
+  },
+} 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;

+ 44 - 0
src/configuration.ts

@@ -0,0 +1,44 @@
+import {
+  Configuration,
+  App,
+  Inject,
+  MidwayDecoratorService,
+} 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 { ReportMiddleware } from './middleware/report.middleware';
+import * as typegoose from '@midwayjs/typegoose';
+import { CustomErrorFilter } from './filter/ServiceError.filter';
+import { CheckTokenMiddleware } from './middleware/CheckToken.middleware';
+import * as busboy from '@midwayjs/busboy';
+import * as jwt from '@midwayjs/jwt';
+
+@Configuration({
+  imports: [
+    koa,
+    validate,
+    typegoose,
+    busboy,
+    jwt,
+    {
+      component: info,
+      enabledEnvironment: ['local'],
+    },
+  ],
+  importConfigs: [join(__dirname, './config')],
+})
+export class MainConfiguration {
+  @App('koa')
+  app: koa.Application;
+  @Inject()
+  decoratorService: MidwayDecoratorService;
+  async onReady() {
+    // add middleware
+    this.app.useMiddleware([ReportMiddleware, CheckTokenMiddleware]);
+    // add filter
+    this.app.useFilter([CustomErrorFilter, 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';
+  }
+}

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

@@ -0,0 +1,22 @@
+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() {
+    await this.service.adminUser();
+    // 未初始化,则执行初始化程序
+    await this.service.initDict();
+    await this.service.initRole();
+    await this.service.initMenus();
+    return RF.success();
+  }
+}

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

@@ -0,0 +1,59 @@
+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';
+
+@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 账户类型
+   */
+  @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);
+  }
+}

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

@@ -0,0 +1,29 @@
+import { 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;
+  @Get('/tokenView')
+  async tokenView() {
+    const token = get(this.ctx, 'request.header.token');
+    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);
+  }
+}

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

@@ -0,0 +1,9 @@
+import { Controller, Get } from '@midwayjs/core';
+import { RF } from '../response/CustomerResponse';
+@Controller('/')
+export class HomeController {
+  @Get('/')
+  async home() {
+    return RF.success('Hello Midwayjs!');
+  }
+}

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

@@ -0,0 +1,51 @@
+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';
+
+@Controller('/admin')
+export class AdminController {
+  @Inject()
+  service: AdminService;
+
+  @Post('/')
+  async create(@Body() body) {
+    await this.service.checkInDB(body);
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.page(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.findOne({ 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: string, @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.page(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.findOne({ 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.page(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.findOne({ 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.findOne({ 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.page(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.findOne({ 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()
+  }
+}

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

@@ -0,0 +1,51 @@
+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';
+
+@Controller('/User')
+export class UserController {
+  @Inject()
+  service: UserService;
+
+  @Post('/')
+  async create(@Body() body) {
+    await this.service.checkInDB(body);
+    const data = await this.service.create(body);
+    return RF.success(data);
+  }
+
+  @Get('/')
+  async query(@Query() query, @Page() page) {
+    const result = await this.service.page(query, page);
+    return RF.success(result);
+  }
+
+  @Get('/:id')
+  async fetch(@Param('id') id: string) {
+    const result = await this.service.findOne({ 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;
+  });
+};

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

@@ -0,0 +1,18 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from '../../frame/BaseModel';
+
+@modelOptions({
+  schemaOptions: { collection: 'loginRecord' },
+})
+export class LoginRecord extends BaseModel {
+  @prop({ required: false, index: false, zh: '用户id' })
+  user_id: string;
+  @prop({ required: false, index: false, zh: 'token' })
+  token: string;
+  @prop({ required: false, index: false, zh: '最后使用时间' })
+  last_time: string;
+  @prop({ required: false, index: false, zh: '过期时间' })
+  expire_time: string;
+  @prop({ required: false, index: false, zh: '最后使用ip' })
+  last_ip: string;
+}

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

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

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

@@ -0,0 +1,11 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from '../../frame/BaseModel';
+@modelOptions({
+  schemaOptions: { collection: 'config' },
+})
+export class Config extends BaseModel {
+  @prop({ required: false, index: false, zh: 'logo' })
+  logo: Array<any>;
+  @prop({ required: false, index: false, zh: '系统名称' })
+  name: String;
+}

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,32 @@
+import { modelOptions, prop } from '@typegoose/typegoose';
+import { BaseModel } from '../../frame/BaseModel';
+@modelOptions({
+  schemaOptions: { collection: 'user' },
+})
+export class User extends BaseModel {
+  @prop({
+    required: false,
+    index: true,
+    zh: '账号',
+    unique: true,
+    esType: 'keyword',
+  })
+  account: string;
+  @prop({ required: false, index: false, zh: '微信小程序id' })
+  openid: string;
+  @prop({ required: false, index: false, zh: '昵称' })
+  nick_name: string;
+  @prop({ required: false, index: false, zh: '电话' })
+  tel: string;
+  @prop({ required: false, index: false, zh: '角色', esType: 'keyword' })
+  role: string;
+  @prop({
+    required: false,
+    index: false,
+    zh: '是否启用',
+    remark: '0:启用;1:禁用',
+    esType: 'keyword',
+    default: '0',
+  })
+  is_use: string;
+}

+ 38 - 0
src/error/Codes.ts

@@ -0,0 +1,38 @@
+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: '账户已存在' },
+};

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

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

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

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

+ 8 - 0
src/frame/BaseModel.ts

@@ -0,0 +1,8 @@
+import { Severity, modelOptions, plugin } from '@typegoose/typegoose';
+import meta from './Meta';
+
+@modelOptions({
+  options: { allowMixed: Severity.ALLOW },
+})
+@plugin(meta)
+export class BaseModel {}

+ 150 - 0
src/frame/BaseService.ts

@@ -0,0 +1,150 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { AnyParamConstructor } from '@typegoose/typegoose/lib/types';
+import { Application, Context } from '@midwayjs/koa';
+import { cloneDeep, get } from 'lodash';
+import { PageOptions, ResultOptions } from './Options';
+import { PopulateOptions } from 'mongoose';
+import { App, Inject } from '@midwayjs/core';
+import { ServiceError } from '../error/CustomerError.error';
+import { ErrorCode } from '../error/Codes';
+import { GetModel } from './Utils';
+/**
+ * Service基类,实现了一些基础的crud
+ */
+export abstract class BaseService<T extends AnyParamConstructor<any>> {
+  @App()
+  app: Application;
+  @Inject()
+  ctx: Context;
+
+  // 要求继承基类的service,必须给上默认的model
+  abstract model: ReturnModelType<T>;
+  /**
+   * 带总数查询
+   * @param {Object} filter 查询条件
+   * @param {object} pageOptions 分页条件
+   * @param {Boolean} lean 是否使用JavaScript形式数据;false为mongoose的模型实例数据
+   * @param {Boolean} populate 是否进行ref关联数据
+   * @returns {Promise<object>} 返回列表
+   */
+  async page(
+    filter: object,
+    pageOptions: PageOptions = {},
+    resultOptions: ResultOptions = { lean: true, populate: true }
+  ) {
+    const data = await this.query(filter, pageOptions, resultOptions);
+    const total = await this.count(filter);
+    return { data, total };
+  }
+
+  /**
+   * 列表查询-无查询条件处理
+   * @param {Object} filter 查询条件
+   * @param {object} pageOptions 分页条件
+   * @param {Boolean} lean 是否使用JavaScript形式数据;false为mongoose的模型实例数据
+   * @param {Boolean} populate 是否进行ref关联数据
+   * @returns {Promise<object>} 返回列表
+   */
+  async query(
+    filter: object,
+    pageOptions: PageOptions = {},
+    resultOptions: ResultOptions = { lean: true, populate: true }
+  ): Promise<Array<any>> {
+    const dup = cloneDeep(filter);
+    const { lean, populate } = resultOptions;
+    let refs = [];
+    if (populate) refs = this.getRefs();
+    const data = await this.model
+      .find(dup, {}, { ...pageOptions })
+      .populate(refs)
+      .lean(lean);
+    return data;
+  }
+
+  /**
+   * 数据总数查询
+   * @param {Object} filter 查询条件
+   * @returns {number} 数据总数
+   */
+  async count(filter: Object): Promise<number> {
+    const total = await this.model.count(filter);
+    return total;
+  }
+
+  /**
+   * 单查询-通过任意条件
+   * @param query 查询条件
+   * @param resultOptions 结果处理
+   */
+  async findOne(
+    query: object = {},
+    resultOptions: ResultOptions = { lean: true, populate: true }
+  ): Promise<object | undefined> {
+    const { lean, populate } = resultOptions;
+    let refs = [];
+    if (populate) refs = this.getRefs();
+    const data = await this.model.findOne(query).populate(refs).lean(lean);
+    return data;
+  }
+
+  /**
+   * 修改
+   * @param {object} filter 修改范围
+   * @param {object} body 要修改的内容
+   */
+  async update(filter: object, body: object): Promise<void> {
+    const filterKeys = Object.keys(filter);
+    if (filterKeys.length <= 0) throw new ServiceError(ErrorCode.NEED_PARAMS);
+    const num = await this.model.count(filter);
+    if (num <= 0) throw new ServiceError(ErrorCode.DATA_NOT_FOUND);
+    await this.model.updateMany(filter, body);
+  }
+
+  /**
+   * 删除
+   * @param {object} filter 要删除的数据范围
+   *
+   */
+  async delete(filter: object): Promise<void> {
+    const filterKeys = Object.keys(filter);
+    if (filterKeys.length <= 0) throw new ServiceError(ErrorCode.NEED_PARAMS);
+    await this.model.deleteMany(filter);
+  }
+
+  /**
+   * 单创建
+   * @param body 要创建的数据内容
+   * @returns {object}
+   */
+  async create(body: object): Promise<object> {
+    const data = await this.model.create(body);
+    return data;
+  }
+
+  /**
+   * 多创建
+   * @param body 要创建的多个数据
+   * @returns {object[]}
+   */
+  async createMany(body: object[]): Promise<Array<object>> {
+    const data = await this.model.insertMany(body);
+    return data;
+  }
+  /**
+   * 获取本服务默认表的ref关系
+   */
+  getRefs(): Array<PopulateOptions> {
+    const schema: any = get(this.model, 'schema.tree');
+    const refs = [];
+    for (const key in schema) {
+      const f = schema[key];
+      const ref = get(f, 'ref');
+      if (ref) {
+        const model = GetModel(ref);
+        const path = key;
+        refs.push({ path, model });
+      }
+    }
+    return refs;
+  }
+}

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

+ 43 - 0
src/frame/Options.ts

@@ -0,0 +1,43 @@
+import { Rule, RuleType } from "@midwayjs/validate";
+import { get } from "lodash";
+
+/**分页处理参数 */
+export interface PageOptions {
+  skip?: number;
+  limit?: number;
+  [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;
+};

+ 35 - 0
src/frame/Utils.ts

@@ -0,0 +1,35 @@
+import { getModelForClass, getClass } from '@typegoose/typegoose';
+import { AnyParamConstructor } from '@typegoose/typegoose/lib/types';
+import { upperFirst } from 'lodash';
+import { ServiceError } from '../error/CustomerError.error';
+import { ErrorCode } from '../error/Codes';
+/**
+ * 根据表名返回model
+ * @param modelName 表名
+ * @returns model
+ */
+export function GetModel(modelName) {
+  let model;
+  try {
+    model = getModelForClass(
+      getClass(upperFirst(modelName)) as AnyParamConstructor<any>
+    );
+  } catch (error) {
+    console.error(error);
+    throw new ServiceError(
+      ErrorCode.SERVICE_FAULT(`${modelName}-生成模型错误`)
+    );
+  }
+  if (!model) {
+    throw new ServiceError(ErrorCode.SERVICE_FAULT(`${modelName}-未找到模型`));
+  }
+
+  return model;
+}
+/**
+ * 生成随机字符串
+ * @param len 位数,默认6位
+ */
+export const randomStr = (len = 6) => {
+  return Math.random().toString(36).slice(-len);
+};

+ 0 - 0
src/interface.ts


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

@@ -0,0 +1,82 @@
+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;
+  @Inject()
+  webRouterService: MidwayWebRouterService;
+  @Inject()
+  jwtService: JwtService;
+
+  async analysisToken(ctx: Context) {
+    // 获取tokenKey,检查header中的token是否存在,并解析出来,放到user中
+    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;
+  }
+
+  resolve() {
+    return async (ctx: Context, next: NextFunction) => {
+      console.log('in Middleware');
+      let token = null;
+      if (!this.useCheckTokenMiddleware) {
+        await next();
+      } else {
+        // 解析token
+        token = await this.analysisToken(ctx);
+        // 获取路由
+        const routeInfo = await this.webRouterService.getMatchedRouterInfo(
+          ctx.path,
+          ctx.method
+        );
+        const path = routeInfo.url as string;
+        // 检查路由是否在 无需检查token列表中
+        if (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 || '服务发生错误',
+    };
+  }
+}

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

@@ -0,0 +1,241 @@
+import { existsSync, lstatSync, mkdirSync, readdirSync } from 'fs';
+import { Context } from '@midwayjs/koa';
+import { dirname, extname, join, sep } from 'path';
+import { flattenDeep, get, last, head } from 'lodash';
+import { moveSync, removeSync } from 'fs-extra';
+import { Provide, Inject, Config } from '@midwayjs/core';
+import { mongoose } from '@typegoose/typegoose';
+
+/**
+ * 文件上传相关服务
+ */
+@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 递归清理未被使用的上传文件
+  /**
+   * 删除不在文件使用表登记的文件
+   * TODO:
+   * 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
+}

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

@@ -0,0 +1,139 @@
+import { Provide } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { ReturnModelType } from '@typegoose/typegoose';
+import { Types } from 'mongoose';
+import { Admin } from '../../entity/system/admin.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';
+const ObjectId = Types.ObjectId;
+@Provide()
+export class InitService {
+  @InjectEntityModel(Admin)
+  adminModel: ReturnModelType<typeof Admin>;
+  @InjectEntityModel(Role)
+  roleModel: ReturnModelType<typeof Role>;
+  @InjectEntityModel(Menus)
+  menusModel: ReturnModelType<typeof Menus>;
+  @InjectEntityModel(DictType)
+  dictTypeModel: ReturnModelType<typeof DictType>;
+  @InjectEntityModel(DictData)
+  dictDataModel: ReturnModelType<typeof DictData>;
+
+  async adminUser() {
+    const data = {
+      account: 'admin',
+      password: '1qaz2wsx',
+      nick_name: '系统管理员',
+      is_super: '0',
+    };
+    const is_exist = await this.adminModel.count({ is_super: '0' });
+    if (!is_exist) return await this.adminModel.create(data);
+    return;
+  }
+
+  async initRole() {
+    const num = await this.roleModel.count();
+    if (num > 0) return;
+    const datas = [{ name: '管理员', code: 'admin' }];
+    await this.roleModel.insertMany(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' }];
+    // 系统设置
+    const smId = new ObjectId();
+    const systemMenus = [
+      { _id: smId, name: '系统设置', path: '/system', order_num: 2, type: '0', route_name: 'system', is_default: '0' },
+      {
+        name: '菜单设置',
+        parent_id: smId.toString(),
+        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.toString(),
+        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.toString(), order_num: 3, path: '/system/dict', component: '/system/dict/index', type: '1', route_name: 'system_dict', is_default: '0' },
+      { name: '字典数据', parent_id: smId.toString(), order_num: 4, path: '/system/dictData', component: '/system/dictData/index', type: '2', route_name: 'system_dict_data', is_default: '0' },
+    ];
+    // 用户管理
+    const umId = new ObjectId();
+    const userMenus = [
+      { _id: umId, name: '用户管理', order_num: 3, path: '/user', type: '0', route_name: 'user', is_default: '1' },
+      { 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' },
+    ];
+    const password = { name: '修改密码', order_num: 999, path: '/acccount/updatepd', component: '/acccount/updatepd/index', type: '1' , route_name: 'password'};
+
+    datas.push(...systemMenus, ...userMenus, password);
+    // 项目业务菜单
+    const busMenus = [];
+    datas.push(...busMenus);
+    await this.menusModel.insertMany(datas);
+  }
+  async initRoleMenu(admin: Admin) {}
+
+  async initDict() {
+    const isUseType = [{ title: '是否使用', code: 'isUse', is_use: '0' }];
+    const isUseData = [
+      { code: 'isUse', label: '使用', value: '0', sort: 1, is_use: '0' },
+      { code: 'isUse', label: '禁用', value: '1', sort: 2, is_use: '0' },
+    ];
+    await this.dictTypeModel.insertMany(isUseType);
+    await this.dictDataModel.insertMany(isUseData);
+  }
+}

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

@@ -0,0 +1,78 @@
+import { Inject, Provide } from '@midwayjs/core';
+import { LoginType, UPwdDTO } from '../../frame/Options';
+import { get, isEqual, upperFirst } from 'lodash';
+import { GetModel, randomStr } from '../../frame/Utils';
+import { ServiceError } from '../../error/CustomerError.error';
+import { ErrorCode } from '../../error/Codes';
+import { RoleService } from '../system/role.service';
+
+@Provide()
+export class LoginService {
+  @Inject()
+  roleService: RoleService;
+  /**
+   * 账密登录
+   * @param data 用户名和密码
+   * @param type 用户类型
+   * @returns 用户信息/空值
+   */
+  async loginByAccount(data, type: LoginType) {
+    const model = GetModel(upperFirst(type));
+    const user = await model
+      .findOne({ account: data.account }, '+password')
+      .lean();
+    if (!user) throw new ServiceError(ErrorCode.USER_NOT_FOUND);
+    await this.checkAccountCanLogin(user, type);
+    if (!isEqual(user.password.secret, data.password))
+      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') === '0') return true;
+    }
+    const role = await this.roleService.findOne({
+      code: type,
+      is_use: '0',
+    });
+    if (!role) throw new ServiceError(ErrorCode.ROLE_IS_DISABLED);
+    if (get(user, 'is_use') === '1')
+      throw new ServiceError(ErrorCode.USER_IS_DISABLED);
+  }
+
+ /**
+  * 修改密码
+  * @param data 修改密码对象
+  * @param type 规定的登录类型
+  * @returns 新密码
+  */
+  async updatePwd(data: UPwdDTO, type: LoginType) {
+    const model = GetModel(upperFirst(type));
+    const user = await model.findById(data._id);
+    if (!user) new ServiceError(ErrorCode.USER_NOT_FOUND);
+    // 没有密码默认给随机一个
+    const password = get(data,'password', randomStr())
+    user.password = password;
+    await user.save();
+    return password;
+  }
+}

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

@@ -0,0 +1,93 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { LoginRecord } from '../../entity/frame/loginRecord.entity';
+import { Config, Inject, Provide } from '@midwayjs/core';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+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';
+
+type modelType = ReturnModelType<typeof LoginRecord>;
+
+@Provide()
+export class LoginRecordService {
+  @InjectEntityModel(LoginRecord)
+  model: ReturnModelType<modelType>;
+  @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 = { token, last_time, expire_time, last_ip };
+    const num = await this.model.count({ user_id });
+    if (num > 0) {
+      // 有过登录记录,那就直接更新token,last_time,expire_time,last_ip就行
+      await this.model.updateOne({ user_id }, data);
+    } else {
+      // 没有记录,那就创建
+      data['user_id'] = user_id;
+      await this.model.create(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({ 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);
+    await this.model.updateOne(
+      { user_id },
+      { last_ip, last_time, expire_time }
+    );
+  }
+  /**检查是否过期 */
+  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 data = await this.model.findOne({ user_id });
+    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;
+  }
+}

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

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

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

@@ -0,0 +1,18 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { Config } from '../../entity/system/config.entity';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+type modelType = ReturnModelType<typeof Config>;
+
+@Provide()
+export class ConfigService extends BaseService<modelType> {
+  @InjectEntityModel(Config)
+  model: ReturnModelType<modelType>;
+  // 只允许有一个config
+  async getConfig() {
+    let data = await this.model.findOne({}).lean();
+    if (!data) data = await this.model.create({ name: 'midwayjs+vue' });
+    return data;
+  }
+}

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

@@ -0,0 +1,12 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { DictData } from '../../entity/system/dictData.entity';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+type modelType = ReturnModelType<typeof DictData>;
+
+@Provide()
+export class DictDataService extends BaseService<modelType> {
+  @InjectEntityModel(DictData)
+  model: ReturnModelType<modelType>;
+}

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

@@ -0,0 +1,12 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { DictType } from '../../entity/system/dictType.entity';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+type modelType = ReturnModelType<typeof DictType>;
+
+@Provide()
+export class DictTypeService extends BaseService<modelType> {
+  @InjectEntityModel(DictType)
+  model: ReturnModelType<modelType>;
+}

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

@@ -0,0 +1,44 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { Menus } from '../../entity/system/menus.entity';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { orderBy } from 'lodash';
+import { Types } from 'mongoose';
+const ObjectId = Types.ObjectId;
+type modelType = ReturnModelType<typeof Menus>;
+
+@Provide()
+export class MenusService extends BaseService<modelType> {
+  @InjectEntityModel(Menus)
+  model: ReturnModelType<modelType>;
+
+  async queryMenu(query: object = {}): Promise<Array<object>> {
+    const data = await this.model
+      .find(query, { meta: 0, __v: 0 })
+      .sort({ order_num: 1 })
+      .lean();
+    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 =>
+        new ObjectId(f.parent_id).equals(_id)
+      );
+      children = this.treeMenu(allMenus, children);
+      if (children.length > 0) nm.children = children;
+      // 换父级组件的名称
+      if (parent_id) {
+        const r = allMenus.find(f => new ObjectId(f._id).equals(parent_id));
+        if (r) nm.parent_name = r.name;
+      }
+    }
+    nowMenu = orderBy(nowMenu, ['order_num'], ['asc']);
+    return nowMenu;
+  }
+}

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

@@ -0,0 +1,71 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from '../../frame/BaseService';
+import { Inject, Provide } from '@midwayjs/core';
+import { Role } from '../../entity/system/role.entity';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { lowerFirst, upperFirst, last, flattenDeep, uniq, get } from 'lodash';
+import { Menus } from '../../entity/system/menus.entity';
+import { ServiceError } from '../../error/CustomerError.error';
+import { MenusService } from './menus.service';
+import { ErrorCode } from '../../error/Codes';
+type modelType = ReturnModelType<typeof Role>;
+
+@Provide()
+export class RoleService extends BaseService<modelType> {
+  @InjectEntityModel(Role)
+  model: modelType;
+  @InjectEntityModel(Menus)
+  menusModel: ReturnModelType<typeof 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 role = await this.model
+      .findOne({ code: roleCode, is_use: '0' })
+      .lean();
+    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 menuList = await this.menusModel.find({ route_name: menu }).lean();
+    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 allMenu = await this.menusModel
+      .find({ _id: menus, is_use: '0' }, { meta: 0, __v: 0 })
+      .sort({ order_num: 1 })
+      .lean();
+    let treeMenu = allMenu.filter(f => !f.parent_id);
+    treeMenu = this.menusService.treeMenu(allMenu, treeMenu);
+    return treeMenu;
+  }
+}

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

@@ -0,0 +1,22 @@
+import { ReturnModelType } from '@typegoose/typegoose';
+import { BaseService } from '../../frame/BaseService';
+import { Provide } from '@midwayjs/core';
+import { User } from '../../entity/system/user.entity';
+import { InjectEntityModel } from '@midwayjs/typegoose';
+import { get } from 'lodash';
+import { ErrorCode } from '../../error/Codes';
+import { ServiceError } from '../../error/CustomerError.error';
+type modelType = ReturnModelType<typeof User>;
+
+@Provide()
+export class UserService extends BaseService<modelType> {
+  @InjectEntityModel(User)
+  model: ReturnModelType<modelType>;
+  /**查重 */
+  async checkInDB(data: object) {
+    // account查重,虽然数据库有unique约束,但是尽量不触发,因为异常捕获不好解析
+    const account = get(data, 'account');
+    const num = await this.model.count({ 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"
+  ]
+}