每天40分玩转Django:Django 实操图书管理系统

Django 实操图书管理系统

一、学习目标

今天是Django框架学习的最后一天,我们将通过实现一个完整的图书管理系统来综合运用之前学习的知识。本项目将包含:

  1. 设计并实现图书相关模型
  2. 使用GraphQL构建API接口
  3. 开发前端页面并与后端集成

二、系统架构

2.1 技术栈选型

层级 技术选择 说明
后端 Django 4.2 Web框架
API Graphene-Django 3.0 GraphQL实现
ORM Django ORM 数据库操作
前端 React 18 用户界面
状态管理 Apollo Client GraphQL客户端
UI库 Ant Design 组件库
数据库 PostgreSQL 关系型数据库

2.2 系统架构图

三、后端实现

3.1 数据模型设计

首先创建必要的模型:

python 复制代码
# books/models.py
from django.db import models
from django.contrib.auth.models import User

class Author(models.Model):
    name = models.CharField(max_length=100)
    biography = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.name

class Category(models.Model):
    name = models.CharField(max_length=50)
    description = models.TextField(blank=True)
    
    class Meta:
        verbose_name_plural = "categories"
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    isbn = models.CharField(max_length=13, unique=True)
    description = models.TextField()
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    publication_date = models.DateField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField(default=0)
    cover_image = models.ImageField(upload_to='book_covers/', null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    def __str__(self):
        return self.title

class BookLoan(models.Model):
    LOAN_STATUS = (
        ('BORROWED', 'Borrowed'),
        ('RETURNED', 'Returned'),
        ('OVERDUE', 'Overdue'),
    )
    
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    borrowed_date = models.DateTimeField(auto_now_add=True)
    due_date = models.DateTimeField()
    returned_date = models.DateTimeField(null=True, blank=True)
    status = models.CharField(max_length=10, choices=LOAN_STATUS, default='BORROWED')
    
    def __str__(self):
        return f"{self.book.title} - {self.user.username}"

3.2 GraphQL Schema实现

python 复制代码
# books/schema.py
import graphene
from graphene_django import DjangoObjectType
from .models import Book, Author, Category, BookLoan
from django.contrib.auth.models import User

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = ('id', 'username', 'email')

class AuthorType(DjangoObjectType):
    class Meta:
        model = Author
        fields = '__all__'

class CategoryType(DjangoObjectType):
    class Meta:
        model = Category
        fields = '__all__'

class BookType(DjangoObjectType):
    class Meta:
        model = Book
        fields = '__all__'

class BookLoanType(DjangoObjectType):
    class Meta:
        model = BookLoan
        fields = '__all__'

class Query(graphene.ObjectType):
    all_books = graphene.List(BookType)
    book = graphene.Field(BookType, id=graphene.Int())
    books_by_author = graphene.List(BookType, author_id=graphene.Int())
    books_by_category = graphene.List(BookType, category_id=graphene.Int())
    user_loans = graphene.List(BookLoanType, user_id=graphene.Int())
    
    def resolve_all_books(self, info):
        return Book.objects.all()
    
    def resolve_book(self, info, id):
        return Book.objects.get(pk=id)
    
    def resolve_books_by_author(self, info, author_id):
        return Book.objects.filter(author_id=author_id)
    
    def resolve_books_by_category(self, info, category_id):
        return Book.objects.filter(category_id=category_id)
    
    def resolve_user_loans(self, info, user_id):
        return BookLoan.objects.filter(user_id=user_id)

class CreateBook(graphene.Mutation):
    class Arguments:
        title = graphene.String(required=True)
        isbn = graphene.String(required=True)
        description = graphene.String()
        author_id = graphene.Int(required=True)
        category_id = graphene.Int()
        publication_date = graphene.Date(required=True)
        price = graphene.Decimal(required=True)
        stock = graphene.Int(required=True)
    
    book = graphene.Field(BookType)
    
    def mutate(self, info, **kwargs):
        book = Book.objects.create(**kwargs)
        return CreateBook(book=book)

class CreateBookLoan(graphene.Mutation):
    class Arguments:
        book_id = graphene.Int(required=True)
        user_id = graphene.Int(required=True)
        due_date = graphene.DateTime(required=True)
    
    book_loan = graphene.Field(BookLoanType)
    
    def mutate(self, info, book_id, user_id, due_date):
        book = Book.objects.get(pk=book_id)
        user = User.objects.get(pk=user_id)
        
        if book.stock <= 0:
            raise graphene.GraphQLError("Book is out of stock")
        
        book_loan = BookLoan.objects.create(
            book=book,
            user=user,
            due_date=due_date
        )
        
        book.stock -= 1
        book.save()
        
        return CreateBookLoan(book_loan=book_loan)

class ReturnBook(graphene.Mutation):
    class Arguments:
        loan_id = graphene.Int(required=True)
    
    book_loan = graphene.Field(BookLoanType)
    
    def mutate(self, info, loan_id):
        from django.utils import timezone
        
        book_loan = BookLoan.objects.get(pk=loan_id)
        book_loan.returned_date = timezone.now()
        book_loan.status = 'RETURNED'
        book_loan.save()
        
        book = book_loan.book
        book.stock += 1
        book.save()
        
        return ReturnBook(book_loan=book_loan)

class Mutation(graphene.ObjectType):
    create_book = CreateBook.Field()
    create_book_loan = CreateBookLoan.Field()
    return_book = ReturnBook.Field()

schema = graphene.Schema(query=Query, mutation=Mutation)

3.3 URL配置

python 复制代码
# library_project/urls.py
from django.contrib import admin
from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
]

四、前端实现

4.1 Apollo Client配置

javascript 复制代码
// src/apollo.js
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';

const httpLink = createHttpLink({
  uri: 'http://localhost:8000/graphql/',
});

const client = new ApolloClient({
  link: httpLink,
  cache: new InMemoryCache()
});

export default client;

4.2 图书列表组件

javascript 复制代码
// src/components/BookList.js
import React from 'react';
import { useQuery, gql } from '@apollo/client';
import { Table, Tag, Space } from 'antd';

const GET_BOOKS = gql`
  query GetBooks {
    allBooks {
      id
      title
      isbn
      author {
        name
      }
      category {
        name
      }
      price
      stock
    }
  }
`;

const BookList = () => {
  const { loading, error, data } = useQuery(GET_BOOKS);

  const columns = [
    {
      title: 'Title',
      dataIndex: 'title',
      key: 'title',
    },
    {
      title: 'Author',
      dataIndex: ['author', 'name'],
      key: 'author',
    },
    {
      title: 'Category',
      dataIndex: ['category', 'name'],
      key: 'category',
    },
    {
      title: 'Price',
      dataIndex: 'price',
      key: 'price',
      render: (price) => `$${price}`,
    },
    {
      title: 'Stock',
      dataIndex: 'stock',
      key: 'stock',
      render: (stock) => (
        <Tag color={stock > 0 ? 'green' : 'red'}>
          {stock > 0 ? 'In Stock' : 'Out of Stock'}
        </Tag>
      ),
    },
  ];

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return (
    <Table 
      columns={columns} 
      dataSource={data.allBooks} 
      rowKey="id"
    />
  );
};

export default BookList;

4.3 借书表单组件

javascript 复制代码
// src/components/LoanBookForm.js
import React from 'react';
import { Form, Input, DatePicker, Button, message } from 'antd';
import { useMutation, gql } from '@apollo/client';

const CREATE_LOAN = gql`
  mutation CreateBookLoan($bookId: Int!, $userId: Int!, $dueDate: DateTime!) {
    createBookLoan(bookId: $bookId, userId: $userId, dueDate: $dueDate) {
      bookLoan {
        id
        borrowedDate
        dueDate
        status
      }
    }
  }
`;

const LoanBookForm = ({ bookId }) => {
  const [createLoan] = useMutation(CREATE_LOAN);
  const [form] = Form.useForm();

  const onFinish = async (values) => {
    try {
      const { data } = await createLoan({
        variables: {
          bookId: bookId,
          userId: values.userId,
          dueDate: values.dueDate.toISOString(),
        },
      });

      message.success('Book loan created successfully!');
      form.resetFields();
    } catch (error) {
      message.error('Failed to create book loan');
    }
  };

  return (
    <Form
      form={form}
      layout="vertical"
      onFinish={onFinish}
    >
      <Form.Item
        name="userId"
        label="User ID"
        rules={[{ required: true, message: 'Please input user ID!' }]}
      >
        <Input type="number" />
      </Form.Item>

      <Form.Item
        name="dueDate"
        label="Due Date"
        rules={[{ required: true, message: 'Please select due date!' }]}
      >
        <DatePicker showTime />
      </Form.Item>

      <Form.Item>
        <Button type="primary" htmlType="submit">
          Create Loan
        </Button>
      </Form.Item>
    </Form>
  );
};

export default LoanBookForm;

五、系统流程图

六、关键功能实现细节

6.1 图书库存管理

python 复制代码
# books/services.py
from django.db import transaction
from django.core.exceptions import ValidationError
from .models import Book, BookLoan

class BookInventoryService:
    @staticmethod
    @transaction.atomic
    def check_out_book(book_id, user_id, due_date):
        """
        处理图书借出业务逻辑
        """
        try:
            book = Book.objects.select_for_update().get(pk=book_id)
            
            if book.stock <= 0:
                raise ValidationError("Book is not available for checkout")
                
            book_loan = BookLoan.objects.create(
                book_id=book_id,
                user_id=user_id,
                due_date=due_date
            )
            
            book.stock -= 1
            book.save()
            
            return book_loan
            
        except Book.DoesNotExist:
            raise ValidationError("Book not found")
            
    @staticmethod
    @transaction.atomic
    def return_book(loan_id):
        """
        处理图书归还业务逻辑
        """
        try:
            loan = BookLoan.objects.select_related('book').get(pk=loan_id)
            
            if loan.status == 'RETURNED':
                raise ValidationError("Book already returned")
                
            loan.mark_as_returned()
            
            book = loan.book
            book.stock += 1
            book.save()
            
            return loan
            
        except BookLoan.DoesNotExist:
            raise ValidationError("Loan record not found")

6.2 图书搜索功能

python 复制代码
# books/schema.py

class Query(graphene.ObjectType):
    # ... 其他查询
    
    search_books = graphene.List(
        BookType,
        search_term=graphene.String(required=True),
        category_id=graphene.Int(),
        min_price=graphene.Float(),
        max_price=graphene.Float(),
    )
    
    def resolve_search_books(self, info, search_term, category_id=None, 
                           min_price=None, max_price=None):
        queryset = Book.objects.all()
        
        # 基本搜索
        queryset = queryset.filter(
            Q(title__icontains=search_term) |
            Q(description__icontains=search_term) |
            Q(author__name__icontains=search_term)
        )
        
        # 分类过滤
        if category_id:
            queryset = queryset.filter(category_id=category_id)
        
        # 价格范围过滤
        if min_price is not None:
            queryset = queryset.filter(price__gte=min_price)
        if max_price is not None:
            queryset = queryset.filter(price__lte=max_price)
            
        return queryset

6.3 前端搜索组件

javascript 复制代码
// src/components/BookSearch.js
import React, { useState } from 'react';
import { Input, Select, Form, Button, Card, List } from 'antd';
import { useQuery, gql } from '@apollo/client';

const { Option } = Select;

const SEARCH_BOOKS = gql`
  query SearchBooks($searchTerm: String!, $categoryId: Int, $minPrice: Float, $maxPrice: Float) {
    searchBooks(
      searchTerm: $searchTerm
      categoryId: $categoryId
      minPrice: $minPrice
      maxPrice: $maxPrice
    ) {
      id
      title
      author {
        name
      }
      category {
        name
      }
      price
      stock
    }
  }
`;

const GET_CATEGORIES = gql`
  query GetCategories {
    allCategories {
      id
      name
    }
  }
`;

const BookSearch = () => {
  const [searchParams, setSearchParams] = useState({
    searchTerm: '',
    categoryId: null,
    minPrice: null,
    maxPrice: null
  });

  const { data: categoriesData } = useQuery(GET_CATEGORIES);
  const { loading, error, data } = useQuery(SEARCH_BOOKS, {
    variables: searchParams,
    skip: !searchParams.searchTerm
  });

  const onFinish = (values) => {
    setSearchParams({
      ...values,
      categoryId: values.categoryId ? parseInt(values.categoryId) : null
    });
  };

  return (
    <div>
      <Form
        layout="inline"
        onFinish={onFinish}
        style={{ marginBottom: 24 }}
      >
        <Form.Item name="searchTerm">
          <Input placeholder="Search books..." />
        </Form.Item>

        <Form.Item name="categoryId">
          <Select placeholder="Select category" allowClear style={{ width: 200 }}>
            {categoriesData?.allCategories.map(category => (
              <Option key={category.id} value={category.id}>
                {category.name}
              </Option>
            ))}
          </Select>
        </Form.Item>

        <Form.Item name="minPrice">
          <Input type="number" placeholder="Min price" />
        </Form.Item>

        <Form.Item name="maxPrice">
          <Input type="number" placeholder="Max price" />
        </Form.Item>

        <Form.Item>
          <Button type="primary" htmlType="submit">
            Search
          </Button>
        </Form.Item>
      </Form>

      {loading && <p>Searching...</p>}
      {error && <p>Error: {error.message}</p>}
      
      {data && (
        <List
          grid={{ gutter: 16, column: 3 }}
          dataSource={data.searchBooks}
          renderItem={book => (
            <List.Item>
              <Card title={book.title}>
                <p>Author: {book.author.name}</p>
                <p>Category: {book.category.name}</p>
                <p>Price: ${book.price}</p>
                <p>Stock: {book.stock}</p>
              </Card>
            </List.Item>
          )}
        />
      )}
    </div>
  );
};

export default BookSearch;

6.4 系统配置

python 复制代码
# settings.py 相关配置

INSTALLED_APPS = [
    # ...
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'graphene_django',
    'corsheaders',
    'books',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    # ... 其他中间件
]

# GraphQL设置
GRAPHENE = {
    'SCHEMA': 'books.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],
}

# CORS设置
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
]

# 文件上传设置
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

# 数据库设置
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'library_db',
        'USER': 'postgres',
        'PASSWORD': 'your_password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

七、部署和运维建议

  1. 数据库优化

    • 为常用查询字段添加索引
    • 配置适当的连接池大小
    • 定期进行数据库维护和备份
  2. 缓存策略

    • 使用Redis缓存热门图书数据
    • 实现图书封面图片的CDN缓存
    • 合理设置缓存失效时间
  3. 性能优化

    • 使用Django的select_related和prefetch_related优化查询
    • 实现分页加载以提高响应速度
    • 合理使用数据库事务确保数据一致性
  4. 安全措施

    • 实现JWT认证
    • 添加请求频率限制
    • 定期更新依赖包
    • 实施SQL注入防护

八、测试用例

python 复制代码
# books/tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta
from .models import Book, Author, Category, BookLoan
from .services import BookInventoryService

class BookInventoryTests(TestCase):
    def setUp(self):
        self.author = Author.objects.create(name="Test Author")
        self.category = Category.objects.create(name="Test Category")
        self.user = User.objects.create_user(
            username="testuser",
            password="testpass"
        )
        
        self.book = Book.objects.create(
            title="Test Book",
            isbn="1234567890123",
            description="Test Description",
            author=self.author,
            category=self.category,
            publication_date=timezone.now().date(),
            price=29.99,
            stock=1
        )
        
    def test_successful_checkout(self):
        due_date = timezone.now() + timedelta(days=14)
        loan = BookInventoryService.check_out_book(
            self.book.id,
            self.user.id,
            due_date
        )
        
        self.assertEqual(loan.status, "BORROWED")
        self.book.refresh_from_db()
        self.assertEqual(self.book.stock, 0)
        
    def test_return_book(self):
        due_date = timezone.now() + timedelta(days=14)
        loan = BookInventoryService.check_out_book(
            self.book.id,
            self.user.id,
            due_date
        )
        
        returned_loan = BookInventoryService.return_book(loan.id)
        self.assertEqual(returned_loan.status, "RETURNED")
        self.book.refresh_from_db()
        self.assertEqual(self.book.stock, 1)

九、总结

本项目综合运用了Django、GraphQL和React技术栈,实现了一个完整的图书管理系统。通过这个项目,我们实践了:

  1. Django模型设计和关系处理
  2. GraphQL API的构建和查询优化
  3. React前端组件开发和状态管理
  4. 数据库事务和并发控制
  5. 系统测试和性能优化

项目提供了基础的图书管理功能,包括图书信息管理、借阅管理、库存控制等。通过合理的架构设计和代码组织,系统具有良好的可维护性和扩展性。未来可以继续添加更多功能,如读者评论、图书推荐、统计报表等。


怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!

相关推荐
背锅浩1 分钟前
python批量删除redis key
redis·python·bootstrap
weixin_4373982118 分钟前
Elasticsearch学习(1) : 简介、索引库操作、文档操作、RestAPI、RestClient操作
java·大数据·spring boot·后端·学习·elasticsearch·全文检索
MasterNeverDown18 分钟前
spring boot controller放到那一层
java·spring boot·后端
誓约酱20 分钟前
git的基本使用
linux·运维·服务器·c++·git·后端
范纹杉想快点毕业21 分钟前
XML通过HTTP POST 请求发送到指定的 API 地址,进行数据回传
xml·c语言·开发语言·数据结构·c++·python·c#
黑客老陈23 分钟前
BaseCTF scxml 详解
开发语言·网络·python·sql·安全·web安全
Ma_si36 分钟前
python 如何调整word 文档页眉页脚
python·c#·word
Gui林1 小时前
【ROS2】☆ launch之Python
python·机器人·开源
憧憬少1 小时前
腾讯云对象存储COS自建图床并配置Obsidian自动上传
后端
AI小欧同学1 小时前
【AIGC-ChatGPT进阶提示词指令】命运之轮:一个融合神秘与智慧的对话系统设计
人工智能·后端