260 lines
7.6 KiB
Go
260 lines
7.6 KiB
Go
|
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
|
|||
|
}
|
|||
|
}
|