# 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 关联的钥匙: 1. 后台「屏幕管理」新增屏幕时**推荐填写设备 ID**(如 `SF-01`),屏幕列表会显示所有屏幕的设备 ID(可一键复制) 2. App 首次启动进入配置界面,填写**服务器地址 + 屏幕 ID**(与后台完全一致,含大小写) 3. App 保存时自动校验:屏幕 ID 不存在则提示错误,不会进入播放 4. 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. 后端 ```bash 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. 管理台 生产模式(后端已托管管理台,构建一次即可): ```bash cd web npm install npm run build # 生成 web/dist,FastAPI 启动时自动托管 ``` 开发模式(热更新): ```bash 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。 --- ## 使用流程 1. **登录管理台** → 上传素材(图片/视频)到「素材管理」 2. 「节目编排」创建节目:左侧素材库**分页网格**可视化点选素材(图片缩略图/视频图标),右侧设置每项展示时长(0=自动:图片默认 10 秒,视频按自身时长)与播放顺序 3. 「屏幕管理」将节目**绑定**到目标屏幕(每块屏可绑定不同节目) 4. 屏幕设备:开机自动注册 → 拉配置 → 循环播放;每 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 `) | 方法 | 路径 | 说明 | |---|---|---| | 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}` | 素材文件访问 | --- ## 生产部署 ### 构建管理台并单端口托管 ```bash cd web && npm run build # 生成 web/dist ``` 后端启动时若检测到 `web/dist` 存在,会自动托管管理台(SPA 路由回退到 index.html),生产环境只需暴露一个端口。 ### 启动后端 ```bash cd server scripts/start_prod.sh # Linux:gunicorn + 2 workers + 日志(logs/) scripts/start_prod.bat # Windows:gunicorn 前台运行 ``` ### MySQL 切换 在 `settings.json` 中创建数据库并填入连接信息,将 `active_db` 改为 `dev` 或 `production`: ```sql 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**:素材正被节目引用,先在「节目编排」中移除。