完善牧安云哨-中间件
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetDeviceID 返回本机原始唯一ID(Linux /etc/machine-id 或 hostname+MAC)
|
||||
func GetDeviceID() string {
|
||||
// 尝试读取 Linux /etc/machine-id
|
||||
if data, err := os.ReadFile("/etc/machine-id"); err == nil {
|
||||
s := strings.TrimSpace(string(data))
|
||||
if s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// fallback: hostname + first non零MAC
|
||||
hn, _ := os.Hostname()
|
||||
mac := getFirstMac()
|
||||
return hn + "|" + mac
|
||||
}
|
||||
|
||||
func getFirstMac() string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, it := range ifaces {
|
||||
if len(it.HardwareAddr) == 0 {
|
||||
continue
|
||||
}
|
||||
mac := it.HardwareAddr.String()
|
||||
if mac != "" && mac != "00:00:00:00:00:00" {
|
||||
return mac
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var logDir = "./logs" // 日志目录,可根据需要修改
|
||||
|
||||
// 初始化日志目录
|
||||
func Init(dir string) {
|
||||
if dir != "" {
|
||||
logDir = dir
|
||||
}
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
fmt.Println("create log dir failed:", err)
|
||||
}
|
||||
cleanupOldLogs()
|
||||
}
|
||||
|
||||
// Cleanup 删除超过7天的日志文件
|
||||
func cleanupOldLogs() {
|
||||
files, err := os.ReadDir(logDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cutoff := time.Now().AddDate(0, 0, -7)
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := f.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if info.ModTime().Before(cutoff) {
|
||||
_ = os.Remove(filepath.Join(logDir, f.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log 内部写文件
|
||||
func logToFile(level, msg string) {
|
||||
fmt.Println(msg)
|
||||
t := time.Now()
|
||||
// 确保日志目录存在
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
fmt.Println("create log dir failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
filename := filepath.Join(logDir, t.Format("2006-01-02")+".log")
|
||||
f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("open log file failed:", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
line := fmt.Sprintf("%s [%s] %s\n", t.Format("2006-01-02 15:04:05"), level, msg)
|
||||
_, _ = f.WriteString(line)
|
||||
}
|
||||
|
||||
// 对外接口
|
||||
func Info(msg string) {
|
||||
logToFile("INFO", msg)
|
||||
}
|
||||
|
||||
// Println 支持多个参数拼接,写 INFO 日志
|
||||
func Println(v ...interface{}) {
|
||||
msg := fmt.Sprint(v...)
|
||||
logToFile("INFO", msg)
|
||||
}
|
||||
func Warn(msg string) {
|
||||
logToFile("WARN", msg)
|
||||
}
|
||||
|
||||
func Error(msg string) {
|
||||
logToFile("ERROR", msg)
|
||||
}
|
||||
func Fatal(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
logToFile("ERROR", err.Error())
|
||||
}
|
||||
|
||||
// Fatal 打印错误日志并退出程序
|
||||
func Fatalf(msg string, args ...interface{}) {
|
||||
if len(args) > 0 {
|
||||
msg = fmt.Sprintf(msg, args...)
|
||||
}
|
||||
logToFile("FATAL", msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type BaseResponse struct {
|
||||
Status bool `json:"status"` // 是否成功
|
||||
Message string `json:"message"` // 提示信息
|
||||
Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MqttTopic struct {
|
||||
Project string
|
||||
Domain string
|
||||
DeviceType string
|
||||
DeviceID string
|
||||
Resource string
|
||||
}
|
||||
|
||||
// 从字符串解析成 MqttTopic
|
||||
func FromStringToMqttTopic(topic string) *MqttTopic {
|
||||
parts := strings.Split(topic, "/")
|
||||
// 补齐不足的部分
|
||||
for len(parts) < 5 {
|
||||
parts = append(parts, "")
|
||||
}
|
||||
return &MqttTopic{
|
||||
Project: parts[0],
|
||||
Domain: parts[1],
|
||||
DeviceType: parts[2],
|
||||
DeviceID: parts[3],
|
||||
Resource: parts[4],
|
||||
}
|
||||
}
|
||||
|
||||
// 从结构体生成 topic 字符串,可用 "+" 表示通配符
|
||||
func (m *MqttTopic) ToString() string {
|
||||
toVal := func(s string) string {
|
||||
if s == "" {
|
||||
return "+"
|
||||
}
|
||||
return s
|
||||
}
|
||||
return strings.Join([]string{
|
||||
toVal(m.Project),
|
||||
toVal(m.Domain),
|
||||
toVal(m.DeviceType),
|
||||
toVal(m.DeviceID),
|
||||
toVal(m.Resource),
|
||||
}, "/")
|
||||
}
|
||||
|
||||
// 严格生成 topic,不允许 "+" 或空
|
||||
func (m *MqttTopic) Build() (string, error) {
|
||||
parts := []string{m.Project, m.Domain, m.DeviceType, m.DeviceID, m.Resource}
|
||||
for _, p := range parts {
|
||||
if p == "" || p == "+" {
|
||||
return "", errors.New("cannot build strict topic, wildcard exists")
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "/"), nil
|
||||
}
|
||||
|
||||
// 判断是否为通配符 topic
|
||||
func (m *MqttTopic) IsWildcard() bool {
|
||||
topic := m.ToString()
|
||||
return strings.Contains(topic, "+") || strings.Contains(topic, "#")
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type Record struct {
|
||||
DeviceId string
|
||||
LicensePlate string
|
||||
LicensePlateImage string
|
||||
VehicleType string
|
||||
VehicleImage string
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package model
|
||||
|
||||
type UpdateInfo struct {
|
||||
Version int `json:"version"`
|
||||
DownloadURL string `json:"url"`
|
||||
Notes bool `json:"notes"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import "sentinel/pkg/model"
|
||||
|
||||
const (
|
||||
updateCheckURL = "/iot/common/update/check"
|
||||
analyticsURL = "/api/public/sentinel-record-analytics"
|
||||
)
|
||||
|
||||
func CheckUpdate(deviceID string) (*model.UpdateInfo, error) {
|
||||
var resp model.UpdateInfo
|
||||
err := Get(
|
||||
updateCheckURL,
|
||||
map[string]string{
|
||||
"deviceID": deviceID,
|
||||
},
|
||||
&resp,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func Analytics(req model.Record) error {
|
||||
return Post(analyticsURL, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sentinel/pkg/model"
|
||||
"time"
|
||||
|
||||
"sentinel/pkg/log"
|
||||
)
|
||||
|
||||
const baseURL = "http://127.0.0.1:13011"
|
||||
|
||||
var client = &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
func Get(path string, query map[string]string, out any) error {
|
||||
return do(http.MethodGet, path, query, nil, out)
|
||||
}
|
||||
|
||||
func Post(path string, body any, out any) error {
|
||||
return do(http.MethodPost, path, nil, body, out)
|
||||
}
|
||||
|
||||
func do(method, path string, query map[string]string, body any, out any) error {
|
||||
u, err := url.Parse(baseURL + path)
|
||||
if err != nil {
|
||||
log.Error("parse url failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if len(query) > 0 {
|
||||
q := u.Query()
|
||||
for k, v := range query {
|
||||
q.Set(k, v)
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
var reqBody *bytes.Reader
|
||||
if body != nil {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
log.Error("marshal body failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
reqBody = bytes.NewReader(b)
|
||||
} else {
|
||||
reqBody = bytes.NewReader(nil)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), reqBody)
|
||||
if err != nil {
|
||||
log.Error("create request failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Error("request failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
log.Error("http status error: " + resp.Status)
|
||||
return fmt.Errorf("http status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 解析成 BaseResponse
|
||||
var baseResp model.BaseResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&baseResp); err != nil {
|
||||
log.Error("decode base response failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if !baseResp.Status {
|
||||
log.Error("server returned error: " + baseResp.Message)
|
||||
return fmt.Errorf(baseResp.Message)
|
||||
}
|
||||
|
||||
if out != nil && baseResp.Data != nil {
|
||||
// 将 Data 转成业务类型
|
||||
b, err := json.Marshal(baseResp.Data)
|
||||
if err != nil {
|
||||
log.Error("marshal data failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(b, out); err != nil {
|
||||
log.Error("unmarshal data to out failed: " + err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
const (
|
||||
minioEndpoint = "ai.ronsunny.cn:9000"
|
||||
minioAccessKey = "minioadmin"
|
||||
minioSecretKey = "minioadmin"
|
||||
useSSL = true
|
||||
)
|
||||
|
||||
var client *minio.Client
|
||||
|
||||
func Init() error {
|
||||
if client != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := minio.New(minioEndpoint, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(minioAccessKey, minioSecretKey, ""),
|
||||
Secure: useSSL,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client = c
|
||||
return nil
|
||||
}
|
||||
func UploadFile(
|
||||
ctx context.Context,
|
||||
bucketName string,
|
||||
objectName string,
|
||||
filePath string,
|
||||
contentType string,
|
||||
) (int64, error) {
|
||||
|
||||
if client == nil {
|
||||
return 0, errors.New("minio client not initialized")
|
||||
}
|
||||
|
||||
info, err := client.FPutObject(
|
||||
ctx,
|
||||
bucketName,
|
||||
objectName,
|
||||
filePath,
|
||||
minio.PutObjectOptions{
|
||||
ContentType: contentType,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return info.Size, nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package utils
|
||||
|
||||
// 变动
|
||||
|
||||
// 常量
|
||||
const (
|
||||
// 版本号
|
||||
APP_VERSION = 0
|
||||
|
||||
Log_file_dic = "./logs"
|
||||
MQTT_HOST = "ai.ronsunny.cn"
|
||||
MQTT_PORT = 8093
|
||||
PASSWORD = "123456"
|
||||
PROJECT = "sentinel"
|
||||
DEVICE_TPYE = "edge"
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
)
|
||||
|
||||
func GetLocalIP() string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
continue // 接口未启用
|
||||
}
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue // 忽略回环
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetHostname() string {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return name
|
||||
}
|
||||
func GetMacAddress() string {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
mac := iface.HardwareAddr.String()
|
||||
if mac != "" {
|
||||
return mac
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetOSInfo() string {
|
||||
return runtime.GOOS // windows, linux, darwin
|
||||
}
|
||||
func GetCPUInfo() string {
|
||||
info, err := cpu.Info()
|
||||
if err != nil || len(info) == 0 {
|
||||
return ""
|
||||
}
|
||||
return info[0].ModelName
|
||||
}
|
||||
|
||||
func GetMemory() string {
|
||||
v, err := mem.VirtualMemory()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%dMB", v.Total/1024/1024)
|
||||
}
|
||||
|
||||
func GetDisk() string {
|
||||
usage, err := disk.Usage("/")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%dGB", usage.Total/1024/1024/1024)
|
||||
}
|
||||
Reference in New Issue
Block a user