This commit is contained in:
2025-07-07 20:11:59 +08:00
parent ab0fdbc447
commit 06e3aa2eb3
2009 changed files with 193082 additions and 0 deletions

259
api/saft_hat/hat_data.go Normal file
View File

@ -0,0 +1,259 @@
package saft_hat
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"github.com/gorilla/websocket"
"io/ioutil"
"log"
"net/http"
"strconv"
"time"
)
func StartWs() {
// 设置 WebSocket 路由
http.HandleFunc("/ws", HandleConnections)
// 开启一个新的协程,处理消息广播
go HandleMessages()
// 启动 WebSocket 服务器
log.Println("http server started on :8222")
err := http.ListenAndServe(":8222", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
// WebSocket 部分
var clients = make(map[*websocket.Conn]bool) // 连接的客户端
var broadcast = make(chan []byte) // 广播通道
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// 处理连接
func HandleConnections(w http.ResponseWriter, r *http.Request) {
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}
defer ws.Close()
// 注册新客户端
clients[ws] = true
// 无限循环,保持连接活跃,但不处理任何消息
for {
if _, _, err := ws.ReadMessage(); err != nil {
log.Printf("error: %v", err)
delete(clients, ws)
break
}
}
}
// 处理消息
func HandleMessages() {
for {
// 从广播通道中获取消息
msg := <-broadcast
// 发送消息到所有连接的客户端
for client := range clients {
err := client.WriteMessage(websocket.TextMessage, msg)
if err != nil {
log.Printf("websocket write error: %v", err)
client.Close()
delete(clients, client)
}
}
}
}
// WS 推送的数据
type BroadcastLocation struct {
DevNum string `json:"devNum"`
Time string `json:"time"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// Webhook 定位数据上传【远程发送给我们平台的数据】
type WebhookRequest struct {
Temperature float64 `json:"temperature"` // 设备采集温度数据
Humidity float64 `json:"humidity"` // 设备采集湿度数据
Posture int `json:"posture"` // 姿态1正常 -1脱帽 -2倒地
BatteryTemp float64 `json:"batteryTemp"` // 电池温度
FixedBy string `json:"fixedBy"` // 定位方式:GPS/BD、WIFI、BT
UtcDateTime int64 `json:"utcDateTime"` // 设备内部上传时间戳
IMEI string `json:"IMEI"` // 设备IMEI
BatteryLevel float64 `json:"batteryLevel"` // 电池电量
Charging int `json:"charging"` // 是否正在充电 1充电 0没充电
BluetoothMac string `json:"bluetoothMac"` // 蓝牙Mac地址
Type string `json:"type"` // 设备上传类型和时间
Latitude float64 `json:"latitude"` // WGS-84是国际标准GPS坐标Google Earth使用、或者GPS模块纬度坐标
Longitude float64 `json:"longitude"` // WGS-84是国际标准GPS坐标Google Earth使用、或者GPS模块经度坐标
Altitude int `json:"altitude"` // 海拔高度
BT string `json:"bt"` // 蓝牙定位相关:{个数}:{地址,信号}
LBS []string `json:"LBS"` // 多基站定位信息
MAC []string `json:"MAC"` // WiFi - MAC地址信号
}
// 定位数据上传请求
type DataReq struct {
g.Meta `path:"/device/data" method:"post" tags:"安全帽相关" summary:"接收安全帽数据(不需要前端调用)"`
}
type DataRes struct {
}
func (h Hat) Data(ctx context.Context, req *DataReq) (res *DataRes, err error) {
res = new(DataRes)
r := g.RequestFromCtx(ctx)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("读取请求头失败: %v", err)
return res, err
}
defer r.Body.Close() // 延迟关闭请求体
// 从请求头中获取时间戳和签名
timestamp := r.GetHeader("timestamp")
signature := r.GetHeader("signature")
// 验证签名的有效性
if !VerifySignature(string(body)+timestamp, signature, secret) {
glog.Errorf(ctx, "签名验证失败")
return res, errors.New("验证签名失败")
}
// 解析请求体中的JSON数据到预定义的结构体
var webhookData WebhookRequest
if err := gjson.DecodeTo(body, &webhookData); err != nil {
return res, err
}
// 拿到数据之后,先取出 IMEI 设备号,根据 IMEI 查询,不存在则先插入数据到 device 表
device := Device{
DevNum: webhookData.IMEI,
Temperature: webhookData.Temperature,
Humidity: webhookData.Humidity,
Posture: webhookData.Posture,
FixedBy: webhookData.FixedBy,
BatteryLevel: webhookData.BatteryLevel,
}
// 查询设备是否存在,存在则更新安全帽的状态信息
count, err := g.Model(&device).Where("dev_num", device.DevNum).Count()
if err != nil {
glog.Errorf(ctx, "查询设备是否存在出错:%v", err)
}
if count > 0 {
// 判断电池是否处于低电量状态
var isLowBattery int
// 电量小于 0.2 则设置为低电量
if device.BatteryLevel < 0.2 {
isLowBattery = 1
} else {
isLowBattery = 0
}
// 更新设备的数据
_, err = g.Model("device").Data(g.Map{
"temperature": device.Temperature,
"humidity": device.Humidity,
"posture": device.Posture,
"battery_temp": device.BatteryTemp,
"fixed_by": device.FixedBy,
"battery_level": device.BatteryLevel * 100,
"is_low_battery": isLowBattery,
"update_time": time.Now(),
}).Where("dev_num", device.DevNum).Update()
if err != nil {
glog.Errorf(ctx, "更新设备数据出错:%v", err)
}
}
// 将接收到的数据转换为 Location 结构体
location := Location{
DevNum: webhookData.IMEI, // 设备编号为 IMEI
Time: time.Now().Format("2006-01-02 15:04:05"), // 当前时间
Latitude: webhookData.Latitude, // 将纬度转换为指针类型
Longitude: webhookData.Longitude, // 将经度转换为指针类型
}
// 构建插入数据的参数
data := g.Map{
"dev_num": location.DevNum,
"time": location.Time,
"latitude": location.Latitude,
"longitude": location.Longitude,
}
// 检查经纬度是否同时为 0如果是则不执行插入操作也不需要调用 Redis
if location.Latitude != 0 || location.Longitude != 0 {
// 执行插入操作
_, err = g.Model("location").Data(data).Insert()
// 获取 Redis 客户端
redis := g.Redis("helmetRedis")
// 构造 Redis 的 key 和 value
key := "safety_helmet:" + location.DevNum
value := strconv.FormatFloat(location.Latitude, 'f', -1, 64) + "," + strconv.FormatFloat(location.Longitude, 'f', -1, 64)
// 插入数据之后,写入 Redis 的发布订阅
_, err = redis.Publish(ctx, key, value)
if err != nil {
glog.Info(ctx, "发布订阅出错")
return res, nil
}
// 插入数据之后,写入 Redis 的键值对
_, err = redis.Set(ctx, key, value)
if err != nil {
glog.Info(ctx, "设置到Redis出错")
return res, nil
}
if err != nil {
// 处理可能的错误
fmt.Println("Insert error:", err)
}
} else {
fmt.Println("Latitude and Longitude are both zero, insertion skipped.")
}
// 返回响应对象
return &DataRes{}, nil
}
// 发送数据到 WS 客户端
func sendToWs(location Location, err error) {
// 创建 WS 需要发送的数据对象
broadcastLocation := BroadcastLocation{
DevNum: location.DevNum,
Time: location.Time,
Latitude: location.Latitude,
Longitude: location.Longitude,
}
// 转换为 JSON 字符串
locationJSON, err := json.Marshal(broadcastLocation)
if err != nil {
log.Printf("location json marshal error: %v", err)
} else {
// 发送转换后的JSON字符串到广播通道
broadcast <- locationJSON
}
}