init: EasyScreen 电子菜单系统

- server: FastAPI 后端(多屏管理、素材上传、节目编排、客户端注册/配置/心跳/崩溃上报、管理台托管)
- android: Java 客户端(minSdk 23,全屏图片/视频轮播、远程配置、开机自启、崩溃上报)
- web: React + Vite + antd 管理台(屏幕/素材/节目管理)
- 屏幕设备 ID 关联机制、gunicorn 生产部署脚本
This commit is contained in:
Tatta
2026-08-12 20:57:55 +08:00
commit 4e2775c8c3
58 changed files with 6451 additions and 0 deletions
+213
View File
@@ -0,0 +1,213 @@
# EasyScreen 电子菜单系统
基于 **Android 6.0 电子显示屏** 的电子菜单(广告屏)解决方案。支持多块屏幕独立配置、远程下发图片/视频节目、开机自启全屏播放。
系统分三端:
| 端 | 技术栈 | 说明 |
|---|---|---|
| `android/` | 原生 JavaminSdk 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/distFastAPI 启动时自动托管
```
开发模式(热更新):
```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 <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}` | 素材文件访问 |
---
## 生产部署
### 构建管理台并单端口托管
```bash
cd web && npm run build # 生成 web/dist
```
后端启动时若检测到 `web/dist` 存在,会自动托管管理台(SPA 路由回退到 index.html),生产环境只需暴露一个端口。
### 启动后端
```bash
cd server
scripts/start_prod.sh # Linuxgunicorn + 2 workers + 日志(logs/
scripts/start_prod.bat # Windowsgunicorn 前台运行
```
### 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**:素材正被节目引用,先在「节目编排」中移除。