- server: FastAPI 后端(多屏管理、素材上传、节目编排、客户端注册/配置/心跳/崩溃上报、管理台托管) - android: Java 客户端(minSdk 23,全屏图片/视频轮播、远程配置、开机自启、崩溃上报) - web: React + Vite + antd 管理台(屏幕/素材/节目管理) - 屏幕设备 ID 关联机制、gunicorn 生产部署脚本
EasyScreen 电子菜单系统
基于 Android 6.0 电子显示屏 的电子菜单(广告屏)解决方案。支持多块屏幕独立配置、远程下发图片/视频节目、开机自启全屏播放。
系统分三端:
| 端 | 技术栈 | 说明 |
|---|---|---|
android/ |
原生 Java(minSdk 23,兼容 Android 6.0+) | 运行在电子显示屏上,全屏轮播图片/视频 |
server/ |
Python FastAPI + SQLAlchemy | 配置管理、素材存储、客户端接口、管理台托管 |
web/ |
React + Vite + antd | 管理台:多屏管理、素材上传、节目编排 |
┌─────────────┐ 开机/重开自动拉取配置 ┌──────────────┐
│ Android 显示屏 │ ─── /api/client/config ───▶│ │
│ (ExoPlayer │ ─── /media/xxx 拉素材 ─────▶│ FastAPI │
│ + Glide) │ ◀── /api/client/register ─── │ 后端服务 │
└─────────────┘ 心跳 /heartbeat │ + MySQL/SQLite
崩溃上报 /crash │ │
┌─────────────┐ 管理台(浏览器) │ │
│ Web 管理台 │ ─── /api/... ──────────────▶│ │
│ (React) │ └──────────────┘
└─────────────┘
核心概念:屏幕 ID 关联
每块显示屏由唯一「设备 ID」标识(如 SF-01),这是后台屏幕与 App 关联的钥匙:
- 后台「屏幕管理」新增屏幕时推荐填写设备 ID(如
SF-01),屏幕列表会显示所有屏幕的设备 ID(可一键复制) - App 首次启动进入配置界面,填写服务器地址 + 屏幕 ID(与后台完全一致,含大小写)
- App 保存时自动校验:屏幕 ID 不存在则提示错误,不会进入播放
- App 用该 ID 注册并拉取这块屏幕绑定的节目 → 多块屏各配各的 ID,各播各的节目
屏幕 ID 留空时,App 用设备硬件 ID(ANDROID_ID)自动注册新屏幕,后台会自动出现该设备,改名后绑定节目即可。
目录结构
EasyScreen/
├── server/ # FastAPI 后端
│ ├── app/
│ │ ├── main.py # 入口(API + /media + 管理台托管)
│ │ ├── config.py # settings.json 读取(多数据源切换)
│ │ ├── database.py # SQLAlchemy 引擎/会话
│ │ ├── models.py # 数据表:screens/assets/playlists/items/bindings
│ │ ├── auth.py # JWT 登录鉴权
│ │ └── routers/ # admin_auth/screens/assets/playlists/client
│ ├── settings.json # 数据库与服务器配置
│ ├── uploads/ # 上传的素材文件(/media 访问)
│ ├── logs/crashes/ # App 崩溃上报日志(自动创建)
│ ├── scripts/ # start_prod.sh/.bat、stop_prod.sh
│ ├── requirements.txt
│ └── run_local.py # 本地开发启动(uvicorn --reload)
├── android/ # Android 客户端工程(Android Studio 打开)
│ └── app/src/main/java/com/easyscreen/player/
│ ├── App.java # 全局崩溃捕获(本地记录 + 上报后端)
│ ├── MainActivity.java # 全屏播放调度(图片/视频轮播、心跳、定期重载)
│ ├── SetupActivity.java # 配置界面(服务器地址 + 屏幕 ID + 记住配置 + 校验)
│ ├── receiver/BootReceiver.java # 开机自启
│ ├── api/ # OkHttp + Gson 网络层(回调自动切主线程)
│ └── util/ # Prefs / DeviceId / CrashHandler
└── web/ # React 管理台
└── src/
├── pages/ # Login/Screens/Assets/Playlists
├── components/Layout.jsx
└── api/client.js # axios 封装(token 持久化)
快速开始
1. 后端
cd server
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt # Windows
.venv\Scripts\python run_local.py # 启动,端口 5889
首次启动自动建表。默认使用 SQLite(easyscreen.db)零配置运行;切换 MySQL 见下方配置说明。
2. 管理台
生产模式(后端已托管管理台,构建一次即可):
cd web
npm install
npm run build # 生成 web/dist,FastAPI 启动时自动托管
开发模式(热更新):
npm run dev # http://localhost:5173 (已代理 /api 和 /media 到 5889)
默认账号 admin / admin123(在 server/settings.json 的 admin 段修改)。
3. Android 客户端
用 Android Studio 打开 android/ 目录,等 Gradle 同步完成后构建 APK 安装到显示屏设备(或直接使用 app/build/outputs/apk/debug/app-debug.apk)。
首次配置:打开 App 进入配置界面——
- 服务器地址:如
http://192.168.1.100:5889(保存时自动测试连通性) - 屏幕 ID(可选):后台「屏幕管理」中的设备 ID;保存时自动校验是否存在
- 记住本次配置(默认勾选):下次打开免输入,直接进入播放
USB 调试小技巧:手机 USB 连电脑时执行 adb reverse tcp:5889 tcp:5889,App 地址填 http://127.0.0.1:5889 即可直连电脑后端,无需局域网 IP。
使用流程
- 登录管理台 → 上传素材(图片/视频)到「素材管理」
- 「节目编排」创建节目:左侧素材库分页网格可视化点选素材(图片缩略图/视频图标),右侧设置每项展示时长(0=自动:图片默认 10 秒,视频按自身时长)与播放顺序
- 「屏幕管理」将节目绑定到目标屏幕(每块屏可绑定不同节目)
- 屏幕设备:开机自动注册 → 拉配置 → 循环播放;每 30 秒心跳上报,后台实时显示在线状态;后台修改节目后 App 10 分钟内自动生效(重启 App 立即生效)
配置说明(server/settings.json)
| 字段 | 说明 |
|---|---|
active_db |
当前生效数据源:dev_sqlite(默认,无需 MySQL)/ dev / production |
databases.* |
各数据源连接信息;MySQL 填 host/port/user/password/database_name,切换时改 active_db 即可,代码零改动 |
server.base_url |
必须修改:部署机器的局域网 IP + 端口(Android 设备通过它访问素材) |
server.max_upload_mb |
单文件上传大小上限(默认 500MB) |
server.allowed_types |
允许上传的 MIME 类型 |
admin.username/password |
管理台登录账号密码(上线前务必修改) |
admin.token_secret |
JWT 签名密钥(上线前务必改为随机字符串) |
API 一览
在线文档:启动后访问 http://localhost:5889/docs(Swagger UI)。
管理端(需 Authorization: Bearer <token>)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/auth/login |
登录,返回 token |
| GET/POST | /api/screens |
屏幕列表 / 新增(推荐填写 device_id 用于 App 关联) |
| PUT/DELETE | /api/screens/{id} |
编辑 / 删除屏幕 |
| POST | /api/screens/{id}/bind |
绑定节目(替换当前生效节目) |
| DELETE | /api/screens/{id}/bind |
解除绑定 |
| GET/POST | /api/assets |
素材列表 / 上传(multipart) |
| DELETE | /api/assets/{id} |
删除素材(被节目引用时返回 409) |
| GET/POST | /api/playlists |
节目列表 / 新建 |
| PUT/DELETE | /api/playlists/{id} |
编辑(全量替换节目项)/ 删除 |
客户端(无需鉴权,以 device_id 标识)
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/client/register |
设备注册(幂等,按 device_id 复用或创建屏幕) |
| GET | /api/client/check?device_id=xxx |
校验屏幕是否已存在(App 配置时提示用) |
| GET | /api/client/config?device_id=xxx |
拉取当前生效播放配置(含 version 版本号) |
| POST | /api/client/heartbeat |
心跳上报(30s 间隔,超 90s 视为离线) |
| POST | /api/client/crash |
崩溃上报(写入 server/logs/crashes/) |
| GET | /media/{file} |
素材文件访问 |
生产部署
构建管理台并单端口托管
cd web && npm run build # 生成 web/dist
后端启动时若检测到 web/dist 存在,会自动托管管理台(SPA 路由回退到 index.html),生产环境只需暴露一个端口。
启动后端
cd server
scripts/start_prod.sh # Linux:gunicorn + 2 workers + 日志(logs/)
scripts/start_prod.bat # Windows:gunicorn 前台运行
MySQL 切换
在 settings.json 中创建数据库并填入连接信息,将 active_db 改为 dev 或 production:
CREATE DATABASE easyscreen DEFAULT CHARACTER SET utf8mb4;
CREATE USER 'easyscreen'@'%' IDENTIFIED BY '你的密码';
GRANT ALL PRIVILEGES ON easyscreen.* TO 'easyscreen'@'%';
常见问题
- App 打开闪退:先看后端
server/logs/crashes/是否有崩溃日志(App 会自动上报),或adb logcat抓AndroidRuntime段;常见为网络回调线程问题与 Glide/ExoPlayer 生命周期问题,最新版本已修复。 - App 提示"屏幕 ID 不存在":确认后台「屏幕管理」已创建该屏幕,且设备 ID 与 App 填写内容完全一致(含大小写)。
- 管理台打不开:确认
web/下已执行npm run build(或使用npm run dev开发模式)。 - 客户端提示"连接服务器失败":检查
settings.json的base_url是否为显示屏可达的局域网地址、防火墙是否放行端口。 - 上传被拒:检查文件类型是否在
allowed_types内(视频建议 MP4/H.264 编码,老设备解码能力有限)。 - Android 6.0 播放卡顿:优先使用 H.264 编码、分辨率不超过屏幕尺寸的 MP4;图片避免超大 PNG。
- 删除素材失败 409:素材正被节目引用,先在「节目编排」中移除。