lch收发助手 · 开发与使用文档

使用手册 + 游戏插件开发接口(本软件为闭源软件,仅游戏开发接口对外开放)
📦 版本 v1.2.0 📅 文档日期 2026-09-30 🔒 闭源 · 仅开放游戏开发 👤 维护者 lch

1文档与产品说明

1.1 这是什么

lch收发助手是一套面向机房、教室、办公室等内部网络场景的 文件共享 + 打字练习 + 打字游戏平台。它由两部分组成:

服务端部署在一台主机上,提供网页界面,学生与管理员都用浏览器访问。
客户端可选的桌面程序。学生机不在同一局域网时用它一键组网接入;另提供下载管理与自动更新。

典型使用场景:机房、教室、办公室、实训室。

1.2 软件的开放范围 重要

范围是否开放说明
游戏插件开发 开放 唯一对外开放的扩展点。第三方可按第 8 章的接口规范自行开发游戏插件,放入平台即可被识别和运行。
使用操作(学生 / 管理员 / 客户端) 本文档涵盖 操作层面的全部说明见第 3–7 章。
服务端与客户端程序本体 闭源 不提供源代码,也不对外说明内部实现、程序结构、数据存储与接口清单。
管理后台的实现细节 闭源 只提供操作说明,不提供内部实现说明。
本文档的编写原则

本软件为闭源软件。因此本文档只描述三类内容:① 产品能做什么;② 用户怎么操作;③ 游戏开发接口怎么用。 凡涉及程序内部实现、代码结构、数据存储、程序构建与发布方式的内容,一律不在本文档范围内。

1.3 版本说明

本软件的不同位置可能显示 1.2.0、1.2.1、1.2.2 等版本号,这是历史迭代留下的显示差异, 不影响功能使用;对外统一以 v1.2.0 作为发行版本标识。游戏开发接口请始终以本文档(最新版)为准。

1.4 文档导航

我想…看哪一章
了解产品有什么功能第 2 章 功能特性
把服务端跑起来第 3 章 快速上手
教学生上传下载、打字、玩游戏第 4 章 学生使用手册
管理账号、文件、成绩、游戏第 5 章 管理员手册
安装并弄懂桌面客户端第 6 章 客户端使用说明
搞清组网、端口、连接不通怎么办第 7 章 网络与组网
自己开发一个游戏插件第 8 章 游戏开发指南
处理报错与常见故障第 9 章 常见问题

2功能特性

模块能力
📁 文件共享 拖拽上传、多文件上传、整文件夹上传;自动去重与同名自动改名;上传进度/速度/剩余时间;文件类型图标;下载计数;上传者可删除自己的文件;管理员可删除任意文件;可配置禁止上传的文件类型;可配置上传/下载限速
⌨️ 打字练习 内置默认素材 + 平台模板 + 本地文件三种题源;注释跳过开关;虚拟键盘与十指提示;实时用时/速度/正确率/输错率;音效反馈;成绩提交入库
🎮 打字游戏 插件式游戏体系,后台可启用/停用/重新扫描;支持单机与联机(房间、实时同步);成绩自动入库与排行榜;对外开放游戏开发接口
👥 账号体系 学生自助注册登录(姓名 + 密码 + 图形验证码);管理员独立后台并分级为超级管理员/普通管理员;首次登录强制改密
🎨 界面皮肤 6 套皮肤:经典、卡通、云母、暗黑、赛博、森林;选择被浏览器记住
🖥️ 桌面客户端 一键组网接入;内嵌服务端网页;下载管理器;自动更新;日志上报;设备心跳
🔐 安全能力 登录验证码;危险文件类型拦截;上传与成绩提交需登录;服务端身份校验(防止连到冒充的服务器);下载路径穿越防护

3快速上手

本章面向部署者(通常是老师或管理员)。本软件以打包好的程序形式分发,目标电脑无需安装开发环境。

3.1 启动服务端

按分发形式二选一:

A. 启动器版:双击程序目录中的「启动器」→ 点「启动服务」
B. EXE 版  :双击「lch收发助手.exe」→ 在窗口里点「启动服务」

启动成功后会显示访问地址与初始管理员信息:

地址   : http://<本机IP>:8080/
后台   : http://<本机IP>:8080/admin/login
管理员 : admin / admin123  (初始默认密码)
⚠️  首次登录后必须修改密码,改完此密码不再显示
启动器还能做什么

除了启动/停止服务,启动器上还可以:改端口、打开前台/后台网页、外网组网(见第 7 章)、 导出客户端配置(组网成功后自动生成客户端所需配置,把该文件与客户端文件夹一起发给学生)、查看与保存运行日志。 启动成功约 3 秒后会自动打开浏览器。

3.2 默认账号与首次登录

项目值
超级管理员账号admin
超级管理员初始密码admin123
首次登录要求必须修改密码,新密码至少 6 位、两次输入一致
后台入口服务端地址 + /admin/login
重要

初始密码在控制台只显示一次。改密后不再显示,请自行妥善保管。默认密码必须在上线前改掉。

3.3 端口与防火墙

  • 默认端口 8080;若被占用,在启动器里改端口后重启即可。
  • 改端口后,所有学生使用的地址与客户端的端口配置都要同步更新,否则会连不上。
  • 需要放行 Windows 防火墙的对应端口,否则同一局域网的其他电脑打不开网页:
    netsh advfirewall firewall add rule name="lch" dir=in action=allow protocol=TCP localport=8080

3.4 数据与备份

  • 程序目录下的数据文件夹在首次运行时自动生成,存放账号、文件索引、成绩与运行配置。
  • 共享的文件本体存放在文件目录中,同样在首次运行时自动创建。
  • 备份建议:定期整体备份数据文件夹与文件目录即可;恢复时保持目录结构不变,整个程序目录一起替换。

3.5 部署检查清单

  • ✅ 整个程序文件夹一起拷贝,不要只复制其中单个可执行文件(会缺少运行库导致启动失败)。
  • ✅ 防火墙已放行服务端口。
  • ✅ 已把杀毒软件对该程序目录设为信任(未签名的打包程序容易被误报)。
  • ✅ 已修改超级管理员默认密码。
  • ✅ 组网使用的默认密码已改为自定义强密码(见第 7 章)。
  • ✅ 已确认学生机器能打开服务端地址。

4学生使用手册

4.1 首次使用(注册)

  1. 用浏览器打开老师提供的地址(形如 http://10.126.126.1:8080/);或打开客户端程序点「开始连接」。
  2. 点击页面上的登录按钮,切换到「注册」页签。
  3. 填写:姓名(即账号)、密码、再次确认密码、图片验证码。
  4. 提交后自动登录,可以开始使用。
账号规则

姓名即账号,全站唯一;汉字最多 5 个或英文最多 10 个字符;只能包含中英文、数字与下划线。密码至少 4 位、最多 64 位。 忘记密码只能找老师(管理员)重置。

4.2 上传文件

  1. 进入「上传」页面。
  2. 把文件拖入虚线区域,或点击按钮选择文件;也可以选择整个文件夹(会保留文件夹结构)。
  3. 在列表中确认上传内容,移除不需要的;重复选择的文件会自动跳过。
  4. 点击开始上传,进度条会显示百分比、速度与预计剩余时间。
上传限制

单次上传体积不超过 100 MB;每名学生的累计配额为 300 MB; 默认禁止上传 46 类可执行与脚本类文件(如 .exe .bat .cmd .dll .ps1 .jar 等)。 超限或命中黑名单时会被拒绝并给出提示。

4.3 下载文件

  • 首页就是文件列表,点条目即可下载;下载不需要登录。
  • 列表显示文件类型图标、大小与上传者。
  • 自己上传的文件旁有删除按钮,可以随时删除(配额同时释放);别人的文件无权删除。
  • 使用客户端时,点下载会弹出保存对话框(默认存到系统「下载」文件夹),并在左侧下载管理面板显示进度。

4.4 打字练习

  1. 进入「打字」页面,选择素材:下拉框中的平台模板,或点击按钮选择自己电脑上的代码/文本文件。
  2. 按需切换「要打注释 / 不打注释」、「显示键盘 / 隐藏键盘」;空行与行首缩进会自动跳过。
  3. 照屏幕文本输入,跟着高亮键盘与手指提示操作。
  4. 顶部实时显示:用时、速度(字符/分钟)、正确率、输错率。
  5. 完成后在弹出的成绩框中确认提交,成绩进入系统,老师可以查看。
提示

若模板下拉框是空的,说明平台还没有放入打字模板,这不影响使用:直接用内置默认文本,或选择本地文件即可。 提交成绩需要先登录,未登录时提交会被拒绝。

4.5 打字游戏

  • 进入「游戏」页面,选择游戏卡片进入。
  • 联机游戏输入相同的房间号即可与同学同场竞技;房间可能有人数上限。
  • 成绩自动记录,可在游戏内查看,也可由老师在后台查看。

4.6 切换皮肤

点击页面右上角的 🎨 图标,选择:经典 / 卡通 / 云母 / 暗黑 / 赛博 / 森林。选择会被浏览器记住,下次访问自动生效。

5管理员手册

5.1 登录与改密

  1. 访问服务端地址 + /admin/login。
  2. 用 admin / admin123 登录(需填验证码)。
  3. 系统强制要求修改密码(至少 6 位、两次一致),改完即可进入后台。

5.2 后台功能地图

菜单能做什么
仪表盘查看学生数、文件数、成绩数、启用中的游戏数、管理员数量,以及最近 10 条打字成绩
学生管理查看全部学生及其登录次数与最后登录时间;重置密码(新密码至少 4 位);删除账号
文件管理查看所有上传文件(大小、上传者、下载次数);下载;删除(会同时删除服务器上的实体文件)
打字成绩查看最近成绩明细;导出 CSV(可直接用 Excel 打开);删除记录
游戏管理重新扫描游戏目录、启用/停用游戏、预览游戏、查看某游戏成绩榜
管理员修改密码;新增/删除管理员(仅超级管理员可操作)
设置配置禁止上传的文件类型;配置客户端上传/下载限速(KB/s)

5.3 常用运维动作

新增一个游戏

  1. 把游戏插件文件夹放入平台的游戏目录(开发方法见第 8 章)。
  2. 在后台「游戏管理」点「重新扫描」,或重启服务端。
  3. 确认该游戏处于「启用」状态,学生端即可看到。

调整上传限制

  • 在「设置」中修改被禁止的文件扩展名列表;填写后完全覆盖系统默认值,清空则恢复为默认的 46 类。
  • 注意:清空黑名单等于放开所有类型,包括可执行文件,请谨慎操作。

限制网速

  • 在「设置」中分别填写上传与下载限速值,单位 KB/s;填 0 表示不限速。
  • 限速对大文件传输效果明显,可用于保证多人同时使用时的网络质量。

重置学生密码

  • 在「学生管理」中找到该学生并执行重置,新密码至少 4 位,然后告知学生。
  • 学生本人无法自行找回密码。

查看哪些学生机在线

  • 客户端会定期上报设备信息。管理端可看到设备的主机名、使用者、系统版本与首次/最近在线时间。
  • 上报的运行日志同样由管理端接收,可用于排查学生机问题。

下发客户端更新

  • 把新的客户端目录替换到服务端的客户端分发目录,并同步更新目标版本号。
  • 客户端下次连接时会自动比对文件并静默更新,学生通常无需操作。
  • 客户端版本号与更新公告的设置入口请咨询软件维护者(不同发行版的界面可能略有差异)。

5.4 安全提醒

  1. 首次部署后立即修改超级管理员默认密码。
  2. 把组网默认密码改成自定义强密码(见第 7 章)。
  3. 不要把服务端直接暴露到公网——本产品面向内部网络设计。
  4. 文件类型黑名单只按扩展名拦截,不能替代杀毒软件。
  5. 定期备份数据文件夹与文件目录。

6客户端使用说明

客户端用于学生机与服务端不在同一局域网时自动完成组网并打开服务端页面,同时提供本地下载管理与自动更新。

6.1 界面构成

区域内容
顶栏版本号、设备标识、主机名、连接状态;按钮:用户协议 / 检查更新 / 上报日志 / 退出
左侧面板下载管理:下载进度、状态、打开文件夹、清空
右侧区域内嵌的服务端网页(未连接时显示提示与「开始连接」按钮)
底部状态栏与实时时钟

6.2 使用流程

  1. 把老师发来的客户端文件夹与配置文件放在同一个目录下。
  2. 双击运行客户端,首次需同意「用户协议」(要滚动到底部才能点同意;不同意则退出程序)。
  3. 点「开始连接」,程序会自动组网并搜索服务端;自动搜索最长约需 45 秒,请耐心等待。
  4. 连接成功后右侧会自动载入服务端页面,像用浏览器一样上传、下载、打字、玩游戏。
  5. 在网页里点击下载时,会弹出保存对话框,并在左侧面板显示下载进度。

6.3 行为特性

特性说明
必须整目录分发客户端文件夹里的运行库、组网程序、驱动与自带浏览器运行时缺一不可,不能只拷贝单个可执行文件
需要管理员权限组网要安装虚拟网卡驱动,因此程序会请求以管理员身份运行
自动更新连接服务端后自动比对文件并静默更新,更新完成后自动重启;也可手动点「检查更新」
设备心跳连接成功后立即上报一次,之后每 300 秒一次,便于管理员了解设备在线情况
日志上报点「上报日志」可把最近的运行日志发送给维护者,是排障的首选手段
身份校验连接时会校验服务端身份,防止连到冒充的服务器

6.4 客户端常见故障

现象原因与处理
窗口打开了但页面一片空白 几乎都是客户端文件夹被拆分拷贝,缺少自带的浏览器运行时。请整目录重新分发。
一直停在「正在连接」 组网未连通或配置不全。请确认:配置文件里的网络名、网络密码、中继节点、端口四项齐全且与服务端一致;客户端文件夹完整(组网程序与驱动文件未被删除或拦截);程序以管理员身份运行;等待足够 45 秒。
下载文件名含中文时失败 已知缺陷。临时办法:改用浏览器下载,或把文件重命名为英文名后再下载。
提示需要同意用户协议 首次使用未滚动到底部点击同意。滚到协议最底部再点「同意」。
总是提示要更新 服务端分发目录内容与目标版本号不匹配。请联系管理员或维护者确认更新包是否已正确替换。

7网络与组网

7.1 两种接入方式

同一局域网学生机与服务端在同一网段,直接用浏览器访问服务端地址即可,无需客户端。
跨网络学生机在别处(如家里),用客户端自动组网接入,无需公网 IP 与端口映射。

7.2 概念说明

概念通俗解释
服务端地址学生访问的网址,由「服务器地址 + 端口」组成
端口默认 8080。被占用或另有安排时管理员可更改,更改后所有人都要用新端口
组网让不在同一局域网的学生机也能访问服务端的技术,由客户端自动完成
网络名 + 网络密码组网的「房间名与门禁密码」。服务端与客户端必须完全一致才能互通
中继节点当两端无法直连时,借助公网上的中转服务器打通连接
虚拟 IP组网后自动分配给每台设备的内部地址,学生无需关心
公钥指纹组网窗口上显示的一串字符,用于确认「连上的是自己的服务器」而不是冒充者

7.3 管理员的组网操作

  1. 在启动器上点「外网组网」,打开组网窗口。
  2. 填写四项:网络名(默认 lch-share)、网络密码(默认值必须改掉,建议 8 位以上)、 本机虚拟 IP(默认 10.126.126.1)、中转节点(形如 tcp://域名:端口)。
  3. 启动组网。窗口底部会显示服务端公钥指纹,供学生核对。
  4. 组网成功后,启动器会自动导出客户端配置文件。把该配置文件与客户端文件夹一起发给学生即可。
安全要求

组网密码是进入虚拟网络的唯一凭据,程序自带的默认值必须修改。请勿把配置文件公开发布到公网。

7.4 连接不通的排查顺序

  1. 先看服务端:本机能打开网页吗?不能 → 服务未启动或端口被占用。
  2. 再看网络:其他电脑能访问服务器吗?不能 → 检查是否同一网段,或客户端是否已成功组网。
  3. 再看防火墙:服务器是否放行了该端口?
  4. 再看地址:是否误把地址写成 127.0.0.1?那是本机地址,别的电脑连不上。
  5. 最后看配置一致性:服务端与客户端的网络名、网络密码、中继节点、端口是否完全一致。

8游戏开发指南 开放接口

这是本软件唯一对外开放的扩展点。任何人都可以按本章规范开发游戏插件,放入游戏目录后由平台加载运行。 本章只描述插件如何开发,不涉及平台自身的内部实现。

8.1 开放范围与开发者守则

允许不允许 / 不保证
  • 自行编写插件的前端页面(HTML/CSS/JS)
  • 注册自己的服务端接口(在约定的前缀下)
  • 注册自己的实时通信事件(在约定的命名空间下)
  • 提交成绩到平台成绩系统
  • 读取当前登录学生的姓名用于游戏内显示
  • 访问平台内部的账号、文件、配置等数据
  • 读写平台程序目录中的任何文件
  • 依赖本文档未公开的内部接口或内部结构
  • 长时间阻塞服务端主流程(耗时任务请用后台任务方式运行)
  • 绕过登录鉴权或伪造学生身份
兼容性声明

平台本身是闭源软件。本章公开的接口会尽量保持稳定,但不承诺永久不变; 依赖未公开内部结构的写法(例如直接操作平台的数据存储)在版本升级后可能失效。 如需长期维护的游戏,请优先只使用本章列出的接口。

8.2 插件目录结构

游戏目录/ └── 你的游戏名/ # 目录名建议用英文/数字/下划线 ├── game.json # 必需:插件元数据 ├── server.py # 可选:服务端逻辑(需要联机时必须有) └── static/ └── index.html # 必需:游戏界面

把该文件夹整体放入平台的游戏目录,然后在后台点「重新扫描」或重启服务端即被加载。 目录名以 _ 开头的文件夹会被平台跳过,可用于存放草稿。

纯前端小游戏

如果游戏不需要服务端参与(单机、纯本地计分),可以只提供 game.json 与 static/index.html,不写 server.py,元数据里把 has_server 设为 false。

8.3 元数据 game.json

字段必填默认值说明
key否目录名游戏唯一标识,决定访问地址与通信命名空间。建议只用英文、数字、下划线
name否目录名游戏显示名称
icon否🎮卡片上显示的图标(可用 Emoji)
description否空一句话玩法介绍,显示在游戏卡片上
author否空作者署名
version否1.0插件版本号
has_server否false是否需要实时通信(联机)。为 true 时平台会为你的插件开启通信命名空间
min_players否1最少人数(信息字段,由你的插件自行使用)
max_players否1最多人数(可用于房间满员判断)
{
  "key": "my_game",
  "name": "我的游戏",
  "icon": "🎲",
  "description": "一句话说明玩法",
  "author": "你的名字",
  "version": "1.0",
  "has_server": true,
  "min_players": 1,
  "max_players": 8
}

8.4 服务端类接口(server.py)

在 server.py 中定义一个名为 Game 的类,继承平台提供的基类 BaseGame,并实现两个可选方法:

from games.base import BaseGame


class Game(BaseGame):

    def register_routes(self, bp):
        """注册自定义 HTTP 接口。
        平台已为你的插件准备好带前缀的路由对象 bp,
        在这里用 @bp.route 注册,最终地址为 /api/game/<你的key>/<你的路径>。
        """

    def register_socketio(self, socketio, namespace):
        """注册实时通信事件(联机游戏用)。
        namespace 已固定为 /game/<你的key>,
        在这里用 @socketio.on("事件名", namespace=namespace) 注册。
        """

基类在实例化时已经为插件准备好以下属性,直接使用即可:

属性含义
self.key游戏标识(来自 game.json)
self.name / self.icon / self.description名称、图标、简介
self.author / self.version作者与版本
self.has_server是否联机
self.min_players / self.max_players人数区间
self.path你的插件目录(可用于定位自己的静态资源)

8.5 平台通路约定

通路约定用途
游戏页面/game/<你的key>玩家进入游戏的页面,平台会以 iframe 载入你的 static/index.html
静态资源/game/<你的key>/static/<文件路径>你的图片、脚本、样式等
自定义接口/api/game/<你的key>/<你注册的路径>在 register_routes 中注册
实时通信命名空间 /game/<你的key>在 register_socketio 中注册;前端用该命名空间连接
每个插件的空间是隔离的

每个游戏有独立的接口前缀与通信命名空间,不同插件之间不会互相干扰,事件名可以自由取。

8.6 提交成绩(当前做法 + SDK 建议)

成绩提交是插件与平台之间唯一的「数据写入」通路。平台上的成绩分两类:

记录类型内容查看位置
打字成绩由平台的打字模块产生,插件不需要关心后台「打字成绩」
游戏成绩游戏标识、学生姓名、分数、详情、记录时间后台「游戏成绩」与学生端排行榜

当前做法(示例):插件在服务端把成绩记录写入平台的成绩表,并调用平台日志以便管理员看到:

# server.py 内,收到前端提交后
from datetime import datetime
from db import execute
from logger import log_game_score

def save_score(game_key, student_name, score, detail=""):
    execute(
        "INSERT INTO game_scores(game_key, student_name, score, detail, created_at) "
        "VALUES(?,?,?,?,?)",
        (game_key, student_name, int(score), detail,
         datetime.now().isoformat(timespec="seconds")),
    )
    log_game_score(game_key, student_name, score)
这是现状,不是推荐做法

上面这段为了「如实可跑」而写出了当前实现:它让插件直接触碰了平台的成绩表结构。 由于平台是闭源软件,这类内部结构可能随版本变化,因此:

  • 建议后续由平台方封装一个官方 SDK 接口,例如 submit_score(game_key, student_name, score, detail),插件只调用这一个函数,不再关心底层存储。
  • 在官方 SDK 就绪前,按上面写法可以正常工作;若升级平台后发现成绩无法写入,请对照最新文档调整。
  • 插件不要去读写成绩表以外的任何平台数据。

8.7 前端页面约定(static/index.html)

  • 你的页面会被平台以 iframe 载入,所以不需要自己写导航栏、登录框等平台级界面。
  • 页面尺寸自适应 iframe 即可;建议同时适配浅色与深色皮肤下的可读性(平台支持 6 套皮肤)。
  • 获取学生姓名:平台会在父页面渲染一个显示当前登录学生的元素(#studentWelcome),插件可通过 parent.document.getElementById("studentWelcome") 读取其文本;取不到时请给出兜底名称。
  • 实时通信:使用平台提供的实时通信客户端库,连接到你自己的命名空间:
    const socket = io("/game/你的key");
    socket.on("connect", () => { socket.emit("join", { name: 学生姓名 }); });
    socket.on("你的自定义事件", (data) => { /* 处理 */ });
  • 建议通过 postMessage 或平台既有的下载入口处理文件下载,不要直接操作父页面。

8.8 最小可运行示例

三个文件即可跑通一个联机游戏(房间 + 实时分数 + 成绩入库):

// ① game.json
{
  "key": "my_game",
  "name": "我的游戏",
  "icon": "🎲",
  "description": "最小联机示例",
  "author": "你",
  "version": "1.0",
  "has_server": true,
  "min_players": 1,
  "max_players": 8
}
# ② server.py
from flask import request, jsonify
from games.base import BaseGame

ROOMS = {}


class Game(BaseGame):

    def register_routes(self, bp):
        # 最终地址:GET /api/game/my_game/hello
        @bp.route("/hello")
        def hello():
            return jsonify(ok=True, msg=f"来自 {self.name} 的问候")

    def register_socketio(self, socketio, namespace):

        def broadcast(room_id):
            room = ROOMS.get(room_id)
            if not room:
                return
            socketio.emit("room_update", {
                "room_id": room_id,
                "players": [{"name": p["name"], "score": p["score"]}
                            for p in room["players"].values()],
            }, namespace=namespace, room=room_id)

        @socketio.on("join", namespace=namespace)
        def on_join(data):
            sid = request.sid
            room_id = str(data.get("room_id") or "default")[:32]
            name = str(data.get("name") or "同学")[:16]
            room = ROOMS.setdefault(room_id, {"players": {}})
            if len(room["players"]) >= self.max_players:
                socketio.emit("error_msg", {"msg": "房间已满"}, to=sid, namespace=namespace)
                return
            room["players"][sid] = {"name": name, "score": 0}
            socketio.server.enter_room(sid, room_id, namespace=namespace)
            broadcast(room_id)

        @socketio.on("add_score", namespace=namespace)
        def on_add_score(data):
            sid = request.sid
            for room_id, room in ROOMS.items():
                if sid in room["players"]:
                    room["players"][sid]["score"] += int(data.get("delta", 1))
                    broadcast(room_id)
                    break

        @socketio.on("submit_score", namespace=namespace)
        def on_submit():
            sid = request.sid
            for room_id, room in ROOMS.items():
                if sid in room["players"]:
                    p = room["players"][sid]
                    save_score(self.key, p["name"], p["score"], f"room={room_id}")
                    socketio.emit("score_saved", {"score": p["score"]},
                                  to=sid, namespace=namespace)
                    break

        @socketio.on("disconnect", namespace=namespace)
        def on_disconnect():
            sid = request.sid
            for room_id, room in list(ROOMS.items()):
                if sid in room["players"]:
                    del room["players"][sid]
                    if room["players"]:
                        broadcast(room_id)
                    else:
                        ROOMS.pop(room_id, None)
                    break


# save_score 见 8.6 节
def save_score(game_key, student_name, score, detail=""):
    from datetime import datetime
    from db import execute
    from logger import log_game_score
    execute(
        "INSERT INTO game_scores(game_key, student_name, score, detail, created_at) "
        "VALUES(?,?,?,?,?)",
        (game_key, student_name, int(score), detail,
         datetime.now().isoformat(timespec="seconds")),
    )
    log_game_score(game_key, student_name, score)
<!-- ③ static/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>我的游戏</title>
  <style>
    body{font-family:system-ui,"Microsoft YaHei",sans-serif;margin:0;padding:20px}
    button{padding:8px 18px;border-radius:10px;border:1px solid #ccd;cursor:pointer}
    #players{margin-top:14px;line-height:1.9}
  </style>
</head>
<body>
  <h2>🎲 我的游戏</h2>
  <button id="addScore">加分 +1</button>
  <button id="submit">提交成绩</button>
  <div id="players">等待加入…</div>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    // 从父页面读取当前登录学生姓名(取不到时兜底)
    var el = parent.document.getElementById("studentWelcome");
    var myName = el && el.textContent.trim() ? el.textContent.trim() : "同学";

    var socket = io("/game/my_game");
    socket.on("connect", function () {
      socket.emit("join", { room_id: "default", name: myName });
    });
    socket.on("room_update", function (d) {
      document.getElementById("players").innerHTML =
        d.players.map(function (p) { return p.name + " : " + p.score; }).join("<br>");
    });
    socket.on("score_saved", function (d) { alert("成绩已提交:" + d.score); });
    socket.on("error_msg", function (d) { alert(d.msg); });

    document.getElementById("addScore").onclick = function () {
      socket.emit("add_score", { delta: 1 });
    };
    document.getElementById("submit").onclick = function () {
      socket.emit("submit_score", {});
    };
  </script>
</body>
</html>

8.9 内置示例游戏的事件设计参考

平台自带两个示例插件,可直接作为命名与流程设计参考(目录名与事件名如下):

示例玩法自定义接口实时事件(前端发 / 服务端发)
example_arena
示例竞技场 ⚔️
输入房间号加入,实时同步分数。适合作为联机模板 /rooms、/leaderboard 发:join、add_score、start、submit_score
收:room_update、score_saved、error_msg
saodache
搜打撤 🎯
随机地图搜集物资、击败敌人,攒够物资后从撤离点撤离;带每秒服务端结算循环 /ping、/status 发:join、move、attack、surrender
收:state、joined、toast、full、finished、game_over
做出好插件的几个建议
  • 服务端权威:分数与判定放在服务端,前端只负责显示与操作,避免学生改前端刷分。
  • 状态推送要节制:高频循环(如每秒一次)只推必要字段,人数多时才不会卡。
  • 断线要清理:务必处理 disconnect,把玩家从房间移除,否则房间会「永远满员」。
  • 名字要截断:学生姓名长度有限,写入前建议截断到 16 个字符以内。
  • 元数据要填全:description 与 icon 会直接影响游戏列表的观感。

8.10 加载与调试

  1. 把插件文件夹放入游戏目录,后台点「重新扫描」,或重启服务端。
  2. 在后台「游戏管理」确认插件已被识别(game.json 解析失败、目录名以 _ 开头都会导致跳过)。
  3. 确认游戏处于启用状态,再到学生端游戏列表查看。
  4. 服务端控制台会输出插件加载相关的提示信息,加载或注册失败时可据此定位。
现象常见原因
后台看不到新游戏目录名以 _ 开头;或 game.json 格式错误(注意用 UTF-8 保存);或未点「重新扫描」
游戏卡片能进但页面 404缺少 static/index.html,或文件名大小写不符
自定义接口 404未实现 register_routes;或访问地址缺少 /api/game/<key> 前缀
前端连不上实时通信game.json 的 has_server 不是 true;或命名空间写错(应为 /game/<key>)
成绩没有记录未调用成绩提交;或学生未登录导致姓名取不到;可在后台「游戏成绩」核对
游戏一开就卡住整个平台在接口或事件里做了耗时阻塞操作,应改为后台任务方式运行

8.11 发布插件的建议

  • 目录名、key、事件名统一使用英文小写加下划线,避免中文与空格带来的兼容问题。
  • 插件内自带说明文件(玩法、操作键位、作者联系方式),便于管理员与学生理解。
  • 只使用本章公开的接口;不要依赖未经公开的内部结构,以免平台升级后失效。
  • 若插件需要额外的 Python 依赖,请在说明中明确提出,由维护者安装后再启用。

9常见问题

端口被占用、服务起不来怎么办?
在启动器里换一个端口再启动。改端口后需要同步通知学生,并把客户端的端口配置一并更新,否则会连不上。
同一局域网的其他电脑打不开网页?
依次检查:① 服务是否已启动;② 服务器防火墙是否放行了该端口;③ 地址是否误写成了 127.0.0.1(那是本机地址,别的电脑连不上)。
杀毒软件报毒、文件被删?
未签名的打包程序常被误判。请把整个程序目录加入杀毒软件的信任区/白名单,不要只添加单个可执行文件。
忘记管理员密码了?
优先用另一个超级管理员账号在后台重置密码。若已没有任何可用管理员账号,只能由维护者清空账号数据后重新初始化—— 这会导致所有账号、文件记录与成绩数据丢失(已上传的实体文件仍在)。操作前请先备份。
学生忘记密码了?
学生无法自行找回。请管理员在「学生管理」中对该学生执行重置密码(新密码至少 4 位),再告知学生。
登录或注册时验证码显示不出来?
说明运行环境缺少验证码所需的组件或字体,属于部署问题,请联系软件维护者处理。
上传提示「空间已满」?
每名学生累计配额为 300 MB。先到首页删除自己之前上传的文件,配额立即释放,然后重新上传。
上传提示「禁止的文件类型」?
这是安全策略,默认拦截 46 类可执行与脚本文件(exe、bat、cmd、dll、ps1、jar 等)。 请把文件压缩为压缩包后再上传;如需长期放开某类文件,请管理员在后台设置中调整。
上传 / 下载很慢?
管理员可能配置了上传或下载限速(KB/s),用于保证多人同时使用时的网络质量。如确需调整,请联系管理员。
首页列表里有一些「没有上传者」的文件?
这些是管理员或维护者直接放入服务器的文件,属于正常现象,可以正常下载。
联机游戏里看不到同学 / 进不去房间?
依次确认:① 大家访问的是同一个服务端地址;② 房间号是否输入一致;③ 房间是否已满员;④ 该游戏是否处于「启用」状态(由管理员确认)。
打字页面的模板下拉框是空的?
说明平台还没放入打字模板文件。此时仍可正常练习:使用内置默认文本,或点击按钮选择自己电脑上的代码/文本文件(cpp、c、txt、js、py、java 等)。
打完字成绩没有记录?
成绩只有在已登录状态下提交才能入库。请先登录再练习,并在完成后点击确认提交;可由管理员在后台的成绩列表中核对。
想删掉自己上传的文件?
首页文件列表中,自己上传的文件旁有删除按钮,可直接删除,配额同时释放。别人的文件无权删除。
换一台服务器或换端口后大家连不上了?
任何地址或端口变化,都必须同步更新客户端配置并重新分发给学生,否则旧配置会一直连向旧地址。
客户端页面空白 / 打不开服务端页面?
最常见原因是客户端文件夹被拆分拷贝,缺少自带的浏览器运行时。请整目录重新分发。 若仍空白,请用客户端的「上报日志」功能把日志发给维护者。
客户端一直显示「正在连接」?
确认四项配置(网络名、网络密码、中继节点、端口)齐全且与服务端一致;客户端文件夹完整、组网程序与驱动未被删除或拦截; 程序以管理员身份运行。自动搜索服务端最长约需 45 秒,请耐心等待。
怎么给平台加一个新游戏?
把游戏插件文件夹放入平台的游戏目录,然后在后台点「重新扫描」或重启服务;确认游戏处于启用状态后学生端即可看到。 自己开发游戏的完整规范见第 8 章 游戏开发指南。
能拿到这个软件的源代码或接口文档吗?
不能。本软件为闭源软件,不提供源代码,也不对外说明程序内部实现、数据存储与接口清单。 唯一对外开放的是游戏插件开发接口,规范见第 8 章。

10安全注意事项

10.1 部署方必做

#事项原因
1修改超级管理员默认密码默认口令是公开的,未改等于后台敞开
2修改组网默认密码组网密码是进入虚拟网络的唯一凭据
3不要暴露到公网本产品面向内部网络设计,缺少公网级防护
4定期备份数据与文件账号、成绩、文件一旦丢失难以恢复
5保持客户端配置不外泄配置文件包含组网凭据,公开发布等于对外开放虚拟网
6把程序目录加入杀软信任避免程序或数据文件被误删

10.2 使用方的边界认知

  • 文件类型黑名单只按扩展名判断,改名的可执行文件仍可能通过,不能替代杀毒软件。
  • 下载不需要登录,能打开页面的人都能下载文件;若文件敏感,请勿上传。
  • 账号跟人不跟机器,请勿共用账号,以免成绩与文件归属混乱。
  • 插件开发者请遵守第 8.1 节的守则:不触碰平台内部数据与文件,不绕过登录鉴权。

10.3 漏洞与问题反馈

发现安全问题或程序缺陷,请联系维护者:📧 [email protected]。 反馈客户端问题时,请一并提供「上报日志」生成的内容,可显著加快定位速度。

11附录

11.1 术语表

术语含义
服务端部署在主机的网页服务,所有人访问它
客户端学生机上的桌面程序,负责组网、内嵌网页与下载管理
管理后台管理员专用页面,入口为服务端地址 + /admin/login
配额每个学生可累计上传的总体积上限,默认 300 MB
黑名单禁止上传的文件扩展名清单,默认 46 类
模板打字练习使用的素材文本
插件放入游戏目录即可被平台加载的游戏扩展包
命名空间实时通信的隔离通道,每个游戏一个(/game/<key>)
房间号联机游戏用于把玩家分到同一局的编号
心跳客户端定期向服务端报告自身状态

11.2 关键参数速查

项目值
默认端口8080
默认超级管理员admin / admin123(必须修改)
管理员新密码长度至少 6 位
学生密码长度4–64 位
单次上传上限100 MB
每学生累计配额300 MB
默认拦截的扩展名46 类
界面皮肤6 套
客户端心跳间隔300 秒
客户端自动搜索服务端上限约 45 秒
联机游戏人数上限由插件元数据决定(示例游戏为 8 人)

11.3 文档范围声明

本软件为闭源软件。本文档只涵盖:产品功能说明、使用操作说明、游戏插件开发接口。 以下内容不在本文档范围内,也不对外提供:

  • 服务端与客户端的源代码、程序结构与内部实现
  • 数据存储结构、程序内部接口清单与网络协议细节
  • 程序的构建、打包、混淆与发布方式
  • 绕过任何限制、破解、修改授权、查看他人密码等请求

如需了解游戏开发以外的技术细节,请联系软件维护者。

11.4 版本与联系方式

项目内容
发行版本v1.2.0(不同位置可能显示 1.2.1 / 1.2.2,不影响使用)
文档日期2026-09-30
维护者lch
联系邮箱[email protected]
开放范围仅游戏插件开发接口;其余部分闭源