
简介面向需要搭建Web管理后台的Python开发者这套基于Tornado框架的装修管理系统源码覆盖登录与角色权限、装修预约、工地信息、每日施工报告及员工档案等模块并通过加密传输保障数据安全适合毕业设计或课程实训参考。包内共59个文件、约4.19MB以png图片素材、py源码、pyc编译文件为主另含依赖清单、日志配置、证书密钥及说明文档解压即可对照学习。目前已有62人浏览学习。代码按数据库、静态资源、日志等目录划分数据库操作采用SQLAlchemy封装静态页面图片素材完整通过入口脚本可快速理解Tornado路由与请求处理流程前端展示与后端逻辑分离适合了解项目结构或权限控制的读者。1. 装修管理系统为什么值得选 Tornado 而不是 Flask/Django从一个源码包说起干装修这一行的人都知道工地上的混乱从来不在图纸上而在微信语音、Excel 排期和项目经理的脑子里。这套基于 Python 的 Tornado 框架的装修管理系统源码就是把客户、工地、材料、合同和施工进度从聊天记录里捞出来变成一套能查能改能审批的在线业务系统。Tornado 在 Python 的 Web 框架里不算大众但它天然支持异步和 WebSocket装修管理里最常见的“工长上报进度、项目经理实时看到”场景用 Tornado 写起来比 Flask 和 Django 都顺手得多。不吹不黑这套源码适合三类人想给自家装修公司做内部管理系统的人、接外包想快速交付的 Python 工程师以及刚学完 Python 基础、想看看真实项目长什么样的初学者。接下来我会按拿到这套源码后实际动手的顺序把项目结构、本地跑通、二次开发、部署避坑一次讲透。2. 先读懂这套装修管理系统的骨架项目结构与 Tornado 异步模型2.1 从 requirements.txt 到 Application 入口源码包怎么搭起来的网上流传的这套装修管理系统源码绝大多数是 zip 压缩包交付。解压之后最常见做法是先看目录而不是急着python app.py。我一般会先扫一眼有没有requirements.txt、README.md和入口文件这三个东西决定你能不能在三分钟内跑起来。site_manager/ ├── app.py # Tornado Application 入口路由在这里注册 ├── config.py # 数据库连接、端口、上传目录等全局配置 ├── models.py # SQLAlchemy 数据模型定义 ├── init_db.py # 建表脚本一般独立成一个文件 ├── handlers/ │ ├── __init__.py │ ├── auth.py # 登录、登录态校验 │ ├── customer.py # 客户管理相关 Handler │ ├── site.py # 工地进度、节点上报 │ └── material.py # 材料采购与入库 ├── templates/ # Tornado 模板文件 ├── static/ # css、js、上传的图片 └── requirements.txt # 依赖清单tornado/sqlalchemy/pymysql 等这个结构基本是 Tornado 项目的标准套路app.py里创建一个tornado.web.Application实例把 URL 正则表达式和对应的 Handler 类成对丢进去然后把模板目录和静态目录路径一块传给 settings。Tornado 不像 Django 那样有强制的 app 目录规范也不像 Flask 那样把路由装饰器写在函数头顶上它用正则路由匹配好处是一眼能看全所有 URL坏处是项目大了以后app.py会越来越臃肿。这套源码规模不大Handler 按业务拆到handlers/子目录里已经是相当规整的写法了。真正让新手头疼的是requirements.txt里的依赖版本。Tornado 6.x 和 5.x 的 API 差异不大但 Python 版本敏感。我见过好几次有人拿 Python 3.12 去跑老源码结果pymysql报兼容错误或者sqlalchemy的declarative_base导入路径变了。后面第三章我会专门讲环境怎么搭这里先记住一个原则源码包里requirements.txt锁定的版本能不动就别动除非你确定要升级哪个依赖。2.2 装修业务里的三个核心模块客户、工地、材料装修管理系统听起来业务面很宽但剥掉登录、权限、合同这些通用模块内核就是三个实体客户、工地、材料。源码里的数据模型也基本围绕这三张表展开理清了它们的关系你就知道二次开发时该往哪个方向加代码。from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey from sqlalchemy.sql import func from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base Base declarative_base() class Customer(Base): 装修客户也就是业主 __tablename__ customer id Column(Integer, primary_keyTrue, autoincrementTrue) name Column(String(50), nullableFalse) # 业主姓名 phone Column(String(20), indexTrue) # 手机号常用作登录账号 address Column(String(200)) # 房屋地址 area Column(Float, default0.0) # 户型面积单位平米 created_at Column(DateTime, server_defaultfunc.now()) class Site(Base): 工地一个客户可能有多套工地但通常一套 __tablename__ site id Column(Integer, primary_keyTrue) customer_id Column(Integer, ForeignKey(customer.id), nullableFalse) status Column(String(20), defaultpending) # 状态机pending 待开工 / designing 设计中 / constructing 施工中 / done 已竣工 manager Column(String(20)) # 项目经理姓名 start_date Column(DateTime) # 计划开工日期 end_date Column(DateTime) # 计划竣工日期 customer relationship(Customer, backrefsites) class Material(Base): 材料每个工地会采购多批材料 __tablename__ material id Column(Integer, primary_keyTrue) site_id Column(Integer, ForeignKey(site.id), nullableFalse) name Column(String(50)) # 材料名称如“立邦乳胶漆” quantity Column(Float, default0) # 数量 unit_price Column(Float, default0) # 单价 supplier Column(String(50)) # 供应商 status Column(String(10), defaultunpaid) # unpaid/paid/returned我习惯把status字段的取值写在注释里因为装修行业的业务状态流转特别容易混乱——比如有些公司工地状态是“量房→设计→报价→开工→验收”有些是“待开工→施工中→已竣工”这套源码用字符串存状态而不是数字枚举好处是数据库里可读性好坏处是你改状态值时忘了同步前端下拉框就会出现列表页显示空白的老问题。这三张表之间的关系很清晰客户一对多工地工地一对多材料。真正的业务逻辑并不复杂复杂的是状态流转的约束——比如材料没入库不能结算、工地没开工不能报进度。把这些约束写在 Handler 里还是写在模型里直接决定你后期维护的体验。我看这套源码一般把简单的约束写在 Handler 里比如创建工地时校验customer_id是否存在复杂一点的、跨模块的状态校验建议你自己补进模型层或者单独抽一个 service 模块。2.3 异步 I/O 在装修管理里到底解决了什么Tornado 最大的招牌是异步非阻塞但落到装修管理系统里它到底解决了什么实际问题我用一个场景说清楚假设你有 30 个在建工地每个工地上 5 个工人工长手机上报节点时不仅写数据库还要给项目经理推送一条消息。同步框架比如 Flask每个请求占一个线程30 个并发请求就得开 30 个线程线程切换开销一上来数据库连接池先扛不住。Tornado 是单线程事件循环一个请求在等数据库返回时事件循环立刻去处理下一个请求线程开销几乎为零。但这里有个技术人员特别喜欢踩的坑Tornado 的异步不解决同步数据库查询的阻塞。这套源码如果用pymysqlSQLAlchemy做同步查询那么session.query(Site).get(site_id)这一行在执行期间整个事件循环照样卡住。正确做法是把同步的数据库操作丢进线程池或者换成aiomysql这类异步驱动。from concurrent.futures import ThreadPoolExecutor from tornado.concurrent import run_on_executor import tornado.web class SiteHandler(tornado.web.RequestHandler): # executor 线程数一般设为 CPU 核心数的 2 倍即可 executor ThreadPoolExecutor(4) run_on_executor def _query_site(self, site_id): # 这里是同步的 SQLAlchemy 查询塞进线程池执行不阻塞事件循环 return session.query(Site).get(site_id) async def get(self): site_id int(self.get_query_argument(id)) site await self._query_site(site_id) self.write({status: site.status, manager: site.manager})这段代码的核心逻辑是把耗时的同步查询通过run_on_executor丢进线程池再用await取回结果。ThreadPoolExecutor(4)的 4 是线程数不是越大越好因为你的数据库连接池通常只有 5 到 10 个连接线程数开多了反而互相等锁。这套源码如果最初就是按这个思路写的那并发能力不会差如果你拿到的版本直接在get()里同步查询那你二次开发时第一件事就是给每个 Handler 补上 executor。3. 在本地把装修管理系统跑起来从解压到浏览器出页面3.1 准备 Python 环境3.8 是最稳的版本为什么不用 3.12拿到源码包先别急着pip install先把 Python 版本定下来。这套装修管理系统是基于 Tornado 框架写的Tornado 5/6 时代的老项目最稳的 Python 版本是 3.8。原因有三第一tornado6.1 在 Python 3.8 上编译安装不会有任何告警第二老源码里常见的async with、await语法在 3.8 已经完全成熟第三pymysql、sqlalchemy这些依赖在 3.8 的 wheel 包最全。用 Python 3.12 跑老项目最常见翻车点是某些依赖还没有适配新版本的 C API编译时报error: command gcc failed。我一般用pyenv管理多版本或者直接用python3.8 -m venv建虚拟环境避免污染系统 Python。这是 python 安装教程里最基础但最重要的一步尤其是你电脑上已经装了 3.12别图省事直接用后面依赖装到你怀疑人生。# 创建 Python 3.8 虚拟环境venv 目录会出现在当前文件夹下 python3.8 -m venv venv # 激活虚拟环境Windows 下是 venv\Scripts\activate source venv/bin/activate # 先升级 pip老版本 pip 装 Tornado 有时会解析出旧版本 pip install --upgrade pip激活虚拟环境后命令行提示符前面会出现(venv)这说明你已经在独立环境里了。这个习惯一定要养成因为装修管理系统这类中小型项目最怕的就是全局环境里装了 A 项目需要的tornado 5.1又被 B 项目覆盖成tornado 6.2改来改去最后整个环境变成黑匣子。虚拟环境就是后悔药装坏了直接rm -rf venv重建五分钟恢复原状。3.2 安装依赖与初始化数据库requirements.txt 是救命文件依赖安装没什么玄学一条命令搞定。真正的问题是数据库。这套源码一般默认用 MySQL你需要本地装好 MySQL 5.7 或 8.0然后建一个库名字常见的是site_manager或decoration字符集要选utf8mb4不然存 emoji 表情或者生僻字会报错。# 安装 requirements.txt 里锁定的依赖 pip install -r requirements.txt # 如果 requirements.txt 缺失手动装这几个就好 # pip install tornado sqlalchemy pymysql # 初始化数据库执行后会自动建表 python init_db.pyinit_db.py的核心逻辑通常是Base.metadata.create_all(engine)也就是把 2.2 节里定义的模型映射成真实表。跑这个脚本前你得先确认config.py里的数据库连接串是对的。常见错误是密码含有特殊字符没转义导致连接报Access denied。连接串格式一般是mysqlpymysql://用户名:密码127.0.0.1:3306/site_manager?charsetutf8mb4改完数据库连接串再跑init_db.py看到create table customer之类的日志就说明建表成功了。3.3 Tornado 启动命令与配置文件的逐项参数照着抄就能跑依赖装好、表建完接下来就是启动服务。这个阶段最容易出错的是配置项我见过有人把debugTrue原封不动带到生产环境结果别人访问你的站能看到详细报错堆栈这是从源码项目里自带的默认配置直接上线导致的安全隐患。# 本地调试启动监听 8080 端口 python app.py --port8080 # 启动后浏览器访问 # http://127.0.0.1:8080# app.py 里常见的 Tornado settings 配置 settings { # 模板文件目录找 templates 文件夹 template_path: os.path.join(os.path.dirname(__file__), templates), # 静态资源目录css/js/图片 static_path: os.path.join(os.path.dirname(__file__), static), # cookie 签名密钥必须改掉默认值否则登录态可以被伪造 cookie_secret: 改成你自己的随机长字符串, # 开启 XSRF 防护所有 POST 请求需要带 _xsrf 参数 xsrf_cookies: True, # 调试模式本地开着方便看报错生产环境必须关闭 debug: False, # 调试模式下代码改动自动重启生产环境设置 False autoreload: False, # 开启 gzip 压缩静态资源体积能小三分之一 compress_response: True, }cookie_secret这个参数特别容易被人忽略但它是整个登录系统的基石。Tornado 用它给 cookie 签名你改了它所有已登录用户的会话立即失效所以上线前改一次就行之后别乱动。xsrf_cookies开启后前端每个 POST 表单都要带{% module xsrf_form_html() %}或者请求头里的X-XSRFToken否则会报HTTP 403: Requests XSRF cookie does not match。如果你二次开发时加了 AJAX 接口记得在fetch里带上这个 token。这两项配置直接关系到系统安全性也是这套源码里最值得你花时间读懂的参数。4. 改造与二次开发把通用源码变成自己公司的业务4.1 先改配置再改路由Tornado 的 handlers 路由写法本地跑通之后你会发现这套源码的默认业务逻辑跟你的实际需求有出入。比如默认权限分管理员和普通员工但你公司还有设计师和材料员两种角色。二次开发第一步不是改业务代码而是改路由和配置把系统结构摸清楚。Tornado 的路由注册集中在app.py的Application构造函数里。# 路由注册URL 正则表达式 - Handler 类的映射 app tornado.web.Application([ # 首页登录后跳转到工地列表 (r/, HomeHandler), # 客户管理列表页和详情页 (r/customer, CustomerListHandler), (r/customer/([0-9]), CustomerDetailHandler), # 工地进度数字 ID 通过正则捕获传给 Handler (r/site/([0-9])/progress, SiteProgressHandler), # 材料管理 (r/material/([0-9]), MaterialHandler), # 放行静态文件static 目录下的文件由 Tornado 直接返回 (r/static/(.*), tornado.web.StaticFileHandler, {path: settings[static_path]}), ], **settings)正则路由是 Tornado 和其他框架最明显的区别。([0-9])捕获的数字会用*args传给 Handler 的get(self, site_id)方法所以 Handler 定义时的参数个数必须和正则捕获组数量一致多一个少一个都会在访问时报TypeError。加新页面时你只需要复制一个现有的 Handler 类改掉里面的模板名和查询逻辑然后在app.py注册一条新路由。这套源码没有用装饰器路由所以别按 Flask 的习惯去蓝图里找路由。4.2 给工地进度加 WebSocket 实时推送Tornado 原生的优势装修管理最痛的需求是进度同步。工长在工地完成水电改造手机上报一个节点项目经理在办公室要立刻看到。用 HTTP 轮询也能做但每隔几秒刷新一次既浪费资源又不实时。Tornado 自带 WebSocket 支持不依赖任何第三方库这是比 Flask 强太多的地方——Flask 要装flask-socketio还得多学一套概念。import tornado.websocket # 维护一个简单的订阅组site_id - [WebSocket 连接列表] site_clients {} class SiteProgressSocket(tornado.websocket.WebSocketHandler): def open(self, site_id): # 客户端连上来了加入对应工地的订阅组 self.site_id site_id self.stream.set_nodelay(True) site_clients.setdefault(site_id, []).append(self) def on_message(self, message): # 工长上报进度节点JSON 字符串例如 {node: water_electric, remark: 完工} # 这里解析消息写入数据库然后广播给订阅了该工地的项目经理 self.write_message({event: progress, data: message}) def on_close(self): # 客户端断开从订阅组里移除防止内存泄漏 if self.site_id in site_clients and self in site_clients[self.site_id]: site_clients[self.site_id].remove(self)这个处理器的核心是site_clients字典以工地 ID 为 key 维护连接列表。open里调用的set_nodelay(True)是给 TCP 连接关掉 Nagle 算法让消息能立刻发出去实时性要求高的场景建议都加上。on_close里如果不移除断开的连接这个字典会越积越大内存泄漏问题在长连接服务里几乎是必然出现的。实战中还要考虑一个细节Socket 两端并不保证你的上一帧和下一帧之间有逻辑关联如果你要下发“项目经理审批通过”这种状态最好还是从数据库里查出来再推给客户端而不是直接依赖客户端传来的数据防止工长端伪造消息乱报进度。4.3 装修合同审批流用 tornado.gen.coroutine 还是 async/await这套源码如果是比较老的版本你会在 Handler 里看到tornado.gen.coroutine装饰器和yield表达式的异步写法。Tornado 5.0 之前的版本不原生支持async/await所以老代码长这样。但如果你拿到的是 Tornado 6.x建议新写的代码一律用async/await官方也已明确gen.coroutine只是兼容过渡方案。# 旧写法老版本 Tornado 的协程用 yield 挂起 tornado.gen.coroutine def get(self): result yield self._query_contract(contract_id) self.write(result) # 新写法Tornado 6 原生支持可读性好异常处理也更自然 async def get(self): try: result await self._query_contract(contract_id) self.write(result) except ContractNotFoundError as e: self.set_status(404) self.write({error: str(e)})两种写法在 Tornado 6 下都能跑但混用会有坑你在一个async def的 Handler 里用了gen.coroutine的yield运行时不会直接报错但可能返回一个未完成的 Future前端拿到空的响应体这种问题特别难排查。我的习惯是拿到老源码先全局搜索gen.coroutine有则统一改成async/await不仅是合同审批流所有涉及数据库查询、文件读取的地方都顺手改掉。改造时注意async/await的异常是标准 try/except而gen.coroutine的异常要捕获的话写法不同改的时候千万别漏了异常分支否则一个查询报错直接返回 500用户连错误信息都看不到。5. 部署与排查Tornado 服务真实环境里的 5 个坑血泪经验5.1 坑一单进程跑不出并发也没有趁手的后悔药现象开发环境一切正常python app.py --port8080跑得欢一上生产30 个用户同时在线系统开始卡顿CPU 使用率飘到 90% 以上偶尔直接无响应。 原因Tornado 默认单进程单线程所有请求共用一条事件循环。即便你的代码全部异步化CPU 密集任务比如生成报表、读取大图片生成缩略图仍会堵塞整个循环。 解决生产环境必须多进程部署。常见做法是用supervisord同时拉起多个 Tornado 实例每个监听不同端口再用 Nginx 做负载均衡。如果你懒得自己写进程管理pip install tornado之后自带的多进程启动方式也可以参考但不建议在 Windows 上跑多进程信号处理支持不完整容易出僵尸进程。5.2 坑二工地图片上传直接卡死整个服务现象工长上传装修前的房屋照片浏览器转圈 30 秒之后不仅上传失败其他用户的所有请求也跟着超时。 原因老版 Tornado 的RequestHandler处理上传文件时会把整个请求体读入内存大文件加上并发上传内存和事件循环直接被拖垮。 解决在配置里限制上传大小同时把文件保存操作丢进线程池。Tornado 的max_buffer_size参数默认 100MB装修照片高清原图动辄十几兆不限制的话很容易被打满。我一般设为 20MB并且对图片做压缩再存盘。settings { max_buffer_size: 1024 * 1024 * 20, # 请求体最大 20MB超出返回 413 }5.3 坑三模板语法和 Jinja2 混用报错现象源码自带的模板里写着{{ value|date(%Y-%m-%d) }}本地跑得好好的。你为了前端方便把模板引擎换成了 Jinja2结果页面直接抛TemplateAssertionError: no filter named date。 原因Tornado 自带模板语法和 Jinja2 的过滤器不兼容。Tornado 的管道符号|后面跟的是 Python 函数名而 Jinja2 有自己的一套过滤器名。 解决要么整套系统统一用 Tornado 模板要么统一用 Jinja2混用的下场就是两边语法都报错。如果坚持用 Tornado 模板想在模板里格式化日期提前在入口定义好函数再传给模板环境别指望它自带date过滤器。5.4 坑四SQLAlchemy 同步查询阻塞了异步事件循环现象按照 2.3 节加了run_on_executor之后某些接口反而变慢了延迟从 50ms 涨到 300ms。 原因线程池只有 4 个线程但你的工地列表页有多个查询每个查询都要排队等线程资源。更麻烦的是SQLAlchemy 的session对象不是线程安全的多个线程共享一个session会导致SQLAlchemyError: This Sessions transaction has been rolled back。 解决每个线程池任务创建独立的session用完即关。或者干脆把同步查询全部换成aiomysql异步驱动代价是 SQLAlchemy 的queryAPI 用不了得手写 SQL。5.5 坑五Nginx 反代之后 WebSocket 握手失败现象WebSocket 在本机http://127.0.0.1:8080测试完全正常部署到 Nginx 后面之后工长端一直显示连接失败浏览器控制台报Error during WebSocket handshake: Unexpected response code: 404。 原因Nginx 默认不会转发 WebSocket 协议升级时必需的Upgrade和Connection请求头Tornado 收到的是普通 HTTP 请求自然找不到对应的处理器返回 404。 解决给 Nginx 的 location 配置补上显式的转发头开启HTTP/1.1支持并延长读超时时间因为 WebSocket 连接是长连接默认 60 秒断掉的话工长上报到一半连接就断了。location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; # 这两行是 WebSocket 握手的关键 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; # 长连接超时设为 1 小时防止中途断连 proxy_read_timeout 3600s; proxy_send_timeout 3600s; }这类问题最气人的地方在于本地开发环境不会复现一上生产就出问题。排查技巧是先直接用服务器的 IP 加端口访问 WebSocket 接口如果直连正常而 Nginx 后面不正常基本可以断定是代理层问题跟源码无关。5.6 部署Supervisor 守护三条 Tornado 进程 Nginx 负载均衡生产环境的标准部署结构是Nginx 监听 80/443 端口前端静态文件由 Nginx 直接返回动态请求负载均衡到后端的多个 Tornado 进程。后端进程用 Supervisor 守护进程挂了能自动拉起。我一般起 3 个 Tornado 实例分别监听 8081、8082、8083然后用 upstream 做负载均衡。# /etc/supervisor/conf.d/site_manager.conf [program:site_manager_8081] commandpython app.py --port8081 directory/data/www/site_manager userwww-data autorestarttrue redirect_stderrtrue stdout_logfile/var/log/site_manager_8081.log# Nginx 负载均衡配置 upstream tornado_backend { server 127.0.0.1:8081; server 127.0.0.1:8082; server 127.0.0.1:8083; } server { listen 80; server_name your_domain.com; location / { proxy_pass http://tornado_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /data/www/site_manager/static/; expires 7d; } location /ws/ { proxy_pass http://tornado_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; } }这套结构跑稳之后你算是真正把 Tornado 用到了生产级别。多进程部署后有个小细节要注意如果源码里用了内存态的数据比如我 4.2 节的site_clients字典每个进程里的数据是独立的A 进程收到工长上报B 进程里的项目经理订阅不到。解决方式是用 Redis 做消息订阅或者把 WebSocket 连接强制粘在同一个进程上——这也是很多人部署 Tornado 应用时容易忽略的分布式状态问题。6. 进阶用压测验证 Tornado 的异步收益或把它换成更合适的框架当你把系统跑上生产并稳定运行后值得做一件事用压测工具量化这套系统的真实性能再把结果作为是否继续投入的决策依据。常见做法是用wrk压测命令很简单但解读压力测试报告比跑命令难得多。# 压测 8 个线程、100 个并发连接、持续 30 秒 wrk -t8 -c100 -d30s http://127.0.0.1:8080/api/site/1压测前请先确认被压接口内部有没有执行同步 SQLAlchemy 查询。如果你测的接口是同步阻塞的压测结果会非常难看QPS 几百就上不去改成异步查询后同等条件 QPS 能翻几倍。这个对比恰好能验证 Tornado 的架构优势——它的异步模型只有在代码真正异步化之后才发挥价值否则还不如 Flask 合适。至于这套装修管理系统值不值得长期投入我的观点很直接如果你的业务量不到几百个并发团队又全员熟悉 Django 的 ORM 和 Admin 后台那趁早把 Tornado 换成 Django 反而是理性决定别为框架殉道。Tornado 最大的优势在 WebSocket 长连接和高并发 I/O 密集场景而装修管理系统的绝大多数操作是 CRUD并发也不高异步带来的收益有限。我早几年接过一个类似的源码当时执着于把整个系统改造成异步想在性能上做到极致结果折腾半个月业务侧一点需求没动最后发现瓶颈根本不在框架而在设计师导出的图纸都在用微信传。那之后我养成了一个习惯先压测确认瓶颈再决定要不要改架构。技术永远是业务让路不是业务迁就技术。希望帮到你。本文还有配套的精品资源点击获取