max 5 days ago
parent
commit
8f1128aac5

+ 37 - 2
LMS-NodeJs/src/book/book.controller.ts

@@ -6,12 +6,13 @@ import {
   Patch,
   Param,
   Delete,
+  Query,
 } from '@nestjs/common';
 import { BookService } from './book.service';
 import { CreateBookDto } from './dto/create-book.dto';
 import { UpdateBookDto } from './dto/update-book.dto';
-import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
-import { Book } from './entities/book.entity';
+import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
+import { BookLanguage } from './entities/book.entity';
 
 @ApiTags('books')
 @Controller('book')
@@ -33,6 +34,40 @@ export class BookController {
     return this.bookService.findAll();
   }
 
+  @Get('publisher/:publisher')
+  @ApiOperation({ 
+    summary: '根据出版社获取图书', 
+    description: '根据出版社名称获取所有相关图书' 
+  })
+  @ApiParam({ name: 'publisher', description: '出版社名称', example: '人民邮电出版社' })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  findByPublisher(@Param('publisher') publisher: string) {
+    return this.bookService.findByPublisher(publisher);
+  }
+
+  @Get('language/:language')
+  @ApiOperation({ 
+    summary: '根据语言获取图书', 
+    description: '根据书籍语言获取所有相关图书' 
+  })
+  @ApiParam({ name: 'language', description: '书籍语言', example: 'zh' })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  findByLanguage(@Param('language') language: BookLanguage) {
+    return this.bookService.findByLanguage(language);
+  }
+
+  @Get('tag/:tagId')
+  @ApiOperation({
+    summary: '根据标签获取图书',
+    description: '根据标签ID获取所有关联的图书',
+  })
+  @ApiParam({ name: 'tagId', description: '标签ID', example: 1 })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 404, description: '标签不存在' })
+  findByTag(@Param('tagId') tagId: string) {
+    return this.bookService.findByTag(+tagId);
+  }
+
   @Get(':id')
   @ApiOperation({
     summary: '根据ID获取图书',

+ 5 - 0
LMS-NodeJs/src/book/book.module.ts

@@ -1,9 +1,14 @@
 import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
 import { BookService } from './book.service';
 import { BookController } from './book.controller';
+import { Book } from './entities/book.entity';
+import { Tag } from '../tag/entities/tag.entity';
 
 @Module({
+  imports: [TypeOrmModule.forFeature([Book, Tag])],
   controllers: [BookController],
   providers: [BookService],
+  exports: [BookService],
 })
 export class BookModule {}

+ 91 - 11
LMS-NodeJs/src/book/book.service.ts

@@ -1,26 +1,106 @@
-import { Injectable } from '@nestjs/common';
+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, BookLanguage } from './entities/book.entity';
+import { Tag } from '../tag/entities/tag.entity';
 
 @Injectable()
 export class BookService {
-  create(createBookDto: CreateBookDto) {
-    return 'This action adds a new book';
+  constructor(
+    @InjectRepository(Book)
+    private bookRepository: Repository<Book>,
+    @InjectRepository(Tag)
+    private tagRepository: Repository<Tag>,
+  ) {}
+
+  async create(createBookDto: CreateBookDto) {
+    const book = this.bookRepository.create(createBookDto);
+    
+    // 转换出版日期字符串为 Date 对象
+    if (createBookDto.publishDate) {
+      book.publishDate = new Date(createBookDto.publishDate);
+    }
+
+    // 如果提供了标签ID,则关联标签
+    if (createBookDto.tagIds && createBookDto.tagIds.length > 0) {
+      const tags = await this.tagRepository.findByIds(createBookDto.tagIds);
+      if (tags.length !== createBookDto.tagIds.length) {
+        throw new NotFoundException('部分标签不存在');
+      }
+      book.tags = tags;
+    }
+
+    return this.bookRepository.save(book);
+  }
+
+  async findAll() {
+    return this.bookRepository.find({
+      relations: ['tags'],
+    });
+  }
+
+  async findOne(id: number) {
+    const book = await this.bookRepository.findOne({
+      where: { id },
+      relations: ['tags'],
+    });
+
+    if (!book) {
+      throw new NotFoundException(`ID为${id}的书籍不存在`);
+    }
+
+    return book;
+  }
+
+  async update(id: number, updateBookDto: UpdateBookDto) {
+    const book = await this.findOne(id);
+
+    // 更新基本字段
+    Object.assign(book, updateBookDto);
+
+    // 转换出版日期字符串为 Date 对象
+    if (updateBookDto.publishDate) {
+      book.publishDate = new Date(updateBookDto.publishDate);
+    }
+
+    // 如果提供了新的标签ID,则更新标签关联
+    if (updateBookDto.tagIds) {
+      const tags = await this.tagRepository.findByIds(updateBookDto.tagIds);
+      if (tags.length !== updateBookDto.tagIds.length) {
+        throw new NotFoundException('部分标签不存在');
+      }
+      book.tags = tags;
+    }
+
+    return this.bookRepository.save(book);
   }
 
-  findAll() {
-    return `This action returns all book`;
+  async remove(id: number) {
+    const book = await this.findOne(id);
+    return this.bookRepository.remove(book);
   }
 
-  findOne(id: number) {
-    return `This action returns a #${id} book`;
+  async findByTag(tagId: number) {
+    return this.bookRepository
+      .createQueryBuilder('book')
+      .leftJoinAndSelect('book.tags', 'tag')
+      .where('tag.id = :tagId', { tagId })
+      .getMany();
   }
 
-  update(id: number, updateBookDto: UpdateBookDto) {
-    return `This action updates a #${id} book`;
+  async findByPublisher(publisher: string) {
+    return this.bookRepository.find({
+      where: { publisher },
+      relations: ['tags'],
+    });
   }
 
-  remove(id: number) {
-    return `This action removes a #${id} book`;
+  async findByLanguage(language: BookLanguage) {
+    return this.bookRepository.find({
+      where: { language },
+      relations: ['tags'],
+    });
   }
 }

+ 136 - 1
LMS-NodeJs/src/book/dto/create-book.dto.ts

@@ -1 +1,136 @@
-export class CreateBookDto {}
+import {
+  IsNotEmpty,
+  IsString,
+  IsOptional,
+  IsArray,
+  IsNumber,
+  IsDateString,
+  IsUrl,
+  Min,
+  IsEnum,
+} from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+
+export enum BookLanguage {
+  CHINESE = 'zh',
+  ENGLISH = 'en',
+  JAPANESE = 'ja',
+  KOREAN = 'ko',
+}
+
+export enum BookStatus {
+  PUBLISHED = 'published',
+  DRAFT = 'draft',
+  OUT_OF_PRINT = 'out_of_print',
+}
+
+export class CreateBookDto {
+  @ApiProperty({
+    description: '书籍标题',
+    example: 'JavaScript高级程序设计',
+  })
+  @IsNotEmpty({ message: '书籍标题不能为空' })
+  @IsString({ message: '书籍标题必须是字符串' })
+  title: string;
+
+  @ApiProperty({
+    description: '书籍描述',
+    example: '这是一本关于JavaScript编程的经典书籍',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '书籍描述必须是字符串' })
+  description?: string;
+
+  @ApiProperty({
+    description: '作者',
+    example: 'Nicholas C. Zakas',
+  })
+  @IsNotEmpty({ message: '作者不能为空' })
+  @IsString({ message: '作者必须是字符串' })
+  author: string;
+
+  @ApiProperty({
+    description: 'ISBN号',
+    example: '9787115545381',
+  })
+  @IsNotEmpty({ message: 'ISBN号不能为空' })
+  @IsString({ message: 'ISBN号必须是字符串' })
+  isbn: string;
+
+  @ApiProperty({
+    description: '出版社',
+    example: '人民邮电出版社',
+  })
+  @IsNotEmpty({ message: '出版社不能为空' })
+  @IsString({ message: '出版社必须是字符串' })
+  publisher: string;
+
+  @ApiProperty({
+    description: '出版日期',
+    example: '2020-01-01',
+    format: 'date',
+  })
+  @IsNotEmpty({ message: '出版日期不能为空' })
+  @IsDateString({}, { message: '出版日期格式不正确' })
+  publishDate: string;
+
+  @ApiProperty({
+    description: '页数',
+    example: 688,
+    minimum: 1,
+  })
+  @IsNotEmpty({ message: '页数不能为空' })
+  @IsNumber({}, { message: '页数必须是数字' })
+  @Min(1, { message: '页数必须大于0' })
+  pages: number;
+
+  @ApiProperty({
+    description: '价格(元)',
+    example: 89.00,
+    minimum: 0,
+  })
+  @IsNotEmpty({ message: '价格不能为空' })
+  @IsNumber({}, { message: '价格必须是数字' })
+  @Min(0, { message: '价格不能为负数' })
+  price: number;
+
+  @ApiProperty({
+    description: '书籍语言',
+    example: BookLanguage.CHINESE,
+    enum: BookLanguage,
+  })
+  @IsNotEmpty({ message: '书籍语言不能为空' })
+  @IsEnum(BookLanguage, { message: '书籍语言格式不正确' })
+  language: BookLanguage;
+
+  @ApiProperty({
+    description: '封面图片URL',
+    example: 'https://example.com/cover.jpg',
+    required: false,
+  })
+  @IsOptional()
+  @IsUrl({}, { message: '封面图片URL格式不正确' })
+  coverImage?: string;
+
+  @ApiProperty({
+    description: '书籍状态',
+    example: BookStatus.PUBLISHED,
+    enum: BookStatus,
+    default: BookStatus.PUBLISHED,
+  })
+  @IsOptional()
+  @IsEnum(BookStatus, { message: '书籍状态格式不正确' })
+  status?: BookStatus;
+
+  @ApiProperty({
+    description: '标签ID数组',
+    example: [1, 2, 3],
+    required: false,
+    type: [Number],
+  })
+  @IsOptional()
+  @IsArray({ message: '标签ID必须是数组' })
+  @IsNumber({}, { each: true, message: '标签ID必须是数字' })
+  tagIds?: number[];
+}

+ 67 - 1
LMS-NodeJs/src/book/entities/book.entity.ts

@@ -1 +1,67 @@
-export class Book {}
+import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
+import { Tag } from '../../tag/entities/tag.entity';
+
+export enum BookLanguage {
+  CHINESE = 'zh',
+  ENGLISH = 'en',
+  JAPANESE = 'ja',
+  KOREAN = 'ko',
+}
+
+export enum BookStatus {
+  PUBLISHED = 'published',
+  DRAFT = 'draft',
+  OUT_OF_PRINT = 'out_of_print',
+}
+
+@Entity('books')
+export class Book {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column({ type: 'varchar', length: 255 })
+  title: string;
+
+  @Column({ type: 'text', nullable: true })
+  description: string;
+
+  @Column({ type: 'varchar', length: 100 })
+  author: string;
+
+  @Column({ type: 'varchar', length: 50, unique: true })
+  isbn: string;
+
+  @Column({ type: 'varchar', length: 100 })
+  publisher: string;
+
+  @Column({ type: 'date' })
+  publishDate: Date;
+
+  @Column({ type: 'int' })
+  pages: number;
+
+  @Column({ type: 'decimal', precision: 10, scale: 2 })
+  price: number;
+
+  @Column({ type: 'enum', enum: BookLanguage })
+  language: BookLanguage;
+
+  @Column({ type: 'varchar', length: 500, nullable: true })
+  coverImage: string;
+
+  @Column({ type: 'enum', enum: BookStatus, default: BookStatus.PUBLISHED })
+  status: BookStatus;
+
+  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
+  createdAt: Date;
+
+  @Column({
+    type: 'timestamp',
+    default: () => 'CURRENT_TIMESTAMP',
+    onUpdate: 'CURRENT_TIMESTAMP',
+  })
+  updatedAt: Date;
+
+  @ManyToMany(() => Tag, (tag) => tag.books)
+  tags: Tag[];
+}

+ 23 - 1
LMS-NodeJs/src/tag/dto/create-tag.dto.ts

@@ -1 +1,23 @@
-export class CreateTagDto {}
+import { IsNotEmpty, IsString, MaxLength, IsOptional } from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class CreateTagDto {
+  @ApiProperty({
+    description: '标签名称',
+    example: '技术',
+    maxLength: 50,
+  })
+  @IsNotEmpty({ message: '标签名称不能为空' })
+  @IsString({ message: '标签名称必须是字符串' })
+  @MaxLength(50, { message: '标签名称长度不能超过50个字符' })
+  name: string;
+
+  @ApiProperty({
+    description: '标签描述',
+    example: '技术相关的内容',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '标签描述必须是字符串' })
+  description?: string;
+}

+ 44 - 1
LMS-NodeJs/src/tag/entities/tag.entity.ts

@@ -1 +1,44 @@
-export class Tag {}
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  ManyToMany,
+  JoinTable,
+} from 'typeorm';
+import { Book } from '../../book/entities/book.entity';
+
+@Entity('tags')
+export class Tag {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column({ type: 'varchar', length: 50, unique: true })
+  name: string;
+
+  @Column({ type: 'text', nullable: true })
+  description: string;
+
+  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
+  createdAt: Date;
+
+  @Column({
+    type: 'timestamp',
+    default: () => 'CURRENT_TIMESTAMP',
+    onUpdate: 'CURRENT_TIMESTAMP',
+  })
+  updatedAt: Date;
+
+  @ManyToMany(() => Book, (book) => book.tags)
+  @JoinTable({
+    name: 'book_tags',
+    joinColumn: {
+      name: 'tag_id',
+      referencedColumnName: 'id',
+    },
+    inverseJoinColumn: {
+      name: 'book_id',
+      referencedColumnName: 'id',
+    },
+  })
+  books: Book[];
+}

+ 6 - 1
LMS-NodeJs/src/tag/tag.controller.ts

@@ -54,9 +54,14 @@ export class TagController {
   }
 
   @Delete(':id')
-  @ApiOperation({ summary: '删除标签', description: '根据ID删除标签' })
+  @ApiOperation({
+    summary: '删除标签',
+    description:
+      '根据ID删除标签。注意:如果标签正在被书籍使用,则无法删除,需要先解除书籍与标签的关联关系。',
+  })
   @ApiParam({ name: 'id', description: '标签ID', example: 1 })
   @ApiResponse({ status: 200, description: '删除成功' })
+  @ApiResponse({ status: 400, description: '标签正在被书籍使用,无法删除' })
   @ApiResponse({ status: 404, description: '标签不存在' })
   remove(@Param('id') id: string) {
     return this.tagService.remove(+id);

+ 5 - 0
LMS-NodeJs/src/tag/tag.module.ts

@@ -1,9 +1,14 @@
 import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
 import { TagService } from './tag.service';
 import { TagController } from './tag.controller';
+import { Tag } from './entities/tag.entity';
+import { Book } from '../book/entities/book.entity';
 
 @Module({
+  imports: [TypeOrmModule.forFeature([Tag, Book])],
   controllers: [TagController],
   providers: [TagService],
+  exports: [TagService],
 })
 export class TagModule {}

+ 63 - 11
LMS-NodeJs/src/tag/tag.service.ts

@@ -1,26 +1,78 @@
-import { Injectable } from '@nestjs/common';
+import {
+  Injectable,
+  NotFoundException,
+  BadRequestException,
+} from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
 import { CreateTagDto } from './dto/create-tag.dto';
 import { UpdateTagDto } from './dto/update-tag.dto';
+import { Tag } from './entities/tag.entity';
+import { Book } from '../book/entities/book.entity';
 
 @Injectable()
 export class TagService {
-  create(createTagDto: CreateTagDto) {
-    return 'This action adds a new tag';
+  constructor(
+    @InjectRepository(Tag)
+    private tagRepository: Repository<Tag>,
+    @InjectRepository(Book)
+    private bookRepository: Repository<Book>,
+  ) {}
+
+  async create(createTagDto: CreateTagDto) {
+    const tag = this.tagRepository.create(createTagDto);
+    return this.tagRepository.save(tag);
+  }
+
+  async findAll() {
+    return this.tagRepository.find({
+      relations: ['books'],
+    });
   }
 
-  findAll() {
-    return `This action returns all tag`;
+  async findOne(id: number) {
+    const tag = await this.tagRepository.findOne({
+      where: { id },
+      relations: ['books'],
+    });
+
+    if (!tag) {
+      throw new NotFoundException(`ID为${id}的标签不存在`);
+    }
+
+    return tag;
   }
 
-  findOne(id: number) {
-    return `This action returns a #${id} tag`;
+  async update(id: number, updateTagDto: UpdateTagDto) {
+    const tag = await this.findOne(id);
+    Object.assign(tag, updateTagDto);
+    return this.tagRepository.save(tag);
   }
 
-  update(id: number, updateTagDto: UpdateTagDto) {
-    return `This action updates a #${id} tag`;
+  async remove(id: number) {
+    const tag = await this.findOne(id);
+
+    // 通过查询书籍来检查标签是否被使用
+    const booksUsingTag = await this.bookRepository
+      .createQueryBuilder('book')
+      .leftJoinAndSelect('book.tags', 'tag')
+      .where('tag.id = :tagId', { tagId: id })
+      .getMany();
+
+    if (booksUsingTag.length > 0) {
+      const bookTitles = booksUsingTag.map((book) => book.title).join('、');
+      throw new BadRequestException(
+        `无法删除标签"${tag.name}",该标签正在被以下书籍使用:${bookTitles}。请先解除书籍与标签的关联关系。`,
+      );
+    }
+
+    return this.tagRepository.remove(tag);
   }
 
-  remove(id: number) {
-    return `This action removes a #${id} tag`;
+  async findByName(name: string) {
+    return this.tagRepository.findOne({
+      where: { name },
+      relations: ['books'],
+    });
   }
 }