This commit is contained in:
2024-12-23 18:34:46 +08:00
commit cbfcc91eec
24 changed files with 1702 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

9
.idea/DT.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/DT.iml" filepath="$PROJECT_DIR$/.idea/DT.iml" />
</modules>
</component>
</project>

39
config.yaml Normal file
View File

@@ -0,0 +1,39 @@
is_debug: true
service:
http:
host: 0.0.0.0 # 默认localhost
port: 8001 # 默认9999
tcp:
host: 0.0.0.0 # 默认localhost
port: 8002 # 默认9999
db:
db_host: "127.0.0.1"
db_port: "3306"
db_name: "dianti"
db_user: "root"
db_pass: "root"
table_prefix: "dt_"
time_zone: "Local"
log_level: 4
slow_threshold: 200
idle_conns: 10
open_conns: 50
#jwt:
# signing_key: iuu
# expires_time: 7d
# buffer_time: 1d
# issuer: iuu.me
#influxdb:
# # host: "http://192.168.0.47:18086"
# # token: "BfG_zW41Wddgbuig0GXt6TuDWpHjUgjTJGDFi9ZI6fOXeYwyWKhakTrwbRT8f4uqFQCWXbRGxs8f5GaChW5tqw=="
# host: "http://192.168.0.9:8086"
# token: "4Sz5qRK-VY0aGh7m7sYDkG5tWCwKjbDYAlkgFpvUcZTBn7XahGqczeye7BQCRjcWP8fqsefPeNkwfvUnqa69oA=="

12
config/config.go Normal file
View File

@@ -0,0 +1,12 @@
package config
const (
AppEnv = "dev"
EnvConfig = "config.yaml"
)
type Config struct {
IsDebug bool `mapstructure:"is_debug" json:"is_debug" yaml:"is_debug"`
Db DbConf `mapstructure:"db" json:"db" yaml:"db"`
Service ServiceConf `mapstructure:"service" json:"service" yaml:"service"`
}

15
config/db.go Normal file
View File

@@ -0,0 +1,15 @@
package config
type DbConf struct {
DbHost string `mapstructure:"db_host" json:"db_host" yaml:"db_host"`
DbPort int `mapstructure:"db_port" json:"db_port" yaml:"db_port"`
DbName string `mapstructure:"db_name" json:"db_name" yaml:"db_name"`
DbUser string `mapstructure:"db_user" json:"db_user" yaml:"db_user"`
DbPass string `mapstructure:"db_pass" json:"db_pass" yaml:"db_pass"`
TablePrefix string `mapstructure:"table_prefix" json:"table_prefix" yaml:"table_prefix"`
TimeZone string `mapstructure:"time_zone" json:"time_zone" yaml:"time_zone"`
LogLevel int `mapstructure:"log_level" json:"log_level" yaml:"log_level"` // LogLevel SQL日志级别 (1-静音 2-错误 3-警告 4-信息)
SlowThreshold int `mapstructure:"slow_threshold" json:"slow_threshold" yaml:"slow_threshold"` // SlowThreshold 慢SQL阈值毫秒。慢SQL会在log_level大于等于3时输出。
IdleConns int `mapstructure:"idle_conns" json:"idle_conns" yaml:"idle_conns"` // 空闲连接池中的最大连接数建议为open_conns的百分之5-20之间
OpenConns int `mapstructure:"open_conns" json:"open_conns" yaml:"open_conns"` // 最大打开连接数建议这里设置为50
}

15
config/service.go Normal file
View File

@@ -0,0 +1,15 @@
package config
type ServiceConf struct {
Http HttpConf `mapstructure:"http" json:"http" yaml:"http"`
Tcp TcpConf `mapstructure:"tcp" json:"tcp" yaml:"tcp"`
}
type HttpConf struct {
Host string `mapstructure:"host" json:"host" yaml:"host"`
Port string `mapstructure:"port" json:"port" yaml:"port"`
}
type TcpConf struct {
Host string `mapstructure:"host" json:"host" yaml:"host"`
Port string `mapstructure:"port" json:"port" yaml:"port"`
}

61
core/gorm/gorm.go Normal file
View File

@@ -0,0 +1,61 @@
package gorm
import (
"DT/global"
"DT/model"
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"os"
"time"
)
func InitGorm() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=%s", global.AppConf.Db.DbUser, global.AppConf.Db.DbPass, global.AppConf.Db.DbHost, global.AppConf.Db.DbPort, global.AppConf.Db.DbName, global.AppConf.Db.TimeZone)
postgresConfig := mysql.Config{
DSN: dsn,
}
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Duration(global.AppConf.Db.SlowThreshold) * time.Millisecond,
LogLevel: logger.LogLevel(global.AppConf.Db.LogLevel),
IgnoreRecordNotFoundError: true,
Colorful: true,
})
MYDB, err := gorm.Open(mysql.New(postgresConfig), &gorm.Config{
Logger: newLogger,
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
TablePrefix: global.AppConf.Db.TablePrefix,
},
})
DB := MYDB
if err != nil {
fmt.Println("[-] 数据库连接失败")
os.Exit(0)
}
sqlDB, _ := DB.DB()
sqlDB.SetMaxIdleConns(global.AppConf.Db.IdleConns)
sqlDB.SetMaxOpenConns(global.AppConf.Db.OpenConns)
sqlDB.SetConnMaxLifetime(time.Hour)
fmt.Println("[+]连接成功!")
global.Db = DB
}
func AutoMigrate(db *gorm.DB) {
err := db.AutoMigrate(
new(model.Device),
)
if err != nil {
fmt.Println("[-] 迁移数据表失败:", err.Error())
os.Exit(0)
}
}

91
core/logger/logger.go Normal file
View File

@@ -0,0 +1,91 @@
package logger
import (
"DT/global"
"fmt"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/sirupsen/logrus"
"runtime"
"strings"
"sync"
"time"
)
func InitLogger() *global.Logger {
L := logrus.New()
path := "./log/log"
file, _ := rotatelogs.New(
path+".%Y%m%d",
rotatelogs.WithLinkName(path),
rotatelogs.WithMaxAge(time.Duration(100*24)*time.Hour), //自动删除
rotatelogs.WithRotationTime(time.Duration(24*60)*time.Minute), //分割时间
)
L.SetOutput(file)
L.SetFormatter(&logrus.JSONFormatter{})
Log := &global.Logger{Logger: L, Heads: []string{}}
global.Log = Log
return Log
}
var errs = &errFunc{lock: new(sync.Mutex)}
func ErrorRegister(f func(string)) {
errs.register(f, false)
}
func ErrorRegisterOnly1(f func(string)) {
errs.register(f, true)
}
func ErrorWx(e ...string) {
errs.run(strings.Join(e, " "))
global.Log.Error(e)
}
type errFunc struct {
f []func(string)
lastTime int64
lock *sync.Mutex
}
func (e *errFunc) register(f func(string), only bool) {
if only {
e.f = []func(string){f}
} else {
e.f = append(e.f, f)
}
}
func (e *errFunc) run(logs string) {
if len(e.f) == 0 {
return
}
if time.Now().Unix()-e.lastTime > 1 {
e.lock.Lock()
e.lastTime = time.Now().Unix()
defer e.lock.Unlock()
for _, v := range e.f {
v(logs)
}
}
}
func StackSend(skip int, e string) {
e += "\n" + Stack(skip)
if global.AppConf.IsDebug {
fmt.Print(e)
}
errs.run(e)
}
func Stack(skip int) (re string) {
for i := skip; ; i++ {
_, file, line, ok := runtime.Caller(i)
if !ok {
break
}
if !strings.Contains(file, "local/go/src") && !strings.Contains(file, "/go/pkg") {
logs := fmt.Sprintf("%s:%d\n", file, line)
re += logs
}
}
return
}

59
core/viper/viper.go Normal file
View File

@@ -0,0 +1,59 @@
package viper
import (
"DT/config"
"DT/global"
"flag"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"os"
)
// InitViper 解析yaml格式文件
func InitViper(path ...string) {
var confPath string
if len(path) == 0 {
flag.StringVar(&confPath, "c", "", "choose config file.")
flag.Parse()
if confPath == "" {
if AppEnv := os.Getenv(config.AppEnv); AppEnv == "" {
confPath = config.EnvConfig
} else {
confPath = AppEnv
}
} else {
}
} else {
confPath = path[0]
}
v := viper.New()
// 指定配置文件路径
v.SetConfigFile(confPath)
// 如果配置文件的名称中没有扩展名,则需要配置此项
v.SetConfigType("yaml")
// 查找并读取配置文件
err := v.ReadInConfig()
if err != nil {
fmt.Printf("[-]读取配置文件错误: %s \n", err)
os.Exit(0)
}
// 监控并重新读取配置文件
v.WatchConfig()
v.OnConfigChange(func(e fsnotify.Event) {
if err = v.Unmarshal(&global.AppConf); err != nil {
fmt.Printf("[-]重新解析配置文件失败: %s \n", err)
os.Exit(0)
}
fmt.Println("[+]重新加载配置文件完成")
})
if err = v.Unmarshal(&global.AppConf); err != nil {
fmt.Printf("[-]解析配置文件失败: %s \n", err)
os.Exit(0)
}
fmt.Println("[+]加载配置文件完成")
//dump.P(global.AppConf)
}

14
global/logger.go Normal file
View File

@@ -0,0 +1,14 @@
package global
import "github.com/sirupsen/logrus"
type Logger struct {
*logrus.Logger
Heads []string
}
func (l *Logger) AddHead(args ...string) {
for _, v := range args {
l.Heads = append(l.Heads, v)
}
}

19
global/var.go Normal file
View File

@@ -0,0 +1,19 @@
package global
import (
"DT/config"
"gorm.io/gorm"
)
var (
// AppConf 配置信息
AppConf *config.Config
// Db 数据库
Db *gorm.DB
//InFluxDb influxdb2.Client
Log *Logger
//Cron *cron.Cron
)

66
go.mod Normal file
View File

@@ -0,0 +1,66 @@
module DT
go 1.23.3
require (
github.com/fsnotify/fsnotify v1.8.0
github.com/gin-gonic/gin v1.10.0
github.com/gookit/goutil v0.6.18
github.com/gorilla/websocket v1.5.3
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.12
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/strftime v1.1.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

162
go.sum Normal file
View File

@@ -0,0 +1,162 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg=
github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

194
httpserver/server.go Normal file
View File

@@ -0,0 +1,194 @@
package httpserver
import (
"DT/tcpserver"
"DT/ws"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"log"
"net/http"
)
// Server HTTP服务器结构
type Server struct {
tcpServer *tcpserver.Server
httpServer *http.Server
router *gin.Engine
hub *ws.Hub
}
// NewServer 创建一个新的HTTP服务器
func NewServer(addr string, tcpServer *tcpserver.Server) *Server {
router := gin.Default()
hub := ws.NewHub()
server := &Server{
tcpServer: tcpServer,
router: router,
httpServer: &http.Server{
Addr: addr,
Handler: router,
},
hub: hub,
}
// 启动 hub
go hub.Run()
// 注册路由
server.registerRoutes()
return server
}
// 添加请求结构体
type SendCommandRequest struct {
Imei string `json:"Imei" binding:"required"`
Command string `json:"command" binding:"required"`
}
// registerRoutes 方法
func (s *Server) registerRoutes() {
// 获取在线客户端列表
s.router.GET("/clients", s.handleGetClients)
// 添加发送命令的路由
s.router.POST("/clients_send", s.handleSendCommand)
// WebSocket 路由 - 使用查询参数获取 IMEI
s.router.GET("/ws/:imei", s.handleWebSocket)
}
// 处理发送命令的方法
func (s *Server) handleSendCommand(c *gin.Context) {
var req SendCommandRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"msg": "invalid request: " + err.Error(),
})
return
}
// 查找目标客户端
clients := s.tcpServer.GetOnlineClients()
var targetClient *tcpserver.Client
found := false
for _, client := range clients {
if client["imei"] == req.Imei {
if clientObj, ok := s.tcpServer.GetClient(client["id"].(string)); ok {
targetClient = clientObj
found = true
break
}
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"msg": "client not found",
})
return
}
// 发送命令
_, err := targetClient.Conn.Write(append([]byte(req.Command), '\r', '\n'))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"msg": "failed to send command: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "success",
"data": gin.H{
"imei": req.Imei,
"command": req.Command,
},
})
}
// handleGetClients 处理获取客户端列表的请求
func (s *Server) handleGetClients(c *gin.Context) {
clients := s.tcpServer.GetOnlineClients()
c.JSON(http.StatusOK, gin.H{
"code": 200,
"msg": "success",
"data": gin.H{
"total": len(clients),
"clients": clients,
},
})
}
// 添加 WebSocket 处理函数
func (s *Server) handleWebSocket(c *gin.Context) {
imei := c.Param("imei")
//imei := c.Query("imei")
if imei == "" {
log.Printf("IMEI 参数为空")
c.String(http.StatusBadRequest, "IMEI parameter is required")
return
}
//devId, err := strconv.Atoi(deviceId)
//if err != nil {
// log.Printf("设备ID转换失败: %v", err)
// c.String(http.StatusBadRequest, "Invalid device ID")
// return
//}
log.Printf("WebSocket 连接请求: imei=%s", imei)
upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("WebSocket 升级失败: %v", err)
return
}
client := &ws.Client{
Hub: s.hub,
Conn: conn,
Send: make(chan *ws.WsMessage, 256),
Imei: imei,
//DeviceId: devId,
}
log.Printf("WebSocket 客户端创建: imei=%s", imei)
client.Hub.Register <- client
go client.WritePump()
go client.ReadPump()
}
// Start 启动HTTP服务器
func (s *Server) Start() error {
return s.httpServer.ListenAndServe()
}
// Stop 优雅关闭HTTP服务器
func (s *Server) Stop(ctx context.Context) error {
fmt.Println("Stopping HTTP server...")
if err := s.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("HTTP server forced to shutdown: %w", err)
}
fmt.Println("HTTP server stopped gracefully")
return nil
}
// GetHub 添加获取 Hub 的方法
func (s *Server) GetHub() *ws.Hub {
return s.hub
}

86
main.go Normal file
View File

@@ -0,0 +1,86 @@
package main
import (
"DT/core/gorm"
"DT/core/logger"
"DT/core/viper"
"DT/global"
"DT/httpserver"
"DT/tcpserver"
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func init() {
viper.InitViper()
logger.InitLogger()
gorm.InitGorm()
//gorm.AutoMigrate(global.Db)
}
func main() {
handler := &tcpserver.TCPHandler{}
tcpHost := global.AppConf.Service.Tcp.Host
tcpPort := global.AppConf.Service.Tcp.Port
tcpServer := tcpserver.NewServer(tcpHost+":"+tcpPort, handler.HandleClient)
handler.Server = tcpServer
httpHost := global.AppConf.Service.Http.Host
httpPort := global.AppConf.Service.Http.Port
httpServer := httpserver.NewServer(httpHost+":"+httpPort, tcpServer)
handler.Hub = httpServer.GetHub()
go func() {
if err := tcpServer.Start(); err != nil {
fmt.Printf("TCP server error: %v\n", err)
}
}()
go func() {
fmt.Printf("Listening and serving HTTP on %s:%s\r\n", httpHost, httpPort)
if err := httpServer.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) {
fmt.Printf("HTTP server error: %v\n", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("Shutting down servers...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := httpServer.Stop(ctx); err != nil {
fmt.Printf("Error stopping HTTP server: %v\n", err)
}
tcpServer.Stop()
fmt.Println("All servers stopped")
}
// 客户端发送 {"Type":"reg","Imei":"8610123456","Pwd":"123456","Ver":"v1.0.0"}
// 服务器回复 {"Type":"reg","Time":1734948442}
// 心跳
// 客户端{"Type":"ping"}
// 服务端{"Type":"pong"}
// 服务端发 {"Type":"ota","Ip":"192.168.31.1:80","File":"/xxx/1.bin"}
// 服务端发 {\"Type\":\"ota\",\"Ip\":\"192.168.31.1:80\",\"File\":\"/xxx/1.bin\"}
// 客户端回复 {"Type":"ota","State":"1"}
// 开始实时上传数据
// 服务端发 {"Type":"start"}
// 客户端回复 {"Type":"start","Data":"{\"Type\":\"ota\",\"Ip\":\"192.168.31.1:80\",\"File\":\"/xxx/1.bin\"}"}
// 客户端回复 {"Type":"start","Data":"{\"Type\":\"ota\",\"Ip\":\"192.168.31.1:80\",\"File\":\"/xxx/1.bin\"}"}
// 停止实时上传数据
// 服务端发 {"Type":"stop"}
// 客户端回复 {"Type":"stop","State":"1"}

25
model/device.go Normal file
View File

@@ -0,0 +1,25 @@
package model
import (
"DT/global"
"gorm.io/gorm"
"time"
)
type Device struct {
Id int `gorm:"column:id;primaryKey" json:"id"`
//DriverId string `gorm:"column:driver_id;comment:设备ID;type:varchar(255)" json:"driver_id"`
Imei string `gorm:"column:imei;comment:IMEI;type:varchar(255)" json:"imei"`
//DriverName string `gorm:"column:driver_name;comment:设备名称;type:varchar(255)" json:"driver_name"`
DriverPass string `gorm:"column:driver_pass;comment:设备密码;type:varchar(255)" json:"driver_pass"`
DriverVer string `gorm:"column:driver_ver;comment:固件版本;type:varchar(255)" json:"driver_ver"`
//DriverFd string `gorm:"column:driver_fd;comment:设备FD;type:varchar(255)" json:"driver_fd"`
Remark string `gorm:"column:remark;comment:备注;type:varchar(255)" json:"remark"`
Created time.Time `gorm:"column:created;autoCreateTime;comment:创建时间" json:"created"`
Updated time.Time `gorm:"column:updated;autoUpdateTime;comment:修改时间" json:"updated"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间" json:"-"`
}
func (r *Device) TableName() string {
return global.AppConf.Db.TablePrefix + "devices"
}

81
repository/device.go Normal file
View File

@@ -0,0 +1,81 @@
package repository
import (
"DT/global"
"DT/model"
)
type Device struct {
}
//func (r *Device) GetDeviceList(qr map[string]interface{}) (p []*model.Device, err error) {
// db := global.Db
// for key, value := range qr {
// db = db.Where(key, value)
// }
// err = db.Order("id DESC").Find(&p).Error
// return
//}
//func (r *Device) GetDevicePage(req *form.DevicePageReq) (count int64, list []*device_model.Device, err error) {
// db := global.Db.Model(&device_model.Device{})
// //for key, value := range qr {
// // db = db.Where(key, value)
// //}
// //if req.DriverId > 0 {
// // db = db.Where("driver_id = ?", req.DriverId)
// //}
// err = db.Count(&count).Error
// if err != nil {
// return
// }
// //err = db.Offset((req.Page.GetPageIndex() - 1) * req.Page.GetPageSize()).Limit(req.Page.GetPageSize()).Order("id desc").Preload("Rule").Find(&list).Error
// err = db.Offset((req.Page.GetPageIndex() - 1) * req.Page.GetPageSize()).Limit(req.Page.GetPageSize()).Order("id desc").Find(&list).Error
// return
//}
func (r *Device) CreateDevice(d *model.Device) error {
return global.Db.Create(d).Error
}
func (r *Device) UpdateDevice(d *model.Device) error {
return global.Db.Save(d).Error
}
func (r *Device) GetDevice(qr map[string]interface{}) (d *model.Device, err error) {
db := global.Db
for key, value := range qr {
db = db.Where(key, value)
}
err = db.First(&d).Error
return
}
//func (r *User) GetPeakValleyQuarterPage(req *form.PeakValleyQuarterListReq) (count int64, list []*peak_valley_model.PeakValleyQuarter, err error) {
// db := global.Db.Model(&peak_valley_model.PeakValleyQuarter{})
//
// //for key, value := range qr {
// // db = db.Where(key, value)
// //}
// err = db.Count(&count).Error
// if err != nil {
// return
// }
// err = db.Offset((req.Page.GetPageIndex() - 1) * req.Page.GetPageSize()).Limit(req.Page.GetPageSize()).Order("id desc").Preload("Rule").Find(&list).Error
// return
//}
//func (r *User) CreatePeakValleyQuarter(d *user_model.User) error {
// return global.Db.Create(d).Error
//}
//func (r *User) UpdatePeakValleyQuarter(d *user_model.User) error {
// return global.Db.Save(d).Error
//}
//func (r *User) GetUser(qr map[string]interface{}) (d *user_model.User, err error) {
// db := global.Db
// for key, value := range qr {
// db = db.Where(key, value)
// }
// err = db.First(&d).Error
// return
//}

7
repository/enter.go Normal file
View File

@@ -0,0 +1,7 @@
package repository
type groupRepository struct {
Device
}
var GroupRepositorys = new(groupRepository)

267
tcpserver/handler.go Normal file
View File

@@ -0,0 +1,267 @@
package tcpserver
import (
"DT/repository"
"DT/ws"
"bufio"
"bytes"
"encoding/json"
"fmt"
"net"
"time"
)
type TCPHandler struct {
Server *Server
Hub *ws.Hub
}
func (h *TCPHandler) HandleClient(conn net.Conn) {
reader := bufio.NewReader(conn)
clientID := conn.RemoteAddr().String()
var client *Client
if value, ok := h.Server.GetClient(clientID); ok {
client = value
} else {
fmt.Println("找不到客户端:", clientID)
return
}
broadcastMessage := func(data []byte) {
if h.Hub != nil {
h.Hub.Broadcast <- &ws.WsMessage{IMEI: client.Imei, Data: string(data)}
}
}
for {
message, err := reader.ReadBytes('\n')
if err != nil {
fmt.Println("客户端已断开连接:", err)
break
}
// 去除末尾的换行符
message = bytes.TrimSpace(message)
fmt.Printf("收到消息来自 %s: %s\n", conn.RemoteAddr(), string(message))
if !json.Valid(message) {
fmt.Printf("来自客户端的数据非法 %s\n", conn.RemoteAddr())
conn.Close()
return
}
var fullMsg map[string]interface{}
if err := json.Unmarshal(message, &fullMsg); err != nil {
fmt.Printf("Error parsing message: %v\n", err)
continue
}
msgType, _ := fullMsg["Type"].(string)
if msgType == "" {
fmt.Printf("接收到的消息类型为空 %s\n", conn.RemoteAddr())
conn.Close()
return
}
switch msgType {
case "reg":
// 处理登录请求
if err := h.Server.HandleAuth(client, message); err != nil {
fmt.Printf("客户端授权失败: %v\n", err)
conn.Close()
return
}
fmt.Printf("客户端已授权: %s\n", client.Imei)
// 广播登录消息
broadcastMessage(message)
case "ping":
// 处理心跳
if !client.IsAuth {
fmt.Printf("来自未授权客户端的心跳 %s\n", conn.RemoteAddr())
conn.Close()
return
}
if err := h.Server.HandleHeartbeat(client, message); err != nil {
fmt.Printf("心跳错误: %v\n", err)
continue
}
// 广播心跳消息
broadcastMessage(message)
case "ota":
// 处理 OTA
if !client.IsAuth {
fmt.Printf("来自未授权客户端的消息 %s\n", conn.RemoteAddr())
conn.Close()
return
}
if err := h.Server.HandleOta(client, message); err != nil {
fmt.Printf("OTA 错误: %v\n", err)
continue
}
// 广播 OTA 消息
broadcastMessage(message)
case "start":
// 处理 客户端实时上报数据
if !client.IsAuth {
fmt.Printf("来自未授权客户端的消息 %s\n", conn.RemoteAddr())
conn.Close()
return
}
if err := h.Server.RealTimeReporting(client, message); err != nil {
fmt.Printf("OTA 错误: %v\n", err)
continue
}
// 广播 OTA 消息
broadcastMessage(message)
default:
// 处理其他消息类型
if !client.IsAuth {
fmt.Printf("来自未授权客户端的消息 %s\n", conn.RemoteAddr())
conn.Close()
return
}
// 广播其他类型的消息
broadcastMessage(message)
}
}
}
func (s *Server) HandleHeartbeat(client *Client, message []byte) error {
if !client.IsAuth {
return fmt.Errorf("unauthorized")
}
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
return err
}
if msg.Type == "ping" {
client.LastPing = time.Now()
response := Message{
MessageType: MessageType{Type: "pong"},
}
responseData, err := json.Marshal(response)
if err != nil {
return err
}
_, err = client.Conn.Write(append(responseData, '\r', '\n'))
return err
}
return nil
}
func (s *Server) HandleAuth(client *Client, message []byte) error {
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
return err
}
if msg.Type != "reg" {
return fmt.Errorf("unauthorized")
}
model, err := repository.GroupRepositorys.Device.GetDevice(map[string]interface{}{"imei": msg.Imei})
if err != nil {
return fmt.Errorf("设备不存在")
}
if msg.Pwd != model.DriverPass {
return fmt.Errorf("设备密码不正确")
}
// 更新版本号
model.DriverVer = msg.Ver
err = repository.GroupRepositorys.Device.UpdateDevice(model)
if err != nil {
return fmt.Errorf("更新设备版本号失败")
}
// 认证成功,停止登录超时定时器
if client.authTimer != nil {
client.authTimer.Stop()
client.authTimer = nil
}
// 认证成功
client.Imei = msg.Imei
client.IsAuth = true
// 发送响应
response := Message{
MessageType: MessageType{Type: "reg"},
MessageTime: MessageTime{Time: time.Now().Unix()},
}
responseData, err := json.Marshal(response)
if err != nil {
return err
}
_, err = client.Conn.Write(append(responseData, '\r', '\n'))
return err
}
func (s *Server) HandleOta(client *Client, message []byte) error {
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
return err
}
if msg.Type != "ota" {
return fmt.Errorf("unauthorized")
}
fmt.Printf("设备升级结果:%s\r\n", msg.State)
//model, err := repository.GroupRepositorys.Device.GetDevice(map[string]interface{}{"imei": msg.Imei})
//if err != nil {
// return fmt.Errorf("设备不存在")
//}
//if msg.Pwd != model.DriverPass {
// return fmt.Errorf("设备密码不正确")
//}
// 更新版本号
//model.DriverVer = msg.Ver
//err = repository.GroupRepositorys.Device.UpdateDevice(model)
//if err != nil {
// return fmt.Errorf("更新设备版本号失败")
//}
// 认证成功,停止登录超时定时器
//if client.authTimer != nil {
// client.authTimer.Stop()
// client.authTimer = nil
//}
// 认证成功
//client.Imei = msg.Imei
//client.IsAuth = true
// 发送响应
//response := Message{
// MessageType: MessageType{Type: "reg"},
// MessageTime: MessageTime{Time: time.Now().Unix()},
//}
//responseData, err := json.Marshal(response)
//if err != nil {
// return err
//}
//
//_, err = client.Conn.Write(append(responseData, '\r', '\n'))
return nil
}
func (s *Server) RealTimeReporting(client *Client, message []byte) error {
var msg Message
if err := json.Unmarshal(message, &msg); err != nil {
return err
}
if msg.Type != "start" {
return fmt.Errorf("unauthorized")
}
fmt.Printf("设备实时上报数据:%s\r\n", msg.Data)
return nil
}

35
tcpserver/message.go Normal file
View File

@@ -0,0 +1,35 @@
package tcpserver
type MessageType struct {
Type string `json:"Type"`
}
type MessagePassword struct {
Pwd string `json:"Pwd,omitempty"`
}
type MessageImei struct {
Imei string `json:"Imei,omitempty"`
}
type MessageVer struct {
Ver string `json:"Ver,omitempty"`
}
type MessageTime struct {
Time int64 `json:"Time,omitempty"`
}
type MessageState struct {
State string `json:"State,omitempty"`
}
type MessageData struct {
Data string `json:"Data,omitempty"`
}
type Message struct {
MessageType
MessageImei
MessagePassword
MessageVer
MessageTime
MessageState
MessageData
}

225
tcpserver/tcpserver.go Normal file
View File

@@ -0,0 +1,225 @@
package tcpserver
import (
"fmt"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// Server 定义 TCP 服务器结构
type Server struct {
Address string // 监听地址
Handler func(net.Conn) // 客户端连接处理函数
listener net.Listener // TCP 监听器
connections sync.Map // 活跃的客户端连接
wg sync.WaitGroup // 等待所有 Goroutine 完成
stopChan chan struct{} // 关闭信号
clients map[string]*Client
clientsMux sync.RWMutex
stopOnce sync.Once // 添加 sync.Once 来确保 Stop 只被执行一次
}
// Client 定义客户端结构
type Client struct {
ID string
Imei string // 添加 IMEI
Conn net.Conn
ConnectedAt time.Time
LastPing time.Time
Done chan struct{}
IsAuth bool // 添加认证状态
authTimer *time.Timer // 添加登录超时定时器
}
// NewServer 创建一个新的 TCP 服务器
func NewServer(address string, handler func(net.Conn)) *Server {
return &Server{
Address: address,
Handler: handler,
stopChan: make(chan struct{}),
clients: make(map[string]*Client),
}
}
// Start 启动服务器
func (s *Server) Start() error {
var err error
s.listener, err = net.Listen("tcp", s.Address)
if err != nil {
return fmt.Errorf("failed to start server: %w", err)
}
fmt.Println("Listening and serving TCP on", s.Address)
// 捕获终止信号
go s.handleShutdown()
for {
// 非阻塞检查关闭信号
select {
case <-s.stopChan:
return nil
default:
}
// 接受新连接
conn, err := s.listener.Accept()
if err != nil {
select {
case <-s.stopChan:
return nil // 服务器已停止
default:
fmt.Println("Error accepting connection:", err)
continue
}
}
// 记录活跃连接并添加到 clients 映射
s.connections.Store(conn.RemoteAddr(), conn)
client := s.addClient(conn)
fmt.Printf("客户端已连接: %s\n", client.ID)
// 使用 Goroutine 处理客户端
s.wg.Add(1)
go func(c net.Conn, clientID string) {
defer s.wg.Done()
defer func() {
s.connections.Delete(c.RemoteAddr())
s.removeClient(clientID)
c.Close()
fmt.Printf("客户端已断开连接: %s\n", clientID)
}()
s.Handler(c)
}(conn, client.ID)
}
}
// handleShutdown 处理优雅关闭
func (s *Server) handleShutdown() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<-c
fmt.Println("\nReceived shutdown signal...")
s.Stop()
}
// GetOnlineClients 获取所有在线客户端信息
func (s *Server) GetOnlineClients() []map[string]interface{} {
s.clientsMux.RLock()
defer s.clientsMux.RUnlock()
clients := make([]map[string]interface{}, 0, len(s.clients))
for _, client := range s.clients {
clientInfo := map[string]interface{}{
"id": client.ID,
"imei": client.Imei,
"addr": client.Conn.RemoteAddr().String(),
"connected_at": client.ConnectedAt,
"last_ping": client.LastPing,
"is_auth": client.IsAuth,
}
clients = append(clients, clientInfo)
}
return clients
}
// addClient 添加新客户端
func (s *Server) addClient(conn net.Conn) *Client {
s.clientsMux.Lock()
defer s.clientsMux.Unlock()
client := &Client{
ID: conn.RemoteAddr().String(),
Conn: conn,
ConnectedAt: time.Now(),
LastPing: time.Now(),
Done: make(chan struct{}),
}
s.clients[client.ID] = client
// 启动心跳检测
go client.startHeartbeat(s)
// 添加登录超时检测
client.authTimer = time.AfterFunc(time.Minute, func() {
if !client.IsAuth {
fmt.Printf("Client %s authentication timeout\n", client.ID)
client.Conn.Close() // 强制关闭连接
}
})
return client
}
// removeClient 移除客户端
func (s *Server) removeClient(id string) {
s.clientsMux.Lock()
defer s.clientsMux.Unlock()
if client, ok := s.clients[id]; ok {
if client.authTimer != nil {
client.authTimer.Stop()
client.authTimer = nil
}
close(client.Done) // 停止心跳检测
delete(s.clients, id)
}
}
// Stop 添加一个新方法用于外部调用关闭服务
func (s *Server) Stop() {
s.stopOnce.Do(func() {
fmt.Println("Stopping TCP server...")
// 关闭监听器
close(s.stopChan)
if s.listener != nil {
s.listener.Close()
}
// 关闭所有连接
s.clientsMux.Lock()
for id, client := range s.clients {
client.Conn.Close()
delete(s.clients, id)
}
s.clientsMux.Unlock()
// 等待所有 Goroutine 完成
s.wg.Wait()
fmt.Println("TCP server stopped.")
})
}
// 添加心跳检测方法
func (c *Client) startHeartbeat(s *Server) {
ticker := time.NewTicker(20 * time.Second) // 每10秒检查一次
defer ticker.Stop()
for {
select {
case <-c.Done:
return
case <-ticker.C:
if time.Since(c.LastPing) > 120*time.Second {
fmt.Printf("客户端 %s 心跳超时 \n", c.ID)
c.Conn.Close() // 强制关闭连接
return
}
}
}
}
// GetClient 获取指定ID的客户端
func (s *Server) GetClient(id string) (*Client, bool) {
s.clientsMux.RLock()
defer s.clientsMux.RUnlock()
client, ok := s.clients[id]
return client, ok
}

116
ws/client.go Normal file
View File

@@ -0,0 +1,116 @@
package ws
import (
"encoding/json"
"github.com/gorilla/websocket"
"log"
"time"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
maxMessageSize = 512
)
// Client WebSocket 客户端连接
type Client struct {
Hub *Hub
Conn *websocket.Conn
Send chan *WsMessage
Imei string
DeviceId int
}
func (c *Client) ReadPump() {
defer func() {
c.Hub.Unregister <- c
c.Conn.Close()
}()
c.Conn.SetReadLimit(maxMessageSize)
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
c.Conn.SetPongHandler(func(string) error {
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, message, err := c.Conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("读取错误: %v", err)
}
return
}
// 检查是否是字符串 "PING"
if string(message) == "PING" {
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
err := c.Conn.WriteMessage(websocket.TextMessage, []byte("PONG"))
if err != nil {
log.Printf("发送 PONG 响应失败: %v", err)
return
}
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
continue
}
// 处理 JSON 消息
var msg struct {
Type string `json:"Type"`
}
if err := json.Unmarshal(message, &msg); err == nil && msg.Type == "ping" {
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
response := struct {
Type string `json:"Type"`
}{
Type: "pong",
}
responseData, _ := json.Marshal(response)
err := c.Conn.WriteMessage(websocket.TextMessage, responseData)
if err != nil {
log.Printf("发送 pong 响应失败: %v", err)
return
}
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
}
}
}
func (c *Client) WritePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.Conn.Close()
}()
for {
select {
case message, ok := <-c.Send:
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
log.Printf("准备发送消息到 WebSocket: IMEI=%s, Data=%s", message.IMEI, message.Data)
// 直接发送原始消息字符串
err := c.Conn.WriteMessage(websocket.TextMessage, []byte(message.Data))
if err != nil {
log.Printf("发送消息失败: %v", err)
return
}
log.Printf("消息发送成功: IMEI=%s", message.IMEI)
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("发送 ping 失败: %v", err)
return
}
}
}
}

88
ws/hub.go Normal file
View File

@@ -0,0 +1,88 @@
package ws
import (
"log"
"sync"
)
// Hub 维护活动客户端的集合并广播消息
type Hub struct {
// 按 IMEI 分组的客户端连接
Clients map[string]map[*Client]bool
// 广播消息的通道(添加 IMEI
Broadcast chan *WsMessage
// 注册请求
Register chan *Client
// 注销请求
Unregister chan *Client
// 互斥锁保护 clients map
mu sync.RWMutex
}
// Message WebSocket 消息结构
type WsMessage struct {
IMEI string `json:"imei"`
Data string `json:"data"`
}
func NewHub() *Hub {
return &Hub{
Broadcast: make(chan *WsMessage),
Register: make(chan *Client),
Unregister: make(chan *Client),
Clients: make(map[string]map[*Client]bool),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.Register:
h.mu.Lock()
if _, ok := h.Clients[client.Imei]; !ok {
h.Clients[client.Imei] = make(map[*Client]bool)
}
h.Clients[client.Imei][client] = true
log.Printf("客户端注册: IMEI=%s", client.Imei)
h.mu.Unlock()
case client := <-h.Unregister:
h.mu.Lock()
if _, ok := h.Clients[client.Imei]; ok {
if _, ok := h.Clients[client.Imei][client]; ok {
delete(h.Clients[client.Imei], client)
close(client.Send)
if len(h.Clients[client.Imei]) == 0 {
delete(h.Clients, client.Imei)
}
log.Printf("客户端注销: IMEI=%s", client.Imei)
}
}
h.mu.Unlock()
case broadcast := <-h.Broadcast:
h.mu.RLock()
log.Printf("收到广播消息: IMEI=%s, Data=%s", broadcast.IMEI, broadcast.Data)
if clients, ok := h.Clients[broadcast.IMEI]; ok {
log.Printf("找到目标客户端组: IMEI=%s, 客户端数=%d", broadcast.IMEI, len(clients))
for client := range clients {
select {
case client.Send <- broadcast:
log.Printf("消息已发送到客户端: IMEI=%s", client.Imei)
default:
log.Printf("发送失败,关闭客户端: IMEI=%s", client.Imei)
close(client.Send)
delete(clients, client)
}
}
} else {
log.Printf("未找到目标客户端组: IMEI=%s", broadcast.IMEI)
}
h.mu.RUnlock()
}
}
}