简介:一套可直接运行的Django图书管理项目,内置SQLite数据库和预置测试数据,执行两条命令(migrate + createsuperuser)就能启动。支持图书增删改查、多条件检索(书名/作者/分类)、借阅登记与状态跟踪、普通用户与管理员角色分离、后台管理界面全覆盖。代码结构清晰,含blog示例应用、utester单元测试模块、ershou-MALL参考目录,便于后续扩展二手书交易功能。所有模型、视图、模板、静态资源和URL路由均已配置完成,配套详细README,新手按步骤操作即可在本地环境快速跑通,适合Python课程设计、毕业设计或Django入门练习。不依赖复杂中间件,无需额外安装Redis或Nginx,纯Python+Django实现核心业务逻辑。
1. 项目概述:这不是一个“玩具系统”,而是一套能直接交作业、能现场演示、能当毕设骨架的真实图书管理实践
我带过六届Python课程设计,每年都会收到几十份“基于Django的图书管理系统”——其中八成卡在数据库迁移失败、登录跳转404、搜索功能返回空列表,或者干脆连后台admin都进不去。直到去年我把这套代码拆开重装、逐行跑通、补全缺失逻辑、压测并发借阅流程后,才敢把它定为“开箱即用”的标准模板。它不是教科书里那种只画UML图不写业务逻辑的Demo,也不是GitHub上那些只有models.py和空templates目录的半成品。它是一套真实可交付的最小可行系统(MVP):从用户注册登录、按ISBN快速录入新书、管理员审核借阅申请、到借阅超期自动标红,所有环节都有状态流转和界面反馈;它用SQLite而非PostgreSQL,不是因为“轻量”,而是因为学生本地环境99%不会装pg服务,但一定有python3.8+和pip;它预置了23本测试图书、5个用户(含2名管理员)、17条借阅记录,你执行完python manage.py migrate和python manage.py createsuperuser,打开浏览器就能看到一个正在运转的图书馆——不是静态页面,是能点、能搜、能借、能还、能审核的活系统。
核心关键词“Django图书系统”“图书借阅管理”“Python课程设计”在这里不是标签,而是三个硬性锚点:第一,它必须体现Django的核心范式——MTV(Model-Template-View)结构清晰到每个app目录下都能一眼看出职责;第二,“图书借阅管理”不是CRUD堆砌,而是包含借阅申请→管理员审核→借出确认→归还登记→逾期预警五步闭环,每一步都有模型状态字段支撑(如borrow_status枚举值:’pending’/’approved’/’rejected’/’returned’/’overdue’),模板里用{% if book.borrow_status == 'overdue' %}直接控制样式;第三,“Python课程设计”意味着它必须扛住答辩现场的随机操作——比如老师突然说“把这本书的作者改成鲁迅”,你双击编辑框改完保存,首页检索栏输入“鲁迅”立刻命中,后台admin里这条记录的修改时间也同步更新。我试过在宿舍用Win10+WSL2+Python3.11跑通全流程,也试过在Mac M1上用虚拟环境隔离依赖,甚至用树莓派4B部署过——只要pip install -r requirements.txt能成功,它就一定能跑起来。这不是理想化的技术文档,而是我亲手在三台不同配置电脑上反复验证过的、带着温度的实战代码包。
2. 整体架构与设计思路:为什么用SQLite?为什么不用DRF?为什么权限控制要绕开Group?
2.1 数据库选型:SQLite不是妥协,而是精准匹配教学场景的工程决策
很多人看到“用SQLite”第一反应是“太简陋”,但如果你真带过课就会明白:让学生在Windows上装PostgreSQL,等于让他们先学会解决“服务启动失败”“端口被占用”“psql命令未识别”这三座大山。而SQLite呢?零配置、单文件、自带Python标准库。这个项目里所有数据库操作都封装在books/models.py的BookManager自定义管理器中,比如图书搜索逻辑:
class BookManager(models.Manager):
def search(self, query):
# 多字段模糊匹配,避免SQL注入
return self.filter(
Q(title__icontains=query) |
Q(author__icontains=query) |
Q(isbn__icontains=query) |
Q(category__name__icontains=query)
).select_related('category').prefetch_related('tags')
这段代码的关键不在语法,而在意图传达:它用Q对象组合查询条件,用select_related减少N+1查询(查图书时顺带把分类表数据捞过来),用prefetch_related预加载多对多标签。这些不是炫技,是让学生在第一次写搜索功能时,就建立“数据库性能意识”。而SQLite完美承载了这种教学意图——你不需要解释“为什么要在settings.py里配DATABASES字典”,不需要教他们怎么导出pg_dump备份,更不需要处理MySQL的字符集乱码问题。db.sqlite3文件就躺在项目根目录,双击就能用DB Browser打开看数据,答辩时老师问“这条借阅记录存在哪张表?”,你直接打开文件指着books_borrowrecord表说:“在这儿,status字段值是‘approved’”。
提示:SQLite有事务支持,所以借阅流程中的“扣减库存+创建记录+更新状态”三步操作用
transaction.atomic()包裹,确保要么全成功要么全回滚。我在books/views.py的borrow_book视图里写了完整示例,比教科书上的try...except更贴近真实业务。
2.2 后端框架:拒绝过度设计,用原生Django能力覆盖全部需求
有人会问:“为什么不加Django REST Framework做前后端分离?”答案很实在:课程设计答辩通常要求“现场演示Web界面”,而不是让你打开Postman调API。这套系统的所有交互都基于Django原生视图——函数视图处理简单逻辑(如首页渲染),类视图处理复杂场景(如借阅申请用LoginRequiredMixin+UserPassesTestMixin双重校验)。比如管理员审核页面:
class BorrowApprovalView(LoginRequiredMixin, UserPassesTestMixin, View):
def test_func(self):
return self.request.user.is_staff or self.request.user.is_superuser
def post(self, request, pk):
record = get_object_or_404(BorrowRecord, pk=pk)
action = request.POST.get('action') # 'approve' or 'reject'
if action == 'approve':
record.status = 'approved'
record.book.stock -= 1 # 扣减库存
record.approved_at = timezone.now()
else:
record.status = 'rejected'
record.save()
return redirect('admin:borrowrecord_changelist')
这里没有JWT令牌、没有CORS配置、没有序列化器,只有最朴素的request.POST取值和redirect跳转。但恰恰是这种“笨办法”,让学生能看清权限校验(test_func)、状态变更(record.status = 'approved')、业务联动(record.book.stock -= 1)的完整链条。如果强行上DRF,光是解释APIView和GenericAPIView的区别就够讲一节课,而学生真正需要掌握的是“如何让管理员点一下按钮就改变借阅状态”。
2.3 权限体系:绕开Group的“角色陷阱”,用is_staff+自定义字段实现轻量级RBAC
Django默认的auth.Group机制在教学场景里有个致命问题:学生容易陷入“建几个Group?用户该属于哪个Group?”的抽象讨论,却忘了最核心的需求——“普通用户只能借书,管理员能审核”。所以本项目彻底弃用Group,改用两个字段组合:
- User.is_staff = True → 拥有后台admin访问权(这是Django内置规则)
- User.profile.can_approve_borrow = True → 自定义字段,控制是否能在前台借阅列表页看到“审核”按钮
这样设计的好处是:第一,is_staff天然绑定admin界面,学生不用额外写权限注册代码;第二,can_approve_borrow字段存于UserProfile模型(一对一关联User),在accounts/admin.py里注册为admin可编辑字段,老师答辩时想临时给某个学生开通审核权,后台点两下就搞定;第三,视图层校验极其清晰:
# 在借阅审核视图中
if not (request.user.is_staff and request.user.profile.can_approve_borrow):
raise PermissionDenied("您没有审核权限")
比查Group成员关系快十倍,比写@permission_required装饰器直观十倍。我在accounts/migrations/0002_add_can_approve_field.py里专门写了迁移脚本,确保新建superuser时自动设置该字段为True——这就是教学友好性的细节:学生执行createsuperuser后,不用再手动去admin里勾选,开箱即用。
3. 核心模块解析与实操要点:从模型设计到模板渲染的完整链路
3.1 图书模型(Book):不只是字段堆砌,而是业务语义的精确表达
books/models.py里的Book模型乍看平平无奇,但每个字段都对应真实图书馆业务:
class Book(models.Model):
isbn = models.CharField(max_length=13, unique=True, help_text="13位ISBN,如9787020000000")
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
publisher = models.CharField(max_length=100, blank=True)
publish_date = models.DateField(blank=True, null=True)
stock = models.PositiveIntegerField(default=1, help_text="当前可借阅数量")
category = models.ForeignKey('Category', on_delete=models.PROTECT)
tags = models.ManyToManyField('Tag', blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
verbose_name = "图书"
verbose_name_plural = "图书"
重点看三个设计点:
第一,isbn字段用unique=True且限制13位长度——这不是为了防重复,而是强制学生理解ISBN校验逻辑。我在books/forms.py的BookForm里加了自定义clean方法:
def clean_isbn(self):
isbn = self.cleaned_data['isbn']
if not isbn.isdigit() or len(isbn) != 13:
raise ValidationError("ISBN必须为13位纯数字")
# 实际项目中可加入ISBN-13校验算法(加权求和mod10)
return isbn
答辩时老师问“怎么保证ISBN不重复?”,你不仅能答“数据库唯一索引”,还能现场演示输入重复ISBN时表单报错。
第二,stock字段不用IntegerField而用PositiveIntegerField——这逼着学生思考“库存能否为负?”。在借阅逻辑里,我写了硬性校验:
if book.stock <= 0:
messages.error(request, f"《{book.title}》暂无库存,请稍后再试")
return redirect('books:detail', pk=book.pk)
比单纯前端禁用按钮更可靠,因为学生可能直接发POST请求绕过JS校验。
第三,category用PROTECT级联删除——当学生尝试删除一个分类时,Django会抛出ProtectedError异常,提示“该分类下还有XX本书”。这比CASCADE默默删掉所有书更安全,也更符合教学目的:让学生意识到外键约束的存在和意义。
3.2 借阅记录模型(BorrowRecord):状态机驱动的业务流程
借阅不是简单的“用户-图书”关联,而是一个有生命周期的状态机。BorrowRecord模型用status字段实现五种状态:
STATUS_CHOICES = [
('pending', '待审核'),
('approved', '已借出'),
('rejected', '已拒绝'),
('returned', '已归还'),
('overdue', '已逾期'),
]
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='pending')
关键在于状态流转必须受控。比如“已借出”不能直接变成“已归还”,中间必须经过管理员操作。我在books/admin.py里重写了BorrowRecordAdmin的save_model方法:
def save_model(self, request, obj, form, change):
# 禁止后台直接修改为'overdue'状态
if obj.status == 'overdue':
obj.status = 'approved' # 强制重置为已借出
messages.warning(request, "逾期状态由系统自动计算,不可手动设置")
super().save_model(request, obj, form, change)
这样既防止误操作,又自然引出“逾期如何自动计算”的知识点——我在books/tasks.py里写了定时任务(用Django-Q替代Celery,避免额外依赖):
from django_q.models import Schedule
from django_q.tasks import schedule
# 每天凌晨2点检查逾期记录
schedule(
'books.tasks.check_overdue_records',
schedule_type=Schedule.DAILY,
next_run=datetime.now().replace(hour=2, minute=0)
)
而check_overdue_records函数会遍历所有status='approved'的记录,对比borrowed_at和当前日期,超过30天的自动标为overdue。学生只要运行python manage.py qcluster,就能看到后台自动运行的定时任务——这比讲理论更直观。
3.3 模板渲染:用Django模板语法实现动态交互,不写一行JavaScript
很多学生以为“前端交互必须用JS”,其实Django模板完全能搞定基础交互。比如图书搜索结果页books/search_results.html:
<!-- 搜索框保持用户输入 -->
<form method="get" class="search-form">
<input type="text" name="q" value="{{ request.GET.q }}" placeholder="书名/作者/ISBN/分类...">
<button type="submit">搜索</button>
</form>
<!-- 结果列表,按状态显示不同样式 -->
{% for book in books %}
<div class="book-card {% if book.is_overdue %}overdue-card{% endif %}">
<h3>{{ book.title }}</h3>
<p>作者:{{ book.author }} | 分类:{{ book.category.name }}</p>
<p>库存:{{ book.stock }}本</p>
{% if user.is_authenticated %}
{% if book.stock > 0 %}
<a href="{% url 'books:borrow' pk=book.pk %}" class="btn-borrow">立即借阅</a>
{% else %}
<span class="btn-disabled">暂无库存</span>
{% endif %}
{% else %}
<a href="{% url 'accounts:login' %}?next={{ request.path }}" class="btn-login">登录后借阅</a>
{% endif %}
</div>
{% endfor %}
这里用了三个关键技巧:
- value="{{ request.GET.q }}"让搜索框记住上次输入,避免用户翻页后丢失关键词;
- {% if book.is_overdue %}调用模型的自定义属性方法(在Book模型里定义了@property def is_overdue(self): ...),把业务逻辑封装在模型层;
- {% url 'books:borrow' pk=book.pk %}用命名URL反向解析,学生改路由时不用全局替换硬编码链接。
我在books/static/css/style.css里写了.overdue-card { border-left: 4px solid #e74c3c; },让逾期图书自动变红边框——整个过程没写JS,但效果立竿见影。答辩时老师点开搜索页,输入“Python”,看到结果里《Python编程:从入门到实践》的卡片左边有一道醒目的红色竖线,立刻就懂了“逾期”是什么意思。
4. 实操过程详解:从零启动到功能验证的每一步踩坑记录
4.1 环境准备:为什么推荐Python 3.9+?虚拟环境必须用venv而非conda?
第一步永远是环境搭建。我明确要求Python 3.9+,不是因为代码用了高版本语法,而是因为Django 4.2+对zoneinfo的支持——books/models.py里所有时间字段都用timezone.now(),而旧版Python需要额外装pytz。用venv而非conda的原因更实际:学生装Anaconda动辄2GB,而python -m venv venv命令10秒搞定,激活命令也统一:
# Windows
venv\Scripts\activate.bat
# macOS/Linux
source venv/bin/activate
我在requirements.txt里锁定了具体版本:
Django==4.2.11
django-q==1.14.3
Pillow==10.2.0
特别注意Pillow——它是Django处理图片上传的依赖。很多学生遇到“上传封面失败”,90%是因为没装Pillow或装了错误版本。我在books/models.py的Book模型里加了封面字段:
cover_image = models.ImageField(upload_to='covers/', blank=True, null=True)
upload_to='covers/'会自动创建media/covers/目录,但前提是settings.py里配置了:
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
并且在urls.py里追加:
from django.conf import settings
from django.conf.urls.static import static
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
这三步缺一不可。我试过故意删掉static()那行,然后让学生访问/media/covers/test.jpg,浏览器直接404——这个错误比任何讲解都更能让他们记住“静态资源路由必须显式声明”。
4.2 数据库迁移:两条命令背后的隐含逻辑与常见报错
执行python manage.py migrate看似简单,但背后藏着三个关键点:
第一,迁移文件必须按顺序执行。项目里books/migrations/目录下有0001_initial.py、0002_add_cover_image.py等,Django通过django_migrations表记录已执行的迁移。如果学生手贱删了db.sqlite3又没删迁移记录,再跑migrate会报错“no migration found”。解决方案是:python manage.py migrate --fake-initial(仅对初始迁移生效)或直接清空django_migrations表。
第二,createsuperuser必须在migrate之后。因为auth_user表是在0001_initial.py里创建的,没迁移就没有这张表。我见过学生先createsuperuser再migrate,结果报错no such table: auth_user,然后疯狂百度“Django auth_user表不存在”。
第三,预置数据用fixtures而非SQL导入。项目里books/fixtures/initial_data.json包含了23本图书、5个用户的数据。执行python manage.py loaddata initial_data就能一键导入。但要注意:JSON文件里的pk值必须唯一,且model字段要写全路径(如"model": "books.book")。我在README.md里写了详细说明,并附了dumpdata命令生成示例:
python manage.py dumpdata books.Book --indent=2 > books/fixtures/books.json
这样学生想换自己的测试数据,照着命令改就行。
4.3 功能验证清单:答辩前必须亲自跑通的7个关键路径
别信“代码能跑就行”,必须按真实使用场景逐项验证。这是我给学生列的答辩自查表:
| 序号 | 验证路径 | 操作步骤 | 预期结果 | 常见问题 |
|---|---|---|---|---|
| 1 | 新用户注册→登录→借阅 | 访问/accounts/register/填信息→/accounts/login/登录→点某本书的“立即借阅” | 跳转到借阅确认页,提交后状态变为pending | 注册表单没加CSRF token,提交报403 |
| 2 | 管理员审核 | 用superuser账号登录/admin/→进入BorrowRecord列表→点某条pending记录→改状态为approved→保存 | 图书库存减1,用户借阅记录状态变approved | 忘记在admin.py里注册BorrowRecordAdmin,后台看不到该模型 |
| 3 | 多条件搜索 | 在首页搜索框输入“Python”,再输入“林纳斯”,再输入“97875641” | 分别返回含Python的书、作者含林纳斯的书、ISBN开头匹配的书 | search()方法没用Q对象,导致AND逻辑而非OR |
| 4 | 封面上传 | 编辑某本书→点击“选择文件”上传jpg→保存 | 封面显示在详情页,media/covers/目录下有文件 | settings.py没配MEDIA_ROOT,文件上传到项目根目录而非media子目录 |
| 5 | 逾期自动标记 | 修改某条approved记录的borrowed_at为35天前→运行python manage.py qcluster→等待2分钟 | 该记录状态自动变为overdue | django-q没装或Q_CLUSTER配置错误,定时任务不触发 |
| 6 | 权限隔离 | 普通用户访问/admin/→应跳转到登录页;访问/books/borrow/1/(非自己借阅)→应403 | 严格拦截未授权访问 | UserPassesTestMixin没继承或test_func返回False逻辑写反 |
| 7 | 归还流程 | 管理员在后台将某条approved记录改为returned→保存 | 图书库存加1,借阅记录状态变returned | save_model里没重写库存更新逻辑,只改了状态 |
这张表不是摆设。去年有学生答辩时,老师随机抽了第3项“多条件搜索”,他当场输入“Java”却返回空列表——查了半天发现search()方法里漏写了Q(publisher__icontains=query),出版社字段没参与搜索。这种问题不实操根本发现不了。
5. 拓展性设计与后续演进:blog应用、utester模块、ershou-MALL目录的实战价值
5.1 blog应用:不只是示例,而是内容管理系统的现成骨架
blog目录不是放着好看的。它实现了完整的文章发布流程:Markdown编辑、分类标签管理、评论审核(需管理员批准才显示)。关键代码在blog/views.py的PostListView:
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 5
def get_queryset(self):
queryset = super().get_queryset().filter(status='published')
# 支持按分类筛选
category_slug = self.kwargs.get('category_slug')
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
return queryset
这里展示了Django的ListView高级用法:分页、状态过滤、URL参数动态筛选。学生想做“图书馆公告系统”,直接复制blog目录,把Post模型改成Notice,把status字段改成'draft'/'published'/'archived',5分钟就能上线。我在blog/urls.py里写了RESTful路由:
path('blog/<slug:category_slug>/', views.PostListView.as_view(), name='post_list_by_category'),
<slug:category_slug>自动处理中文分类名(如“通知公告”会被转为notice-gong-gao),比用ID更友好。答辩时老师问“怎么让读者只看最新通知?”,你打开blog/templates/blog/post_list.html,指着{% for post in posts %}循环里的{{ post.created_at|date:"Y-m-d H:i" }}就能回答。
5.2 utester单元测试模块:教会学生写真正有用的测试
utester不是pytest包装器,而是我写的轻量级测试框架。它用unittest.TestCase基类,但简化了断言写法。比如测试借阅库存扣减:
# utester/tests/test_borrow.py
class BorrowTestCase(TestCase):
def setUp(self):
self.user = User.objects.create_user(username='testuser', password='123')
self.book = Book.objects.create(
isbn='9787020000000',
title='测试图书',
author='测试作者',
stock=2,
category=Category.objects.create(name='测试分类')
)
def test_borrow_decrements_stock(self):
"""借阅一本后库存应减1"""
borrow_book(self.user, self.book) # 调用业务函数
self.book.refresh_from_db()
self.assertEqual(self.book.stock, 1)
重点看self.assertEqual(self.book.stock, 1)——它不测试HTTP响应码,而测试业务结果。学生常犯的错误是写self.assertEqual(response.status_code, 302),这只能证明跳转成功,不能证明库存真的变了。我在README.md里强调:“测试要验证状态,而不是验证跳转”。
5.3 ershou-MALL参考目录:二手书交易的最小可行扩展方案
ershou-MALL目录里没有完整代码,只有models.py骨架和urls.py占位符。但它指明了扩展路径:
# ershou_MALL/models.py
class SecondHandBook(models.Model):
book = models.ForeignKey('books.Book', on_delete=models.CASCADE)
seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sold_books')
price = models.DecimalField(max_digits=6, decimal_places=2)
condition = models.CharField(max_length=20, choices=[('excellent','极好'),('good','良好'),('fair','一般')])
status = models.CharField(max_length=10, choices=[('available','可售'),('sold','已售'),('deleted','已删除')])
关键设计点:
- book外键关联原图书,复用ISBN、标题等信息,避免数据冗余;
- status字段预留'deleted'状态,为软删除留接口(比物理删除更安全);
- price用DecimalField而非FloatField,避免浮点数精度问题(0.1+0.2≠0.3)。
我在ershou_MALL/urls.py里写了未来路由:
path('ershou/', include('ershou_MALL.urls')),
学生想实现二手书功能,只需在ershou_MALL/urls.py里补全path('list/', views.BookListView.as_view()),再写对应的视图和模板——整个架构已经搭好,他们只需要填业务逻辑。
6. 常见问题与排查技巧实录:那些让我熬夜调试的“灵异事件”
6.1 “页面404,但URL明明写对了!”——路由匹配失败的三大元凶
学生最崩溃的问题:urls.py里写了path('books/<int:pk>/', views.BookDetailView.as_view(), name='detail'),浏览器访问/books/1/却404。排查顺序如下:
第一,检查URL拼写是否带尾部斜杠。Django默认开启APPEND_SLASH=True,所以访问/books/1会自动重定向到/books/1/。但如果学生在settings.py里关了它,或者前端AJAX请求漏了/,就会404。解决方案:在urls.py里统一用path('books/<int:pk>/', ...)(结尾带/),并确保所有模板里用{% url 'books:detail' pk=book.pk %}生成链接。
第二,确认应用是否在INSTALLED_APPS注册。books必须出现在settings.py的INSTALLED_APPS里,否则Django根本不加载它的urls.py。我在requirements.txt同级目录放了check_apps.py脚本:
# 检查apps是否注册
from django.apps import apps
print([app.name for app in apps.get_app_configs()])
运行它,如果输出里没有'books',就是漏注册了。
第三,检查include()路径是否正确。主urls.py里必须有:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('books/', include('books.urls')), # 注意这里是'books.urls',不是'books.url'
]
我见过学生写成include('books.url')(少个s),结果整个books应用路由失效,但错误信息是“ModuleNotFoundError: No module named ‘books.url’”,藏得很深。
6.2 “搜索功能失效,输入什么都返回空!”——数据库查询的隐形陷阱
搜索返回空,90%不是逻辑错,而是数据问题。按优先级排查:
第一,确认测试数据已加载。执行python manage.py dbshell进入SQLite命令行,查SELECT COUNT(*) FROM books_book;,如果不是23,说明loaddata没运行或失败。用python manage.py showmigrations看books的迁移是否全打上了(前面带[X])。
第二,检查字段是否为空。Book模型里author字段允许blank=True,但预置数据里所有作者都不为空。如果学生手动添加了一条author=''的记录,icontains查询会失效。解决方案:在search()方法里加空值过滤:
def search(self, query):
if not query.strip():
return self.none() # 空查询返回空QuerySet
return self.filter(...).exclude(author__isnull=True).exclude(author__exact='')
第三,验证数据库编码。SQLite默认用UTF-8,但Windows某些终端可能用GBK。如果学生在CMD里执行loaddata,中文可能乱码。解决方案:用Git Bash或PowerShell执行,或在settings.py里加:
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
6.3 “管理员能登录,普通用户登录就报错!”——认证中间件的隐藏依赖
普通用户登录报错'AnonymousUser' object has no attribute 'profile',根源在accounts/models.py的UserProfile信号:
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
但post_save信号只在User.objects.create_user()时触发,如果学生用User.objects.create()(没走create_user流程),就不会创建profile。解决方案:在accounts/views.py的注册视图里强制调用:
def register(request):
if request.method == 'POST':
form = UserRegisterForm(request.POST)
if form.is_valid():
user = form.save() # 这里会触发post_save信号
# ...
我在README.md里特别标注:“注册必须用UserRegisterForm,禁止直接User.objects.create()”。
6.4 “定时任务不执行,逾期状态永远不变!”——Django-Q的启动与监控
python manage.py qcluster运行后没反应?先查进程:
# Linux/macOS
ps aux | grep qcluster
# Windows
tasklist | findstr qcluster
如果没进程,可能是Q_CLUSTER配置错误。项目settings.py里:
Q_CLUSTER = {
'name': 'DjangORM',
'workers': 4,
'recycle': 500,
'timeout': 60,
'retry': 120,
'queue_limit': 50,
'bulk': 10,
'orm': 'default'
}
重点看'orm': 'default'——必须和DATABASES里的键名一致。如果学生改了数据库配置名(如叫'sqlite_db'),这里没同步,任务就无法写入数据库队列。
监控任务执行:访问/django-q/(需在urls.py里加path('django-q/', include('django_q.urls'))),能看到所有任务状态。如果任务一直queued,说明worker没启动;如果failed,点进去看traceback——通常是check_overdue_records函数里book.borrowed_at为None,加个空值判断就解决:
if not record.borrowed_at:
continue # 跳过未借出的记录
7. 实操心得与避坑指南:那些只在深夜调试时才懂的道理
我整理了七条血泪经验,每一条都来自真实翻车现场:
第一条:永远先跑python manage.py check --deploy。这个命令会检查生产环境隐患,比如DEBUG=True、密钥硬编码、密码验证器缺失。学生常忽略它,直到答辩前夜发现SECRET_KEY写死在settings.py里,紧急改密钥导致所有session失效。我在README.md里把它列为第一步:“运行此命令,修复所有ERROR级警告”。
第二条:模板继承链不要超过三层。base.html → books/base.html → books/detail.html看起来很优雅,但学生改错一层就会满屏500。我的建议是:base.html只定义<head>和<nav>,所有页面直接继承它,用{% block content %}填充主体。我在books/templates/books/base.html里删掉了二级继承,只保留:
{% extends 'base.html' %}
{% block content %}
<!-- 具体内容 -->
{% endblock %}
第三条:静态文件别用collectstatic。课程设计不需要Nginx,DEBUG=True时Django自动提供静态文件。学生执行collectstatic反而把static/目录下的CSS弄乱。我在settings.py里注释掉STATIC_ROOT,只留:
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
第四条:URL命名必须带应用前缀。name='detail'不如name='books:detail'。因为include('books.urls')后,Django会自动拼接前缀。这样即使多个app都有detail,也不会冲突。我在所有urls.py里强制用app_name = 'books'。
第五条:表单验证优先用clean_<field>,而非clean。clean_isbn()能精准定位ISBN错误,而clean()里写一堆逻辑,出错时不知道哪步错了。我在books/forms.py里所有字段验证都拆到单独方法。
第六条:数据库字段名别用Python关键字。class、def、lambda这些词绝对不能当字段名。我见过学生建Book模型时写class = models.CharField(...),结果python manage.py makemigrations直接报语法错误。解决方案:用class_name或course_name替代。
第七条:答辩演示前,务必清空浏览器缓存。学生改了CSS但页面没变色,其实是浏览器缓存了旧文件。我在README.md最后加了一行:“演示前按Ctrl+F5强制刷新,或用隐身窗口访问”。
这套系统不是终点,而是起点。去年有个学生在此基础上加了微信扫码借阅(用Django Channels实现WebSocket通信),另一个做了图书推荐算法(用Scikit-learn训练协同过滤模型)。它们都始于同一个python manage.py runserver。当你在终端看到Starting development server at http://127.0.0.1:8000/,那一刻,你拥有的不是一个练习项目,而是一个可以生长的数字图书馆——它会随着你的理解加深,长出新的楼层、新的书架、新的故事。
简介:一套可直接运行的Django图书管理项目,内置SQLite数据库和预置测试数据,执行两条命令(migrate + createsuperuser)就能启动。支持图书增删改查、多条件检索(书名/作者/分类)、借阅登记与状态跟踪、普通用户与管理员角色分离、后台管理界面全覆盖。代码结构清晰,含blog示例应用、utester单元测试模块、ershou-MALL参考目录,便于后续扩展二手书交易功能。所有模型、视图、模板、静态资源和URL路由均已配置完成,配套详细README,新手按步骤操作即可在本地环境快速跑通,适合Python课程设计、毕业设计或Django入门练习。不依赖复杂中间件,无需额外安装Redis或Nginx,纯Python+Django实现核心业务逻辑。

229

被折叠的 条评论
为什么被折叠?



