Python Flask+SQLite+ECharts宿舍管理系统设计与部署实践
学生宿舍管理系统用PythonFlask来做听起来不像什么大工程但真正落地的时候你会发现它踩的坑一点也不比大型项目少。我这次做的这套带可视化的宿舍管理系统就是从一个非常具体的痛点出发宿舍管理员手里的那堆台账已经乱到没法看了。和楼管阿姨聊了一个下午之后我决定用Flask搭一个轻量化的网页端后台目标很明确——能管理学生入住退宿、能查宿舍空床、能记报修然后把所有数据汇总成可视化图表让管理者一眼看清整栋楼的运转情况。这套系统最后是能本地部署、单机跑起来就能用的技术栈就是PythonFlaskSQLiteECharts。本文我把完整的设计思路、数据库拆分、可视化接口实现、Flask代码组织方式以及部署运行时踩过的坑都写出来。如果你正准备做一个类似的宿舍管理课设、毕设或者单位内部小工具这一篇应该能帮你省掉不少搜索的时间。1. 宿舍管理的真实痛点Excel台账撑不住的地方1.1 一个楼管的下午需求是从这里长出来的在写第一行代码之前我在学校宿舍值班室蹲了差不多一个下午。楼管阿姨打开她的电脑屏幕上是一个巨大的Excel文件里面分了好几个工作表住宿名单、退宿记录、报修登记、查寝情况。每个表用不同的颜色标记状态绿色是正常在住黄色是申请调宿红色是已退宿。问题在于这些表之间没有关联。一个人调宿之后住宿名单改了但报修记录里还是旧房间号晚归记录是手写的晚上十一点之后学生进来阿姨在纸质本子上签个字第二天再手工录入Excel。到了月底要做入住率统计、空床位数、晚归次数汇总的时候阿姨需要同时打开三四个文件用COUNTIF函数来回数。这个场景我相信做过管理类系统的人都很熟悉业务本身不复杂但数据的产生是持续的、多来源的人工维护的效率低到让人崩溃。所以这个学生宿舍管理系统的第一个需求不是要有漂亮的可视化大屏而是先把散落的业务数据集中起来让它们有关联、可查、可统计。可视化是在这个基础上才有意义的。1.2 功能清单我最终确定下来的模块边界为了避免把项目做成一个什么都想管、什么都管不好的大杂烩我在需求阶段就划定了功能边界。这套宿舍管理系统最终落地的模块如下模块核心功能解决的人工痛点学生管理学生信息录入、在住/退宿状态管理、调宿记录住宿名单多次维护、信息不同步宿舍管理楼栋房间管理、床位分配、剩余空床统计空床位靠问、靠数报修管理学生提交报修、管理员派单、维修完成回执纸质报修单容易丢、无法跟踪晚归/查寝记录晚归登记、按日期和宿舍条件查询手工台账整理繁琐水电用量管理每月按宿舍录入用水用电、生成费用统计Excel公式链复杂且容易算错可视化总览入住率、空床分布、报修趋势、水电用量变化月底汇总报表耗时数小时每个模块都对应我在需求调研中看到的具体痛点。比如水电用量这一块原来阿姨每个月要拿着一叠纸质抄表记录对照Excel逐宿舍录入再用公式算出费用。系统里做一张录入表单加一张趋势图整个流程从半天压缩到二十分钟。1.3 为什么选PythonFlask而不是别的技术栈技术选型这个环节我想多说两句。很多人在做类似系统时一上来就想用Spring Boot、Vue全家桶但实际情况是一个宿舍管理系统的并发量可能就几十个人同时用部署环境是宿舍管理室的一台普通电脑维护人员连命令行都不太熟。这时候重框架、重工程化带来的复杂度远超收益。我选Flask有几个非常现实的理由起步成本极低。一个Python文件就能跑起一个带路由的Web服务不需要依赖注入、不需要配置容器。模板渲染开箱即用。Jinja2可以直接在HTML里渲染数据对表单提交-数据库写入-页面显示这种简单请求链路比前后端分离少一层接口联调。SQLAlchemy接入数据库很快。SQLite单文件存储部署时直接把数据库文件带走不像MySQL还要安装服务、配置账号密码。配套可视化生态成熟。Flask返回JSON给前端ECharts渲染这是最成熟也最小成本的组合。如果是几十万用户的高并发平台我肯定不会推荐Flask。但像宿舍管理系统这种典型的局域网点应用PythonFlask恰好处于性能和开发效率的最佳平衡点。说白了别用高射炮打蚊子把精力放在业务逻辑的梳理上才是正事。2. 数据库四张核心表把宿舍业务拆成可管理的数据结构2.1 实体关系一栋楼里的数据是怎么串起来的宿舍管理系统的数据模型不复杂但有几个关键的业务规则要在表结构设计时就定清楚否则后面写逻辑会反复返工。最核心的实体就三个宿舍Dormitory、学生Student、记录Record。记录又可以拆成报修、晚归、水电三类。它们之间的关系是一个宿舍有多个学生1对多学生表保存宿舍ID一个学生可以有多条报修记录1对多报修表保存学生ID一个宿舍每月有一条水电记录1对多水电表保存宿舍ID晚归记录挂在学生上也挂在宿舍上为了按楼栋汇总方便这里我要重点提一个容易忽略的细节学生和宿舍的关系是有状态的。同一个学生可能大二住3号楼大三搬到5号楼如果不记录入住时间和退宿/调宿时间历史数据就说不清了。所以我在学生表里加了status字段在住/退宿/调宿还加了调宿记录表来保存每次宿舍变动的轨迹。2.2 核心表结构设计与关键字段用Flask-SQLAlchemy定义模型时我习惯先画清楚字段再写代码。下面这几张表是系统的核心# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Dormitory(db.Model): __tablename__ dormitory id db.Column(db.Integer, primary_keyTrue) building db.Column(db.String(10), nullableFalse) # 楼栋名如梅苑 room_no db.Column(db.String(10), nullableFalse) # 房间号 capacity db.Column(db.Integer, default6) # 房间容量 gender db.Column(db.String(2), default男) # 性别限制 students db.relationship(Student, backrefdorm, lazydynamic) class Student(db.Model): __tablename__ student id db.Column(db.Integer, primary_keyTrue) student_no db.Column(db.String(20), uniqueTrue, nullableFalse) # 学号 name db.Column(db.String(50), nullableFalse) gender db.Column(db.String(2)) major db.Column(db.String(50)) phone db.Column(db.String(20)) dorm_id db.Column(db.Integer, db.ForeignKey(dormitory.id)) check_in_date db.Column(db.Date, defaultdatetime.now().date()) status db.Column(db.String(10), default在住) # 在住/退宿/调宿 class Repair(db.Model): __tablename__ repair id db.Column(db.Integer, primary_keyTrue) student_id db.Column(db.Integer, db.ForeignKey(student.id)) dorm_id db.Column(db.Integer, db.ForeignKey(dormitory.id)) description db.Column(db.Text) # 报修描述 status db.Column(db.String(10), default待处理) # 待处理/维修中/已完成 created_at db.Column(db.DateTime, defaultdatetime.now()) finished_at db.Column(db.DateTime) class Utility(db.Model): __tablename__ utility id db.Column(db.Integer, primary_keyTrue) dorm_id db.Column(db.Integer, db.ForeignKey(dormitory.id)) month db.Column(db.String(7), nullableFalse) # 2025-03 electricity db.Column(db.Float, default0) # 用电量 water db.Column(db.Float, default0) # 用水量 cost db.Column(db.Float, default0) # 费用这里我特别强调一个设计Repair表同时保存了student_id和dorm_id。为什么有这么冗余的字段因为报修单上既要显示是谁报的也要显示修哪个房间。如果只存学生ID一旦学生调宿了报修记录的宿舍信息就跟着历史记录错乱了。把宿舍ID冗余进去报修单就永久锁定了当时的房间这是业务上的刚需。2.3 SQLite对这个小项目来说太够用了技术圈子现在流行一上来就上MySQL、PostgreSQL但对宿舍管理这种应用场景SQLite的成本优势太明显了零配置文件数据库就是一个.db文件备份等于复制文件。不依赖额外服务从开发到部署不需要安装数据库服务端也不需要维护账号密码。Flask-SQLAlchemy无缝支持连接串只需要sqlite:///dorm.db一行搞定。当然SQLite有自己要注意的地方最典型的就是并发写锁问题。默认情况下SQLite同一时间只允许一个进程写库读多写少的场景问题不大但如果多个管理员同时提交报修单、录入水电数据就可能出现database is locked报错。解决办法我在第五章踩坑部分会详细说这里先给个结论开启WAL日志模式加一个timeout参数就够这个小系统用了。3. 可视化看板从数据表到管理大屏的实现过程3.1 可视化不是炫技管理者真正需要看什么很多人一提可视化就是大屏、酷炫动效但宿舍管理系统里最需要的是让管理者在30秒内掌握核心状态。我在设计可视化总览页时明确回答了三个问题现在全楼住了多少人还剩多少床位哪个楼栋最挤、哪层楼的空床最多最近一个月报修的变化趋势怎么样维修跟得上吗围绕这三个问题我用ECharts做了四块核心图表总览卡片区入住率、在住人数、空床位数、今日待处理报修数环形图男女住宿比例 / 各楼栋入住率柱状图各楼栋空床数量分布折线图近30天报修数量趋势另外还加了一个横向条形图展示各类型报修占比水电、门窗、家具、电器这个对后勤安排维修资源很有参考价值。原来看报表只能看到总数现在能看到水电类报修占了多少、有多少是某栋楼集中出现的排查方向清晰很多。3.2 数据从哪来Flask写一个JSON接口给前端可视化的核心链路很简单Flask后端从数据库查询聚合数据返回JSON前端ECharts用AJAX拿到JSON后渲染。重点在聚合查询的写法上。以各楼栋空床数量分布为例它的SQL逻辑是每个宿舍的容量之和减去已被在住学生占用的床位数量。用SQLAlchemy写出来是这样# routes/dashboard.py from flask import Blueprint, jsonify from models import db, Dormitory, Student from sqlalchemy import func dashboard_bp Blueprint(dashboard, __name__) dashboard_bp.route(/api/occupancy/by_building) def occupancy_by_building(): buildings [梅苑, 兰苑, 竹苑, 菊苑] result [] for b in buildings: total_beds db.session.query(func.sum(Dormitory.capacity))\ .filter(Dormitory.building b).scalar() or 0 used_beds db.session.query(func.count(Student.id))\ .join(Dormitory, Student.dorm_id Dormitory.id)\ .filter(Dormitory.building b, Student.status 在住).scalar() result.append({ building: b, total: total_beds, used: used_beds, empty: total_beds - used_beds, rate: round(used_beds / total_beds * 100, 1) if total_beds else 0 }) return jsonify({data: result})这个接口返回的数据结构很规整前端拿到之后几乎不需要二次处理直接往ECharts的x轴、y轴数据里填就行。这里我还想提醒一点后端聚合比前端逐个计算要靠谱得多。有时候图个省事把原始数据全量返回给前端让JavaScript去算总数、去分组。数据量小的时候看不出问题等数据积累到几百张表时页面会越来越卡。把聚合逻辑放在SQLAlchemy里接口只返回结果前端只负责渲染这个分工要一开始就定清楚。3.3 ECharts接入和页面渲染的几个关键写法前端部分我用的不是前后端分离而是Flask的Jinja2模板渲染页面骨架 AJAX拉取JSON填充图表的混合模式。页面的HTML结构由Flask渲染图表里的数据全部由JS通过fetch从接口获取。一个典型图表的完整实现流程!-- templates/dashboard.html -- div classchart-card h3各楼栋空床数量/h3 div idbuildingChart styleheight: 350px;/div /div script src{{ url_for(static, filenamejs/echarts.min.js) }}/script script fetch(/api/occupancy/by_building) .then(resp resp.json()) .then(res { const data res.data; const chart echarts.init(document.getElementById(buildingChart)); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(item item.building) }, yAxis: { type: value }, series: [{ name: 空床数, type: bar, data: data.map(item item.empty), itemStyle: { color: #5B8FF9 } }] }); }); /scriptECharts的使用有几个坑这里提前说容器必须先有高度。如果外层div没有设置height或者height为0ECharts会画不出来。所以上面示例里我直接给图表容器加了styleheight: 350px;。数据格式要和series类型匹配。很多初学者把API返回的元组直接塞给ECharts结果图表空白。一定要通过map把字段提取成ECharts需要的数组格式。窗口缩放后要调用chart.resize()。如果不监听window的resize事件浏览器改变大小时图表不会自适应。3.4 动态刷新轮询比WebSocket更实用热搜词里有python可视化实时刷新这个需求在宿舍管理系统里确实存在。比如楼管阿姨希望报修处理状态在页面上自动更新不用每次都要手动刷新。我的方案是setInterval定时轮询// 每30秒自动刷新一次待处理报修卡片 setInterval(() { fetch(/api/repair/pending_count) .then(resp resp.json()) .then(res { document.getElementById(pendingCount).innerText res.count; }); }, 30000);为什么不引入WebSocket宿舍管理系统的用户量就几十人实时性要求是30秒级别轮询完全够用。WebSocket的引入意味着要维护长连接、处理断线重连、增加服务端复杂度对这个小项目来说得不偿失。选择技术方案时够用就是最好的标准。4. Flask代码组织蓝图、接口与网页绑定的关键写法4.1 用蓝图拆模块别把所有路由堆在一个文件里Flask项目最忌讳的就是一个app.py里塞几百行路由。我这个项目用蓝图Blueprint按业务模块拆分了代码project/ ├── app.py # 应用入口 ├── models.py # 数据库模型 ├── requirements.txt ├── data/ │ └── dorm.db # SQLite数据库文件 ├── routes/ │ ├── __init__.py │ ├── dashboard.py # 总览看板 │ ├── student.py # 学生管理 │ ├── dormitory.py # 宿舍管理 │ ├── repair.py # 报修管理 │ └── utility.py # 水电管理 ├── static/ │ ├── css/ │ ├── js/ │ │ └── echarts.min.js │ └── images/ ├── templates/ │ ├── base.html │ ├── dashboard.html │ ├── student_list.html │ ├── repair_list.html │ └── ... └── utils/ └── helpers.py # 通用函数入口文件里注册蓝图的方式非常直观# app.py from flask import Flask from models import db from routes.dashboard import dashboard_bp from routes.student import student_bp from routes.repair import repair_bp app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///data/dorm.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) app.register_blueprint(dashboard_bp) app.register_blueprint(student_bp, url_prefix/student) app.register_blueprint(repair_bp, url_prefix/repair)用蓝图的好处是每个业务模块的路由、业务逻辑、数据处理都在自己对应的文件里新增需求时只改对应模块不会牵一发动全身。4.2 关键接口的设计规范让前端调用不用猜我在设计API时给自己定了一个简单的规范所有的接口都返回统一格式——{code: 0, msg: ok, data: ...}。好处是前端可以统一封装错误处理而不需要每个页面单独判断数据结构。# utils/helpers.py def success(dataNone, msgok): return jsonify({code: 0, msg: msg, data: data}) def error(msgerror, code1): return jsonify({code: code, msg: msg, data: None})以学生入住操作为例核心接口的逻辑是先检查目标宿舍是否还有空床有则分配并写入学生记录同时更新宿舍占用状态。# routes/student.py from flask import request from models import db, Student, Dormitory from utils.helpers import success, error student_bp.route(/checkin, methods[POST]) def checkin(): data request.get_json() name data.get(name) student_no data.get(student_no) dorm_id data.get(dorm_id) # 查重学号不能重复 if Student.query.filter_by(student_nostudent_no, status在住).first(): return error(该学号已存在在住记录) # 检查宿舍空床 dorm Dormitory.query.get(dorm_id) used Student.query.filter_by(dorm_iddorm_id, status在住).count() if used dorm.capacity: return error(该宿舍已无空床) student Student(namename, student_nostudent_no, dorm_iddorm_id, status在住) db.session.add(student) db.session.commit() return success({student_id: student.id})这个接口虽然逻辑简单但有两个细节是很多初学者容易漏掉的幂等性同一个学号连续提交两次入住必须有防重检查。我这里是按student_no status在住查重避免重复入住。容量边界宿舍床位已满时不能硬塞。入住前先统计在住人数和容量比较后再执行插入。4.3 绑定到网页元素到底是怎么绑的热搜词里有一个很典型的痛点flask如何绑定到网页元素。这个问题的本质是——后端的数据怎么安全地出现在前端页面上Flask里有三种常见的绑定方式我按实际使用顺序说一下方式一Jinja2服务端渲染。适合表单页、列表页等数据实时性要求不高的场景。student_bp.route(/list) def student_list(): students Student.query.filter_by(status在住).all() return render_template(student_list.html, studentsstudents)模板里直接遍历table {% for s in students %} tr td{{ s.student_no }}/td td{{ s.name }}/td td{{ s.dorm.building }}{{ s.dorm.room_no }}/td /tr {% endfor %} /table方式二AJAX JSON。适合图表渲染、动态刷新的场景就是前面可视化部分的做法。方式三表单POST提交。适合表单处理场景前端提交数据后端处理完再重定向到结果页。这三种方式各有适用场景混用是正常的。理解的关键是服务端渲染负责页面第一次展示的数据AJAX负责页面展示之后需要动态更新的数据分工明确就不会乱。5. 开发途中绕不开的坑中文乱码、并发锁与图表渲染5.1 Flask返回中文变\uXXXX一个配置让所有接口恢复正常这个坑几乎每个用Flask写JSON接口的人都会踩。第一次写完接口用浏览器打开/api/occupancy/by_building看到的不是梅苑兰苑而是\u6885\u82d1这种转义字符。浏览器能显示中文前端fetch也能正常解析但调试的时候极度难受而且如果接口被第三方以非JSON方式调用中文直接变成乱码。原因很简单Flask的jsonify默认会对中文做ASCII转义。解决办法是在app配置里加一行app.config[JSON_AS_ASCII] False加完之后重新启动项目所有接口返回的中文就正常显示。这个配置在Flask 2.3之后的版本改名为app.json.ensure_ascii False新版本里写法略有不同但思路一样。5.2 SQLite的database is lockedWAL模式解决写并发冲突项目测试阶段我开两个浏览器窗口同时操作——一个录入报修单一个修改学生信息结果控制台直接报sqlite3.OperationalError: database is locked。搜了一下才知道SQLite默认的日志模式在并发写场景下会锁库。解决方案是连接SQLite时加几个参数开启WAL模式并设置等待超时app.config[SQLALCHEMY_ENGINE_OPTIONS] { connect_args: { timeout: 15, journal_mode: WAL } }WALWrite-Ahead Logging模式的好处是读操作和写操作可以并发进行写操作之间也不需要互相等待那么长时间。加上15秒的timeout即使在极端情况下两个管理员同时提交数据后一个请求也会等待前一个写完成而不是直接报错。这个配置对宿舍管理系统的使用场景来说足够了。如果以后并发量真的上去了那再迁移到PostgreSQL也不迟SQLAlchemy的ORM模型迁移成本并不高。5.3 ECharts图表白屏90%是容器高度问题前面提到ECharts需要容器有明确高度这里把我踩坑的完整过程说一下。我在一个页面里放了两个图表一个能显示一个白屏折腾了好久最后打开浏览器开发者工具发现白屏图表的容器元素高度是0。原因是我把图表容器放在了一个display: flex的父元素里父元素没有设置高度子元素继承了100%的高度但父元素的高度是0所以子元素实际高度也是0。ECharts初始化时检测到高度为0就会画一个空白画布。解决办法有两个给每个图表容器写死高度比如styleheight: 350px;。用CSS确保父元素有高度例如min-height: 400px;。我的建议是方案1最稳妥。因为图表的高度本来就该根据数据量和卡片布局来定写死反而好控制。5.4 静态文件404蓝图子路径下引不到CSS和JS用蓝图设置了url_prefix/student之后如果模板里的静态文件路径写成了相对路径就容易出现404。比如script srcjs/echarts.min.js当页面访问的是/student/list时浏览器会去请求/student/js/echarts.min.js但文件实际在/static/js/echarts.min.js自然就404了。解决办法是始终使用Flask的url_for生成静态文件路径script src{{ url_for(static, filenamejs/echarts.min.js) }}/script这个写法不管页面路由嵌套多少层最终都会生成正确的/static/...路径。同样的道理也适用于CSS和图片引用。6. 本地部署与交给楼管使用的那台电脑6.1 环境准备虚拟环境加依赖清单项目开发完交付部署这一步往往最容易出问题。我习惯每个项目建独立的Python虚拟环境避免依赖版本冲突cd dorm-system python -m venv venv # Windows下激活 venv\Scripts\activate # macOS/Linux下激活 source venv/bin/activate pip install flask flask-sqlalchemy依赖清单用pip freeze生成这样换一台电脑部署时可以直接装pip freeze requirements.txt新的机器上pip install -r requirements.txtrequirements.txt的内容大致是Flask3.0.0 Flask-SQLAlchemy3.1.1我个人建议把关键依赖的版本固定下来不要用这种宽松约束。宿舍管理系统的运行环境变化不大固定版本能避免因为依赖升级导致的不兼容问题。6.2 启动与访问从开发服务器到局域网访问开发阶段直接用Flask自带的开发服务器就行python app.py默认跑在127.0.0.1:5000浏览器访问http://localhost:5000即可。但真正给宿舍管理室用时肯定不能只在自己电脑上访问。两层楼的办公室、门卫室都要看数据这就需要让服务监听所有网卡地址让同一局域网的其他设备都能访问if __name__ __main__: # 让局域网内其他设备可以通过IP访问 app.run(host0.0.0.0, port5000, debugFalse)注意这里debugFalse一定要设置开发服务器的调试模式暴露出去很不安全。启动后用ipconfigWindows或ifconfigmacOS/Linux查一下本机IP比如192.168.1.100那么其他设备在浏览器里访问http://192.168.1.100:5000就能打开了。6.3 交付过程中容易被忽视的几件事做一个内部项目系统能跑起来只是第一步让用户真正用起来才是关键。我在交付时踩过的坑和经验如下防火墙放行端口。Windows机器默认会拦截外部对5000端口的访问需要在防火墙设置里加一条入站规则允许TCP 5000端口。我第一次部署就是因为没放行端口楼管那边一直打不开页面。数据库备份。SQLite是单文件数据库我写了一个简单的备份脚本每天晚上把dorm.db复制一份加日期后缀。宿舍管理系统里的住宿记录是重要数据丢不起。浏览器兼容性。宿舍管理室的电脑可能还是老版本的浏览器ECharts 5对IE基本不兼容。如果目标环境有老浏览器要么用ECharts 4要么提醒对方升级浏览器。我用的是Chrome提前装好了。说明文档要写清楚。我给了楼管阿姨一张A4纸上面写了怎么打开系统、怎么录入学生、怎么处理报修配了几张截图。内部小工具不需要多专业的文档但一定要把日常操作路径写明白。6.4 后续可以扩展的方向这套系统的代码组织方式决定了它很好扩展。如果你打算在这个基础上继续做下去有几个方向可以参考接入微信小程序让学生用手机就能报修、查宿舍分配。增加晚归消息推送晚归记录自动通知辅导员。引入宿舍评比功能根据卫生检查、晚归情况、水电用量综合评分。这些扩展在现有的蓝图模块上做增量开发基本不动核心结构。最后分享一点个人体会宿舍管理系统这类项目的技术难度其实不高真正考验人的是需求拆解和数据建模。如果你也准备动手做一套建议先找真正管宿舍的人聊半小时把痛点记下来再开始写代码。系统做出来是给人用的数据模型和页面的设计都该从使用场景倒推而不是从技术框架正推。这套FlaskSQLiteECharts的组合从开发到部署再到迭代算是轻量级管理系统里性价比很高的方案值得一试。