├── .dockerignore
├── .prettierrc
├── src
├── interfaces
│ ├── user.interface.ts
│ ├── account.interface.ts
│ ├── common.interface.ts
│ └── http.interface.ts
├── enum
│ ├── common.enum.ts
│ ├── api-code.enum.ts
│ └── user.enum.ts
├── utils
│ ├── common.ts
│ ├── format-date.ts
│ ├── terminal-help-text-console.ts
│ └── log4js.ts
├── app.controller.ts
├── config
│ ├── module
│ │ ├── redis-temp.ts
│ │ ├── cmomon.text.ts
│ │ ├── get-banner.ts
│ │ └── log4js.ts
│ └── env
│ │ ├── jwt.config.ts
│ │ ├── redis.config.ts
│ │ ├── email.config.ts
│ │ ├── swagger.config.ts
│ │ └── databse.config.ts
├── modules
│ ├── common
│ │ ├── jwt.module.ts
│ │ ├── redis.module.ts
│ │ ├── common.module.ts
│ │ └── code.module.ts
│ ├── base
│ │ ├── entitiy.module.ts
│ │ ├── redis.module.ts
│ │ ├── database.module.ts
│ │ ├── email.module.ts
│ │ └── config.module.ts
│ ├── user
│ │ └── user.module.ts
│ └── account
│ │ └── account.module.ts
├── assets
│ ├── email-template
│ │ ├── register.ejs
│ │ └── notice.ejs
│ └── banner.txt
├── decorators
│ ├── ip.address.ts
│ └── current.user.ts
├── dtos
│ ├── common.dto.ts
│ └── user
│ │ └── user.dto.ts
├── services
│ ├── common
│ │ ├── code
│ │ │ ├── img-captcha.service.ts
│ │ │ └── email-code.service.ts
│ │ ├── jwt
│ │ │ └── jwt.service.ts
│ │ └── redis
│ │ │ ├── redis-client.service.ts
│ │ │ └── redis.cache.service.ts
│ ├── account
│ │ └── account.service.ts
│ └── user
│ │ └── user.service.ts
├── exception
│ └── api-exception.ts
├── app.module.ts
├── controllers
│ ├── account
│ │ └── account.controller.ts
│ └── user
│ │ └── user.controller.ts
├── entities
│ ├── user-code.entity.ts
│ ├── public.entity.ts
│ └── user.entity.ts
├── pipes
│ ├── parse.page.pipe.ts
│ └── validation.pipe.ts
├── interceptor
│ ├── transform.interceptor.ts
│ └── logger.interceptor.ts
├── guard
│ └── auth.guard.ts
├── filters
│ └── http-exception.filter.ts
└── main.ts
├── tsconfig.build.json
├── Dockerfile
├── nest-cli.json
├── .vscode
├── tasks.json
└── launch.json
├── public
├── views
│ └── index.ejs
└── assets
│ └── logo.svg
├── tsconfig.json
├── .gitignore
├── .eslintrc.js
├── default.env
├── LICENSE
├── .cz-config.js
├── README.md
└── package.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | /node_modules
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/src/interfaces/user.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IToken {
2 | sub: number;
3 | account: string;
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/enum/common.enum.ts:
--------------------------------------------------------------------------------
1 | export enum DelEnum {
2 | Y = 1,
3 | N = 0,
4 | }
5 |
6 | export enum StatusEnum {
7 | Y = 1,
8 | N = 0,
9 | }
10 |
--------------------------------------------------------------------------------
/src/utils/common.ts:
--------------------------------------------------------------------------------
1 | import * as uuid from 'uuid';
2 |
3 | /**
4 | * 获取UUID
5 | */
6 | export const getUUID = () => {
7 | return uuid.v4();
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/format-date.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-30 13:59:54
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-11-30 13:59:54
6 | */
7 |
--------------------------------------------------------------------------------
/src/interfaces/account.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IAccountInfo {
2 | id: number;
3 | }
4 |
5 | export interface ICurrentUser {
6 | userId: string | number;
7 | account: string;
8 | }
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:12
2 |
3 | WORKDIR /app/src
4 |
5 | COPY ./ /app/src
6 |
7 | RUN npm install && npm run build && rm node_modules -rf
8 |
9 | EXPOSE 3000
10 |
11 | CMD ["npm","run","start"]
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get, Render } from '@nestjs/common';
2 | @Controller()
3 | export class AppController {
4 | @Get()
5 | @Render('index')
6 | root() {
7 | return 'Hello!';
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src",
4 | "compilerOptions": {
5 | "assets": [
6 | {
7 | "include": "assets/**/*",
8 | "watchAssets": true
9 | }
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/config/module/redis-temp.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-03 17:02:36
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-03 19:06:52
6 | */
7 |
8 | export const RedisTemp = {
9 | IMAGE_CAPTCHA: 'IMAGE_CAPTCHA:',
10 | };
11 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "npm",
6 | "script": "start:debug",
7 | "group": "rebuild",
8 | "problemMatcher": [],
9 | "label": "nest debug",
10 | "detail": "nest start --debug --watch"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/modules/common/jwt.module.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule } from '@nestjs/config';
2 | import { JwtService } from '@/services/common/jwt/jwt.service';
3 | import { Module } from '@nestjs/common';
4 |
5 | @Module({
6 | imports: [ConfigModule],
7 | providers: [JwtService],
8 | exports: [JwtService],
9 | })
10 | export class JwtModule {}
11 |
--------------------------------------------------------------------------------
/src/assets/email-template/register.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 | Welcome, your register code is <%= locals.code %>
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/email-template/notice.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 | Welcome, Account <%= locals.account %> registration is successful
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 | <%= result %>
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/decorators/ip.address.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator } from '@nestjs/common';
2 |
3 | import * as requestIp from 'request-ip';
4 |
5 | export const IpAddress = createParamDecorator((data, req) => {
6 | if (req.clientIp) {
7 | return req.clientIp;
8 | }
9 | return requestIp.getClientIp(req); // In case we forgot to include requestIp.mw() in main.ts
10 | });
11 |
--------------------------------------------------------------------------------
/src/enum/api-code.enum.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 请求状态码
3 | * @export ApiCodeEnum
4 | * @enum {number}
5 | */
6 | export enum ApiCodeEnum {
7 | /**
8 | * 请求成功状态码
9 | */
10 | SUCCESS = 0,
11 | /**
12 | * 请求失败状态码
13 | */
14 | ERROR = 2000,
15 | /**
16 | * 请求警告状态码
17 | */
18 | WARN = 3000,
19 | /**
20 | * 需要登录
21 | */
22 | SHOULD_LOGIN = 4001,
23 | }
24 |
--------------------------------------------------------------------------------
/src/enum/user.enum.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-26 18:41:26
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-11-26 18:49:18
6 | */
7 |
8 | /**
9 | * 账号枚举
10 | */
11 | export enum AccountEnum {
12 | EMAIL = 0,
13 | TEL,
14 | }
15 |
16 | /**
17 | * 性别枚举
18 | */
19 | export enum SexEnum {
20 | M = 0,
21 | W,
22 | UNKWON,
23 | }
24 |
--------------------------------------------------------------------------------
/src/interfaces/common.interface.ts:
--------------------------------------------------------------------------------
1 | export interface IAnyObject {
2 | [propsName: string]: any;
3 | }
4 |
5 | /**
6 | * 邮箱模版
7 | */
8 | type EmailTemplateType = 'register' | 'notice';
9 |
10 | /**
11 | * 邮箱参数
12 | */
13 | export interface IEmailParams {
14 | to: string;
15 | title: string;
16 | content: string;
17 | template?: EmailTemplateType;
18 | context?: IAnyObject;
19 | }
20 |
--------------------------------------------------------------------------------
/src/modules/common/redis.module.ts:
--------------------------------------------------------------------------------
1 | import { RedisClientService } from '@/services/common/redis/redis-client.service';
2 | import { RedisCacheService } from '@/services/common/redis/redis.cache.service';
3 | import { Module } from '@nestjs/common';
4 |
5 | @Module({
6 | providers: [RedisClientService, RedisCacheService],
7 | exports: [RedisClientService, RedisCacheService],
8 | })
9 | export class RedisModule {}
10 |
--------------------------------------------------------------------------------
/src/decorators/current.user.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 |
3 | export const CurrentUser = createParamDecorator(
4 | (key: string, ctx: ExecutionContext) => {
5 | const request = ctx.switchToHttp().getRequest();
6 | if (key && request.currentUser) {
7 | return request.currentUser[key] || '';
8 | } else {
9 | return request.currentUser;
10 | }
11 | },
12 | );
13 |
--------------------------------------------------------------------------------
/src/modules/common/common.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { CodeModule } from './code.module';
3 | import { JwtModule } from './jwt.module';
4 | import { RedisModule } from './redis.module';
5 |
6 | /**
7 | * 基础服务模块
8 | */
9 | @Module({
10 | imports: [CodeModule, RedisModule, JwtModule],
11 | controllers: [],
12 | exports: [CodeModule, RedisModule, JwtModule],
13 | })
14 | export class CommonModule {}
15 |
--------------------------------------------------------------------------------
/src/modules/common/code.module.ts:
--------------------------------------------------------------------------------
1 | import { EmailCodeService } from '@/services/common/code/email-code.service';
2 | import { ImageCaptchaService } from '@/services/common/code/img-captcha.service';
3 | import { Module } from '@nestjs/common';
4 |
5 | @Module({
6 | imports: [],
7 | providers: [ImageCaptchaService, EmailCodeService],
8 | controllers: [],
9 | exports: [ImageCaptchaService, EmailCodeService],
10 | })
11 | export class CodeModule {}
12 |
--------------------------------------------------------------------------------
/src/modules/base/entitiy.module.ts:
--------------------------------------------------------------------------------
1 | import { UserCodeEntity } from '@/entities/user-code.entity';
2 | import { UserEntity } from '@/entities/user.entity';
3 | import { Module } from '@nestjs/common';
4 | import { TypeOrmModule } from '@nestjs/typeorm';
5 | const entityList = [UserEntity, UserCodeEntity];
6 |
7 | @Module({
8 | imports: [TypeOrmModule.forFeature(entityList)],
9 | exports: [TypeOrmModule.forFeature(entityList)],
10 | })
11 | export class EntityModule {}
12 |
--------------------------------------------------------------------------------
/src/dtos/common.dto.ts:
--------------------------------------------------------------------------------
1 | import { CommonText } from '@/config/module/cmomon.text';
2 | import { ApiProperty } from '@nestjs/swagger';
3 |
4 | /*
5 | * @Author: ahwgs
6 | * @Date: 2020-11-26 18:59:19
7 | * @Last Modified by: ahwgs
8 | * @Last Modified time: 2020-11-26 19:01:52
9 | */
10 |
11 | export class DateDto {
12 | @ApiProperty({ description: CommonText.FRONT_DATE })
13 | frontDate?: Date;
14 | @ApiProperty({ description: CommonText.END_DATE })
15 | endDate?: Date;
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./src",
13 | "incremental": true,
14 | "paths": {
15 | "@": ["./"],
16 | "@/*": ["./*"]
17 | },
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/config/env/jwt.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-03 17:08:08
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-04 00:22:02
6 | */
7 | import { registerAs } from '@nestjs/config';
8 | export interface EnvJwtOptions {
9 | secret: string;
10 | expiresIn: string;
11 | }
12 | export default registerAs(
13 | 'EnvJwtOptions',
14 | (): EnvJwtOptions => ({
15 | secret: process.env.TOKEN_SECRET,
16 | expiresIn: process.env.TOKEN_EXPIRES,
17 | }),
18 | );
19 |
--------------------------------------------------------------------------------
/src/modules/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { UserService } from '@/services/user/user.service';
2 | import { Module } from '@nestjs/common';
3 | import { UserController } from '@/controllers/user/user.controller';
4 | import { CommonModule } from '../common/common.module';
5 | import { EntityModule } from '../base/entitiy.module';
6 | @Module({
7 | imports: [EntityModule, CommonModule],
8 | providers: [UserService],
9 | controllers: [UserController],
10 | exports: [UserService],
11 | })
12 | export class UserModule {}
13 |
--------------------------------------------------------------------------------
/src/interfaces/http.interface.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 带分页带返回类型定义
3 | */
4 | export interface IHttpResultPagination {
5 | list: T;
6 | totalCount: number;
7 | }
8 |
9 | /**
10 | * 接口响应返回基础
11 | */
12 | export interface IHttpResponseBase {
13 | message: string;
14 | code: number;
15 | path: string;
16 | method: string;
17 | timestamp: number;
18 | }
19 |
20 | export type THttpResponse = IHttpResponseBase & {
21 | result: T;
22 | };
23 |
24 | export interface IHttpResponse extends IHttpResponseBase {
25 | result: any;
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
35 |
36 | .env
--------------------------------------------------------------------------------
/src/modules/account/account.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { CommonModule } from '../common/common.module';
3 | import { EntityModule } from '../base/entitiy.module';
4 | import { AccountService } from '@/services/account/account.service';
5 | import { AccountController } from '@/controllers/account/account.controller';
6 | @Module({
7 | imports: [EntityModule, CommonModule],
8 | providers: [AccountService],
9 | controllers: [AccountController],
10 | exports: [AccountService],
11 | })
12 | export class AccountModule {}
13 |
--------------------------------------------------------------------------------
/src/config/module/cmomon.text.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * 公共文案
3 | * @Author: ahwgs
4 | * @Date: 2020-11-20 20:04:50
5 | * @Last Modified by: ahwgs
6 | * @Last Modified time: 2020-12-02 18:07:12
7 | */
8 |
9 | interface IText {
10 | [propName: string]: string;
11 | }
12 |
13 | export const CommonText: IText = {
14 | REQUEST_ERROR: '请求失败',
15 | PARAMES_MUST_NUM: '需为整数,当前输入的为:',
16 | FRONT_DATE: '开始时间',
17 | END_DATE: '结束时间',
18 | REGISTER_CODE: '[Fast-Nest-Temp] Your Register Email Code',
19 | REGISTER_SUCCESS: '[Fast-Nest-Temp] Account registration is successful',
20 | };
21 |
--------------------------------------------------------------------------------
/src/config/env/redis.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-03 17:08:08
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-03 17:13:57
6 | */
7 | import { registerAs } from '@nestjs/config';
8 | export interface EnvRedisOptions {
9 | host: string;
10 | port: string | number;
11 | password: string;
12 | }
13 | export default registerAs(
14 | 'EnvRedisOptions',
15 | (): EnvRedisOptions => ({
16 | host: process.env.REDIS_HOST,
17 | port: process.env.REDIS_PORT,
18 | password: process.env.REDIS_PASSWORD || '',
19 | }),
20 | );
21 |
--------------------------------------------------------------------------------
/src/config/module/get-banner.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * 获取运行banner
3 | * @Author: ahwgs
4 | * @Date: 2020-11-20 20:05:09
5 | * @Last Modified by: ahwgs
6 | * @Last Modified time: 2020-11-20 20:05:30
7 | */
8 |
9 | import * as fs from 'fs';
10 | import * as path from 'path';
11 | const bannerPath = path.join(process.cwd(), 'src/assets/banner.txt');
12 |
13 | const getBanner = async () => {
14 | try {
15 | const result = await fs.readFileSync(bannerPath, 'utf-8');
16 | return result;
17 | } catch (e) {
18 | console.log(e);
19 | }
20 | };
21 |
22 | export const BannerLog = getBanner();
23 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // 使用 IntelliSense 了解相关属性。
3 | // 悬停以查看现有属性的描述。
4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "node",
9 | "request": "launch",
10 | "name": "启动程序",
11 | "skipFiles": [
12 | "/**"
13 | ],
14 | "program": "${workspaceFolder}/start",
15 | "preLaunchTask": "nest debug",
16 | "outFiles": [
17 | "${workspaceFolder}/dist/**/*.js"
18 | ]
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/services/common/code/img-captcha.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import * as svgCaptcha from 'svg-captcha';
3 |
4 | @Injectable()
5 | export class ImageCaptchaService {
6 | /**
7 | * 生成图形验证码
8 | */
9 | public createSvgCaptcha(length?: number) {
10 | const defaultLen = 4;
11 | const captcha: { data: any; text: string } = svgCaptcha.create({
12 | size: length || defaultLen,
13 | fontSize: 50,
14 | width: 100,
15 | height: 34,
16 | ignoreChars: '0o1i',
17 | background: '#01458E',
18 | inverse: false,
19 | });
20 | return captcha;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/services/account/account.service.ts:
--------------------------------------------------------------------------------
1 | import { UserEntity } from '@/entities/user.entity';
2 | import { IAccountInfo } from '@/interfaces/account.interface';
3 | import { Injectable } from '@nestjs/common';
4 | import { InjectRepository } from '@nestjs/typeorm';
5 | import { Repository } from 'typeorm';
6 |
7 | @Injectable()
8 | export class AccountService {
9 | constructor(
10 | @InjectRepository(UserEntity)
11 | private readonly userRepo: Repository,
12 | ) {}
13 |
14 | /**
15 | * 获取用户信息
16 | */
17 | getUserInfo(userId: number) {
18 | const result = {} as IAccountInfo;
19 | console.log('userId', userId);
20 | return result;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/config/env/email.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-03 17:08:08
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-04 00:22:02
6 | */
7 | import { registerAs } from '@nestjs/config';
8 | export interface EnvEmailOptions {
9 | host: string;
10 | port: string;
11 | user: string;
12 | password: string;
13 | from: string;
14 | }
15 | export default registerAs(
16 | 'EnvEmailOptions',
17 | (): EnvEmailOptions => ({
18 | host: process.env.EMAIL_HOST,
19 | port: process.env.EMAIL_PORT,
20 | user: process.env.EAMIL_AUTH_USER,
21 | password: process.env.EMAIL_AUTH_PASSWORD,
22 | from: process.env.EMAIL_FROM,
23 | }),
24 | );
25 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'prettier/@typescript-eslint',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/src/config/env/swagger.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-16 14:50:29
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-11-30 14:38:59
6 | */
7 | import { registerAs } from '@nestjs/config';
8 | const VERSION = process.env.npm_package_version;
9 | export interface EnvSwaggerOptions {
10 | title: string;
11 | setupUrl: string;
12 | desc?: string;
13 | prefix: string;
14 | version: string;
15 | }
16 | export default registerAs(
17 | 'EnvSwaggerOptions',
18 | (): EnvSwaggerOptions => ({
19 | title: process.env.SWAGGER_UI_TITLE,
20 | desc: process.env.SWAGGER_UI_TITLE_DESC,
21 | version: VERSION || process.env.SWAGGER_API_VERSION,
22 | setupUrl: process.env.SWAGGER_SETUP_PATH,
23 | prefix: process.env.SWAGGER_ENDPOINT_PREFIX,
24 | }),
25 | );
26 |
--------------------------------------------------------------------------------
/src/exception/api-exception.ts:
--------------------------------------------------------------------------------
1 | import { ApiCodeEnum } from '@/enum/api-code.enum';
2 | import { HttpException, HttpStatus } from '@nestjs/common';
3 |
4 | /**
5 | * api请求异常类
6 | *
7 | * @export
8 | * @class ApiException
9 | * @extends {HttpException}
10 | */
11 | export class ApiException extends HttpException {
12 | private errorMessage: string;
13 | private errorCode: ApiCodeEnum;
14 |
15 | constructor(
16 | errorMessage: string,
17 | errorCode: ApiCodeEnum,
18 | statusCode: HttpStatus = HttpStatus.OK,
19 | ) {
20 | super(errorMessage, statusCode);
21 | this.errorMessage = errorMessage;
22 | this.errorCode = errorCode;
23 | }
24 | getErrorCode(): ApiCodeEnum {
25 | return this.errorCode;
26 | }
27 | getErrorMessage(): string {
28 | return this.errorMessage;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AccountModule } from './modules/account/account.module';
4 | import { CustomConfigModule } from './modules/base/config.module';
5 | import { DataBaseModule } from './modules/base/database.module';
6 | import { EmailModule } from './modules/base/email.module';
7 | import { CustomRedisModule } from './modules/base/redis.module';
8 | import { CommonModule } from './modules/common/common.module';
9 | import { UserModule } from './modules/user/user.module';
10 |
11 | @Module({
12 | imports: [
13 | CustomConfigModule,
14 | DataBaseModule,
15 | CustomRedisModule,
16 | EmailModule,
17 | CommonModule,
18 | UserModule,
19 | AccountModule,
20 | ],
21 | controllers: [AppController],
22 | })
23 | export class AppModule {}
24 |
--------------------------------------------------------------------------------
/src/services/common/code/email-code.service.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-05 00:02:03
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-05 00:03:39
6 | */
7 |
8 | import { Injectable } from '@nestjs/common';
9 | import { MailerService } from '@nestjs-modules/mailer';
10 | import { IEmailParams } from '@/interfaces/common.interface';
11 |
12 | @Injectable()
13 | export class EmailCodeService {
14 | constructor(private readonly mailerService: MailerService) {}
15 | /**
16 | * 邮箱发送
17 | * @param params IEmailParams
18 | */
19 | public async sendEmail(params: IEmailParams) {
20 | const { to, title, content, template, context } = params;
21 | return await this.mailerService.sendMail({
22 | to: to,
23 | subject: title,
24 | text: content,
25 | template,
26 | context,
27 | });
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/default.env:
--------------------------------------------------------------------------------
1 | # ------- 环境变量模版 ---------
2 |
3 | # 服务启动端口
4 | SERVE_LISTENER_PORT=3000
5 |
6 | # Swagger 文档相关
7 | SWAGGER_UI_TITLE = Fast-nest-temp 接口文档
8 | SWAGGER_UI_TITLE_DESC = 接口文档
9 | SWAGGER_API_VERSION = 0.0.1
10 | SWAGGER_SETUP_PATH = api-docs
11 | SWAGGER_ENDPOINT_PREFIX = nest_api
12 |
13 |
14 | # 开发模式相关
15 | NODE_ENV=development
16 |
17 | # 应用配置
18 |
19 | # 数据库相关
20 | DB_TYPE = mysql
21 | DB_HOST = 127.0.0.1
22 | DB_PORT = 3306
23 | DB_DATABASE = fast_nest
24 | DB_USERNAME = root
25 | DB_PASSWORD = 123456
26 | DB_SYNCHRONIZE = 1
27 | DB_LOGGING = 1
28 | DB_TABLE_PREFIX = t_
29 |
30 | # Redis相关
31 | REDIS_HOST = localhost
32 | REDIS_PORT = 6379
33 | REDIS_PASSWORD =
34 |
35 | # Token相关
36 | TOKEN_SECRET = secret
37 | TOKEN_EXPIRES = 7d
38 |
39 | # Email相关
40 | EMAIL_HOST = smtp.126.com
41 | EMAIL_PORT = 465
42 | EAMIL_AUTH_USER = xxxxx
43 | EMAIL_AUTH_PASSWORD = xxxxx
44 | EMAIL_FROM = "FAST_NEST_TEMP ROBOT"
--------------------------------------------------------------------------------
/src/utils/terminal-help-text-console.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-16 15:17:26
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-03 13:47:26
6 | */
7 | import { BannerLog } from '@/config/module/get-banner';
8 | import { Logger } from './log4js';
9 |
10 | type paramType = {
11 | Port: string | number;
12 | DocUrl: string;
13 | ApiPrefix: string;
14 | };
15 | /**
16 | * 打印相关的帮助信息到终端
17 | * @param params
18 | */
19 | export function terminalHelpTextConsole(): void {
20 | const params = {
21 | Port: process.env.SERVE_LISTENER_PORT,
22 | DocUrl: process.env.SWAGGER_SETUP_PATH,
23 | ApiPrefix: process.env.SWAGGER_ENDPOINT_PREFIX,
24 | } as paramType;
25 |
26 | const Host = `http://localhost`;
27 | Logger.log(BannerLog);
28 | Logger.log('Swagger文档链接:', `${Host}:${params.Port}/${params.DocUrl}`);
29 | Logger.log('Restful接口链接:', `${Host}:${params.Port}/${params.ApiPrefix}`);
30 | }
31 |
--------------------------------------------------------------------------------
/src/controllers/account/account.controller.ts:
--------------------------------------------------------------------------------
1 | import { CurrentUser } from '@/decorators/current.user';
2 | import { AuthGuard } from '@/guard/auth.guard';
3 | import { IAccountInfo } from '@/interfaces/account.interface';
4 | import { AccountService } from '@/services/account/account.service';
5 | import { Controller, Get, UseGuards } from '@nestjs/common';
6 | import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
7 |
8 | @ApiBearerAuth()
9 | @ApiTags('账号模块')
10 | @ApiBearerAuth()
11 | @Controller('account')
12 | @UseGuards(AuthGuard)
13 | export class AccountController {
14 | constructor(private readonly accountService: AccountService) {}
15 |
16 | /**
17 | * 获取用户信息
18 | * @param userId
19 | */
20 | @ApiOperation({ summary: '用户信息', description: '或者当前用户信息' })
21 | @Get('info')
22 | async getInfo(@CurrentUser('userId') userId: number): Promise {
23 | return this.accountService.getUserInfo(userId);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/entities/user-code.entity.ts:
--------------------------------------------------------------------------------
1 | import { PublicEntity } from './public.entity';
2 | import { Column, Entity } from 'typeorm';
3 |
4 | @Entity({ name: 'user_code' })
5 | export class UserCodeEntity extends PublicEntity {
6 | @Column({
7 | name: 'code',
8 | comment: '验证码',
9 | nullable: false,
10 | default: '',
11 | type: 'varchar',
12 | length: '10',
13 | })
14 | code: string;
15 |
16 | @Column({
17 | name: 'ip',
18 | comment: '请求地址ip',
19 | nullable: false,
20 | default: '',
21 | type: 'varchar',
22 | length: '20',
23 | })
24 | ip: string;
25 |
26 | @Column({
27 | name: 'status',
28 | comment: '状态 0:未使用 1已使用',
29 | nullable: false,
30 | default: 0,
31 | type: 'tinyint',
32 | })
33 | status: number;
34 |
35 | @Column({
36 | type: 'varchar',
37 | name: 'account',
38 | comment: '账号',
39 | default: '',
40 | nullable: false,
41 | })
42 | account: string;
43 | }
44 |
--------------------------------------------------------------------------------
/src/pipes/parse.page.pipe.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-20 20:04:41
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-02 10:15:08
6 | */
7 |
8 | import { CommonText } from '@/config/module/cmomon.text';
9 | import { ApiException } from '@/exception/api-exception';
10 | import { ApiCodeEnum } from '@/enum/api-code.enum';
11 | import {
12 | ArgumentMetadata,
13 | Injectable,
14 | PipeTransform,
15 | HttpStatus,
16 | } from '@nestjs/common';
17 |
18 | @Injectable()
19 | export class ParsePagePipe implements PipeTransform {
20 | async transform(value: any, metadata: ArgumentMetadata) {
21 | const { data: key } = metadata;
22 | const val = parseFloat(value);
23 | if (isNaN(val) || typeof val !== 'number' || val <= 0) {
24 | throw new ApiException(
25 | `${key} ${CommonText.PARAMES_MUST_NUM}:${value}`,
26 | ApiCodeEnum.WARN,
27 | HttpStatus.OK,
28 | );
29 | }
30 | return val;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/base/redis.module.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-03 17:16:39
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-03 18:58:05
6 | */
7 | import { Module } from '@nestjs/common';
8 | import { RedisModule } from 'nestjs-redis';
9 | import { ConfigModule, ConfigService } from '@nestjs/config';
10 | import { EnvRedisOptions } from '@/config/env/redis.config';
11 |
12 | @Module({
13 | imports: [
14 | RedisModule.forRootAsync({
15 | imports: [ConfigModule],
16 | inject: [ConfigService],
17 | useFactory: (configService: ConfigService) => {
18 | const redisOptions = configService.get(
19 | 'EnvRedisOptions',
20 | );
21 | return {
22 | host: redisOptions.host,
23 | port: redisOptions.port as number,
24 | password: redisOptions.password,
25 | db: 0, // default
26 | };
27 | },
28 | }),
29 | ],
30 | exports: [],
31 | })
32 | export class CustomRedisModule {}
33 |
--------------------------------------------------------------------------------
/src/entities/public.entity.ts:
--------------------------------------------------------------------------------
1 | import { DelEnum } from '@/enum/common.enum';
2 | import {
3 | CreateDateColumn,
4 | PrimaryGeneratedColumn,
5 | UpdateDateColumn,
6 | BaseEntity,
7 | Column,
8 | } from 'typeorm';
9 | import { Exclude } from 'class-transformer';
10 |
11 | export class PublicEntity extends BaseEntity {
12 | @PrimaryGeneratedColumn({
13 | type: 'int',
14 | name: 'id',
15 | comment: '主键id',
16 | })
17 | id: number;
18 |
19 | @Exclude() // 表示排除字段不返回给前端
20 | @Column({
21 | type: 'tinyint',
22 | nullable: false,
23 | name: 'is_del',
24 | default: DelEnum.N,
25 | comment: '是否删除,1表示删除,0表示正常',
26 | })
27 | isDel: number;
28 |
29 | @CreateDateColumn({
30 | type: 'timestamp',
31 | nullable: false,
32 | name: 'created_at',
33 | comment: '创建时间',
34 | })
35 | createdAt: Date;
36 |
37 | @UpdateDateColumn({
38 | type: 'timestamp',
39 | nullable: false,
40 | name: 'updated_at',
41 | comment: '更新时间',
42 | })
43 | updatedAt: Date;
44 | }
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 w候人兮猗
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/interceptor/transform.interceptor.ts:
--------------------------------------------------------------------------------
1 | import { ApiCodeEnum } from '@/enum/api-code.enum';
2 | import { THttpResponse } from '@/interfaces/http.interface';
3 | import {
4 | CallHandler,
5 | ExecutionContext,
6 | Injectable,
7 | NestInterceptor,
8 | } from '@nestjs/common';
9 | import { Observable } from 'rxjs';
10 | import { map } from 'rxjs/operators';
11 | import * as express from 'express';
12 | import { classToPlain } from 'class-transformer';
13 |
14 | @Injectable()
15 | export class TransformInterceptor
16 | implements NestInterceptor> {
17 | intercept(
18 | context: ExecutionContext,
19 | next: CallHandler,
20 | ): Observable> {
21 | const httpArguments = context.switchToHttp();
22 | const request: express.Request = httpArguments.getRequest();
23 | const timestamp = Date.now();
24 | return next.handle().pipe(
25 | map((data) => ({
26 | result: classToPlain(data) || data,
27 | code: ApiCodeEnum.SUCCESS,
28 | message: 'success',
29 | timestamp,
30 | path: request.url,
31 | method: request.method,
32 | })),
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/base/database.module.ts:
--------------------------------------------------------------------------------
1 | import { EnvDataBaseOptions } from '@/config/env/databse.config';
2 | import { Module } from '@nestjs/common';
3 | import { TypeOrmModule } from '@nestjs/typeorm';
4 | import { ConfigModule, ConfigService } from '@nestjs/config';
5 |
6 | @Module({
7 | imports: [
8 | TypeOrmModule.forRootAsync({
9 | imports: [ConfigModule],
10 | inject: [ConfigService],
11 | useFactory: (configService: ConfigService) => {
12 | const dataBaseOptions = configService.get(
13 | 'EnvDataBaseOptions',
14 | );
15 | return {
16 | type: dataBaseOptions.type,
17 | host: dataBaseOptions.host,
18 | port: dataBaseOptions.port as number,
19 | username: dataBaseOptions.username,
20 | password: dataBaseOptions.password,
21 | database: dataBaseOptions.database,
22 | entities: dataBaseOptions.entities,
23 | synchronize: dataBaseOptions.synchronize === '1',
24 | logging: dataBaseOptions.logging === '1',
25 | entityPrefix: dataBaseOptions.entityPrefix,
26 | };
27 | },
28 | }),
29 | ],
30 | })
31 | export class DataBaseModule {}
32 |
--------------------------------------------------------------------------------
/src/services/common/jwt/jwt.service.ts:
--------------------------------------------------------------------------------
1 | import { EnvJwtOptions } from '@/config/env/jwt.config';
2 | import { ConfigService } from '@nestjs/config';
3 | import { IAnyObject } from '@/interfaces/common.interface';
4 | import * as jwt from 'jsonwebtoken';
5 | import { Injectable } from '@nestjs/common';
6 |
7 | /**
8 | * jwt实现类
9 | */
10 | @Injectable()
11 | export class JwtService {
12 | constructor(private readonly config: ConfigService) {}
13 | /**
14 | * 生成token
15 | * @param payload
16 | */
17 | public sign(payload: string | IAnyObject | Buffer): string {
18 | const tokenOptions = this.config.get('EnvJwtOptions');
19 | const secretOrPrivateKey = tokenOptions.secret;
20 | const options: jwt.SignOptions = {
21 | expiresIn: tokenOptions.expiresIn,
22 | };
23 | return jwt.sign(payload, secretOrPrivateKey, options);
24 | }
25 |
26 | /**
27 | * 校验token
28 | * @param token
29 | */
30 | public async verifyToken(token: string) {
31 | try {
32 | const tokenOptions = this.config.get('EnvJwtOptions');
33 | const secretOrPrivateKey = tokenOptions.secret;
34 | return await jwt.verify(token, secretOrPrivateKey);
35 | } catch (e) {
36 | throw e;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/services/common/redis/redis-client.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { RedisService } from 'nestjs-redis';
3 | import { Redis } from 'ioredis';
4 |
5 | @Injectable()
6 | export class RedisClientService {
7 | public client: Redis;
8 | constructor(private redisService: RedisService) {}
9 |
10 | onModuleInit() {
11 | this.getClient();
12 | }
13 |
14 | public getClient() {
15 | this.client = this.redisService.getClient();
16 | }
17 |
18 | public async set(
19 | key: string,
20 | value: Record | string,
21 | second?: number,
22 | ) {
23 | value = JSON.stringify(value);
24 | // 如果没有传递时间就默认时间
25 | if (!second) {
26 | await this.client.setex(key, 24 * 60 * 60, value); // 秒为单位
27 | } else {
28 | await this.client.set(key, value, 'EX', second);
29 | }
30 | }
31 |
32 | public async get(key: string): Promise {
33 | const data = await this.client.get(key);
34 | if (data) {
35 | return JSON.parse(data);
36 | } else {
37 | return null;
38 | }
39 | }
40 |
41 | public async del(key: string): Promise {
42 | await this.client.del(key);
43 | }
44 |
45 | public async flushall(): Promise {
46 | await this.client.flushall();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/services/common/redis/redis.cache.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { RedisClientService } from './redis-client.service';
3 | @Injectable()
4 | export class RedisCacheService extends RedisClientService {
5 | /**
6 | * 设置
7 | * @param key
8 | * @param value
9 | * @param second
10 | */
11 | public async set(key: string, value: any, second?: number): Promise {
12 | value = JSON.stringify(value);
13 | if (!this.client) {
14 | this.getClient();
15 | }
16 | if (!second) {
17 | await this.client.set(key, value);
18 | } else {
19 | await this.client.set(key, value, 'EX', second);
20 | }
21 | }
22 |
23 | public async get(key: string): Promise {
24 | if (!this.client) {
25 | this.getClient();
26 | }
27 | const data = await this.client.get(key);
28 | if (data) {
29 | return JSON.parse(data);
30 | } else {
31 | return null;
32 | }
33 | }
34 |
35 | public async del(key: string): Promise {
36 | if (!this.client) {
37 | this.getClient();
38 | }
39 | await this.client.del(key);
40 | }
41 |
42 | public async flushall(): Promise {
43 | if (!this.client) {
44 | this.getClient();
45 | }
46 | await this.client.flushall();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/.cz-config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | types: [
5 | {
6 | value: 'feat',
7 | name: '✨ feat: 新功能',
8 | },
9 | {
10 | value: 'fix',
11 | name: '🐛 fix: 修复bug',
12 | },
13 | {
14 | value: 'refactor',
15 | name: '♻️ refactor: 代码重构(既不是新功能也不是改bug)',
16 | },
17 | {
18 | value: 'chore',
19 | name: '🎫 chore: 修改流程配置',
20 | },
21 | {
22 | value: 'docs',
23 | name: '📝 docs: 修改了文档',
24 | },
25 | {
26 | value: 'test',
27 | name: '✅ test: 更新了测试用例',
28 | },
29 | {
30 | value: 'style',
31 | name: '💄 style: 修改了样式文件',
32 | },
33 | {
34 | value: 'perf',
35 | name: '⚡️ perf: 新能优化',
36 | },
37 | {
38 | value: 'revert',
39 | name: '⏪ revert: 回退提交',
40 | },
41 | ],
42 | scopes: [],
43 | allowCustomScopes: true,
44 | allowBreakingChanges: ['feat', 'fix'],
45 | subjectLimit: 50,
46 | messages: {
47 | type: '请选择你本次改动的修改类型',
48 | customScope: '\n请明确本次改动的范围(可填):',
49 | subject: '简短描述本次改动:\n',
50 | body: '详细描述本次改动 (可填). 使用 "|" 换行:\n',
51 | breaking: '请列出任何 BREAKING CHANGES (可填):\n',
52 | footer: '请列出本次改动关闭的ISSUE (可填). 比如: #31, #34:\n',
53 | confirmCommit: '你确定提交本次改动吗?',
54 | },
55 | };
56 |
--------------------------------------------------------------------------------
/src/guard/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { ICurrentUser } from '@/interfaces/account.interface';
2 | import { IToken } from '@/interfaces/user.interface';
3 | import { ApiCodeEnum } from '@/enum/api-code.enum';
4 | import { ApiException } from '@/exception/api-exception';
5 | import { JwtService } from '@/services/common/jwt/jwt.service';
6 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
7 |
8 | @Injectable()
9 | export class AuthGuard implements CanActivate {
10 | constructor(private readonly jwtService: JwtService) {}
11 | async canActivate(context: ExecutionContext): Promise {
12 | const request = context.switchToHttp().getRequest();
13 | const requestToken =
14 | request.headers['authtoken'] || request.headers['AuthToken'];
15 | if (requestToken) {
16 | try {
17 | const ret = await this.jwtService.verifyToken(requestToken);
18 | const { sub, account } = ret as IToken;
19 | const currentUser: ICurrentUser = {
20 | userId: sub,
21 | account,
22 | };
23 | request.currentUser = currentUser;
24 | } catch (e) {
25 | throw new ApiException('token格式不正确', ApiCodeEnum.ERROR);
26 | }
27 | } else {
28 | throw new ApiException('你还没登录,请先登录', ApiCodeEnum.SHOULD_LOGIN);
29 | }
30 | return true;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/modules/base/email.module.ts:
--------------------------------------------------------------------------------
1 | import { ConfigModule, ConfigService } from '@nestjs/config';
2 | import { Module } from '@nestjs/common';
3 | import { MailerModule } from '@nestjs-modules/mailer';
4 | import { EjsAdapter } from '@nestjs-modules/mailer/dist/adapters/ejs.adapter';
5 | import { EnvEmailOptions } from '@/config/env/email.config';
6 | import * as path from 'path';
7 |
8 | @Module({
9 | imports: [
10 | MailerModule.forRootAsync({
11 | imports: [ConfigModule],
12 | inject: [ConfigService],
13 | useFactory: (config: ConfigService) => {
14 | const emailOptions = config.get('EnvEmailOptions');
15 | return {
16 | transport: {
17 | host: emailOptions.host,
18 | port: emailOptions.port,
19 | auth: {
20 | user: emailOptions.user,
21 | pass: emailOptions.password,
22 | },
23 | },
24 | defaults: {
25 | from: emailOptions.from,
26 | },
27 | template: {
28 | dir: path.join(__dirname, '../../assets/email-template'),
29 | adapter: new EjsAdapter(),
30 | options: {
31 | strict: true,
32 | },
33 | },
34 | };
35 | },
36 | }),
37 | ],
38 | })
39 | export class EmailModule {}
40 |
--------------------------------------------------------------------------------
/src/pipes/validation.pipe.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-02 10:22:19
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-03 13:48:40
6 | */
7 |
8 | import {
9 | ArgumentMetadata,
10 | Injectable,
11 | PipeTransform,
12 | HttpStatus,
13 | } from '@nestjs/common';
14 | import { validate } from 'class-validator';
15 | import { plainToClass } from 'class-transformer';
16 | import { ApiException } from '@/exception/api-exception';
17 | import { ApiCodeEnum } from '@/enum/api-code.enum';
18 |
19 | @Injectable()
20 | export class ValidationPipe implements PipeTransform {
21 | async transform(value: any, metadata: ArgumentMetadata) {
22 | const { metatype } = metadata;
23 | // 如果没有传入验证规则,则不验证,直接返回数据
24 | if (!metatype || !this.toValidate(metatype)) {
25 | return value;
26 | }
27 | // 将对象转换为 Class 来验证
28 | const object = plainToClass(metatype, value);
29 | const errors = await validate(object);
30 | if (errors.length > 0) {
31 | //获取第一个错误并且返回
32 | const msg = Object.values(errors[0].constraints)[0];
33 | // 统一抛出异常
34 | throw new ApiException(`${msg}`, ApiCodeEnum.WARN, HttpStatus.OK);
35 | }
36 | return value;
37 | }
38 |
39 | private toValidate(metatype: any): boolean {
40 | const types = [String, Boolean, Number, Array, Object];
41 | return !types.includes(metatype);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/controllers/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { RegisterDTO, LoginDTO, EmailDTO } from '@/dtos/user/user.dto';
2 | import { UserService } from '@/services/user/user.service';
3 | import { Controller, Post, Body, Get } from '@nestjs/common';
4 | import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
5 |
6 | /*
7 | * 用户模块控制器
8 | * @Author: ahwgs
9 | * @Date: 2020-11-21 14:46:44
10 | * @Last Modified by: ahwgs
11 | * @Last Modified time: 2020-12-03 16:59:51
12 | */
13 |
14 | @ApiBearerAuth()
15 | @ApiTags('用户模块')
16 | @ApiBearerAuth()
17 | @Controller('user')
18 | export class UserController {
19 | constructor(private readonly userService: UserService) {}
20 |
21 | @ApiOperation({ summary: '注册', description: '账号注册' })
22 | @Post('register')
23 | async register(@Body() body: RegisterDTO) {
24 | return this.userService.register(body);
25 | }
26 |
27 | @ApiOperation({ summary: '图形验证码', description: '账号注册' })
28 | @Get('captcha')
29 | async imgCaptcha() {
30 | return this.userService.createImgCaptcha();
31 | }
32 |
33 | @ApiOperation({ summary: '登录', description: '账号登录' })
34 | @Post('login')
35 | async login(@Body() body: LoginDTO) {
36 | return this.userService.login(body);
37 | }
38 |
39 | @ApiOperation({ summary: '邮箱验证码', description: '邮箱验证码' })
40 | @Post('email_code')
41 | async registerEmail(@Body() body: EmailDTO) {
42 | return this.userService.emailCode(body);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/entities/user.entity.ts:
--------------------------------------------------------------------------------
1 | import { PublicEntity } from './public.entity';
2 | import { Column, Entity } from 'typeorm';
3 | import { Exclude } from 'class-transformer';
4 | import { AccountEnum, SexEnum } from '@/enum/user.enum';
5 |
6 | /*
7 | * 用户实体类
8 | * @Author: ahwgs
9 | * @Date: 2020-11-20 20:05:46
10 | * @Last Modified by: ahwgs
11 | * @Last Modified time: 2020-12-03 16:30:40
12 | */
13 |
14 | @Entity({ name: 'user' })
15 | export class UserEntity extends PublicEntity {
16 | @Column({
17 | type: 'varchar',
18 | name: 'account',
19 | comment: '账号',
20 | default: '',
21 | nullable: false,
22 | })
23 | account: string;
24 |
25 | @Exclude() // 表示排除字段不返回给前端
26 | @Column({
27 | type: 'varchar',
28 | name: 'password',
29 | comment: '密码',
30 | nullable: false,
31 | default: '',
32 | })
33 | password: string;
34 |
35 | @Column({
36 | type: 'tinyint',
37 | default: AccountEnum.EMAIL,
38 | comment: '账号类型 0:邮箱 1:手机号',
39 | name: 'account_type',
40 | nullable: false,
41 | })
42 | accountType: number;
43 |
44 | @Column({
45 | type: 'tinyint',
46 | default: SexEnum.UNKWON,
47 | nullable: false,
48 | comment: '性别 0:男 1:女 2:未知',
49 | })
50 | sex: number;
51 |
52 | @Exclude() // 表示排除字段不返回给前端
53 | @Column({
54 | name: 'password_slat',
55 | comment: '密码盐',
56 | type: 'varchar',
57 | nullable: false,
58 | })
59 | passwordSalt: string;
60 | }
61 |
--------------------------------------------------------------------------------
/src/assets/banner.txt:
--------------------------------------------------------------------------------
1 | ////////////////////////////////////////////////////////////////////
2 | // _ooOoo_ //
3 | // o8888888o //
4 | // 88" . "88 //
5 | // (| ^_^ |) //
6 | // O\ = /O //
7 | // ____/`---'\____ //
8 | // .' \\| |// `. //
9 | // / \\||| : |||// \ //
10 | // / _||||| -:- |||||- \ //
11 | // | | \\\ - /// | | //
12 | // | \_| ''\---/'' | | //
13 | // \ .-\__ `-` ___/-. / //
14 | // ___`. .' /--.--\ `. . ___ //
15 | // ."" '< `.___\_<|>_/___.' >'"". //
16 | // | | : `- \`.;`\ _ /`;.`/ - ` : | | //
17 | // \ \ `-. \_ __\ /__ _/ .-` / / //
18 | // ========`-.____`-.___\_____/___.-`____.-'======== //
19 | // `=---=' //
20 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
21 | // 佛祖保佑 永无BUG 永不修改 //
22 | ////////////////////////////////////////////////////////////////////
--------------------------------------------------------------------------------
/src/config/env/databse.config.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * 数据库配置
3 | * @Author: ahwgs
4 | * @Date: 2020-11-20 20:07:54
5 | * @Last Modified by: ahwgs
6 | * @Last Modified time: 2020-11-30 14:38:52
7 | */
8 |
9 | import { registerAs } from '@nestjs/config';
10 | import * as path from 'path';
11 | export type DataBaseType = 'mysql' | 'mariadb';
12 |
13 | export interface EnvDataBaseOptions {
14 | /**
15 | * 数据库类型
16 | */
17 | type: DataBaseType;
18 | /**
19 | * 数据库主机地址
20 | */
21 | host: string;
22 | /**
23 | * 数据库端口
24 | */
25 | port: number | string;
26 |
27 | /**
28 | * 数据库用户名
29 | */
30 | username: string;
31 |
32 | /**
33 | * 数据库密码
34 | */
35 | password: string;
36 | /**
37 | * 数据库名
38 | */
39 | database: string;
40 | /**
41 | * 实体路径
42 | */
43 | entities: any[];
44 |
45 | /**
46 | * 日志是否开启 执行sql语句时候输出原生sql
47 | */
48 | logging: string;
49 |
50 | /**
51 | * 是否同步true表示会自动将src/entity里面定义的数据模块同步到数据库生成数据表(已经存在的表的时候再运行会报错)
52 | */
53 | synchronize: string;
54 | /**
55 | * 实体表 公共前缀
56 | */
57 | entityPrefix: string;
58 | }
59 |
60 | // 实体文件应该使用js
61 | const entitiesPath = path.resolve('./**/*.entity.js');
62 |
63 | export default registerAs(
64 | 'EnvDataBaseOptions',
65 | (): EnvDataBaseOptions => ({
66 | type: process.env.DB_TYPE as DataBaseType,
67 | host: process.env.DB_HOST,
68 | port: process.env.DB_PORT,
69 | username: process.env.DB_USERNAME,
70 | password: process.env.DB_PASSWORD,
71 | database: process.env.DB_DATABASE,
72 | entities: [entitiesPath],
73 | logging: process.env.DB_LOGGING,
74 | synchronize: process.env.DB_SYNCHRONIZE,
75 | entityPrefix: process.env.DB_TABLE_PREFIX,
76 | }),
77 | );
78 |
--------------------------------------------------------------------------------
/src/filters/http-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import { ApiCodeEnum } from '@/enum/api-code.enum';
2 | import { ApiException } from '@/exception/api-exception';
3 | import { CommonText } from '@/config/module/cmomon.text';
4 | import { IHttpResponse } from '@/interfaces/http.interface';
5 |
6 | import {
7 | ArgumentsHost,
8 | Catch,
9 | ExceptionFilter,
10 | HttpException,
11 | HttpStatus,
12 | } from '@nestjs/common';
13 |
14 | @Catch()
15 | export class HttpExceptionFilter implements ExceptionFilter {
16 | catch(exception: HttpException, host: ArgumentsHost) {
17 | const ctx = host.switchToHttp();
18 | const response = ctx.getResponse();
19 | const request = ctx.getRequest();
20 | const timestamp = Date.now();
21 | let errorResponse: IHttpResponse = null;
22 | const message = exception.message;
23 | const path = request.url;
24 | const method = request.method;
25 | const result = null;
26 | if (exception instanceof ApiException) {
27 | const message = exception.getErrorMessage();
28 | errorResponse = {
29 | result,
30 | code: exception.getErrorCode(),
31 | message,
32 | path,
33 | method,
34 | timestamp,
35 | };
36 | } else {
37 | errorResponse = {
38 | result,
39 | message:
40 | typeof message === 'string'
41 | ? message || CommonText.REQUEST_ERROR
42 | : JSON.stringify(message),
43 | path,
44 | method,
45 | timestamp,
46 | code: ApiCodeEnum.ERROR,
47 | };
48 | }
49 |
50 | response.status(HttpStatus.OK);
51 | response.header('Content-Type', 'application/json; charset=utf-8');
52 | response.send(errorResponse);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/dtos/user/user.dto.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-12-01 15:31:12
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-12-03 16:57:28
6 | */
7 |
8 | import {
9 | IsEmail,
10 | IsNotEmpty,
11 | IsNumber,
12 | IsString,
13 | IsUUID,
14 | } from 'class-validator';
15 | import { ApiProperty } from '@nestjs/swagger';
16 |
17 | export class UserAccountDTO {
18 | @ApiProperty({
19 | name: 'account',
20 | })
21 | @IsNotEmpty({ message: '账号不能为空' })
22 | @IsString({ message: '账号必须是 String 类型' })
23 | readonly account: string;
24 |
25 | @ApiProperty({
26 | name: 'password',
27 | })
28 | @IsNotEmpty({ message: '密码不能为空' })
29 | @IsString({ message: '密码必须是 String 类型' })
30 | readonly password: string;
31 |
32 | @ApiProperty({
33 | name: 'accountType',
34 | })
35 | @IsNotEmpty({ message: '账号类型不能为空' })
36 | @IsNumber(
37 | {},
38 | {
39 | message: '账号类型只能是数字',
40 | },
41 | )
42 | readonly accountType: number;
43 | }
44 | export class LoginDTO extends UserAccountDTO {
45 | @ApiProperty({
46 | name: 'code',
47 | })
48 | @IsNotEmpty({ message: '图形验证码不能为空' })
49 | @IsString({ message: '图形验证码必须是 String 类型' })
50 | readonly code: string;
51 |
52 | @ApiProperty({
53 | name: 'codeId',
54 | })
55 | @IsNotEmpty({ message: '图形验证码唯一id不能为空' })
56 | @IsUUID(4, { message: 'codeId不是UUID' })
57 | readonly codeId: string;
58 | }
59 |
60 | export class RegisterDTO extends UserAccountDTO {
61 | @ApiProperty({
62 | name: 'code',
63 | })
64 | @IsNotEmpty({ message: '验证码不能为空' })
65 | @IsString({ message: '验证码必须是 String 类型' })
66 | readonly code: string;
67 | }
68 |
69 | export class EmailDTO {
70 | @ApiProperty({
71 | name: 'email',
72 | })
73 | @IsNotEmpty({ message: '邮箱不能为空' })
74 | @IsEmail({}, { message: '请输入正确的邮箱' })
75 | readonly email: string;
76 | }
77 |
--------------------------------------------------------------------------------
/src/interceptor/logger.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CallHandler,
3 | ExecutionContext,
4 | Injectable,
5 | NestInterceptor,
6 | } from '@nestjs/common';
7 | import { Observable } from 'rxjs';
8 | import { catchError, tap } from 'rxjs/operators';
9 | import { Logger } from '@/utils/log4js';
10 | import { ApiException } from '@/exception/api-exception';
11 | @Injectable()
12 | export class LoggingInterceptor implements NestInterceptor {
13 | private genAccessLog(request, time, res, status, context): any {
14 | const log = {
15 | statusCode: status,
16 | responseTime: `${Date.now() - time}ms`,
17 | ip: request.ip,
18 | header: request.headers,
19 | query: request.query,
20 | params: request.params,
21 | body: request.body,
22 | response: res,
23 | };
24 | Logger.access(JSON.stringify(log), `${context.getClass().name}`);
25 | }
26 | intercept(context: ExecutionContext, next: CallHandler): Observable {
27 | const request = context.switchToHttp().getRequest();
28 | const response = context.switchToHttp().getResponse();
29 | const status = response.statusCode;
30 | const now = Date.now();
31 |
32 | return next.handle().pipe(
33 | tap((res) => {
34 | // 其他的都进access
35 | this.genAccessLog(
36 | request,
37 | `${Date.now() - now}ms`,
38 | res,
39 | status,
40 | context,
41 | );
42 | }),
43 | catchError((err) => {
44 | if (err instanceof ApiException) {
45 | // 其他的都进access
46 | this.genAccessLog(
47 | request,
48 | `${Date.now() - now}ms`,
49 | err.getErrorMessage(),
50 | status,
51 | context,
52 | );
53 | Logger.error(err);
54 | } else {
55 | Logger.error(err);
56 | }
57 | // 返回原异常
58 | throw err;
59 | }),
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/config/module/log4js.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-25 02:40:20
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-11-25 02:43:09
6 | */
7 | const log4jsConfig = {
8 | appenders: {
9 | console: {
10 | type: 'console', // 会打印到控制台
11 | },
12 | access: {
13 | type: 'dateFile', // 会写入文件,并按照日期分类
14 | filename: `logs/access/access.log`, // 日志文件名
15 | alwaysIncludePattern: true,
16 | pattern: 'yyyy-MM-dd',
17 | daysToKeep: 60,
18 | numBackups: 3,
19 | category: 'http',
20 | keepFileExt: true, // 是否保留文件后缀
21 | },
22 | app: {
23 | type: 'dateFile',
24 | filename: `logs/app-out/app.log`,
25 | alwaysIncludePattern: true,
26 | layout: {
27 | type: 'pattern',
28 | pattern:
29 | '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
30 | },
31 | // 日志文件按日期(天)切割
32 | pattern: 'yyyy-MM-dd',
33 | daysToKeep: 60,
34 | numBackups: 3,
35 | keepFileExt: true,
36 | },
37 | errorFile: {
38 | type: 'dateFile',
39 | filename: `logs/errors/error.log`,
40 | alwaysIncludePattern: true,
41 | layout: {
42 | type: 'pattern',
43 | pattern:
44 | '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}',
45 | },
46 | // 日志文件按日期(天)切割
47 | pattern: 'yyyy-MM-dd',
48 | daysToKeep: 60,
49 | // maxLogSize: 10485760,
50 | numBackups: 3,
51 | keepFileExt: true,
52 | },
53 | errors: {
54 | type: 'logLevelFilter',
55 | level: 'ERROR',
56 | appender: 'errorFile',
57 | },
58 | },
59 | categories: {
60 | default: {
61 | appenders: ['console', 'app', 'errors'],
62 | level: 'DEBUG',
63 | },
64 | info: { appenders: ['console', 'app', 'errors'], level: 'info' },
65 | access: { appenders: ['console', 'app', 'errors'], level: 'info' },
66 | http: { appenders: ['access'], level: 'DEBUG' },
67 | },
68 | pm2: true, // 使用 pm2 来管理项目时,打开
69 | pm2InstanceVar: 'INSTANCE_ID', // 会根据 pm2 分配的 id 进行区分,以免各进程在写日志时造成冲突
70 | };
71 |
72 | export default log4jsConfig;
73 |
--------------------------------------------------------------------------------
/src/modules/base/config.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import envSwaggerConfig from '@/config/env/swagger.config';
3 | import envDataBaseConfig from '@/config/env/databse.config';
4 | import envRedisConfig from '@/config/env/redis.config';
5 | import envEmailConfig from '@/config/env/email.config';
6 | import * as Joi from 'joi';
7 | import { ConfigModule } from '@nestjs/config';
8 | import envJwtConfig from '@/config/env/jwt.config';
9 | @Module({
10 | imports: [
11 | ConfigModule.forRoot({
12 | encoding: 'utf-8',
13 | load: [
14 | envSwaggerConfig,
15 | envDataBaseConfig,
16 | envRedisConfig,
17 | envJwtConfig,
18 | envEmailConfig,
19 | ],
20 | expandVariables: true, // 开启嵌套变量
21 | ignoreEnvVars: true,
22 | validationSchema: Joi.object({
23 | SERVE_LINTENER_PORT: Joi.number().default(3000),
24 | SWAGGER_SETUP_PATH: Joi.string().default(''),
25 | SWAGGER_ENDPOINT_PREFIX: Joi.string().default(''),
26 | SWAGGER_UI_TITLE: Joi.string().default(''),
27 | SWAGGER_UI_TITLE_DESC: Joi.string().default(''),
28 | SWAGGER_API_VERSION: Joi.string().default(''),
29 | NODE_ENV: Joi.string()
30 | .valid('development', 'production', 'test', 'provision')
31 | .default('development'),
32 | DB_TYPE: Joi.string().default('mysql'),
33 | DB_HOST: Joi.string().default('localhost'),
34 | DB_PORT: Joi.number().default(3306),
35 | DB_DATABASE: Joi.string().default(''),
36 | DB_USERNAME: Joi.string().default('root'),
37 | DB_PASSWORD: Joi.string().default(''),
38 | DB_SYNCHRONIZE: Joi.string().default('1'), // 1 true 0 false
39 | DB_LOGGING: Joi.string().default('1'), // 1 true 0 false
40 | DB_TABLE_PREFIX: Joi.string().default('t_'), // 1 true 0 false
41 | REDIS_HOST: Joi.string().default('localhost'), // 1 true 0 false
42 | REDIS_PORT: Joi.number().default(6379),
43 | REDIS_PASSWORD: Joi.string().default('').allow(''),
44 | TOKEN_SECRET: Joi.string().default('').allow(''),
45 | TOKEN_EXPIRES: Joi.string().default('').allow(''),
46 | EMAIL_HOST: Joi.string().default(''),
47 | EMAIL_PORT: Joi.string().default(''),
48 | EAMIL_AUTH_USER: Joi.string().default(''),
49 | EMAIL_AUTH_PASSWORD: Joi.string().default(''),
50 | EMAIL_FROM: Joi.string().default(''),
51 | validationOptions: {
52 | allowUnknown: false, // 控制是否允许环境变量中未知的键。默认为true。
53 | abortEarly: true, // 如果为true,在遇到第一个错误时就停止验证;如果为false,返回所有错误。默认为false。
54 | },
55 | }),
56 | }),
57 | ],
58 | })
59 | export class CustomConfigModule {}
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Fast-nest-temp
2 |
3 | ## 介绍
4 |
5 | 基于`Nest.js@7.x`快速构建 Web 应用
6 |
7 | ### 依赖
8 |
9 | - @nestjs/core 7.5.1 核心包
10 | - @nestjs/config 环境变量治理
11 | - @nestjs/swagger 生成接口文档
12 | - swagger-ui-express 装@nestjs/swagger 必装的包 处理接口文档样式
13 | - joi 校验参数
14 | - log4js 日志处理
15 | - helmet 处理基础 web 漏洞
16 | - compression 服务端压缩中间件
17 | - express-rate-limit 请求次数限制
18 | - typeorm 数据库 orm 框架
19 | - @nestjs/typeorm nest typeorm 集成
20 | - ejs 模版引擎
21 | - class-validator 校验参数
22 | - ioredis redis 客户端
23 | - nestjs-redis nest redis 配置模块
24 | - uuid uuid 生成器
25 | - @nestjs-modules/mailer 邮箱发送
26 |
27 |
28 |
29 |
30 | ### 如何使用
31 |
32 | - 复制根目录下`default.env`文件,重命名为`.env`文件,修改其配置
33 | - `yarn start:dev` 开始开发
34 | - `yarn start:debug` 开始debug模式
35 | - `yarn commit` 用于提交代码,默认遵循业内规则
36 | - `yarn start` 启动项目
37 |
38 | ### 约束
39 |
40 | - 接口返回值约束 `interface IHttpResponse`
41 |
42 | ```json
43 | {
44 | "result": null,
45 | "message": "", // 消息提示,错误消息
46 | "code": 0, // 业务状态码
47 | "path": "/url", // 接口请求地址
48 | "method": "GET", // 接口方法
49 | "timestamp": 1 // 接口响应时间
50 | }
51 | ```
52 |
53 | - 接口 `HttpExceptionFilter` 过滤器
54 | - 业务状态码与`Http StatusCode`约定
55 |
56 | 无论接口是否异常或错误,`Http StatusCode`都为`200`
57 |
58 | ### 管道
59 |
60 | - 管道 `ParsePagePipe` 校验分页入参
61 | - 管道 `ValidationPipe` 结合 `DTO` 校验入参
62 |
63 | ### 过滤器
64 |
65 | - `HttpExceptionFilter` 异常过滤器
66 |
67 | 默认处理所有异常,返回`Http StatusCode`都为 200
68 |
69 | ### 拦截器
70 |
71 | - `LoggingInterceptor` 日志拦截器
72 |
73 | 处理日志,结合`log4js`使用
74 |
75 | - `TransformInterceptor` 数据转换拦截器
76 |
77 | 处理返回结果,返回结果如`THttpResponse`
78 |
79 | ### 日志处理
80 |
81 | - 默认输出 `logs/access logs/app-out logs/errors`
82 | - 可修改 `module/log4js` 下配置
83 |
84 | ### 守卫
85 |
86 | - `AuthGuard` 授权守卫
87 |
88 | 对于要登录才能访问的接口,请使用该守卫进行校验
89 |
90 | 在请求头中设置 `AuthToken : tokenxxx`
91 |
92 | ### 装饰器
93 |
94 | - `@CurrentUser`
95 |
96 | 获取当前登录用户信息
97 |
98 | ### 常见问题
99 |
100 | - 如何修改接口文档地址
101 |
102 | 设置`.env`文件内相应环境变量
103 |
104 | - 如何修改启动 banner
105 |
106 | 目前启动`banner`读取的是`src/assets/banner.txt`,自行修改该文件即可
107 |
108 | - 使用 log4js 作为默认日志库
109 |
110 | ```typescript
111 | const app = await NestFactory.create(AppModule, {
112 | logger: new Logger(),
113 | });
114 | ```
115 |
116 | 通过重写`LoggerService`上的相关方法,实现 `Nest` 日志自定义
117 |
118 | - 邮箱配置提示报错
119 |
120 | 因邮箱没区分环境,默认使用了一份配置,所以把邮箱配置忽略上传了,可参考根目录下 `default.email.env`
121 | 在项目`condif/env/` 下新建`email.env`文件
122 |
123 | - 验证码
124 |
125 | 简单起见,登录验证码直接使用图形验证码+Redis实现
126 | 注册验证码使用邮箱服务+mysql code表实现,为了安全建议邮箱验证码也使用Redis实现
127 |
128 | - 邮箱模版提示未找到文件
129 |
130 | 需要修改`nest-cli.json`配置`assets`属性
131 |
132 | 如:
133 |
134 | ```json
135 | {
136 | "collection": "@nestjs/schematics",
137 | "sourceRoot": "src",
138 | "compilerOptions": {
139 | "assets": [
140 | {
141 | "include": "assets/email-template/**/*",
142 | "watchAssets": true
143 | }
144 | ]
145 | }
146 | }
147 | ```
148 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fast-nest-temp",
3 | "version": "0.0.1",
4 | "author": "ahwgs ",
5 | "license": "MIT",
6 | "description": "",
7 | "private": true,
8 | "scripts": {
9 | "start": "node dist/main.js",
10 | "start:dev": "npm run prebuild && nest start --watch",
11 | "start:debug": "nest start --debug --watch",
12 | "prebuild": "rimraf dist",
13 | "build": "nest build",
14 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "commit": "git cz"
17 | },
18 | "dependencies": {
19 | "@nestjs-modules/mailer": "^1.5.1",
20 | "@nestjs/common": "^7.5.1",
21 | "@nestjs/config": "^0.5.0",
22 | "@nestjs/core": "^7.5.1",
23 | "@nestjs/platform-express": "^7.5.1",
24 | "@nestjs/swagger": "^4.7.3",
25 | "@nestjs/typeorm": "7.1.4",
26 | "add": "^2.0.6",
27 | "bcrypt": "^5.0.0",
28 | "class-transformer": "^0.3.1",
29 | "class-validator": "^0.12.2",
30 | "compression": "^1.7.4",
31 | "ejs": "^3.1.5",
32 | "express-rate-limit": "^5.1.3",
33 | "helmet": "^4.2.0",
34 | "ioredis": "^4.19.2",
35 | "joi": "^17.3.0",
36 | "jsonwebtoken": "^8.5.1",
37 | "lodash": "^4.17.20",
38 | "log4js": "^6.3.0",
39 | "moment": "^2.29.1",
40 | "mysql": "^2.18.1",
41 | "nestjs-redis": "^1.2.8",
42 | "nodemailer": "^6.4.16",
43 | "reflect-metadata": "^0.1.13",
44 | "request-ip": "^2.1.3",
45 | "rimraf": "^3.0.2",
46 | "rxjs": "^6.6.3",
47 | "stacktrace-js": "^2.0.2",
48 | "svg-captcha": "^1.4.0",
49 | "swagger-ui-express": "^4.1.4",
50 | "typeorm": "0.2.28",
51 | "uuid": "^8.3.1",
52 | "yarn": "^1.22.10"
53 | },
54 | "devDependencies": {
55 | "@commitlint/cli": "^11.0.0",
56 | "@commitlint/config-conventional": "^11.0.0",
57 | "@nestjs/cli": "^7.5.1",
58 | "@nestjs/schematics": "^7.1.3",
59 | "@nestjs/testing": "^7.5.1",
60 | "@types/ejs": "^3.0.5",
61 | "@types/express": "^4.17.8",
62 | "@types/hapi__joi": "^17.1.6",
63 | "@types/jsonwebtoken": "^8.5.0",
64 | "@types/lodash": "^4.14.165",
65 | "@types/node": "^14.14.6",
66 | "@types/supertest": "^2.0.10",
67 | "@typescript-eslint/eslint-plugin": "^4.6.1",
68 | "@typescript-eslint/parser": "^4.6.1",
69 | "commitizen": "^4.2.2",
70 | "cross-env": "^7.0.2",
71 | "cz-customizable": "^6.3.0",
72 | "eslint": "^7.12.1",
73 | "eslint-config-prettier": "^6.15.0",
74 | "eslint-plugin-prettier": "^3.1.4",
75 | "husky": "^4.3.0",
76 | "lint-staged": "^10.5.1",
77 | "prettier": "^2.1.2",
78 | "supertest": "^6.0.0",
79 | "ts-loader": "^8.0.8",
80 | "ts-node": "^9.0.0",
81 | "tsconfig-paths": "^3.9.0",
82 | "typescript": "^4.0.5"
83 | },
84 | "husky": {
85 | "hooks": {
86 | "pre-commit": "lint-staged",
87 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS "
88 | }
89 | },
90 | "lint-staged": {
91 | "src/**/*.{js,ts,tsx,jsx}": [
92 | "prettier --write",
93 | "eslint --fix"
94 | ]
95 | },
96 | "config": {
97 | "commitizen": {
98 | "path": "node_modules/cz-customizable"
99 | }
100 | },
101 | "commitlint": {
102 | "extends": [
103 | "@commitlint/config-conventional"
104 | ]
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { LoggingInterceptor } from '@/interceptor/logger.interceptor';
2 | import { TransformInterceptor } from '@/interceptor/transform.interceptor';
3 | import { NestFactory } from '@nestjs/core';
4 | import { AppModule } from './app.module';
5 | import { ConfigService } from '@nestjs/config';
6 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
7 | import * as helmet from 'helmet';
8 | import * as compression from 'compression';
9 | import * as rateLimit from 'express-rate-limit';
10 | import * as path from 'path';
11 | import { NestExpressApplication } from '@nestjs/platform-express';
12 | import { renderFile } from 'ejs';
13 | import { Logger } from '@/utils/log4js';
14 | import { EnvSwaggerOptions } from '@/config/env/swagger.config';
15 | import { HttpExceptionFilter } from '@/filters/http-exception.filter';
16 | import { ValidationPipe } from '@/pipes/validation.pipe';
17 | import { terminalHelpTextConsole } from '@/utils/terminal-help-text-console';
18 |
19 | /**
20 | * 启动函数
21 | */
22 | async function bootstrap() {
23 | try {
24 | const app = await NestFactory.create(AppModule, {
25 | logger: new Logger(),
26 | });
27 |
28 | // app.use(logger);
29 |
30 | const configService = app.get(ConfigService);
31 | const swaggerOptions = configService.get(
32 | 'EnvSwaggerOptions',
33 | );
34 |
35 | /**
36 | * 设置CORS
37 | * https://github.com/expressjs/cors#configuration-options
38 | */
39 | app.enableCors({
40 | origin: true,
41 | credentials: true,
42 | });
43 |
44 | /**
45 | * web安全
46 | */
47 | app.use(helmet());
48 |
49 | /**
50 | * 压缩中间件
51 | */
52 | app.use(compression());
53 |
54 | // 给请求添加prefix
55 | app.setGlobalPrefix(swaggerOptions.prefix);
56 |
57 | /**
58 | * 限速
59 | */
60 | app.use(
61 | rateLimit({
62 | windowMs: 15 * 60 * 1000, // 15 minutes
63 | max: 100, // limit each IP to 100 requests per windowMs
64 | }),
65 | );
66 |
67 | app.useStaticAssets(path.join(__dirname, '..', 'public'));
68 | app.setBaseViewsDir(path.join(__dirname, '..', 'public/views'));
69 | app.engine('html', renderFile);
70 | app.setViewEngine('ejs');
71 |
72 | // 全局注册错误的过滤器(错误异常)
73 | app.useGlobalFilters(new HttpExceptionFilter());
74 |
75 | // 全局注册拦截器
76 | app.useGlobalInterceptors(
77 | new TransformInterceptor(),
78 | new LoggingInterceptor(),
79 | );
80 |
81 | // 全局注册管道
82 | app.useGlobalPipes(new ValidationPipe());
83 |
84 | const options = new DocumentBuilder()
85 | .setTitle(swaggerOptions.title)
86 | .setDescription(swaggerOptions.desc)
87 | .setVersion(swaggerOptions.version)
88 | .addBearerAuth()
89 | .build();
90 | const document = SwaggerModule.createDocument(app, options);
91 |
92 | SwaggerModule.setup(swaggerOptions.setupUrl, app, document, {
93 | customSiteTitle: swaggerOptions.title,
94 | swaggerOptions: {
95 | explorer: true,
96 | docExpansion: 'list',
97 | filter: true,
98 | showRequestDuration: true,
99 | syntaxHighlight: {
100 | active: true,
101 | theme: 'tomorrow-night',
102 | },
103 | },
104 | });
105 | await app.listen(configService.get('SERVE_LISTENER_PORT'));
106 | } catch (err) {
107 | throw err;
108 | }
109 | }
110 | bootstrap()
111 | .then(() => {
112 | terminalHelpTextConsole();
113 | })
114 | .catch((e) => {
115 | console.log(e);
116 | Logger.error(e);
117 | });
118 |
--------------------------------------------------------------------------------
/src/utils/log4js.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * @Author: ahwgs
3 | * @Date: 2020-11-25 21:32:23
4 | * @Last Modified by: ahwgs
5 | * @Last Modified time: 2020-11-26 01:20:20
6 | */
7 | import * as Path from 'path';
8 | import * as Log4js from 'log4js';
9 | import * as Util from 'util';
10 | import * as moment from 'moment'; // 处理时间的工具
11 | import * as StackTrace from 'stacktrace-js';
12 | import Chalk from 'chalk';
13 | import log4jsConfig from '@/config/module/log4js';
14 | import { LoggerService } from '@nestjs/common';
15 |
16 | // 日志级别
17 | export enum LoggerLevel {
18 | ALL = 'ALL',
19 | MARK = 'MARK',
20 | TRACE = 'TRACE',
21 | DEBUG = 'DEBUG',
22 | INFO = 'INFO',
23 | WARN = 'WARN',
24 | ERROR = 'ERROR',
25 | FATAL = 'FATAL',
26 | OFF = 'OFF',
27 | }
28 |
29 | // 内容跟踪类
30 | export class ContextTrace {
31 | constructor(
32 | public readonly context: string,
33 | public readonly path?: string,
34 | public readonly lineNumber?: number,
35 | public readonly columnNumber?: number,
36 | ) {}
37 | }
38 |
39 | Log4js.addLayout('Awesome-nest', (logConfig: any) => {
40 | return (logEvent: Log4js.LoggingEvent): string => {
41 | let moduleName = '';
42 | let position = '';
43 |
44 | // 日志组装
45 | const messageList: string[] = [];
46 | logEvent.data.forEach((value: any) => {
47 | if (value instanceof ContextTrace) {
48 | moduleName = value.context;
49 | // 显示触发日志的坐标(行,列)
50 | if (value.lineNumber && value.columnNumber) {
51 | position = `${value.lineNumber}, ${value.columnNumber}`;
52 | }
53 | return;
54 | }
55 |
56 | if (typeof value !== 'string') {
57 | value = Util.inspect(value, false, 3, true);
58 | }
59 |
60 | messageList.push(value);
61 | });
62 |
63 | // 日志组成部分
64 | const messageOutput: string = messageList.join(' ');
65 | const positionOutput: string = position ? ` [${position}]` : '';
66 | const typeOutput = `[${logConfig.type}] ${logEvent.pid.toString()} - `;
67 | const dateOutput = `${moment(logEvent.startTime).format(
68 | 'YYYY-MM-DD HH:mm:ss',
69 | )}`;
70 | const moduleOutput: string = moduleName
71 | ? `[${moduleName}] `
72 | : '[LoggerService] ';
73 | let levelOutput = `[${logEvent.level}] ${messageOutput}`;
74 |
75 | // 根据日志级别,用不同颜色区分
76 | switch (logEvent.level.toString()) {
77 | case LoggerLevel.DEBUG:
78 | levelOutput = Chalk.green(levelOutput);
79 | break;
80 | case LoggerLevel.INFO:
81 | levelOutput = Chalk.cyan(levelOutput);
82 | break;
83 | case LoggerLevel.WARN:
84 | levelOutput = Chalk.yellow(levelOutput);
85 | break;
86 | case LoggerLevel.ERROR:
87 | levelOutput = Chalk.red(levelOutput);
88 | break;
89 | case LoggerLevel.FATAL:
90 | levelOutput = Chalk.hex('#DD4C35')(levelOutput);
91 | break;
92 | default:
93 | levelOutput = Chalk.grey(levelOutput);
94 | break;
95 | }
96 |
97 | return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow(
98 | moduleOutput,
99 | )}${levelOutput}${positionOutput}`;
100 | };
101 | });
102 |
103 | // 注入配置
104 | Log4js.configure(log4jsConfig);
105 |
106 | // 实例化
107 | const logger = Log4js.getLogger();
108 | logger.level = LoggerLevel.TRACE;
109 |
110 | export class Logger implements LoggerService {
111 | log(...args) {
112 | logger.info(Logger.getStackTrace(), ...args);
113 | }
114 | error(message: string, trace: string, ...args) {
115 | logger.error(message, trace, ...args);
116 | }
117 | warn(...args) {
118 | logger.warn(Logger.getStackTrace(), ...args);
119 | }
120 |
121 | static trace(...args) {
122 | logger.trace(Logger.getStackTrace(), ...args);
123 | }
124 |
125 | static debug(...args) {
126 | logger.debug(Logger.getStackTrace(), ...args);
127 | }
128 |
129 | static log(...args) {
130 | logger.info(Logger.getStackTrace(), ...args);
131 | }
132 |
133 | static info(...args) {
134 | logger.info(Logger.getStackTrace(), ...args);
135 | }
136 |
137 | static warn(...args) {
138 | logger.warn(Logger.getStackTrace(), ...args);
139 | }
140 |
141 | static warning(...args) {
142 | logger.warn(Logger.getStackTrace(), ...args);
143 | }
144 |
145 | static error(...args) {
146 | logger.error(Logger.getStackTrace(), ...args);
147 | }
148 |
149 | static fatal(...args) {
150 | logger.fatal(Logger.getStackTrace(), ...args);
151 | }
152 |
153 | static access(message: string, ...args) {
154 | const loggerCustom = Log4js.getLogger('http');
155 | loggerCustom.info(message, ...args);
156 | }
157 |
158 | // 日志追踪,可以追溯到哪个文件、第几行第几列
159 | static getStackTrace(deep = 2): string {
160 | const stackList: StackTrace.StackFrame[] = StackTrace.getSync();
161 | const stackInfo: StackTrace.StackFrame = stackList[deep];
162 |
163 | const lineNumber: number = stackInfo.lineNumber;
164 | const columnNumber: number = stackInfo.columnNumber;
165 | const fileName: string = stackInfo.fileName;
166 | const basename: string = Path.basename(fileName);
167 | return `${basename}(line: ${lineNumber}, column: ${columnNumber}): \n`;
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/services/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { CommonText } from '@/config/module/cmomon.text';
2 | import { DelEnum, StatusEnum } from '@/enum/common.enum';
3 | import { RedisTemp } from '@/config/module/redis-temp';
4 | import { RedisCacheService } from '@/services/common/redis/redis.cache.service';
5 | import { UserEntity } from '@/entities/user.entity';
6 | import { Injectable } from '@nestjs/common';
7 | import { InjectRepository } from '@nestjs/typeorm';
8 | import { Repository } from 'typeorm';
9 | import { ApiException } from '@/exception/api-exception';
10 | import { ImageCaptchaService } from '@/services/common/code/img-captcha.service';
11 | import { RegisterDTO, LoginDTO, EmailDTO } from '@/dtos/user/user.dto';
12 | import { ApiCodeEnum } from '@/enum/api-code.enum';
13 | import * as bcrypt from 'bcrypt';
14 | import { getUUID } from '@/utils/common';
15 | import { Logger } from '@/utils/log4js';
16 | import { IToken } from '@/interfaces/user.interface';
17 | import { JwtService } from '@/services/common/jwt/jwt.service';
18 | import { UserCodeEntity } from '@/entities/user-code.entity';
19 | import { EmailCodeService } from '@/services/common/code/email-code.service';
20 | import { IEmailParams } from '@/interfaces/common.interface';
21 | import { AccountEnum } from '@/enum/user.enum';
22 | /*
23 | * 用户模块服务类
24 | * @Author: ahwgs
25 | * @Date: 2020-11-21 14:46:51
26 | * @Last Modified by: ahwgs
27 | * @Last Modified time: 2020-12-04 16:11:08
28 | */
29 |
30 | @Injectable()
31 | export class UserService {
32 | constructor(
33 | @InjectRepository(UserEntity)
34 | private readonly usersRepository: Repository,
35 | @InjectRepository(UserCodeEntity)
36 | private readonly usersCodeResp: Repository,
37 | private readonly imageCaptchaService: ImageCaptchaService,
38 | private readonly redisService: RedisCacheService,
39 | private readonly jwt: JwtService,
40 | private readonly email: EmailCodeService,
41 | ) {}
42 |
43 | /**
44 | * 生成token
45 | * @param payload
46 | */
47 | private signToken(payload: IToken) {
48 | return this.jwt.sign(payload);
49 | }
50 |
51 | /**
52 | * 校验图形验证码是否正确
53 | * @param code 验证码
54 | * @param codeId 验证码唯一id
55 | */
56 | private async chenckImageCaptcha(code: string, codeId: string) {
57 | const key = `${RedisTemp.IMAGE_CAPTCHA}${codeId}`;
58 | const data: string | null = await this.redisService.get(key);
59 | if (!data) {
60 | throw new ApiException('验证码已过期', ApiCodeEnum.WARN);
61 | }
62 | return code.toLocaleLowerCase() === data.toLocaleLowerCase();
63 | }
64 |
65 | /**
66 | * 生成hash password
67 | * @param pwd
68 | * @param slat
69 | */
70 | private async genHashPassword(pwd: string, slat: string) {
71 | return await bcrypt.hash(pwd, slat);
72 | }
73 |
74 | /**
75 | * 生成盐
76 | */
77 | private async genSalt() {
78 | return await bcrypt.genSalt(10);
79 | }
80 |
81 | /**
82 | *
83 | * @param loginPwd 检查密码是否相同
84 | * @param sourcePwd
85 | */
86 | private async checkPassword(loginPwd: string, sourcePwd: string) {
87 | return await bcrypt.compare(loginPwd, sourcePwd);
88 | }
89 |
90 | /**
91 | * 根据账号,账号类型查询用户
92 | * @param account
93 | * @param accountType
94 | */
95 | private async findUser(account: string, accountType: number) {
96 | const user = await this.usersRepository.findOne({
97 | where: { account, isDel: DelEnum.N, accountType },
98 | });
99 | return user;
100 | }
101 |
102 | /**
103 | * 获取图形验证码
104 | */
105 | public async createImgCaptcha() {
106 | // 返回地址与id给前端 前端带过来校验
107 | const uuid = getUUID();
108 | const captcha = this.imageCaptchaService.createSvgCaptcha();
109 | const { data, text } = captcha || {};
110 | Logger.info(`图形验证码:--->${uuid}----${text}`);
111 | const time = 5 * 60;
112 | this.redisService.set(`${RedisTemp.IMAGE_CAPTCHA}${uuid}`, text, time);
113 | // 存redis
114 | return {
115 | data,
116 | codeId: uuid,
117 | };
118 | }
119 |
120 | private async findUserCode(code: string, account: string) {
121 | // 验证码表 没被使用的
122 | const result = await this.usersCodeResp.findOne({
123 | where: {
124 | account,
125 | code,
126 | isDel: DelEnum.N,
127 | status: StatusEnum.N,
128 | },
129 | });
130 | // 如果验证码查出有 需要改状态
131 | if (result) {
132 | result.status = StatusEnum.Y;
133 | this.usersCodeResp.save(result);
134 | }
135 | return result;
136 | }
137 |
138 | /**
139 | * 注册
140 | * @param RegisterDTO 注册参数
141 | */
142 | public async register(body: RegisterDTO) {
143 | const { account, accountType, password, code } = body;
144 |
145 | // 根据code 校验验证码
146 | const checkCode = await this.findUserCode(code, account);
147 | if (!checkCode) {
148 | throw new ApiException('验证码已失效', ApiCodeEnum.ERROR);
149 | }
150 | // 校验账户是否存在
151 | const check = await this.findUser(account, accountType);
152 | if (check) {
153 | throw new ApiException('当前账户已存在', ApiCodeEnum.ERROR);
154 | }
155 | const slat = await this.genSalt();
156 | const hashPwd = await this.genHashPassword(password, slat);
157 | await this.usersRepository.insert({
158 | account,
159 | accountType,
160 | password: hashPwd,
161 | passwordSalt: slat,
162 | });
163 | if (accountType === AccountEnum.EMAIL) {
164 | this.email.sendEmail({
165 | to: account,
166 | template: 'notice',
167 | title: CommonText.REGISTER_SUCCESS,
168 | content: '',
169 | context: {
170 | account,
171 | },
172 | } as IEmailParams);
173 | }
174 |
175 | return true;
176 | }
177 |
178 | /**
179 | * 登录
180 | * @param body LoginDTO
181 | */
182 | public async login(body: LoginDTO) {
183 | const { account, accountType, password, code, codeId } = body;
184 | // 校验图形验证码
185 | const checkCode = await this.chenckImageCaptcha(code, codeId);
186 | if (!checkCode) {
187 | throw new ApiException('图形验证码不正确', ApiCodeEnum.ERROR);
188 | }
189 | // 校验账户是否存在
190 | const user = await this.findUser(account, accountType);
191 | if (!user) {
192 | throw new ApiException('当前账户不存在', ApiCodeEnum.ERROR);
193 | }
194 | const { password: sourcePwd } = user;
195 | const checkPwd = await this.checkPassword(password, sourcePwd);
196 | if (!checkPwd) {
197 | throw new ApiException('请检查你的用户名与密码', ApiCodeEnum.ERROR);
198 | }
199 | // 登录成功 给token
200 | const { id } = user || {};
201 | const tokenPayload: IToken = {
202 | account,
203 | sub: id,
204 | };
205 | return await this.signToken(tokenPayload);
206 | }
207 |
208 | /**
209 | * 发送邮件
210 | * @param body
211 | */
212 | public async emailCode(body: EmailDTO) {
213 | const { email } = body;
214 | let code = '';
215 | // 查询有没有该账号并且还没被使用的code
216 | const checkCode = await this.usersCodeResp.findOne({
217 | where: {
218 | account: email,
219 | isDel: DelEnum.N,
220 | status: StatusEnum.N,
221 | },
222 | });
223 | if (checkCode) {
224 | code = checkCode.code;
225 | } else {
226 | const captcha = this.imageCaptchaService.createSvgCaptcha();
227 | const { text } = captcha || {};
228 | // 自动生成
229 | code = text;
230 | }
231 | Logger.info(`邮箱验证码:--->${email}----${code}`);
232 | // 生成code
233 | const params: IEmailParams = {
234 | to: email,
235 | title: CommonText.REGISTER_CODE,
236 | content: code,
237 | template: 'register',
238 | context: {
239 | code: code,
240 | },
241 | };
242 | await this.email.sendEmail(params);
243 | // 发送成功 插库
244 | if (!checkCode) {
245 | this.usersCodeResp.insert({
246 | code,
247 | account: email,
248 | status: StatusEnum.N,
249 | });
250 | }
251 | return true;
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/public/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
44 |
--------------------------------------------------------------------------------