Django 实操图书管理系统
一、学习目标
今天是Django框架学习的最后一天,我们将通过实现一个完整的图书管理系统来综合运用之前学习的知识。本项目将包含:
- 设计并实现图书相关模型
- 使用GraphQL构建API接口
- 开发前端页面并与后端集成
二、系统架构
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',
}
}
七、部署和运维建议
-
数据库优化
- 为常用查询字段添加索引
- 配置适当的连接池大小
- 定期进行数据库维护和备份
-
缓存策略
- 使用Redis缓存热门图书数据
- 实现图书封面图片的CDN缓存
- 合理设置缓存失效时间
-
性能优化
- 使用Django的select_related和prefetch_related优化查询
- 实现分页加载以提高响应速度
- 合理使用数据库事务确保数据一致性
-
安全措施
- 实现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技术栈,实现了一个完整的图书管理系统。通过这个项目,我们实践了:
- Django模型设计和关系处理
- GraphQL API的构建和查询优化
- React前端组件开发和状态管理
- 数据库事务和并发控制
- 系统测试和性能优化
项目提供了基础的图书管理功能,包括图书信息管理、借阅管理、库存控制等。通过合理的架构设计和代码组织,系统具有良好的可维护性和扩展性。未来可以继续添加更多功能,如读者评论、图书推荐、统计报表等。
怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!