Merge pull request #3 from m5stack/server-dev

add server backend source code
This commit is contained in:
IAMLIUBO
2026-01-08 09:17:59 +08:00
committed by GitHub
102 changed files with 3502 additions and 1 deletions
+7
View File
@@ -0,0 +1,7 @@
ROOT_DIR = $(shell pwd)
NAMESPACE = "default"
DEPLOY_NAME = "template-single"
DOCKER_NAME = "template-single"
include ./hack/hack-cli.mk
include ./hack/hack.mk
+44 -1
View File
@@ -1 +1,44 @@
# StackChan Server
# StackChan Server
**StackChan Server** is the Server of the open-source StackChan project. It handles core functionalities such
as device interactions, post management, and comment systems, providing stable and efficient API support.
---
## Features
- App and StackChan communication and interaction
- Device post creation and management (text and image support, similar to a social feed)
- Comment CRUD (Create, Read, Update, Delete) operations
- Dance control and data management
- Persistent storage using a relational database
---
## Getting Started
### Prerequisites
- **Go**: The project is developed in Go. Install **Go 1.24+** from
the [official download page](https://golang.google.cn/dl/).
Verify installation:
```bash
go version
# Expected output: "go version go1.24.x ..." (or similar)
### Clone the Repository
```bash
git clone https://github.com/m5stack/StackChan # Replace with the actual repository URL
cd StackChan/server
# Download dependencies
go mod download
# build
go build -o StackChan main.go
# Start running
StackChan # Linux/macOS
StackChan.exe # Windows
+18
View File
@@ -0,0 +1,18 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package dance
import (
"context"
"stackChan/api/dance/v1"
)
type IDanceV1 interface {
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error)
Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error)
}
+45
View File
@@ -0,0 +1,45 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package v1
import (
"stackChan/internal/model"
"github.com/gogf/gf/v2/frame/g"
)
type CreateReq struct {
g.Meta `path:"/dance" method:"post" tags:"Dance" summary:"Dance create request"`
Mac string `json:"mac" v:"required"`
Index int `json:"index" v:"required"`
List []model.DanceData `json:"list" v:"required"`
}
type CreateRes string
type DeleteReq struct {
g.Meta `path:"/dance" method:"delete" tags:"Dance" summary:"Dance delete request"`
Mac string `json:"mac" v:"required"`
Index int `json:"index" v:"required"`
}
type DeleteRes string
type UpdateReq struct {
g.Meta `path:"/dance" method:"put" tags:"Dance" summary:"Dance put request"`
Mac string `json:"mac" v:"required"`
Index int `json:"index" v:"required"`
Data []model.DanceData `json:"list" v:"required"`
}
type UpdateRes string
type GetListReq struct {
g.Meta `path:"/dance" method:"get" tags:"Dance" summary:"Dance get request"`
Mac string `json:"mac" v:"required"`
}
type GetListRes map[string][]model.DanceData
+19
View File
@@ -0,0 +1,19 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package device
import (
"context"
"stackChan/api/device/v1"
)
type IDeviceV1 interface {
Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error)
Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error)
GetRandomDevice(ctx context.Context, req *v1.GetRandomDeviceReq) (res *v1.GetRandomDeviceRes, err error)
GetDeviceInfo(ctx context.Context, req *v1.GetDeviceInfoReq) (res *v1.GetDeviceInfoRes, err error)
UpdateDeviceInfo(ctx context.Context, req *v1.UpdateDeviceInfoReq) (res *v1.UpdateDeviceInfoRes, err error)
}
+53
View File
@@ -0,0 +1,53 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package v1
import (
"stackChan/internal/model"
"stackChan/internal/model/entity"
"github.com/gogf/gf/v2/frame/g"
)
type CreateReq struct {
g.Meta `path:"/device" method:"post" tags:"Device" summary:"Device create request"`
Mac string `json:"mac" v:"required" description:"Mac address"`
Name string `json:"name,omitempty" description:"Device name"`
}
type CreateRes struct {
Id int64 `json:"id" dc:"Device id"`
}
type UpdateReq struct {
g.Meta `path:"/device" method:"put" tags:"Device" summary:"Device update request"`
Mac string `json:"mac" v:"required" description:"Mac address"`
Name string `json:"name" description:"Device name"`
}
type UpdateRes struct{}
type GetRandomDeviceReq struct {
g.Meta `path:"/device/randomList" method:"get" tags:"Device" summary:"Device get Random"`
Mac string `json:"mac" v:"required" description:"Mac address"`
}
type GetRandomDeviceRes []entity.Device
type GetDeviceInfoReq struct {
g.Meta `path:"/device/info" method:"get" tags:"Device" summary:"Device Info Get request"`
Mac string `json:"mac" v:"required" description:"Mac address"`
}
type GetDeviceInfoRes model.DeviceInfo
type UpdateDeviceInfoReq struct {
g.Meta `path:"/device/info" method:"put" tags:"Device" summary:"Device Info Put request"`
Mac string `json:"mac" v:"required" description:"Mac address"`
Name string `json:"name" description:"Device name"`
}
type UpdateDeviceInfoRes string
+15
View File
@@ -0,0 +1,15 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package file
import (
"context"
"stackChan/api/file/v1"
)
type IFileV1 interface {
File(ctx context.Context, req *v1.FileReq) (res *v1.FileRes, err error)
}
+22
View File
@@ -0,0 +1,22 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package v1
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
type FileReq struct {
g.Meta `path:"/uploadFile" method:"post" tags:"File" summary:"File upload request"`
File *ghttp.UploadFile `json:"file" v:"required" description:"File upload request"`
Name string `json:"name" v:"required" description:"File Name"`
Directory string `json:"directory" description:"Directory upload request"`
}
type FileRes struct {
Path string `json:"path" description:"file path"`
}
+15
View File
@@ -0,0 +1,15 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package friend
import (
"context"
"stackChan/api/friend/v1"
)
type IFriendV1 interface {
Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error)
}
+16
View File
@@ -0,0 +1,16 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package v1
import "github.com/gogf/gf/v2/frame/g"
type AddReq struct {
g.Meta `path:"/friend" method:"post" tags:"Friend" summary:"Friend add request"`
Mac string `json:"mac" v:"required" description:"Mac address"`
FriendMac string `json:"friendMac" v:"required" description:"Friend Mac address"`
}
type AddRes string
+20
View File
@@ -0,0 +1,20 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package post
import (
"context"
"stackChan/api/post/v1"
)
type IPostV1 interface {
CreatePost(ctx context.Context, req *v1.CreatePostReq) (res *v1.CreatePostRes, err error)
GetPost(ctx context.Context, req *v1.GetPostReq) (res *v1.GetPostRes, err error)
DeletePost(ctx context.Context, req *v1.DeletePostReq) (res *v1.DeletePostRes, err error)
CreatePostComment(ctx context.Context, req *v1.CreatePostCommentReq) (res *v1.CreatePostCommentRes, err error)
DeletePostComment(ctx context.Context, req *v1.DeletePostCommentReq) (res *v1.DeletePostCommentRes, err error)
GetPostComment(ctx context.Context, req *v1.GetPostCommentReq) (res *v1.GetPostCommentRes, err error)
}
+70
View File
@@ -0,0 +1,70 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package v1
import (
"stackChan/internal/model"
"github.com/gogf/gf/v2/frame/g"
)
type CreatePostReq struct {
g.Meta `path:"/post/add" method:"post" tags:"Post" summary:"Post create request"`
Mac string `json:"mac" v:"required" description:"Mac address"`
ContentText string `json:"content_text" v:"required" description:"Content text"`
ContentImage string `json:"content_image" v:"required" description:"Content image"`
}
type CreatePostRes struct {
Id int64 `json:"id"`
}
type GetPostReq struct {
g.Meta `path:"/post/get" method:"get" tags:"Post" summary:"Post get request"`
Page int `json:"page" v:"required#Page不能为空" description:"页码"`
PageSize int `json:"pageSize" v:"required#每页数量不能为空" description:"每页条数"`
}
type GetPostRes []model.Post
type DeletePostReq struct {
g.Meta `path:"/post/delete" method:"delete" tags:"Post" summary:"Post delete request"`
Id int `json:"id" summary:"Post id"`
}
type DeletePostRes string
type CreatePostCommentReq struct {
g.Meta `path:"/post/comment/create" method:"post" tags:"Post" summary:"Post create comment"`
Mac string `json:"mac" v:"required" description:"Mac address"`
PostId int64 `json:"postId" v:"required" summary:"Post comment id"`
Content string `json:"content" description:"评论内容"`
}
type CreatePostCommentRes struct {
Id int64 `json:"id"`
}
type DeletePostCommentReq struct {
g.Meta `path:"/post/comment/delete" method:"post" tags:"Post" summary:"Post delete comment"`
Mac string `json:"mac" v:"required" description:"Mac address"`
Id int `json:"id" summary:"Post comment id"`
}
type DeletePostCommentRes struct{}
type GetPostCommentReq struct {
g.Meta `path:"/post/comment/get" method:"get" tags:"Post" summary:"Post get comment"`
PostId int64 `json:"postId" summary:"Post comment id"`
Mac string `json:"mac" v:"required" description:"Mac address"`
Page int `json:"page" summary:"Post comment page"`
PageSize int `json:"pageSize" summary:"Post comment page"`
}
type GetPostCommentRes struct {
List []*model.PostComment `json:"list"`
Total int `json:"total"`
}
+46
View File
@@ -0,0 +1,46 @@
module stackChan
go 1.24.0
require (
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7
github.com/gogf/gf/v2 v2.9.7
github.com/gorilla/websocket v1.5.3
)
require (
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
+100
View File
@@ -0,0 +1,100 @@
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7 h1:mzs0MblNT0pOlUB00c/hTcAenQ5N/cB651wh9VCJitc=
github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.9.7/go.mod h1:Z2MgGyag0fZQ2+9ykafD7tQhau9h5ie3Chg/GUzfy5E=
github.com/gogf/gf/v2 v2.9.7 h1:Vp3VGZ7drPs89tZslT6j6BKBTaw7Xs3DMGWx4MlVtMA=
github.com/gogf/gf/v2 v2.9.7/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+12
View File
@@ -0,0 +1,12 @@
#SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
#SPDX-License-Identifier: MIT
gfcli:
gen:
dao:
- link: "sqlite::@file(/stackChan.sqlite)"
descriptionTag: true
docker:
build: "-a amd64 -s linux -p temp -ew"
tagPrefixes:
- my.image.pub/my-app
+21
View File
@@ -0,0 +1,21 @@
#SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
#SPDX-License-Identifier: MIT
# Install/Update to the latest CLI tool.
.PHONY: cli
cli:
@set -e; \
wget -O gf \
https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \
chmod +x gf && \
./gf install -y && \
rm ./gf
# Check and install CLI tool.
.PHONY: cli.install
cli.install:
@set -e; \
gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \
echo "GoFame CLI is not installed, start proceeding auto installation..."; \
make cli; \
fi;
+78
View File
@@ -0,0 +1,78 @@
#SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
#SPDX-License-Identifier: MIT
.DEFAULT_GOAL := build
# Update GoFrame and its CLI to latest stable version.
.PHONY: up
up: cli.install
@gf up -a
# Build binary using configuration from hack/config.yaml.
.PHONY: build
build: cli.install
@gf build -ew
# Parse api and generate controller/sdk.
.PHONY: ctrl
ctrl: cli.install
@gf gen ctrl
# Generate Go files for DAO/DO/Entity.
.PHONY: dao
dao: cli.install
@gf gen dao
# Parse current project go files and generate enums go file.
.PHONY: enums
enums: cli.install
@gf gen enums
# Generate Go files for Service.
.PHONY: service
service: cli.install
@gf gen service
# Build docker image.
.PHONY: image
image: cli.install
$(eval _TAG = $(shell git rev-parse --short HEAD))
ifneq (, $(shell git status --porcelain 2>/dev/null))
$(eval _TAG = $(_TAG).dirty)
endif
$(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG)))
$(eval _PUSH = $(if ${PUSH}, ${PUSH}, ))
@gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG};
# Build docker image and automatically push to docker repo.
.PHONY: image.push
image.push: cli.install
@make image PUSH=-p;
# Deploy image and yaml to current kubectl environment.
.PHONY: deploy
deploy: cli.install
$(eval _TAG = $(if ${TAG}, ${TAG}, develop))
@set -e; \
mkdir -p $(ROOT_DIR)/temp/kustomize;\
cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\
kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\
kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \
if [ $(DEPLOY_NAME) != "" ]; then \
kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \
fi;
# Parsing protobuf files and generating go files.
.PHONY: pb
pb: cli.install
@gf gen pb
# Generate protobuf files for database tables.
.PHONY: pbentity
pbentity: cli.install
@gf gen pbentity
+92
View File
@@ -0,0 +1,92 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package cmd
import (
"context"
"fmt"
"net"
"net/http"
"path/filepath"
"stackChan/internal/controller/dance"
"stackChan/internal/controller/device"
"stackChan/internal/controller/file"
"stackChan/internal/controller/friend"
"stackChan/internal/controller/post"
"stackChan/internal/web_socket"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtimer"
)
var (
Main = gcmd.Command{
Name: "main",
Usage: "main",
Brief: "start http server",
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
PrintIPAddr()
//Start a scheduled task to send ping messages
gtimer.SetInterval(ctx, time.Second*5, func(ctx context.Context) {
web_socket.StartPingTime(ctx)
})
//Start a timer to clean up long-lived connections that have been inactive for a long time on the app.
gtimer.SetInterval(ctx, time.Second*15, func(ctx context.Context) {
web_socket.CheckExpiredLinks(ctx)
})
s := g.Server()
s.BindHandler("/stackChan/ws", web_socket.Handler)
///Configuration file access
s.Group("/file", func(group *ghttp.RouterGroup) {
group.GET("/*filepath", func(r *ghttp.Request) {
relativePath := r.Get("filepath").String()
if relativePath == "" {
r.Response.WriteHeader(http.StatusNotFound)
r.Response.Write("File not found")
return
}
filePath := filepath.Join("file", relativePath)
if !gfile.Exists(filePath) {
r.Response.WriteHeader(http.StatusNotFound)
r.Response.Write("File not found")
return
}
r.Response.ServeFile(filePath)
})
})
s.Group("/stackChan", func(group *ghttp.RouterGroup) {
group.Middleware(ghttp.MiddlewareHandlerResponse)
group.Bind(device.NewV1(), friend.NewV1(), dance.NewV1(), file.NewV1(), post.NewV1())
})
s.Run()
return nil
},
}
)
func PrintIPAddr() {
addrs, err := net.InterfaceAddrs()
if err == nil {
fmt.Println("Local IP addresses detected on this machine:")
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
fmt.Println(" -", ipnet.IP.String())
}
}
} else {
fmt.Println("Could not detect local IP addresses:", err)
}
fmt.Println("Please update the StackChan and iOS client access addresses to use one of the above local IPs as needed.")
}
+6
View File
@@ -0,0 +1,6 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package consts
@@ -0,0 +1,9 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package dance
@@ -0,0 +1,19 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package dance
import (
"stackChan/api/dance"
)
type ControllerV1 struct{}
func NewV1() dance.IDanceV1 {
return &ControllerV1{}
}
@@ -0,0 +1,67 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package dance
import (
"context"
"encoding/json"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/api/dance/v1"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
if req.Index < 0 {
return nil, gerror.NewCode(gcode.CodeInvalidParameter, "Index cannot be negative")
}
device, err := dao.Device.Ctx(ctx).Where("mac=", req.Mac).One()
if err != nil {
return nil, err
}
if device.IsEmpty() {
_, err = dao.Device.Ctx(ctx).Data(dao.Device.Columns().Mac, req.Mac).Insert()
if err != nil {
return nil, err
}
}
dance, err := dao.DeviceDance.Ctx(ctx).Where("mac=?", req.Mac).Where("dance_index=?", req.Index).One()
if err != nil {
return nil, err
}
danceListJSON, err := json.Marshal(req.List)
if err != nil {
return nil, err
}
if dance.IsEmpty() {
_, err = dao.DeviceDance.Ctx(ctx).Data(do.DeviceDance{
Mac: req.Mac,
DanceIndex: req.Index,
DanceData: danceListJSON,
}).Insert()
if err != nil {
return nil, err
}
} else {
_, err = dao.DeviceDance.Ctx(ctx).Where("mac=?", req.Mac).Where("dance_index=?", req.Index).Data(do.DeviceDance{
DanceData: danceListJSON,
}).Update()
if err != nil {
return nil, err
}
}
response := v1.CreateRes("Dance data saved successfully")
return &response, nil
}
@@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package dance
import (
"context"
"stackChan/internal/dao"
"stackChan/api/dance/v1"
)
func (c *ControllerV1) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) {
_, err = dao.DeviceDance.Ctx(ctx).Where("mac=", req.Mac).Where("dance_index=", req.Index).Delete()
return
}
@@ -0,0 +1,44 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package dance
import (
"context"
"encoding/json"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/do"
"stackChan/internal/model/entity"
"strconv"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"stackChan/api/dance/v1"
)
func (c *ControllerV1) GetList(ctx context.Context, req *v1.GetListReq) (res *v1.GetListRes, err error) {
danceMap := make(map[string][]model.DanceData)
var list []entity.DeviceDance
err = dao.DeviceDance.Ctx(ctx).Where(do.DeviceDance{
Mac: req.Mac,
}).Scan(&list)
if err != nil {
return nil, err
}
if len(list) > 0 {
deviceDance := list[0]
var danceList []model.DanceData
err = json.Unmarshal([]byte(deviceDance.DanceData), &danceList)
if err != nil {
return nil, gerror.WrapCode(gcode.CodeInvalidParameter, err)
}
key := strconv.Itoa(deviceDance.DanceIndex)
danceMap[key] = danceList
}
response := v1.GetListRes(danceMap)
return &response, nil
}
@@ -0,0 +1,31 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package dance
import (
"context"
"encoding/json"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/api/dance/v1"
)
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
response := v1.UpdateRes("")
danceJSON, err := json.Marshal(req.Data)
if err != nil {
return nil, err
}
_, err = dao.DeviceDance.Ctx(ctx).Where("mac=?", req.Mac).Where("dance_index=?", req.Index).Data(do.DeviceDance{
DanceData: danceJSON,
}).Update()
if err != nil {
return nil, err
}
response = "Update successful"
return &response, nil
}
@@ -0,0 +1,10 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package device
@@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package device
import (
"stackChan/api/device"
)
type ControllerV1 struct{}
func NewV1() device.IDeviceV1 {
return &ControllerV1{}
}
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package device
import (
"context"
"stackChan/api/device/v1"
"stackChan/internal/dao"
"stackChan/internal/model/do"
)
func (c *ControllerV1) Create(ctx context.Context, req *v1.CreateReq) (res *v1.CreateRes, err error) {
insertId, err := dao.Device.Ctx(ctx).Data(do.Device{
Mac: req.Mac,
Name: req.Name,
}).InsertAndGetId()
if err != nil {
return nil, err
}
res = &v1.CreateRes{
Id: insertId,
}
return
}
@@ -0,0 +1,24 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package device
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/api/device/v1"
)
func (c *ControllerV1) GetDeviceInfo(ctx context.Context, req *v1.GetDeviceInfoReq) (res *v1.GetDeviceInfoRes, err error) {
var info model.DeviceInfo
err = dao.Device.Ctx(ctx).WherePri(req.Mac).Scan(&info)
if err != nil {
return nil, err
}
res = (*v1.GetDeviceInfoRes)(&info)
return res, nil
}
@@ -0,0 +1,37 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package device
import (
"context"
"stackChan/api/device/v1"
"stackChan/internal/dao"
"stackChan/internal/model/entity"
"stackChan/internal/web_socket"
)
func (c *ControllerV1) GetRandomDevice(ctx context.Context, req *v1.GetRandomDeviceReq) (res *v1.GetRandomDeviceRes, err error) {
// Obtain the list of online StackChan mac addresses (excluding the current user) from the websocket layer.
macList := web_socket.GetRandomStackChanDevice(req.Mac, 6)
if len(macList) == 0 {
res = (*v1.GetRandomDeviceRes)(&[]entity.Device{})
return res, nil
}
// Query device information based on the Mac list
list := make([]entity.Device, 0, len(macList))
err = dao.Device.
Ctx(ctx).
WhereIn("mac", macList).
Scan(&list)
if err != nil {
return nil, err
}
res = (*v1.GetRandomDeviceRes)(&list)
return res, nil
}
@@ -0,0 +1,21 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package device
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/api/device/v1"
)
func (c *ControllerV1) Update(ctx context.Context, req *v1.UpdateReq) (res *v1.UpdateRes, err error) {
_, err = dao.Device.Ctx(ctx).Data(do.Device{
Name: req.Name,
}).WherePri(req.Mac).Update()
return
}
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package device
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/api/device/v1"
)
func (c *ControllerV1) UpdateDeviceInfo(ctx context.Context, req *v1.UpdateDeviceInfoReq) (res *v1.UpdateDeviceInfoRes, err error) {
doDevice := do.Device{}
if req.Name != "" {
doDevice.Name = req.Name
}
_, err = dao.Device.Ctx(ctx).Data(doDevice).WherePri(req.Mac).Update()
if err != nil {
return nil, err
}
response := v1.UpdateDeviceInfoRes("Update successful")
return &response, nil
}
+10
View File
@@ -0,0 +1,10 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package file
@@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package file
import (
"stackChan/api/file"
)
type ControllerV1 struct{}
func NewV1() file.IFileV1 {
return &ControllerV1{}
}
@@ -0,0 +1,17 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package file
import (
"context"
"stackChan/internal/service"
"stackChan/api/file/v1"
)
func (c *ControllerV1) File(ctx context.Context, req *v1.FileReq) (res *v1.FileRes, err error) {
return service.AddFile(ctx, req)
}
@@ -0,0 +1,10 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package friend
@@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package friend
import (
"stackChan/api/friend"
)
type ControllerV1 struct{}
func NewV1() friend.IFriendV1 {
return &ControllerV1{}
}
@@ -0,0 +1,54 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package friend
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model/entity"
"github.com/gogf/gf/v2/errors/gerror"
"stackChan/api/friend/v1"
)
func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) {
if req.Mac == req.FriendMac {
return nil, gerror.New("You cannot add yourself as a friend")
}
macA := req.Mac
macB := req.FriendMac
var friend entity.DeviceFriend
err = dao.DeviceFriend.Ctx(ctx).
Where("mac_a", macA).
Where("mac_b", macB).
Scan(&friend)
if err != nil {
return nil, err
}
if friend.MacA == "" {
err = dao.DeviceFriend.Ctx(ctx).
Where("mac_a", macB).
Where("mac_b", macA).
Scan(&friend)
if err != nil {
return nil, err
}
}
if friend.MacA != "" {
res1 := v1.AddRes("Successfully added a friend")
return &res1, nil
}
_, err = dao.DeviceFriend.Ctx(ctx).Data(entity.DeviceFriend{
MacA: macA,
MacB: macB,
}).Insert()
if err != nil {
return nil, err
}
res2 := v1.AddRes("Successfully added a friend")
return &res2, nil
}
+10
View File
@@ -0,0 +1,10 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package post
@@ -0,0 +1,20 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish.
// =================================================================================
package post
import (
"stackChan/api/post"
)
type ControllerV1 struct{}
func NewV1() post.IPostV1 {
return &ControllerV1{}
}
@@ -0,0 +1,39 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package post
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"stackChan/api/post/v1"
)
func (c *ControllerV1) CreatePost(ctx context.Context, req *v1.CreatePostReq) (res *v1.CreatePostRes, err error) {
device, err := dao.Device.Ctx(ctx).Where("mac", req.Mac).One()
if err != nil {
return nil, err
}
if device == nil {
return nil, gerror.NewCode(gcode.CodeInvalidRequest, "The device does not exist or the Mac address is incorrect")
}
insertId, err := dao.DevicePost.Ctx(ctx).Data(do.DevicePost{
Mac: req.Mac,
ContentText: req.ContentText,
ContentImage: req.ContentImage,
}).InsertAndGetId()
if err != nil {
return nil, err
}
res = &v1.CreatePostRes{
Id: insertId,
}
return res, nil
}
@@ -0,0 +1,29 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package post
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/api/post/v1"
)
func (c *ControllerV1) CreatePostComment(ctx context.Context, req *v1.CreatePostCommentReq) (res *v1.CreatePostCommentRes, err error) {
id, err := dao.DevicePostComment.Ctx(ctx).Data(do.DevicePostComment{
PostId: req.PostId,
Mac: req.Mac,
Content: req.Content,
}).InsertAndGetId()
if err != nil {
return nil, err
}
res = &v1.CreatePostCommentRes{
Id: id,
}
return res, err
}
@@ -0,0 +1,21 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package post
import (
"context"
"stackChan/api/post/v1"
"stackChan/internal/dao"
)
func (c *ControllerV1) DeletePost(ctx context.Context, req *v1.DeletePostReq) (res *v1.DeletePostRes, err error) {
_, err = dao.DevicePost.Ctx(ctx).WherePri(req.Id).Delete()
if err != nil {
return nil, err
}
response := v1.DeletePostRes("Deletion successful")
return &response, nil
}
@@ -0,0 +1,42 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package post
import (
"context"
"errors"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/api/post/v1"
)
func (c *ControllerV1) DeletePostComment(ctx context.Context, req *v1.DeletePostCommentReq) (res *v1.DeletePostCommentRes, err error) {
var postComment model.PostComment
err = dao.DevicePostComment.Ctx(ctx).Where("id=?", req.Id).Scan(&postComment)
if err != nil {
return nil, err
}
if postComment.Id == 0 {
return nil, errors.New("post not found")
}
if postComment.Mac != req.Mac {
return nil, errors.New("no authority to delete")
}
_, err = dao.DevicePostComment.
Ctx(ctx).
Where("id = ? AND mac = ?", req.Id, req.Mac).
Delete()
if err != nil {
return nil, err
}
return &v1.DeletePostCommentRes{}, nil
}
@@ -0,0 +1,69 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package post
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/entity"
"stackChan/api/post/v1"
)
func (c *ControllerV1) GetPost(ctx context.Context, req *v1.GetPostReq) (res *v1.GetPostRes, err error) {
page := req.Page
pageSize := req.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
db := dao.DevicePost.Ctx(ctx).As("dp").
LeftJoin("device d", "dp.mac = d.mac")
_, err = db.Count("dp.id")
if err != nil {
return nil, err
}
var list []model.Post
err = db.Fields(
"dp.id",
"dp.mac",
"d.name",
"dp.content_text",
"dp.content_image",
"dp.created_at",
).Order("dp.created_at DESC").Limit((page-1)*pageSize, pageSize).Scan(&list)
if err != nil {
return nil, err
}
for i := 0; i < len(list); i++ {
var comments []*model.PostComment
err = dao.DevicePostComment.Ctx(ctx).Where("post_id", list[i].Id).Order("created_at ASC").Scan(&comments)
if err != nil {
return nil, err
}
for j := 0; j < len(comments); j++ {
mac := comments[j].Mac
var device entity.Device
err = dao.Device.Ctx(ctx).Where("mac", mac).Scan(&device)
if err != nil {
return nil, err
}
comments[j].Name = device.Name
}
list[i].PostCommentList = comments
}
res = (*v1.GetPostRes)(&list)
return res, nil
}
@@ -0,0 +1,57 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package post
import (
"context"
"stackChan/api/post/v1"
"stackChan/internal/dao"
"stackChan/internal/model"
"stackChan/internal/model/entity"
)
func (c *ControllerV1) GetPostComment(ctx context.Context, req *v1.GetPostCommentReq) (res *v1.GetPostCommentRes, err error) {
page := req.Page
pageSize := req.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
offset := (page - 1) * pageSize
var list []*model.PostComment
db := dao.DevicePostComment.Ctx(ctx).As("dp").Where("mac = ? AND post_id = ?", req.Mac, req.PostId)
total, err := db.Count()
if err != nil {
return
}
err = db.Order("created_at ASC").Limit(offset, pageSize).Scan(&list)
if err != nil {
return nil, err
}
for i := 0; i < len(list); i++ {
mac := list[i].Mac
var device entity.Device
err = dao.Device.Ctx(ctx).Where("mac", mac).Scan(&device)
if err != nil {
return nil, err
}
list[i].Name = device.Name
}
res = &v1.GetPostCommentRes{
List: list,
Total: total,
}
return res, nil
}
View File
+27
View File
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"stackChan/internal/dao/internal"
)
// deviceDao is the data access object for the table device.
// You can define custom methods on it to extend its functionality as needed.
type deviceDao struct {
*internal.DeviceDao
}
var (
// Device is a globally accessible object for table device operations.
Device = deviceDao{internal.NewDeviceDao()}
)
// Add your custom methods and functionality below.
+27
View File
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"stackChan/internal/dao/internal"
)
// deviceDanceDao is the data access object for the table device_dance.
// You can define custom methods on it to extend its functionality as needed.
type deviceDanceDao struct {
*internal.DeviceDanceDao
}
var (
// DeviceDance is a globally accessible object for table device_dance operations.
DeviceDance = deviceDanceDao{internal.NewDeviceDanceDao()}
)
// Add your custom methods and functionality below.
+27
View File
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"stackChan/internal/dao/internal"
)
// deviceFriendDao is the data access object for the table device_friend.
// You can define custom methods on it to extend its functionality as needed.
type deviceFriendDao struct {
*internal.DeviceFriendDao
}
var (
// DeviceFriend is a globally accessible object for table device_friend operations.
DeviceFriend = deviceFriendDao{internal.NewDeviceFriendDao()}
)
// Add your custom methods and functionality below.
+27
View File
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"stackChan/internal/dao/internal"
)
// devicePostDao is the data access object for the table device_post.
// You can define custom methods on it to extend its functionality as needed.
type devicePostDao struct {
*internal.DevicePostDao
}
var (
// DevicePost is a globally accessible object for table device_post operations.
DevicePost = devicePostDao{internal.NewDevicePostDao()}
)
// Add your custom methods and functionality below.
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"stackChan/internal/dao/internal"
)
// devicePostCommentDao is the data access object for the table device_post_comment.
// You can define custom methods on it to extend its functionality as needed.
type devicePostCommentDao struct {
*internal.DevicePostCommentDao
}
var (
// DevicePostComment is a globally accessible object for table device_post_comment operations.
DevicePostComment = devicePostCommentDao{internal.NewDevicePostCommentDao()}
)
// Add your custom methods and functionality below.
+81
View File
@@ -0,0 +1,81 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// DeviceDao is the data access object for the table device.
type DeviceDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns DeviceColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// DeviceColumns defines and stores column names for the table device.
type DeviceColumns struct {
Mac string //
Name string //
}
// deviceColumns holds the columns for the table device.
var deviceColumns = DeviceColumns{
Mac: "mac",
Name: "name",
}
// NewDeviceDao creates and returns a new DAO object for table data access.
func NewDeviceDao(handlers ...gdb.ModelHandler) *DeviceDao {
return &DeviceDao{
group: "default",
table: "device",
columns: deviceColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *DeviceDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *DeviceDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *DeviceDao) Columns() DeviceColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *DeviceDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *DeviceDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *DeviceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,89 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// DeviceDanceDao is the data access object for the table device_dance.
type DeviceDanceDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns DeviceDanceColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// DeviceDanceColumns defines and stores column names for the table device_dance.
type DeviceDanceColumns struct {
Id string //
Mac string // 设备MAC地址
DanceIndex string // 舞蹈编号,初始为1~3,可扩展
DanceData string // MotionData
CreatedAt string //
UpdatedAt string //
}
// deviceDanceColumns holds the columns for the table device_dance.
var deviceDanceColumns = DeviceDanceColumns{
Id: "id",
Mac: "mac",
DanceIndex: "dance_index",
DanceData: "dance_data",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
}
// NewDeviceDanceDao creates and returns a new DAO object for table data access.
func NewDeviceDanceDao(handlers ...gdb.ModelHandler) *DeviceDanceDao {
return &DeviceDanceDao{
group: "default",
table: "device_dance",
columns: deviceDanceColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *DeviceDanceDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *DeviceDanceDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *DeviceDanceDao) Columns() DeviceDanceColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *DeviceDanceDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *DeviceDanceDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *DeviceDanceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,81 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// DeviceFriendDao is the data access object for the table device_friend.
type DeviceFriendDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns DeviceFriendColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// DeviceFriendColumns defines and stores column names for the table device_friend.
type DeviceFriendColumns struct {
MacA string //
MacB string //
}
// deviceFriendColumns holds the columns for the table device_friend.
var deviceFriendColumns = DeviceFriendColumns{
MacA: "mac_a",
MacB: "mac_b",
}
// NewDeviceFriendDao creates and returns a new DAO object for table data access.
func NewDeviceFriendDao(handlers ...gdb.ModelHandler) *DeviceFriendDao {
return &DeviceFriendDao{
group: "default",
table: "device_friend",
columns: deviceFriendColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *DeviceFriendDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *DeviceFriendDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *DeviceFriendDao) Columns() DeviceFriendColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *DeviceFriendDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *DeviceFriendDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *DeviceFriendDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,87 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// DevicePostDao is the data access object for the table device_post.
type DevicePostDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns DevicePostColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// DevicePostColumns defines and stores column names for the table device_post.
type DevicePostColumns struct {
Id string //
Mac string // 发帖设备MAC
ContentText string // 文本内容
ContentImage string // 图片URL
CreatedAt string // 发帖时间
}
// devicePostColumns holds the columns for the table device_post.
var devicePostColumns = DevicePostColumns{
Id: "id",
Mac: "mac",
ContentText: "content_text",
ContentImage: "content_image",
CreatedAt: "created_at",
}
// NewDevicePostDao creates and returns a new DAO object for table data access.
func NewDevicePostDao(handlers ...gdb.ModelHandler) *DevicePostDao {
return &DevicePostDao{
group: "default",
table: "device_post",
columns: devicePostColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *DevicePostDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *DevicePostDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *DevicePostDao) Columns() DevicePostColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *DevicePostDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *DevicePostDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *DevicePostDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,87 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// DevicePostCommentDao is the data access object for the table device_post_comment.
type DevicePostCommentDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns DevicePostCommentColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// DevicePostCommentColumns defines and stores column names for the table device_post_comment.
type DevicePostCommentColumns struct {
Id string //
PostId string // 帖子ID
Mac string // 评论设备MAC
Content string // 评论内容
CreatedAt string // 评论时间
}
// devicePostCommentColumns holds the columns for the table device_post_comment.
var devicePostCommentColumns = DevicePostCommentColumns{
Id: "id",
PostId: "post_id",
Mac: "mac",
Content: "content",
CreatedAt: "created_at",
}
// NewDevicePostCommentDao creates and returns a new DAO object for table data access.
func NewDevicePostCommentDao(handlers ...gdb.ModelHandler) *DevicePostCommentDao {
return &DevicePostCommentDao{
group: "default",
table: "device_post_comment",
columns: devicePostCommentColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *DevicePostCommentDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *DevicePostCommentDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *DevicePostCommentDao) Columns() DevicePostCommentColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *DevicePostCommentDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *DevicePostCommentDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *DevicePostCommentDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
@@ -0,0 +1,81 @@
// ==========================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// ==========================================================================
package internal
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// SqliteSequenceDao is the data access object for the table sqlite_sequence.
type SqliteSequenceDao struct {
table string // table is the underlying table name of the DAO.
group string // group is the database configuration group name of the current DAO.
columns SqliteSequenceColumns // columns contains all the column names of Table for convenient usage.
handlers []gdb.ModelHandler // handlers for customized model modification.
}
// SqliteSequenceColumns defines and stores column names for the table sqlite_sequence.
type SqliteSequenceColumns struct {
Name string //
Seq string //
}
// sqliteSequenceColumns holds the columns for the table sqlite_sequence.
var sqliteSequenceColumns = SqliteSequenceColumns{
Name: "name",
Seq: "seq",
}
// NewSqliteSequenceDao creates and returns a new DAO object for table data access.
func NewSqliteSequenceDao(handlers ...gdb.ModelHandler) *SqliteSequenceDao {
return &SqliteSequenceDao{
group: "default",
table: "sqlite_sequence",
columns: sqliteSequenceColumns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *SqliteSequenceDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *SqliteSequenceDao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *SqliteSequenceDao) Columns() SqliteSequenceColumns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *SqliteSequenceDao) Group() string {
return dao.group
}
// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation.
func (dao *SqliteSequenceDao) Ctx(ctx context.Context) *gdb.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// Note: Do not commit or roll back the transaction in function f,
// as it is automatically handled by this function.
func (dao *SqliteSequenceDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
+27
View File
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
// =================================================================================
// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed.
// =================================================================================
package dao
import (
"stackChan/internal/dao/internal"
)
// sqliteSequenceDao is the data access object for the table sqlite_sequence.
// You can define custom methods on it to extend its functionality as needed.
type sqliteSequenceDao struct {
*internal.SqliteSequenceDao
}
var (
// SqliteSequence is a globally accessible object for table sqlite_sequence operations.
SqliteSequence = sqliteSequenceDao{internal.NewSqliteSequenceDao()}
)
// Add your custom methods and functionality below.
View File
View File
+11
View File
@@ -0,0 +1,11 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package model
type DeviceInfo struct {
Mac string `json:"mac" v:"required" description:"Mac address"`
Name string `json:"name" v:"required" description:"Name"`
}
+16
View File
@@ -0,0 +1,16 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
)
// Device is the golang structure of table device for DAO operations like Where/Data.
type Device struct {
g.Meta `orm:"table:device, do:true"`
Mac any //
Name any //
}
+21
View File
@@ -0,0 +1,21 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// DeviceDance is the golang structure of table device_dance for DAO operations like Where/Data.
type DeviceDance struct {
g.Meta `orm:"table:device_dance, do:true"`
Id any //
Mac any // 设备MAC地址
DanceIndex any // 舞蹈编号,初始为1~3,可扩展
DanceData any // MotionData
CreatedAt *gtime.Time //
UpdatedAt *gtime.Time //
}
+16
View File
@@ -0,0 +1,16 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
)
// DeviceFriend is the golang structure of table device_friend for DAO operations like Where/Data.
type DeviceFriend struct {
g.Meta `orm:"table:device_friend, do:true"`
MacA any //
MacB any //
}
+20
View File
@@ -0,0 +1,20 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// DevicePost is the golang structure of table device_post for DAO operations like Where/Data.
type DevicePost struct {
g.Meta `orm:"table:device_post, do:true"`
Id any //
Mac any // 发帖设备MAC
ContentText any // 文本内容
ContentImage any // 图片URL
CreatedAt *gtime.Time // 发帖时间
}
@@ -0,0 +1,20 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// DevicePostComment is the golang structure of table device_post_comment for DAO operations like Where/Data.
type DevicePostComment struct {
g.Meta `orm:"table:device_post_comment, do:true"`
Id any //
PostId any // 帖子ID
Mac any // 评论设备MAC
Content any // 评论内容
CreatedAt *gtime.Time // 评论时间
}
@@ -0,0 +1,16 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package do
import (
"github.com/gogf/gf/v2/frame/g"
)
// SqliteSequence is the golang structure of table sqlite_sequence for DAO operations like Where/Data.
type SqliteSequence struct {
g.Meta `orm:"table:sqlite_sequence, do:true"`
Name any //
Seq any //
}
+11
View File
@@ -0,0 +1,11 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
// Device is the golang structure for table device.
type Device struct {
Mac string `json:"mac" orm:"mac" description:""` //
Name string `json:"name" orm:"name" description:""` //
}
@@ -0,0 +1,19 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// DeviceDance is the golang structure for table device_dance.
type DeviceDance struct {
Id int64 `json:"id" orm:"id" description:""` //
Mac string `json:"mac" orm:"mac" description:"设备MAC地址"` // 设备MAC地址
DanceIndex int `json:"danceIndex" orm:"dance_index" description:"舞蹈编号,初始为1~3,可扩展"` // 舞蹈编号,初始为1~3,可扩展
DanceData string `json:"danceData" orm:"dance_data" description:"MotionData"` // MotionData
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` //
UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:""` //
}
@@ -0,0 +1,11 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
// DeviceFriend is the golang structure for table device_friend.
type DeviceFriend struct {
MacA string `json:"macA" orm:"mac_a" description:""` //
MacB string `json:"macB" orm:"mac_b" description:""` //
}
@@ -0,0 +1,18 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// DevicePost is the golang structure for table device_post.
type DevicePost struct {
Id int64 `json:"id" orm:"id" description:""` //
Mac string `json:"mac" orm:"mac" description:"发帖设备MAC"` // 发帖设备MAC
ContentText string `json:"contentText" orm:"content_text" description:"文本内容"` // 文本内容
ContentImage string `json:"contentImage" orm:"content_image" description:"图片URL"` // 图片URL
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"发帖时间"` // 发帖时间
}
@@ -0,0 +1,18 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
import (
"github.com/gogf/gf/v2/os/gtime"
)
// DevicePostComment is the golang structure for table device_post_comment.
type DevicePostComment struct {
Id int64 `json:"id" orm:"id" description:""` //
PostId int64 `json:"postId" orm:"post_id" description:"帖子ID"` // 帖子ID
Mac string `json:"mac" orm:"mac" description:"评论设备MAC"` // 评论设备MAC
Content string `json:"content" orm:"content" description:"评论内容"` // 评论内容
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"评论时间"` // 评论时间
}
@@ -0,0 +1,11 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package entity
// SqliteSequence is the golang structure for table sqlite_sequence.
type SqliteSequence struct {
Name string `json:"name" orm:"name" description:""` //
Seq string `json:"seq" orm:"seq" description:""` //
}
+42
View File
@@ -0,0 +1,42 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package model
type ExpressionData struct {
Type string `json:"type"`
LeftEye ExpressionItem `json:"leftEye"`
RightEye ExpressionItem `json:"rightEye"`
Mouth ExpressionItem `json:"mouth"`
}
type ExpressionItem struct {
X int `json:"x"`
Y int `json:"y"`
Rotation int `json:"rotation"`
Weight int `json:"weight"`
Size int `json:"size"`
}
type MotionData struct {
Type string `json:"type"`
PitchServo MotionDataItem `json:"pitchServo"`
YawServo MotionDataItem `json:"yawServo"`
}
type MotionDataItem struct {
Angle int `json:"angle"`
Speed int `json:"speed"`
Rotate int `json:"rotate"`
}
type DanceData struct {
LeftEye ExpressionItem `json:"leftEye"`
RightEye ExpressionItem `json:"rightEye"`
Mouth ExpressionItem `json:"mouth"`
PitchServo MotionDataItem `json:"pitchServo"`
YawServo MotionDataItem `json:"yawServo"`
DurationMs int `json:"durationMs"`
}
+27
View File
@@ -0,0 +1,27 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package model
import "github.com/gogf/gf/v2/os/gtime"
type Post struct {
Id int64 `json:"id" orm:"id" description:"帖子ID"`
Mac string `json:"mac" orm:"mac" description:"发帖设备MAC"`
Name string `json:"name" orm:"name" description:"发帖设备名称"`
ContentText string `json:"contentText" orm:"content_text" description:"文本内容"`
ContentImage string `json:"contentImage" orm:"content_image" description:"图片URL"`
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"发帖时间"`
PostCommentList []*PostComment `json:"postCommentList" orm:"postCommentList" description:"评论"`
}
type PostComment struct {
Id int `json:"id" orm:"id" description:""` //
PostId int `json:"postId" orm:"post_id" description:""` //
Mac string `json:"mac" orm:"mac" description:""` //
Name string `json:"name" orm:"name" description:""` //
Content string `json:"content" orm:"content" description:""` //
CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:""` //
}
+6
View File
@@ -0,0 +1,6 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package packed
View File
+40
View File
@@ -0,0 +1,40 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package service
import (
"context"
"stackChan/internal/dao"
"stackChan/internal/model/do"
"stackChan/internal/model/entity"
)
func CreateMacIfNotExists(ctx context.Context, mac string) (id int64, err error) {
count, err := dao.Device.Ctx(ctx).Where("mac = ?", mac).Count()
if err != nil {
return 0, err
}
if count > 0 {
return 0, nil
}
id, err = dao.Device.Ctx(ctx).Data(do.Device{
Mac: mac,
}).InsertAndGetId()
if err != nil {
return 0, err
}
return id, nil
}
func GetDeviceName(ctx context.Context, mac string) (name string, err error) {
var device entity.Device
err = dao.Device.Ctx(ctx).Where("mac = ?", mac).Fields("name").Scan(&device)
if err != nil {
return "", err
}
return device.Name, nil
}
+13
View File
@@ -0,0 +1,13 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package service
import (
"testing"
)
func TestCreateMac(t *testing.T) {
}
+70
View File
@@ -0,0 +1,70 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package service
import (
"context"
"io"
"os"
"path/filepath"
v1 "stackChan/api/file/v1"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
)
func AddFile(ctx context.Context, req *v1.FileReq) (res *v1.FileRes, err error) {
currentDir, err := os.Getwd()
if err != nil {
return nil, err
}
baseDir := "file"
fileDir := filepath.Join(currentDir, baseDir)
if req.Directory != "" {
fileDir = filepath.Join(fileDir, req.Directory)
}
if _, err := os.Stat(fileDir); os.IsNotExist(err) {
if err := os.MkdirAll(fileDir, os.ModePerm); err != nil {
return nil, err
}
}
if req.File.Size == 0 || req.Name == "" {
return nil, gerror.NewCode(gcode.CodeInvalidParameter, "file or filename is empty")
}
filePath := filepath.Join(fileDir, req.Name)
file, err := req.File.Open()
if err != nil {
return nil, err
}
fileBytes, err := io.ReadAll(file)
if err != nil {
return nil, err
}
if err := os.WriteFile(filePath, fileBytes, os.ModePerm); err != nil {
return nil, err
}
_ = file.Close()
var returnPath string
if req.Directory != "" {
returnPath = filepath.Join(baseDir, req.Directory, req.Name)
} else {
returnPath = filepath.Join(baseDir, req.Name)
}
return &v1.FileRes{
Path: returnPath,
}, nil
}
+830
View File
@@ -0,0 +1,830 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package web_socket
import (
"context"
"encoding/binary"
"errors"
"math/rand"
"net"
"net/http"
"stackChan/internal/service"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gorilla/websocket"
)
const (
Opus byte = 0x01
Jpeg byte = 0x02
ControlAvatar byte = 0x03
ControlMotion byte = 0x04
OnCamera byte = 0x05
OffCamera byte = 0x06
TextMessage byte = 0x07
RequestCall byte = 0x09
RefuseCall byte = 0x0A
AgreeCall byte = 0x0B
HangupCall byte = 0x0C
UpdateDeviceName byte = 0x0D
GetDeviceName byte = 0x0E
inCall byte = 0x0F
ping byte = 0x10
pong byte = 0x11
OnPhoneScreen byte = 0x12
OffPhoneScreen byte = 0x13
Dance byte = 0x14
GetAvatarPosture byte = 0x15
DeviceOffline byte = 0x16
DeviceOnline byte = 0x17
)
var (
wsUpGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) {
logger.Errorf(r.Context(), "WebSocket Upgrade failed: %v", reason)
},
}
logger = g.Log()
stackChanClientPool = sync.Map{}
appClientPool = sync.Map{}
)
// AppClient indicates a WebSocket client connection on the App side
type AppClient struct {
Mac string
Conn *websocket.Conn
mu *sync.RWMutex
DeviceId string
LastTime time.Time
}
// StackChanClient indicates a WebSocket client connection for the device end of a StackChan
type StackChanClient struct {
Mac string
Conn *websocket.Conn
mu *sync.RWMutex
CameraSubscriptionList []*AppClient
CallAppClient *AppClient
phoneScreen bool
LastTime time.Time
}
func Handler(r *ghttp.Request) {
ctx := r.Context()
mac := r.Get("mac").String()
deviceType := r.Get("deviceType").String()
if mac == "" || deviceType == "" {
r.Response.Write("The mac and deviceType parameters are empty.")
return
}
ws, err := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil)
if err != nil {
r.Response.Write(err.Error())
return
}
if deviceType == "StackChan" {
isHave := false
var client *StackChanClient
stackChanClientPool.Range(func(key, value any) bool {
macAddr := key.(string)
stackChanClient := value.(*StackChanClient)
if macAddr == mac {
isHave = true
client = stackChanClient
client.mu.Lock()
client.Conn = ws
if client.CallAppClient != nil {
reconnectMsg := createStringMessage(TextMessage, "The equipment has been reconnected.")
msgType := websocket.BinaryMessage
forwardMessage(ctx, client.CallAppClient.Conn, &msgType, reconnectMsg, client.CallAppClient.mu)
}
if len(client.CameraSubscriptionList) > 0 {
onMsg := createMessage(OnCamera, nil)
onType := websocket.BinaryMessage
forwardMessage(ctx, client.Conn, &onType, onMsg, client.mu)
}
client.LastTime = time.Now()
client.mu.Unlock()
return false
}
return true
})
if !isHave {
client = &StackChanClient{
Mac: mac,
Conn: ws,
mu: &sync.RWMutex{},
phoneScreen: false,
LastTime: time.Now(),
}
addStackChenClient(ctx, client)
} else {
// notify app
onlineMsg := createStringMessage(DeviceOnline, "Your StackChan has been launched.")
msgType := websocket.BinaryMessage
// Notify App
appClients := getAppClients(client.Mac)
for _, appClient := range appClients {
forwardMessage(ctx, appClient.Conn, &msgType, onlineMsg, appClient.mu)
}
}
logger.Info(ctx, "There is a StackChen connected to the service.", client.Mac)
defer func() {
logger.Info(ctx, "There is a StackChan that has disconnected.", mac, deviceType)
}()
for {
messageType, msg, err := ws.ReadMessage()
if err != nil {
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
logger.Infof(ctx, "StackChan Normal disconnection: mac=%s, deviceType=%s, Reason=%v", mac, deviceType, err)
break
}
var ne net.Error
if errors.As(err, &ne) && ne.Temporary() {
logger.Infof(ctx, "StackChan Temporary network error. Continue reading.: mac=%s,deviceType=%s,Error=%v", mac, deviceType, err)
continue
}
logger.Errorf(ctx, "StackChan Abnormal disconnection: mac=%s, deviceType=%s, Error=%v", mac, deviceType, err)
break
}
//logger.Infof(ctx, "收到StackChan端消息%d", len(msg))
readStackChanMessage(ctx, client, &messageType, &msg)
}
} else if deviceType == "App" {
deviceId := r.Get("deviceId").String()
if deviceId == "" {
r.Response.Write("The deviceId parameter in the App end is empty.")
return
}
var client *AppClient
found := false
clients := getAppClients(mac)
for _, appClient := range clients {
if appClient.DeviceId == deviceId && appClient.Mac == mac {
// Already available. Update the connection.
client = appClient
client.mu.Lock()
client.Conn = ws
client.mu.Unlock()
client.LastTime = time.Now()
found = true
break
}
}
if !found {
client = &AppClient{
Mac: mac,
Conn: ws,
DeviceId: deviceId,
mu: &sync.RWMutex{},
LastTime: time.Now(),
}
addAppClient(client)
}
logger.Info(ctx, "There is an App connected to the service.", client.Mac)
// check StackChan status
stackChanClient := getStackChanClient(client.Mac)
if stackChanClient == nil {
offlineMsg := createStringMessage(DeviceOffline, "Your StackChan is offline.")
msgType := websocket.BinaryMessage
forwardMessage(ctx, client.Conn, &msgType, offlineMsg, client.mu)
} else {
onlineMsg := createStringMessage(DeviceOnline, "Your StackChan has been launched.")
msgType := websocket.BinaryMessage
forwardMessage(ctx, client.Conn, &msgType, onlineMsg, client.mu)
}
defer func() {
logger.Info(ctx, "There is an App that has disconnected.", mac, deviceType)
}()
for {
messageType, msg, err := ws.ReadMessage()
if err != nil {
var ne net.Error
if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
logger.Infof(ctx, "App Normal disconnection: mac=%s, deviceType=%s, Error=%v", mac, deviceType, err)
break
}
if errors.As(err, &ne) && ne.Temporary() {
logger.Infof(ctx, "App Temporary network error. Continue reading.: mac=%s,deviceType=%s,Error=%v", mac, deviceType, err)
continue
}
if errors.As(err, &ne) && ne.Timeout() {
logger.Infof(ctx, "App Timeout disconnection: mac=%s, deviceType=%s", mac, deviceType)
break
}
logger.Errorf(ctx, "App Abnormal disconnection: mac=%s, deviceType=%s, Error=%v", mac, deviceType, err)
break
}
client.LastTime = time.Now()
readAppClientMessage(ctx, client, &messageType, &msg)
}
}
}
// addStackChenClient adds a StackChan client to the connection pool and ensures the MAC is registered
func addStackChenClient(ctx context.Context, c *StackChanClient) {
stackChanClientPool.Store(c.Mac, c)
_, _ = service.CreateMacIfNotExists(ctx, c.Mac)
}
// addAppClient adds an App client to the App connection pool (multiple Apps per MAC allowed)
func addAppClient(c *AppClient) {
val, _ := appClientPool.Load(c.Mac)
var clients []*AppClient
if val == nil {
clients = []*AppClient{c}
} else {
clients = val.([]*AppClient)
clients = append(clients, c)
}
appClientPool.Store(c.Mac, clients)
}
// getAppClients gets all App clients for the specified MAC address
func getAppClients(mac string) []*AppClient {
if val, ok := appClientPool.Load(mac); ok {
return val.([]*AppClient)
}
return nil
}
// getStackChanClient gets the StackChan client corresponding to the specified MAC address
func getStackChanClient(mac string) *StackChanClient {
if val, ok := stackChanClientPool.Load(mac); ok {
return val.(*StackChanClient)
}
return nil
}
// parseBinaryMessage parses a custom binary protocol message, returns type, length, payload, and success status
func parseBinaryMessage(ctx context.Context, msg *[]byte) (byte, int, []byte, bool) {
if len(*msg) < 1+4 {
logger.Warning(ctx, "Message too short, cannot parse header, message not forwarded")
return 0, 0, nil, false
}
msgType := (*msg)[0]
dataLen := int(binary.BigEndian.Uint32((*msg)[1:5]))
payload := (*msg)[5 : 5+dataLen]
if len(*msg)-5 != dataLen {
logger.Warningf(ctx, "Length mismatch: header says %d, actual is %d, message not forwarded", dataLen, len(*msg)-5)
return 0, 0, nil, false
}
return msgType, dataLen, payload, true
}
// StartPingTime sends Ping messages to all connected clients for heartbeat detection
func StartPingTime(ctx context.Context) {
message := createMessage(ping, nil)
messageType := websocket.BinaryMessage
// Iterate over StackChanClientPool
stackChanClientPool.Range(func(_, value any) bool {
client := value.(*StackChanClient)
forwardMessage(ctx, client.Conn, &messageType, message, client.mu)
return true // continue iteration
})
// Iterate over AppClientPool
appClientPool.Range(func(_, value any) bool {
clients := value.([]*AppClient)
for _, client := range clients {
forwardMessage(ctx, client.Conn, &messageType, message, client.mu)
}
return true // continue iteration
})
}
// CheckExpiredLinks checks and cleans up App client connections that have been inactive for over 60 seconds
func CheckExpiredLinks(ctx context.Context) {
now := time.Now()
var expiredClients []*AppClient
// First, iterate over AppClientPool
appClientPool.Range(func(mac, value any) bool {
clients := value.([]*AppClient)
newClients := clients[:0]
for _, client := range clients {
if now.Sub(client.LastTime) > time.Second*15 {
// Found expired client
// Iterate over StackChanClientPool to clean up CallAppClient and CameraSubscriptionList
stackChanClientPool.Range(func(_, scValue any) bool {
stackChanClient, ok := scValue.(*StackChanClient)
stackChanClient.mu.Lock()
if !ok {
return true
}
// Clean up CallAppClient
if stackChanClient.CallAppClient == client {
stackChanClient.CallAppClient = nil
}
// Update camera subscription list
newCamera := stackChanClient.CameraSubscriptionList[:0]
removedCamera := false
for _, sub := range stackChanClient.CameraSubscriptionList {
if sub != client {
newCamera = append(newCamera, sub)
} else {
removedCamera = true
}
}
stackChanClient.CameraSubscriptionList = newCamera
stackChanClient.mu.Unlock()
if removedCamera && len(newCamera) == 0 {
msg := createMessage(OffCamera, nil)
msgType := websocket.BinaryMessage
forwardMessage(ctx, stackChanClient.Conn, &msgType, msg, stackChanClient.mu)
}
return true
})
expiredClients = append(expiredClients, client)
} else {
newClients = append(newClients, client)
}
}
if len(newClients) == 0 {
appClientPool.Delete(mac)
} else {
appClientPool.Store(mac, newClients)
}
return true
})
for _, client := range expiredClients {
logger.Infof(ctx, "Kicked out an expired App client: %s", client.Mac)
err := client.Conn.Close()
if err != nil {
}
}
}
// readStackChanMessage handles messages from the StackChan device side
func readStackChanMessage(ctx context.Context, client *StackChanClient, messageType *int, msg *[]byte) {
if *messageType == websocket.BinaryMessage {
msgType, _, _, ok := parseBinaryMessage(ctx, msg)
if !ok {
return
}
switch msgType {
case pong:
break
case ControlAvatar, ControlMotion, OnCamera, OffCamera:
break
case RefuseCall:
// Refused call, remove and notify appClient
appClient := client.CallAppClient
if appClient != nil {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
client.mu.Lock()
client.CallAppClient = nil
client.mu.Unlock()
}
break
case AgreeCall:
// Agreed to call
appClient := client.CallAppClient
if appClient != nil {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
client.mu.Lock()
client.CameraSubscriptionList = append(client.CameraSubscriptionList, appClient)
client.mu.Unlock()
if len(client.CameraSubscriptionList) == 1 {
onMsg := createMessage(OnCamera, nil)
onType := websocket.BinaryMessage
forwardMessage(ctx, client.Conn, &onType, onMsg, client.mu)
}
}
break
case HangupCall:
// Hang up call
appClient := client.CallAppClient
if appClient != nil {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
// Remove the client from the subscription list
newList := client.CameraSubscriptionList[:0]
for _, subClient := range client.CameraSubscriptionList {
if subClient != appClient {
newList = append(newList, subClient)
}
}
client.mu.Lock()
client.CameraSubscriptionList = newList
client.mu.Unlock()
// If the subscription list is empty, notify to turn off the camera
if len(client.CameraSubscriptionList) == 0 {
offMsg := createMessage(OffCamera, nil)
offType := websocket.BinaryMessage
forwardMessage(ctx, client.Conn, &offType, offMsg, client.mu)
}
}
break
case GetDeviceName:
// Query device name
name, err := service.GetDeviceName(ctx, client.Mac)
if err != nil {
return
}
if name == "" {
logger.Infof(ctx, "Queried device nickname is empty")
return
}
newMsg := createStringMessage(GetDeviceName, name)
forwardMessage(ctx, client.Conn, messageType, newMsg, client.mu)
break
case Opus:
break
case Jpeg:
subscribers := client.CameraSubscriptionList
if len(subscribers) > 0 {
var isAll = true
for _, subClient := range subscribers {
if subClient.Conn != nil {
isAll = false
}
forwardMessage(ctx, subClient.Conn, messageType, msg, subClient.mu)
}
if isAll {
msg = createMessage(OffCamera, nil)
forwardMessage(ctx, client.Conn, messageType, msg, client.mu)
}
} else {
msg = createMessage(OffCamera, nil)
forwardMessage(ctx, client.Conn, messageType, msg, client.mu)
}
break
case GetAvatarPosture:
appClients := getAppClients(client.Mac)
for _, appClient := range appClients {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
}
break
default:
logger.Infof(ctx, "Unknown binary msgType: %d", msgType)
appClients := getAppClients(client.Mac)
if appClients != nil {
for _, appClient := range appClients {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
}
}
}
} else if *messageType == websocket.TextMessage {
appClients := getAppClients(client.Mac)
if appClients != nil {
for _, appClient := range appClients {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
}
}
} else if *messageType == websocket.PingMessage {
logger.Info(ctx, "Received ping message from StackChan side")
}
}
// readAppClientMessage handles messages from the App side
func readAppClientMessage(ctx context.Context, client *AppClient, messageType *int, msg *[]byte) {
if *messageType == websocket.BinaryMessage {
msgType, _, payload, ok := parseBinaryMessage(ctx, msg)
if !ok {
return
}
switch msgType {
case pong:
break
case GetDeviceName:
// Query device name
name, err := service.GetDeviceName(ctx, client.Mac)
if err != nil {
logger.Errorf(ctx, err.Error())
return
}
if name == "" {
logger.Infof(ctx, "Queried device nickname is empty")
return
}
newMsg := createStringMessage(GetDeviceName, name)
logger.Infof(ctx, "Device name found, returning: "+name)
forwardMessage(ctx, client.Conn, messageType, newMsg, client.mu)
break
case UpdateDeviceName:
stackChanClient := getStackChanClient(client.Mac)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
}
appClients := getAppClients(client.Mac)
for _, appClient := range appClients {
forwardMessage(ctx, appClient.Conn, messageType, msg, appClient.mu)
}
break
case Opus:
break
case Jpeg:
if len(payload) < 12 {
logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload)
return
}
macAddrBytes := payload[:12]
data := payload[12:]
macAddr := string(macAddrBytes)
newMsg := createMessage(msgType, data)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
if stackChanClient.phoneScreen {
forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu)
}
}
break
case ControlAvatar, ControlMotion:
if len(payload) < 12 {
logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload)
return
}
macAddrBytes := payload[:12]
data := payload[12:]
macAddr := string(macAddrBytes)
newMsg := createMessage(msgType, data)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu)
} else {
logger.Infof(ctx, "StackChan is currently offline")
}
break
case TextMessage:
if len(payload) < 12 {
logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload)
return
}
macAddr := string(payload[:12])
data := payload[12:]
newMsg := createMessage(msgType, data)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu)
}
appClients := getAppClients(macAddr)
if appClients != nil {
for _, appClient := range appClients {
forwardMessage(ctx, appClient.Conn, messageType, newMsg, appClient.mu)
}
}
break
case RequestCall:
// Request call
if len(payload) < 12 {
logger.Warningf(ctx, "Payload too short, cannot parse MAC address: %v", payload)
return
}
macAddr := string(payload[:12])
data := payload[12:]
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
stackChanClient.mu.Lock()
if stackChanClient.CallAppClient == nil || stackChanClient.CallAppClient == client {
stackChanClient.CallAppClient = client
stackChanClient.mu.Unlock()
newMsg := createMessage(msgType, data)
forwardMessage(ctx, stackChanClient.Conn, messageType, newMsg, stackChanClient.mu)
} else {
stackChanClient.mu.Unlock()
// Notify App that the other side is already in a call
newMsg := createStringMessage(inCall, "The other party is currently in a call")
forwardMessage(ctx, client.Conn, messageType, newMsg, client.mu)
}
}
break
case HangupCall:
stackChanClientPool.Range(func(_, value any) bool {
stackChanClient := value.(*StackChanClient)
if stackChanClient.CallAppClient == client {
// Found corresponding call
stackChanClient.mu.Lock()
stackChanClient.CallAppClient = nil
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
newList := stackChanClient.CameraSubscriptionList[:0]
for _, sub := range stackChanClient.CameraSubscriptionList {
if sub != client {
newList = append(newList, sub)
}
}
stackChanClient.CameraSubscriptionList = newList
stackChanClient.mu.Unlock()
if len(stackChanClient.CameraSubscriptionList) == 0 {
offMsg := createMessage(OffCamera, nil)
offType := websocket.BinaryMessage
forwardMessage(ctx, stackChanClient.Conn, &offType, offMsg, stackChanClient.mu)
}
return false
}
return true
})
break
case OnCamera:
macAddr := string(payload)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
stackChanClient.mu.Lock()
alreadySubscribed := false
for _, sub := range stackChanClient.CameraSubscriptionList {
if sub == client {
alreadySubscribed = true
break
}
}
if !alreadySubscribed {
stackChanClient.CameraSubscriptionList = append(stackChanClient.CameraSubscriptionList, client)
stackChanClient.mu.Unlock()
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
} else {
stackChanClient.mu.Unlock()
}
}
break
case OffCamera:
macAddr := string(payload)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
stackChanClient.mu.Lock()
existed := false
newList := stackChanClient.CameraSubscriptionList[:0]
for _, subClient := range stackChanClient.CameraSubscriptionList {
if subClient == client {
existed = true
} else {
newList = append(newList, subClient)
}
}
shouldNotify := existed && len(newList) == 0
stackChanClient.CameraSubscriptionList = newList
stackChanClient.mu.Unlock()
if shouldNotify {
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
}
}
break
case OnPhoneScreen:
// Show phone screen
macAddr := string(payload)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
stackChanClient.mu.Lock()
if stackChanClient.phoneScreen == false {
stackChanClient.phoneScreen = true
stackChanClient.mu.Unlock()
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
} else {
stackChanClient.mu.Unlock()
}
}
break
case OffPhoneScreen:
// Hide phone screen
macAddr := string(payload)
stackChanClient := getStackChanClient(macAddr)
if stackChanClient != nil {
stackChanClient.mu.Lock()
if stackChanClient.phoneScreen == true {
stackChanClient.phoneScreen = false
stackChanClient.mu.Unlock()
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
} else {
stackChanClient.mu.Unlock()
}
}
break
case Dance:
// Dance message
stackChanClient := getStackChanClient(client.Mac)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
}
break
case GetAvatarPosture:
stackChanClient := getStackChanClient(client.Mac)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
}
default:
logger.Infof(ctx, "Unknown binary msgType: %d", msgType)
stackChanClient := getStackChanClient(client.Mac)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
}
}
} else if *messageType == websocket.TextMessage {
// Directly forward other message types
stackChanClient := getStackChanClient(client.Mac)
if stackChanClient != nil {
forwardMessage(ctx, stackChanClient.Conn, messageType, msg, stackChanClient.mu)
}
} else if *messageType == websocket.PingMessage {
logger.Info(ctx, "Received ping message from App side")
}
}
// forwardMessage forwards a message to the specified connection, with mutex for concurrency safety
func forwardMessage(ctx context.Context, conn *websocket.Conn, messageType *int, msg *[]byte, mu *sync.RWMutex) {
if conn == nil {
logger.Infof(ctx, "StackChan is currently offline")
return
}
mu.Lock()
defer mu.Unlock()
err := conn.WriteMessage(*messageType, *msg)
if err != nil {
//logger.Info(ctx, "Message forwarding failed: %v", err)
} else {
//logger.Info(ctx, "Message sent successfully")
}
}
// createMessage encapsulates a binary message according to custom protocol (type + length + data)
func createMessage(msgType byte, data []byte) *[]byte {
var dataLen int
if data != nil {
dataLen = len(data)
} else {
dataLen = 0
}
msg := make([]byte, 1+4+dataLen)
msg[0] = msgType
binary.BigEndian.PutUint32(msg[1:5], uint32(dataLen))
if dataLen > 0 {
copy(msg[5:], data)
}
return &msg
}
// createStringMessage creates a binary message with a string payload
func createStringMessage(msgType byte, data string) *[]byte {
return createMessage(msgType, []byte(data))
}
// GetRandomStackChanDevice get Random StackChan Device list
func GetRandomStackChanDevice(userMac string, maxLength int) (list []string) {
if maxLength <= 0 {
return []string{}
}
var macs []string
stackChanClientPool.Range(func(key, value interface{}) bool {
mac := key.(string)
client := value.(*StackChanClient)
if mac == userMac {
return true
}
client.mu.RLock()
online := client.Conn != nil
client.mu.RUnlock()
if online {
macs = append(macs, mac)
}
return true
})
if len(macs) == 0 {
return []string{}
}
r := rand.New(rand.NewSource(time.Now().UnixNano()))
r.Shuffle(len(macs), func(i, j int) {
macs[i], macs[j] = macs[j], macs[i]
})
if len(macs) > maxLength {
macs = macs[:maxLength]
}
return macs
}
+18
View File
@@ -0,0 +1,18 @@
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
package main
import (
"stackChan/internal/cmd"
_ "stackChan/internal/packed"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/os/gctx"
)
func main() {
cmd.Main.Run(gctx.GetInitCtx())
}
+15
View File
@@ -0,0 +1,15 @@
# https://goframe.org/docs/web/server-config-file-template
server:
address: ":12800"
# openapiPath: "/api.json"
swaggerPath: "/swagger"
# https://goframe.org/docs/core/glog-config
logger:
level : "all"
stdout: true
# https://goframe.org/docs/core/gdb-config-file
database:
default:
link: "sqlite::@file(/stackChan.sqlite)"
@@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: template-single
labels:
app: template-single
spec:
replicas: 1
selector:
matchLabels:
app: template-single
template:
metadata:
labels:
app: template-single
spec:
containers:
- name : main
image: template-single
imagePullPolicy: Always
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: template-single
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8000
selector:
app: template-single
@@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: template-single-configmap
data:
config.yaml: |
server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"
logger:
level : "all"
stdout: true
@@ -0,0 +1,10 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: template-single
spec:
template:
spec:
containers:
- name : main
image: template-single:develop
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- configmap.yaml
patchesStrategicMerge:
- deployment.yaml
namespace: default
+7
View File
@@ -0,0 +1,7 @@
FROM alpine:latest
ENV WORKDIR=/app
WORKDIR $WORKDIR
COPY ./stackChan $WORKDIR/stackChan
COPY ./config.yaml $WORKDIR/config.yaml
RUN chmod +x $WORKDIR/stackChan
CMD ["./stackChan"]
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
# This shell is executed before docker build.
View File
View File

Some files were not shown because too many files have changed in this diff Show More