天云物联网云平台:Flask+Echarts设备数据可视化闭环

📅 发布时间:2026/9/20 4:43:48
天云物联网云平台:Flask+Echarts设备数据可视化闭环
简介本资源为计算机科学与技术专业本科毕业论文《天云物联网云平台的设计与实现》完整文档面向物联网方向本科生、相关从业人员及个人开发者。论文围绕通过局部网络或互联网将智能设备、传感器、人与物体互联这一目标实现设备与传感器的一键创建、删除、数据上传存储及可视化展示可用于物联网、智能制造、智能家居等场景的快速原型搭建。系统采用Python语言与Flask框架开发数据库选用MySQL并集成百度开源的Echarts完成数据可视化。资源为1个docx文件压缩包约757KB内容涵盖绪论、系统开发环境、总体设计、数据库设计与权限表结构、Web模块与API模块设计、系统测试等章节附中外文摘要与参考文献规划。论文给出从环境搭建到功能实现的具体步骤对理解并复现同类物联网云平台有较高参考价值。目前已有61人学习下载。1. 天云物联网云平台设备数据从接入到可视化的闭环一个温湿度传感器在 ESP 板子上跑通串口读数只是物联网项目的第一公里。真正让人头疼的是后面数据往哪落、设备多了怎么分组、用户凭什么只能看自己的设备、小程序和浏览器怎么共用同一套数据。天云物联网云平台这个课题围绕的就是这条链路——用 Python Flask 起一套轻量后端对设备和传感器做 CRUD上传的数据落到 MySQL再用 Echarts 画成折线图。它不追求阿里云 IoT 那种大而全胜在可私有部署、结构透明、一台笔记本就能跑通全流程。适合想搞懂 IoT 云平台内部怎么运转的在校生和中小开发者尤其适合拿它当毕业设计骨架或者当自建轻量设备管理后台的起手模板。2. Flask 蓝本拆分与开发环境落地2.1 这个体量为什么选 Flask 而不是 DjangoDjango 自带 admin、ORM、模板引擎、用户认证和表单验证走的是大而全路线代价是配置黑盒多、目录约定死改一个字段要先理解它的 migration 机制和 app 加载顺序。天云云平台的功能边界很清楚一批 Web 页面加一套对外 API没有复杂 CMS 需求也不需要 Django admin 那种后台。Flask 微框架恰好适合——只带路由和 Jinja2 模板渲染、WSGI 适配其余交给扩展拼装。这里说的“微”不是功能少而是简单且易于扩展数据库用 flask-sqlalchemy 接迁移用 flask-migrate认证用 flask-loginAPI 认证用 flask-httpauth缺什么补什么。维度FlaskDjango定位微框架需自行拼装扩展全家桶约定优于配置ORMflask-sqlalchemy 扩展自带 ORM认证flask-login / flask-httpauth自带 auth 模块后台管理需自行开发 manager 蓝本自带 admin本例适用轻量 Web API 双通道偏重功能过剩选型判断标准就一条功能边界是否固定。边界固定、体量轻Flask 的灵活性收益最大如果后面还要做内容管理、权限矩阵、多级审核Django 反而能省下大量造轮子的时间。2.2 用 Blueprint 把 WEB 和 API 拆成四条蓝本所有路由堆在一个views.py里超过两百行就会失控。Flask 的 Blueprint蓝本可以把实现同一功能的代码归到一组再统一挂到应用上。这个平台的划分是auth管注册登录和邮箱验证main管普通用户的设备/传感器 CRUD 与数据可视化manager管管理员的全局用户设备管理api_1.0管对外 JSON 接口。前三条服务浏览器最后一条服务小程序、智能硬件和移动端。# app/__init__.py from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate db SQLAlchemy() migrate Migrate() def create_app(config_namedefault): app Flask(__name__) app.config.from_object(config[config_name]) db.init_app(app) migrate.init_app(app, db) # 按业务边界注册蓝本避免所有路由挤在一个文件里 from .auth import auth as auth_blueprint from .main import main as main_blueprint from .manager import manager as manager_blueprint from .api_1_0 import api as api_blueprint app.register_blueprint(auth_blueprint) # 认证注册/登录/邮箱验证 app.register_blueprint(main_blueprint, url_prefix/main) # 普通用户功能 app.register_blueprint(manager_blueprint, url_prefix/manager) # 管理员功能 app.register_blueprint(api_blueprint, url_prefix/api/v1) # 对外 API return app这段代码用了 app factory 模式。好处是配置可以在创建时注入测试时能换一套内存数据库跑url_prefix把四条蓝本在 URL 空间上分开API 那条固定成/api/v1/以后升级到 v2 只需新注册一条蓝本老接口不动。更关键的是 WEB 和 API 分离后两者的认证方式互不干扰浏览器侧靠 session cookieAPI 侧靠 HTTP token混在一起写会不断出现“为什么登录了但接口还是 401”这种问题。2.3 依赖隔离与 Windows 到 Linux 的缩进坑环境搭建第一步是虚拟环境不要往系统 Python 里直接装包。把依赖固定进requirements.txt换机器时一条命令还原。python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows pip install -r requirements.txt # 还原依赖# requirements.txt 关键几项 Flask1.1.2 Flask-SQLAlchemy2.4.4 Flask-Migrate2.5.3 Flask-Login0.5.0 Flask-HTTPAuth4.2.0 PyMySQL0.10.1PyMySQL 是纯 Python 实现的 MySQL 驱动不需要编译 C 扩展Windows 上装它比 mysqlclient 省事。连接串写成mysqlpymysql://user:pwd127.0.0.1:3306/tianyun?charsetutf8mb4utf8mb4是必须的用默认的 utf8 存中文设备名会出乱码。编辑器用 PyCharm社区版免费、专业版支持 Django 模板和远程解释器。跨平台迁移时有两个必踩的坑一是缩进Windows 上混用 Tab 和空格的文件挪到 Linux 跑会直接TabError统一用 4 个空格二是换行符CRLF 会让某些 shell 脚本报/bin/bash^M: bad interpreter。dos2unix manage.py # 清掉 CRLF autopep8 --in-place --aggressive app/ # 顺手规范缩进3. 位图权限模型与 MySQL 五张表设计3.1 用位图把九个权限压进一个整数字段权限设计最容易写歪的地方是给每个权限建一列布尔字段九个权限就是九列判断时一串if a and b and not c。这个平台用位图每个权限占一个二进制位一个整数就能表示全部权限组合。# app/models.py 权限常量每个权限独占一位 ADD_DEVICE 0x0001 # 0000 0001 DELETE_DEVICE 0x0002 # 0000 0010 EDIT_DEVICE 0x0004 # 0000 0100 ADD_SENSOR 0x0008 DELETE_SENSOR 0x0010 EDIT_SENSOR 0x0020 ADD_USER 0x0040 DELETE_USER 0x0080 EDIT_USER 0x0100 ADMIN 0xffff # 全部权限判断权限时只做一次按位与def has_permission(role_bits, perm): # 结果非 0 说明目标位被置上 return (role_bits perm) perm role_bits ADD_DEVICE | DELETE_DEVICE | EDIT_DEVICE # 0x0007 print(has_permission(role_bits, ADD_DEVICE)) # True print(has_permission(role_bits, ADD_USER)) # False赋值用按位或叠加收回权限用 ~perm清除整个过程不涉及任何字符串比较和关联表查询。边界也很清楚一个整数在多数数据库里是 32 位或 64 位权限数量一旦超过位宽就得换方案所以位图适合权限项稳定、数量在几十个以内的场景这个平台九项权限正好落在舒适区。3.2 五张表结构与 SQLAlchemy 映射看一个陌生 Flask 项目最快的路径是先读models.py。类对应表类属性对应列关系用db.relationship声明。这个平台五张表User、Role、Device、Censor、Data。表关键字段说明userid, name, password_hash, email, confirmed, API_KEY, role_id用户password_hash存加密值不存明文roleid, name, permissions角色permissions就是位图整数deviceid, name, user_id, sensors设备user_id外键指向用户censorid, name, unit, type, device_id传感器unit保证上传数据单位统一dataid, value, created_at, censor_id数据记录created_at供折线图做时间轴User 和 Role 是多对一——一个角色对应多个用户一个用户只属于一个角色所以role_id放在 User 侧。Device 和 Censor 同样是多对一。密码用 werkzeug 的generate_password_hash生成API_KEY 在注册时用secrets.token_hex(16)一次性下发。confirmed字段标记邮箱是否验证通过未验证用户在 auth 蓝本里被重定向。class Censor(db.Model): __tablename__ censor id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), nullableFalse) unit db.Column(db.String(16)) # 如 ℃、%、ppm type db.Column(db.String(32)) # 数据类型用于校验上传值 device_id db.Column(db.Integer, db.ForeignKey(device.id)) data db.relationship(Data, backrefcensor, lazydynamic) class Data(db.Model): __tablename__ data id db.Column(db.Integer, primary_keyTrue) value db.Column(db.Float, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow, indexTrue) censor_id db.Column(db.Integer, db.ForeignKey(censor.id))Data.created_at上加了索引因为折线图按时间倒序取最新一页数据是最频繁的查询没索引时数据量上万条后接口延迟会明显上升。lazydynamic让sensor.data返回查询对象而不是列表方便后面做分页。3.3 迁移命令与初始化顺序表结构不要靠手写 SQL 建用 flask-migrate 版本化。三条命令的执行顺序不能反init只跑一次生成migrations目录migrate扫描模型差异生成脚本upgrade才真正落到 MySQL。export FLASK_APPmanage.py # Windows 用 set FLASK_APPmanage.py flask db init flask db migrate -m init user role device censor data flask db upgrade改过模型字段后重复后两条即可。常见报错是Target database is not up to date说明有未应用的迁移先flask db upgrade再生成新的。初始化数据管理员账号、默认角色不要塞进迁移脚本单独写一个flask seed自定义命令避免迁移回滚时把业务数据一起带走。4. API 蓝本的 token 认证与数据上传下载4.1 session 和 HTTPAuth 的分工浏览器侧用 flask-login 的 session cookie用户登录后 cookie 自动带上。但小程序和硬件端没有 cookie 概念每次请求都要自带凭证。API 模块用 flask-httpauth 的 HTTPBasicAuth把 API_KEY 当用户名传、密码位留空即可。from flask_httpauth import HTTPBasicAuth from flask import g, jsonify, request auth HTTPBasicAuth() auth.verify_password def verify_password(api_key, _password): # API 场景不用账号密码直接用注册时下发的 API_KEY 校验身份 user User.query.filter_by(API_KEYapi_key).first() if not user: return False g.current_user user return True回调返回值决定是否放行把用户对象挂到g上后续视图函数直接用g.current_user不用重复查库。API_KEY 用 Basic 方式传输时务必配 HTTPS明文 HTTP 下它等价于裸露的密码。4.2 上传数据与分页拉取接口上传接口要同时做三件事认证、归属校验、参数校验。归属校验最容易被漏掉——只验证 API_KEY 合法不代表这个传感器属于当前用户漏了就会出现 A 用户往 B 用户传感器写数据。api.route(/sensors/int:sensor_id/data, methods[POST]) auth.login_required def upload_data(sensor_id): sensor Censor.query.get_or_404(sensor_id) if sensor.device.user_id ! g.current_user.id: return jsonify({code: 403, msg: sensor not owned}), 403 payload request.get_json() or {} value payload.get(value) if value is None: return jsonify({code: 400, msg: value is required}), 400 record Data(valuefloat(value), censor_idsensor_id) db.session.add(record) db.session.commit() return jsonify({code: 0, data_id: record.id}), 201对应设备端的调用# -u 后面是 API_KEY 加冒号密码留空 curl -u 3f9a2c7e8b1d4f60: \ -H Content-Type: application/json \ -d {value: 26.5} \ http://127.0.0.1:5000/api/v1/sensors/1/data拉取最新数据用于图表首屏分页拉取用于历史回看接口里用paginate控制api.route(/sensors/int:sensor_id/data, methods[GET]) auth.login_required def list_data(sensor_id): page request.args.get(page, 1, typeint) per_page min(request.args.get(per_page, 50, typeint), 200) # 上限 200 防拖库 pagination (Data.query.filter_by(censor_idsensor_id) .order_by(Data.created_at.desc()) .paginate(page, per_page, error_outFalse)) return jsonify({ code: 0, total: pagination.total, data: [{value: d.value, created_at: d.created_at.isoformat()} for d in pagination.items] })per_page强制封顶 200 是必须的否则客户端传个 100000一次查询就把内存和带宽吃满。时间统一用 ISO 8601 格式返回前端new Date()可以直接解析。4.3 返回码与排查对照现象原因排查方向401 UnauthorizedAPI_KEY 错或没带检查-u格式、用户表里 API_KEY 是否为空403 sensor not owned归属校验拦截确认传感器挂在当前用户名下的设备上400 value is required请求体缺字段检查Content-Type: application/json是否声明500 float 转换失败value 传了字符串前端统一用数字类型接口侧做 try 捕获返回空数组分页越界核对 total 与 page、per_page 的关系5. Echarts 折线图接入与数据链路验证Echarts 由数据驱动接到接口 JSON 后几行配置就能出图不需要额外转换数据结构。const chart echarts.init(document.getElementById(sensor-chart)); fetch(/api/v1/sensors/1/data?page1per_page50, { headers: { Authorization: Basic btoa(apiKey :) } }) .then(res res.json()) .then(res { const rows res.data.reverse(); // 接口是倒序画图要正序 chart.setOption({ xAxis: { type: category, data: rows.map(d d.created_at) }, yAxis: { type: value, name: ℃ }, series: [{ type: line, smooth: true, data: rows.map(d d.value) }] }); });setOption是增量合并而不是整体替换定时刷新时只把新的 series 数据喂进去Echarts 会自动找出两组数据的差异并用动画过渡不用手动清空画布。btoa(apiKey :)拼出来的就是 Basic 认证头和前面 curl 的-u等价。单位℃从传感器记录的unit字段取直接写进yAxis.name避免图上有数据却看不出量纲。排错时不要一上来就调图表把链路拆成三段落最小化定位范围先用手写 JSON 喂接口确认能落库直接SELECT * FROM data ORDER BY id DESC LIMIT 5看再用 curl 从 API 拉一次确认返回结构一致最后才接前端。折线图不显示、只有坐标轴九成是xAxis.data和series.data长度不等对照接口返回逐条打印长度即可。历史数据量大的场景把首屏限定在最近 50 条翻页按需加载比一次请求全量再切片稳得多。本文还有配套的精品资源点击获取