🔬 深度技术解析 · 融梦跃视(上海)体育科技

智慧体育 AI 体测系统
技术全景分析

基于计算机视觉 · 深度学习 · 边缘计算的校园 AI 体测解决方案。 覆盖嵌入式终端、AI 推理引擎、云端数据平台三大层级, 深度解析技术栈、数据存储策略与设备-服务端同步机制。

0
体测项目
全覆盖
0
AI 体测
人次
0
进校
数量
0
省级行政区
覆盖
0
终端存储
容量
0
摄像头
像素
Technical Stack

三层系统架构 · 技术栈解析

系统由云端管理平台、边缘 AI 设备、移动端三个层级构成, 采用「边缘优先推理、异步汇聚上传」的分布式架构。

🖥️ 云端 / 校园服务器层
Spring Boot / FastAPI MySQL · Redis · MinIO Vue 3 · ECharts WebSocket · MQTT Broker Docker · Nginx RBAC 权限
HTTP/HTTPS · MQTT · WebSocket
📱 边缘 AI 设备层(AI 体测智能机)
主控模块RK3568 · Android
AI 摄像机Hi3516 / RV1126 · Linux
触摸屏 UI21" · 1920×1080
Android 10/11 定制 Embedded Linux RKNN / MNN / NCNN OpenCV · SQLite WAL RTSP 私有 SDK 离线推理,无需 GPU 服务器
微信小程序 / uni-app · 私有 UDP 投屏
📲 移动端层(教师 / 学生)
微信小程序 uni-app 跨平台 投屏控制 体育作业布置 居家 AI 锻炼检测

📦 硬件平台规格

组件规格推断芯片
主控处理器≥4核(长跑≥8核),≥2.0GHzRK3568
主控内存RAM ≥4GB,Flash ≥64GBLPDDR4+eMMC
AI 摄像机算力RAM ≥2GB,Flash ≥16GB,内置 NPURV1126
摄像头传感器800W 像素,1/1.8",3840×2160,≥30fpsIMX415
视频编码H.265 / H.264 / MJPEG硬编码
显示屏≥21",1920×1080,电容触控LVDS/eDP
音响额定功率 ≥30W,SNR >70dBI2S+D类功放
网络接口2×千兆 RJ45,2×USB,1×HDMI板载千兆 PHY

⚙️ 软件技术栈

🤖
嵌入式操作系统
主控:定制 Android 10/11,支持触控 UI、GPU 渲染、摄像头 HAL。摄像机端:独立 嵌入式 Linux(Buildroot 裁剪),承载 AI 推理
📱
设备端应用层
Android 原生 Java/KotlinFlutter;OpenGL ES 实时姿态骨架叠加渲染;RTSP 流 + 私有 SDK 摄像机通信
💾
本地持久化
SQLite WAL 模式,保证断电事务完整性;Room ORM(Android)封装;人脸底库加密本地存储
🔌
离线独立运行
AI 模型、评分标准、学生底库全本地化,支持外部断网使用,测试过程不受网络波动影响

🗄️ 服务端技术栈

Web 框架
Spring Boot FastAPI RESTful API
数据库
MySQL Redis ZSET MinIO/OSS
消息 / 实时
MQTT EMQ X WebSocket Kafka
部署
Docker Nginx 私有化校园服务器
安全
HMAC-SHA256 签名 HTTPS RBAC 权限
排行榜
Redis 有序集合 MQTT 推送 实时计算

🔧 业务模块

👥
基础管理
学生/班级/账号管理,RBAC 权限模型,支持批量导入 Excel,学期初向设备下发底库
📊
体育测试管理
多维度成绩统计可视化(校/年级/班/个人),≥10 个项目报告,ECharts 图表
🏆
赛事管理
创建比赛、排行榜实时更新(Redis ZSET),MQTT 广播到所有设备屏幕
🎬
视频管理
测试录像分片上传存入 MinIO/OSS,断点续传,服务端合并后供平台回放

🌐 Web 管理平台

SPA 框架
Vue 3 Element Plus Pinia
可视化
ECharts 5 折线图/雷达图 热力图
视频播放
西瓜播放器 video.js
数据交互
xlsx.js 导出 Axios WebSocket
构建工具
Vite Webpack
移动端
微信小程序 uni-app 投屏协议

📐 模块分布

📈
体育测试管理
ECharts 多维可视化:合格率/平均分/年级/性别分布;支持≥10个测试项目成绩报告查看
🏅
赛事排行榜
WebSocket 实时刷新排行榜数据;支持最佳/累计双模式,校级/年级/班级多维度
🎓
课程设置管理
AI 运动课程构建器,≥40个动作库供选择,可设置时间/组数/间歇,投屏到设备
📋
测试计划管理
选择国标评分标准,设置测试名称/日期/班级,查看各班进度及平均分,支持免考设置

🧠 AI 核心能力

🦴
人体姿态估计
HRNet / MoveNet / OpenPose 类架构,实时捕捉 17+ 关节点,支持多人同帧检测
🏃
动作识别 / 计数
关节点时序模型 LSTM / Transformer,识别跳绳/深蹲等周期动作,实时计数
🚨
违规行为检测
YOLO 系列目标检测 + 规则引擎,识别踩线/漏绕杆/球出界/单手投球等违规
👤
人脸识别
ArcFace / InsightFace 轻量模型,本地离线身份认证,举手即开始,全程无人工
🎯
多目标追踪
DeepSORT / ByteTrack,多人运动区域独立追踪,互不干扰

⚡ 推理框架 & 部署

推理框架
RKNN(瑞芯微NPU) MNN TNN NCNN
图像处理
OpenCV ROI 裁剪 ISP 处理
模型优化
INT8 量化 NPU 加速 本地化部署
训练框架
PyTorch TensorFlow PaddlePaddle
双芯协同架构

摄像机端(RV1126/Hi3516) 负责 AI 推理(姿态估计、违规检测、人脸识别),通过私有 SDK 将推理结果传给主控。

主控端(RK3568) 负责业务逻辑(成绩计算、评分、UI交互、DB写入、网络同步),两芯分工协作, 无需外部 GPU 服务器

Data Storage

两级存储策略

设备本地作为主存储(Primary),校园服务器/云端作为汇聚存储(Aggregate), 测试全程在本地完成,网络仅作为数据同步通道。

📦 存储层级架构

📷 AI 摄像机模块(eMMC)
≥16 GB
视频原始缓冲(环形覆盖) NPU 模型文件 触发片段打标
🖥️ 主控模块(eMMC)
≥64 GB
SQLite 成绩数据库(WAL) 测试视频(30~90天) 学生人脸底库(加密) AI 推理模型 评分标准配置 待上传队列
🗄️ 校园服务器
TB 级
全校成绩汇聚(MySQL) 测试视频长期存储(MinIO) 学生档案主库 排行榜缓存(Redis ZSET) 测试计划 / 评分标准主库
☁️ 云端(可选)
弹性扩展
校外居家运动数据 区级/市级数据汇总 微信小程序数据直传

🗂️ 数据分类归属

🔒
人脸底库(本地优先)
加密存储于设备本地,断网可完成身份认证。上传服务端时仅传特征向量,不传原始图像(符合《个人信息保护法》)
🏅
测试成绩(双写)
本地 SQLite 立即原子写入(sync_status=PENDING),网络可用时 HTTP POST 上传服务端,成功后标记 SYNCED
🎬
测试视频(本地→服务端)
摄像机 eMMC 环形缓存,测试触发后拷贝到主控;后台低优先级分片上传到服务端,本地保留 30~90 天后自动清理
⚠️
草稿记录(违规待审核)
违规记录标记 sync_status=DRAFT,不进入上传队列,需教师人工审核确认后才提交为有效成绩
📋
测试计划 / 评分标准(下行)
服务端为主库,教师在 Web 平台创建后,通过 MQTT 推送或设备轮询拉取同步到本地

🎬 视频数据生命周期

摄像机 eMMC
环形缓冲覆盖
测试触发
片段打标拷贝
主控 eMMC
本地保留 30~90天
分片上传
低优先级后台
服务端 MinIO
长期保留
本地定期清理
释放存储空间
Sync Mechanism

设备 → 服务端同步机制

采用「本地优先 · 异步事件驱动 · 断点续传」三大核心策略, 保证考场零故障的同时实现全校数据统一管理。

📤 成绩上传完整流程

① 测试完成
AI 推理结果回调,成绩计算完成
② 本地落盘(原子操作)
SQLite WAL 写入,sync_status=PENDING,断电不丢失
③ 检测网络状态
Android ConnectivityManager / HEAD 请求探测
有网:立即 HTTP POST
HMAC-SHA256 签名
指数退避重试(最多5次)
断网:写入待上传队列
网络恢复后自动
flush_pending_queue()
④ 服务端幂等确认
record_id 去重,返回 code=0,设备标记 SYNCED
⑤ MQTT 事件广播
服务端重算排行榜,MQTT 推送到所有设备实时更新屏幕

🔄 sync_status 状态机

PENDING待上传
UPLOADING上传中
SYNCED已同步
FAILED重试超限
DRAFT违规待审
PENDING→ 开始上传 →UPLOADING
UPLOADING→ 服务端确认 →SYNCED
UPLOADING→ 重试>5次 →FAILED
FAILED→ 下次批量重试 →PENDING
DRAFT→ 教师审核通过 →PENDING

🔐 安全机制

🔑
HMAC-SHA256 请求签名
HMAC(device_id + timestamp + sorted_payload),服务端验签防篡改和重放攻击
🛡️
人脸数据合规
设备本地仅存特征向量,上传时不携带原始图像,符合《个人信息保护法》要求
📟
幂等设计
服务端以 record_id 去重,设备重传时直接返回成功,避免成绩重复计入

📡 MQTT 上下行 Topic 规范

📤
设备上行
device/{id}/events device/{id}/heartbeat

score_completed 事件、设备状态心跳(30s)

📥
服务端下行
device/{id}/commands

下发测试计划、学生底库更新、远程配置指令

🏆
排行榜推送
school/{id}/ranking/{project}

成绩入库后实时广播,设备屏幕实时刷新榜单

Code Simulation

上传链路代码实现

以下代码完整模拟设备端数据上传全流程,包含本地 SQLite 存储、 HMAC 签名、指数退避重试、断点续传视频上传及 MQTT 实时推送, 使用 Python 标准库实现,无需外部依赖。

▶ 实际运行输出

python main.py — AI体测智能机 上传链路模拟
════════════════════════════════════════════════════════════ AI 体测智能机 → 数据平台 上传链路完整模拟 ════════════════════════════════════════════════════════════ 22:21:18 MockServer 🚀 模拟服务端启动: http://127.0.0.1:8765 22:21:18 Uploader 🎬 视频上传后台线程启动 【场景一】正常联网 - 测试后立即上传 ────────────────────────────────────────────────── 22:21:18 TestSession [立定跳远] 学生 张伟 开始测试 22:21:18 TestSession ✓ 成绩入库: 177.42 (良好) | 同步状态: PENDING 22:21:18 MockServer [Server] ✅ 成绩入库: [DEV-RK3568-A001] 张伟 立定跳远 177.42 (total=1) 22:21:18 Uploader ✅ 上传成功: 张伟 立定跳远 177.42 [尝试#1] 22:21:18 MQTT 📡 MQTT 上行: score_completed student=张伟 project=立定跳远 22:21:18 MQTT 🏆 [服务端] 排行榜更新 [立定跳远] 共1人,第一名: 张伟 177.42 22:21:18 MQTT 🏆 收到排行榜更新 [立定跳远]: TOP1=张伟 177.42 【场景二】网络断开 - 成绩缓存本地 ────────────────────────────────────────────────── 22:21:18 Main 📴 网络已断开(模拟) 22:21:18 TestSession [足球绕杆] 学生 张伟 开始测试 22:21:18 TestSession ✓ 成绩入库: 24.22 (及格) | 同步状态: PENDING 22:21:18 TestSession [排球垫球] 学生 李娜 开始测试 22:21:18 TestSession ✓ 成绩入库: 20.69 (良好) | 同步状态: PENDING 本地 DB 统计: 总计=7 | 已同步=4 | 待上传=3 | 草稿=0 【场景三】网络恢复 - 自动批量补传 ────────────────────────────────────────────────── 22:21:19 Main 📶 网络恢复(模拟) 22:21:19 Uploader 📤 刷新上传队列,共 3 条待同步 22:21:19 Uploader ✅ 上传成功: 张伟 足球绕杆 24.22 [尝试#1] 22:21:19 Uploader ✅ 上传成功: 李娜 排球垫球 20.69 [尝试#1] 22:21:19 Uploader ✅ 上传成功: 王芳 排球垫球 26.44 [尝试#1] 【场景四】服务端下发 MQTT 指令 ────────────────────────────────────────────────── 22:21:19 MQTT 📥 收到指令: update_test_plan 22:21:19 Main [设备] 收到新测试计划: 2025年期末体育考试 22:21:19 MQTT 📥 收到指令: sync_students 22:21:20 Main 💓 心跳已发送(含设备状态信息) ════════════════════════════════════════════════════════════ 最终同步统计 ════════════════════════════════════════════════════════════ 本地 DB 总记录 : 7 已同步到服务端 : 7 (100.0%) 待上传 (PENDING) : 0 草稿 (违规待审核) : 0 服务端已接收成绩 : 7 ✅ 演示完成
🐍device_sync / models.py
"""
本地 SQLite 数据库管理 - 设备端持久化层
使用 WAL 模式保证断电数据完整性(对应 Android Room ORM)
"""

class SyncStatus(IntEnum):
    PENDING   = 0  # 本地已写入,等待上传
    UPLOADING = 1  # 上传中
    SYNCED    = 2  # 服务端已确认
    FAILED    = 3  # 重试超限,进入死信
    DRAFT     = 4  # 违规/未完整,待审核列表

@dataclass
class TestRecord:
    record_id:     str   = field(default_factory=lambda: str(uuid.uuid4()))
    device_id:     str   = ""
    student_id:    str   = ""
    student_name:  str   = ""
    project:       str   = ""
    score:         float = 0.0
    grade:         str   = ""
    is_valid:      bool  = True
    violation:     str   = ""          # 违规描述,空=无违规
    motion_params: dict  = field(default_factory=dict)
    sync_status:   int   = SyncStatus.PENDING
    video_path:    str   = ""

    def to_upload_payload(self) -> dict:
        """序列化为上传给服务端的 JSON 载荷"""
        return { ... }

class LocalDatabase:
    def __init__(self, db_path: str = ":memory:"):
        self._conn = sqlite3.connect(db_path, check_same_thread=False)
        self._init_schema()

    def _init_schema(self):
        self._conn.executescript("""
            PRAGMA journal_mode=WAL;  -- 关键:WAL 模式防断电丢数据

            CREATE TABLE IF NOT EXISTS test_records (
                record_id    TEXT PRIMARY KEY,
                device_id    TEXT NOT NULL,
                student_id   TEXT NOT NULL,
                project      TEXT NOT NULL,
                score        REAL,
                grade        TEXT,
                sync_status  INTEGER DEFAULT 0,  -- SyncStatus 枚举
                retry_count  INTEGER DEFAULT 0,
                video_path   TEXT DEFAULT '',
                created_at   REAL
            );
            CREATE INDEX IF NOT EXISTS idx_records_sync
                ON test_records(sync_status, retry_count);
        """)

    def get_pending_records(self, limit: int = 20) -> List[TestRecord]:
        """获取待上传队列(PENDING + FAILED && retry < 5)"""
        rows = self._conn.execute("""
            SELECT * FROM test_records
            WHERE sync_status IN (0, 3)  -- PENDING or FAILED
              AND retry_count < 5
            ORDER BY created_at ASC
            LIMIT ?
        """, (limit,)).fetchall()
        return [self._row_to_record(r) for r in rows]

    def update_sync_status(self, record_id: str,
                           status: SyncStatus, retry_delta: int = 0):
        self._conn.execute("""
            UPDATE test_records
            SET sync_status = ?, retry_count = retry_count + ?
            WHERE record_id = ?
        """, (int(status), retry_delta, record_id))
        self._conn.commit()
🐍device_sync / uploader.py
"""
HTTP 上传引擎 - 含 HMAC 签名、指数退避重试、断点续传
对应 Android WorkManager OneTimeWorkRequest 调度
"""

class RequestSigner:
    """HMAC-SHA256 请求签名,防止数据篡改和重放攻击"""
    def sign(self, payload: dict, timestamp: int) -> str:
        body = f"{self.device_id}{timestamp}{json.dumps(payload, sort_keys=True)}"
        return hmac.new(self.secret_key, body.encode(), hashlib.sha256).hexdigest()

    def build_headers(self, payload: dict) -> dict:
        ts = int(time.time())
        return {
            "X-Device-Id":  self.device_id,
            "X-Timestamp":  str(ts),
            "X-Signature":  self.sign(payload, ts),  # 签名
        }

class ScoreUploader:
    MAX_RETRY    = 5
    BASE_BACKOFF = 2.0   # 指数退避基数(秒)

    def upload_single(self, record: TestRecord) -> bool:
        if not self.net.is_online:
            return False  # 离线,留在本地队列

        for attempt in range(1, self.MAX_RETRY + 1):
            try:
                resp = http_post(endpoint, payload, headers)
                if resp["code"] == 0:
                    self.db.update_sync_status(record_id, SYNCED)
                    return True  # ✅ 成功
            except NetworkError as e:
                # 指数退避:2¹=2s, 2²=4s, 2³=8s ...
                backoff = self.BASE_BACKOFF ** attempt + random.uniform(0, 1)
                time.sleep(backoff)

        # 超过最大重试 → 标记 FAILED,等待下次 flush
        self.db.update_sync_status(record_id, FAILED, retry_delta=1)
        return False

    def flush_pending_queue(self):
        """批量刷新离线积压的队列(网络恢复时自动触发)"""
        records = self.db.get_pending_records(limit=50)
        for r in records:
            if not self.net.is_online: break
            self.upload_single(r)

class VideoUploader:
    """视频分片断点续传(类 TUS 协议)"""
    CHUNK_SIZE = 512 * 1024  # 512 KB / 片

    def _upload_video(self, task: VideoUploadTask):
        # Step1: 向服务端申请上传会话,获取已接收字节偏移
        offset = self._init_upload_session(task)  # 断点位置

        # Step2: 从断点处分片循环上传
        while offset < task.file_size:
            if not self.net.is_online:
                # 保存断点,下次从此处继续
                self.db.update_video_progress(task_id, offset, PENDING)
                return

            chunk = read_file(task.local_path, offset, CHUNK_SIZE)
            headers["Content-Range"] = f"bytes {offset}-{offset+len(chunk)-1}/{task.file_size}"
            if upload_chunk(chunk, headers):
                offset += len(chunk)

        # Step3: 通知服务端合并所有分片
        self._finalize_upload(task)
🐍device_sync / mqtt_client.py
"""
MQTT 实时推送 - 上行事件 + 下行指令 + 排行榜广播
生产使用 Eclipse Paho MQTT 客户端库(Android)
"""

# Topic 规范
TOPICS = {
    "上行事件": "device/{device_id}/events",
    "下行指令": "device/{device_id}/commands",
    "心跳":    "device/{device_id}/heartbeat",
    "排行榜":  "school/{school_id}/ranking/{project}",
}

class DeviceMQTTClient:
    def publish_score_event(self, record_payload: dict):
        """
        测试完成后,HTTP 上传的同时也通过 MQTT 发布事件,
        触发服务端实时重算排行榜并广播
        """
        self.broker.publish(
            topic   = f"device/{self.device_id}/events",
            payload = {
                "event_type": "score_completed",
                "data":       record_payload,
            }
        )

    @mqtt_device.on_command("update_test_plan")
    def on_test_plan(payload):
        """接收服务端下发的新测试计划"""
        save_to_local_db(payload)

    @mqtt_device.on_command("sync_students")
    def on_sync_students(payload):
        """接收学生底库更新指令,拉取增量数据"""
        pull_student_delta(last_sync_ts)

class ServerMQTTPusher:
    """服务端:收到成绩事件后,重算排行榜并广播到所有设备"""

    def _update_ranking(self, record: dict):
        project = record["project"]
        # Redis ZSET 维护排行榜(生产)/ 内存列表(模拟)
        ranking = self.rankings.setdefault(project, [])
        ranking.append((record["score"], record["student_name"]))
        ranking.sort(reverse=True)  # 分值高优先

        # 广播给所有订阅该项目排行榜的设备屏幕
        self.broker.publish(
            topic   = f"school/{self.school_id}/ranking/{project}",
            payload = {
                "top1_name":  ranking[0][1],
                "top1_score": ranking[0][0],
                "total":      len(ranking),
            }
        )
🐍device_sync / mock_server.py
"""
模拟校园服务端 - Python 内置 http.server
对应生产环境:Spring Boot / FastAPI
"""

# API 路由表
ROUTES = {
    "POST  /api/v1/records":         "接收成绩上传(幂等)",
    "POST  /api/v1/videos/init":     "初始化视频上传会话",
    "PATCH /api/v1/videos/chunk":    "接收视频分片",
    "POST  /api/v1/videos/complete": "完成视频上传,触发合并",
    "HEAD  /health":                 "健康探测",
    "GET   /api/v1/students/sync":   "下发学生底库(设备拉取)",
}

def _handle_score_upload(self, body: dict):
    record_id = body.get("record_id")

    # 幂等设计:record_id 已存在则直接返回成功
    if record_id in _records_store:
        self._respond(200, {"code": 0, "msg": "ok (idempotent)"})
        return

    # 持久化(模拟 MySQL 写入)
    _records_store[record_id] = {
        **body,
        "server_received_at": time.time(),
    }
    self._respond(200, {"code": 0, "record_id": record_id})

def _handle_video_init(self, body: dict):
    """支持断点续传:返回已接收字节偏移"""
    task_id = body["task_id"]
    existing = _video_sessions.get(task_id)

    if existing:
        offset = existing["offset"]  # 从断点继续
    else:
        offset = 0
        _video_sessions[task_id] = {"offset": 0, "size": body["file_size"]}

    self._respond(200, {"code": 0, "offset": offset})

def _handle_student_sync(self):
    """下发学生底库增量(含人脸特征向量)"""
    students = [
        {
            "student_id":    "S001",
            "name":          "张伟",
            "face_feature":  "BASE64_FEATURE_VECTOR",  # 非原始图像
        },
        # ...
    ]
    self._respond(200, {"students": students, "sync_ts": int(time.time())})

📁 代码文件结构

💾
models.py

SQLite Schema、SyncStatus 枚举、TestRecord 数据类、CRUD 操作、WAL 模式配置

🤖
test_session.py

AI 推理结果处理、违规检测器、评分引擎、测试会话生命周期管理

📤
uploader.py

HMAC 签名、ScoreUploader 指数退避、VideoUploader 断点续传、NetworkMonitor

📡
mqtt_client.py

进程内 Broker 模拟、设备端 MQTT 客户端、服务端排行榜推送器

🖥️
mock_server.py

HTTP 服务端模拟、幂等成绩接收、视频分片存储、学生底库下发

main.py

五大场景完整演示:正常联网→断网缓存→恢复补传→MQTT 下行→心跳上报