Эх сурвалжийг харах

添加作者,书籍,内容之间的关联关系

max 6 өдөр өмнө
parent
commit
9cebfc91c5

+ 96 - 0
LMS-NodeJs/AUTHOR_BOOK_RELATIONSHIP.md

@@ -0,0 +1,96 @@
+# 作者和书籍关系实现总结
+
+## 完成的工作
+
+### 1. 完善了 Author 相关字段
+
+**文件:`src/author/dto/create-author.dto.ts`**
+- 添加了完整的作者字段:姓名、简介、国籍、出生日期、性别、头像、个人网站、邮箱
+- 使用了适当的验证装饰器
+- 添加了 Swagger 文档注解
+
+**文件:`src/author/entities/author.entity.ts`**
+- 定义了完整的 Author 实体
+- 包含所有必要的数据库字段
+- 建立了与 Book 的一对多关系
+
+### 2. 建立了 Author 和 Book 的关系
+
+**关系类型:一对多(One-to-Many)**
+- 一个作者可以对应多个书籍
+- 每本书只能对应一个作者
+
+**数据库设计:**
+- `authors` 表:存储作者信息
+- `books` 表:添加 `authorId` 外键字段,关联到 `authors.id`
+
+### 3. 更新了 Book 相关文件
+
+**文件:`src/book/entities/book.entity.ts`**
+- 移除了原来的 `author` 字符串字段
+- 添加了 `authorId` 外键字段
+- 建立了与 Author 的多对一关系
+
+**文件:`src/book/dto/create-book.dto.ts`**
+- 将 `author` 字符串字段改为 `authorId` 数字字段
+- 更新了验证规则和文档注解
+
+### 4. 更新了服务层
+
+**文件:`src/author/author.service.ts`**
+- 实现了完整的 CRUD 操作
+- 在查询时包含关联的书籍信息
+- 删除作者时检查是否有关联的书籍
+
+**文件:`src/book/book.service.ts`**
+- 创建书籍时验证作者是否存在
+- 查询时包含作者信息
+- 添加了根据作者查找书籍的方法
+
+### 5. 更新了控制器
+
+**文件:`src/book/book.controller.ts`**
+- 添加了根据作者ID查找书籍的端点:`GET /book/author/:authorId`
+
+### 6. 更新了模块配置
+
+**文件:`src/author/author.module.ts`**
+- 添加了 TypeORM 实体导入
+- 导出了 AuthorService
+
+**文件:`src/book/book.module.ts`**
+- 添加了 Author 实体的导入
+
+## API 端点
+
+### 作者相关端点
+- `POST /author` - 创建作者
+- `GET /author` - 获取所有作者(包含书籍信息)
+- `GET /author/:id` - 获取单个作者(包含书籍信息)
+- `PATCH /author/:id` - 更新作者
+- `DELETE /author/:id` - 删除作者
+
+### 书籍相关端点
+- `POST /book` - 创建书籍(需要提供 authorId)
+- `GET /book` - 获取所有书籍(包含作者信息)
+- `GET /book/:id` - 获取单个书籍(包含作者信息)
+- `GET /book/author/:authorId` - 根据作者ID获取书籍
+- `PATCH /book/:id` - 更新书籍
+- `DELETE /book/:id` - 删除书籍
+
+## 数据验证
+
+1. **创建书籍时**:必须提供有效的 `authorId`
+2. **删除作者时**:如果作者有关联的书籍,会阻止删除
+3. **更新书籍时**:如果提供新的 `authorId`,会验证作者是否存在
+
+## 关系特点
+
+- **一对多关系**:一个作者可以写多本书
+- **数据完整性**:删除作者前会检查关联的书籍
+- **查询优化**:查询时会自动加载关联数据
+- **类型安全**:使用 TypeScript 确保类型安全
+
+## 测试建议
+
+使用提供的测试文档 `test-author-book-relationship.md` 来验证关系是否正确工作。 

+ 2 - 0
LMS-NodeJs/src/app.module.ts

@@ -11,6 +11,7 @@ import { HotModule } from './hot/hot.module';
 import { AideModule } from './aide/aide.module';
 import { LineModule } from './line/line.module';
 import { FileModule } from './file/file.module';
+import { ContentModule } from './content/content.module';
 
 @Module({
   imports: [
@@ -35,6 +36,7 @@ import { FileModule } from './file/file.module';
     AideModule,
     LineModule,
     FileModule,
+    ContentModule,
   ],
   controllers: [AppController],
   providers: [AppService],

+ 4 - 0
LMS-NodeJs/src/author/author.module.ts

@@ -1,9 +1,13 @@
 import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
 import { AuthorService } from './author.service';
 import { AuthorController } from './author.controller';
+import { Author } from './entities/author.entity';
 
 @Module({
+  imports: [TypeOrmModule.forFeature([Author])],
   controllers: [AuthorController],
   providers: [AuthorService],
+  exports: [AuthorService],
 })
 export class AuthorModule {}

+ 40 - 11
LMS-NodeJs/src/author/author.service.ts

@@ -1,26 +1,55 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
 import { CreateAuthorDto } from './dto/create-author.dto';
 import { UpdateAuthorDto } from './dto/update-author.dto';
+import { Author } from './entities/author.entity';
 
 @Injectable()
 export class AuthorService {
-  create(createAuthorDto: CreateAuthorDto) {
-    return 'This action adds a new author';
+  constructor(
+    @InjectRepository(Author)
+    private authorRepository: Repository<Author>,
+  ) {}
+
+  async create(createAuthorDto: CreateAuthorDto): Promise<Author> {
+    const author = this.authorRepository.create(createAuthorDto);
+    return await this.authorRepository.save(author);
   }
 
-  findAll() {
-    return `This action returns all author`;
+  async findAll(): Promise<Author[]> {
+    return await this.authorRepository.find({
+      relations: ['books'],
+    });
   }
 
-  findOne(id: number) {
-    return `This action returns a #${id} author`;
+  async findOne(id: number): Promise<Author> {
+    const author = await this.authorRepository.findOne({
+      where: { id },
+      relations: ['books'],
+    });
+    
+    if (!author) {
+      throw new NotFoundException(`作者ID ${id} 不存在`);
+    }
+    
+    return author;
   }
 
-  update(id: number, updateAuthorDto: UpdateAuthorDto) {
-    return `This action updates a #${id} author`;
+  async update(id: number, updateAuthorDto: UpdateAuthorDto): Promise<Author> {
+    const author = await this.findOne(id);
+    Object.assign(author, updateAuthorDto);
+    return await this.authorRepository.save(author);
   }
 
-  remove(id: number) {
-    return `This action removes a #${id} author`;
+  async remove(id: number): Promise<void> {
+    const author = await this.findOne(id);
+    
+    // 检查作者是否有关联的书籍
+    if (author.books && author.books.length > 0) {
+      throw new BadRequestException(`无法删除作者,该作者还有 ${author.books.length} 本关联的书籍`);
+    }
+    
+    await this.authorRepository.remove(author);
   }
 }

+ 90 - 1
LMS-NodeJs/src/author/dto/create-author.dto.ts

@@ -1 +1,90 @@
-export class CreateAuthorDto {}
+import {
+  IsNotEmpty,
+  IsString,
+  IsOptional,
+  IsDateString,
+  IsUrl,
+  IsEnum,
+} from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+
+export enum AuthorGender {
+  MALE = 'male',
+  FEMALE = 'female',
+  OTHER = 'other',
+}
+
+export class CreateAuthorDto {
+  @ApiProperty({
+    description: '作者姓名',
+    example: 'Nicholas C. Zakas',
+  })
+  @IsNotEmpty({ message: '作者姓名不能为空' })
+  @IsString({ message: '作者姓名必须是字符串' })
+  name: string;
+
+  @ApiProperty({
+    description: '作者简介',
+    example: 'Nicholas C. Zakas是一位著名的JavaScript专家和作家',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '作者简介必须是字符串' })
+  biography?: string;
+
+  @ApiProperty({
+    description: '国籍',
+    example: '美国',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '国籍必须是字符串' })
+  nationality?: string;
+
+  @ApiProperty({
+    description: '出生日期',
+    example: '1975-01-01',
+    format: 'date',
+    required: false,
+  })
+  @IsOptional()
+  @IsDateString({}, { message: '出生日期格式不正确' })
+  birthDate?: string;
+
+  @ApiProperty({
+    description: '性别',
+    example: AuthorGender.MALE,
+    enum: AuthorGender,
+    required: false,
+  })
+  @IsOptional()
+  @IsEnum(AuthorGender, { message: '性别格式不正确' })
+  gender?: AuthorGender;
+
+  @ApiProperty({
+    description: '作者头像URL',
+    example: 'https://example.com/author-avatar.jpg',
+    required: false,
+  })
+  @IsOptional()
+  @IsUrl({}, { message: '作者头像URL格式不正确' })
+  avatar?: string;
+
+  @ApiProperty({
+    description: '个人网站',
+    example: 'https://www.nczonline.net',
+    required: false,
+  })
+  @IsOptional()
+  @IsUrl({}, { message: '个人网站URL格式不正确' })
+  website?: string;
+
+  @ApiProperty({
+    description: '邮箱',
+    example: 'author@example.com',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '邮箱必须是字符串' })
+  email?: string;
+}

+ 54 - 1
LMS-NodeJs/src/author/entities/author.entity.ts

@@ -1 +1,54 @@
-export class Author {}
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  OneToMany,
+  CreateDateColumn,
+  UpdateDateColumn,
+} from 'typeorm';
+import { Book } from '../../book/entities/book.entity';
+
+export enum AuthorGender {
+  MALE = 'male',
+  FEMALE = 'female',
+  OTHER = 'other',
+}
+
+@Entity('authors')
+export class Author {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column({ type: 'varchar', length: 100 })
+  name: string;
+
+  @Column({ type: 'text', nullable: true })
+  biography: string;
+
+  @Column({ type: 'varchar', length: 50, nullable: true })
+  nationality: string;
+
+  @Column({ type: 'date', nullable: true })
+  birthDate: Date;
+
+  @Column({ type: 'enum', enum: AuthorGender, nullable: true })
+  gender: AuthorGender;
+
+  @Column({ type: 'varchar', length: 500, nullable: true })
+  avatar: string;
+
+  @Column({ type: 'varchar', length: 255, nullable: true })
+  website: string;
+
+  @Column({ type: 'varchar', length: 100, nullable: true })
+  email: string;
+
+  @CreateDateColumn()
+  createdAt: Date;
+
+  @UpdateDateColumn()
+  updatedAt: Date;
+
+  @OneToMany(() => Book, (book) => book.author)
+  books: Book[];
+}

+ 30 - 8
LMS-NodeJs/src/book/book.controller.ts

@@ -11,7 +11,13 @@ import {
 import { BookService } from './book.service';
 import { CreateBookDto } from './dto/create-book.dto';
 import { UpdateBookDto } from './dto/update-book.dto';
-import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
+import {
+  ApiTags,
+  ApiOperation,
+  ApiResponse,
+  ApiParam,
+  ApiQuery,
+} from '@nestjs/swagger';
 import { BookLanguage } from './entities/book.entity';
 
 @ApiTags('books')
@@ -35,20 +41,24 @@ export class BookController {
   }
 
   @Get('publisher/:publisher')
-  @ApiOperation({ 
-    summary: '根据出版社获取图书', 
-    description: '根据出版社名称获取所有相关图书' 
+  @ApiOperation({
+    summary: '根据出版社获取图书',
+    description: '根据出版社名称获取所有相关图书',
+  })
+  @ApiParam({
+    name: 'publisher',
+    description: '出版社名称',
+    example: '人民邮电出版社',
   })
-  @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: '根据书籍语言获取所有相关图书' 
+  @ApiOperation({
+    summary: '根据语言获取图书',
+    description: '根据书籍语言获取所有相关图书',
   })
   @ApiParam({ name: 'language', description: '书籍语言', example: 'zh' })
   @ApiResponse({ status: 200, description: '获取成功' })
@@ -68,6 +78,18 @@ export class BookController {
     return this.bookService.findByTag(+tagId);
   }
 
+  @Get('author/:authorId')
+  @ApiOperation({
+    summary: '根据作者获取图书',
+    description: '根据作者ID获取所有相关图书',
+  })
+  @ApiParam({ name: 'authorId', description: '作者ID', example: 1 })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 404, description: '作者不存在' })
+  findByAuthor(@Param('authorId') authorId: string) {
+    return this.bookService.findByAuthor(+authorId);
+  }
+
   @Get(':id')
   @ApiOperation({
     summary: '根据ID获取图书',

+ 2 - 1
LMS-NodeJs/src/book/book.module.ts

@@ -4,9 +4,10 @@ import { BookService } from './book.service';
 import { BookController } from './book.controller';
 import { Book } from './entities/book.entity';
 import { Tag } from '../tag/entities/tag.entity';
+import { Author } from '../author/entities/author.entity';
 
 @Module({
-  imports: [TypeOrmModule.forFeature([Book, Tag])],
+  imports: [TypeOrmModule.forFeature([Book, Tag, Author])],
   controllers: [BookController],
   providers: [BookService],
   exports: [BookService],

+ 37 - 5
LMS-NodeJs/src/book/book.service.ts

@@ -5,6 +5,7 @@ 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';
+import { Author } from '../author/entities/author.entity';
 
 @Injectable()
 export class BookService {
@@ -13,16 +14,29 @@ export class BookService {
     private bookRepository: Repository<Book>,
     @InjectRepository(Tag)
     private tagRepository: Repository<Tag>,
+    @InjectRepository(Author)
+    private authorRepository: Repository<Author>,
   ) {}
 
   async create(createBookDto: CreateBookDto) {
     const book = this.bookRepository.create(createBookDto);
-    
+
     // 转换出版日期字符串为 Date 对象
     if (createBookDto.publishDate) {
       book.publishDate = new Date(createBookDto.publishDate);
     }
 
+    // 验证作者是否存在
+    if (createBookDto.authorId) {
+      const author = await this.authorRepository.findOne({
+        where: { id: createBookDto.authorId },
+      });
+      if (!author) {
+        throw new NotFoundException(`作者ID ${createBookDto.authorId} 不存在`);
+      }
+      book.author = author;
+    }
+
     // 如果提供了标签ID,则关联标签
     if (createBookDto.tagIds && createBookDto.tagIds.length > 0) {
       const tags = await this.tagRepository.findByIds(createBookDto.tagIds);
@@ -37,14 +51,14 @@ export class BookService {
 
   async findAll() {
     return this.bookRepository.find({
-      relations: ['tags'],
+      relations: ['tags', 'author'],
     });
   }
 
   async findOne(id: number) {
     const book = await this.bookRepository.findOne({
       where: { id },
-      relations: ['tags'],
+      relations: ['tags', 'author'],
     });
 
     if (!book) {
@@ -65,6 +79,17 @@ export class BookService {
       book.publishDate = new Date(updateBookDto.publishDate);
     }
 
+    // 如果提供了新的作者ID,则验证并更新作者关联
+    if (updateBookDto.authorId) {
+      const author = await this.authorRepository.findOne({
+        where: { id: updateBookDto.authorId },
+      });
+      if (!author) {
+        throw new NotFoundException(`作者ID ${updateBookDto.authorId} 不存在`);
+      }
+      book.author = author;
+    }
+
     // 如果提供了新的标签ID,则更新标签关联
     if (updateBookDto.tagIds) {
       const tags = await this.tagRepository.findByIds(updateBookDto.tagIds);
@@ -93,14 +118,21 @@ export class BookService {
   async findByPublisher(publisher: string) {
     return this.bookRepository.find({
       where: { publisher },
-      relations: ['tags'],
+      relations: ['tags', 'author'],
     });
   }
 
   async findByLanguage(language: BookLanguage) {
     return this.bookRepository.find({
       where: { language },
-      relations: ['tags'],
+      relations: ['tags', 'author'],
+    });
+  }
+
+  async findByAuthor(authorId: number) {
+    return this.bookRepository.find({
+      where: { authorId },
+      relations: ['tags', 'author'],
     });
   }
 }

+ 6 - 6
LMS-NodeJs/src/book/dto/create-book.dto.ts

@@ -43,12 +43,12 @@ export class CreateBookDto {
   description?: string;
 
   @ApiProperty({
-    description: '作者',
-    example: 'Nicholas C. Zakas',
+    description: '作者ID',
+    example: 1,
   })
-  @IsNotEmpty({ message: '作者不能为空' })
-  @IsString({ message: '作者必须是字符串' })
-  author: string;
+  @IsNotEmpty({ message: '作者ID不能为空' })
+  @IsNumber({}, { message: '作者ID必须是数字' })
+  authorId: number;
 
   @ApiProperty({
     description: 'ISBN号',
@@ -87,7 +87,7 @@ export class CreateBookDto {
 
   @ApiProperty({
     description: '价格(元)',
-    example: 89.00,
+    example: 89.0,
     minimum: 0,
   })
   @IsNotEmpty({ message: '价格不能为空' })

+ 20 - 3
LMS-NodeJs/src/book/entities/book.entity.ts

@@ -1,5 +1,15 @@
-import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  ManyToMany,
+  OneToMany,
+  ManyToOne,
+  JoinColumn,
+} from 'typeorm';
 import { Tag } from '../../tag/entities/tag.entity';
+import { Chapter } from '../../chapter/entities/chapter.entity';
+import { Author } from '../../author/entities/author.entity';
 
 export enum BookLanguage {
   CHINESE = 'zh',
@@ -25,8 +35,12 @@ export class Book {
   @Column({ type: 'text', nullable: true })
   description: string;
 
-  @Column({ type: 'varchar', length: 100 })
-  author: string;
+  @ManyToOne(() => Author, (author) => author.books)
+  @JoinColumn({ name: 'authorId' })
+  author: Author;
+
+  @Column({ type: 'int' })
+  authorId: number;
 
   @Column({ type: 'varchar', length: 50, unique: true })
   isbn: string;
@@ -64,4 +78,7 @@ export class Book {
 
   @ManyToMany(() => Tag, (tag) => tag.books)
   tags: Tag[];
+
+  @OneToMany(() => Chapter, (chapter) => chapter.book)
+  chapters: Chapter[];
 }

+ 61 - 0
LMS-NodeJs/src/chapter/chapter.controller.ts

@@ -11,6 +11,7 @@ import { ChapterService } from './chapter.service';
 import { CreateChapterDto } from './dto/create-chapter.dto';
 import { UpdateChapterDto } from './dto/update-chapter.dto';
 import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
+import { ChapterStatus } from './entities/chapter.entity';
 
 @ApiTags('chapters')
 @Controller('chapter')
@@ -32,6 +33,66 @@ export class ChapterController {
     return this.chapterService.findAll();
   }
 
+  @Get('book/:bookId')
+  @ApiOperation({ 
+    summary: '根据书籍获取章节', 
+    description: '根据书籍ID获取所有相关章节' 
+  })
+  @ApiParam({ name: 'bookId', description: '书籍ID', example: 1 })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  findByBook(@Param('bookId') bookId: string) {
+    return this.chapterService.findByBook(+bookId);
+  }
+
+  @Get('parent/:parentId')
+  @ApiOperation({ 
+    summary: '根据父章节获取子章节', 
+    description: '根据父章节ID获取所有子章节' 
+  })
+  @ApiParam({ name: 'parentId', description: '父章节ID', example: 1 })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  findByParent(@Param('parentId') parentId: string) {
+    return this.chapterService.findByParent(+parentId);
+  }
+
+  @Get('chapterId/:chapterId')
+  @ApiOperation({ 
+    summary: '根据章节ID获取章节', 
+    description: '根据章节的唯一ID获取章节信息' 
+  })
+  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 404, description: '章节不存在' })
+  findByChapterId(@Param('chapterId') chapterId: string) {
+    return this.chapterService.findByChapterId(chapterId);
+  }
+
+  @Patch(':id/sort/:sort')
+  @ApiOperation({ 
+    summary: '更新章节排序', 
+    description: '更新章节的排序位置' 
+  })
+  @ApiParam({ name: 'id', description: '章节ID', example: 1 })
+  @ApiParam({ name: 'sort', description: '新的排序值', example: 2 })
+  @ApiResponse({ status: 200, description: '排序更新成功' })
+  @ApiResponse({ status: 404, description: '章节不存在' })
+  updateSort(@Param('id') id: string, @Param('sort') sort: string) {
+    return this.chapterService.updateSort(+id, +sort);
+  }
+
+  @Patch(':id/status/:status')
+  @ApiOperation({ 
+    summary: '更新章节状态', 
+    description: '更新章节的发布状态' 
+  })
+  @ApiParam({ name: 'id', description: '章节ID', example: 1 })
+  @ApiParam({ name: 'status', description: '章节状态', example: 'published' })
+  @ApiResponse({ status: 200, description: '状态更新成功' })
+  @ApiResponse({ status: 404, description: '章节不存在' })
+  updateStatus(@Param('id') id: string, @Param('status') status: ChapterStatus) {
+    return this.chapterService.updateStatus(+id, status);
+  }
+
   @Get(':id')
   @ApiOperation({
     summary: '根据ID获取章节',

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

@@ -1,9 +1,14 @@
 import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
 import { ChapterService } from './chapter.service';
 import { ChapterController } from './chapter.controller';
+import { Chapter } from './entities/chapter.entity';
+import { Book } from '../book/entities/book.entity';
 
 @Module({
+  imports: [TypeOrmModule.forFeature([Chapter, Book])],
   controllers: [ChapterController],
   providers: [ChapterService],
+  exports: [ChapterService],
 })
 export class ChapterModule {}

+ 137 - 11
LMS-NodeJs/src/chapter/chapter.service.ts

@@ -1,26 +1,152 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
 import { CreateChapterDto } from './dto/create-chapter.dto';
 import { UpdateChapterDto } from './dto/update-chapter.dto';
+import { Chapter, ChapterStatus } from './entities/chapter.entity';
+import { Book } from '../book/entities/book.entity';
 
 @Injectable()
 export class ChapterService {
-  create(createChapterDto: CreateChapterDto) {
-    return 'This action adds a new chapter';
+  constructor(
+    @InjectRepository(Chapter)
+    private chapterRepository: Repository<Chapter>,
+    @InjectRepository(Book)
+    private bookRepository: Repository<Book>,
+  ) {}
+
+  private generateChapterId(bookId: number): string {
+    const now = new Date();
+    const timestamp = now.getFullYear().toString() +
+      (now.getMonth() + 1).toString().padStart(2, '0') +
+      now.getDate().toString().padStart(2, '0') +
+      now.getHours().toString().padStart(2, '0') +
+      now.getMinutes().toString().padStart(2, '0') +
+      now.getSeconds().toString().padStart(2, '0');
+    
+    // 生成6位随机字符串
+    const randomStr = Math.random().toString(36).substring(2, 8);
+    
+    return `book_${bookId}_${timestamp}_${randomStr}`;
   }
 
-  findAll() {
-    return `This action returns all chapter`;
+  async create(createChapterDto: CreateChapterDto) {
+    // 验证书籍是否存在
+    const book = await this.bookRepository.findOne({
+      where: { id: createChapterDto.bookId },
+    });
+    if (!book) {
+      throw new NotFoundException(`ID为${createChapterDto.bookId}的书籍不存在`);
+    }
+
+    const chapter = this.chapterRepository.create(createChapterDto);
+
+    // 自动生成 chapterId(如果未提供)
+    if (!chapter.chapterId) {
+      chapter.chapterId = this.generateChapterId(createChapterDto.bookId);
+    }
+
+    // 转换日期字符串为 Date 对象
+    if (createChapterDto.createdAt) {
+      chapter.createdAt = new Date(createChapterDto.createdAt);
+    }
+    if (createChapterDto.updatedAt) {
+      chapter.updatedAt = new Date(createChapterDto.updatedAt);
+    }
+
+    // 设置默认值
+    if (!chapter.status) {
+      chapter.status = ChapterStatus.DRAFT;
+    }
+    if (chapter.isVisible === undefined) {
+      chapter.isVisible = true;
+    }
+
+    return this.chapterRepository.save(chapter);
+  }
+
+  async findAll() {
+    return this.chapterRepository.find({
+      relations: ['book', 'parent', 'children'],
+      order: { sort: 'ASC' },
+    });
+  }
+
+  async findOne(id: number) {
+    const chapter = await this.chapterRepository.findOne({
+      where: { id },
+      relations: ['book', 'parent', 'children'],
+    });
+
+    if (!chapter) {
+      throw new NotFoundException(`ID为${id}的章节不存在`);
+    }
+
+    return chapter;
+  }
+
+  async findByChapterId(chapterId: string) {
+    const chapter = await this.chapterRepository.findOne({
+      where: { chapterId },
+      relations: ['book', 'parent', 'children'],
+    });
+
+    if (!chapter) {
+      throw new NotFoundException(`章节ID为${chapterId}的章节不存在`);
+    }
+
+    return chapter;
+  }
+
+  async findByBook(bookId: number) {
+    return this.chapterRepository.find({
+      where: { bookId },
+      relations: ['parent', 'children'],
+      order: { sort: 'ASC' },
+    });
+  }
+
+  async findByParent(parentId: number) {
+    return this.chapterRepository.find({
+      where: { parentId },
+      relations: ['book', 'children'],
+      order: { sort: 'ASC' },
+    });
+  }
+
+  async update(id: number, updateChapterDto: UpdateChapterDto) {
+    const chapter = await this.findOne(id);
+
+    // 更新基本字段
+    Object.assign(chapter, updateChapterDto);
+
+    // 转换日期字符串为 Date 对象
+    if (updateChapterDto.createdAt) {
+      chapter.createdAt = new Date(updateChapterDto.createdAt);
+    }
+    if (updateChapterDto.updatedAt) {
+      chapter.updatedAt = new Date(updateChapterDto.updatedAt);
+    }
+
+    return this.chapterRepository.save(chapter);
   }
 
-  findOne(id: number) {
-    return `This action returns a #${id} chapter`;
+  async remove(id: number) {
+    const chapter = await this.findOne(id);
+    return this.chapterRepository.remove(chapter);
   }
 
-  update(id: number, updateChapterDto: UpdateChapterDto) {
-    return `This action updates a #${id} chapter`;
+  async updateSort(id: number, newSort: number) {
+    const chapter = await this.findOne(id);
+    chapter.sort = newSort;
+    chapter.updatedAt = new Date();
+    return this.chapterRepository.save(chapter);
   }
 
-  remove(id: number) {
-    return `This action removes a #${id} chapter`;
+  async updateStatus(id: number, status: ChapterStatus) {
+    const chapter = await this.findOne(id);
+    chapter.status = status;
+    chapter.updatedAt = new Date();
+    return this.chapterRepository.save(chapter);
   }
 }

+ 131 - 1
LMS-NodeJs/src/chapter/dto/create-chapter.dto.ts

@@ -1 +1,131 @@
-export class CreateChapterDto {}
+import {
+  IsNotEmpty,
+  IsString,
+  IsOptional,
+  IsNumber,
+  IsDateString,
+  Min,
+  Max,
+} from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+
+export class CreateChapterDto {
+  @ApiProperty({
+    description: '章节ID(基于书籍ID和时间自动生成)',
+    example: 'book_1_20240101120000_abc123',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '章节ID必须是字符串' })
+  chapterId?: string;
+
+  @ApiProperty({
+    description: '章节标题',
+    example: '第一章 JavaScript简介',
+  })
+  @IsNotEmpty({ message: '章节标题不能为空' })
+  @IsString({ message: '章节标题必须是字符串' })
+  title: string;
+
+  @ApiProperty({
+    description: '章节内容',
+    example: 'JavaScript是一种具有函数优先的轻量级...',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '章节内容必须是字符串' })
+  content?: string;
+
+  @ApiProperty({
+    description: '章节排序(数字越小越靠前)',
+    example: 1,
+    minimum: 1,
+  })
+  @IsNotEmpty({ message: '章节排序不能为空' })
+  @IsNumber({}, { message: '章节排序必须是数字' })
+  @Min(1, { message: '章节排序必须大于0' })
+  sort: number;
+
+  @ApiProperty({
+    description: '章节简介',
+    example: '本章将介绍JavaScript的基本概念和历史背景',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '章节简介必须是字符串' })
+  summary?: string;
+
+  @ApiProperty({
+    description: '章节状态',
+    example: 'published',
+    enum: ['draft', 'published', 'archived'],
+    default: 'draft',
+  })
+  @IsOptional()
+  @IsString({ message: '章节状态必须是字符串' })
+  status?: string = 'draft';
+
+  @ApiProperty({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00.000Z',
+    format: 'date-time',
+  })
+  @IsNotEmpty({ message: '创建时间不能为空' })
+  @IsDateString({}, { message: '创建时间格式不正确' })
+  createdAt: string;
+
+  @ApiProperty({
+    description: '修改时间',
+    example: '2024-01-01T00:00:00.000Z',
+    format: 'date-time',
+  })
+  @IsNotEmpty({ message: '修改时间不能为空' })
+  @IsDateString({}, { message: '修改时间格式不正确' })
+  updatedAt: string;
+
+  @ApiProperty({
+    description: '修改人',
+    example: 'admin',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '修改人必须是字符串' })
+  updatedBy?: string;
+
+  @ApiProperty({
+    description: '所属书籍ID',
+    example: 1,
+  })
+  @IsNotEmpty({ message: '所属书籍ID不能为空' })
+  @IsNumber({}, { message: '所属书籍ID必须是数字' })
+  bookId: number;
+
+  @ApiProperty({
+    description: '父章节ID(用于子章节)',
+    example: 1,
+    required: false,
+  })
+  @IsOptional()
+  @IsNumber({}, { message: '父章节ID必须是数字' })
+  parentId?: number;
+
+  @ApiProperty({
+    description: '章节层级(1为一级章节,2为二级章节等)',
+    example: 1,
+    minimum: 1,
+    maximum: 5,
+  })
+  @IsNotEmpty({ message: '章节层级不能为空' })
+  @IsNumber({}, { message: '章节层级必须是数字' })
+  @Min(1, { message: '章节层级必须大于0' })
+  @Max(5, { message: '章节层级不能超过5' })
+  level: number;
+
+  @ApiProperty({
+    description: '是否可见',
+    example: true,
+    default: true,
+  })
+  @IsOptional()
+  isVisible?: boolean = true;
+}

+ 76 - 1
LMS-NodeJs/src/chapter/entities/chapter.entity.ts

@@ -1 +1,76 @@
-export class Chapter {}
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  ManyToOne,
+  OneToMany,
+  JoinColumn,
+} from 'typeorm';
+import { Book } from '../../book/entities/book.entity';
+import { Content } from '../../content/entities/content.entity';
+
+export enum ChapterStatus {
+  DRAFT = 'draft',
+  PUBLISHED = 'published',
+  ARCHIVED = 'archived',
+}
+
+@Entity('chapters')
+export class Chapter {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column({ type: 'varchar', length: 100, unique: true })
+  chapterId: string;
+
+  @Column({ type: 'varchar', length: 255 })
+  title: string;
+
+  @Column({ type: 'text', nullable: true })
+  content: string;
+
+  @Column({ type: 'int' })
+  sort: number;
+
+  @Column({ type: 'text', nullable: true })
+  summary: string;
+
+  @Column({ type: 'enum', enum: ChapterStatus, default: ChapterStatus.DRAFT })
+  status: ChapterStatus;
+
+  @Column({ type: 'timestamp' })
+  createdAt: Date;
+
+  @Column({ type: 'timestamp' })
+  updatedAt: Date;
+
+  @Column({ type: 'varchar', length: 100, nullable: true })
+  updatedBy: string;
+
+  @Column({ type: 'int' })
+  bookId: number;
+
+  @Column({ type: 'int', nullable: true })
+  parentId: number;
+
+  @Column({ type: 'int' })
+  level: number;
+
+  @Column({ type: 'boolean', default: true })
+  isVisible: boolean;
+
+  // 关联关系
+  @ManyToOne(() => Book, (book) => book.chapters)
+  @JoinColumn({ name: 'bookId' })
+  book: Book;
+
+  @ManyToOne(() => Chapter, (chapter) => chapter.children)
+  @JoinColumn({ name: 'parentId' })
+  parent: Chapter;
+
+  @OneToMany(() => Chapter, (chapter) => chapter.parent)
+  children: Chapter[];
+
+  @OneToMany(() => Content, (content) => content.chapter)
+  contents: Content[];
+}

+ 20 - 0
LMS-NodeJs/src/content/content.controller.spec.ts

@@ -0,0 +1,20 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ContentController } from './content.controller';
+import { ContentService } from './content.service';
+
+describe('ContentController', () => {
+  let controller: ContentController;
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      controllers: [ContentController],
+      providers: [ContentService],
+    }).compile();
+
+    controller = module.get<ContentController>(ContentController);
+  });
+
+  it('should be defined', () => {
+    expect(controller).toBeDefined();
+  });
+});

+ 117 - 0
LMS-NodeJs/src/content/content.controller.ts

@@ -0,0 +1,117 @@
+import {
+  Controller,
+  Get,
+  Post,
+  Body,
+  Patch,
+  Param,
+  Delete,
+} from '@nestjs/common';
+import { ContentService } from './content.service';
+import { CreateContentDto } from './dto/create-content.dto';
+import { UpdateContentDto } from './dto/update-content.dto';
+import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
+import { ContentStatus } from './entities/content.entity';
+
+@ApiTags('contents')
+@Controller('content')
+export class ContentController {
+  constructor(private readonly contentService: ContentService) {}
+
+  @Post()
+  @ApiOperation({ summary: '创建内容', description: '创建新的内容记录,自动生成新版本' })
+  @ApiResponse({ status: 201, description: '内容创建成功' })
+  @ApiResponse({ status: 400, description: '请求参数错误' })
+  create(@Body() createContentDto: CreateContentDto) {
+    return this.contentService.create(createContentDto);
+  }
+
+  @Get()
+  @ApiOperation({ summary: '获取所有内容', description: '获取所有内容列表' })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  findAll() {
+    return this.contentService.findAll();
+  }
+
+  @Get('chapter/:chapterId')
+  @ApiOperation({ 
+    summary: '获取章节最新内容', 
+    description: '根据章节ID获取最新版本的内容' 
+  })
+  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 404, description: '内容不存在' })
+  findByChapterId(@Param('chapterId') chapterId: string) {
+    return this.contentService.findByChapterId(chapterId);
+  }
+
+  @Get('chapter/:chapterId/versions')
+  @ApiOperation({ 
+    summary: '获取章节所有版本', 
+    description: '根据章节ID获取所有版本的内容' 
+  })
+  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  findVersionsByChapterId(@Param('chapterId') chapterId: string) {
+    return this.contentService.findVersionsByChapterId(chapterId);
+  }
+
+  @Get('chapter/:chapterId/version/:version')
+  @ApiOperation({ 
+    summary: '获取指定版本内容', 
+    description: '根据章节ID和版本号获取指定版本的内容' 
+  })
+  @ApiParam({ name: 'chapterId', description: '章节ID', example: 'book_1_20240101120000_abc123' })
+  @ApiParam({ name: 'version', description: '版本号', example: 1 })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 404, description: '内容不存在' })
+  findByChapterIdAndVersion(@Param('chapterId') chapterId: string, @Param('version') version: string) {
+    return this.contentService.findByChapterIdAndVersion(chapterId, +version);
+  }
+
+  @Patch(':id/status/:status')
+  @ApiOperation({ 
+    summary: '更新内容状态', 
+    description: '更新内容的发布状态' 
+  })
+  @ApiParam({ name: 'id', description: '内容ID', example: 1 })
+  @ApiParam({ name: 'status', description: '内容状态', example: 'published' })
+  @ApiResponse({ status: 200, description: '状态更新成功' })
+  @ApiResponse({ status: 404, description: '内容不存在' })
+  updateStatus(@Param('id') id: string, @Param('status') status: ContentStatus) {
+    return this.contentService.updateStatus(+id, status);
+  }
+
+  @Get(':id')
+  @ApiOperation({
+    summary: '根据ID获取内容',
+    description: '根据内容ID获取单个内容',
+  })
+  @ApiParam({ name: 'id', description: '内容ID', example: 1 })
+  @ApiResponse({ status: 200, description: '获取成功' })
+  @ApiResponse({ status: 404, description: '内容不存在' })
+  findOne(@Param('id') id: string) {
+    return this.contentService.findOne(+id);
+  }
+
+  @Patch(':id')
+  @ApiOperation({ 
+    summary: '更新内容', 
+    description: '根据ID更新内容,会创建新版本而不是修改现有版本' 
+  })
+  @ApiParam({ name: 'id', description: '内容ID', example: 1 })
+  @ApiResponse({ status: 200, description: '更新成功' })
+  @ApiResponse({ status: 404, description: '内容不存在' })
+  update(@Param('id') id: string, @Body() updateContentDto: UpdateContentDto) {
+    return this.contentService.update(+id, updateContentDto);
+  }
+
+  @Delete(':id')
+  @ApiOperation({ summary: '删除内容', description: '根据ID删除内容' })
+  @ApiParam({ name: 'id', description: '内容ID', example: 1 })
+  @ApiResponse({ status: 200, description: '删除成功' })
+  @ApiResponse({ status: 404, description: '内容不存在' })
+  remove(@Param('id') id: string) {
+    return this.contentService.remove(+id);
+  }
+}

+ 14 - 0
LMS-NodeJs/src/content/content.module.ts

@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { ContentService } from './content.service';
+import { ContentController } from './content.controller';
+import { Content } from './entities/content.entity';
+import { Chapter } from '../chapter/entities/chapter.entity';
+
+@Module({
+  imports: [TypeOrmModule.forFeature([Content, Chapter])],
+  controllers: [ContentController],
+  providers: [ContentService],
+  exports: [ContentService],
+})
+export class ContentModule {}

+ 18 - 0
LMS-NodeJs/src/content/content.service.spec.ts

@@ -0,0 +1,18 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { ContentService } from './content.service';
+
+describe('ContentService', () => {
+  let service: ContentService;
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      providers: [ContentService],
+    }).compile();
+
+    service = module.get<ContentService>(ContentService);
+  });
+
+  it('should be defined', () => {
+    expect(service).toBeDefined();
+  });
+});

+ 186 - 0
LMS-NodeJs/src/content/content.service.ts

@@ -0,0 +1,186 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import { CreateContentDto } from './dto/create-content.dto';
+import { UpdateContentDto } from './dto/update-content.dto';
+import { Content, ContentStatus } from './entities/content.entity';
+import { Chapter } from '../chapter/entities/chapter.entity';
+
+@Injectable()
+export class ContentService {
+  constructor(
+    @InjectRepository(Content)
+    private contentRepository: Repository<Content>,
+    @InjectRepository(Chapter)
+    private chapterRepository: Repository<Chapter>,
+  ) {}
+
+  async create(createContentDto: CreateContentDto) {
+    // 验证章节是否存在
+    const chapter = await this.chapterRepository.findOne({
+      where: { chapterId: createContentDto.chapterId },
+    });
+    if (!chapter) {
+      throw new NotFoundException(`章节ID为${createContentDto.chapterId}的章节不存在`);
+    }
+
+    // 获取当前章节的最新版本号
+    const latestVersion = await this.getLatestVersion(createContentDto.chapterId);
+    const newVersion = latestVersion + 1;
+
+    const content = this.contentRepository.create(createContentDto);
+    content.version = newVersion;
+
+    // 转换日期字符串为 Date 对象
+    if (createContentDto.createdAt) {
+      content.createdAt = new Date(createContentDto.createdAt);
+    }
+    if (createContentDto.updatedAt) {
+      content.updatedAt = new Date(createContentDto.updatedAt);
+    }
+
+    // 设置默认值
+    if (!content.status) {
+      content.status = ContentStatus.DRAFT;
+    }
+    if (content.allowComments === undefined) {
+      content.allowComments = true;
+    }
+    if (content.isPublic === undefined) {
+      content.isPublic = true;
+    }
+    if (content.sortWeight === undefined) {
+      content.sortWeight = 100;
+    }
+
+    // 自动计算字数统计
+    if (!content.wordCount && content.content) {
+      content.wordCount = this.calculateWordCount(content.content);
+    }
+
+    // 自动计算阅读时间
+    if (!content.readingTime && content.wordCount) {
+      content.readingTime = this.calculateReadingTime(content.wordCount);
+    }
+
+    return this.contentRepository.save(content);
+  }
+
+  async findAll() {
+    return this.contentRepository.find({
+      relations: ['chapter'],
+      order: { createdAt: 'DESC' },
+    });
+  }
+
+  async findOne(id: number) {
+    const content = await this.contentRepository.findOne({
+      where: { id },
+      relations: ['chapter'],
+    });
+
+    if (!content) {
+      throw new NotFoundException(`ID为${id}的内容不存在`);
+    }
+
+    return content;
+  }
+
+  async findByChapterId(chapterId: string) {
+    // 返回章节的最新版本内容
+    const content = await this.contentRepository.findOne({
+      where: { chapterId },
+      relations: ['chapter'],
+      order: { version: 'DESC' },
+    });
+
+    if (!content) {
+      throw new NotFoundException(`章节ID为${chapterId}的内容不存在`);
+    }
+
+    return content;
+  }
+
+  async findByChapterIdAndVersion(chapterId: string, version: number) {
+    const content = await this.contentRepository.findOne({
+      where: { chapterId, version },
+      relations: ['chapter'],
+    });
+
+    if (!content) {
+      throw new NotFoundException(`章节ID为${chapterId}版本${version}的内容不存在`);
+    }
+
+    return content;
+  }
+
+  async findVersionsByChapterId(chapterId: string) {
+    return this.contentRepository.find({
+      where: { chapterId },
+      relations: ['chapter'],
+      order: { version: 'DESC' },
+    });
+  }
+
+  async update(id: number, updateContentDto: UpdateContentDto) {
+    const content = await this.findOne(id);
+
+    // 创建新版本而不是更新现有版本
+    const newContent = this.contentRepository.create({
+      ...content,
+      ...updateContentDto,
+      id: undefined, // 清除ID以创建新记录
+    });
+
+    // 获取新版本号
+    const latestVersion = await this.getLatestVersion(content.chapterId);
+    newContent.version = latestVersion + 1;
+
+    // 转换日期字符串为 Date 对象
+    if (updateContentDto.createdAt) {
+      newContent.createdAt = new Date(updateContentDto.createdAt);
+    }
+    if (updateContentDto.updatedAt) {
+      newContent.updatedAt = new Date(updateContentDto.updatedAt);
+    }
+
+    // 重新计算字数统计和阅读时间
+    if (newContent.content) {
+      newContent.wordCount = this.calculateWordCount(newContent.content);
+      newContent.readingTime = this.calculateReadingTime(newContent.wordCount);
+    }
+
+    return this.contentRepository.save(newContent);
+  }
+
+  async remove(id: number) {
+    const content = await this.findOne(id);
+    return this.contentRepository.remove(content);
+  }
+
+  async updateStatus(id: number, status: ContentStatus) {
+    const content = await this.findOne(id);
+    content.status = status;
+    content.updatedAt = new Date();
+    return this.contentRepository.save(content);
+  }
+
+  private async getLatestVersion(chapterId: string): Promise<number> {
+    const latestContent = await this.contentRepository.findOne({
+      where: { chapterId },
+      order: { version: 'DESC' },
+    });
+    return latestContent ? latestContent.version : 0;
+  }
+
+  private calculateWordCount(content: string): number {
+    // 简单的字数统计,可以根据需要优化
+    return content.replace(/\s+/g, '').length;
+  }
+
+  private calculateReadingTime(wordCount: number): number {
+    // 假设每分钟阅读300字
+    const wordsPerMinute = 300;
+    return Math.ceil(wordCount / wordsPerMinute);
+  }
+}

+ 190 - 0
LMS-NodeJs/src/content/dto/create-content.dto.ts

@@ -0,0 +1,190 @@
+import {
+  IsNotEmpty,
+  IsString,
+  IsOptional,
+  IsNumber,
+  IsDateString,
+  Min,
+  IsEnum,
+} from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+
+export enum ContentType {
+  TEXT = 'text',
+  MARKDOWN = 'markdown',
+  HTML = 'html',
+  RICH_TEXT = 'rich_text',
+}
+
+export enum ContentStatus {
+  DRAFT = 'draft',
+  PUBLISHED = 'published',
+  ARCHIVED = 'archived',
+}
+
+export class CreateContentDto {
+  @ApiProperty({
+    description: '所属章节ID',
+    example: 'book_1_20240101120000_abc123',
+  })
+  @IsNotEmpty({ message: '章节ID不能为空' })
+  @IsString({ message: '章节ID必须是字符串' })
+  chapterId: string;
+
+  @ApiProperty({
+    description: '内容版本号(自动生成)',
+    example: 1,
+    required: false,
+  })
+  @IsOptional()
+  @IsNumber({}, { message: '版本号必须是数字' })
+  @Min(1, { message: '版本号必须大于0' })
+  version?: number;
+
+  @ApiProperty({
+    description: '内容标题',
+    example: 'JavaScript基础语法',
+  })
+  @IsNotEmpty({ message: '内容标题不能为空' })
+  @IsString({ message: '内容标题必须是字符串' })
+  title: string;
+
+  @ApiProperty({
+    description: '内容主体',
+    example: 'JavaScript是一种具有函数优先的轻量级...',
+  })
+  @IsNotEmpty({ message: '内容主体不能为空' })
+  @IsString({ message: '内容主体必须是字符串' })
+  content: string;
+
+  @ApiProperty({
+    description: '内容类型',
+    example: ContentType.MARKDOWN,
+    enum: ContentType,
+  })
+  @IsNotEmpty({ message: '内容类型不能为空' })
+  @IsEnum(ContentType, { message: '内容类型格式不正确' })
+  contentType: ContentType;
+
+  @ApiProperty({
+    description: '内容状态',
+    example: ContentStatus.DRAFT,
+    enum: ContentStatus,
+    default: ContentStatus.DRAFT,
+  })
+  @IsOptional()
+  @IsEnum(ContentStatus, { message: '内容状态格式不正确' })
+  status?: ContentStatus = ContentStatus.DRAFT;
+
+  @ApiProperty({
+    description: '内容摘要',
+    example: '本章介绍JavaScript的基本语法和概念',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '内容摘要必须是字符串' })
+  summary?: string;
+
+  @ApiProperty({
+    description: '关键词(逗号分隔)',
+    example: 'JavaScript,语法,变量,函数',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '关键词必须是字符串' })
+  keywords?: string;
+
+  @ApiProperty({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00.000Z',
+    format: 'date-time',
+  })
+  @IsNotEmpty({ message: '创建时间不能为空' })
+  @IsDateString({}, { message: '创建时间格式不正确' })
+  createdAt: string;
+
+  @ApiProperty({
+    description: '修改时间',
+    example: '2024-01-01T00:00:00.000Z',
+    format: 'date-time',
+  })
+  @IsNotEmpty({ message: '修改时间不能为空' })
+  @IsDateString({}, { message: '修改时间格式不正确' })
+  updatedAt: string;
+
+  @ApiProperty({
+    description: '创建人',
+    example: 'admin',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '创建人必须是字符串' })
+  createdBy?: string;
+
+  @ApiProperty({
+    description: '修改人',
+    example: 'editor',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '修改人必须是字符串' })
+  updatedBy?: string;
+
+  @ApiProperty({
+    description: '字数统计',
+    example: 2500,
+    minimum: 0,
+    required: false,
+  })
+  @IsOptional()
+  @IsNumber({}, { message: '字数统计必须是数字' })
+  @Min(0, { message: '字数统计不能为负数' })
+  wordCount?: number;
+
+  @ApiProperty({
+    description: '阅读时间(分钟)',
+    example: 15,
+    minimum: 1,
+    required: false,
+  })
+  @IsOptional()
+  @IsNumber({}, { message: '阅读时间必须是数字' })
+  @Min(1, { message: '阅读时间必须大于0' })
+  readingTime?: number;
+
+  @ApiProperty({
+    description: '是否允许评论',
+    example: true,
+    default: true,
+  })
+  @IsOptional()
+  allowComments?: boolean = true;
+
+  @ApiProperty({
+    description: '是否公开',
+    example: true,
+    default: true,
+  })
+  @IsOptional()
+  isPublic?: boolean = true;
+
+  @ApiProperty({
+    description: '排序权重(数字越大越靠前)',
+    example: 100,
+    minimum: 0,
+    required: false,
+  })
+  @IsOptional()
+  @IsNumber({}, { message: '排序权重必须是数字' })
+  @Min(0, { message: '排序权重不能为负数' })
+  sortWeight?: number = 100;
+
+  @ApiProperty({
+    description: '备注信息',
+    example: '这是第一版内容,后续会继续完善',
+    required: false,
+  })
+  @IsOptional()
+  @IsString({ message: '备注信息必须是字符串' })
+  remarks?: string;
+}

+ 4 - 0
LMS-NodeJs/src/content/dto/update-content.dto.ts

@@ -0,0 +1,4 @@
+import { PartialType } from '@nestjs/swagger';
+import { CreateContentDto } from './create-content.dto';
+
+export class UpdateContentDto extends PartialType(CreateContentDto) {}

+ 88 - 0
LMS-NodeJs/src/content/entities/content.entity.ts

@@ -0,0 +1,88 @@
+import {
+  Entity,
+  PrimaryGeneratedColumn,
+  Column,
+  ManyToOne,
+  JoinColumn,
+  Index,
+} from 'typeorm';
+import { Chapter } from '../../chapter/entities/chapter.entity';
+
+export enum ContentType {
+  TEXT = 'text',
+  MARKDOWN = 'markdown',
+  HTML = 'html',
+  RICH_TEXT = 'rich_text',
+}
+
+export enum ContentStatus {
+  DRAFT = 'draft',
+  PUBLISHED = 'published',
+  ARCHIVED = 'archived',
+}
+
+@Entity('contents')
+@Index(['chapterId', 'version'], { unique: true })
+export class Content {
+  @PrimaryGeneratedColumn()
+  id: number;
+
+  @Column({ type: 'varchar', length: 100 })
+  chapterId: string;
+
+  @Column({ type: 'int' })
+  version: number;
+
+  @Column({ type: 'varchar', length: 255 })
+  title: string;
+
+  @Column({ type: 'longtext' })
+  content: string;
+
+  @Column({ type: 'enum', enum: ContentType })
+  contentType: ContentType;
+
+  @Column({ type: 'enum', enum: ContentStatus, default: ContentStatus.DRAFT })
+  status: ContentStatus;
+
+  @Column({ type: 'text', nullable: true })
+  summary: string;
+
+  @Column({ type: 'text', nullable: true })
+  keywords: string;
+
+  @Column({ type: 'timestamp' })
+  createdAt: Date;
+
+  @Column({ type: 'timestamp' })
+  updatedAt: Date;
+
+  @Column({ type: 'varchar', length: 100, nullable: true })
+  createdBy: string;
+
+  @Column({ type: 'varchar', length: 100, nullable: true })
+  updatedBy: string;
+
+  @Column({ type: 'int', nullable: true })
+  wordCount: number;
+
+  @Column({ type: 'int', nullable: true })
+  readingTime: number;
+
+  @Column({ type: 'boolean', default: true })
+  allowComments: boolean;
+
+  @Column({ type: 'boolean', default: true })
+  isPublic: boolean;
+
+  @Column({ type: 'int', default: 100 })
+  sortWeight: number;
+
+  @Column({ type: 'text', nullable: true })
+  remarks: string;
+
+  // 关联关系
+  @ManyToOne(() => Chapter, (chapter) => chapter.contents)
+  @JoinColumn({ name: 'chapterId', referencedColumnName: 'chapterId' })
+  chapter: Chapter;
+}

+ 70 - 0
LMS-NodeJs/test-author-book-relationship.md

@@ -0,0 +1,70 @@
+# 作者和书籍关系测试
+
+## 测试步骤
+
+### 1. 创建作者
+```bash
+curl -X POST http://localhost:3000/author \
+  -H "Content-Type: application/json" \
+  -d '{
+    "name": "Nicholas C. Zakas",
+    "biography": "Nicholas C. Zakas是一位著名的JavaScript专家和作家",
+    "nationality": "美国",
+    "birthDate": "1975-01-01",
+    "gender": "male",
+    "email": "nicholas@example.com"
+  }'
+```
+
+### 2. 创建书籍(关联到作者)
+```bash
+curl -X POST http://localhost:3000/book \
+  -H "Content-Type: application/json" \
+  -d '{
+    "title": "JavaScript高级程序设计",
+    "description": "这是一本关于JavaScript编程的经典书籍",
+    "authorId": 1,
+    "isbn": "9787115545381",
+    "publisher": "人民邮电出版社",
+    "publishDate": "2020-01-01",
+    "pages": 688,
+    "price": 89.0,
+    "language": "zh"
+  }'
+```
+
+### 3. 获取所有作者(包含书籍信息)
+```bash
+curl http://localhost:3000/author
+```
+
+### 4. 获取所有书籍(包含作者信息)
+```bash
+curl http://localhost:3000/book
+```
+
+### 5. 根据作者ID获取书籍
+```bash
+curl http://localhost:3000/book/author/1
+```
+
+### 6. 获取单个作者(包含书籍信息)
+```bash
+curl http://localhost:3000/author/1
+```
+
+## 预期结果
+
+1. 作者创建成功,返回包含ID的作者信息
+2. 书籍创建成功,返回包含作者信息的书籍
+3. 获取作者列表时,每个作者包含其关联的书籍数组
+4. 获取书籍列表时,每本书包含其作者信息
+5. 根据作者ID查询书籍时,返回该作者的所有书籍
+6. 获取单个作者时,包含该作者的所有书籍信息
+
+## 关系验证
+
+- 一个作者可以对应多个书籍(一对多关系)
+- 每本书只能对应一个作者(多对一关系)
+- 删除作者时,需要先处理其关联的书籍
+- 创建书籍时必须提供有效的作者ID