web-step-counter-pro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -0
- package/package.json +26 -0
- package/step-counter.js +348 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Web Step Counter API 文档
|
|
2
|
+
|
|
3
|
+
`web-step-counter` 是一个专为移动端 Web 应用设计的高精度、防作弊步数统计组件。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 如果发布到 npm (示例)
|
|
9
|
+
npm install web-step-counter
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
或者直接引入 JS 文件:
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
import { WebStepCounter } from './step-counter.js';
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 快速开始
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
const counter = new WebStepCounter({
|
|
22
|
+
onStep: (steps) => console.log(`步数: ${steps}`),
|
|
23
|
+
onStatus: (msg, type) => console.log(`状态: ${msg}`),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// 必须在用户交互(点击事件)中调用
|
|
27
|
+
document.getElementById('btn').onclick = () => {
|
|
28
|
+
counter.start();
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 配置项 (Configuration)
|
|
35
|
+
|
|
36
|
+
初始化时可传入配置对象:`new WebStepCounter(config)`
|
|
37
|
+
|
|
38
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
39
|
+
| :--- | :--- | :--- | :--- |
|
|
40
|
+
| `sensitivity` | Number | `12.0` | 加速度灵敏度阈值 (m/s²)。数值越小越灵敏,但也越容易误判。 |
|
|
41
|
+
| `minStepInterval` | Number | `330` | 最小步频间隔 (ms)。小于此间隔视为震动干扰。 |
|
|
42
|
+
| `maxStepInterval` | Number | `2000` | 最大步频间隔 (ms)。超过此间隔视为停顿。 |
|
|
43
|
+
| `minConsecutiveSteps`| Number | `7` | 连续步数门槛。连续走满 N 步才开始计入总数,防止误触。 |
|
|
44
|
+
| `cheatCheckInterval` | Number | `5000` | 防作弊检查周期 (ms)。每隔多久检查一次 GPS 和姿态。 |
|
|
45
|
+
| `gpsAccuracyLimit` | Number | `20` | GPS 精度要求 (米)。精度低于此值时不进行 GPS 相关的防作弊判定。 |
|
|
46
|
+
| `maxSpeed` | Number | `2.8` | 最大允许速度 (m/s)。约 10km/h,超过此速度不计步。 |
|
|
47
|
+
| `debug` | Boolean | `false` | 是否在控制台打印详细调试日志。 |
|
|
48
|
+
| `onStep` | Function | `null` | 步数更新回调 `(steps) => void` |
|
|
49
|
+
| `onStatus` | Function | `null` | 状态/作弊回调 `(msg, type) => void` |
|
|
50
|
+
| `onSensorData` | Function | `null` | 传感器原始数据回调 `(acc, orient) => void` |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 方法 (Methods)
|
|
55
|
+
|
|
56
|
+
### `start(): Promise<boolean>`
|
|
57
|
+
初始化传感器并开始计步。
|
|
58
|
+
* **必须在用户点击事件中调用**(iOS 权限要求)。
|
|
59
|
+
* 自动处理 iOS 13+ 的权限请求流程。
|
|
60
|
+
* 返回 `true` 表示启动成功,`false` 表示被拒绝或出错。
|
|
61
|
+
|
|
62
|
+
### `stop(): void`
|
|
63
|
+
停止计步,移除所有传感器监听器,停止 GPS 轮询。
|
|
64
|
+
|
|
65
|
+
### `reset(): void`
|
|
66
|
+
重置步数、防作弊历史数据和内部缓冲区。
|
|
67
|
+
|
|
68
|
+
### `requestOrientationPermission(): Promise<boolean>`
|
|
69
|
+
手动请求设备方向权限的辅助方法。
|
|
70
|
+
* 用于解决 iOS 上某些情况下 Orientation 权限未被正确授予的问题。
|
|
71
|
+
* 建议在 UI 上提供一个“修复权限”按钮调用此方法。
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 状态与防作弊代码 (Status & Cheating Types)
|
|
76
|
+
|
|
77
|
+
`onStatus(msg, type)` 回调会返回当前系统的运行状态或检测到的异常。
|
|
78
|
+
|
|
79
|
+
### 状态类型 (`type`)
|
|
80
|
+
|
|
81
|
+
| 类型 (Type) | 说明 | 示例消息 |
|
|
82
|
+
| :--- | :--- | :--- |
|
|
83
|
+
| `'success'` | 正常运行 | "正在运行", "GPS 验证正常" |
|
|
84
|
+
| `'info'` | 一般信息 | "已停止", "已重置" |
|
|
85
|
+
| `'warning'` | 警告/非阻断性问题 | "GPS 信号弱", "需手动授权 Orientation" |
|
|
86
|
+
| `'error'` | 错误/阻断性问题 | "Motion 权限被拒绝", "必须使用 HTTPS" |
|
|
87
|
+
| `'cheating'` | **检测到作弊行为** | 见下方作弊类型 |
|
|
88
|
+
|
|
89
|
+
### 常见作弊/异常消息
|
|
90
|
+
|
|
91
|
+
| 消息内容 | 触发原因 | 算法逻辑 |
|
|
92
|
+
| :--- | :--- | :--- |
|
|
93
|
+
| `"检测到剧烈摇晃"` | 陀螺仪检测到高频旋转 | `RotationRate > 300 deg/s` |
|
|
94
|
+
| `"检测到机械节奏"` | 步频方差极低 (摇步机) | 最近 20 步的时间间隔标准差 `< 5ms` |
|
|
95
|
+
| `"检测到原地运动 (GPS)"` | 步数增加但 GPS 未移动 | `StepsDiff > 15` 且 `GPSDistance < 3m` |
|
|
96
|
+
| `"速度过快 (xx km/h)"` | 移动速度超过阈值 | `Speed > 10 km/h` (判定为骑车/开车) |
|
|
97
|
+
| *(静默丢弃)* | 手机水平放置/拍桌子 | `Beta < 5°` 且 `Gamma < 5°` |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 最佳实践
|
|
102
|
+
|
|
103
|
+
1. **HTTPS**: 必须在 HTTPS 环境下使用,否则现代浏览器拒绝访问传感器。
|
|
104
|
+
2. **iOS 权限**: 务必设计引导 UI,提示用户在弹窗中点击“允许”。
|
|
105
|
+
3. **UI 反馈**: 当 `type === 'cheating'` 时,建议在 UI 上显示醒目的红色警告,提示用户“步数可能无效”。
|
|
106
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web-step-counter-pro",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A professional-grade web step counting library with advanced anti-cheat mechanisms (GPS, Gyroscope, Rhythm Analysis).",
|
|
5
|
+
"main": "step-counter.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pedometer",
|
|
12
|
+
"step-counter",
|
|
13
|
+
"anti-cheat",
|
|
14
|
+
"gps",
|
|
15
|
+
"gyroscope",
|
|
16
|
+
"fitness",
|
|
17
|
+
"web-api"
|
|
18
|
+
],
|
|
19
|
+
"author": "Your Name",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"files": [
|
|
22
|
+
"step-counter.js",
|
|
23
|
+
"README.md",
|
|
24
|
+
"package.json"
|
|
25
|
+
]
|
|
26
|
+
}
|
package/step-counter.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebStepCounter - 专业级 Web 步数统计与防作弊组件
|
|
3
|
+
*/
|
|
4
|
+
export class WebStepCounter {
|
|
5
|
+
/**
|
|
6
|
+
* 状态类型枚举
|
|
7
|
+
* @readonly
|
|
8
|
+
* @enum {string}
|
|
9
|
+
*/
|
|
10
|
+
static StatusType = {
|
|
11
|
+
SUCCESS: 'success',
|
|
12
|
+
INFO: 'info',
|
|
13
|
+
WARNING: 'warning',
|
|
14
|
+
ERROR: 'error',
|
|
15
|
+
CHEATING: 'cheating'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 错误/状态码枚举
|
|
20
|
+
* @readonly
|
|
21
|
+
* @enum {string}
|
|
22
|
+
*/
|
|
23
|
+
static StatusCode = {
|
|
24
|
+
// 正常
|
|
25
|
+
RUNNING: 'RUNNING',
|
|
26
|
+
GPS_OK: 'GPS_OK',
|
|
27
|
+
|
|
28
|
+
// 错误
|
|
29
|
+
HTTPS_REQUIRED: 'HTTPS_REQUIRED',
|
|
30
|
+
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
31
|
+
GPS_ERROR: 'GPS_ERROR',
|
|
32
|
+
|
|
33
|
+
// 作弊/异常
|
|
34
|
+
CHEAT_SHAKING: 'CHEAT_SHAKING', // 剧烈摇晃 (陀螺仪)
|
|
35
|
+
CHEAT_MECHANICAL: 'CHEAT_MECHANICAL', // 机械节奏 (摇步机)
|
|
36
|
+
CHEAT_STATIONARY: 'CHEAT_STATIONARY', // 原地踏步 (GPS)
|
|
37
|
+
CHEAT_OVERSPEED: 'CHEAT_OVERSPEED', // 速度过快 (开车)
|
|
38
|
+
INVALID_ORIENTATION: 'INVALID_ORIENTATION' // 姿态无效 (拍桌子)
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
// ... (保持原有构造函数逻辑,但推荐使用常量)
|
|
43
|
+
this.config = {
|
|
44
|
+
sensitivity: 12.0,
|
|
45
|
+
minStepInterval: 330,
|
|
46
|
+
maxStepInterval: 2000,
|
|
47
|
+
minConsecutiveSteps: 7,
|
|
48
|
+
cheatCheckInterval: 5000,
|
|
49
|
+
gpsAccuracyLimit: 20,
|
|
50
|
+
maxSpeed: 2.8,
|
|
51
|
+
debug: false,
|
|
52
|
+
onStep: null,
|
|
53
|
+
onStatus: null,
|
|
54
|
+
onSensorData: null,
|
|
55
|
+
...config
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 内部状态
|
|
59
|
+
this.stepCount = 0;
|
|
60
|
+
this.isTracking = false;
|
|
61
|
+
this.candidateSteps = 0;
|
|
62
|
+
this.lastStepTime = 0;
|
|
63
|
+
this.lastCandidateStepTime = 0;
|
|
64
|
+
|
|
65
|
+
this.accBuffer = [];
|
|
66
|
+
this.timestampBuffer = [];
|
|
67
|
+
this.windowSize = 50;
|
|
68
|
+
|
|
69
|
+
this.lastCheckTime = 0;
|
|
70
|
+
this.lastCheckStepCount = 0;
|
|
71
|
+
this.lastCheckLat = null;
|
|
72
|
+
this.lastCheckLon = null;
|
|
73
|
+
this.stepIntervalHistory = [];
|
|
74
|
+
this.gyroCheating = false;
|
|
75
|
+
this.orientationValid = true;
|
|
76
|
+
|
|
77
|
+
this.orientationData = { alpha: 0, beta: 0, gamma: 0 };
|
|
78
|
+
|
|
79
|
+
this.handleMotion = this.handleMotion.bind(this);
|
|
80
|
+
this.handleOrientation = this.handleOrientation.bind(this);
|
|
81
|
+
this.handleGeolocation = this.handleGeolocation.bind(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async start() {
|
|
85
|
+
if (this.isTracking) return;
|
|
86
|
+
|
|
87
|
+
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
|
88
|
+
this._reportStatus("必须使用 HTTPS 协议", WebStepCounter.StatusType.ERROR, WebStepCounter.StatusCode.HTTPS_REQUIRED);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this._reportStatus("正在请求传感器权限...", WebStepCounter.StatusType.INFO);
|
|
93
|
+
|
|
94
|
+
const permissionGranted = await this._requestPermissions();
|
|
95
|
+
if (!permissionGranted) return false;
|
|
96
|
+
|
|
97
|
+
if ("geolocation" in navigator) {
|
|
98
|
+
this.watchId = navigator.geolocation.watchPosition(
|
|
99
|
+
this.handleGeolocation,
|
|
100
|
+
(err) => this._reportStatus(`GPS 错误: ${err.message}`, WebStepCounter.StatusType.WARNING, WebStepCounter.StatusCode.GPS_ERROR),
|
|
101
|
+
{ enableHighAccuracy: true, timeout: 5000, maximumAge: 0 }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.isTracking = true;
|
|
106
|
+
this._reportStatus("正在运行", WebStepCounter.StatusType.SUCCESS, WebStepCounter.StatusCode.RUNNING);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
stop() {
|
|
111
|
+
this.isTracking = false;
|
|
112
|
+
window.removeEventListener('devicemotion', this.handleMotion);
|
|
113
|
+
window.removeEventListener('deviceorientation', this.handleOrientation);
|
|
114
|
+
window.removeEventListener('deviceorientationabsolute', this.handleOrientation);
|
|
115
|
+
|
|
116
|
+
if (this.watchId) {
|
|
117
|
+
navigator.geolocation.clearWatch(this.watchId);
|
|
118
|
+
}
|
|
119
|
+
this._reportStatus("已停止", WebStepCounter.StatusType.INFO);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
reset() {
|
|
123
|
+
this.stepCount = 0;
|
|
124
|
+
this.candidateSteps = 0;
|
|
125
|
+
this.accBuffer = [];
|
|
126
|
+
this.stepIntervalHistory = [];
|
|
127
|
+
this.lastCheckLat = null;
|
|
128
|
+
this._triggerStepUpdate();
|
|
129
|
+
this._reportStatus("已重置", WebStepCounter.StatusType.INFO);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async requestOrientationPermission() {
|
|
133
|
+
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
|
|
134
|
+
try {
|
|
135
|
+
const permission = await DeviceOrientationEvent.requestPermission();
|
|
136
|
+
if (permission === 'granted') {
|
|
137
|
+
window.addEventListener('deviceorientation', this.handleOrientation);
|
|
138
|
+
window.addEventListener('deviceorientationabsolute', this.handleOrientation);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.error(e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Internal ---
|
|
149
|
+
|
|
150
|
+
async _requestPermissions() {
|
|
151
|
+
if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {
|
|
152
|
+
try {
|
|
153
|
+
const motionPerm = await DeviceMotionEvent.requestPermission();
|
|
154
|
+
if (motionPerm !== 'granted') {
|
|
155
|
+
this._reportStatus("Motion 权限被拒绝", WebStepCounter.StatusType.ERROR, WebStepCounter.StatusCode.PERMISSION_DENIED);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
window.addEventListener('devicemotion', this.handleMotion);
|
|
159
|
+
|
|
160
|
+
if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function') {
|
|
161
|
+
try {
|
|
162
|
+
const orientPerm = await DeviceOrientationEvent.requestPermission();
|
|
163
|
+
if (orientPerm === 'granted') {
|
|
164
|
+
window.addEventListener('deviceorientation', this.handleOrientation);
|
|
165
|
+
window.addEventListener('deviceorientationabsolute', this.handleOrientation);
|
|
166
|
+
} else {
|
|
167
|
+
this._reportStatus("需手动授权 Orientation", WebStepCounter.StatusType.WARNING, WebStepCounter.StatusCode.PERMISSION_DENIED);
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
this._reportStatus("需手动授权 Orientation", WebStepCounter.StatusType.WARNING);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
this._reportStatus(`权限请求出错: ${e.message}`, WebStepCounter.StatusType.ERROR);
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
window.addEventListener('devicemotion', this.handleMotion);
|
|
179
|
+
if (window.DeviceOrientationEvent) {
|
|
180
|
+
window.addEventListener('deviceorientation', this.handleOrientation);
|
|
181
|
+
}
|
|
182
|
+
if ('ondeviceorientationabsolute' in window) {
|
|
183
|
+
window.addEventListener('deviceorientationabsolute', this.handleOrientation);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
handleMotion(event) {
|
|
190
|
+
const acc = event.accelerationIncludingGravity;
|
|
191
|
+
const rotation = event.rotationRate;
|
|
192
|
+
if (!acc) return;
|
|
193
|
+
|
|
194
|
+
if (this._checkGyroCheating(rotation)) {
|
|
195
|
+
this._reportStatus("检测到剧烈摇晃", WebStepCounter.StatusType.CHEATING, WebStepCounter.StatusCode.CHEAT_SHAKING);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!this._checkOrientationValidity()) {
|
|
200
|
+
// 静默丢弃,不频繁刷日志
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (this.config.onSensorData) {
|
|
205
|
+
this.config.onSensorData({ x: acc.x, y: acc.y, z: acc.z }, this.orientationData);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const magnitude = Math.sqrt(acc.x*acc.x + acc.y*acc.y + acc.z*acc.z);
|
|
209
|
+
this._processGait(magnitude, Date.now());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
handleOrientation(event) {
|
|
213
|
+
this.orientationData = {
|
|
214
|
+
alpha: event.alpha || 0,
|
|
215
|
+
beta: event.beta || 0,
|
|
216
|
+
gamma: event.gamma || 0
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
handleGeolocation(position) {
|
|
221
|
+
const { latitude, longitude, accuracy, speed } = position.coords;
|
|
222
|
+
this._checkAntiCheatGPS(latitude, longitude, accuracy, speed);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
_processGait(magnitude, now) {
|
|
226
|
+
this.accBuffer.push(magnitude);
|
|
227
|
+
this.timestampBuffer.push(now);
|
|
228
|
+
|
|
229
|
+
if (this.accBuffer.length > this.windowSize) {
|
|
230
|
+
this.accBuffer.shift();
|
|
231
|
+
this.timestampBuffer.shift();
|
|
232
|
+
}
|
|
233
|
+
if (this.accBuffer.length < 15) return;
|
|
234
|
+
|
|
235
|
+
let min = Infinity, max = -Infinity;
|
|
236
|
+
for (let val of this.accBuffer) {
|
|
237
|
+
if (val < min) min = val;
|
|
238
|
+
if (val > max) max = val;
|
|
239
|
+
}
|
|
240
|
+
const range = max - min;
|
|
241
|
+
if (range < 2.0) return;
|
|
242
|
+
|
|
243
|
+
const currentThreshold = (min + max) / 2;
|
|
244
|
+
const midIndex = Math.floor(this.accBuffer.length / 2);
|
|
245
|
+
const midVal = this.accBuffer[midIndex];
|
|
246
|
+
const midTime = this.timestampBuffer[midIndex];
|
|
247
|
+
|
|
248
|
+
if (midVal >= currentThreshold &&
|
|
249
|
+
midVal > this.accBuffer[midIndex-1] &&
|
|
250
|
+
midVal > this.accBuffer[midIndex+1]) {
|
|
251
|
+
|
|
252
|
+
const timeSinceLast = midTime - this.lastCandidateStepTime;
|
|
253
|
+
|
|
254
|
+
if (timeSinceLast > this.config.minStepInterval &&
|
|
255
|
+
timeSinceLast < this.config.maxStepInterval) {
|
|
256
|
+
|
|
257
|
+
if (this._checkMechanicalRhythm(timeSinceLast)) {
|
|
258
|
+
this._reportStatus("检测到机械节奏", WebStepCounter.StatusType.CHEATING, WebStepCounter.StatusCode.CHEAT_MECHANICAL);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.candidateSteps++;
|
|
263
|
+
this.lastCandidateStepTime = midTime;
|
|
264
|
+
|
|
265
|
+
if (this.candidateSteps === this.config.minConsecutiveSteps) {
|
|
266
|
+
this.stepCount += this.config.minConsecutiveSteps;
|
|
267
|
+
this._triggerStepUpdate();
|
|
268
|
+
} else if (this.candidateSteps > this.config.minConsecutiveSteps) {
|
|
269
|
+
this.stepCount++;
|
|
270
|
+
this._triggerStepUpdate();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
} else if (timeSinceLast > this.config.maxStepInterval) {
|
|
274
|
+
this.candidateSteps = 1;
|
|
275
|
+
this.lastCandidateStepTime = midTime;
|
|
276
|
+
this.stepIntervalHistory = [];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_checkGyroCheating(rotation) {
|
|
282
|
+
if (!rotation) return false;
|
|
283
|
+
const mag = Math.sqrt(rotation.alpha**2 + rotation.beta**2 + rotation.gamma**2);
|
|
284
|
+
return mag > 300;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
_checkOrientationValidity() {
|
|
288
|
+
const { beta, gamma } = this.orientationData;
|
|
289
|
+
return !(Math.abs(beta) < 5 && Math.abs(gamma) < 5);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_checkMechanicalRhythm(interval) {
|
|
293
|
+
this.stepIntervalHistory.push(interval);
|
|
294
|
+
if (this.stepIntervalHistory.length > 20) this.stepIntervalHistory.shift();
|
|
295
|
+
if (this.stepIntervalHistory.length < 20) return false;
|
|
296
|
+
|
|
297
|
+
const avg = this.stepIntervalHistory.reduce((a,b)=>a+b,0) / 20;
|
|
298
|
+
const variance = this.stepIntervalHistory.reduce((a,b)=>a+Math.pow(b-avg,2),0) / 20;
|
|
299
|
+
return Math.sqrt(variance) < 5;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_checkAntiCheatGPS(lat, lon, accuracy, speed) {
|
|
303
|
+
const now = Date.now();
|
|
304
|
+
|
|
305
|
+
if (speed && speed > this.config.maxSpeed) {
|
|
306
|
+
this._reportStatus(`速度过快 (${(speed*3.6).toFixed(1)}km/h)`, WebStepCounter.StatusType.CHEATING, WebStepCounter.StatusCode.CHEAT_OVERSPEED);
|
|
307
|
+
this.lastCheckLat = lat; this.lastCheckLon = lon; this.lastCheckTime = now;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (this.lastCheckLat === null) {
|
|
312
|
+
this.lastCheckLat = lat; this.lastCheckLon = lon;
|
|
313
|
+
this.lastCheckTime = now; this.lastCheckStepCount = this.stepCount;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (now - this.lastCheckTime > this.config.cheatCheckInterval) {
|
|
318
|
+
const stepsDiff = this.stepCount - this.lastCheckStepCount;
|
|
319
|
+
const dist = this._getDist(this.lastCheckLat, this.lastCheckLon, lat, lon);
|
|
320
|
+
|
|
321
|
+
if (stepsDiff > 15 && dist < 3.0 && accuracy < this.config.gpsAccuracyLimit) {
|
|
322
|
+
this._reportStatus("检测到原地运动 (GPS)", WebStepCounter.StatusType.CHEATING, WebStepCounter.StatusCode.CHEAT_STATIONARY);
|
|
323
|
+
} else {
|
|
324
|
+
this._reportStatus("GPS 验证正常", WebStepCounter.StatusType.SUCCESS, WebStepCounter.StatusCode.GPS_OK);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.lastCheckLat = lat; this.lastCheckLon = lon;
|
|
328
|
+
this.lastCheckTime = now; this.lastCheckStepCount = this.stepCount;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
_getDist(lat1, lon1, lat2, lon2) {
|
|
333
|
+
const R = 6371000;
|
|
334
|
+
const dLat = (lat2-lat1) * Math.PI/180;
|
|
335
|
+
const dLon = (lon2-lon1) * Math.PI/180;
|
|
336
|
+
const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;
|
|
337
|
+
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_triggerStepUpdate() {
|
|
341
|
+
if (this.config.onStep) this.config.onStep(this.stepCount);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_reportStatus(msg, type, code) {
|
|
345
|
+
if (this.config.debug) console.log(`[WebStepCounter] ${msg} [${code || type}]`);
|
|
346
|
+
if (this.config.onStatus) this.config.onStatus(msg, type, code);
|
|
347
|
+
}
|
|
348
|
+
}
|