重新整理物联网服务,分离业务逻辑。

This commit is contained in:
BBIT-Kai
2026-01-05 11:54:33 +08:00
parent e2982b141d
commit 892cb2494e
53 changed files with 558 additions and 26 deletions
+20
View File
@@ -0,0 +1,20 @@
package config
// 变动
// 常量
const (
// 版本号
APP_VERSION = 1
BASE_URL = "https://ai.ronsunny.cn:8090"
//BASE_URL = "http://127.0.0.1:13011"
Log_file_dic = "./logs"
MQTT_BROKER = "tls://ai.ronsunny.cn:8093"
PASSWORD = "123456"
)
var (
// DeviceType string
// DeptId string
)
+40
View File
@@ -0,0 +1,40 @@
package device
import (
"net"
"os"
"strings"
)
// GetDeviceID 返回本机原始唯一IDLinux /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 ""
}
+97
View File
@@ -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)
}
+7
View File
@@ -0,0 +1,7 @@
package model
type BaseResponse struct {
Status bool `json:"status"` // 是否成功
Message string `json:"message"` // 提示信息
Data interface{} `json:"data,omitempty"` // 泛型数据,用 interface{} 接收任意类型
}
+64
View File
@@ -0,0 +1,64 @@
package model
import (
"errors"
"strings"
)
type MqttTopic struct {
DeptId 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{
DeptId: 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.DeptId),
toVal(m.Domain),
toVal(m.DeviceType),
toVal(m.DeviceID),
toVal(m.Resource),
}, "/")
}
// 严格生成 topic,不允许 "+" 或空
func (m *MqttTopic) Build() (string, error) {
parts := []string{m.DeptId, 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, "#")
}
+7
View File
@@ -0,0 +1,7 @@
package model
type UpdateInfo struct {
Version int `json:"version"`
DownloadURL string `json:"url"`
Notes string `json:"notes"`
}
+22
View File
@@ -0,0 +1,22 @@
package api
import "sentinel/pkg/model"
const (
updateCheckURL = "/iot/common/update/check"
)
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
}
+101
View File
@@ -0,0 +1,101 @@
package api
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"sentinel/pkg/model"
"time"
"sentinel/pkg/config"
"sentinel/pkg/log"
)
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(config.BASE_URL + 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")
req.Header.Set("apikey", "NzusyzcLIUoZ22tflHN2sOjHrry3W7zJ")
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,11 @@
package platform
type UpdaterLauncher interface {
Start(args []string) error
}
type MainProgramStarter interface {
Start(targetExe string) error
GetMainName() string
}
+100
View File
@@ -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)
}