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

98
api/saft_hat/device.go Normal file
View File

@ -0,0 +1,98 @@
package saft_hat
import (
"context"
"github.com/gogf/gf/v2/frame/g"
"time"
)
// 新增安全帽设备
type CreateDeviceReq struct {
g.Meta `path:"/device/create" method:"post" tags:"设备管理" summary:"创建新设备"`
DevNum string `json:"devNum" dc:"设备编号"`
DevName string `json:"devName" dc:"设备名称"`
ProjectID int64 `json:"projectId" dc:"项目ID"`
}
type CreateDeviceRes struct {
}
func (h Hat) CreateDevice(ctx context.Context, req *CreateDeviceReq) (res *CreateDeviceRes, err error) {
res = new(CreateDeviceRes)
device := g.Map{
"dev_num": req.DevNum,
"dev_name": req.DevName,
"project_id": req.ProjectID,
"create_time": time.Now(),
}
_, err = g.Model("device").Ctx(ctx).Insert(device)
if err != nil {
return res, err
}
return res, nil
}
// 获取安全帽设备列表
type DeviceListReq struct {
g.Meta `path:"/device/list" method:"get" tags:"设备管理" summary:"获取设备信息"`
ProjectId int64 `json:"projectId" dc:"项目ID"`
Page int64 `json:"page" dc:"请求的页码" v:"required"`
PageSize int64 `json:"pageSize" dc:"每页显示的条目数" v:"required"`
}
type DeviceListRes struct {
Devices []Device `json:"devices"` // 设备信息
}
func (h Hat) GetDevice(ctx context.Context, req *DeviceListReq) (res *DeviceListRes, err error) {
res = new(DeviceListRes)
offset := (req.Page - 1) * req.PageSize
var devices []Device
err = g.Model("device").Ctx(ctx).Where("project_id = ?", req.ProjectId).Offset(int(offset)).Limit(int(req.PageSize)).Scan(&devices)
if err != nil {
return nil, err
}
res.Devices = devices
return res, nil
}
// 删除设备信息
type DeleteDeviceReq struct {
g.Meta `path:"/device/delete" method:"delete" tags:"设备管理" summary:"删除设备"`
DevNum string `json:"devNum" dc:"设备编号"` // 设备编号
}
type DeleteDeviceRes struct {
}
func (h Hat) DeleteDevice(ctx context.Context, req *DeleteDeviceReq) (res *DeleteDeviceRes, err error) {
res = new(DeleteDeviceRes)
// 执行删除操作
_, err = g.Model("device").Ctx(ctx).Where("dev_num", req.DevNum).Delete()
if err != nil {
return res, err
}
return res, nil
}
// 修改安全帽的名称
type UpdateDeviceNameReq struct {
g.Meta `path:"/device/update/name" method:"post" tags:"设备管理" summary:"更新安全帽设备名称"`
DevNum string `json:"devNum" dc:"设备编号"`
DevName string `json:"devName" dc:"设备名称"`
}
type UpdateDeviceNameRes struct {
}
func (h Hat) UpdateDeviceName(ctx context.Context, req *UpdateDeviceNameReq) (res *UpdateDeviceNameRes, err error) {
res = new(UpdateDeviceNameRes)
_, err = g.Model("device").Data(g.Map{
"dev_name": req.DevName,
}).Where("dev_num", req.DevNum).Update()
if err != nil {
return res, err
}
return res, nil
}

39
api/saft_hat/entity.go Normal file
View File

@ -0,0 +1,39 @@
package saft_hat
import "time"
type Device struct {
DevNum string `json:"devNum" dc:"设备编号"`
DevName string `json:"devName" dc:"设备名称"`
Status int `json:"status" dc:"状态"`
CreateTime time.Time `json:"createTime" dc:"添加时间"`
UpdateTime time.Time `json:"updateTime" dc:"修改时间"`
ProjectID int64 `json:"projectId" dc:"项目id"`
Temperature float64 `json:"temperature" dc:"设备采集温度"`
Humidity float64 `json:"humidity" dc:"设备采集湿度"`
Posture int `json:"posture" dc:"姿势1表示正常-1表示脱帽-2表示倒地"`
BatteryTemp float64 `json:"batteryTemp" dc:"电池温度"`
FixedBy string `json:"fixedBy" dc:"定位方"`
BatteryLevel float64 `json:"batteryLevel" dc:"电量"`
IsLowBattery int `json:"isLowBattery" dc:"是否低电量 1为低电量0为正常"`
}
type Location struct {
DevNum string `json:"devNum" dc:"设备编号"`
Time string `json:"time" dc:"时间"`
Alarm *int `json:"alarm" dc:"告警信息"`
Status *int `json:"status" dc:"状态"`
Latitude float64 `json:"latitude" dc:"纬度"`
Longitude float64 `json:"longitude" dc:"经度"`
Elevation *int16 `json:"elevation" dc:"海拔"`
Speed *int16 `json:"speed" dc:"速度"`
Direction *int16 `json:"direction" dc:"方向"`
Mileage *int64 `json:"mileage" dc:"里程数"`
AccEnable *bool `json:"accEnable" dc:"ACC开关"`
LocateEnable *bool `json:"locateEnable" dc:"定位开关"`
LatitudeType *int8 `json:"latitudeType" dc:"纬度类型"` // 0:北纬, 1:南纬
LongitudeType *int8 `json:"longitudeType" dc:"经度类型"` // 0:东经, 1:西经
SpeedingWarn *bool `json:"speedingWarn" dc:"超速报警"`
PowerVoltageWarn *bool `json:"powerVoltageWarn" dc:"电压告警"`
PowerFailure *bool `json:"powerFailure" dc:"电源掉电"`
}

8
api/saft_hat/hat.go Normal file
View File

@ -0,0 +1,8 @@
package saft_hat
// 定义签名的 APPID 和密钥常量
const secret = "5366474e589c4dcfadeef223a466ca0b"
const appid = "0c9ab925c6684ab4a33350e15ee35062"
type Hat struct {
}

View File

@ -0,0 +1,54 @@
package saft_hat
import (
"context"
"errors"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"io/ioutil"
"log"
)
// 危险源数据上传结构体
type Danger struct {
BT_MAC string `json:"BT_MAC"` // 危险源MAC
IMEI string `json:"IMEI"` // 设备IMEI编号
}
// 危险源数据上传请求
type DangerReq struct {
g.Meta `path:"/device/danger" method:"post" tags:"安全帽相关" summary:"接收危险源数据(不需要前端调用)"`
}
type DangerRes struct {
}
func (h Hat) Danger(ctx context.Context, req *DangerReq) (res *DangerRes, err error) {
res = new(DangerRes)
r := g.RequestFromCtx(ctx)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
return nil, err
}
defer r.Body.Close()
timestamp := r.GetHeader("timestamp")
signature := r.GetHeader("signature")
if !VerifySignature(string(body)+timestamp, signature, secret) {
glog.Errorf(ctx, "Signature verification failed")
return nil, errors.New("signature verification failed")
}
var danger Danger
if err := gjson.DecodeTo(body, &danger); err != nil {
return nil, err
}
fmt.Println("危险源数据", danger)
return res, nil
}

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
}
}

View File

@ -0,0 +1,78 @@
package saft_hat
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
"github.com/gogf/gf/v2/frame/g"
)
// 更新设备定位数据
type LocationReq struct {
g.Meta `path:"/device/location" method:"post" tags:"安全帽相关" summary:"刷新设备定位数据"`
Data string `json:"data" dc:"设备号"` // 设备数据
}
type LocationRes struct {
Code int `json:"code"` // 响应代码
Data bool `json:"data"` // 是否成功
Msg string `json:"msg"` // 响应消息
}
// 处理设备开关机时间的更新
func (h Hat) Location(ctx context.Context, req *LocationReq) (res *LocationRes, err error) {
res = new(LocationRes)
data := req.Data
timestamp := time.Now().Unix()
// 准备生成签名的参数
params := map[string]string{
"appid": appid,
"data": data,
"secret": secret,
"timestamp": strconv.FormatInt(timestamp, 10),
}
// 生成签名
sign := GenerateSignature(params)
// 构造请求体
payload := map[string]interface{}{
"appid": appid,
"data": data,
"secret": secret,
"timestamp": timestamp,
"sign": sign,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return res, err
}
// 发送POST请求
resp, err := http.Post("https://www.loctp.com/api/crm/v1/refresh/location", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return res, err
}
defer resp.Body.Close()
// 检查响应
respBody, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(respBody))
if err != nil {
return res, err
}
err = json.Unmarshal(respBody, res)
if err != nil {
return res, err
}
return res, nil
}

81
api/saft_hat/hat_rate.go Normal file
View File

@ -0,0 +1,81 @@
package saft_hat
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"io/ioutil"
"net/http"
"strconv"
"time"
)
// 定义设备上传速度频率请求【我们请求平台】
type UploadRateReq struct {
g.Meta `path:"/device/rate" method:"post" tags:"安全帽相关" summary:"更新设备数据发送频率"`
Data string `json:"data" dc:"设备号"` // 设备数据
RateTime int `json:"rateTime" dc:"上传频率单位是S"` // 上传频率
}
type UploadRateRes struct {
Code int `json:"code"` // 响应代码
Data string `json:"data"` // 设备数据
Msg string `json:"msg"` // 响应消息
}
func (h Hat) UpdateRate(ctx context.Context, req *UploadRateReq) (res *UploadRateRes, err error) {
res = new(UploadRateRes)
// 获取请求参数
rateTime := req.RateTime
data := req.Data
timestamp := time.Now().Unix()
// 准备生成签名的参数
params := map[string]string{
"appid": appid,
"data": data,
"rate_time": strconv.Itoa(rateTime),
"secret": secret,
"timestamp": strconv.FormatInt(timestamp, 10),
}
// 生成签名
sign := GenerateSignature(params)
// 构造请求体
payload := map[string]interface{}{
"appid": appid,
"data": data,
"rate_time": rateTime,
"timestamp": timestamp,
"secret": secret,
"sign": sign,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return res, err
}
// 发送POST请求
resp, err := http.Post("https://www.loctp.com/api/crm/v1/uploadRate", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return res, err
}
defer resp.Body.Close()
// 检查响应
respBody, err := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(respBody, &res)
if err != nil {
return res, err
}
// 把 respBody 转化为 UploadRateRes
err = json.Unmarshal(respBody, &res)
if err != nil {
// 处理错误,例如打印或返回错误
fmt.Println("Error unmarshalling res:", err)
}
return res, err
}

62
api/saft_hat/hat_sos.go Normal file
View File

@ -0,0 +1,62 @@
package saft_hat
import (
"context"
"errors"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"io/ioutil"
"log"
)
// SOS报警数据结构体
type SOS struct {
Flag int `json:"flag"` // 两次数据上传标志0:第二次 1:第一次
UTCDateTime int64 `json:"utcDateTime"` // 设备内部上传时间戳
Latitude float64 `json:"latitude"` // WGS-84 纬度坐标
IMEI string `json:"IMEI"` // 设备IMEI编号
Type string `json:"type"` // 设备上传类型和时间
Longitude float64 `json:"longitude"` // WGS-84 经度坐标
}
// SOS报警数据上传请求【其它平台发给我们】
type SOSReq struct {
g.Meta `path:"/device/alarm" method:"post" tags:"安全帽相关" summary:"接收SOS报警数据(不需要前端调用)"`
}
type SOSRes struct {
}
func (h Hat) SOS(ctx context.Context, req *SOSReq) (res *SOSRes, err error) {
r := g.RequestFromCtx(ctx) // 从上下文中获取请求对象
body, err := ioutil.ReadAll(r.Body) // 读取请求体
if err != nil {
log.Printf("读取请求头失败: %v", err)
return nil, err
}
defer r.Body.Close() // 延迟关闭请求体
// 从请求头中获取时间戳和签名
timestamp := r.GetHeader("timestamp")
signature := r.GetHeader("signature")
// 验证签名的有效性
if !VerifySignature(string(body)+timestamp, signature, secret) {
glog.Errorf(ctx, "签名验证失败")
return nil, errors.New("验证签名失败")
}
// 解析请求体中的 JSON 数据到预定义的结构体
var sos SOS
if err := gjson.DecodeTo(body, &sos); err != nil {
return nil, err
}
// 输出 SOS 的报警信息
fmt.Println("报警信息:", sos)
// 返回响应对象
return &SOSRes{}, nil
}

133
api/saft_hat/hat_status.go Normal file
View File

@ -0,0 +1,133 @@
package saft_hat
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
)
// 获取设备状态的请求结构
type DeviceStatusReq struct {
g.Meta `path:"/device/status" method:"post" tags:"安全帽相关" summary:"获取安全帽状态"`
Data []string `json:"data" dc:"设备号"`
}
// 获取设备状态
func (h Hat) DeviceStatus(ctx context.Context, req *DeviceStatusReq) (res *DeviceStatusRes, err error) {
return StatusCheck(ctx, req.Data)
}
type DeviceStatusRes struct {
Code int `json:"code"` // 响应代码
Data []DeviceStatusInfo `json:"data"` // 设备数据
Msg string `json:"msg"` // 响应消息
}
// 定义数组中每个对象的结构
type DeviceStatusInfo struct {
IMEI string `json:"imei"`
Status int `json:"status"`
}
// 检查设备的状态情况
func StatusCheck(ctx context.Context, devNums []string) (res *DeviceStatusRes, err error) {
res = new(DeviceStatusRes)
data := strings.Join(devNums, ",")
timestamp := time.Now().Unix()
// 准备生成签名的参数
params := map[string]string{
"appid": appid,
"data": data,
"secret": secret,
"timestamp": strconv.FormatInt(timestamp, 10),
}
// 生成签名
sign := GenerateSignature(params)
// 构造请求体
payload := map[string]interface{}{
"appid": appid,
"data": data,
"secret": secret,
"sign": sign,
"timestamp": timestamp,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return res, err
}
// 发送POST请求
resp, err := http.Post("https://www.loctp.com/api/crm/v1/getStatus", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return res, err
}
defer resp.Body.Close()
// 检查响应
respBody, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(respBody))
if err != nil {
return res, err
}
err = json.Unmarshal(respBody, res)
if err != nil {
return res, err
}
return res, nil
}
// 心跳检测
func HeartCheck() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop() // 确保ticker被适当释放
for {
select {
case <-ticker.C:
var devices []Device
err := g.Model("device").Fields("dev_num").Scan(&devices)
if err != nil {
fmt.Println("获取设备列表出错:", err)
continue // 如果获取失败,则跳过本次循环
}
if len(devices) == 0 {
fmt.Println("没有找到任何设备")
continue // 如果没有设备,则跳过本次循环
}
// 构建devNums切片
var devNums []string
for _, device := range devices {
devNums = append(devNums, device.DevNum)
}
res, err := StatusCheck(context.Background(), devNums)
if err != nil {
fmt.Println("检查设备状态出错:", err)
continue // 如果检查失败,则跳过本次循环
}
for _, deviceStatus := range res.Data {
// 更新 device 表中的 status 字段
_, err := g.Model("device").Data("status", deviceStatus.Status).Where("dev_num", deviceStatus.IMEI).Update()
if err != nil {
fmt.Printf("更新设备 %s 状态出错: %v\n", deviceStatus.IMEI, err)
continue // 如果更新失败,则跳过本次设备的更新
}
}
}
}
}

View File

@ -0,0 +1,78 @@
package saft_hat
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"io/ioutil"
"net/http"
"strconv"
"time"
)
// TextToAudioReq 定义发送自定义语音的请求结构体
type TextToAudioReq struct {
g.Meta `path:"/device/text" method:"post" tags:"安全帽相关" summary:"发送自定义语言数据"`
Data string `json:"data" dc:"设备号"`
Text string `json:"text" dc:"发送语音的文字内容"`
Time int `json:"time" dc:"播放次数 最少一次 最多三次"`
}
type TextToAudioRes struct {
Code int `json:"code"` // 响应代码
Data string `json:"data"` // 设备数据
Msg string `json:"msg"` // 响应消息
}
// 发送自定义语音
func (h Hat) SendTextToAudio(ctx context.Context, req *TextToAudioReq) (res *TextToAudioRes, err error) {
res = new(TextToAudioRes)
timestamp := time.Now().Unix()
// 准备生成签名的参数
params := map[string]string{
"appid": appid,
"data": req.Data,
"secret": secret,
"text": req.Text,
"time": strconv.Itoa(req.Time),
"timestamp": strconv.FormatInt(timestamp, 10),
}
// 生成签名
sign := GenerateSignature(params)
// 构造请求体
reqBody := map[string]interface{}{
"data": req.Data,
"appid": appid,
"sign": sign,
"secret": secret,
"time": req.Time,
"text": req.Text,
"timestamp": strconv.FormatInt(timestamp, 10),
}
payloadBytes, err := json.Marshal(reqBody)
if err != nil {
return res, err
}
// 发送POST请求
resp, err := http.Post("https://www.loctp.com/api/crm/v1/textToAudio", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return res, err
}
defer resp.Body.Close()
// 读取响应
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return res, err
}
fmt.Println("Response:", string(respBody))
return res, nil
}

114
api/saft_hat/hat_time.go Normal file
View File

@ -0,0 +1,114 @@
package saft_hat
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"io/ioutil"
"net/http"
"strconv"
"strings"
"time"
)
// 更新设备开关机时间
type UpdateTimeReq struct {
g.Meta `path:"/device/uploadTime" method:"post" tags:"安全帽相关" summary:"更新设备开关机时间"`
Data string `json:"data" dc:"设备号"` // 设备数据
OnTime string `json:"onTime" dc:"设备开机时间,例如 09:00"` // 设备开机时间
OffTime string `json:"OffTime" dc:"设备关机时间,例如 23:00"` // 设备关机时间
}
type UpdateTimeRes struct {
Code int `json:"code"` // 响应代码
Data string `json:"data"` // 设备数据
Msg string `json:"msg"` // 响应消息
}
// 处理设备开关机时间的更新
func (h Hat) UpdateTime(ctx context.Context, req *UpdateTimeReq) (res *UpdateTimeRes, err error) {
res = new(UpdateTimeRes)
data := req.Data
timestamp := time.Now().Unix()
// 准备生成签名的参数
params := map[string]string{
"appid": appid,
"data": data,
"off_time": req.OffTime,
"on_time": req.OnTime,
"secret": secret,
"timestamp": strconv.FormatInt(timestamp, 10),
}
// 生成签名
sign := GenerateSignature(params)
// 构造请求体
payload := map[string]interface{}{
"appid": appid,
"data": data,
"off_time": req.OffTime,
"on_time": req.OnTime,
"secret": secret,
"timestamp": timestamp,
"sign": sign,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return res, err
}
// 发送POST请求
resp, err := http.Post("https://www.loctp.com/api/crm/v1/uploadTime", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return res, err
}
defer resp.Body.Close()
// 检查响应
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return res, err
}
// 解析包含多个设备号的字符串,设备号通过英文逗号分割
devNums := strings.Split(req.Data, ",")
// 对每个设备号执行插入或更新操作
for _, devNum := range devNums {
// 首先去除可能的空格
devNum = strings.TrimSpace(devNum)
type StutusType struct {
Status int
}
statusType := new(StutusType)
// 先查看该设备的状态
g.Model("device").Fields("status").Where("dev_num = ?", devNum).Scan(&statusType)
// 如果设备在线status=1才允许更新它的开关机时间信息
if statusType.Status == 1 {
// 插入或更新电池开关机时间
_, err := g.Model("device").Data(g.Map{
"dev_num": devNum,
"battery_on": req.OnTime,
"battery_off": req.OffTime,
}).Where("dev_num", devNum).Save()
// 如果发生错误,立即返回
if err != nil {
fmt.Println("Error updating/inserting for device", devNum, ":", err)
return nil, err
}
} else {
fmt.Println("Device", devNum, "is not online; skipping update.")
}
}
err = json.Unmarshal(respBody, res)
if err != nil {
return res, err
}
return res, nil
}

53
api/saft_hat/hat_tip.go Normal file
View File

@ -0,0 +1,53 @@
package saft_hat
import (
"context"
"errors"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/glog"
"io/ioutil"
"log"
)
// 脱帽提示数据上传结构体
type HatTip struct {
UTCDateTime int64 `json:"utcDateTime"` // 设备内部上传时间戳
IMEI string `json:"IMEI"` // 设备IMEI编号
}
type HatTipReq struct {
g.Meta `path:"/device/tip" method:"post" tags:"安全帽相关" summary:"脱帽提醒(不需要前端调用)"`
}
type HatTipRes struct {
}
func (h Hat) HatTip(ctx context.Context, req *HatTipReq) (res *HatTipRes, err error) {
res = new(HatTipRes)
r := g.RequestFromCtx(ctx)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Printf("Failed to read request body: %v", err)
return nil, err
}
defer r.Body.Close()
timestamp := r.GetHeader("timestamp")
signature := r.GetHeader("signature")
if !VerifySignature(string(body)+timestamp, signature, secret) {
glog.Errorf(ctx, "Signature verification failed")
return nil, errors.New("signature verification failed")
}
var hatTip HatTip
if err := gjson.DecodeTo(body, &hatTip); err != nil {
return nil, err
}
fmt.Println("脱帽提示数据:", hatTip)
return res, nil
}

81
api/saft_hat/hat_voice.go Normal file
View File

@ -0,0 +1,81 @@
package saft_hat
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"io/ioutil"
"net/http"
"strconv"
"time"
)
// 定义发送内置语音的请求结构
type SendVoiceReq struct {
g.Meta `path:"/device/voice" method:"post" tags:"安全帽相关" summary:"发送内置语音"`
Data string `json:"data" dc:"设备号"`
VoiceType int `json:"voice_type" dc:"内置语音种类具体参照温度1、2、3、4、5....."`
}
type SendVoiceRes struct {
Code int `json:"code"` // 响应代码
Data string `json:"data"` // 设备数据
Msg string `json:"msg"` // 响应消息
}
// 处理设备开关机时间的更新
func (h Hat) SendVoice(ctx context.Context, req *SendVoiceReq) (res *SendVoiceRes, err error) {
res = new(SendVoiceRes)
data := req.Data
timestamp := time.Now().Unix()
// 准备生成签名的参数
params := map[string]string{
"appid": appid,
"data": data,
"secret": secret,
"timestamp": strconv.FormatInt(timestamp, 10),
"voice_type": strconv.Itoa(req.VoiceType),
}
// 生成签名
sign := GenerateSignature(params)
// 构造请求体
payload := map[string]interface{}{
"appid": appid,
"data": data,
"secret": secret,
"sign": sign,
"timestamp": timestamp,
"voice_type": strconv.Itoa(req.VoiceType),
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return res, err
}
// 发送POST请求
resp, err := http.Post("https://www.loctp.com/api/crm/v1/sendVoice", "application/json", bytes.NewBuffer(payloadBytes))
if err != nil {
return res, err
}
defer resp.Body.Close()
// 检查响应
respBody, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(respBody))
if err != nil {
return res, err
}
err = json.Unmarshal(respBody, res)
if err != nil {
return res, err
}
return res, nil
}

14
api/saft_hat/router.go Normal file
View File

@ -0,0 +1,14 @@
package saft_hat
import (
"github.com/gogf/gf/v2/net/ghttp"
)
func InitHatAPI(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareCORS)
group.Group("/manage", func(group *ghttp.RouterGroup) {
group.Group("/api/v1", func(group *ghttp.RouterGroup) {
group.Bind(new(Hat))
})
})
}

35
api/saft_hat/util.go Normal file
View File

@ -0,0 +1,35 @@
package saft_hat
import (
"crypto/md5"
"encoding/hex"
"sort"
"strings"
)
// 使用参数名的ASCII升序排序后将参数的值进行拼接并加密生成签名
func GenerateSignature(params map[string]string) string {
// 获取所有的键并按照ASCII码排序
var keys []string
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// 按排序后的键,拼接它们的值
var values []string
for _, k := range keys {
values = append(values, params[k])
}
signStr := strings.Join(values, "&")
// 将拼接后的字符串转换为小写
signStr = strings.ToLower(signStr)
// 使用MD5加密
hash := md5.New()
hash.Write([]byte(signStr))
md5String := hex.EncodeToString(hash.Sum(nil))
return md5String
}

21
api/saft_hat/verify.go Normal file
View File

@ -0,0 +1,21 @@
package saft_hat
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"log"
)
// 函数用于验证请求的签名
func VerifySignature(message, messageSignature, secret string) bool {
mac := hmac.New(sha1.New, []byte(secret)) // 初始化HMAC-SHA1
mac.Write([]byte(message)) // 写入消息体以计算摘要
expectedMAC := mac.Sum(nil) // 计算消息的摘要
signature, err := hex.DecodeString(messageSignature) // 将签名从十六进制字符串解码
if err != nil {
log.Printf("解码失败: %v", err)
return false
}
return hmac.Equal(signature, expectedMAC) // 比较计算得到的摘要和传入的签名
}