Procházet zdrojové kódy

事件上报开始

chengqian před 1 rokem
rodič
revize
1e0f0b2886

+ 5 - 1
server/api/v1/devices/enter.go

@@ -4,8 +4,12 @@ import "server/service"
 
 type ApiGroup struct {
 	ScreensApi
+	ProgramApi
+	SoundPeriodApi
 }
 
 var (
-	ScreensService = service.ServiceGroupApp.DevicesServiceGroup.ScreensService
+	ScreensService     = service.ServiceGroupApp.DevicesServiceGroup.ScreensService
+	programService     = service.ServiceGroupApp.DevicesServiceGroup.ProgramService
+	soundPeriodService = service.ServiceGroupApp.DevicesServiceGroup.SoundPeriodService
 )

+ 45 - 0
server/api/v1/devices/program.go

@@ -0,0 +1,45 @@
+package devices
+
+import (
+	"github.com/gin-gonic/gin"
+	"go.uber.org/zap"
+	"server/global"
+	"server/model/common/response"
+	"server/model/devices"
+)
+
+type ProgramApi struct{}
+
+func (pa *ProgramApi) CreateProgram(c *gin.Context) {
+	var info devices.ProgramReq
+	if err := c.ShouldBindJSON(&info); err != nil {
+		global.GVA_LOG.Error("CreateProgram === ", zap.Error(err))
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err := programService.CreateProgram(info)
+	if err != nil {
+		global.GVA_LOG.Error("CreateProgram === ", zap.Error(err))
+		response.FailWithMessage("新增失败", c)
+		return
+	}
+
+	response.Ok(c)
+}
+
+func (pa *ProgramApi) UpdateProgram(c *gin.Context) {
+	var info devices.ProgramReq
+	if err := c.ShouldBindJSON(&info); err != nil {
+		global.GVA_LOG.Error("UpdateProgram === ", zap.Error(err))
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err := programService.UpdateProgram(info)
+	if err != nil {
+		global.GVA_LOG.Error("UpdateProgram === ", zap.Error(err))
+		response.FailWithMessage("更新失败", c)
+		return
+	}
+
+	response.Ok(c)
+}

+ 43 - 0
server/api/v1/devices/soundPeriod.go

@@ -0,0 +1,43 @@
+package devices
+
+import (
+	"github.com/gin-gonic/gin"
+	"go.uber.org/zap"
+	"server/global"
+	"server/model/common/response"
+	"server/model/devices"
+)
+
+type SoundPeriodApi struct{}
+
+func (spa *SoundPeriodApi) CreateSoundPeriod(c *gin.Context) {
+	var req devices.SoundPeriodReq
+	if err := c.ShouldBindJSON(&req); err != nil {
+		global.GVA_LOG.Error("CreateSoundPeriod === ", zap.Error(err))
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err := soundPeriodService.CreateSoundPeriod(req)
+	if err != nil {
+		global.GVA_LOG.Error("CreateSoundPeriod === ", zap.Error(err))
+		response.FailWithMessage("新增成功", c)
+		return
+	}
+	response.Ok(c)
+}
+
+func (spa *SoundPeriodApi) UpdateSoundPeriod(c *gin.Context) {
+	var req devices.SoundPeriodReq
+	if err := c.ShouldBindJSON(&req); err != nil {
+		global.GVA_LOG.Error("UpdateSoundPeriod === ", zap.Error(err))
+		response.FailWithMessage("参数错误", c)
+		return
+	}
+	err := soundPeriodService.UpdateSoundPeriod(req)
+	if err != nil {
+		global.GVA_LOG.Error("UpdateSoundPeriod === ", zap.Error(err))
+		response.FailWithMessage("更新成功", c)
+		return
+	}
+	response.Ok(c)
+}

+ 6 - 1
server/config.yaml

@@ -125,7 +125,7 @@ mysql:
     config: charset=utf8mb4&parseTime=True&loc=Local
     db-name: smart_intersection2.0
     username: root
-    password: 123456
+    password: root
     path: 127.0.0.1
     engine: ""
     log-mode: error
@@ -219,4 +219,9 @@ zap:
     max-age: 0
     show-line: true
     log-in-console: false
+mqtt:
+    server: "tcp://106.52.134.22:1883"
+    id: "mini_program_service_v20230426"
+    user: "admin"
+    password: "admin"
 

+ 1 - 0
server/config/config.go

@@ -27,4 +27,5 @@ type Server struct {
 
 	// 跨域配置
 	Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"`
+	Mqtt Mqtt `mapstructure:"mqtt" json:"mqtt" yaml:"mqtt"`
 }

+ 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"`
+}

+ 26 - 0
server/dao/dev_program.go

@@ -0,0 +1,26 @@
+package dao
+
+import "server/global"
+
+type Program struct {
+	global.GVA_MODEL
+	Num     string `json:"num" gorm:"comment:显示内容编号"`
+	Effect  string `json:"effect" gorm:"comment:显示方式"`
+	Speed   string `json:"speed" gorm:"comment:移动速度"`
+	Stay    string `json:"stay" gorm:"comment:节目停留时间"`
+	Total   string `json:"total" gorm:"comment:预留"`
+	Color   string `json:"color" gorm:"comment:颜色"`
+	Content string `json:"content" gorm:"comment:节目内容"`
+}
+
+func (Program) TableName() string {
+	return "dev_program"
+}
+
+func (p Program) CreateProgram() error {
+	return global.GVA_DB.Create(&p).Error
+}
+
+func (p Program) UpdateProgram() error {
+	return global.GVA_DB.Model(&Program{}).Where("id = ?", p.ID).Updates(&p).Error
+}

+ 13 - 9
server/dao/dev_screens.go

@@ -3,7 +3,6 @@ package dao
 import (
 	"gorm.io/gorm"
 	"server/global"
-	"server/model/devices"
 )
 
 type Screens struct {
@@ -17,6 +16,12 @@ type Screens struct {
 	Status      int            `gorm:"type:int;default:0" json:"status"`    //在线状态 0=离线,1=在线
 
 	Project Project `gorm:"foreignkey:ProjectId"`
+
+	ProgramId int     `gorm:"type:int" json:"programId"` // 节目id
+	Program   Program `gorm:"foreignkey:ProgramId"`
+
+	SoundPeriodId int         `gorm:"type:int" json:"soundPeriodId"`
+	SoundPeriod   SoundPeriod `gorm:"foreignkey:SoundPeriodId"`
 }
 
 func (Screens) TableName() string {
@@ -32,17 +37,16 @@ func (s Screens) DelScreens(id int) error {
 	return err
 }
 
-func (s Screens) GetScreensList(info devices.SearchInfo, uid uint) (screensList []Screens, total int64, err error) {
-	limit := info.PageSize
-	offset := info.PageSize * (info.Page - 1)
+func (s Screens) GetScreensList(limit, offset, projectId int, sn string, uid uint) (screensList []Screens, total int64, err error) {
+
 	db := global.GVA_DB.Model(&Screens{})
 
-	if info.Sn != "" {
-		db.Where("sn like ?", "%"+info.Sn+"%")
+	if sn != "" {
+		db.Where("sn like ?", "%"+sn+"%")
 	}
 
-	if info.ProjectId != 0 {
-		db.Where("project_id = ?", info.ProjectId)
+	if projectId != 0 {
+		db.Where("project_id = ?", projectId)
 	} else {
 		var projectIds []uint
 		list, _ := GetProjectListByUserIDNoPage(uid)
@@ -56,7 +60,7 @@ func (s Screens) GetScreensList(info devices.SearchInfo, uid uint) (screensList
 	if err != nil {
 		return
 	}
-	err = db.Limit(limit).Offset(offset).Preload("Project").Find(&screensList).Error
+	err = db.Limit(limit).Offset(offset).Preload("SoundPeriod").Preload("Program").Preload("Project").Find(&screensList).Error
 	return
 }
 

+ 41 - 0
server/dao/dev_sound_period.go

@@ -0,0 +1,41 @@
+package dao
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"fmt"
+	"gorm.io/gorm"
+)
+
+type SoundPeriod struct {
+	gorm.Model
+	Time    string   `json:"time" gorm:"column:time;comment:'星期'"`
+	Period0 IntArray `json:"period0" gorm:"column:period0;comment:'时间音量0';type:json"`
+	Period1 IntArray `json:"period1" gorm:"column:period1;comment:'时间音量1';type:json"`
+	Period2 IntArray `json:"period2" gorm:"column:period2;comment:'时间音量2';type:json"`
+	Period3 IntArray `json:"period3" gorm:"column:period3;comment:'时间音量3';type:json"`
+	Period4 IntArray `json:"period4" gorm:"column:period4;comment:'时间音量4';type:json"`
+	Period5 IntArray `json:"period5" gorm:"column:period5;comment:'时间音量5';type:json"`
+	Period6 IntArray `json:"period6" gorm:"column:period6;comment:'时间音量6';type:json"`
+	Period7 IntArray `json:"period7" gorm:"column:period7;comment:'时间音量7';type:json"`
+}
+
+func (SoundPeriod) TableName() string {
+	return "dev_sound_period"
+}
+
+type IntArray []int
+
+// Value 实现 driver.Valuer 接口,用于保存到数据库
+func (a IntArray) Value() (driver.Value, error) {
+	return json.Marshal(a)
+}
+
+// Scan 实现 sql.Scanner 接口,用于从数据库读取值
+func (a *IntArray) Scan(value interface{}) error {
+	b, ok := value.([]byte)
+	if !ok {
+		return fmt.Errorf("failed to unmarshal IntArray value")
+	}
+	return json.Unmarshal(b, &a)
+}

+ 99 - 97
server/go.mod

@@ -1,143 +1,145 @@
 module server
 
-go 1.22
+go 1.23
+
+toolchain go1.23.0
 
 require (
-	github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible
-	github.com/aws/aws-sdk-go v1.44.307
-	github.com/casbin/casbin/v2 v2.87.1
-	github.com/casbin/gorm-adapter/v3 v3.18.0
+	github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
+	github.com/aws/aws-sdk-go v1.55.6
+	github.com/casbin/casbin/v2 v2.103.0
+	github.com/casbin/gorm-adapter/v3 v3.32.0
+	github.com/eclipse/paho.mqtt.golang v1.5.0
+	github.com/flipped-aurora/gin-vue-admin/server v0.0.0-20250612051302-55bf69c810bf
 	github.com/flipped-aurora/ws v1.0.2
-	github.com/fsnotify/fsnotify v1.6.0
+	github.com/fsnotify/fsnotify v1.8.0
 	github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6
-	github.com/gin-gonic/gin v1.9.1
-	github.com/glebarez/sqlite v1.8.0
-	github.com/go-sql-driver/mysql v1.7.1
+	github.com/gin-gonic/gin v1.10.0
+	github.com/glebarez/sqlite v1.11.0
+	github.com/go-sql-driver/mysql v1.8.1
 	github.com/gofrs/uuid/v5 v5.0.0
 	github.com/golang-jwt/jwt/v4 v4.5.0
-	github.com/gookit/color v1.5.4
-	github.com/huaweicloud/huaweicloud-sdk-go-obs v3.21.8+incompatible
-	github.com/jordan-wright/email v0.0.0-20200824153738-3f5bafa1cd84
-	github.com/mojocn/base64Captcha v1.3.6
-	github.com/otiai10/copy v1.7.0
+	github.com/google/uuid v1.6.0
+	github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
+	github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
+	github.com/mojocn/base64Captcha v1.3.8
 	github.com/pkg/errors v0.9.1
 	github.com/qiniu/api.v7/v7 v7.4.1
-	github.com/qiniu/qmgo v1.1.8
-	github.com/redis/go-redis/v9 v9.0.5
+	github.com/qiniu/qmgo v1.1.9
+	github.com/redis/go-redis/v9 v9.7.0
 	github.com/robfig/cron/v3 v3.0.1
-	github.com/shirou/gopsutil/v3 v3.23.6
-	github.com/songzhibin97/gkit v1.2.11
-	github.com/spf13/viper v1.16.0
-	github.com/stretchr/testify v1.8.4
+	github.com/shirou/gopsutil/v3 v3.24.5
+	github.com/songzhibin97/gkit v1.2.13
+	github.com/spf13/viper v1.19.0
+	github.com/stretchr/testify v1.10.0
 	github.com/swaggo/files v1.0.1
 	github.com/swaggo/gin-swagger v1.6.0
-	github.com/swaggo/swag v1.16.2
-	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
-	gorm.io/driver/mysql v1.5.6
-	gorm.io/driver/postgres v1.5.7
-	gorm.io/driver/sqlserver v1.5.1
-	gorm.io/gorm v1.25.9
+	github.com/swaggo/swag v1.16.4
+	github.com/tencentyun/cos-go-sdk-v5 v0.7.60
+	github.com/unrolled/secure v1.17.0
+	go.mongodb.org/mongo-driver v1.17.2
+	go.uber.org/automaxprocs v1.6.0
+	go.uber.org/zap v1.27.0
+	golang.org/x/crypto v0.32.0
+	golang.org/x/sync v0.10.0
+	golang.org/x/text v0.21.0
+	gorm.io/driver/mysql v1.5.7
+	gorm.io/driver/postgres v1.5.11
+	gorm.io/driver/sqlserver v1.5.4
+	gorm.io/gorm v1.25.12
 	nhooyr.io/websocket v1.8.7
 )
 
 require (
+	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/KyleBanks/depth v1.2.1 // indirect
-	github.com/bytedance/sonic v1.9.1 // indirect
-	github.com/casbin/govaluate v1.1.1 // indirect
-	github.com/cespare/xxhash/v2 v2.2.0 // indirect
-	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+	github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect
+	github.com/bytedance/sonic v1.12.7 // indirect
+	github.com/bytedance/sonic/loader v0.2.3 // indirect
+	github.com/casbin/govaluate v1.3.0 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/clbanning/mxj v1.8.4 // indirect
-	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/cloudwego/base64x v0.1.5 // indirect
+	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
 	github.com/dustin/go-humanize v1.0.1 // 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
-	github.com/go-ole/go-ole v1.2.6 // indirect
-	github.com/go-openapi/jsonpointer v0.20.2 // indirect
-	github.com/go-openapi/jsonreference v0.20.3 // indirect
-	github.com/go-openapi/spec v0.20.12 // indirect
-	github.com/go-openapi/swag v0.22.5 // indirect
+	github.com/gabriel-vasile/mimetype v1.4.8 // indirect
+	github.com/gin-contrib/sse v1.0.0 // indirect
+	github.com/glebarez/go-sqlite v1.22.0 // indirect
+	github.com/go-ole/go-ole v1.3.0 // indirect
+	github.com/go-openapi/jsonpointer v0.21.0 // indirect
+	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/go-openapi/spec v0.21.0 // indirect
+	github.com/go-openapi/swag v0.23.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.14.0 // indirect
-	github.com/goccy/go-json v0.10.2 // indirect
+	github.com/go-playground/validator/v10 v10.24.0 // indirect
+	github.com/goccy/go-json v0.10.4 // indirect
 	github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
 	github.com/golang-sql/sqlexp v0.1.0 // indirect
 	github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
-	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/golang/snappy v0.0.4 // indirect
+	github.com/google/go-querystring v1.1.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
-	github.com/jackc/pgx/v5 v5.5.5 // indirect
-	github.com/jackc/puddle/v2 v2.2.1 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+	github.com/jackc/pgx/v5 v5.7.2 // indirect
+	github.com/jackc/puddle/v2 v2.2.2 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
-	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/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
-	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/mattn/go-isatty v0.0.19 // indirect
-	github.com/microsoft/go-mssqldb v1.1.0 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/klauspost/cpuid/v2 v2.2.9 // indirect
+	github.com/leodido/go-urn v1.4.0 // indirect
+	github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
+	github.com/magiconair/properties v1.8.9 // indirect
+	github.com/mailru/easyjson v0.9.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/microsoft/go-mssqldb v1.8.0 // 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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
-	github.com/montanaflynn/stats v0.7.0 // indirect
-	github.com/mozillazg/go-httpheader v0.2.1 // indirect
-	github.com/pelletier/go-toml/v2 v2.0.8 // indirect
-	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+	github.com/montanaflynn/stats v0.7.1 // indirect
+	github.com/mozillazg/go-httpheader v0.4.0 // indirect
+	github.com/ncruces/go-strftime v0.1.9 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
+	github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
 	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/sagikazarmark/locafero v0.7.0 // indirect
+	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
 	github.com/shoenig/go-m1cpu v0.1.6 // 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
+	github.com/sourcegraph/conc v0.3.0 // indirect
+	github.com/spf13/afero v1.12.0 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
-	github.com/subosito/gotenv v1.4.2 // indirect
-	github.com/tklauser/go-sysconf v0.3.11 // indirect
-	github.com/tklauser/numcpus v0.6.0 // indirect
+	github.com/subosito/gotenv v1.6.0 // indirect
+	github.com/tklauser/go-sysconf v0.3.14 // indirect
+	github.com/tklauser/numcpus v0.9.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.11 // indirect
+	github.com/ugorji/go/codec v1.2.12 // 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
-	github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
-	github.com/xuri/efp v0.0.0-20230802181842-ad255f2331ca // indirect
-	github.com/xuri/nfp v0.0.0-20230819163627-dc951e3ffe1a // indirect
-	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
-	github.com/yusufpapurcu/wmi v1.2.3 // indirect
-	go.uber.org/atomic v1.9.0 // indirect
-	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/time v0.1.0 // indirect
-	golang.org/x/tools v0.16.1 // indirect
-	google.golang.org/protobuf v1.33.0 // indirect
+	github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
+	github.com/yusufpapurcu/wmi v1.2.4 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/arch v0.13.0 // indirect
+	golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
+	golang.org/x/image v0.23.0 // indirect
+	golang.org/x/net v0.34.0 // indirect
+	golang.org/x/sys v0.29.0 // indirect
+	golang.org/x/time v0.9.0 // indirect
+	golang.org/x/tools v0.29.0 // indirect
+	google.golang.org/protobuf v1.36.3 // indirect
 	gopkg.in/ini.v1 v1.67.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
+	gorm.io/plugin/dbresolver v1.5.3 // indirect
+	modernc.org/libc v1.61.9 // indirect
+	modernc.org/mathutil v1.7.1 // indirect
+	modernc.org/memory v1.8.2 // indirect
+	modernc.org/sqlite v1.34.5 // indirect
 )

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 308 - 628
server/go.sum


+ 2 - 0
server/initialize/gorm.go

@@ -49,6 +49,8 @@ func RegisterTables() {
 		dao.Project{},
 
 		dao.Screens{},
+		dao.Program{},
+		dao.SoundPeriod{},
 	)
 	if err != nil {
 		global.GVA_LOG.Error("register table failed", zap.Error(err))

+ 2 - 0
server/initialize/router.go

@@ -90,6 +90,8 @@ func Routers() *gin.Engine {
 
 		projectRouter.InitProjectRouter(PrivateGroup) //注册项目管理路由
 		devicesRouter.InitScreensRouter(PrivateGroup) //注册设备管理路由
+		devicesRouter.InitProgramRouter(PrivateGroup)
+		devicesRouter.InitSoundPeriodRouter(PrivateGroup)
 	}
 
 	global.GVA_LOG.Info("router register success")

+ 6 - 0
server/main.go

@@ -6,6 +6,11 @@ import (
 	"server/core"
 	"server/global"
 	"server/initialize"
+<<<<<<< HEAD
+=======
+	"server/service/devices"
+	"server/service/tcp"
+>>>>>>> 4d31bdcda1e7cd18ff151516af4409a652ed845d
 )
 
 //go:generate go env -w GO111MODULE=on
@@ -34,6 +39,7 @@ func main() {
 		db, _ := global.GVA_DB.DB()
 		defer db.Close()
 	}
+	devices.InitMqtt()
 	//tcp.InitDevices()
 	//go tcp.ListenTcp()
 	//go tcp.IsOnline()

+ 42 - 0
server/model/devices/common.go

@@ -1,5 +1,7 @@
 package devices
 
+import "server/dao"
+
 type SearchInfo struct {
 	Page      int    `json:"page" form:"page"`           // 页码
 	PageSize  int    `json:"pageSize" form:"pageSize"`   // 每页大小
@@ -15,3 +17,43 @@ type ReqScreens struct {
 	IpAddress   string `json:"ipAddress"`   //ip地址
 	Remark      string `json:"remark"`      //备注
 }
+
+type ProgramReq struct {
+	DeviceSn string      `json:"deviceSn"`
+	Program  dao.Program `json:"program"`
+}
+
+type SoundPeriodReq struct {
+	DeviceSn    string          `json:"deviceSn"`
+	SoundPeriod dao.SoundPeriod `json:"soundPeriod"`
+}
+
+type SetDisContent struct {
+	Num     string `json:"num"`
+	Effect  string `json:"effect"`
+	Speed   string `json:"speed"`
+	Stay    string `json:"stay"`
+	Total   string `json:"total"`
+	Color   string `json:"color"`
+	Content string `json:"content"`
+}
+
+type DiscontentJSON struct {
+	Setdiscontent0 SetDisContent `json:"setdiscontent0"`
+}
+
+type SetPeropdtime struct {
+	Time    string       `json:"time"`
+	Period0 dao.IntArray `json:"period0"`
+	Period1 dao.IntArray `json:"period1"`
+	Period2 dao.IntArray `json:"period2"`
+	Period3 dao.IntArray `json:"period3"`
+	Period4 dao.IntArray `json:"period4"`
+	Period5 dao.IntArray `json:"period5"`
+	Period6 dao.IntArray `json:"period6"`
+	Period7 dao.IntArray `json:"period7"`
+}
+
+type PeriodtimeJSON struct {
+	Setperiodtime0 SetPeropdtime `json:"setperiodtime0"`
+}

+ 2 - 0
server/router/devices/enter.go

@@ -2,4 +2,6 @@ package devices
 
 type RouterGroup struct {
 	ScreensRouter
+	ProgramRouter
+	SoundPeriodRouter
 }

+ 23 - 0
server/router/devices/program.go

@@ -0,0 +1,23 @@
+package devices
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type ProgramRouter struct{}
+
+func (pr *ProgramRouter) InitProgramRouter(Router *gin.RouterGroup) {
+	programRouter := Router.Group("program").Use(middleware.OperationRecord())
+
+	programApi := v1.ApiGroupApp.DevicesApiGroup.ProgramApi
+
+	{
+		programRouter.POST("createProgram", programApi.CreateProgram) // 增加LED
+		programRouter.PUT("updateProgram", programApi.UpdateProgram)  // 编辑
+	}
+
+	{
+	}
+}

+ 22 - 0
server/router/devices/soundPeriod.go

@@ -0,0 +1,22 @@
+package devices
+
+import (
+	"github.com/gin-gonic/gin"
+	v1 "server/api/v1"
+	"server/middleware"
+)
+
+type SoundPeriodRouter struct{}
+
+func (s *SoundPeriodRouter) InitSoundPeriodRouter(Router *gin.RouterGroup) {
+	soundPeriodRouter := Router.Group("soundPeriod").Use(middleware.OperationRecord())
+
+	soundPeriodApi := v1.ApiGroupApp.DevicesApiGroup.SoundPeriodApi
+
+	{
+		soundPeriodRouter.POST("createSoundPeriod", soundPeriodApi.CreateSoundPeriod) // 增加LED
+		soundPeriodRouter.PUT("updateSoundPeriod", soundPeriodApi.UpdateSoundPeriod)  // 编辑
+	}
+	{
+	}
+}

+ 2 - 0
server/service/devices/enter.go

@@ -2,4 +2,6 @@ package devices
 
 type ServiceGroup struct {
 	ScreensService
+	ProgramService
+	SoundPeriodService
 }

+ 119 - 0
server/service/devices/mqtt.go

@@ -0,0 +1,119 @@
+package devices
+
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"runtime"
+	"runtime/debug"
+	"server/global"
+	"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("screens/#", mqtt.AtLeastOnce, o.HandlerData)
+}
+
+func (o *MqttHandler) HandlerData(m mqtt.Message) {
+	for {
+		ok, cnt := o.queue.Put(&m)
+		if ok {
+			break
+		} else {
+			global.GVA_LOG.Error(fmt.Sprintf("HandlerData:查询队列失败,队列消息数量:%d", cnt))
+			runtime.Gosched()
+		}
+	}
+}
+
+func (o *MqttHandler) Handler() interface{} {
+	defer func() {
+		if err := recover(); err != nil {
+			go GetHandler().Handler()
+			global.GVA_LOG.Error(fmt.Sprintf("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 {
+			global.GVA_LOG.Error(fmt.Sprintf("数据队列累积过多,请注意优化,当前队列条数:%d", quantity))
+		}
+		m, ok := msg.(*mqtt.Message)
+		if !ok {
+			continue
+		}
+
+		_, topic, err := parseTopic(m.Topic())
+		if err != nil {
+			global.GVA_LOG.Error("parseTopic err")
+			continue
+		}
+
+		switch topic {
+
+		}
+
+	}
+}
+
+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("screens/%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
+}
+
+func Sending(sn, json string) error {
+	err := MqttService.Publish(MqttService.GetTopic(sn, protocol.TopicChanStatus), []byte(json))
+	if err != nil {
+		return fmt.Errorf("error updating: %v", err)
+	}
+	return nil
+}

+ 76 - 0
server/service/devices/program.go

@@ -0,0 +1,76 @@
+package devices
+
+import (
+	"encoding/json"
+	"fmt"
+	"server/dao"
+	"server/global"
+	"server/model/devices"
+	"server/utils/protocol"
+)
+
+type ProgramService struct {
+}
+
+func (ps *ProgramService) CreateProgram(ref devices.ProgramReq) error {
+	result := devices.DiscontentJSON{
+		Setdiscontent0: devices.SetDisContent{
+			Num:     ref.Program.Num,
+			Effect:  ref.Program.Effect,
+			Speed:   ref.Program.Speed,
+			Stay:    ref.Program.Stay,
+			Total:   ref.Program.Total,
+			Color:   ref.Program.Color,
+			Content: ref.Program.Content,
+		},
+	}
+
+	// 序列化为 JSON 字符串
+	jsonBytes, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return fmt.Errorf("转换失败: %v", err)
+	}
+
+	err = MqttService.Publish(MqttService.GetTopic(ref.DeviceSn, protocol.TopicChanStatus), jsonBytes)
+	if err != nil {
+		return fmt.Errorf("error updating: %v", err)
+	}
+
+	err = global.GVA_DB.Create(&ref.Program).Error
+	if err != nil {
+		return err
+	}
+	fmt.Println("Program CreateProgram === ", ref.Program)
+	err = global.GVA_DB.Model(dao.Screens{}).Where("sn = ?", ref.DeviceSn).Update("program_id", ref.Program.ID).Error
+	if err != nil {
+		return err
+	}
+	return err
+}
+
+func (ps *ProgramService) UpdateProgram(ref devices.ProgramReq) error {
+	result := devices.DiscontentJSON{
+		Setdiscontent0: devices.SetDisContent{
+			Num:     ref.Program.Num,
+			Effect:  ref.Program.Effect,
+			Speed:   ref.Program.Speed,
+			Stay:    ref.Program.Stay,
+			Total:   ref.Program.Total,
+			Color:   ref.Program.Color,
+			Content: ref.Program.Content,
+		},
+	}
+
+	// 序列化为 JSON 字符串
+	jsonBytes, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return fmt.Errorf("转换失败: %v", err)
+	}
+
+	err = MqttService.Publish(MqttService.GetTopic(ref.DeviceSn, protocol.TopicChanStatus), jsonBytes)
+	if err != nil {
+		return fmt.Errorf("error updating: %v", err)
+	}
+
+	return ref.Program.UpdateProgram()
+}

+ 4 - 1
server/service/devices/screens.go

@@ -22,6 +22,9 @@ func (s ScreensService) UpdateScreens(screens dao.Screens) error {
 	return err
 }
 func (s ScreensService) GetScreensList(info devices.SearchInfo, userId uint) (list interface{}, total int64, err error) {
-	screensList, t, err := dao.Screens{}.GetScreensList(info, userId)
+	limit := info.PageSize
+	offset := info.PageSize * (info.Page - 1)
+
+	screensList, t, err := dao.Screens{}.GetScreensList(limit, offset, int(info.ProjectId), info.Sn, userId)
 	return screensList, t, err
 }

+ 84 - 0
server/service/devices/soundPeriod.go

@@ -0,0 +1,84 @@
+package devices
+
+import (
+	"encoding/json"
+	"fmt"
+	"server/dao"
+	"server/global"
+	"server/model/devices"
+	"server/utils/protocol"
+)
+
+type SoundPeriodService struct {
+}
+
+func (sps *SoundPeriodService) CreateSoundPeriod(req devices.SoundPeriodReq) error {
+	result := devices.PeriodtimeJSON{
+		Setperiodtime0: devices.SetPeropdtime{
+			Time:    req.SoundPeriod.Time,
+			Period0: req.SoundPeriod.Period0,
+			Period1: req.SoundPeriod.Period1,
+			Period2: req.SoundPeriod.Period2,
+			Period3: req.SoundPeriod.Period3,
+			Period4: req.SoundPeriod.Period4,
+			Period5: req.SoundPeriod.Period5,
+			Period6: req.SoundPeriod.Period6,
+			Period7: req.SoundPeriod.Period7,
+		},
+	}
+
+	// 序列化为 JSON 字符串
+	jsonBytes, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return fmt.Errorf("转换失败: %v", err)
+	}
+
+	err = MqttService.Publish(MqttService.GetTopic(req.DeviceSn, protocol.TopicChanStatus), jsonBytes)
+	if err != nil {
+		return fmt.Errorf("error updating: %v", err)
+	}
+
+	err = global.GVA_DB.Create(&req.SoundPeriod).Error
+	if err != nil {
+		return err
+	}
+	fmt.Println("SoundPeriod CreateSoundPeriod === ", req.SoundPeriod)
+	err = global.GVA_DB.Model(dao.Screens{}).Where("sn = ?", req.DeviceSn).Update("sound_period_id", req.SoundPeriod.ID).Error
+	if err != nil {
+		return err
+	}
+	return err
+}
+
+func (sps *SoundPeriodService) UpdateSoundPeriod(req devices.SoundPeriodReq) error {
+	result := devices.PeriodtimeJSON{
+		Setperiodtime0: devices.SetPeropdtime{
+			Time:    req.SoundPeriod.Time,
+			Period0: req.SoundPeriod.Period0,
+			Period1: req.SoundPeriod.Period1,
+			Period2: req.SoundPeriod.Period2,
+			Period3: req.SoundPeriod.Period3,
+			Period4: req.SoundPeriod.Period4,
+			Period5: req.SoundPeriod.Period5,
+			Period6: req.SoundPeriod.Period6,
+			Period7: req.SoundPeriod.Period7,
+		},
+	}
+
+	// 序列化为 JSON 字符串
+	jsonBytes, err := json.MarshalIndent(result, "", "  ")
+	if err != nil {
+		return fmt.Errorf("转换失败: %v", err)
+	}
+
+	err = MqttService.Publish(MqttService.GetTopic(req.DeviceSn, protocol.TopicChanStatus), jsonBytes)
+	if err != nil {
+		return fmt.Errorf("error updating: %v", err)
+	}
+
+	err = global.GVA_DB.Where("id = ?", req.SoundPeriod.ID).Updates(&req.SoundPeriod).Error
+	if err != nil {
+		return err
+	}
+	return err
+}

+ 1 - 1
server/service/system/sys_casbin.go

@@ -74,7 +74,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri
 func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) {
 	e := casbinService.Casbin()
 	authorityId := strconv.Itoa(int(AuthorityID))
-	list := e.GetFilteredPolicy(0, authorityId)
+	list, _ := e.GetFilteredPolicy(0, authorityId)
 	for _, v := range list {
 		pathMaps = append(pathMaps, request.CasbinInfo{
 			Path:   v[1],

+ 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/global"
+	"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) {
+	global.GVA_LOG.Error(fmt.Sprintf("MClient.ConnectionLostHandler:MQTT连接已经断开,原因:%s", err))
+}
+
+func (o *MClient) OnConnectHandler() {
+	global.GVA_LOG.Info("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
+}

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

@@ -0,0 +1,9 @@
+package protocol
+
+const (
+	TopicChanStatus = "chanStatus" //上报状态
+	TopicHighSpeed  = "highSpeed"  //超速时
+	TopicLowSpeed   = "lowSpeed"   //低速时
+
+	TopicSetControl = "setControl" //云台下发控制
+)

+ 3 - 1
server/utils/upload/obs.go

@@ -30,7 +30,9 @@ func (o *Obs) UploadFile(file *multipart.FileHeader) (string, string, error) {
 				Bucket: global.GVA_CONFIG.HuaWeiObs.Bucket,
 				Key:    filename,
 			},
-			ContentType: file.Header.Get("content-type"),
+			HttpHeader: obs.HttpHeader{
+				ContentType: file.Header.Get("content-type"),
+			},
 		},
 		Body: open,
 	}

+ 17 - 0
web/src/api/program.js

@@ -0,0 +1,17 @@
+import service from '@/utils/request'
+
+export const createProgram = (data) => {
+  return service({
+    url: '/program/createProgram',
+    method: 'post',
+    data
+  })
+}
+
+export const updateProgram = (data) => {
+  return service({
+    url: '/program/updateProgram',
+    method: 'put',
+    data
+  })
+}

+ 17 - 0
web/src/api/soundPeriod.js

@@ -0,0 +1,17 @@
+import service from '@/utils/request'
+
+export const createSoundPeriod = (data) => {
+  return service({
+    url: '/soundPeriod/createSoundPeriod',
+    method: 'post',
+    data
+  })
+}
+
+export const updateSoundPeriod = (data) => {
+  return service({
+    url: '/soundPeriod/updateSoundPeriod',
+    method: 'put',
+    data
+  })
+}

+ 400 - 4
web/src/view/devicesAdmin/screens/screens.vue

@@ -219,10 +219,170 @@
 
       <el-dialog
         v-model="showSettingDialog"
-        title="操作"
-        width="800"
+        :title="`操作,设备序号=`+deviceSn"
+        width="1000"
       >
-        <span>This is a message</span>
+        <el-tabs v-model="activeName">
+          <el-tab-pane
+            label="节目"
+            name="first"
+          >
+            <el-form
+              :model="programData"
+              label-width="auto"
+            >
+              <el-form-item label="内容编号">
+                <el-select
+                  v-model="programData.num"
+                  placeholder="请选择"
+                  style="width: 240px"
+                >
+                  <el-option
+                    v-for="item in contentNumberOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="显示方式">
+                <el-select
+                  v-model="programData.effect"
+                  placeholder="请选择"
+                  style="width: 240px"
+                >
+                  <el-option
+                    v-for="item in displayModeOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="移动速度">
+                <el-input
+                  v-model="programData.speed"
+                  placeholder="默认推荐 3"
+                  style="width: 240px;"
+                />
+              </el-form-item>
+              <el-form-item label="节目停留时间">
+                <el-input
+                  v-model="programData.stay"
+                  placeholder="默认推荐 5"
+                  style="width: 240px;"
+                />
+              </el-form-item>
+              <el-form-item label="预留">
+                <el-input
+                  v-model="programData.total"
+                  placeholder="默认推荐 100"
+                  style="width: 240px;"
+                />
+              </el-form-item>
+              <el-form-item label="节目颜色">
+                <el-select
+                  v-model="programData.color"
+                  placeholder="请选择"
+                  style="width: 240px"
+                >
+                  <el-option
+                    v-for="item in colorOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="节目内容">
+                <el-input
+                  v-model="programData.content"
+                  placeholder="默认推荐 [32M]注意弯道"
+                  style="width: 240px;"
+                />
+              </el-form-item>
+              <el-form-item>
+                <el-button
+                  type="success"
+                  @click="storageProgram"
+                >存储</el-button>
+                <el-button type="primary">发送</el-button>
+              </el-form-item>
+            </el-form>
+          </el-tab-pane>
+          <el-tab-pane
+            label="声音"
+            name="second"
+          >
+            <el-form
+              :model="soundPeriodData"
+              label-width="auto"
+            >
+              <el-alert
+                v-if="isWarning"
+                title="当前值未保存"
+                type="warning"
+              />
+              <el-form-item label="星期">
+                <el-select
+                  v-model="soundPeriodData.time"
+                  placeholder="请选择"
+                  style="width: 240px"
+                >
+                  <el-option
+                    v-for="item in timeOptions"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+              </el-form-item>
+              <div
+                v-for="(period, key) in formDataPeriods"
+                :key="key"
+              >
+                <el-form-item :label="key">
+                  <el-row :gutter="10">
+                    <el-col :span="8">
+                      <span>小时:</span>
+                      <el-input-number
+                        v-model.number="period[0]"
+                        :min="0"
+                        :max="23"
+                        label="小时"
+                      />
+                    </el-col>
+                    <el-col :span="8">
+                      <span>分钟:</span>
+                      <el-input-number
+                        v-model.number="period[1]"
+                        :min="0"
+                        :max="59"
+                        label="分钟"
+                      />
+                    </el-col>
+                    <el-col :span="8">
+                      <span>音量:</span>
+                      <el-input-number
+                        v-model.number="period[2]"
+                        :min="0"
+                        :max="10"
+                        label="音量"
+                      />
+                    </el-col>
+                  </el-row>
+                </el-form-item>
+              </div>
+              <el-form-item>
+                <el-button
+                  type="success"
+                  @click="storageSoundPeriod"
+                >存储</el-button>
+                <el-button type="primary">发送</el-button>
+              </el-form-item>
+            </el-form>
+          </el-tab-pane>
+        </el-tabs>
         <template #footer>
           <div class="dialog-footer">
             <el-button @click="showSettingDialog = false">取消</el-button>
@@ -243,6 +403,9 @@
 import { addScreens, delScreens, getProjectList, getScreensList, updateScreens } from '@/api/screens'
 import { ref, onMounted } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { createProgram, updateProgram } from '@/api/program'
+import {createSoundPeriod, updateSoundPeriod} from "@/api/soundPeriod";
+
 const page = ref(1)
 const total = ref(0)
 const pageSize = ref(10)
@@ -383,10 +546,243 @@ const deleteScreens = (obj) => {
 }
 const showSettingDialog = ref(false)
 
-const openSet = () => {
+const openSet = (val) => {
   showSettingDialog.value = true
+  deviceSn.value = val.sn
+  programData.value = val.Program
+  if (val.soundPeriodId === 0) {
+    isWarning.value = true
+    soundPeriodData.value = soundPeriodForm.value
+  } else {
+    isWarning.value = false
+    soundPeriodData.value = val.SoundPeriod
+  }
+  formDataPeriods.value = {
+    period0: soundPeriodData.value.period0,
+    period1: soundPeriodData.value.period1,
+    period2: soundPeriodData.value.period2,
+    period3: soundPeriodData.value.period3,
+    period4: soundPeriodData.value.period4,
+    period5: soundPeriodData.value.period5,
+    period6: soundPeriodData.value.period6,
+    period7: soundPeriodData.value.period7,
+  }
+}
+
+const isWarning = ref(false)
+
+// 切换
+const activeName = ref('first')
+
+const deviceSn = ref()
+
+// 节目program
+const programData = ref({
+  mum: undefined,
+  effect: undefined,
+  speed: '',
+  stay: '',
+  total: '',
+  color: '',
+  content: '',
+})
+
+// 节目选择参数
+const contentNumberOptions = [
+  {
+    value: '0',
+    label: '待机',
+  },
+  {
+    value: '1',
+    label: '正常速度',
+  },
+  {
+    value: '2',
+    label: '超速',
+  },
+  {
+    value: '3',
+    label: '对向',
+  },
+  {
+    value: '4',
+    label: '双向',
+  },
+  {
+    value: '5',
+    label: '无车',
+  },
+
+  {
+    value: '6',
+    label: '快速显示配置',
+  },
+
+  {
+    value: '7',
+    label: '无信号',
+  },
+]
+
+const displayModeOptions = [
+  {
+    value: '10',
+    label: '闪烁显示',
+  },
+  {
+    value: '11',
+    label: '立即打出',
+  }
+]
+
+const colorOptions = [
+  {
+    value: '0',
+    label: '红',
+  },
+  {
+    value: '1',
+    label: '绿',
+  },
+  {
+    value: '2',
+    label: '蓝',
+  },
+  {
+    value: '3',
+    label: '黄',
+  },
+]
+
+// 存储
+const storageProgram = async() => {
+  if (programData.value.ID === 0) {
+    console.log('create')
+    await createProgram({
+      deviceSn: deviceSn.value,
+      program: programData.value,
+    }).then(res => {
+      if (res.code === 0) {
+        ElMessage.success('存储成功')
+      }
+      getTableData()
+    })
+  } else {
+    console.log('update')
+    await updateProgram({
+      deviceSn: deviceSn.value,
+      program: programData.value,
+    }).then(res => {
+      if (res.code === 0) {
+        ElMessage.success('存储成功')
+      }
+      getTableData()
+    })
+  }
 }
 
+// 声音调节soundPeriod
+const soundPeriodData = ref({
+  ID: 0,
+  time: '0',
+  period0: [],
+  period1: [],
+  period2: [],
+  period3: [],
+  period4: [],
+  period5: [],
+  period6: [],
+  period7: [],
+})
+
+const timeOptions = [
+  {
+    label: '整周',
+    value: '0',
+  },
+  {
+    label: '星期一',
+    value: '1',
+  },
+  {
+    label: '星期二',
+    value: '2',
+  },
+  {
+    label: '星期三',
+    value: '3',
+  },
+  {
+    label: '星期四',
+    value: '4',
+  },
+  {
+    label: '星期五',
+    value: '5',
+  },
+  {
+    label: '星期六',
+    value: '6',
+  },
+  {
+    label: '星期七',
+    value: '7',
+  },
+]
+
+// 初始化表单数据
+const soundPeriodForm = ref({
+  ID: 0,
+  time: '1',
+  period0: [0, 0, 0],
+  period1: [7, 0, 4],
+  period2: [8, 30, 8],
+  period3: [12, 0, 6],
+  period4: [13, 30, 8],
+  period5: [19, 0, 0],
+  period6: [22, 0, 0],
+  period7: [23, 59, 5],
+})
+
+const formDataPeriods = ref({
+  period0: soundPeriodData.value.period0,
+  period1: soundPeriodData.value.period1,
+  period2: soundPeriodData.value.period2,
+  period3: soundPeriodData.value.period3,
+  period4: soundPeriodData.value.period4,
+  period5: soundPeriodData.value.period5,
+  period6: soundPeriodData.value.period6,
+  period7: soundPeriodData.value.period7,
+})
+
+// 存储
+const storageSoundPeriod = async() => {
+  console.log(soundPeriodData.value)
+  if (soundPeriodData.value.ID === 0) {
+    console.log('create')
+    await createSoundPeriod({
+      deviceSn: deviceSn.value,
+      soundPeriod: soundPeriodData.value,
+    }).then(res => {
+      if (res.code === 0) {
+        ElMessage.success('存储成功')
+      }
+      getTableData()
+    })
+  } else {
+    console.log('update')
+    await updateSoundPeriod({
+      deviceSn: deviceSn.value,
+      soundPeriod: soundPeriodData.value,
+    }).then(res => {
+      if (res.code === 0) {
+        ElMessage.success('存储成功')
+      }
+      getTableData()
+    })
+  }
+}
 </script>
 <style>
 .el-table .success-row {