@ -2,11 +2,12 @@ package emqx
import (
"context"
"encoding/json"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson "
"github.com/gogf/gf/v2/uti l/g conv "
"github.com/gogf/gf/v2/os/gtime "
"server/interna l/consts "
"server/internal/dao"
"strconv "
"server/internal/model/do "
"sync"
"time"
@ -88,12 +89,29 @@ func (e *emqxClient) Subscribe(topic string, handler func(topic string, payload
return err
}
type Device Data struct {
NetbarAccount str ing ` json:"netbarAccount" `
D eviceId string ` json:"d eviceId" `
D eviceName string ` json:"d eviceName" `
IP string ` json:"ip" `
MACAddress string ` json:"macAddress" `
type MemberLevel Data struct {
LevelID int ` json:"level_id" ` // 会员等级ID( 如0表示临时卡)
L evelName string ` json:"l evel_name" ` // 等级名称
L evel int ` json:"l evel" ` // 等级顺序,值越大等级越高
OlType int8 ` json:"ol_type" ` // 在线类型, 例如: 1=线上
Status int8 ` json:"status" ` // 状态: 1=启用, 2=禁用
}
type ClientData struct {
AreaName string ` json:"area_name" `
ClientName string ` json:"client_name" `
Status int8 ` json:"status" `
}
type ClientSession struct {
AreaName string ` json:"area_name" `
ClientName string ` json:"client_name" `
CardId string ` json:"card_id" `
XyUserId string ` json:"xy_user_id" `
Level int ` json:"level" `
LevelName string ` json:"level_name" `
StartTime string ` json:"start_time" `
EndTime string ` json:"end_time" `
UsedTime int ` json:"used_time" `
}
// init 注册emqx客户端
@ -116,181 +134,210 @@ func init() {
mqtt . Register ( "emqx" , client )
glog . Infof ( ctx , "EMQX 客户端注册完成, broker=%s, clientID=%s" , broker , clientId )
// 订阅设备上线消息
// 订阅设备上线消息
go func ( ) {
ctx := context . Background ( )
err := client. Subscribe ( "/+/up" , func ( topic string , payload [ ] byte ) {
glog . Infof ( ctx , "收到 MQTT 消息, topic=%s" , topic )
var data DeviceData
if err := g json. Unmarshal ( payload , & data ) ; err != nil {
glog . Errorf ( ctx , "[/up] 解析设备信息失败: %v" , err )
// 监听数据上报通道
client . Subscribe ( "/+/up" , func ( topic string , payload [ ] byte ) {
var base struct {
Cmd int ` json:"cmd" `
StoreId int ` json:"storeId" `
Data json . RawMessage ` json:"data" `
}
if err := json . Unmarshal ( payload , & base ) ; err != nil {
glog . Errorf ( ctx , "解析数据失败, topic=%s, 错误: %v" , topic , err )
return
}
deviceId := data . DeviceId
netbarAccount := data . NetbarAccount
now := time . Now ( ) . Unix ( )
// Redis 统一 key
onlineDevicesKey := "system_device:online_devices"
lastOnlineKey := fmt . Sprintf ( "system_device:last_online:%s" , deviceId )
deviceInfoKey := fmt . Sprintf ( "system_device:info:%s" , deviceId )
netbarDeviceCountKey := fmt . Sprintf ( "system_device:netbar_device_count:%s" , netbarAccount )
// 判断设备是否在线
exists , err := g . Redis ( ) . HExists ( ctx , onlineDevicesKey , deviceId )
if err != nil {
glog . Errorf ( ctx , "查询设备在线状态失败 %s: %v" , deviceId , err )
return
}
// 获取上次上线时间,判断是否断线重连
lastOnlineStr , err := g . Redis ( ) . Get ( ctx , lastOnlineKey )
shouldSend := false
needIncr := false
if exists != 1 {
// 设备不在线,首次上线
shouldSend = true
needIncr = true
} else if err != nil || lastOnlineStr . IsEmpty ( ) {
// 无上线时间记录,断线重连
shouldSend = true
needIncr = true
} else {
lastOnline , err := strconv . ParseInt ( lastOnlineStr . String ( ) , 10 , 64 )
if err != nil || now - lastOnline > 10 {
// 超过10秒断线重连
shouldSend = true
needIncr = true
} else {
// 在线且未断线,不加计数,不发送配置
shouldSend = false
needIncr = false
switch base . Cmd {
case consts . CmdMemberLevels :
// 当前门店会员等级列表数据
levels := make ( [ ] MemberLevelData , 0 )
if err := json . Unmarshal ( base . Data , & levels ) ; err != nil {
glog . Errorf ( ctx , "解析会员等级数据失败, topic=%s, 错误: %v" , topic , err )
return
}
}
// 更新上线时间并设置20秒过期
if _ , err := g . Redis ( ) . Set ( ctx , lastOnlineKey , now ) ; err != nil {
glog . Errorf ( ctx , "更新上线时间失败 %s: %v" , lastOnlineKey , err )
}
if _ , err := g . Redis ( ) . Expire ( ctx , lastOnlineKey , 20 ) ; err != nil {
glog . Errorf ( ctx , "设置上线时间过期失败 %s: %v" , lastOnlineKey , err )
}
// 需要时增加在线设备计数
if needIncr {
if _ , err := g . Redis ( ) . Incr ( ctx , netbarDeviceCountKey ) ; err != nil {
glog . Errorf ( ctx , "增加 Netbar 在线设备数失败 %s: %v" , netbarDeviceCountKey , err )
saveOrUpdateLevels ( ctx , base . StoreId , levels )
glog . Infof ( ctx , "成功保存会员等级数据, store_id=%d" , base . StoreId )
case consts . CmdClientList :
// 当前门店客户机列表数据
clients := make ( [ ] ClientData , 0 )
if err := json . Unmarshal ( base . Data , & clients ) ; err != nil {
glog . Errorf ( ctx , "解析客户机数据失败, topic=%s, 错误: %v" , topic , err )
return
}
}
// 更新在线设备集合(时间戳)
if _ , err := g . Redis ( ) . HSet ( ctx , onlineDevicesKey , map [ string ] interface { } {
deviceId : now ,
} ) ; err != nil {
glog . Errorf ( ctx , "更新在线设备集合失败 %s: %v" , onlineDevicesKey , err )
return
}
// 更新设备基础信息, 并设置1小时过期
if _ , err := g . Redis ( ) . HSet ( ctx , deviceInfoKey , map [ string ] interface { } {
"netbarAccount" : netbarAccount ,
"deviceName" : data . DeviceName ,
"ip" : data . IP ,
"macAddress" : data . MACAddress ,
} ) ; err != nil {
glog . Errorf ( ctx , "存储设备信息失败 %s: %v" , deviceInfoKey , err )
}
if _ , err := g . Redis ( ) . Expire ( ctx , deviceInfoKey , 3600 ) ; err != nil {
glog . Errorf ( ctx , "设置设备信息过期失败 %s: %v" , deviceInfoKey , err )
}
// 首次或断线重连发送配置
if shouldSend {
one , err := dao . Stores . Ctx ( ctx ) . InnerJoin ( dao . StoreDesktopSettings . Table ( ) , "sds" , "sds.store_id = stores.id" ) .
Where ( "stores.netbar_account = ?" , netbarAccount ) .
Fields ( "stores.netbar_account netbarAccount, sds.top_component_visible topComponentVisible, sds.right_component_visible rightComponentVisible" ) . One ( )
if err != nil {
glog . Errorf ( ctx , "获取门店信息失败 %s: %v" , deviceInfoKey , err )
}
if err = client . Publish ( fmt . Sprintf ( "/%s/down" , netbarAccount ) , gconv . Bytes ( one . Json ( ) ) ) ; err != nil {
glog . Errorf ( ctx , "发布消息失败 %s: %v" , deviceInfoKey , err )
saveOrUpdateClients ( ctx , base . StoreId , clients )
case consts . CmdClientUp , consts . CmdClientDown :
session := ClientSession { }
if err := json . Unmarshal ( base . Data , & session ) ; err != nil {
glog . Errorf ( ctx , "解析客户机数据失败, topic=%s, 错误: %v" , topic , err )
return
}
saveOrUpdateClientSession ( ctx , base . StoreId , session )
}
} )
if err != nil {
glog . Errorf ( ctx , "订阅 /+/up 失败: %v" , err )
}
} ( )
}
// 监控离线设备
go func ( ) {
ctx := context . Background ( )
ticker := time . NewTicker ( 10 * time . Secon d)
defer ticker . Stop ( )
func saveOrUpdateClientSession ( ctx context . Context , storeId int , session ClientSession ) {
// 查询客户机信息
client , err := dao . StoreClients . Ctx ( ctx ) .
Where ( "store_id" , storeI d) .
Where ( "client_name" , session . ClientName ) .
One ( )
if err != nil || client . IsEmpty ( ) {
glog . Errorf ( ctx , "未找到客户机信息: store_id=%d client_name=%s" , storeId , session . ClientName )
return
}
clientId := client [ "id" ] . Int64 ( )
areaId := client [ "area_id" ] . Int64 ( )
for {
select {
case <- ctx . Done ( ) :
glog . Info ( ctx , "停止离线设备监控" )
return
case <- ticker . C :
onlineDevicesKey := "system_device:online_devices"
devicesVar , err := g . Redis ( ) . HGetAll ( ctx , onlineDevicesKey )
if err != nil {
glog . Errorf ( ctx , "获取在线设备失败: %v" , err )
continue
}
// 上机逻辑(没有 end_time)
if session . EndTime == "" {
// 插入上机记录
_ , err := dao . StoreClientSessions . Ctx ( ctx ) . Data ( do . StoreClientSessions {
StoreId : int64 ( storeId ) ,
ClientId : clientId ,
AreaId : areaId ,
XyUserId : session . XyUserId ,
LevelId : session . Level ,
LevelName : session . LevelName ,
StartTime : gtime . NewFromStr ( session . StartTime ) ,
AreaName : session . AreaName ,
} ) . Insert ( )
if err != nil {
glog . Errorf ( ctx , "插入上机记录失败: store_id=%d client_id=%d xy_user_id=%s, 错误: %v" , storeId , clientId , session . XyUserId , err )
return
}
// 更新客户机状态为 上机中( 3)
_ , err = dao . StoreClients . Ctx ( ctx ) .
Where ( "id" , clientId ) .
Data ( g . Map { "status" : 3 } ) .
Update ( )
if err != nil {
glog . Errorf ( ctx , "更新客户机状态为上机中失败: client_id=%d, 错误: %v" , clientId , err )
}
return
}
devices := devicesVar . MapStrStr ( )
if len ( devices ) == 0 {
continue
}
// 下机逻辑( end_time 存在)
_ , err = dao . StoreClientSessions . Ctx ( ctx ) .
Where ( "store_id" , storeId ) .
Where ( "client_id" , clientId ) .
Where ( "xy_user_id" , session . XyUserId ) .
Where ( "start_time" , session . StartTime ) .
Data ( g . Map {
"end_time" : session . EndTime ,
"used_time" : session . UsedTime ,
} ) . Update ( )
if err != nil {
glog . Errorf ( ctx , "更新下机记录失败: store_id=%d client_id=%d xy_user_id=%s, 错误: %v" , storeId , clientId , session . XyUserId , err )
return
}
// 更新客户机状态为 空闲( 1)
_ , err = dao . StoreClients . Ctx ( ctx ) .
Where ( "id" , clientId ) .
Data ( g . Map { "status" : 1 } ) .
Update ( )
if err != nil {
glog . Errorf ( ctx , "更新客户机状态为空闲失败: client_id=%d, 错误: %v" , clientId , err )
}
}
now := time . Now ( ) . Unix ( )
for deviceId , timestampStr := range device s {
timestamp , err := strconv . ParseInt ( timestampStr , 10 , 64 )
if err != nil {
glog . Errorf ( ctx , "无效时间戳 for 设备 %s: %v" , deviceId , err )
continue
}
func saveOrUpdateClients ( ctx context . Context , storeId int , clients [ ] ClientData ) {
for _ , client := range client s {
var areaId int64
// Step 1: 查询区域 ID
value , err := dao . StoreAreas . Ctx ( ctx ) . Where ( do . StoreAreas { StoreId : storeId , AreaName : client . AreaName } ) . Fields ( dao . StoreAreas . Columns ( ) . Id ) . Value ( )
if err != nil {
glog . Errorf ( ctx , "查询区域失败 store_id=%d area_name=%s err=%v" , storeId , client . AreaName , err )
continue
}
if now - timestamp > 10 {
// 超过10秒未更新, 认定离线
deviceInfoKey := fmt . Sprintf ( "system_device:info:%s" , deviceId )
dataVar , err := g . Redis ( ) . HGetAll ( ctx , deviceInfoKey )
if err != nil {
glog . Errorf ( ctx , "获取设备数据失败 %s: %v" , deviceInfoKey , err )
continue
}
if value . IsEmpty ( ) {
// 不存在则插入数据
id , err := dao . StoreAreas . Ctx ( ctx ) . Data ( do . StoreAreas {
StoreId : storeId ,
AreaName : client . AreaName ,
} ) . InsertAndGetId ( )
if err != nil {
glog . Errorf ( ctx , "插入区域失败 store_id=%d area_name=%s err=%v" , storeId , client . AreaName , err )
continue
}
areaId = id
} else {
areaId = value . Int64 ( )
}
data := dataVar . MapStrStr ( )
netbarAccount , exists := data [ "netbarAcc ount" ]
if ! exists {
glog . Errorf ( ctx , "设备 %s 缺少 netbarAccount" , deviceId )
continue
}
// Step 2: 查询客户机是否存在
count , err := dao . StoreClients . Ctx ( ctx ) . Where ( do . StoreClients { StoreId : storeId , ClientName : client . ClientName , AreaId : areaId } ) . C ount( )
if err != nil {
glog . Errorf ( ctx , "查询客户机失败 store_id=%d client_name=%s err=%v" , storeId , client . ClientName , err )
continue
}
// 新增: system_device命名空间, 按账号统计在线设备数 -1
netbarDeviceCountKey := fmt . Sprintf ( "system_device:netbar_device_count:%s " , netbarAccount )
if _ , err := g . Redis ( ) . Decr ( ctx , netbarDeviceCountKey ) ; err != nil {
glog . Errorf ( ctx , "减少 Netbar 在线设备数失败 %s: %v" , netbarDeviceCountKey , err )
}
data := g . Map {
"store_id ": storeId ,
"client_name" : client . ClientName ,
"area_id" : areaId ,
"status" : client . Status ,
}
// 从在线设备集合中删除
if _ , err := g . Redis ( ) . HDel ( ctx , onlineDevicesKey , deviceId ) ; err != nil {
glog . Errorf ( ctx , "移除设备 %s 从在线集合失败: %v" , deviceId , err )
} else {
glog . Infof ( ctx , "设备 %s 已标记为离线 " , devic eId)
}
}
}
// Step 3: 更新 or 插入
if count > 0 {
_ , err = dao . StoreClients . Ctx ( ctx ) .
Data ( data ) .
Where ( "store_id ", stor eId) .
Where ( "client_name" , client . ClientName ) .
Update ( )
if err != nil {
glog . Errorf ( ctx , "更新客户机失败 store_id=%d client_name=%s err=%v" , storeId , client . ClientName , err )
}
} else {
_ , err = dao . StoreClients . Ctx ( ctx ) .
Data ( data ) .
Insert ( )
if err != nil {
glog . Errorf ( ctx , "插入客户机失败 store_id=%d client_name=%s err=%v" , storeId , client . ClientName , err )
}
}
} ( )
}
}
func saveOrUpdateLevels ( ctx context . Context , storeId int , levels [ ] MemberLevelData ) {
for _ , level := range levels {
exists , err := dao . StoreMemberLevels . Ctx ( ctx ) .
Where ( "store_id" , storeId ) .
Where ( "level_id" , level . LevelID ) .
Count ( )
if err != nil {
glog . Infof ( ctx , "检查会员等级是否存在失败, store_id=%d level_id=%d" , storeId , level . LevelID )
}
data := g . Map {
"store_id" : storeId ,
"level_id" : level . LevelID ,
"level_name" : level . LevelName ,
"level" : level . Level ,
"ol_type" : level . OlType ,
"status" : level . Status ,
}
if exists > 0 {
_ , err = dao . StoreMemberLevels . Ctx ( ctx ) .
Where ( "store_id" , storeId ) .
Where ( "level_id" , level . LevelID ) .
Data ( data ) .
Update ( )
} else {
_ , err = dao . StoreMemberLevels . Ctx ( ctx ) .
Data ( data ) .
Insert ( )
}
if err != nil {
glog . Errorf ( ctx , "保存会员等级数据失败, store_id=%d level_id=%d" , storeId , level . LevelID )
}
}
}
type DownData struct {
CMD int ` json:"cmd" `
StoreId int ` json:"storeId" `
Data interface { } ` json:"data" `
}