简介:基于Flask与MySQL实现的租房后台系统完整源码包,面向正在学习Python Web开发、需要完成课程设计或毕业设计的开发者。项目集成了支付宝支付功能,覆盖租客管理、房源信息维护、订单处理等典型后台业务,适合作为Flask框架综合练习与二次开发模板。压缩包共37个文件,包含19个Python源码文件、部署文档、数据库脚本、密钥文件、图片素材等,其中py文件承载核心业务逻辑,md文档说明环境配置与运行流程,sql文件提供初始化数据,整体仅478KB,结构清晰便于查阅。已有104人学习下载。借助部署文档可直接搭建运行环境,快速理解Flask蓝图、MySQL ORM、支付接口对接等关键环节,同时可通过已有业务代码学习后台系统分层、登录鉴权与异常处理的实现思路,是上手真实项目较实用的参考资料。
1. 租房后台这套 Flask+MySQL 项目,解压之后从哪下手
接手一份 "Flask+MySQL 租房后台系统源码+部署文档+全部数据资料(含支付宝支付)",多数人第一反应是 python run.py,跑不起来才翻文档。这套系统的价值不在界面,而在串起 Web 后台最常见的三条链路:租客与房东账号、房源与订单流转、支付异步通知。想系统走一遍 Python 全栈的人,它是能对着改的参照物。
Flask 负责路由组装,MySQL 承担持久化,支付宝支付把项目从"后台演示"推到"能真实下单",三者结合点恰是新手最容易卡的位置:蓝图划分、SQLAlchemy 建模、支付签名与回调验签。下文按骨架、表设计、支付对接、部署验证展开,读完能回答解压后先看什么、改哪里能跑、回调翻车查哪里。
2. Flask 项目骨架与 MySQL 连接配置:先读懂目录再改连接串
2.1 zip 包里的分层目录,先读这几处再动手
解开压缩包后一般能看到这样一组目录,具体命名可能不同,但分层逻辑接近:
rental_backend/ ├── app/ │ ├── __init__.py # 应用工厂:初始化 db 并注册蓝图 │ ├── config.py # 环境配置(连接串、密钥、支付参数) │ ├── models/ # SQLAlchemy 模型,按业务拆文件 │ ├── views/ # 蓝图路由,auth/house/order/pay 分开 │ └── utils/ # 支付签名、统一响应、权限装饰器 ├── sql/ │ └── rental.sql # 建库建表脚本 + 演示数据(全部数据资料) ├── deploy_docs/ # 部署文档说明 ├── requirements.txt └── run.py # 本地启动入口建议按 config.py → requirements.txt → sql/rental.sql 的建表部分 → run.py 的顺序读。前两个决定环境能不能起来,第三个决定数据库结构和演示数据落到哪,最后一个决定启动端口和 debug 开关。大多数"跑不起来"的报错都集中在这四个文件的衔接上:requirements.txt 缺 PyMySQL、config.py 里 host 写成了远端地址、SQL 文件没有完整导入 MySQL 8,这些都是先要排除的。
2.2 应用工厂与蓝图装配,避免循环导入
Flask 项目一旦拆成多个模块,最典型的问题是 models 里 import db、views 里又 import models,最后谁先初始化谁都说不清。常见做法是应用工厂(App Factory),把 app 的创建和扩展初始化解耦:
# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from config import Config db = SQLAlchemy() def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) db.init_app(app) from app.views.auth import auth_bp from app.views.house import house_bp from app.views.order import order_bp 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") return app逻辑说明:SQLAlchemy 实例在模块顶层创建,却不绑定任何 app,create_app 被调用时才通过 init_app 绑定。蓝图在函数内部 import 而不是写在文件顶部,这样 app 尚未创建时视图模块不会提前执行,循环导入报错随之消失。url_prefix 把同类接口统一挂到 /api/house 这类路径下,调试时看路由表也清楚,run.py 里可以这样收尾:
# run.py from app import create_app app = create_app() if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)create_app 每次生成新实例,测试时还能直接拿 app.test_client() 模拟请求,不需要单独起服务。
2.3 MySQL 连接串、连接池与字符集三件套
config.py 里与 MySQL 相关的配置通常是这样的:
class Config: SECRET_KEY = "please-change-me" SQLALCHEMY_DATABASE_URI = ( "mysql+pymysql://root:your_password@127.0.0.1:3306/" "rental_db?charset=utf8mb4" ) SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_ENGINE_OPTIONS = { "pool_size": 10, "pool_recycle": 3600, "pool_pre_ping": True, }连接串形如 驱动://用户:密码@地址:端口/库名?charset=utf8mb4,驱动必须写成 mysql+pymysql 而不是 mysql,PyMySQL 是纯 Python 实现的客户端,pip 安装后即可用,不依赖系统编译。charset=utf8mb4 必填,省略时新建会话落到 MySQL 默认字符集,房源标题里出现生僻字或 emoji,写入会报 Incorrect string value。连接池三个参数的作用如下:
| 参数 | 作用 | 建议值 |
|---|---|---|
| pool_size | 连接池保持的连接数 | 小型后台 10 即可 |
| pool_recycle | 连接超过该秒数强制回收 | 3600,避免 wait_timeout 断连 |
| pool_pre_ping | 取连接前先 ping 探活 | True,配合 pool_recycle 使用 |
补充一个易踩坑点:MySQL 8.0 默认认证插件是 caching_sha2_password,旧版本 PyMySQL 连接时直接报认证插件不支持。两种解法,升级 PyMySQL 到支持该插件的新版本,或建库用户时指定 mysql_native_password;取决于部署文档里 MySQL 版本的约定,不要上来改 my.cnf。连接池参数里最容易被忽略的是 pool_pre_ping,没开时 MySQL 空闲断连后 Flask 会偶发 2006 报错,属于典型的间歇性故障。
3. 租房后台核心表设计:用户、房源、订单与支付流水
3.1 从 rental.sql 看四张表的关系和字段取舍
sql/rental.sql 是"全部数据资料"里最先要导入的文件。租房后台至少四张核心表,关系一句话概括:一个房东拥有多套房源,一个房源能被多个租客下订单,每个订单关联一条支付流水。演示数据的字段设计一般带冗余和枚举值,导入后先看清再决定要不要调整:
| 表名 | 职责 | 关键字段设计 |
|---|---|---|
| user | 租客与房东统一账号 | id、mobile(唯一索引)、role(0租客/1房东)、status |
| house | 房源 | id、owner_id(FK user.id)、title、price(Decimal)、status |
| house_order | 租房订单 | id、house_id、tenant_id、start_date、end_date、amount、status |
| payment_record | 支付流水 | id、order_id、out_trade_no(唯一)、total_amount、pay_time |
建表时最容易出错的是外键约束和唯一索引。payment_record 的 out_trade_no 是支付宝交易号的本地关联字段,后续支付回调靠它定位订单,必须建唯一索引;house_order 的 house_id 与 status 组合是房东端列表查询的高频谓词,值得加组合索引。演示数据里的日期字段保持 Date 类型,租期按天计算,没必要用 DateTime,租期场景里跨时区全是噪声。
3.1.1 导入 SQL 数据的一句话命令
没有 Workbench 等图形客户端时,命令行导入一条命令就够:
mysql -uroot -p rental_db < sql/rental.sql先执行 CREATE DATABASE rental_db DEFAULT CHARSET utf8mb4,再执行上面这条重定向导入。导入报错多半是 SQL 文件里带了 USE 语句或字符集声明不完整,在文件开头补一行 SET NAMES utf8mb4 即可。导入后验证三件事:四张表行数不为 0、外键能正常关联、out_trade_no 上能看到唯一索引。
3.2 Flask-SQLAlchemy 建模:金额用 Numeric,日期用 Date
模型层与 SQL 文件对应,以订单表为例:
# app/models/order.py from datetime import date from app import db class HouseOrder(db.Model): __tablename__ = "house_order" id = db.Column(db.Integer, primary_key=True) house_id = db.Column(db.Integer, db.ForeignKey("house.id"), nullable=False) tenant_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) start_date = db.Column(db.Date, nullable=False) end_date = db.Column(db.Date, nullable=False) amount = db.Column(db.Numeric(10, 2), nullable=False) status = db.Column(db.SmallInteger, default=0, index=True)两个容易写错的点。金额字段不要用 Float,支付金额在元与分之间转换,Float 的二进制表示误差在累计对账时会暴露成几分钱的差异;Numeric(10, 2) 让 MySQL 用定点数存储,回传后是 Decimal,和支付宝通知里的金额字符串比较时先转 Decimal,就不会踩浮点比较的坑。status 用 SmallInteger 配注释,不用字符串枚举,回调更新只做 int 比较,少一层映射。
Date 与 DateTime 的选择同理:租房订单只需要日历日期,DateTime 会引入零点偏移和时区概念。计算租期直接用 (end_date - start_date).days,跨月也不用手算天数,这是 Date 类型自带的行为。
3.3 联表统计的查询写法与索引口径
房东端最常用的统计是"我这个月出租了几套、收入多少",典型写法如下:
# 统计每个房东已支付订单的金额合计 from sqlalchemy import func from app import db from app.models import House, HouseOrder rows = ( db.session.query(House.owner_id, func.sum(HouseOrder.amount)) .join(HouseOrder, HouseOrder.house_id == House.id) .filter(HouseOrder.status == 1) .group_by(House.owner_id) .all() )join 条件写在第二个参数里,相当于 ON HouseOrder.house_id = House.id;func.sum 在 SQL 层完成聚合,而不是把订单拉回 Python 再求和,数据量上万时二者性能差一个数量级。查询只覆盖 HouseOrder.status 与 House.owner_id 两个字段,前者有索引、后者是主键,扫描路径基本是索引覆盖,不需要额外优化。
这类查询最常见的反模式是 N+1 问题:先查出所有房源,再循环查每个房源的订单,Flask-SQLAlchemy 里有 relationship 字段时尤其容易触发。调试时把 config 里的 SQLALCHEMY_ECHO 设为 True,控制台会打印每条 SQL,看到循环查询就改成 join 或 selectinload 预加载。租期校验则要留心重叠加订:新订单与已有订单日期区间冲突时,先查 NOT EXISTS 再落库,这属于业务层约束,模型层不用管。
4. 支付宝支付功能对接:下单签名、异步回调与状态机
4.1 先理清支付时序,再去注册沙箱应用
租房后台的支付链路比普通商品订单多一层异步,时序是固定的:租客提交订单 → 后端生成 out_trade_no 并组装支付参数 → 前端拿到参数跳转支付宝收银台或拉起二维码 → 用户完成付款 → 支付宝服务器向 notify_url 发送异步通知 → 后端验签后更新订单状态。同步跳转的 return_url 只做页面展示,不能作为订单已支付的依据,这一点部署文档通常写过,但实操中最容易忽略。
对接前准备沙箱环境。支付宝开放平台的沙箱(网关地址 openapi.alipaydev.com/gateway.do)提供独立的 app_id、应用私钥和支付宝公钥,沙箱买家账号余额是虚拟的,可以反复测订单闭环。注意沙箱 app_id 与正式环境的 app_id 不通用,密钥也要重新生成,git 提交项目时不要把私钥文件提交上去,常见做法是把密钥路径写进 config.py 的占位符,真实值放本地环境变量。
4.2 alipay.trade.page.pay 下单参数与 RSA2 签名
后台管理系统用得最多的是 alipay.trade.page.pay(电脑网站支付)。组装参数有个铁律:除 sign、sign_type 外的所有参数按 key 的 ASCII 升序排列,拼成 k=v 用 & 连接,再对原串做 RSA2(SHA256)签名:
import json from datetime import datetime from urllib.parse import urlencode GATEWAY = "https://openapi.alipaydev.com/gateway.do" # 上线换成正式网关 def build_pay_form(order_no, amount, subject, notify_url, app_id, private_key): biz_content = json.dumps( { "out_trade_no": order_no, "total_amount": "%.2f" % amount, "subject": subject, "product_code": "FAST_INSTANT_TRADE_PAY", }, ensure_ascii=False, ) params = { "app_id": app_id, "method": "alipay.trade.page.pay", "charset": "utf-8", "sign_type": "RSA2", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "version": "1.0", "notify_url": notify_url, "biz_content": biz_content, } sign_str = "&".join(f"{k}={params[k]}" for k in sorted(params)) params["sign"] = rsa2_sign(sign_str, private_key) return GATEWAY + "?" + urlencode(params)rsa2_sign 内部用 Crypto.Signature.pkcs1_15 配 SHA256 对 sign_str 签名,再 Base64 编码。这里容易踩三处:total_amount 必须转成两位小数字符串,Decimal 直接拼进去会带科学计数法;biz_content 里 JSON key 顺序不影响签名,因为签名对象是整个 biz_content 字符串;urlencode 的编码层级作用于整个 URL,而签名串用的是编码前的原文,不能在拼接 URL 之后再拿编码结果去签名。生产环境建议直接用官方 SDK 或 python-alipay-sdk 封装,手写代码的价值在于理解签名过程和排查返回的 sign check fail。
4.3 notify_url 回调验签与幂等更新
异步回调是支付功能里最需要仔细写的部分。支付宝通知以 POST 表单形式发送到 notify_url,验签方向与下单签名相反:取出除 sign 之外的全部参数,同样按 ASCII 排序拼串,用支付宝公钥(注意不是应用公钥)验 RSA2 签名:
def handle_alipay_notify(form): data = form.copy() # request.form 不可变,先转可变副本 sign = data.pop("sign", "") params = {k: v for k, v in data.items() if v not in ("", None)} sign_str = "&".join(f"{k}={params[k]}" for k in sorted(params)) if not rsa2_verify(sign_str, sign, ALIPAY_PUBLIC_KEY): return "failure" if form.get("trade_status") != "TRADE_SUCCESS": return "success" # 非终态通知,先确认但不落库 order = HouseOrder.query.filter_by(out_trade_no=form.get("out_trade_no")).first() if not order or order.status != 0: return "success" # 重复通知,已处理过,直接确认 if Decimal(order.amount) != Decimal(form.get("total_amount")): return "failure" # 金额不符,拒绝落库 order.status = 1 db.session.commit() return "success" # 必须返回纯文本 success通知里的关键字段要对得上,否则回调静默失败:
| 字段 | 含义 | 校验要点 |
|---|---|---|
| out_trade_no | 商户订单号 | 与本地订单一一对应,走唯一索引 |
| trade_status | 交易状态 | 只有 TRADE_SUCCESS 才落库 |
| total_amount | 订单金额 | 与本地订单金额转 Decimal 再比较 |
| sign | 签名串 | 用支付宝公钥验 RSA2 |
回调处理里三个细节决定线上可靠性。第一,金额必须二次校验,支付宝通知的 total_amount 与本地订单金额不一致时拒绝更新,这是防止伪造回调的基本盘。第二,更新订单前先判断当前状态,只有待支付才更新为已支付,支付宝可能因网络原因重复推送同一笔通知,重复消费会把状态覆盖成旧值。第三,返回值必须是纯文本 success,不能返回 JSON 或空串,否则支付宝按失败策略在 24 小时内重试最多 8 次,重试本身无害,但日志里会刷出一屏干扰项。
订单状态机通常这样收敛:0 待支付 → 1 已支付 → 已完成,0 待支付 → 2 已取消。支付成功后若要跟进退款逻辑,在状态机上再加退费中的中间态。对账环节每天早上拉一次支付宝账单,用 out_trade_no 与本地 payment_record 做差集,找出"支付宝已扣款但本地未更新"的订单,这类单子是投诉高发区,靠回调日志补漏不现实,必须靠对账任务兜底。
5. 部署上线快速验证:gunicorn 启动和支付闭环自检
5.1 用 gunicorn 替代 flask run 的启动命令
本地 python run.py 验证的是调试服务器,生产环境要换 gunicorn 多进程承载。安装依赖后一条命令即可验证启动:
gunicorn -w 4 -b 0.0.0.0:5000 run:app-w 指定 worker 数,这个系统按 4 核机器给 4 个 worker 起步即可,不必超配;-b 绑定地址与端口,0.0.0.0 让内网其他机器也能访问,配合 nginx 把 80 端口的请求转发到 5000。启动后先 curl 一个健康检查接口,返回 200 且响应体是 JSON 结构,说明 Flask 应用与 MySQL 连接都通了,此时再进入支付自检。
5.2 上线前核对的三项配置与验证顺序
支付闭环的验证顺序是固定的:先用沙箱买家账号完成一笔完整流程的模拟支付,确认订单变为已支付,再切正式参数。上线前至少核对下表三项,任何一项不对,回调都会静默失败:
| 配置项 | 错误表现 | 检查方式 |
|---|---|---|
| notify_url | 必须是公网可访问的地址 | curl -I 带回调路径访问,非 200 即有问题 |
| MySQL 字符集 | 标题含生僻字写入报错 | 改一条演示房源标题再保存 |
| SECRET_KEY | 会话串号或外露 | 用环境变量注入,勿写死在 config.py |
最后强调机房部署时的时区问题:支付回调里的时间戳是东八区,数据库连接串如果加了 serverTimezone 一类的参数,容易产生八小时偏移,订单显示的支付时间与实际不符。验证时直接查 payment_record 的 pay_time 与支付宝截图时间对比,差八小时就去检查连接串时区,而不是改应用代码。
本文还有配套的精品资源,点击获取