1. 爱心图书馆到底缺一个什么样的借阅系统
1.1 从一张手写借阅登记表说起
很多社区爱心图书馆、学校图书角,最初的管理方式就是一张A4纸登记表。读者在纸上写下姓名、联系方式、书名、借书日期,还书的时候再划掉。这个模式对几十本藏书还能对付,一旦图书超过几百本、读者超过几十个人,问题就全冒出来了:书被谁借走了查不到、一本书什么时候该还全靠管理员脑子记、有人借了半年没还也没发现、想统计哪个类型的书最受欢迎更是不可能的事。这个项目要做的,就是用Python写后端、Vue写前端,搭一套能真正跑起来的图书借阅管理系统。
我之前在帮一个社区爱心图书馆做这类系统的时候,最大的感受是:这类系统的核心需求其实非常克制,不需要什么高深算法,关键是把"谁在什么时间借走了哪本书、什么时候该还、书还剩几本可借"这几件事记录清楚、查询方便、操作顺手。技术栈选Python加Vue加Pycharm也不是赶时髦,而是这个组合在开发效率、学习资料、后期维护之间取得了很实际的平衡。
1.2 三类用户角色和他们的真实需求
先别急着写代码,把用户角色理清楚是这类系统能不能落地的关键。爱心图书馆的使用者基本可以分成三类:
- 普通读者:能注册登录、浏览藏书、搜索图书、查看详情、借书、还书、查看自己的借阅历史和逾期情况。
- 图书管理员:能处理借还书操作、录入新书、修改图书信息、查看所有读者的借阅记录、处理逾期、管理读者状态。
- 系统管理员:在管理员基础上,还能管用户权限、查看统计报表、备份数据。
从需求角度说,普通读者最在意的是"能不能快速找到我想看的书、我还有几本没还、有没有超期";管理员在意的是"借还操作够不够快、信息准不准";而作为开发人员,我们真正要解决的技术问题其实是三个:图书库存的准确性、借阅状态的正确流转、前后端数据的同步一致。
我见过不少类似项目,功能越加越多,最后变成了一个大杂烩,反而把最核心的借还流程做得一塌糊涂。做这个系统一定要记住一个原则:先把借书、还书、续借、查询这几条主干流程跑通,再考虑锦上添花的功能。
2. 技术选型分析:django还是flask,Vue在这套系统里的位置
2.1 django和flask的取舍笔记
标题里同时出现了django和flask,很多刚接触Python的朋友会纠结到底学哪个、用哪个。我在实际项目中两个都深度用过,说说我的判断。
django走的是"电池全带"路线,自带ORM、Admin后台、认证系统、表单处理、迁移工具。优点是你不需要自己去拼装太多东西,按它的MVT模式组织代码,项目结构天然清晰,安全防护(SQL注入、XSS跨站脚本)也做得比较到位。对图书借阅系统这种典型的增删改查业务,django的Admin后台几乎可以白送一个管理界面,早期非常省事。缺点是框架比较重,ORM的复杂查询一旦没用好,性能会出问题,而且它的"约定优于配置"风格,初学者有时候不太理解为什么要这么分层。
flask是轻量级微框架,核心只处理路由和请求响应,ORM、表单校验、认证这些都要自己选配。它的优势是透明、灵活,每个组件都是自己亲手装上去的,出问题了容易定位。适合中小型项目,也适合想真正理解web框架原理的人。缺点就是"自由度过高",如果项目负责人心里没有清晰的架构,很容易把代码写成一团乱麻。
以图书借阅系统这个项目为例,我个人推荐:如果你希望快速交付、还想要现成的Admin后台,选django;如果你想保持代码最小化、每个模块都自己掌控,选flask。但无论选哪个,后端的API设计思路完全是相通的。下面核心技术部分我以django为例展开,因为这些逻辑在flask里同样能用SQLAlchemy实现,差别不大。
2.2 前后端分离的通信机制:axios和RESTful API
这个项目的显著特征是"Python + Vue",也就是典型的前后端分离。Vue负责浏览器里的页面渲染和用户交互,Python后端只负责提供JSON格式的数据接口,两边通过HTTP协议通信。
Vue项目里通常用axios这个库来发请求。比如前端要获取图书列表,就往后端发一个GET /api/books/请求,后端查询数据库后返回一串JSON数据,Vue拿到数据后渲染成页面表格。整个过程的核心是我要说的RESTful API设计:把图书、读者、借阅记录都当作"资源",用HTTP方法表达操作语义。
一个最简单的登录接口,前端这样调用:
import axios from 'axios' axios.post('/api/auth/login/', { username: this.loginForm.username, password: this.loginForm.password }).then(res => { // 登录成功,保存token并跳转到首页 localStorage.setItem('token', res.data.token) this.$router.push('/') }).catch(err => { this.$message.error('用户名或密码错误') })后端在django里对应的视图可能长这样:
import json from django.contrib.auth import authenticate from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt @csrf_exempt def login(request): if request.method == 'POST': data = json.loads(request.body) user = authenticate(username=data.get('username'), password=data.get('password')) if user: token = generate_token(user) return JsonResponse({'code': 0, 'token': token, 'username': user.username}) return JsonResponse({'code': 1, 'msg': '用户名或密码错误'})看到区别没有:后端不再返回HTML页面,而是返回结构化数据。页面长什么样、按钮怎么摆,全部交给Vue去处理。这种分工让前端工程师和后端工程师可以并行开发,也方便以后做小程序、App时复用同一套后端接口。
2.3 Pycharm里的项目搭建流程
在Pycharm里搭建这个项目,我建议按下面几步来:
- 创建虚拟环境。项目打开后,在
File -> Settings -> Project -> Python Interpreter里新建一个虚拟环境,避免依赖冲突。 - 安装后端依赖。在终端执行
pip install django djangorestframework django-cors-headers pillow,如果走flask路线则是pip install flask flask-cors flask-sqlalchemy。 - 创建django项目和应用:
django-admin startproject library_system,再python manage.py startapp books。
一个常见的坑是环境变量和解释器路径搞错。很多新手在Pycharm里换了虚拟环境之后,终端还是用系统Python执行命令,结果报ModuleNotFoundError: No module named 'django',其实不是代码问题,而是解释器没切过来。Pycharm的Terminal左下角可以查看当前激活的环境,确保路径指向项目内的venv。
Vue前端项目我用命令行工具构建:npm create vue@latest或者vue create library-web,装好之后用npm run serve启动开发服务器。开发阶段前端跑在8080端口,后端跑在8000端口,两边端口不同,必然会遇到跨域问题,这个是后面联调章节要讲的第一个坑。
3. 数据库建模:把借书还书这个动作拆成可落地的数据关系
3.1 核心表结构设计
图书借阅系统的数据库设计,最核心的就是三张表:图书表、读者表、借阅记录表。很多新手在这里容易犯的错误是"一拍脑袋就建表",结果后期加需求发现根本改不动。
先说图书表。图书信息不只是书名和作者,至少还要有ISBN编号、分类、出版社、馆藏数量、当前可借数量、存放位置。可借数量这个字段非常关键,它不等于馆藏数量,因为有一部分书可能正在被借走。我习惯用total_count表示馆藏总量,available_count表示当前可借数量,每次借书扣1,还书加1,这样查询首页和列表页的时候不用实时join借阅记录表去算数量,性能好很多。
再说读者表。在这个场景里,读者和系统用户其实可以合并成一张表,加上角色字段区分是普通读者还是管理员。除了用户名密码,还需要手机号、借阅卡号、最大借阅数量、当前借阅数量、状态。这里有一个容易忽略的设计点:借阅卡号要保证唯一,很多人直接用自增id当卡号,一旦数据迁移或者合并就容易冲突。
最后是借阅记录表,这是整个系统的核心。记录里要存借书时间、应还时间、实际还书时间、续借次数、状态。特别要注意的是,应还时间最好在建记录的时候就按"借书时间+借阅周期"算好存进去,而不是等查询的时候临时计算,这样即使以后调整借阅周期规则,历史记录也不受影响。
3.2 借阅记录的状态流转
借阅状态看起来简单,但设计不好就会混乱。我把它做成一个明确的状态列表:
borrowing:在借中,还没还。returned:已归还,正常。overdue:已归还但发生了逾期。overdue_borrowing:在借且已超过应还时间。
用几个短语还是不够直观,我用一段伪代码来描述状态的迁移规则:
借书时:创建记录,状态=borrowing,应还时间=当前时间+30天,图书available_count减1 还书时: 如果当前时间 <= 应还时间:状态=returned 如果当前时间 > 应还时间:状态=overdue 图书available_count加1 记录实际还书时间 续借时: 如果状态!=borrowing:不允许 如果续借次数>=2:不允许 应还时间+=15天,续借次数加1这套规则在数据库层面还需要一个辅助:查询某个读者当前有多少本逾期未还的书,这决定了能不能继续借书。有些公益图书馆会规定有逾期未还就不能借新书,这个逻辑在借书接口的校验里体现。
3.3 用ORM建表的示范
django的ORM写起来比较直观。以借阅记录为例:
from django.db import models from django.conf import settings class BorrowRecord(models.Model): STATUS_CHOICES = ( ('borrowing', '在借中'), ('returned', '已归还'), ('overdue', '逾期归还'), ('overdue_borrowing', '逾期未还'), ) book = models.ForeignKey('books.Book', on_delete=models.CASCADE, verbose_name='图书') reader = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='读者') borrow_time = models.DateTimeField(auto_now_add=True, verbose_name='借书时间') due_time = models.DateTimeField(verbose_name='应还时间') return_time = models.DateTimeField(null=True, blank=True, verbose_name='实际还书时间') renew_count = models.IntegerField(default=0, verbose_name='续借次数') status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='borrowing', verbose_name='状态') class Meta: db_table = 'borrow_record' ordering = ['-borrow_time']用flask-SQLAlchemy的写法也差不多:
class BorrowRecord(db.Model): __tablename__ = 'borrow_record' id = db.Column(db.Integer, primary_key=True) book_id = db.Column(db.Integer, db.ForeignKey('book.id')) reader_id = db.Column(db.Integer, db.ForeignKey('user.id')) borrow_time = db.Column(db.DateTime, default=datetime.now) due_time = db.Column(db.DateTime) return_time = db.Column(db.DateTime) renew_count = db.Column(db.Integer, default=0) status = db.Column(db.String(20), default='borrowing')在Pycharm里用ORM建完表之后,记得执行python manage.py makemigrations和python manage.py migrate把模型同步到数据库。第一次做的时候很多人会漏掉迁移这一步,然后报"table books_book does not exist"的错误,这个其实是操作流程问题,不是代码问题。
4. 后端接口实现:借还书业务的核心逻辑与并发控制
4.1 登录认证和权限控制
接口设计的第一步,是把需要登录才能访问的接口和后端对应的权限控制做对。django自带的auth模块提供用户表和会话机制,但前后端分离项目里更常用的是JWT(JSON Web Token)方案。用djangorestframework-simplejwt这个库,安装配置之后,登录接口会自动返回一个token,前端在后续请求的请求头里带上Authorization: Bearer <token>,后端就能识别用户身份。
权限控制我一般用django-rest-framework的IsAuthenticated和自定义权限类:
from rest_framework.permissions import BasePermission class IsAdminUser(BasePermission): def has_permission(self, request, view): return request.user and request.user.is_staff class IsReaderUser(BasePermission): def has_permission(self, request, view): return request.user and not request.user.is_staff借书接口是读者权限,图书录入接口是管理员权限,这样在视图上标注清楚,就不会出现普通读者跑到管理后台乱改数据的问题。
4.2 借书接口的状态校验清单
借书接口是整个系统里"坑"最多的地方,因为它涉及多个条件的组合判断。我把它拆成一个清单:
- 用户必须已登录。
- 图书必须存在且状态为上架。
available_count必须大于0。- 用户当前在借数量不能超过上限(比如5本)。
- 用户不能有逾期未还的记录。
- 同一本书用户不能重复借(除非已还)。
这些校验看着琐碎,但少一个都会在生产环境出问题。比如第6条,如果不检查,用户可以在未还的情况下反复借同一本书,系统中就会出现多条在借记录,账目全乱。
对应的django视图核心逻辑如下:
from django.db import transaction from django.utils import timezone from datetime import timedelta from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @api_view(['POST']) @permission_classes([IsAuthenticated]) def borrow_book(request): book_id = request.data.get('book_id') book = Book.objects.select_for_update().get(id=book_id) # 校验图书是否可借 if book.available_count <= 0: return Response({'code': 1, 'msg': '此书暂时没有可借的库存'}) # 校验读者是否有逾期记录 has_overdue = BorrowRecord.objects.filter( reader=request.user, status='overdue_borrowing' ).exists() if has_overdue: return Response({'code': 1, 'msg': '你有逾期未还的书,请先归还'}) # 校验在借数量 borrowing_count = BorrowRecord.objects.filter( reader=request.user, status='borrowing' ).count() if borrowing_count >= 5: return Response({'code': 1, 'msg': '已达到最大借阅数量'}) with transaction.atomic(): record = BorrowRecord.objects.create( book=book, reader=request.user, due_time=timezone.now() + timedelta(days=30), status='borrowing' ) book.available_count -= 1 book.save() return Response({'code': 0, 'msg': '借书成功', 'data': {'record_id': record.id}})4.3 还书接口和逾期计算
还书接口相对简单,但有一个隐藏逻辑必须处理:判断当前时间是否超过了due_time,从而决定记录状态是正常归还还是逾期归还。很多人在这里很容易犯一个错误,就是用前端传来的时间或者用户选择的时间,正确做法是以后端服务器时间为准,不要让客户端传时间,因为客户端时间可以随意修改。
@api_view(['POST']) @permission_classes([IsAuthenticated]) def return_book(request): record_id = request.data.get('record_id') try: record = BorrowRecord.objects.select_for_update().get( id=record_id, reader=request.user, status='borrowing' ) except BorrowRecord.DoesNotExist: return Response({'code': 1, 'msg': '借阅记录不存在或已归还'}) now = timezone.now() record.return_time = now record.status = 'overdue' if now > record.due_time else 'returned' record.save() book = record.book book.available_count += 1 book.save() return Response({'code': 0, 'msg': '还书成功'})4.4 事务与行锁:防止图书库存被"借超"
这是整个模块里最容易翻车的地方。假设某本书只剩1本库存,两个读者在同一秒发起借书请求,如果没有做并发控制,两个请求都查询到available_count == 1,然后都在各自的事务里减1,最终这本书的available_count可能变成-1,数据库里出现"借超"的严重数据错误。
解决方案是在读取库存时给数据库行加锁。我上面代码里写的select_for_update()就是干这个的。它会在数据库层面锁定这一行,直到当前事务提交或回滚,其他事务必须等锁释放才能读取或修改这行数据。
为了加深理解,我补充说明一下这个锁的使用时机:必须先启动一个事务,再执行select_for_update()查询。一个常见的错误写法是前面已经查询过了,后面才想起要加锁,于是又查了一次,但两次查询之间数据已经被别的请求改掉了。正确做法是:用transaction.atomic()包裹住整个"查询库存+扣减数量+创建记录"的过程,且第一行读取就加锁。
在flask里对应的写法是使用with_for_update():
book = Book.query.filter_by(id=book_id).with_for_update().first()配合SQLAlchemy的db.session.commit()手动控制事务边界。这个点属于教科书里一句话带过、但实际开发必踩的坑,做图书类系统一定要重视。
5. Vue前端:从登录页到管理后台的完整页面链路
5.1 前端工程初始化与目录规划
Vue端我用的是Vue 3 + Vue Router + Pinia(也可以选Vuex)+ Element Plus组件库。Element Plus提供了一套现成的表格、表单、弹窗、消息提示组件,做后台管理类页面效率特别高。
在Pycharm的控制台先跑一遍初始化命令:
npm create vue@latest library-web cd library-web npm install npm install axios element-plus vue-router@4 pinia目录规划上,我习惯把所有页面组件放在src/views下,按业务模块分文件夹:
src/views/ LoginPage.vue 登录页 RegisterPage.vue 注册页 HomePage.vue 图书列表首页 BookDetailPage.vue 图书详情页 MyRecords.vue 我的借阅记录 admin/ AdminDashboard.vue 管理后台框架 BookManage.vue 图书管理 BorrowManage.vue 借还管理 ReaderManage.vue 读者管理 src/api/ request.js axios实例封装 auth.js 认证相关接口 books.js 图书相关接口 records.js 借阅记录相关接口 src/router/ index.js 路由配置,含守卫 src/store/ user.js 用户状态这种按业务模块划分的目录结构,比按组件类型划分更适合项目中期扩展。等系统做到第三四个版本要加统计报表的时候,新增一个Report.vue和reports.js就行了,不会把原来的代码搅乱。
5.2 axios封装与API对接
axios不能直接在组件里到处new实例,不然拦截器、统一错误处理根本没法维护。我用一个独立文件把axios封装成统一出口:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动附带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理业务码和登录过期 request.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) } return res }, error => { if (error.response && error.response.status === 401) { ElMessage.error('登录已过期,请重新登录') localStorage.removeItem('token') router.push('/login') } else { ElMessage.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request注意baseURL: '/api'这种写法,是把请求代理给后端,后面在Vite配置文件里要设置proxy,不然开发环境下前端8080端口请求8000端口直接跨域。Vite配置如下:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } } })这个配置意味着前端把所有/api开头的请求转发到后端8000端口,浏览器里看到的请求是同源的,跨域问题在开发阶段就绕开了。
5.3 图书检索与分页展示
图书列表页面是整个系统访问量最大的页面,我在设计上做了三点:搜索、分类筛选、分页。搜索关键字匹配书名和作者,分类筛选按图书分类下拉选择,分页用Element Plus的el-pagination组件。
Vue组件里比较关键的代码如下:
<template> <div class="book-list"> <el-input v-model="searchKeyword" placeholder="搜索书名或作者" clearable @input="handleSearch" /> <el-select v-model="selectedCategory" placeholder="按分类筛选" @change="loadBooks"> <el-option label="全部" value="" /> <el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" /> </el-select> <el-row :gutter="16"> <el-col :span="6" v-for="book in bookList" :key="book.id"> <el-card @click="$router.push(`/book/${book.id}`)"> <img :src="book.cover_url" class="book-cover" alt="封面" /> <h4>{{ book.title }}</h4> <p>{{ book.author }}</p> <p>可借数量:{{ book.available_count }} / {{ book.total_count }}</p> </el-card> </el-col> </el-row> <el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total" @current-change="loadBooks" /> </div> </template>搜索功能我这里用的是防抖:用户停止输入300毫秒后才发请求,不然每敲一个字母就打一次后端,会把接口打爆。防抖的实现不需要引入额外库,写一个简单的定时器即可:
handleSearch() { clearTimeout(this.timer) this.timer = setTimeout(() => { this.currentPage = 1 this.loadBooks() }, 300) }5.4 管理员操作面板的交互设计
管理后台的交互重点是"快"。图书管理员一天可能要处理几十次借还操作,如果每借一本书都要打开好几个页面填表单,效率会很差。我的做法是在主界面放一个"借还处理"区域,管理员输入读者借阅卡号或手机号,系统自动带出读者信息及其当前在借书籍,然后通过扫码枪或手动输入图书ISBN完成借书,还书则直接点记录列表里的"归还"按钮。
这个交互里面隐藏了一个后端接口:通过借阅卡号查读者。接口返回读者的基本信息、当前借了几本、有没有逾期。管理员借书时不需要让读者输密码,这是管理员代操作和读者自助操作的根本区别,所以在权限上要单独给一个IsAdminUser。
还书操作还有一个容易踩的细节:如果同一本书被同一个读者借了多次,归还时不能只凭图书id找记录,必须带上借阅记录id,否则系统不知道你还的是哪一次。我前端传record_id,管理员点击某条具体的借阅记录后面的"归还",这样就不会混淆。
6. 联调和部署阶段的高频问题排查实录
6.1 跨域请求被拦截
前后端分离开发时,最常遇到的报错就是浏览器控制台里出现Access to XMLHttpRequest at 'http://localhost:8000/api/...' has been blocked by CORS policy。原因很好理解:前端运行在http://localhost:8080,后端在http://localhost:8000,浏览器认为这是两个不同源的服务,出于安全策略拦截了跨域请求。
开发阶段我推荐用我在5.2节说的Vite代理方案,因为最省事。如果前端部署后和后端不在同一个域名下,就必须要后端开启CORS。django项目安装django-cors-headers,在settings.py里配置:
INSTALLED_APPS = [ ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ... ] CORS_ALLOWED_ORIGINS = [ 'http://你的前端域名', ]一个常见问题是部署到线上后忘了前端请求携带token,导致跨域预检请求失败。记得在后端配置CORS_ALLOW_HEADERS里包含authorization,否则前端请求头里带token的话,跨域会被拦。
6.2 借书日期显示差一天的时区问题
我在测试阶段发现一个诡异的现象:借书成功后,页面显示的借书日期总是比实际时间早8个小时。排查后确认是时区设置问题。django默认的TIME_ZONE是UTC,如果你的USE_TZ = True,数据库里存的是UTC时间,前端拿到后如果不做转换,直接显示就会比北京时间少8小时。
解决方案有两个,根据项目部署方式二选一:
- 项目只在国内用,直接设置
TIME_ZONE = 'Asia/Shanghai'和USE_TZ = False,django存的就是本地时间,前端拿到就直接显示,简单粗暴。 - 项目需要服务多时区用户,数据库统一存UTC(
USE_TZ = True),前端拿到时间后使用new Date(value).toLocaleString('zh-CN')这样的方法在浏览器本地时区显示。
图书借阅系统一般只在国内某个社区运行,我推荐方案一,省掉很多麻烦。但要注意,改了USE_TZ之后,原来已经写入数据库的UTC时间不会自动转换,需要跑一次数据修正脚本。
6.3 Django静态文件加载不出图片
图书封面图片加载不出来,是这类系统上线初期的高频问题。这里要分清楚两个概念:开发环境的静态文件访问,和生产环境的静态文件服务。
开发环境里,如果你在项目根目录建了media/文件夹用来存放上传的封面图片,必须在settings.py里这样配置:
MEDIA_URL = '/media/' MEDIA_ROOT = 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)很多新手只配置了MEDIA_ROOT,忘了加最后那行static(),结果图片地址404。还有个细节:模板或者前端请求图片的地址,前面一定要拼接MEDIA_URL。前后端分离项目里,前端直接请求http://后端域名/media/covers/xxx.jpg,这个地址是后端静态文件服务返回的。
生产环境里Nginx要单独配置一个location指向media目录,不能依赖django处理静态文件,这一点放到部署部分说。
6.4 上线部署:waitress、gunicorn与Nginx的配合
项目做完之后要上线,不能用开发服务器python manage.py runserver跑生产,这个服务器既慢又不安全。django项目生产环境我一般用gunicorn(Linux服务器)或waitress(Windows服务器)作为WSGI服务器。flask项目同理。
以gunicorn为例,启动命令:
gunicorn library_system.wsgi:application -b 0.0.0.0:8000 --workers 3--workers 3表示开3个工作进程,具体数量参考服务器CPU核心数,一般是2*CPU核心数+1。worker数量不是越多越好,因为每个worker都会申请数据库连接和内存。
在生产拓扑上,Nginx承担两个职责:一是反向代理,把外部80端口的请求转发给内部8000端口的gunicorn;二是托管Vue打包后的静态文件。Vue项目在本地执行npm run build后生成dist目录,把整个dist目录上传到服务器,然后Nginx配置指向它:
server { listen 80; server_name your.domain.com; # Vue静态文件 location / { root /var/www/library-web/dist; index index.html; try_files $uri $uri/ /index.html; # 前端路由history模式必须加这一行 } # 后端API接口 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 上传的图书封面 location /media/ { alias /opt/library_system/media/; } }注意try_files $uri $uri/ /index.html这一行,Vue Router如果开了history模式,刷新非首页路由时如果没有这个配置会直接404。这是个非常经典的部署坑,很多项目本地跑得好好的,一部署刷新就白屏,问题就出在这里。不想处理这个规则的话,Vue Router改用hash模式可以规避,但URL会多一个#号,不太好看。
部署踩坑之后,我一般还会写一个简单的启动脚本,把gunicorn启动、日志输出、进程守护都包进去,配合systemd做成服务。这样服务器重启之后系统能自动起来,不用每次手动敲命令,公益图书馆的志愿者也不用懂Linux命令才能重启系统。
我在实际交付这个项目之后最大的体会是:图书借阅系统虽然业务不复杂,但它把前后端分离、数据库设计、认证权限、并发控制、部署运维这些web开发的核心环节都涵盖全了,是很好的练手项目。如果你是学生或者准备转行的开发者,照着这个项目完整做一遍,比看十本教程都管用。只要把核心借还流程的数据一致性和遇到问题时的排查思路吃透,这套系统不管换成django还是flask、Vue2还是Vue3,都能从容应对。