拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python FastAPI与Vue3的宿舍后勤管理系统开发实战

基于Python FastAPI与Vue3的宿舍后勤管理系统开发实战

基于 Python + Vue3 的员工宿舍后勤管理系统:从业务建模到部署落地的完整实践

后勤管理系统是很多公司容易忽略、但真实痛点集中的领域。员工宿舍管理,表面上看是“记录谁住哪间房”,但实际做起来,涉及入住退宿、费用结算、床位资源统计、报修闭环、权限区分等一堆琐碎环节。本文基于 Python(FastAPI)+ Vue3 的完整前后端分离方案,梳理整个系统从零搭建的过程:为什么选这套技术栈、数据表怎么设计、前端页面如何组织、联调期有哪些高频坑、最后怎么部署上线,以及未来可以扩展的方向。适合正在做类似小型管理系统的开发者、做毕业设计的同学,以及公司内部需要快速搭建后勤工具的技术团队参考。

1. 为什么“宿舍管理”听着简单,做起来全是细节

1.1 业务场景里最容易漏掉的需求

我见过不少团队把宿舍管理系统当成“学生作业”来做,结果一上线就被宿管阿姨和行政同事连续反馈打蒙。这个系统真正的复杂度,不在技术,而在业务。

先说入住登记。一个员工入职,分配宿舍,看起来就是“填个表单选个房间”的事。但实际问题包括:这人是住单人间还是双人间?这栋楼和这层的朝向有没有特殊要求?同一部门的人要不要尽量安排在一起?有没有禁烟楼层?还有更常见的——某个房间已经住了三个人,但系统只记录“是否住满”,没用记录“还剩几个空床”,那协调入住的时候就只能靠翻纸质台账。

再说退宿和调宿。员工离职要办退宿,宿管检查房间设施、收回钥匙、结算水电费,这些线下流程如果系统不跟踪,就很容易出现“人走了,系统里还显示占着床位”。调宿更麻烦:员工从 A 房间换到 B 房间,中间有没有费用差异?原房间剩余的水电费怎么处理?这些细节不提前建模,后面写代码会很痛苦。

还有费用计算。宿舍水电费通常按房间结算,不是按人头。有的公司每月固定补贴一部分,超出部分员工自理。补贴额度、阶梯电价、公摊损耗,每个公司规则都不一样。如果系统里只存一个“金额”字段,那等于没有设计——你需要把计算规则抽象出来,而不是把结果写死。

权限也经常被忽略。普通员工应该只能看到自己的入住信息;宿管员能管理房间和入住;行政主管看统计报表;财务看账单但不需要碰房间管理。这种多角色控制,如果一开始图省事不做,后面补起来成本极高。

最后是维修报修。宿舍里的空调、热水器、门锁,坏的概率比想象中高。报修不是简单“记录一条工单”,而是要有状态流转:待接单、处理中、已完成、已回访。如果系统不做状态机,工单就会变成一条只读通知。

1.2 为什么选 Python + Vue3,而不是传统模板渲染

这个项目我用的是“前后端分离 + 后端接口 + 前端单页应用”的方案。后端用 Python 的 FastAPI 框架,前端用 Vue3 + Element Plus 组件库。

选 Python 的原因很直接:它是很多内部系统团队最熟悉的技术栈。用 FastAPI 而不是 Flask 或 Django,是因为这类系统接口数量大概在 30~50 个左右,需要清晰的参数校验和自动生成的接口文档。FastAPI 自带 OpenAPI 文档,后端写完接口,前端可以对着文档联调,省去大量口头沟通。

Vue3 的选择也很好理解。对于这种中后台管理系统,Vue3 的 Composition API 配合 Element Plus,组件复用和页面组织效率很高。相比 Vue2,Vue3 的响应式系统重写后,在处理表格、弹窗、表单这类高频交互时性能更好,而且 TypeScript 支持更友好。这套系统要处理的数据结构不算复杂,但页面之间的状态关联不少——比如选房间时要实时看到剩余床位和入住员工列表,这种场景下组件化开发比传统多页模板自然得多。

1.3 系统模块怎么拆

我把整个系统拆成 5 个核心模块,每个模块对应一张主表加若干关联表:

模块解决的核心问题主要数据对象
房间管理宿舍资源可视化,空闲/入住/维修状态一目了然房间、床位、楼栋
入住管理入住、退宿、调宿全流程,历史记录可追溯入住记录、员工信息
水电账单按月生成房间账单,记录缴费状态账单、缴费记录
报修管理工单流程闭环,状态可跟踪报修单、处理记录
系统管理用户账号、角色权限、基础数据用户、角色、日志

另外有一个 dashboard 首页,用于展示入住率、月度水电费趋势、待处理工单数等关键指标,方便管理员一眼掌握宿舍运行状态。

2. 后端设计:FastAPI 怎么把“后勤账本”做得清楚

2.1 数据模型设计——先想清楚宿舍业务再建表

我强烈建议先画数据关系,再写代码。宿舍系统的核心是“房间”和“入住记录”,一切费用和工单都挂在这两条主线下面。

房间表(rooms),字段大致如下:

class Room(Base): __tablename__ = "rooms" id = Column(Integer, primary_key=True) room_no = Column(String(20), unique=True, nullable=False) # 房间号,如 A-3-501 building = Column(String(20), nullable=False) # 楼栋 floor = Column(Integer, nullable=False) # 楼层 room_type = Column(String(10), nullable=False) # single / double / four bed_count = Column(Integer, nullable=False) # 床位数 bed_used = Column(Integer, default=0) # 已用床位数 status = Column(String(10), default="available") # available / occupied / repair remark = Column(String(255))

注意:不要直接通过“查入住记录里有多少未退宿的人”来实时推导房间实住人数,那样每次刷新页面都要做聚合查询,性能差且逻辑分散。我这里在房间表里维护一个bed_used冗余字段,办理入住时 +1、退宿时 -1,然后用数据库事务保证准确。这是小系统里非常实用的做法。

员工表(employees)和入住记录表(check_ins):

class CheckIn(Base): __tablename__ = "check_ins" id = Column(Integer, primary_key=True) employee_id = Column(Integer, ForeignKey("employees.id")) room_id = Column(Integer, ForeignKey("rooms.id")) bed_no = Column(Integer) # 入住的床位编号,例如 2 check_in_date = Column(Date) check_out_date = Column(Date, nullable=True) status = Column(String(10), default="active") # active / closed

入住的“快照”是关键。员工当前住哪、历史上住过哪、每个时间段对应的房间和床位是多少,都通过这条记录查出来。退宿不删除记录,只把status改成closed并填上check_out_date。这样后续想追溯“上季度住宿情况”时,一条 SQL 就能查出来。

水电账单表需要冗余房间号和期间,避免之后房间换号导致账单归属错乱:

class UtilityBill(Base): __tablename__ = "utility_bills" id = Column(Integer, primary_key=True) room_id = Column(Integer, ForeignKey("rooms.id")) period = Column(String(7), nullable=False) # 月份,如 2025-06 water_amount = Column(Numeric(10, 2), default=0) electricity_amount = Column(Numeric(10, 2), default=0) water_fee = Column(Numeric(10, 2), default=0) electricity_fee = Column(Numeric(10, 2), default=0) total_fee = Column(Numeric(10, 2), default=0) status = Column(String(10), default="unpaid") # unpaid / paid

这里金额字段统一用Numeric(10,2)。用 Python 的 float 存金额,会在累计和比较时出现精度问题,这种坑我已经踩过不止一次。Numeric转出来的 Decimal 对象在 JSON 序列化时需要处理,FastAPI 默认会输出成字符串,后面联调期我会讲到怎么解决。

报修单表(repair_orders)核心就是一个状态机:

class RepairOrder(Base): __tablename__ = "repair_orders" id = Column(Integer, primary_key=True) room_id = Column(Integer, ForeignKey("rooms.id")) report_by = Column(Integer, ForeignKey("employees.id")) category = Column(String(20)) # 空调 / 水电 / 门窗 / 其他 description = Column(Text) status = Column(String(10), default="pending") # pending / processing / done / closed handler = Column(String(50)) create_time = Column(DateTime, default=datetime.now) finish_time = Column(DateTime, nullable=True)

状态流转我用简单的 if 判断来控制,没有上 Python 的状态机库——因为状态只有 4 个,硬上状态机库反而增加阅读难度。

2.2 接口风格与统一返回格式

所有接口约定统一的 JSON 结构,这是我不管做什么后端项目都坚持的规范:

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

业务成功时code=0;参数错误返回code=40001;鉴权失败返回code=40100;服务器内部异常返回code=50000。这样前端 axios 拦截器里只需要判断code一个字段,不用每次分别处理HTTP 200 但业务失败和HTTP 500的混乱情况。

接口命名走 RESTful 风格。房间相关:

GET /api/rooms 分页查询,筛选 building / status POST /api/rooms 新增房间 PUT /api/rooms/{id} 编辑房间 DELETE /api/rooms/{id} 删除房间

入住相关:

POST /api/check-ins 办理入住(传入员工ID、房间ID、床位号) POST /api/check-ins/{id}/checkout 办理退宿 GET /api/check-ins/history 入住记录查询

报表相关:

GET /api/dashboard/summary 入住率总览 GET /api/dashboard/utility-trend 近6个月水电费

分页参数统一用page和page_size,返回结构里带上total:

{ "code": 0, "data": { "items": [], "total": 137, "page": 1, "page_size": 20 } }

2.3 鉴权与权限管控

这个系统有三种角色:普通员工、宿管员、管理员。我用 JWT 做登录态,密码用bcrypt哈希存储。

登录流程很简单:用户提交账号密码,后端校验通过后用 PyJWT 签发一个有效期 24 小时的 token,token 里只放user_id和role。前端把 token 存在 localStorage,每次请求通过 axios 拦截器放到Authorization: Bearer <token>头里。

权限管控我用了 FastAPI 的依赖注入机制,写一个通用的require_role依赖:

async def require_role(role: str): def verify( authorization: str = Header(...), db: Session = Depends(get_db) ): token = authorization.replace("Bearer ", "") payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) user = db.query(User).get(payload["user_id"]) if user.role != role: raise HTTPException(status_code=403, detail="无权限") return user return verify

然后在需要权限的接口上直接套:

@app.post("/api/rooms", dependencies=[Depends(require_role("admin"))]) def create_room(...): ...

普通员工登录后只能调用自身相关的查询接口;宿管员能处理入住和工单;admin 才能管理房间和账号。这个模型简单,但足够覆盖大部分内部系统的权限需求。

3. Vue3 前端:一个后台管理的页面怎么组织才不乱

3.1 项目骨架与依赖版本

前端我用的工程化方案是 Vite + Vue3 + TypeScript + Element Plus + Pinia + Vue Router。Vite 作为开发服务器和打包工具,启动速度和 HMR 体验比 Webpack 时代好太多。

创建项目可以直接用 Vite 官方脚手架:

pnpm create vite my-dormitory-admin --template vue-ts cd my-dormitory-admin pnpm add vue-router@4 pinia axios element-plus

这里有个细节:Element Plus 的完整引入和按需引入。开发阶段图方便可以全量引入,但打包体积会大不少。我建议用 unplugin-auto-import 和 unplugin-vue-components 这两个插件做按需引入,只打包用到的组件。

TypeScript 配置方面,我在 tsconfig.json 里开了"strict": true。刚开始确实会有大量类型报错,但项目跑起来后,改一处牵扯到其他页面时的收益非常明显。热搜词里有人问“若依 vue3 ts 报错”,大概率就是因为模板项目开了 strict 而自己没有空值保护习惯,后面我会在联调章节提到几个高频类型问题。

3.2 路由、登录态与页面权限设计

路由结构按页面模块划分:

/login 登录页 / 主布局(侧边栏 + 顶栏 + 内容区) /dashboard 首页看板 /rooms 房间管理 /check-ins 入住管理 /bills 水电账单 /repairs 报修管理 /system/users 用户管理

路由守卫是登录态控制的关键。我在router.beforeEach里做两步判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem("token") if (to.path === "/login") { next() } else if (!token) { next("/login") } else { next() } })

这只是最基础的登录拦截。权限按钮级的控制,我写了一个自定义指令v-permission,传入需要的角色数组,如果当前用户角色不在里面,就自动把这个元素移出 DOM。这样宿管员账号看房间页面时只会看到“办理入住”按钮,而看不到“新增房间”按钮。

Pinia 里维护两个核心 store:userStore存 token、用户信息、角色和权限列表;roomStore存当前筛选条件下的房间列表和加载状态。不要在多个组件里各自请求同一份房间数据,否则切换页面时会出现画面跳动。

3.3 核心页面拆解:房间可视化、费用账单、报修流程

房间管理页是这个系统最核心的界面,我用的是“左侧房间卡片网格 + 右侧抽屉详情”的交互形式。每个房间渲染成一张卡片,卡片颜色按状态区分:绿色表示可用、橙色表示部分入住、红色表示已住满、灰色表示维修中。点开卡片,右侧抽屉显示房间详细信息和入住员工列表,抽屉底部放“办理入住”“办理退宿”“发起报修”三个操作按钮。

卡片组件里有一个细节:房间状态的判定不能只靠room_status字段。比如一个双人间住了 1 个人,状态可能是available但又有bed_used>0,前端要根据bed_used和bed_count的对比关系动态算出“部分入住”的展示状态。也就是说,数据库存的status字段只是业务数据,展示层要基于业务规则做二次计算。

水电账单页我用了 Element Plus 的 el-table 加月份筛选。表格列展示房间号、上月表底、本月表底、用电量、电费、水费、合计、缴费状态。缴费状态用 el-tag 展示颜色区分。这里有个小交互点:财务人员需要批量确认缴费,所以表格第一列是全选 checkbox,顶部一个“批量标记已缴”按钮,调后端接口批量更新账单状态。

报修管理页是典型的“列表 + 状态流转”页面。列表按状态筛选,工单详情用对话框展示。操作按钮根据状态动态渲染:待处理状态显示“接单”,接单后显示“完成维修”,完成后显示“关闭工单”。每次操作后回调刷新接口,避免页面出现脏状态。

4. 前后端联调期,我踩过的几个坑(以及解法)

4.1 跨域问题

开发阶段前端跑在localhost:5173,后端跑在localhost:8000,天然跨域。后端的 FastAPI 需要配 CORSMiddleware:

app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:5173"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )

注意allow_origins用列表明确写好来源,不要图省事用["*"]。配合allow_credentials=True时,["*"]会导致浏览器直接拦截,必须写明具体源。

生产环境我不用 CORS 放行,而是用 Nginx 反向代理把/api前缀转发到后端服务,这样前后端在浏览器看来同源,彻底绕开跨域问题。开发期配 CORS,生产期用代理,这是我固定的处理策略。

4.2 日期与金额字段的类型陷阱

FastAPI 返回的 datetime 字段默认序列化成 ISO 格式,比如2025-06-01T10:30:00Z。前端如果用原生 Date 对象去接收,直接显示出来会是一行莫名其妙的英文日期。我的做法是后端在 Pydantic 模型里统一指定格式:

class CheckInOut(BaseModel): check_in_date: str ...

然后从 ORM 对象转 dict 时展开成date.strftime("%Y-%m-%d")。这样前端拿到的是2025-06-01,直接用字符串渲染就行,根本不用 dayjs 解析。

金额字段的坑更隐蔽。FastAPI 序列化Decimal(10, 2)时,默认会输出成字符串"23.50"而不是数字23.5。前端表格展示没问题,但如果你要做金额汇总计算,字符串参与运算就会出错。我推荐前端拿金额字段后统一走一个formatMoney工具函数,入参声明为string | number,内部用 Number 转换后再运算、再格式化输出。别指望后端给你数字类型,因为 Decimal 转 float 又有精度损失,两害相权,字符串 + 前端转换反而最稳定。

4.3 状态枚举不一致

这是联调期最容易出现 bug 的地方。后端房间状态的取值是available / occupied / repair,前端组件里写的却是"可用" / "已满" / "维修中",结果就是前端传参时传的是中文,后端直接校验报错。或者后端改了枚举值,前端没同步更新,界面上所有房间都变成灰色不可用。

这些问题靠沟通效率太低。我的解决方案是后端提供一份字典接口:

GET /api/dicts

返回所有枚举取值及其说明,前端在全局状态里拉取一次,生成映射表。页面渲染时通过映射表把英文取值翻译成中文标签;提交表单时反向翻译成英文。这套机制建好后,前后端枚举再也不用手动同步,改后端枚举值,前端刷新页面自动适配。

此外,前后端要约定枚举值只允许“后端定义的所有合法取值”。前端输入枚举值前先向后端字典接口校验,避免出现“改了一处类型之后其他全是 undefined”的连锁问题。

4.4 axios 拦截器与 token 过期刷新

axios 拦截器是前后端联调的最后一个大坑。我的拦截器分两层:请求拦截器自动给请求头加 token;响应拦截器统一处理业务码、HTTP 错误和 token 过期。

service.interceptors.response.use( (response) => { const res = response.data if (res.code !== 0) { ElMessage.error(res.message || "请求失败") return Promise.reject(new Error(res.message)) } return res.data }, (error) => { if (error.response?.status === 401) { localStorage.removeItem("token") router.push("/login") } ElMessage.error(error.response?.data?.detail || "网络异常") return Promise.reject(error) } )

token 过期刷新的逻辑我没做太复杂:因为内部系统 token 有效期设成 24 小时,上班时间基本不会过期。真过期了就重新登录,成本和风险都可控。如果你要在 token 过期时静默刷新,需要额外维护 refresh_token 的接口和并发请求队列,复杂度会上一个台阶,对于这种后勤系统,我认为不值得。

5. 从开发机到服务器:部署上线那点事

5.1 服务器环境与数据库选型

我部署用的是一台 2C4G 的云服务器,跑 CentOS 系统。小系统的后端完全够用。数据库这里有一个现实而微妙的取舍:数据量不大时用 SQLite 就够了,省去一台数据库服务器的维护成本。但 SQLite 在并发写入场景下表现确实较弱,如果公司人多、同时办理入住退宿和录入水电费,可能会出现database is locked的错误。

我的建议:系统上线初期月活不超过 200 人,用 SQLite 加 WAL 模式完全能撑住。等到真的数据量大了,后端代码里把 SQLAlchemy 的连接串从 SQLite 换成 PostgreSQL 即可,模型代码不用改。切换成本存在于部署环境,而不是代码层。

5.2 后端进程托管

后端用 uvicorn 启动只适合开发调试,生产环境需要 gunicorn 配合 uvicorn worker。写一个 systemd 服务文件:

[Unit] Description=Dormitory API Server After=network.target [Service] User=www WorkingDirectory=/opt/dormitory-api Environment="PATH=/opt/dormitory-api/venv/bin" ExecStart=/opt/dormitory-api/venv/bin/gunicorn main:app \ -k uvicorn.workers.UvicornWorker \ -w 2 \ -b 127.0.0.1:8000 Restart=always [Install] WantedBy=multi-user.target

一个容易忽略的点:gunicorn 的 worker 数量不要盲目设成CPU 核心数 * 2 + 1。2 核服务器用 2 个 worker 就够,每个 uvicorn worker 是事件循环模型,本身就能处理大量并发,worker 数设太多反而增加内存占用。内存只有 4G 的服务器,开 4 个 worker 很有可能 OOM。

启动服务后用curl http://127.0.0.1:8000/api/dashboard/summary测一次,能通再进入下一步。

5.3 前端构建与 Nginx 反向代理

前端打包:

pnpm build

产物在dist/目录,上传到服务器的/opt/dormitory-web,然后配置 Nginx:

server { listen 80; server_name your-domain.com; root /opt/dormitory-web; index index.html; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }

try_files那行很重要——Vue Router 用的 history 模式,前端路由跳转刷新页面时,Nginx 要回退到 index.html,否则刷新二级页面会 404。如果你不想处理这个问题,可以直接改用 hash 模式,但 URL 多一个#,观感差一些。

数据库备份我用最简单的方案:每天凌晨用 cron 执行一次 SQLite 文件复制到备份目录,保留 7 天。PostgreSQL 的话可以用pg_dump。小系统的备份策略不需要复杂,只要能防住误删除和服务器故障就够。

6. 这套系统还能怎么长:几个我推荐的扩展方向

6.1 与企业微信/钉钉通知打通

报修工单状态变化,目前要靠用户刷新页面才能看到。可以对接企业微信群机器人或者钉钉自定义机器人,通过 webhook 发消息:提交工单时通知宿管员,工单完成时通知报修人。这一块用 FastAPI 的 BackgroundTasks 做异步发送即可,不用引入消息队列。实际上一个简单的定时扫描加回调推送,就能满足内部通知需求。

6.2 门禁与人脸识别联动

如果宿舍楼本身有门禁系统,可以考虑把入住状态同步到门禁白名单。员工办退宿后,门禁权限自动失效。技术方向上,后端定时任务读取当天的入住变动记录,把结果写入门禁系统的接口。这个场景比“人脸识别整套方案”现实得多——很多公司已经在用第三方门禁平台,系统只需要对接现有 API,而不是从零做一套识别算法。

6.3 数据大屏与月度报表

宿舍管理员喜欢看大屏。用 ECharts 做入住率趋势、各楼栋人员分布、水电费环比等展示。技术实现不复杂,前端增加一个只读的大屏路由,后端提供聚合统计接口即可。更有价值的是月度报表导出——把本月入住变动、费用数据、工单完成率汇总成一个 Excel,用 Python 的 openpyxl 库坐在后端生成,管理员每个月月底下载一次。我在实际使用中觉得,报表导出比线上看数据更重要,因为行政流程需要留档。

这套系统开发下来,真正耗费精力的部分不是增删改查,而是把宿舍管理中的真实规则抽象成数据结构。如果你打算复刻,我建议先反向梳理一遍自己公司的住宿流程,把“换房怎么计费”“水电公摊怎么算”“临时借宿算不算占用床位”这些口径确定了再动手,否则前端写得再漂亮,业务规则对不上也是白搭。我个人的习惯是:哪怕时间紧,也先把流程清单写到一张 A4 纸上,再开始建表写代码,前期多花一天,后期能省一周。

返回列表