一、创建项目工程
$ npm i -g @nestjs/cli
# nest new <project name>
$ nest new platform-server
二、项目工程基本配置
1. prettierrc配置
- .prettierrc
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 200,
"tabWidth": 4,
"endOfLine": "crlf",
"useTabs": true
}
- .vscode/settings.json
{
"editor.formatOnSave": true, // Ctrl + S 保存代码自动格式化
"editor.tabSize": 4,
"prettier.printWidth": 1000, // 行內匹配代码数
"prettier.bracketSameLine": true, // 超出后是否单独一行
"prettier.tabWidth": 4,
"prettier.useEditorConfig": false,
"prettier.semi": true, // 是否带分号
"prettier.singleQuote": true, // 是否为单引
"prettier.proseWrap": "never",
"prettier.endOfLine": "crlf",
"prettier.requireConfig": true,
"prettier.useTabs": true,
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"stylelint.validate": [
"css",
"less",
"scss",
"vue"
],
// 开启eslint检测
"eslint.enable": true,
"i18n-ally.localesPaths": [
"src/i18n",
"src/i18n/lang"
],
"vite.autoStart": false,
"vue.codeActions.enabled": false,
"git.ignoreLimitWarning": true
}
- .vscode/extensions.json
{
"recommendations": [
"esbenp.prettier-vscode",
],
}
2. eslint配置
- eslint.config.mjs
import globals from 'globals';
import pluginJs from '@eslint/js';
import tseslint from 'typescript-eslint';
/** @type {import('eslint').Linter.Config[]} */
export default [
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{js,mjs,cjs,ts}'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
},
ignores: ['node_modules/', 'build/', 'dist/'],
},
];
3. tsconfig.json配置
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false,
// ☆☆☆☆☆ 可以使用import xx from 'xx'的形式引入模块,兼容CommonJS 和 ES6 模块
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
]
}
},
"include": [
"src/**/*",
"./types/**/*.d.ts",
"test/**/*"
]
}
4. tsconfig.build.json配置
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"module": "commonjs",
},
"exclude": [
"node_modules",
"test",
"dist",
"build",
"**/*spec.ts"
]
}
5. nest-cli.json配置
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true,
"builder": "tsc"
}
}
6. package.json配置
- 安装的依赖包根据项目具体需求
{
"name": "XXX",
"version": "0.0.1",
"description": "XXX",
"author": "huangteng",
"private": true,
"scripts": {
"build": "nest build && node scripts/esbuild.config.mjs",
"format": "prettier --write "src/**/*.ts" "test/**/*.ts"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint "{src,apps,libs,test}/**/*.ts" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"prettier": "prettier --config .prettierrc -l -w "./src/**/*.{ts,js,json}"",
"prepare": "git init && concurrently "pnpm run postinstall"",
"postinstall": "simple-git-hooks",
"build:prod": "set "NODE_ENV=production" && rollup -c scripts/rollup.build.prod.mjs"
},
"dependencies": {
"@nestjs/axios": "^3.1.3",
"@nestjs/cache-manager": "^2.3.0",
"@nestjs/common": "^10.4.15",
"@nestjs/core": "^10.4.15",
"@nestjs/jwt": "^10.2.0",
"@nestjs/mapped-types": "^2.1.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/schedule": "^4.1.2",
"@nestjs/serve-static": "^5.0.2",
"@nestjs/swagger": "^8.1.1",
"@nestjs/throttler": "^6.4.0",
"@nestjs/typeorm": "^10.0.2",
"axios": "^1.7.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"compression": "^1.7.5",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"express": "^4.21.2",
"express-session": "^1.18.1",
"geoip-lite": "^1.4.10",
"helmet": "^8.0.0",
"ioredis": "^5.5.0",
"multer": "1.4.5-lts.1",
"mysql2": "^3.12.0",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"svg-captcha": "^1.4.0",
"typeorm": "^0.3.20",
"uuid": "^11.0.5"
},
"devDependencies": {
"@eslint/js": "^9.20.0",
"@nestjs/cli": "^10.4.9",
"@nestjs/schematics": "^10.2.3",
"@nestjs/testing": "^10.4.15",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.3.1",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-url": "^8.0.2",
"@types/compression": "^1.7.5",
"@types/crypto-js": "^4.2.2",
"@types/express": "^5.0.0",
"@types/express-session": "^1.18.1",
"@types/geoip-lite": "^1.4.4",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^20.17.17",
"@types/supertest": "^6.0.2",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.23.0",
"@typescript-eslint/parser": "^8.23.0",
"concurrently": "^9.1.2",
"esbuild": "^0.24.2",
"eslint": "^9.20.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^15.14.0",
"jest": "^29.7.0",
"lint-staged": "^15.4.3",
"prettier": "^3.4.2",
"rollup": "^4.34.6",
"rollup-plugin-copy": "^3.5.0",
"simple-git-hooks": "^2.11.1",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.23.0"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,json}": [
"eslint --fix"
]
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\.spec\.ts$",
"transform": {
"^.+\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
7. 创建scripts打包构建的脚步文件
- esbuild构建项目,保持项目工程的目录结构(不构建为单文件),只打包源码。
- scripts/esbuild.config.mjs
import { build } from 'esbuild';
import fs from 'fs';
import path from 'path';
// 定义输出目录
const outDir = 'build';
const outputDir = path.resolve(outDir);
// 确保输出目录存在
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
} else {
// 目录存在,先清空目录内容
const files = fs.readdirSync(outputDir);
files.forEach((file) => {
const filePath = path.join(outputDir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 如果是目录,递归删除
fs.rmSync(filePath, { recursive: true });
} else {
// 如果是文件,删除文件
fs.unlinkSync(filePath);
}
});
}
// 复制文件的实用函数
const copyFile = (src, dest) => {
if (!fs.existsSync(src)) return;
const stat = fs.statSync(src);
if (stat.isDirectory()) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
fs.readdirSync(src).forEach((file) => {
copyFile(path.join(src, file), path.join(dest, file));
});
} else {
fs.copyFileSync(src, dest);
}
};
const copyStaticFiles = () => {
// 复制目标文件
const targets = [
// { src: 'public', dest: path.join(outputDir, 'public') },
{ src: 'localstorage', dest: path.join(outputDir, 'localstorage') },
{ src: 'docker-compose.yml', dest: path.join(outputDir, 'docker-compose.yml') },
{ src: 'Dockerfile', dest: path.join(outputDir, 'Dockerfile') },
];
// 执行复制
targets.forEach(({ src, dest }) => {
copyFile(path.resolve(src), dest);
});
// 处理 package.json
const packageJsonPath = path.resolve('package.json');
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageJson = { ...pkg };
delete packageJson.scripts;
delete packageJson['simple-git-hooks'];
delete packageJson['lint-staged'];
delete packageJson.jest;
delete packageJson['devDependencies'];
const packageJsonContent = JSON.stringify(packageJson, null, 4);
fs.writeFileSync(path.join(outputDir, 'package.json'), packageJsonContent, 'utf-8');
}
console.log('文件复制完成!');
};
build({
entryPoints: ['./dist/**/*.js'],
outdir: `./${outDir}/dist`,
bundle: false, // false不打包为单文件
minify: true, // 压缩代码
minifyIdentifiers: false, // 禁用标识压缩
minifySyntax: false, //禁用语法压缩
sourcemap: false,
keepNames: true, // 保留所有标识符的原始名称,处理关联查询报错问题
platform: 'node',
format: 'cjs',
target: 'es2018',
define: {
// 'process.env.NODE_ENV': '"production"',
},
})
.then(() => {
copyStaticFiles();
console.log('Build complete');
})
.catch(() => {
console.log('Build error');
});
- rollup打包构建,打包为单文件
- scripts/rollup.build.prod.mjs
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import terser from '@rollup/plugin-terser';
import url from '@rollup/plugin-url';
import copy from 'rollup-plugin-copy';
import replace from '@rollup/plugin-replace';
import path from 'path';
import fs from 'fs';
// 定义输出目录
const outDir = 'build-prod';
const outputDir = path.resolve(outDir);
// 确保输出目录存在
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
} else {
// 目录存在,先清空目录内容
const files = fs.readdirSync(outputDir);
files.forEach((file) => {
const filePath = path.join(outputDir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
// 如果是目录,递归删除
fs.rmSync(filePath, { recursive: true });
} else {
// 如果是文件,删除文件
fs.unlinkSync(filePath);
}
});
}
const files_config = [
{
fileName: 'Dockerfile',
content: [
'FROM node:20.18.0',
'WORKDIR /app',
'',
'# 设置时区为 Asia/Shanghai',
'ENV TZ=Asia/Shanghai',
'',
'COPY package*.json ./',
'RUN npm config set registry https://registry.npmmirror.com',
'',
'RUN npm install -g pnpm',
'',
'RUN npm install -g pm2',
'',
'RUN pnpm install --prod',
'',
'COPY . .',
'',
'CMD ["pm2-runtime", "start", "dist/main.js"]',
].join('\n'),
},
{
fileName: 'docker-compose.yml',
content: [
'version: "3"',
'',
'services:',
' farmpack-service:',
' image: farmpack-service',
' build:',
' context: .',
' dockerfile: Dockerfile',
' ports:',
' - "3100:3100"',
' volumes:',
' - ./localstorage:/app/localstorage',
' - ./public:/app/public',
' - ./logs:/app/logs',
' environment:',
' - PORT=3100',
' - NODE_ENV=production',
].join('\n'),
},
];
files_config.forEach((target) => {
fs.writeFileSync(path.join(outputDir, target.fileName), target.content, 'utf-8');
console.log(`✅ ${target.fileName} 文件生成完成!`);
});
// 处理 package.json
const packageJsonPath = path.resolve('package.json');
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageJson = { ...pkg };
delete packageJson.scripts;
delete packageJson['simple-git-hooks'];
delete packageJson['lint-staged'];
delete packageJson.jest;
delete packageJson['devDependencies'];
const packageJsonContent = JSON.stringify(packageJson, null, 4);
fs.writeFileSync(path.join(outputDir, 'package.json'), packageJsonContent, 'utf-8');
console.log('✅ package.json 文件复制完成!');
}
export default {
input: './dist/main.js',
output: {
format: 'cjs',
dir: outputDir + '/dist',
entryFileNames: 'main.js',
},
plugins: [
nodeResolve({
preferBuiltins: true, // 确保 Node.js 内置模块被处理
}),
commonjs(),
json(),
replace({
preventAssignment: true,
// 'process.env.NODE_ENV': JSON.stringify('production'),
}),
terser({
compress: {
warnings: false,
drop_console: false,
drop_debugger: true,
},
format: {
comments: false,
beautify: false,
},
mangle: false, // 不混淆变量名
}),
url({
limit: 0,
include: ['**/*.ttf'],
fileName: '[name][extname]',
destDir: path.resolve(outDir, 'public'),
publicPath: './',
emitFiles: true,
}),
copy({
targets: [
{
src: 'public/*',
dest: path.resolve(outDir, 'public'),
},
{
src: 'templates/*',
dest: path.resolve(outDir, 'templates'),
},
{
src: 'localstorage/*',
dest: path.resolve(outDir, 'localstorage'),
},
],
}),
],
external: [
'@nestjs/common',
'@nestjs/core',
'@nestjs/jwt',
'@nestjs/mapped-types',
'@nestjs/passport',
'@nestjs/platform-express',
'@nestjs/typeorm',
'@nestjs/serve-static',
'@nestjs/swagger',
'class-transformer',
'class-validator',
'crypto-js',
'dayjs',
'express-session',
'mysql2',
'reflect-metadata',
'rxjs',
'svg-captcha',
'typeorm',
'geoip-lite',
],
onwarn: (warning, warn) => {
if (warning.code === 'CIRCULAR_DEPENDENCY') {
// 忽略关于 '(!) Circular dependency' 的警告
return;
}
warn(warning);
},
};
8. 创建全局ts类型声明的扩展(根据tsconfig.json的配置创建对应的目录)
- types/global.d.ts
declare global {
interface QueryPageInterface {
pageIndex: number;
pageSize: number;
}
interface ResponsePageInterface<T = any> {
records: T[];
total: number;
pageIndex?: number;
pageSize?: number;
}
interface ResponseIngerface<T = any> {
code: number;
message?: string;
data?: T;
}
}
export {};
- types/express.d.ts
import type { Request as Req } from 'express';
declare global {
namespace Express {
namespace Multer {
interface File {
_path?: string;
}
}
}
}
declare module 'express' {
interface Request extends Req {
userInfo?: any;
isAdmin?: boolean;
isSuperAdmin?: boolean;
}
}
declare module '@nestjs/common' {
interface Request extends Req {
userInfo?: any;
isAdmin?: boolean;
isSuperAdmin?: boolean;
}
}
三、项目工程基本目录结构
1. 整体工程目录结构
│ .gitignore
│ .prettierrc
│ docker-compose.yml
│ Dockerfile
│ ecosystem.config.js
│ eslint.config.mjs
│ nest-cli.json
│ package.json
│ pnpm-lock.yaml
│ README.md
│ tsconfig.build.json
│ tsconfig.json
│
├─.vscode
│ extensions.json
│ settings.json
│
├─scripts
│ esbuild.config.mjs
│ rollup.build.prod.mjs
│
├─src
│ │ app.module.ts
│ │ main.ts
│ │ swagger.ts
│ │ ...
├─test
│ app.e2e-spec.ts
│ jest-e2e.json
│
└─types
express.d.ts
global.d.ts
2. 源码src下目录结构
│ app.module.ts
│ main.ts
│ swagger.ts
│
├─common
│ ├─decorators
│ │ public.decorator.ts
│ │ tenant.decorator.ts
│ │
│ ├─exceptions
│ ├─filters
│ │ catch.filter.ts
│ │
│ ├─interceptors
│ │ http-status.interceptor.ts
│ │ tenant.intercepter.ts
│ │
│ ├─middleware
│ │ permission.middleware.ts
│ │ request-logger.middleware.ts
│ │
│ ├─pipes
│ │ file.validation.pipe.ts
│ │ remove-unknown-filds.pipe.ts
│ │
│ └─utils
│ crypto-js.ts
│ jwt-auth.guard.ts
│ methods.ts
│ multer-file.ts
│ redis.ts
│ response.ts
│ tools.ts
│
├─config
│ enums.ts
│ env.ts
│
└─modules
├─common
│ │ common.controller.ts
│ │ common.module.ts
│ │ common.service.ts
│ │ ...
│ ...
└─system
│ system.module.ts
│
├─dept
│ │ dept.controller.ts
│ │ dept.module.ts
│ │ dept.service.ts
│ │
│ ├─dto
│ │ create-dept.dto.ts
│ │ update-dept.dto.ts
│ │
│ └─entities
│ dept.entity.ts
│
├─file
│ │ file.controller.ts
│ │ file.module.ts
│ │ file.service.ts
│ │
│ ├─dto
│ │ create-file.dto.ts
│ │ update-file.dto.ts
│ │
│ └─entities
│ file.entity.ts
│
├─menu
│ │ menu.controller.ts
│ │ menu.module.ts
│ │ menu.service.ts
│ │
│ ├─dto
│ │ create-menu.dto.ts
│ │ update-menu.dto.ts
│ │
│ └─entities
│ menu.entity.ts
│
├─region
│ │ region.controller.ts
│ │ region.module.ts
│ │ region.service.ts
│ │
│ ├─dto
│ │ create-region.dto.ts
│ │ update-region.dto.ts
│ │
│ └─entities
│ region.entity.ts
│
├─role
│ │ role.controller.ts
│ │ role.module.ts
│ │ role.service.ts
│ │
│ ├─dto
│ │ create-role.dto.ts
│ │ update-role.dto.ts
│ │
│ └─entities
│ role.entity.ts
│
├─tenant
│ │ tenant.controller.ts
│ │ tenant.module.ts
│ │ tenant.service.ts
│ │
│ ├─dto
│ │ create-tenant.dto.ts
│ │ update-tenant.dto.ts
│ │
│ └─entities
│ tenant.entity.ts
│
├─tlog
│ │ tlog.controller.ts
│ │ tlog.module.ts
│ │ tlog.service.ts
│ │
│ ├─dto
│ │ create-tlog.dto.ts
│ │ update-tlog.dto.ts
│ │
│ └─entities
│ tlog.entity.ts
│
└─user
│ user.controller.ts
│ user.module.ts
│ user.service.ts
│
├─dto
│ user.dto.ts
│
└─entities
user.entity.ts
四、项目基础功能及源码
1. 主入口main.ts文件
import { NestFactory } from '@nestjs/core';
import compression from 'compression';
import { AppModule } from './app.module';
import session from 'express-session';
import helmet from 'helmet';
import express from 'express';
import { CatchExceptionsFilter } from './common/filters/catch.filter';
import { session_secret } from './config/env';
import { SwaggerApp } from './swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
// 'log' | 'error' | 'warn' | 'debug' | 'verbose' | 'fatal'
logger: ['error', 'warn', 'debug', 'verbose'],
});
// 设置请求体的大小限制为 50MB
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
app.use(
helmet({
frameguard: {
action: 'sameorigin',
},
hidePoweredBy: true, // 隐藏 X-Powered-By 标头,避免泄露技术栈信息。
noSniff: true, // 防止浏览器猜测响应的 MIME 类型。
xssFilter: true, // 防止跨站脚本攻击(XSS)。
crossOriginEmbedderPolicy: true, // 防止跨源嵌入(COEP)。
crossOriginOpenerPolicy: true, // 防止跨源打开(COOP)。
crossOriginResourcePolicy: true, // 防止跨源资源共享(CORS)。
contentSecurityPolicy: false, // 禁用 CSP,才可以通过ip访问@nestjs/swagger
}),
);
app.use(
compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
// 不要压缩
return false;
}
// gzip压缩指定文件类型
const contentType = (res.getHeader('Content-Type') || '') as string;
const compressibleTypes = [
'application/json',
'application/javascript',
'text/css',
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'font/woff',
'font/woff2',
'application/font-woff',
'application/font-woff2',
'font/ttf',
'application/font-ttf',
'font/otf',
'image/svg+xml',
'application/vnd.ms-fontobject',
];
return compressibleTypes.some((type) => contentType.includes(type));
},
}),
);
app.use(
session({
secret: session_secret,
resave: true,
saveUninitialized: false,
rolling: true,
cookie: {
maxAge: 1000 * 60 * 5, // 5分钟时间
httpOnly: true,
},
}),
);
app.useGlobalFilters(new CatchExceptionsFilter());
SwaggerApp(app);
await app.listen(process.env.PORT ?? 3000);
console.log(`Application is running on: ${await app.getUrl()}`);
console.log(`NODE_ENV: ${process.env.NODE_ENV || 'development'}`);
}
bootstrap();
2. app.module.ts 文件
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { JwtModule } from '@nestjs/jwt';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { jwt_secret, jwt_expires_in, DATABASE_CONFIG, PG_DB_CONFIG, PUBLIC_DIR, STORAGE_FILE_DIR } from './config/env';
import { DB_CONNECTION_NAME_ENUM } from './config/enums';
import { JwtAuthGuard } from './common/utils/jwt-auth.guard';
import { HttpStatusInterceptor } from './common/interceptors/http-status.interceptor';
import { RequestLoggerMiddleware } from './common/middleware/request-logger.middleware';
import { OauthModule } from './modules/oauth/oauth.module';
import { SystemModule } from './modules/system/system.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
...DATABASE_CONFIG['xxx'],
legacySpatialSupport: false, // true不支持空间类型
// // synchronize: true,
autoLoadEntities: true,
}),
TypeOrmModule.forRoot({
name: DB_CONNECTION_NAME_ENUM.POSTGRES_IOT,
type: 'postgres',
host: PG_DB_CONFIG.host,
port: PG_DB_CONFIG.port,
database: PG_DB_CONFIG.database,
username: PG_DB_CONFIG.username,
password: PG_DB_CONFIG.password,
autoLoadEntities: true,
}),
JwtModule.register({
global: true,
secret: jwt_secret,
signOptions: { expiresIn: jwt_expires_in },
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', PUBLIC_DIR),
serveRoot: '/static静态资源',
serveStaticOptions: {
cacheControl: true,
},
}),
ThrottlerModule.forRoot([
{
ttl: 60 * 1000,
limit: 100,
},
]),
OauthModule,
SystemModule,
],
controllers: [],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: HttpStatusInterceptor,
},
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestLoggerMiddleware)
.exclude(
{
path: '/oauth/captcha',
method: RequestMethod.ALL,
},
'/system/tlog/(.*)',
'/iot/(.*)',
'/static/(.*)',
)
.forRoutes('*');
}
}
3. 常用工具与方法
- crypto-js和crypto
import CryptoJS from 'crypto-js';
import crypto from 'crypto';
const key_str = 'xxx';
// 加密
export const Encrypt = function (val: string) {
const key = CryptoJS.enc.Utf8.parse(key_str).toString();
const srcs = CryptoJS.enc.Utf8.parse(val);
const encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
return encrypted.toString();
};
// 解密
export const Decrypt = function (val: string) {
const key = CryptoJS.enc.Utf8.parse(key_str).toString();
const decrypted = CryptoJS.AES.decrypt(val, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
return decrypted.toString(CryptoJS.enc.Utf8);
};
/**
* bufer数据加密
* @returns
*/
export const cryptoBuffer = () => {
const encryptionKey = 'xxx';//32位
const iv = crypto.randomBytes(16);
const Encrypt = function (fileBuffer: Buffer) {
const cipher = crypto.createCipheriv('aes-256-cbc', encryptionKey, iv);
const encryptedBuffer = Buffer.concat([cipher.update(fileBuffer), cipher.final()]);
return encryptedBuffer;
};
return {
iv,
Encrypt,
};
};
- jwt认证拦截
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { jwt_secret } from '@/config/env';
import { IS_PUBLIC_KEY } from '@/common/decorators/public.decorator';
import { Reflector } from '@nestjs/core';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [context.getHandler(), context.getClass()]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: jwt_secret,
});
request['userInfo'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
- 常用基础方法
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);
/**
* 数据库中的时间转换为本地时间
* @param date
* @param format
* @returns
*/
export const utcTransformLocal = (date: dayjs.ConfigType, format = 'YYYY-MM-DD HH:mm:ss') => {
if (!date) return null;
return dayjs.utc(date).local().format(format);
};
/**
* 格式化时间
* @param {*} date
* @param {*} format
* @returns
*/
export const formatDate = (date: Date, format = 'YYYY-MM-DD HH:mm:ss'): string => {
let time = date;
if (!time) time = new Date();
return dayjs(time).format(format);
};
/**
* 格式时间 YYYYMMDDHHmmss
* @param date
* @returns
*/
export const compactDatetimeFormat = (date: Date): string => {
let time = date;
if (!time) time = new Date();
return formatDate(time, 'YYYYMMDDHHmmss');
};
/**
* 解析时间 YYYYMMDDHHmmss -> YYYY-MM-DD HH:mm:ss
* @param dateString
* @param format
* @returns
* */
export const parseCompactDatetimeFormat = (dateString: string, format = 'YYYY-MM-DD HH:mm:ss'): string => {
const parsedDate = dayjs(dateString, 'YYYYMMDDHHmmss');
return parsedDate.format(format);
};
/**
* 常规列表数据转数结构
* @param list
* @param rowkey
* @param relationKey
* @returns
*/
export const arrayTransformTree = (list: any[], rowkey = 'id', relationKey = 'parentId', rootValue = 0) => {
const nodeMap = {};
const roots = [];
list.forEach((item) => {
const id = item[rowkey];
const parentId = item[relationKey];
nodeMap[id] = nodeMap[id] || { children: [] };
Object.assign(nodeMap[id], {
...item,
[rowkey]: id,
[relationKey]: parentId,
});
const node = nodeMap[id];
if (parentId === null || parentId === rootValue) {
roots.push(node);
} else {
nodeMap[parentId] = nodeMap[parentId] || { children: [] };
nodeMap[parentId].children.push(node);
}
});
return roots;
};
/**
* 权限菜单树结构,增加权限按钮
* @param list
* @param btns
* @param rowkey
* @param relationKey
* @returns
*/
export const arrayTransformPermissionMenuTree = (list: any[], btns = {}, rowkey = 'id', relationKey = 'parentId') => {
const nodeMap = {};
const roots = [];
list.forEach((item) => {
const id = item[rowkey];
const parentId = item[relationKey];
nodeMap[id] = nodeMap[id] || { children: [] };
Object.assign(nodeMap[id], {
...item,
[rowkey]: id,
[relationKey]: parentId,
});
const node = nodeMap[id];
if (btns[id]) {
node.permissionBtns = btns[id];
}
if (parentId === null || parentId === 0) {
roots.push(node);
} else {
nodeMap[parentId] = nodeMap[parentId] || { children: [] };
nodeMap[parentId].children.push(node);
}
});
const loopLevel = (list, level = 1) => {
list.forEach((item) => {
item.level = level;
if (item.children && item.children.length > 0) {
loopLevel(item.children, level + 1);
}
});
return list;
};
return loopLevel(roots);
};
/**
* 将wkt格式的点坐标解析为经纬度
* @param wkt
* @returns
*/
export const parseWktPoint = (wkt: string): [number, number] => {
if (!wkt) return;
const regex = /^POINT((-?\d+.\d+) (-?\d+.\d+))$/;
const match = wkt.match(regex);
if (match) {
const longitude = parseFloat(match[1]);
const latitude = parseFloat(match[2]);
return [longitude, latitude];
} else {
throw new Error('Invalid WKT format');
}
};
/**
* 将wkt格式的多边形坐标解析为经纬度数组
* @param wkt
* @returns
*/
export const parseWktPolygon = (wkt: string): [number, number][] => {
const regex = /^POLYGON(((.*)))$/;
const match = wkt.match(regex);
if (match) {
const rings = match[1].split(',').map((lnglat) => lnglat.split(' ').map((coord) => parseFloat(coord)) as [number, number]);
const start = rings[0];
const end = rings[rings.length - 1];
// 判断是否闭合, 若闭合则删除最后一个坐标【只是返回前端时做处理】
if (start[0] === end[0] && start[1] === end[1]) {
rings.pop();
}
const paths: [number, number][] = rings;
return paths;
} else {
throw new Error('Invalid WKT format for POLYGON');
}
};
- 文件处理
import fs from 'fs';
import path from 'path';
import { v4 as UUID } from 'uuid';
import { diskStorage } from 'multer';
import { STORAGE_FILE_DIR } from '@/config/env';
import { formatDate } from '@/common/utils/methods';
/**
* 获取文件路径
* @param type
* @param filename
* @returns
*/
export const getResourceFilePath = (type: string, filename: string, ext: string) => {
const filePath = path.join(STORAGE_FILE_DIR, type, filename + ext);
if (!fs.existsSync(filePath)) {
return '';
}
return filePath;
};
export const getFileFullPath = (filePath: string) => {
const fullPath = path.resolve(process.cwd(), STORAGE_FILE_DIR, filePath);
return fullPath;
};
/**
* 自动创建文件目录
* @param dir
* @returns
*/
export const mkdirSync = (dir: string) => {
const dirname = path.resolve(process.cwd(), dir);
if (fs.existsSync(dirname)) {
return true;
} else {
if (mkdirSync(path.dirname(dirname))) {
fs.mkdirSync(dirname, { recursive: true });
return true;
}
}
return false;
};
/**
* 生成uuid 文件名
* @param ext
* @returns
*/
export const formatFileNameToUUID = (ext = '') => {
const uuid = UUID().replace(/-/g, '');
const filename = formatDate(new Date(), 'YYYY-MM-DD~HH-mm') + '__' + uuid + ext;
return filename;
};
/**
* 根据文件类型,存储文件到类型目录下
* @returns
*/
export const diskStorageFileType = () => {
const storate = diskStorage({
destination: (req, file, callback) => {
const mimeType = file.mimetype;
const time = formatDate(new Date(), 'YYYY-MM-DD');
let root_dir = `${STORAGE_FILE_DIR}/uploads`;
if (mimeType.startsWith('image/')) {
root_dir += '/images';
} else if (mimeType === 'application/pdf') {
root_dir += '/pdfs';
} else if (mimeType === 'application/msword') {
root_dir += '/word';
} else if (mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
root_dir += '/word';
} else if (mimeType === 'application/vnd.ms-excel') {
root_dir += '/excel';
} else if (mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
root_dir += '/excel';
} else if (mimeType === 'application/vnd.ms-powerpoint') {
root_dir += '/ppt';
}
root_dir += `/${time}`;
mkdirSync(root_dir);
callback(null, root_dir);
},
filename: (req, file, callback) => {
const originalname = Buffer.from(file.originalname, 'latin1').toString('utf-8');
const ext = path.extname(originalname);
const fileName = formatFileNameToUUID(ext);
callback(null, fileName);
},
});
return storate;
};
/**
* 存储文件到指定目录
* @param dir
* @returns
*/
export const diskStorageFile = (dir: string) => {
const storate = diskStorage({
destination: (req, file, callback) => {
const params = req.params;
const fileType = params.fileType || 'temp';
const root_dir = `${STORAGE_FILE_DIR}/${dir}/${fileType}`;
mkdirSync(root_dir);
callback(null, root_dir);
},
filename: (req, file, callback) => {
const originalname = Buffer.from(file.originalname, 'latin1').toString('utf-8');
const ext = path.extname(originalname);
const fileName = formatFileNameToUUID(ext);
callback(null, fileName);
},
});
return storate;
};
/**
* 移动文件到指定目录
* @param filePath
* @param dir
* @returns
*/
export const cutFileToDisk = (filePath: string, dir: string, fileName?: string) => {
const ext = path.extname(filePath);
if (!fileName) {
fileName = formatFileNameToUUID(ext);
}
const root_dir = `${STORAGE_FILE_DIR}/${dir}`;
mkdirSync(root_dir);
const fullPath = path.resolve(process.cwd(), filePath);
if (fs.existsSync(fullPath)) {
fs.copyFileSync(fullPath, path.resolve(root_dir, fileName));
fs.unlinkSync(fullPath);
}
return `${dir}/${fileName}`;
};
/**
* 删除文件
* @param filePath
*/
export const unlinkFile = (filePath: string) => {
const fullPath = path.resolve(process.cwd(), STORAGE_FILE_DIR, filePath);
if (fs.existsSync(fullPath)) {
fs.unlinkSync(fullPath);
}
};
五、打包构建及部署
使用docker部署到生产环境
# 打包单文件部署
$ pnpm run build:prod
执行rollup.build.prod.mjs脚步文件,自动打包复制静态资源文件和生成Dockerfile和docker-compsoe.yml文件
# 构建
$ docker-compose build
# 启动
$ docker-compose up -d