├── .gitignore ├── .prettierrc ├── README.md ├── nodemon.json ├── package.json ├── src ├── config │ ├── base.config.ts │ ├── dev.config.ts │ ├── local.config.ts │ ├── main.config.ts │ └── winston.config.ts ├── constants │ ├── cache.constant.ts │ ├── error │ │ ├── general.constant.ts │ │ └── recharge.constant.ts │ ├── meta.constant.ts │ ├── system.constant.ts │ └── text.constant.ts ├── decorators │ ├── cache.decorator.ts │ ├── inject_service.decorator.ts │ └── response.decorator.ts ├── exceptions │ ├── bad_request.error.ts │ ├── forbidden.error.ts │ ├── platform.error.ts │ ├── unauthorized.error.ts │ └── validation.error.ts ├── filters │ └── exception.filter.ts ├── interceptors │ ├── cache.interceptor.ts │ ├── error.interceptor.ts │ ├── logging.interceptor.ts │ └── transform.interceptor.ts ├── interfaces │ ├── config.interface.ts │ └── response.interface.ts ├── middlewares │ ├── cors.middleware.ts │ └── origin.middleware.ts ├── models │ ├── anothor_lifeblood.model.ts │ ├── assist.model.ts │ ├── lifeblood.model.ts │ ├── monster.model.ts │ ├── naked.model.ts │ ├── strengthen.model.ts │ └── undisguised.model.ts ├── modules │ ├── account │ │ └── recharge │ │ │ ├── recharge.controller.ts │ │ │ ├── recharge.dto.ts │ │ │ ├── recharge.module.ts │ │ │ └── recharge.service.ts │ ├── app │ │ ├── app.controller.ts │ │ └── app.module.ts │ └── authorize │ │ ├── authorize.controller.ts │ │ ├── authorize.dto.ts │ │ └── authorize.module.ts ├── pipes │ └── validation.pipe.ts ├── processors │ ├── base │ │ └── base.service.ts │ ├── cache │ │ ├── cache.module.ts │ │ ├── cache.service.config.ts │ │ └── cache.service.ts │ └── helper │ │ ├── crypto.serivce.ts │ │ ├── crypto.spec.ts │ │ └── helper.module.ts └── server.ts ├── tsconfig.build.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | .vscode 4 | .git2 5 | .git3 6 | logs 7 | # compiled output 8 | /dist 9 | /tmp 10 | /out-tsc 11 | # Only exists if Bazel was run 12 | /bazel-out 13 | 14 | # dependencies 15 | /node_modules 16 | 17 | # profiling files 18 | chrome-profiler-events.json 19 | speed-measure-plugin.json 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json 36 | .history/* 37 | 38 | # misc 39 | /connect.lock 40 | /coverage 41 | /libpeerconnection.log 42 | npm-debug.log 43 | yarn-error.log 44 | testem.log 45 | /typings 46 | 47 | # System Files 48 | .DS_Store 49 | Thumbs.db 50 | 51 | # Clinic 52 | .clinic/ 53 | 54 | # Temporary files 55 | .tmp/ 56 | 57 | # Environment files 58 | .env 59 | .env.test 60 | .env.local 61 | .env.development 62 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 100 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | [travis-image]: https://api.travis-ci.org/nestjs/nest.svg?branch=master 6 | [travis-url]: https://travis-ci.org/nestjs/nest 7 | [linux-image]: https://img.shields.io/travis/nestjs/nest/master.svg?label=linux 8 | [linux-url]: https://travis-ci.org/nestjs/nest 9 | 10 |

A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.

11 |

12 | NPM Version 13 | Package License 14 | NPM Downloads 15 | Travis 16 | Linux 17 | Coverage 18 | Gitter 19 | Backers on Open Collective 20 | Sponsors on Open Collective 21 | 22 | 23 |

24 | 26 | 27 | ## Description 28 | 29 | [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. 30 | 31 | ## Installation 32 | 33 | ```bash 34 | $ npm install 35 | ``` 36 | 37 | ## Running the app 38 | 39 | ```bash 40 | # development 41 | $ npm run start 42 | 43 | # watch mode 44 | $ npm run start:dev 45 | 46 | # production mode 47 | $ npm run start:prod 48 | ``` 49 | 50 | ## Test 51 | 52 | ```bash 53 | # unit tests 54 | $ npm run test 55 | 56 | # e2e tests 57 | $ npm run test:e2e 58 | 59 | # test coverage 60 | $ npm run test:cov 61 | ``` 62 | 63 | ## Support 64 | 65 | Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). 66 | 67 | ## Stay in touch 68 | 69 | - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) 70 | - Website - [https://nestjs.com](https://nestjs.com/) 71 | - Twitter - [@nestframework](https://twitter.com/nestframework) 72 | 73 | ## License 74 | 75 | Nest is [MIT licensed](LICENSE). 76 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/server.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-fastify", 3 | "version": "0.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "build": "tsc -p tsconfig.build.json", 7 | "format": "prettier --write 'src/**/*.{ts,json,yml,yaml}'", 8 | "start": "ts-node -r tsconfig-paths/register src/server.ts", 9 | "start:dev": "npm run format && concurrently --handle-input \"wait-on dist/server.js && nodemon\" \"tsc -w -p tsconfig.build.json\" ", 10 | "prestart:prod": "rimraf dist && npm run build", 11 | "start:prod": "node dist/server.js", 12 | "lint": "tslint -p tsconfig.json -c tslint.json", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "test:cov": "jest --coverage", 16 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 17 | "test:e2e": "jest --config ./test/jest-e2e.json" 18 | }, 19 | "engines": { 20 | "node": ">=8.9.0" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^6.0.0", 24 | "@nestjs/core": "^6.0.0", 25 | "@nestjs/platform-fastify": "^6.0.0", 26 | "@nestjs/swagger": "^3.1.0", 27 | "@nestjs/typeorm": "^6.0.0", 28 | "@types/extend": "^3.0.1", 29 | "@types/lodash": "^4.14.138", 30 | "cache-manager": "^2.10.0", 31 | "cache-manager-redis-store": "^1.5.0", 32 | "class-transformer": "^0.2.2", 33 | "class-validator": "^0.9.1", 34 | "crypto-js": "^3.1.9-1", 35 | "extend": "^3.0.2", 36 | "fastify-compress": "^0.11.0", 37 | "fastify-helmet": "^3.0.1", 38 | "fastify-rate-limit": "^2.3.0", 39 | "fastify-swagger": "^2.4.0", 40 | "lodash": "^4.17.15", 41 | "mssql": "^5.1.0", 42 | "mysql": "2.17.1", 43 | "nest-winston": "^1.1.2", 44 | "node-rsa": "^1.0.6", 45 | "reflect-metadata": "^0.1.13", 46 | "rxjs": "^6.5.2", 47 | "typeorm": "^0.2.17", 48 | "winston": "^3.2.1", 49 | "winston-daily-rotate-file": "^4.1.0" 50 | }, 51 | "devDependencies": { 52 | "@nestjs/testing": "^6.7.2", 53 | "@types/faker": "latest", 54 | "@types/jest": "latest", 55 | "@types/node": "latest", 56 | "@types/pino": "latest", 57 | "@types/supertest": "latest", 58 | "concurrently": "^4.1.1", 59 | "faker": "latest", 60 | "husky": "latest", 61 | "jest": "latest", 62 | "nodemon": "latest", 63 | "pino-pretty": "latest", 64 | "prettier": "latest", 65 | "pretty-quick": "latest", 66 | "rimraf": "^2.6.3", 67 | "supertest": "latest", 68 | "ts-jest": "latest", 69 | "ts-node": "latest", 70 | "tsconfig-paths": "latest", 71 | "tslint": "latest", 72 | "tslint-config-prettier": "latest", 73 | "typescript": "^3.5.0", 74 | "wait-on": "^3.2.0" 75 | }, 76 | "jest": { 77 | "moduleFileExtensions": [ 78 | "js", 79 | "json", 80 | "ts" 81 | ], 82 | "rootDir": "src", 83 | "testRegex": ".spec.ts$", 84 | "transform": { 85 | "^.+\\.(t|j)s$": "ts-jest" 86 | }, 87 | "coverageDirectory": "../coverage", 88 | "testEnvironment": "node" 89 | }, 90 | "husky": { 91 | "hooks": { 92 | "pre-commit": "pretty-quick --staged" 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/config/base.config.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '@interfaces/config.interface' 2 | 3 | export default (config: IConfig) => { 4 | config.crossDomain = { 5 | allowedOrigins: '*', 6 | allowedReferer: '*' 7 | } 8 | 9 | return config 10 | } 11 | -------------------------------------------------------------------------------- /src/config/dev.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import { IConfig } from '@interfaces/config.interface' 3 | 4 | export default (config: IConfig) => { 5 | config.port = 3001 6 | 7 | config.logger = { 8 | level: 'debug', 9 | logDir: path.join(config.baseDir, 'logs') 10 | } 11 | 12 | config.redis = { 13 | host: 'localhost', 14 | port: 6379, 15 | auth_pass: '', 16 | ttl: null, 17 | db: 1, 18 | defaultCacheTTL: 60 * 60 * 24 19 | } 20 | 21 | config.typeOrm = { 22 | type: 'mssql', 23 | host: 'locahost', 24 | database: 'fuck', 25 | username: 'bitch', 26 | password: '123456', 27 | entities: [`${config.baseDir}/**/**.model.ts`], 28 | synchronize: false, 29 | migrationsRun: false, 30 | logging: ['query'], 31 | maxQueryExecutionTime: 1500 // 慢查询记录 32 | } 33 | 34 | return config 35 | } 36 | -------------------------------------------------------------------------------- /src/config/local.config.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from '@interfaces/config.interface' 2 | import devConfig from './dev.config' 3 | 4 | export default (config: IConfig) => { 5 | return devConfig(config) 6 | } 7 | -------------------------------------------------------------------------------- /src/config/main.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as extend from 'extend' 3 | import baseConfig from './base.config' 4 | import winstonConfig from './winston.config' 5 | import { IConfig } from '@interfaces/config.interface' 6 | 7 | class Config { 8 | static instance: IConfig 9 | 10 | static getInstace(): IConfig { 11 | if (!Config.instance) { 12 | Config.instance = getConfig() 13 | } 14 | return Config.instance 15 | } 16 | } 17 | 18 | function getConfig() { 19 | const env = (process.env.NODE_ENV || 'local').toLowerCase() 20 | const config: any = { 21 | env, 22 | baseDir: path.resolve(__dirname, '..', '..'), 23 | isProd: env === 'prod' || env === 'production' 24 | } 25 | const envPath = path.resolve(__dirname, `./${config.env}.config`) 26 | try { 27 | const tempConfig = extend(config, baseConfig(config)) 28 | extend(config, require(envPath).default(tempConfig)) 29 | winstonConfig(config) 30 | } catch (err) { 31 | throw err 32 | } 33 | return config 34 | } 35 | 36 | export default Config.getInstace() 37 | -------------------------------------------------------------------------------- /src/config/winston.config.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston' 2 | import { IConfig } from '@interfaces/config.interface' 3 | 4 | require('winston-daily-rotate-file') 5 | 6 | export default (config: IConfig) => { 7 | const { level, logDir } = config.logger 8 | 9 | // @ts-ignore 10 | const transport = new winston.transports.DailyRotateFile({ 11 | dirname: logDir, 12 | filename: 'app-%DATE%.log', 13 | datePattern: 'YYYY-MM-DD', 14 | zippedArchive: true, 15 | maxSize: '20m', 16 | maxFiles: '14d' 17 | }) 18 | 19 | config.winston = { 20 | level, 21 | format: winston.format.combine( 22 | winston.format.colorize({ 23 | colors: { 24 | info: 'blue', 25 | warn: 'yellow', 26 | debug: 'blue' 27 | } 28 | }), 29 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 30 | winston.format.printf(info => { 31 | if (info.stack) { 32 | info.message = info.stack 33 | } 34 | return `${info.timestamp} [${info.pid}] ${info.level}: [${info.context || 'Application'}] ${ 35 | info.message 36 | }` 37 | }) 38 | ), 39 | defaultMeta: { pid: process.pid }, 40 | transports: [ 41 | transport, 42 | new winston.transports.Console(), 43 | new winston.transports.File({ dirname: logDir, filename: 'error.log', level: 'error' }) 44 | ] 45 | } 46 | return config 47 | } 48 | -------------------------------------------------------------------------------- /src/constants/cache.constant.ts: -------------------------------------------------------------------------------- 1 | export const HOME_PAGE_CACHE = 'home_page' 2 | -------------------------------------------------------------------------------- /src/constants/error/general.constant.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_ERROR = { 2 | code: 100001, 3 | msg: '请求失败' 4 | } 5 | 6 | export const NOT_FOUND_ERROR = { 7 | code: 100002, 8 | msg: '找不到页面' 9 | } 10 | 11 | export const BAD_REQUEST_ERROR = { 12 | code: 100003, 13 | msg: '错误的请求' 14 | } 15 | 16 | export const FORBIDDEN_ERROR = { 17 | code: 100004, 18 | msg: '无权限访问' 19 | } 20 | 21 | export const UNAUTHORIZED_ERROR = { 22 | code: 100005, 23 | msg: '无授权访问' 24 | } 25 | 26 | export const ANONYMOUSE_ERROR = { 27 | code: 100006, 28 | msg: '非法访问' 29 | } 30 | 31 | export const VALIDATION_ERROR = { 32 | code: 100007, 33 | msg: '参数校验失败' 34 | } 35 | -------------------------------------------------------------------------------- /src/constants/error/recharge.constant.ts: -------------------------------------------------------------------------------- 1 | export const USER_ACCOUNT_NOT_FOUND_ERROR = { 2 | code: 200001, 3 | msg: '用户账户不存在' 4 | } 5 | -------------------------------------------------------------------------------- /src/constants/meta.constant.ts: -------------------------------------------------------------------------------- 1 | import { CACHE_KEY_METADATA } from '@nestjs/common/cache/cache.constants' 2 | 3 | export const HTTP_ERROR_RESPONSE_CODE = '__customHttpErrorResponseCode__' 4 | export const HTTP_ERROR_RESPONSE_MESSAGE = '__customHttpErrorResponseMessage__' 5 | export const HTTP_ERROR_RESPONSE_STATUS = '__customHttpErrorResponseStatus__' 6 | export const HTTP_SUCCESS_RESPONSE_CODE = '__customHttpSuccessResponseCode__' 7 | export const HTTP_SUCCESS_RESPONSE_MESSAGE = '__customHttpSuccessResponseMessage__' 8 | export const HTTP_SUCCESS_RESPONSE_STATUS = '__customHttpSuccessResponseStatus__' 9 | export const HTTP_RESPONSE_TRANSFORM_PAGINATE = '__customHttpResponseTransformPagenate__' 10 | 11 | export const HTTP_CACHE_KEY_METADATA = CACHE_KEY_METADATA 12 | export const HTTP_CACHE_TTL_METADATA = '__customHttpCacheTTL__' 13 | -------------------------------------------------------------------------------- /src/constants/system.constant.ts: -------------------------------------------------------------------------------- 1 | export const ADMIN_NO = 'saber' 2 | -------------------------------------------------------------------------------- /src/constants/text.constant.ts: -------------------------------------------------------------------------------- 1 | export const HTTP_ERROR_SUFFIX = '失败' 2 | export const HTTP_SUCCESS_SUFFIX = '成功' 3 | export const HTTP_DEFAULT_TEXT = '数据请求' 4 | export const HTTP_DEFAULT_ERROR_TEXT = HTTP_DEFAULT_TEXT + HTTP_ERROR_SUFFIX 5 | export const HTTP_DEFAULT_SUCCESS_TEXT = HTTP_DEFAULT_TEXT + HTTP_SUCCESS_SUFFIX 6 | -------------------------------------------------------------------------------- /src/decorators/cache.decorator.ts: -------------------------------------------------------------------------------- 1 | import * as lodash from 'lodash' 2 | import * as META from '@constants/meta.constant' 3 | import { SetMetadata, CacheKey } from '@nestjs/common' 4 | 5 | // 缓存器配置 6 | interface ICacheOption { 7 | ttl?: number 8 | key?: string 9 | } 10 | 11 | /** 12 | * 统配构造器 13 | * @function HttpCache 14 | * @description 两种用法 15 | * @example @HttpCache(CACHE_KEY, 60 * 60) 16 | * @example @HttpCache({ key: CACHE_KEY, ttl: 60 * 60 }) 17 | */ 18 | export function HttpCache(option: ICacheOption): MethodDecorator 19 | export function HttpCache(key: string, ttl?: number): MethodDecorator 20 | export function HttpCache(...args: any[]) { 21 | const option = args[0] 22 | const isOption = (value): value is ICacheOption => lodash.isObject(value) 23 | const key: string = isOption(option) ? option.key : option 24 | const ttl: number = isOption(option) ? option.ttl : args[1] || null 25 | return (_, __, descriptor: PropertyDescriptor) => { 26 | if (key) { 27 | CacheKey(key)(descriptor.value) 28 | // SetMetadata(META.HTTP_CACHE_KEY_METADATA, key)(descriptor.value); 29 | } 30 | if (ttl) { 31 | SetMetadata(META.HTTP_CACHE_TTL_METADATA, ttl)(descriptor.value) 32 | } 33 | return descriptor 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/decorators/inject_service.decorator.ts: -------------------------------------------------------------------------------- 1 | export function InjectService(Repository: Function) { 2 | const metaKey = 'injected_services' 3 | return (target, name) => { 4 | const constructor = target.constructor 5 | const services = Reflect.getMetadata(metaKey, constructor) || new Map() 6 | services.set(name, Repository) 7 | Reflect.defineMetadata(metaKey, services, constructor) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/decorators/response.decorator.ts: -------------------------------------------------------------------------------- 1 | import * as lodash from 'lodash' 2 | import * as META from '@constants/meta.constant' 3 | import * as TEXT from '@constants/text.constant' 4 | import { DEFAULT_ERROR } from '@constants/error/general.constant' 5 | import { SetMetadata, HttpStatus } from '@nestjs/common' 6 | 7 | // 构造器参数 8 | interface IBuildDecoratorOption { 9 | errorCode?: number 10 | errorStatus?: HttpStatus 11 | errorMsg?: string 12 | successCode?: number 13 | successStatus?: HttpStatus 14 | successMsg?: string 15 | usePaginate?: boolean 16 | } 17 | 18 | // handle 参数 19 | interface IHandleOption { 20 | errorCode: number 21 | errorStatus: HttpStatus 22 | message: string 23 | usePaginate?: boolean 24 | } 25 | 26 | type THandleOption = string | IHandleOption 27 | 28 | // 构造请求装饰器 29 | const buildHttpDecorator = (options: IBuildDecoratorOption): MethodDecorator => { 30 | const { 31 | errorCode, 32 | successCode, 33 | errorMsg, 34 | successMsg, 35 | errorStatus, 36 | successStatus, 37 | usePaginate 38 | } = options 39 | return (_, __, descriptor: PropertyDescriptor) => { 40 | if (errorCode) { 41 | SetMetadata(META.HTTP_ERROR_RESPONSE_CODE, errorCode)(descriptor.value) 42 | } 43 | if (successCode) { 44 | SetMetadata(META.HTTP_SUCCESS_RESPONSE_CODE, successCode)(descriptor.value) 45 | } 46 | if (errorMsg) { 47 | SetMetadata(META.HTTP_ERROR_RESPONSE_MESSAGE, errorMsg)(descriptor.value) 48 | } 49 | if (successMsg) { 50 | SetMetadata(META.HTTP_SUCCESS_RESPONSE_MESSAGE, successMsg)(descriptor.value) 51 | } 52 | if (errorStatus) { 53 | SetMetadata(META.HTTP_ERROR_RESPONSE_STATUS, errorStatus)(descriptor.value) 54 | } 55 | if (successStatus) { 56 | SetMetadata(META.HTTP_SUCCESS_RESPONSE_STATUS, successStatus)(descriptor.value) 57 | } 58 | if (usePaginate) { 59 | SetMetadata(META.HTTP_RESPONSE_TRANSFORM_PAGINATE, true)(descriptor.value) 60 | } 61 | return descriptor 62 | } 63 | } 64 | 65 | /** 66 | * 异常响应装饰器 67 | * @exports success 68 | * @example @HttpProcessor.success('error message', 100010, 200) 69 | * @example @HttpProcessor.success(DEFAULT_ERROR, 200) 70 | */ 71 | export function error( 72 | message: string | { msg: string; code: number }, 73 | code?: number, 74 | status?: HttpStatus 75 | ): MethodDecorator { 76 | const isString = (value): value is string => lodash.isString(value) 77 | if (isString(message)) { 78 | return buildHttpDecorator({ 79 | errorMsg: message || DEFAULT_ERROR.msg, 80 | errorCode: code || DEFAULT_ERROR.code, 81 | errorStatus: status || HttpStatus.INTERNAL_SERVER_ERROR 82 | }) 83 | } 84 | return buildHttpDecorator({ errorMsg: message.msg, errorCode: message.code, errorStatus: status }) 85 | } 86 | /** 87 | * 成功响应装饰器 88 | * @exports success 89 | * @example @HttpProcessor.success('success message') 90 | */ 91 | export const success = (message?: string, code?: number): MethodDecorator => { 92 | return buildHttpDecorator({ 93 | successMsg: message || TEXT.HTTP_DEFAULT_SUCCESS_TEXT, 94 | successCode: code || HttpStatus.OK, 95 | successStatus: HttpStatus.OK 96 | }) 97 | } 98 | 99 | /** 100 | * 统配构造器 101 | * @function handle 102 | * @description 两种用法 103 | * @example @HttpProcessor.handle('获取某项数据') 104 | * @example @HttpProcessor.handle({ message: '操作', errorCode: 500, errorStatus, usePaginate: true }) 105 | */ 106 | export function handle(args: THandleOption): MethodDecorator 107 | export function handle(...args: any[]) { 108 | const option = args[0] 109 | const isOption = (value: THandleOption): value is IHandleOption => lodash.isObject(value) 110 | const message: string = isOption(option) ? option.message : option 111 | const errorMsg: string = message + '失败' 112 | const successMsg: string = message + '成功' 113 | const errorCode: number = isOption(option) ? option.errorCode : DEFAULT_ERROR.code 114 | const errorStatus: number = isOption(option) 115 | ? option.errorStatus 116 | : HttpStatus.INTERNAL_SERVER_ERROR 117 | const successCode: number = HttpStatus.OK 118 | const usePaginate: boolean = isOption(option) ? option.usePaginate || false : false 119 | return buildHttpDecorator({ 120 | errorCode, 121 | successCode, 122 | errorMsg, 123 | successMsg, 124 | errorStatus, 125 | usePaginate 126 | }) 127 | } 128 | 129 | /** 130 | * 分页装饰器 131 | * @exports paginate 132 | * @example @HttpProcessor.paginate() 133 | */ 134 | export const paginate = (): MethodDecorator => { 135 | return buildHttpDecorator({ usePaginate: true }) 136 | } 137 | 138 | /** 139 | * 导出的不同模块 140 | * @exports HttpProcessor 141 | * @description { error, success, paginate } 142 | */ 143 | export const HttpProcessor = { error, success, paginate } 144 | -------------------------------------------------------------------------------- /src/exceptions/bad_request.error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common' 2 | import { BAD_REQUEST_ERROR } from '@constants/error/general.constant' 3 | import { TExceptionOption } from '@interfaces/response.interface' 4 | 5 | /** 6 | * @class HttpBadRequestError 7 | * @classdesc 400 -> 请求不合法 8 | */ 9 | export class HttpBadRequestError extends HttpException { 10 | constructor(error?: TExceptionOption) { 11 | super(error || BAD_REQUEST_ERROR, HttpStatus.BAD_REQUEST) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/exceptions/forbidden.error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common' 2 | import { FORBIDDEN_ERROR } from '@constants/error/general.constant' 3 | import { TExceptionOption } from '@interfaces/response.interface' 4 | 5 | /** 6 | * @class HttpForbiddenError 7 | * @classdesc 403 -> 无权限/权限不足 8 | */ 9 | export class HttpForbiddenError extends HttpException { 10 | constructor(error?: TExceptionOption) { 11 | super(error || FORBIDDEN_ERROR, HttpStatus.FORBIDDEN) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/exceptions/platform.error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common' 2 | import { TExceptionOption } from '@interfaces/response.interface' 3 | 4 | /** 5 | * @class PlatformError 6 | * @classdesc 默认 500 -> 服务端出错 7 | */ 8 | export class PlatformError extends HttpException { 9 | constructor(options: TExceptionOption, statusCode?: HttpStatus) { 10 | super(options, statusCode || HttpStatus.INTERNAL_SERVER_ERROR) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/exceptions/unauthorized.error.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedException } from '@nestjs/common' 2 | import { FORBIDDEN_ERROR } from '@constants/error/general.constant' 3 | import { TExceptionOption } from '@interfaces/response.interface' 4 | 5 | /** 6 | * @class HttpUnauthorizedError 7 | * @classdesc 401 -> 未授权/权限验证失败 8 | */ 9 | export class HttpUnauthorizedError extends UnauthorizedException { 10 | constructor(message?: TExceptionOption, error?: string) { 11 | super(message || FORBIDDEN_ERROR, error) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/exceptions/validation.error.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common' 2 | import { VALIDATION_ERROR } from '@constants/error/general.constant' 3 | import { TExceptionOption } from '@interfaces/response.interface' 4 | 5 | /** 6 | * @class ValidationError 7 | * @classdesc 400 -> 请求有问题,这个错误经常发生在错误内层,所以 code 没有意义 8 | */ 9 | export class ValidationError extends HttpException { 10 | constructor(error?: TExceptionOption) { 11 | super(error || VALIDATION_ERROR, HttpStatus.BAD_REQUEST) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/filters/exception.filter.ts: -------------------------------------------------------------------------------- 1 | import * as lodash from 'lodash' 2 | import { Logger } from 'winston' 3 | import { ExceptionFilter, Catch, HttpException, ArgumentsHost, HttpStatus } from '@nestjs/common' 4 | import { THttpErrorResponse, TExceptionOption } from '@interfaces/response.interface' 5 | import { NOT_FOUND_ERROR, DEFAULT_ERROR } from '@constants/error/general.constant' 6 | 7 | /** 8 | * @class HttpExceptionFilter 9 | * @classdesc 拦截全局抛出的所有异常,同时任何错误将在这里被规范化输出 THttpErrorResponse 10 | */ 11 | @Catch() 12 | export class HttpExceptionFilter implements ExceptionFilter { 13 | constructor(private readonly logger: Logger) {} 14 | 15 | catch(exception: HttpException, host: ArgumentsHost) { 16 | this.logger.error(exception.toString()) 17 | const request = host.switchToHttp().getRequest().req 18 | const response = host.switchToHttp().getResponse() 19 | try { 20 | const status = exception.getStatus() || HttpStatus.INTERNAL_SERVER_ERROR 21 | const errorOption: TExceptionOption = exception.getResponse() as TExceptionOption 22 | const isString = (value): value is string => lodash.isString(value) 23 | const errCode = isString(errorOption) 24 | ? NOT_FOUND_ERROR.code 25 | : errorOption.code || NOT_FOUND_ERROR.code 26 | const errMessage = isString(errorOption) ? errorOption : errorOption.msg 27 | const errorInfo = isString(errorOption) ? null : errorOption.error 28 | const parentErrorInfo = errorInfo ? String(errorInfo) : null 29 | const isChildrenError = errorInfo && errorInfo.code && errorInfo.msg 30 | const resultError = (isChildrenError && errorInfo.msg) || parentErrorInfo 31 | const data: THttpErrorResponse = { 32 | code: errCode, 33 | msg: errMessage, 34 | error: resultError 35 | } 36 | // 对默认的 404 进行特殊处理 37 | if (status === HttpStatus.NOT_FOUND) { 38 | data.error = `资源不存在` 39 | data.msg = `接口 ${request.method} -> ${request.url} 无效` 40 | } 41 | return response.code(status).send(data) 42 | } catch (err) { 43 | return response.code(status).send(DEFAULT_ERROR) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/interceptors/cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { tap } from 'rxjs/operators' 2 | import { Observable, of } from 'rxjs' 3 | import { Logger } from 'winston' 4 | import { 5 | CacheInterceptor, 6 | ExecutionContext, 7 | CallHandler, 8 | Injectable, 9 | Inject, 10 | RequestMethod 11 | } from '@nestjs/common' 12 | import { FastifyAdapter } from '@nestjs/platform-fastify' 13 | import * as META from '@constants/meta.constant' 14 | import config from '@config' 15 | 16 | /** 17 | * @class HttpCacheInterceptor 18 | * @classdesc 自定义这个拦截器是是要弥补框架不支持 ttl 参数的缺陷 19 | */ 20 | @Injectable() 21 | export class HttpCacheInterceptor extends CacheInterceptor { 22 | constructor(cacheManager: any, reflector: any, @Inject('winston') private logger: Logger) { 23 | super(cacheManager, reflector) 24 | } 25 | // 自定义装饰器,修饰 ttl 参数 26 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 27 | const call$ = next.handle() 28 | // 如果想彻底禁用缓存服务,还是直接返回数据吧 29 | // return call$; 30 | const key = this.trackBy(context) 31 | const target = context.getHandler() 32 | const request = context.switchToHttp().getRequest().req 33 | const metaTTL = this.reflector.get(META.HTTP_CACHE_TTL_METADATA, target) 34 | const ttl = metaTTL || config.redis.ttl 35 | if (!key) { 36 | return call$ 37 | } 38 | try { 39 | const value = await this.cacheManager.get(key) 40 | if (value) { 41 | this.logger.info( 42 | `返回缓存数据:${request.method} -> ${request.url} ${JSON.stringify(value)}` 43 | ) 44 | return of(value) 45 | } 46 | return call$.pipe(tap(response => this.cacheManager.set(key, response, { ttl }))) 47 | } catch (error) { 48 | return call$ 49 | } 50 | } 51 | 52 | /** 53 | * @function trackBy 54 | * @description 目前的命中规则是:必须手动设置了 CacheKey 才会启用缓存机制,默认 ttl 为 APP_CONFIG.REDIS.defaultCacheTTL 55 | */ 56 | trackBy(context: ExecutionContext): string | undefined { 57 | const request = context.switchToHttp().getRequest() 58 | const httpServer: FastifyAdapter = this.httpAdapterHost.httpAdapter 59 | const isHttpApp = httpServer && !!httpServer.getRequestMethod 60 | const isGetRequest = 61 | isHttpApp && httpServer.getRequestMethod(request) === RequestMethod[RequestMethod.GET] 62 | const requestUrl = httpServer.getRequestUrl(request) 63 | const cacheKey = this.reflector.get(META.HTTP_CACHE_KEY_METADATA, context.getHandler()) 64 | const isMatchedCache = isHttpApp && isGetRequest && cacheKey 65 | // 缓存命中策略 -> http -> GET -> cachekey -> url -> undefined; 66 | return isMatchedCache ? cacheKey || requestUrl : undefined 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/interceptors/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable, throwError } from 'rxjs' 2 | import { catchError } from 'rxjs/operators' 3 | import { Logger } from 'winston' 4 | import { Reflector } from '@nestjs/core' 5 | import { 6 | Injectable, 7 | NestInterceptor, 8 | CallHandler, 9 | ExecutionContext, 10 | HttpStatus 11 | } from '@nestjs/common' 12 | import { PlatformError } from '@exceptions/platform.error' 13 | import { DEFAULT_ERROR } from '@constants/error/general.constant' 14 | import * as META from '@constants/meta.constant' 15 | import * as TEXT from '@constants/text.constant' 16 | 17 | /** 18 | * @class ErrorInterceptor 19 | * @classdesc 当控制器所需的 Promise service 发生错误时,错误将在此被捕获 20 | */ 21 | @Injectable() 22 | export class ErrorInterceptor implements NestInterceptor { 23 | constructor(private readonly reflector: Reflector, readonly logger: Logger) { } 24 | intercept(context: ExecutionContext, next: CallHandler): Observable { 25 | const call$ = next.handle() 26 | const target = context.getHandler() 27 | const code = 28 | this.reflector.get(META.HTTP_SUCCESS_RESPONSE_CODE, target) || DEFAULT_ERROR.code 29 | const status = 30 | this.reflector.get(META.HTTP_ERROR_RESPONSE_STATUS, target) || 31 | HttpStatus.INTERNAL_SERVER_ERROR 32 | const msg = 33 | this.reflector.get(META.HTTP_ERROR_RESPONSE_MESSAGE, target) || 34 | TEXT.HTTP_DEFAULT_ERROR_TEXT 35 | return call$.pipe( 36 | catchError(error => { 37 | if (error.response && error.status) { 38 | return throwError(error) 39 | } 40 | return throwError( 41 | new PlatformError( 42 | { 43 | code: Number(code), 44 | msg, 45 | error: error.message 46 | }, 47 | status 48 | ) 49 | ) 50 | }) 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/interceptors/logging.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { tap } from 'rxjs/operators' 3 | import { Logger } from 'winston' 4 | import { Injectable, NestInterceptor, CallHandler, ExecutionContext } from '@nestjs/common' 5 | 6 | @Injectable() 7 | export class LoggingInterceptor implements NestInterceptor { 8 | constructor(private readonly logger: Logger) {} 9 | 10 | intercept(context: ExecutionContext, next: CallHandler): Observable { 11 | const call$ = next.handle() 12 | const request = context.switchToHttp().getRequest().req 13 | const content = request.method + ' -> ' + request.url 14 | const body = request.body ? JSON.stringify(request.body) : '{}' 15 | this.logger.info(`收到请求:${content} body: ${body}`) 16 | const now = Date.now() 17 | return call$.pipe(tap(() => this.logger.info(`响应请求:${content} ${Date.now() - now}ms`))) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/interceptors/transform.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { map } from 'rxjs/operators' 3 | import { Reflector } from '@nestjs/core' 4 | import { 5 | Injectable, 6 | NestInterceptor, 7 | CallHandler, 8 | ExecutionContext, 9 | HttpStatus 10 | } from '@nestjs/common' 11 | import { THttpSuccessResponse, IHttpResultPaginate } from '@interfaces/response.interface' 12 | import * as META from '@constants/meta.constant' 13 | import * as TEXT from '@constants/text.constant' 14 | 15 | // 转换为标准的数据结构 16 | export function transformDataToPaginate(data: { 17 | rows: T[] 18 | count: number 19 | }): IHttpResultPaginate { 20 | return { 21 | list: data.rows, 22 | total: data.count 23 | } 24 | } 25 | 26 | /** 27 | * @class TransformInterceptor 28 | * @classdesc 当控制器所需的 Promise service 成功响应时,将在此被转换为标准的数据结构 IHttpResultPaginate 29 | */ 30 | @Injectable() 31 | export class TransformInterceptor implements NestInterceptor> { 32 | constructor(private readonly reflector: Reflector) {} 33 | intercept(context: ExecutionContext, next: CallHandler): Observable> { 34 | const call$ = next.handle() 35 | const target = context.getHandler() 36 | const code = 37 | this.reflector.get(META.HTTP_SUCCESS_RESPONSE_CODE, target) || HttpStatus.OK 38 | const msg = 39 | this.reflector.get(META.HTTP_SUCCESS_RESPONSE_MESSAGE, target) || 40 | TEXT.HTTP_DEFAULT_SUCCESS_TEXT 41 | const usePaginate = this.reflector.get(META.HTTP_RESPONSE_TRANSFORM_PAGINATE, target) 42 | return call$.pipe( 43 | map((item: any) => { 44 | const data = !usePaginate ? item : transformDataToPaginate(item) 45 | return { code: Number(code), msg, data } 46 | }) 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/interfaces/config.interface.ts: -------------------------------------------------------------------------------- 1 | import { LoggerOptions } from 'winston' 2 | import { ConnectionOptions } from 'typeorm' 3 | 4 | export interface IConfig { 5 | readonly env: string 6 | readonly isProd: boolean 7 | readonly baseDir: string 8 | port: number 9 | crossDomain: { 10 | allowedOrigins: string | string[] 11 | allowedReferer: string | string[] 12 | } 13 | typeOrm: ConnectionOptions 14 | winston: LoggerOptions 15 | logger: { 16 | level?: string 17 | logDir?: string 18 | } 19 | [propName: string]: any 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/response.interface.ts: -------------------------------------------------------------------------------- 1 | export type TExceptionOption = 2 | | string 3 | | { 4 | code: number 5 | msg: string 6 | error?: any 7 | } 8 | 9 | // 分页数据 10 | export interface IHttpResultPaginate { 11 | list: T 12 | total: number 13 | } 14 | 15 | // HTTP 状态返回 16 | export interface IHttpResponseBase { 17 | code: number 18 | msg: string 19 | } 20 | 21 | // HTTP error 22 | export type THttpErrorResponse = IHttpResponseBase & { 23 | error: any 24 | } 25 | 26 | // HTTP success 返回 27 | export type THttpSuccessResponse = IHttpResponseBase & { 28 | data: T | IHttpResultPaginate 29 | } 30 | 31 | // HTTP Response 32 | export type THttpResponse = THttpErrorResponse | THttpSuccessResponse 33 | -------------------------------------------------------------------------------- /src/middlewares/cors.middleware.ts: -------------------------------------------------------------------------------- 1 | import { ServerResponse } from 'http' 2 | import { Injectable, NestMiddleware, HttpStatus, RequestMethod } from '@nestjs/common' 3 | import config from '@config' 4 | 5 | /** 6 | * @class CorsMiddleware 7 | * @classdesc 用于处理 CORS 跨域 8 | */ 9 | @Injectable() 10 | export class CorsMiddleware implements NestMiddleware { 11 | use(request, response: ServerResponse, next) { 12 | const { crossDomain } = config 13 | const getMethod = method => RequestMethod[method] 14 | const origins = request.headers.origin 15 | const origin = (Array.isArray(origins) ? origins[0] : origins) || '' 16 | const allowedOrigins = [...crossDomain.allowedOrigins] 17 | const allowedMethods = [ 18 | RequestMethod.GET, 19 | RequestMethod.HEAD, 20 | RequestMethod.PUT, 21 | RequestMethod.PATCH, 22 | RequestMethod.POST, 23 | RequestMethod.DELETE 24 | ] 25 | const allowedHeaders = [ 26 | 'Authorization', 27 | 'Origin', 28 | 'No-Cache', 29 | 'X-Requested-With', 30 | 'If-Modified-Since', 31 | 'Pragma', 32 | 'Last-Modified', 33 | 'Cache-Control', 34 | 'Expires', 35 | 'Content-Type', 36 | 'X-E4M-With' 37 | ] 38 | 39 | // Allow Origin 40 | if (!origin || allowedOrigins.includes(origin)) { 41 | response.setHeader('Access-Control-Allow-Origin', origin || '*') 42 | } 43 | // Headers 44 | response.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(',')) 45 | response.setHeader('Access-Control-Allow-Methods', allowedMethods.map(getMethod).join(',')) 46 | response.setHeader('Access-Control-Max-Age', '1728000') 47 | response.setHeader('Content-Type', 'application/json; charset=utf-8') 48 | 49 | // OPTIONS Request 50 | if (request.method === getMethod(RequestMethod.OPTIONS)) { 51 | response.statusCode = HttpStatus.NO_CONTENT 52 | response.end() 53 | } else { 54 | return next() 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/middlewares/origin.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestMiddleware, HttpStatus } from '@nestjs/common' 2 | import { ANONYMOUSE_ERROR } from '@constants/error/general.constant' 3 | import config from '@config' 4 | 5 | /** 6 | * @class OriginMiddleware 7 | * @classdesc 用于验证是否为非法来源请求 8 | */ 9 | @Injectable() 10 | export class OriginMiddleware implements NestMiddleware { 11 | use(request, response, next) { 12 | const { crossDomain } = config 13 | const { origin, referer } = request.headers 14 | const checkHeader = field => 15 | !field || crossDomain.allowedReferer === '*' || crossDomain.allowedReferer.includes(field) 16 | const isVerifiedOrigin = checkHeader(origin) 17 | const isVerifiedReferer = checkHeader(referer) 18 | if (!isVerifiedOrigin && !isVerifiedReferer) { 19 | response.statusCode = HttpStatus.UNAUTHORIZED 20 | return response.end( 21 | JSON.stringify({ 22 | ...ANONYMOUSE_ERROR 23 | }) 24 | ) 25 | } 26 | // 其他通行 27 | return next() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/models/anothor_lifeblood.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 联机账户表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { Entity, Column, UpdateDateColumn } from 'typeorm' 6 | 7 | @Entity({ 8 | name: 'dt_ac_user' 9 | }) 10 | export class AnotherLifeblood { 11 | @Column({ 12 | primary: true, 13 | name: 'user_serial', 14 | type: 'bigint', 15 | comment: '用户id;这是主键。。。。' 16 | }) 17 | userSerial?: number 18 | 19 | @Column({ 20 | name: 'ac_money', 21 | type: 'money', 22 | comment: '账户总余额(分),现金+补贴' 23 | }) 24 | totalBalance?: number 25 | 26 | @Column({ 27 | name: 'ac_addo', 28 | type: 'money', 29 | comment: '现金总余额(分)' 30 | }) 31 | moneyBalance?: number 32 | 33 | @Column({ 34 | name: 'ac_subo', 35 | type: 'money', 36 | comment: '补贴总余额(分)' 37 | }) 38 | subsidyBalance?: number 39 | 40 | @Column({ 41 | name: 'ac_addm', 42 | type: 'money', 43 | comment: '累计现金充值余额(分)' 44 | }) 45 | accAmont?: number 46 | 47 | @Column({ 48 | name: 'ac_subm', 49 | type: 'money', 50 | comment: '累计补贴充值余额(分)' 51 | }) 52 | accSubsidy?: number 53 | 54 | @UpdateDateColumn({ 55 | name: 'sj', 56 | type: 'datetime', 57 | comment: '修改时间' 58 | }) 59 | updatedDate?: Date 60 | } 61 | -------------------------------------------------------------------------------- /src/models/assist.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 补贴充值记录表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm' 6 | 7 | import { ADMIN_NO } from '@constants/system.constant' 8 | 9 | export enum SubsidyMode { 10 | MISAKA_NO_0 = 0, // 手动金额补贴 11 | MISAKA_NO_1 = 1, // 自动金额补贴 12 | MISAKA_NO_2 = 2, // 提现转入已发放 13 | MISAKA_NO_3 = 3, // 提现转入未发放 14 | MISAKA_NO_4 = 4, // 旧卡金额录入 15 | MISAKA_NO_5 = 5, // 纠错录入 16 | MISAKA_NO_6 = 6, // 补卡转入已发补贴 17 | MISAKA_NO_7 = 7, // 补卡转入未发补贴 18 | MISAKA_NO_8 = 8 // 银行app充值 19 | } 20 | 21 | export enum RechargeType { 22 | COWARD = 0, // 脱机 23 | HERO = 1 // 联机 24 | } 25 | 26 | export const enum IsCleanUp { 27 | FUCK_NOW = 1, // 清除 28 | NO_FUCK = 0 // 不清除 29 | } 30 | 31 | export const enum SubsidyStatus { 32 | WAIT = 0, // 未发送充值 33 | SENDED = 1, // 已发送充值 34 | SUCCESS = 2, // 充值成功 35 | FAIL = 3 // 充值失败 36 | } 37 | 38 | @Entity({ 39 | name: 'xf_subsidy' 40 | }) 41 | export class Assist { 42 | @PrimaryGeneratedColumn('increment', { 43 | name: 'xh' 44 | }) 45 | id?: number 46 | 47 | @Column({ 48 | name: 'sub_money', 49 | type: 'money', 50 | comment: '补贴金额(分)' 51 | }) 52 | subAmount?: number 53 | 54 | @Column({ 55 | name: 'sj', 56 | type: 'datetime', 57 | comment: '创建时间' 58 | }) 59 | createdDate?: Date 60 | 61 | @Column({ 62 | name: 'update_sj', 63 | type: 'datetime', 64 | comment: '修改时间' 65 | }) 66 | updatedDate?: Date 67 | 68 | @Column({ 69 | name: 'lx', 70 | type: 'int', 71 | default: SubsidyMode.MISAKA_NO_8, 72 | comment: '补贴方式' 73 | }) 74 | subsidyMode?: SubsidyMode 75 | 76 | @Column({ 77 | name: 'sub_state', 78 | type: 'int', 79 | default: SubsidyStatus.WAIT, 80 | comment: '补贴状态' 81 | }) 82 | status?: SubsidyStatus 83 | 84 | @Column({ 85 | name: 'sub_new', 86 | type: 'money', 87 | comment: '补贴余额(分),不知道啥意思,咱也不敢问,再也不敢说,反正为0就行了', 88 | default: 0 89 | }) 90 | subBalance?: number 91 | 92 | @Column({ 93 | name: 'sub_del', 94 | type: 'int', 95 | comment: '是否清除之前的补贴;0不清,1清', 96 | default: IsCleanUp.NO_FUCK 97 | }) 98 | isCleanUp?: IsCleanUp 99 | 100 | @Column({ 101 | name: 'gly_no', 102 | type: 'nvarchar', 103 | length: 50, 104 | nullable: true, 105 | default: ADMIN_NO, 106 | comment: '操作人员编号' 107 | }) 108 | operatorCode?: string 109 | 110 | @Column({ 111 | type: 'varchar', 112 | length: 50, 113 | comment: '交易流水号' 114 | }) 115 | tranno?: string 116 | 117 | @Column({ 118 | name: 'type', 119 | type: 'int', 120 | default: RechargeType.HERO, 121 | comment: '充值类型,0脱机,1联机' 122 | }) 123 | rechargeType?: RechargeType 124 | 125 | @Column({ 126 | name: 'sub_order', 127 | type: 'int', 128 | comment: '序号,当前人最大序号+1。。。' 129 | }) 130 | order?: number 131 | 132 | @Column({ 133 | name: 'user_serial', 134 | type: 'bigint', 135 | comment: '用户id' 136 | }) 137 | userSerial?: number 138 | } 139 | -------------------------------------------------------------------------------- /src/models/lifeblood.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 联机账户表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { Entity, Column, PrimaryGeneratedColumn, UpdateDateColumn, VersionColumn } from 'typeorm' 6 | 7 | @Entity({ 8 | name: 'dt_ac_link' 9 | }) 10 | export class Lifeblood { 11 | @PrimaryGeneratedColumn('increment', { 12 | name: 'xh' 13 | }) 14 | id?: number 15 | 16 | @Column({ 17 | name: 'ac_money', 18 | type: 'money', 19 | comment: '账户总余额(分),现金+补贴' 20 | }) 21 | totalBalance?: number 22 | 23 | @Column({ 24 | name: 'ac_addo', 25 | type: 'money', 26 | comment: '现金总余额(分)' 27 | }) 28 | moneyBalance?: number 29 | 30 | @Column({ 31 | name: 'ac_subo', 32 | type: 'money', 33 | comment: '补贴总余额(分)' 34 | }) 35 | subsidyBalance?: number 36 | 37 | @Column({ 38 | name: 'ac_eacho', 39 | type: 'int', 40 | comment: '账户份数余额' 41 | }) 42 | totalCopies?: number 43 | 44 | @VersionColumn({ 45 | name: 'jl_count', 46 | type: 'int', 47 | comment: '修改次数' 48 | }) 49 | modifyCount?: number 50 | 51 | @UpdateDateColumn({ 52 | name: 'sj', 53 | type: 'datetime', 54 | comment: '修改时间' 55 | }) 56 | updatedDate?: Date 57 | 58 | @Column({ 59 | name: 'user_serial', 60 | type: 'bigint', 61 | comment: '用户id' 62 | }) 63 | userSerial?: number 64 | } 65 | -------------------------------------------------------------------------------- /src/models/monster.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 现金充值记录表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm' 6 | 7 | export const enum StrengthenStatus { 8 | WAIT = 0, // 未发送充值 9 | SENDED = 1, // 已发送充值 10 | SUCCESS = 2, // 充值成功 11 | FAIL = 3 // 充值失败 12 | } 13 | 14 | export enum RechargeMode { 15 | ONE_BOOLD = 0, // 软件读卡充值 16 | DOUBLE_KILL = 1, // 充值名单充值 17 | TRIPLE_KILL = 2, // 提现现金充值 18 | QUADRA_KILL = 3, // 补卡充值 19 | PENTA_KILL = 4 // 银行充值 20 | } 21 | 22 | export enum RechargeType { 23 | COWARD = 0, // 脱机 24 | HERO = 1 // 联机 25 | } 26 | 27 | @Entity({ 28 | name: 'dt_user' 29 | }) 30 | export class Monster { 31 | @PrimaryGeneratedColumn('increment', { 32 | name: 'user_serial', 33 | type: 'bigint', 34 | }) 35 | id?: number 36 | 37 | @Column({ 38 | name: 'user_no', 39 | type: 'varchar', 40 | comment: '用户编号' 41 | }) 42 | userNo?: number 43 | 44 | @Column({ 45 | name: 'dep_no', 46 | type: 'varchar', 47 | comment: 'string' 48 | }) 49 | depNo?: string 50 | 51 | @Column({ 52 | name: 'user_lname', 53 | type: 'varchar', 54 | comment: '用户名' 55 | }) 56 | username?: string 57 | } 58 | -------------------------------------------------------------------------------- /src/models/naked.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 原始消费记录表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn } from 'typeorm' 6 | 7 | import { ADMIN_NO } from '@constants/system.constant' 8 | 9 | export const enum Mould { 10 | SWIM = 0, // 现金模式 11 | KEEP = 1 // 计份模式 12 | } 13 | 14 | export const enum Status { 15 | GENUINE = 0, // 有效记录 16 | FORGERY = 1 // 无效记录 17 | } 18 | 19 | export const enum RecordType { 20 | SABER_NO_1 = 1, // 消费 21 | SABER_NO_2 = 2, // 正常充值 22 | SABER_NO_3 = 3, // 提现金额充值 23 | SABER_NO_4 = 4, // 补贴 24 | SABER_NO_5 = 5, // 纠错补贴 25 | SABER_NO_8 = 8, // 提现 26 | SABER_NO_12 = 12, // 无卡充值 27 | SABER_NO_13 = 13, // 补卡充值 28 | SABER_NO_16 = 16, // 补贴清零 29 | SABER_NO_31 = 31, // 金额消费纠错 30 | SABER_NO_32 = 32, // 冲正 31 | SABER_NO_34 = 34, // 日补贴 32 | SABER_NO_35 = 35, // 时段补贴 33 | SABER_NO_36 = 36, // 冻结 34 | SABER_NO_37 = 37, // 解冻 35 | SABER_NO_51 = 51, // 联机记账 36 | SABER_NO_52 = 52, // 联机份记账 37 | SABER_NO_61 = 61 // 银行app充值 38 | } 39 | 40 | export const enum RecordSource { 41 | LUCKY = 0, // 设备读取的记录 42 | LARRY = 1, // 系统生成的记录 43 | ANDY = 2, // 系统补录的记录 44 | CHERRY = 3, // 手工补录的记录 45 | HONNY = 4, // 联机收到的记录 46 | TONY = 5 // 银行app充值 47 | } 48 | 49 | @Entity({ 50 | name: 'xf_jl' 51 | }) 52 | export class Naked { 53 | @PrimaryGeneratedColumn('increment', { 54 | name: 'xh' 55 | }) 56 | id?: number 57 | 58 | @Column({ 59 | name: 'mould', 60 | type: 'int', 61 | default: Mould.SWIM, 62 | comment: '消费模式,0:现金模式 1:计份模式' 63 | }) 64 | mould?: Mould 65 | 66 | @Column({ 67 | name: 'lx', 68 | type: 'int', 69 | default: RecordType.SABER_NO_61, 70 | comment: '记录类型' 71 | }) 72 | recordType?: RecordType 73 | 74 | @Column({ 75 | name: 'jl_bh', 76 | type: 'varchar', 77 | length: 50, 78 | default: '0', 79 | comment: '记录编号' 80 | }) 81 | recordNo?: string 82 | 83 | @Column({ 84 | type: 'varchar', 85 | length: 50, 86 | comment: '交易流水号' 87 | }) 88 | tranno?: string 89 | 90 | @Column({ 91 | name: 'dev_serial', 92 | type: 'char', 93 | default: '0000000', 94 | comment: '设备id' 95 | }) 96 | devSerial?: string 97 | 98 | @Column({ 99 | name: 'sam_serial', 100 | type: 'varchar', 101 | default: '0000000', 102 | comment: 'SAM卡应用序列号:00+SAM卡前5个字节。12位十进制' 103 | }) 104 | samSerial?: string 105 | 106 | @Column({ 107 | name: 'dep_serial', 108 | type: 'bigint', 109 | comment: '部门id' 110 | }) 111 | depSerial?: number 112 | 113 | @Column({ 114 | name: 'old_money', 115 | type: 'money', 116 | comment: '交易前总余额(分)' 117 | }) 118 | preTotalBalance?: number 119 | 120 | @Column({ 121 | name: 'new_money', 122 | type: 'money', 123 | default: 0, 124 | comment: '消费/充值金额(分)' 125 | }) 126 | amount?: number 127 | 128 | @Column({ 129 | name: 'user_serial', 130 | type: 'bigint', 131 | comment: '用户id' 132 | }) 133 | userSerial?: number 134 | 135 | @Column({ 136 | name: 'sub_xh', 137 | type: 'int', 138 | nullable: true, 139 | comment: '补贴id' 140 | }) 141 | assistId?: number 142 | 143 | @Column({ 144 | name: 'state', 145 | type: 'int', 146 | comment: '记录状态,0有效,1无效', 147 | default: Status.GENUINE 148 | }) 149 | status?: Status 150 | 151 | @Column({ 152 | name: 'type', 153 | type: 'int', 154 | comment: '记录来源', 155 | default: RecordSource.TONY 156 | }) 157 | recordSource?: RecordSource 158 | 159 | @Column({ 160 | name: 'time_order', 161 | type: 'char', 162 | comment: '设备时段编号', 163 | default: '0000000000000000' 164 | }) 165 | devTimeNo?: string 166 | 167 | @Column({ 168 | name: 'time_no', 169 | type: 'char', 170 | comment: '个人时段编号', 171 | default: '0000000000000000' 172 | }) 173 | userTimeNo?: string 174 | 175 | @Column({ 176 | name: 'del_type', 177 | type: 'int', 178 | comment: '是否删除', 179 | default: 0 180 | }) 181 | isDelete?: number 182 | 183 | @Column({ 184 | name: 'sub_old', 185 | type: 'money', 186 | comment: '补贴交易前余额(分)' 187 | }) 188 | preSubsidyBalance?: number 189 | 190 | @Column({ 191 | name: 'sub_new', 192 | type: 'money', 193 | default: 0, 194 | comment: '消费/充值补贴(分)' 195 | }) 196 | subsidy?: number 197 | 198 | @Column({ 199 | name: 'each_old', 200 | type: 'int', 201 | comment: '交易前份数', 202 | default: 0 203 | }) 204 | preCopiesNum?: number 205 | 206 | @Column({ 207 | name: 'each_new', 208 | type: 'int', 209 | comment: '操作份数', 210 | default: 0 211 | }) 212 | copiesNum?: number 213 | 214 | @Column({ 215 | name: 'each_unit', 216 | type: 'int', 217 | default: 0, 218 | comment: '交易单价(分)' 219 | }) 220 | unitPrice?: number 221 | 222 | @Column({ 223 | name: 'more_money', 224 | type: 'money', 225 | default: 0, 226 | comment: '应收金额(分)' 227 | }) 228 | amountReceivable?: number 229 | 230 | @Column({ 231 | name: 'discount_rate', 232 | type: 'int', 233 | default: 100, 234 | comment: '消费折扣比例,百分比数字' 235 | }) 236 | discountRate?: number 237 | 238 | @Column({ 239 | name: 'jl_count', 240 | type: 'int', 241 | comment: '修改次数' 242 | }) 243 | modifyCount?: number 244 | 245 | @CreateDateColumn({ 246 | name: 'jl_sj', 247 | type: 'int', 248 | comment: '记录时间' 249 | }) 250 | createdDate?: Date 251 | 252 | @Column({ 253 | name: 'sj', 254 | type: 'datetime', 255 | comment: '修改时间' 256 | }) 257 | updatedDate?: Date 258 | 259 | @Column({ 260 | name: 'gly_no', 261 | type: 'nvarchar', 262 | length: 50, 263 | nullable: true, 264 | default: ADMIN_NO, 265 | comment: '操作人员编号' 266 | }) 267 | operatorCode?: string 268 | } 269 | -------------------------------------------------------------------------------- /src/models/strengthen.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 现金充值记录表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm' 6 | import { ADMIN_NO } from '@constants/system.constant' 7 | 8 | export const enum StrengthenStatus { 9 | WAIT = 0, // 未发送充值 10 | SENDED = 1, // 已发送充值 11 | SUCCESS = 2, // 充值成功 12 | FAIL = 3 // 充值失败 13 | } 14 | 15 | export enum RechargeMode { 16 | ONE_BOOLD = 0, // 软件读卡充值 17 | DOUBLE_KILL = 1, // 充值名单充值 18 | TRIPLE_KILL = 2, // 提现现金充值 19 | QUADRA_KILL = 3, // 补卡充值 20 | PENTA_KILL = 4 // 银行充值 21 | } 22 | 23 | export enum RechargeType { 24 | COWARD = 0, // 脱机 25 | HERO = 1 // 联机 26 | } 27 | 28 | @Entity({ 29 | name: 'xf_addmoney' 30 | }) 31 | export class Strengthen { 32 | @PrimaryGeneratedColumn('increment', { 33 | name: 'xh' 34 | }) 35 | id?: number 36 | 37 | @Column({ 38 | name: 'input_money', 39 | type: 'money', 40 | comment: '输入金额(分)' 41 | }) 42 | inputMoney?: number 43 | 44 | @Column({ 45 | name: 'add_money', 46 | type: 'money', 47 | comment: '实际金额(分)' 48 | }) 49 | realMoney?: number 50 | 51 | @Column({ 52 | name: 'gly_money', 53 | type: 'money', 54 | default: 0, 55 | nullable: true, 56 | comment: '管理费?手续费?(分)' 57 | }) 58 | commission?: number 59 | 60 | @Column({ 61 | name: 'sj', 62 | type: 'datetime', 63 | comment: '创建时间' 64 | }) 65 | createdDate?: Date 66 | 67 | @Column({ 68 | name: 'update_sj', 69 | type: 'datetime', 70 | comment: '修改时间' 71 | }) 72 | updatedDate?: Date 73 | 74 | @Column({ 75 | name: 'add_state', 76 | type: 'int', 77 | default: StrengthenStatus.SUCCESS, 78 | comment: '充值状态' 79 | }) 80 | status?: StrengthenStatus 81 | 82 | @Column({ 83 | name: 'gly_no', 84 | type: 'nvarchar', 85 | length: 50, 86 | nullable: true, 87 | default: ADMIN_NO, 88 | comment: '操作人员编号' 89 | }) 90 | operatorCode?: string 91 | 92 | @Column({ 93 | name: 'lx', 94 | type: 'int', 95 | default: RechargeMode.PENTA_KILL, 96 | comment: '充值方式' 97 | }) 98 | rechargeMode?: RechargeMode 99 | 100 | @Column({ 101 | name: 'type', 102 | type: 'int', 103 | default: RechargeType.HERO, 104 | comment: '充值类型,0脱机,1联机' 105 | }) 106 | rechargeType?: RechargeType 107 | 108 | @Column({ 109 | type: 'varchar', 110 | length: 50, 111 | comment: '交易流水号' 112 | }) 113 | tranno?: string 114 | 115 | @Column({ 116 | name: 'user_serial', 117 | type: 'bigint', 118 | comment: '用户id' 119 | }) 120 | userSerial?: number 121 | 122 | // @Column({ 123 | // name: 'bank_id', 124 | // type: 'int', 125 | // comment: '银行id' 126 | // }) 127 | // bankId?: number; 128 | 129 | // @Column({ 130 | // name: 'recharge_user', 131 | // type: 'int', 132 | // comment: '充值人id' 133 | // }) 134 | // recharge_user?: number; 135 | } 136 | -------------------------------------------------------------------------------- /src/models/undisguised.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 消费明细记录表 3 | * 想疯狂打人!!!!!!1 4 | */ 5 | import { 6 | Entity, 7 | Column, 8 | PrimaryGeneratedColumn, 9 | BeforeInsert 10 | } from 'typeorm' 11 | import { ADMIN_NO } from '@constants/system.constant' 12 | 13 | export const enum RecordSource { 14 | BITCH = 0, // 脱机 15 | DOG = 1 // 联机 16 | } 17 | 18 | export const enum Mould { 19 | SWIM = 0, // 现金模式 20 | KEEP = 1 // 计份模式 21 | } 22 | 23 | export const enum RecordType { 24 | SABER_NO_1 = 1, // 消费 25 | SABER_NO_2 = 2, // 正常充值 26 | SABER_NO_3 = 3, // 提现金额充值 27 | SABER_NO_4 = 4, // 补贴 28 | SABER_NO_5 = 5, // 纠错补贴 29 | SABER_NO_8 = 8, // 提现 30 | SABER_NO_12 = 12, // 无卡充值 31 | SABER_NO_13 = 13, // 补卡充值 32 | SABER_NO_16 = 16, // 补贴清零 33 | SABER_NO_31 = 31, // 金额消费纠错 34 | SABER_NO_32 = 32, // 冲正 35 | SABER_NO_34 = 34, // 日补贴 36 | SABER_NO_35 = 35, // 时段补贴 37 | SABER_NO_36 = 36, // 冻结 38 | SABER_NO_37 = 37, // 解冻 39 | SABER_NO_51 = 51, // 联机记账 40 | SABER_NO_52 = 52, // 联机份记账 41 | SABER_NO_61 = 61 // 银行app充值 42 | } 43 | 44 | @Entity({ 45 | name: 'xf_mx' 46 | }) 47 | export class Undisguised { 48 | @PrimaryGeneratedColumn('increment', { 49 | name: 'xh' 50 | }) 51 | id?: number 52 | 53 | @Column({ 54 | name: 'mould', 55 | type: 'int', 56 | default: Mould.SWIM, 57 | comment: '消费模式,0:现金模式 1:计份模式' 58 | }) 59 | mould?: Mould 60 | 61 | @Column({ 62 | name: 'lx', 63 | type: 'int', 64 | default: RecordType.SABER_NO_61, 65 | comment: '记录类型' 66 | }) 67 | recordType?: RecordType 68 | 69 | @Column({ 70 | name: 'type', 71 | type: 'int', 72 | comment: '记录来源', 73 | default: RecordSource.DOG 74 | }) 75 | recordSource?: RecordSource 76 | 77 | @Column({ 78 | name: 'jl_bh', 79 | type: 'varchar', 80 | default: '00000000', 81 | comment: '记录编号' 82 | }) 83 | recordNo?: string 84 | 85 | @Column({ 86 | name: 'jl_count', 87 | type: 'int', 88 | comment: '修改次数' 89 | }) 90 | modifyCount?: number 91 | 92 | @Column({ 93 | name: 'user_serial', 94 | type: 'bigint', 95 | comment: '用户id' 96 | }) 97 | userSerial?: number 98 | 99 | @Column({ 100 | name: 'dep_serial', 101 | type: 'bigint', 102 | comment: '部门id' 103 | }) 104 | depSerial?: number 105 | 106 | @Column({ 107 | name: 'rq', 108 | type: 'datetime', 109 | comment: '消费日期' 110 | }) 111 | createdDate?: Date 112 | 113 | @Column({ 114 | name: 'jl_sj', 115 | type: 'datetime', 116 | comment: '记录时间' 117 | }) 118 | recordTime?: Date 119 | 120 | @Column({ 121 | name: 'dev_serial', 122 | type: 'char', 123 | default: '0000000', 124 | comment: '设备id' 125 | }) 126 | devSerial?: string 127 | 128 | @Column({ 129 | name: 'old_money', 130 | type: 'money', 131 | comment: '交易前总余额(分)' 132 | }) 133 | preTotalBalance?: number 134 | 135 | @Column({ 136 | name: 'new_money', 137 | type: 'money', 138 | default: 0, 139 | comment: '实收金额金额(分)。鬼知道怎么用!默认添0就行了!' 140 | }) 141 | retardedField?: number 142 | 143 | @Column({ 144 | name: 'new_add', 145 | type: 'money', 146 | default: 0, 147 | comment: '充值金额(分)' 148 | }) 149 | rechargeAmount?: number 150 | 151 | @Column({ 152 | name: 'old_add', 153 | type: 'money', 154 | default: 0, 155 | comment: '充值前金额(分)' 156 | }) 157 | preRechargeAmount?: number 158 | 159 | @Column({ 160 | name: 'old_sub', 161 | type: 'money', 162 | comment: '补贴交易前余额(分)' 163 | }) 164 | preSubsidyBalance?: number 165 | 166 | @Column({ 167 | name: 'new_sub', 168 | type: 'money', 169 | default: 0, 170 | comment: '消费/充值补贴(分)' 171 | }) 172 | subsidy?: number 173 | 174 | @Column({ 175 | name: 'new_del', 176 | type: 'int', 177 | comment: '提现的金额', 178 | default: 0 179 | }) 180 | withdrawAmount?: number 181 | 182 | @Column({ 183 | name: 'del_count', 184 | type: 'int', 185 | comment: '提现次数', 186 | default: 0 187 | }) 188 | withdrawCount?: number 189 | 190 | @Column({ 191 | name: 'add_count', 192 | type: 'int', 193 | default: 1, 194 | comment: '充值次数。呵呵呵呵呵呵' 195 | }) 196 | rechargeCount?: number 197 | 198 | @Column({ 199 | name: 'save_add', 200 | type: 'money', 201 | comment: 'new_add + old_add' 202 | }) 203 | currentAmount?: number 204 | 205 | @Column({ 206 | name: 'save_sub', 207 | type: 'money', 208 | comment: 'new_sub + old_sub' 209 | }) 210 | currentSubsidy?: number 211 | 212 | @Column({ 213 | name: 'save_money', 214 | type: 'money', 215 | comment: 'new_add + old_money' 216 | }) 217 | currentTotalBalance?: number 218 | 219 | @Column({ 220 | name: 'new_each', 221 | type: 'money', 222 | default: 0, 223 | comment: '按份消费的金额' 224 | }) 225 | eachAmount?: number 226 | 227 | @Column({ 228 | name: 'new_zero', 229 | type: 'money', 230 | default: 0, 231 | comment: '补贴清零的金额' 232 | }) 233 | zeroAmount?: number 234 | 235 | @Column({ 236 | name: 'del_zero', 237 | type: 'int', 238 | default: 0, 239 | comment: '补贴清零的次数' 240 | }) 241 | zeroCount?: number 242 | 243 | @Column({ 244 | name: 'new_edit', 245 | type: 'money', 246 | default: 0, 247 | comment: '充正的金额' 248 | }) 249 | editAmount?: number 250 | 251 | @Column({ 252 | name: 'del_edit', 253 | type: 'int', 254 | default: 0, 255 | comment: '充正的次数' 256 | }) 257 | editCount?: number 258 | 259 | @Column({ 260 | name: 'dev_time_no', 261 | type: 'char', 262 | comment: '设备时段编号', 263 | default: '0000000000000000' 264 | }) 265 | devTimeNo?: string 266 | 267 | @Column({ 268 | name: 'time_no', 269 | type: 'char', 270 | comment: '个人时段编号', 271 | default: '0000000000000000' 272 | }) 273 | userTimeNo?: string 274 | 275 | @Column({ 276 | name: 'sj', 277 | type: 'datetime', 278 | comment: '修改时间' 279 | }) 280 | updatedDate?: Date 281 | 282 | @Column({ 283 | name: 'gly_no', 284 | type: 'nvarchar', 285 | length: 50, 286 | nullable: true, 287 | default: ADMIN_NO, 288 | comment: '操作人员编号' 289 | }) 290 | operatorCode?: string 291 | 292 | @Column({ 293 | type: 'varchar', 294 | length: 50, 295 | comment: '交易流水号' 296 | }) 297 | tranno?: string 298 | 299 | @BeforeInsert() 300 | computedCurrentAmount() { 301 | this.currentAmount = (this.preRechargeAmount || 0) + (this.rechargeAmount || 0) 302 | } 303 | 304 | @BeforeInsert() 305 | computedCurrentSubsidy() { 306 | this.currentSubsidy = (this.preSubsidyBalance || 0) + (this.subsidy || 0) 307 | } 308 | 309 | @BeforeInsert() 310 | computedCurrentTotalBalance() { 311 | this.currentTotalBalance = (this.preTotalBalance || 0) + (this.rechargeAmount || 0) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/modules/account/recharge/recharge.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Body, Post, Inject } from '@nestjs/common' 2 | import { ApiUseTags, ApiOperation } from '@nestjs/swagger' 3 | import { RechargeParamsDto } from './recharge.dto' 4 | import { RechargeService } from './recharge.service' 5 | 6 | @ApiUseTags('账户充值服务') 7 | @Controller('account/recharge') 8 | export class RechargeController { 9 | constructor( 10 | @Inject(RechargeService) 11 | private readonly rechargeService: RechargeService 12 | ) {} 13 | 14 | @Post() 15 | @ApiOperation({ title: '人员账户充值' }) 16 | async recharge(@Body() body: RechargeParamsDto) { 17 | return await this.rechargeService.recharge(body) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/account/recharge/recharge.dto.ts: -------------------------------------------------------------------------------- 1 | import { Max, Min, IsInt, IsNumber, MaxLength } from 'class-validator' 2 | import { Type } from 'class-transformer' 3 | import { ApiModelProperty } from '@nestjs/swagger' 4 | 5 | export const enum RechargeType { 6 | MONEY = 'money', 7 | SUBSIDY = 'subsidy' 8 | } 9 | 10 | export class RechargeParamsDto { 11 | @ApiModelProperty({ 12 | required: true, 13 | enum: [RechargeType.MONEY, RechargeType.SUBSIDY], 14 | description: '充值类型:金额,补贴' 15 | }) 16 | readonly type: RechargeType 17 | 18 | @ApiModelProperty({ 19 | required: true, 20 | description: '充值金额,单位(元)' 21 | }) 22 | @IsNumber({ allowNaN: false }, { message: '错误的充值金额' }) 23 | @Max(100000, { message: '充值金额最多不能超过10万' }) 24 | @Min(1, { message: '充值金额不能小于1元' }) 25 | @Type(() => Number) 26 | readonly amount: number 27 | 28 | @ApiModelProperty({ 29 | required: true, 30 | description: '需要充值的用户id' 31 | }) 32 | @IsInt({ message: '错误的用户id' }) 33 | @Type(() => Number) 34 | readonly userId: number 35 | 36 | @ApiModelProperty({ 37 | required: true, 38 | description: '交易流水号' 39 | }) 40 | @MaxLength(50, { 41 | message: '交易流水号最多不能超过50个字符' 42 | }) 43 | readonly tranno?: string 44 | } 45 | -------------------------------------------------------------------------------- /src/modules/account/recharge/recharge.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { TypeOrmModule } from '@nestjs/typeorm' 3 | import { RechargeController } from '@modules/account/recharge/recharge.controller' 4 | import { RechargeService } from '@modules/account/recharge/recharge.service' 5 | import { Strengthen } from '@models/strengthen.model' 6 | import { Lifeblood } from '@models/lifeblood.model' 7 | import { AnotherLifeblood } from '@models/anothor_lifeblood.model' 8 | import { Naked } from '@models/naked.model' 9 | import { Undisguised } from '@models/undisguised.model' 10 | import { Monster } from '@models/monster.model' 11 | import { Assist } from '@models/assist.model' 12 | 13 | @Module({ 14 | imports: [ 15 | TypeOrmModule.forFeature([Strengthen, Lifeblood, Naked, Undisguised, AnotherLifeblood, Monster, Assist]) 16 | ], 17 | controllers: [RechargeController], 18 | providers: [RechargeService] 19 | }) 20 | export class RechargeModule { } 21 | -------------------------------------------------------------------------------- /src/modules/account/recharge/recharge.service.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'winston' 2 | import { Injectable, Inject } from '@nestjs/common' 3 | import { InjectRepository } from '@nestjs/typeorm' 4 | import { ADMIN_NO } from '@constants/system.constant' 5 | import { Repository, Transaction, TransactionRepository } from 'typeorm' 6 | import { Strengthen, StrengthenStatus, RechargeMode, RechargeType as RechargeModelType } from '@models/strengthen.model' 7 | import { Lifeblood } from '@models/lifeblood.model' 8 | import { AnotherLifeblood } from '@models/anothor_lifeblood.model' 9 | import { Naked, Mould, RecordType, Status, RecordSource } from '@models/naked.model' 10 | import { Undisguised, RecordType as UndisRecordType } from '@models/undisguised.model' 11 | import { Monster } from '@models/monster.model' 12 | import { Assist, SubsidyMode, SubsidyStatus, IsCleanUp, RechargeType as AssistRechargeType } from '@models/assist.model' 13 | import { BaseService, Service } from '@processors/base/base.service' 14 | import { PlatformError } from '@exceptions/platform.error' 15 | import { RechargeParamsDto, RechargeType } from './recharge.dto' 16 | import { USER_ACCOUNT_NOT_FOUND_ERROR } from '@constants/error/recharge.constant' 17 | 18 | /** 19 | * 注释是不可能写的, 20 | * 它不配拥有注释 21 | */ 22 | @Injectable() 23 | export class RechargeService extends BaseService { 24 | constructor( 25 | @Inject('winston') private logger: Logger, 26 | @InjectRepository(Strengthen) private strengthenRepo: Repository, 27 | @InjectRepository(Lifeblood) private lifebloodRepo: Repository, 28 | @InjectRepository(AnotherLifeblood) private anotherlifebloodRepo: Repository, 29 | @InjectRepository(Naked) private nakedRepo: Repository, 30 | @InjectRepository(Undisguised) private undisguisedRepo: Repository, 31 | @InjectRepository(Monster) private monsterRepo: Repository, 32 | @InjectRepository(Assist) private assistRepo: Repository 33 | ) { 34 | super() 35 | } 36 | private strengthenService: Service 37 | private lifebloodService: Service 38 | private anotherlifebloodService: Service 39 | private nakedService: Service 40 | private undisguisedService: Service 41 | private monsterService: Service 42 | private assistService: Service 43 | 44 | @Transaction() 45 | async recharge( 46 | body: RechargeParamsDto, 47 | @TransactionRepository(Strengthen) strengthenRepo?: Repository, 48 | @TransactionRepository(Lifeblood) lifebloodRepo?: Repository, 49 | @TransactionRepository(AnotherLifeblood) anotherlifebloodRepo?: Repository, 50 | @TransactionRepository(Naked) nakedRepo?: Repository, 51 | @TransactionRepository(Undisguised) undisguisedRepo?: Repository, 52 | @TransactionRepository(Monster) monsterRepo?: Repository, 53 | @TransactionRepository(Assist) assistRepo?: Repository 54 | ) { 55 | this.strengthenService = this.getService(strengthenRepo || this.strengthenRepo) 56 | this.lifebloodService = this.getService(lifebloodRepo || this.lifebloodRepo) 57 | this.anotherlifebloodService = this.getService( 58 | anotherlifebloodRepo || this.anotherlifebloodRepo 59 | ) 60 | this.nakedService = this.getService(nakedRepo || this.nakedRepo) 61 | this.undisguisedService = this.getService(undisguisedRepo || this.undisguisedRepo) 62 | this.monsterService = this.getService(monsterRepo || this.monsterRepo) 63 | this.assistService = this.getService(assistRepo || this.assistRepo) 64 | 65 | const [userinfo, lifeblood, anotherLifeblood] = await Promise.all([ 66 | await this.monsterService.findOne({ 67 | id: body.userId 68 | }), 69 | await this.lifebloodService.findOne({ 70 | userSerial: body.userId 71 | }), 72 | await this.anotherlifebloodService.findOne({ 73 | userSerial: body.userId 74 | }) 75 | ]) 76 | if (!lifeblood || !anotherLifeblood || !userinfo) { 77 | throw new PlatformError(USER_ACCOUNT_NOT_FOUND_ERROR) 78 | } 79 | try { 80 | switch (body.type) { 81 | case RechargeType.MONEY: 82 | return await this.rechargeMoney(body, userinfo, lifeblood, anotherLifeblood) 83 | case RechargeType.SUBSIDY: 84 | return await this.rechargeSubsidy(body, userinfo, lifeblood, anotherLifeblood) 85 | default: 86 | } 87 | } catch (err) { 88 | this.logger.error(err) 89 | throw err 90 | } 91 | } 92 | 93 | async rechargeMoney( 94 | { tranno, userId, amount }: RechargeParamsDto, 95 | userinfo: Monster, 96 | lifeblood: Lifeblood, 97 | anotherLifeblood: AnotherLifeblood 98 | ) { 99 | const rechargeAmount = amount * 100 100 | const totalBalance = (Number(lifeblood.totalBalance) || 0) + rechargeAmount 101 | const moneyBalance = (Number(lifeblood.moneyBalance) || 0) + rechargeAmount 102 | const accAmont = (Number(anotherLifeblood.accAmont) || 0) + rechargeAmount 103 | return await Promise.all([ 104 | await this.strengthenService.create({ 105 | tranno, 106 | commission: 0, 107 | createdDate: new Date(), 108 | updatedDate: new Date(), 109 | status: StrengthenStatus.SUCCESS, 110 | operatorCode: ADMIN_NO, 111 | userSerial: userId, 112 | rechargeType: RechargeModelType.HERO, 113 | rechargeMode: RechargeMode.PENTA_KILL, 114 | inputMoney: rechargeAmount, 115 | realMoney: rechargeAmount, 116 | }), 117 | await this.lifebloodService.update( 118 | { 119 | totalBalance, 120 | moneyBalance, 121 | updatedDate: new Date(), 122 | }, 123 | { userSerial: userId } 124 | ), 125 | await this.anotherlifebloodService.update( 126 | { 127 | totalBalance, 128 | moneyBalance, 129 | accAmont, 130 | updatedDate: new Date(), 131 | }, 132 | { userSerial: userId } 133 | ), 134 | await this.nakedService.create({ 135 | tranno, 136 | recordNo: '0', 137 | devSerial: '0000000', 138 | samSerial: '0', 139 | devTimeNo: '0000000000000000', 140 | userTimeNo: '0000000000000000', 141 | subsidy: 0, 142 | preCopiesNum: 0, 143 | unitPrice: 0, 144 | depSerial: Number(userinfo.depNo), 145 | isDelete: 0, 146 | operatorCode: ADMIN_NO, 147 | amountReceivable: 0, 148 | discountRate: 100, 149 | createdDate: new Date(), 150 | updatedDate: new Date(), 151 | mould: Mould.SWIM, 152 | status: Status.GENUINE, 153 | amount: rechargeAmount, 154 | modifyCount: lifeblood.modifyCount, 155 | recordSource: RecordSource.TONY, 156 | recordType: RecordType.SABER_NO_61, 157 | userSerial: userId, 158 | copiesNum: lifeblood.totalCopies, 159 | preSubsidyBalance: lifeblood.subsidyBalance, 160 | preTotalBalance: lifeblood.totalBalance 161 | }), 162 | await this.undisguisedService.create({ 163 | tranno, 164 | rechargeAmount, 165 | subsidy: 0, 166 | retardedField: 0, 167 | withdrawAmount: 0, 168 | withdrawCount: 0, 169 | eachAmount: 0, 170 | zeroAmount: 0, 171 | zeroCount: 0, 172 | editAmount: 0, 173 | editCount: 0, 174 | recordNo: '0', 175 | devSerial: '0000000', 176 | devTimeNo: '0000000000000000', 177 | userTimeNo: '0000000000000000', 178 | userSerial: userId, 179 | recordTime: new Date(), 180 | createdDate: new Date(), 181 | updatedDate: new Date(), 182 | depSerial: Number(userinfo.depNo), 183 | operatorCode: ADMIN_NO, 184 | modifyCount: lifeblood.modifyCount, 185 | recordType: UndisRecordType.SABER_NO_61, 186 | preSubsidyBalance: lifeblood.subsidyBalance, 187 | preRechargeAmount: anotherLifeblood.moneyBalance, 188 | preTotalBalance: lifeblood.totalBalance 189 | }) 190 | ]) 191 | } 192 | 193 | async rechargeSubsidy( 194 | { tranno, userId, amount }: RechargeParamsDto, 195 | userinfo: Monster, 196 | lifeblood: Lifeblood, 197 | anotherLifeblood: AnotherLifeblood 198 | ) { 199 | const rechargeAmount = amount * 100 200 | const totalBalance = (Number(lifeblood.totalBalance) || 0) + rechargeAmount 201 | const subsidyBalance = (Number(lifeblood.subsidyBalance) || 0) + rechargeAmount 202 | const accSubsidy = (Number(anotherLifeblood.accSubsidy) || 0) + rechargeAmount 203 | const maxOrderAssist = await this.assistService.find({ 204 | limit: 1, 205 | where: { userSerial: userId }, 206 | orderBy: 'order_desc' 207 | }) 208 | const order = maxOrderAssist.length > 0 ? Number(maxOrderAssist[0].order) + 1 : 1 209 | const newAssist = await this.assistService.create({ 210 | order, 211 | tranno, 212 | subBalance: 0, 213 | createdDate: new Date(), 214 | updatedDate: new Date(), 215 | rechargeType: AssistRechargeType.HERO, 216 | subAmount: rechargeAmount, 217 | operatorCode: ADMIN_NO, 218 | userSerial: userId, 219 | isCleanUp: IsCleanUp.NO_FUCK, 220 | status: SubsidyStatus.SUCCESS, 221 | subsidyMode: SubsidyMode.MISAKA_NO_8, 222 | }); 223 | return await Promise.all([ 224 | await this.lifebloodService.update( 225 | { 226 | totalBalance, 227 | subsidyBalance, 228 | updatedDate: new Date(), 229 | }, 230 | { userSerial: userId } 231 | ), 232 | await this.anotherlifebloodService.update( 233 | { 234 | totalBalance, 235 | subsidyBalance, 236 | accSubsidy, 237 | updatedDate: new Date(), 238 | }, 239 | { userSerial: userId } 240 | ), 241 | await this.nakedService.create({ 242 | tranno, 243 | recordNo: '0', 244 | devSerial: '0000000', 245 | samSerial: '0', 246 | devTimeNo: '0000000000000000', 247 | userTimeNo: '0000000000000000', 248 | preCopiesNum: 0, 249 | unitPrice: 0, 250 | depSerial: Number(userinfo.depNo), 251 | isDelete: 0, 252 | operatorCode: ADMIN_NO, 253 | amountReceivable: 0, 254 | discountRate: 100, 255 | createdDate: new Date(), 256 | updatedDate: new Date(), 257 | mould: Mould.SWIM, 258 | status: Status.GENUINE, 259 | amount: 0, 260 | assistId: newAssist.id, 261 | subsidy: rechargeAmount, 262 | modifyCount: lifeblood.modifyCount, 263 | recordSource: RecordSource.TONY, 264 | recordType: RecordType.SABER_NO_61, 265 | userSerial: userId, 266 | copiesNum: lifeblood.totalCopies, 267 | preSubsidyBalance: lifeblood.subsidyBalance, 268 | preTotalBalance: lifeblood.totalBalance 269 | }), 270 | await this.undisguisedService.create({ 271 | tranno, 272 | rechargeAmount: 0, 273 | retardedField: 0, 274 | withdrawAmount: 0, 275 | withdrawCount: 0, 276 | eachAmount: 0, 277 | zeroAmount: 0, 278 | zeroCount: 0, 279 | editAmount: 0, 280 | editCount: 0, 281 | recordNo: '0', 282 | devSerial: '0000000', 283 | devTimeNo: '0000000000000000', 284 | userTimeNo: '0000000000000000', 285 | userSerial: userId, 286 | recordTime: new Date(), 287 | createdDate: new Date(), 288 | updatedDate: new Date(), 289 | depSerial: Number(userinfo.depNo), 290 | subsidy: rechargeAmount, 291 | operatorCode: ADMIN_NO, 292 | modifyCount: lifeblood.modifyCount, 293 | recordType: UndisRecordType.SABER_NO_61, 294 | preSubsidyBalance: lifeblood.subsidyBalance, 295 | preRechargeAmount: anotherLifeblood.moneyBalance, 296 | preTotalBalance: lifeblood.totalBalance 297 | }) 298 | ]) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/modules/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Get, Controller } from '@nestjs/common' 2 | import { ApiExcludeEndpoint } from '@nestjs/swagger' 3 | import { HttpCache } from '@decorators/cache.decorator' 4 | import { HOME_PAGE_CACHE } from '@constants/cache.constant' 5 | 6 | @Controller() 7 | export class AppController { 8 | @Get() 9 | @ApiExcludeEndpoint() 10 | @HttpCache(HOME_PAGE_CACHE, 60 * 60) 11 | root(): string { 12 | return 'hello world!' 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, MiddlewareConsumer, RequestMethod } from '@nestjs/common' 2 | import { WinstonModule } from 'nest-winston' 3 | import { APP_INTERCEPTOR } from '@nestjs/core' 4 | import { TypeOrmModule } from '@nestjs/typeorm' 5 | import config from '@config' 6 | 7 | // 首页控制器 8 | import { AppController } from './app.controller' 9 | // 拦截器 10 | import { HttpCacheInterceptor } from '@interceptors/cache.interceptor' 11 | // 中间件 12 | import { CorsMiddleware } from '@middlewares/cors.middleware' 13 | import { OriginMiddleware } from '@middlewares/origin.middleware' 14 | // 公共模块 15 | import { HttpCacheModule } from '@processors/cache/cache.module' 16 | import { HelperModule } from '@processors/helper/helper.module' 17 | // 业务模块 18 | import { AuthorizeModule } from '@modules/authorize/authorize.module' 19 | import { RechargeModule } from '@modules/account/recharge/recharge.module' 20 | 21 | @Module({ 22 | imports: [ 23 | WinstonModule.forRoot(config.winston), 24 | TypeOrmModule.forRoot({ ...config.typeOrm }), 25 | HttpCacheModule, 26 | HelperModule, 27 | AuthorizeModule, 28 | RechargeModule 29 | ], 30 | controllers: [AppController], 31 | providers: [ 32 | { 33 | provide: APP_INTERCEPTOR, 34 | useClass: HttpCacheInterceptor 35 | } 36 | ] 37 | }) 38 | export class AppModule { 39 | configure(consumer: MiddlewareConsumer) { 40 | consumer.apply(CorsMiddleware, OriginMiddleware).forRoutes({ 41 | path: '(.*)?', 42 | method: RequestMethod.ALL 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modules/authorize/authorize.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Inject, Query, Get } from '@nestjs/common' 2 | import { ApiUseTags, ApiOperation } from '@nestjs/swagger' 3 | import { RsaService } from '@processors/helper/crypto.serivce' 4 | import { ISecretKeys } from '@processors/helper/crypto.serivce' 5 | import { RsaOptionsDto } from './authorize.dto' 6 | 7 | @ApiUseTags('授权认证') 8 | @Controller('authorize') 9 | export class AuthorizeController { 10 | constructor(@Inject(RsaService) private rsaService: RsaService) {} 11 | 12 | @Get('gen_secret_key') 13 | @ApiOperation({ title: '生成RSA公钥和私钥' }) 14 | genPubKey(@Query() { pkcsSize = 128, pkcsType = 'pkcs1' }: RsaOptionsDto): ISecretKeys { 15 | return this.rsaService 16 | .setPkcsType(pkcsType) 17 | .setPkcsSize(pkcsSize) 18 | .genSecretKeys() 19 | } 20 | 21 | @Get('gen_temporary_token') 22 | @ApiOperation({ title: '生成临时token' }) 23 | genTemporaryToken(@Query() { pkcsSize = 128, pkcsType = 'pkcs1' }: RsaOptionsDto): string { 24 | const { privateKey } = this.rsaService 25 | .setPkcsType(pkcsType) 26 | .setPkcsSize(pkcsSize) 27 | .genSecretKeys() 28 | // 获取当前的时间戳 29 | const timestamp = String(new Date().getTime()) 30 | // 时间戳倒序 31 | const reverseTimestamp = timestamp 32 | .split('') 33 | .reverse() 34 | .join('') 35 | // 私钥加密 36 | return this.rsaService.priKeyEncrypt(privateKey, reverseTimestamp) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/authorize/authorize.dto.ts: -------------------------------------------------------------------------------- 1 | import { Max, IsInt, Min } from 'class-validator' 2 | import { Type } from 'class-transformer' 3 | import { ApiModelProperty } from '@nestjs/swagger' 4 | 5 | export class RsaOptionsDto { 6 | @ApiModelProperty({ 7 | enum: ['pkcs1', 'pkcs8'], 8 | description: '加密类型' 9 | }) 10 | readonly pkcsType?: string 11 | 12 | @ApiModelProperty({ 13 | default: 128, 14 | description: '密钥长度' 15 | }) 16 | @IsInt({ message: '必须是整数' }) 17 | @Max(512, { message: '大小不能超过512' }) 18 | @Min(64, { message: '大小不能少于64' }) 19 | @Type(() => Number) 20 | readonly pkcsSize?: number 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/authorize/authorize.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { AuthorizeController } from '@modules/authorize/authorize.controller' 3 | 4 | @Module({ 5 | controllers: [AuthorizeController] 6 | }) 7 | export class AuthorizeModule {} 8 | -------------------------------------------------------------------------------- /src/pipes/validation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { validate } from 'class-validator' 2 | import { plainToClass } from 'class-transformer' 3 | import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common' 4 | import { ValidationError } from '@exceptions/validation.error' 5 | import { VALIDATION_ERROR } from '@constants/error/general.constant' 6 | 7 | /** 8 | * @class ValidationPipe 9 | * @classdesc 验证所有使用 class-validator 的地方的 class 模型 10 | */ 11 | @Injectable() 12 | export class ValidationPipe implements PipeTransform { 13 | async transform(value: any, { metatype }: ArgumentMetadata) { 14 | if (!metatype || !this.toValidate(metatype)) { 15 | return value 16 | } 17 | const object = plainToClass(metatype, value) 18 | const errors = await validate(object) 19 | if (errors.length > 0) { 20 | const errorMessage = errors.map(error => Object.values(error.constraints).join(';')).join(';') 21 | throw new ValidationError({ 22 | ...VALIDATION_ERROR, 23 | error: errorMessage 24 | }) 25 | } 26 | return value 27 | } 28 | 29 | private toValidate(metatype: any): boolean { 30 | const types = [String, Boolean, Number, Array, Object] 31 | return !types.find(type => metatype === type) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/processors/base/base.service.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | import { validate } from 'class-validator' 3 | import { 4 | DeepPartial, 5 | FindManyOptions, 6 | FindOperator, 7 | Repository, 8 | Equal, 9 | In, 10 | IsNull, 11 | LessThan, 12 | MoreThan, 13 | Not, 14 | Raw 15 | } from 'typeorm' 16 | 17 | export interface IFindParam { 18 | where?: any 19 | orderBy?: any 20 | limit?: number 21 | offset?: number 22 | fields?: string[] 23 | } 24 | 25 | export class BaseService { 26 | static serviceMap: Map> = new Map() 27 | 28 | constructor(...args) { 29 | const self = this 30 | const serviceClass = this.constructor 31 | const injectedServices = Reflect.getMetadata('injected_services', serviceClass) || [] 32 | injectedServices.forEach((value, key) => { 33 | args.forEach(obj => { 34 | if (obj instanceof Repository && obj.metadata.target === value) { 35 | self[key] = this.getService(obj) 36 | } 37 | }) 38 | assert(self[key], `未找到${key}属性对应的Repository实例,请在构造函数中注入此实例`) 39 | }) 40 | } 41 | 42 | static getService(repository: Repository): Service { 43 | const className = repository.metadata.name 44 | if (this.serviceMap.has(className)) { 45 | const service = this.serviceMap.get(className) 46 | if (service && service.repository === repository) { 47 | return service 48 | } 49 | } 50 | const newService = new Service(repository) 51 | this.serviceMap.set(className, newService) 52 | return newService 53 | } 54 | 55 | getService(repository: Repository): Service { 56 | return BaseService.getService(repository) 57 | } 58 | } 59 | 60 | export class Service { 61 | constructor(public readonly repository: Repository) { } 62 | 63 | private async findOption({ where, limit, offset, fields, orderBy }: IFindParam = {}): Promise< 64 | any 65 | > { 66 | const findOptions: FindManyOptions = {} 67 | 68 | if (limit) { 69 | findOptions.take = limit 70 | } 71 | if (offset) { 72 | findOptions.skip = offset 73 | } 74 | if (fields) { 75 | findOptions.select = fields 76 | } 77 | if (orderBy) { 78 | const parts = orderBy.toString().split('_') 79 | const attr = parts[0] 80 | const direction: 'ASC' | 'DESC' = parts[1] as 'ASC' | 'DESC' 81 | findOptions.order = { 82 | [attr]: direction 83 | } 84 | } 85 | findOptions.where = this.processWhereOptions(where) 86 | return findOptions 87 | } 88 | 89 | async find({ where, limit, offset, fields, orderBy }: IFindParam = {}): Promise { 90 | const findOptions: any = await this.findOption({ where, limit, offset, fields, orderBy }) 91 | return await this.repository.find(findOptions) 92 | } 93 | 94 | async count({ where }: IFindParam = {}): Promise { 95 | const findOptions: any = await this.findOption({ where }) 96 | return await this.repository.count(findOptions) 97 | } 98 | 99 | async findAndCount({ where, limit, offset, fields, orderBy }: IFindParam = {}): Promise<{ 100 | rows: E[] 101 | count: number 102 | }> { 103 | const findOptions: any = await this.findOption({ where, limit, offset, fields, orderBy }) 104 | const [rows, count] = await this.repository.findAndCount(findOptions) 105 | return { rows, count } 106 | } 107 | 108 | async findOne(where, fields?: string[], orderBy?: string): Promise { 109 | const items = await this.find({ where, fields, orderBy }) 110 | if (!items.length) { 111 | return null 112 | } else if (items.length > 1) { 113 | throw new Error( 114 | `Found ${items.length} ${this.repository.metadata.name}s where ${JSON.stringify(where)}` 115 | ) 116 | } 117 | return items[0] 118 | } 119 | 120 | async create(data: DeepPartial): Promise { 121 | const results = await this.repository.create([data]) 122 | const obj = results[0] 123 | 124 | const errors = await validate(obj, { skipMissingProperties: true }) 125 | if (errors.length) { 126 | throw new Error(JSON.stringify(errors)) 127 | } 128 | return await this.repository.save(obj, { reload: true }) 129 | } 130 | 131 | async createMany(data: Array>): Promise { 132 | data = data.map(item => { 133 | return { ...item } 134 | }) 135 | 136 | const results = await this.repository.create(data) 137 | 138 | results.forEach(async obj => { 139 | const errors = await validate(obj, { skipMissingProperties: true }) 140 | if (errors.length) { 141 | throw new Error(JSON.stringify(errors)) 142 | } 143 | }) 144 | 145 | return await this.repository.save(results, { reload: true }) 146 | } 147 | 148 | async update(data: DeepPartial, where): Promise { 149 | const found = await this.findOne(where) 150 | if (!found) { 151 | return false 152 | } 153 | const merged = this.repository.merge(found, data) 154 | 155 | const errors = await validate(merged, { skipMissingProperties: true }) 156 | if (errors.length) { 157 | throw new Error(JSON.stringify(errors)) 158 | } 159 | return await this.repository.save(merged) 160 | } 161 | 162 | processWhereOptions(where: W) { 163 | if (Array.isArray(where)) { 164 | const whereOptions: Array<{ [key: string]: FindOperator }> = [] 165 | Object.keys(where).forEach(k => { 166 | const options: any = {} 167 | for (const index in where[k]) { 168 | const key = index as keyof W 169 | if (where[k][key] === undefined) { 170 | continue 171 | } 172 | const [attr, operator] = getFindOperator(String(key), where[k][key]) 173 | options[attr] = operator 174 | } 175 | whereOptions.push(options) 176 | }) 177 | return whereOptions 178 | } else { 179 | const whereOptions: { [key: string]: FindOperator } = {} 180 | Object.keys(where).forEach(k => { 181 | const key = k as keyof W 182 | if (where[key] !== undefined) { 183 | const [attr, operator] = getFindOperator(String(key), where[key]) 184 | whereOptions[attr] = operator 185 | } 186 | }) 187 | return whereOptions 188 | } 189 | } 190 | } 191 | 192 | export function getFindOperator(key: string, value: any): [string, FindOperator] { 193 | const parts = key.toString().split('_') 194 | const attr = parts[0] 195 | const operator = parts.length > 1 ? parts[1] : 'eq' 196 | 197 | switch (operator) { 198 | case 'eq': 199 | if (value === null) { 200 | return [attr, IsNull()] 201 | } 202 | return [attr, Equal(value)] 203 | // // Not using "not" yet 204 | // case 'not': 205 | // return [attr, Not(value)]; 206 | case 'lt': 207 | return [attr, LessThan(value)] 208 | case 'lte': 209 | return [attr, Not(MoreThan(value))] 210 | case 'gt': 211 | return [attr, MoreThan(value)] 212 | case 'gte': 213 | return [attr, Not(LessThan(value))] 214 | case 'in': 215 | return [attr, In(value)] 216 | case 'contains': 217 | return [attr, Raw(alias => `LOWER(${alias}) LIKE LOWER('%${value}%')`)] 218 | case 'startsWith': 219 | return [attr, Raw(alias => `LOWER(${alias}) LIKE LOWER('${value}%')`)] 220 | case 'endsWith': 221 | return [attr, Raw(alias => `LOWER(${alias}) LIKE LOWER('%${value}')`)] 222 | default: 223 | throw new Error(`Can't find operator ${operator}`) 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/processors/cache/cache.module.ts: -------------------------------------------------------------------------------- 1 | import { CacheModule as NestCacheModule, Global, Module } from '@nestjs/common' 2 | import { CacheConfigService } from './cache.service.config' 3 | import { CacheService } from './cache.service' 4 | 5 | @Global() 6 | @Module({ 7 | imports: [ 8 | NestCacheModule.registerAsync({ 9 | useClass: CacheConfigService, 10 | inject: [CacheConfigService] 11 | }) 12 | ], 13 | providers: [CacheConfigService, CacheService], 14 | exports: [NestCacheModule, CacheService] 15 | }) 16 | export class HttpCacheModule {} 17 | -------------------------------------------------------------------------------- /src/processors/cache/cache.service.config.ts: -------------------------------------------------------------------------------- 1 | import * as redisStore from 'cache-manager-redis-store' 2 | import { Logger } from 'winston' 3 | import { CacheModuleOptions, CacheOptionsFactory, Injectable, Inject } from '@nestjs/common' 4 | import config from '@config' 5 | 6 | @Injectable() 7 | export class CacheConfigService implements CacheOptionsFactory { 8 | constructor(@Inject('winston') private readonly logger: Logger) {} 9 | 10 | // 重试策略 11 | public retryStrategy() { 12 | return { 13 | retry_strategy: options => { 14 | if (options.error && options.error.code === 'ECONNREFUSED') { 15 | this.logger.warn('Redis 连接出了问题:', options.error) 16 | return new Error('Redis 服务器拒绝连接') 17 | } 18 | if (options.total_retry_time > 1000 * 60) { 19 | return new Error('重试时间已用完') 20 | } 21 | if (options.attempt > 6) { 22 | return new Error('尝试次数已达极限') 23 | } 24 | return Math.min(options.attempt * 100, 3000) 25 | } 26 | } 27 | } 28 | 29 | // 缓存配置 30 | public createCacheOptions(): CacheModuleOptions { 31 | return { store: redisStore, ...config.redis } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/processors/cache/cache.service.ts: -------------------------------------------------------------------------------- 1 | import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common' 2 | 3 | // Cache 客户端管理器 4 | export interface ICacheManager { 5 | store: any 6 | get(key: TCacheKey): any 7 | set(key: TCacheKey, value: string, options?: { ttl: number }): any 8 | } 9 | 10 | // 获取器 11 | export type TCacheKey = string 12 | export type TCacheResult = Promise 13 | 14 | // IO 模式通用返回结构 15 | export interface ICacheIoResult { 16 | get(): TCacheResult 17 | update(): TCacheResult 18 | } 19 | 20 | // Promise 模式参数 21 | export interface ICachePromiseOption { 22 | key: TCacheKey 23 | promise(): TCacheResult 24 | } 25 | 26 | // Promise & IO 模式参数 27 | export interface ICachePromiseIoOption extends ICachePromiseOption { 28 | ioMode?: boolean 29 | } 30 | 31 | // Interval & Timeout 超时模式参数 32 | export interface ICacheIntervalTimeoutOption { 33 | error?: number 34 | success?: number 35 | } 36 | 37 | // Interval & Timing 定时模式参数 38 | export interface ICacheIntervalTimingOption { 39 | error: number 40 | schedule: any 41 | } 42 | 43 | // Interval 模式参数 44 | export interface ICacheIntervalOption { 45 | key: TCacheKey 46 | promise(): TCacheResult 47 | timeout?: ICacheIntervalTimeoutOption 48 | timing?: ICacheIntervalTimingOption 49 | } 50 | 51 | // Interval 模式返回类型 52 | export type TCacheIntervalResult = () => TCacheResult 53 | 54 | // Interval & IO 模式参数 55 | export interface ICacheIntervalIOOption extends ICacheIntervalOption { 56 | ioMode?: boolean 57 | } 58 | 59 | /** 60 | * @class CacheService 61 | * @classdesc 承载缓存服务 62 | * @example CacheService.get(CacheKey).then() 63 | * @example CacheService.set(CacheKey).then() 64 | * @example CacheService.promise({ option })() 65 | * @example CacheService.interval({ option })() 66 | */ 67 | @Injectable() 68 | export class CacheService { 69 | private cache!: ICacheManager 70 | 71 | constructor(@Inject(CACHE_MANAGER) cache: ICacheManager) { 72 | this.cache = cache 73 | } 74 | 75 | // 客户端是否可用 76 | private get checkCacheServiceAvailable(): boolean { 77 | const client = this.cache.store.getClient() 78 | return client.connected && client.ready 79 | } 80 | 81 | public get(key: TCacheKey): TCacheResult { 82 | if (!this.checkCacheServiceAvailable) { 83 | return Promise.reject('缓存客户端没准备好') 84 | } 85 | return this.cache.get(key) 86 | } 87 | 88 | public set(key: TCacheKey, value: any, options?: { ttl: number }): TCacheResult { 89 | if (!this.checkCacheServiceAvailable) { 90 | return Promise.reject('缓存客户端没准备好') 91 | } 92 | return this.cache.set(key, value, options) 93 | } 94 | 95 | /** 96 | * @function promise 97 | * @description 被动更新 | 双向同步 模式,Promise -> Redis 98 | * @example CacheService.promise({ key: CacheKey, promise() }) -> promise() 99 | * @example CacheService.promise({ key: CacheKey, promise(), ioMode: true }) -> { get: promise(), update: promise() } 100 | */ 101 | promise(options: ICachePromiseOption): TCacheResult 102 | promise(options: ICachePromiseIoOption): ICacheIoResult 103 | promise(options) { 104 | const { key, promise, ioMode = false } = options 105 | 106 | // 包装任务 107 | const promiseTask = (resolve, reject) => { 108 | return promise() 109 | .then(data => { 110 | this.set(key, data) 111 | resolve(data) 112 | }) 113 | .catch(reject) 114 | } 115 | 116 | // Promise 拦截模式(返回死数据) 117 | const handlePromiseMode = () => { 118 | return new Promise((resolve, reject) => { 119 | this.get(key) 120 | .then(value => { 121 | const isValidValue = value !== null && value !== undefined 122 | isValidValue ? resolve(value) : promiseTask(resolve, reject) 123 | }) 124 | .catch(reject) 125 | }) 126 | } 127 | 128 | // 双向同步模式(返回获取器和更新器) 129 | const handleIoMode = () => ({ 130 | get: handlePromiseMode, 131 | update: () => new Promise(promiseTask) 132 | }) 133 | return ioMode ? handleIoMode() : handlePromiseMode() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/processors/helper/crypto.serivce.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import * as NodeRSA from 'node-rsa' 3 | import * as CryptoJS from 'crypto-js' 4 | 5 | export interface ISecretKeys { 6 | publicKey: string 7 | privateKey: string 8 | } 9 | 10 | @Injectable() 11 | export class RsaService { 12 | private rsaMap: Map = new Map() 13 | 14 | private _pkcsType: string = 'pkcs1' 15 | 16 | private _pkcsSize: number = 128 17 | 18 | /** 19 | * 设置加密类型 20 | * @param pkcsType 21 | */ 22 | setPkcsType(pkcsType: string): RsaService { 23 | this._pkcsType = pkcsType 24 | return this 25 | } 26 | 27 | /** 28 | * 设置密钥长度 29 | * @param pkcsSize 30 | */ 31 | setPkcsSize(pkcsSize: number): RsaService { 32 | this._pkcsSize = pkcsSize 33 | return this 34 | } 35 | 36 | /** 37 | * 创建rsa实例,并缓存 38 | */ 39 | private getInstance(isNew?: boolean) { 40 | if (isNew) { 41 | return new NodeRSA({ b: this._pkcsSize }) 42 | } 43 | const key = `${this._pkcsType}_${this._pkcsSize}` // 创建RSA对象,并指定秘钥长度 44 | if (!this.rsaMap.has(key)) { 45 | const rsa = new NodeRSA({ b: this._pkcsSize }) 46 | rsa.setOptions({ encryptionScheme: this._pkcsType }) // 指定加密格式 47 | this.rsaMap.set(key, rsa) 48 | } 49 | return this.rsaMap.get(key) 50 | } 51 | 52 | /** 53 | * 生成公钥、私钥 54 | */ 55 | public genSecretKeys(isNew: boolean = true): ISecretKeys { 56 | const rsa = this.getInstance(isNew) 57 | return { 58 | publicKey: rsa.exportKey(`${this._pkcsType}-public-pem`), 59 | privateKey: rsa.exportKey(`${this._pkcsType}-private-pem`) 60 | } 61 | } 62 | 63 | /** 64 | * 私钥加密 65 | * @param priKey 66 | * @param data 67 | */ 68 | public priKeyEncrypt(priKey: string, data: string): string { 69 | const rsa = this.getInstance() 70 | rsa.importKey(priKey) 71 | return rsa.encryptPrivate(data, 'base64', 'utf8') 72 | } 73 | 74 | /** 75 | * 公钥解密 76 | * @param pubKey 77 | * @param data 78 | */ 79 | public pubKeyDecrypt(pubKey: string, data: string): string { 80 | const rsa = this.getInstance() 81 | rsa.importKey(pubKey) 82 | return rsa.decryptPublic(data, 'utf8') 83 | } 84 | } 85 | 86 | @Injectable() 87 | export class CryptoService { 88 | keyStr = 'k;)*(+nmjdsf$#@d' 89 | 90 | encrypt(word) { 91 | const key = CryptoJS.enc.Utf8.parse(this.keyStr) 92 | const srcs = CryptoJS.enc.Utf8.parse(word) 93 | return CryptoJS.AES.encrypt(srcs, key, { 94 | mode: CryptoJS.mode.ECB, 95 | padding: CryptoJS.pad.Pkcs7 96 | }).toString() 97 | } 98 | 99 | decrypt(word) { 100 | const key = CryptoJS.enc.Utf8.parse(this.keyStr) 101 | return CryptoJS.AES.decrypt(word, key, { 102 | mode: CryptoJS.mode.ECB, 103 | padding: CryptoJS.pad.Pkcs7 104 | }).toString(CryptoJS.enc.Utf8) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/processors/helper/crypto.spec.ts: -------------------------------------------------------------------------------- 1 | import { RsaService } from './crypto.serivce' 2 | 3 | test('ras加密解密', () => { 4 | const service = new RsaService() 5 | const { privateKey, publicKey } = service.genSecretKeys() 6 | const str = 'fuck!!!哟哟哟!!' 7 | const exec = () => { 8 | const encodeStr = service.priKeyEncrypt(privateKey, str) 9 | return service.pubKeyDecrypt(publicKey, encodeStr) 10 | } 11 | expect(exec()).toBe(str) 12 | }) 13 | -------------------------------------------------------------------------------- /src/processors/helper/helper.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, Global } from '@nestjs/common' 2 | import { RsaService } from './crypto.serivce' 3 | 4 | @Global() 5 | @Module({ 6 | providers: [RsaService], 7 | exports: [RsaService] 8 | }) 9 | export class HelperModule {} 10 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as rateLimit from 'fastify-rate-limit' 2 | import * as helmet from 'fastify-helmet' 3 | import * as compress from 'fastify-compress' 4 | import { NestFactory, Reflector } from '@nestjs/core' 5 | import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify' 6 | import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' 7 | import { AppModule } from '@modules/app/app.module' 8 | import { ValidationPipe } from '@pipes/validation.pipe' 9 | import { HttpExceptionFilter } from '@filters/exception.filter' 10 | import { TransformInterceptor } from '@interceptors/transform.interceptor' 11 | import { ErrorInterceptor } from '@interceptors/error.interceptor' 12 | import { LoggingInterceptor } from '@interceptors/logging.interceptor' 13 | import config from '@config' 14 | 15 | async function bootstrap() { 16 | const urlPrefix = 'api/v1' 17 | const adapter = new FastifyAdapter() 18 | // 安全防护 19 | adapter.register(helmet) 20 | // 压缩请求 21 | adapter.register(compress) 22 | // 限制访问频率,多实例建议走redis 23 | adapter.register(rateLimit, { 24 | max: 100, 25 | timeWindow: 6000 // 一分钟 26 | }) 27 | const nestApp = await NestFactory.create(AppModule, adapter) 28 | // 设置全局前缀 29 | nestApp.setGlobalPrefix(urlPrefix) 30 | // 全局使用winston 31 | const nestWinston = nestApp.get('NestWinston') 32 | nestApp.useLogger(nestWinston) 33 | // 捕获全局错误 34 | nestApp.useGlobalFilters(new HttpExceptionFilter(nestWinston.logger)) 35 | // class-validator效验 36 | nestApp.useGlobalPipes(new ValidationPipe()) 37 | // 添加拦截器 38 | nestApp.useGlobalInterceptors( 39 | new TransformInterceptor(new Reflector()), 40 | new ErrorInterceptor(new Reflector(), nestWinston.logger), 41 | new LoggingInterceptor(nestWinston.logger) 42 | ) 43 | // Swagger文档 44 | const options = new DocumentBuilder() 45 | .setBasePath(urlPrefix) 46 | .setTitle('接口文档') 47 | .setDescription('接口文档') 48 | .setVersion('1.0') 49 | .build() 50 | const document = SwaggerModule.createDocument(nestApp, options) 51 | SwaggerModule.setup('/docs', nestApp, document) 52 | // 监听服务端口 53 | await nestApp.listen(config.port) 54 | return nestWinston.logger 55 | } 56 | bootstrap().then(logger => { 57 | logger.info(`url: http://127.0.0.1:${config.port}, env: ${config.env}`) 58 | }) 59 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "removeComments": true, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2018" 10 | ], 11 | "module": "commonjs", 12 | "noImplicitAny": false, 13 | "moduleResolution": "node", 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "strict": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "sourceMap": true, 20 | "target": "es2017", 21 | "charset": "utf8", 22 | "allowJs": false, 23 | "pretty": true, 24 | "noEmitOnError": false, 25 | "allowUnreachableCode": false, 26 | "allowUnusedLabels": false, 27 | "strictPropertyInitialization": false, 28 | "noFallthroughCasesInSwitch": true, 29 | "skipDefaultLibCheck": true, 30 | "importHelpers": true, 31 | "outDir": "./dist", 32 | "baseUrl": "./", 33 | "paths": { 34 | "@src/*": [ 35 | "./src/*" 36 | ], 37 | "@config/*": [ 38 | "./src/config/*" 39 | ], 40 | "@config": [ 41 | "./src/config/main.config.ts" 42 | ], 43 | "@interfaces/*": [ 44 | "./src/interfaces/*" 45 | ], 46 | "@filters/*": [ 47 | "./src/filters/*" 48 | ], 49 | "@processors/*": [ 50 | "./src/processors/*" 51 | ], 52 | "@modules/*": [ 53 | "./src/modules/*" 54 | ], 55 | "@interceptors/*": [ 56 | "./src/interceptors/*" 57 | ], 58 | "@exceptions/*": [ 59 | "./src/exceptions/*" 60 | ], 61 | "@constants/*": [ 62 | "./src/constants/*" 63 | ], 64 | "@pipes/*": [ 65 | "./src/pipes/*" 66 | ], 67 | "@utils/*": [ 68 | "./src/utils/*" 69 | ], 70 | "@transforms/*": [ 71 | "./src/transforms/*" 72 | ], 73 | "@decorators/*": [ 74 | "./src/decorators/*" 75 | ], 76 | "@middlewares/*": [ 77 | "./src/middlewares/*" 78 | ], 79 | "@models/*": [ 80 | "./src/models/*" 81 | ] 82 | } 83 | }, 84 | "exclude": [ 85 | "node_modules" 86 | ], 87 | "include": [ 88 | "**/*.ts", 89 | "**/*.tsx" 90 | ] 91 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended", "tslint-config-prettier"], 4 | "jsRules": { 5 | "no-unused-expression": true 6 | }, 7 | "rules": { 8 | "quotemark": [true, "single"], 9 | "member-access": [false], 10 | "ordered-imports": [false], 11 | "max-line-length": [true, 100], 12 | "member-ordering": [false], 13 | "interface-name": [false], 14 | "arrow-parens": false, 15 | "object-literal-sort-keys": false, 16 | "max-classes-per-file": false 17 | }, 18 | "rulesDirectory": [] 19 | } 20 | --------------------------------------------------------------------------------