fix(server): stabilize device bootstrap and default dance initialization

- create missing device records safely with insert-ignore
- seed default dance data transactionally to avoid duplicate rows
- restore real default-agent reset behavior with MAC validation
- bind devices inside a transaction after ensuring the device exists
This commit is contained in:
yuanzhihong
2026-06-24 09:57:23 +08:00
parent 6ffb35f51b
commit 2291d4cc01
26 changed files with 1225 additions and 332 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 M5Stack Technology CO LTD
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1030 -184
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
-5
View File
@@ -1,8 +1,3 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
+1 -1
View File
@@ -1,6 +1,6 @@
module stackChan
go 1.26.2
go 1.26.3
require (
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0
+38
View File
@@ -0,0 +1,38 @@
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0/go.mod h1:6v7oGBF9wv59WERJIOJxXmLhkUcxwON3tPYW3AZ7wbY=
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -1
View File
@@ -81,7 +81,7 @@ var (
})
// Do not use SetServerRoot, globally only provide frontend entry via /web
//s.SetServerRoot("web/management")
s.SetServerRoot("web/management")
s.SetPort(12800)
s.Run()
@@ -13,12 +13,13 @@ import (
"stackChan/api/admin/v1"
"stackChan/internal/dao"
"github.com/gogf/gf/v2/frame/g"
"stackChan/internal/model/do"
)
func (c *ControllerV1) DeleteApp(ctx context.Context, req *v1.DeleteAppReq) (res *v1.DeleteAppRes, err error) {
_, err = dao.AppStore.Ctx(ctx).Where("id", req.Id).Data(g.Map{"is_deleted": 1}).Update()
_, err = dao.AppStore.Ctx(ctx).WherePri(req.Id).Data(do.AppStore{
IsDeleted: 1,
}).Update()
if err != nil {
return nil, gerror.WrapCode(gcode.CodeInternalError, err, "Failed to delete app")
}
@@ -7,9 +7,8 @@ package dance
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/do"
"stackChan/internal/service"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
@@ -24,35 +23,10 @@ func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1
return nil, gerror.NewCode(gcode.CodeInvalidParameter)
}
var danceList []model.Dance
err = dao.DeviceDance.Ctx(ctx).Where(do.DeviceDance{
Mac: mac,
}).Scan(&danceList)
danceList, err := service.GetOrCreateDanceList(ctx, mac, "file/music/stackchan_music.mp3")
if err != nil {
return nil, err
}
// Core modification: insert default data only when query result is empty
if len(danceList) == 0 {
// Insert single default dance data
defaultDance := do.DeviceDance{
Mac: mac,
MusicUrl: "file/music/stackchan_music.mp3",
DanceData: model.DefaultDanceData,
}
_, err = dao.DeviceDance.Ctx(ctx).Data(defaultDance).Insert()
if err != nil {
return nil, err
}
// Re-query list (one data exists now)
err = dao.DeviceDance.Ctx(ctx).Where(do.DeviceDance{
Mac: mac,
}).Scan(&danceList)
if err != nil {
return nil, err
}
}
return new(v1.GetListRes(danceList)), nil
}
@@ -8,43 +8,14 @@ package dance
import (
"context"
"stackChan/api/dance/v2"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/do"
"stackChan/internal/service"
)
func (c *ControllerV2) GetList(ctx context.Context, req *v2.GetListReq) (res *v2.GetListRes, err error) {
mac := req.Mac
var danceList []model.Dance
err = dao.DeviceDance.Ctx(ctx).Where(do.DeviceDance{
Mac: mac,
}).Scan(&danceList)
danceList, err := service.GetOrCreateDanceList(ctx, req.Mac, "http://47.113.125.164:12800/file/music/stackchan_music.mp3")
if err != nil {
return nil, err
}
// Core modification: insert default data only when query result is empty
if len(danceList) == 0 {
// Insert single default dance data
defaultDance := do.DeviceDance{
DanceName: "StackChan Dance",
Mac: mac,
MusicUrl: "http://47.113.125.164:12800/file/music/stackchan_music.mp3",
DanceData: model.DefaultDanceData,
}
_, err = dao.DeviceDance.Ctx(ctx).Data(defaultDance).Insert()
if err != nil {
return nil, err
}
// Re-query list (one data exists now)
err = dao.DeviceDance.Ctx(ctx).Where(do.DeviceDance{
Mac: mac,
}).Scan(&danceList)
if err != nil {
return nil, err
}
}
return new(v2.GetListRes(danceList)), nil
}
@@ -8,19 +8,22 @@ package device
import (
"context"
"stackChan/api/device/v2"
"stackChan/internal/service"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
func (c *ControllerV2) AgentRestoreDefault(ctx context.Context, req *v2.AgentRestoreDefaultReq) (res *v2.AgentRestoreDefaultRes, err error) {
return new(v2.AgentRestoreDefaultRes(true)), err
//if req.Mac == "" {
// return nil, gerror.NewCode(gcode.CodeMissingParameter, "Device MAC address cannot be empty")
//}
//restoreResponse, err := service.RestoreDefaultAgent(req.Mac)
//if err != nil {
// return nil, err
//}
//if !restoreResponse {
// return nil, gerror.NewCode(gcode.CodeInternalError, "Failed to restore default configuration")
//}
//return new(v2.AgentRestoreDefaultRes(true)), nil
if req.Mac == "" {
return nil, gerror.NewCode(gcode.CodeMissingParameter, "Device MAC address cannot be empty")
}
restoreResponse, err := service.RestoreDefaultAgent(req.Mac)
if err != nil {
return nil, err
}
if !restoreResponse {
return nil, gerror.NewCode(gcode.CodeInternalError, "Failed to restore default configuration")
}
return new(v2.AgentRestoreDefaultRes(true)), nil
}
@@ -9,8 +9,10 @@ import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/do"
"stackChan/internal/service"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
@@ -21,8 +23,6 @@ import (
// BindDevice Device binding interface
func (c *ControllerV2) BindDevice(ctx context.Context, req *v2.BindDeviceReq) (res *v2.BindDeviceRes, err error) {
// 1. Get current logged-in user UID (from context)
_, err = service.CreateMacIfNotExists(ctx, req.Mac)
uid := g.RequestFromCtx(ctx).GetCtxVar(model.Uid).Int64()
if uid == 0 {
return nil, gerror.NewCode(gcode.CodeMissingParameter, "User UID cannot be empty")
@@ -30,10 +30,21 @@ func (c *ControllerV2) BindDevice(ctx context.Context, req *v2.BindDeviceReq) (r
if req.Mac == "" {
return nil, gerror.NewCode(gcode.CodeMissingParameter, "Device MAC address cannot be empty")
}
_, err = dao.Device.Ctx(ctx).
Where("mac = ?", req.Mac).
Data("uid", uid, "bind_time", gtime.Now().Format("Y-m-d H:i:s")).
Update()
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if err = service.CreateMacIfNotExistsWithTx(ctx, tx, req.Mac); err != nil {
return err
}
_, err = dao.Device.Ctx(ctx).TX(tx).
Where("mac = ?", req.Mac).
Data(do.Device{
Uid: uid,
BindTime: gtime.Now().Format("Y-m-d H:i:s"),
}).
Update()
return err
})
if err != nil {
return nil, gerror.WrapCode(gcode.CodeDbOperationError, err, "Device binding failed")
}
+55
View File
@@ -0,0 +1,55 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package service
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/do"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
const defaultDanceName = "StackChan Dance"
func GetOrCreateDanceList(ctx context.Context, mac string, defaultMusicURL string) (danceList []model.Dance, err error) {
err = g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
if err = CreateMacIfNotExistsWithTx(ctx, tx, mac); err != nil {
return err
}
// Serialize default initialization for the same device to avoid duplicate seed rows.
if _, err = dao.Device.Ctx(ctx).TX(tx).Where("mac", mac).LockUpdate().One(); err != nil {
return err
}
if err = dao.DeviceDance.Ctx(ctx).TX(tx).Where(do.DeviceDance{
Mac: mac,
}).Scan(&danceList); err != nil {
return err
}
if len(danceList) > 0 {
return nil
}
if _, err = dao.DeviceDance.Ctx(ctx).TX(tx).Data(do.DeviceDance{
DanceName: defaultDanceName,
Mac: mac,
MusicUrl: defaultMusicURL,
DanceData: model.DefaultDanceData,
}).Insert(); err != nil {
return err
}
return dao.DeviceDance.Ctx(ctx).TX(tx).Where(do.DeviceDance{
Mac: mac,
}).Scan(&danceList)
})
return danceList, err
}
+27 -9
View File
@@ -10,24 +10,42 @@ import (
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
func CreateMacIfNotExists(ctx context.Context, mac string) (id int64, err error) {
count, err := dao.Device.Ctx(ctx).Where("mac = ?", mac).Count()
if err != nil {
if err = validateMac(mac); err != nil {
return 0, err
}
if count > 0 {
return 0, nil
}
id, err = dao.Device.Ctx(ctx).Data(do.Device{
Mac: mac,
}).InsertAndGetId()
_, err = dao.Device.Ctx(ctx).Data(do.Device{
Mac: mac,
}).InsertIgnore()
if err != nil {
return 0, err
}
return id, nil
return 0, nil
}
func CreateMacIfNotExistsWithTx(ctx context.Context, tx gdb.TX, mac string) error {
if err := validateMac(mac); err != nil {
return err
}
_, err := dao.Device.Ctx(ctx).TX(tx).Data(do.Device{
Mac: mac,
}).InsertIgnore()
return err
}
func validateMac(mac string) error {
if mac == "" {
return gerror.NewCode(gcode.CodeMissingParameter, "Device MAC address cannot be empty")
}
return nil
}
func GetDeviceName(ctx context.Context, mac string) (name string, err error) {
@@ -0,0 +1 @@
2026-04-28T11:21:26.547+08:00 [INFO] {38544d753667aa188bdf205b0608ffab} xiaozhi token auto refresh ticker started, refresh cycle: 24 hours
+1 -2
View File
@@ -141,8 +141,7 @@ func Registration(ctx context.Context, req *v2.RegistrationReq) (res *v2.Registr
if err != nil {
return nil, err
}
responseData := v2.RegistrationRes(remoteResp)
return &responseData, nil
return new(v2.RegistrationRes(remoteResp)), nil
}
// callRemoteRegister Call remote registration interface
+10
View File
@@ -12,6 +12,7 @@ import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
@@ -261,6 +262,15 @@ func GenerateFourKeys() {
clientPrivateKey, clientPublicKey = generateKeyPair(2048)
// Print PEM
//fmt.Println("=== Server Private Key ===")
//fmt.Println(keyToPEM(serverPrivateKey, true))
//fmt.Println("=== Server Public Key ===")
//fmt.Println(keyToPEM(serverPublicKey, false))
fmt.Println("=== Client Private Key ===")
fmt.Println(keyToPEM(clientPrivateKey, true))
fmt.Println("=== Client Public Key ===")
fmt.Println(keyToPEM(clientPublicKey, false))
// Mark initialization complete
initialized = true