edu-signalr-sdk
v2.2.2
Published
教学服务实训平台跟踪上报 SDK —— 通过 SignalR 长连接实现实训时长跟踪、真实实训行为检测与浏览器指纹防作弊
Maintainers
Readme
edu-signalr-sdk
教学服务实训平台跟踪上报 SDK。通过 SignalR 长连接实现:
- 学生实训累计时长跟踪
- 真实实训行为检测(鼠标、键盘、触摸、滚动、页面可见性)
- 浏览器设备指纹防作弊(基于 FingerprintJS 专业指纹库)
- 自动化工具/模拟器检测
- Token 过期自动刷新
- 教辅端远程踢人下线
安装
npm install edu-signalr-sdk一键接入(推荐)
SDK 内置 fastGo 快捷方法,自动完成 URL 参数解析、自动登录、SignalR 连接建立、事件注册与销毁绑定。
import { fastGo, FastGoError } from "edu-signalr-sdk";
const { sdk, loginRet } = await fastGo({
hubUrl: "https://你的教辅端域名/hubs/traincloud",
autoLoginApi: "http://localhost:3031/api/training/auto-login",
srRefreshTokenApi: "http://localhost:3031/api/training/refresh-token",
interceptors: {
request: (params) => params,
response: (res) => res,
},
});fastGo 启动流程
URL 参数解析 → interceptors.request 加工 → autoLoginApi 登录
→ interceptors.response 加工响应
→ 提取 signalr Token → 创建 SignalR 连接
→ 返回 { sdk, loginRet }若服务端未返回
signalr(或signalr.accessToken为空),则跳过 SignalR 建连步骤,sdk为null,loginRet仍可用。
fastGo 完整配置
import { fastGo, FastGoError } from "edu-signalr-sdk";
try {
const { sdk, loginRet } = await fastGo({
// ==================== 必填 ====================
/** SignalR Hub 地址 */
hubUrl: "https://教辅端域名/hubs/traincloud",
/** 自动登录接口(获取用户信息 + SignalR Token) */
autoLoginApi: "/api/training/auto-login",
/** SignalR Token 刷新接口 */
srRefreshTokenApi: "/api/training/refresh-token",
/** 请求/响应拦截器 */
interceptors: {
// 对自动从 URL 解析的参数进行加工
request: (params: Record<string, any>) => params,
// 对登录接口返回的数据进行加工
response: (res: any) => res,
},
// ==================== 可选配置 ====================
/**
* 判断是否需要初始化 SignalR 连接。
* 返回 false 时仅执行登录流程(loginRet 仍可用),不建立长连接。
* @param params URL 查询参数
*/
requireSrFn: (params) => params.role === "student",
/** 心跳间隔(毫秒),默认 30000 */
heartbeatInterval: 30000,
/** 挂机判定阈值(毫秒),默认 120000 */
idleTimeout: 120000,
/** 最大重连次数,默认 3 */
maxReconnectTimes: 3,
// ==================== 事件回调 ====================
onConnected: () => console.log("已连接"),
onDisconnect: () => console.log("已断开"),
onReconnecting: () => console.log("正在重连..."),
onReconnected: () => console.log("重连成功"),
onKick: (msg) => {
alert(msg);
window.location.href = "/login";
},
onReconnectFail: () => alert("连接异常,请重新进入"),
// ==================== 自定义 Hub 方法注册 ====================
hubMethods: {
"OnScoreUpdate": (data) => updateScoreBoard(data),
"OnClassNotice": (msg) => showNotice(msg),
"OnReward": (payload) => showReward(payload),
},
// ==================== 行为开关 ====================
/** 是否自动绑定 beforeunload 销毁,默认 true */
autoDestroy: true,
});
// ===== 使用返回的 SDK 实例 =====
if (sdk) {
console.log(sdk.state); // "Connected"
// 调用服务端 Hub 方法(兼容后期扩展)
await sdk.invoke("SubmitAnswer", { questionId: 1, answer: "A" });
// 动态注册新事件
sdk.on("OnNewFeature", (data) => console.log(data));
}
// ===== 使用登录返回信息 =====
const { userName, accessToken, refreshToken, tokenExpires, refreshTokenExpires } = loginRet;
// 将 Token 写入状态管理...
} catch (reason) {
if (reason instanceof FastGoError) {
switch (reason.code) {
case "LOGIN_FAILED":
console.error("登录失败,服务端返回:", reason.data);
break;
case "UNKNOWN_ERROR":
console.error("未知错误:", reason.data);
break;
}
}
}说明:
fastGo内部基于EduSignalrSdk.create()实现。如需更细粒度的控制(如手动管理 Token 获取流程),请使用下方「标准接入」方式。
业务系统集成示例(Vue 2)
以下是一个实训系统入口页面的完整实现,展示 SDK 在真实项目中的典型用法:
<template>
<div class="init-container">
<div class="init-content">
<div class="logo-container">
<div class="logo-icon"></div>
</div>
<h1 class="title">网店数据分析平台</h1>
<div class="loading-container">
<div class="loading-spinner">
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
<div class="spinner-ring"></div>
</div>
<p class="loading-text">{{ message }}</p>
<div class="loading-dots">
<span></span><span></span><span></span>
</div>
</div>
<div class="footer-text">生意参谋 - 数据驱动决策</div>
</div>
</div>
</template>
<script lang="ts">
import user from "@/store/module/user";
import { fastGo, FastGoError } from "edu-signalr-sdk";
export default {
data() {
return { message: "正在初始化系统" };
},
async mounted() {
try {
// ===== 一键启动 SDK =====
const ret = await fastGo({
hubUrl: "http://192.168.31.206:5001/hubs/traincloud",
autoLoginApi: "http://localhost:3031/api/training/auto-login",
srRefreshTokenApi: "http://localhost:3031/api/training/refresh-token",
interceptors: {
request: (pdata: Record<string, any>) => pdata,
response: (res: any) => res,
},
onConnected: () => console.log("已连接"),
onDisconnect: () => console.log("已断开"),
onKick: (msg) => {
alert(msg);
window.location.href = "/login";
},
});
ret.sdk && console.log(ret.sdk.state); // "Connected"
if (ret.loginRet) {
const { userName, accessToken, refreshToken, tokenExpires, refreshTokenExpires } = ret.loginRet;
user.setToken(accessToken);
user.setUserName(userName);
user.setTraining(true);
this.$router.push({ name: this.$config.homeName });
} else {
this.$router.push("/");
}
} catch (reason) {
// ===== 类型安全的错误处理 =====
if (reason instanceof FastGoError) {
switch (reason.code) {
case "LOGIN_FAILED":
this.$Message.error({ content: reason.data.message });
break;
case "UNKNOWN_ERROR":
this.$Message.error({ content: reason.data.message });
break;
}
}
}
},
};
</script>核心集成要点:
fastGo— 一行代码完成 URL 参数解析 → 登录 → SignalR 建连interceptors— 对请求参数和响应数据进行加工(至少做透传)loginRet— 获取业务系统的用户名、Token 等信息写入状态管理FastGoError— 通过instanceof+code实现类型安全的错误分支处理
标准接入(高级用法)
当需要完全手动控制 Token 获取流程或连接生命周期时,使用 EduSignalrSdk.create():
import { EduSignalrSdk } from "edu-signalr-sdk";
const initSdk = async () => {
// 1. 获取实训临时 JWT
const res = await fetch("/api/train/get-jwt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ /* 自定义参数 */ }),
});
const data = await res.json();
// 2. 创建 SDK 实例(自动完成 FingerprintJS 指纹采集 + 连接建立)
const sdk = await EduSignalrSdk.create(
{
hubUrl: "https://你的教辅端域名/studyHub",
accessToken: data.accessToken,
expires: data.timestamp,
refreshExpires: data.refreshTimestamp,
// 可选:提供 token 刷新函数,重连时自动获取新 token
tokenRefreshFn: async () => {
const res = await fetch("/api/train/refresh-jwt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: data.refreshToken }),
});
const d = await res.json();
return d.sRToken || d.token || d.accessToken;
},
// 可选配置
heartbeatInterval: 30000, // 心跳间隔 30s
idleTimeout: 120000, // 无操作 2 分钟视为挂机
maxReconnectTimes: 3, // 最多重连 3 次
},
{
onConnected: () => console.log("SignalR 连接成功,开始实训计时"),
onDisconnect: () => console.log("连接已断开"),
onReconnecting: () => console.log("正在重连..."),
onReconnected: () => console.log("重连成功"),
onKick: (msg) => {
alert(msg);
window.location.href = "/login";
},
onReconnectFail: () => {
alert("与教辅服务连接异常,请关闭页面重新从教辅端进入");
},
}
);
// 查询连接状态
console.log("当前状态:", sdk.state); // "Connected"
// 页面关闭时销毁(sendBeacon 保证下线通知送达)
window.addEventListener("beforeunload", () => {
sdk.destroy();
});
};
initSdk();API 参考
fastGo(options)
一键启动方法,返回 Promise<{ sdk: EduSignalrSdk | null, loginRet: LoginRet }>。
自动完成:
- URL 参数解析
- 通过
interceptors.request加工请求参数 - 调用
autoLoginApi获取登录信息及 SignalR Token - 通过
interceptors.response加工响应数据 - 创建 SignalR 连接(若存在 SignalR Token)
- 注册事件回调与自定义 Hub 方法
- 自动绑定
beforeunload销毁
FastGoOptions
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| hubUrl | string | ✅ | - | 教辅端 SignalR Hub 地址 |
| autoLoginApi | string | ✅ | - | 自动登录接口地址 |
| srRefreshTokenApi | string | ✅ | - | SignalR Token 刷新接口地址 |
| interceptors | { request, response } | ✅ | - | 请求/响应拦截器(见下方说明) |
| requireSrFn | (params) => boolean | ❌ | - | 判断是否需要初始化 SignalR,返回 false 时仅登录不建连 |
| heartbeatInterval | number | ❌ | 30000 | 心跳间隔(毫秒) |
| idleTimeout | number | ❌ | 120000 | 挂机判定阈值(毫秒) |
| maxReconnectTimes | number | ❌ | 3 | 最大重连次数 |
| onConnected | () => void | ❌ | - | 首次连接成功回调 |
| onDisconnect | () => void | ❌ | - | 连接断开回调 |
| onReconnecting | () => void | ❌ | - | 正在重连回调 |
| onReconnected | () => void | ❌ | - | 重连成功回调 |
| onKick | (msg: string) => void | ❌ | - | 被踢下线回调 |
| onReconnectFail | () => void | ❌ | - | 重连次数耗尽回调 |
| hubMethods | Record<string, (...args: any[]) => void> | ❌ | - | 自定义 Hub 方法监听注册 |
| autoDestroy | boolean | ❌ | true | 是否自动绑定 beforeunload 销毁 |
interceptors 说明
| 拦截器 | 签名 | 说明 |
|--------|------|------|
| request | (params: Record<string, any>) => Record<string, any> | 在调用登录接口前,对自动从 URL 解析的参数进行加工 |
| response | (res: any) => any | 对登录接口返回的原始响应进行加工后交给 SDK |
返回值
{
sdk: EduSignalrSdk | null, // SignalR 实例(未建连时为 null)
loginRet: LoginRet // 登录返回的用户信息与 Token
}LoginRet
| 字段 | 类型 | 说明 |
|------|------|------|
| userName | string | 用户名 |
| accessToken | string | 业务系统访问令牌 |
| refreshToken | string | 业务系统刷新令牌 |
| tokenExpires | number | 业务访问令牌过期时间(秒级 Unix 时间戳) |
| refreshTokenExpires | number | 业务刷新令牌过期时间(秒级 Unix 时间戳) |
| [key: string] | any | 业务系统可返回更多自定义字段 |
FastGoError
当 fastGo 抛出异常时,可通过 instanceof 判断错误类型:
import { FastGoError } from "edu-signalr-sdk";
try {
await fastGo({...});
} catch (reason) {
if (reason instanceof FastGoError) {
console.log(reason.code); // "LOGIN_FAILED" | "UNKNOWN_ERROR"
console.log(reason.data); // 附加错误数据
}
}| 错误码 | 说明 |
|--------|------|
| LOGIN_FAILED | 登录接口返回 ok:false,data 为服务端返回数据 |
| UNKNOWN_ERROR | 网络异常或 SDK 创建失败,data 为原始异常对象 |
EduSignalrSdk.create(config, events)
异步工厂方法,返回 Promise<EduSignalrSdk>。自动完成指纹采集、SignalR 连接建立。
config: EduSignalrConfig
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| hubUrl | string | ✅ | - | 教辅端 SignalR Hub 地址 |
| accessToken | string | ✅ | - | SignalR 通信用 JWT |
| expires | number | ✅ | - | Token 过期时间(秒级 Unix 时间戳) |
| refreshExpires | number | ✅ | - | 刷新 Token 过期时间(秒级 Unix 时间戳) |
| tokenRefreshFn | () => Promise<string> | ❌ | - | Token 刷新函数,重连时自动调用 |
| heartbeatInterval | number | ❌ | 30000 | 心跳间隔(毫秒) |
| idleTimeout | number | ❌ | 120000 | 挂机判定阈值(毫秒) |
| maxReconnectTimes | number | ❌ | 3 | 最大重连次数 |
events: EduSdkEvents
| 回调 | 参数 | 说明 |
|------|------|------|
| onConnected | 无 | 首次连接成功 |
| onDisconnect | 无 | 连接断开 |
| onReconnecting | 无 | 正在重连 |
| onReconnected | 无 | 重连成功 |
| onKick | msg: string | 被服务端踢下线 |
| onReconnectFail | 无 | 重连次数耗尽 |
实例属性与方法
| 属性/方法 | 说明 |
|-----------|------|
| sdk.state | 只读属性,返回 SdkConnectionState 枚举值 |
| sdk.destroy() | 销毁实例,发送下线通知并关闭连接 |
| sdk.invoke(methodName, ...args) | 调用服务端 Hub 方法(兼容后期扩展) |
| sdk.on(methodName, handler) | 注册服务端推送事件监听(兼容后期扩展) |
| sdk.off(methodName, handler) | 移除服务端推送事件监听 |
sdk.invoke(methodName, ...args) 示例
// 调用服务端自定义 Hub 方法,无需升级 SDK
await sdk.invoke("SubmitAnswer", { questionId: 1, answer: "A" });
await sdk.invoke("RequestHint", questionId);sdk.on(methodName, handler) 示例
// 在 fastGo 初始化后动态注册新事件
sdk.on("OnScoreUpdate", (data) => {
console.log("成绩更新:", data);
});
sdk.on("OnNewMessage", (msg) => {
console.log("新消息:", msg);
});辅助工具函数
import {
getDeviceFingerprint,
isUserActive,
getIdleSeconds,
isPageVisible,
detectAutomation,
getBrowserInfo,
} from "edu-signalr-sdk";
// 获取浏览器设备指纹(基于 FingerprintJS 专业指纹库)
const fp = await getDeviceFingerprint();
// 检测自动化工具特征
const autoInfo = detectAutomation();
if (autoInfo.isAutomated) {
console.warn("检测到自动化特征:", autoInfo.flags);
}
// 查看当前空闲秒数
console.log("已空闲:", getIdleSeconds(), "秒");实训平台对接要求
自动登录接口 (autoLoginApi)
SDK 通过 POST 请求该接口,Body 为经过 interceptors.request 加工后的 URL 查询参数对象。
Response:
{
"ok": true,
"data": {
"userName": "张三",
"accessToken": "eyJhbG...(业务系统访问令牌)",
"refreshToken": "eyJhbG...(业务系统刷新令牌)",
"tokenExpires": 1700000000,
"refreshTokenExpires": 1700604800,
"signalr": {
"accessToken": "eyJhbG...(SignalR 通信用 Token)",
"refreshToken": "eyJhbG...(SignalR 刷新 Token)",
"timestamp": 1700000000,
"refreshTimestamp": 1700604800
}
}
}说明:
signalr字段为可选。如果服务端不返回signalr或signalr.accessToken为空,fastGo将仅执行登录流程,不建立 SignalR 连接(此时ret.sdk为null,ret.loginRet仍可用)。data中的额外字段(除signalr外)均会出现在loginRet中。
SignalR Token 刷新接口 (srRefreshTokenApi)
SDK 在重连时通过 POST 请求该接口以获取新 Token,Body 包含 refreshToken 字段。
Response(兼容多种字段名):
{
"sRToken": "eyJhbG...(新 Token)"
}或
{
"token": "eyJhbG..."
}或
{
"accessToken": "eyJhbG..."
}心跳数据协议
SDK 每 30 秒向 Hub 发送 SendHeartbeat:
{
"msgType": "heartbeat",
"timestamp": 1687000000000,
"duration": 3600,
"isActive": true,
"finger": "a1b2c3d4..."
}duration:累计实训时长(秒),从首次连接开始计算isActive:当前是否活跃(有真实操作且页面可见)finger:浏览器设备指纹 ID(由 FingerprintJS 生成,稳定度极高)
教辅端 Hub 接口规范
实训平台需要实现以下 SignalR Hub 方法:
| 方向 | 方法名 | 说明 |
|------|--------|------|
| SDK → Hub | SendHeartbeat | 心跳上报 |
| SDK → Hub | SendLogout | 主动下线通知 |
| Hub → SDK | KickNotify | 服务端踢人通知 |
自定义扩展
服务端可随时新增 Hub 方法,SDK 无需升级即可支持:
// 服务端新增客户端推送事件 → 前端注册监听
sdk.on("OnNewServerEvent", (payload) => { /* 处理 */ });
// 服务端新增可调用方法 → 前端调用
await sdk.invoke("NewServerMethod", { key: "value" });集成检查清单
- [ ] 安装 SDK —
npm install edu-signalr-sdk - [ ] 配置
hubUrl— 指向教辅端 SignalR Hub 地址 - [ ] 实现
autoLoginApi接口 — 返回{ ok, data: { userName, accessToken, ..., signalr } } - [ ] 实现
srRefreshTokenApi接口 — 接收refreshToken,返回新 Token - [ ] 配置
interceptors— 至少做透传{ request: p => p, response: r => r } - [ ] 处理
loginRet— 将 Token 和用户信息写入状态管理 - [ ] 处理
FastGoError— 捕获LOGIN_FAILED和UNKNOWN_ERROR并提示用户 - [ ] 注册必要的事件回调 — 至少
onKick(被踢下线跳转登录页) - [ ] (可选)配置
requireSrFn— 当部分页面不需要长连接时跳过建连 - [ ] (可选)配置
hubMethods— 注册服务端自定义推送事件
License
MIT
