适配表接口

This commit is contained in:
jason
2025-08-11 17:42:11 +08:00
parent a4a9765232
commit bbc80b006a
49 changed files with 184 additions and 744 deletions

View File

@@ -1,28 +0,0 @@
package device
import (
"github.com/god-jason/iot-master/product"
"time"
)
type Device struct {
Id string `json:"id,omitempty" xorm:"pk"`
ProductId string `json:"product_id,omitempty" xorm:"index"`
LinkId string `json:"link_id,omitempty" xorm:"index"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Station map[string]any `json:"station,omitempty" xorm:"json"` //从站信息(协议定义表单)
Disabled bool `json:"disabled,omitempty"` //禁用
Created time.Time `json:"created,omitempty" xorm:"created"`
}
type DeviceModel struct {
Id string `json:"id,omitempty" xorm:"pk"`
Validators []*product.Validator `json:"validators,omitempty" xorm:"json"`
Created time.Time `json:"created,omitempty" xorm:"created"`
}
type Status struct {
Online bool `json:"online,omitempty"`
Error string `json:"error,omitempty"`
}

10
go.mod
View File

@@ -4,15 +4,15 @@ go 1.24
require (
github.com/PaesslerAG/gval v1.2.4
github.com/busy-cloud/boat v0.6.5
github.com/busy-cloud/boat v0.6.9
github.com/busy-cloud/boat-ui v0.5.7
github.com/busy-cloud/dash v0.5.0
github.com/busy-cloud/influxdb v0.2.5
github.com/busy-cloud/modbus v0.4.3
github.com/busy-cloud/saas v0.0.1
github.com/busy-cloud/influxdb v0.2.6
github.com/busy-cloud/modbus v0.4.4
github.com/busy-cloud/saas v0.0.6
github.com/busy-cloud/tcp-client v0.0.2
github.com/busy-cloud/tcp-server v0.1.3
github.com/busy-cloud/user v0.6.2
github.com/busy-cloud/user v0.6.4
github.com/gin-gonic/gin v1.10.1
github.com/spf13/cast v1.9.2
github.com/spf13/pflag v1.0.7

20
go.sum
View File

@@ -12,24 +12,24 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMz
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/busy-cloud/boat v0.6.5 h1:CzrHDgdqNxWkMsAPXQ0GgOMPi+NGIeBjQ83a1fFybQw=
github.com/busy-cloud/boat v0.6.5/go.mod h1:sBfieDzgV5vdl9EIrTSOKjXiNvbOc2rgmwUYDUbXS5k=
github.com/busy-cloud/boat v0.6.9 h1:DYzj0t6s1ESkZcxpFyYoMjTA+dZ7i1pnLOqE2TDg3AY=
github.com/busy-cloud/boat v0.6.9/go.mod h1:sBfieDzgV5vdl9EIrTSOKjXiNvbOc2rgmwUYDUbXS5k=
github.com/busy-cloud/boat-ui v0.5.7 h1:tRFlyTUkQOXqWsAKBa7bSkD1XdXqbcS1WBVoUCN/cj0=
github.com/busy-cloud/boat-ui v0.5.7/go.mod h1:t9DwLmJyjbPHcm+IKzC9qUE7futO0pLigI9HIPbkhg0=
github.com/busy-cloud/dash v0.5.0 h1:v7onTsSlRKKPN/OHnczyAfPL8+h0pIzrF6FHNh1IJT8=
github.com/busy-cloud/dash v0.5.0/go.mod h1:YpLLCh50vwRy47NM9FFrAeg07Mh0qLxz6dzqYPJZm9c=
github.com/busy-cloud/influxdb v0.2.5 h1:sitTN2yWcjrwVVLHSfBAt2GEmzBjH9ntMDwqFZoMP18=
github.com/busy-cloud/influxdb v0.2.5/go.mod h1:lyDWx/SM5vhqzfCAuZIm5Ev68+sOZ0PzT0UsSRdC5tw=
github.com/busy-cloud/modbus v0.4.3 h1:9IEHw8iCxiMDfCYHTYsn7lM4W7EPgu9oRGmL/t1iGKY=
github.com/busy-cloud/modbus v0.4.3/go.mod h1:aEPgQ/s/lxLrLMZGhVXiy3EWWHZocFhMZK9mxEcjiMA=
github.com/busy-cloud/saas v0.0.1 h1:8YY1cYeqf3utMdYKrFTXRVe1NYVRST4XcoFKlj7g2Io=
github.com/busy-cloud/saas v0.0.1/go.mod h1:uOF0RuwyNcYWd8A1jiWEdi4xUWx4DUc/Cdk3nWg9gjw=
github.com/busy-cloud/influxdb v0.2.6 h1:z1wqzwEI4e6WlB8rQtrFDc41epVuPRmnOrD4r3tLtcM=
github.com/busy-cloud/influxdb v0.2.6/go.mod h1:NGR/zRY4RsaktCGCUcKPAdvyHU2vWksocW3HiEir0Kk=
github.com/busy-cloud/modbus v0.4.4 h1:IRQv18rPHY8gOj6AWxFDMLFaM7vf9z/awVdQDMMtybg=
github.com/busy-cloud/modbus v0.4.4/go.mod h1:G6pdpajTm/v/Sfebi+FJaOiG9tPoc99MszfVekT+2rs=
github.com/busy-cloud/saas v0.0.6 h1:q/Oum/K5IkoLmML2If4AgjPQoxLZgtcVgd6TZ0u4Mfg=
github.com/busy-cloud/saas v0.0.6/go.mod h1:69snbR84ru4IvpfkgA93GJLEjnLVRrKK3VN/g8DVREo=
github.com/busy-cloud/tcp-client v0.0.2 h1:ozvncfSrtQoaklnMpxV8bxI4jD6TEctDSupoZcgSPTo=
github.com/busy-cloud/tcp-client v0.0.2/go.mod h1:T81IvysuVrHRzA6L0MNKSqmKTJZXCltZbWWiBRkHXoQ=
github.com/busy-cloud/tcp-server v0.1.3 h1:J1jqBMaEPEXiCkiuMpy5PZOmyj88WnH4Jr47RCdVdC8=
github.com/busy-cloud/tcp-server v0.1.3/go.mod h1:ygXe9jPZqIXqpxakNUNn9JpoMClNukuAOK/HRDwxDkI=
github.com/busy-cloud/user v0.6.2 h1:GBJ0picxzZX8ql6w1UQ5w9MKyuPQcCprBU/Vg2mXxYM=
github.com/busy-cloud/user v0.6.2/go.mod h1:gXcx1ToO5o0pOzwIRSjHHqTLD3v2Ah41BDgaxIKzgeg=
github.com/busy-cloud/user v0.6.4 h1:kfaCAUZDYyZvlZubrz5TrrtjTO5lH1ewmxhb7zbbIpE=
github.com/busy-cloud/user v0.6.4/go.mod h1:azm+7ClpvAP81D5TRrENlhHxDbdnqPxyGbVE9/xJWls=
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=

15
internal/alarm.go Normal file
View File

@@ -0,0 +1,15 @@
package internal
import "time"
type Alarm struct {
Id int64 `json:"id,omitempty"`
DeviceId string `json:"device_id,omitempty" xorm:"index"`
ProjectId string `json:"project_id,omitempty" xorm:"index"`
Device string `json:"device,omitempty" xorm:"-"`
Project string `json:"project,omitempty" xorm:"-"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
Level int `json:"level,omitempty"`
Created time.Time `json:"created,omitempty" xorm:"created"`
}

View File

@@ -2,11 +2,8 @@ package internal
import (
"github.com/busy-cloud/boat/boot"
_ "github.com/god-jason/iot-master/device"
_ "github.com/god-jason/iot-master/product"
_ "github.com/god-jason/iot-master/project"
_ "github.com/god-jason/iot-master/protocol"
_ "github.com/god-jason/iot-master/space"
)
func init() {

View File

@@ -6,18 +6,23 @@ import (
"github.com/busy-cloud/boat/lib"
"github.com/busy-cloud/boat/log"
"github.com/busy-cloud/boat/mqtt"
"github.com/god-jason/iot-master/device"
"github.com/god-jason/iot-master/project"
"github.com/god-jason/iot-master/product"
"github.com/god-jason/iot-master/protocol"
"github.com/god-jason/iot-master/space"
"math/rand"
"strconv"
"time"
)
type Device struct {
device.Device `xorm:"extends"`
device.Status `xorm:"-"`
//device.Device `xorm:"extends"`
Id string `json:"id,omitempty" xorm:"pk"`
ProductId string `json:"product_id,omitempty" xorm:"index"`
LinkId string `json:"link_id,omitempty" xorm:"index"`
Name string `json:"name,omitempty"`
Station map[string]any `json:"station,omitempty" xorm:"json"` //从站信息(协议定义表单)
Disabled bool `json:"disabled,omitempty"` //禁用
Status `xorm:"-"`
values Values
@@ -34,27 +39,38 @@ type Device struct {
waiting lib.Map[chan any]
}
type DeviceModel struct {
Id string `json:"id,omitempty" xorm:"pk"`
Validators []*product.Validator `json:"validators,omitempty" xorm:"json"`
Created time.Time `json:"created,omitempty" xorm:"created"`
}
type Status struct {
Online bool `json:"online,omitempty"`
Error string `json:"error,omitempty"`
}
func (d *Device) Open() error {
d.Online = true
//查询绑定的项目
var ps []*project.ProjectDevice
err := db.Engine().Where("device_id=?", d.Id).Find(&ps) //.Distinct("project_id")
var ps []map[string]interface{}
err := db.Engine().Cols("project_id").Where("device_id=?", d.Id).Find(&ps) //.Distinct("project_id")
if err != nil {
return err
}
for _, p := range ps {
d.projects = append(d.projects, p.ProjectId)
d.projects = append(d.projects, p["project_id"].(string))
}
//查询绑定的设备
var ss []*space.SpaceDevice
err = db.Engine().Where("device_id=?", d.Id).Find(&ss) //.Distinct("space_id")
var ss []map[string]interface{}
err = db.Engine().Cols("space_id").Where("device_id=?", d.Id).Find(&ss) //.Distinct("space_id")
if err != nil {
return err
}
for _, s := range ss {
d.spaces = append(d.spaces, s.SpaceId)
d.spaces = append(d.spaces, s["space_id"].(string))
}
//加载产品物模型
@@ -78,7 +94,7 @@ func (d *Device) Open() error {
}
//加载设备模型
var deviceModel device.DeviceModel
var deviceModel DeviceModel
has, err := db.Engine().ID(d.Id).Get(&deviceModel)
if err != nil {
return err

View File

@@ -5,37 +5,11 @@ import (
"github.com/busy-cloud/boat/curd"
"github.com/busy-cloud/boat/db"
"github.com/gin-gonic/gin"
"github.com/god-jason/iot-master/device"
)
func init() {
api.Register("GET", "iot/device/list", curd.ApiListHook[Device](getDevicesInfo))
api.Register("POST", "iot/device/search", curd.ApiSearchHook[Device](getDevicesInfo))
api.Register("POST", "iot/device/create", curd.ApiCreateHook[Device](nil, func(m *Device) error {
//TODO 加载设备
return nil
}))
api.Register("GET", "iot/device/:id", curd.ApiGetHook[Device](getDeviceInfo))
api.Register("POST", "iot/device/:id", curd.ApiUpdateHook[Device](nil, func(m *Device) error {
//TODO 重新加载设备
return nil
}, "id", "name", "description", "product_id", "link_id", "disabled", "station"))
api.Register("GET", "iot/device/:id/delete", curd.ApiDeleteHook[Device](nil, func(m *Device) error {
return UnloadDevice(m.Id)
}))
api.Register("GET", "iot/device/:id/enable", curd.ApiDisableHook[Device](false, nil, func(id any) error {
_, err := LoadDevice(id.(string))
return err
}))
api.Register("GET", "iot/device/:id/disable", curd.ApiDisableHook[Device](true, nil, func(id any) error {
return UnloadDevice(id.(string))
}))
//物模型
api.Register("GET", "iot/device/:id/model", curd.ApiGet[device.DeviceModel]())
api.Register("GET", "iot/device/:id/model", curd.ApiGet[DeviceModel]())
api.Register("POST", "iot/device/:id/model", deviceModelUpdate)
}
@@ -58,7 +32,7 @@ func getDeviceInfo(d *Device) error {
func deviceModelUpdate(ctx *gin.Context) {
id := ctx.Param("id")
var model device.DeviceModel
var model DeviceModel
err := ctx.ShouldBind(&model)
if err != nil {
api.Error(ctx, err)
@@ -66,7 +40,7 @@ func deviceModelUpdate(ctx *gin.Context) {
}
model.Id = id
_, err = db.Engine().ID(id).Delete(new(device.DeviceModel)) //不管有没有都删掉
_, err = db.Engine().ID(id).Delete(new(DeviceModel)) //不管有没有都删掉
_, err = db.Engine().ID(id).Insert(&model)
if err != nil {
api.Error(ctx, err)

View File

@@ -14,7 +14,7 @@ func GetDevice(id string) *Device {
func LoadDevice(id string) (*Device, error) {
d := &Device{}
has, err := db.Engine().ID(id).Get(&d.Device)
has, err := db.Engine().ID(id).Get(&d)
if err != nil {
return nil, err
}

View File

@@ -1,8 +1,8 @@
package internal
import (
"encoding/json"
"fmt"
"github.com/busy-cloud/boat/json"
"github.com/busy-cloud/boat/log"
"github.com/busy-cloud/boat/mqtt"
"github.com/god-jason/iot-master/protocol"

View File

@@ -10,14 +10,6 @@ import (
)
func init() {
api.Register("GET", "iot/product/list", curd.ApiList[product.Product]())
api.Register("POST", "iot/product/search", curd.ApiSearch[product.Product]())
api.Register("POST", "iot/product/create", curd.ApiCreate[product.Product]())
api.Register("GET", "iot/product/:id", curd.ApiGet[product.Product]())
api.Register("POST", "iot/product/:id", curd.ApiUpdate[product.Product]("id", "name", "description", "type", "version", "protocol", "disabled"))
api.Register("GET", "iot/product/:id/delete", curd.ApiDelete[product.Product]())
api.Register("GET", "iot/product/:id/enable", curd.ApiDisable[product.Product](false))
api.Register("GET", "iot/product/:id/disable", curd.ApiDisable[product.Product](true))
//物模型
api.Register("GET", "iot/product/:id/model", curd.ApiGet[product.Model]())

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"github.com/PaesslerAG/gval"
"github.com/god-jason/iot-master/alarm"
"github.com/god-jason/iot-master/calc"
"github.com/god-jason/iot-master/product"
"github.com/spf13/cast"
@@ -29,7 +28,7 @@ func (v *Validator) Build() (err error) {
return err
}
func (v *Validator) Evaluate(ctx map[string]any) (*alarm.Alarm, error) {
func (v *Validator) Evaluate(ctx map[string]any) (*Alarm, error) {
var err error
var ret bool
@@ -98,7 +97,7 @@ func (v *Validator) Evaluate(ctx map[string]any) (*alarm.Alarm, error) {
v.times = v.times + 1
//产生报警
a := &alarm.Alarm{
a := &Alarm{
Title: replaceParams(v.Title, ctx),
Message: replaceParams(v.Message, ctx),
Level: v.Level,

View File

@@ -7,7 +7,7 @@
"items": [],
"mount": "this.load_device(); this.btn_action={type:'dialog',page:'iot/device-action',params_func:'return {id:this.params.id, action:data}'}",
"methods": {
"load_device": "this.request.get('iot/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})",
"load_device": "this.request.get('table/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})",
"load_model": ["pid","this.request.get('iot/product/'+pid+'/model').subscribe(res=>{if(res.error)return; this.content.toolbar=res.data.actions.map(p=>{return{type:'button', label:p.label||p.name, action:{type:'dialog',page:'iot/device-action',params:{id:this.params.id, action:p.name, parameters:p.parameters}}}}); })"]
}
}

View File

@@ -66,5 +66,5 @@
"type": "date"
}
],
"search_api": "iot/device/search"
"search_api": "table/device/search"
}

View File

@@ -65,11 +65,11 @@
"type": "switch"
}
],
"submit_api": "iot/device/create",
"submit_api": "table/device/create",
"submit_success": "this.navigate('/page/iot/device-detail?id='+data.id)",
"mount": "this.data.product_id=this.params.product_id; setTimeout(()=>this.load_product(), 100)",
"methods": {
"load_product": "this.editor.value.product_id && this.request.get('iot/product/'+this.editor.value.product_id).subscribe(res=>{if(!res.error) this.load_protocol_station(res.data.protocol)})",
"load_product": "this.editor.value.product_id && this.request.get('table/device/'+this.editor.value.product_id).subscribe(res=>{if(!res.error) this.load_protocol_station(res.data.protocol)})",
"load_protocol_station": ["p", "this.request.get('iot/protocol/'+p).subscribe(res=>{this.content.fields[5].children=res.station; setTimeout(()=>this.editor.rebuild(), 200)})"]
}
}

View File

@@ -19,7 +19,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/device/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/device')})"
"script": "this.request.get('table/device/delete/'+data.id).subscribe(res=>{this.navigate('/page/iot/device')})"
}
},
{
@@ -84,7 +84,7 @@
"label": "禁用"
}
],
"load_api": "iot/device/:id",
"load_api": "table/device/detail/:id",
"tabs": [
{
"title": "实时状态",

View File

@@ -64,13 +64,13 @@
"type": "switch"
}
],
"load_api": "iot/device/:id",
"load_api": "table/device/detail/:id",
"load_success": "setTimeout(()=>this.load_product(), 200)",
"submit_api": "iot/device/:id",
"submit_api": "table/device/update/:id",
"submit_success": "this.navigate('/page/iot/device-detail?id='+data.id)",
"mount": "",
"methods": {
"load_product": "this.editor.value.product_id && this.request.get('iot/product/'+this.editor.value.product_id).subscribe(res=>{if(!res.error) this.load_protocol_station(res.data.protocol)})",
"load_product": "this.editor.value.product_id && this.request.get('table/device/'+this.editor.value.product_id).subscribe(res=>{if(!res.error) this.load_protocol_station(res.data.protocol)})",
"load_protocol_station": ["p", "this.request.get('iot/protocol/'+p).subscribe(res=>{this.content.fields[5].children=res.station; setTimeout(()=>this.editor.rebuild(), 200)})"]
}
}

View File

@@ -7,6 +7,11 @@
"label": "ID",
"type": "text"
},
{
"key": "product_id",
"label": "产品ID",
"type": "text"
},
{
"key": "name",
"label": "名称",
@@ -17,15 +22,6 @@
"label": "说明",
"type": "text"
},
{
"key": "product_id",
"label": "产品ID",
"type": "text",
"change_action": {
"type": "script",
"script": "setTimeout(()=>this.load_product(), 100)"
}
},
{
"key": "link_id",
"label": "连接ID",
@@ -44,5 +40,5 @@
"type": "switch"
}
],
"submit_api": "iot/device/:id"
"submit_api": "table/device/create"
}

View File

@@ -16,7 +16,7 @@
"mount": "this.load_device()",
"submit": "this.request.post('iot/device/'+this.params.id+'/write', {[data.key]:data.value}).subscribe(res=>{if(res.error)return; })",
"methods": {
"load_device": "this.request.get('iot/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})",
"load_device": "this.request.get('table/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})",
"load_model": ["pid","this.request.get('iot/product/'+pid+'/model').subscribe(res=>{if(res.error)return; this.content.fields[0].options=res.data.properties.map(p=>{return {value:p.name,label:p.label}}) })"]
}
}

View File

@@ -18,7 +18,7 @@
"methods": {
"load_values": "this.request.get('iot/device/'+this.params.id+'/values').subscribe(res=>{if(res.error)return; this.data=res.data; this.content.toolbar[0].type='text'; label=res.data.__update})",
"refresh_values": "this.request.get('iot/device/'+this.params.id+'/sync').subscribe(res=>{if(res.error)return; this.data=res.data})",
"load_device": "this.request.get('iot/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})",
"load_device": "this.request.get('table/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})",
"load_model": ["pid","this.request.get('iot/product/'+pid+'/model').subscribe(res=>{if(res.error)return; this.content.items=res.data.properties.map(p=>{return{key:p.name,label:p.label,suffix:p.unit,span:6,action:this.value_action}}); })"]
}
}

View File

@@ -63,7 +63,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/device/'+data.id+'/delete').subscribe(res=>{this.load()})"
"script": "this.request.get('table/device/delete/'+data.id).subscribe(res=>{this.load()})"
}
}
],
@@ -119,6 +119,6 @@
"type": "date"
}
],
"search_api": "iot/device/search",
"search_api": "table/device/search",
"mount": "if(this.params.link_id)this.filter.link_id=this.params.link_id;"
}

View File

@@ -55,5 +55,5 @@
"type": "bool"
}
],
"search_api": "iot/link/search"
"search_api": "table/link/search"
}

View File

@@ -46,5 +46,5 @@
"label": "版本"
}
],
"search_api": "iot/product/search"
"search_api": "table/product/search"
}

View File

@@ -41,7 +41,7 @@
"type": "switch"
}
],
"submit_api": "iot/product/create",
"submit_api": "table/product/create",
"submit_success": "this.navigate('/page/iot/product-detail?id='+data.id)",
"mount": "this.load_protocols()",
"methods": {

View File

@@ -29,7 +29,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/product/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/product')})"
"script": "this.request.get('table/device/delete/'+data.id).subscribe(res=>{this.navigate('/page/iot/product')})"
}
}
],
@@ -64,7 +64,7 @@
"type": "boolean"
}
],
"load_api": "iot/product/:id",
"load_api": "table/product/detail/:id",
"tabs": [
{
"title": "产品设备",

View File

@@ -55,7 +55,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/device/'+data.id+'/delete').subscribe(res=>{this.load()})"
"script": "this.request.get('table/device/delete/'+data.id).subscribe(res=>{this.load()})"
}
}
],
@@ -102,6 +102,6 @@
"type": "date"
}
],
"search_api": "iot/device/search",
"search_api": "table/device/search",
"mount": "if(this.params.product_id)this.filter.product_id=this.params.product_id"
}

View File

@@ -41,8 +41,8 @@
"type": "switch"
}
],
"load_api": "iot/product/:id",
"submit_api": "iot/product/:id",
"load_api": "table/product/detail/:id",
"submit_api": "table/product/update/:id",
"submit_success": "this.navigate('/page/iot/product-detail?id='+data.id)",
"mount": "this.load_protocols()",
"methods": {

View File

@@ -54,7 +54,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/product/'+data.id+'/delete').subscribe(res=>{this.load()})"
"script": "this.request.get('table/device/'+data.id+'/delete').subscribe(res=>{this.load()})"
}
}
],
@@ -99,5 +99,5 @@
"type": "date"
}
],
"search_api": "iot/product/search"
"search_api": "table/product/search"
}

View File

@@ -46,5 +46,5 @@
"label": "说明"
}
],
"search_api": "iot/project/search"
"search_api": "table/project/search"
}

View File

@@ -25,7 +25,7 @@
"type": "switch"
}
],
"submit_api": "iot/project/create",
"submit_api": "table/project/create",
"submit_success": "this.navigate('/page/iot/project-detail?id='+data.id)",
"methods": {
}

View File

@@ -19,7 +19,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/project/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/project')})"
"script": "this.request.get('table/project/delete/'+data.id).subscribe(res=>{this.navigate('/page/iot/project')})"
}
}
],
@@ -41,7 +41,7 @@
"label": "禁用"
}
],
"load_api": "iot/project/:id",
"load_api": "table/project/detail/:id",
"tabs": [
{
"title": "项目空间",

View File

@@ -25,8 +25,8 @@
"type": "switch"
}
],
"load_api": "iot/project/:id",
"submit_api": "iot/project/:id",
"load_api": "table/project/detail/:id",
"submit_api": "table/project/update/:id",
"submit_success": "this.navigate('/page/iot/project-detail?id='+data.id)",
"methods": {
}

View File

@@ -54,7 +54,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/project/'+data.id+'/delete').subscribe(res=>{this.load()})"
"script": "this.request.get('table/project/delete/'+data.id).subscribe(res=>{this.load()})"
}
}
],
@@ -87,5 +87,5 @@
"type": "date"
}
],
"search_api": "iot/project/search"
"search_api": "table/project/search"
}

View File

@@ -46,5 +46,5 @@
"label": "说明"
}
],
"search_api": "iot/space/search"
"search_api": "table/space/search"
}

View File

@@ -54,7 +54,7 @@
"type": "switch"
}
],
"submit_api": "iot/space/create",
"submit_api": "table/space/create",
"submit_success": "this.navigate('/page/iot/space-detail?id='+data.id)",
"mount": "",
"methods": {

View File

@@ -19,7 +19,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/space/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/project?id='+data.project_id)})"
"script": "this.request.get('iot/space/delete/'+data.id).subscribe(res=>{this.navigate('/page/iot/project?id='+data.project_id)})"
}
}
],
@@ -60,7 +60,7 @@
"type": "boolean"
}
],
"load_api": "iot/space/:id",
"load_api": "table/space/detail/:id",
"tabs": [
{
"title": "空间设备",

View File

@@ -54,8 +54,8 @@
"type": "switch"
}
],
"load_api": "iot/space/:id",
"submit_api": "iot/space/:id",
"load_api": "table/space/detail/:id",
"submit_api": "table/space/update/:id",
"submit_success": "this.navigate('/page/iot/space-detail?id='+data.id)",
"mount": "",
"methods": {

View File

@@ -35,7 +35,7 @@
"confirm": "确认删除?",
"action": {
"type": "script",
"script": "this.request.get('iot/space/'+data.id+'/delete').subscribe(res=>{this.load()})"
"script": "this.request.get('iot/space/delete/'+data.id).subscribe(res=>{this.load()})"
}
}
],
@@ -77,6 +77,6 @@
"type": "date"
}
],
"search_api": "iot/space/search",
"search_api": "table/space/search",
"mount": "if(this.params.project_id)this.filter.project_id=this.params.project_id; if(this.params.parent_id)this.filter.parent_id=this.params.parent_id"
}

View File

@@ -1,14 +1,9 @@
package product
import (
"github.com/busy-cloud/boat/db"
"time"
)
func init() {
db.Register(&Product{}, &ProductConfig{}, &Model{})
}
type Product struct {
Id string `json:"id,omitempty" xorm:"pk"`
Name string `json:"name,omitempty"`

View File

@@ -1,141 +0,0 @@
package project
import (
"github.com/busy-cloud/boat/api"
"github.com/busy-cloud/boat/db"
"github.com/gin-gonic/gin"
"xorm.io/xorm/schemas"
)
func init() {
api.Register("GET", "iot/project/:id/app/list", projectAppList)
api.Register("GET", "iot/project/:id/app/:app/exists", projectAppExists)
api.Register("GET", "iot/project/:id/app/:app/bind", projectAppBind)
api.Register("GET", "iot/project/:id/app/:app/unbind", projectAppUnbind)
api.Register("GET", "iot/project/:id/app/:app/disable", projectAppDisable)
api.Register("GET", "iot/project/:id/app/:app/enable", projectAppEnable)
}
// @Summary 项目应用列表
// @Schemes
// @Description 项目应用列表
// @Tags project-app
// @Param id path int true "项目ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[[]ProjectApp] 返回项目应用信息
// @Router iot/project/{id}/app/list [get]
func projectAppList(ctx *gin.Context) {
var pds []ProjectApp
err := db.Engine().Where("project_id=?", ctx.Param("id")).Find(&pds)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, pds)
}
// @Summary 判断项目应用是否存在
// @Schemes
// @Description 判断项目应用是否存在
// @Tags project-app
// @Param id path int true "项目ID"
// @Param app path int true "应用ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[bool]
// @Router iot/project/{id}/app/{app}/exists [get]
func projectAppExists(ctx *gin.Context) {
pd := ProjectApp{
ProjectId: ctx.Param("id"),
AppId: ctx.Param("app"),
}
has, err := db.Engine().Exist(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, has)
}
// @Summary 绑定项目应用
// @Schemes
// @Description 绑定项目应用
// @Tags project-app
// @Param id path int true "项目ID"
// @Param app path int true "应用ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/app/{app}/bind [get]
func projectAppBind(ctx *gin.Context) {
pd := ProjectApp{
ProjectId: ctx.Param("id"),
AppId: ctx.Param("app"),
}
_, err := db.Engine().InsertOne(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 删除项目应用
// @Schemes
// @Description 删除项目应用
// @Tags project-app
// @Param id path int true "项目ID"
// @Param app path int true "应用ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/app/{app}/unbind [get]
func projectAppUnbind(ctx *gin.Context) {
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("app")}).Delete(new(ProjectApp))
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 禁用项目应用
// @Schemes
// @Description 禁用项目应用
// @Tags project-app
// @Param id path int true "项目ID"
// @Param app path int true "应用ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/app/{app}/disable [get]
func projectAppDisable(ctx *gin.Context) {
pd := ProjectApp{Disabled: true}
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("app")}).Cols("disabled").Update(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 启用项目应用
// @Schemes
// @Description 启用项目应用
// @Tags project-app
// @Param id path int true "项目ID"
// @Param app path int true "应用ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/app/{app}/enable [get]
func projectAppEnable(ctx *gin.Context) {
pd := ProjectApp{Disabled: false}
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("app")}).Cols("disabled").Update(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}

View File

@@ -1,108 +0,0 @@
package project
import (
"github.com/busy-cloud/boat/api"
"github.com/busy-cloud/boat/db"
"github.com/gin-gonic/gin"
"xorm.io/xorm/schemas"
)
func init() {
api.Register("GET", "iot/project/:id/device/list", projectDeviceList)
api.Register("GET", "iot/project/:id/device/:device/bind", projectDeviceBind)
api.Register("GET", "iot/project/:id/device/:device/unbind", projectDeviceUnbind)
api.Register("POST", "iot/project/:id/device/:device", projectDeviceUpdate)
}
// @Summary 空间设备列表
// @Schemes
// @Description 空间设备列表
// @Tags project-device
// @Param id path int true "项目ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[[]ProjectDevice] 返回空间设备信息
// @Router iot/project/{id}/device/list [get]
func projectDeviceList(ctx *gin.Context) {
var pds []ProjectDevice
err := db.Engine().
Select("project_device.project_id, project_device.device_id, project_device.name, project_device.created, device.name as device").
Join("INNER", "device", "device.id=project_device.device_id").
Where("project_device.project_id=?", ctx.Param("id")).
Find(&pds)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, pds)
}
// @Summary 绑定空间设备
// @Schemes
// @Description 绑定空间设备
// @Tags project-device
// @Param id path int true "项目ID"
// @Param device path int true "设备ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/device/{device}/bind [get]
func projectDeviceBind(ctx *gin.Context) {
pd := ProjectDevice{
ProjectId: ctx.Param("id"),
DeviceId: ctx.Param("device"),
}
_, err := db.Engine().InsertOne(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 删除空间设备
// @Schemes
// @Description 删除空间设备
// @Tags project-device
// @Param id path int true "项目ID"
// @Param device path int true "设备ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/device/{device}/unbind [get]
func projectDeviceUnbind(ctx *gin.Context) {
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("device")}).Delete(new(ProjectDevice))
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 修改空间设备
// @Schemes
// @Description 修改空间设备
// @Tags project-device
// @Param id path int true "项目ID"
// @Param device path int true "设备ID"
// @Param project-device body ProjectDevice true "空间设备信息"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/device/{device} [post]
func projectDeviceUpdate(ctx *gin.Context) {
var pd ProjectDevice
err := ctx.ShouldBindJSON(&pd)
if err != nil {
api.Error(ctx, err)
return
}
_, err = db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("device")}).
Cols("device_id", "name", "disabled").
Update(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}

View File

@@ -1,97 +0,0 @@
package project
import (
"github.com/busy-cloud/boat/api"
"github.com/busy-cloud/boat/curd"
)
func init() {
api.Register("POST", "iot/project/count", curd.ApiCount[Project]())
api.Register("POST", "iot/project/search", curd.ApiSearch[Project]())
api.Register("GET", "iot/project/list", curd.ApiList[Project]())
api.Register("POST", "iot/project/create", curd.ApiCreate[Project]())
api.Register("GET", "iot/project/:id", curd.ApiGet[Project]())
api.Register("POST", "iot/project/:id", curd.ApiUpdate[Project]())
api.Register("GET", "iot/project/:id/delete", curd.ApiDelete[Project]())
api.Register("GET", "iot/project/:id/disable", curd.ApiDisable[Project](true))
api.Register("GET", "iot/project/:id/enable", curd.ApiDisable[Project](false))
}
// @Summary 查询项目
// @Schemes
// @Description 查询项目
// @Tags project
// @Param search body curd.ParamSearch true "查询参数"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyList[Project] 返回项目信息
// @Router iot/project/search [post]
func noopProjectSearch() {}
// @Summary 查询项目
// @Schemes
// @Description 查询项目
// @Tags project
// @Param search query curd.ParamList true "查询参数"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyList[Project] 返回项目信息
// @Router iot/project/list [get]
func noopProjectList() {}
// @Summary 创建项目
// @Schemes
// @Description 创建项目
// @Tags project
// @Param search body Project true "项目信息"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[Project] 返回项目信息
// @Router iot/project/create [post]
func noopProjectCreate() {}
// @Summary 修改项目
// @Schemes
// @Description 修改项目
// @Tags project
// @Param id path int true "项目ID"
// @Param project body Project true "项目信息"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[Project] 返回项目信息
// @Router iot/project/{id} [post]
func noopProjectUpdate() {}
// @Summary 删除项目
// @Schemes
// @Description 删除项目
// @Tags project
// @Param id path int true "项目ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[Project] 返回项目信息
// @Router iot/project/{id}/delete [get]
func noopProjectDelete() {}
// @Summary 启用项目
// @Schemes
// @Description 启用项目
// @Tags project
// @Param id path int true "项目ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[Project] 返回项目信息
// @Router iot/project/{id}/enable [get]
func noopProjectEnable() {}
// @Summary 禁用项目
// @Schemes
// @Description 禁用项目
// @Tags project
// @Param id path int true "项目ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[Project] 返回项目信息
// @Router iot/project/{id}/disable [get]
func noopProjectDisable() {}

View File

@@ -1,45 +0,0 @@
package project
import (
"github.com/busy-cloud/boat/db"
"time"
)
func init() {
db.Register(new(Project), new(ProjectUser), new(ProjectDevice), new(ProjectApp))
}
type Project struct {
Id string `json:"id" xorm:"pk"`
Name string `json:"name,omitempty"` //名称
Description string `json:"description,omitempty"` //说明
Keywords []string `json:"keywords,omitempty"` //关键字
Disabled bool `json:"disabled,omitempty"`
Created time.Time `json:"created" xorm:"created"`
}
type ProjectUser struct {
ProjectId string `json:"project_id,omitempty" xorm:"pk"`
Project string `json:"project,omitempty" xorm:"<-"`
UserId string `json:"user_id,omitempty" xorm:"pk"`
User string `json:"user,omitempty" xorm:"<-"`
Admin bool `json:"admin,omitempty"`
Disabled bool `json:"disabled,omitempty"`
Created time.Time `json:"created" xorm:"created"`
}
type ProjectDevice struct {
ProjectId string `json:"project_id,omitempty" xorm:"pk"`
Project string `json:"project,omitempty" xorm:"<-"`
DeviceId string `json:"device_id,omitempty" xorm:"pk"`
Device string `json:"device,omitempty" xorm:"<-"`
Name string `json:"name,omitempty"` //编程别名
Created time.Time `json:"created" xorm:"created"`
}
type ProjectApp struct {
ProjectId string `json:"project_id,omitempty" xorm:"pk"`
AppId string `json:"app_id,omitempty" xorm:"pk"`
Disabled bool `json:"disabled,omitempty"`
Created time.Time `json:"created" xorm:"created"`
}

View File

@@ -1,198 +0,0 @@
package project
import (
"github.com/busy-cloud/boat/api"
"github.com/busy-cloud/boat/db"
"github.com/gin-gonic/gin"
"xorm.io/xorm/schemas"
)
func init() {
api.Register("GET", "iot/project/:id/user/list", projectUserList)
api.Register("GET", "iot/project/:id/user/:user/exists", projectUserExists)
api.Register("GET", "iot/project/:id/user/:user/bind", projectUserBind)
api.Register("GET", "iot/project/:id/user/:user/unbind", projectUserUnbind)
api.Register("GET", "iot/project/:id/user/:user/disable", projectUserDisable)
api.Register("GET", "iot/project/:id/user/:user/enable", projectUserEnable)
api.Register("POST", "iot/project/:id/user/:user", projectUserUpdate)
//个人项目
api.Register("GET", "/user/:id/projects", userProjects)
}
// @Summary 项目用户列表
// @Schemes
// @Description 项目用户列表
// @Tags project-user
// @Param id path int true "项目ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[[]ProjectUser] 返回项目用户信息
// @Router iot/project/{id}/user/list [get]
func projectUserList(ctx *gin.Context) {
var pds []ProjectUser
err := db.Engine().
Select("project_user.project_id, project_user.user_id, project_user.admin, project_user.disabled, project_user.created, user.name as user").
Join("INNER", "user", "user.id=project_user.user_id").
Where("project_user.project_id=?", ctx.Param("id")).
Find(&pds)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, pds)
}
// @Summary 判断项目用户是否存在
// @Schemes
// @Description 判断项目用户是否存在
// @Tags project-user
// @Param id path int true "项目ID"
// @Param user path int true "用户ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[bool]
// @Router iot/project/{id}/user/{user}/exists [get]
func projectUserExists(ctx *gin.Context) {
pd := ProjectUser{
ProjectId: ctx.Param("id"),
UserId: ctx.Param("user"),
}
has, err := db.Engine().Exist(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, has)
}
// @Summary 绑定项目用户
// @Schemes
// @Description 绑定项目用户
// @Tags project-user
// @Param id path int true "项目ID"
// @Param user path int true "用户ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/user/{user}/bind [get]
func projectUserBind(ctx *gin.Context) {
pd := ProjectUser{
ProjectId: ctx.Param("id"),
UserId: ctx.Param("user"),
}
_, err := db.Engine().InsertOne(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 删除项目用户
// @Schemes
// @Description 删除项目用户
// @Tags project-user
// @Param id path int true "项目ID"
// @Param user path int true "用户ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/user/{user}/unbind [get]
func projectUserUnbind(ctx *gin.Context) {
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).Delete(new(ProjectUser))
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 禁用项目用户
// @Schemes
// @Description 禁用项目用户
// @Tags project-user
// @Param id path int true "项目ID"
// @Param user path int true "用户ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/user/{user}/disable [get]
func projectUserDisable(ctx *gin.Context) {
pd := ProjectUser{Disabled: true}
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).Cols("disabled").Update(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 启用项目用户
// @Schemes
// @Description 启用项目用户
// @Tags project-user
// @Param id path int true "项目ID"
// @Param user path int true "用户ID"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/user/{user}/enable [get]
func projectUserEnable(ctx *gin.Context) {
pd := ProjectUser{Disabled: false}
_, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).Cols("disabled").Update(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 修改项目用户
// @Schemes
// @Description 修改项目用户
// @Tags project-user
// @Param id path int true "项目ID"
// @Param user path int true "用户ID"
// @Param project-user body ProjectUser true "项目用户信息"
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[int]
// @Router iot/project/{id}/user/{user} [post]
func projectUserUpdate(ctx *gin.Context) {
var pd ProjectUser
err := ctx.ShouldBindJSON(&pd)
if err != nil {
api.Error(ctx, err)
return
}
_, err = db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).
Cols("user_id", "disabled").
Update(&pd)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, nil)
}
// @Summary 获取用户的项目列表
// @Schemes
// @Description 获取用户的项目列表
// @Tags user
// @Accept json
// @Produce json
// @Success 200 {object} curd.ReplyData[[]Project] 返回项目列表
// @Router /user/{id}iot/projects [get]
func userProjects(ctx *gin.Context) {
id := ctx.GetString("id")
var projects []*Project
err := db.Engine().Join("INNER", "project_user", "project_user.project_id=id").
Where("project_user.user_id=?", id).Find(&projects)
if err != nil {
api.Error(ctx, err)
return
}
api.OK(ctx, projects)
}

View File

@@ -1,6 +1,22 @@
{
"name": "device",
"comment": "设备",
"joins": [
{
"table": "product",
"local_field": "product_id",
"foreign_field": "id",
"field": "name",
"as": "product"
},
{
"table": "tenant",
"local_field": "tenant_id",
"foreign_field": "id",
"field": "name",
"as": "tenant"
}
],
"fields": [
{
"name": "id",

View File

@@ -1,6 +1,15 @@
{
"name": "project-app",
"comment": "项目App",
"joins": [
{
"table": "project",
"local_field": "project_id",
"foreign_field": "id",
"field": "name",
"as": "project"
}
],
"fields": [
{
"name": "project_id",

View File

@@ -1,6 +1,22 @@
{
"name": "project-device",
"comment": "项目设备",
"joins": [
{
"table": "project",
"local_field": "project_id",
"foreign_field": "id",
"field": "name",
"as": "project"
},
{
"table": "device",
"local_field": "device_id",
"foreign_field": "id",
"field": "name",
"as": "device"
}
],
"fields": [
{
"name": "project_id",

View File

@@ -1,6 +1,22 @@
{
"name": "project-user",
"comment": "项目用户",
"joins": [
{
"table": "project",
"local_field": "project_id",
"foreign_field": "id",
"field": "name",
"as": "project"
},
{
"table": "user",
"local_field": "user_id",
"foreign_field": "id",
"field": "name",
"as": "user"
}
],
"fields": [
{
"name": "project_id",

View File

@@ -1,6 +1,22 @@
{
"name": "space-device",
"comment": "空间设备",
"joins": [
{
"table": "space",
"local_field": "space_id",
"foreign_field": "id",
"field": "name",
"as": "space"
},
{
"table": "device",
"local_field": "device_id",
"foreign_field": "id",
"field": "name",
"as": "device"
}
],
"fields": [
{
"name": "space_id",