Compare commits

...

36 Commits

Author SHA1 Message Date
douxu 458f7afdbf optimize doc of measurement recommend api 2025-10-20 17:30:55 +08:00
douxu 54128bedac fix bug of measurement recommend api 2025-10-20 15:06:23 +08:00
douxu 86199269f8 add deploy.md of deploy modelRT project 2025-10-17 17:10:10 +08:00
douxu 14d2a7ff65 update ingore file 2025-10-17 11:16:39 +08:00
douxu 3442984657 Stop tracking config/config.yaml 2025-10-16 17:48:03 +08:00
douxu 68a800ce63 add shield item of config in .gitignore 2025-10-16 17:45:10 +08:00
douxu f0a66263a3 optimize measurement recommend api 2025-10-16 17:18:57 +08:00
douxu 62e897190d optimize code of measurement recommend and logger output 2025-10-15 17:08:32 +08:00
douxu bcf80842b0 fix bug of main.go 2025-10-14 16:12:00 +08:00
douxu 5d02ca9fca add measurement recommend api 2025-09-29 16:37:38 +08:00
douxu 453e6f9851 add GetLongestCommonPrefixLength func 2025-09-27 15:56:46 +08:00
douxu 0d7890f6aa optimize code of RedissearchRecommend func 2025-09-26 16:47:40 +08:00
douxu 5f5eb22b39 optimize code of redis search query 2025-09-25 16:39:45 +08:00
douxu 151f7f22c5 fix bug of grid condition process 2025-09-24 17:26:46 +08:00
douxu 4ee836c70f add redis search code of query model object 2025-09-24 16:43:11 +08:00
douxu 7d8c442f9f optimize cache item of kafka monitor topic 2025-09-19 16:15:59 +08:00
douxu 51e8a677ca optimize real time data model 2025-09-18 16:53:25 +08:00
douxu 71366828f4 add real time cache 2025-09-17 16:41:30 +08:00
douxu a9532debe9 add client token of redis operation 2025-09-16 15:50:22 +08:00
douxu 0c09e7bd25 add func of generate service token 2025-09-12 17:12:02 +08:00
douxu e670720a96 optimize query measurement api 2025-09-10 17:03:33 +08:00
douxu 55a606a3f3 add redis zset structure 2025-09-09 16:02:36 +08:00
douxu 3120cfc3a5 add code of init measurement api 2025-09-05 17:10:34 +08:00
douxu 727b9a98ec add CL3611 and power104 proto code 2025-09-02 16:38:03 +08:00
douxu 3aab2c8a37 add telemetry machine code 2025-09-01 16:15:30 +08:00
douxu 37a1ccaadc modify the query conditions to tagname and fix building bug 2025-08-29 15:24:21 +08:00
douxu 858d02f955 add attr handlers 2025-08-27 17:33:10 +08:00
douxu 349d3398b2 add TAGNAME column in table grid、zone、station 2025-08-26 17:09:49 +08:00
douxu f8f83c38d9 add del func of redis string type 2025-08-21 17:04:10 +08:00
douxu 3fa0a8c6ca optimize covert func of component info 2025-08-18 17:02:38 +08:00
douxu f4ab4e4ea4 refactor(orm/circuit_diagram_component): fix compilation issues caused by structure field changes
http://server.baseware.net:9000/project/datart/task/47
2025-08-15 16:25:48 +08:00
douxu f7a1ea2540 feat(Implement-measurement-and-bay-structure-by-go): Implement measurement and bay structure by go
http://server.baseware.net:9000/project/datart/us/38?milestone=13
2025-08-13 16:24:29 +08:00
douxu 49fbd04644 optimize component struct http://server.baseware.net:9000/project/datart/task/47 2025-08-12 17:19:38 +08:00
douxu 426409ed91 feat(redis-string-class): 1. data token parse 2. redis string get 3. redis string set 4. redis string incr 2025-08-08 15:27:51 +08:00
douxu 3e833909d1 feat(token-parse): 1. add func of parse token 2.add func of query grid、zone、station、component 3.modify package of constant
http://server.baseware.net:9000/project/datart/task/22
2025-08-05 15:20:07 +08:00
douxu 1b6211b34b init attribute key struct 2025-07-31 16:52:21 +08:00
96 changed files with 3255 additions and 726 deletions

6
.gitignore vendored
View File

@ -21,4 +21,8 @@
# Go workspace file
go.work
.vscode
.vscode
# Shield all log files in the log folder
/log/
# Shield config files in the configs folder
/configs/**/*.yaml

View File

@ -5,7 +5,7 @@ import (
"sort"
"sync"
constants "modelRT/constant"
"modelRT/constants"
)
var (
@ -16,11 +16,11 @@ var (
// Event define alert event struct
type Event struct {
ComponentID int64
AnchorName string
Level constants.AlertLevel
Message string
StartTime int64
ComponentUUID string
AnchorName string
Level constants.AlertLevel
Message string
StartTime int64
}
// EventManager define store and manager alert event struct

View File

@ -2,7 +2,7 @@
package config
import (
constants "modelRT/constant"
"modelRT/constants"
)
// AnchorParamListConfig define anchor params list config struct
@ -15,7 +15,7 @@ type AnchorParamListConfig struct {
// AnchorParamBaseConfig define anchor params base config struct
type AnchorParamBaseConfig struct {
ComponentID int64 // component表 ID
ComponentUUID string // componentUUID
AnchorName string // 锚定参量名称
CompareValUpperLimit float64 // 比较值上限
CompareValLowerLimit float64 // 比较值下限

View File

@ -7,14 +7,20 @@ import (
"github.com/spf13/viper"
)
// BaseConfig define config stuct of base params config
// BaseConfig define config struct of base params config
type BaseConfig struct {
GridID int64 `mapstructure:"grid_id"`
ZoneID int64 `mapstructure:"zone_id"`
StationID int64 `mapstructure:"station_id"`
}
// KafkaConfig define config stuct of kafka config
// ServiceConfig define config struct of service config
type ServiceConfig struct {
ServiceName string `mapstructure:"service_name"`
SecretKey string `mapstructure:"secret_key"`
}
// KafkaConfig define config struct of kafka config
type KafkaConfig struct {
Servers string `mapstructure:"Servers"`
GroupID string `mapstructure:"group_id"`
@ -24,7 +30,7 @@ type KafkaConfig struct {
ReadMessageTimeDuration string `mapstructure:"read_message_time_duration"`
}
// PostgresConfig define config stuct of postgres config
// PostgresConfig define config struct of postgres config
type PostgresConfig struct {
Port int `mapstructure:"port"`
Host string `mapstructure:"host"`
@ -33,7 +39,7 @@ type PostgresConfig struct {
Password string `mapstructure:"password"`
}
// LoggerConfig define config stuct of zap logger config
// LoggerConfig define config struct of zap logger config
type LoggerConfig struct {
Mode string `mapstructure:"mode"`
Level string `mapstructure:"level"`
@ -44,7 +50,7 @@ type LoggerConfig struct {
Compress bool `mapstructure:"compress"`
}
// RedisConfig define config stuct of redis config
// RedisConfig define config struct of redis config
type RedisConfig struct {
Addr string `mapstructure:"addr"`
Password string `mapstructure:"password"`
@ -53,13 +59,13 @@ type RedisConfig struct {
Timeout int `mapstructure:"timeout"`
}
// AntsConfig define config stuct of ants pool config
// AntsConfig define config struct of ants pool config
type AntsConfig struct {
ParseConcurrentQuantity int `mapstructure:"parse_concurrent_quantity"` // parse comtrade file concurrent quantity
RTDReceiveConcurrentQuantity int `mapstructure:"rtd_receive_concurrent_quantity"` // polling real time data concurrent quantity
}
// DataRTConfig define config stuct of data runtime server api config
// DataRTConfig define config struct of data runtime server api config
type DataRTConfig struct {
Host string `mapstructure:"host"`
Port int64 `mapstructure:"port"`
@ -67,9 +73,10 @@ type DataRTConfig struct {
Method string `mapstructure:"polling_api_method"`
}
// ModelRTConfig define config stuct of model runtime server
// ModelRTConfig define config struct of model runtime server
type ModelRTConfig struct {
BaseConfig `mapstructure:"base"`
ServiceConfig `mapstructure:"service"`
PostgresConfig `mapstructure:"postgres"`
KafkaConfig `mapstructure:"kafka"`
LoggerConfig `mapstructure:"logger"`

View File

@ -1,65 +0,0 @@
postgres:
host: "192.168.2.103"
port: 5432
database: "demo"
user: "postgres"
password: "coslight"
kafka:
servers: "localhost:9092"
port: 9092
group_id: "modelRT"
topic: ""
auto_offset_reset: "earliest"
enable_auto_commit: "false"
read_message_time_duration: ”0.5s"
# influxdb:
# host: "localhost"
# port: "8086"
# token: "lCuiQ316qlly3iFeoi1EUokPJ0XxW-5lnG-3rXsKaaZSjfuxO5EaZfFdrNGM7Zlrdk1PrN_7TOsM_SCu9Onyew=="
# org: "coslight"
# bucket: "wave_record"
# zap logger config
logger:
mode: "development"
level: "debug"
filepath: "/home/douxu/log/modelRT-%s.log"
maxsize: 1
maxbackups: 5
maxage: 30
compress: false
# ants config
ants:
parse_concurrent_quantity: 10
rtd_receive_concurrent_quantity: 10
# redis config
locker_redis:
addr: "192.168.2.104:6379"
password: ""
db: 1
poolsize: 50
timeout: 10
storage_redis:
addr: "192.168.2.104:6379"
password: ""
db: 0
poolsize: 50
timeout: 10
# modelRT base config
base:
grid_id: 1
zone_id: 1
station_id: 1
# dataRT api config
dataRT:
host: "http://127.0.0.1"
port: 8888
polling_api: "datart/getPointData"
polling_api_method: "GET"

View File

@ -1,21 +0,0 @@
// Package constants define constant variable
package constants
import "errors"
var (
// ErrUUIDFromCheckT1 define error of check uuid from value failed in uuid from change type
ErrUUIDFromCheckT1 = errors.New("in uuid from change type, value of new uuid_from is equal value of old uuid_from")
// ErrUUIDToCheckT1 define error of check uuid to value failed in uuid from change type
ErrUUIDToCheckT1 = errors.New("in uuid from change type, value of new uuid_to is not equal value of old uuid_to")
// ErrUUIDFromCheckT2 define error of check uuid from value failed in uuid to change type
ErrUUIDFromCheckT2 = errors.New("in uuid to change type, value of new uuid_from is not equal value of old uuid_from")
// ErrUUIDToCheckT2 define error of check uuid to value failed in uuid to change type
ErrUUIDToCheckT2 = errors.New("in uuid to change type, value of new uuid_to is equal value of old uuid_to")
// ErrUUIDFromCheckT3 define error of check uuid from value failed in uuid add change type
ErrUUIDFromCheckT3 = errors.New("in uuid add change type, value of old uuid_from is not empty")
// ErrUUIDToCheckT3 define error of check uuid to value failed in uuid add change type
ErrUUIDToCheckT3 = errors.New("in uuid add change type, value of old uuid_to is not empty")
)

11
constants/attrs_key.go Normal file
View File

@ -0,0 +1,11 @@
// Package constants define constant variable
package constants
const (
// ShortAttrKeyLenth define short attribute key length
ShortAttrKeyLenth int = 4
// LongAttrKeyLenth define long attribute key length
LongAttrKeyLenth int = 7
)
// component、base_extend、rated、setup、model、stable、bay、craft、integrity、behavior

51
constants/error.go Normal file
View File

@ -0,0 +1,51 @@
// Package constants define constant variable
package constants
import "errors"
var (
// ErrUUIDFromCheckT1 define error of check uuid from value failed in uuid from change type
ErrUUIDFromCheckT1 = errors.New("in uuid from change type, value of new uuid_from is equal value of old uuid_from")
// ErrUUIDToCheckT1 define error of check uuid to value failed in uuid from change type
ErrUUIDToCheckT1 = errors.New("in uuid from change type, value of new uuid_to is not equal value of old uuid_to")
// ErrUUIDFromCheckT2 define error of check uuid from value failed in uuid to change type
ErrUUIDFromCheckT2 = errors.New("in uuid to change type, value of new uuid_from is not equal value of old uuid_from")
// ErrUUIDToCheckT2 define error of check uuid to value failed in uuid to change type
ErrUUIDToCheckT2 = errors.New("in uuid to change type, value of new uuid_to is equal value of old uuid_to")
// ErrUUIDFromCheckT3 define error of check uuid from value failed in uuid add change type
ErrUUIDFromCheckT3 = errors.New("in uuid add change type, value of old uuid_from is not empty")
// ErrUUIDToCheckT3 define error of check uuid to value failed in uuid add change type
ErrUUIDToCheckT3 = errors.New("in uuid add change type, value of old uuid_to is not empty")
)
var (
// ErrInvalidAddressType define error of invalid io address type
ErrInvalidAddressType = errors.New("invalid address type")
// ErrUnknownDataType define error of unknown measurement data source type
ErrUnknownDataType = errors.New("unknown data type")
// ErrExceedsLimitType define error of channel number exceeds limit for telemetry
ErrExceedsLimitType = errors.New("channel number exceeds limit for Telemetry")
// ErrUnsupportedChannelPrefixType define error of unsupported channel prefix
ErrUnsupportedChannelPrefixType = errors.New("unsupported channel prefix")
)
var (
// ErrFormatUUID define error of format uuid string to uuid.UUID type failed
ErrFormatUUID = errors.New("format string type to uuid.UUID type failed")
// ErrFormatCache define error of format cache with any type to cacheItem type failed
ErrFormatCache = errors.New("format any teype to cache item type failed")
)
// ErrGetClientToken define error of can not get client_token from context
var ErrGetClientToken = errors.New("can not get client_token from context")
// ErrQueryComponentByUUID define error of query component from db by uuid failed
var ErrQueryComponentByUUID = errors.New("query component from db failed by uuid")
// ErrChanIsNil define error of channel is nil
var ErrChanIsNil = errors.New("this channel is nil")
// ErrConcurrentModify define error of concurrent modification detected
var ErrConcurrentModify = errors.New("existed concurrent modification risk")

24
constants/keys.go Normal file
View File

@ -0,0 +1,24 @@
// Package constants define constant variable
package constants
const (
// RedisAllGridSetKey define redis set key which store all grid keys
RedisAllGridSetKey = "grid_keys"
// RedisSpecGridZoneSetKey define redis set key which store all zone keys under specific grid
RedisSpecGridZoneSetKey = "grid_%s_zones_keys"
// RedisAllZoneSetKey define redis set key which store all zone keys
RedisAllZoneSetKey = "zone_keys"
// RedisSpecZoneStationSetKey define redis set key which store all station keys under specific zone
RedisSpecZoneStationSetKey = "zone_%s_stations_keys"
// RedisAllStationSetKey define redis set key which store all station keys
RedisAllStationSetKey = "station_keys"
// RedisSpecStationComponentSetKey define redis set key which store all component keys under specific station
RedisSpecStationComponentSetKey = "station_%s_components_keys"
// RedisAllComponentSetKey define redis set key which store all component keys
RedisAllComponentSetKey = "component_keys"
// RedisSpecComponentSetKey define redis set key which store all component keys under specific zone
RedisSpecComponentSetKey = "zone_%s_components_keys"
)

31
constants/measurement.go Normal file
View File

@ -0,0 +1,31 @@
// Package constants define constant variable
package constants
const (
// DataSourceTypeCL3611 define CL3611 type
DataSourceTypeCL3611 = 1
// DataSourceTypePower104 define electricity 104 protocol type
DataSourceTypePower104 = 2
)
// channel name prefix
const (
ChannelPrefixTelemetry = "Telemetry"
ChannelPrefixTelesignal = "Telesignal"
ChannelPrefixTelecommand = "Telecommand"
ChannelPrefixTeleadjusting = "Teleadjusting"
ChannelPrefixSetpoints = "Setpoints"
)
// channel name suffix
const (
ChannelSuffixP = "P"
ChannelSuffixQ = "Q"
ChannelSuffixS = "S"
ChannelSuffixPS = "PS"
ChannelSuffixF = "F"
ChannelSuffixDeltaF = "deltaF"
ChannelSuffixUAB = "UAB"
ChannelSuffixUBC = "UBC"
ChannelSuffixUCA = "UCA"
)

7
constants/redis.go Normal file
View File

@ -0,0 +1,7 @@
// Package constants define constant variable
package constants
const (
// RedisSearchDictName define redis search dictionary name
RedisSearchDictName = "search_suggestions_dict"
)

9
constants/trace.go Normal file
View File

@ -0,0 +1,9 @@
// Package constants define constant variable
package constants
// Assuming the B3 specification
const (
HeaderTraceID = "X-B3-TraceId"
HeaderSpanID = "X-B3-SpanId"
HeaderParentSpanID = "X-B3-ParentSpanId"
)

View File

@ -16,27 +16,25 @@ import (
)
// CreateComponentIntoDB define create component info of the circuit diagram into DB
func CreateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo network.ComponentCreateInfo) (int64, error) {
func CreateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo network.ComponentCreateInfo) (string, error) {
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
globalUUID, err := uuid.FromString(componentInfo.UUID)
if err != nil {
return -1, fmt.Errorf("format uuid from string type failed:%w", err)
return "", fmt.Errorf("format uuid from string type failed:%w", err)
}
component := orm.Component{
GlobalUUID: globalUUID,
GridID: strconv.FormatInt(componentInfo.GridID, 10),
ZoneID: strconv.FormatInt(componentInfo.ZoneID, 10),
StationID: strconv.FormatInt(componentInfo.StationID, 10),
PageID: componentInfo.PageID,
Tag: componentInfo.Tag,
ComponentType: componentInfo.ComponentType,
Name: componentInfo.Name,
Context: componentInfo.Context,
Op: componentInfo.Op,
Ts: time.Now(),
GlobalUUID: globalUUID,
GridID: strconv.FormatInt(componentInfo.GridID, 10),
ZoneID: strconv.FormatInt(componentInfo.ZoneID, 10),
StationID: strconv.FormatInt(componentInfo.StationID, 10),
Tag: componentInfo.Tag,
Name: componentInfo.Name,
Context: componentInfo.Context,
Op: componentInfo.Op,
Ts: time.Now(),
}
result := tx.WithContext(cancelCtx).Create(&component)
@ -45,7 +43,7 @@ func CreateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo netwo
if result.RowsAffected == 0 {
err = fmt.Errorf("%w:please check insert component slice", errcode.ErrInsertRowUnexpected)
}
return -1, fmt.Errorf("insert component info failed:%w", err)
return "", fmt.Errorf("insert component info failed:%w", err)
}
return component.ID, nil
return component.GlobalUUID.String(), nil
}

View File

@ -0,0 +1,97 @@
// Package database define database operation functions
package database
import (
"context"
"errors"
"fmt"
"strings"
"modelRT/diagram"
"modelRT/model"
"gorm.io/gorm"
)
// ParseAttrToken define return the attribute model interface based on the input attribute token. doc addr http://server.baseware.net:6875/books/product-design-docs/page/d6baf
func ParseAttrToken(ctx context.Context, tx *gorm.DB, attrToken, clientToken string) (model.AttrModelInterface, error) {
rs := diagram.NewRedisString(ctx, attrToken, clientToken, 10, true)
attrSlice := strings.Split(attrToken, ".")
attrLen := len(attrSlice)
if attrLen == 4 {
short := &model.ShortAttrInfo{
AttrGroupName: attrSlice[2],
AttrKey: attrSlice[3],
}
err := FillingShortAttrModel(ctx, tx, attrSlice, short)
if err != nil {
return nil, err
}
attrValue, err := rs.Get(attrToken)
if err != nil {
return nil, err
}
short.AttrValue = attrValue
return short, nil
} else if attrLen == 7 {
long := &model.LongAttrInfo{
AttrGroupName: attrSlice[5],
AttrKey: attrSlice[6],
}
err := FillingLongAttrModel(ctx, tx, attrSlice, long)
if err != nil {
return nil, err
}
attrValue, err := rs.Get(attrToken)
if err != nil {
return nil, err
}
long.AttrValue = attrValue
return long, nil
}
return nil, errors.New("invalid attribute token format")
}
// FillingShortAttrModel define filling short attribute model info
func FillingShortAttrModel(ctx context.Context, tx *gorm.DB, attrItems []string, attrModel *model.ShortAttrInfo) error {
component, err := QueryComponentByNsPath(ctx, tx, attrItems[0])
if err != nil {
return err
}
attrModel.ComponentInfo = &component
return nil
}
// FillingLongAttrModel define filling long attribute model info
func FillingLongAttrModel(ctx context.Context, tx *gorm.DB, attrItems []string, attrModel *model.LongAttrInfo) error {
grid, err := QueryGridByTagName(ctx, tx, attrItems[0])
if err != nil {
return err
}
attrModel.GridInfo = &grid
zone, err := QueryZoneByTagName(ctx, tx, attrItems[1])
if err != nil {
return err
}
attrModel.ZoneInfo = &zone
station, err := QueryStationByTagName(ctx, tx, attrItems[2])
if err != nil {
return err
}
attrModel.StationInfo = &station
component, err := QueryComponentByNsPath(ctx, tx, attrItems[3])
if err != nil {
return err
}
attrModel.ComponentInfo = &component
return nil
}
// QueryAttrValueFromRedis define query attribute value from redis by attrKey
func QueryAttrValueFromRedis(attrKey string) string {
fmt.Println(attrKey)
return ""
}

View File

@ -5,42 +5,37 @@ import (
"context"
"time"
"modelRT/config"
"modelRT/logger"
"modelRT/orm"
"github.com/gofrs/uuid"
"github.com/panjf2000/ants/v2"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// QueryCircuitDiagramComponentFromDB return the result of query circuit diagram component info order by page id from postgresDB
func QueryCircuitDiagramComponentFromDB(ctx context.Context, tx *gorm.DB, pool *ants.PoolWithFunc) (map[uuid.UUID]int, error) {
var components []orm.Component
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// func QueryCircuitDiagramComponentFromDB(ctx context.Context, tx *gorm.DB, pool *ants.PoolWithFunc) (map[uuid.UUID]string, error) {
// var components []orm.Component
// // ctx超时判断
// cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
// defer cancel()
result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&components)
if result.Error != nil {
logger.Error(ctx, "query circuit diagram component info failed", "error", result.Error)
return nil, result.Error
}
// result := tx.WithContext(cancelCtx).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&components)
// if result.Error != nil {
// logger.Error(ctx, "query circuit diagram component info failed", "error", result.Error)
// return nil, result.Error
// }
// TODO 优化componentTypeMap输出
componentTypeMap := make(map[uuid.UUID]int, len(components))
// componentTypeMap := make(map[uuid.UUID]string, len(components))
// for _, component := range components {
// pool.Invoke(config.ModelParseConfig{
// ComponentInfo: component,
// Ctx: ctx,
// })
for _, component := range components {
pool.Invoke(config.ModelParseConfig{
ComponentInfo: component,
Ctx: ctx,
})
componentTypeMap[component.GlobalUUID] = component.ComponentType
}
return componentTypeMap, nil
}
// componentTypeMap[component.GlobalUUID] = component.GlobalUUID.String()
// }
// return componentTypeMap, nil
// }
// QueryComponentByUUID return the result of query circuit diagram component info by uuid from postgresDB
func QueryComponentByUUID(ctx context.Context, tx *gorm.DB, uuid uuid.UUID) (orm.Component, error) {
@ -48,8 +43,11 @@ func QueryComponentByUUID(ctx context.Context, tx *gorm.DB, uuid uuid.UUID) (orm
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).
Where("global_uuid = ?", uuid).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&component)
result := tx.WithContext(cancelCtx).Where("global_uuid = ? ", uuid).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&component)
if result.Error != nil {
return orm.Component{}, result.Error
}
@ -69,3 +67,17 @@ func QueryComponentByPageID(ctx context.Context, tx *gorm.DB, uuid uuid.UUID) (o
}
return component, nil
}
// QueryComponentByNsPath return the result of query circuit diagram component info by ns path from postgresDB
func QueryComponentByNsPath(ctx context.Context, tx *gorm.DB, nsPath string) (orm.Component, error) {
var component orm.Component
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).Where("NAME = ? ", nsPath).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&component)
if result.Error != nil {
return orm.Component{}, result.Error
}
return component, nil
}

26
database/query_grid.go Normal file
View File

@ -0,0 +1,26 @@
// Package database define database operation functions
package database
import (
"context"
"time"
"modelRT/orm"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// QueryGridByTagName return the result of query circuit diagram grid info by tagName from postgresDB
func QueryGridByTagName(ctx context.Context, tx *gorm.DB, tagName string) (orm.Grid, error) {
var grid orm.Grid
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).Where("TAGNAME = ? ", tagName).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&grid)
if result.Error != nil {
return orm.Grid{}, result.Error
}
return grid, nil
}

View File

@ -0,0 +1,29 @@
// Package database define database operation functions
package database
import (
"context"
"time"
"modelRT/orm"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// QueryMeasurementByID return the result of query circuit diagram component measurement info by id from postgresDB
func QueryMeasurementByID(ctx context.Context, tx *gorm.DB, id int64) (orm.Measurement, error) {
var component orm.Measurement
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).
Where("id = ?", id).
Clauses(clause.Locking{Strength: "UPDATE"}).
First(&component)
if result.Error != nil {
return orm.Measurement{}, result.Error
}
return component, nil
}

26
database/query_station.go Normal file
View File

@ -0,0 +1,26 @@
// Package database define database operation functions
package database
import (
"context"
"time"
"modelRT/orm"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// QueryStationByTagName return the result of query circuit diagram Station info by tagName from postgresDB
func QueryStationByTagName(ctx context.Context, tx *gorm.DB, tagName string) (orm.Station, error) {
var station orm.Station
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).Where("TAGNAME = ? ", tagName).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&station)
if result.Error != nil {
return orm.Station{}, result.Error
}
return station, nil
}

View File

@ -6,7 +6,7 @@ import (
"fmt"
"time"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/orm"
@ -33,14 +33,14 @@ func QueryTopologic(ctx context.Context, tx *gorm.DB) ([]orm.Topologic, error) {
}
// QueryTopologicFromDB return the result of query topologic info from DB
func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB, componentTypeMap map[uuid.UUID]int) (*diagram.MultiBranchTreeNode, error) {
func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB) (*diagram.MultiBranchTreeNode, error) {
topologicInfos, err := QueryTopologic(ctx, tx)
if err != nil {
logger.Error(ctx, "query topologic info failed", "error", err)
return nil, err
}
tree, err := BuildMultiBranchTree(topologicInfos, componentTypeMap)
tree, err := BuildMultiBranchTree(topologicInfos)
if err != nil {
logger.Error(ctx, "init topologic failed", "error", err)
return nil, err
@ -49,17 +49,11 @@ func QueryTopologicFromDB(ctx context.Context, tx *gorm.DB, componentTypeMap map
}
// InitCircuitDiagramTopologic return circuit diagram topologic info from postgres
func InitCircuitDiagramTopologic(topologicNodes []orm.Topologic, componentTypeMap map[uuid.UUID]int) error {
func InitCircuitDiagramTopologic(topologicNodes []orm.Topologic) error {
var rootVertex *diagram.MultiBranchTreeNode
for _, node := range topologicNodes {
if node.UUIDFrom == constants.UUIDNil {
// rootVertex = node.UUIDTo
var componentType int
componentType, ok := componentTypeMap[node.UUIDFrom]
if !ok {
return fmt.Errorf("can not get component type by uuid: %s", node.UUIDFrom)
}
rootVertex = diagram.NewMultiBranchTree(node.UUIDFrom, componentType)
rootVertex = diagram.NewMultiBranchTree(node.UUIDFrom)
break
}
}
@ -70,13 +64,7 @@ func InitCircuitDiagramTopologic(topologicNodes []orm.Topologic, componentTypeMa
for _, node := range topologicNodes {
if node.UUIDFrom == constants.UUIDNil {
var componentType int
componentType, ok := componentTypeMap[node.UUIDTo]
if !ok {
return fmt.Errorf("can not get component type by uuid: %s", node.UUIDTo)
}
nodeVertex := diagram.NewMultiBranchTree(node.UUIDTo, componentType)
nodeVertex := diagram.NewMultiBranchTree(node.UUIDTo)
rootVertex.AddChild(nodeVertex)
}
}
@ -91,9 +79,7 @@ func InitCircuitDiagramTopologic(topologicNodes []orm.Topologic, componentTypeMa
// TODO 电流互感器不单独划分间隔,以母线、浇筑母线、变压器为间隔原件
func IntervalBoundaryDetermine(uuid uuid.UUID) bool {
fmt.Println(uuid)
var componentID int64
diagram.GetComponentMap(componentID)
diagram.GetComponentMap(uuid.String())
// TODO 判断 component 的类型是否为间隔
// TODO 0xA1B2C3D4,高四位表示可以成为间隔的compoent类型的值为FFFF,普通 component 类型的值为 0000。低四位中前二位表示component的一级类型例如母线 PT、母联/母分、进线等,低四位中后二位表示一级类型中包含的具体类型,例如母线 PT中包含的电压互感器、隔离开关、接地开关、避雷器、带电显示器等。
num := uint32(0xA1B2C3D4) // 八位16进制数
@ -104,23 +90,16 @@ func IntervalBoundaryDetermine(uuid uuid.UUID) bool {
}
// BuildMultiBranchTree return the multi branch tree by topologic info and component type map
func BuildMultiBranchTree(topologics []orm.Topologic, componentTypeMap map[uuid.UUID]int) (*diagram.MultiBranchTreeNode, error) {
func BuildMultiBranchTree(topologics []orm.Topologic) (*diagram.MultiBranchTreeNode, error) {
nodeMap := make(map[uuid.UUID]*diagram.MultiBranchTreeNode, len(topologics)*2)
for _, topo := range topologics {
if _, exists := nodeMap[topo.UUIDFrom]; !exists {
// skip special uuid
if topo.UUIDTo != constants.UUIDNil {
var ok bool
componentType, ok := componentTypeMap[topo.UUIDFrom]
if !ok {
return nil, fmt.Errorf("can not get component type by uuid: %s", topo.UUIDFrom)
}
nodeMap[topo.UUIDFrom] = &diagram.MultiBranchTreeNode{
ID: topo.UUIDFrom,
NodeComponentType: componentType,
Children: make([]*diagram.MultiBranchTreeNode, 0),
ID: topo.UUIDFrom,
Children: make([]*diagram.MultiBranchTreeNode, 0),
}
}
}
@ -128,16 +107,9 @@ func BuildMultiBranchTree(topologics []orm.Topologic, componentTypeMap map[uuid.
if _, exists := nodeMap[topo.UUIDTo]; !exists {
// skip special uuid
if topo.UUIDTo != constants.UUIDNil {
var ok bool
componentType, ok := componentTypeMap[topo.UUIDTo]
if !ok {
return nil, fmt.Errorf("can not get component type by uuid: %s", topo.UUIDTo)
}
nodeMap[topo.UUIDTo] = &diagram.MultiBranchTreeNode{
ID: topo.UUIDTo,
NodeComponentType: componentType,
Children: make([]*diagram.MultiBranchTreeNode, 0),
ID: topo.UUIDTo,
Children: make([]*diagram.MultiBranchTreeNode, 0),
}
}
}
@ -146,10 +118,8 @@ func BuildMultiBranchTree(topologics []orm.Topologic, componentTypeMap map[uuid.
for _, topo := range topologics {
var parent *diagram.MultiBranchTreeNode
if topo.UUIDFrom == constants.UUIDNil {
var componentType int
parent = &diagram.MultiBranchTreeNode{
ID: constants.UUIDNil,
NodeComponentType: componentType,
ID: constants.UUIDNil,
}
nodeMap[constants.UUIDNil] = parent
} else {
@ -158,10 +128,8 @@ func BuildMultiBranchTree(topologics []orm.Topologic, componentTypeMap map[uuid.
var child *diagram.MultiBranchTreeNode
if topo.UUIDTo == constants.UUIDNil {
var componentType int
child = &diagram.MultiBranchTreeNode{
ID: topo.UUIDTo,
NodeComponentType: componentType,
ID: topo.UUIDTo,
}
} else {
child = nodeMap[topo.UUIDTo]

26
database/query_zone.go Normal file
View File

@ -0,0 +1,26 @@
// Package database define database operation functions
package database
import (
"context"
"time"
"modelRT/orm"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// QueryZoneByTagName return the result of query circuit diagram Zone info by tagName from postgresDB
func QueryZoneByTagName(ctx context.Context, tx *gorm.DB, tagName string) (orm.Zone, error) {
var zone orm.Zone
// ctx超时判断
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
result := tx.WithContext(cancelCtx).Where("TAGNAME = ? ", tagName).Clauses(clause.Locking{Strength: "UPDATE"}).Find(&zone)
if result.Error != nil {
return orm.Zone{}, result.Error
}
return zone, nil
}

View File

@ -16,13 +16,13 @@ import (
)
// UpdateComponentIntoDB define update component info of the circuit diagram into DB
func UpdateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo network.ComponentUpdateInfo) (int64, error) {
func UpdateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo network.ComponentUpdateInfo) (string, error) {
cancelCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
globalUUID, err := uuid.FromString(componentInfo.UUID)
if err != nil {
return -1, fmt.Errorf("format uuid from string type failed:%w", err)
return "", fmt.Errorf("format uuid from string type failed:%w", err)
}
var component orm.Component
@ -32,31 +32,29 @@ func UpdateComponentIntoDB(ctx context.Context, tx *gorm.DB, componentInfo netwo
if result.RowsAffected == 0 {
err = fmt.Errorf("%w:please check update component conditions", errcode.ErrUpdateRowZero)
}
return -1, fmt.Errorf("query component info failed:%w", err)
return "", fmt.Errorf("query component info failed:%w", err)
}
updateParams := orm.Component{
GlobalUUID: globalUUID,
GridID: strconv.FormatInt(componentInfo.GridID, 10),
ZoneID: strconv.FormatInt(componentInfo.ZoneID, 10),
StationID: strconv.FormatInt(componentInfo.StationID, 10),
PageID: componentInfo.PageID,
Tag: componentInfo.Tag,
ComponentType: componentInfo.ComponentType,
Name: componentInfo.Name,
Context: componentInfo.Context,
Op: componentInfo.Op,
Ts: time.Now(),
GlobalUUID: globalUUID,
GridID: strconv.FormatInt(componentInfo.GridID, 10),
ZoneID: strconv.FormatInt(componentInfo.ZoneID, 10),
StationID: strconv.FormatInt(componentInfo.StationID, 10),
Tag: componentInfo.Tag,
Name: componentInfo.Name,
Context: componentInfo.Context,
Op: componentInfo.Op,
Ts: time.Now(),
}
result = tx.Model(&orm.Component{}).WithContext(cancelCtx).Where("id = ?", component.ID).Updates(&updateParams)
result = tx.Model(&orm.Component{}).WithContext(cancelCtx).Where("GLOBAL_UUID = ?", component.GlobalUUID).Updates(&updateParams)
if result.Error != nil || result.RowsAffected == 0 {
err := result.Error
if result.RowsAffected == 0 {
err = fmt.Errorf("%w:please check update component conditions", errcode.ErrUpdateRowZero)
}
return -1, fmt.Errorf("update component info failed:%w", err)
return "", fmt.Errorf("update component info failed:%w", err)
}
return component.ID, nil
return component.GlobalUUID.String(), nil
}

View File

@ -7,7 +7,7 @@ import (
"time"
"modelRT/common/errcode"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/network"
"modelRT/orm"

211
deploy/deploy.md Normal file
View File

@ -0,0 +1,211 @@
# 项目依赖服务部署指南
本项目依赖于 $\text{PostgreSQL}$ 数据库和 $\text{Redis Stack Server}$(包含 $\text{Redisearch}$ 等模块)部署文档将使用 $\text{Docker}$ 容器化技术部署这两个依赖服务
## 前提条件
1. 已安装 $\text{Docker}$
2. 下载相关容器镜像
3. 确保主机的 $\text{5432}$ 端口($\text{Postgres}$)和 $\text{6379}$ 端口($\text{Redis}$)未被占用
### 1\. 部署 PostgreSQL 数据库
使用官方的 `postgres:13.16` 镜像,并设置默认的用户、密码和端口
#### 1.1 部署命令
运行以下命令启动 $\text{PostgreSQL}$ 容器
```bash
docker run --name postgres \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=coslight \
-p 5432:5432 \
-d postgres:13.16
```
#### 1.2 连接信息
| 参数 | 值 | 说明 |
| :--- | :--- | :--- |
| **容器名称** | `postgres` | 容器名 |
| **镜像版本** | `postgres:13.16` | 镜像名 |
| **主机端口** | `5432` | 外部应用连接使用的端口 |
| **用户名** | `postgres` | 默认超级用户 |
| **密码** | `coslight` | 配置的密码 |
#### 1.3 状态检查
要确认容器是否正在运行,请执行
```bash
# 检查容器启动状态
docker ps -a grep postgres
# 检查容器启动日志信息
docker logs postgres
```
### 2\. 部署 Redis Stack Server
我们将使用 `redis/redis-stack-server:latest` 镜像该镜像内置了 $\text{Redisearch}$ 模块,用于 $\text{ModelRT}$ 项目中补全功能
#### 2.1 部署命令
运行以下命令启动 $\text{Redis Stack Server}$ 容器
```bash
docker run --name redis -p 6379:6379 \
-d redis/redis-stack-server:latest
```
#### 2.2 连接信息
| 参数 | 值 | 说明 |
| :--- | :--- | :--- |
| **容器名称** | `redis` | 容器名 |
| **镜像版本** | `redis/redis-stack-server:latest` | 镜像名 |
| **主机端口** | `6379` | 外部应用连接使用的端口 |
| **地址** | `localhost:6379` | |
| **密码** | **无** | 默认未设置密码 |
> **注意:** 生产环境中建议使用 `-e REDIS_PASSWORD=<your_secure_password>` 参数来设置 $\text{Redis}$ 访问密码
#### 2.3 状态检查
要确认容器是否正在运行,请执行
```bash
# 检查容器启动状态
docker ps -a grep redis
# 检查容器启动日志信息
docker logs redis
```
#### 2.4 数据注入
测试数据注入
##### 2.4.1 Postgres数据注入
```SQL
INSERT INTO public."Topologic" VALUES (2, 1, '70c190f2-8a60-42a9-b143-ec5f87e0aa6b', '10f155cf-bd27-4557-85b2-d126b6e2657f', 1, NULL);
INSERT INTO public."Topologic" VALUES (3, 1, '70c190f2-8a60-42a9-b143-ec5f87e0aa6b', 'e32bc0be-67f4-4d79-a5da-eaa40a5bd77d', 1, NULL);
INSERT INTO public."Topologic" VALUES (4, 1, '70c190f2-8a60-42a9-b143-ec5f87e0aa6b', '70c190f2-8a75-42a9-b166-ec5f87e0aa6b', 1, NULL);
INSERT INTO public."Topologic" VALUES (5, 1, 'e32bc0be-67f4-4d79-a5da-eaa40a5bd77d', '70c200f2-8a75-42a9-c166-bf5f87e0aa6b', 1, NULL);
INSERT INTO public."Topologic" VALUES (1, 1, '00000000-0000-0000-0000-000000000000', '70c190f2-8a60-42a9-b143-ec5f87e0aa6b', 1, NULL);
```
##### 2.4.2 Redis数据注入
Redis数据脚本
```Lua
redis.call('SADD', 'grid_keys', 'transformfeeder1_220', 'transformfeeder1_220_35', 'transformfeeder1_220_36')
redis.call('SADD', 'grid_transformfeeder1_220_zones_keys', 'I_A_rms', 'I_B_rms', 'I_C_rms')
redis.call('SADD', 'grid_transformfeeder1_220_35_zones_keys', 'I_A_rms', 'I_B_rms', 'I_C_rms')
redis.call('SADD', 'grid_transformfeeder1_220_36_zones_keys', 'I_A_rms', 'I_B_rms', 'I_C_rms')
local dict_key = 'search_suggestions_dict'
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_35', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_36', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220.I_A_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220.I_B_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220.I_C_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_35.I_A_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_35.I_B_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_35.I_C_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_36.I_A_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_36.I_B_rms', 1)
redis.call('FT.SUGADD', dict_key, 'transformfeeder1_220_36.I_C_rms', 1)
return 'OK'
```
在Redis CLI 中导入命令
1. 使用 `EVAL "lua脚本" 0`即可成功导入数据
2. 使用 `SCRIPT LOAD "lua脚本"`加载脚本,然后使用 `EVAL SHA1值 0` 命令执行上一步存储命令返回的哈希值即可
### 3\. 启动 ModelRT 服务
#### 3.1 配置服务配置文件
以下表格为配置文件参数说明表
| 类别 | 参数名 | 作用描述 | 示例值 |
| :--- | :--- | :--- | :--- |
| **Postgres** | `host` | PostgreSQL 数据库服务器的 $\text{IP}$ 地址或域名。 | `"192.168.1.101"` |
| | `port` | PostgreSQL 数据库服务器的端口号。 | `5432` |
| | `database` | 连接的数据库名称。 | `"demo"` |
| | `user` | 连接数据库所使用的用户名。 | `"postgres"` |
| | `password` | 连接数据库所使用的密码。 | `"coslight"` |
| **Kafka** | `servers` | Kafka 集群的 $\text{Bootstrap Server}$ 地址列表(通常是 $\text{host:port}$ 形式,多个地址用逗号分隔)。 | `"localhost:9092"` |
| | `port` | Kafka 服务器的端口号。 | `9092` |
| | `group_id` | 消费者组 $\text{ID}$,用于标识和管理一组相关的消费者。 | `"modelRT"` |
| | `topic` | Kafka 消息的主题名称。 | `""` |
| | `auto_offset_reset` | 消费者首次启动或 $\text{Offset}$ 无效时,从哪个位置开始消费(如 `earliest``latest`)。 | `"earliest"` |
| | `enable_auto_commit` | 是否自动提交 $\text{Offset}$。设为 $\text{false}$ 通常用于手动控制 $\text{Offset}$ 提交。 | `"false"` |
| | `read_message_time_duration` | 读取消息时的超时或等待时间。 | `”0.5s"` |
| **Logger (Zap)** | `mode` | 日志模式,通常为 `development`(开发)或 `production`(生产)。影响日志格式。 | `"development"` |
| | `level` | 最低日志级别(如 $\text{debug, info, warn, error}$)。 | `"debug"` |
| | `filepath` | 日志文件的输出路径和名称格式(`%s` 会被替换为日期等)。 | `"/Users/douxu/Workspace/coslight/modelRT/modelRT-%s.log"` |
| | `maxsize` | 单个日志文件最大大小(单位:$\text{MB}$)。 | `1` |
| | `maxbackups` | 保留旧日志文件的最大个数。 | `5` |
| | `maxage` | 保留旧日志文件的最大天数。 | `30` |
| | `compress` | 是否压缩备份的日志文件。 | `false` |
| **Ants Pool** | `parse_concurrent_quantity` | 用于解析任务的协程池最大并发数量。 | `10` |
| | `rtd_receive_concurrent_quantity` | 用于实时数据接收任务的协程池最大并发数量。 | `10` |
| **Locker Redis** | `addr` | 分布式锁服务所使用的 $\text{Redis}$ 地址。 | `"127.0.0.1:6379"` |
| | `password` | $\text{Locker Redis}$ 的密码。 | `""` |
| | `db` | $\text{Locker Redis}$ 使用的数据库编号。 | `1` |
| | `poolsize` | $\text{Locker Redis}$ 连接池的最大连接数。 | `50` |
| | `timeout` | $\text{Locker Redis}$ 连接操作的超时时间(单位:毫秒)。 | `10` |
| **Storage Redis** | `addr` | 数据存储服务所使用的 $\text{Redis}$ 地址(例如 $\text{Redisearch}$)。 | `"127.0.0.1:6379"` |
| | `password` | $\text{Storage Redis}$ 的密码。 | `""` |
| | `db` | $\text{Storage Redis}$ 使用的数据库编号。 | `0` |
| | `poolsize` | $\text{Storage Redis}$ 连接池的最大连接数。 | `50` |
| | `timeout` | $\text{Storage Redis}$ 连接操作的超时时间(单位:毫秒)。 | `10` |
| **Base Config** | `grid_id` | 项目所操作的默认电网 $\text{ID}$。 | `1` |
| | `zone_id` | 项目所操作的默认区域 $\text{ID}$。 | `1` |
| | `station_id` | 项目所操作的默认变电站 $\text{ID}$。 | `1` |
| **Service Config** | `service_name` | 服务名称,用于日志、监控等标识。 | `"modelRT"` |
| | `secret_key` | 服务内部使用的秘钥,用于签名或认证。 | `"modelrt_key"` |
| **DataRT API** | `host` | 外部 $\text{DataRT}$ 服务的主机地址。 | `"http://127.0.0.1"` |
| | `port` | $\text{DataRT}$ 服务的端口号。 | `8888` |
| | `polling_api` | 轮询数据的 $\text{API}$ 路径。 | `"datart/getPointData"` |
| | `polling_api_method` | 调用该 $\text{API}$ 使用的 $\text{HTTP}$ 方法。 | `"GET"` |
#### 3.2 编译 ModelRT 服务
```bash
go build -o model-rt main.go
```
#### 3.3 启动服务
使用编译好的二进制文件进行启动
```bash
./model-rt
```
#### 3.4 检测服务启动日志
在发现控制台输出如下信息`starting ModelRT server`
后即代表服务启动成功
### 4\. 后续操作(停止与清理)
#### 4.1 停止容器
```bash
docker stop postgres redis
```
#### 4.2 删除容器(删除后数据将丢失)
```bash
docker rm postgres redis
```

View File

@ -10,10 +10,10 @@ import (
var anchorValueOverview sync.Map
// GetAnchorValue define func of get circuit diagram data by componentID
func GetAnchorValue(componentID int64) (string, error) {
value, ok := diagramsOverview.Load(componentID)
func GetAnchorValue(componentUUID string) (string, error) {
value, ok := diagramsOverview.Load(componentUUID)
if !ok {
return "", fmt.Errorf("can not find anchor value by componentID:%d", componentID)
return "", fmt.Errorf("can not find anchor value by componentUUID:%s", componentUUID)
}
anchorValue, ok := value.(string)
if !ok {
@ -22,20 +22,20 @@ func GetAnchorValue(componentID int64) (string, error) {
return anchorValue, nil
}
// UpdateAnchorValue define func of update anchor value by componentID and anchor name
func UpdateAnchorValue(componentID int64, anchorValue string) bool {
_, result := anchorValueOverview.Swap(componentID, anchorValue)
// UpdateAnchorValue define func of update anchor value by componentUUID and anchor name
func UpdateAnchorValue(componentUUID string, anchorValue string) bool {
_, result := anchorValueOverview.Swap(componentUUID, anchorValue)
return result
}
// StoreAnchorValue define func of store anchor value with componentID and anchor name
func StoreAnchorValue(componentID int64, anchorValue string) {
anchorValueOverview.Store(componentID, anchorValue)
// StoreAnchorValue define func of store anchor value with componentUUID and anchor name
func StoreAnchorValue(componentUUID string, anchorValue string) {
anchorValueOverview.Store(componentUUID, anchorValue)
return
}
// DeleteAnchorValue define func of delete anchor value with componentID
func DeleteAnchorValue(componentID int64) {
anchorValueOverview.Delete(componentID)
// DeleteAnchorValue define func of delete anchor value with componentUUID
func DeleteAnchorValue(componentUUID string) {
anchorValueOverview.Delete(componentUUID)
return
}

View File

@ -4,38 +4,40 @@ import (
"errors"
"fmt"
"sync"
"modelRT/orm"
)
// diagramsOverview define struct of storage all circuit diagram data
var diagramsOverview sync.Map
// GetComponentMap define func of get circuit diagram data by component id
func GetComponentMap(componentID int64) (map[string]interface{}, error) {
value, ok := diagramsOverview.Load(componentID)
// GetComponentMap define func of get circuit diagram data by component uuid
func GetComponentMap(componentUUID string) (*orm.Component, error) {
value, ok := diagramsOverview.Load(componentUUID)
if !ok {
return nil, fmt.Errorf("can not find graph by global uuid:%d", componentID)
return nil, fmt.Errorf("can not find graph by global uuid:%s", componentUUID)
}
paramsMap, ok := value.(map[string]interface{})
componentInfo, ok := value.(*orm.Component)
if !ok {
return nil, errors.New("convert to component map struct failed")
}
return paramsMap, nil
return componentInfo, nil
}
// UpdateComponentMap define func of update circuit diagram data by component id and component info
func UpdateComponentMap(componentID int64, componentInfo map[string]interface{}) bool {
// UpdateComponentMap define func of update circuit diagram data by component uuid and component info
func UpdateComponentMap(componentID int64, componentInfo *orm.Component) bool {
_, result := diagramsOverview.Swap(componentID, componentInfo)
return result
}
// StoreComponentMap define func of store circuit diagram data with component id and component info
func StoreComponentMap(componentID int64, componentInfo map[string]interface{}) {
diagramsOverview.Store(componentID, componentInfo)
// StoreComponentMap define func of store circuit diagram data with component uuid and component info
func StoreComponentMap(componentUUID string, componentInfo *orm.Component) {
diagramsOverview.Store(componentUUID, componentInfo)
return
}
// DeleteComponentMap define func of delete circuit diagram data with component id
func DeleteComponentMap(componentID int64) {
diagramsOverview.Delete(componentID)
// DeleteComponentMap define func of delete circuit diagram data with component uuid
func DeleteComponentMap(componentUUID string) {
diagramsOverview.Delete(componentUUID)
return
}

View File

@ -5,7 +5,7 @@ import (
"fmt"
"sync"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/network"
"github.com/gofrs/uuid"

View File

@ -10,17 +10,15 @@ var GlobalTree *MultiBranchTreeNode
// MultiBranchTreeNode represents a topological structure using an multi branch tree
type MultiBranchTreeNode struct {
ID uuid.UUID // 节点唯一标识
NodeComponentType int // 节点组件类型
Parent *MultiBranchTreeNode // 指向父节点的指针
Children []*MultiBranchTreeNode // 指向所有子节点的指针切片
ID uuid.UUID // 节点唯一标识
Parent *MultiBranchTreeNode // 指向父节点的指针
Children []*MultiBranchTreeNode // 指向所有子节点的指针切片
}
func NewMultiBranchTree(id uuid.UUID, componentType int) *MultiBranchTreeNode {
func NewMultiBranchTree(id uuid.UUID) *MultiBranchTreeNode {
return &MultiBranchTreeNode{
ID: id,
NodeComponentType: componentType,
Children: make([]*MultiBranchTreeNode, 0),
ID: id,
Children: make([]*MultiBranchTreeNode, 0),
}
}
@ -58,7 +56,7 @@ func (n *MultiBranchTreeNode) PrintTree(level int) {
fmt.Print(" ")
}
fmt.Printf("- ComponentType:%d,(ID: %s)\n", n.NodeComponentType, n.ID)
fmt.Printf("-ID: %s\n", n.ID)
for _, child := range n.Children {
child.PrintTree(level + 1)

View File

@ -30,8 +30,8 @@ func initClient(rCfg config.RedisConfig) *redis.Client {
return client
}
// InitClientInstance define func of return instance of redis client
func InitClientInstance(rCfg config.RedisConfig) *redis.Client {
// InitRedisClientInstance define func of return instance of redis client
func InitRedisClientInstance(rCfg config.RedisConfig) *redis.Client {
once.Do(func() {
_globalStorageClient = initClient(rCfg)
})

View File

@ -20,10 +20,11 @@ type RedisSet struct {
}
// NewRedisSet define func of new redis set instance
func NewRedisSet(ctx context.Context, hashKey string, token string, lockLeaseTime uint64, needRefresh bool) *RedisSet {
func NewRedisSet(ctx context.Context, setKey string, lockLeaseTime uint64, needRefresh bool) *RedisSet {
token := ctx.Value("client_token").(string)
return &RedisSet{
ctx: ctx,
rwLocker: locker.InitRWLocker(hashKey, token, lockLeaseTime, needRefresh),
rwLocker: locker.InitRWLocker(setKey, token, lockLeaseTime, needRefresh),
storageClient: GetRedisClientInstance(),
logger: logger.GetLoggerInstance(),
}

114
diagram/redis_string.go Normal file
View File

@ -0,0 +1,114 @@
package diagram
import (
"context"
locker "modelRT/distributedlock"
"modelRT/logger"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
// RedisString defines the encapsulation struct of redis string type
type RedisString struct {
ctx context.Context
rwLocker *locker.RedissionRWLocker
storageClient *redis.Client
logger *zap.Logger
}
// NewRedisString define func of new redis string instance
func NewRedisString(ctx context.Context, stringKey string, token string, lockLeaseTime uint64, needRefresh bool) *RedisString {
return &RedisString{
ctx: ctx,
rwLocker: locker.InitRWLocker(stringKey, token, lockLeaseTime, needRefresh),
storageClient: GetRedisClientInstance(),
logger: logger.GetLoggerInstance(),
}
}
// Get define func of get the value of key
func (rs *RedisString) Get(stringKey string) (string, error) {
err := rs.rwLocker.RLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock rLock by stringKey failed", "string_key", stringKey, "error", err)
return "", err
}
defer rs.rwLocker.UnRLock(rs.ctx)
value, err := rs.storageClient.Get(rs.ctx, stringKey).Result()
if err != nil {
logger.Error(rs.ctx, "get string value by key failed", "string_key", stringKey, "error", err)
return "", err
}
return value, nil
}
// Set define func of set the value of key
func (rs *RedisString) Set(stringKey string, value interface{}) error {
err := rs.rwLocker.WLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock wLock by stringKey failed", "string_key", stringKey, "error", err)
return err
}
defer rs.rwLocker.UnWLock(rs.ctx)
err = rs.storageClient.Set(rs.ctx, stringKey, value, redis.KeepTTL).Err()
if err != nil {
logger.Error(rs.ctx, "get string value by key failed", "string_key", stringKey, "error", err)
return err
}
return nil
}
// Incr define func of increments the number stored at key by one
func (rs *RedisString) Incr(stringKey string) error {
err := rs.rwLocker.WLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock wLock by stringKey failed", "string_key", stringKey, "error", err)
return err
}
defer rs.rwLocker.UnWLock(rs.ctx)
err = rs.storageClient.Incr(rs.ctx, stringKey).Err()
if err != nil {
logger.Error(rs.ctx, "incr the number stored at key by one failed", "string_key", stringKey, "error", err)
return err
}
return nil
}
// IncrBy define func of increments the number stored at key by increment
func (rs *RedisString) IncrBy(stringKey string, value int64) error {
err := rs.rwLocker.WLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock wLock by stringKey failed", "string_key", stringKey, "error", err)
return err
}
defer rs.rwLocker.UnWLock(rs.ctx)
err = rs.storageClient.IncrBy(rs.ctx, stringKey, value).Err()
if err != nil {
logger.Error(rs.ctx, "incr the number stored at key by increment", "string_key", stringKey, "error", err)
return err
}
return nil
}
// GETDEL define func of get the value of key and delete the key
func (rs *RedisString) GETDEL(stringKey string) error {
err := rs.rwLocker.WLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock wLock by stringKey failed", "string_key", stringKey, "error", err)
return err
}
defer rs.rwLocker.UnWLock(rs.ctx)
err = rs.storageClient.GetDel(rs.ctx, stringKey).Err()
if err != nil {
logger.Error(rs.ctx, "del the key failed", "string_key", stringKey, "error", err)
return err
}
return nil
}

123
diagram/redis_zset.go Normal file
View File

@ -0,0 +1,123 @@
// Package diagram provide diagram data structure and operation
package diagram
import (
"context"
"iter"
"maps"
locker "modelRT/distributedlock"
"modelRT/logger"
"github.com/redis/go-redis/v9"
)
// RedisZSet defines the encapsulation struct of redis zset type
type RedisZSet struct {
ctx context.Context
rwLocker *locker.RedissionRWLocker
storageClient *redis.Client
}
// NewRedisZSet define func of new redis zset instance
func NewRedisZSet(ctx context.Context, key string, token string, lockLeaseTime uint64, needRefresh bool) *RedisZSet {
return &RedisZSet{
ctx: ctx,
rwLocker: locker.InitRWLocker(key, token, lockLeaseTime, needRefresh),
storageClient: GetRedisClientInstance(),
}
}
// ZADD define func of add redis zset by members
func (rs *RedisZSet) ZADD(setKey string, score float64, member interface{}) error {
err := rs.rwLocker.WLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock wLock by setKey failed", "set_key", setKey, "error", err)
return err
}
defer rs.rwLocker.UnWLock(rs.ctx)
err = rs.storageClient.ZAdd(rs.ctx, setKey, redis.Z{Score: score, Member: member}).Err()
if err != nil {
logger.Error(rs.ctx, "add set by score and memebers failed", "set_key", setKey, "members", member, "error", err)
return err
}
return nil
}
// ZRANGE define func of returns the specified range of elements in the sorted set stored by key
func (rs *RedisZSet) ZRANGE(setKey string, start, stop int64) ([]string, error) {
var results []string
err := rs.rwLocker.RLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "lock RLock by setKey failed", "set_key", setKey, "error", err)
return nil, err
}
defer func() {
err = rs.rwLocker.UnRLock(rs.ctx)
if err != nil {
logger.Error(rs.ctx, "unlock RLock by setKey failed", "set_key", setKey, "error", err)
}
}()
results, err = rs.storageClient.ZRange(rs.ctx, setKey, start, stop).Result()
if err != nil {
logger.Error(rs.ctx, "range set by key failed", "set_key", setKey, "start", start, "stop", stop, "error", err)
return nil, err
}
return results, nil
}
type Comparer[T any] interface {
Compare(T) int
}
type ComparableComparer[T any] interface {
Compare(T) int
comparable // 直接嵌入 comparable 约束
}
type methodNode[E Comparer[E]] struct {
value E
left *methodNode[E]
right *methodNode[E]
}
type MethodTree[E Comparer[E]] struct {
root *methodNode[E]
}
type OrderedSet[E interface {
comparable
Comparer[E]
}] struct {
tree MethodTree[E]
elements map[E]bool
}
type ComparableOrderedSet[E ComparableComparer[E]] struct {
tree MethodTree[E]
elements map[E]bool
}
type Set[E any] interface {
Insert(E)
Delete(E)
Has(E) bool
All() iter.Seq[E]
}
func InsertAll[E any](set Set[E], seq iter.Seq[E]) {
for v := range seq {
set.Insert(v)
}
}
type HashSet[E comparable] map[E]bool
func (s HashSet[E]) Insert(v E) { s[v] = true }
func (s HashSet[E]) Delete(v E) { delete(s, v) }
func (s HashSet[E]) Has(v E) bool { return s[v] }
func (s HashSet[E]) All() iter.Seq[E] { return maps.Keys(s) }

View File

@ -9,12 +9,72 @@ const docTemplate = `{
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"contact": {
"name": "douxu",
"url": "http://www.swagger.io/support",
"email": "douxu@clea.com.cn"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/measurement/recommend": {
"get": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Measurement Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"description": "查询输入参数,例如 'trans' 或 'transformfeeder1_220.'",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.MeasurementRecommendRequest"
}
}
],
"responses": {
"200": {
"description": "返回推荐列表成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.MeasurementRecommendPayload"
}
}
}
]
}
},
"400": {
"description": "返回推荐列表失败",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/model/diagram_load/{page_id}": {
"get": {
"description": "load circuit diagram info by page id",
@ -55,56 +115,68 @@ const docTemplate = `{
}
},
"definitions": {
"network.FailResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"type": "integer",
"example": 400
}
}
},
"network.FailureResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.FailResponseHeader"
"code": {
"type": "integer",
"example": 500
},
"msg": {
"type": "string",
"example": "failed to get recommend data from redis"
},
"payload": {
"type": "object",
"additionalProperties": true
"type": "object"
}
}
},
"network.MeasurementRecommendPayload": {
"type": "object",
"properties": {
"input": {
"type": "string",
"example": "transformfeeder1_220."
},
"offset": {
"type": "integer",
"example": 21
},
"recommended_list": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"[\"I_A_rms\"",
" \"I_B_rms\"",
"\"I_C_rms\"]"
]
}
}
},
"network.MeasurementRecommendRequest": {
"type": "object",
"properties": {
"input": {
"type": "string",
"example": "trans"
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.SuccessResponseHeader"
},
"payload": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"key": "value"
}
}
}
},
"network.SuccessResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"code": {
"type": "integer",
"example": 200
},
"msg": {
"type": "string",
"example": "success"
},
"payload": {
"type": "object"
}
}
}
@ -113,12 +185,12 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Version: "1.0",
Host: "localhost:8080",
BasePath: "/api/v1",
Schemes: []string{},
Title: "",
Description: "",
Title: "ModelRT 实时模型服务 API 文档",
Description: "实时数据计算和模型运行服务的 API 服务",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",

View File

@ -1,9 +1,74 @@
{
"swagger": "2.0",
"info": {
"contact": {}
"description": "实时数据计算和模型运行服务的 API 服务",
"title": "ModelRT 实时模型服务 API 文档",
"contact": {
"name": "douxu",
"url": "http://www.swagger.io/support",
"email": "douxu@clea.com.cn"
},
"license": {
"name": "Apache 2.0",
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
},
"version": "1.0"
},
"host": "localhost:8080",
"basePath": "/api/v1",
"paths": {
"/measurement/recommend": {
"get": {
"description": "根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Measurement Recommend"
],
"summary": "测量点推荐(搜索框自动补全)",
"parameters": [
{
"description": "查询输入参数,例如 'trans' 或 'transformfeeder1_220.'",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/network.MeasurementRecommendRequest"
}
}
],
"responses": {
"200": {
"description": "返回推荐列表成功",
"schema": {
"allOf": [
{
"$ref": "#/definitions/network.SuccessResponse"
},
{
"type": "object",
"properties": {
"payload": {
"$ref": "#/definitions/network.MeasurementRecommendPayload"
}
}
}
]
}
},
"400": {
"description": "返回推荐列表失败",
"schema": {
"$ref": "#/definitions/network.FailureResponse"
}
}
}
}
},
"/model/diagram_load/{page_id}": {
"get": {
"description": "load circuit diagram info by page id",
@ -44,56 +109,68 @@
}
},
"definitions": {
"network.FailResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"type": "integer",
"example": 400
}
}
},
"network.FailureResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.FailResponseHeader"
"code": {
"type": "integer",
"example": 500
},
"msg": {
"type": "string",
"example": "failed to get recommend data from redis"
},
"payload": {
"type": "object",
"additionalProperties": true
"type": "object"
}
}
},
"network.MeasurementRecommendPayload": {
"type": "object",
"properties": {
"input": {
"type": "string",
"example": "transformfeeder1_220."
},
"offset": {
"type": "integer",
"example": 21
},
"recommended_list": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"[\"I_A_rms\"",
" \"I_B_rms\"",
"\"I_C_rms\"]"
]
}
}
},
"network.MeasurementRecommendRequest": {
"type": "object",
"properties": {
"input": {
"type": "string",
"example": "trans"
}
}
},
"network.SuccessResponse": {
"type": "object",
"properties": {
"header": {
"$ref": "#/definitions/network.SuccessResponseHeader"
},
"payload": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"example": {
"key": "value"
}
}
}
},
"network.SuccessResponseHeader": {
"type": "object",
"properties": {
"err_msg": {
"type": "string"
},
"status": {
"code": {
"type": "integer",
"example": 200
},
"msg": {
"type": "string",
"example": "success"
},
"payload": {
"type": "object"
}
}
}

View File

@ -1,42 +1,94 @@
basePath: /api/v1
definitions:
network.FailResponseHeader:
properties:
err_msg:
type: string
status:
example: 400
type: integer
type: object
network.FailureResponse:
properties:
header:
$ref: '#/definitions/network.FailResponseHeader'
code:
example: 500
type: integer
msg:
example: failed to get recommend data from redis
type: string
payload:
additionalProperties: true
type: object
type: object
network.MeasurementRecommendPayload:
properties:
input:
example: transformfeeder1_220.
type: string
offset:
example: 21
type: integer
recommended_list:
example:
- '["I_A_rms"'
- ' "I_B_rms"'
- '"I_C_rms"]'
items:
type: string
type: array
type: object
network.MeasurementRecommendRequest:
properties:
input:
example: trans
type: string
type: object
network.SuccessResponse:
properties:
header:
$ref: '#/definitions/network.SuccessResponseHeader'
payload:
additionalProperties:
type: string
example:
key: value
type: object
type: object
network.SuccessResponseHeader:
properties:
err_msg:
type: string
status:
code:
example: 200
type: integer
msg:
example: success
type: string
payload:
type: object
type: object
host: localhost:8080
info:
contact: {}
contact:
email: douxu@clea.com.cn
name: douxu
url: http://www.swagger.io/support
description: 实时数据计算和模型运行服务的 API 服务
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
title: ModelRT 实时模型服务 API 文档
version: "1.0"
paths:
/measurement/recommend:
get:
consumes:
- application/json
description: 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
parameters:
- description: 查询输入参数,例如 'trans' 或 'transformfeeder1_220.'
in: body
name: request
required: true
schema:
$ref: '#/definitions/network.MeasurementRecommendRequest'
produces:
- application/json
responses:
"200":
description: 返回推荐列表成功
schema:
allOf:
- $ref: '#/definitions/network.SuccessResponse'
- properties:
payload:
$ref: '#/definitions/network.MeasurementRecommendPayload'
type: object
"400":
description: 返回推荐列表失败
schema:
$ref: '#/definitions/network.FailureResponse'
summary: 测量点推荐(搜索框自动补全)
tags:
- Measurement Recommend
/model/diagram_load/{page_id}:
get:
consumes:

4
go.mod
View File

@ -1,13 +1,15 @@
module modelRT
go 1.22.5
go 1.24.1
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/RediSearch/redisearch-go/v2 v2.1.1
github.com/bitly/go-simplejson v0.5.1
github.com/confluentinc/confluent-kafka-go v1.9.2
github.com/gin-gonic/gin v1.10.0
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gomodule/redigo v1.8.9
github.com/gorilla/websocket v1.5.3
github.com/json-iterator/go v1.1.12
github.com/natefinch/lumberjack v2.0.0+incompatible

4
go.sum
View File

@ -7,6 +7,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7Oputl
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/RediSearch/redisearch-go/v2 v2.1.1 h1:cCn3i40uLsVD8cxwrdrGfhdAgbR5Cld9q11eYyVOwpM=
github.com/RediSearch/redisearch-go/v2 v2.1.1/go.mod h1:Uw93Wi97QqAsw1DwbQrhVd88dBorGTfSuCS42zfh1iA=
github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA=
github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0RsT/ee8YIgGY/xpEQgQ=
github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc=
@ -113,6 +115,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws=
github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=

View File

@ -6,7 +6,7 @@ import (
"strconv"
"modelRT/alert"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/logger"
"modelRT/network"

View File

@ -8,11 +8,9 @@ import (
"time"
"modelRT/common/errcode"
constants "modelRT/constant"
"modelRT/database"
"modelRT/diagram"
"modelRT/logger"
"modelRT/model"
"modelRT/network"
"modelRT/orm"
@ -65,40 +63,7 @@ func ComponentAnchorReplaceHandler(c *gin.Context) {
c.JSON(http.StatusOK, resp)
return
}
cancelCtx, cancel = context.WithTimeout(c, 5*time.Second)
defer cancel()
unmarshalMap := make(map[string]interface{})
tableName := model.SelectModelNameByType(componentInfo.ComponentType)
result = pgClient.WithContext(cancelCtx).Table(tableName).Where("global_uuid = ?", uuid).Find(&unmarshalMap)
if result.Error != nil {
logger.Error(c, "query model detail info failed", "error", result.Error)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: result.Error.Error(),
}
c.JSON(http.StatusOK, resp)
return
}
if unmarshalMap == nil {
err := fmt.Errorf("query model detail info by uuid failed:%w", errcode.ErrQueryRowZero)
logger.Error(c, "query model detail info from table is empty", "table_name", tableName)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
}
c.JSON(http.StatusOK, resp)
return
}
componentType := unmarshalMap["component_type"].(int)
if componentType != constants.DemoType {
logger.Error(c, "can not process real time data of component type not equal DemoType", "component_id", componentInfo.ID)
}
diagram.UpdateAnchorValue(componentInfo.ID, anchorName)
diagram.UpdateAnchorValue(componentInfo.GlobalUUID.String(), anchorName)
resp := network.SuccessResponse{
Code: http.StatusOK,

56
handler/attr_delete.go Normal file
View File

@ -0,0 +1,56 @@
package handler
import (
"net/http"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/network"
"github.com/gin-gonic/gin"
)
// AttrDeleteHandler deletes a data attribute
func AttrDeleteHandler(c *gin.Context) {
var request network.AttrDeleteRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := constants.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal attribute delete request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
if err := rs.GETDEL(request.AttrToken); err != nil {
logger.Error(c, "failed to delete attribute from Redis", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{"attr_token": request.AttrToken},
})
return
}
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
PayLoad: map[string]interface{}{
"attr_token": request.AttrToken,
},
})
}

67
handler/attr_load.go Normal file
View File

@ -0,0 +1,67 @@
package handler
import (
"net/http"
"modelRT/constants"
"modelRT/database"
"modelRT/logger"
"modelRT/network"
"github.com/gin-gonic/gin"
)
// AttrGetHandler retrieves the value of a data attribute
func AttrGetHandler(c *gin.Context) {
var request network.AttrGetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := constants.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal attribute get request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
pgClient := database.GetPostgresDBClient()
tx := pgClient.Begin()
attrModel, err := database.ParseAttrToken(c, tx, request.AttrToken, clientToken)
if err != nil {
tx.Rollback()
logger.Error(c, "failed to parse attribute token", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{"attr_token": request.AttrToken},
})
return
}
tx.Commit()
// The GetAttrValue method is assumed to exist on the AttrModelInterface.
// You need to add this method to your attribute_model.go interface definition.
attrValue := attrModel.GetAttrValue()
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
PayLoad: map[string]interface{}{
"attr_token": request.AttrToken,
"attr_value": attrValue,
},
})
}

58
handler/attr_update.go Normal file
View File

@ -0,0 +1,58 @@
package handler
import (
"net/http"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/network"
"github.com/gin-gonic/gin"
)
// AttrSetHandler sets the value of a data attribute
func AttrSetHandler(c *gin.Context) {
var request network.AttrSetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := constants.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal attribute set request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
// The logic for handling Redis operations directly from the handler
rs := diagram.NewRedisString(c, request.AttrToken, clientToken, 10, true)
if err := rs.Set(request.AttrToken, request.AttrValue); err != nil {
logger.Error(c, "failed to set attribute value in Redis", "attr_token", request.AttrToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{"attr_token": request.AttrToken},
})
return
}
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
PayLoad: map[string]interface{}{
"attr_token": request.AttrToken,
},
})
}

View File

@ -10,7 +10,6 @@ import (
"modelRT/logger"
"modelRT/network"
"github.com/bitly/go-simplejson"
"github.com/gin-gonic/gin"
"github.com/gofrs/uuid"
)
@ -102,8 +101,8 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
graph.AddEdge(topologicCreateInfo.UUIDFrom, topologicCreateInfo.UUIDTo)
}
for index, componentInfo := range request.ComponentInfos {
componentID, err := database.CreateComponentIntoDB(c, tx, componentInfo)
for index, info := range request.ComponentInfos {
componentUUID, err := database.CreateComponentIntoDB(c, tx, info)
if err != nil {
tx.Rollback()
@ -119,64 +118,27 @@ func CircuitDiagramCreateHandler(c *gin.Context) {
c.JSON(http.StatusOK, resp)
return
}
request.ComponentInfos[index].ID = componentID
err = database.CreateModelIntoDB(c, tx, componentID, componentInfo.ComponentType, componentInfo.Params)
if err != nil {
tx.Rollback()
logger.Error(c, "create component model into DB failed", "component_infos", request.ComponentInfos, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"uuid": request.PageID,
"component_infos": request.ComponentInfos,
},
}
c.JSON(http.StatusOK, resp)
return
}
request.ComponentInfos[index].UUID = componentUUID
}
for _, componentInfo := range request.ComponentInfos {
paramsJSON, err := simplejson.NewJson([]byte(componentInfo.Params))
for _, info := range request.ComponentInfos {
// TODO 修复赋值问题
component, err := network.ConvertComponentCreateInfosToComponents(info)
if err != nil {
tx.Rollback()
logger.Error(c, "unmarshal component params info failed", "component_params", componentInfo.Params, "error", err)
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"uuid": componentInfo.UUID,
"component_params": componentInfo.Params,
"uuid": info.UUID,
"component_params": info.Params,
},
}
c.JSON(http.StatusOK, resp)
return
}
componentMap, err := paramsJSON.Map()
if err != nil {
tx.Rollback()
logger.Error(c, "format params json info to map failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"uuid": componentInfo.UUID,
"component_params": componentInfo.Params,
},
}
c.JSON(http.StatusOK, resp)
return
}
diagram.StoreComponentMap(componentInfo.ID, componentMap)
diagram.StoreComponentMap(info.UUID, component)
}
if len(request.FreeVertexs) > 0 {

View File

@ -11,7 +11,6 @@ import (
"modelRT/database"
"modelRT/diagram"
"modelRT/logger"
"modelRT/model"
"modelRT/network"
"modelRT/orm"
@ -192,32 +191,7 @@ func CircuitDiagramDeleteHandler(c *gin.Context) {
c.JSON(http.StatusOK, resp)
return
}
modelStruct := model.SelectModelByType(component.ComponentType)
modelStruct.SetComponentID(component.ID)
result = tx.WithContext(cancelCtx).Where("component_id = ?", component.ID).Delete(modelStruct)
if result.Error != nil || result.RowsAffected == 0 {
tx.Rollback()
err := result.Error
if result.RowsAffected == 0 {
err = fmt.Errorf("%w:please check uuid conditions", errcode.ErrDeleteRowZero)
}
msg := fmt.Sprintf("delete component info from table %s failed", modelStruct.ReturnTableName())
logger.Error(c, msg, "component_global_uuid", componentInfo.UUID, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"uuid": componentInfo.UUID,
},
}
c.JSON(http.StatusOK, resp)
return
}
diagram.DeleteComponentMap(component.ID)
diagram.DeleteComponentMap(component.GlobalUUID.String())
}
if len(request.FreeVertexs) > 0 {

View File

@ -77,7 +77,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
return
}
componentParams, err := diagram.GetComponentMap(component.ID)
componentParams, err := diagram.GetComponentMap(component.GlobalUUID.String())
if err != nil {
logger.Error(c, "get component data from set by uuid failed", "error", err)
@ -111,7 +111,7 @@ func CircuitDiagramLoadHandler(c *gin.Context) {
return
}
rootComponentParam, err := diagram.GetComponentMap(rootComponent.ID)
rootComponentParam, err := diagram.GetComponentMap(rootComponent.GlobalUUID.String())
if err != nil {
logger.Error(c, "get component data from set by uuid failed", "error", err)

View File

@ -9,7 +9,6 @@ import (
"modelRT/logger"
"modelRT/network"
"github.com/bitly/go-simplejson"
"github.com/gin-gonic/gin"
)
@ -103,7 +102,7 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
}
for index, componentInfo := range request.ComponentInfos {
componentID, err := database.UpdateComponentIntoDB(c, tx, componentInfo)
componentUUID, err := database.UpdateComponentIntoDB(c, tx, componentInfo)
if err != nil {
logger.Error(c, "udpate component info into DB failed", "error", err)
@ -118,59 +117,27 @@ func CircuitDiagramUpdateHandler(c *gin.Context) {
c.JSON(http.StatusOK, resp)
return
}
request.ComponentInfos[index].ID = componentID
err = database.UpdateModelIntoDB(c, tx, componentID, componentInfo.ComponentType, componentInfo.Params)
if err != nil {
logger.Error(c, "udpate component model info into DB failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"page_id": request.PageID,
"component_info": request.ComponentInfos,
},
}
c.JSON(http.StatusOK, resp)
return
}
request.ComponentInfos[index].UUID = componentUUID
}
for _, componentInfo := range request.ComponentInfos {
paramsJSON, err := simplejson.NewJson([]byte(componentInfo.Params))
for _, info := range request.ComponentInfos {
// TODO 修复赋值问题
component, err := network.ConvertComponentUpdateInfosToComponents(info)
if err != nil {
logger.Error(c, "unmarshal component info by concurrent map failed", "component_params", componentInfo.Params, "error", err)
logger.Error(c, "convert component params info failed", "component_info", info, "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"uuid": componentInfo.UUID,
"component_params": componentInfo.Params,
"uuid": info.UUID,
"component_params": info.Params,
},
}
c.JSON(http.StatusOK, resp)
return
}
componentMap, err := paramsJSON.Map()
if err != nil {
logger.Error(c, "format params json info to map failed", "error", err)
resp := network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"uuid": componentInfo.UUID,
"component_params": componentInfo.Params,
},
}
c.JSON(http.StatusOK, resp)
return
}
diagram.UpdateComponentMap(componentInfo.ID, componentMap)
diagram.UpdateComponentMap(info.ID, component)
}
if len(request.FreeVertexs) > 0 {

View File

@ -0,0 +1,82 @@
// Package handler provides HTTP handlers for various endpoints.
package handler
import (
"net/http"
"modelRT/constants"
"modelRT/database"
"modelRT/diagram"
"modelRT/logger"
"modelRT/network"
"github.com/gin-gonic/gin"
)
// MeasurementGetHandler define measurement query API
func MeasurementGetHandler(c *gin.Context) {
var request network.MeasurementGetRequest
clientToken := c.GetString("client_token")
if clientToken == "" {
err := constants.ErrGetClientToken
logger.Error(c, "failed to get client token from context", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal measurement get request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
zset := diagram.NewRedisZSet(c, request.MeasurementToken, clientToken, 0, false)
points, err := zset.ZRANGE(request.MeasurementToken, 0, -1)
if err != nil {
logger.Error(c, "failed to get measurement data from redis", "measurement_token", request.MeasurementToken, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"measurement_id": request.MeasurementID,
"measurement_token": request.MeasurementToken,
},
})
return
}
pgClient := database.GetPostgresDBClient()
measurementInfo, err := database.QueryMeasurementByID(c, pgClient, request.MeasurementID)
if err != nil {
logger.Error(c, "failed to query measurement by id", "measurement_id", request.MeasurementID, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
PayLoad: map[string]interface{}{
"measurement_id": request.MeasurementID,
"measurement_token": request.MeasurementToken,
"measurement_value": points,
},
})
return
}
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
PayLoad: map[string]interface{}{
"measurement_id": request.MeasurementID,
"measurement_token": request.MeasurementToken,
"measurement_info": measurementInfo,
"measurement_value": points,
},
})
}

View File

@ -0,0 +1,116 @@
// Package handler provides HTTP handlers for various endpoints.
package handler
import (
"net/http"
"modelRT/logger"
"modelRT/model"
"modelRT/network"
"github.com/gin-gonic/gin"
)
// MeasurementRecommendHandler define measurement recommend API
// @Summary 测量点推荐(搜索框自动补全)
// @Description 根据用户输入的字符串,从 Redis 中查询可能的测量点或结构路径,并提供推荐列表。
// @Tags Measurement Recommend
// @Accept json
// @Produce json
// @Param request body network.MeasurementRecommendRequest true "查询输入参数,例如 'trans' 或 'transformfeeder1_220.'"
// @Success 200 {object} network.SuccessResponse{payload=network.MeasurementRecommendPayload} "返回推荐列表成功"
//
// @Example 200 {
// "code": 200,
// "msg": "success",
// "payload": {
// "input": "transformfeeder1_220.",
// "offset": 21,
// "recommended_list": [
// "I_A_rms",
// "I_B_rms",
// "I_C_rms",
// ]
// }
// }
//
// @Failure 400 {object} network.FailureResponse "返回推荐列表失败"
// @Example 400 {
// "code": 400,
// "msg": "failed to get recommend data from redis",
// }
// @Router /measurement/recommend [get]
func MeasurementRecommendHandler(c *gin.Context) {
var request network.MeasurementRecommendRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error(c, "failed to unmarshal measurement recommend request", "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusBadRequest,
Msg: err.Error(),
})
return
}
recommends, isFuzzy, err := model.RedisSearchRecommend(c, request.Input)
if err != nil {
logger.Error(c, "failed to get recommend data from redis", "input", request.Input, "error", err)
c.JSON(http.StatusOK, network.FailureResponse{
Code: http.StatusInternalServerError,
Msg: err.Error(),
PayLoad: map[string]any{
"input": request.Input,
},
})
return
}
var finalOffset int
if isFuzzy {
var maxOffset int
for index, recommend := range recommends {
offset := model.GetLongestCommonPrefixLength(request.Input, recommend)
if index == 0 || offset > maxOffset {
maxOffset = offset
}
}
finalOffset = maxOffset
} else {
var minOffset int
for index, recommend := range recommends {
offset := model.GetLongestCommonPrefixLength(request.Input, recommend)
if index == 0 || offset < minOffset {
minOffset = offset
}
}
finalOffset = minOffset
}
resultRecommends := make([]string, 0, len(recommends))
seen := make(map[string]struct{})
for _, recommend := range recommends {
recommendTerm := recommend[finalOffset:]
if len(recommendTerm) != 0 {
if _, exists := seen[recommendTerm]; !exists {
seen[recommendTerm] = struct{}{}
resultRecommends = append(resultRecommends, recommendTerm)
}
}
}
c.JSON(http.StatusOK, network.SuccessResponse{
Code: http.StatusOK,
Msg: "success",
// PayLoad: map[string]any{
// "input": request.Input,
// "offset": finalOffset,
// "recommended_list": resultRecommends,
// },
PayLoad: &network.MeasurementRecommendPayload{
Input: request.Input,
Offset: finalOffset,
RecommendedList: resultRecommends,
},
})
}

View File

@ -6,7 +6,7 @@ import (
"strconv"
"modelRT/alert"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/logger"
"modelRT/network"

View File

@ -65,8 +65,8 @@ func RealTimeDataReceivehandler(c *gin.Context) {
realtimedata.RealTimeDataChan <- request
payload := map[string]interface{}{
"component_id": request.PayLoad.ComponentID,
"point": request.PayLoad.Point,
"component_uuid": request.PayLoad.ComponentUUID,
"point": request.PayLoad.Point,
}
respByte := processResponse(0, "success", payload)
if len(respByte) == 0 {

View File

@ -6,6 +6,8 @@ import (
"path"
"runtime"
"modelRT/constants"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
@ -46,7 +48,7 @@ func makeLogFields(ctx context.Context, kv ...any) []zap.Field {
kv = append(kv, "unknown")
}
kv = append(kv, "traceID", ctx.Value("traceID"), "spanID", ctx.Value("spanID"), "pspanID", ctx.Value("pspanID"))
kv = append(kv, "traceID", ctx.Value(constants.HeaderTraceID), "spanID", ctx.Value(constants.HeaderSpanID), "parentSpanID", ctx.Value(constants.HeaderParentSpanID))
funcName, file, line := getLoggerCallerInfo()
kv = append(kv, "func", funcName, "file", file, "line", line)

View File

@ -6,7 +6,7 @@ import (
"sync"
"modelRT/config"
constants "modelRT/constant"
"modelRT/constants"
"github.com/natefinch/lumberjack"
"go.uber.org/zap"

127
main.go
View File

@ -7,6 +7,7 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
@ -19,8 +20,10 @@ import (
"modelRT/handler"
"modelRT/logger"
"modelRT/middleware"
"modelRT/model"
"modelRT/pool"
"modelRT/router"
"modelRT/util"
realtimedata "modelRT/real-time-data"
@ -38,7 +41,7 @@ func init() {
}
var (
modelRTConfigDir = flag.String("modelRT_config_dir", "./config", "config file dir of model runtime service")
modelRTConfigDir = flag.String("modelRT_config_dir", "./configs", "config file dir of model runtime service")
modelRTConfigName = flag.String("modelRT_config_name", "config", "config file name of model runtime service")
modelRTConfigType = flag.String("modelRT_config_type", "yaml", "config file type of model runtime service")
)
@ -50,6 +53,22 @@ var (
)
// TODO 使用 wire 依赖注入管理 DVIE 面板注册的 panel
// @title ModelRT 实时模型服务 API 文档
// @version 1.0
// @description 实时数据计算和模型运行服务的 API 服务
// TODO termsOfService服务条款待后续优化
// // @termsOfService http://swagger.io/terms/
//
// @contact.name douxu
// TODO 修改支持的文档地址
// @contact.url http://www.swagger.io/support
// @contact.email douxu@clea.com.cn
//
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
//
// @host localhost:8080
// @BasePath /api/v1
func main() {
flag.Parse()
ctx := context.TODO()
@ -57,7 +76,36 @@ func main() {
// init logger
logger.InitLoggerInstance(modelRTConfig.LoggerConfig)
configPath := filepath.Join(*modelRTConfigDir, *modelRTConfigName+"."+*modelRTConfigType)
if _, err := os.Stat(configPath); os.IsNotExist(err) {
logger.Info(ctx, "configuration file not found,checking for example file")
exampleConfigPath := filepath.Join(*modelRTConfigDir, *modelRTConfigName+".example."+*modelRTConfigType)
if _, err := os.Stat(exampleConfigPath); err == nil {
if err := util.CopyFile(exampleConfigPath, configPath); err != nil {
logger.Error(ctx, "failed to copy example config file", "error", err)
panic(err)
}
logger.Info(ctx, "successfully copied example config to actual config file")
} else {
logger.Error(ctx, "No config file and no config example file found.")
}
}
modelRTConfig = config.ReadAndInitConfig(*modelRTConfigDir, *modelRTConfigName, *modelRTConfigType)
hostName, err := os.Hostname()
if err != nil {
logger.Error(ctx, "get host name failed", "error", err)
panic(err)
}
serviceToken, err := util.GenerateClientToken(hostName, modelRTConfig.ServiceConfig.ServiceName, modelRTConfig.ServiceConfig.SecretKey)
if err != nil {
logger.Error(ctx, "generate client token failed", "error", err)
panic(err)
}
// init postgresDBClient
postgresDBClient = database.InitPostgresDBInstance(ctx, modelRTConfig.PostgresDBURI)
@ -80,7 +128,11 @@ func main() {
}
defer parsePool.Release()
storageClient := diagram.InitClientInstance(modelRTConfig.StorageRedisConfig)
searchPool, err := util.NewRedigoPool(modelRTConfig.StorageRedisConfig)
defer searchPool.Close()
model.InitAutocompleterWithPool(searchPool)
storageClient := diagram.InitRedisClientInstance(modelRTConfig.StorageRedisConfig)
defer storageClient.Close()
lockerClient := locker.InitClientInstance(modelRTConfig.LockerRedisConfig)
@ -102,14 +154,14 @@ func main() {
postgresDBClient.Transaction(func(tx *gorm.DB) error {
// load circuit diagram from postgres
componentTypeMap, err := database.QueryCircuitDiagramComponentFromDB(cancelCtx, tx, parsePool)
if err != nil {
logger.Error(ctx, "load circuit diagrams from postgres failed", "error", err)
panic(err)
}
// componentTypeMap, err := database.QueryCircuitDiagramComponentFromDB(cancelCtx, tx, parsePool)
// if err != nil {
// logger.Error(ctx, "load circuit diagrams from postgres failed", "error", err)
// panic(err)
// }
// TODO 暂时屏蔽完成 swagger 启动测试
tree, err := database.QueryTopologicFromDB(ctx, tx, componentTypeMap)
tree, err := database.QueryTopologicFromDB(ctx, tx)
if err != nil {
logger.Error(ctx, "load topologic info from postgres failed", "error", err)
panic(err)
@ -122,34 +174,10 @@ func main() {
// TODO 暂时屏蔽完成 swagger 启动测试
// go realtimedata.RealTimeDataComputer(ctx, nil, []string{}, "")
// use release mode in productio
// gin.SetMode(gin.ReleaseMode)
engine := gin.New()
router.RegisterRoutes(engine)
server := http.Server{
Addr: ":8080",
Handler: engine,
}
// creating a System Signal Receiver
done := make(chan os.Signal, 10)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-done
if err := server.Shutdown(context.Background()); err != nil {
logger.Error(ctx, "ShutdownServerError", "err", err)
}
}()
logger.Info(ctx, "Starting ModelRT server...")
err = server.ListenAndServe()
if err != nil {
if err == http.ErrServerClosed {
// the service receives the shutdown signal normally and then closes
logger.Info(ctx, "Server closed under request")
} else {
// abnormal shutdown of service
logger.Error(ctx, "Server closed unexpected", "err", err)
}
}
router.RegisterRoutes(engine, serviceToken)
// real time data api
engine.GET("/ws/rtdatas", handler.RealTimeDataReceivehandler)
@ -173,7 +201,6 @@ func main() {
dashboard.POST("/delete", nil)
}
// engine.Group()
// Swagger UI
engine.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
@ -187,10 +214,30 @@ func main() {
// }
// }
// start route with 8080 port
engine.Run(":8080")
server := http.Server{
Addr: ":8080",
Handler: engine,
}
// Redis hashmap 母线模型、异步电动机模型
// creating a System Signal Receiver
done := make(chan os.Signal, 10)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-done
if err := server.Shutdown(context.Background()); err != nil {
logger.Error(ctx, "ShutdownServerError", "err", err)
}
}()
// kv key name value busx
logger.Info(ctx, "starting ModelRT server")
err = server.ListenAndServe()
if err != nil {
if err == http.ErrServerClosed {
// the service receives the shutdown signal normally and then closes
logger.Info(ctx, "Server closed under request")
} else {
// abnormal shutdown of service
logger.Error(ctx, "Server closed unexpected", "err", err)
}
}
}

11
middleware/token.go Normal file
View File

@ -0,0 +1,11 @@
package middleware
import "github.com/gin-gonic/gin"
// SetTokenMiddleware define a middleware for set token in context
func SetTokenMiddleware(clientToken string) gin.HandlerFunc {
return func(c *gin.Context) {
c.Set("client_token", clientToken)
c.Next()
}
}

View File

@ -6,6 +6,7 @@ import (
"strings"
"time"
"modelRT/constants"
"modelRT/logger"
"modelRT/util"
@ -15,15 +16,17 @@ import (
// StartTrace define func of set trace info from request header
func StartTrace() gin.HandlerFunc {
return func(c *gin.Context) {
traceID := c.Request.Header.Get("traceid")
pSpanID := c.Request.Header.Get("spanid")
traceID := c.Request.Header.Get(constants.HeaderTraceID)
parentSpanID := c.Request.Header.Get(constants.HeaderSpanID)
spanID := util.GenerateSpanID(c.Request.RemoteAddr)
if traceID == "" { // 如果traceId 为空证明是链路的发端把它设置成此次的spanId发端的spanId是root spanId
traceID = spanID // trace 标识整个请求的链路, span则标识链路中的不同服务
// if traceId is empty, it means it is the origin of the link. Set it to the spanId of this time. The originating spanId is the root spanId.
if traceID == "" {
// traceId identifies the entire request link, and spanId identifies the different services in the link.
traceID = spanID
}
c.Set("traceid", traceID)
c.Set("spanid", spanID)
c.Set("pspanid", pSpanID)
c.Set(constants.HeaderTraceID, traceID)
c.Set(constants.HeaderSpanID, spanID)
c.Set(constants.HeaderParentSpanID, parentSpanID)
c.Next()
}
}
@ -33,7 +36,7 @@ type bodyLogWriter struct {
body *bytes.Buffer
}
// 包装一下 gin.ResponseWriter通过这种方式拦截写响应
// TODO 包装一下 gin.ResponseWriter通过这种方式拦截写响应
// 让gin写响应的时候先写到 bodyLogWriter 再写gin.ResponseWriter
// 这样利用中间件里输出访问日志时就能拿到响应了
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
@ -42,6 +45,8 @@ func (w bodyLogWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
// TODO 用于访问request与reponse的日志记录
// LogAccess define func of log access info
func LogAccess() gin.HandlerFunc {
return func(c *gin.Context) {
// 保存body

95
model/attribute_model.go Normal file
View File

@ -0,0 +1,95 @@
// Package model define model struct of model runtime service
package model
import (
"modelRT/orm"
)
// AttrModelInterface define basic attr model type interface
type AttrModelInterface interface {
GetGridInfo() *orm.Grid
GetZoneInfo() *orm.Zone
GetStationInfo() *orm.Station
GetComponentInfo() *orm.Component
GetAttrValue() interface{} // New method to get the attribute value
IsLocal() bool
}
// LongAttrInfo structure define long attribute key info of component
type LongAttrInfo struct {
AttrGroupName string
AttrKey string
AttrValue interface{}
GridInfo *orm.Grid
ZoneInfo *orm.Zone
StationInfo *orm.Station
ComponentInfo *orm.Component
}
// GetGridInfo define return the grid information in the long attribute
func (l *LongAttrInfo) GetGridInfo() *orm.Grid {
return l.GridInfo
}
// GetZoneInfo define return the zone information in the long attribute
func (l *LongAttrInfo) GetZoneInfo() *orm.Zone {
return l.ZoneInfo
}
// GetStationInfo define return the station information in the long attribute
func (l *LongAttrInfo) GetStationInfo() *orm.Station {
return l.StationInfo
}
// GetComponentInfo define return the component information in the long attribute
func (l *LongAttrInfo) GetComponentInfo() *orm.Component {
return l.ComponentInfo
}
// IsLocal define return the is_local information in the long attribute
func (l *LongAttrInfo) IsLocal() bool {
return true
}
// GetAttrValue define return the attribute value
func (l *LongAttrInfo) GetAttrValue() interface{} {
return l.AttrValue
}
// ShortAttrInfo structure define short attribute key info of component
type ShortAttrInfo struct {
AttrGroupName string
AttrKey string
AttrValue interface{}
ComponentInfo *orm.Component
}
// GetGridInfo define return the grid information in the short attribute
func (s *ShortAttrInfo) GetGridInfo() *orm.Grid {
return nil
}
// GetZoneInfo define return the zone information in the short attribute
func (s *ShortAttrInfo) GetZoneInfo() *orm.Zone {
return nil
}
// GetStationInfo define return the station information in the short attribute
func (s *ShortAttrInfo) GetStationInfo() *orm.Station {
return nil
}
// GetComponentInfo define return the component information in the short attribute
func (s *ShortAttrInfo) GetComponentInfo() *orm.Component {
return s.ComponentInfo
}
// IsLocal define return the is_local information in the short attribute
func (s *ShortAttrInfo) IsLocal() bool {
return false
}
// GetAttrValue define return the attribute value
func (l *ShortAttrInfo) GetAttrValue() interface{} {
return l.AttrValue
}

176
model/measurement_model.go Normal file
View File

@ -0,0 +1,176 @@
// Package model define model struct of model runtime service
package model
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"modelRT/constants"
)
// MeasurementDataSource define measurement data source struct
type MeasurementDataSource struct {
Type int `json:"type"`
IOAddress IOAddress `json:"io_address"`
}
// IOAddress define interface of IO address
type IOAddress interface{}
// CL3611Address define CL3611 protol struct
type CL3611Address struct {
Station string `json:"station"`
Device string `json:"device"`
Channel string `json:"channel"`
}
// Power104Address define electricity 104 protol struct
type Power104Address struct {
Station string `json:"station"`
Packet int `json:"packet"`
Offset int `json:"offset"`
}
// NewCL3611DataSource define func of create CL3611 data source
func NewCL3611DataSource(station, device, channel string) (*MeasurementDataSource, error) {
return &MeasurementDataSource{
Type: constants.DataSourceTypeCL3611,
IOAddress: CL3611Address{
Station: station,
Device: device,
Channel: channel,
},
}, nil
}
// NewPower104DataSource define func of create Power104 data source
func NewPower104DataSource(station string, packet, offset int) (*MeasurementDataSource, error) {
return &MeasurementDataSource{
Type: constants.DataSourceTypePower104,
IOAddress: Power104Address{
Station: station,
Packet: packet,
Offset: offset,
},
}, nil
}
func generateChannelName(prefix string, number int, suffix string) (string, error) {
switch prefix {
case constants.ChannelPrefixTelemetry:
if number > 10 {
return "", constants.ErrExceedsLimitType
}
var builder strings.Builder
numberStr := strconv.Itoa(number)
builder.Grow(len(prefix) + len(numberStr) + len(suffix))
builder.WriteString(prefix)
builder.WriteString(numberStr)
builder.WriteString(suffix)
channelName := builder.String()
return channelName, nil
case constants.ChannelPrefixTelesignal:
var numberStr string
if number < 10 {
numberStr = "0" + strconv.Itoa(number)
}
numberStr = strconv.Itoa(number)
var builder strings.Builder
builder.Grow(len(prefix) + len(numberStr) + len(suffix))
builder.WriteString(prefix)
builder.WriteString(numberStr)
builder.WriteString(suffix)
channelName := builder.String()
return channelName, nil
default:
return "", constants.ErrUnsupportedChannelPrefixType
}
}
// NewTelemetryChannel define func of generate telemetry channel CL3611 data source
func NewTelemetryChannel(station, device, channelNameSuffix string, channelNumber int) (*MeasurementDataSource, error) {
channelName, err := generateChannelName(constants.ChannelPrefixTelemetry, channelNumber, channelNameSuffix)
if err != nil {
return nil, fmt.Errorf("failed to generate channel name: %w", err)
}
return NewCL3611DataSource(station, device, channelName)
}
// NewTelesignalChannel define func of generate telesignal channel CL3611 data source
func NewTelesignalChannel(station, device, channelNameSuffix string, channelNumber int) (*MeasurementDataSource, error) {
channelName, err := generateChannelName(constants.ChannelPrefixTelesignal, channelNumber, channelNameSuffix)
if err != nil {
return nil, fmt.Errorf("failed to generate channel name: %w", err)
}
return NewCL3611DataSource(station, device, channelName)
}
// NewStandardChannel define func of generate standard channel CL3611 data source
func NewStandardChannel(station, device, channelType string) (*MeasurementDataSource, error) {
return NewCL3611DataSource(station, device, channelType)
}
// ParseDataSourceFromJSON define func of parse data source from json string
func ParseDataSourceFromJSON(jsonStr string) (MeasurementDataSource, error) {
var data struct {
Type int `json:"type"`
IOAddress json.RawMessage `json:"io_address"`
}
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return MeasurementDataSource{}, err
}
var source MeasurementDataSource
source.Type = data.Type
switch data.Type {
case constants.DataSourceTypeCL3611:
var addr CL3611Address
if err := json.Unmarshal(data.IOAddress, &addr); err != nil {
return MeasurementDataSource{}, err
}
source.IOAddress = addr
case constants.DataSourceTypePower104:
var addr Power104Address
if err := json.Unmarshal(data.IOAddress, &addr); err != nil {
return MeasurementDataSource{}, err
}
source.IOAddress = addr
default:
// 对于未知类型保持原始JSON数据
source.IOAddress = data.IOAddress
}
return source, nil
}
// ToJSON define func of convert data source to json string
func (m MeasurementDataSource) ToJSON() (string, error) {
bytes, err := json.Marshal(m)
if err != nil {
return "", err
}
return string(bytes), nil
}
// GetIOAddress define func of get IO address with correct type
func (m MeasurementDataSource) GetIOAddress() (IOAddress, error) {
switch m.Type {
case constants.DataSourceTypeCL3611:
if addr, ok := m.IOAddress.(CL3611Address); ok {
return addr, nil
}
return nil, constants.ErrInvalidAddressType
case constants.DataSourceTypePower104:
if addr, ok := m.IOAddress.(Power104Address); ok {
return addr, nil
}
return nil, constants.ErrInvalidAddressType
default:
return nil, constants.ErrUnknownDataType
}
}

View File

@ -1,8 +1,7 @@
// Package model define model struct of model runtime service
package model
import (
constants "modelRT/constant"
"modelRT/constants"
"modelRT/orm"
)

213
model/redis_recommend.go Normal file
View File

@ -0,0 +1,213 @@
// Package model define model struct of model runtime service
package model
import (
"context"
"fmt"
"math"
"strings"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"github.com/RediSearch/redisearch-go/v2/redisearch"
redigo "github.com/gomodule/redigo/redis"
)
var ac *redisearch.Autocompleter
// InitAutocompleterWithPool define func of initialize the Autocompleter with redigo pool
func InitAutocompleterWithPool(pool *redigo.Pool) {
ac = redisearch.NewAutocompleterFromPool(pool, constants.RedisSearchDictName)
}
// RedisSearchRecommend define func of redis search by input string and return recommend results
func RedisSearchRecommend(ctx context.Context, input string) ([]string, bool, error) {
rdb := diagram.GetRedisClientInstance()
if input == "" {
// 返回所有 grid 名
return getAllGridKeys(ctx, constants.RedisAllGridSetKey)
}
inputSlice := strings.Split(input, ".")
inputSliceLen := len(inputSlice)
originInputLen := len(inputSlice)
switch inputSliceLen {
case 1:
// TODO 优化成NewSet的形式
gridExist, err := rdb.SIsMember(ctx, constants.RedisAllGridSetKey, input).Result()
if err != nil {
logger.Error(ctx, "check grid key exist failed ", "grid_key", input, "error", err)
return []string{}, false, err
}
searchInput := input
inputLen := inputSliceLen
for inputLen != 0 && !gridExist {
results, err := ac.SuggestOpts(searchInput, redisearch.SuggestOptions{
Num: math.MaxInt16,
Fuzzy: true,
WithScores: false,
WithPayloads: false,
})
if err != nil {
logger.Error(ctx, "query info by fuzzy failed", "query_key", input, "error", err)
return []string{}, false, err
}
if len(results) == 0 {
// TODO 构建 for 循环返回所有可能的补全
searchInput = searchInput[:len(searchInput)-1]
inputLen = len(searchInput)
continue
}
var recommends []string
for _, result := range results {
termSlice := strings.Split(result.Term, ".")
if len(termSlice) <= originInputLen {
recommends = append(recommends, result.Term)
}
}
// 返回模糊查询结果
return recommends, true, nil
}
// 处理 input 不为空、不含有.并且 input 是一个完整的 grid key 的情况
if strings.HasSuffix(input, ".") == false {
recommend := input + "."
return []string{recommend}, false, nil
}
default:
lastInput := inputSlice[inputSliceLen-1]
// 判断 queryKey 是否是空值空值则返回上一级别下的所有key
if lastInput == "" {
setKey := getCombinedConstantsKeyByLength(inputSlice[inputSliceLen-2], inputSliceLen)
targetSet := diagram.NewRedisSet(ctx, setKey, 10, true)
keys, err := targetSet.SMembers(setKey)
if err != nil {
logger.Error(ctx, "get all recommend key by setKey failed", "set_key", setKey, "error", err)
return []string{}, false, fmt.Errorf("get all recommend key by setKey failed,%w", err)
}
var results []string
for _, key := range keys {
result := input + key
results = append(results, result)
}
return results, false, nil
}
setKey := getCombinedConstantsKeyByLength(inputSlice[inputSliceLen-2], inputSliceLen)
targetSet := diagram.NewRedisSet(ctx, setKey, 10, true)
exist, err := targetSet.SIsMember(setKey, lastInput)
if err != nil {
logger.Error(ctx, "check keys exist failed", "set_key", setKey, "query_key", lastInput, "error", err)
return []string{}, false, fmt.Errorf("check keys failed,%w", err)
}
searchInput := input
inputLen := len(searchInput)
for inputLen != 0 && !exist {
logger.Info(ctx, "use fuzzy query", "input", input)
results, err := ac.SuggestOpts(searchInput, redisearch.SuggestOptions{
Num: math.MaxInt16,
Fuzzy: true,
WithScores: false,
WithPayloads: false,
})
if err != nil {
logger.Error(ctx, "query info by fuzzy failed", "query_key", input, "error", err)
return []string{}, false, err
}
if len(results) == 0 {
// TODO 构建 for 循环返回所有可能的补全
searchInput = input[:inputLen-1]
inputLen = len(searchInput)
continue
}
var terms []string
for _, result := range results {
terms = append(terms, result.Term)
}
// 返回模糊查询结果
return terms, true, nil
}
return []string{input}, false, nil
}
return []string{}, false, nil
}
func getAllGridKeys(ctx context.Context, setKey string) ([]string, bool, error) {
// 从redis set 中获取所有的 grid key
gridSets := diagram.NewRedisSet(ctx, setKey, 10, true)
keys, err := gridSets.SMembers("grid_keys")
if err != nil {
return []string{}, false, fmt.Errorf("get all root keys failed, error: %v", err)
}
return keys, false, nil
}
func getSpecificZoneKeys(ctx context.Context, input string) ([]string, bool, error) {
setKey := fmt.Sprintf(constants.RedisSpecGridZoneSetKey, input)
// TODO 从redis set 中获取指定 grid 下的 zone key
zoneSets := diagram.NewRedisSet(ctx, setKey, 10, true)
keys, err := zoneSets.SMembers(setKey)
if err != nil {
return []string{}, false, fmt.Errorf("get all root keys failed, error: %v", err)
}
var results []string
for _, key := range keys {
result := input + "." + key
results = append(results, result)
}
return results, false, nil
}
func getConstantsKeyByLength(inputLen int) string {
switch inputLen {
case 1:
return constants.RedisAllGridSetKey
case 2:
return constants.RedisAllZoneSetKey
case 3:
return constants.RedisAllStationSetKey
case 4:
return constants.RedisAllComponentSetKey
default:
return constants.RedisAllGridSetKey
}
}
func getCombinedConstantsKeyByLength(key string, inputLen int) string {
switch inputLen {
case 2:
return fmt.Sprintf(constants.RedisSpecGridZoneSetKey, key)
case 3:
return fmt.Sprintf(constants.RedisSpecZoneStationSetKey, key)
case 4:
return fmt.Sprintf(constants.RedisSpecStationComponentSetKey, key)
default:
return constants.RedisAllGridSetKey
}
}
// GetLongestCommonPrefixLength define func of get longest common prefix length between two strings
func GetLongestCommonPrefixLength(input string, recommendResult string) int {
if input == "" {
return 0
}
minLen := min(len(input), len(recommendResult))
for i := range minLen {
if input[i] != recommendResult[i] {
return i
}
}
return minLen
}

18
network/attr_request.go Normal file
View File

@ -0,0 +1,18 @@
// Package network define struct of network operation
package network
// AttrGetRequest defines the request payload for getting an attribute
type AttrGetRequest struct {
AttrToken string `json:"attr_token"`
}
// AttrSetRequest defines the request payload for setting an attribute
type AttrSetRequest struct {
AttrToken string `json:"attr_token"`
AttrValue interface{} `json:"attr_value"`
}
// AttrDeleteRequest defines the request payload for deleting an attribute
type AttrDeleteRequest struct {
AttrToken string `json:"attr_token"`
}

View File

@ -1,7 +1,11 @@
// Package network define struct of network operation
package network
import "github.com/gofrs/uuid"
import (
"modelRT/orm"
"github.com/gofrs/uuid"
)
// TopologicCreateInfo defines circuit diagram topologic create info
type TopologicCreateInfo struct {
@ -21,18 +25,16 @@ type TopologicUUIDCreateInfo struct {
// ComponentCreateInfo defines circuit diagram component create index info
type ComponentCreateInfo struct {
ID int64 `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Context string `json:"context"`
GridID int64 `json:"grid_id"`
ZoneID int64 `json:"zone_id"`
StationID int64 `json:"station_id"`
PageID int64 `json:"page_id"`
Tag string `json:"tag"`
ComponentType int `json:"component_type"`
Params string `json:"params"`
Op int `json:"op"`
UUID string `json:"uuid"`
Name string `json:"name"`
Context string `json:"context"`
GridID int64 `json:"grid_id"`
ZoneID int64 `json:"zone_id"`
StationID int64 `json:"station_id"`
PageID int64 `json:"page_id"`
Tag string `json:"tag"`
Params string `json:"params"`
Op int `json:"op"`
}
// CircuitDiagramCreateRequest defines request params of circuit diagram create api
@ -42,3 +44,33 @@ type CircuitDiagramCreateRequest struct {
TopologicLinks []TopologicCreateInfo `json:"topologics"`
ComponentInfos []ComponentCreateInfo `json:"component_infos"`
}
// ConvertComponentCreateInfosToComponents define convert component create info to component struct
func ConvertComponentCreateInfosToComponents(info ComponentCreateInfo) (*orm.Component, error) {
uuidVal, err := uuid.FromString(info.UUID)
if err != nil {
return nil, err
}
component := &orm.Component{
GlobalUUID: uuidVal,
// NsPath: info.NsPath,
Tag: info.Tag,
Name: info.Name,
// ModelName: info.ModelName,
// Description: info.Description,
// GridID: info.GridID,
// ZoneID: info.ZoneID,
// StationID: info.StationID,
// Type: info.Type,
// InService: info.InService,
// State: info.State,
// Status: info.Status,
// Connection: info.Connection,
// Label: info.Label,
Context: info.Context,
Op: info.Op,
// Ts: info.Ts,
}
return component, nil
}

View File

@ -3,9 +3,11 @@ package network
import (
"fmt"
"time"
"modelRT/common/errcode"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/orm"
"github.com/gofrs/uuid"
)
@ -34,18 +36,16 @@ type TopologicUUIDChangeInfos struct {
// ComponentUpdateInfo defines circuit diagram component params info
type ComponentUpdateInfo struct {
ID int64 `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Context string `json:"context"`
GridID int64 `json:"grid_id"`
ZoneID int64 `json:"zone_id"`
StationID int64 `json:"station_id"`
PageID int64 `json:"page_id"`
ComponentType int `json:"component_type"`
Params string `json:"params"`
Op int `json:"op"`
Tag string `json:"tag"`
ID int64 `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Context string `json:"context"`
GridID int64 `json:"grid_id"`
ZoneID int64 `json:"zone_id"`
StationID int64 `json:"station_id"`
Params string `json:"params"`
Op int `json:"op"`
Tag string `json:"tag"`
}
// CircuitDiagramUpdateRequest defines request params of circuit diagram update api
@ -140,3 +140,24 @@ func ParseUUID(info TopologicChangeInfo) (TopologicUUIDChangeInfos, error) {
UUIDChangeInfo.Comment = info.Comment
return UUIDChangeInfo, nil
}
// ConvertComponentUpdateInfosToComponents define convert component update info to component struct
func ConvertComponentUpdateInfosToComponents(updateInfo ComponentUpdateInfo) (*orm.Component, error) {
uuidVal, err := uuid.FromString(updateInfo.UUID)
if err != nil {
return nil, err
}
component := &orm.Component{
GlobalUUID: uuidVal,
// Name: info.Name,
// Context: info.Context,
// GridID: fmt.Sprintf("%d", info.GridID),
// ZoneID: fmt.Sprintf("%d", info.ZoneID),
// StationID: fmt.Sprintf("%d", info.StationID),
// Op: info.Op,
// Tag: info.Tag,
// 其他字段可根据需要补充
Ts: time.Now(),
}
return component, nil
}

View File

@ -0,0 +1,13 @@
// Package network define struct of network operation
package network
// MeasurementGetRequest defines the request payload for getting an measurement
type MeasurementGetRequest struct {
MeasurementID int64 `json:"measurement_id" example:"1001"`
MeasurementToken string `json:"token" example:"some-token"`
}
// MeasurementRecommendRequest defines the request payload for an measurement recommend
type MeasurementRecommendRequest struct {
Input string `json:"input" example:"trans"`
}

View File

@ -8,9 +8,9 @@ type RealTimeDataReceiveRequest struct {
// RealTimeDataReceivePayload defines request payload of real time data receive api
type RealTimeDataReceivePayload struct {
ComponentID int64 `json:"component_id"`
Point string `json:"point"`
Values []RealTimeDataReceiveParam `json:"values"`
ComponentUUID string `json:"component_uuid"`
Point string `json:"point"`
Values []RealTimeDataReceiveParam `json:"values"`
}
// RealTimeDataReceiveParam defines request param of real time data receive api

View File

@ -0,0 +1,20 @@
// Package network define struct of network operation
package network
import (
"fmt"
"modelRT/orm"
)
// ConvertAnyComponentInfosToComponents define convert any component request info to component struct
func ConvertAnyComponentInfosToComponents(anyInfo interface{}) (*orm.Component, error) {
switch info := anyInfo.(type) {
case ComponentCreateInfo:
return ConvertComponentCreateInfosToComponents(info)
case ComponentUpdateInfo:
return ConvertComponentUpdateInfosToComponents(info)
default:
return nil, fmt.Errorf("unsupported type")
}
}

View File

@ -1,14 +1,24 @@
// Package network define struct of network operation
package network
// FailureResponse define struct of standard failure API response format
type FailureResponse struct {
Code int `json:"code" example:"200"`
Msg string `json:"msg"`
PayLoad map[string]interface{} `json:"payload" swaggertype:"object,string" example:"key:value"`
Code int `json:"code" example:"500"`
Msg string `json:"msg" example:"failed to get recommend data from redis"`
PayLoad any `json:"payload" swaggertype:"object"`
}
// SuccessResponse define struct of standard successful API response format
type SuccessResponse struct {
Code int `json:"code" example:"200"`
Msg string `json:"msg"`
PayLoad map[string]interface{} `json:"payload" swaggertype:"object,string" example:"key:value"`
Code int `json:"code" example:"200"`
Msg string `json:"msg" example:"success"`
PayLoad any `json:"payload" swaggertype:"object"`
}
// MeasurementRecommendPayload define struct of represents the data payload for the successful recommendation response.
type MeasurementRecommendPayload struct {
Input string `json:"input" example:"transformfeeder1_220."`
Offset int `json:"offset" example:"21"`
RecommendedList []string `json:"recommended_list" example:"[\"I_A_rms\", \"I_B_rms\",\"I_C_rms\"]"`
// RecommendedList []string `json:"recommended_list"`
}

View File

@ -0,0 +1,70 @@
// Package orm define database data struct
package orm
import (
"time"
"github.com/gofrs/uuid"
)
// Bay structure define abstracted info set of electrical bay
type Bay struct {
BayUUID uuid.UUID `gorm:"column:BAY_UUID;type:uuid;primaryKey;default:gen_random_uuid()"`
Name string `gorm:"column:NAME;size:64;not null;default:''"`
Type string `gorm:"column:TYPE;size:64;not null;default:''"`
Unom float64 `gorm:"column:UNOM;not null;default:-1"`
Fla float64 `gorm:"column:FLA;not null;default:-1"`
Capacity float64 `gorm:"column:CAPACITY;not null;default:-1"`
Description string `gorm:"column:DESCRIPTION;size:512;not null;default:''"`
InService bool `gorm:"column:IN_SERVICE;not null;default:false"`
State int `gorm:"column:STATE;not null;default:-1"`
Grid string `gorm:"column:GRID;size:64;not null;default:''"`
Zone string `gorm:"column:ZONE;size:64;not null;default:''"`
Station string `gorm:"column:STATION;size:64;not null;default:''"`
Business map[string]interface{} `gorm:"column:BUSINESS;type:jsonb;not null;default:'{}'"`
Context map[string]interface{} `gorm:"column:CONTEXT;type:jsonb;not null;default:'{}'"`
FromUUIDs []uuid.UUID `gorm:"column:FROM_UUIDS;type:jsonb;not null;default:'[]'"`
ToUUIDs []uuid.UUID `gorm:"column:TO_UUIDS;type:jsonb;not null;default:'[]'"`
DevProtect []interface{} `gorm:"column:DEV_PROTECT;type:jsonb;not null;default:'[]'"`
DevFaultRecord []interface{} `gorm:"column:DEV_FAULT_RECORD;type:jsonb;not null;default:'[]'"`
DevStatus []interface{} `gorm:"column:DEV_STATUS;type:jsonb;not null;default:'[]'"`
DevDynSense []interface{} `gorm:"column:DEV_DYN_SENSE;type:jsonb;not null;default:'[]'"`
DevInstruct []interface{} `gorm:"column:DEV_INSTRUCT;type:jsonb;not null;default:'[]'"`
DevEtc []interface{} `gorm:"column:DEV_ETC;type:jsonb;not null;default:'[]'"`
Components []uuid.UUID `gorm:"column:COMPONENTS;type:uuid[];not null;default:'{}'"`
Op int `gorm:"column:OP;not null;default:-1"`
Ts time.Time `gorm:"column:TS;type:timestamptz;not null;default:CURRENT_TIMESTAMP"`
}
// TableName func respresent return table name of Bay
func (Bay) TableName() string {
return "BAY"
}
// CREATE TABLE PUBLIC.BAY (
// BAY_UUID UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
// NAME VARCHAR(64) NOT NULL DEFAULT '',
// TYPE VARCHAR(64) NOT NULL DEFAULT '',
// UNOM DOUBLE PRECISION NOT NULL DEFAULT -1,
// FLA DOUBLE PRECISION NOT NULL DEFAULT -1,
// CAPACITY DOUBLE PRECISION NOT NULL DEFAULT -1,
// DESCRIPTION VARCHAR(512) NOT NULL DEFAULT '',
// IN_SERVICE BOOLEAN NOT NULL DEFAULT FALSE,
// STATE INTEGER NOT NULL DEFAULT -1,
// GRID VARCHAR(64) NOT NULL DEFAULT '',
// ZONE VARCHAR(64) NOT NULL DEFAULT '',
// STATION VARCHAR(64) NOT NULL DEFAULT '',
// BUSINESS JSONB NOT NULL DEFAULT '{}', -- for Server
// CONTEXT JSONB NOT NULL DEFAULT '{}', -- for UI
// FROM_UUIDS JSONB NOT NULL DEFAULT '[]', -- uuids
// TO_UUIDS JSONB NOT NULL DEFAULT '[]', -- uuids
// DEV_PROTECT JSONB NOT NULL DEFAULT '[]', -- devices
// DEV_FAULT_RECORD JSONB NOT NULL DEFAULT '[]', -- devices
// DEV_STATUS JSONB NOT NULL DEFAULT '[]', -- devices
// DEV_DYN_SENSE JSONB NOT NULL DEFAULT '[]', -- devices
// DEV_INSTRUCT JSONB NOT NULL DEFAULT '[]', -- devices
// DEV_ETC JSONB NOT NULL DEFAULT '[]', -- devices
// COMPONENTS UUID[] NOT NULL DEFAULT '{}',
// OP INTEGER NOT NULL DEFAULT -1,
// TS TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
// );

View File

@ -9,21 +9,27 @@ import (
// Component structure define abstracted info set of electrical component
type Component struct {
ID int64 `gorm:"column:id;primaryKey"`
GlobalUUID uuid.UUID `gorm:"column:global_uuid"`
Tag string `gorm:"column:tag"`
GridID string `gorm:"column:grid"`
ZoneID string `gorm:"column:zone"`
StationID string `gorm:"column:station"`
PageID int64 `gorm:"column:page_id"`
ComponentType int `gorm:"column:type"`
Name string `gorm:"column:name"`
Context string `gorm:"column:context"`
Op int `gorm:"column:op"`
Ts time.Time `gorm:"column:ts"`
GlobalUUID uuid.UUID `gorm:"column:GLOBAL_UUID;primaryKey"`
NsPath string `gorm:"column:NSPATH"`
Tag string `gorm:"column:TAG"`
Name string `gorm:"column:NAME"`
ModelName string `gorm:"column:MODEL_NAME"`
Description string `gorm:"column:DESCRIPTION"`
GridID string `gorm:"column:GRID"`
ZoneID string `gorm:"column:ZONE"`
StationID string `gorm:"column:STATION"`
Type int `gorm:"column:TYPE"`
InService bool `gorm:"column:IN_SERVICE"`
State int `gorm:"column:STATE"`
Status int `gorm:"column:STATUS"`
Connection map[string]interface{} `gorm:"column:CONNECTION;type:jsonb;default:'{}'"`
Label map[string]interface{} `gorm:"column:LABEL;type:jsonb;default:'{}'"`
Context string `gorm:"column:CONTEXT"`
Op int `gorm:"column:OP"`
Ts time.Time `gorm:"column:TS"`
}
// TableName func respresent return table name of Component
func (c *Component) TableName() string {
return "component"
return "COMPONENT"
}

View File

@ -0,0 +1,21 @@
// Package orm define database data struct
package orm
import (
"time"
)
// Grid structure define abstracted info set of electrical grid
type Grid struct {
ID int64 `gorm:"column:ID;primaryKey"`
TAGNAME string `gorm:"column:TAGNAME"`
Name string `gorm:"column:NAME"`
Description string `gorm:"column:DESCRIPTION"`
Op int `gorm:"column:OP"`
Ts time.Time `gorm:"column:TS"`
}
// TableName func respresent return table name of Grid
func (g *Grid) TableName() string {
return "GRID"
}

View File

@ -0,0 +1,28 @@
// Package orm define database data struct
package orm
import (
"time"
"github.com/gofrs/uuid"
)
// Measurement structure define abstracted info set of electrical measurement
type Measurement struct {
ID int64 `gorm:"column:ID;primaryKey;autoIncrement"`
Tag string `gorm:"column:TAG;size:64;not null;default:''"`
Name string `gorm:"column:NAME;size:64;not null;default:''"`
Type int16 `gorm:"column:TYPE;not null;default:-1"`
Size int `gorm:"column:SIZE;not null;default:-1"`
DataSource map[string]interface{} `gorm:"column:DATA_SOURCE;type:jsonb;not null;default:'{}'"`
EventPlan map[string]interface{} `gorm:"column:EVENT_PLAN;type:jsonb;not null;default:'{}'"`
BayUUID uuid.UUID `gorm:"column:BAY_UUID;type:uuid;not null"`
ComponentUUID uuid.UUID `gorm:"column:COMPONENT_UUID;type:uuid;not null"`
Op int `gorm:"column:OP;not null;default:-1"`
Ts time.Time `gorm:"column:TS;type:timestamptz;not null;default:CURRENT_TIMESTAMP"`
}
// TableName func respresent return table name of Measurement
func (Measurement) TableName() string {
return "MEASUREMENT"
}

View File

@ -5,18 +5,17 @@ import "time"
// Page structure define circuit diagram page info set
type Page struct {
ID int64 `gorm:"column:id;primaryKey"`
Status int `gorm:"column:status"`
Op int `gorm:"column:op"`
Label string `gorm:"column:label"`
Context string `gorm:"column:context"`
Description string `gorm:"column:description"`
Tag string `gorm:"column:tag"`
Name string `gorm:"column:name"`
Ts time.Time `gorm:"column:ts"`
ID int64 `gorm:"column:ID;primaryKey"`
Tag string `gorm:"column:TAG"`
Name string `gorm:"column:NAME"`
Label map[string]interface{} `gorm:"column:LABEL;type:jsonb;default:'{}'"`
Context map[string]interface{} `gorm:"column:CONTEXT;type:jsonb;default:'{}'"`
Description string `gorm:"column:DESCRIPTION"`
Op int `gorm:"column:OP"`
Ts time.Time `gorm:"column:TS"`
}
// TableName func respresent return table name of Page
func (p *Page) TableName() string {
return "page"
return "PAGE"
}

View File

@ -0,0 +1,23 @@
// Package orm define database data struct
package orm
import (
"time"
)
// Station structure define abstracted info set of electrical Station
type Station struct {
ID int64 `gorm:"column:ID;primaryKey"`
ZoneID int64 `gorm:"column:ZONE_ID"`
TAGNAME string `gorm:"column:TAGNAME"`
Name string `gorm:"column:NAME"`
Description string `gorm:"column:DESCRIPTION"`
IsLocal bool `gorm:"column:IS_LOCAL"`
Op int `gorm:"column:OP"`
Ts time.Time `gorm:"column:TS"`
}
// TableName func respresent return table name of Station
func (s *Station) TableName() string {
return "STATION"
}

View File

@ -0,0 +1,22 @@
// Package orm define database data struct
package orm
import (
"time"
)
// Zone structure define abstracted info set of electrical zone
type Zone struct {
ID int64 `gorm:"column:ID;primaryKey"`
GridID int64 `gorm:"column:GRID_ID"`
TAGNAME string `gorm:"column:TAGNAME"`
Name string `gorm:"column:NAME"`
Description string `gorm:"column:DESCRIPTION"`
Op int `gorm:"column:OP"`
Ts time.Time `gorm:"column:TS"`
}
// TableName func respresent return table name of Zone
func (z *Zone) TableName() string {
return "ZONE"
}

View File

@ -2,6 +2,7 @@ package pool
import (
"context"
"fmt"
"sync"
"modelRT/config"
@ -13,7 +14,10 @@ func init() {
}
}
var globalComponentChanSet *ComponentChanSet
var (
globalComponentChanMap sync.Map
globalComponentChanSet *ComponentChanSet
)
// ComponentChanSet defines component anchor real time data process channel set
type ComponentChanSet struct {
@ -21,36 +25,21 @@ type ComponentChanSet struct {
AnchorChans []chan config.AnchorParamConfig
}
func GetComponentChan(ctx context.Context, componentID int64) chan config.AnchorParamConfig {
globalComponentChanSet.RLock()
componentChan := globalComponentChanSet.AnchorChans[componentID]
if componentChan == nil {
globalComponentChanSet.RUnlock()
globalComponentChanSet.Lock()
defer globalComponentChanSet.Unlock()
return CreateNewComponentChan(ctx, componentID)
func GetAnchorParamChan(ctx context.Context, componentUUID string) (chan config.AnchorParamConfig, error) {
paramChan, ok := globalComponentChanMap.Load(componentUUID)
if !ok {
return CreateNewAnchorParamChan(ctx, componentUUID), nil
}
globalComponentChanSet.RUnlock()
return componentChan
anchorParamChan, ok := paramChan.(chan config.AnchorParamConfig)
if !ok {
return nil, fmt.Errorf("conversion component anchor param chan type failed for componentUUID: %s", componentUUID)
}
return anchorParamChan, nil
}
func CreateNewComponentChan(ctx context.Context, componentID int64) chan config.AnchorParamConfig {
componentChan := globalComponentChanSet.AnchorChans[componentID]
if componentChan == nil {
componentChan = make(chan config.AnchorParamConfig, 100)
globalComponentChanSet.AnchorChans[componentID] = componentChan
readyChan := make(chan struct{})
chanConfig := config.AnchorChanConfig{
Ctx: ctx,
AnchorChan: componentChan,
ReadyChan: readyChan,
}
AnchorRealTimePool.Invoke(chanConfig)
<-readyChan
return componentChan
}
return componentChan
func CreateNewAnchorParamChan(ctx context.Context, componentUUID string) chan config.AnchorParamConfig {
anchorParamChan := make(chan config.AnchorParamConfig, 100)
globalComponentChanMap.Store(componentUUID, anchorParamChan)
return anchorParamChan
}

View File

@ -7,7 +7,7 @@ import (
"modelRT/alert"
"modelRT/config"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
@ -48,13 +48,13 @@ var AnchorFunc = func(poolConfig interface{}) {
firstStart = false
}
componentID := anchorParaConfig.ComponentID
componentUUID := anchorParaConfig.ComponentUUID
anchorRealTimeDatas := anchorParaConfig.AnchorRealTimeData
for _, value := range anchorRealTimeDatas {
anchorName, err := diagram.GetAnchorValue(componentID)
anchorName, err := diagram.GetAnchorValue(componentUUID)
if err != nil {
logger.Error(anchorChanConfig.Ctx, "can not get anchor value from map by uuid", "component_id", componentID, "error", err)
logger.Error(anchorChanConfig.Ctx, "can not get anchor value from map by uuid", "component_uuid", componentUUID, "error", err)
continue
}
@ -70,11 +70,11 @@ var AnchorFunc = func(poolConfig interface{}) {
message := fmt.Sprintf("anchor param %s value out of range, value:%f, upper limit:%f, lower limit:%f", anchorName,
compareValue, upperLimitVal, lowerlimitVal)
event := alert.Event{
ComponentID: componentID,
AnchorName: anchorName,
Level: constants.InfoAlertLevel,
Message: message,
StartTime: time.Now().Unix(),
ComponentUUID: componentUUID,
AnchorName: anchorName,
Level: constants.InfoAlertLevel,
Message: message,
StartTime: time.Now().Unix(),
}
alertManager.AddEvent(event)
}

View File

@ -9,7 +9,6 @@ import (
"modelRT/database"
"modelRT/diagram"
"modelRT/logger"
"modelRT/model"
)
// ParseFunc defines func that parses the model data from postgres
@ -23,43 +22,16 @@ var ParseFunc = func(parseConfig interface{}) {
cancelCtx, cancel := context.WithTimeout(modelParseConfig.Ctx, 5*time.Second)
defer cancel()
pgClient := database.GetPostgresDBClient()
unmarshalMap := make(map[string]interface{})
tableName := model.SelectModelNameByType(modelParseConfig.ComponentInfo.ComponentType)
result := pgClient.WithContext(cancelCtx).Table(tableName).Where("component_id = ?", modelParseConfig.ComponentInfo.ID).Find(&unmarshalMap)
if result.Error != nil {
logger.Error(modelParseConfig.Ctx, "query component detail info failed", "error", result.Error)
return
} else if result.RowsAffected == 0 {
logger.Error(modelParseConfig.Ctx, "query component detail info from table is empty", "table_name", tableName)
component, err := database.QueryComponentByUUID(cancelCtx, pgClient, modelParseConfig.ComponentInfo.GlobalUUID)
if err != nil {
logger.Error(modelParseConfig.Ctx, "query component info failed", "error", err)
return
}
var anchorName string
if anchoringV := unmarshalMap["anchor_v"].(bool); anchoringV {
anchorName = "voltage"
} else {
anchorName = "current"
}
diagram.StoreAnchorValue(modelParseConfig.ComponentInfo.ID, anchorName)
diagram.StoreAnchorValue(modelParseConfig.ComponentInfo.GlobalUUID.String(), "")
GetComponentChan(modelParseConfig.Ctx, modelParseConfig.ComponentInfo.ID)
GetAnchorParamChan(modelParseConfig.Ctx, modelParseConfig.ComponentInfo.GlobalUUID.String())
uuid := modelParseConfig.ComponentInfo.GlobalUUID.String()
unmarshalMap["id"] = modelParseConfig.ComponentInfo.ID
unmarshalMap["uuid"] = uuid
unmarshalMap["uuid"] = modelParseConfig.ComponentInfo.Tag
unmarshalMap["grid_id"] = modelParseConfig.ComponentInfo.GridID
unmarshalMap["zone_id"] = modelParseConfig.ComponentInfo.ZoneID
unmarshalMap["station_id"] = modelParseConfig.ComponentInfo.StationID
unmarshalMap["page_id"] = modelParseConfig.ComponentInfo.PageID
unmarshalMap["component_type"] = modelParseConfig.ComponentInfo.ComponentType
unmarshalMap["name"] = modelParseConfig.ComponentInfo.Name
unmarshalMap["context"] = modelParseConfig.ComponentInfo.Context
unmarshalMap["op"] = modelParseConfig.ComponentInfo.Op
unmarshalMap["ts"] = modelParseConfig.ComponentInfo.Ts
diagram.StoreComponentMap(modelParseConfig.ComponentInfo.ID, unmarshalMap)
diagram.StoreComponentMap(modelParseConfig.ComponentInfo.GlobalUUID.String(), &component)
return
}

View File

@ -0,0 +1,200 @@
// Package realtimedata define real time data operation functions
package realtimedata
import (
"context"
"encoding/json"
"sync"
"time"
"modelRT/constants"
"modelRT/database"
"modelRT/logger"
"modelRT/network"
"github.com/confluentinc/confluent-kafka-go/kafka"
"github.com/gofrs/uuid"
"gorm.io/gorm"
)
// CacheManager define structure for managing cache items
type CacheManager struct {
mutex sync.RWMutex
store sync.Map
pool sync.Pool
dbClient *gorm.DB
kafkaConsumer *kafka.Consumer
ttl time.Duration
}
// NewCacheManager define func to create new cache manager
func NewCacheManager(db *gorm.DB, kf *kafka.Consumer, ttl time.Duration) *CacheManager {
cm := &CacheManager{
dbClient: db,
kafkaConsumer: kf,
ttl: ttl,
}
cm.pool.New = func() any {
item := &CacheItem{}
return item
}
return cm
}
// RealTimeComponentMonitor define func to continuously processing component info and build real time component data monitor from kafka specified topic
func (cm *CacheManager) RealTimeComponentMonitor(ctx context.Context, topic string, duration string) {
// context for graceful shutdown
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
monitorConsumer := cm.kafkaConsumer
// subscribe the monitor topic
err := monitorConsumer.SubscribeTopics([]string{topic}, nil)
if err != nil {
logger.Error(ctx, "subscribe to the monitor topic failed", "topic", topic, "error", err)
return
}
defer monitorConsumer.Close()
timeoutDuration, err := time.ParseDuration(duration)
if err != nil {
logger.Error(ctx, "parse duration failed", "duration", duration, "error", err)
return
}
// continuously read messages from kafka
for {
select {
case <-cancelCtx.Done():
logger.Info(ctx, "context canceled, stopping read loop")
return
default:
msg, err := monitorConsumer.ReadMessage(timeoutDuration)
if err != nil {
if err.(kafka.Error).Code() == kafka.ErrTimedOut {
continue
}
logger.Error(ctx, "consumer read message failed", "error", err)
continue
}
var realTimeData network.RealTimeDataReceiveRequest
if err := json.Unmarshal(msg.Value, &realTimeData); err != nil {
logger.Error(ctx, "unmarshal kafka message failed", "message", string(msg.Value), "error", err)
continue
}
key := realTimeData.PayLoad.ComponentUUID
_, err = cm.StoreIntoCache(ctx, key)
if err != nil {
logger.Error(ctx, "store into cache failed", "key", key, "error", err)
continue
}
_, err = monitorConsumer.CommitMessage(msg)
if err != nil {
logger.Error(ctx, "manual submission information failed", "message", msg, "error", err)
}
}
}
}
// GetCacheItemFromPool define func to get a cache item from the pool
func (cm *CacheManager) GetCacheItemFromPool() *CacheItem {
item := cm.pool.Get().(*CacheItem)
item.Reset()
return item
}
// PutCacheItemToPool define func to put a cache item back to the pool
func (cm *CacheManager) PutCacheItemToPool(item *CacheItem) {
if item != nil {
item.Reset()
cm.pool.Put(item)
}
}
// StoreIntoCache define func to store data into cache, if the key already exists and is not expired, return the existing item
func (cm *CacheManager) StoreIntoCache(ctx context.Context, key string) (*CacheItem, error) {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cachedItemRaw, ok := cm.store.Load(key); ok {
cachedItem := cachedItemRaw.(*CacheItem)
if !cachedItem.IsExpired(cm.ttl) {
cachedItem.lastAccessed = time.Now()
return cachedItem, nil
}
cm.PutCacheItemToPool(cachedItem)
cm.store.Delete(key)
}
uuid, err := uuid.FromString(key)
if err != nil {
logger.Error(ctx, "format key to UUID failed", "key", key, "error", err)
return nil, constants.ErrFormatUUID
}
componentInfo, err := database.QueryComponentByUUID(ctx, cm.dbClient, uuid)
if err != nil {
logger.Error(ctx, "query component from db failed by uuid", "component_uuid", key, "error", err)
return nil, constants.ErrQueryComponentByUUID
}
newItem := cm.GetCacheItemFromPool()
newItem.key = key
newItem.value = []any{componentInfo.Context}
// newItem.calculatorFunc = componentInfo.CalculatorFunc
newItem.lastAccessed = time.Now()
// 在存储前启动goroutine
if newItem.calculatorFunc != nil {
newCtx, newCancel := context.WithCancel(ctx)
newItem.cancelFunc = newCancel
go newItem.calculatorFunc(newCtx, newItem.topic, newItem.value)
}
cm.store.Store(key, newItem)
return newItem, nil
}
// UpdateCacheItem define func to update cache item with new data and trigger new calculation
func (cm *CacheManager) UpdateCacheItem(ctx context.Context, key string, newData any) error {
cm.mutex.Lock()
defer cm.mutex.Unlock()
cache, existed := cm.store.Load(key)
if !existed {
return nil
}
cacheItem, ok := cache.(*CacheItem)
if !ok {
return constants.ErrFormatCache
}
// stop old calculation goroutine
if cacheItem.cancelFunc != nil {
cacheItem.cancelFunc()
}
oldValue := cacheItem.value
cacheItem.value = []any{newData}
cacheItem.lastAccessed = time.Now()
newCtx, newCancel := context.WithCancel(ctx)
cacheItem.cancelFunc = newCancel
swapped := cm.store.CompareAndSwap(key, oldValue, cacheItem)
if !swapped {
cacheValue, _ := cm.store.Load(key)
logger.Error(ctx, "store new value into cache failed,existed concurrent modification risk", "key", key, "old_value", oldValue, "cache_value", cacheValue, "store_value", cacheItem.value)
return constants.ErrConcurrentModify
}
// start new calculation goroutine
if cacheItem.calculatorFunc != nil {
go cacheItem.calculatorFunc(newCtx, cacheItem.topic, cacheItem.value)
}
return nil
}

View File

@ -0,0 +1,35 @@
// Package realtimedata define real time data operation functions
package realtimedata
import (
"context"
"time"
)
// CacheItem define structure for caching real time data with calculation capabilities
type CacheItem struct {
key string
value []any
// TODO 增加实时数据的topic
topic string
cancelFunc context.CancelFunc
calculatorFunc func(ctx context.Context, topic string, params []any)
lastAccessed time.Time
}
// Reset defines func to reset the CacheItem to its zero state
func (ci *CacheItem) Reset() {
ci.key = ""
ci.value = nil
if ci.cancelFunc != nil {
ci.cancelFunc()
}
ci.cancelFunc = nil
ci.calculatorFunc = nil
ci.lastAccessed = time.Time{}
}
// IsExpired defines func to check if the cache item has expired based on a given TTL
func (ci *CacheItem) IsExpired(ttl time.Duration) bool {
return time.Since(ci.lastAccessed) > ttl
}

View File

@ -5,7 +5,7 @@ import (
"context"
"modelRT/config"
constants "modelRT/constant"
"modelRT/constants"
"modelRT/diagram"
"modelRT/logger"
"modelRT/network"
@ -26,17 +26,16 @@ func ReceiveChan(ctx context.Context) {
case <-ctx.Done():
return
case realTimeData := <-RealTimeDataChan:
// TODO 根据 componentID 缓存到来的实时数据
componentID := realTimeData.PayLoad.ComponentID
component, err := diagram.GetComponentMap(componentID)
componentUUID := realTimeData.PayLoad.ComponentUUID
component, err := diagram.GetComponentMap(componentUUID)
if err != nil {
logger.Error(ctx, "query component info from diagram map by componet id failed", "component_id", componentID, "error", err)
logger.Error(ctx, "query component info from diagram map by componet id failed", "component_uuid", componentUUID, "error", err)
continue
}
componentType := component["component_type"].(int)
componentType := component.Type
if componentType != constants.DemoType {
logger.Error(ctx, "can not process real time data of component type not equal DemoType", "component_id", componentID)
logger.Error(ctx, "can not process real time data of component type not equal DemoType", "component_uuid", componentUUID)
continue
}
@ -44,21 +43,8 @@ func ReceiveChan(ctx context.Context) {
var compareValUpperLimit, compareValLowerLimit float64
var anchorRealTimeData []float64
var calculateFunc func(archorValue float64, args ...float64) float64
if anchoringV := component["anchor_v"].(bool); anchoringV {
anchorName = "voltage"
compareValUpperLimit = component["uv_alarm"].(float64)
compareValLowerLimit = component["ov_alarm"].(float64)
} else {
anchorName = "current"
compareValUpperLimit = component["ui_alarm"].(float64)
compareValLowerLimit = component["oi_alarm"].(float64)
}
componentData := map[string]interface{}{
"resistance": component["resistance"].(float64),
}
calculateFunc, params := config.SelectAnchorCalculateFuncAndParams(componentType, anchorName, componentData)
// calculateFunc, params := config.SelectAnchorCalculateFuncAndParams(componentType, anchorName, componentData)
for _, param := range realTimeData.PayLoad.Values {
anchorRealTimeData = append(anchorRealTimeData, param.Value)
@ -66,16 +52,21 @@ func ReceiveChan(ctx context.Context) {
anchorConfig := config.AnchorParamConfig{
AnchorParamBaseConfig: config.AnchorParamBaseConfig{
ComponentID: componentID,
ComponentUUID: componentUUID,
AnchorName: anchorName,
CompareValUpperLimit: compareValUpperLimit,
CompareValLowerLimit: compareValLowerLimit,
AnchorRealTimeData: anchorRealTimeData,
},
CalculateFunc: calculateFunc,
CalculateParams: params,
CalculateParams: []float64{},
}
pool.GetComponentChan(ctx, componentID) <- anchorConfig
anchorChan, err := pool.GetAnchorParamChan(ctx, componentUUID)
if err != nil {
logger.Error(ctx, "get anchor param chan failed", "component_uuid", componentUUID, "error", err)
continue
}
anchorChan <- anchorConfig
default:
}
}

18
router/attr.go Normal file
View File

@ -0,0 +1,18 @@
// Package router provides router config
package router
import (
"modelRT/handler"
"github.com/gin-gonic/gin"
)
// registerAttrRoutes define func of register attr routes
func registerAttrRoutes(rg *gin.RouterGroup) {
g := rg.Group("/attr/")
// TODO add attr middleware
g.GET("load", handler.AttrGetHandler)
g.POST("create", handler.AttrGetHandler)
g.POST("update", handler.AttrSetHandler)
g.POST("delete", handler.AttrDeleteHandler)
}

View File

@ -1,3 +1,4 @@
// Package router provides router config
package router
import (
@ -6,7 +7,7 @@ import (
"github.com/gin-gonic/gin"
)
// RegisterRoutes define func of register diagram routes
// registerDiagramRoutes define func of register diagram routes
func registerDiagramRoutes(rg *gin.RouterGroup) {
g := rg.Group("/diagram/")
// TODO add diagram middleware

17
router/measurement.go Normal file
View File

@ -0,0 +1,17 @@
// Package router provides router config
package router
import (
"modelRT/handler"
"modelRT/middleware"
"github.com/gin-gonic/gin"
)
// registerMeasurementRoutes define func of register measurement routes
func registerMeasurementRoutes(rg *gin.RouterGroup, clientToken string) {
g := rg.Group("/measurement/")
g.Use(middleware.SetTokenMiddleware(clientToken))
g.GET("load", handler.MeasurementGetHandler)
g.GET("recommend", handler.MeasurementRecommendHandler)
}

View File

@ -1,3 +1,4 @@
// Package router provides router config
package router
import (
@ -14,9 +15,12 @@ func init() {
limiter = middleware.NewLimiter(10, 1*time.Minute) // 设置限流器允许每分钟最多请求10次
}
func RegisterRoutes(engine *gin.Engine) {
// RegisterRoutes define func of register all routes
func RegisterRoutes(engine *gin.Engine, clientToken string) {
// use global middlewares
engine.Use(middleware.StartTrace(), limiter.Middleware)
routeGroup := engine.Group("")
registerDiagramRoutes(routeGroup)
registerAttrRoutes(routeGroup)
registerMeasurementRoutes(routeGroup, clientToken)
}

View File

@ -63,7 +63,6 @@ func WriteComponentInShareMemory(key uintptr, componentInfo *orm.Component) erro
obj := (*orm.Component)(unsafe.Pointer(shmAddr + unsafe.Sizeof(structSize)))
fmt.Println(obj)
obj.ComponentType = componentInfo.ComponentType
// id integer NOT NULL DEFAULT nextval('component_id_seq'::regclass),
// global_uuid uuid NOT NULL DEFAULT gen_random_uuid(),

View File

@ -7,7 +7,7 @@ var RecursiveSQL = `WITH RECURSIVE recursive_tree as (
FROM "Topologic"
WHERE uuid_from = ?
UNION ALL
SELECT t.uuid_from,t.uuid_to,t.page_id,t.flag
SELECT t.uuid_from,t.uuid_to,t.flag
FROM "Topologic" t
JOIN recursive_tree rt ON t.uuid_from = rt.uuid_to
)

View File

@ -0,0 +1,178 @@
package handler_test
// TODO 使用 mock测试 handler
// 一、安装 GoMock
// 首先需要安装 GoMock 和 mockgen 工具:
// bash
// 复制
// # 安装 GoMock 包
// go get github.com/golang/mock/gomock
// # 安装 mockgen 工具
// go install github.com/golang/mock/mockgen@latest
// 二、基本使用步骤
// 1. 定义接口
// 假设我们有一个简单的接口:
// go
// 复制
// // greeter.go
// package main
// type Greeter interface {
// Greet(name string) string
// }
// 2. 生成 mock 代码
// 使用 mockgen 为接口生成 mock 实现:
// bash
// 复制
// mockgen -source=greeter.go -destination=mock_greeter.go -package=main
// 这会生成一个 mock_greeter.go 文件,包含 MockGreeter 结构体。
// 3. 编写测试代码
// go
// 复制
// // greeter_test.go
// package main
// import (
// "testing"
// "github.com/golang/mock/gomock"
// )
// func TestGreet(t *testing.T) {
// // 创建控制器
// ctrl := gomock.NewController(t)
// defer ctrl.Finish() // 断言所有期望都被满足
// // 创建 mock 对象
// mockGreeter := NewMockGreeter(ctrl)
// // 设置期望
// mockGreeter.EXPECT().
// Greet("Alice").
// Return("Hello, Alice!").
// Times(1)
// // 使用 mock 对象
// result := mockGreeter.Greet("Alice")
// // 验证结果
// if result != "Hello, Alice!" {
// t.Errorf("Expected 'Hello, Alice!', got '%s'", result)
// }
// }
// 三、高级功能
// 1. 参数匹配
// GoMock 提供了多种参数匹配方式:
// go
// 复制
// // 精确匹配
// mockGreeter.EXPECT().Greet("Alice")
// // 任意值匹配
// mockGreeter.EXPECT().Greet(gomock.Any())
// // 自定义匹配
// mockGreeter.EXPECT().Greet(gomock.AssignableToTypeOf("")).DoAndReturn(
// func(name string) string {
// return "Hello, " + name + "!"
// },
// )
// 2. 调用次数控制
// go
// 复制
// // 精确调用次数
// mockGreeter.EXPECT().Greet("Alice").Times(3)
// // 至少调用一次
// mockGreeter.EXPECT().Greet("Alice").MinTimes(1)
// // 最多调用一次
// mockGreeter.EXPECT().Greet("Alice").MaxTimes(1)
// // 任意次数(0或多次)
// mockGreeter.EXPECT().Greet("Alice").AnyTimes()
// 3. 调用顺序控制
// go
// 复制
// gomock.InOrder(
// mockGreeter.EXPECT().Greet("Alice"),
// mockGreeter.EXPECT().Greet("Bob"),
// )
// 4. 模拟副作用
// go
// 复制
// // 使用 Do
// mockGreeter.EXPECT().Greet("Alice").Do(func(name string) {
// t.Logf("Greet was called with %s", name)
// })
// // 使用 DoAndReturn
// mockGreeter.EXPECT().Greet("Alice").DoAndReturn(
// func(name string) string {
// return "Hi, " + name + "!"
// },
// )
// 四、实际应用示例
// 假设我们有一个服务依赖 Greeter 接口:
// go
// 复制
// // greeter_service.go
// package main
// type GreeterService struct {
// greeter Greeter
// }
// func (s *GreeterService) WelcomePeople(names []string) []string {
// var greetings []string
// for _, name := range names {
// greetings = append(greetings, s.greeter.Greet(name))
// }
// return greetings
// }
// 对应的测试:
// go
// 复制
// // greeter_service_test.go
// package main
// import (
// "testing"
// "github.com/golang/mock/gomock"
// )
// func TestWelcomePeople(t *testing.T) {
// ctrl := gomock.NewController(t)
// defer ctrl.Finish()
// mockGreeter := NewMockGreeter(ctrl)
// service := &GreeterService{greeter: mockGreeter}
// // 设置期望
// mockGreeter.EXPECT().Greet("Alice").Return("Hello, Alice!")
// mockGreeter.EXPECT().Greet("Bob").Return("Hello, Bob!")
// // 测试
// result := service.WelcomePeople([]string{"Alice", "Bob"})
// // 验证
// expected := []string{"Hello, Alice!", "Hello, Bob!"}
// if len(result) != len(expected) {
// t.Fatalf("Expected %d greetings, got %d", len(expected), len(result))
// }
// for i := range expected {
// if result[i] != expected[i] {
// t.Errorf("At index %d: expected %q, got %q", i, expected[i], result[i])
// }
// }
// }

35
util/copy.go Normal file
View File

@ -0,0 +1,35 @@
// Package util provide some utility functions
package util
import (
"fmt"
"io"
"os"
)
// CopyFile define func of copies a file from src to dst.
// If the destination file exists, it will be overwritten.
func CopyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file %s: %w", src, err)
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination file %s: %w", dst, err)
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return fmt.Errorf("failed to copy file contents from %s to %s: %w", src, dst, err)
}
err = destFile.Sync()
if err != nil {
return fmt.Errorf("failed to sync destination file %s: %w", dst, err)
}
return nil
}

View File

@ -1,9 +1,14 @@
// Package util provide some utility functions
package util
import (
"context"
"fmt"
"time"
"modelRT/config"
redigo "github.com/gomodule/redigo/redis"
"github.com/redis/go-redis/v9"
)
@ -34,3 +39,46 @@ func NewRedisClient(addr string, opts ...RedisOption) (*redis.Client, error) {
}
return client, nil
}
// NewRedigoPool define func of initialize the Redigo pool
func NewRedigoPool(rCfg config.RedisConfig) (*redigo.Pool, error) {
pool := &redigo.Pool{
MaxIdle: rCfg.PoolSize / 2,
MaxActive: rCfg.PoolSize,
// TODO optimize IdleTimeout with config parameter
IdleTimeout: 240 * time.Second,
// Dial function to create the connection
Dial: func() (redigo.Conn, error) {
timeout := time.Duration(rCfg.Timeout) * time.Millisecond // 假设 rCfg.Timeout 是毫秒
opts := []redigo.DialOption{
redigo.DialDatabase(rCfg.DB),
redigo.DialPassword(rCfg.Password),
redigo.DialConnectTimeout(timeout),
// redigo.DialReadTimeout(timeout),
// redigo.DialWriteTimeout(timeout),
// TODO add redigo.DialUsername when redis open acl
// redis.DialUsername("username"),
}
c, err := redigo.Dial("tcp", rCfg.Addr, opts...)
if err != nil {
return nil, fmt.Errorf("redigo dial failed: %w", err)
}
return c, nil
},
}
conn := pool.Get()
defer conn.Close()
if conn.Err() != nil {
return nil, fmt.Errorf("failed to get connection from pool: %w", conn.Err())
}
_, err := conn.Do("PING")
if err != nil {
return nil, fmt.Errorf("redis connection test (PING) failed: %w", err)
}
return pool, nil
}

View File

@ -1,3 +1,4 @@
// Package util provide some utility functions
package util
import (

68
util/token.go Normal file
View File

@ -0,0 +1,68 @@
// Package util provide some utility functions
package util
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"os"
"strings"
"time"
)
// GenerateClientToken define func of generate a secure token for client identification
func GenerateClientToken(host string, serviceName string, secretKey string) (string, error) {
finalSecretKey := secretKey
if finalSecretKey == "" {
finalSecretKey = os.Getenv("TOKEN_SECRET_KEY")
}
if finalSecretKey == "" {
return "", fmt.Errorf("TOKEN_SECRET_KEY environment variable not set and no key provided in parameters")
}
uniqueID := fmt.Sprintf("%d", time.Now().UnixNano())
clientInfo := fmt.Sprintf("host=%s;service=%s;id=%s", host, serviceName, uniqueID)
mac := hmac.New(sha256.New, []byte(finalSecretKey))
mac.Write([]byte(clientInfo))
signature := mac.Sum(nil)
token := fmt.Sprintf("%s.%s",
base64.URLEncoding.EncodeToString([]byte(clientInfo)),
base64.URLEncoding.EncodeToString(signature))
return token, nil
}
// verifyClientToken define func of verify the client token
func verifyClientToken(token string, secretKey string) (bool, string, error) {
parts := strings.Split(token, ".")
if len(parts) != 2 {
return false, "", fmt.Errorf("invalid token format")
}
encodedClientInfo := parts[0]
encodedSignature := parts[1]
clientInfoBytes, err := base64.URLEncoding.DecodeString(encodedClientInfo)
if err != nil {
return false, "", fmt.Errorf("failed to decode client info: %w", err)
}
signatureBytes, err := base64.URLEncoding.DecodeString(encodedSignature)
if err != nil {
return false, "", fmt.Errorf("failed to decode signature: %w", err)
}
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(clientInfoBytes)
expectedSignature := mac.Sum(nil)
if !hmac.Equal(signatureBytes, expectedSignature) {
return false, "", nil // 签名不匹配token 无效
}
clientInfo := string(clientInfoBytes)
return true, clientInfo, nil
}

View File

@ -1,3 +1,4 @@
// Package util provide some utility functions
package util
import (