网文系统基础搭建

54 阅读5分钟

一、服务端

- 项目概述

1.书籍管理

  • 创建新书籍 (POST /books)
  • 查询所有书籍 (GET /books)
  • 查询单本书籍 (GET /books/:id)
  • 更新书籍信息 (PATCH /books/:id)
  • 删除书籍 (DELETE /books/:id)

2.数据验证

  • 使用class-validator进行请求数据验证
  • 书名、作者、分类不能为空
  • 状态必须是:连载中、已完结或暂停更新
  • 字数必须是数字

3、项目结构

  • 模块化设计 :使用NestJS的模块化架构,创建了专门的Books模块
  • 控制器层 :处理HTTP请求,路由管理
  • 服务层 :实现业务逻辑
  • DTO :定义数据传输对象,用于验证和传输数据
  • 实体类 :定义书籍的数据结构

4、技术栈

  • NestJS :基于Node.js的服务器端框架
  • TypeScript :提供类型安全
  • class-validator :数据验证
  • class-transformer :数据转换
  • @nestjs/mapped-types :用于DTO类型转换

- 创建NestJS项目

1. 准备工作

//使用npm初始化一个新的NestJS项目
npm init -y
//安装NestJS CLI
npm install -g @nestjs/cli

2.项目搭建

//命名为novel-api
nest new novel-api --package-manager npm
- 书籍模块
//创建一个书籍模块,包括控制器和服务,用于实现书籍的CRUD操作
nest generate module books
//创建书籍控制器,用于处理HTTP请求
nest generate controller books 
//创建书籍服务,用于实现业务逻辑
nest generate service books 
//创建书籍的DTO(数据传输对象)和实体类,用于定义书籍的数据结构
mkdir src\books\dto
mkdir src\books\entities
1. 创建书籍实体类,定义书籍的数据结构
//novel-api\src\books\entities\book.entity.ts
import { ApiProperty } from '@nestjs/swagger';
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('books')
export class Book {
  @ApiProperty({ description: '书籍ID', example: 1 })
  @PrimaryGeneratedColumn()
  id: number;

  @ApiProperty({ description: '书籍标题', example: '三体' })
  @Column({ length: 100 })
  title: string;

  @ApiProperty({ description: '作者名称', example: '刘慈欣' })
  @Column({ length: 50 })
  author: string;

  @ApiProperty({ description: '书籍描述', example: '科幻小说' })
  @Column({ type: 'text' })
  description: string;

  @ApiProperty({ description: '书籍分类', example: '科幻' })
  @Column({ length: 50 })
  category: string;

  @ApiProperty({ description: '书籍字数', example: 500000 })
  @Column({ type: 'int' })
  wordCount: number;

  @ApiProperty({ description: '书籍状态', example: '已完结', enum: ['连载中', '已完结', '暂停更新'] })
  @Column({ length: 20 })
  status: string; // 连载中、已完结等

  @ApiProperty({ description: '创建时间' })
  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @ApiProperty({ description: '更新时间' })
  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}
2、创建书籍的DTO类,用于验证和传输创建书籍的数据
//novel-api\src\books\dto\create-book.dto.ts
import { IsNotEmpty, IsString, IsNumber, IsOptional, IsEnum } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export enum BookStatus {
  ONGOING = '连载中',
  COMPLETED = '已完结',
  PAUSED = '暂停更新'
}

export class CreateBookDto {
  @ApiProperty({ description: '书籍标题', example: '三体', required: true })
  @IsNotEmpty({ message: '书名不能为空' })
  @IsString({ message: '书名必须是字符串' })
  title: string;

  @ApiProperty({ description: '作者名称', example: '刘慈欣', required: true })
  @IsNotEmpty({ message: '作者不能为空' })
  @IsString({ message: '作者必须是字符串' })
  author: string;

  @ApiProperty({ description: '书籍描述', example: '科幻小说', required: false })
  @IsString({ message: '描述必须是字符串' })
  @IsOptional()
  description: string;

  @ApiProperty({ description: '书籍分类', example: '科幻', required: true })
  @IsString({ message: '分类必须是字符串' })
  @IsNotEmpty({ message: '分类不能为空' })
  category: string;

  @ApiProperty({ description: '书籍字数', example: 500000, required: false })
  @IsNumber({}, { message: '字数必须是数字' })
  @IsOptional()
  wordCount: number;

  @ApiProperty({ description: '书籍状态', example: '已完结', enum: BookStatus, enumName: 'BookStatus', required: true })
  @IsEnum(BookStatus, { message: '状态必须是:连载中、已完结或暂停更新' })
  @IsNotEmpty({ message: '状态不能为空' })
  status: string;
}
3、创建更新书籍的DTO类,用于验证和传输更新书籍的数据
//novel-api\src\books\dto\update-book.dto.ts
import { PartialType } from '@nestjs/swagger';
import { CreateBookDto } from './create-book.dto';

// 使用PartialType将CreateBookDto中的所有属性变为可选
export class UpdateBookDto extends PartialType(CreateBookDto) {}
4、书籍服务,提供书籍的CRUD操作
//novel-api\src\books\books.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';
import { Book } from './entities/book.entity';

@Injectable()
export class BooksService {
  constructor(
    @InjectRepository(Book)
    private bookRepository: Repository<Book>,
  ) {}

  async create(createBookDto: CreateBookDto): Promise<Book> {
    const book = this.bookRepository.create(createBookDto);
    return await this.bookRepository.save(book);
  }

  async findAll(): Promise<Book[]> {
    return await this.bookRepository.find();
  }

  async findOne(id: number): Promise<Book> {
    const book = await this.bookRepository.findOne({ where: { id } });
    if (!book) {
      throw new NotFoundException(`Book with ID ${id} not found`);
    }
    return book;
  }

  async update(id: number, updateBookDto: UpdateBookDto): Promise<Book> {
    const book = await this.findOne(id);
    
    // 合并更新的数据
    const updatedBook = this.bookRepository.merge(book, updateBookDto);
    
    // 保存更新后的数据
    return await this.bookRepository.save(updatedBook);
  }

  async remove(id: number): Promise<void> {
    const book = await this.findOne(id);
    await this.bookRepository.remove(book);
  }
}

//novel-api\src\books\books.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BooksController } from './books.controller';
import { BooksService } from './books.service';
import { Book } from './entities/book.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Book])],
  controllers: [BooksController],
  providers: [BooksService]
})
export class BooksModule {}

5、书籍控制器的CRUD方法,处理HTTP请求并调用服务层的方法
//novel-api\src\books\books.controller.ts
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe, HttpStatus, HttpCode } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
import { BooksService } from './books.service';
import { CreateBookDto } from './dto/create-book.dto';
import { UpdateBookDto } from './dto/update-book.dto';

@ApiTags('books')
@Controller('books')
export class BooksController {
  constructor(private readonly booksService: BooksService) {}

  @ApiOperation({ summary: '创建书籍', description: '创建一本新书籍' })
  @ApiBody({ type: CreateBookDto })
  @ApiResponse({ status: HttpStatus.CREATED, description: '书籍创建成功', type: CreateBookDto })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '请求参数验证失败' })
  @Post()
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() createBookDto: CreateBookDto) {
    return await this.booksService.create(createBookDto);
  }

  @ApiOperation({ summary: '获取所有书籍', description: '获取所有书籍的列表' })
  @ApiResponse({ status: HttpStatus.OK, description: '成功获取所有书籍', type: [CreateBookDto] })
  @Get()
  async findAll() {
    return await this.booksService.findAll();
  }

  @ApiOperation({ summary: '获取单本书籍', description: '根据ID获取单本书籍的详细信息' })
  @ApiParam({ name: 'id', description: '书籍ID', type: 'number' })
  @ApiResponse({ status: HttpStatus.OK, description: '成功获取书籍', type: CreateBookDto })
  @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定ID的书籍不存在' })
  @Get(':id')
  async findOne(@Param('id', ParseIntPipe) id: number) {
    return await this.booksService.findOne(id);
  }

  @ApiOperation({ summary: '更新书籍', description: '更新指定ID的书籍信息' })
  @ApiParam({ name: 'id', description: '书籍ID', type: 'number' })
  @ApiBody({ type: UpdateBookDto })
  @ApiResponse({ status: HttpStatus.OK, description: '书籍更新成功', type: CreateBookDto })
  @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '请求参数验证失败' })
  @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定ID的书籍不存在' })
  @Patch(':id')
  async update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateBookDto: UpdateBookDto,
  ) {
    return await this.booksService.update(id, updateBookDto);
  }

  @ApiOperation({ summary: '删除书籍', description: '删除指定ID的书籍' })
  @ApiParam({ name: 'id', description: '书籍ID', type: 'number' })
  @ApiResponse({ status: HttpStatus.OK, description: '书籍删除成功' })
  @ApiResponse({ status: HttpStatus.NOT_FOUND, description: '指定ID的书籍不存在' })
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id', ParseIntPipe) id: number) {
    await this.booksService.remove(id);
    return null;
  }
}
6、安装class-validator和class-transformer,用于验证请求数据
npm install class-validator class-transformer
7、main.ts文件,启用全局验证管道
//novel-api\src\main.ts
import * as dotenv from 'dotenv';

// 加载环境变量(在导入其他模块之前)
dotenv.config();

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 启用全局验证管道
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, // 过滤掉未在DTO中声明的属性
    transform: true, // 自动转换类型
    forbidNonWhitelisted: true, // 禁止未在DTO中声明的属性
    transformOptions: {
      enableImplicitConversion: true, // 启用隐式类型转换
    },
  }));
  
  // 启用CORS
  app.enableCors();
  
  // 配置Swagger文档
  const config = new DocumentBuilder()
    .setTitle('网文系统API')
    .setDescription('网文系统的API文档,包括书籍的CRUD操作')
    .setVersion('1.0')
    .addTag('books', '书籍相关接口')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api-docs', app, document);
  
  const port = process.env.PORT || 3000;
  await app.listen(port);
  console.log(`应用已启动,监听端口:${port}`);
  console.log(`API文档地址: http://localhost:${port}/api-docs`);
}
bootstrap();
8、启动应用程序

安装@nestjs/mapped-types包,以便使用PartialType。

安装Swagger相关的包,以便生成API文档

npm install @nestjs/swagger swagger-ui-express
npm install @nestjs/mapped-types
//启动
npm run start:dev

3.数据库接入

- 安装TypeORM和MySQL相关的依赖包
npm install @nestjs/typeorm typeorm mysql2
- 数据库配置文件
//novel-api\src\config\database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';

export const databaseConfig: TypeOrmModuleOptions = {
  type: 'mysql',
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '3306'),
  username: process.env.DB_USERNAME || 'root',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_DATABASE || 'novel_db',
  entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  synchronize: process.env.NODE_ENV !== 'production', // 自动同步数据库结构,生产环境中应该禁用
  logging: process.env.NODE_ENV !== 'production',
  migrations: [__dirname + '/../database/migrations/*{.ts,.js}'],
  migrationsRun: true, // 应用启动时自动运行迁移
};

//novel-api\src\app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BooksModule } from './books/books.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { databaseConfig } from './config/database.config';
import { DatabaseModule } from './database/database.module';
import { HealthModule } from './health/health.module';

@Module({
  imports: [
    TypeOrmModule.forRoot(databaseConfig),
    DatabaseModule,
    BooksModule,
    HealthModule
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

//novel-api\.env
# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=666666
DB_DATABASE=novel_db

# 应用配置
PORT=3000
NODE_ENV=development
- 安装dotenv模块来加载环境变量
npm install dotenv
- 初始化数据库
-- 创建数据库
CREATE DATABASE IF NOT EXISTS novel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 使用数据库
USE novel_db;

-- 创建书籍表(仅供参考,实际表结构会由TypeORM自动创建)
CREATE TABLE IF NOT EXISTS books (
  id INT AUTO_INCREMENT PRIMARY KEY,
  title VARCHAR(100) NOT NULL,
  author VARCHAR(50) NOT NULL,
  description TEXT,
  category VARCHAR(50) NOT NULL,
  word_count INT,
  status VARCHAR(20) NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
- 数据库服务
//创建一个数据库服务,用于提供数据库连接和操作
//novel-api\src\database\database.service.ts
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

@Injectable()
export class DatabaseService {
  constructor(
    @InjectDataSource()
    private dataSource: DataSource,
  ) {}

  /**
   * 获取数据库连接
   */
  getDataSource(): DataSource {
    return this.dataSource;
  }

  /**
   * 执行原始SQL查询
   * @param query SQL查询语句
   * @param parameters 查询参数
   */
  async executeQuery(query: string, parameters: any[] = []): Promise<any> {
    return await this.dataSource.query(query, parameters);
  }

  /**
   * 检查数据库连接状态
   */
  async checkConnection(): Promise<boolean> {
    try {
      if (!this.dataSource.isInitialized) {
        await this.dataSource.initialize();
      }
      return this.dataSource.isInitialized;
    } catch (error) {
      console.error('数据库连接失败:', error);
      return false;
    }
  }
}

//创建一个数据库模块,用于提供数据库服务
//novel-api\src\database\database.module.ts
import { Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}