基于Flask+Vue的房屋租赁管理系统设计与前后端联调实践 做房屋租赁这类管理系统的关键难点从来不是 CRUD 本身而是业务状态怎么设计、前后端怎么接、权限怎么控制。一套完整的房屋租赁系统表面上只是“房源展示 订单管理”实际上会牵扯到用户角色、上架审核、租赁状态流转、文件上传、跨域联调、部署上线这一整条链路。我最近正好把一套基于 Flask 的房屋租赁系统完整过了一遍从前端 Vue 页面到后端 API再到 PyCharm 联调踩了不少坑也梳理出一些可以直接照抄的方案。先说明一下这个项目的定位。它本质上是前后端分离的租赁管理平台前端负责展示房源、提交订单、管理个人信息后端负责处理业务逻辑、数据存储和权限校验。技术栈上后端以 Flask 为主前端用 Vue开发工具用 PyCharm。这里要提一个很多人看到标题都会困惑的点标题里同时出现了 Flask 和 Django但实际工程里几乎不会把两个后端框架同时跑在一个系统里。这多半是写题目时把“框架关键词”一起堆上去了我们可以把它理解成项目主体基于 Flask在模块组织和 ORM 设计上借鉴了一些主流框架的思路再用 Vue 把前台页面做出来。下文所有内容都按照这个技术路线展开。1. 项目整体设计思路拆解1.1 需求分析先理清用户、房东、管理员三种角色做这类系统最容易犯的错是一上来就写代码写到一半发现“房东怎么发布房源”“管理员怎么审核”这些业务关系根本没理顺。房屋租赁系统最核心的不是页面多好看而是角色边界和业务状态清晰。从实际业务看系统至少应该包含三类角色普通租客也就是找房的人。能浏览房源、按条件筛选、查看详情、提交租赁申请、管理自己的订单和收藏。房东也就是发布房源的人。能发布房源、上下架房源、查看自己房源收到的租赁申请并处理。管理员后台运营人员。能审核房源、管理用户、处理异常订单、查看系统概览数据。1.2 技术选型前后端分离为什么选 FlaskVue有人会问既然写管理系统直接 Django 自带模板和 admin 后台不是更快吗为什么还要用 Flask Vue 做前后端分离这个选择背后是有实际原因的。Flask 的优势在于轻和灵活。它的核心只是路由和视图其余功能全通过扩展组合对于课程设计、中小型个人项目来说不会引入大量用不到的东西代码结构容易被看清楚。而 Vue 负责前端渲染之后后端只需要提供 JSON 接口页面和后端逻辑完全解耦后期想换前端框架或者加一个小程序端后端可以几乎不动。标题里那个 Django 不必太较真。很多项目描述在写选型时会把主流框架都堆上去实际落地时 Alice 用 Flask 写了正交、Bob 用 Django 写了别的模块这在团队项目里偶尔也会发生。我的建议是单个学员完成系统时后端选一个就好避免维护两套后端代码。这篇文章以 Flask 为主路线在项目结构上反而会借鉴 Django 的“按模块拆 App”思路让 Flask 小型项目也能保持清晰这一点后面讲蓝图时会体现。1.3 数据库建模4 张核心表搞定租赁业务数据库设计是整个系统的地基。我见过有同学把所有字段塞进一张大表后面做订单状态流转时痛苦不堪。房屋租赁系统的核心数据其实可以收敛成四张表用户表、房源表、订单表、收藏表。用户表存储三类角色的公共字段用 role 字段区分class User(db.Model): __tablename__ users id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) role db.Column(db.String(20), defaulttenant) # tenant / landlord / admin phone db.Column(db.String(20)) create_time db.Column(db.DateTime, defaultdatetime.utcnow)房源表关联房东用户同时记录房源的状态。状态字段建议用整型或短字符串不要用布尔值因为“待审核”“已上架”“已下架”是三种状态布尔值表达不了class House(db.Model): __tablename__ houses id db.Column(db.Integer, primary_keyTrue) owner_id db.Column(db.Integer, db.ForeignKey(users.id), nullableFalse) title db.Column(db.String(120), nullableFalse) description db.Column(db.Text) price db.Column(db.Numeric(10, 2), nullableFalse) # 月租 area db.Column(db.Float, default0) # 平米数 room_num db.Column(db.String(20)) # 房型如 1室1厅 address db.Column(db.String(255)) # 小区地址 status db.Column(db.Integer, default0) # 0待审核 1已上架 2已下架 create_time db.Column(db.DateTime, defaultdatetime.utcnow)订单表是租赁关系的核心关联房源、租客、房东状态字段需要仔细设计。我的建议是0 待房东确认、1 租赁中、2 已退租、3 已被拒绝、4 租客取消。这个状态机基本覆盖了租赁全流程。收藏表只需要关联用户和房源两个外键加上创建时间用于“我的收藏”功能。实际建表时再加一个重复收藏的唯一约束避免用户多次点击收藏出现多条脏数据。数据库方案上本地开发可以用 SQLite 零配置起步但建议从最开始就按 MySQL 的连接方式写配置后面切换成本更低。SQLAlchemy 的模型代码是通用的切数据库只要改连接串。2. 后端 Flask 核心实现2.1 项目结构与蓝图拆分别再把路由全塞一个文件Flask 官方入门教程通常把路由写在 app.py 一个文件里小 demo 没问题但房屋租赁系统有十几个接口之后单一文件会变成灾难。我这边推荐的目录结构长这样flask_house/ ├── app.py # 应用入口创建 app、注册蓝图 ├── config.py # 配置数据库连接、密钥、token 过期时间 ├── models.py # 数据模型集中管理 ├── extensions.py # 单独实例化 db、jwt避免循环导入 ├── blueprints/ │ ├── auth.py # 注册登录 │ ├── house.py # 房源相关 │ ├── order.py # 订单相关 │ └── admin.py # 管理后台为什么要单独建一个 extensions.py因为 SQLAlchemy 实例和 JWT 实例如果直接放在 models.py 或 app.py 里蓝图导入模型时很容易出现循环导入。单独一个文件用来实例化扩展谁需要谁导入这是一个能减少很多头疼问题的习惯。蓝图注册放在入口文件里from flask import Flask from extensions import db, jwt from blueprints import auth, house, order, admin app Flask(__name__) app.config.from_object(config) db.init_app(app) jwt.init_app(app) app.register_blueprint(auth.bp, url_prefix/api/auth) app.register_blueprint(house.bp, url_prefix/api/house) app.register_blueprint(order.bp, url_prefix/api/order) app.register_blueprint(admin.bp, url_prefix/api/admin)url_prefix 把每个模块的接口前缀都定好联调阶段看接口路径就会很清晰。例如房源接口就是 /api/house/list、/api/house/detail/1不用猜是哪个模块。2.2 用户注册登录密码加密与 JWT 鉴权登录注册是几乎所有系统的第一个接口但很多新手会在密码存储上犯大错直接用明文存数据库或者自己写一个简单的哈希。正确做法是用 werkzeug.security 自带的密码加密方法它内部会加盐安全性比自己写的拼接字符串可靠得多。from werkzeug.security import generate_password_hash, check_password_hash auth.bp.route(/register, methods[POST]) def register(): data request.get_json() username data.get(username) password data.get(password) if not username or not password: return jsonify({code: 400, msg: 用户名和密码不能为空}), 400 if User.query.filter_by(usernameusername).first(): return jsonify({code: 400, msg: 用户名已存在}), 400 user User(usernameusername, roletenant) user.password_hash generate_password_hash(password) db.session.add(user) db.session.commit() return jsonify({code: 200, msg: 注册成功})登录接口则校验密码后签发 token。前后端分离项目中看到很多用 session cookie 的方案但那套方案在前端部署在不同域名下时会出现各种 session 不同步问题。我建议直接用 flask-jwt-extended签发 JWT 字符串前端存在本地存储里每次请求带在请求头中干净利落。from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity auth.bp.route(/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if not user or not user.check_password(data.get(password)): return jsonify({code: 400, msg: 用户名或密码错误}), 400 token create_access_token(identity{id: user.id, role: user.role}) return jsonify({code: 200, data: {token: token, role: user.role}})这里有个细节不要在 token 里塞太多用户信息只放 id 和 role 就够了。其他信息每次请求从数据库查JWT 一旦签发不能主动失效塞太多敏感信息在里面反而增加风险。2.3 房源接口列表分页、关键词搜索、上下架状态房源模块是整个系统接口量最大的部分。除了新增房源还需要列表、详情、搜索、筛选、上下架。这里说两个核心点。第一个是列表接口的分页。不要一次性把全表数据返回给前端数据量小看不出来一旦房源上千条页面会卡死网络传输也会很慢。用 Flask-SQLAlchemy 内置的 paginate 方法house.bp.route(/list, methods[GET]) def house_list(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 10, typeint) keyword request.args.get(keyword, ) query House.query.filter(House.status 1) # 只展示已上架房源 if keyword: query query.filter(House.title.contains(keyword)) pagination query.order_by(House.create_time.desc()).paginate(pagepage, per_pageper_page, error_outFalse) return jsonify({ code: 200, data: { items: [house.to_dict() for house in pagination.items], total: pagination.total, page: page, per_page: per_page } })返回的数据里加上 total、page、per_page前端才能正确渲染分页组件不然页码永远只有一页。第二个是状态流转。房东发布房源时状态默认 0 待审核管理员审核通过后变成 1 已上架房东也可以把房源变成 2 已下架。这三个状态的切换接口必须做权限校验不能普通租客调一下接口就把房源下架了。用 JWT 装饰器加身份判断house.bp.route(/int:house_id/offline, methods[POST]) jwt_required() def offline(house_id): current_user get_jwt_identity() house House.query.get_or_404(house_id) if house.owner_id ! current_user[id] and current_user[role] ! admin: return jsonify({code: 403, msg: 没有权限操作该房源}), 403 house.status 2 db.session.commit() return jsonify({code: 200, msg: 已下架})这种“先取到资源再判断当前用户是否和资源有关联”的写法是权限控制里最常见也最稳妥的方式。2.4 订单接口租赁状态流转与权限校验订单模块是业务上最容易乱的部分。先说创建订单租客在详情页点击“申请租房”后端生成一条订单记录状态为 0 待房东确认。这里要注意一个校验同一房源同一时间段不能重复下单否则会出现一房多卖的情况。简单实现可以在创建前查一下是否已存在状态在 0 或 1 的订单。order.bp.route(/create, methods[POST]) jwt_required() def create_order(): current_user get_jwt_identity() if current_user[role] ! tenant: return jsonify({code: 403, msg: 只有租客可以发起租赁申请}), 403 data request.get_json() house_id data.get(house_id) house House.query.get_or_404(house_id) if house.status ! 1: return jsonify({code: 400, msg: 该房源当前不可租}), 400 existed Order.query.filter( Order.house_id house_id, Order.tenant_id current_user[id], Order.status.in_([0, 1]) ).first() if existed: return jsonify({code: 400, msg: 你已申请过该房源请勿重复申请}), 400 # 创建订单并提交 order Order(...) db.session.add(order) db.session.commit() return jsonify({code: 200, msg: 申请成功等待房东确认})房东端处理申请时操作的是“确认”和“拒绝”两个动作本质都是更新订单状态。租客退租也是更新状态。我把所有状态的变化收敛成少数几个接口明确标注当前状态和目标状态避免前端直接传一个任意状态值给后端改库。后端永远不要把“前端传什么就存什么”当作常态状态值的正确性最终要由后端兜底。3. 前端 Vue 核心实现3.1 工程搭建Vite 初始化与目录规划前端部分如果还停留在 Vue2 Vue CLI 的旧模式现在新项目建议直接用 Vue3 Vite构建速度快生态也主流。Vite 创建项目只需要一行命令npm create vitelatest house_front -- --template vue创建完项目后目录里我会额外建这几个目录src/api 放 axios 封装和接口定义src/router 放路由配置src/store 放用户状态src/components 放页面级复用组件src/views 放页面。这里有个小建议路由的 name 字段一定要定义。Vue Router 在编程式导航时用 name 跳转比写死路径更抗重构。比如房源详情页路由可以这么配{ path: /house/:id, name: HouseDetail, component: () import(../views/HouseDetail.vue) }3.2 axios 封装统一处理 token 和错误提示前后端分离后axios 是前端请求后端的唯一通道。如果每个页面都自己写 axios 实例token 处理会到处重复。正确做法是封装一个 request.js统一拦截器。import axios from axios const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default requestbaseURL 这里先写 /api配合 Vite 的 proxy 转发到 Flask开发环境跨域问题就解决了一半。请求拦截器统一注入 token响应拦截器统一处理 401这两段代码能让你少写几百行重复逻辑。3.3 核心页面拆解房源列表页、详情页、订单页房源列表页的核心是一张卡片网格加一个分页器。卡片组件接收一个 house 对象展示标题、价格、房型、面积和小区地址。列表页只需维护 page 和 keyword 两个响应式变量切换页码或搜索关键词时重新请求接口。详情页要注意的是按钮逻辑。页面会同时存在“申请租房”“编辑房源”“房源已出租”三种状态具体显示什么按钮要由用户的角色和房源状态共同决定。让后端返回当前用户对该房源的操作权限会比前端自己猜逻辑要可靠。我用一个 join 查询判断该房源的 owner_id 是否等于当前登录用户然后返回 canEdit、canApply 布尔值前端按布尔值渲染按钮。订单页分布在租客端和房东端。租客端看到的是自己发起的申请记录房东端看到的是自己房源收到的申请。字段是同一份订单表但列表查询条件不同。前端可以共用同一个订单表格组件用 props 传查询接口和状态映射表。3.4 管理后台视角角色路由与权限控制后台管理页面我建议和前台页面共用一套登录体系但在路由层面区分。加一个 meta 字段记录需要的角色在路由守卫里读取当前用户角色做拦截。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next({ name: Login }) return } if (to.meta.role localStorage.getItem(role) ! to.meta.role) { next({ name: Home }) return } next() })taiya 这种方式简单有效但需要注意角色判断只在前端做并不能保证安全真正的权限校验始终要在后端完成。前端路由守卫的作用是提升体验不是安全边界。4. PyCharm 配置与前后端联调实录4.1 PyCharm 虚拟环境与启动配置PyCharm 在这套系统里主要承担后端 Python 的开发调试。新建项目时直接用 PyCharm 创建虚拟环境选择一个 Python 3.8 以上的解释器然后安装依赖pip install flask flask-sqlalchemy flask-jwt-extended flask-cors pymysql依赖装好后配置运行入口编辑 Run Configuration将 script path 指向 app.pyEnvironment variables 加上数据库连接字符串然后点击运行。PyCharm 的调试工具比 print 方便太多。在业务接口处打一个断点点击右上角爬虫图标启动 Debug 模式能逐行看到请求参数和数据库查询结果。前后端联调中如果遇到前端传参为空我会先在 request.get_json() 那行打断点看 data 到底是什么这比在前端一遍遍 console.log 效率高得多。4.2 CORS 跨域问题的根因与两种解法前后端分离后Flask 运行在 8000 端口Vite 开发服务器运行在 5173 端口浏览器认为这是两个不同的源就会触发跨域限制。这个问题让很多新手崩溃页面看起来一切正常控制台却报 CORS error。解法有两种我建议开发阶段直接走 Vite proxy。在 vite.config.js 里配置server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } }这样前端代码里的 /api 请求会被 Vite 服务器代理转发到 Flask浏览器看到的请求是同源的跨域问题自然消失。如果前端不打算用 proxy而是在生产环境把打包后的静态文件交给某个静态服务器托管同时后端接口在另一个域名那就必须启用 flask-corsfrom flask_cors import CORS CORS(app, resources{r/api/*: {origins: *}})生产环境不要把 origins 设置成 *最好配置成自己前端域名的白名单这是我在真实项目里被安全提醒过才补上的。4.3 联调中的接口约定与数据格式规范前后端各做各的之后接口格式不统一是最大的沟通成本。我强烈建议从第一天就定一个返回格式规范。项目里我统一用的是{ code: 200, msg: success, data: {} }错误时对应的 code 分别为 400 参数错误、401 未登录、403 无权限、404 资源不存在。前端 axios 的响应拦截器只做基础处理具体业务判断统一看 code 字段而不是 HTTP 状态码。数据格式的问题同样不能忽视。后端返回日期格式如果直接是 datetime 对象jsonify 会报错需要在模型里写序列化方法把 datetime 转成字符串。价格字段是 Decimal 类型json 序列化也会报错转成 float 或者保留两位字符串。这些在联调阶段都能用一面常见请求报错解决但定义好 to_dict 方法可以从源头规避def to_dict(self): return { id: self.id, title: self.title, price: float(self.price), create_time: self.create_time.strftime(%Y-%m-%d %H:%M:%S) }5. 常见问题与排查技巧实录5.1 高频报错速查表整个项目从搭建到联调最容易遇到的报错基本集中在下面这张表。把这些问题列出来也算是我自己踩坑路上的一个小总结现象常见原因解决办法前端请求返回 401token 未带或已过期检查 axios 请求拦截器确认 Authorization 头正确页面报跨域错误未配置 proxy 或 flask-cors开发用 Vite proxy生产用 CORS 白名单数据库中文乱码连接字符集不对pymysql 连接串加 charsetutf8mb4后端返回对象无法 JSON 序列化datetime 或 Decimal 类型未处理给模型写 to_dict统一转字符串和 float端口被占用上次运行进程未退出命令行查找对应进程并结束或换端口前端页面刷新 404路由用了 history 模式且未配置回退开发环境配置 Vite 的 appType生产环境用 hash 模式或回退到 index.html注册成功后无法登录密码加密时使用了不同字段名确认 register 与 login 中密码字段一致这些报错中最常见也最容易忽略的是第二个和第六个因为它们属于“环境问题”而不是“代码问题”。第一次遇到时不妨先判断是不是重启后端后端口没释放是不是前端静态服务器没有把路由回退到 index.html往往解决方案比想象中简单。5.2 关于房源图片和静态资源的一点点经验房源展示必须有图片但图片处理容易被忽略。项目前期为了省事我直接把图片存到后端静态目录数据库只存文件名看起来可行但后来发现两个问题一是后端重启后图片路径容易乱二是前端如果要传 base64 图片给后端请求体太大接口会慢得离谱。最终的稳妥方案是后端提供图片上传接口用 Flask 内置的 request.files 接收文件限制文件类型和大小保存到独立的上传目录再通过静态路由 /static/upload/xx.jpg 访问。前端上传时用 FormData 而不是 JSON这是很多人容易踩的坑const formData new FormData() formData.append(file, file) request.post(/house/upload, formData)另外房源列表页和详情页对图片的需求不同列表页加载大量高清大图会非常卡建议前端在列表页使用缩略图或懒加载详情页才加载原图。这个优化不做页面在本地开发时没感觉部署到服务器后带宽一紧张差别立刻体现出来。最后的最后说点个人体会。房屋租赁系统这种项目技术难度不算高真正考验人的是“业务状态不乱、边界条件不漏、前后端约定清楚”。如果你从头到尾自己敲一遍把用户权限、订单流转、文件上传这几种常见场景都经历过后续再去做电商、二手平台、预约类系统会发现大部分套路是相通的。把这些基础打牢比追着新框架跑要实在得多。