本文完整源码
一、项目框架搭建
Nest中文网
关于Nest对node版本要求,以及Nest CLI的安装,这里不再陈述。
1. 创建项目
nest new nest-example
2. 创建和引入模块Module
若不知道如何创建module、controller、service、middleware等,可直接运行命令 nest g --help 查看
2.1 创建
# admin:管理系统模块
nest g module module/admin
2.2 引入
创建完成后,程序会在 src/app.module.ts中自动引入。
import { Module } from '@nestjs/common';
import { AdminModule } from './module/admin/admin.module';
@Module({
imports: [AdminModule],
})
export class AppModule {}
3. 创建和引入控制器Controller
值得注意的是:创建服务Service前,将
src/app.module.ts中controllers: [AppController]配置删除,暂时用不到。
3.1 创建
# 用户 users 控制器
nest g controller module/admin/controller/admin
3.2 引入
创建完成后,程序会在对应的module文件(src/module/admin/admin.module.ts、src/module/default/default.module.ts)中自动引入。
4. 创建服务Service
4.1 创建公共工具 Service
# 命令
nest g service service/tools
// 先安装插件:npm i @types/svg-captcha @types/md5
import {Injectable} from '@nestjs/common';
import {Response} from 'express';
import * as svgCaptcha from "svg-captcha";
import * as md5 from "md5";
interface ArgType {
type?: string,
redirectUrl?: string,
message?: string,
}
@Injectable()
export class ToolsService {
// 获取图形验证码
getCaptcha() {
return svgCaptcha.create({
size: 1,
fontSize: 50,
width: 100,
height: 34,
background: '#cc9966',
ignoreChars: '0o1i',
noise: 2
})
}
// Md5加密
getMd5(str: string) {
return md5(str)
}
// 公共提示页面
async tips(res: Response, arg: ArgType) {
const {type = '', redirectUrl = '', message = ''} = arg
await res.render(`admin/public/${type}`, {redirectUrl, message})
}
}
4.2 创建其他 Service
# 用户 users 服务
nest g service service/users
值得注意的是:创建服务Service后,程序会在
src/app.module.ts中自动引入providers: [AppService, UsersService],,这里我们直接删除,后期会在单独的模块入口(例如:module/**/**.module.ts)文件中引入。
5. 静态资源配置
根目录下创建public目录,并在 src/main.ts 中配置,其中 NestFactory.create 不要忘记指定基于 Express 平台
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import {NestExpressApplication} from "@nestjs/platform-express";
import * as path from "path";
async function bootstrap() {
// 创建 NestJS 应用实例,并指定基于 Express 平台
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 静态资源目录配置
app.useStaticAssets(path.join(__dirname, '..', 'public'), {
prefix: '/static/' // 为静态资源文件添加一个虚拟路径前缀,可选
});
await app.listen(3000);
}
bootstrap();
在public中添加一图片(logo.png),此时运行项目 npm run start:dev,即可访问静态资源:http://localhost:3000/static/logo.png
6. 模板引擎(EJS)安装配置以及ejs文档创建
EJS -- 嵌入式 JavaScript 模板引擎 | EJS 中文文档
6.1 安装
npm install ejs
6.2 配置
根目录下创建views目录,并在 src/main.ts 中配置
// 其他代码...
async function bootstrap() {
// 其他代码...
// 配置模板引擎
app.setBaseViewsDir('views');
app.setViewEngine('ejs');
// 其他代码...
}
6.3 ejs文档创建、基本布局、路由关联
- 在
views文件夹下创建以下文件夹:
views
├─ dashboard.ejs
├─ login.ejs
├─ userinfo.ejs
├─ users.ejs
│
└─public
├─ error.ejs
├─ header.ejs
└─ success.ejs
- 布局代码
此处不贴代码,参考源码即可
7. mysql + typeorm 安装配置以及实体类创建
7.1 数据库准备
/*
Navicat Premium Data Transfer
Source Server : my-ali
Source Server Type : MySQL
Source Server Version : 50650
Source Host : 8.137.14.201:3306
Source Schema : nest_example
Target Server Type : MySQL
Target Server Version : 50650
File Encoding : 65001
Date: 07/03/2025 16:26:22
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`status` tinyint(4) NULL DEFAULT 1 COMMENT '用户状态,1: 正常, 0: 禁用',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE,
UNIQUE INDEX `email`(`email`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'superadmin', 'e10adc3949ba59abbe56e057f20f883e', 'VincentLee', 'admin@example.com', 1, '2025-02-28 15:43:34');
INSERT INTO `users` VALUES (2, 'zhangsan', 'e10adc3949ba59abbe56e057f20f883e', '张三', 'zhangsan@example.com', 1, '2025-03-07 16:24:05');
INSERT INTO `users` VALUES (3, 'lisi', 'e10adc3949ba59abbe56e057f20f883e', '李四', 'lisi@example.com', 1, '2025-03-07 16:24:48');
INSERT INTO `users` VALUES (4, 'wangwu', 'e10adc3949ba59abbe56e057f20f883e', '王五', 'wangwu@example.com', 1, '2025-03-07 16:25:51');
SET FOREIGN_KEY_CHECKS = 1;
7.2 插件安装
npm install mysql typeorm @nestjs/typeorm
7.3 配置数据库链接
在 src/app.module.ts 文件下配置
// 其他代码 ...
import {TypeOrmModule} from "@nestjs/typeorm";
@Module({
imports: [
// 其他代码 ...
// 配置数据库链接
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'nest_example',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
})
],
// 其他代码 ...
})
export class AppModule {
}
7.4 实体类和接口创建
7.4.1 实体类创建
在 src 新建 一个目录 entity,并新建 users 实体类
// src/entity/users.entity.ts
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn} from 'typeorm';
@Entity('users') // 指定表名为 'users'
export class User {
@PrimaryGeneratedColumn({type: 'int', unsigned: true}) // 自增主键
id: number;
@Column({type: 'varchar', length: 50}) // 用户名
username: string;
@Column({type: 'varchar', length: 255}) // 密码
password: string;
@Column({type: 'varchar', length: 50, nullable: true}) // 昵称(允许为空)
nickname: string;
@Column({type: 'varchar', length: 100, unique: true}) // 邮箱(唯一)
email: string;
@Column({type: 'tinyint', width: 4, default: 1}) // 状态(默认值为 1)
status: number;
@CreateDateColumn({type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'}) // 创建时间
create_time: Date;
}
7.4.2 创建接口 interface 并添加配置
- users
# 执行命令
nest g interface interface/users
export interface UsersInterface {
id?: number;
username?: string;
password?: string;
nickname?: string;
email?: string;
status?: number;
page?: number;
pageSize?: number;
}
8. 配置Cookie、Session
8.1 安装插件
npm i cookie-parser express-session @types/cookie-parser @types/express-session
8.2 配置
在 src/main.ts 中配置
// 其他代码...
import * as cookieParser from "cookie-parser";
import * as session from "express-session";
async function bootstrap() {
// 其他代码...
// 配置Cookie、Session
app.use(cookieParser('CF8136B13D46495CA6AFF3297FD4D8FC'))
app.use(session({
secret: 'B125C1B83DEAB9B667E571489421CB71',
resave: false,
saveUninitialized: false,
cookie: {maxAge: 3600000, httpOnly: true},
rolling: false
}))
// 其他代码...
}
// 其他代码...
9. 扩展方法
在目录 src 下新建文件 extend/helper.ts,后期将公共放入此处,方便调用。
// 先安装插件:npm i dayjs
import * as dayjs from "dayjs";
export class Helper {
static formatDate(date: string | number | Date | dayjs.Dayjs, template: string) {
return dayjs(date).format(template)
}
}
二、登录鉴权
1. 配置中间件(限制页面访问 + 初始化中间件)
1.1 创建中间件并编码
# 命令
nest g middleware middleware/adminauth
nest g middleware middleware/init
- adminauth 中间件
该中间件通过req.session.userinfo获取用户信息判断用户是否已登录,如果未登录,则重定向到登录页面(除非请求的路由在excludes数组中),通过excludes数组,可以配置不需要权限验证的路由(例如登录页面)。
import {Injectable, NestMiddleware} from '@nestjs/common';
@Injectable()
export class AdminauthMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const excludes: string[] = ['/admin/auth/login', '/admin/auth/doLogin', '/admin/auth/code'];
const userinfo = req.session.userinfo;
if (userinfo && userinfo.username) {
res.locals.userinfo = userinfo;
next();
} else {
if (excludes.includes(req.baseUrl)) {
next();
} else {
res.redirect(301, '/admin/auth/login')
}
}
}
}
- init 中间件
该中间件主要是将自定义的工具类Helper挂载到res.locals上,以便在后续的请求处理中使用;其他工具类也可在此处添加。
import {Injectable, NestMiddleware} from '@nestjs/common';
import {Helper} from "../../extend/helper";
@Injectable()
export class InitMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
res.locals.helper = Helper
next();
}
}
1.2 配置中间件
// 其他代码...
import {Module, NestModule, MiddlewareConsumer} from '@nestjs/common';
import {AdminauthMiddleware} from "./middleware/adminauth/adminauth.middleware";
import {InitMiddleware} from "./middleware/init/init.middleware";
// 其他代码...
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminauthMiddleware)
.forRoutes('/admin/*')
.apply(InitMiddleware)
.forRoutes('*');
}
}
2. users 服务 Service
2.1 源码
import {Injectable} from '@nestjs/common';
import {InjectRepository} from "@nestjs/typeorm"; // 用于注入 TypeORM 的 Repository
import {Repository} from "typeorm"; // TypeORM 的 Repository 类,用于操作数据库
import {Users} from "../../entity/users.entity";
import {UsersInterface} from "../../interface/users/users.interface"; // 用户实体类,对应数据库中的用户表
@Injectable()
export class UsersService {
constructor(
// 注入 Users 实体的 Repository
@InjectRepository(Users)
// 声明一个私有的只读属性,用于操作 Users 实体
private readonly usersRepository: Repository<Users>
) {
}
async findAll(json: UsersInterface = {}) {
const {page = 1, pageSize = 10} = json;
return await this.usersRepository.findAndCount({
skip: (page - 1) * pageSize,
take: pageSize,
order: {id: 'DESC'}
})
}
async findDetail(id) {
return await this.usersRepository.findBy({id})
}
async findOne(where) {
return await this.usersRepository.findOne({ where });
}
async save(createUserDto: Partial<Users>) {
const user = this.usersRepository.create(createUserDto)
return await this.usersRepository.save(user);
}
async update(id: number, updateUserDto: Partial<Users>) {
return await this.usersRepository.update(id, updateUserDto);
}
async del(id) {
return await this.usersRepository.delete(id)
}
}
2.2 代码简单解释
2.2.1 导入依赖
简单解释一下以下代码:
其他方法可参考官网文档:typeorm.bootcss.com/
Injectable:来自@nestjs/common,用于将UsersService类标记为可注入的服务。InjectRepository:来自@nestjs/typeorm,用于将 TypeORM 的Repository注入到服务中。Repository:来自typeorm,是 TypeORM 的核心类,用于操作数据库。Users:用户实体类,对应数据库中的用户表。UsersInterface:用户相关的接口定义,可能用于类型检查或数据传输。
2.2.2 UsersService 类
-
@Injectable():将UsersService标记为一个可注入的服务,NestJS 的依赖注入系统会管理它的生命周期。 -
constructor:通过@InjectRepository(Users)注入Users实体的Repository,并将其赋值给usersRepository属性。usersRepository是Repository<Users>类型的实例,用于操作Users实体对应的数据库表。
2.2.3 方法
a. findAll 方法
-
功能:分页查询用户列表。
-
参数:
json:包含分页参数的对象,默认值为{}。page:当前页码,默认为1。pageSize:每页显示的记录数,默认为10。
-
实现:
- 使用
findAndCount方法查询用户列表并返回总记录数。 skip:跳过前面的记录数,用于分页。take:每页显示的记录数。order:按id降序排列。
- 使用
b. findDetail 方法
- 功能:根据用户 ID 查询用户详情。
- 参数:
id:用户的唯一标识符。
- 实现:
- 使用
findBy方法查询符合条件的用户记录。
- 使用
c. findOne 方法
- 功能:根据条件查询单个用户。
- 参数:
where:查询条件对象。
- 实现:
- 使用
findOne方法查询符合条件的单个用户记录。
- 使用
d. save 方法
- 功能:创建新用户。
- 参数:
createUserDto:包含用户信息的对象,类型为Partial<Users>(即Users实体的部分属性)。
- 实现:
- 使用
create方法创建一个新的用户实体。 - 使用
save方法将用户实体保存到数据库。
- 使用
e. update 方法
- 功能:更新用户信息。
- 参数:
id:用户的唯一标识符。updateUserDto:包含更新信息的对象,类型为Partial<Users>。
- 实现:
- 使用
update方法更新指定 ID 的用户记录。
f. del 方法
- 功能:删除用户。
- 参数:
id:用户的唯一标识符。
- 实现:
- 使用
delete方法删除指定 ID 的用户记录。
- 使用
3. auth 控制器 Controller
3.1 重点源码解析
3.1.1 生成验证码
- 路由:
GET /admin/auth/code - 功能:生成验证码图片并返回给客户端。
this.toolsService.getCaptcha():调用工具服务生成验证码(返回一个包含验证码文本和 SVG 数据的对象)。req.session.captcha:将验证码文本存储到会话(session)中,用于后续验证。res.type('image/svg+xml'):设置响应类型为 SVG 图片。res.send(svgCaptcha.data):将生成的 SVG 图片发送给客户端。
@Get('code')
getCode(@Request() req, @Response() res) {
const svgCaptcha = this.toolsService.getCaptcha();
req.session.captcha = svgCaptcha.text;
res.type('image/svg+xml');
res.send(svgCaptcha.data);
}
3.1.2 处理登录请求
- 路由:
POST /admin/auth/doLogin - 功能:处理用户登录请求。
- 步骤 1:验证验证码
- 从请求体中获取
username、password和captcha。 - 检查验证码是否为空或与会话中存储的验证码不一致(忽略大小写)。
- 如果验证码错误,调用
this.toolsService.tips方法返回错误提示,并重定向到登录页面。
- 从请求体中获取
- 步骤 2:验证用户名和密码
- 调用
this.usersService.findOne方法查询用户信息。 - 对用户输入的密码进行 MD5 加密后与数据库中的密码进行比较。
- 如果用户不存在或密码错误,返回错误提示并重定向到登录页面。
- 调用
- 步骤 3:登录成功
- 将用户信息存储到会话中(
req.session.userinfo)。 - 重定向到仪表盘页面(
/admin/dashboard)。
- 将用户信息存储到会话中(
- 步骤 1:验证验证码
@Post('doLogin')
async doLogin(@Body() body, @Request() req, @Response() res) {
const {username, password, captcha} = body;
if (!captcha || captcha.toUpperCase() !== req.session.captcha.toUpperCase()) {
await this.toolsService.tips(res, {
type: 'error',
redirectUrl: '/admin/auth/login',
message: '验证码错误!'
})
return;
}
const resp = await this.usersService.findOne({
username,
password: this.toolsService.getMd5(password),
})
if (!resp || !resp.id) {
await this.toolsService.tips(res, {
type: 'error',
redirectUrl: '/admin/auth/login',
message: '账号或密码错误!'
})
return;
}
req.session.userinfo = resp;
res.redirect('/admin/dashboard')
}
3.2 完整源码
import {Body, Controller, Get, Post, Query, Render, Request, Response} from '@nestjs/common';
import {ToolsService} from "../../../../service/tools/tools.service";
import {UsersService} from "../../../../service/users/users.service";
@Controller('admin/auth')
export class AuthController {
constructor(
private readonly toolsService: ToolsService,
private readonly usersService: UsersService
) {
}
// 生成验证码图片并返回给客户端。
@Get('code')
getCode(@Request() req, @Response() res) {
const svgCaptcha = this.toolsService.getCaptcha();
req.session.captcha = svgCaptcha.text; // 将验证码文本存储到会话(session)中,用于后续验证。
res.type('image/svg+xml'); // 设置响应类型为 SVG 图片。
res.send(svgCaptcha.data); // 将生成的 SVG 图片发送给客户端。
}
// 登录页面渲染
@Get('login')
@Render('login')
login() {
return {}
}
// 处理登录请求
@Post('doLogin')
async doLogin(@Body() body, @Request() req, @Response() res) {
const {username, password, captcha} = body;
// 步骤 1:验证验证码
if (!captcha || captcha.toUpperCase() !== req.session.captcha.toUpperCase()) {
await this.toolsService.tips(res, {
type: 'error',
redirectUrl: '/admin/auth/login',
message: '验证码错误!'
})
return;
}
// 步骤 2:验证用户名和密码
const resp = await this.usersService.findOne({
username,
password: this.toolsService.getMd5(password),
})
if (!resp || !resp.id) {
await this.toolsService.tips(res, {
type: 'error',
redirectUrl: '/admin/auth/login',
message: '账号或密码错误!'
})
return;
}
// 登录成功
req.session.userinfo = resp;
res.redirect('/admin/dashboard')
}
// 退出登录
@Get('logout')
async logout(@Request() req, @Response() res) {
req.session.userinfo = null;
res.redirect('/admin/auth/login')
}
}
三、系统用户管理
基于以上登录鉴权结果,对当前系统中的用户进行维护(增删改查),以下是源码:
import {Body, Controller, Get, Post, Query, Render, Request, Response} from '@nestjs/common';
import {UsersService} from "../../../../service/users/users.service";
import {ToolsService} from "../../../../service/tools/tools.service";
@Controller('admin/users')
export class UsersController {
constructor(
private readonly usersService: UsersService,
private readonly toolsService: ToolsService
) {
}
// 查询用户列表
@Get()
@Render('users')
async list() {
// 调用 usersService 的 findAll 方法获取用户列表和总数
const [list, count] = await this.usersService.findAll();
// 返回用户列表和总数,用于渲染 users 模板
return {
list,
count
}
}
// 查询用户详细信息
@Get('userinfo')
@Render('userinfo')
async userinfo(@Query() query) {
// 如果查询参数中没有 id,返回空对象
if (!query.id) return {info: {}};
// 调用 usersService 的 findDetail 方法,根据 id 查询用户详细信息
const infoArray = await this.usersService.findDetail(query.id);
// 返回用户信息,用于渲染 userinfo 模板
return {info: infoArray[0]}
}
// 添加用户
@Post('add')
async add(@Body() body, @Response() res, @Request() req) {
// 从请求体中获取用户名、密码、昵称和邮箱
const {username, password, nickname, email} = body;
// 检查用户名是否已存在
const isExist = await this.usersService.findOne({username});
if (isExist && isExist.username) {
// 如果用户名已存在,返回错误提示并重定向到用户信息页面
await this.toolsService.tips(res, {
type: 'error',
message: '当前用户名已存在',
redirectUrl: `/admin/users/userinfo`
})
return;
}
// 检查表单是否填写完整
if (!Object.values(body).filter(it => it).length) {
// 如果表单未填写完整,返回错误提示并重定向到用户信息页面
await this.toolsService.tips(res, {
type: 'error',
message: '请将表单每一项填写完整',
redirectUrl: `/admin/users/userinfo`
})
return;
}
// 对密码进行 MD5 加密
const pwd = await this.toolsService.getMd5(password);
// 调用 usersService 的 save 方法保存用户信息
const resp = await this.usersService.save({
username, password: pwd, nickname, email
});
// 根据保存结果返回提示信息
if (resp) {
await this.toolsService.tips(res, {
type: 'success',
redirectUrl: '/admin/users',
})
} else {
await this.toolsService.tips(res, {
type: 'error',
message: '修改失败,请稍后重试',
redirectUrl: '/admin/users',
})
}
}
// 编辑用户信息
@Post('editUserinfo')
async editUserinfo(@Body() body, @Response() res, @Request() req) {
// 从请求体中获取用户 id
const {id} = body;
// 构建更新对象,过滤掉空值并对密码进行 MD5 加密
const obj = {};
Object.entries(body).forEach(it => {
const [key, value = ''] = it;
if (value) {
if (key === 'password') obj[key] = this.toolsService.getMd5(value)
else obj[key] = value
}
});
// 调用 usersService 的 update 方法更新用户信息
const resp = await this.usersService.update(id, obj);
// 根据更新结果返回提示信息
if (resp) {
const arg = {
type: 'success',
redirectUrl: '/admin/users',
};
// 如果当前登录用户修改了自己的信息,清空 session 并提示重新登录
if (Number(req.session.userinfo.id) === Number(id)) {
req.session.userinfo = null;
Object.assign(arg, {
redirectUrl: '/admin/auth/login',
message: '已修改用户信息,请重新登录!'
})
}
await this.toolsService.tips(res, arg)
} else {
await this.toolsService.tips(res, {
type: 'error',
message: '修改失败,请稍后重试',
redirectUrl: `/admin/auth/userinfo?id=${id}`
})
}
}
// 删除用户
@Get('delete')
async del(@Query() query, @Response() res) {
// 调用 usersService 的 del 方法删除用户
const resp = await this.usersService.del(query.id);
// 根据删除结果返回提示信息
if (resp) {
await this.toolsService.tips(res, {
type: 'success',
redirectUrl: '/admin/users',
})
} else {
await this.toolsService.tips(res, {
type: 'error',
message: '删除失败,请稍后重试',
redirectUrl: '/admin/users',
})
}
}
}