我第一次拿到“悦读圈图书共享系统”这个题目时,心里想的是:这不就是图书借阅的增删改查吗?真正动手梳理需求后才发现完全不是这么回事。这个题目最考验人的地方在于,它把共享、借阅、捐赠三条业务线压进了同一个系统里,而Django后端和微信小程序之间还隔着身份认证、状态同步、ISBN图书规范化等一系列看不见的工作量。
后来我按毕业设计答辩的标准把整套系统做完,从需求梳理、后端设计、小程序开发到联调部署前前后后用了两周多。这篇就把过程中真正有价值的部分写透,重点涉及Django借阅状态机设计、事务并发控制、扫码借书的完整链路,以及联调阶段那些能把人逼疯的怪问题。适合正在做图书共享/借阅/捐赠类毕设,或者想快速上手Django加微信小程序全栈项目的同学参考。
1. “悦读圈”这类题目,真正要解决的是什么需求
很多同学看到这类题目,第一反应是打开Django admin,建一张Book表然后开始写增删改查。等被指导老师问一句“共享和借阅有什么区别”“捐赠之后这本书归谁”,现场多半会卡壳。先把业务想清楚,比先写代码重要得多。
1.1 三种角色、三条业务链,别把它们当成一张表
一个完整的图书共享系统里,最基础的角色有三种:
- 普通用户:可以上传自己闲置的书,也可以借阅别人共享的书,还能提交捐赠意向。
- 平台管理方:审核捐赠、处理异常订单、维护书目数据,通常就是你自己或者图书馆管理员。
- 系统游客:在未登录状态下可以浏览书库,但触发借阅、收藏、评论等行为时必须登录。
对应到业务,至少有三条不能互相混淆的链路:
- 共享借阅链:用户上架图书,其他用户浏览检索并发起借阅,借阅到期后归还,书重新回到可借状态。
- 捐赠处理链:用户提交捐赠申请,管理员审核图书品相和信息,通过后录入库存(所有权转移给平台),不通过则退回。
- 轻社区链:悦读圈这个“圈”字暗示了社区属性,比如对某本书写短评、点赞、收藏某个书单。但如果项目重心是共享和借阅,社区功能要做到足够轻,别把用户评价、消息通知、帖子管理都堆进来,否则工作量会失控。
我见过不少项目把用户上传的书、平台库存、捐赠入库的书全塞在同一张表里,最后统计数据时根本分不清哪些是用户共享的、哪些是平台资产。为了避免这种混乱,数据模型上一定要把“书目信息”和“实体书副本”分开。这个我会在第2章详细展开。
1.2 从找书到还书的完整故事线
我习惯在写代码前,先把一条完整的用户故事线画出来,这样后面建表、写接口都不会漏字段。一个最基本的共享借阅流程是这样的:
- 用户A登录小程序,给一本闲置书拍照、填书名作者ISBN等信息,提交上架。
- 用户B在首页或书库浏览,通过搜索、分类或扫码找到这本书,看到状态是“可借”。
- 用户B点击借阅,系统生成一条借阅预约记录,书本状态从“可借”变成“待取/被预约”。
- 两人线下见面或通过平台约定取书方式,B确认取走,状态变为“借出中”。
- 阅读一段时间后B归还,状态变为“已归还”,A可以选择重新上架或转为捐赠。
这里的关键点是:图书的“物理状态”和“借阅记录的流转状态”是两个维度,必须在设计初期分清。物理状态描述这本书当前是否在架、是否被别人拿走;流转状态描述一条借阅申请目前处于哪个阶段。把这两个概念混在一起,到写查询逻辑时一定会乱。
1.3 为什么“圈的属性”建议后置
有些同学看到悦读圈,第一反应是做一个像论坛一样的书评社区,花大量时间做发帖、回帖、关注、私信,结果核心的图书流通逻辑反而做得粗糙。我的建议是,在B端和C端核心闭环没跑通之前,先不要做社区。
“圈”的轻量替代方案有很多,最实用的是给每本书加一个短评字段,用户归还后可以填写阅读体验,管理员审核后展示在图书详情页。这样既有“圈”的味道,又不破坏系统主结构。等共享借阅捐赠全部跑通,还有余力时,再去加独立书评模块也不迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Django后端骨架:数据模型、事务与借阅状态机
后端是整套系统的心脏。选Django的好处是ORM、Admin后台、认证体系都是现成的,一个学生项目两三天就能把基本架子搭起来。但架子搭得快不代表逻辑做得深,这一章会重点讲最容易被忽视的模型设计和状态流转。
2.1 App拆分与数据库选型
按照功能边界,我建议至少拆成下面几个App,不要所有模型堆在同一个models.py里:
- users:用户模型,继承Django的AbstractUser。
- books:书目信息、实体书副本、图书分类。
- borrow:借阅记录、预约记录、归还记录。
- donation:捐赠申请单、审核记录。
- community:轻量书评、点赞等(可视进度决定)。
数据库如果只是做毕设或者演示,直接使用Django默认的SQLite就够了,迁移、备份、部署几乎零成本。如果老师明确要求MySQL,也建议开发阶段先用SQLite,最后上线前再切换数据库并重新跑一遍迁移。为了切换顺畅,模型层千万不要写某个数据库特有的字段类型。
框架方面,纯接口用Django原生视图加JsonResponse反而最可控,适合规模不大的项目。DRF虽然能省不少序列化代码,但对新手来说要额外理解Serializer、ViewSet、权限类这些概念,如果时间紧容易踩坑。我最终采用了原生视图加模块化的service函数,接口逻辑清楚,答辩时也更好讲。
2.2 核心模型字段设计:书目与副本分离
前面提到的“书目信息”和“实体书副本”分离,落地成代码大概是这样的:
python复制# books/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Category(models.Model):
name = models.CharField(max_length=32, unique=True)
class BookInfo(models.Model):
# 一本“书”,以ISBN或书名作者去重
isbn = models.CharField(max_length=20, db_index=True, blank=True, default='')
title = models.CharField(max_length=128)
author = models.CharField(max_length=64, blank=True, default='')
publisher = models.CharField(max_length=64, blank=True, default='')
cover = models.URLField(blank=True, default='')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
python复制# books/models.py
class BookCopy(models.Model):
# 一本用户手上的“实体书”
book_info = models.ForeignKey(BookInfo, on_delete=models.CASCADE, related_name='copies')
owner = models.ForeignKey(User, on_delete=models.PROTECT, related_name='owned_copies')
status = models.CharField(max_length=24, default='published')
borrow_count = models.IntegerField(default=0)
remark = models.CharField(max_length=255, blank=True, default='') # 品相备注
created_at = models.DateTimeField(auto_now_add=True)
为什么要这样做?因为同一本《活着》,可能同时有5个用户愿意共享,每个人手头的书是独立的一个可借副本。如果直接把书名当主键,就只能支持一个所有者,整个“共享”概念就崩了。BookInfo负责存“这本书长什么样”,BookCopy负责存“谁手头有这本书、现在能不能借”。
owner字段用on_delete=models.PROTECT,是很重要的细节。如果某个测试用户被删,名下还有共享书副本时会阻止删除,提醒你先处理这些书的归属,避免历史借阅记录跟着变成孤儿数据。这个坑会在第5章详细复盘。
2.3 借阅状态机:一本书的一生
图书副本的status字段,我建议用一组固定状态控制,不要在代码里到处写魔法字符串。可以定义成模块级常量:
python复制# books/constants.py
COPY_PUBLISHED = 'published' # 可借
COPY_PENDING = 'pending' # 已被预约,等待取书
COPY_BORROWED = 'borrowed' # 借出中
COPY_OVERDUE = 'overdue' # 逾期未还
COPY_RETURNED = 'returned' # 最近一次已归还
COPY_OFF_SHELF = 'off_shelf' # 下架/暂停共享
COPY_LOST = 'lost' # 丢失或损坏
这些状态之间不是随便互转的,必须有明确约束。下面是我实际使用的流转表:
| 当前状态 | 可流转到 | 触发动作 |
|---|---|---|
| published | pending / off_shelf | 用户申请借阅 / 所有者下架 |
| pending | published / borrowed | 预约取消 / 借阅者确认取走 |
| borrowed | overdue / returned / lost | 超过应还时间 / 用户归还 / 上报异常 |
| overdue | returned / lost | 补还 / 上报丢失 |
| returned | published / donated | 所有者重新上架 / 转入捐赠 |
| off_shelf | published | 所有者重新上架 |
这个状态机写清楚后,后端每个接口只需要关心“当前状态能不能走到目标状态”,逻辑简单,接口也不会被滥用。比如有人想直接对一本“借出中”的书再次发起借阅,状态机这一层就会拦截,不用再写一堆if判断。
借阅记录同样有状态,但它的状态和图书副本状态是两码事。借阅记录包含:申请中、待取书、借阅中、已逾期、已归还、已取消。为了保留完整历史,每本书被借一次,就生成一条新记录,不应删除旧记录去覆盖。
2.4 借阅与归还:事务边界和并发控制
图书共享系统并发量不大,但“两个人同时借同一本书”这种竞争条件是真实存在的。如果不在后端处理,可能出现两人都看到“可借”,也都提交成功,最后书的实际状态和记录对不上。
解决思路是在Django中使用select_for_update对图书副本加行锁:
python复制from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from books.models import BookCopy
from borrow.models import BorrowRecord
@csrf_exempt
@transaction.atomic
def apply_borrow(request, copy_id):
if not request.user.is_authenticated:
return JsonResponse({'code': 403, 'msg': '请先登录'})
copy = BookCopy.objects.select_for_update().get(pk=copy_id)
if copy.status != 'published':
return JsonResponse({'code': 400, 'msg': '当前状态不可借阅'})
overdue_records = BorrowRecord.objects.filter(
copy=copy,
user=request.user,
status__in=['borrowing', 'overdue']
)
if overdue_records.exists():
return JsonResponse({'code': 400, 'msg': '你有未归还记录,暂不能继续借阅'})
copy.status = 'pending'
copy.save(update_fields=['status', 'updated_at'])
record = BorrowRecord.objects.create(
copy=copy,
user=request.user,
status='pending',
apply_time=timezone.now()
)
return JsonResponse({'code': 0, 'data': {'record_id': record.id}})
select_for_update加在事务里,才能保证在事务提交前,其他请求无法读取或修改这一行。同时判断用户是否有逾期未还记录,一个人欠着书不还还继续借,平台规则上就说不过去。
还书逻辑也有一个很容易忽略的点:借阅者归还后,副本状态不能无脑置为“可借”,而是先置为“returned”,由所有者确认书况后,再手动点击“重新上架”变为published。这个确认步骤虽然多了一次操作,但能避免归还的书已经破损却还在共享池里流转的问题。
3. 扫码找书与图书信息规范化:ISBN的一本正经玩法
图书系统和普通商品系统有一个非常大的区别:普通商品可以用自增ID标识,但图书天然有国际标准书号ISBN。如果系统不支持ISBN,每个用户手动输书名作者出版社,数据质量会迅速恶化到没法看。这一章讲讲我如何把扫码和ISBN融入全流程。
3.1 前端实现:wx.scanCode 到书目详情
小程序端扫码非常直接,调用微信提供的扫一扫能力就行:
javascript复制// pages/scan/scan.js
scanBook() {
wx.scanCode({
onlyFromCamera: true,
success: (res) => {
const isbn = res.result;
if (!/^[0-9]{10,13}$/.test(isbn.replace(/-/g, ''))) {
wx.showToast({ title: '无法识别的图书条码', icon: 'none' });
return;
}
wx.navigateTo({
url: `/pages/book/detail?isbn=${isbn}`
});
},
fail: () => {
wx.showToast({ title: '已取消扫码', icon: 'none' });
}
});
}
这里建议用onlyFromCamera: true,因为相册识别条码的体验不稳定,尤其翻拍书背条码时经常失败。另外在首页底部TabBar中放一个“扫一扫”入口,路由跳转不能在tabBar页面用navigateTo,需要切换tab或跳转到非tab页面,这个细节很容易让新手卡住。
3.2 后端处理ISBN的靠谱方案
小程序把ISBN传给后端后,后端要做的第一件事是清洗数据。条码扫描结果里可能带横杠、空格、字母X,需要统一处理:
python复制import re
def normalize_isbn(raw):
code = re.sub(r'[^0-9Xx]', '', raw.strip())
if len(code) == 10:
# ISBN-10 转 ISBN-13,大多数图书信息接口只认13位
if code.endswith(('X', 'x')):
code = code[:-1] + '0'
core = '978' + code[:9]
check = 0
for i, ch in enumerate(core):
if i % 2 == 0:
check += int(ch)
else:
check += int(ch) * 3
code = core + str((10 - check % 10) % 10)
return code
拿到13位ISBN后,优先查本地BookInfo表。查不到时,可以尝试通过开放图书数据接口补全书名作者封面。但外部接口不可能一直稳定,一旦失败,要给用户一个“手动补全信息”的兜底入口,别让扫码流程成为死胡同。
我在设计里把“手动录入”做成了表单弹层,扫码识别失败或者书太老查不到数据时,用户可以直接拍照上传封面,再自己填书名ISBN作者,至少管理员审核时还能介入修正。
3.3 一书多副本的库存逻辑
基于第2章的BookInfo和BookCopy分离,扫码之后实际发生的是:先按ISBN找到或创建BookInfo,再在该BookInfo下生成一条属于当前用户的BookCopy。
这就回到共享系统的核心体验:一本书详情页能展示“全书库有几本可借”“有几本已经借出”,同时让用户只对标记为published的副本发起借阅请求。查询逻辑大概是:
python复制book_list = BookInfo.objects.filter(
title__icontains=keyword
).annotate(
available_count=Count('copies', filter=Q(copies__status='published'))
).order_by('-created_at')
如果某些书有多个副本,用户看到的是同一本BookInfo下有多条可借副本。归还时也只需要锁定某一条BookCopy,不牵动整个书目。这套逻辑写顺之后,后面的统计报表也简单很多,每个书目的共享次数等于其下所有副本的borrow_count总和。
4. 前端小程序和后端联调前必须处理好的三件小事
很多项目死在了接口联调阶段,而不是功能实现阶段。因为Django默认的后台模板和Admin太好用了,给人“系统已经跑通”的错觉,但小程序端一发请求,全是跨域、登录态、字段格式问题。这一章分享三个我提前处理好之后少走很多弯路的点。
4.1 登录态:wx.login 换 token 的完整链路
小程序不能直接使用用户名密码登录,微信生态要求的流程是:
- 前端调用wx.login获得一个临时code。
- 后端拿着code去微信接口换openid和session_key。
- 后端用openid找到或创建用户,并签发给小程序一个自有的token。
- 小程序后续所有请求都在header里带这个token。
后端不保存session_key会更安全,只把openid作为用户唯一标识。token建议存一张表,包含token值、对应用户、过期时间,简单可控:
python复制# users/models.py
class UserToken(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens')
token = models.CharField(max_length=64, unique=True, db_index=True)
expire_at = models.DateTimeField()
created_at = models.DateTimeField(auto_now_add=True)
签发token时用secrets.token_hex(32),不要把微信AppSecret暴露给前端,也不要把openid直接写进返回结果。小程序端请求登录接口后把token存到wx.storage,请求拦截器统一添加Authorization头,后端写一个简单的登录装饰器做校验。
4.2 wx.request 的统一封装与错误处理
不建议每个页面都直接调wx.request,那样一旦token过期或后端返回固定错误码,改起来会疯。先把请求封装成一个模块:
javascript复制// utils/request.js
const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.request({
url: getApp().globalData.baseUrl + url,
method,
data,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Token ${token}` : ''
},
success: (res) => {
if (res.data && res.data.code === 0) {
resolve(res.data.data);
} else if (res.statusCode === 401) {
// token 失效时统一跳转登录
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
reject(res.data);
} else {
wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' });
reject(res.data);
}
},
fail: (err) => {
wx.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
};
Django后端返回统一格式结构,比如{code: 0, msg: 'success', data: {...}},前端处理起来会非常省心。不要有时直接返回数组、有时返回对象,前端的判断逻辑会被你逼疯。
baseUrl在开发阶段可以直接填本机局域网IP加Django端口,前提是手机和电脑在同一网络,并且Django的ALLOWED_HOSTS里加上局域网IP。正式发布时再换成自己的HTTPS域名。
4.3 图片上传与静态资源路径
图书封面和捐赠凭证图片是小程序端最核心的上传场景。前端用wx.chooseMedia选图,然后用wx.uploadFile传给Django:
javascript复制wx.chooseMedia({
count: 1,
mediaType: ['image'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFiles[0].tempFilePath;
wx.uploadFile({
url: getApp().globalData.baseUrl + '/api/upload/',
filePath: tempFilePath,
name: 'file',
header: { 'Authorization': `Token ${token}` },
success: (uploadRes) => {
const data = JSON.parse(uploadRes.data);
console.log(data.data.url);
}
});
}
});
Django端需要处理文件上传,推荐用django-cors-headers解决跨域,同时限制上传后缀和大小。图片上传后返回可访问的URL,存到对应模型的cover字段。开发阶段直接用Django自带的media服务,上线时让Nginx代理/media/路径即可。这里要提醒一句:Django的settings里MEDIA_ROOT和MEDIA_URL一定要配好,部署后忘记配静态文件和media映射,会导致所有图片变成404,演示现场会非常尴尬。
5. 我踩过的联调阶段坑:从孤儿数据到开发工具怪现象
整个项目最费时间的不是写功能,是排查各种“看起来没道理”的问题。这一章全是真实翻车记录和排查思路,每一条都能帮读者少走弯路。
5.1 Django删除对象引发的数据蒸发事故
第一个坑出现在清理测试数据时。我随手创建一个测试用户,绑定了几本共享书,然后为了清掉脏数据直接执行了删除:
python复制BookCopy.objects.filter(owner__username='test_del').delete()
结果发现不仅书没了,这批书对应的借阅历史也全部消失。原因是我早期把借阅记录的copy字段外键设成了默认的CASCADE级联删除。这在数据库层面看起来“干净”,但对图书系统来说是灾难:管理员删除一本违规书,所有用户的借阅历史全被连坐抹掉,数据统计完全失真。
正确的做法是:
- 借阅记录本质上属于流水日志,不应该物理删除。
- 图书副本如果确实需要下架,应该走状态流转,把status改成off_shelf。
- 如果必须物理删除副本,外键要改成SET_NULL,同时给记录冗余保存一份图书标题快照,防止历史记录变成无头记录。
我当时在BorrowRecord里额外加了book_title_snapshot字段,在创建记录时把BookInfo.title冗余保存一份,这样就算BookInfo后续被改,或者副本被删,借阅记录仍然能展示书名。这是很多真实电商系统里都会用的快照思路。
5.2 token过期后小程序的连环报错
第二个坑是token过期导致的前端连环报错。小程序端如果只检测单个请求失败,会出现一种很糟糕的体验:用户明明还在浏览首页,突然点开详情页时连续弹好几个“登录已过期”,页面跳转也异常。
排查后发现原因是多个页面同时并发请求,每个请求都收到401,每个请求都执行了各自的“跳到登录页”逻辑,导致页面栈被重复压入。
解决办法就是4.2节那段封装,加一个全局的isRedirecting标记:
javascript复制let isRedirecting = false;
function handleAuthFail() {
if (isRedirecting) return;
isRedirecting = true;
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
setTimeout(() => { isRedirecting = false; }, 2000);
}
另外token过期不应该只在前端拦截,Django端的认证装饰器返回401时要附带明确的错误码,比如{code: 401, msg: 'token_expired'},方便前端区分“参数错误”和“登录态失效”。
5.3 开发者工具里的身份校验与本地调试限制
第三个坑和微信开发者工具有关。如果你用HBuilderX或微信开发者工具直接运行小程序,有时会提示“当前微信号不是开发者”或者“不在开发者权限列表”。这个提示只跟小程序后台的账号权限有关,和代码一点关系都没有。
解决办法是在微信公众平台把当前测试微信号加入项目成员,并赋予开发者权限。如果只是临时调试,也可以使用测试号,或者用开发者工具中的“不使用真实AppID”模式。但要注意,测试号无法调用微信扫码、订阅消息等部分能力,所以完整功能调试还是得用真实小程序的AppID。
还有一个本地调试的大坑:wx.request默认要求请求地址是HTTPS,并且必须在公众平台配置request合法域名。开发阶段如果坚持要直接用http://localhost:8000,可以在开发者工具的“详情-本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”。这个选项只对开发者工具和真机调试的预览模式有效,上线前必须换成HTTPS正式域名。
5.4 自定义导航栏、视频组件一类的小程序细节坑
如果项目里加了“书评视频”或者首页轮播图,大概率会遇到两个问题。
第一个是iOS端swiper组件里嵌video组件,视频全屏播放时错位或无法正常旋转。这种现象的根源是swiper内部滚动容器和video原生组件的层级冲突。解决思路是不要让视频直接作为swiper-item的子元素,改成在swiper里只放封面图,点击封面后再调起一个全屏页面播放视频,绕开嵌套。
第二个是video的播放地址必须是HTTPS且在小程序后台配置了downloadFile合法域名。如果我只在request合法域名里配了域名,而视频地址没有加到downloadFile合法域名,播放会一直转圈。这个区分很多人不知道,因为普通图片走的是request或download域名,视频属于媒体资源,同样受到域名白名单限制。
自定义导航栏也是个容易翻车的点。一旦项目为了视觉统一使用了自定义顶部导航,就需要手动计算状态栏高度和胶囊按钮位置:
javascript复制const menuRect = wx.getMenuButtonBoundingClientRect();
const windowInfo = wx.getWindowInfo();
const navBarHeight = (menuRect.top - windowInfo.statusBarHeight) * 2 + menuRect.height;
这套计算代码要放在页面onLoad里获取,不要在App.onLaunch里提前获取,因为某些安卓机型上胶囊按钮信息尚未完全初始化,获取到的高度会是0。如果用的是uni-app,对应方法是uni.getMenuButtonBoundingClientRect,名称差异很容易踩坑。
6. 演示部署的准备清单,以及还能往哪儿扩展
很多学生项目在本地写得好好的,一上演示就翻车:要么老师用手机访问时接口全挂,要么图片加载不出来,要么登录状态混乱。这一章整理一份我跑完整套演示流程后沉淀下来的检查清单,按顺序过一遍,能省去现场的大半尴尬。
6.1 从开发到演示环境的完整操作清单
- Django侧的settings.py把DEBUG设为False,ALLOWED_HOSTS里加入服务器IP或域名。
- 收集静态文件到STATIC_ROOT,并确认图片上传目录已经被Web服务器正确代理。
- 后端接口全部跑一遍冒烟测试,特别是借阅、归还、捐赠审核这些有状态流转的接口,确保非法状态流转返回的是友好提示而不是500。
- 小程序的baseUrl换成线上地址,不要留在localhost或局域网IP。
- 准备一批真实的演示数据:5本以上封面完整可访问的图书,覆盖不同分类;至少一个用户有“可借”“借出中”“已逾期”的记录;一条待审核的捐赠申请;一条审核通过的捐赠记录。这样答辩演示时每个模块都有东西可点。
- 真机预览一遍,重点测试扫码借书和图片上传。开发者工具里能跑通不代表真机也能跑通,域名白名单和TLS证书的问题只有真机才会暴露。
如果暂时没有云服务器,采用本地开发环境加内网穿透工具的方式也能完成答辩演示,但一定要提前测试现场网络,并准备一个断网情况下的备用方案,比如提前录好一段完整操作视频。
6.2 安全基线上必须做的几件事
学生项目常被人忽略安全问题,但一旦被老师追问,回答不上来很减分。以下几件是最基本的:
- SECRET_KEY和微信AppSecret不要提交到任何公开仓库,用环境变量或本地配置文件加载。
- Django Admin不要用admin/admin这种组合,密码生成一个强密码。
- 上传文件要做后缀白名单校验,不能允许上传.py或.html。
- 用户密码必须使用Django内置的密码哈希机制,不要自己存明文。
- 接口返回时不要泄露其他用户的openid、手机号。比如查询借阅记录列表时,展示昵称头像即可,不需要把完整手机号返回给前端。
6.3 后续还能扩展哪些方向
项目做完之后,并不代表系统到终点就停了。如果时间充裕或者想在答辩时展示设计能力,有三个方向性价比很高。
第一个是逾期自动提醒。给借阅记录加应还时间,每天定时扫描逾期记录并把状态置为overdue,再给用户推送订阅消息提醒。因为微信订阅消息是用户主动授权一次触发一次的机制,所以提醒功能需要在借阅成功时引导用户订阅“归还提醒”,否则服务端无法在到期时主动推送。
第二个是信用分机制。把逾期、丢书、及时归还都变成信用分变量,当用户信用分低于阈值时限制借阅。这个设计能让导师看到你对平台风控有思考。
第三个是积分激励体系。用户每成功共享一本书并获得他人借阅,可获得积分,积分可以用来换取捐赠图书的优先权或线下小礼品。积分体系的好处是让“共享”这个概念真正转起来,而不只是做一个图书管理工具。
我自己的体会是,这种全栈项目最锻炼人的地方不在单个技术点,而在状态管理和业务闭环的思考。很多问题看起来是代码问题,实际是设计阶段没有把“一本书从哪来到哪去”的完整生命周期想清楚。只要你把共享、借阅、捐赠这三条链路的边界和状态流转理清了,Django和小程序只是实现工具而已。踩过一轮坑之后,再回头看最初那个“图书共享不就是增删改查”的想法,确实太天真了。
