lrf 4 months ago
commit
5de6b88dba
57 changed files with 12730 additions and 0 deletions
  1. 11 0
      .editorconfig
  2. 17 0
      .eslintrc.json
  3. 13 0
      .gitignore
  4. 3 0
      .prettierrc.js
  5. 29 0
      README.md
  6. 29 0
      README.zh-CN.md
  7. 2 0
      bootstrap.js
  8. 6 0
      jest.config.js
  9. 8954 0
      package-lock.json
  10. 52 0
      package.json
  11. 15 0
      src/config/config.default.ts
  12. 34 0
      src/config/config.local.ts
  13. 7 0
      src/config/config.unittest.ts
  14. 36 0
      src/configuration.ts
  15. 42 0
      src/controller/common.controller.ts
  16. 90 0
      src/controller/frame/File.controller.ts
  17. 17 0
      src/controller/frame/Init.controller.ts
  18. 9 0
      src/controller/home.controller.ts
  19. 24 0
      src/decorator/page.decorator.ts
  20. 34 0
      src/entity/jc_comment.ts
  21. 13 0
      src/entity/jc_comment_ext.ts
  22. 40 0
      src/entity/jc_content.ts
  23. 17 0
      src/entity/jc_content_attachment.ts
  24. 11 0
      src/entity/jc_content_attr.ts
  25. 9 0
      src/entity/jc_content_channel.ts
  26. 47 0
      src/entity/jc_content_ext.ts
  27. 13 0
      src/entity/jc_content_picture.ts
  28. 11 0
      src/entity/jc_content_tag.ts
  29. 9 0
      src/entity/jc_content_topic.ts
  30. 15 0
      src/entity/jc_content_txt.ts
  31. 17 0
      src/entity/jc_content_type.ts
  32. 11 0
      src/entity/jc_contenttag.ts
  33. 27 0
      src/entity/jc_friendlink.ts
  34. 13 0
      src/entity/jc_friendlink_ctg.ts
  35. 25 0
      src/entity/jc_site_company.ts
  36. 27 0
      src/entity/jc_topic.ts
  37. 40 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. 147 0
      src/frame/BaseService.ts
  43. 21 0
      src/frame/Meta.ts
  44. 44 0
      src/frame/Options.ts
  45. 64 0
      src/frame/QueryUtils.ts
  46. 7 0
      src/frame/Utils.ts
  47. 6 0
      src/interface.ts
  48. 27 0
      src/middleware/report.middleware.ts
  49. 15 0
      src/response/CustomerResponse.ts
  50. 238 0
      src/service/frame/File.service.ts
  51. 9 0
      src/service/frame/Init.service.ts
  52. 50 0
      src/service/frame/common.service.ts
  53. 14 0
      src/service/user.service.ts
  54. 20 0
      test/controller/api.test.ts
  55. 21 0
      test/controller/home.test.ts
  56. 28 0
      tsconfig.json
  57. 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 }]
+  }
+}

+ 13 - 0
.gitignore

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

+ 3 - 0
.prettierrc.js

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

+ 29 - 0
README.md

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

+ 29 - 0
README.zh-CN.md

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

+ 2 - 0
bootstrap.js

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

+ 6 - 0
jest.config.js

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

File diff suppressed because it is too large
+ 8954 - 0
package-lock.json


+ 52 - 0
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"
+}

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

@@ -0,0 +1,15 @@
+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,
+  },
+} as MidwayConfig;

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

@@ -0,0 +1,34 @@
+import { MidwayConfig } from '@midwayjs/core';
+
+/**数据库ip */
+const ip = '192.168.1.153';
+/**原数据库名称 */
+const dbName = 'water_service';
+/**数据库用户名 */
+const dbUsername = 'root';
+/**数据库密码 */
+const dbPwd = 'root';
+
+export default {
+  keys: '1697684406848_4978',
+  koa: {
+    port: 9000,
+    globalPrefix: '/warter/front/v2/api',
+    queryParseMode: 'extended',
+  },
+  typeorm: {
+    dataSource: {
+      default: {
+        type: 'mysql',
+        host: ip,
+        port: 3306,
+        database: dbName,
+        username: dbUsername,
+        password: dbPwd,
+        entities: ['./entity'],
+        synchronize: false, // 如果第一次使用,不存在表,有同步的需求可以写 true,注意会丢数据
+        logging: true,
+      },
+    },
+  },
+} 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;

+ 36 - 0
src/configuration.ts

@@ -0,0 +1,36 @@
+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';
+@Configuration({
+  imports: [
+    koa,
+    validate,
+    busboy,
+    orm,
+    {
+      component: info,
+      enabledEnvironment: ['local'],
+    },
+  ],
+  importConfigs: [join(__dirname, './config')],
+})
+export class MainConfiguration {
+  @App('koa')
+  app: koa.Application;
+
+  async onReady() {
+    // add middleware
+    this.app.useMiddleware([ReportMiddleware]);
+    // add filter
+    this.app.useFilter([CustomErrorFilter]);
+    // this.app.useFilter([NotFoundFilter, DefaultErrorFilter]);
+  }
+}

+ 42 - 0
src/controller/common.controller.ts

@@ -0,0 +1,42 @@
+import { Controller, Get, Inject, Param } from '@midwayjs/core';
+import { Page, Query } from '../decorator/page.decorator';
+import { RF } from '../response/CustomerResponse';
+import { CommonService } from '../service/frame/common.service';
+
+@Controller('/common')
+export class CommonController {
+  @Inject()
+  service: CommonService
+
+  /**
+   * 通用查询列表
+   * @param table 表名
+   * @param query 查询条件
+   * @param page 分页设置
+   * @returns RF
+   */
+  @Get('/:table/list')
+  async list(
+    @Param('table') table: string,
+    @Query() query: object,
+    @Page() page: object
+  ) {
+    const model = this.service.getModel(table);
+    const res = await this.service.search(model, query, page);
+    return RF.success(res);
+  }
+  /**
+   * 通用单查询
+   * @param table 表名
+   * @param column 查询列名
+   * @param value 对应值
+   * @returns RF
+   */
+  @Get('/:table/:column/:value')
+  async detail(@Param('table') table: string, @Param('column') column: string, @Param('value') value: string) {
+    const query = { [column]: value }
+    const model = this.service.getModel(table)
+    const data = await this.service.getOne(model, query)
+    return RF.success(data)
+  }
+}

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

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

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

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

+ 34 - 0
src/entity/jc_comment.ts

@@ -0,0 +1,34 @@
+
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_comment_ext', { comment: 'CMS评论扩展表' })
+export class JcCommentExt {
+  @PrimaryColumn()
+  comment_id: number;
+  @Column({ comment: '评论用户ID' })
+  comment_user_id: number;
+  @Column({ comment: '回复用户ID' })
+  reply_user_id: number;
+  @Column({ comment: '内容ID' })
+  content_id: number;
+  @Column({ comment: '站点ID' })
+  site_id: number;
+  @Column({ comment: '评论时间' })
+  create_time: Date;
+  @Column({ comment: '回复时间' })
+  reply_time: string;
+  @Column({ comment: '支持数' })
+  ups: number;
+  @Column({ comment: '反对数' })
+  downs: number;
+  @Column({ comment: '是否推荐' })
+  is_recommend: number;
+  @Column({ comment: '是否审核' })
+  is_checked: number;
+  @Column({ comment: '评分' })
+  score: number;
+  @Column({ comment: '父级评论' })
+  parent_id: number;
+  @Column({ comment: '回复数' })
+  reply_count: number;
+}

+ 13 - 0
src/entity/jc_comment_ext.ts

@@ -0,0 +1,13 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_comment_ext', { comment: 'CMS评论扩展表' })
+export class JcCommentExt {
+  @PrimaryColumn()
+  comment_id: number;
+  @Column({ comment: 'IP地址' })
+  ip: string;
+  @Column({ comment: '评论内容' })
+  text: string;
+  @Column({ comment: '回复内容' })
+  reply: string;
+}

+ 40 - 0
src/entity/jc_content.ts

@@ -0,0 +1,40 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content', { comment: 'CMS内容表' })
+export class JcContent {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '栏目ID' })
+  channel_id: number;
+  @Column({ comment: '用户ID' })
+  user_id: number;
+  @Column({ comment: '属性ID' })
+  type_id: number;
+  @Column({ comment: '模型ID' })
+  model_id: number;
+  @Column({ comment: '站点ID' })
+  site_id: number;
+  @Column({ comment: '排序日期' })
+  sort_date: Date;
+
+  @Column({ comment: '固顶级别' })
+  top_level: number;
+  @Column({ comment: '是否有标题图' })
+  has_title_img: number;
+  @Column({ comment: '是否推荐' })
+  is_recommend: number;
+  @Column({ comment: '状态(0:草稿;1:审核中;2:审核通过;3:回收站;4:投稿;5:归档)' })
+  status: number;
+  @Column({ comment: '日访问数' })
+  views_day: number;
+  @Column({ comment: '日评论数' })
+  comments_day: number;
+  @Column({ comment: '日下载数' })
+  downloads_day: number;
+  @Column({ comment: '日顶数' })
+  ups_day: number;
+  @Column({ comment: '得分' })
+  score: number;
+  @Column({ comment: '推荐级别' })
+  recommend_level: number;
+}

+ 17 - 0
src/entity/jc_content_attachment.ts

@@ -0,0 +1,17 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_attachment', { comment: 'CMS内容附件表' })
+export class JcContentAttachment {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '排列顺序' })
+  priority: number;
+  @Column({ comment: '附件路径' })
+  attachment_path: string;
+  @Column({ comment: '附件名称' })
+  attachment_name: string;
+  @Column({ comment: '文件名' })
+  filename: string;
+  @Column({ comment: '下载次数' })
+  download_count: number;
+}

+ 11 - 0
src/entity/jc_content_attr.ts

@@ -0,0 +1,11 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_attr', { comment: 'CMS内容扩展属性表' })
+export class JcContentAttr {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '名称' })
+  attr_name: string;
+  @Column({ comment: '值' })
+  attr_value: string;
+}

+ 9 - 0
src/entity/jc_content_channel.ts

@@ -0,0 +1,9 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_channel', { comment: 'CMS内容栏目关联表' })
+export class JcContentChannel {
+  @PrimaryColumn()
+  channel_id: number;
+  @Column({ comment: '' })
+  content_id: number;
+}

+ 47 - 0
src/entity/jc_content_ext.ts

@@ -0,0 +1,47 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_ext', { comment: 'CMS内容扩展表' })
+export class JcContentTag {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '标题' })
+  title: string;
+  @Column({ comment: '简短标题' })
+  short_title: string;
+  @Column({ comment: '作者' })
+  author: string;
+  @Column({ comment: '来源' })
+  origin: string;
+  @Column({ comment: '来源链接' })
+  origin_url: string;
+  @Column({ comment: '描述' })
+  description: string;
+  @Column({ comment: '发布日期' })
+  release_date: Date;
+  @Column({ comment: '媒体路径' })
+  media_path: string;
+  @Column({ comment: '媒体类型' })
+  media_type: string;
+  @Column({ comment: '标题颜色' })
+  title_color: string;
+  @Column({ comment: '是否加粗' })
+  is_bold: number;
+  @Column({ comment: '标题图片' })
+  title_img: string;
+  @Column({ comment: '内容图片' })
+  content_img: string;
+  @Column({ comment: '类型图片' })
+  type_img: string;
+  @Column({ comment: '外部链接' })
+  link: string;
+  @Column({ comment: '指定模板' })
+  tpl_content: string;
+  @Column({ comment: '需要重新生成静态页' })
+  need_regenerate: number;
+  @Column({ comment: '手机内容页模板' })
+  tpl_mobile_content: string;
+  @Column({ comment: '固顶到期日期' })
+  toplevel_date: Date;
+  @Column({ comment: '归档日期' })
+  pigeonhole_date: Date;
+}

+ 13 - 0
src/entity/jc_content_picture.ts

@@ -0,0 +1,13 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_tag', { comment: 'CMS内容图片表' })
+export class JcContentTag {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '排列顺序' })
+  priority: number;
+  @Column({ comment: '图片地址' })
+  img_path: string;
+  @Column({ comment: '描述' })
+  description: string;
+}

+ 11 - 0
src/entity/jc_content_tag.ts

@@ -0,0 +1,11 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_tag', { comment: 'CMS内容TAG表' })
+export class JcContentTag {
+  @PrimaryColumn()
+  tag_id: number;
+  @Column({ comment: 'tag名称' })
+  tag_name: number;
+  @Column({ comment: '被引用的次数' })
+  ref_counter: number;
+}

+ 9 - 0
src/entity/jc_content_topic.ts

@@ -0,0 +1,9 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_type', { comment: 'CMS内容类型表' })
+export class JcContentType {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '' })
+  topic_id: number;
+}

+ 15 - 0
src/entity/jc_content_txt.ts

@@ -0,0 +1,15 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_txt', { comment: 'CMS内容文本表' })
+export class JcContentTxt {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '文章内容' })
+  txt: number;
+  @Column({ comment: '扩展内容1' })
+  txt1: string;
+  @Column({ comment: '扩展内容2' })
+  txt2: string;
+  @Column({ comment: '扩展内容3' })
+  txt3: string;
+}

+ 17 - 0
src/entity/jc_content_type.ts

@@ -0,0 +1,17 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_content_type', { comment: 'CMS内容类型表' })
+export class JcContentType {
+  @PrimaryColumn()
+  type_id: number;
+  @Column({ comment: '名称' })
+  type_name: number;
+  @Column({ comment: '图片宽' })
+  img_width: string;
+  @Column({ comment: '图片高' })
+  img_height: string;
+  @Column({ comment: '是否有图片' })
+  has_image: string;
+  @Column({ comment: '是否禁用' })
+  is_disabled: string;
+}

+ 11 - 0
src/entity/jc_contenttag.ts

@@ -0,0 +1,11 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_contenttag', { comment: 'CMS内容标签关联表' })
+export class jcContenttag {
+  @PrimaryColumn()
+  content_id: number;
+  @Column({ comment: '' })
+  tag_id: number;
+  @Column({ comment: '' })
+  priority: number;
+}

+ 27 - 0
src/entity/jc_friendlink.ts

@@ -0,0 +1,27 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_friendlink', { comment: 'CMS友情链接' })
+export class JcFriendlink {
+  @PrimaryColumn()
+  friendlink_id: number;
+  @Column({ comment: '' })
+  site_id: number;
+  @Column({ comment: '' })
+  friendlinkctg_id: string;
+  @Column({ comment: '网站名称' })
+  site_name: string;
+  @Column({ comment: '网站地址' })
+  domain: string;
+  @Column({ comment: '图标' })
+  logo: string;
+  @Column({ comment: '站长邮箱' })
+  email: string;
+  @Column({ comment: '描述' })
+  description: string;
+  @Column({ comment: '点击次数' })
+  views: string;
+  @Column({ comment: '是否显示' })
+  is_enabled: string;
+  @Column({ comment: '排列顺序' })
+  priority: number;
+}

+ 13 - 0
src/entity/jc_friendlink_ctg.ts

@@ -0,0 +1,13 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_friendlink_ctg', { comment: 'CMS友情链接' })
+export class JcFriendlinkCtg {
+  @PrimaryColumn()
+  friendlinkctg_id: number;
+  @Column({ comment: '' })
+  site_id: number;
+  @Column({ comment: '名称' })
+  friendlinkctg_name: string;
+  @Column({ comment: '排列顺序' })
+  priority: number;
+}

+ 25 - 0
src/entity/jc_site_company.ts

@@ -0,0 +1,25 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+@Entity('jc_site_company', { comment: '公司信息' })
+export class JcSiteCompany {
+  @PrimaryColumn()
+  site_id: number
+  @Column({ comment: '公司名称' })
+  name: string;
+  @Column({ comment: '公司规模' })
+  scale: string;
+  @Column({ comment: '公司行业' })
+  nature: string;
+  @Column({ comment: '公司行业' })
+  industry: string;
+  @Column({ comment: '联系方式' })
+  contact: string
+  @Column({ comment: '公司简介' })
+  description: string
+  @Column({ comment: '公司地址' })
+  address: string
+  @Column({ comment: '经度' })
+  longitude: number
+  @Column({ comment: '纬度' })
+  latitude: number
+
+}

+ 27 - 0
src/entity/jc_topic.ts

@@ -0,0 +1,27 @@
+import { Column, Entity, PrimaryColumn } from 'typeorm';
+
+@Entity('jc_topic', { comment: 'CMS专题表' })
+export class JcTopic {
+  @PrimaryColumn()
+  topic_id: number;
+  @Column({ comment: '' })
+  channel_id: number;
+  @Column({ comment: '名称' })
+  topic_name: string;
+  @Column({ comment: '简称' })
+  short_name: string;
+  @Column({ comment: '关键字' })
+  keywords: string;
+  @Column({ comment: '描述' })
+  description: string;
+  @Column({ comment: '标题图' })
+  title_img: string;
+  @Column({ comment: '内容图' })
+  content_img: string;
+  @Column({ comment: '专题模板' })
+  tpl_content: string;
+  @Column({ comment: '排列顺序' })
+  priority: number;
+  @Column({ comment: '是否推荐' })
+  is_recommend: number;
+}

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

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

+ 147 - 0
src/frame/BaseService.ts

@@ -0,0 +1,147 @@
+// import { Application, Context } from '@midwayjs/koa';
+// import { cloneDeep, get, omit } from 'lodash';
+// import { PageOptions, ResultOptions } from './Options';
+// import { App, Inject } from '@midwayjs/core';
+// import { ServiceError } from '../error/CustomerError.error';
+// import { ErrorCode } from '../error/Codes';
+// /**
+//  * 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);
+//     body = omit(body, ['meta'])
+//     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',
+  });
+};

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

+ 6 - 0
src/interface.ts

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

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

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

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

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

@@ -0,0 +1,9 @@
+import { Autoload, Init, Scope, ScopeEnum } from '@midwayjs/core';
+@Autoload()
+@Scope(ScopeEnum.Singleton)
+export class InitService {
+  @Init()
+  async init() {
+    console.log('to init');
+  }
+}

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

@@ -0,0 +1,50 @@
+import { Provide } from "@midwayjs/core";
+import { InjectDataSource } from "@midwayjs/typeorm";
+import { 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 data = await model.find({ ...query, ...page, ...others })
+    const total = await model.count(query)
+    return { data, total };
+  }
+  /**
+   * 单查询
+   * @param model 表实体
+   * @param query 查询条件
+   * @returns data 数据
+   */
+  async getOne(model: Repository<ObjectLiteral>, query: object) {
+    const data = await model.findOne(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), " ", '') === tableName)
+    if (!entity) throw new ServiceError(ErrorCode.ENTITY_NOT_FOUND)
+    const entityClass = entity.target;
+    const model = this.defaultDataSource.getRepository(entityClass)
+    return model
+  }
+}

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

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

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

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

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

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

+ 28 - 0
tsconfig.json

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

File diff suppressed because it is too large
+ 2189 - 0
water_service.sql