mirror of
https://github.com/m5stack/StackChan.git
synced 2026-04-28 03:22:39 +00:00
server code
This commit is contained in:
@@ -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.")
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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 //
|
||||
}
|
||||
@@ -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:""` //
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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:""` //
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
package packed
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
|
||||
SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateMac(t *testing.T) {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user