diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index f28b667..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,559 +0,0 @@ -# Badge Admin - KKS P240/P241 蓝牙工牌管理系统 - -## 项目概览 - -KKS P240/P241 蓝牙工牌管理后台,基于 FastAPI + SQLAlchemy + asyncio TCP 服务器。 -支持设备管理、实时定位、告警、考勤打卡、蓝牙记录、指令下发、TTS语音播报等功能。 - -## 项目结构 - -``` -/home/gpsystem/ -├── run.py # 启动脚本 (uvicorn) -├── .env.example # 环境变量模板 (复制为 .env 使用) -├── requirements.txt # Python 依赖 -├── frpc.toml # FRP 客户端配置 (TCP隧道) -├── badge_admin.db # SQLite 数据库 -├── server.log # 服务器日志 -│ -├── app/ -│ ├── main.py # FastAPI 应用入口, 挂载静态文件, 启动TCP服务器 -│ ├── config.py # 配置 (pydantic-settings, .env支持, 端口/API密钥/缓存/限流) -│ ├── database.py # SQLAlchemy async 数据库连接 -│ ├── dependencies.py # FastAPI 依赖 (多API Key认证 + 权限控制: read/write/admin) -│ ├── extensions.py # 共享实例 (rate limiter, 真实IP提取) -│ ├── websocket_manager.py # WebSocket 连接管理器 (topic订阅, 实时广播) -│ ├── models.py # ORM 模型 (Device, LocationRecord, AlarmRecord, HeartbeatRecord, AttendanceRecord, BluetoothRecord, BeaconConfig, CommandLog, ApiKey) -│ ├── schemas.py # Pydantic 请求/响应模型 -│ ├── tcp_server.py # TCP 服务器核心 (~2400行), 管理设备连接、协议处理、数据持久化 -│ ├── geocoding.py # 高德地理编码服务 (基站/WiFi → 经纬度 + 经纬度 → 地址) -│ │ -│ ├── protocol/ -│ │ ├── constants.py # 协议常量 (协议号、告警类型snake_case、信号等级等) -│ │ ├── parser.py # 二进制协议解析器 -│ │ ├── builder.py # 响应包构建器 -│ │ └── crc.py # CRC-ITU (CRC-16/X-25, 多项式 0x8408) -│ │ -│ ├── services/ -│ │ ├── device_service.py # 设备 CRUD -│ │ ├── command_service.py # 指令日志 CRUD -│ │ ├── location_service.py # 位置记录查询 -│ │ ├── beacon_service.py # 蓝牙信标 CRUD -│ │ └── tcp_command_service.py # TCP指令抽象层 (解耦routers↔tcp_server) -│ │ -│ ├── routers/ -│ │ ├── devices.py # /api/devices (含 /stats, /batch, /batch-delete, /all-latest-locations) -│ │ ├── commands.py # /api/commands (含 /send, /message, /tts, /batch) -│ │ ├── locations.py # /api/locations (含 /latest, /batch-latest, /track, /{id}) -│ │ ├── alarms.py # /api/alarms (含 acknowledge, alarm_source过滤) -│ │ ├── attendance.py # /api/attendance (含 /stats, /{id}) -│ │ ├── bluetooth.py # /api/bluetooth (含 beacon_mac过滤, /{id}) -│ │ ├── heartbeats.py # /api/heartbeats (心跳记录查询) -│ │ ├── beacons.py # /api/beacons (信标管理 CRUD) -│ │ ├── api_keys.py # /api/keys (API密钥管理 CRUD, admin only) -│ │ └── ws.py # /ws (WebSocket实时推送, topic订阅) -│ │ -│ └── static/ -│ └── admin.html # 管理后台 SPA (暗色主题, 8个页面) -│ -└── doc/ - └── KKS_Protocol_P240_P241.md # 协议文档 (从PDF转换) -``` - -## 架构设计 - -### 网络拓扑 - -``` -[P241 工牌] --TCP--> [152.69.205.186:5001 (frps)] - | - FRP Tunnel - | - [Container:5000 (frpc)] - | - [TCP Server (asyncio)] - | - [FastAPI :8088] --CF Tunnel--> [Web 访问] -``` - -### 端口说明 -- **8088**: FastAPI HTTP API + 管理后台 UI -- **5000**: TCP 服务器 (KKS 二进制协议) -- **5001**: 远程服务器 FRP 映射端口 (152.69.205.186) -- **7000**: FRP 服务端管理端口 -- **7500**: FRP Dashboard (admin/PassWord0325) - -### 配置管理 -- `app/config.py` 使用 **pydantic-settings** (`BaseSettings`),支持 `.env` 文件覆盖默认值 -- `.env.example` 提供所有可配置项模板,复制为 `.env` 使用 -- DATABASE_URL 使用绝对路径 (基于 `__file__` 计算项目根目录) -- 高德 API 密钥集中在 `config.py`,`geocoding.py` 从 `settings` 导入 -- 端口号有 pydantic 校验 (ge=1, le=65535) - -## 启动服务 - -```bash -# 启动服务 -cd /home/gpsystem -nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8088 > server.log 2>&1 & - -# 启动 FRP 客户端 (TCP隧道) -nohup /tmp/frpc -c /home/gpsystem/frpc.toml > /tmp/frpc.log 2>&1 & - -# 检查服务状态 -curl http://localhost:8088/health -curl http://localhost:8088/api/devices -``` - -## 管理后台 UI - -访问 `/admin` 路径,包含以下页面: -1. **仪表盘** - 设备统计、告警概览、在线状态 -2. **设备管理** - 添加/编辑/删除设备 -3. **位置追踪** - Leaflet 地图、轨迹回放、地址显示 -4. **告警管理** - 告警列表、确认处理、位置/地址显示 -5. **考勤记录** - 上下班打卡记录 (0xB0/0xB1 GPS+LBS+WiFi) -6. **蓝牙记录** - 蓝牙打卡(0xB2)/定位(0xB3)记录,含信标MAC/UUID/RSSI等 -7. **信标管理** - 蓝牙信标注册、位置配置(MAC/UUID/楼层/区域/经纬度) -8. **指令管理** - 发送指令/留言/TTS语音 - -## 协议详情 - -KKS 二进制协议,详见 `doc/KKS_Protocol_P240_P241.md` - -### 已支持协议号 -| 协议号 | 名称 | 方向 | -|--------|------|------| -| 0x01 | Login 登录 | 终端→服务器 | -| 0x13 | Heartbeat 心跳 | 双向 | -| 0x1F | Time Sync 时间同步 | 双向 | -| 0x22 | GPS 定位 | 终端→服务器 | -| 0x26 | Alarm ACK 告警确认 | 服务器→终端 | -| 0x28 | LBS 多基站 | 终端→服务器 | -| 0x2C | WiFi 定位 | 终端→服务器 | -| 0x36 | Heartbeat Extended 扩展心跳 | 双向 | -| 0x80 | Online Command 在线指令 | 服务器→终端 | -| 0x81 | Online Command Reply | 终端→服务器 | -| 0x82 | Message 留言 | 服务器→终端 | -| 0x94 | General Info (ICCID/IMSI等) | 双向 | -| 0xA0 | 4G GPS 定位 | 终端→服务器 | -| 0xA1 | 4G LBS 多基站 | 终端→服务器 | -| 0xA2 | 4G WiFi 定位 | 终端→服务器 | -| 0xA3-0xA5 | 4G 告警 (围栏/LBS) | 终端→服务器 | -| 0xA9 | WiFi 告警 | 终端→服务器 | -| 0xB0-0xB1 | 考勤打卡 | 双向 | -| 0xB2 | 蓝牙打卡 | 双向 | -| 0xB3 | 蓝牙定位 | 终端→服务器 | - -### 数据包格式 -``` -[Start 0x7878/0x7979] [Length 1-2B] [Protocol 1B] [Content NB] [Serial 2B] [CRC 2B] [Stop 0x0D0A] -``` - -## 当前设备 - -| 字段 | 值 | -|------|-----| -| IMEI | 868120334031363 | -| 型号 | P241 | -| 名称 | 测试 | -| DB ID | 1 | -| ICCID | 89860848102570005286 | -| IMSI | 460240388355286 | -| SIM卡 | 中国移动 IoT (MCC=460, MNC=0) | -| 位置 | 成都地区 (~30.605°N, 103.936°E) | - -## 重要实现细节 - -### IMEI 解析 -- BCD 编码: 8字节 = 16 hex digits, 首位为填充 0 -- 使用 `raw_hex[1:]` 只移除首位填充0 (不用 lstrip 避免移除多个) - -### 设备登录 -- 登录时不覆盖用户设置的 device_type (避免被覆盖为 raw hex) -- 重连时先关闭旧连接再建立新连接 -- 断开连接时检查 writer 是否匹配,避免误将新连接标记为 offline - -### 告警处理 -- 告警响应使用 **0x26 ACK** (非原始协议号回复) -- 告警类型常量使用 **snake_case** (如 `sos`, `vibration`, `enter_fence`) -- 4G 告警包解析: - - **0xA3/0xA4** (围栏告警): datetime(6) + gps(12) + lbs_length(1) + MCC/MNC + LAC(4) + CellID(8) + terminal_info(1) + voltage_level(**1**字节) + gsm_signal(1) + alarm_code(1) + language(1) - - **0xA5** (LBS告警): **无datetime, 无GPS** — 直接从 MCC 开始 - - **0xA9** (WiFi告警): datetime(6) + MCC/MNC + cell_type(1) + cell_count(1) + 基站列表 + timing_advance(1) + WiFi列表 + alarm_code(1) + language(1) -- MCC 高位 `0x8000` 标志: 表示 MNC 为 2 字节 (非默认1字节) -- voltage_level 是 **1字节** (0x00-0x06 等级),不是2字节毫伏值 -- LBS/WiFi 报警自动执行前向地理编码(基站→经纬度)+ 逆地理编码(经纬度→地址) - -### 位置数据与地理编码 -- GPS 定位 (0x22/0xA0): 直接包含经纬度坐标,精度最高 (~10m) -- LBS 基站定位 (0x28/0xA1): 包含 MCC/MNC/LAC/CellID,需要地理编码转换为经纬度 -- WiFi 定位 (0x2C/0xA2): 包含基站数据 + WiFi AP MAC地址列表,需要地理编码 -- **所有地理编码服务统一使用高德 (Amap)** -- **前向地理编码** (`geocode_location`): 基站/WiFi → 经纬度 - - **高德智能硬件定位**: `apilocate.amap.com/position`,需企业认证 (认证中,个人账号返回 10012) - - WiFi+基站混合定位精度 ~30m,企业认证通过后自动生效 -- **逆地理编码** (`reverse_geocode`): 经纬度 → 中文地址 - - **高德**: `restapi.amap.com/v3/geocode/regeo`,需 WGS84→GCJ02 坐标转换 (服务端 Python 实现) - - 缓存策略: 坐标四舍五入到3位小数 (~100m) 作为缓存key -- 内置 LRU 缓存 (maxsize=10000),避免重复请求相同基站/坐标 -- **WGS84→GCJ02 服务端转换**: geocoding.py 内置 `wgs84_to_gcj02()` 函数 (与前端 JS 版一致) -- **高德数字签名**: 参数按key排序拼接 + AMAP_SECRET → MD5 → sig 参数 - -### API 认证与限流 -- **认证**: 设置 `API_KEY` 环境变量后,所有 `/api/` 请求需携带 `X-API-Key` 请求头 -- **多 API Key**: 支持 master key (环境变量) + 数据库管理的 API Key (SHA-256 hash) -- **权限级别**: `read` (只读) < `write` (读写) < `admin` (管理,含 key 管理) -- **权限控制**: 所有 POST/PUT/DELETE 端点需要 `write` 权限,`/api/keys` 需要 `admin` 权限 -- **Key 管理**: `POST /api/keys` 创建 key (返回明文一次), `GET /api/keys` 列表, `PUT /api/keys/{id}` 更新, `DELETE /api/keys/{id}` 停用 -- **限流**: 全局 60/min (default_limits),写操作 30/min (`@limiter.limit`) -- **真实 IP**: 从 `X-Forwarded-For` → `CF-Connecting-IP` → `request.client.host` 提取 -- **CORS**: `CORS_ORIGINS=*` 时自动禁用 `allow_credentials` - -### WebSocket 实时推送 -- **端点**: `ws://host/ws?api_key=xxx&topics=location,alarm` -- **Topics**: location, alarm, device_status, attendance, bluetooth -- **认证**: query param api_key (支持 master key + DB key) -- **最大连接**: 100 个 WebSocket 连接 -- **消息格式**: JSON `{"topic": "...", "data": {...}, "timestamp": "..."}` -- **广播点**: 位置存储、报警存储、设备上下线、考勤存储、蓝牙存储 -- **Ping/Pong**: 客户端发送 "ping" 保活,服务器回复 "pong" - -### 数据清理 -- **自动清理**: 后台定时任务,每 `DATA_CLEANUP_INTERVAL_HOURS`(默认24) 小时执行 -- **保留天数**: `DATA_RETENTION_DAYS`(默认90) 天,删除过期的 location/heartbeat/alarm/attendance/bluetooth 记录 -- **手动清理**: `POST /api/system/cleanup` (admin only) - -### 批量操作 API -- `POST /api/devices/batch` — 批量创建 (最多500),输入去重 + DB去重,结果按输入顺序 -- `PUT /api/devices/batch` — 批量更新,单次 WHERE IN 查询 + 单次 flush -- `POST /api/devices/batch-delete` — 批量删除 (最多100),通过 body 传 device_ids -- `POST /api/locations/batch-latest` — 批量获取多设备最新位置 (最多100) -- `GET /api/devices/all-latest-locations` — 获取所有在线设备最新位置 -- `POST /api/commands/batch` — 批量发送指令 (最多100),`model_validator` 互斥校验 -- 所有批量操作使用 WHERE IN 批量查询,避免 N+1 - -### API 分页 -- page_size 最大限制为 100 (schema 层校验) -- 前端设备选择器使用 page_size=100 (不能超过限制) - -### CRC 算法 -- CRC-ITU / CRC-16/X-25 -- Reflected polynomial: 0x8408 -- Initial value: 0xFFFF -- Final XOR: 0xFFFF - -### TCP 指令下发 -- 0x80 (Online Command): ASCII 指令 + 2字节语言字段 (`0x0001`=中文) -- 0x81 (Command Reply): 设备回复,格式 length(1) + server_flag(4) + content(n) + language(2) -- 0x82 (Message): **UTF-16 BE** 编码的文本消息 (非UTF-8) -- TTS: 通过 0x80 发送 `TTS,<文本>` 格式 -- 常用指令: `GPSON#` 开启GPS, `BTON#` 开启蓝牙, `BTSCAN,1#` 开启BLE扫描 -- 已验证可用指令: `BTON#`, `BTSCAN,1#`, `GPSON#`, `MODE,1/3#`, `PARAM#`, `CHECK#`, `VERSION#`, `TIMER#`, `WHERE#`, `STATUS#`, `RESET#` -- **架构**: `tcp_command_service.py` 作为抽象层解耦 routers↔tcp_server 的循环导入,通过 lazy import 访问 tcp_manager -- **重要**: TCP 层 (`send_command`/`send_message`) 只负责发送,不创建 CommandLog。CommandLog 由 API 层 (commands.py) 管理 -- **重要**: 服务器启动时自动将所有设备标记为 offline,等待设备重新登录 - -### 蓝牙信标管理 -- **BeaconConfig 表**: 注册蓝牙信标,配置 MAC/UUID/Major/Minor/楼层/区域/经纬度/地址 -- **自动关联**: 0xB2 打卡和 0xB3 定位时,根据 beacon_mac 查询 beacon_configs 表 -- **位置写入**: 将已注册信标的经纬度写入 BluetoothRecord 的 latitude/longitude -- **多信标定位**: 0xB3 多信标场景,取 RSSI 信号最强的已注册信标位置 -- **设备端配置**: 需通过 0x80 指令发送 `BTON#` 开启蓝牙、`BTSCAN,1#` 开启扫描 -- **已知有效指令**: `BTON#`(btON:1), `BTSCAN,1#`(btSCAN:1) — 设备确认设置成功 -- **当前状态**: ✅ 0xB2 蓝牙打卡已验证可用 (2026-03-18),设备成功检测信标并上报数据 -- **已验证信标**: MAC=`C3:00:00:34:43:5E`, UUID=FDA50693-A4E2-4FB1-AFCF-C6EB07647825, Major=10001, Minor=19641 -- **注意**: 信标需配置经纬度/地址,否则打卡记录中位置为空 -- **制造商**: 几米物联 (Jimi IoT / jimiiot.com.cn),P240/P241 智能电子工牌系列 - -### 0x94 General Info 子协议 -- 子协议 0x0A: IMEI(8字节) + IMSI(8字节) + ICCID(10字节) -- 子协议 0x09: GPS 卫星状态 -- 子协议 0x00: ICCID(10字节) -- 子协议 0x04: 设备配置上报 `ALM2=40;ALM4=E0;MODE=03;IMSI=...` - -### 前端字段映射 (admin.html) -- 设备信号: `d.gsm_signal` (非 `d.signal_strength`) -- 指令响应: `c.response_content` (非 `c.response`) -- 响应时间: `c.response_at || c.sent_at` (非 `c.updated_at`) -- 位置地址: `l.address` (高德逆地理编码结果) -- 卫星数: `l.gps_satellites` (非 `l.accuracy`) -- 记录时间: `l.recorded_at` (非 `l.timestamp`) -- 报警来源: `a.alarm_source` (非 `a.source`) -- 报警信号: `a.gsm_signal` (非 `a.signal_strength`) -- 报警类型: snake_case (如 `vibration`, `power_cut`, `voice_alarm`) - -## FRP 配置 - -```toml -# frpc.toml (容器端) -serverAddr = "152.69.205.186" -serverPort = 7000 -auth.method = "token" -auth.token = "PassWord0325" - -[[proxies]] -name = "badge-tcp" -type = "tcp" -localIP = "127.0.0.1" -localPort = 5000 -remotePort = 5001 -``` - -## 外部服务器 - -- IP: 152.69.205.186 -- OS: Ubuntu 20.04, Python 3.11, 24GB RAM -- FRP Server: frps v0.62.1 (1panel Docker, network_mode: host) -- Config: /opt/1panel/apps/frps/frps/data/frps.toml - -## 高德地图 API - -### Key -- **Web服务 Key**: `a9f4e04f5c8e739e5efb07175333f30b` -- **安全密钥**: `bfc4e002c49ab5f47df71e0aeaa086a5` -- **账号类型**: 个人认证开发者 (正在申请企业认证) - -### 已接入服务 -- **✅ 逆地理编码** (`restapi.amap.com/v3/geocode/regeo`): 经纬度 → 地址文本,服务端 WGS84→GCJ02 坐标转换 -- **✅ 智能硬件定位** (`apilocate.amap.com/position`): WiFi+基站 → 经纬度 (代码就绪,企业认证通过前返回 10012) -- **✅ 前端地图瓦片**: 高德瓦片 (GCJ-02, 标准Mercator),前端 WGS84→GCJ02 坐标转换 -- **数字签名**: `_amap_sign()` — 参数按key排序拼接 + AMAP_SECRET → MD5 → sig 参数 - -### 配额 (个人认证开发者) -- 基础LBS服务: 5,000 次/日 (逆地理编码等) -- 在线定位: 50,000 次/日 (企业认证后 1,000,000 次/日) - -## 已知限制 - -1. **IoT SIM 卡不支持 SMS** - 144 号段的物联网卡无法收发短信,需通过平台或 TCP 连接配置设备 -2. **Cloudflare Tunnel 仅代理 HTTP** - TCP 流量必须通过 FRP 转发 -3. **SQLite 单写** - 高并发场景需切换 PostgreSQL -4. **设备最多 100 台列表** - 受 page_size 限制,超过需翻页查询 -5. **基站/WiFi定位需企业认证** - 高德智能硬件定位 API 已接入但个人账号返回 10012,企业认证通过后自动生效 - -## 已修复的问题 (Bug Fix 记录) - -### 数据链路修复 -1. **IMEI 解析** - 修复 BCD 解码逻辑,确保正确提取 15 位 IMEI -2. **设备类型覆盖** - 登录时不再覆盖用户设置的 device_type -3. **设备重连** - 新连接建立前先关闭旧连接,断开时检查 writer 身份 -4. **告警类型提取** - 修正 alarm byte 位置,从 terminal_info+voltage+gsm_signal 之后读取 - -### 协议修复 -5. **0x80 指令包** - 添加缺失的 2 字节语言字段 (`0x0001`) -6. **0x82 消息编码** - 从 UTF-8 改为 UTF-16 BE (协议要求) -7. **0x94 通用信息** - 完善子协议 0x0A (IMEI+IMSI+ICCID) 和 0x09 (GPS卫星状态) 解析 - -### 前端修复 -8. **设备选择器为空** - `page_size=500` 超过 API 最大限制 100,返回 422 错误 -9. **位置页面崩溃** - API 返回 `data: null` 时未做空值检查 -10. **轨迹查询失败** - 缺少必填的 start_time/end_time 参数 -11. **字段名不匹配** - 修复 signal_strength→gsm_signal, response→response_content, source→alarm_source 等 - -### 定位功能修复 -12. **WiFi/LBS 无坐标** - 添加 wifi/wifi_4g 解析分支 (原代码缺失) -13. **地理编码集成** - LBS/WiFi 定位数据自动转换为经纬度坐标 -14. **邻近基站和WiFi数据** - 存储到 LocationRecord 的 neighbor_cells 和 wifi_data 字段 - -### 告警功能修复 -15. **告警响应协议** - 改为使用 0x26 ACK 响应 (原来错误使用地址回复) -16. **voltage_level 解析** - 从2字节改为1字节 (0x00-0x06等级) -17. **0xA5 LBS告警格式** - 移除错误的 datetime/lbs_length 前缀,直接从 MCC 开始 -18. **0xA9 WiFi告警** - 独立解析器 (非GPS格式),支持 cell_type/cell_count/WiFi AP 列表 -19. **MNC 2字节标志** - `_parse_mcc_mnc_from_content` 正确处理 MCC 高位 0x8000 标志 -20. **告警类型常量** - 改为 snake_case,新增 voice_alarm(0x16), fake_base_station(0x17), cover_open(0x18), internal_low_battery(0x19) -21. **LBS/WiFi 报警定位** - 为无GPS的报警添加前向地理编码 - -### 逆地理编码 -22. **逆地理编码集成** - 位置和报警记录自动获取中文地址 (高德逆地理编码) -23. **地址字段** - LocationRecord 新增 address 字段,前端位置表/报警表/地图弹窗显示地址 - -### 蓝牙与考勤功能 -24. **0xB0/0xB1 考勤解析** - 完整解析 GPS+LBS+WiFi 考勤包,含基站邻区、WiFi AP、前向+逆地理编码 -25. **0xB2 蓝牙打卡解析** - 解析 iBeacon 数据 (RSSI/MAC/UUID/Major/Minor/电池/打卡类型) -26. **0xB3 蓝牙定位解析** - 解析多信标数据,每信标 30 字节,支持电压(V)/百分比(%)电池单位 -27. **考勤类型检测** - 从 terminal_info 字节 bits[5:2] 判断 clock_in(0001)/clock_out(0010) -28. **信标管理系统** - BeaconConfig CRUD,注册信标物理位置,TCP 自动关联信标经纬度 - -### 指令发送修复 -29. **server_flag_str 未定义** - send_command 中记录日志时变量未定义导致 NameError -30. **TCP 层重复创建日志** - 简化 send_command/send_message 只负责发送,CommandLog 由 API 层管理 -31. **IMEI lstrip 问题** - `lstrip("0")` 可能删除多个前导零,改为 `raw_hex[1:]` -32. **设备启动状态** - 服务器启动时重置所有设备为 offline,避免显示在线但实际未连接 -33. **0x81 回复解析** - 去除末尾 2 字节语言字段,正确提取回复文本 - -### 协议全面审计修复 (2026-03-16) -34. **0x1F 时间同步格式** - 从6字节(YY MM DD HH MM SS)改为4字节Unix时间戳+2字节语言字段 -35. **地址回复完整格式** - 重写 `_send_address_reply`,实现完整格式: server_flag(4)+ADDRESS/ALARMSMS(7-8)+&&+UTF16BE地址+&&+电话(21)+## -36. **报警地址回复** - 报警包在发送0x26 ACK后,额外发送0x17地址回复(含ALARMSMS标记) -37. **0x82 留言格式** - 添加 server_flag(4字节) + language(2字节) 字段 -38. **0x22 GPS Cell ID** - 从2字节改为3字节解析 (2G基站CellID为3字节) -39. **登录包时区/语言** - 解析 content[10:12] 提取时区(bit15-04)和语言(bit00),存入Device -40. **0x94 不需回复** - 移除服务器响应,从 PROTOCOLS_REQUIRING_RESPONSE 中删除 -41. **WiFi缓存+LRU淘汰** - 新增 LRUCache(OrderedDict, maxsize=10000),替换所有dict缓存,修复WiFi缓存未写入 -42. **前端4G筛选** - 位置类型筛选添加 gps_4g/wifi_4g/lbs_4g 选项 -43. **DATA_REPORT_MODES** - 修正所有模式名称匹配协议文档 - -### 架构优化 (2026-03-17) -44. **配置集中化** - API密钥从 geocoding.py 硬编码移至 config.py (pydantic-settings),支持 .env 覆盖 -45. **数据库绝对路径** - DATABASE_URL 从相对路径改为基于 `__file__` 的绝对路径,避免 CWD 依赖 -46. **tcp_command_service 抽象层** - 新建 services/tcp_command_service.py,通过 lazy import 解耦 routers↔tcp_server 循环依赖 -47. **commands.py 去重** - send_command/send_message/send_tts 提取 `_send_to_device()` 通用函数 -48. **协议常量扩展** - constants.py 新增 DEFAULT_DEVICE_TYPE, SERVER_FLAG_BYTES, ATTENDANCE_TYPES, COURSE_BIT_*, MCC_MNC2_FLAG, VOLTAGE_LEVELS -49. **前端侧边面板** - 位置追踪/信标管理页面添加左侧设备/信标列表面板,自动选中最近活跃设备 -50. **前端面板解耦** - 提取 PANEL_IDS 配置 + _initPanelRender 通用函数,toggleSidePanel 仅在 locations 页调用 invalidateSize - -### 连接修复 (2026-03-19) -51. **PROTO_ADDRESS_REPLY_EN 未导入** - 0xA5 报警地址回复时 NameError,补充 import -52. **心跳自动恢复 online** - 心跳处理时自动将设备 status 设为 online + 重新注册 connections - -### 全面切换高德 API (2026-03-23) -53. **高德逆地理编码** - 接入 restapi.amap.com/v3/geocode/regeo,服务端 WGS84→GCJ02 坐标转换 -54. **高德智能硬件定位** - 接入 apilocate.amap.com/position (基站+WiFi定位,企业认证通过前返回 10012) -55. **高德数字签名** - 实现 `_amap_sign()` 函数 (参数排序 + AMAP_SECRET + MD5) -56. **服务端坐标转换** - geocoding.py 内置 `wgs84_to_gcj02()` Python 实现 -57. **高德地图瓦片** - 前端替换为高德瓦片 (GCJ-02, 标准Mercator),前端 WGS84→GCJ02 坐标转换 -58. **移除第三方地理编码** - 清理天地图/百度/Google/Unwired/Mylnikov,统一使用高德 - -### API 安全加固 & 批量管理 (2026-03-20) -59. **API Key 认证** - `dependencies.py` 实现 X-API-Key 头认证,`secrets.compare_digest` 防时序攻击 -60. **CORS + 限流** - `SlowAPIMiddleware` 全局限流 (60/min),写操作独立限速 (30/min) -61. **限流器真实 IP** - `extensions.py` 从 `X-Forwarded-For` / `CF-Connecting-IP` 提取真实客户端 IP -62. **全局异常处理** - 拦截未处理异常返回 500,不泄露堆栈;放行 HTTPException/ValidationError -63. **Schema 校验加强** - IMEI pattern、经纬度范围、Literal 枚举、command max_length、BeaconConfig MAC/UUID pattern -64. **Health 端点增强** - `/health` 检测数据库连通性 + TCP 连接设备数 -65. **批量设备创建** - `POST /api/devices/batch` (最多500台),WHERE IN 单次查询去重,输入列表内 IMEI 去重 -66. **批量设备更新** - `PUT /api/devices/batch`,单次查询 + 批量更新 + 单次 flush -67. **批量设备删除** - `POST /api/devices/batch-delete`,通过 body 传递避免 URL 长度限制 -68. **批量指令发送** - `POST /api/commands/batch` (最多100台),`model_validator` 互斥校验 device_ids/imeis -69. **Heartbeats 路由** - 新增 `GET /api/heartbeats` 心跳记录查询 + 按 ID 获取 -70. **按 ID 查询端点** - locations/{id}, attendance/{id}, bluetooth/{id} 放在路由末尾避免冲突 -71. **Beacons double-commit 修复** - 移除 router 层多余的 flush/refresh,依赖 service 层 - -### 全面改进 (2026-03-22) - -#### Protocol 修复 -72. **parser.py 0x22 CellID** - 从2字节改为3字节解析 -73. **parser.py 0xA5 LBS告警** - 移除错误的 datetime+lbs_length 前缀 -74. **parser.py 0xA9 WiFi告警** - 完全重写为 datetime+MCC/MNC+cell_type+cell_count+基站+TA+WiFi+alarm_code -75. **parser.py voltage** - 0xA3/0xA5/0xA9 voltage 从2字节改为1字节 -76. **parser.py 0xA4** - 新增多围栏告警解析器 (含 fence_id) -77. **parser.py 0xB2** - 完整 iBeacon 解析 (RSSI/MAC/UUID/Major/Minor/Battery) -78. **parser.py 0xB3** - beacon_count + 每信标30字节解析 -79. **parser.py 0xB0/0xB1** - 补充 gps_positioned/terminal_info/voltage/gsm_signal 等中间字段 -80. **parser.py 0x81** - 新增指令回复解析器 -81. **builder.py 0x1F** - 时间同步添加2字节 language 参数 -82. **builder.py 地址回复** - 重写 CN/EN 地址回复完整格式 -83. **builder.py 0x80/0x82** - cmd_len 正确包含 language(2) -84. **constants.py** - 从 PROTOCOLS_REQUIRING_RESPONSE 移除 0x28 - -#### 批量 API + 索引 -85. **batch-latest** - `POST /api/locations/batch-latest` 批量获取多设备最新位置 -86. **all-latest-locations** - `GET /api/devices/all-latest-locations` 所有在线设备位置 -87. **数据库索引** - 新增5个索引 (alarm_type, acknowledged, beacon_mac, location_type, attendance_type) - -#### 多 API Key + 权限控制 -88. **ApiKey 模型** - SHA-256 hash, permissions(read/write/admin), is_active -89. **多 Key 认证** - master key (env) + DB keys, last_used_at 追踪 -90. **权限控制** - require_write/require_admin 依赖,写端点需 write 权限 -91. **Key 管理 API** - `/api/keys` CRUD (admin only),创建时返回明文一次 -92. **DeviceUpdate** - 移除 status 字段 (设备状态由系统管理) - -#### WebSocket 实时推送 -93. **WebSocketManager** - 连接管理器,topic 订阅,最大100连接 -94. **WS 端点** - `/ws?api_key=xxx&topics=location,alarm` WebSocket 认证 -95. **TCP 广播集成** - 7个广播点 (location, alarm, device_status x2, attendance, bluetooth x2) - -#### 数据清理 + 补充 -96. **自动数据清理** - 后台定时任务,DATA_RETENTION_DAYS=90, DATA_CLEANUP_INTERVAL_HOURS=24 -97. **手动清理 API** - `POST /api/system/cleanup` (admin only) -98. **alarm_source 过滤** - alarms 列表新增 alarm_source 参数 -99. **beacon_mac 过滤** - bluetooth 列表新增 beacon_mac 参数 -100. **command_type 过滤** - commands 列表新增 command_type 参数 -101. **sort_order 参数** - alarms/bluetooth 列表支持 asc/desc 排序 - -#### 协议层统一 -102. **PacketBuilder 统一** - tcp_server.py 内嵌 PacketBuilder 改为委托 protocol/builder.py - -### 审计修复 (2026-03-23) -103. **require_admin 导入** - main.py 缺少 require_admin 导入,设置 API_KEY 后 cleanup 端点崩溃 -104. **0x28/0x2C CellID 3字节** - tcp_server.py 2G 基站 CellID 从2字节修正为3字节 (0x28 LBS, 0x2C WiFi, 邻区解析) -105. **parser.py CellID 3字节** - parse_lbs_station 默认 cell_id_size=2→3,新增3字节分支; _parse_lbs_address_req CellID 2→3字节 -106. **alarm_source 字段长度** - models.py String(10)→String(20), schemas.py max_length=10→20 ("single_fence"=12字符) -107. **AlarmRecord wifi_data/fence_data** - 0xA9 WiFi报警存储 wifi_data; 0xA4 多围栏报警提取并存储 fence_id -108. **CommandLog.sent_at** - 指令发送成功后设置 sent_at 时间戳 (原来始终为 NULL) -109. **geocoding IMEI 参数化** - 移除硬编码 IMEI,新增 GEOCODING_DEFAULT_IMEI 配置项,geocode_location 接受 imei 参数,TCP 层传递设备 IMEI -110. **parser.py 循环长度检查** - _parse_lbs_multi 和 _parse_wifi 站点循环检查从 pos+5 改为 pos+6 (LAC=2+CellID=3+RSSI=1=6) -111. **batch sent_at** - batch_send_command 批量指令路径也设置 cmd_log.sent_at (与单条路径一致) -112. **GPS 经度符号反转** - course_status bit 11 含义为 0=东经/1=西经 (非 1=东经),`is_east` 改为 `is_west`,修复成都定位到北美洲的问题 (tcp_server.py + parser.py) - -## 待完成功能 - -1. **心跳扩展模块解析** - 计步器、外部电压等模块未解析 -2. **前端 WebSocket 集成** - admin.html Dashboard 改用 WebSocket 替代 30s 轮询,报警页实时通知 -3. **协议层深度统一** - tcp_server.py 辅助方法 (_parse_gps, _parse_datetime 等) 逐步迁移到 protocol/parser.py - -## 调试技巧 - -```bash -# 查看实时日志 -tail -f /home/gpsystem/server.log | grep -aE "TCP|login|heartbeat|error|geocod|Amap" --line-buffered - -# 检查数据库 -python3 -c " -import sqlite3 -conn = sqlite3.connect('/home/gpsystem/badge_admin.db') -cur = conn.cursor() -cur.execute('SELECT id, imei, device_type, status, battery_level FROM devices') -for row in cur.fetchall(): print(row) -cur.execute('SELECT id, location_type, latitude, longitude, address, recorded_at FROM location_records ORDER BY id DESC LIMIT 5') -for row in cur.fetchall(): print(row) -" - -# 测试 API -curl -s http://localhost:8088/api/devices | python3 -m json.tool -curl -s http://localhost:8088/api/locations/latest/1 | python3 -m json.tool -curl -s http://localhost:8088/health - -# 发送指令 -curl -s -X POST http://localhost:8088/api/commands/send \ - -H "Content-Type: application/json" \ - -d '{"device_id":1,"command_type":"online_cmd","command_content":"GPSON#"}' | python3 -m json.tool - -# 开启蓝牙扫描 -curl -s -X POST http://localhost:8088/api/commands/send \ - -H "Content-Type: application/json" \ - -d '{"device_id":1,"command_type":"online_cmd","command_content":"BTON#"}' | python3 -m json.tool - -# 管理信标 -curl -s http://localhost:8088/api/beacons | python3 -m json.tool -curl -s -X POST http://localhost:8088/api/beacons \ - -H "Content-Type: application/json" \ - -d '{"beacon_mac":"AA:BB:CC:DD:EE:FF","name":"前台","floor":"1F","latitude":30.27,"longitude":120.15}' | python3 -m json.tool - -# 检查 FRP 连接 -ps aux | grep frpc -# FRP Dashboard: http://152.69.205.186:7500 (admin/PassWord0325) - -# 检查端口 -python3 -c " -with open('/proc/net/tcp') as f: - for line in f: - parts = line.split() - if len(parts)>=2 and ':' in parts[1]: - port = int(parts[1].split(':')[1], 16) - if port in (8088, 5000): print(f'Port {port}: listening') -" - -# 测试地理编码 -python3 -c " -import asyncio -from app.geocoding import geocode_location, reverse_geocode -async def test(): - lat, lon = await geocode_location(mcc=460, mnc=0, lac=32775, cell_id=205098688) - print(f'lat={lat}, lon={lon}') - if lat: print(await reverse_geocode(lat, lon)) -asyncio.run(test()) -" -``` diff --git a/app/routers/attendance.py b/app/routers/attendance.py index a99864d..82a68dc 100644 --- a/app/routers/attendance.py +++ b/app/routers/attendance.py @@ -192,6 +192,36 @@ async def device_attendance( ) +@router.post( + "/batch-delete", + response_model=APIResponse[dict], + summary="批量删除考勤记录 / Batch delete attendance records", +) +async def batch_delete_attendance( + body: dict, + db: AsyncSession = Depends(get_db), +): + """ + 批量删除考勤记录,通过 body 传递 attendance_ids 列表,最多 500 条。 + Batch delete attendance records by IDs (max 500). + """ + attendance_ids = body.get("attendance_ids", []) + if not attendance_ids: + raise HTTPException(status_code=400, detail="attendance_ids is required") + if len(attendance_ids) > 500: + raise HTTPException(status_code=400, detail="Max 500 records per request") + + result = await db.execute( + select(AttendanceRecord).where(AttendanceRecord.id.in_(attendance_ids)) + ) + records = list(result.scalars().all()) + for r in records: + await db.delete(r) + await db.flush() + + return APIResponse(data={"deleted": len(records)}) + + # NOTE: /{attendance_id} must be after /stats and /device/{device_id} to avoid route conflicts @router.get( "/{attendance_id}", @@ -207,3 +237,21 @@ async def get_attendance(attendance_id: int, db: AsyncSession = Depends(get_db)) if record is None: raise HTTPException(status_code=404, detail=f"Attendance {attendance_id} not found") return APIResponse(data=AttendanceRecordResponse.model_validate(record)) + + +@router.delete( + "/{attendance_id}", + response_model=APIResponse[dict], + summary="删除单条考勤记录 / Delete attendance record", +) +async def delete_attendance(attendance_id: int, db: AsyncSession = Depends(get_db)): + """按ID删除单条考勤记录 / Delete a single attendance record by ID.""" + result = await db.execute( + select(AttendanceRecord).where(AttendanceRecord.id == attendance_id) + ) + record = result.scalar_one_or_none() + if record is None: + raise HTTPException(status_code=404, detail=f"Attendance {attendance_id} not found") + await db.delete(record) + await db.flush() + return APIResponse(data={"deleted": 1}) diff --git a/app/services/fence_checker.py b/app/services/fence_checker.py index 9109c4b..39e41a8 100644 --- a/app/services/fence_checker.py +++ b/app/services/fence_checker.py @@ -4,6 +4,7 @@ Checks whether a device's reported coordinates fall inside its bound fences. Creates automatic attendance records (clock_in/clock_out) on state transitions. """ +import json import logging import math from datetime import datetime, timedelta, timezone diff --git a/app/static/admin.html b/app/static/admin.html index cc0f2e5..e4b05a9 100644 --- a/app/static/admin.html +++ b/app/static/admin.html @@ -431,8 +431,8 @@
- + - -
-
- + +
+ +
-
-
-
-
- - -
- - + +
+
+ +
+ + +
+
+
- -
+
+
+
+
+ @@ -864,24 +866,21 @@
- -