news 2026/10/24 5:51:00

图书借阅管理系统实战:Flask+SQLite前后端分离与踩坑复盘

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
图书借阅管理系统实战:Flask+SQLite前后端分离与踩坑复盘

这周的第三次作业总算交上去了,长出一口气。如果你也在做类似的课程设计作业——尤其是图书管理系统、学生信息管理、库存管理这类Web开发作业,那这篇应该对你有用。我这次做的是某高校软件设计课程的第三个作业,题目是“图书借阅管理系统”,要求用Web方式实现,支持图书信息的增删改查、用户登录和借阅归还记录,技术栈不限。作业本身不算难,但从题目解读、技术选型到联调上线,中间坑其实不少,尤其是第一次做前后端分离的人,很容易卡在接口和跨域上。下面把我的完整思路、关键代码和踩坑记录都写出来,你们可以直接参考。

1. 题目到手之后,我为什么选了这个方案

1.1 先别急着写代码,把作业要求拆成功能清单

我拿到“图书借阅管理系统”这个题目时,第一反应是这玩意太经典了,网上一抓一大把。但经典题目反而容易被做成千篇一律的“教材代码”,所以我先花了一个小时把需求拆成功能清单,确保自己知道到底要交付什么。

课程要求原文大致是:系统要有图书信息管理、借阅者管理、借书还书操作和基本的借阅记录查询。我把它拆成了下面这个清单:

  • 用户端:登录、注册(可选)、查看图书列表、借书、还书、查看自己的借阅记录。
  • 管理端:图书的增删改查、借阅记录的查看和删除、用户列表。

这里有个很重要的点:作业要求的“图书借阅管理”,重点在“借阅流程”而不是单纯对图书表做CRUD。很多同学把功夫全花在图书的增删改查上,结果借书还书只是简单地改个字段,这样其实没有完全命中题目。所以我额外设计了借阅记录表,把每次借书、还书都留痕,这样演示给助教看的时候,才能清楚地说明“哪个用户、借了哪本书、什么时候借的、什么时候还的”。

1.2 技术选型:先求稳,再谈酷

技术栈我选了 Flask + SQLite + 原生HTML和JavaScript。没有用Vue,也没有用Spring Boot或者MySQL。原因很简单:这是一次课程作业,我只有一周多的时间,而且要考虑演示环境。

  • 后端用Flask,因为Python写起来快,路由和请求处理直观,不需要像Java那样配置一大堆东西。
  • 数据库用SQLite,免安装、单文件、随代码一起交即可。老师或助教拿到项目后不需要额外装数据库服务,跑一下就能看到结果。
  • 前端用原生JavaScript,没有用构建工具,避免node_modules和打包带来的环境问题。

有人说“技术栈太简单会不会显得没水平”,我的看法是,作业首先要保证能跑通、能演示、逻辑完整。如果时间充裕,可以在关键地方做优化,比如加缓存、加权限控制,而不是一上来就上微服务那一套。把基础功能做扎实,比堆砌一堆用不上的框架名词要重要得多。

2. 数据模型与接口设计:作业能不能跑通,90%看这一层

2.1 数据库表怎么拆:用户、图书、借阅记录的关系

我设计了三张表。很多同学习惯把用户和图书数据混在一起,或者不建借阅记录表,直接在图书表里加一个“是否被借出”的字段。但这样会丢失历史记录,比如“谁借过这本书”“还了没有”都查不到。实际开发里,业务流程的数据必须单独建表。

建表SQL如下,我直接写在schema.sql里:

CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT, isbn TEXT NOT NULL UNIQUE, title TEXT NOT NULL, author TEXT NOT NULL, publisher TEXT, total_copies INTEGER NOT NULL DEFAULT 1, available_copies INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE borrow_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, book_id INTEGER NOT NULL, borrow_time TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), return_time TEXT, status TEXT NOT NULL DEFAULT 'borrowed', FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id) );

这里有几个细节容易忽略:

  • 图书表里既有total_copies(总库存),又有available_copies(可借数量)。为什么拆分?因为同一本书可能有多本,借书时只减少可借数量,而不是把整本书删掉。如果所有图书都只存一条记录,就无法处理“同一本书有两本,其中一本被借走”的情况。
  • 借阅记录表里的status字段,我用了'borrowed'和'returned'两个字符串值。有人喜欢用0和1,但我建议用可读字符串,这样调试时看数据库一眼就明白,不需要查表说明。
  • password字段存储的是哈希后的结果,不是明文。课程作业虽然要求不高,但至少不能把明文密码放到数据库里,否则太难看。

2.2 接口路径和返回格式,照着这个规范写就行

接口设计我遵循了最基础的REST风格,统一返回格式。后端所有接口返回的都是JSON,结构如下:

{ "code": 0, "msg": "success", "data": {} }

code为0表示成功,非0表示失败。这样前端只用判断code,不用去解析各种乱七八糟的错误结构。

实际接口清单如下表:

方法路径功能是否需要登录
POST/api/login用户登录否
GET/api/books图书列表(支持分页和搜索)是
POST/api/books新增图书管理员
PUT/api/books/修改图书信息管理员
DELETE/api/books/删除图书管理员
POST/api/borrow借书是
POST/api/return还书是
GET/api/records借阅记录是

关于登录和权限,后面第三章会细说。返回格式统一非常重要,前端代码可以抽一个共用的处理函数,比如这样:

async function request(url, options) { const res = await fetch(url, options); const data = await res.json(); if (data.code !== 0) { alert(data.msg); throw new Error(data.msg); } return data.data; }

这样所有接口调用都走一个函数,不用每个请求都重复处理错误,代码干净很多。

2.3 一个容易被忽略的细节:时间字段和状态字段

时间字段我用的是SQLite自带的datetime('now', 'localtime'),这里特别注意有'localtime'和没有'localtime'的区别。如果直接用datetime('now'),存进去的是UTC时间。国内环境下差8个小时,我在调试时就遇到过:明明下午借的书,列表里显示凌晨时间,看着特别奇怪。所以建表的时候默认值一定要写'localtime'。

状态字段背后还有一个业务判断:还书接口要区分“这本书真的被这个人借过吗”。所以还书不是简单地传一个book_id,而是要把user_id和book_id一起传过来,然后在borrow_record表里找一条status='borrowed'且user_id和book_id匹配的记录,把它的status改成'returned'并写入return_time。这样既保证了流程闭环,也防止有人乱调接口把没借过的书“还”了。

3. 前后端联调的完整流程与关键代码

3.1 后端骨架怎么搭

Flask项目的目录并不复杂,我的结构是这样的:

app.py schema.sql static/ index.html style.css app.js

app.py是启动入口,Flask路由都写在里面。数据库操作我可以直接用Python内置的sqlite3模块,不需要额外的ORM。ORM虽然写起来省事,但查询逻辑不透明,作业演示时被追问“这个查询是怎么实现的”容易答不上来。直接用sqlite3反而更好解释。

核心初始化部分如下:

from flask import Flask, request, jsonify, session import sqlite3 import hashlib app = Flask(__name__) app.secret_key = 'course-work-secret-key' DATABASE = 'library.db' def get_db(): conn = sqlite3.connect(DATABASE) conn.row_factory = sqlite3.Row return conn def init_db(): with sqlite3.connect(DATABASE) as conn: with open('schema.sql', 'r', encoding='utf-8') as f: conn.executescript(f.read()) if __name__ == '__main__': init_db() app.run(debug=True, port=5000)

这里flask的session需要设置secret_key,否则session无法正常工作。课程作业里写死一个字符串足够了,但正式项目里肯定要用环境变量注入。

登录接口的实现也比较直接,密码用SHA256哈希后比对:

@app.route('/api/login', methods=['POST']) def login(): data = request.get_json() username = data.get('username') password = data.get('password') if not username or not password: return jsonify({'code': 1, 'msg': '用户名和密码不能为空'}) conn = get_db() user = conn.execute( 'SELECT * FROM user WHERE username = ?', (username,) ).fetchone() conn.close() if user and user['password'] == hashlib.sha256(password.encode()).hexdigest(): session['user_id'] = user['id'] session['username'] = user['username'] session['role'] = user['role'] return jsonify({'code': 0, 'msg': '登录成功', 'data': { 'username': user['username'], 'role': user['role'] }}) return jsonify({'code': 1, 'msg': '用户名或密码错误'})

注意这里用了SQL参数化查询,而不是把username变量直接拼进SQL字符串里。以前见过不少同学在作业里用format拼接SQL,最后被一句“xxx' OR '1'='1”把整个用户表举出来了,这个习惯哪怕是一次课程作业也要改掉。

3.2 前端页面怎么调用接口

前端我做了两个页面视图,实际上是一个index.html里两个区块:登录区和主功能区。用原生JS控制显示还是隐藏。

<div id="loginPanel"> <h2>登录</h2> <input type="text" id="username" placeholder="用户名"> <input type="password" id="password" placeholder="密码"> <button onclick="login()">登录</button> </div> <div id="mainPanel" style="display:none"> <h2>图书管理</h2> <button onclick="logout()">退出登录</button> <table id="bookTable">...</table> </div>

登录后把session中的用户信息展示出来,再把主面板显示。加载图书列表:

async function loadBooks() { const data = await request('/api/books'); const tbody = document.querySelector('#bookTable tbody'); tbody.innerHTML = ''; data.books.forEach(function(book) { const tr = document.createElement('tr'); tr.innerHTML = ` <td>${book.title}</td> <td>${book.author}</td> <td>${book.available_copies}/${book.total_copies}</td> <td> <button onclick="borrowBook(${book.id})">借书</button> </td> `; tbody.appendChild(tr); }); }

用模板字符串拼接HTML确实没有Vue的响应式那么优雅,但作业场景下完全够用。这里有一个安全性问题:如果书名等内容里包含HTML标签,会被浏览器直接解析。课程作业里的数据是自己录入的,问题不大,但我在输出时还是做了简单的escape函数,这是习惯问题。

3.3 联调时最容易翻车的CORS和静态文件路径

这个部分我要重点说,因为我身边至少三个同学都卡在这里。

情况是这样的:开发时前端代码在VS Code里用Live Server打开,地址是127.0.0.1:5500,而后端Flask跑在127.0.0.1:5000。前端用fetch请求http://127.0.0.1:5000/api/books,浏览器会拦截,报这个错:

Access to fetch at 'http://127.0.0.1:5000/api/books' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy

这就是跨域问题。前端和后端端口不一致,浏览器出于安全策略不允许直接跨端口访问。解决办法有几个:

  • 给Flask安装flask-cors扩展,允许所有来源访问。
  • 把所有前端文件放到Flask的static目录里,直接访问5000端口,这样就不存在跨域。
  • 用Nginx做反向代理,统一到同一个域名和端口。

我选了第二种办法,因为课程作业不需要单独部署前端静态文件服务器。把index.html、app.js、style.css放在static目录后,访问http://127.0.0.1:5000/就能打开完整页面,后端接口地址直接写相对路径/api/books,不用拼接域名端口,也顺带解决了跨域。

如果你的项目已经用了前后端分离开发,没办法直接塞进Flask里,再去用flask-cors:pip安装后加两行:

from flask_cors import CORS CORS(app)

不过,de全局允许跨域在演示时没问题,生产环境就不行了,需要配置白名单。

4. 我把哪些“本来能交差就好”的地方额外打磨了

4.1 登录状态我用Session而不是每次传密码

有人图省事,每个接口都要求前端把用户名密码传过来,后端每次查库验证。这样也能跑,但既不安全也不合理。我用Flask的session机制,登录成功后把用户信息存进Cookie,后续请求后端通过session就能判断身份。

需要登录才能访问的接口,我抽了一个装饰器:

from functools import wraps def login_required(f): @wraps(f) def wrapper(*args, **kwargs): if 'user_id' not in session: return jsonify({'code': 401, 'msg': '请先登录'}) return f(*args, **kwargs) return wrapper

在路由上加上这个装饰器就可以保护接口:

@app.route('/api/books', methods=['GET']) @login_required def list_books(): pass

说实话,如果之前没接触过装饰器,这个概念有点绕。简单理解就是:你要访问图书列表接口,先过login_required这个“门卫”,它检查你有没有登录,没登录直接拦下。

4.2 错误提示和信息反馈,决定作业演示的观感

很多练习作业的后端接口,一旦出错就是返回一段英文错误文本,或者干脆500。演示的时候,助教随手往登录框里输入一个不存在的用户名,页面弹出一条“Internal Server Error”,观感会非常差。

我的做法是每个接口都考虑“出错路径”:

  • 用户名不存在:返回“用户名不存在,请检查输入”。
  • 密码错误:返回“密码错误,请重新输入”。
  • 借书时库存不足:返回“这本书暂时没有可借副本”。
  • 添加图书时ISBN重复:返回“该ISBN已存在”。

这些提示不是硬编码在前端的,而是由后端返回标准格式里的msg字段,前端统一弹出或显示。这么做有几个好处:一是后端逻辑清晰,二是换一个前端页面也能复用,三是演示时显得你考虑周全。

4.3 数据校验:别让随便输入的ISBN把后端搞崩

作业演示时,总有人喜欢在输入框里乱填。如果后端不校验,输入框里填一个长度是100的字符串,数据库字段存不下就会报错;填一个负数作为库存,页面就会显示-1本可借书,非常离谱。

我做了两层校验。第一层在HTML里限制类型:

<input type="number" min="1" max="999" id="totalCopies">

第二层在后端接口里处理逻辑。比如添加图书:

@app.route('/api/books', methods=['POST']) @login_required def add_book(): if session.get('role') != 'admin': return jsonify({'code': 403, 'msg': '仅管理员可操作'}) data = request.get_json() title = (data.get('title') or '').strip() author = (data.get('author') or '').strip() isbn = (data.get('isbn') or '').strip() if not title: return jsonify({'code': 1, 'msg': '书名不能为空'}) try: total_copies = int(data.get('total_copies', 1)) if total_copies < 1: raise ValueError except (TypeError, ValueError): return jsonify({'code': 1, 'msg': '库存必须是不小于1的整数'}) ...

后面还要检测ISBN是否已存在,存在则提示。其实一个完整的信息系统,校验逻辑会很多,但作业至少要做到“不会因为非法输入直接500”。

5. 踩坑实录与排查技巧

5.1 数据库写入中文变成乱码

第一次在SQLite里插入“三体”这本书,前端页面显示出来是一堆“锟斤拷”之类的乱码。我排查了好一会儿,发现SQLite本身是支持UTF-8的,问题出在Python文件读取schema.sql时没指定编码。

当时我在get_db里面执行schema.sql用的是:

with open('schema.sql') as f:

系统默认编码在Windows下是GBK,中文字符就被错误解码了。解决办法是打开文件时明确指定编码:

with open('schema.sql', 'r', encoding='utf-8') as f: conn.executescript(f.read())

还有一个相关坑:如果直接在SQLite命令行工具里手动插入中文数据,而终端代码页是936,也容易乱码。最好的做法是不要在命令行里插数据,全让Python代码处理。

5.2 前端一直拿不到数据,但用Postman测接口又是好的

这个问题很典型。前端页面打开控制台,fetch返回的数据不对,但同样一个接口放到Postman里测,完全正常。

后来定位到两个原因。第一个是浏览器缓存了旧的JavaScript文件,代码改了但没生效。解决方法是强制刷新,或者开发时在HTML里引用JS的路径后面加一个版本参数:

<script src="/app.js?v=20250412"></script>

第二个原因是后端代码报错,但Flask在debug模式下返回的HTML错误页被前端当成正常消息展示了。前端请求拿到的是500状态,再调用res.json()时就解析失败。解决办法是fetch里先判断res.ok,不是2xx就读取text输出到控制台:

const res = await fetch(url, options); if (!res.ok) { const text = await res.text(); console.error(text); throw new Error('请求失败'); }

这样排查后端错误会快很多。

5.3 端口被占用和SQLite数据库被锁

Flask默认端口5000,被其他进程占用时会报Address already in use。我解决的办法是换端口运行:

python app.py --port 5001

如果不想每次启动都指定端口,也可以在app.py里用环境变量读取:

import os port = int(os.environ.get('PORT', 5000)) app.run(debug=True, port=port)

另一个坑是SQLite数据库文件被锁,报database is locked错误。原因是Python的sqlite3连接默认不是线程安全的,而Flask的调试模式会启动多个线程。我在写数据时没有及时关闭连接,导致锁冲突。解决方法是每次操作后用with语句或显式close,避免长时间占用连接。

5.4 常见问题速查表

症状可能原因解决方法
登录后刷新就掉线没设置secret_key或Cookie过期设置app.secret_key,检查session存储
前端接口跨域报错前端和后端端口不同统一部署到Flask的static目录,或用flask-cors
中文乱码schema.sql读取编码错误open时指定encoding='utf-8'
借书数量不对available_copies和total_copies混用借书时先查可用副本数量,大于0才允许借出
页面改了没反应浏览器缓存强制刷新或JS路径带版本参数
500错误但日志没输出debug模式未开启app.run(debug=True),同时看控制台堆栈

6. 这份作业做完,我的几点体会

程序能跑起来只是最基本的要求,真正拉开差距的是细节:数据库字段设计是否合理、接口返回是否有统一格式、错误提示是否友好、身份校验是否安全。这些不会在功能演示时直接体现,但助教深挖代码或者追问的时候,你就能从容应对。

我个人的体会是,做这类管理系统的作业,先把借阅流程走通,再考虑什么“优雅”设计,顺序不能反。我第一次实现的时候,试图先做一个完美的权限模型,结果写了一堆用不上的抽象类,基本功能还没影,后来全推倒重来了。如果你也在赶作业,我建议先把登录、图书增删改查、借书还书四个完整流程跑通,这就有80分了,剩下的边际效用没有你想的那么大。

还有一个小技巧分享给大家:演示前一定要准备一个“破坏性测试清单”。比如故意输入不存在的用户名、连续点两次借书、只填一半表单点提交。我在检查时发现,快速连点两次借书按钮会造成重复借阅记录,后来在按钮上加了禁用状态才解决。这些细节不测出来,演示现场就会非常尴尬。

最后说一句,很多同学会在期末的时候熬夜赶这种作业,但课程设计的前两次作业其实都是在为最终的大作业打基础。这次把Flask的会话机制、SQLite的建表流程、前后端联调思路搞清楚,后面做完整项目时就会顺畅很多。如果你也卡在某一步,欢迎把这篇文章里提到的坑对照自己的项目查一遍,大概率能找到问题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 5:50:55

xss-filters上下文输出编码:XSS防护与属性逃逸实战

简介&#xff1a;面向前端开发者的 XSS 过滤开源库源码包&#xff0c;聚焦用户输入输出环节的安全防护&#xff0c;适合需要理解 XSS 防御机制、在项目中落地转义过滤策略的初中级开发者。压缩包共 30 个文件&#xff0c;约 66KB&#xff0c;以 20 个 JavaScript 源文件与测试脚…

作者头像 李华
网站建设 2026/10/24 5:49:14

OpenClaw本地部署全指南:从环境验证到运行验证

我最早接触 OpenClaw 这个项目&#xff0c;其实带着很强的“轻敌”心态——本地部署个开源玩意儿嘛&#xff0c;不就是拉代码、装依赖、跑起来三步。结果真正上手才发现&#xff0c;整个流程里陷阱最多的地方恰恰就是环境验证。你在编译时报的错&#xff0c;十有八九不是项目本…

作者头像 李华
网站建设 2026/10/24 5:48:57

洛本兔子IP拆解:情绪化极简设计如何引爆共鸣

宠物文创界这几年冒出过不少“一张脸吃遍全网”的IP形象&#xff0c;但能让我这个做插画设计多年的人真心服气的&#xff0c;洛本兔子算一个。第一次刷到这只兔子时&#xff0c;我第一反应是——“这货也太不按套路出牌了”。别的IP都在拼命往萌、软、甜的方向卷&#xff0c;它…

作者头像 李华
网站建设 2026/10/24 5:48:54

火锅门店用电监测与节能管理:安科瑞EIoT方案实战解析

火锅店后厨的用电&#xff0c;一直是个没人愿意细算的糊涂账。电磁炉、冰柜、排风机、热水器、制冰机……一到饭点全部满负荷运转&#xff0c;非饭点设备又偷偷待机&#xff0c;电表上的数字蹭蹭往上跳&#xff0c;月底交电费的时候才发现利润被吃掉一大截。我这两年帮不少餐饮…

作者头像 李华
网站建设 2026/10/24 5:48:52

计算机开题报告写作指南:技术路线、文献综述与避坑全解析

又到毕业设计开题季&#xff0c;我这两年收到的开题报告初稿里&#xff0c;有一大半都长着同一张脸&#xff1a;研究背景从“随着信息化时代的到来”写起&#xff0c;文献综述就是按时间顺序堆了十来篇摘要&#xff0c;技术路线一句话带过——“本系统采用前后端分离架构”&…

作者头像 李华
网站建设 2026/10/24 5:47:40

AI辅助课程论文写作:从选题到定稿的完整流程与提示词技巧

很多学生第一次接触课程论文&#xff0c;都会陷入同一种困境&#xff1a;题目看起来不难&#xff0c;资料也能搜到一大堆&#xff0c;可真要动笔了&#xff0c;文档从第一页空到最后一页。我带项目、帮人改稿这些年&#xff0c;反复见到同一类初稿——"看着有内容&#xf…

作者头像 李华