实现 pc 服务端数据上传处理

This commit is contained in:
2025-07-05 10:59:23 +08:00
parent 0c6de3db61
commit 357ada8455
7 changed files with 252 additions and 173 deletions

15
internal/consts/emqx.go Normal file
View File

@ -0,0 +1,15 @@
package consts
// UP 上行消息 cmd 常量
const (
CmdMemberLevels = 1 // 会员等级数据
CmdClientList = 2 // 客户机列表数据
CmdClientUp = 104 // 上机记录
CmdClientDown = 106 // 下机记录
)
// DOWN 下行消息 cmd 常量
const (
CmdDesktopSetting = 10001 // 桌面组件显示设置
CmdUserFee = 10002 // 用户网费下发
)

View File

@ -33,6 +33,7 @@ type StoreClientSessionsColumns struct {
CreatedAt string // 创建时间 CreatedAt string // 创建时间
UpdatedAt string // 更新时间 UpdatedAt string // 更新时间
DeletedAt string // 软删除时间戳 DeletedAt string // 软删除时间戳
AreaName string //
} }
// storeClientSessionsColumns holds the columns for the table store_client_sessions. // storeClientSessionsColumns holds the columns for the table store_client_sessions.
@ -50,6 +51,7 @@ var storeClientSessionsColumns = StoreClientSessionsColumns{
CreatedAt: "created_at", CreatedAt: "created_at",
UpdatedAt: "updated_at", UpdatedAt: "updated_at",
DeletedAt: "deleted_at", DeletedAt: "deleted_at",
AreaName: "area_name",
} }
// NewStoreClientSessionsDao creates and returns a new DAO object for table data access. // NewStoreClientSessionsDao creates and returns a new DAO object for table data access.

View File

@ -5,11 +5,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/errors/gerror"
"server/internal/consts"
"server/internal/dao" "server/internal/dao"
"server/internal/model" "server/internal/model"
"server/internal/model/do" "server/internal/model/do"
"server/internal/service" "server/internal/service"
"server/utility/mqtt" "server/utility/mqtt"
"server/utility/mqtt/emqx"
) )
type sStoreDesktopSetting struct { type sStoreDesktopSetting struct {
@ -63,15 +65,26 @@ func (s *sStoreDesktopSetting) Save(ctx context.Context, in model.SaveDesktopSet
if err != nil { if err != nil {
return nil, err return nil, err
} }
marshal, err := json.Marshal(in)
if err != nil {
return nil, err
}
client, b := mqtt.GetClient("emqx") client, b := mqtt.GetClient("emqx")
if !b { if !b {
return nil, gerror.New("获取MQTT客户端失败") return nil, gerror.New("获取MQTT客户端失败")
} }
err = client.Publish(fmt.Sprintf("/desktop/%d", in.StoreId), marshal) downData := emqx.DownData{
CMD: consts.CmdDesktopSetting,
StoreId: int(in.StoreId),
Data: struct {
RightComponentVisible int `json:"rightComponentVisible"`
TopComponentVisible int `json:"topComponentVisible"`
}{
RightComponentVisible: in.RightComponentVisible,
TopComponentVisible: in.TopComponentVisible,
},
}
marshal, err := json.Marshal(downData)
if err != nil {
return nil, err
}
err = client.Publish(fmt.Sprintf("/%d/down", in.StoreId), marshal)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -25,4 +25,5 @@ type StoreClientSessions struct {
CreatedAt *gtime.Time // 创建时间 CreatedAt *gtime.Time // 创建时间
UpdatedAt *gtime.Time // 更新时间 UpdatedAt *gtime.Time // 更新时间
DeletedAt *gtime.Time // 软删除时间戳 DeletedAt *gtime.Time // 软删除时间戳
AreaName interface{} //
} }

View File

@ -23,4 +23,5 @@ type StoreClientSessions struct {
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间 CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间 UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间
DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"软删除时间戳"` // 软删除时间戳 DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"软删除时间戳"` // 软删除时间戳
AreaName string `json:"areaName" orm:"area_name" description:""` //
} }

View File

@ -4,7 +4,7 @@ import (
_ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/drivers/mysql/v2"
_ "github.com/gogf/gf/contrib/nosql/redis/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2"
_ "server/utility/gamelife" _ "server/utility/gamelife"
//_ "server/utility/mqtt/emqx" _ "server/utility/mqtt/emqx"
_ "server/utility/myCasbin" _ "server/utility/myCasbin"
_ "server/utility/oss/aliyun" _ "server/utility/oss/aliyun"
_ "server/utility/rsa" _ "server/utility/rsa"

View File

@ -2,11 +2,12 @@ package emqx
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv" "server/internal/consts"
"server/internal/dao" "server/internal/dao"
"strconv" "server/internal/model/do"
"sync" "sync"
"time" "time"
@ -88,12 +89,29 @@ func (e *emqxClient) Subscribe(topic string, handler func(topic string, payload
return err return err
} }
type DeviceData struct { type MemberLevelData struct {
NetbarAccount string `json:"netbarAccount"` LevelID int `json:"level_id"` // 会员等级ID如0表示临时卡
DeviceId string `json:"deviceId"` LevelName string `json:"level_name"` // 等级名称
DeviceName string `json:"deviceName"` Level int `json:"level"` // 等级顺序,值越大等级越高
IP string `json:"ip"` OlType int8 `json:"ol_type"` // 在线类型例如1=线上
MACAddress string `json:"macAddress"` 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客户端 // init 注册emqx客户端
@ -116,181 +134,210 @@ func init() {
mqtt.Register("emqx", client) mqtt.Register("emqx", client)
glog.Infof(ctx, "EMQX 客户端注册完成broker=%s, clientID=%s", broker, clientId) glog.Infof(ctx, "EMQX 客户端注册完成broker=%s, clientID=%s", broker, clientId)
// 订阅设备上线消息
// 订阅设备上线消息
go func() { go func() {
ctx := context.Background() // 监听数据上报通道
err := client.Subscribe("/+/up", func(topic string, payload []byte) { client.Subscribe("/+/up", func(topic string, payload []byte) {
glog.Infof(ctx, "收到 MQTT 消息topic=%s", topic) var base struct {
Cmd int `json:"cmd"`
var data DeviceData StoreId int `json:"storeId"`
if err := gjson.Unmarshal(payload, &data); err != nil { Data json.RawMessage `json:"data"`
glog.Errorf(ctx, "[/up] 解析设备信息失败: %v", err) }
if err := json.Unmarshal(payload, &base); err != nil {
glog.Errorf(ctx, "解析数据失败topic=%s错误%v", topic, err)
return return
} }
switch base.Cmd {
deviceId := data.DeviceId case consts.CmdMemberLevels:
netbarAccount := data.NetbarAccount // 当前门店会员等级列表数据
now := time.Now().Unix() levels := make([]MemberLevelData, 0)
if err := json.Unmarshal(base.Data, &levels); err != nil {
// Redis 统一 key glog.Errorf(ctx, "解析会员等级数据失败topic=%s错误%v", topic, err)
onlineDevicesKey := "system_device:online_devices" return
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
} }
} saveOrUpdateLevels(ctx, base.StoreId, levels)
glog.Infof(ctx, "成功保存会员等级数据store_id=%d", base.StoreId)
// 更新上线时间并设置20秒过期 case consts.CmdClientList:
if _, err := g.Redis().Set(ctx, lastOnlineKey, now); err != nil { // 当前门店客户机列表数据
glog.Errorf(ctx, "更新上线时间失败 %s: %v", lastOnlineKey, err) clients := make([]ClientData, 0)
} if err := json.Unmarshal(base.Data, &clients); err != nil {
if _, err := g.Redis().Expire(ctx, lastOnlineKey, 20); err != nil { glog.Errorf(ctx, "解析客户机数据失败topic=%s错误%v", topic, err)
glog.Errorf(ctx, "设置上线时间过期失败 %s: %v", lastOnlineKey, err) return
}
// 需要时增加在线设备计数
if needIncr {
if _, err := g.Redis().Incr(ctx, netbarDeviceCountKey); err != nil {
glog.Errorf(ctx, "增加 Netbar 在线设备数失败 %s: %v", netbarDeviceCountKey, err)
} }
} saveOrUpdateClients(ctx, base.StoreId, clients)
case consts.CmdClientUp, consts.CmdClientDown:
// 更新在线设备集合(时间戳) session := ClientSession{}
if _, err := g.Redis().HSet(ctx, onlineDevicesKey, map[string]interface{}{ if err := json.Unmarshal(base.Data, &session); err != nil {
deviceId: now, glog.Errorf(ctx, "解析客户机数据失败topic=%s错误%v", topic, err)
}); err != nil { return
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)
} }
saveOrUpdateClientSession(ctx, base.StoreId, session)
} }
}) })
if err != nil {
glog.Errorf(ctx, "订阅 /+/up 失败: %v", err)
}
}() }()
}
// 监控离线设备 func saveOrUpdateClientSession(ctx context.Context, storeId int, session ClientSession) {
go func() { // 查询客户机信息
ctx := context.Background() client, err := dao.StoreClients.Ctx(ctx).
ticker := time.NewTicker(10 * time.Second) Where("store_id", storeId).
defer ticker.Stop() 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 { // 上机逻辑(没有 end_time
select { if session.EndTime == "" {
case <-ctx.Done(): // 插入上机记录
glog.Info(ctx, "停止离线设备监控") _, err := dao.StoreClientSessions.Ctx(ctx).Data(do.StoreClientSessions{
return StoreId: int64(storeId),
case <-ticker.C: ClientId: clientId,
onlineDevicesKey := "system_device:online_devices" AreaId: areaId,
devicesVar, err := g.Redis().HGetAll(ctx, onlineDevicesKey) XyUserId: session.XyUserId,
if err != nil { LevelId: session.Level,
glog.Errorf(ctx, "获取在线设备失败: %v", err) LevelName: session.LevelName,
continue 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() // 下机逻辑end_time 存在)
if len(devices) == 0 { _, err = dao.StoreClientSessions.Ctx(ctx).
continue 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() func saveOrUpdateClients(ctx context.Context, storeId int, clients []ClientData) {
for deviceId, timestampStr := range devices { for _, client := range clients {
timestamp, err := strconv.ParseInt(timestampStr, 10, 64) var areaId int64
if err != nil { // Step 1: 查询区域 ID
glog.Errorf(ctx, "无效时间戳 for 设备 %s: %v", deviceId, err) value, err := dao.StoreAreas.Ctx(ctx).Where(do.StoreAreas{StoreId: storeId, AreaName: client.AreaName}).Fields(dao.StoreAreas.Columns().Id).Value()
continue if err != nil {
} glog.Errorf(ctx, "查询区域失败 store_id=%d area_name=%s err=%v", storeId, client.AreaName, err)
continue
}
if now-timestamp > 10 { if value.IsEmpty() {
// 超过10秒未更新认定离线 // 不存在则插入数据
deviceInfoKey := fmt.Sprintf("system_device:info:%s", deviceId) id, err := dao.StoreAreas.Ctx(ctx).Data(do.StoreAreas{
dataVar, err := g.Redis().HGetAll(ctx, deviceInfoKey) StoreId: storeId,
if err != nil { AreaName: client.AreaName,
glog.Errorf(ctx, "获取设备数据失败 %s: %v", deviceInfoKey, err) }).InsertAndGetId()
continue 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() // Step 2: 查询客户机是否存在
netbarAccount, exists := data["netbarAccount"] count, err := dao.StoreClients.Ctx(ctx).Where(do.StoreClients{StoreId: storeId, ClientName: client.ClientName, AreaId: areaId}).Count()
if !exists { if err != nil {
glog.Errorf(ctx, "设备 %s 缺少 netbarAccount", deviceId) glog.Errorf(ctx, "查询客户机失败 store_id=%d client_name=%s err=%v", storeId, client.ClientName, err)
continue continue
} }
// 新增system_device命名空间按账号统计在线设备数 -1 data := g.Map{
netbarDeviceCountKey := fmt.Sprintf("system_device:netbar_device_count:%s", netbarAccount) "store_id": storeId,
if _, err := g.Redis().Decr(ctx, netbarDeviceCountKey); err != nil { "client_name": client.ClientName,
glog.Errorf(ctx, "减少 Netbar 在线设备数失败 %s: %v", netbarDeviceCountKey, err) "area_id": areaId,
} "status": client.Status,
}
// 从在线设备集合中删除 // Step 3: 更新 or 插入
if _, err := g.Redis().HDel(ctx, onlineDevicesKey, deviceId); err != nil { if count > 0 {
glog.Errorf(ctx, "移除设备 %s 从在线集合失败: %v", deviceId, err) _, err = dao.StoreClients.Ctx(ctx).
} else { Data(data).
glog.Infof(ctx, "设备 %s 已标记为离线", deviceId) Where("store_id", storeId).
} 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"`
} }