浏览代码

基础搭建1

xu 8 月之前
父节点
当前提交
fcbf6ccd02
共有 51 个文件被更改,包括 2339 次插入23 次删除
  1. 94 0
      server/api/v1/admin/device.go
  2. 71 0
      server/api/v1/admin/device_genre.go
  3. 17 0
      server/api/v1/admin/enter.go
  4. 69 0
      server/api/v1/admin/region.go
  5. 69 0
      server/api/v1/admin/tunnel.go
  6. 2 0
      server/api/v1/enter.go
  7. 6 1
      server/config.yaml
  8. 2 2
      server/config/config.go
  9. 1 1
      server/config/gorm_mssql.go
  10. 8 0
      server/config/mqtt.go
  11. 54 0
      server/dao/device.go
  12. 29 0
      server/dao/device_genre.go
  13. 35 0
      server/dao/region.go
  14. 1 0
      server/dao/sys_user.go
  15. 30 0
      server/dao/tunnel.go
  16. 18 7
      server/go.mod
  17. 568 0
      server/go.sum
  18. 7 3
      server/initialize/gorm.go
  19. 6 0
      server/initialize/router.go
  20. 8 0
      server/main.go
  21. 12 0
      server/model/common/request/common.go
  22. 0 1
      server/plugin/plugin-tool/utils/check.go
  23. 24 0
      server/router/admin/device.go
  24. 23 0
      server/router/admin/device_genre.go
  25. 8 0
      server/router/admin/enter.go
  26. 23 0
      server/router/admin/region.go
  27. 23 0
      server/router/admin/tunnel.go
  28. 2 0
      server/router/enter.go
  29. 30 0
      server/service/admin/device.go
  30. 21 0
      server/service/admin/device_genre.go
  31. 8 0
      server/service/admin/enter.go
  32. 118 0
      server/service/admin/mqtt.go
  33. 21 0
      server/service/admin/region.go
  34. 21 0
      server/service/admin/tunnel.go
  35. 2 0
      server/service/enter.go
  36. 1 2
      server/service/system/jwt_black_list.go
  37. 1 2
      server/service/system/sys_base_menu.go
  38. 1 2
      server/service/system/sys_dictionary.go
  39. 1 2
      server/service/system/sys_menu.go
  40. 56 0
      server/utils/logger/initLog.go
  41. 164 0
      server/utils/mqtt/mqtt.go
  42. 125 0
      server/utils/mqtt/mqttclient.go
  43. 51 0
      server/utils/mqtt/mqttmgr.go
  44. 46 0
      server/utils/mqtt/publish.go
  45. 156 0
      server/utils/mqtt/queue.go
  46. 124 0
      server/utils/mqtt/router.go
  47. 99 0
      server/utils/mqtt/subscribe.go
  48. 8 0
      server/utils/protocol/protocol.go
  49. 16 0
      web/src/api/device.js
  50. 36 0
      web/src/view/admin/device/device.vue
  51. 23 0
      web/src/view/admin/index.vue

+ 94 - 0
server/api/v1/admin/device.go

@@ -0,0 +1,94 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/dao"
+	"server/model/common/request"
+	"server/model/common/response"
+	"server/utils/logger"
+	"strconv"
+)
+
+type DeviceApi struct{}
+
+func (da *DeviceApi) QueryAllDevices(c *gin.Context) {
+	devices, err := deviceService.QueryAllDevices()
+	if err != nil {
+		logger.Get().Error("QueryAllDevices ---- " + err.Error())
+		response.FailWithMessage("设备获取设备", c)
+		return
+	}
+	response.OkWithData(devices, c)
+}
+
+func (da *DeviceApi) QueryDeviceList(c *gin.Context) {
+	var info request.DeviceSearch
+	err := c.ShouldBindJSON(&info)
+	if err != nil {
+		logger.Get().Error("QueryDeviceList ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	list, total, err := deviceService.QueryDeviceList(info)
+	if err != nil {
+		logger.Get().Error("QueryDeviceList ---- " + err.Error())
+		response.FailWithMessage("设备获取列表错误", c)
+		return
+	}
+	response.OkWithDetailed(response.PageResult{
+		List:     list,
+		Total:    total,
+		Page:     info.PageInfo.Page,
+		PageSize: info.PageInfo.PageSize,
+	}, "获取成功", c)
+}
+
+func (da *DeviceApi) CreateDevice(c *gin.Context) {
+	var device dao.Device
+	err := c.ShouldBindJSON(&device)
+	if err != nil {
+		logger.Get().Error("CreateDevice ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err = deviceService.CreateDevice(device)
+	if err != nil {
+		logger.Get().Error("CreateDevice ---- " + err.Error())
+		response.FailWithMessage("设备创建失败", c)
+		return
+	}
+	response.OkWithMessage("创建成功", c)
+}
+
+func (da *DeviceApi) UpdateDevice(c *gin.Context) {
+	var device dao.Device
+	err := c.ShouldBindJSON(&device)
+	if err != nil {
+		logger.Get().Error("UpdateDevice ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err = deviceService.UpdateDevice(device)
+	if err != nil {
+		logger.Get().Error("UpdateDevice ---- " + err.Error())
+		response.FailWithMessage("设备更新失败", c)
+		return
+	}
+	response.OkWithMessage("更新成功", c)
+}
+
+func (da *DeviceApi) DeleteDevice(c *gin.Context) {
+	id, err := strconv.Atoi(c.Query("id"))
+	if err != nil {
+		logger.Get().Error("DeleteDevice ---- " + err.Error())
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err = deviceService.DeleteDevice(id)
+	if err != nil {
+		logger.Get().Error("DeleteDevice ---- " + err.Error())
+		response.FailWithMessage("设备删除失败", c)
+		return
+	}
+	response.OkWithMessage("删除成功", c)
+}

+ 71 - 0
server/api/v1/admin/device_genre.go

@@ -0,0 +1,71 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/dao"
+	"server/model/common/response"
+	"server/utils/logger"
+	"strconv"
+)
+
+type DeviceGenreApi struct{}
+
+func (dga *DeviceGenreApi) QueryAllDeviceGenres(c *gin.Context) {
+	deviceGenres, err := deviceGenreService.QueryAllDeviceGenres()
+	if err != nil {
+		logger.Get().Error("QueryAllDeviceGenres ---- " + err.Error())
+		response.FailWithMessage("获取设备类型失败", c)
+		return
+	}
+	response.OkWithData(deviceGenres, c)
+}
+
+func (dga *DeviceGenreApi) CreateDeviceGenre(c *gin.Context) {
+	var deviceGenre dao.DeviceGenre
+	err := c.ShouldBindJSON(&deviceGenre)
+	if err != nil {
+		logger.Get().Error("CreateDeviceGenre ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err = deviceGenreService.CreateDeviceGenre(deviceGenre)
+	if err != nil {
+		logger.Get().Error("CreateDeviceGenre ---- " + err.Error())
+		response.FailWithMessage("创建设备类型失败", c)
+		return
+	}
+	response.OkWithMessage("创建成功", c)
+}
+
+func (dga *DeviceGenreApi) UpdateDeviceGenre(c *gin.Context) {
+	var deviceGenre dao.DeviceGenre
+	err := c.ShouldBindJSON(&deviceGenre)
+	if err != nil {
+		logger.Get().Error("UpdateDeviceGenre ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err = deviceGenreService.UpdateDeviceGenre(deviceGenre)
+	if err != nil {
+		logger.Get().Error("UpdateDeviceGenre ---- " + err.Error())
+		response.FailWithMessage("更新设备类型失败", c)
+		return
+	}
+	response.OkWithMessage("更新成功", c)
+}
+
+func (dga *DeviceGenreApi) DeleteDeviceGenre(c *gin.Context) {
+	id, err := strconv.Atoi(c.Query("id"))
+	if err != nil {
+		logger.Get().Error("DeleteDeviceGenre ---- " + err.Error())
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err = deviceGenreService.DeleteDeviceGenre(id)
+	if err != nil {
+		logger.Get().Error("DeleteDeviceGenre ---- " + err.Error())
+		response.FailWithMessage("删除设备类型失败", c)
+		return
+	}
+	response.OkWithMessage("删除成功", c)
+}

+ 17 - 0
server/api/v1/admin/enter.go

@@ -0,0 +1,17 @@
+package admin
+
+import "server/service"
+
+type ApiGroup struct {
+	DeviceApi
+	DeviceGenreApi
+	RegionApi
+	TunnelApi
+}
+
+var (
+	deviceService      = service.ServiceGroupApp.AdminServiceGroup.DeviceService
+	deviceGenreService = service.ServiceGroupApp.AdminServiceGroup.DeviceGenreService
+	regionService      = service.ServiceGroupApp.AdminServiceGroup.RegionService
+	tunnelService      = service.ServiceGroupApp.AdminServiceGroup.TunnelService
+)

+ 69 - 0
server/api/v1/admin/region.go

@@ -0,0 +1,69 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/dao"
+	"server/model/common/response"
+	"server/utils/logger"
+	"strconv"
+)
+
+type RegionApi struct{}
+
+func (ra *RegionApi) QueryAllRegions(c *gin.Context) {
+	regions, err := regionService.QueryAllRegions()
+	if err != nil {
+		logger.Get().Error("QueryAllRegions ---- " + err.Error())
+		response.FailWithMessage("获取地区失败", c)
+		return
+	}
+	response.OkWithData(regions, c)
+}
+
+func (ra *RegionApi) CreateRegion(c *gin.Context) {
+	var region dao.Region
+	if err := c.ShouldBindJSON(&region); err != nil {
+		logger.Get().Error("CreateRegions ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err := regionService.CreateRegion(region)
+	if err != nil {
+		logger.Get().Error("CreateRegions ---- " + err.Error())
+		response.FailWithMessage("创建地区失败", c)
+		return
+	}
+	response.OkWithMessage("创建地区成功", c)
+}
+
+func (ra *RegionApi) UpdateRegion(c *gin.Context) {
+	var region dao.Region
+	if err := c.ShouldBindJSON(&region); err != nil {
+		logger.Get().Error("UpdateRegions ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err := regionService.UpdateRegion(region)
+	if err != nil {
+		logger.Get().Error("UpdateRegions ---- " + err.Error())
+		response.FailWithMessage("更新地区失败", c)
+		return
+	}
+	response.OkWithMessage("更新地区成功", c)
+}
+
+func (ra *RegionApi) DeleteRegion(c *gin.Context) {
+	id, err := strconv.Atoi(c.Query("id"))
+	if err != nil {
+		logger.Get().Error("DeleteRegions ---- " + err.Error())
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err = regionService.DeleteRegion(id)
+	if err != nil {
+		logger.Get().Error("DeleteRegions ---- " + err.Error())
+		response.FailWithMessage("删除地区失败", c)
+		return
+	}
+	response.OkWithMessage("删除地区成功", c)
+}

+ 69 - 0
server/api/v1/admin/tunnel.go

@@ -0,0 +1,69 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	"server/dao"
+	"server/model/common/response"
+	"server/utils/logger"
+	"strconv"
+)
+
+type TunnelApi struct{}
+
+func (ta *TunnelApi) QueryAllTunnels(c *gin.Context) {
+	tunnels, err := tunnelService.QueryAllTunnels()
+	if err != nil {
+		logger.Get().Error("QueryAllTunnels ---- " + err.Error())
+		response.FailWithMessage("获取隧道失败", c)
+		return
+	}
+	response.OkWithData(tunnels, c)
+}
+
+func (ta *TunnelApi) CreateTunnel(c *gin.Context) {
+	var tunnel dao.Tunnel
+	if err := c.ShouldBindJSON(&tunnel); err != nil {
+		logger.Get().Error("CreateTunnel ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err := tunnelService.CreateTunnel(tunnel)
+	if err != nil {
+		logger.Get().Error("CreateTunnel ---- " + err.Error())
+		response.FailWithMessage("创建隧道失败", c)
+		return
+	}
+	response.OkWithMessage("创建隧道成功", c)
+}
+
+func (ta *TunnelApi) UpdateTunnel(c *gin.Context) {
+	var tunnel dao.Tunnel
+	if err := c.ShouldBindJSON(&tunnel); err != nil {
+		logger.Get().Error("UpdateTunnel ---- " + err.Error())
+		response.FailWithMessage("参数解析失败", c)
+		return
+	}
+	err := tunnelService.UpdateTunnel(tunnel)
+	if err != nil {
+		logger.Get().Error("UpdateTunnel ---- " + err.Error())
+		response.FailWithMessage("更新隧道失败", c)
+		return
+	}
+	response.OkWithMessage("更新隧道成功", c)
+}
+
+func (ta *TunnelApi) DeleteTunnel(c *gin.Context) {
+	id, err := strconv.Atoi(c.Query("id"))
+	if err != nil {
+		logger.Get().Error("DeleteTunnel ---- " + err.Error())
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err = tunnelService.DeleteTunnel(id)
+	if err != nil {
+		logger.Get().Error("DeleteTunnel ---- " + err.Error())
+		response.FailWithMessage("删除隧道失败", c)
+		return
+	}
+	response.OkWithMessage("删除隧道成功", c)
+}

+ 2 - 0
server/api/v1/enter.go

@@ -1,6 +1,7 @@
 package v1
 
 import (
+	"server/api/v1/admin"
 	"server/api/v1/example"
 	"server/api/v1/system"
 )
@@ -8,6 +9,7 @@ import (
 type ApiGroup struct {
 	SystemApiGroup  system.ApiGroup
 	ExampleApiGroup example.ApiGroup
+	AdminApiGroup   admin.ApiGroup
 }
 
 var ApiGroupApp = new(ApiGroup)

+ 6 - 1
server/config.yaml

@@ -31,7 +31,7 @@ aws-s3:
     s3-force-path-style: false
     disable-ssl: false
 captcha:
-    key-long: 6
+    key-long: 4
     img-width: 240
     img-height: 80
     open-captcha: 0
@@ -219,3 +219,8 @@ zap:
     max-age: 0
     show-line: true
     log-in-console: true
+mqtt:
+    server: "tcp://106.52.134.22:1883"
+    id: "mini_program_service_v20230426"
+    user: "admin"
+    password: "admin"

+ 2 - 2
server/config/config.go

@@ -22,8 +22,8 @@ type Server struct {
 	HuaWeiObs  HuaWeiObs  `mapstructure:"hua-wei-obs" json:"hua-wei-obs" yaml:"hua-wei-obs"`
 	TencentCOS TencentCOS `mapstructure:"tencent-cos" json:"tencent-cos" yaml:"tencent-cos"`
 	AwsS3      AwsS3      `mapstructure:"aws-s3" json:"aws-s3" yaml:"aws-s3"`
-
-	Excel Excel `mapstructure:"excel" json:"excel" yaml:"excel"`
+	Mqtt       Mqtt       `mapstructure:"mqtt" json:"mqtt" yaml:"mqtt"`
+	Excel      Excel      `mapstructure:"excel" json:"excel" yaml:"excel"`
 
 	// 跨域配置
 	Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"`

+ 1 - 1
server/config/gorm_mssql.go

@@ -4,7 +4,7 @@ type Mssql struct {
 	GeneralDB `yaml:",inline" mapstructure:",squash"`
 }
 
-//dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
+// dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
 func (m *Mssql) Dsn() string {
 	return "sqlserver://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "?database=" + m.Dbname + "&encrypt=disable"
 }

+ 8 - 0
server/config/mqtt.go

@@ -0,0 +1,8 @@
+package config
+
+type Mqtt struct {
+	Server   string `mapstructure:"server" json:"server"  yaml:"server"`
+	Id       string `mapstructure:"id" json:"id"  yaml:"id"`
+	User     string `mapstructure:"user" json:"user"  yaml:"user"`
+	Password string `mapstructure:"password" json:"password"  yaml:"password"`
+}

+ 54 - 0
server/dao/device.go

@@ -0,0 +1,54 @@
+package dao
+
+import "server/global"
+
+type Device struct {
+	global.GVA_MODEL
+	Sn          string      `json:"sn" gorm:"comment:设备sn"`
+	Name        string      `json:"name" gorm:"comment:名称"`
+	Genre       int         `json:"genre" gorm:"index;comment:类型id"`
+	DeviceGenre DeviceGenre `json:"deviceGenre" gorm:"foreignKey:Genre;"`
+}
+
+func (Device) TableName() string {
+	return "device"
+}
+
+func QueryAllDevices() (devices []Device, err error) {
+	err = global.GVA_DB.Model(Device{}).Find(&devices).Error
+	return
+}
+
+func QueryDeviceList(sn, name string, genre, limit, offset int) (devices []Device, total int64, err error) {
+	db := global.GVA_DB.Model(Device{})
+
+	if sn != "" {
+		db = db.Where("sn LIKE ?", "%"+sn+"%")
+	}
+
+	if name != "" {
+		db = db.Where("name LIKE ?", "%"+name+"%")
+	}
+
+	if genre != 0 {
+		db = db.Where("genre = ?", genre)
+	}
+	err = db.Count(&total).Error
+	if err != nil {
+		return
+	}
+	err = db.Limit(limit).Offset(offset).Preload("DeviceGenre").Find(&devices).Error
+	return
+}
+
+func (d Device) CreateDevice() error {
+	return global.GVA_DB.Create(&d).Error
+}
+
+func (d Device) UpdateDevice() error {
+	return global.GVA_DB.Where("id = ?", d.ID).Updates(&d).Error
+}
+
+func DeleteDevice(id int) error {
+	return global.GVA_DB.Unscoped().Where("id = ?", id).Delete(&Device{}).Error
+}

+ 29 - 0
server/dao/device_genre.go

@@ -0,0 +1,29 @@
+package dao
+
+import "server/global"
+
+type DeviceGenre struct {
+	global.GVA_MODEL
+	Name string `json:"name" gorm:"comment:类型名称"`
+}
+
+func (DeviceGenre) TableName() string {
+	return "device_genre"
+}
+
+func QueryAllDeviceGenres() (genres []DeviceGenre, err error) {
+	err = global.GVA_DB.Create(DeviceGenre{}).Find(&genres).Error
+	return
+}
+
+func (d DeviceGenre) CreateDeviceGenre() error {
+	return global.GVA_DB.Create(&d).Error
+}
+
+func (d DeviceGenre) UpdateDeviceGenre() error {
+	return global.GVA_DB.Where("id = ?", d.ID).Updates(&d).Error
+}
+
+func DeleteDeviceGenre(id int) error {
+	return global.GVA_DB.Unscoped().Where("id = ?", id).Delete(&DeviceGenre{}).Error
+}

+ 35 - 0
server/dao/region.go

@@ -0,0 +1,35 @@
+package dao
+
+import "server/global"
+
+type Region struct {
+	global.GVA_MODEL
+	Name    string   `json:"name" form:"name" gorm:"comment:名称;column:name"`
+	Tunnels []Tunnel `json:"tunnels" form:"tunnels" gorm:"-"`
+}
+
+func (Region) TableName() string {
+	return "region"
+}
+
+func QueryAllRegions() (regions []Region, err error) {
+	err = global.GVA_DB.Preload("Tunnels").Find(&regions).Error
+	return regions, err
+}
+
+func QueryRegionByID(id int) (region Region, err error) {
+	err = global.GVA_DB.Where("id = ?", id).Preload("Tunnels").First(&region).Error
+	return region, err
+}
+
+func (r Region) CreateRegion() error {
+	return global.GVA_DB.Create(&r).Error
+}
+
+func (r Region) UpdateRegion() error {
+	return global.GVA_DB.Where("id = ?", r.ID).Updates(&r).Error
+}
+
+func DeleteRegion(id int) error {
+	return global.GVA_DB.Unscoped().Where("id = ?", id).Delete(&Region{}).Error
+}

+ 1 - 0
server/dao/sys_user.go

@@ -19,6 +19,7 @@ type SysUser struct {
 	ActiveColor string       `json:"activeColor" gorm:"default:#1890ff;comment:活跃颜色"`                                      // 活跃颜色
 	AuthorityId uint         `json:"authorityId" gorm:"default:888;comment:用户角色ID"`                                        // 用户角色ID
 	Authority   SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"`
+	Tunnels     []Tunnel     `json:"tunnels" gorm:"many2many:user_tunnel"`
 	Phone       string       `json:"phone"  gorm:"comment:用户手机号"`                     // 用户手机号
 	Email       string       `json:"email"  gorm:"comment:用户邮箱"`                      // 用户邮箱
 	Enable      int          `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结

+ 30 - 0
server/dao/tunnel.go

@@ -0,0 +1,30 @@
+package dao
+
+import "server/global"
+
+type Tunnel struct {
+	global.GVA_MODEL
+	Name  string    `json:"name" gorm:"comment:名称"`
+	Users []SysUser `json:"users" gorm:"many2many:user_tunnel"`
+}
+
+func (Tunnel) TableName() string {
+	return "tunnel"
+}
+
+func QueryAllTunnels() (tunnels []Tunnel, err error) {
+	err = global.GVA_DB.Find(&tunnels).Error
+	return tunnels, err
+}
+
+func (t Tunnel) CreateTunnel() error {
+	return global.GVA_DB.Create(&t).Error
+}
+
+func (t Tunnel) UpdateTunnel() error {
+	return global.GVA_DB.Where("id = ?", t.ID).Updates(&t).Error
+}
+
+func DeleteTunnel(id int) error {
+	return global.GVA_DB.Unscoped().Where("id = ?", id).Delete(&Tunnel{}).Error
+}

+ 18 - 7
server/go.mod

@@ -31,16 +31,16 @@ require (
 	github.com/stretchr/testify v1.8.4
 	github.com/swaggo/files v1.0.1
 	github.com/swaggo/gin-swagger v1.6.0
-	github.com/swaggo/swag v1.16.2
+	github.com/swaggo/swag v1.16.4
 	github.com/tencentyun/cos-go-sdk-v5 v0.7.42
 	github.com/unrolled/secure v1.13.0
 	github.com/xuri/excelize/v2 v2.8.0
 	go.mongodb.org/mongo-driver v1.12.1
 	go.uber.org/automaxprocs v1.5.3
 	go.uber.org/zap v1.24.0
-	golang.org/x/crypto v0.22.0
-	golang.org/x/sync v0.5.0
-	golang.org/x/text v0.14.0
+	golang.org/x/crypto v0.25.0
+	golang.org/x/sync v0.7.0
+	golang.org/x/text v0.16.0
 	gorm.io/driver/mysql v1.5.6
 	gorm.io/driver/postgres v1.5.7
 	gorm.io/driver/sqlserver v1.5.1
@@ -55,9 +55,11 @@ require (
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 	github.com/clbanning/mxj v1.8.4 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.2 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/glebarez/go-sqlite v1.21.1 // indirect
@@ -76,6 +78,7 @@ require (
 	github.com/golang/snappy v0.0.1 // indirect
 	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/google/uuid v1.3.0 // indirect
+	github.com/gorilla/websocket v1.5.3 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/jackc/pgpassfile v1.0.0 // indirect
 	github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
@@ -89,6 +92,8 @@ require (
 	github.com/klauspost/compress v1.13.6 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
 	github.com/leodido/go-urn v1.2.4 // indirect
+	github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
+	github.com/lestrrat-go/strftime v1.1.0 // indirect
 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
 	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
@@ -106,7 +111,10 @@ require (
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/richardlehane/mscfb v1.0.4 // indirect
 	github.com/richardlehane/msoleps v1.0.3 // indirect
+	github.com/russross/blackfriday/v2 v2.0.1 // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
+	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
+	github.com/sirupsen/logrus v1.9.3 // indirect
 	github.com/spf13/afero v1.9.5 // indirect
 	github.com/spf13/cast v1.5.1 // indirect
 	github.com/spf13/jwalterweatherman v1.1.0 // indirect
@@ -116,6 +124,7 @@ require (
 	github.com/tklauser/numcpus v0.6.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.11 // indirect
+	github.com/urfave/cli/v2 v2.3.0 // indirect
 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
 	github.com/xdg-go/scram v1.1.2 // indirect
 	github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -128,16 +137,18 @@ require (
 	go.uber.org/multierr v1.8.0 // indirect
 	golang.org/x/arch v0.3.0 // indirect
 	golang.org/x/image v0.15.0 // indirect
-	golang.org/x/net v0.21.0 // indirect
-	golang.org/x/sys v0.19.0 // indirect
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
 	golang.org/x/time v0.1.0 // indirect
-	golang.org/x/tools v0.16.1 // indirect
+	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
 	google.golang.org/protobuf v1.33.0 // indirect
 	gopkg.in/ini.v1 v1.67.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	gorm.io/plugin/dbresolver v1.4.1 // indirect
 	modernc.org/libc v1.24.1 // indirect
 	modernc.org/mathutil v1.5.0 // indirect
 	modernc.org/memory v1.6.0 // indirect
 	modernc.org/sqlite v1.23.0 // indirect
+	sigs.k8s.io/yaml v1.3.0 // indirect
 )

文件差异内容过多而无法显示
+ 568 - 0
server/go.sum


+ 7 - 3
server/initialize/gorm.go

@@ -1,11 +1,10 @@
 package initialize
 
 import (
-	"os"
-	"server/dao"
-
 	"go.uber.org/zap"
 	"gorm.io/gorm"
+	"os"
+	"server/dao"
 	"server/global"
 	"server/model/example"
 )
@@ -46,6 +45,11 @@ func RegisterTables() {
 		example.ExaFile{},
 		example.ExaFileChunk{},
 		dao.ExaFileUploadAndDownload{},
+
+		dao.Region{},
+		dao.Tunnel{},
+		dao.Device{},
+		dao.DeviceGenre{},
 	)
 	if err != nil {
 		global.GVA_LOG.Error("register table failed", zap.Error(err))

+ 6 - 0
server/initialize/router.go

@@ -42,6 +42,7 @@ func Routers() *gin.Engine {
 	InstallPlugin(Router) // 安装插件
 	systemRouter := router.RouterGroupApp.System
 	exampleRouter := router.RouterGroupApp.Example
+	adminRouter := router.RouterGroupApp.Admin
 	// 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的
 	// VUE_APP_BASE_API = /
 	// VUE_APP_BASE_PATH = http://localhost
@@ -85,6 +86,11 @@ func Routers() *gin.Engine {
 		systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup)  // 字典详情管理
 
 		exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup)
+
+		adminRouter.InitDeviceRouter(PrivateGroup)      //设备
+		adminRouter.InitDeviceGenreRouter(PrivateGroup) //设备类型
+		adminRouter.InitRegionRouter(PrivateGroup)      //地区
+		adminRouter.InitTunnelRouter(PrivateGroup)      // 隧道
 	}
 
 	global.GVA_LOG.Info("router register success")

+ 8 - 0
server/main.go

@@ -3,6 +3,9 @@ package main
 import (
 	_ "go.uber.org/automaxprocs"
 	"go.uber.org/zap"
+	"log"
+	"server/service/admin"
+	"server/utils/logger"
 
 	"server/core"
 	"server/global"
@@ -23,12 +26,17 @@ import (
 // @BasePath                    /
 func main() {
 	global.GVA_VP = core.Viper() // 初始化Viper
+	logPath := "./logs/app.log"
+	if err := logger.Initialize(logPath); err != nil {
+		log.Fatalf("Failed to initialize logger: %v", err)
+	}
 	initialize.OtherInit()
 	global.GVA_LOG = core.Zap() // 初始化zap日志库
 	zap.ReplaceGlobals(global.GVA_LOG)
 	global.GVA_DB = initialize.Gorm() // gorm连接数据库
 	initialize.Timer()
 	initialize.DBList()
+	admin.InitMqtt() //mqtt
 	if global.GVA_DB != nil {
 		initialize.RegisterTables() // 初始化表
 		// 程序结束前关闭数据库链接

+ 12 - 0
server/model/common/request/common.go

@@ -25,4 +25,16 @@ type GetAuthorityId struct {
 	AuthorityId uint `json:"authorityId" form:"authorityId"` // 角色ID
 }
 
+type DeviceSearch struct {
+	PageInfo PageInfo `json:"pageInfo" form:"pageInfo"` // 分页信息
+	Sn       string   `json:"sn" form:"sn"`             //设备sn
+	Name     string   `json:"name" form:"name"`         // 设备名称
+	Genre    int      `json:"genre" form:"genre"`       //设备类型
+}
+
+type TunnelSearch struct {
+	PageInfo PageInfo `json:"pageInfo" form:"pageInfo"` // 分页信息
+	Name     string   `json:"name" form:"name"`         //隧道名称
+}
+
 type Empty struct{}

+ 0 - 1
server/plugin/plugin-tool/utils/check.go

@@ -3,7 +3,6 @@ package utils
 import (
 	"fmt"
 	"server/dao"
-
 	"server/global"
 )
 

+ 24 - 0
server/router/admin/device.go

@@ -0,0 +1,24 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type DeviceRouter struct{}
+
+func (dr *DeviceRouter) InitDeviceRouter(Router *gin.RouterGroup) {
+	deviceRouter := Router.Group("device").Use(middleware.OperationRecord())
+	deviceRouterWithoutRecord := Router.Group("device")
+	deviceRouterApi := v1.ApiGroupApp.AdminApiGroup.DeviceApi
+	{
+		deviceRouter.POST("queryDeviceList", deviceRouterApi.QueryDeviceList)
+		deviceRouter.POST("createDevice", deviceRouterApi.CreateDevice)
+		deviceRouter.PUT("updateDevice", deviceRouterApi.UpdateDevice)
+		deviceRouter.DELETE("deleteDevice", deviceRouterApi.DeleteDevice)
+	}
+	{
+		deviceRouterWithoutRecord.GET("queryAllDevices", deviceRouterApi.QueryAllDevices)
+	}
+}

+ 23 - 0
server/router/admin/device_genre.go

@@ -0,0 +1,23 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type DeviceGenreRouter struct{}
+
+func (dgr *DeviceGenreRouter) InitDeviceGenreRouter(Router *gin.RouterGroup) {
+	deviceGenreRouter := Router.Group("deviceGenre").Use(middleware.OperationRecord())
+	deviceGenreRouterWithoutRecord := Router.Group("deviceGenre")
+	deviceGenreRouterApi := v1.ApiGroupApp.AdminApiGroup.DeviceGenreApi
+	{
+		deviceGenreRouter.POST("createDeviceGenre", deviceGenreRouterApi.CreateDeviceGenre)
+		deviceGenreRouter.PUT("updateDeviceGenre", deviceGenreRouterApi.UpdateDeviceGenre)
+		deviceGenreRouter.DELETE("deleteDeviceGenre", deviceGenreRouterApi.DeleteDeviceGenre)
+	}
+	{
+		deviceGenreRouterWithoutRecord.GET("queryAllDeviceGenres", deviceGenreRouterApi.QueryAllDeviceGenres)
+	}
+}

+ 8 - 0
server/router/admin/enter.go

@@ -0,0 +1,8 @@
+package admin
+
+type RouterGroup struct {
+	DeviceRouter
+	DeviceGenreRouter
+	RegionRouter
+	TunnelRouter
+}

+ 23 - 0
server/router/admin/region.go

@@ -0,0 +1,23 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type RegionRouter struct{}
+
+func (rr *RegionRouter) InitRegionRouter(Router *gin.RouterGroup) {
+	regionRouter := Router.Group("region").Use(middleware.OperationRecord())
+	regionRouterWithoutRecord := Router.Group("region")
+	regionRouterApi := v1.ApiGroupApp.AdminApiGroup.RegionApi
+	{
+		regionRouter.POST("createRegion", regionRouterApi.CreateRegion)
+		regionRouter.PUT("updateRegion", regionRouterApi.UpdateRegion)
+		regionRouter.DELETE("deleteRegion", regionRouterApi.DeleteRegion)
+	}
+	{
+		regionRouterWithoutRecord.GET("queryAllRegions", regionRouterApi.QueryAllRegions)
+	}
+}

+ 23 - 0
server/router/admin/tunnel.go

@@ -0,0 +1,23 @@
+package admin
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type TunnelRouter struct{}
+
+func (tr *TunnelRouter) InitTunnelRouter(Router *gin.RouterGroup) {
+	tunnelRouter := Router.Group("tunnel").Use(middleware.OperationRecord())
+	tunnelRouterWithoutRecord := Router.Group("tunnel")
+	tunnelRouterApi := v1.ApiGroupApp.AdminApiGroup.TunnelApi
+	{
+		tunnelRouter.POST("createTunnel", tunnelRouterApi.CreateTunnel)
+		tunnelRouter.PUT("updateTunnel", tunnelRouterApi.UpdateTunnel)
+		tunnelRouter.DELETE("deleteTunnel", tunnelRouterApi.DeleteTunnel)
+	}
+	{
+		tunnelRouterWithoutRecord.GET("queryAllTunnels", tunnelRouterApi.QueryAllTunnels)
+	}
+}

+ 2 - 0
server/router/enter.go

@@ -1,6 +1,7 @@
 package router
 
 import (
+	"server/router/admin"
 	"server/router/example"
 	"server/router/system"
 )
@@ -8,6 +9,7 @@ import (
 type RouterGroup struct {
 	System  system.RouterGroup
 	Example example.RouterGroup
+	Admin   admin.RouterGroup
 }
 
 var RouterGroupApp = new(RouterGroup)

+ 30 - 0
server/service/admin/device.go

@@ -0,0 +1,30 @@
+package admin
+
+import (
+	"server/dao"
+	"server/model/common/request"
+)
+
+type DeviceService struct{}
+
+func (ds *DeviceService) QueryAllDevices() ([]dao.Device, error) {
+	return dao.QueryAllDevices()
+}
+
+func (ds *DeviceService) QueryDeviceList(info request.DeviceSearch) ([]dao.Device, int64, error) {
+	limit := info.PageInfo.PageSize
+	offset := info.PageInfo.PageSize * (info.PageInfo.Page - 1)
+	return dao.QueryDeviceList(info.Sn, info.Name, info.Genre, limit, offset)
+}
+
+func (ds *DeviceService) CreateDevice(device dao.Device) error {
+	return device.CreateDevice()
+}
+
+func (ds *DeviceService) UpdateDevice(device dao.Device) error {
+	return device.UpdateDevice()
+}
+
+func (ds *DeviceService) DeleteDevice(id int) error {
+	return dao.DeleteDevice(id)
+}

+ 21 - 0
server/service/admin/device_genre.go

@@ -0,0 +1,21 @@
+package admin
+
+import "server/dao"
+
+type DeviceGenreService struct{}
+
+func (dgs *DeviceGenreService) QueryAllDeviceGenres() ([]dao.DeviceGenre, error) {
+	return dao.QueryAllDeviceGenres()
+}
+
+func (dgs *DeviceGenreService) CreateDeviceGenre(genre dao.DeviceGenre) error {
+	return genre.CreateDeviceGenre()
+}
+
+func (dgs *DeviceGenreService) UpdateDeviceGenre(genre dao.DeviceGenre) error {
+	return genre.UpdateDeviceGenre()
+}
+
+func (dgs *DeviceGenreService) DeleteDeviceGenre(id int) error {
+	return dao.DeleteDeviceGenre(id)
+}

+ 8 - 0
server/service/admin/enter.go

@@ -0,0 +1,8 @@
+package admin
+
+type ServiceGroup struct {
+	DeviceService
+	DeviceGenreService
+	RegionService
+	TunnelService
+}

+ 118 - 0
server/service/admin/mqtt.go

@@ -0,0 +1,118 @@
+package admin
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"runtime"
+	"server/utils/logger"
+	"server/utils/mqtt"
+	"server/utils/protocol"
+	"strings"
+	"sync"
+	"time"
+)
+
+func InitMqtt() {
+	MqttService = GetHandler()
+	MqttService.SubscribeTopics()
+	go MqttService.Handler()
+}
+
+var MqttService *MqttHandler
+
+var timeoutReg = regexp.MustCompile("Client .* has exceeded timeout")
+var connectReg = regexp.MustCompile(`New client connected from .* as .*\(`)
+var disconnectReg = regexp.MustCompile("Client mqttx_893e4b7d disconnected")
+
+type MqttHandler struct {
+	queue *mqtt.MlQueue
+}
+
+var _handlerOnce sync.Once
+var _handlerSingle *MqttHandler
+
+func GetHandler() *MqttHandler {
+	_handlerOnce.Do(func() {
+		_handlerSingle = &MqttHandler{
+			queue: mqtt.NewQueue(10000),
+		}
+	})
+	return _handlerSingle
+}
+
+func (o *MqttHandler) SubscribeTopics() {
+	mqtt.GetMQTTMgr().Subscribe("mini/#", mqtt.AtLeastOnce, o.HandlerData)
+	mqtt.GetMQTTMgr().Subscribe("$SYS/broker/log/N", mqtt.AtLeastOnce, o.HandlerData)
+}
+
+func (o *MqttHandler) HandlerData(m mqtt.Message) {
+	for {
+		ok, cnt := o.queue.Put(&m)
+		if ok {
+			break
+		} else {
+			logger.Get().Error("HandlerData:查询队列失败,队列消息数量:%d", cnt)
+			runtime.Gosched()
+		}
+	}
+}
+
+func (o *MqttHandler) Handler() interface{} {
+	defer func() {
+		if err := recover(); err != nil {
+			go GetHandler().Handler()
+			//logger.Logger.Errorf("MqttHandler.Handler:发生异常:%s", string(debug.Stack()))
+		}
+	}()
+	for {
+		msg, ok, quantity := o.queue.Get()
+		if !ok {
+			time.Sleep(10 * time.Millisecond)
+			continue
+		} else if quantity > 1000 {
+			logger.Get().Warnf("数据队列累积过多,请注意优化,当前队列条数:%d", quantity)
+		}
+		m, ok := msg.(*mqtt.Message)
+		if !ok {
+			continue
+		}
+
+		_, topic, err := parseTopic(m.Topic())
+		if err != nil {
+			//logger.Logger.Errorf("parseTopic err")
+			continue
+		}
+
+		switch topic {
+		case protocol.TopicSwitchControlAck:
+
+		case protocol.TopicSwitchControlDg:
+		case protocol.TopicIlluminance:
+
+			logger.Get().Debugf("MqttHandler Illuminance = %s", m.PayloadString())
+			//TODO: 更新光照
+			//protocol.SetCurrentIlluminance(m.PayloadString())
+		}
+
+	}
+}
+
+func (o *MqttHandler) Publish(topic string, data interface{}) error {
+	return mqtt.GetMQTTMgr().Publish(topic, data, mqtt.AtLeastOnce)
+}
+
+func (o *MqttHandler) GetTopic(deviceSn, protocol string) string {
+	return fmt.Sprintf("mini/%s/%s", deviceSn, protocol)
+}
+
+// parseTopic 获取设备SN, topic
+// "mini/*****/switch_control/ack"
+func parseTopic(topic string) (string, string, error) {
+	strList := strings.Split(topic, "/")
+	if len(strList) < 4 {
+		return "", "", errors.New("不支持的topic")
+	}
+	topic = strings.Join(strList[2:], "/")
+	return strList[1], topic, nil
+}

+ 21 - 0
server/service/admin/region.go

@@ -0,0 +1,21 @@
+package admin
+
+import "server/dao"
+
+type RegionService struct{}
+
+func (rs *RegionService) QueryAllRegions() ([]dao.Region, error) {
+	return dao.QueryAllRegions()
+}
+
+func (rs *RegionService) CreateRegion(region dao.Region) error {
+	return region.CreateRegion()
+}
+
+func (rs *RegionService) UpdateRegion(region dao.Region) error {
+	return region.UpdateRegion()
+}
+
+func (rs *RegionService) DeleteRegion(id int) error {
+	return dao.DeleteRegion(id)
+}

+ 21 - 0
server/service/admin/tunnel.go

@@ -0,0 +1,21 @@
+package admin
+
+import "server/dao"
+
+type TunnelService struct{}
+
+func (ts *TunnelService) QueryAllTunnels() ([]dao.Tunnel, error) {
+	return dao.QueryAllTunnels()
+}
+
+func (ts *TunnelService) CreateTunnel(tunnel dao.Tunnel) error {
+	return tunnel.CreateTunnel()
+}
+
+func (ts *TunnelService) UpdateTunnel(tunnel dao.Tunnel) error {
+	return tunnel.UpdateTunnel()
+}
+
+func (ts *TunnelService) DeleteTunnel(id int) error {
+	return dao.DeleteTunnel(id)
+}

+ 2 - 0
server/service/enter.go

@@ -1,6 +1,7 @@
 package service
 
 import (
+	"server/service/admin"
 	"server/service/example"
 	"server/service/system"
 )
@@ -8,6 +9,7 @@ import (
 type ServiceGroup struct {
 	SystemServiceGroup  system.ServiceGroup
 	ExampleServiceGroup example.ServiceGroup
+	AdminServiceGroup   admin.ServiceGroup
 }
 
 var ServiceGroupApp = new(ServiceGroup)

+ 1 - 2
server/service/system/jwt_black_list.go

@@ -2,9 +2,8 @@ package system
 
 import (
 	"context"
-	"server/dao"
-
 	"go.uber.org/zap"
+	"server/dao"
 
 	"server/global"
 	"server/utils"

+ 1 - 2
server/service/system/sys_base_menu.go

@@ -2,9 +2,8 @@ package system
 
 import (
 	"errors"
-	"server/dao"
-
 	"gorm.io/gorm"
+	"server/dao"
 	"server/global"
 )
 

+ 1 - 2
server/service/system/sys_dictionary.go

@@ -2,9 +2,8 @@ package system
 
 import (
 	"errors"
-	"server/dao"
-
 	"gorm.io/gorm"
+	"server/dao"
 	"server/global"
 )
 

+ 1 - 2
server/service/system/sys_menu.go

@@ -2,9 +2,8 @@ package system
 
 import (
 	"errors"
-	"server/dao"
-
 	"gorm.io/gorm"
+	"server/dao"
 	"server/model/common/request"
 )
 

+ 56 - 0
server/utils/logger/initLog.go

@@ -0,0 +1,56 @@
+package logger
+
+import (
+	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
+	"os"
+	"path/filepath"
+	"time"
+
+	log "github.com/sirupsen/logrus"
+)
+
+var globalLogger *log.Logger
+
+// Initialize 初始化全局 Logger 实例。
+func Initialize(logPath string) error {
+	// 创建日志目录
+	if err := os.MkdirAll(filepath.Dir(logPath), os.ModePerm); err != nil {
+		return err
+	}
+
+	// 使用 rotatelogs 进行日志轮转
+	writer, err := rotatelogs.New(
+		logPath+".%Y%m%d.log",                     // 文件名模式
+		rotatelogs.WithMaxAge(15*24*time.Hour),    // 最大保留时间3天
+		rotatelogs.WithRotationTime(24*time.Hour), // 每天轮转一次
+		rotatelogs.WithLinkName(logPath),          // 符号链接指向最新日志文件
+		rotatelogs.WithClock(rotatelogs.Local),    // 使用本地时区
+	)
+	if err != nil {
+		return err
+	}
+
+	// 设置日志格式为JSON
+	formatter := &log.JSONFormatter{
+		TimestampFormat: time.RFC3339,
+	}
+
+	// 创建一个新的 Logger 实例
+	logger := log.New()
+	logger.SetFormatter(formatter)
+	logger.SetLevel(log.InfoLevel)
+	logger.SetOutput(writer) // 设置输出到 rotatelogs 的 writer
+
+	// 将全局变量设置为新创建的 Logger 实例
+	globalLogger = logger
+
+	return nil
+}
+
+// Get 获取全局 Logger 实例。
+func Get() *log.Logger {
+	if globalLogger == nil {
+		panic("Logger not initialized")
+	}
+	return globalLogger
+}

+ 164 - 0
server/utils/mqtt/mqtt.go

@@ -0,0 +1,164 @@
+package mqtt
+
+import (
+	"context"
+	"crypto/tls"
+	"errors"
+
+	paho "github.com/eclipse/paho.mqtt.golang"
+	"github.com/google/uuid"
+)
+
+type ConnHandler interface {
+	ConnectionLostHandler(err error)
+	OnConnectHandler()
+	GetWill() (topic string, payload string)
+}
+
+// Client for talking using mqtt
+type Client struct {
+	Options     ClientOptions // The options that were used to create this client
+	client      paho.Client
+	router      *router
+	connHandler ConnHandler
+}
+
+// ClientOptions is the list of options used to create a client
+type ClientOptions struct {
+	Servers  []string // The list of broker hostnames to connect to
+	ClientID string   // If left empty a uuid will automatically be generated
+	Username string   // If not set then authentication will not be used
+	Password string   // Will only be used if the username is set
+
+	AutoReconnect bool // If the client should automatically try to reconnect when the connection is lost
+}
+
+// QOS describes the quality of service of an mqtt publish
+type QOS byte
+
+const (
+	// AtMostOnce means the broker will deliver at most once to every subscriber - this means message delivery is not guaranteed
+	AtMostOnce QOS = iota
+	// AtLeastOnce means the broker will deliver a message at least once to every subscriber
+	AtLeastOnce
+	// ExactlyOnce means the broker will deliver a message exactly once to every subscriber
+	ExactlyOnce
+)
+
+var (
+	// ErrMinimumOneServer means that at least one server should be specified in the client options
+	ErrMinimumOneServer = errors.New("mqtt: at least one server needs to be specified")
+)
+
+func handle(callback MessageHandler) paho.MessageHandler {
+	return func(client paho.Client, message paho.Message) {
+		if callback != nil {
+			callback(Message{message: message})
+		}
+	}
+}
+
+// NewClient creates a new client with the specified options
+func NewClient(options ClientOptions, connhandler ConnHandler) (*Client, error) {
+	pahoOptions := paho.NewClientOptions()
+
+	// brokers
+	if options.Servers != nil && len(options.Servers) > 0 {
+		for _, server := range options.Servers {
+			pahoOptions.AddBroker(server)
+		}
+	} else {
+		return nil, ErrMinimumOneServer
+	}
+
+	// client id
+	if options.ClientID == "" {
+		options.ClientID = uuid.New().String()
+	}
+	pahoOptions.SetClientID(options.ClientID)
+
+	t := &tls.Config{
+		InsecureSkipVerify: true,
+	}
+	pahoOptions.SetTLSConfig(t)
+
+	// auth
+	if options.Username != "" {
+		pahoOptions.SetUsername(options.Username)
+		pahoOptions.SetPassword(options.Password)
+	}
+
+	// auto reconnect
+	pahoOptions.SetAutoReconnect(options.AutoReconnect)
+
+	pahoOptions.SetCleanSession(false)
+
+	var client Client
+	pahoOptions.SetConnectionLostHandler(client.ConnectionLostHandler) //断连
+	pahoOptions.SetOnConnectHandler(client.OnConnectHandler)           //连接
+	if t, m := connhandler.GetWill(); t != "" {
+		pahoOptions.SetWill(t, m, 0, false) //遗嘱消息
+	}
+
+	pahoClient := paho.NewClient(pahoOptions)
+	router := newRouter()
+	pahoClient.AddRoute("#", handle(func(message Message) {
+		routes := router.match(&message)
+		for _, route := range routes {
+			m := message
+			m.vars = route.vars(&message)
+			route.handler(m)
+		}
+	}))
+
+	client.client = pahoClient
+	client.Options = options
+	client.router = router
+	client.connHandler = connhandler
+
+	return &client, nil
+}
+
+// Connect tries to establish a connection with the mqtt servers
+func (c *Client) Connect(ctx context.Context) error {
+	// try to connect to the client
+	token := c.client.Connect()
+	return tokenWithContext(ctx, token)
+}
+
+// Connect tries to establish a connection with the mqtt servers
+func (c *Client) IsConnected() bool {
+	// try to connect to the client
+	return c.client.IsConnected()
+}
+
+// DisconnectImmediately will immediately close the connection with the mqtt servers
+func (c *Client) DisconnectImmediately() {
+	c.client.Disconnect(0)
+}
+
+func tokenWithContext(ctx context.Context, token paho.Token) error {
+	completer := make(chan error)
+
+	// TODO: This go routine will not be removed up if the ctx is cancelled or a the ctx timeout passes
+	go func() {
+		token.Wait()
+		completer <- token.Error()
+	}()
+
+	for {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case err := <-completer:
+			return err
+		}
+	}
+}
+func (c *Client) ConnectionLostHandler(client paho.Client, err error) {
+	c.connHandler.ConnectionLostHandler(err)
+}
+
+func (c *Client) OnConnectHandler(client paho.Client) {
+	c.connHandler.OnConnectHandler()
+}

+ 125 - 0
server/utils/mqtt/mqttclient.go

@@ -0,0 +1,125 @@
+package mqtt
+
+import (
+	"context"
+	"fmt"
+	"server/utils/logger"
+	"sync"
+	"time"
+)
+
+type BaseMqttOnline interface {
+	GetOnlineMsg() (string, string)
+	GetWillMsg() (string, string)
+}
+
+type EmptyMqttOnline struct {
+}
+
+func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
+	return "", ""
+}
+func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
+	return "", ""
+}
+
+type MClient struct {
+	mqtt       *Client
+	mu         sync.Mutex     //保护mapTopics
+	mapTopics  map[string]QOS //订阅的主题
+	timeout    uint           //超时时间,毫秒为单位
+	MqttOnline BaseMqttOnline //是否发布上线消息&遗嘱消息
+}
+
+func NewMqttClient(server, clientId, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MClient {
+	o := MClient{
+		mapTopics:  make(map[string]QOS),
+		timeout:    timeout,
+		MqttOnline: mqttOnline,
+	}
+	client, err := NewClient(ClientOptions{
+		Servers:       []string{server},
+		ClientID:      clientId,
+		Username:      user,
+		Password:      password,
+		AutoReconnect: true,
+	}, &o)
+	if err != nil {
+		panic(fmt.Sprintf("MQTT错误: %s", err.Error()))
+		return nil
+	}
+	o.mqtt = client
+	err = client.Connect(o.Ctx())
+	return &o
+}
+
+func (o *MClient) ConnectionLostHandler(err error) {
+	logger.Get().Errorf("MClient.ConnectionLostHandler:MQTT连接已经断开,原因:%s", err)
+}
+
+func (o *MClient) OnConnectHandler() {
+	logger.Get().Infoln("MClient.OnConnectHandler:MQTT连接成功")
+	//连接成功则订阅主题
+	for k, v := range o.mapTopics {
+		err := o.Subscribe(k, v)
+		if err != nil {
+			return
+		}
+	}
+	topic, str := o.MqttOnline.GetOnlineMsg()
+	if topic != "" {
+		err := o.PublishString(topic, str, 0)
+		if err != nil {
+			return
+		}
+	}
+}
+
+func (o *MClient) GetWill() (topic string, payload string) {
+	return o.MqttOnline.GetWillMsg()
+}
+
+func (o *MClient) Connect() error {
+	return o.mqtt.Connect(o.Ctx())
+}
+
+func (o *MClient) IsConnected() bool {
+	return o.mqtt.IsConnected()
+}
+
+func (o *MClient) Publish(topic string, payload interface{}, qos QOS) error {
+	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
+}
+func (o *MClient) PublishString(topic string, payload string, qos QOS) error {
+	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
+}
+func (o *MClient) PublishJSON(topic string, payload interface{}, qos QOS) error {
+	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
+}
+
+func (o *MClient) Subscribe(topic string, qos QOS) error {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	if _, ok := o.mapTopics[topic]; !ok {
+		o.mapTopics[topic] = qos
+	}
+	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
+}
+
+func (o *MClient) Unsubscribe(topic string) error {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	if _, ok := o.mapTopics[topic]; ok {
+		delete(o.mapTopics, topic)
+	}
+	return o.mqtt.Unsubscribe(o.Ctx(), topic)
+}
+
+func (o *MClient) Handle(topic string, handler MessageHandler) Route {
+	return o.mqtt.Handle(topic, handler)
+}
+
+func (o *MClient) Ctx() context.Context {
+	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
+	return ctx
+}

+ 51 - 0
server/utils/mqtt/mqttmgr.go

@@ -0,0 +1,51 @@
+package mqtt
+
+import (
+	"server/global"
+	"sync"
+)
+
+var _once sync.Once
+var _mgr *Mgr
+
+func GetMQTTMgr() *Mgr {
+	_once.Do(func() {
+		_mgr = _newMQTTMgr()
+	})
+	return _mgr
+}
+
+type Mgr struct {
+	Cloud *MClient
+}
+
+func _newMQTTMgr() *Mgr {
+	cfg := global.GVA_CONFIG.Mqtt
+	return &Mgr{
+		Cloud: NewMqttClient(cfg.Server,
+			cfg.Id,
+			cfg.User,
+			cfg.Password,
+			3000, &EmptyMqttOnline{}),
+	}
+
+}
+
+func (o *Mgr) Subscribe(topic string, qos QOS, handler MessageHandler) {
+	o.Cloud.Handle(topic, handler)
+	err := o.Cloud.Subscribe(topic, qos)
+	if err != nil {
+		return
+	}
+}
+
+func (o *Mgr) UnSubscribe(topic string) {
+	err := o.Cloud.Unsubscribe(topic)
+	if err != nil {
+		return
+	}
+}
+
+func (o *Mgr) Publish(topic string, payload interface{}, qos QOS) error {
+	return o.Cloud.Publish(topic, payload, qos)
+}

+ 46 - 0
server/utils/mqtt/publish.go

@@ -0,0 +1,46 @@
+package mqtt
+
+import (
+	"context"
+	"encoding/json"
+)
+
+// PublishOption are extra options when publishing a message
+type PublishOption int
+
+const (
+	// Retain tells the broker to retain a message and send it as the first message to new subscribers.
+	Retain PublishOption = iota
+)
+
+// Publish a message with a byte array payload
+func (c *Client) Publish(ctx context.Context, topic string, payload interface{}, qos QOS, options ...PublishOption) error {
+	return c.publish(ctx, topic, payload, qos, options)
+}
+
+// PublishString publishes a message with a string payload
+func (c *Client) PublishString(ctx context.Context, topic string, payload string, qos QOS, options ...PublishOption) error {
+	return c.publish(ctx, topic, []byte(payload), qos, options)
+}
+
+// PublishJSON publishes a message with the payload encoded as JSON using encoding/json
+func (c *Client) PublishJSON(ctx context.Context, topic string, payload interface{}, qos QOS, options ...PublishOption) error {
+	data, err := json.Marshal(payload)
+	if err != nil {
+		return err
+	}
+	return c.publish(ctx, topic, data, qos, options)
+}
+
+func (c *Client) publish(ctx context.Context, topic string, payload interface{}, qos QOS, options []PublishOption) error {
+	var retained = false
+	for _, option := range options {
+		switch option {
+		case Retain:
+			retained = true
+		}
+	}
+
+	token := c.client.Publish(topic, byte(qos), retained, payload)
+	return tokenWithContext(ctx, token)
+}

+ 156 - 0
server/utils/mqtt/queue.go

@@ -0,0 +1,156 @@
+package mqtt
+
+import (
+	"fmt"
+	"runtime"
+	"sync/atomic"
+)
+
+type mlCache struct {
+	putNo uint32
+	getNo uint32
+	value interface{}
+}
+
+type MlQueue struct {
+	capacity uint32
+	capMod   uint32
+	putPos   uint32
+	getPos   uint32
+	cache    []mlCache
+}
+
+func NewQueue(capacity uint32) *MlQueue {
+	q := new(MlQueue)
+	q.capacity = minQuantity(capacity)
+	q.capMod = q.capacity - 1
+	q.putPos = 0
+	q.getPos = 0
+	q.cache = make([]mlCache, q.capacity)
+	for i := range q.cache {
+		cache := &q.cache[i]
+		cache.getNo = uint32(i)
+		cache.putNo = uint32(i)
+	}
+	cache := &q.cache[0]
+	cache.getNo = q.capacity
+	cache.putNo = q.capacity
+	return q
+}
+
+func (q *MlQueue) String() string {
+	getPos := atomic.LoadUint32(&q.getPos)
+	putPos := atomic.LoadUint32(&q.putPos)
+	return fmt.Sprintf("Queue{capacity: %v, capMod: %v, putPos: %v, getPos: %v}",
+		q.capacity, q.capMod, putPos, getPos)
+}
+
+func (q *MlQueue) Capacity() uint32 {
+	return q.capacity
+}
+
+func (q *MlQueue) Quantity() uint32 {
+	var putPos, getPos uint32
+	var quantity uint32
+	getPos = atomic.LoadUint32(&q.getPos)
+	putPos = atomic.LoadUint32(&q.putPos)
+
+	if putPos >= getPos {
+		quantity = putPos - getPos
+	} else {
+		quantity = q.capMod + (putPos - getPos)
+	}
+
+	return quantity
+}
+
+func (q *MlQueue) Put(val interface{}) (ok bool, quantity uint32) {
+	var putPos, putPosNew, getPos, posCnt uint32
+	var cache *mlCache
+	capMod := q.capMod
+
+	getPos = atomic.LoadUint32(&q.getPos)
+	putPos = atomic.LoadUint32(&q.putPos)
+
+	if putPos >= getPos {
+		posCnt = putPos - getPos
+	} else {
+		posCnt = capMod + (putPos - getPos)
+	}
+
+	if posCnt >= capMod-1 {
+		runtime.Gosched()
+		return false, posCnt
+	}
+
+	putPosNew = putPos + 1
+	if !atomic.CompareAndSwapUint32(&q.putPos, putPos, putPosNew) {
+		runtime.Gosched()
+		return false, posCnt
+	}
+
+	cache = &q.cache[putPosNew&capMod]
+
+	for {
+		getNo := atomic.LoadUint32(&cache.getNo)
+		putNo := atomic.LoadUint32(&cache.putNo)
+		if putPosNew == putNo && getNo == putNo {
+			cache.value = val
+			atomic.AddUint32(&cache.putNo, q.capacity)
+			return true, posCnt + 1
+		} else {
+			runtime.Gosched()
+		}
+	}
+}
+
+func (q *MlQueue) Get() (val interface{}, ok bool, quantity uint32) {
+	var putPos, getPos, getPosNew, posCnt uint32
+	var cache *mlCache
+	capMod := q.capMod
+
+	putPos = atomic.LoadUint32(&q.putPos)
+	getPos = atomic.LoadUint32(&q.getPos)
+
+	if putPos >= getPos {
+		posCnt = putPos - getPos
+	} else {
+		posCnt = capMod + (putPos - getPos)
+	}
+
+	if posCnt < 1 {
+		runtime.Gosched()
+		return nil, false, posCnt
+	}
+
+	getPosNew = getPos + 1
+	if !atomic.CompareAndSwapUint32(&q.getPos, getPos, getPosNew) {
+		runtime.Gosched()
+		return nil, false, posCnt
+	}
+
+	cache = &q.cache[getPosNew&capMod]
+
+	for {
+		getNo := atomic.LoadUint32(&cache.getNo)
+		putNo := atomic.LoadUint32(&cache.putNo)
+		if getPosNew == getNo && getNo == putNo-q.capacity {
+			val = cache.value
+			atomic.AddUint32(&cache.getNo, q.capacity)
+			return val, true, posCnt - 1
+		} else {
+			runtime.Gosched()
+		}
+	}
+}
+
+func minQuantity(v uint32) uint32 {
+	v--
+	v |= v >> 1
+	v |= v >> 2
+	v |= v >> 4
+	v |= v >> 8
+	v |= v >> 16
+	v++
+	return v
+}

+ 124 - 0
server/utils/mqtt/router.go

@@ -0,0 +1,124 @@
+package mqtt
+
+import (
+	"github.com/google/uuid"
+	"strings"
+	"sync"
+)
+
+type router struct {
+	routes []Route
+	lock   sync.RWMutex
+}
+
+func newRouter() *router {
+	return &router{routes: []Route{}, lock: sync.RWMutex{}}
+}
+
+// Route is a receipt for listening or handling certain topic
+type Route struct {
+	router  *router
+	id      string
+	topic   string
+	handler MessageHandler
+}
+
+func newRoute(router *router, topic string, handler MessageHandler) Route {
+	return Route{router: router, id: uuid.New().String(), topic: topic, handler: handler}
+}
+
+func match(route []string, topic []string) bool {
+	if len(route) == 0 {
+		return len(topic) == 0
+	}
+
+	if len(topic) == 0 {
+		return route[0] == "#"
+	}
+
+	if route[0] == "#" {
+		return true
+	}
+
+	if (route[0] == "+") || (route[0] == topic[0]) {
+		return match(route[1:], topic[1:])
+	}
+	return false
+}
+
+func routeIncludesTopic(route, topic string) bool {
+	return match(routeSplit(route), strings.Split(topic, "/"))
+}
+
+func routeSplit(route string) []string {
+	var result []string
+	if strings.HasPrefix(route, "$share") {
+		result = strings.Split(route, "/")[2:]
+	} else {
+		result = strings.Split(route, "/")
+	}
+	return result
+}
+
+func (r *Route) match(message *Message) bool {
+	return r.topic == message.Topic() || routeIncludesTopic(r.topic, message.Topic())
+}
+
+func (r *Route) vars(message *Message) []string {
+	var vars []string
+	route := routeSplit(r.topic)
+	topic := strings.Split(message.Topic(), "/")
+
+	for i, section := range route {
+		if section == "+" {
+			if len(topic) > i {
+				vars = append(vars, topic[i])
+			}
+		} else if section == "#" {
+			if len(topic) > i {
+				vars = append(vars, topic[i:]...)
+			}
+		}
+	}
+
+	return vars
+}
+
+func (r *router) addRoute(topic string, handler MessageHandler) Route {
+	if handler != nil {
+		route := newRoute(r, topic, handler)
+		r.lock.Lock()
+		r.routes = append(r.routes, route)
+		r.lock.Unlock()
+		return route
+	}
+	return Route{router: r}
+}
+
+func (r *router) removeRoute(removeRoute *Route) {
+	r.lock.Lock()
+	for i, route := range r.routes {
+		if route.id == removeRoute.id {
+			r.routes[i] = r.routes[len(r.routes)-1]
+			r.routes = r.routes[:len(r.routes)-1]
+		}
+	}
+	r.lock.Unlock()
+}
+
+func (r *router) match(message *Message) []Route {
+	routes := []Route{}
+	r.lock.RLock()
+	for _, route := range r.routes {
+		if route.match(message) {
+			routes = append(routes, route)
+		}
+	}
+	r.lock.RUnlock()
+	return routes
+}
+
+// Stop removes this route from the router and stops matching it
+func (r *Route) Stop() {
+	r.router.removeRoute(r)
+}

+ 99 - 0
server/utils/mqtt/subscribe.go

@@ -0,0 +1,99 @@
+package mqtt
+
+import (
+	"context"
+	"encoding/json"
+
+	paho "github.com/eclipse/paho.mqtt.golang"
+)
+
+// A Message from or to the broker
+type Message struct {
+	message paho.Message
+	vars    []string
+}
+
+// A MessageHandler to handle incoming messages
+type MessageHandler func(Message)
+
+// TopicVars is a list of all the message specific matches for a wildcard in a route topic.
+// If the route would be `config/+/full` and the messages topic is `config/server_1/full` then thous would return `[]string{"server_1"}`
+func (m *Message) TopicVars() []string {
+	return m.vars
+}
+
+// Topic is the topic the message was recieved on
+func (m *Message) Topic() string {
+	return m.message.Topic()
+}
+
+// QOS is the quality of service the message was recieved with
+func (m *Message) QOS() QOS {
+	return QOS(m.message.Qos())
+}
+
+// IsDuplicate is true if this exact message has been recieved before (due to a AtLeastOnce QOS)
+func (m *Message) IsDuplicate() bool {
+	return m.message.Duplicate()
+}
+
+// Acknowledge explicitly acknowledges to a broker that the message has been recieved
+func (m *Message) Acknowledge() {
+	m.message.Ack()
+}
+
+// Payload returns the payload as a byte array
+func (m *Message) Payload() []byte {
+	return m.message.Payload()
+}
+
+// PayloadString returns the payload as a string
+func (m *Message) PayloadString() string {
+	return string(m.message.Payload())
+}
+
+// PayloadJSON unmarshal the payload into the provided interface using encoding/json and returns an error if anything fails
+func (m *Message) PayloadJSON(v interface{}) error {
+	return json.Unmarshal(m.message.Payload(), v)
+}
+
+// Handle adds a handler for a certain topic. This handler gets called if any message arrives that matches the topic.
+// Also returns a route that can be used to unsubscribe. Does not automatically subscribe.
+func (c *Client) Handle(topic string, handler MessageHandler) Route {
+	return c.router.addRoute(topic, handler)
+}
+
+// Listen returns a stream of messages that match the topic.
+// Also returns a route that can be used to unsubscribe. Does not automatically subscribe.
+func (c *Client) Listen(topic string) (chan Message, Route) {
+	queue := make(chan Message)
+	route := c.router.addRoute(topic, func(message Message) {
+		queue <- message
+	})
+	return queue, route
+}
+
+// Subscribe subscribes to a certain topic and errors if this fails.
+func (c *Client) Subscribe(ctx context.Context, topic string, qos QOS) error {
+	token := c.client.Subscribe(topic, byte(qos), nil)
+	err := tokenWithContext(ctx, token)
+	return err
+}
+
+// SubscribeMultiple subscribes to multiple topics and errors if this fails.
+func (c *Client) SubscribeMultiple(ctx context.Context, subscriptions map[string]QOS) error {
+	subs := make(map[string]byte, len(subscriptions))
+	for topic, qos := range subscriptions {
+		subs[topic] = byte(qos)
+	}
+	token := c.client.SubscribeMultiple(subs, nil)
+	err := tokenWithContext(ctx, token)
+	return err
+}
+
+// Unsubscribe unsubscribes from a certain topic and errors if this fails.
+func (c *Client) Unsubscribe(ctx context.Context, topic string) error {
+	token := c.client.Unsubscribe(topic)
+	err := tokenWithContext(ctx, token)
+	return err
+}

+ 8 - 0
server/utils/protocol/protocol.go

@@ -0,0 +1,8 @@
+package protocol
+
+const (
+	TopicSwitchControlCmd = "switch_control/cmd"
+	TopicSwitchControlAck = "switch_control/ack"
+	TopicSwitchControlDg  = "switch_control/dg"
+	TopicIlluminance      = "illuminance/data"
+)

+ 16 - 0
web/src/api/device.js

@@ -0,0 +1,16 @@
+import service from '@/utils/request'
+
+export const queryAllDevices = () => {
+  return service({
+    url: '/device/queryAllDevices',
+    method: 'get'
+  })
+}
+
+export const queryDeviceList = (data) => {
+  return service({
+    url: '/device/queryDeviceList',
+    method: 'post',
+    data
+  })
+}

+ 36 - 0
web/src/view/admin/device/device.vue

@@ -0,0 +1,36 @@
+<template>
+  <el-container>
+    <el-header>
+      <el-form v-model="deviceSearch">
+        <el-form-item>
+          <el-input
+            v-model="deviceSearch.name"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-input
+            v-model="deviceSearch.sn"
+          />
+        </el-form-item>
+      </el-form>
+    </el-header>
+    <el-main>
+      ti
+    </el-main>
+  </el-container>
+</template>
+
+<script setup>
+
+const deviceSearch = ref(
+  {
+    name: '',
+    sn: '',
+    genre: 0
+  }
+)
+</script>
+
+<style scoped lang="scss">
+
+</style>

+ 23 - 0
web/src/view/admin/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div>
+    <router-view v-slot="{ Component }">
+      <transition
+        mode="out-in"
+        name="el-fade-in-linear"
+      >
+        <keep-alive :include="routerStore.keepAliveRouters">
+          <component :is="Component" />
+        </keep-alive>
+      </transition>
+    </router-view>
+  </div>
+</template>
+
+<script setup>
+import { useRouterStore } from '@/pinia/modules/router'
+const routerStore = useRouterStore()
+
+defineOptions({
+  name: 'Admin'
+})
+</script>